Merge "Adds loading indicator for new search module"

This commit is contained in:
jenkins-bot 2020-09-08 14:24:09 +00:00 committed by Gerrit Code Review
commit b65de993dc
10 changed files with 262 additions and 18 deletions

View File

@ -5,7 +5,7 @@
},
{
"resourceModule": "skins.vector.styles",
"maxSize": "9.0 kB"
"maxSize": "9.1 kB"
},
{
"resourceModule": "skins.vector.styles.responsive",

View File

@ -24,5 +24,6 @@
"vector-view-viewsource": "View source",
"vector-jumptonavigation": "Jump to navigation",
"vector-jumptosearch": "Jump to search",
"vector-more-actions": "More"
"vector-more-actions": "More",
"vector-search-loader": "Loading search suggestions"
}

View File

@ -35,5 +35,6 @@
"vector-view-viewsource": "Tab label in the Vector skin.\n{{Identical|View source}}",
"vector-jumptonavigation": "Accessibility link for jumping to the navigation links. Visually hidden by default.\n\nSee also\n* {{msg-mw|Navigation}}",
"vector-jumptosearch": "Accessibility link for jumping to the site search. Visually hidden by default.\n\nSee also\n* {{msg-mw|Search}}",
"vector-more-actions": "Label in the Vector skin's menu for the less-important or rarer actions which are not shown as tabs (like moving the page, or for sysops deleting or protecting the page), as well as (for users with a narrow viewing window in their browser) the less-important tab actions which the user's browser is unable to fit in. {{Identical|More}}"
"vector-more-actions": "Label in the Vector skin's menu for the less-important or rarer actions which are not shown as tabs (like moving the page, or for sysops deleting or protecting the page), as well as (for users with a narrow viewing window in their browser) the less-important tab actions which the user's browser is unable to fit in. {{Identical|More}}",
"vector-search-loader": "Text to display below search input while the search suggestion module is loading"
}

View File

@ -17,9 +17,12 @@
},
"wmf": {
"linkMap": {
"\"addEventListener\"": "https://developer.mozilla.org/docs/Web/API/EventTarget/addEventListener",
"Document": "https://developer.mozilla.org/docs/Web/API/Document",
"Event": "https://developer.mozilla.org/docs/Web/API/Event",
"HTMLElement": "https://developer.mozilla.org/docs/Web/API/HTMLElement",
"HTMLInputElement": "https://developer.mozilla.org/docs/Web/API/HTMLInputElement",
"\"removeEventListener\"": "https://developer.mozilla.org/docs/Web/API/EventTarget/removeEventListener",
"Window": "https://developer.mozilla.org/docs/Web/API/Window",
"CheckboxHack": "https://doc.wikimedia.org/mediawiki-core/master/js",

View File

@ -24,7 +24,36 @@ interface MediaWiki {
Api: MwApiConstructor;
config: {
get( configKey: string|null ): string;
}
},
loader: {
/**
* Execute a function after one or more modules are ready.
*
* @param moduleName
*/
using( moduleName: string|null ): JQuery.Promise<any>;
/**
* Load a given resourceLoader module.
*
* @param moduleName
*/
load( moduleName: string|null ): () => void;
/**
* Get the loading state of the module.
* On of 'registered', 'loaded', 'loading', 'ready', 'error', or 'missing'.
*
* @param moduleName
*/
getState( moduleName: string|null ): string;
},
/**
* Loads the specified i18n message string.
* Shortcut for `mw.message( key, parameters... ).text()`.
*
* @param messageName i18n message name
*/
msg( messageName: string|null ): string;
}
declare const mw: MediaWiki;

View File

@ -0,0 +1,139 @@
/**
* 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' ) ),
SEARCH_FORM_ID = 'simpleSearch',
SEARCH_INPUT_ID = 'searchInput',
SEARCH_LOADING_CLASS = 'search-form__loader',
SEARCH_MODULE_NAME = config.wgVectorUseCoreSearch ?
'mediawiki.searchSuggest' :
'skins.vector.search';
/**
* 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 ) {
function requestSearchModule() {
mw.loader.using( moduleName ).then( afterLoadFn );
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 {HTMLElement} 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 );
}
}
/**
* 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 searchForm = document.getElementById( SEARCH_FORM_ID ),
searchInput = document.getElementById( SEARCH_INPUT_ID );
if ( !searchForm || !searchInput ) {
return;
}
/**
* 1. If we're using the search module from MediaWiki Core (searchSuggest),
* load the module.
* 2. If we're using a different search module, enable the loading indicator
* before the search module loads.
**/
if ( config.wgVectorUseCoreSearch ) {
loadSearchModule( searchInput, SEARCH_MODULE_NAME, function () {} );
} else {
setLoadingIndicatorListeners( searchForm, true, renderSearchLoadingIndicator );
loadSearchModule(
searchInput,
SEARCH_MODULE_NAME,
setLoadingIndicatorListeners.bind( null,
searchForm, false, renderSearchLoadingIndicator )
);
}
}
module.exports = {
initSearchLoader: initSearchLoader
};

