mediawiki-skins-Vector/resources/skins.vector.js/searchLoader.js
jdlrobson d8382ec96b Drop search related feature flags
Given Wikidata is the only project using modern Vector,
and the only project where the search API is not applicable,
this will result in a loss of autocomplete on Wikidata.org
which will fall back to the non-JS mode.

Bug: T290688
Change-Id: Iece5a4efd43e09cd90c842c9c134ca115b35f2b2
2022-01-31 21:00:22 +00:00

204 lines
6.4 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' ),
isWikidata = mw.config.get( 'wgWikiID' ) === 'wikidatawiki';
// Allow developers to defined $wgVectorSearchHost in LocalSettings to target different APIs
if ( config.wgVectorSearchHost ) {
mw.config.set( 'wgVectorSearchHost', config.wgVectorSearchHost );
}
if ( !searchBoxes.length ) {
return;
}
/**
* 1. If we are in a browser that doesn't support fetch fall back to non-JS version.
* The check for window.fetch
* can be removed when IE11 support is finally officially dropped.
* 2. Disable on Wikidata per T281318 until the REST API is ready.
**/
if ( isWikidata || !window.fetch ) {
document.body.classList.remove(
'skin-vector-search-vue'
);
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
};