From bd8339865990edcc3c80ceb3370bba59c10e19f4 Mon Sep 17 00:00:00 2001 From: Jan Drewniak Date: Thu, 19 Nov 2020 18:12:53 +0100 Subject: [PATCH] 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 --- bundlesize.config.json | 4 + includes/Hooks.php | 39 +++++++++ jsdoc.json | 3 +- package-lock.json | 67 ++++++--------- package.json | 3 +- resources/skins.vector.js/searchLoader.js | 20 +++-- resources/skins.vector.search/App.vue | 86 +++++++++++++++++++ .../skins.vector.search.js | 45 ++++++++++ .../VueEnhancedSearchBox.less | 42 +++++++++ .../skins.vector.styles/layout-default.less | 2 + resources/skins.vector.styles/skin.less | 2 +- resources/vue.d.ts | 4 + skin.json | 6 +- variables.less | 3 +- 14 files changed, 266 insertions(+), 60 deletions(-) create mode 100644 resources/skins.vector.search/App.vue create mode 100644 resources/skins.vector.search/skins.vector.search.js create mode 100644 resources/skins.vector.styles/VueEnhancedSearchBox.less create mode 100644 resources/vue.d.ts diff --git a/bundlesize.config.json b/bundlesize.config.json index 0d9c2a7..1710f06 100644 --- a/bundlesize.config.json +++ b/bundlesize.config.json @@ -14,5 +14,9 @@ { "resourceModule": "skins.vector.legacy.js", "maxSize": "1.8 kB" + }, + { + "resourceModule": "skins.vector.search", + "maxSize": "2.8 kB" } ] diff --git a/includes/Hooks.php b/includes/Hooks.php index 9fde378..03dd586 100644 --- a/includes/Hooks.php +++ b/includes/Hooks.php @@ -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 ); + } + } } diff --git a/jsdoc.json b/jsdoc.json index 0aa173c..1f7788e 100644 --- a/jsdoc.json +++ b/jsdoc.json @@ -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" } } } diff --git a/package-lock.json b/package-lock.json index 110e547..c265048 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index b66b5c7..51314c4 100644 --- a/package.json +++ b/package.json @@ -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" } } diff --git a/resources/skins.vector.js/searchLoader.js b/resources/skins.vector.js/searchLoader.js index 074e56c..372a286 100644 --- a/resources/skins.vector.js/searchLoader.js +++ b/resources/skins.vector.js/searchLoader.js @@ -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(); diff --git a/resources/skins.vector.search/App.vue b/resources/skins.vector.search/App.vue new file mode 100644 index 0000000..27a7891 --- /dev/null +++ b/resources/skins.vector.search/App.vue @@ -0,0 +1,86 @@ + + + diff --git a/resources/skins.vector.search/skins.vector.search.js b/resources/skins.vector.search/skins.vector.search.js new file mode 100644 index 0000000..5b6006f --- /dev/null +++ b/resources/skins.vector.search/skins.vector.search.js @@ -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 ); diff --git a/resources/skins.vector.styles/VueEnhancedSearchBox.less b/resources/skins.vector.styles/VueEnhancedSearchBox.less new file mode 100644 index 0000000..86cd28c --- /dev/null +++ b/resources/skins.vector.styles/VueEnhancedSearchBox.less @@ -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; + } +} diff --git a/resources/skins.vector.styles/layout-default.less b/resources/skins.vector.styles/layout-default.less index 0f8dc4e..8b828a8 100644 --- a/resources/skins.vector.styles/layout-default.less +++ b/resources/skins.vector.styles/layout-default.less @@ -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; } diff --git a/resources/skins.vector.styles/skin.less b/resources/skins.vector.styles/skin.less index 69b68f7..c1676c4 100644 --- a/resources/skins.vector.styles/skin.less +++ b/resources/skins.vector.styles/skin.less @@ -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'; diff --git a/resources/vue.d.ts b/resources/vue.d.ts new file mode 100644 index 0000000..8f3a240 --- /dev/null +++ b/resources/vue.d.ts @@ -0,0 +1,4 @@ +declare module "*.vue" { + import Vue from 'vue'; + export default Vue; +} diff --git a/skin.json b/skin.json index bed3c25..45f8b46 100644 --- a/skin.json +++ b/skin.json @@ -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", diff --git a/variables.less b/variables.less index 45e5545..64f4263 100644 --- a/variables.less +++ b/variables.less @@ -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;