VectorGOLEM/resources/skins.vector.js/searchLoader.js

209 lines
6.5 KiB
JavaScript

/**
* Disabling this rule as it's only necessary for
* combining multiple class names and documenting the output.
* That doesn't happen in this file but the linter still throws an error.
* https://github.com/wikimedia/eslint-plugin-mediawiki/blob/master/docs/rules/class-doc.md
*/
/* eslint-disable mediawiki/class-doc */
/** @interface VectorResourceLoaderVirtualConfig */
/** @interface MediaWikiPageReadyModule */
var /** @type {VectorResourceLoaderVirtualConfig} */
config = require( /** @type {string} */ ( './config.json' ) ),
// T251544: Collect search performance metrics to compare Vue search with
// mediawiki.searchSuggest performance.
CAN_TEST_SEARCH = !!(
window.performance &&
/* eslint-disable compat/compat */
performance.mark &&
performance.measure &&
performance.getEntriesByName ),
/* eslint-enable compat/compat */
LOAD_START_MARK = 'mwVectorVueSearchLoadStart',
LOAD_END_MARK = 'mwVectorVueSearchLoadEnd',
LOAD_MEASURE = 'mwVectorVueSearchLoadStartToLoadEnd',
SEARCH_INPUT_ID = 'searchInput',
SEARCH_LOADING_CLASS = 'search-form__loader';
/**
* 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 {Element} element search input.
* @param {string} moduleName resourceLoader module to load.
* @param {string|null} startMarker
* @param {null|function(): void} afterLoadFn function to execute after search module loads.
*/
function loadSearchModule( element, moduleName, startMarker, afterLoadFn ) {
var SHOULD_TEST_SEARCH = CAN_TEST_SEARCH &&
moduleName === 'skins.vector.search';
function requestSearchModule() {
if ( SHOULD_TEST_SEARCH && startMarker !== null && afterLoadFn !== null ) {
performance.mark( startMarker );
mw.loader.using( moduleName, afterLoadFn );
} else {
mw.loader.load( moduleName );
}
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 ) {
var 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 ( !form.dataset.loadingMsg ) {
form.dataset.loadingMsg = mw.msg( 'vector-search-loader' );
}
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 {Element} element
* @param {boolean} attach
* @param {function(Event): void} eventCallback
*/
function setLoadingIndicatorListeners( element, attach, eventCallback ) {
/** @type { "addEventListener" | "removeEventListener" } */
var addOrRemoveListener = ( attach ? 'addEventListener' : 'removeEventListener' );
[ 'input', 'focusin', 'focusout' ].forEach( function ( eventType ) {
element[ addOrRemoveListener ]( eventType, eventCallback );
} );
if ( !attach ) {
element.classList.remove( SEARCH_LOADING_CLASS );
}
}
/**
* Marks when the lazy load has completed.
*
* @param {string} startMarker
* @param {string} endMarker
* @param {string} measureMarker
*/
function markLoadEnd( startMarker, endMarker, measureMarker ) {
if ( performance.getEntriesByName( startMarker ).length ) {
performance.mark( endMarker );
performance.measure( measureMarker, startMarker, endMarker );
}
}
/**
* Initialize the loading of the search module as well as the loading indicator.
* Only initialize the loading indicator when not using the core search module.
*
* @param {Document} document
*/
function initSearchLoader( document ) {
var searchBoxes = document.querySelectorAll( '.vector-search-box' ),
shouldUseCoreSearch;
// Allow developers to defined $wgVectorSearchHost in LocalSettings to target different APIs
if ( config.wgVectorSearchHost ) {
mw.config.set( 'wgVectorSearchHost', config.wgVectorSearchHost );
}
if ( !searchBoxes.length ) {
return;
}
shouldUseCoreSearch = !document.body.classList.contains( 'skin-vector-search-vue' );
/**
* 1. If $wgVectorUseWvuiSearch is false,
* or we are in a browser that doesn't support fetch
* load the legacy searchSuggest module. The check for window.fetch
* can be removed when IE11 support is finally officially dropped.
* 2. If we're using a different search module, enable the loading indicator
* before the search module loads.
**/
if ( shouldUseCoreSearch || !window.fetch ) {
searchBoxes.forEach( function ( searchBox ) {
var input = searchBox.querySelector( 'input[name="search"]' );
if ( input ) {
loadSearchModule(
input,
'mediawiki.searchSuggest',
null,
null
);
}
} );
return;
}
searchBoxes.forEach( function ( searchBox ) {
var searchInner = searchBox.querySelector( 'form > div' ),
searchInput = searchBox.querySelector( 'input[name="search"]' ),
isPrimarySearch = searchInput && searchInput.getAttribute( 'id' ) === 'searchInput';
if ( !searchInput || !searchInner ) {
return;
}
// Remove tooltips while Vue search is still loading
searchInput.setAttribute( 'autocomplete', 'off' );
searchInput.removeAttribute( 'title' );
setLoadingIndicatorListeners( searchInner, true, renderSearchLoadingIndicator );
loadSearchModule(
searchInput,
'skins.vector.search',
isPrimarySearch ? LOAD_START_MARK : null,
isPrimarySearch ? function () {
markLoadEnd( LOAD_START_MARK, LOAD_END_MARK, LOAD_MEASURE );
setLoadingIndicatorListeners(
// @ts-ignore
searchInner,
false,
renderSearchLoadingIndicator
);
} : null
);
} );
}
module.exports = {
initSearchLoader: initSearchLoader
};