/* Some of the functions are based on Vector */ /* ESLint does not like having class names as const */ const SEARCH_INPUT_ID = 'searchInput', SEARCH_LOADING_CLASS = 'citizen-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 ) ) { 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(); } } /** * Check if the element is a HTML form element or content editable * This is to prevent trigger search box when user is typing on a textfield, input, etc. * * @param {HTMLElement} element * @return {boolean} */ function isFormField( element ) { if ( !( element instanceof HTMLElement ) ) { return false; } const name = element.nodeName.toLowerCase(); const type = ( element.getAttribute( 'type' ) || '' ).toLowerCase(); return ( name === 'select' || name === 'textarea' || ( name === 'input' && type !== 'submit' && type !== 'reset' && type !== 'checkbox' && type !== 'radio' ) || element.isContentEditable ); } /** * 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 === '/' && !isFormField( event.target ) ) { // Since Firefox quickfind interfere with this event.preventDefault(); checkbox.checked = true; focusOnChecked( checkbox, input ); } }; window.addEventListener( 'keydown', onExpandOnSlash, true ); } /** * @param {Window} window * @return {void} */ function initSearch( window ) { const searchConfig = require( './config.json' ).wgCitizenEnableSearch, searchBoxes = document.querySelectorAll( '.citizen-search-box' ); if ( !searchBoxes.length ) { return; } searchBoxes.forEach( ( searchBox ) => { const input = searchBox.querySelector( 'input[name="search"]' ), isPrimarySearch = input && input.getAttribute( 'id' ) === 'searchInput'; if ( !input ) { return; } // Set up primary search box interactions if ( isPrimarySearch ) { const checkbox = document.getElementById( 'citizen-search__checkbox' ); bindExpandOnSlash( window, checkbox, input ); // Focus when toggled checkbox.addEventListener( 'input', () => { focusOnChecked( checkbox, input ); } ); } if ( searchConfig ) { setLoadingIndicatorListeners( searchBox, true, renderSearchLoadingIndicator ); loadSearchModule( input, 'skins.citizen.search', () => { setLoadingIndicatorListeners( searchBox, false, renderSearchLoadingIndicator ); } ); } else { loadSearchModule( input, 'mediawiki.searchSuggest', () => {} ); } } ); } module.exports = { init: initSearch };