/* Some of the functions are based on Vector */ /* ESLint does not like having class names as const */ /* eslint-disable mediawiki/class-doc */ const SEARCH_INPUT_ID = 'searchInput', SEARCH_LOADING_CLASS = 'search-form__loading'; /** * Loads the search module via `mw.loader.using` on the element's * focus event. Or, if the element is already focused, loads the * search module immediately. * After the search module is loaded, executes a function to remove * the loading indicator. * * @param {HTMLElement} element search input. * @param {string} moduleName resourceLoader module to load. * @param {function(): void} afterLoadFn function to execute after search module loads. */ function loadSearchModule( element, moduleName, afterLoadFn ) { const requestSearchModule = () => { mw.loader.using( moduleName, afterLoadFn ); element.removeEventListener( 'focus', requestSearchModule ); }; if ( document.activeElement === element ) { requestSearchModule(); } else { element.addEventListener( 'focus', requestSearchModule ); } } /** * Event callback that shows or hides the loading indicator based on the event type. * The loading indicator states are: * 1. Show on input event (while user is typing) * 2. Hide on focusout event (when user removes focus from the input ) * 3. Show when input is focused, if it contains a query. (in case user re-focuses on input) * * @param {Event} event */ function renderSearchLoadingIndicator( event ) { const form = /** @type {HTMLElement} */ ( event.currentTarget ), input = /** @type {HTMLInputElement} */ ( event.target ); if ( !( event.currentTarget instanceof HTMLElement ) || !( event.target instanceof HTMLInputElement ) || !( input.id === SEARCH_INPUT_ID ) ) { return; } if ( event.type === 'input' ) { form.classList.add( SEARCH_LOADING_CLASS ); } else if ( event.type === 'focusout' ) { form.classList.remove( SEARCH_LOADING_CLASS ); } else if ( event.type === 'focusin' && input.value.trim() ) { form.classList.add( SEARCH_LOADING_CLASS ); } } /** * Attaches or detaches the event listeners responsible for activating * the loading indicator. * * @param {HTMLElement} element * @param {boolean} attach * @param {function(Event): void} eventCallback */ function setLoadingIndicatorListeners( element, attach, eventCallback ) { /** @type { "addEventListener" | "removeEventListener" } */ const addOrRemoveListener = ( attach ? 'addEventListener' : 'removeEventListener' ); [ 'input', 'focusin', 'focusout' ].forEach( function ( eventType ) { element[ addOrRemoveListener ]( eventType, eventCallback ); } ); if ( !attach ) { element.classList.remove( SEARCH_LOADING_CLASS ); } } /** * Manually focus on the input field if checkbox is checked * * @param {HTMLInputElement} checkbox * @param {HTMLInputElement} input * @return {void} */ function focusOnChecked( checkbox, input ) { if ( checkbox.checked ) { input.focus(); } else { input.blur(); } } /** * Manually check the checkbox state when the button is SLASH is pressed. * * @param {Window} window * @param {HTMLInputElement} checkbox * @param {HTMLInputElement} input * @return {void} */ function bindExpandOnSlash( window, checkbox, input ) { const onExpandOnSlash = ( /** @type {KeyboardEvent} */ event ) => { // Only handle SPACE and ENTER. if ( event.key === '/' && !event.target.matches( 'input' ) && !event.target.matches( 'textarea' ) ) { checkbox.checked = true; focusOnChecked( checkbox, input ); } }; window.addEventListener( 'keyup', onExpandOnSlash, true ); } /** * @param {Window} window * @param {HTMLInputElement} input * @param {HTMLElement} target * @return {void} */ function initCheckboxHack( window, input, target ) { const checkboxHack = require( './checkboxHack.js' ), button = document.getElementById( 'search-toggle' ), checkbox = document.getElementById( 'search-checkbox' ); if ( checkbox instanceof HTMLInputElement && button ) { checkboxHack.bindToggleOnClick( checkbox, button ); checkboxHack.bindUpdateAriaExpandedOnInput( checkbox, button ); checkboxHack.updateAriaExpanded( checkbox, button ); checkboxHack.bindToggleOnSpaceEnter( checkbox, button ); checkboxHack.bindDismissOnClickOutside( window, checkbox, button, target ); checkboxHack.bindDismissOnFocusLoss( window, checkbox, button, target ); checkboxHack.bindDismissOnEscape( window, checkbox ); } bindExpandOnSlash( window, checkbox, input ); // Focus when toggled checkbox.addEventListener( 'input', () => { focusOnChecked( checkbox, input ); } ); } /** * @param {Window} window * @return {void} */ function initSearch( window ) { const searchForm = document.getElementById( 'searchform' ), searchInput = document.getElementById( SEARCH_INPUT_ID ); initCheckboxHack( window, searchInput, searchForm ); if ( mw.config.get( 'wgCitizenEnableSearch' ) ) { setLoadingIndicatorListeners( searchForm, true, renderSearchLoadingIndicator ); loadSearchModule( searchInput, 'skins.citizen.scripts.search', () => { setLoadingIndicatorListeners( searchForm, false, renderSearchLoadingIndicator ); } ); } else { loadSearchModule( searchInput, 'mediawiki.searchSuggest', () => {} ); } } module.exports = { init: initSearch };