mediawiki-skins-Vector/resources/skins.vector.js/searchLoader.js
Nicholas Ray 76eb7caa2f Update typescript to latest (v4.5.5)
Vector's tsc version is currently reporting errors that the latest
typescript version doesn't report and vice versa. Upgrade to the latest
version to avoid these discrepancies.

Additionally:

Fix tsc errors that are now reported by updated version

Bug: T297614
Change-Id: I0eb67bcc1a5ce214fcf2d6f6433a5de3a845b6a6
2022-01-24 16:35:45 -07:00

216 lines
6.7 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 */
// @ts-ignore
performance.mark &&
// @ts-ignore
performance.measure &&
performance.getEntriesByName ),
/* eslint-enable compat/compat */
LOAD_START_MARK = 'mwVectorVueSearchLoadStart',
LOAD_END_MARK = 'mwVectorVueSearchLoadEnd',
LOAD_MEASURE = 'mwVectorVueSearchLoadStartToLoadEnd',
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 )
) {
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 ) {
Array.prototype.forEach.call( searchBoxes, function ( searchBox ) {
var input = searchBox.querySelector( 'input[name="search"]' );
if ( input ) {
loadSearchModule(
input,
'mediawiki.searchSuggest',
null,
null
);
}
} );
return;
}
Array.prototype.forEach.call( searchBoxes, function ( searchBox ) {
var searchInner = searchBox.querySelector( 'form > div' ),
searchInput = searchBox.querySelector( 'input[name="search"]' ),
clearLoadingIndicators = function () {
setLoadingIndicatorListeners(
// @ts-ignore
searchInner,
false,
renderSearchLoadingIndicator
);
},
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,
// Make sure we clearLoadingIndicators so that event listeners are removed.
// Note, loading Vue.js will remove the element from the DOM.
isPrimarySearch ? function () {
markLoadEnd( LOAD_START_MARK, LOAD_END_MARK, LOAD_MEASURE );
clearLoadingIndicators();
} : clearLoadingIndicators
);
} );
}
module.exports = {
initSearchLoader: initSearchLoader
};