Integrate WVUI search into Vector

Creates a new skins.vector.search module that
replaces the searchSuggest module from MediaWiki core.

This module creates a new Vue app using the WVUI
search widget for the new search experience.

The legacy search input form is still retains on pageload,
and the new search kicks on search input focus.

In order to manage that transition, the legacy search
input is styled to resemble the new WVUI input, and the
new input is manually focused after the component mounts.

Vue is also added as a dev-dependency to help with
type-checking.

Other changes:
* the entry in skin.json is reordered alphabetically after
skins.vector.js

Bug: T264355
Change-Id: Ibb9561a77a14734297cb4d0ddcd415fc0750b45d
This commit is contained in:
Jan Drewniak 2020-11-19 18:12:53 +01:00 committed by jdlrobson
parent 2ff4d4d6ae
commit bd83398659
14 changed files with 266 additions and 60 deletions

View File

@ -14,5 +14,9 @@
{
"resourceModule": "skins.vector.legacy.js",
"maxSize": "1.8 kB"
},
{
"resourceModule": "skins.vector.search",
"maxSize": "2.8 kB"
}
]

View File

@ -7,6 +7,7 @@ use ExtensionRegistry;
use HTMLForm;
use MediaWiki\MediaWikiServices;
use OutputPage;
use ResourceLoader;
use ResourceLoaderContext;
use Skin;
use SkinTemplate;
@ -263,6 +264,9 @@ class Hooks {
return;
}
if ( !$out->getConfig()->get( 'VectorUseCoreSearch' ) ) {
$bodyAttrs['class'] .= ' skin-vector-search-vue';
}
$bodyAttrs['class'] .= ' skin-vector-max-width';
// As of 2020/08/12, the following CSS classes are referred to by the following deployed
@ -329,4 +333,39 @@ class Hooks {
private static function isSkinVersionLegacy(): bool {
return !VectorServices::getFeatureManager()->isFeatureEnabled( Constants::FEATURE_LATEST_SKIN );
}
/**
* ResourceLoaderRegisterModules hook handler
*
* Register the new search module.
* This hook will be removed when wvui is available in core when the patch
* https://gerrit.wikimedia.org/r/c/mediawiki/core/+/641052 is merged.
*
* @see https://www.mediawiki.org/wiki/Manual:Hooks/ResourceLoaderRegisterModules
* @param ResourceLoader $rl
*/
public static function onResourceLoaderRegisterModules( ResourceLoader $rl ) {
if ( $rl->isModuleRegistered( 'wvui' ) ) {
$module = [
'localBasePath' => __DIR__ . '/..',
'remoteExtPath' => 'Vector',
'dependencies' => [
'mediawiki.util',
],
"packageFiles" => [
"resources/skins.vector.search/skins.vector.search.js",
"resources/skins.vector.search/App.vue"
],
"dependencies" => [
"wvui"
],
"messages" => [
"search",
"searchresults",
"searchsuggest-containing"
],
];
$rl->register( 'skins.vector.search', $module );
}
}
}

View File

@ -31,7 +31,8 @@
"MediaWikiPageReadyModule": "https://doc.wikimedia.org/mediawiki-core/master/js/#!/api/mw.plugin.page.ready",
"JQueryStatic": "https://api.jquery.com",
"VectorResourceLoaderVirtualConfig": "#",
"void": "#"
"void": "#",
"Vue.VNode": "https://vuejs.org/v2/api/#VNode-Interface"
}
}
}

67
package-lock.json generated
View File

