The search box would be useful even on a small screen. #5093

Open
opened 2026-02-05 09:39:35 +03:00 by OVERLORD · 4 comments
Owner

Originally created by @toras9000 on GitHub (Dec 30, 2024).

Describe the feature you'd like

The search box in the header bar of the BookStack screen is useful and I use it frequently.
However, as the browser window gets smaller, this search box disappears relatively early and is stored in a drop-down menu.
To refer to a more specific situation, if the display resolution is 1920x1080, the search box disappears from the header bar when the width of the browser window is set to 960, half of the screen width.
For example, Windows has the ability to snap a window to half of the screen area, and I believe that there are a reasonable number of uses for placing a window on half of the screen area in other environments as well.

We think it would be more convenient to have the search box displayed even when the window is a little narrower.

Describe the benefits this would bring to existing BookStack users

The immediate availability of a search box facilitates access to articles.

Can the goal of this request already be achieved via other means?

Perhaps it is possible to customize it through a visual theme system.
However, it was not so easy as far as I myself tried.
Simply rewriting the CSS values will result in a broken layout and multiple lines.
It seems to me that if you use that method, you would probably need to customize the entire header bar.

Have you searched for an existing open/closed issue?

  • I have searched for existing issues and none cover my fundamental request

How long have you been using BookStack?

1 to 5 years

Additional context

No response

Originally created by @toras9000 on GitHub (Dec 30, 2024). ### Describe the feature you'd like The search box in the header bar of the BookStack screen is useful and I use it frequently. However, as the browser window gets smaller, this search box disappears relatively early and is stored in a drop-down menu. To refer to a more specific situation, if the display resolution is 1920x1080, the search box disappears from the header bar when the width of the browser window is set to 960, half of the screen width. For example, Windows has the ability to snap a window to half of the screen area, and I believe that there are a reasonable number of uses for placing a window on half of the screen area in other environments as well. We think it would be more convenient to have the search box displayed even when the window is a little narrower. ### Describe the benefits this would bring to existing BookStack users The immediate availability of a search box facilitates access to articles. ### Can the goal of this request already be achieved via other means? Perhaps it is possible to customize it through a visual theme system. However, it was not so easy as far as I myself tried. Simply rewriting the CSS values will result in a broken layout and multiple lines. It seems to me that if you use that method, you would probably need to customize the entire header bar. ### Have you searched for an existing open/closed issue? - [X] I have searched for existing issues and none cover my fundamental request ### How long have you been using BookStack? 1 to 5 years ### Additional context _No response_
Author
Owner

@DiscordDigital commented on GitHub (Dec 30, 2024):

I can confirm that I've noticed this before while using BookStack, there's definitely enough space to fit a search bar into the "mobile view", which could enhance the accessibility, I'm the type of user that jumps across the platform using the search

@DiscordDigital commented on GitHub (Dec 30, 2024): I can confirm that I've noticed this before while using BookStack, there's definitely enough space to fit a search bar into the "mobile view", which could enhance the accessibility, I'm the type of user that jumps across the platform using the search
Author
Owner

@DiscordDigital commented on GitHub (Dec 30, 2024):

I wrote a codeblock to achieve something like this just now, this goes into the "Custom HTML Head Content" in the Customization settings:

<style>
    @media screen and (max-width: 1000px) {
        .mobileNavSearch {
            position: absolute;
            left: 50%;
            transform: translate(-50%, 14px);
        }

        .mobileNavSearchWithLogo {
            transform: translate(-50%, 17px);
        }

        .logo-text {
            display: none;
        }
    }

    @media screen and (max-width: 470px) {
        .mobileNavSearch #header-search-box-input {
            width: 240px;
        }
    }

    @media screen and (max-width: 420px) {
        .mobileNavSearch #header-search-box-input {
            width: 200px;
        }
    }
</style>
<script>
    const waitFor = (search, callback, timeout = 2000, options = { childList: true, subtree: true }) => {
        if (typeof search !== 'string' || typeof callback !== 'function') {
            throw new Error('Invalid parameters. Expected string for search and function for callback.');
        }

        const observerAction = (observer, addedNode, observerTimeout) => {
            callback(addedNode);
            observer.disconnect();
            clearTimeout(observerTimeout);
        };

        const observer = new MutationObserver(function (mutationsList, observer) {
            mutationsList.forEach(function (mutation) {
                if (mutation.type === 'childList') {
                    mutation.addedNodes.forEach(function (addedNode) {
                        if (search.startsWith(".")) {
                            if (addedNode.classList && addedNode.classList.contains(search.substring(1))) {
                                observerAction(observer, addedNode, observerTimeout);
                            }
                        } else if (search.startsWith("#")) {
                            if (addedNode.id && addedNode.id === search.substring(1)) {
                                observerAction(observer, addedNode, observerTimeout);
                            }
                        } else {
                            if (addedNode.innerText && addedNode.innerText == search) {
                                observerAction(observer, addedNode, observerTimeout);
                            }
                        }
                    });
                }
            });
        });

        const observerTimeout = setTimeout(() => {
            observer.disconnect();
        }, timeout);

        observer.observe(document.documentElement, options);
    }

    function mobileSearchBar() {
        const normalNav = document.getElementsByClassName("search-box")[0].parentElement;
        normalNav.classList.add("mobileNavSearch");
        normalNav.classList.remove("hide-under-l");

        if (document.getElementsByClassName("logo-image")[0]) {
            if (!document.getElementsByClassName("logo-image")[0].classList.contains("none")) {
                normalNav.classList.add("mobileNavSearchWithLogo");
            }
        }
    }

    waitFor(".mobile-menu-toggle", mobileSearchBar);
