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;