@ -2429,8 +2429,7 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
"integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=",
"dev": true,
"optional": true
"dev": true
},
"assign-symbols": {
"version": "1.0.0",
@ -5953,8 +5952,7 @@
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz",
"integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=",
"dev": true,
"optional": true
"dev": true
},
"fast-deep-equal": {
"version": "2.0.1",
@ -6456,8 +6454,7 @@
"ansi-regex": {
"version": "2.1.1",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"aproba": {
"version": "1.2.0",
@ -6478,14 +6475,12 @@
"balanced-match": {
"version": "1.0.0",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"brace-expansion": {
"version": "1.1.11",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@ -6500,20 +6495,17 @@
"code-point-at": {
"version": "1.1.0",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"concat-map": {
"version": "0.0.1",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"console-control-strings": {
"version": "1.1.0",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"core-util-is": {
"version": "1.0.2",
@ -6630,8 +6622,7 @@
"inherits": {
"version": "2.0.4",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"ini": {
"version": "1.3.5",
@ -6643,7 +6634,6 @@
"version": "1.0.0",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"number-is-nan": "^1.0.0"
}
@ -6658,7 +6648,6 @@
"version": "3.0.4",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"brace-expansion": "^1.1.7"
}
@ -6666,14 +6655,12 @@
"minimist": {
"version": "0.0.8",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"minipass": {
"version": "2.9.0",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"safe-buffer": "^5.1.2",
"yallist": "^3.0.0"
@ -6692,7 +6679,6 @@
"version": "0.5.1",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"minimist": "0.0.8"
}
@ -6782,8 +6768,7 @@
"number-is-nan": {
"version": "1.0.1",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"object-assign": {
"version": "4.1.1",
@ -6795,7 +6780,6 @@
"version": "1.4.0",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"wrappy": "1"
}
@ -6881,8 +6865,7 @@
"safe-buffer": {
"version": "5.1.2",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"safer-buffer": {
"version": "2.1.2",
@ -6918,7 +6901,6 @@
"version": "1.0.2",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0",
@ -6938,7 +6920,6 @@
"version": "3.0.1",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"ansi-regex": "^2.0.0"
}
@ -6982,14 +6963,12 @@
"wrappy": {
"version": "1.0.2",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"yallist": {
"version": "3.1.1",
"bundled": true,
"dev": true,
"optional": true
"dev": true
}
}
},
@ -8315,8 +8294,7 @@
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
"integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=",
"dev": true,
"optional": true
"dev": true
},
"jsdoc": {
"version": "3.6.3",
@ -9847,7 +9825,7 @@
},
"os-tmpdir": {
"version": "1.0.2",
"resolved": "http://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
"resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
"integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=",
"dev": true
},
@ -9998,7 +9976,7 @@
},
"path-is-absolute": {
"version": "1.0.1",
"resolved": "http://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
"dev": true
},
@ -11645,7 +11623,7 @@
},
"safe-regex": {
"version": "1.1.0",
"resolved": "http://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz",
"resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz",
"integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=",
"dev": true,
"requires": {
@ -13357,7 +13335,7 @@
},
"through": {
"version": "2.3.8",
"resolved": "http://registry.npmjs.org/through/-/through-2.3.8.tgz",
"resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
"integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=",
"dev": true
},
@ -13516,8 +13494,7 @@
"version": "0.14.5",
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
"integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=",
"dev": true,
"optional": true
"dev": true
},
"type-check": {
"version": "0.4.0",
@ -14042,6 +14019,12 @@
"integrity": "sha512-8TEXQxlldWAuIODdukIb+TR5s+9Ds40eSJrw+1iDDA9IFORPjMELarNQE3myz5XIkWWpdprmJjm1/SxMlWOC8A==",
"dev": true
},
"vue": {
"version": "2.6.11",
"resolved": "https://registry.npmjs.org/vue/-/vue-2.6.11.tgz",
"integrity": "sha512-VfPwgcGABbGAue9+sfrD4PuwFar7gPb1yl1UK1MwXoQPAw0BKSqWfoYCT/ThFrdEVWoI51dBuyCoiNU9bZDZxQ==",
"dev": true
},
"vue-eslint-parser": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-7.1.0.tgz",

View File

@ -36,6 +36,7 @@
"pre-commit": "1.2.2",
"stylelint-config-wikimedia": "0.10.3",
"svgo": "1.3.2",
"typescript": "3.8.3"
"typescript": "3.8.3",
"vue": "2.6.11"
}
}

View File

@ -24,10 +24,7 @@ var /** @type {VectorResourceLoaderVirtualConfig} */
LOAD_MEASURE = 'mwVectorVueSearchLoadStartToLoadEnd',
SEARCH_FORM_ID = 'simpleSearch',
SEARCH_INPUT_ID = 'searchInput',
SEARCH_LOADING_CLASS = 'search-form__loader',
SEARCH_MODULE_NAME = config.wgVectorUseCoreSearch ?
'mediawiki.searchSuggest' :
'skins.vector.search';
SEARCH_LOADING_CLASS = 'search-form__loader';
/**
* Loads the search module via `mw.loader.using` on the element's
@ -140,18 +137,23 @@ function initSearchLoader( document ) {
}
/**
* 1. If we're using the search module from MediaWiki Core (searchSuggest),
* load the module.
* 1. If $wgVectorUseCoreSearch is true,
* 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 ( config.wgVectorUseCoreSearch ) {
loadSearchModule( searchInput, SEARCH_MODULE_NAME, function () {} );
if ( config.wgVectorUseCoreSearch || !window.fetch ) {
loadSearchModule( searchInput, 'mediawiki.searchSuggest', function () {} );
} else {
// Remove tooltips while Vue search is still loading
searchInput.setAttribute( 'autocomplete', 'off' );
searchInput.removeAttribute( 'title' );
setLoadingIndicatorListeners( searchForm, true, renderSearchLoadingIndicator );
loadSearchModule(
searchInput,
SEARCH_MODULE_NAME,
'skins.vector.search',
function () {
markLoadEnd();

View File

@ -0,0 +1,86 @@
<template>
<div id="p-search">
<wvui-typeahead-search
id="searchform"
ref="searchForm"
:domain="domain"
:footer-search-text="$i18n('searchsuggest-containing').escaped()"
:suggestions-label="$i18n('searchresults').escaped()"
:accesskey="searchAccessKey"
:title="searchTitle"
:placeholder="searchPlaceholder"
:aria-label="searchPlaceholder"
:initial-input-value="searchQuery"
:button-label="$i18n( 'search' ).escaped()"
:form-action="action"
:search-language="language"
>
<input type="hidden"
name="title"
value="Special:Search"
>
</wvui-typeahead-search>
</div>
</template>
<script>
var wvui = require( 'wvui' );
module.exports = {
name: 'App',
components: wvui,
mounted: function () {
// access the element associated with the wvui-typeahead-search component
// eslint-disable-next-line no-jquery/variable-pattern
var wvuiSearchForm = this.$refs.searchForm.$el;
if ( this.autofocusInput ) {
// TODO: The wvui-typeahead-search component accepts an id prop but does not
// display that value as an HTML attribute on the form element.
wvuiSearchForm.querySelector( 'form' ).setAttribute( 'id', 'searchform' );
// TODO: The wvui-typeahead-search component does not accept an autofocus parameter
// or directive. This can be removed when its does.
wvuiSearchForm.querySelector( 'input' ).focus();
}
},
computed: {
language: function () {
return mw.config.get( 'wgUserLanguage' );
},
domain: function () {
// It might be helpful to allow this to be configurable in future.
return location.host;
}
},
props: {
autofocusInput: {
type: Boolean,
default: false
},
action: {
type: String,
default: ''
},
/** The keyboard shortcut to focus search. */
searchAccessKey: {
type: String
},
/** The access key informational tip for search. */
searchTitle: {
type: String
},
/** The ghost text shown when no search query is entered. */
searchPlaceholder: {
type: String
},
/**
* The search query string taken from the server-side rendered input immediately before
* client render.
*/
searchQuery: {
type: String
}
}
};
</script>

View File

@ -0,0 +1,45 @@
var
Vue = require( 'vue' ).default || require( 'vue' ),
App = require( './App.vue' );
/**
* @param {HTMLElement} searchForm
* @param {HTMLInputElement} search
* @return {void}
*/
function initApp( searchForm, search ) {
// eslint-disable-next-line no-new
new Vue( {
el: '#p-search',
/**
*
* @param {Function} createElement
* @return {Vue.VNode}
*/
render: function ( createElement ) {
return createElement( App, {
props: {
autofocusInput: search === document.activeElement,
action: searchForm.getAttribute( 'action' ),
searchAccessKey: search.getAttribute( 'accessKey' ),
searchTitle: search.getAttribute( 'title' ),
searchPlaceholder: search.getAttribute( 'placeholder' ),
searchQuery: search.value
}
} );
}
} );
}
/**
* @param {Document} document
* @return {void}
*/
function main( document ) {
var
searchForm = /** @type {HTMLElement} */ ( document.querySelector( '#searchform' ) ),
search = /** @type {HTMLInputElement|null} */ ( document.getElementById( 'searchInput' ) );
if ( search && searchForm ) {
initApp( searchForm, search );
}
}
main( document );

View File

@ -0,0 +1,42 @@
@import 'SearchBox.less';
/**
* Minimal styling for initial no-JS server-rendered
* search form, which gets replaced by WVUI on focus.
* Most values are hard-coded since they aim to
* mimic WVUI-specific variables and disable the ResourceLoader LESS transformation of `calc`.
*/
// Parent class can be removed when $wgVectorUseCoreSearch is no longer supported.
.skin-vector-search-vue {
// Position search in header.
#searchInput {
padding-left: 36px;
font-size: inherit;
.transition( none );
// Recreate WVUI expanding input.
&:focus {
position: relative;
// Use ~ and fixed values to disable the LESS transformation in ResourceLoader LESS implementation
padding-left: ~'calc(12px + 2.57142857em + 12px)';
width: ~'calc( 100% + 24px )';
left: ~'calc( -1 * 24px )';
}
}
// Move & resize search icon to match WVUI.
#searchButton {
top: 0;
right: auto;
left: 0;
width: 36px;
min-height: 36px;
background-size: 20px auto;
}
// Reposition search icon for expanded input.
#searchInput:focus ~ #searchButton {
left: -9px;
}
}

View File

@ -38,6 +38,8 @@ body {
flex-wrap: wrap;
// https://caniuse.com/#search=align-items
align-items: center;
// allow z-index to apply so search results overlay article
position: relative;
z-index: @z-index-header;
}

View File

@ -12,7 +12,7 @@
@import 'Indicators.less';
@import 'SiteNotice.less';
@import 'Menu.less';
@import 'SearchBox.less';
@import 'VueEnhancedSearchBox.less';
@import 'SearchBoxLoader.less';
@import 'MenuTabs.less';
@import 'TabWatchstarLink.less';

4
resources/vue.d.ts vendored Normal file
View File

@ -0,0 +1,4 @@
declare module "*.vue" {
import Vue from 'vue';
export default Vue;
}

View File

@ -66,6 +66,7 @@
"SkinPageReadyConfig": "Vector\\Hooks::onSkinPageReadyConfig",
"GetPreferences": "Vector\\Hooks::onGetPreferences",
"PreferencesFormPreSave": "Vector\\Hooks::onPreferencesFormPreSave",
"ResourceLoaderRegisterModules": "Vector\\Hooks::onResourceLoaderRegisterModules",
"SkinTemplateNavigation::Universal": "Vector\\Hooks::onSkinTemplateNavigation",
"LocalUserCreated": "Vector\\Hooks::onLocalUserCreated",
"OutputPageBodyAttributes": "Vector\\Hooks::onOutputPageBodyAttributes",
@ -111,11 +112,6 @@
],
"styles": [ "resources/skins.vector.styles.responsive.less" ]
},
"skins.vector.search": {
"dependencies": [
"vue"
]
},
"skins.vector.js": {
"packageFiles": [
"resources/skins.vector.js/skin.js",

View File

@ -116,7 +116,8 @@
// See skinStyles/jquery.ui/jquery.ui.datepicker.css.
// @z-index-ui-datepicker-cover: -1;
@z-index-base: 0;
@z-index-header: 1;
// Header z-index-header higher than z-index-menu so that search results overlay variants and more menu
@z-index-header: 3;
@z-index-sidebar: 1;
@z-index-menu-checkbox: 1;
@z-index-search-button: 1;