</script>

Please keep in mind that this will also remove the logo text on the mobile header when enabled, so there's enough space for the search bar.

@DiscordDigital commented on GitHub (Dec 30, 2024): I wrote a codeblock to achieve something like this just now, this goes into the "Custom HTML Head Content" in the Customization settings: ```html <style> @media screen and (max-width: 1000px) { .mobileNavSearch { position: absolute; left: 50%; transform: translate(-50%, 14px); } .mobileNavSearchWithLogo { transform: translate(-50%, 17px); } .logo-text { display: none; } } @media screen and (max-width: 470px) { .mobileNavSearch #header-search-box-input { width: 240px; } } @media screen and (max-width: 420px) { .mobileNavSearch #header-search-box-input { width: 200px; } } </style> <script> const waitFor = (search, callback, timeout = 2000, options = { childList: true, subtree: true }) => { if (typeof search !== 'string' || typeof callback !== 'function') { throw new Error('Invalid parameters. Expected string for search and function for callback.'); } const observerAction = (observer, addedNode, observerTimeout) => { callback(addedNode); observer.disconnect(); clearTimeout(observerTimeout); }; const observer = new MutationObserver(function (mutationsList, observer) { mutationsList.forEach(function (mutation) { if (mutation.type === 'childList') { mutation.addedNodes.forEach(function (addedNode) { if (search.startsWith(".")) { if (addedNode.classList && addedNode.classList.contains(search.substring(1))) { observerAction(observer, addedNode, observerTimeout); } } else if (search.startsWith("#")) { if (addedNode.id && addedNode.id === search.substring(1)) { observerAction(observer, addedNode, observerTimeout); } } else { if (addedNode.innerText && addedNode.innerText == search) { observerAction(observer, addedNode, observerTimeout); } } }); } }); }); const observerTimeout = setTimeout(() => { observer.disconnect(); }, timeout); observer.observe(document.documentElement, options); } function mobileSearchBar() { const normalNav = document.getElementsByClassName("search-box")[0].parentElement; normalNav.classList.add("mobileNavSearch"); normalNav.classList.remove("hide-under-l"); if (document.getElementsByClassName("logo-image")[0]) { if (!document.getElementsByClassName("logo-image")[0].classList.contains("none")) { normalNav.classList.add("mobileNavSearchWithLogo"); } } } waitFor(".mobile-menu-toggle", mobileSearchBar); </script> ``` Please keep in mind that this will also remove the logo text on the mobile header when enabled, so there's enough space for the search bar.
Author
Owner

@ssddanbrown commented on GitHub (Dec 31, 2024):

I wouldn't be opposed to making the responsive breakdown a bit more nuanced, so instead of going from:
[logo]--[search]--[buttons] >> [logo]----[mobile-menu]

we have another stage in the middle:

[logo]--[search]--[buttons] >> [logo]--[search]--[mobile-menu] >> [logo]----[mobile-menu]

@ssddanbrown commented on GitHub (Dec 31, 2024): I wouldn't be opposed to making the responsive breakdown a bit more nuanced, so instead of going from: `[logo]--[search]--[buttons] >> [logo]----[mobile-menu]` we have another stage in the middle: `[logo]--[search]--[buttons] >> [logo]--[search]--[mobile-menu] >> [logo]----[mobile-menu]`
Author
Owner

@whoamiafterall commented on GitHub (Jan 7, 2025):

Preview on wiki.aktivismus.org

We solved the same problem in all-blade.php (through the theme system)

