Răsfoiți Sursa

Header: search is a member toolbar menu item, editable in Studio (#5905)

* Header: search is a member toolbar menu item, not a hardcoded button

The Artificer header drew its search button straight from
_page_toolbar.html, so Studio could not hide it or change its icon. The
core 'search' menu item now lives in sys_toolbar_member (first, active,
visible to everyone, active_api kept) instead of sys_toolbar_site, where
Artificer, Lucid and Decorous all hid it. Artificer renders it like the
other 44px toolbar buttons and points it at the same floating search bar
(or at the search page when "Enable floating search bar" is off). Lucid
and Decorous keep hiding the core item; Protean and Ocean show it on the
right.

Also: the floating bar no longer leaves a document click handler behind
after it closes, and AGENTS.md names the real enable-app-*.sql paths.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Toolbar search: leave the API item alone

In API mode a menu item has no class_add, so the phone-hide line in
BxBaseMenuToolbar warned into api.php output, now for guests too since
search is a member toolbar item. Artificer's 'search' case skips the API
item, so the app gets the item as configured in the menu builder. The
BxBaseMenuToolbar guard is the same one as in #5906.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Andrey Yasko 5 zile în urmă
părinte
comite
56788c30c3

+ 1 - 1
AGENTS.md

@@ -36,7 +36,7 @@ These apply to UNA PHP (`inc/`, `modules/`, `studio/`, `template/`, and install
 - Do not call module by a literal name (`BxDolModule::getInstance('bx_posts')`, `BxDolService::call('bx_posts', ...)`, `new BxPostsModule`) without checking if module is installed `BxDolModuleQuery::getInstance()->isEnabledByName('bx_posts')`.
 - Call `getParam` only with a name that exists in `sys_options`. Declare the option in that module's install SQL before reading it.
 - Do not add or edit anything under `upgrade/files/`. That folder records past releases. Do not put new migrations there.
-- Set `active_api` in `install/sql/enable-app-pages.sql` (page blocks) and `install/sql/enable-app-menus.sql` (menu items) for core, for modules use `modules/boonex/*/install/sql/enable-app.sql` files. Do not set it in `install.sql`, `enable.sql`, or other common install and enable scripts.
+- Set `active_api` in `modules/system/install/sql/enable-app-pages.sql` (page blocks) and `modules/system/install/sql/enable-app-menus.sql` (menu items) for core, for modules use `modules/boonex/*/install/sql/enable-app.sql` files. Do not set it in `install.sql`, `enable.sql`, or other common install and enable scripts.
 
 ## Studio app icons
 

+ 3 - 3
install/sql/system.sql

@@ -5100,11 +5100,11 @@ INSERT INTO `sys_menu_items` (`set_name`, `module`, `name`, `title_system`, `tit
 
 -- site toolbar menu
 INSERT INTO `sys_menu_items`(`set_name`, `module`, `name`, `title_system`, `title`, `link`, `onclick`, `target`, `icon`, `submenu_object`, `visible_for_levels`, `active`, `copyable`, `order`) VALUES 
-('sys_toolbar_site', 'system', 'main-menu', '_sys_menu_item_title_system_main_menu', '', 'javascript:void(0);', 'bx_menu_slide_inline(\'#bx-sliding-menu-sys_site\', this, \'site\');', '', 'bars', '', 2147483647, 1, 1, 1),
-('sys_toolbar_site', 'system', 'search', '_sys_menu_item_title_system_search', '', 'javascript:void(0);', 'bx_menu_slide_inline(''#bx-sliding-menu-search'', this, ''site'');', '', 'search', '', 2147483647, 1, 1, 2);
+('sys_toolbar_site', 'system', 'main-menu', '_sys_menu_item_title_system_main_menu', '', 'javascript:void(0);', 'bx_menu_slide_inline(\'#bx-sliding-menu-sys_site\', this, \'site\');', '', 'bars', '', 2147483647, 1, 1, 1);
 
 -- member toolbar menu
-INSERT INTO `sys_menu_items`(`set_name`, `module`, `name`, `title_system`, `title`, `link`, `onclick`, `target`, `icon`, `addon`, `submenu_object`, `submenu_popup`, `visible_for_levels`, `hidden_on_pt`, `active`, `copyable`, `order`) VALUES 
+INSERT INTO `sys_menu_items`(`set_name`, `module`, `name`, `title_system`, `title`, `link`, `onclick`, `target`, `icon`, `addon`, `submenu_object`, `submenu_popup`, `visible_for_levels`, `hidden_on_pt`, `active`, `copyable`, `order`) VALUES
+('sys_toolbar_member', 'system', 'search', '_sys_menu_item_title_system_search', '', 'javascript:void(0);', 'bx_menu_slide_inline(''#bx-sliding-menu-search'', this, ''site'');', '', 'search', '', '', 0, 2147483647, 0, 1, 1, 0),
 ('sys_toolbar_member', 'system', 'add-content', '_sys_menu_item_title_system_add_content', '', 'javascript:void(0);', 'bx_menu_slide_inline(\'#bx-sliding-menu-sys_add_content\', this, \'site\');', '', 'plus', '', '', 0, 2147483646, 0, 1, 1, 0),
 ('sys_toolbar_member', 'system', 'apps', '_sys_menu_item_title_system_apps', '', 'javascript:void(0);', '', '', 'qrcode', '', '', 0, 2147483646, 3, 1, 1, 0),
 ('sys_toolbar_member', 'system', 'account', '_sys_menu_item_title_system_account_menu', '_sys_menu_item_title_account_menu', 'javascript:void(0);', 'bx_menu_slide_inline(''#bx-sliding-menu-account'', this, ''site'');', '', 'user',  'a:3:{s:6:"module";s:6:"system";s:6:"method";s:21:"profile_notifications";s:5:"class";s:20:"TemplServiceProfiles";}', 'sys_account_popup', 1, 2147483646, 0, 1, 0, 1),

+ 26 - 23
modules/boonex/artificer/data/template/system/_header_js_custom.html

@@ -105,28 +105,39 @@
         return false;
     }
 
-    function bx_site_search_show(oButtom) {
-        var oButton = $(oButtom).parents('.bx-ti-search-button');
-        oButton.addClass('bx-tis-button-hidden').find('button,a').attr('aria-expanded', 'true');
+    /**
+     * The 'search' item of the member toolbar opens the floating search bar (#bx-ti-search-box), which takes the item's place until a click outside or Escape.
+     */
+    function bx_site_search_show(oLink) {
+        var oBox = $('#bx-ti-search-box');
+        if(!oBox.length)
+            return;
+
+        $(oLink).attr('aria-expanded', 'true').parents('.bx-menu-toolbar-item:first').addClass('bx-tis-button-hidden');
 
-        var oBox = oButton.parents('.bx-ti-search').find('.bx-ti-search-box');
         oBox.addClass('bx-tis-box-shown');
-        
         oBox.find('.bx-form-input-text').focus();
 
         setTimeout(function () {
-            $(document).on('click.bx-site-search-phone touchend.bx-site-search-phone', function (event) {
-                if ($(event.target).parents('.sys-search-results-quick,.bx-ti-search-box,.bx-ti-search-button').length || $(event.target).filter('.sys-search-results-quick,.bx-ti-search-box,.bx-ti-search-button').length)
-                    event.stopPropagation();
-                else {
-                    bx_site_search_close_all_opened();
-                    oBox.removeClass('bx-tis-box-shown');
-                    oButton.removeClass('bx-tis-button-hidden').find('button,a').attr('aria-expanded', 'false').focus();
-                }
+            $(document).on('click.bx-site-search-box touchend.bx-site-search-box', function (event) {
+                if(!$(event.target).closest('.sys-search-results-quick,.bx-ti-search-box').length)
+                    bx_site_search_hide();
             });
         }, 10);
     }
 
+    function bx_site_search_hide() {
+        var oBox = $('#bx-ti-search-box');
+        if(!oBox.hasClass('bx-tis-box-shown'))
+            return;
+
+        $(document).off('.bx-site-search-box');
+        bx_site_search_close_all_opened();
+
+        oBox.removeClass('bx-tis-box-shown');
+        $('.bx-tis-button-hidden').removeClass('bx-tis-button-hidden').find('a[aria-controls="bx-ti-search-box"]').attr('aria-expanded', 'false').focus();
+    }
+
     function bx_site_search_complete(oContainer, oData) {
         if(!oData) {
             if(oContainer.is(':visible'))
@@ -159,16 +170,8 @@
 
     function bx_site_search_cancel(oForm)
     {
-        const oBox = $(oForm).parents('.bx-ti-search').find('.bx-ti-search-box');
-        const oButton = $(oForm).parents('.bx-ti-search').find('.bx-ti-search-button');
-
-        const sClassBox = 'bx-tis-box-shown';
-        const sClassButton = 'bx-tis-button-hidden';
-        if(oBox.hasClass(sClassBox) && oButton.hasClass(sClassButton)) {
-            bx_site_search_close_all_opened();
-            oBox.removeClass(sClassBox);
-            oButton.removeClass(sClassButton).find('button,a').attr('aria-expanded', 'false').focus();
-        }
+        if($(oForm).parents('#bx-ti-search-box').length)
+            bx_site_search_hide();
     }
 
     function bx_site_search_close_all_opened() {

+ 16 - 14
modules/boonex/artificer/data/template/system/css/general.css

@@ -27,35 +27,37 @@ import url(../../../../../../../template/css/general.css);
     max-height: 100%;
     width: auto;
 }
-.bx-toolbar-item.bx-ti-search .bx-ti-search-button a {
-    text-decoration: none;
-}
-.bx-toolbar-item.bx-ti-search .bx-ti-search-button img {
-    width: 1.5rem;
-    height: 1.5rem;
-}
-.bx-toolbar-item.bx-ti-search .bx-tis-button-hidden {
+/*--- Floating search bar: takes the place of the member toolbar's 'search' item, at the end of the toolbar's items group (phones: the whole group) ---*/
+#bx-toolbar .bx-tis-button-hidden {
     display: none;
 }
-.bx-toolbar-item.bx-ti-search .bx-tis-box-shown {
+.bx-ti-search-box.bx-tis-box-shown {
     position: absolute;
     display: block;
+    top: 50%;
+    inset-inline-end: 0;
+    transform: translateY(-50%);
+}
+@media (max-width: 767px) {
+    .bx-ti-search-box.bx-tis-box-shown {
+        inset-inline-start: 0;
+    }
 }
-.bx-toolbar-item.bx-ti-search .bx-form-advanced .bx-loading-ajax {
+.bx-ti-search-box .bx-form-advanced .bx-loading-ajax {
     top: 0;
     max-height: 100%;
 }
-.bx-toolbar-item.bx-ti-search .bx-form-advanced-wrapper .bx-form-section-wrapper:first-child {
+.bx-ti-search-box .bx-form-advanced-wrapper .bx-form-section-wrapper:first-child {
     margin-top: 0;
 }
-.bx-toolbar-item.bx-ti-search .sys-search-results-quick {
+.bx-ti-search-box .sys-search-results-quick {
     position: absolute;
     z-index: 10;
 }
-.bx-toolbar-item.bx-ti-search .sys-search-results-quick .bx-btn.sys-agents-ask {
+.bx-ti-search-box .sys-search-results-quick .bx-btn.sys-agents-ask {
     margin-bottom: 0.5rem;
 }
-.bx-toolbar-item.bx-ti-search .sys-search-results-quick .bx-btn.sys-agents-ask:last-child {
+.bx-ti-search-box .sys-search-results-quick .bx-btn.sys-agents-ask:last-child {
     margin-bottom: 0;
 }
 

+ 2 - 4
modules/boonex/artificer/data/template/system/pt_application.html

@@ -34,10 +34,8 @@
                                 <bx_menu:sys_application />
                             </div>
                         </div>
-                        <!-- Search -->
-                        <div class="bx-toolbar-item bx-ti-search flex flex-1 items-center justify-end">
-                            __sys_toolbar_search__
-                        </div>
+                        <!-- Search box, opened by the 'search' item of the member toolbar -->
+                        __sys_toolbar_search__
                     </div>
                 </div>
                 <bx_menu:sys_toolbar_member />

+ 19 - 0
modules/boonex/artificer/data/template/system/scripts/BxTemplMenuToolbar.php

@@ -76,6 +76,25 @@ class BxTemplMenuToolbar extends BxBaseMenuToolbar
                 $sAttrsAdd = 'aria-haspopup="true" aria-expanded="false" aria-controls="bx-sliding-menu-sys_add_content"';
                 break;
 
+            case 'search':
+                //--- The app gets the item as it is in the menu builder.
+                if($this->_bIsApi)
+                    break;
+
+                $a['class_add'] = str_replace('bx-def-media-phone-hide', '',  $a['class_add']);
+
+                //--- The floating search bar (see _page_toolbar_search.html) takes the button's place. Without it the button opens the search page.
+                if(getParam('bx_artificer_header_search') == 'on') {
+                    $a['link'] = 'javascript:void(0)';
+                    $a['bx_if:onclick'] = ['condition' => true, 'content' => ['onclick' => 'bx_site_search_show(this)']];
+                    $sAttrsAdd = 'aria-expanded="false" aria-controls="bx-ti-search-box"';
+                }
+                else {
+                    $a['link'] = BX_DOL_URL_ROOT . 'searchKeyword.php';
+                    $a['bx_if:onclick'] = ['condition' => false, 'content' => ['onclick' => '']];
+                }
+                break;
+
             case 'notifications-preview':
                 if(isset($a['bx_if:onclick']['content']['onclick']))
                     $a['bx_if:onclick']['content']['onclick'] = str_replace(["bx_menu_slide", "'site', "], ['bx_menu_popup'], $a['bx_if:onclick']['content']['onclick']);

+ 2 - 2
modules/boonex/artificer/template/header.html

@@ -15,8 +15,8 @@
                         <bx_menu:sys_site />
                     </div>
                 </div>
-                <!-- Search -->
-                <div class="bx-toolbar-item bx-ti-search flex-1 lg:flex-none flex items-center justify-end">
+                <!-- Search box, opened by the 'search' item of the member toolbar; this cell also keeps the toolbar on the right on phones -->
+                <div class="relative flex-1 lg:flex-none h-16">
                     __sys_toolbar_search__
                 </div>
                 <bx_menu:sys_toolbar_member />

+ 2 - 2
modules/boonex/decorous/data/template/system/scripts/BxTemplMenuToolbar.php

@@ -23,8 +23,8 @@ class BxTemplMenuToolbar extends BxBaseMenuToolbar
 
     protected function _getMenuItem ($a)
     {
-    	//--- Hide 'Search' from Site Toolbar
-        if($this->_bSiteToolbar && in_array($a['name'], array('search')))
+    	//--- Hide 'Search' from both toolbars
+        if(in_array($a['name'], array('search')))
             return false;
 
         $a = parent::_getMenuItem ($a);

+ 2 - 2
modules/boonex/lucid/data/template/system/scripts/BxTemplMenuToolbar.php

@@ -23,8 +23,8 @@ class BxTemplMenuToolbar extends BxBaseMenuToolbar
 
     protected function _getMenuItem ($a)
     {
-    	//--- Hide 'Main Menu' and 'Search' from Site Toolbar
-        if($this->_bSiteToolbar && in_array($a['name'], array('search')))
+    	//--- Hide system 'Search' from both toolbars, 'bx_lucid_search' replaces it
+        if(in_array($a['name'], array('search')))
             return false;
 
         //--- Hide 'Login' from Member Toolbar

+ 1 - 1
modules/system/install/sql/enable-app-menus.sql

@@ -23,7 +23,7 @@ UPDATE `sys_menu_items` SET `active_api`=1 WHERE `set_name`='sys_footer' AND `mo
 UPDATE `sys_menu_items` SET `active_api`=1 WHERE `set_name`='sys_footer' AND `module`='system' AND `name`='terms';
 UPDATE `sys_menu_items` SET `active_api`=1 WHERE `set_name`='sys_footer' AND `module`='system' AND `name`='privacy';
 UPDATE `sys_menu_items` SET `active_api`=1 WHERE `set_name`='sys_toolbar_site' AND `module`='system' AND `name`='main-menu';
-UPDATE `sys_menu_items` SET `active_api`=1 WHERE `set_name`='sys_toolbar_site' AND `module`='system' AND `name`='search';
+UPDATE `sys_menu_items` SET `active_api`=1 WHERE `set_name`='sys_toolbar_member' AND `module`='system' AND `name`='search';
 UPDATE `sys_menu_items` SET `active_api`=1 WHERE `set_name`='sys_toolbar_member' AND `module`='system' AND `name`='add-content';
 UPDATE `sys_menu_items` SET `active_api`=1 WHERE `set_name`='sys_toolbar_member' AND `module`='system' AND `name`='apps';
 UPDATE `sys_menu_items` SET `active_api`=1 WHERE `set_name`='sys_toolbar_member' AND `module`='system' AND `name`='account';

+ 2 - 4
template/_page_toolbar.html

@@ -17,10 +17,8 @@
                             <bx_menu:sys_site />
                         </nav>
                     </div>
-                    <!-- Search -->
-                    <div class="bx-toolbar-item bx-ti-search flex-1 lg:flex-none flex items-center justify-end">
-                        __sys_toolbar_search__
-                    </div>
+                    <!-- Search box, opened by the 'search' item of the member toolbar -->
+                    __sys_toolbar_search__
                 </div>
                 <bx_menu:sys_toolbar_member />
             </div>

+ 1 - 11
template/_page_toolbar_search.html

@@ -1,13 +1,3 @@
-<div id="bx-ti-search-box" class="bx-ti-search-box flex-auto flex items-start md:min-w-88 ltr:ml-2 rtl:mr-2 bg-white/95 dark:bg-gray-800/95 rounded-md shadow-md hidden">
+<div id="bx-ti-search-box" class="bx-ti-search-box md:min-w-88 bg-white/95 dark:bg-gray-800/95 rounded-md shadow-md hidden">
     <div class="relative w-full">__sys_site_search__</div>
-</div>
-<div class="bx-ti-search-button group relative inline-flex items-center ltr:ml-2 rtl:mr-2 text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200 text-base font-medium leading-6 bg-gray-100 hover:bg-gray-200 active:bg-gray-300 dark:bg-gray-700/50 dark:hover:bg-gray-700 dark:active:bg-gray-600 rounded-full">
-    <button type="button" class="inline-block rounded-full" onclick="javascript:bx_site_search_show(this)" aria-expanded="false" aria-controls="bx-ti-search-box">
-        <span class="relative flex items-center justify-center h-11 w-11">
-            <div class="bx-icon">
-                <bx_image_auto:search />
-            </div>
-        </span>
-        <span class="sr-only"><bx_text:_sys_menu_item_title_system_search /></span>
-    </button>
 </div>

+ 1 - 1
template/scripts/BxBaseMenuToolbar.php

@@ -75,7 +75,7 @@ class BxBaseMenuToolbar extends BxTemplMenu
         	'content' => array()
         );
 
-        if ('add-content' == $a['name'] || 'search' == $a['name'])
+        if (isset($a['class_add']) && ('add-content' == $a['name'] || 'search' == $a['name']))
             $a['class_add'] .= ' bx-def-media-phone-hide';
 
         return $a;

+ 36 - 0
tests/units/install/SystemSqlDefaultsTest.php

@@ -42,4 +42,40 @@ class SystemSqlDefaultsTest extends \PHPUnit\Framework\TestCase
         $this->assertSame('browse_recommendations_friends', $aContent['method']);
         $this->assertTrue($aContent['params'][1]['empty_state'] ?? false, 'With nothing to suggest, the block explains why instead of leaving the column blank.');
     }
+
+    public function testSearchIsAnActiveMemberToolbarItem(): void
+    {
+        $sSql = self::systemSql();
+
+        // Member toolbar columns: ... icon, addon, submenu_object, submenu_popup, visible_for_levels, hidden_on_pt, active, copyable, order.
+        $bFound = preg_match("/\\('sys_toolbar_member', 'system', 'search', '_sys_menu_item_title_system_search', [^\\n]*?, '([^']*)', '', '', 0, (\\d+), (\\d+), (\\d+), \\d+, (\\d+)\\)/", $sSql, $aMatch);
+
+        $this->assertSame(1, $bFound, 'system.sql adds the search button to the member toolbar, where it can be edited in Studio like its neighbours.');
+        $this->assertSame('search', $aMatch[1], 'It is drawn with the Lucide search icon.');
+        $this->assertSame(2147483647, (int)$aMatch[2], 'Guests and members both see it.');
+        $this->assertSame(0, (int)$aMatch[3], 'It is not hidden on any page type.');
+        $this->assertSame(1, (int)$aMatch[4], 'It is active on a fresh install.');
+        $this->assertSame(0, (int)$aMatch[5], 'It comes first in the toolbar.');
+
+        $this->assertStringNotContainsString("('sys_toolbar_site', 'system', 'search'", $sSql, 'One search item only: the site toolbar no longer has its own.');
+    }
+
+    public function testSearchIsActiveInTheApi(): void
+    {
+        $sSql = file_get_contents(__DIR__ . '/../../../modules/system/install/sql/enable-app-menus.sql');
+
+        $this->assertStringContainsString("SET `active_api`=1 WHERE `set_name`='sys_toolbar_member' AND `module`='system' AND `name`='search';", $sSql, 'The app receives the same search item.');
+        $this->assertStringNotContainsString("`set_name`='sys_toolbar_site' AND `module`='system' AND `name`='search'", $sSql);
+    }
+
+    public function testToolbarHasNoHardcodedSearchButton(): void
+    {
+        $sRoot = __DIR__ . '/../../../';
+        foreach (['template/_page_toolbar.html', 'template/_page_toolbar_search.html', 'modules/boonex/artificer/template/header.html', 'modules/boonex/artificer/data/template/system/pt_application.html'] as $sFile) {
+            $sHtml = file_get_contents($sRoot . $sFile);
+
+            $this->assertStringNotContainsString('bx-ti-search-button', $sHtml, $sFile . ': the search button comes from the member toolbar menu, not from the template.');
+            $this->assertStringNotContainsString('bx_site_search_show', $sHtml, $sFile);
+        }
+    }
 }