View File

@ -1,12 +1,6 @@
/** @interface VectorResourceLoaderVirtualConfig */
/** @interface MediaWikiPageReadyModule */
var collapsibleTabs = require( '../skins.vector.legacy.js/collapsibleTabs.js' ),
vector = require( '../skins.vector.legacy.js/vector.js' ),
/** @type {VectorResourceLoaderVirtualConfig} */
config = require( /** @type {string} */ ( './config.json' ) ),
/** @type {MediaWikiPageReadyModule} */
pageReady = require( /** @type {string} */( 'mediawiki.page.ready' ) ),
initSearchLoader = require( './searchLoader.js' ).initSearchLoader,
sidebar = require( './sidebar.js' );
/**
@ -50,11 +44,7 @@ function main( window ) {
collapsibleTabs.init();
sidebar.init( window );
$( vector.init );
pageReady.loadSearchModule(
// Decide between new Vue implementation or old.
config.wgVectorUseCoreSearch ?
'mediawiki.searchSuggest' : 'skins.vector.search'
);
initSearchLoader( document );
}
main( window );

View File

@ -0,0 +1,78 @@
/**
* Loading indicator for search widget
*
* By adding a class on the parent search form
* <div id="simpleSearch" class="search-form__loader"></div>
* A pseudo element is added via ':after' that mimics the appearance
* of the search suggestion and contains the text
* "Loading search suggestions" (message key: vector-search-loader).
*
* After appearing for more than a second, a progress-bar animation appears
* above the loading indicator.
*
**/
#simpleSearch.search-form__loader:after {
// Set the i18n message.
content: attr( data-loading-msg );
//
// Position loader below the input.
display: block;
position: absolute;
//
// IE9-11 have some issues, but not with this basic use-case.
top: 100%;
width: 100%;
//
// Ensure it doesn't extend beyond the input.
box-sizing: border-box;
//
// Align loader style with input.
border: @border-base;
border-top-width: 0; // Hide the top border so it doesn't interfere with focus state.
border-radius: 0 0 @border-radius-base @border-radius-base;
color: @color-base--disabled;
font-size: @font-size-notification;
padding: 0.4em;
.box-shadow( @boxShadowWidget );
//
// Hide text in case it extends beyond the input.
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
//
// Add a progress-bar to the loading indicator,
// but only show it animating after 1 second of loading.
background: /*image*/ linear-gradient( 90deg, @colorProgressive 0%, @colorProgressive 100% )
/* position / size*/ -10% 0 ~'/' 0 2px
/* repeat */ no-repeat,/*second bg, just color*/#fff;
//
// Animates the progress-bar.
animation: /* name */ search-loader-progress-bar
/* duration */ 1200ms
/* timing function */ linear
/* delay */ 1000ms
/* iteration count */ infinite
/* fill direction */ alternate;
}
@keyframes search-loader-progress-bar {
0% {
background-size: 0 2px;
background-position: -10% 0;
}
30% {
background-size: 30% 2px;
background-position: -10% 0;
}
70% {
background-size: 30% 2px;
background-position: 110% 0;
}
100% {
background-size: 0 2px;
background-position: 110% 0;
}
}

View File

@ -13,6 +13,7 @@
@import 'SiteNotice.less';
@import 'Menu.less';
@import 'SearchBox.less';
@import 'SearchBoxLoader.less';
@import 'MenuTabs.less';
@import 'TabWatchstarLink.less';
@import 'MenuDropdown.less';

View File

@ -108,12 +108,14 @@
},
"resources/skins.vector.js/sidebar.js",
"resources/skins.vector.legacy.js/collapsibleTabs.js",
"resources/skins.vector.legacy.js/vector.js"
"resources/skins.vector.legacy.js/vector.js",
"resources/skins.vector.js/searchLoader.js"
],
"dependencies": [
"mediawiki.page.ready",
"mediawiki.util"
]
],
"messages": [ "vector-search-loader" ]
},
"skins.vector.legacy.js": {
"packageFiles": [