Code

 @extends('layouts.simple')

 @section('body')
    <div class="container mt-xl" id="search-system">

        <div class="grid right-focus reverse-collapse gap-xl">
            <div>
                <div>
                    <h5>{{ trans('entities.search_advanced') }}</h5>

                    <form method="get" action="{{ url('/search') }}">
                        <h6>{{ trans('entities.search_terms') }}</h6>
                        <input type="text" name="search" value="{{ implode(' ', $options->searches) }}">

                        <h6>{{ trans('entities.search_content_type') }}</h6>
                        <div class="form-group">

                            <?php
                            $types = explode('|', $options->filters['type'] ?? '');
                            $hasTypes = $types[0] !== '';
                            ?>
                            @include('search.parts.type-filter', ['checked' => !$hasTypes || in_array('page', $types), 'entity' => 'page', 'transKey' => 'page'])
                            @include('search.parts.type-filter', ['checked' => !$hasTypes || in_array('chapter', $types), 'entity' => 'chapter', 'transKey' => 'chapter'])
                            <br>
                                @include('search.parts.type-filter', ['checked' => !$hasTypes || in_array('book', $types), 'entity' => 'book', 'transKey' => 'book'])
                                @include('search.parts.type-filter', ['checked' => !$hasTypes || in_array('bookshelf', $types), 'entity' => 'bookshelf', 'transKey' => 'shelf'])
                        </div>

                        <h6>{{ trans('entities.search_exact_matches') }}</h6>
                        @include('search.parts.term-list', ['type' => 'exact', 'currentList' => $options->exacts])

                        <h6>{{ trans('entities.search_tags') }}</h6>
                        @include('search.parts.term-list', ['type' => 'tags', 'currentList' => $options->tags])

                        @if(!user()->isGuest())
                            <h6>{{ trans('entities.search_options') }}</h6>

                            @component('search.parts.boolean-filter', ['filters' => $options->filters, 'name' => 'viewed_by_me', 'value' => null])
                                {{ trans('entities.search_viewed_by_me') }}
                            @endcomponent
                            @component('search.parts.boolean-filter', ['filters' => $options->filters, 'name' => 'not_viewed_by_me', 'value' => null])
                                {{ trans('entities.search_not_viewed_by_me') }}
                            @endcomponent
                            @component('search.parts.boolean-filter', ['filters' => $options->filters, 'name' => 'is_restricted', 'value' => null])
                                {{ trans('entities.search_permissions_set') }}
                            @endcomponent
                            @component('search.parts.boolean-filter', ['filters' => $options->filters, 'name' => 'created_by', 'value' => 'me'])
                                {{ trans('entities.search_created_by_me') }}
                            @endcomponent
                            @component('search.parts.boolean-filter', ['filters' => $options->filters, 'name' => 'updated_by', 'value' => 'me'])
                                {{ trans('entities.search_updated_by_me') }}
                            @endcomponent
                            @component('search.parts.boolean-filter', ['filters' => $options->filters, 'name' => 'owned_by', 'value' => 'me'])
                                {{ trans('entities.search_owned_by_me') }}
                            @endcomponent
                        @endif

                        <h6>{{ trans('entities.search_date_options') }}</h6>
                        @include('search.parts.date-filter', ['name' => 'updated_after', 'filters' => $options->filters])
                        @include('search.parts.date-filter', ['name' => 'updated_before', 'filters' => $options->filters])
                        @include('search.parts.date-filter', ['name' => 'created_after', 'filters' => $options->filters])
                        @include('search.parts.date-filter', ['name' => 'created_before', 'filters' => $options->filters])

                        @if(isset($options->filters['created_by']) && $options->filters['created_by'] !== "me")
                            <input type="hidden" name="filters[created_by]" value="{{ $options->filters['created_by'] }}">
                        @endif
                        @if(isset($options->filters['updated_by']) && $options->filters['updated_by'] !== "me")
                            <input type="hidden" name="filters[updated_by]" value="{{ $options->filters['updated_by'] }}">
                        @endif

                        <button type="submit" class="button">{{ trans('entities.search_update') }}</button>
                    </form>

                </div>
            </div>
            <div>
                <div class="card content-wrap">
                    <h1 class="list-heading">{{ trans('entities.search_results') }}</h1>

                    <form action="{{ url('/search') }}" method="GET"  class="search-box flexible hide-over-l">
                        <input value="{{$searchTerm}}" type="text" name="term" placeholder="{{ trans('common.search') }}">
                        <button tabindex="-1" type="submit">@icon('search')</button>
                    </form>

                    <h6 class="text-muted">{{ trans_choice('entities.search_total_results_found', $totalResults, ['count' => $totalResults]) }}</h6>
                    <div class="book-contents">
                        @include('entities.list', ['entities' => $entities, 'showPath' => true, 'showTags' => true])
                    </div>

                    @if($hasNextPage)
                        <div class="text-right mt-m">
                            <a href="{{ $nextPageLink }}" class="button outline">{{ trans('entities.search_more') }}</a>
                        </div>
                    @endif
                </div>
            </div>
        </div>

    </div>
 @stop

