/** * Provides a {@link jQuery} plugin that add suggestions to a text box. * * @module jquery.suggestions */ ( function () { /** * Cancel any delayed maybeFetch() call and callback the context so * they can cancel any async fetching if they use AJAX or something. * * @param {Object} context */ function cancel( context ) { if ( context.data.timerID !== null ) { clearTimeout( context.data.timerID ); } if ( typeof context.config.cancel === 'function' ) { context.config.cancel.call( context.data.$textbox ); } } /** * Hide the element with suggestions and clean up some state. * * @param {Object} context */ function hide( context ) { // Remove any highlights, including on "special" items context.data.$container.find( '.suggestions-result-current' ).removeClass( 'suggestions-result-current' ); // Hide the container context.data.$container.hide(); } /** * Restore the text the user originally typed in the textbox, before it * was overwritten by highlight(). This restores the value the currently * displayed suggestions are based on, rather than the value just before * highlight() overwrote it; the former is arguably slightly more sensible. * * @param {Object} context */ function restore( context ) { context.data.$textbox.val( context.data.prevText ); } /** * @param {Object} context */ function special( context ) { // Allow custom rendering - but otherwise don't do any rendering if ( typeof context.config.special.render === 'function' ) { // Wait for the browser to update the value setTimeout( () => { // Render special const $special = context.data.$container.find( '.suggestions-special' ); context.config.special.render.call( $special, context.data.$textbox.val(), context ); }, 1 ); } } /** * Ask the user-specified callback for new suggestions. Any previous delayed * call to this function still pending will be canceled. If the value in the * textbox is empty or hasn't changed since the last time suggestions were fetched, * this function does nothing. * * @param {Object} context * @param {boolean} delayed Whether or not to delay this by the currently configured amount of time */ function update( context, delayed ) { function maybeFetch() { if ( typeof context.config.update.before === 'function' ) { context.config.update.before.call( context.data.$textbox ); } const val = context.data.$textbox.val(), cache = context.data.cache; let cacheHit = false; // Only fetch if the value in the textbox changed and is not empty, or if the results were hidden // if the textbox is empty then clear the result div, but leave other settings intouched if ( val.length === 0 ) { hide( context ); context.data.prevText = ''; } else if ( val !== context.data.prevText || // eslint-disable-next-line no-jquery/no-sizzle !context.data.$container.is( ':visible' ) ) { context.data.prevText = val; // Try cache first if ( context.config.cache && val in cache ) { if ( mw.now() - cache[ val ].timestamp < context.config.cacheMaxAge ) { context.data.$textbox.suggestions( 'suggestions', cache[ val ].suggestions ); if ( typeof context.config.update.after === 'function' ) { context.config.update.after.call( context.data.$textbox, cache[ val ].metadata ); } cacheHit = true; } else { // Cache expired delete cache[ val ]; } } if ( !cacheHit && typeof context.config.fetch === 'function' ) { context.config.fetch.call( context.data.$textbox, val, ( suggestions, metadata ) => { suggestions = suggestions.slice( 0, context.config.maxRows ); context.data.$textbox.suggestions( 'suggestions', suggestions ); if ( typeof context.config.update.after === 'function' ) { context.config.update.after.call( context.data.$textbox, metadata ); } if ( context.config.cache ) { cache[ val ] = { suggestions: suggestions, metadata: metadata, timestamp: mw.now() }; } }, context.config.maxRows ); } } // Always update special rendering special( context ); } // Cancels any delayed maybeFetch call, and invokes context.config.cancel. cancel( context ); if ( delayed ) { // To avoid many started/aborted requests while typing, we're gonna take a short // break before trying to fetch data. context.data.timerID = setTimeout( maybeFetch, context.config.delay ); } else { maybeFetch(); } } /** * Highlight a result in the results table * * @param {Object} context * @param {jQuery|string} $result `