2021-04-22 16:09:14 +00:00
|
|
|
/* Some of the functions are based on Vector */
|
|
|
|
/* ESLint does not like having class names as const */
|
2022-05-13 04:17:27 +00:00
|
|
|
|
2022-12-03 18:42:01 +00:00
|
|
|
const SEARCH_LOADING_CLASS = 'citizen-loading';
|
2021-04-22 16:09:14 +00:00
|
|
|
|
2021-04-22 15:44:48 +00:00
|
|
|
/**
|
|
|
|
* 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 );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-04-22 16:09:14 +00:00
|
|
|
/**
|
|
|
|
* 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 ) ||
|
2022-12-02 22:39:05 +00:00
|
|
|
!( event.target instanceof HTMLInputElement )
|
2022-12-02 22:35:15 +00:00
|
|
|
) {
|
2021-04-22 16:09:14 +00:00
|
|
|
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 );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-01-16 22:01:58 +00:00
|
|
|
/**
|
2021-04-21 22:47:03 +00:00
|
|
|
* Manually focus on the input field if checkbox is checked
|
2021-01-16 22:01:58 +00:00
|
|
|
*
|
2021-04-21 22:47:03 +00:00
|
|
|
* @param {HTMLInputElement} checkbox
|
|
|
|
* @param {HTMLInputElement} input
|
2021-04-21 17:44:28 +00:00
|
|
|
* @return {void}
|
2021-01-16 22:01:58 +00:00
|
|
|
*/
|
2021-04-21 22:47:03 +00:00
|
|
|
function focusOnChecked( checkbox, input ) {
|
|
|
|
if ( checkbox.checked ) {
|
|
|
|
input.focus();
|
2021-04-12 20:22:16 +00:00
|
|
|
} else {
|
2021-04-21 22:47:03 +00:00
|
|
|
input.blur();
|
2021-04-12 19:55:59 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-05-10 17:26:39 +00:00
|
|
|
/**
|
|
|
|
* Check if the element is a HTML form element or content editable
|
2022-12-02 22:35:15 +00:00
|
|
|
* This is to prevent trigger search box when user is typing on a textfield, input, etc.
|
2021-05-10 17:26:39 +00:00
|
|
|
*
|
|
|
|
* @param {HTMLElement} element
|
|
|
|
* @return {boolean}
|
|
|
|
*/
|
|
|
|
function isFormField( element ) {
|
2021-05-10 17:27:36 +00:00
|
|
|
if ( !( element instanceof HTMLElement ) ) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
const name = element.nodeName.toLowerCase();
|
|
|
|
const type = ( element.getAttribute( 'type' ) || '' ).toLowerCase();
|
|
|
|
return ( name === 'select' ||
|
2021-05-10 17:26:39 +00:00
|
|
|
name === 'textarea' ||
|
|
|
|
( name === 'input' && type !== 'submit' && type !== 'reset' && type !== 'checkbox' && type !== 'radio' ) ||
|
|
|
|
element.isContentEditable );
|
|
|
|
}
|
|
|
|
|
2021-04-12 19:09:28 +00:00
|
|
|
/**
|
2021-04-21 22:47:03 +00:00
|
|
|
* Manually check the checkbox state when the button is SLASH is pressed.
|
2021-04-12 19:09:28 +00:00
|
|
|
*
|
2021-04-21 22:47:03 +00:00
|
|
|
* @param {Window} window
|
|
|
|
* @param {HTMLInputElement} checkbox
|
|
|
|
* @param {HTMLInputElement} input
|
2021-04-21 17:44:28 +00:00
|
|
|
* @return {void}
|
2021-04-12 19:09:28 +00:00
|
|
|
*/
|
2021-04-21 22:47:03 +00:00
|
|
|
function bindExpandOnSlash( window, checkbox, input ) {
|
|
|
|
const onExpandOnSlash = ( /** @type {KeyboardEvent} */ event ) => {
|
2023-02-15 21:08:39 +00:00
|
|
|
const isKeyPressed = () => {
|
|
|
|
// "/" key is standard on many sites
|
|
|
|
if ( event.key === '/' ) {
|
|
|
|
return true;
|
|
|
|
// "Alt" + "Shift" + "F" is the MW standard key
|
|
|
|
// Shift key might makes F key goes capital, so we need to make it lowercase
|
|
|
|
} else if ( event.altKey && event.shiftKey && event.key.toLowerCase() === 'f' ) {
|
|
|
|
return true;
|
|
|
|
} else {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
if ( isKeyPressed() && !isFormField( event.target ) ) {
|
2021-05-10 17:36:44 +00:00
|
|
|
// Since Firefox quickfind interfere with this
|
|
|
|
event.preventDefault();
|
2021-04-21 17:44:28 +00:00
|
|
|
checkbox.checked = true;
|
2021-04-21 22:47:03 +00:00
|
|
|
focusOnChecked( checkbox, input );
|
2021-04-12 19:55:59 +00:00
|
|
|
}
|
2021-04-21 22:47:03 +00:00
|
|
|
};
|
|
|
|
|
2021-05-10 17:36:44 +00:00
|
|
|
window.addEventListener( 'keydown', onExpandOnSlash, true );
|
2021-04-12 19:09:28 +00:00
|
|
|
}
|
|
|
|
|
2023-07-10 20:23:48 +00:00
|
|
|
/**
|
|
|
|
* Add clear button to search field when there are input value
|
|
|
|
*
|
|
|
|
* @param {HTMLInputElement} input
|
|
|
|
* @return {void}
|
|
|
|
*/
|
|
|
|
function renderSearchClearButton( input ) {
|
|
|
|
const
|
2023-07-16 20:48:14 +00:00
|
|
|
clearButton = document.createElement( 'span' ),
|
2023-07-10 20:23:48 +00:00
|
|
|
clearIcon = document.createElement( 'span' );
|
|
|
|
|
2023-08-29 01:36:51 +00:00
|
|
|
let hasClearButton = false;
|
|
|
|
|
2023-07-16 20:48:14 +00:00
|
|
|
clearButton.classList.add( 'citizen-search__clear', 'citizen-search__formButton' );
|
2024-05-21 22:08:28 +00:00
|
|
|
// TODO: Add i18n for the message below
|
|
|
|
// clearButton.setAttribute( 'aria-label', 'Clear search input' );
|
2023-07-10 20:23:48 +00:00
|
|
|
clearIcon.classList.add( 'citizen-ui-icon', 'mw-ui-icon-wikimedia-clear' );
|
|
|
|
clearButton.append( clearIcon );
|
|
|
|
|
|
|
|
clearButton.addEventListener( 'click', ( event ) => {
|
|
|
|
event.preventDefault();
|
2024-05-21 22:08:28 +00:00
|
|
|
clearButton.classList.add( 'hidden' );
|
2023-07-10 20:23:48 +00:00
|
|
|
input.value = '';
|
2023-08-29 01:36:51 +00:00
|
|
|
input.dispatchEvent( new Event( 'input' ) );
|
2024-05-21 22:08:28 +00:00
|
|
|
requestAnimationFrame( () => {
|
2023-08-29 01:36:51 +00:00
|
|
|
input.focus();
|
2024-05-21 22:08:28 +00:00
|
|
|
} );
|
2023-07-10 20:23:48 +00:00
|
|
|
} );
|
|
|
|
|
2023-07-16 07:50:43 +00:00
|
|
|
input.addEventListener( 'input', () => {
|
2024-05-21 22:08:28 +00:00
|
|
|
const value = input.value;
|
|
|
|
const shouldDisplay = value !== '';
|
|
|
|
clearButton.classList.toggle( 'hidden', !shouldDisplay );
|
|
|
|
if ( shouldDisplay && !hasClearButton ) {
|
2023-07-16 07:50:43 +00:00
|
|
|
input.after( clearButton );
|
2023-07-10 20:23:48 +00:00
|
|
|
}
|
2024-05-21 22:08:28 +00:00
|
|
|
hasClearButton = shouldDisplay;
|
2023-07-10 20:23:48 +00:00
|
|
|
} );
|
|
|
|
}
|
|
|
|
|
2021-04-21 22:47:03 +00:00
|
|
|
/**
|
2024-05-21 22:08:28 +00:00
|
|
|
* Initializes the search functionality for the Citizen search boxes.
|
|
|
|
*
|
2021-04-21 22:47:03 +00:00
|
|
|
* @param {Window} window
|
|
|
|
* @return {void}
|
|
|
|
*/
|
|
|
|
function initSearch( window ) {
|
2022-12-02 23:00:32 +00:00
|
|
|
const
|
2022-12-02 22:59:45 +00:00
|
|
|
searchModule = require( './config.json' ).wgCitizenSearchModule,
|
2022-12-02 22:35:15 +00:00
|
|
|
searchBoxes = document.querySelectorAll( '.citizen-search-box' );
|
2022-05-13 02:37:28 +00:00
|
|
|
|
2022-12-02 22:35:15 +00:00
|
|
|
if ( !searchBoxes.length ) {
|
|
|
|
return;
|
|
|
|
}
|
2021-04-21 22:47:03 +00:00
|
|
|
|
2022-12-02 22:35:15 +00:00
|
|
|
searchBoxes.forEach( ( searchBox ) => {
|
2022-12-02 22:39:05 +00:00
|
|
|
const
|
2022-12-02 22:35:15 +00:00
|
|
|
input = searchBox.querySelector( 'input[name="search"]' ),
|
|
|
|
isPrimarySearch = input && input.getAttribute( 'id' ) === 'searchInput';
|
2021-04-22 15:44:48 +00:00
|
|
|
|
2022-12-02 22:35:15 +00:00
|
|
|
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 );
|
|
|
|
} );
|
|
|
|
}
|
|
|
|
|
2023-07-16 07:50:43 +00:00
|
|
|
renderSearchClearButton( input );
|
2022-12-02 22:59:45 +00:00
|
|
|
setLoadingIndicatorListeners( searchBox, true, renderSearchLoadingIndicator );
|
|
|
|
loadSearchModule( input, searchModule, () => {
|
|
|
|
setLoadingIndicatorListeners( searchBox, false, renderSearchLoadingIndicator );
|
|
|
|
} );
|
2022-12-02 22:35:15 +00:00
|
|
|
} );
|
2021-01-16 22:01:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
module.exports = {
|
2021-04-21 17:44:28 +00:00
|
|
|
init: initSearch
|
2021-01-16 22:01:58 +00:00
|
|
|
};
|