@whoamiafterall commented on GitHub (Jan 7, 2025): Preview on wiki.aktivismus.org We solved the same problem in all-blade.php (through the theme system) <details><summary>Code</summary> <p> ```html @extends('layouts.simple') @section('body') <div class="container mt-xl" id="search-system"> <div class="grid right-focus reverse-collapse gap-xl"> <div> <div> <h5>{{ trans('entities.search_advanced') }}</h5> <form method="get" action="{{ url('/search') }}"> <h6>{{ trans('entities.search_terms') }}</h6> <input type="text" name="search" value="{{ implode(' ', $options->searches) }}"> <h6>{{ trans('entities.search_content_type') }}</h6> <div class="form-group"> <?php $types = explode('|', $options->filters['type'] ?? ''); $hasTypes = $types[0] !== ''; ?> @include('search.parts.type-filter', ['checked' => !$hasTypes || in_array('page', $types), 'entity' => 'page', 'transKey' => 'page']) @include('search.parts.type-filter', ['checked' => !$hasTypes || in_array('chapter', $types), 'entity' => 'chapter', 'transKey' => 'chapter']) <br> @include('search.parts.type-filter', ['checked' => !$hasTypes || in_array('book', $types), 'entity' => 'book', 'transKey' => 'book']) @include('search.parts.type-filter', ['checked' => !$hasTypes || in_array('bookshelf', $types), 'entity' => 'bookshelf', 'transKey' => 'shelf']) </div> <h6>{{ trans('entities.search_exact_matches') }}</h6> @include('search.parts.term-list', ['type' => 'exact', 'currentList' => $options->exacts]) <h6>{{ trans('entities.search_tags') }}</h6> @include('search.parts.term-list', ['type' => 'tags', 'currentList' => $options->tags]) @if(!user()->isGuest()) <h6>{{ trans('entities.search_options') }}</h6> @component('search.parts.boolean-filter', ['filters' => $options->filters, 'name' => 'viewed_by_me', 'value' => null]) {{ trans('entities.search_viewed_by_me') }} @endcomponent @component('search.parts.boolean-filter', ['filters' => $options->filters, 'name' => 'not_viewed_by_me', 'value' => null]) {{ trans('entities.search_not_viewed_by_me') }} @endcomponent @component('search.parts.boolean-filter', ['filters' => $options->filters, 'name' => 'is_restricted', 'value' => null]) {{ trans('entities.search_permissions_set') }} @endcomponent @component('search.parts.boolean-filter', ['filters' => $options->filters, 'name' => 'created_by', 'value' => 'me']) {{ trans('entities.search_created_by_me') }} @endcomponent @component('search.parts.boolean-filter', ['filters' => $options->filters, 'name' => 'updated_by', 'value' => 'me']) {{ trans('entities.search_updated_by_me') }} @endcomponent @component('search.parts.boolean-filter', ['filters' => $options->filters, 'name' => 'owned_by', 'value' => 'me']) {{ trans('entities.search_owned_by_me') }} @endcomponent @endif <h6>{{ trans('entities.search_date_options') }}</h6> @include('search.parts.date-filter', ['name' => 'updated_after', 'filters' => $options->filters]) @include('search.parts.date-filter', ['name' => 'updated_before', 'filters' => $options->filters]) @include('search.parts.date-filter', ['name' => 'created_after', 'filters' => $options->filters]) @include('search.parts.date-filter', ['name' => 'created_before', 'filters' => $options->filters]) @if(isset($options->filters['created_by']) && $options->filters['created_by'] !== "me") <input type="hidden" name="filters[created_by]" value="{{ $options->filters['created_by'] }}"> @endif @if(isset($options->filters['updated_by']) && $options->filters['updated_by'] !== "me") <input type="hidden" name="filters[updated_by]" value="{{ $options->filters['updated_by'] }}"> @endif <button type="submit" class="button">{{ trans('entities.search_update') }}</button> </form> </div> </div> <div> <div class="card content-wrap"> <h1 class="list-heading">{{ trans('entities.search_results') }}</h1> <form action="{{ url('/search') }}" method="GET" class="search-box flexible hide-over-l"> <input value="{{$searchTerm}}" type="text" name="term" placeholder="{{ trans('common.search') }}"> <button tabindex="-1" type="submit">@icon('search')</button> </form> <h6 class="text-muted">{{ trans_choice('entities.search_total_results_found', $totalResults, ['count' => $totalResults]) }}</h6> <div class="book-contents"> @include('entities.list', ['entities' => $entities, 'showPath' => true, 'showTags' => true]) </div> @if($hasNextPage) <div class="text-right mt-m"> <a href="{{ $nextPageLink }}" class="button outline">{{ trans('entities.search_more') }}</a> </div> @endif </div> </div> </div> </div> @stop ``` </p> </details>
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: starred/BookStack#5093