diff --git a/includes/SkinVector.php b/includes/SkinVector.php index 5685caa..9aeb7ba 100644 --- a/includes/SkinVector.php +++ b/includes/SkinVector.php @@ -65,6 +65,7 @@ class SkinVector extends SkinMustache { 'is-quiet' => true, 'class' => 'sticky-header-icon' ]; + private const SEARCH_EXPANDING_CLASS = 'vector-search-box-show-thumbnail'; /** * T243281: Code used to track clicks to opt-out link. @@ -325,7 +326,16 @@ class SkinVector extends SkinMustache { private function getStickyHeaderData() { return [ 'data-primary-action' => !$this->shouldHideLanguages() ? $this->getULSButtonData() : '', - 'data-button-start' => self::NO_ICON, + 'data-button-start' => [ + 'href' => '#p-search', + 'label' => $this->msg( 'search' ), + 'icon' => 'wikimedia-search', + 'is-quiet' => true, + 'class' => 'vector-sticky-header-search-toggle', + ], + 'data-search' => [ + 'class' => $this->shouldSearchExpand() ? self::SEARCH_EXPANDING_CLASS : '', + ], 'data-buttons' => [ self::TALK_ICON, self::HISTORY_ICON, self::NO_ICON, self::NO_ICON ] @@ -423,7 +433,7 @@ class SkinVector extends SkinMustache { } if ( $this->shouldSearchExpand() ) { - $searchClass .= ' vector-search-box-show-thumbnail'; + $searchClass .= " " . self::SEARCH_EXPANDING_CLASS; } // Annotate search box with a component class. diff --git a/includes/templates/StickyHeader.mustache b/includes/templates/StickyHeader.mustache index fe531fc..616ba9f 100644 --- a/includes/templates/StickyHeader.mustache +++ b/includes/templates/StickyHeader.mustache @@ -6,6 +6,11 @@ {{>Button}} {{/data-button-start}} + {{#data-search}} + + {{/data-search}}
{{html-title}}
diff --git a/jsdoc.json b/jsdoc.json index 28eedc2..9c3e5bf 100644 --- a/jsdoc.json +++ b/jsdoc.json @@ -22,6 +22,7 @@ "Element": "https://developer.mozilla.org/docs/Web/API/Element", "Event": "https://developer.mozilla.org/docs/Web/API/Event", "HTMLElement": "https://developer.mozilla.org/docs/Web/API/HTMLElement", + "NodeList": "https://developer.mozilla.org/docs/Web/API/NodeList", "HTMLInputElement": "https://developer.mozilla.org/docs/Web/API/HTMLInputElement", "\"removeEventListener\"": "https://developer.mozilla.org/docs/Web/API/EventTarget/removeEventListener", "Window": "https://developer.mozilla.org/docs/Web/API/Window", diff --git a/resources/skins.vector.js/searchToggle.js b/resources/skins.vector.js/searchToggle.js index 015a4c0..591156d 100644 --- a/resources/skins.vector.js/searchToggle.js +++ b/resources/skins.vector.js/searchToggle.js @@ -1,7 +1,6 @@ var - HEADER_SELECTOR = '.mw-header', - SEARCH_TOGGLE_SELECTOR = '.search-toggle', - SEARCH_BOX_ID = 'p-search', + HEADER_SELECTOR = 'header', + SEARCH_BOX_SELECTOR = '.vector-search-box', SEARCH_VISIBLE_CLASS = 'vector-header-search-toggled'; /** @@ -22,7 +21,9 @@ function bindSearchBoxHandler( searchBox, header ) { // Check if the click target was a suggestion link. WVUI clears the // suggestion elements from the DOM when a suggestion is clicked so we // can't test if the suggestion is a child of the searchBox. - !$( ev.target ).closest( '.wvui-typeahead-suggestion' ).length && + // + // Note: The .closest API is feature detected in `initSearchToggle`. + !ev.target.closest( '.wvui-typeahead-suggestion' ) && !searchBox.contains( ev.target ) ) { // eslint-disable-next-line mediawiki/class-doc @@ -53,15 +54,20 @@ function bindToggleClickHandler( searchBox, header, searchToggle ) { // from the page when clicked. ev.preventDefault(); - bindSearchBoxHandler( searchBox, header ); - // eslint-disable-next-line mediawiki/class-doc header.classList.add( SEARCH_VISIBLE_CLASS ); - // Defer focusing the input to another task in the event loop. At the time + // Defer binding the search box handler until after the event bubbles to the + // top of the document so that the handler isn't called when the user clicks + // the search toggle. Event bubbled callbacks execute within the same task + // in the event loop. + // + // Also, defer focusing the input to another task in the event loop. At the time // of this writing, Safari 14.0.3 has trouble changing the visibility of the // element and focusing the input within the same task. setTimeout( function () { + bindSearchBoxHandler( searchBox, header ); + var searchInput = /** @type {HTMLInputElement|null} */ ( searchBox.querySelector( 'input[type="search"]' ) ); if ( searchInput ) { @@ -73,14 +79,34 @@ function bindToggleClickHandler( searchBox, header, searchToggle ) { searchToggle.addEventListener( 'click', handler ); } -module.exports = function initSearchToggle() { - var - header = /** @type {HTMLElement|null} */ ( document.querySelector( HEADER_SELECTOR ) ), - searchBox = /** @type {HTMLElement|null} */ ( document.getElementById( SEARCH_BOX_ID ) ), - searchToggle = - /** @type {HTMLElement|null} */ ( document.querySelector( SEARCH_TOGGLE_SELECTOR ) ); +/** + * Enables search toggling behavior in a header given a toggle element (e.g. + * search icon). When the toggle element is clicked, a class, + * `SEARCH_VISIBLE_CLASS`, will be applied to a header matching the selector + * `HEADER_SELECTOR` and the input inside the element, SEARCH_BOX_SELECTOR, will + * be focused. This class can be used in CSS to show/hide the necessary + * elements. When the user clicks outside of SEARCH_BOX_SELECTOR, the class will + * be removed. + * + * @param {HTMLElement|null} searchToggle + */ +module.exports = function initSearchToggle( searchToggle ) { + // Check if .closest API is available (IE11 does not support it). + if ( !searchToggle || !searchToggle.closest ) { + return; + } - if ( !( searchBox && searchToggle && header ) ) { + var header = + /** @type {HTMLElement|null} */ ( searchToggle.closest( HEADER_SELECTOR ) ); + + if ( !header ) { + return; + } + + var searchBox = + /** @type {HTMLElement|null} */ ( header.querySelector( SEARCH_BOX_SELECTOR ) ); + + if ( !searchBox ) { return; } diff --git a/resources/skins.vector.js/skin.js b/resources/skins.vector.js/skin.js index 27592db..431102b 100644 --- a/resources/skins.vector.js/skin.js +++ b/resources/skins.vector.js/skin.js @@ -50,7 +50,9 @@ function main( window ) { dropdownMenus(); vector.init(); initSearchLoader( document ); - searchToggle(); + // Initialize the search toggle for the main header only. The sticky header + // toggle is initialized after wvui search loads. + searchToggle( document.querySelector( '.mw-header .search-toggle' ) ); languageButton(); stickyHeader(); } diff --git a/resources/skins.vector.js/stickyHeader.js b/resources/skins.vector.js/stickyHeader.js index f9f9e79..bbb41fc 100644 --- a/resources/skins.vector.js/stickyHeader.js +++ b/resources/skins.vector.js/stickyHeader.js @@ -6,7 +6,8 @@ var FIRST_HEADING_ID = 'firstHeading', USER_MENU_ID = 'p-personal', VECTOR_USER_LINKS_SELECTOR = '.vector-user-links', - VECTOR_MENU_CONTENT_LIST_SELECTOR = '.vector-menu-content-list'; + VECTOR_MENU_CONTENT_LIST_SELECTOR = '.vector-menu-content-list', + SEARCH_TOGGLE_SELECTOR = '.vector-sticky-header-search-toggle'; /** * Copies attribute from an element to another. @@ -141,6 +142,31 @@ function makeStickyHeaderFunctional( stickyObserver.observe( stickyIntersection ); } +/** + * @param {HTMLElement} header + */ +function setupSearchIfNeeded( header ) { + var + searchToggle = header.querySelector( SEARCH_TOGGLE_SELECTOR ); + + if ( !( + searchToggle && + window.fetch && + document.body.classList.contains( 'skin-vector-search-vue' ) + ) ) { + return; + } + + // Load the `skins.vector.search` module here or setup an event handler to + // load it depending on the outcome of T289718. After it loads, initialize the + // search toggle. + // + // Example: + // mw.loader.using( 'skins.vector.search', function () { + // initSearchToggle( searchToggle ); + // } ); +} + module.exports = function initStickyHeader() { var header = document.getElementById( STICKY_HEADER_ID ), stickyIntersection = document.getElementById( @@ -162,4 +188,5 @@ module.exports = function initStickyHeader() { } makeStickyHeaderFunctional( header, stickyIntersection, userMenu, userMenuStickyContainer ); + setupSearchIfNeeded( header ); }; diff --git a/resources/skins.vector.search/App.vue b/resources/skins.vector.search/App.vue index 48a48b9..61301fe 100644 --- a/resources/skins.vector.search/App.vue +++ b/resources/skins.vector.search/App.vue @@ -1,6 +1,6 @@