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;