Merge "Adds loading indicator for new search module"
This commit is contained in:
commit
b65de993dc
|
@ -5,7 +5,7 @@
|
|||
},
|
||||
{
|
||||
"resourceModule": "skins.vector.styles",
|
||||
"maxSize": "9.0 kB"
|
||||
"maxSize": "9.1 kB"
|
||||
},
|
||||
{
|
||||
"resourceModule": "skins.vector.styles.responsive",
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
};
|
|
@ -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 );
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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';
|
||||
|
|
|
@ -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": [
|
||||
|
|
Loading…
Reference in New Issue