Merge "Allow multiple search components on the same page"

This commit is contained in:
jenkins-bot 2021-09-24 17:04:14 +00:00 committed by Gerrit Code Review
commit 84023ff39c
17 changed files with 206 additions and 120 deletions

View file

@ -5,7 +5,7 @@
}, },
{ {
"resourceModule": "skins.vector.styles", "resourceModule": "skins.vector.styles",
"maxSize": "9.8 kB" "maxSize": "10.1 kB"
}, },
{ {
"resourceModule": "skins.vector.legacy.js", "resourceModule": "skins.vector.legacy.js",

View file

@ -411,7 +411,9 @@ class SkinVector extends SkinMustache {
$commonSkinData['data-search-box'] = $this->getSearchData( $commonSkinData['data-search-box'] = $this->getSearchData(
$commonSkinData['data-search-box'], $commonSkinData['data-search-box'],
!$this->isLegacy() !$this->isLegacy(),
true,
'searchform'
); );
return $commonSkinData; return $commonSkinData;
@ -422,22 +424,31 @@ class SkinVector extends SkinMustache {
* *
* @param array $searchBoxData * @param array $searchBoxData
* @param bool $isCollapsible * @param bool $isCollapsible
* @param bool $isPrimary
* @param string $formId
* @return array modified version of $searchBoxData * @return array modified version of $searchBoxData
*/ */
private function getSearchData( array $searchBoxData, bool $isCollapsible ) { private function getSearchData( array $searchBoxData, bool $isCollapsible, bool $isPrimary, string $formId ) {
$searchClass = 'vector-search-box'; $searchClass = '';
// Determine the search widget treatment to send to the user
if ( VectorServices::getFeatureManager()->isFeatureEnabled( Constants::FEATURE_USE_WVUI_SEARCH ) ) {
$searchClass .= 'vector-search-box-vue ';
}
if ( $isCollapsible ) { if ( $isCollapsible ) {
$searchClass .= ' vector-search-box-collapses'; $searchClass .= ' vector-search-box-collapses ';
} }
if ( $this->shouldSearchExpand() ) { if ( $this->shouldSearchExpand() ) {
$searchClass .= " " . self::SEARCH_EXPANDING_CLASS; $searchClass .= ' ' . self::SEARCH_EXPANDING_CLASS;
} }
// Annotate search box with a component class. // Annotate search box with a component class.
$searchBoxData['class'] = $searchClass; $searchBoxData['class'] = trim( $searchClass );
$searchBoxData['is-collapsible'] = $isCollapsible; $searchBoxData['is-collapsible'] = $isCollapsible;
$searchBoxData['is-primary'] = $isPrimary;
$searchBoxData['form-id'] = $formId;
// At lower resolutions the search input is hidden search and only the submit button is shown. // At lower resolutions the search input is hidden search and only the submit button is shown.
// It should behave like a form submit link (e.g. submit the form with no input value). // It should behave like a form submit link (e.g. submit the form with no input value).

View file

@ -1,16 +1,20 @@
{{! {{!
See @typedef SearchData See @typedef SearchData
}} }}
<div id="p-search" role="search" class="{{class}}"> <div {{#is-primary}}id="p-search"{{/is-primary}} role="search" class="{{class}} vector-search-box">
<div> <div>
{{#is-legacy}} {{#is-legacy}}
<h3 {{{html-user-language-attributes}}}> <h3 {{{html-user-language-attributes}}}>
<label for="searchInput">{{msg-search}}</label> <label {{#is-primary}}for="searchInput"{{/is-primary}}>{{msg-search}}</label>
</h3> </h3>
{{/is-legacy}} {{/is-legacy}}
<form action="{{form-action}}" id="searchform"> <form action="{{form-action}}" id="{{form-id}}"
<div id="simpleSearch"{{#input-location}} data-search-loc="{{.}}"{{/input-location}}> class="vector-search-box-form">
{{{html-input}}} <div {{#is-primary}}id="simpleSearch"{{/is-primary}}
class="vector-search-box-inner"
{{#input-location}} data-search-loc="{{.}}"{{/input-location}}>
<input class="vector-search-box-input"
{{{html-input-attributes}}} {{#is-primary}}id="searchInput"{{/is-primary}} />
<input type="hidden" name="title" value="{{page-title}}"/> <input type="hidden" name="title" value="{{page-title}}"/>
{{! We construct two buttons (for 'go' and 'fulltext' search modes), but only one will be {{! We construct two buttons (for 'go' and 'fulltext' search modes), but only one will be
visible and actionable at a time (they are overlaid on top of each other in CSS). visible and actionable at a time (they are overlaid on top of each other in CSS).
@ -20,8 +24,10 @@
* The mediawiki.searchSuggest module, after doing tests for the broken browsers, removes * The mediawiki.searchSuggest module, after doing tests for the broken browsers, removes
the 'fulltext' button and handles 'fulltext' search itself; this will reveal the 'go' the 'fulltext' button and handles 'fulltext' search itself; this will reveal the 'go'
button and cause it to be used. !}} button and cause it to be used. !}}
{{{html-button-search-fallback}}} <input {{#is-primary}}id="mw-searchButton"{{/is-primary}}
{{{html-button-search}}} {{{html-button-fulltext-attributes}}} value="{{msg-searchbutton}}" />
<input {{#is-primary}}id="searchButton"{{/is-primary}}
{{{html-button-go-attributes}}} value="{{msg-searcharticle}}" />
</div> </div>
</form> </form>
</div> </div>

View file

@ -4,13 +4,20 @@
// Defined as `div`. // Defined as `div`.
// Provide extra element for gadgets due to `form` already carrying an `id`. // Provide extra element for gadgets due to `form` already carrying an `id`.
// FIXME: Remove #simpleSearch when cache has cleared
.vector-search-box-inner,
#simpleSearch { #simpleSearch {
position: relative; position: relative;
height: 100%; height: 100%;
} }
// The search input. // The search input.
#searchInput { // Note that these rules only apply to the non-Vue enabled search input field.
// When Vue.js has loaded this element will no longer be in the page and subsituted with
// a WVUI element.
// FIXME: Remove searchInput selector when cache has cleared.
#searchInput,
.vector-search-box-input {
background-color: rgba( 255, 255, 255, 0.5 ); background-color: rgba( 255, 255, 255, 0.5 );
color: @color-base--emphasized; color: @color-base--emphasized;
width: 100%; width: 100%;
@ -34,11 +41,15 @@
// Support: Firefox. // Support: Firefox.
-moz-appearance: textfield; -moz-appearance: textfield;
// FIXME: Remove #simpleSearch when cache has cleared
.vector-search-box-inner:hover &,
#simpleSearch:hover & { #simpleSearch:hover & {
border-color: @colorGray7; border-color: @colorGray7;
} }
// FIXME: #simpleSearch can be removed when cache has cleared.
&:focus, &:focus,
.vector-search-box-inner:hover &:focus,
#simpleSearch:hover &:focus { #simpleSearch:hover &:focus {
outline: 0; outline: 0;
border-color: @border-color-base--focus; border-color: @border-color-base--focus;
@ -60,8 +71,7 @@
// The search buttons. Fallback and search button are displayed in the same position, // The search buttons. Fallback and search button are displayed in the same position,
// and if both are present the fulltext search one obscures the 'Go' one. // and if both are present the fulltext search one obscures the 'Go' one.
#searchButton, .searchButton {
#mw-searchButton {
background-color: transparent; background-color: transparent;
position: absolute; position: absolute;
top: @border-width-base; top: @border-width-base;
@ -85,7 +95,7 @@
z-index: @z-index-search-button; z-index: @z-index-search-button;
} }
#searchButton { .searchButton[ name='go' ] {
background: no-repeat center/unit( 16 / @font-size-browser / @font-size-search-input, em ) url( images/search.svg ); background: no-repeat center/unit( 16 / @font-size-browser / @font-size-search-input, em ) url( images/search.svg );
opacity: 0.67; opacity: 0.67;
} }

View file

@ -23,7 +23,6 @@ var /** @type {VectorResourceLoaderVirtualConfig} */
LOAD_START_MARK = 'mwVectorVueSearchLoadStart', LOAD_START_MARK = 'mwVectorVueSearchLoadStart',
LOAD_END_MARK = 'mwVectorVueSearchLoadEnd', LOAD_END_MARK = 'mwVectorVueSearchLoadEnd',
LOAD_MEASURE = 'mwVectorVueSearchLoadStartToLoadEnd', LOAD_MEASURE = 'mwVectorVueSearchLoadStartToLoadEnd',
SEARCH_FORM_ID = 'simpleSearch',
SEARCH_INPUT_ID = 'searchInput', SEARCH_INPUT_ID = 'searchInput',
SEARCH_LOADING_CLASS = 'search-form__loader'; SEARCH_LOADING_CLASS = 'search-form__loader';
@ -34,18 +33,22 @@ var /** @type {VectorResourceLoaderVirtualConfig} */
* After the search module is loaded, executes a function to remove * After the search module is loaded, executes a function to remove
* the loading indicator. * the loading indicator.
* *
* @param {HTMLElement} element search input. * @param {Element} element search input.
* @param {string} moduleName resourceLoader module to load. * @param {string} moduleName resourceLoader module to load.
* @param {function(): void} afterLoadFn function to execute after search module loads. * @param {string|null} startMarker
* @param {null|function(): void} afterLoadFn function to execute after search module loads.
*/ */
function loadSearchModule( element, moduleName, afterLoadFn ) { function loadSearchModule( element, moduleName, startMarker, afterLoadFn ) {
var SHOULD_TEST_SEARCH = CAN_TEST_SEARCH && moduleName === 'skins.vector.search'; var SHOULD_TEST_SEARCH = CAN_TEST_SEARCH &&
moduleName === 'skins.vector.search';
function requestSearchModule() { function requestSearchModule() {
if ( SHOULD_TEST_SEARCH ) { if ( SHOULD_TEST_SEARCH && startMarker !== null && afterLoadFn !== null ) {
performance.mark( LOAD_START_MARK ); performance.mark( startMarker );
mw.loader.using( moduleName, afterLoadFn );
} else {
mw.loader.load( moduleName );
} }
mw.loader.using( moduleName, afterLoadFn );
element.removeEventListener( 'focus', requestSearchModule ); element.removeEventListener( 'focus', requestSearchModule );
} }
@ -96,7 +99,7 @@ function renderSearchLoadingIndicator( event ) {
* Attaches or detaches the event listeners responsible for activating * Attaches or detaches the event listeners responsible for activating
* the loading indicator. * the loading indicator.
* *
* @param {HTMLElement} element * @param {Element} element
* @param {boolean} attach * @param {boolean} attach
* @param {function(Event): void} eventCallback * @param {function(Event): void} eventCallback
*/ */
@ -116,11 +119,15 @@ function setLoadingIndicatorListeners( element, attach, eventCallback ) {
/** /**
* Marks when the lazy load has completed. * Marks when the lazy load has completed.
*
* @param {string} startMarker
* @param {string} endMarker
* @param {string} measureMarker
*/ */
function markLoadEnd() { function markLoadEnd( startMarker, endMarker, measureMarker ) {
if ( performance.getEntriesByName( LOAD_START_MARK ).length ) { if ( performance.getEntriesByName( startMarker ).length ) {
performance.mark( LOAD_END_MARK ); performance.mark( endMarker );
performance.measure( LOAD_MEASURE, LOAD_START_MARK, LOAD_END_MARK ); performance.measure( measureMarker, startMarker, endMarker );
} }
} }
@ -131,8 +138,7 @@ function markLoadEnd() {
* @param {Document} document * @param {Document} document
*/ */
function initSearchLoader( document ) { function initSearchLoader( document ) {
var searchForm = document.getElementById( SEARCH_FORM_ID ), var searchBoxes = document.querySelectorAll( '.vector-search-box' ),
searchInput = document.getElementById( SEARCH_INPUT_ID ),
shouldUseCoreSearch; shouldUseCoreSearch;
// Allow developers to defined $wgVectorSearchHost in LocalSettings to target different APIs // Allow developers to defined $wgVectorSearchHost in LocalSettings to target different APIs
@ -140,7 +146,7 @@ function initSearchLoader( document ) {
mw.config.set( 'wgVectorSearchHost', config.wgVectorSearchHost ); mw.config.set( 'wgVectorSearchHost', config.wgVectorSearchHost );
} }
if ( !searchForm || !searchInput ) { if ( !searchBoxes.length ) {
return; return;
} }
@ -155,27 +161,46 @@ function initSearchLoader( document ) {
* before the search module loads. * before the search module loads.
**/ **/
if ( shouldUseCoreSearch || !window.fetch ) { if ( shouldUseCoreSearch || !window.fetch ) {
loadSearchModule( searchInput, 'mediawiki.searchSuggest', function () {} ); searchBoxes.forEach( function ( searchBox ) {
} else { var input = searchBox.querySelector( 'input[name="search"]' );
if ( input ) {
loadSearchModule(
input,
'mediawiki.searchSuggest',
null,
null
);
}
} );
return;
}
searchBoxes.forEach( function ( searchBox ) {
var searchInner = searchBox.querySelector( 'form > div' ),
searchInput = searchBox.querySelector( 'input[name="search"]' ),
isPrimarySearch = searchInput && searchInput.getAttribute( 'id' ) === 'searchInput';
if ( !searchInput || !searchInner ) {
return;
}
// Remove tooltips while Vue search is still loading // Remove tooltips while Vue search is still loading
searchInput.setAttribute( 'autocomplete', 'off' ); searchInput.setAttribute( 'autocomplete', 'off' );
searchInput.removeAttribute( 'title' ); searchInput.removeAttribute( 'title' );
setLoadingIndicatorListeners( searchForm, true, renderSearchLoadingIndicator ); setLoadingIndicatorListeners( searchInner, true, renderSearchLoadingIndicator );
loadSearchModule( loadSearchModule(
searchInput, searchInput,
'skins.vector.search', 'skins.vector.search',
function () { isPrimarySearch ? LOAD_START_MARK : null,
markLoadEnd(); isPrimarySearch ? function () {
markLoadEnd( LOAD_START_MARK, LOAD_END_MARK, LOAD_MEASURE );
setLoadingIndicatorListeners( setLoadingIndicatorListeners(
/** @type {HTMLElement} */ ( searchForm ), // @ts-ignore
searchInner,
false, false,
renderSearchLoadingIndicator renderSearchLoadingIndicator
); );
} } : null
); );
} );
}
} }
module.exports = { module.exports = {

View file

@ -42,7 +42,7 @@ function bindSearchBoxHandler( searchBox, header ) {
* *
* @param {HTMLElement} searchBox * @param {HTMLElement} searchBox
* @param {HTMLElement} header * @param {HTMLElement} header
* @param {HTMLElement} searchToggle * @param {Element} searchToggle
*/ */
function bindToggleClickHandler( searchBox, header, searchToggle ) { function bindToggleClickHandler( searchBox, header, searchToggle ) {
/** /**
@ -88,7 +88,7 @@ function bindToggleClickHandler( searchBox, header, searchToggle ) {
* elements. When the user clicks outside of SEARCH_BOX_SELECTOR, the class will * elements. When the user clicks outside of SEARCH_BOX_SELECTOR, the class will
* be removed. * be removed.
* *
* @param {HTMLElement|null} searchToggle * @param {HTMLElement|null|Element} searchToggle
*/ */
module.exports = function initSearchToggle( searchToggle ) { module.exports = function initSearchToggle( searchToggle ) {
// Check if .closest API is available (IE11 does not support it). // Check if .closest API is available (IE11 does not support it).

View file

@ -89,8 +89,8 @@ function makeStickyHeaderFunctional(
userMenu, userMenu,
userMenuStickyContainer userMenuStickyContainer
) { ) {
/* eslint-disable-next-line compat/compat */
var var
/* eslint-disable-next-line compat/compat */
stickyObserver = new IntersectionObserver( function ( entries ) { stickyObserver = new IntersectionObserver( function ( entries ) {
if ( !entries[ 0 ].isIntersecting && entries[ 0 ].boundingClientRect.top < 0 ) { if ( !entries[ 0 ].isIntersecting && entries[ 0 ].boundingClientRect.top < 0 ) {
// Viewport has crossed the bottom edge of firstHeading so show sticky header. // Viewport has crossed the bottom edge of firstHeading so show sticky header.
@ -105,7 +105,8 @@ function makeStickyHeaderFunctional(
// Type declaration needed because of https://github.com/Microsoft/TypeScript/issues/3734#issuecomment-118934518 // Type declaration needed because of https://github.com/Microsoft/TypeScript/issues/3734#issuecomment-118934518
userMenuClone = /** @type {HTMLElement} */( userMenu.cloneNode( true ) ), userMenuClone = /** @type {HTMLElement} */( userMenu.cloneNode( true ) ),
userMenuStickyElementsWithIds = userMenuClone.querySelectorAll( '[ id ], [ data-event-name ]' ), userMenuStickyElementsWithIds = userMenuClone.querySelectorAll( '[ id ], [ data-event-name ]' ),
userMenuStickyContainerInner = userMenuStickyContainer.querySelector( VECTOR_USER_LINKS_SELECTOR ); userMenuStickyContainerInner = userMenuStickyContainer
.querySelector( VECTOR_USER_LINKS_SELECTOR );
// Update all ids of the cloned user menu to make them unique. // Update all ids of the cloned user menu to make them unique.
makeNodeTrackable( userMenuClone ); makeNodeTrackable( userMenuClone );

View file

@ -5,53 +5,52 @@ var
config = require( './config.json' ); config = require( './config.json' );
/** /**
* @param {HTMLElement} searchForm * @param {Function} createElement
* @param {NodeList} secondarySearchElements * @param {Element} searchForm
* @param {HTMLInputElement} search * @return {Vue.VNode}
* @param {string|null} searchPageTitle title of page used for searching e.g. Special:Search * @throws {Error} if the searchForm does not
* If null then this will default to Special:Search. * contain input[name=title] and input[name="search"] elements.
*/
function renderFn( createElement, searchForm ) {
var
titleInput = /** @type {HTMLInputElement|null} */ (
searchForm.querySelector( 'input[name=title]' )
),
search = /** @type {HTMLInputElement|null} */ ( searchForm.querySelector( 'input[name="search"]' ) ),
searchPageTitle = titleInput && titleInput.value;
if ( !search || !titleInput ) {
throw new Error( 'Attempted to create Vue search element from an incompatible element.' );
}
return createElement( App, {
props: $.extend( {
id: searchForm.id,
autofocusInput: search === document.activeElement,
action: searchForm.getAttribute( 'action' ),
searchAccessKey: search.getAttribute( 'accessKey' ),
searchPageTitle: searchPageTitle,
searchTitle: search.getAttribute( 'title' ),
searchPlaceholder: search.getAttribute( 'placeholder' ),
searchQuery: search.value
},
// Pass additional config from server.
config
)
} );
}
/**
* @param {NodeList} searchForms
* @return {void} * @return {void}
*/ */
function initApp( searchForm, secondarySearchElements, search, searchPageTitle ) { function initApp( searchForms ) {
/** searchForms.forEach( function ( searchForm ) {
*
* @ignore
* @param {Function} createElement
* @param {string} id
* @return {Vue.VNode}
*/
var renderFn = function ( createElement, id ) {
return createElement( App, {
props: $.extend( {
id: id,
autofocusInput: search === document.activeElement,
action: searchForm.getAttribute( 'action' ),
searchAccessKey: search.getAttribute( 'accessKey' ),
searchPageTitle: searchPageTitle,
searchTitle: search.getAttribute( 'title' ),
searchPlaceholder: search.getAttribute( 'placeholder' ),
searchQuery: search.value
},
// Pass additional config from server.
config
)
} );
};
// eslint-disable-next-line no-new
new Vue( {
el: searchForm,
render: function ( createElement ) {
return renderFn( createElement, 'searchform' );
}
} );
// Initialize secondary search elements like the search in the sticky header.
Array.prototype.forEach.call( secondarySearchElements, function ( secondarySearchElement ) {
// eslint-disable-next-line no-new // eslint-disable-next-line no-new
new Vue( { new Vue( {
el: secondarySearchElement, el: /** @type {Element} */ ( searchForm ),
render: function ( createElement ) { render: function ( createElement ) {
return renderFn( createElement, secondarySearchElement.id ); return renderFn( createElement, /** @type {Element} */ ( searchForm ) );
} }
} ); } );
} ); } );
@ -62,16 +61,9 @@ function initApp( searchForm, secondarySearchElements, search, searchPageTitle )
*/ */
function main( document ) { function main( document ) {
var var
searchForm = /** @type {HTMLElement} */ ( document.querySelector( '#searchform' ) ), // FIXME: Use .vector-search-box-form instead when cache allows.
titleInput = /** @type {HTMLInputElement|null} */ ( searchForms = document.querySelectorAll( '.vector-search-box form' );
searchForm.querySelector( 'input[name=title]' )
),
search = /** @type {HTMLInputElement|null} */ ( document.getElementById( 'searchInput' ) ),
// Since App.vue requires a unique id prop, only query elements with an id attribute.
secondarySearchElements = document.querySelectorAll( '.vector-secondary-search[id]' );
if ( search && searchForm ) { initApp( searchForms );
initApp( searchForm, secondarySearchElements, search, titleInput && titleInput.value );
}
} }
main( document ); main( document );

View file

@ -1,6 +1,6 @@
@import 'mediawiki.mixins.less'; @import 'mediawiki.mixins.less';
// Search portlet. // Search portlet.
#p-search h3 { .vector-search-box h3 {
.mixin-screen-reader-text(); .mixin-screen-reader-text();
} }

View file

@ -128,8 +128,10 @@ body {
// Defined as `div`. // Defined as `div`.
// Provide extra element for gadgets due to `form` already carrying an `id`. // Provide extra element for gadgets due to `form` already carrying an `id`.
// FIXME: This selector requires knowledge of the internals of the search component // FIXME: This selector requires knowledge of the internals of the search component
// FIXME: #simpleSearch selector can be removed when cache has cleared.
// and should not be used here. // and should not be used here.
#simpleSearch { #simpleSearch,
.vector-search-box-inner {
min-width: 5em; min-width: 5em;
// Support: IE 8, Firefox 18-, Chrome 19-, Safari 5.1-, Opera 19-, Android 4.4.4-. // Support: IE 8, Firefox 18-, Chrome 19-, Safari 5.1-, Opera 19-, Android 4.4.4-.
width: 13.2em; width: 13.2em;
@ -180,7 +182,9 @@ body {
padding-left: 0.5em; padding-left: 0.5em;
} }
#p-search { // FIXME: p-search is for cached HTML only. Can be removed in 1 week.
#p-search,
.vector-search-box {
margin-right: 1em; margin-right: 1em;
} }

View file

@ -45,7 +45,9 @@
min-width: @min-width-search-desktop; min-width: @min-width-search-desktop;
flex-basis: @min-width-search; flex-basis: @min-width-search;
> div > #searchform, // FIXME: Modify to use .vector-search-box-form when cache allows.
// When changing check the specificity is strong enough so that is still applies.
> div > form,
.wvui-typeahead-search { .wvui-typeahead-search {
max-width: @max-width-search; max-width: @max-width-search;
} }

View file

@ -21,7 +21,7 @@
// https://gerrit.wikimedia.org/r/plugins/gitiles/wvui/+/e32b54f3b8d1118b6a25cdc46b5638d6d048533e/src/themes/wikimedia-ui.less#27 // https://gerrit.wikimedia.org/r/plugins/gitiles/wvui/+/e32b54f3b8d1118b6a25cdc46b5638d6d048533e/src/themes/wikimedia-ui.less#27
@padding-vertical-typeahead-suggestion: 8px; @padding-vertical-typeahead-suggestion: 8px;
#simpleSearch.search-form__loader:after { .search-form__loader:after {
// Set the i18n message. // Set the i18n message.
content: attr( data-loading-msg ); content: attr( data-loading-msg );
// //

View file

@ -27,25 +27,33 @@
} }
// Typeahead search elements // Typeahead search elements
// FIXME: remove ID selectors when cache has cleared.
#searchInput, #searchInput,
#searchButton, #searchButton,
#mw-searchButton { #mw-searchButton,
.vector-search-box-vue .vector-search-box-input,
.vector-search-box-vue .searchButton {
// Overrides #mw-searchButton in resources/skins.vector.styles/SearchBox.less // Overrides #mw-searchButton in resources/skins.vector.styles/SearchBox.less
font-size: inherit; font-size: inherit;
} }
// FIXME: remove #searchInput selector when cache has cleared.
.vector-search-box-vue .vector-search-box-input,
#searchInput { #searchInput {
height: @size-base; height: @size-base;
} }
#searchButton, // FIXME: Remove searchButton when cache has cleared.
#mw-searchButton { .vector-search-box-vue .searchButton,
#searchButton {
background-size: @background-size-x-search-button auto; background-size: @background-size-x-search-button auto;
} }
// Only apply the following WVUI-related rules to clients who have js enabled. // Only apply the following WVUI-related rules to clients who have js enabled.
// TODO: .skin-vector-search-vue class can be removed when $wgVectorUseWvuiSearch is no longer supported. // TODO: .skin-vector-search-vue class can be removed when $wgVectorUseWvuiSearch is no longer supported
.client-js .skin-vector-search-vue { // OR .vector-search-box-vue is in cached HTML.
.client-js .skin-vector-search-vue,
.client-js .vector-search-box-vue {
// Derived from @size-search-figure in WVUI // Derived from @size-search-figure in WVUI
// https://gerrit.wikimedia.org/r/plugins/gitiles/wvui/+/e32b54f3b8d1118b6a25cdc46b5638d6d048533e/src/themes/wikimedia-ui.less#21 // https://gerrit.wikimedia.org/r/plugins/gitiles/wvui/+/e32b54f3b8d1118b6a25cdc46b5638d6d048533e/src/themes/wikimedia-ui.less#21
@size-search-figure: unit( 36px / @font-size-browser / @font-size-base, em ); @size-search-figure: unit( 36px / @font-size-browser / @font-size-base, em );
@ -56,11 +64,13 @@
text-decoration: none; text-decoration: none;
} }
#searchform-suggestions li { .wvui-typeahead-search__suggestions li {
// Remove margin-bottom on li elements that is applied by mediawiki.skinning/elements.css. // Remove margin-bottom on li elements that is applied by mediawiki.skinning/elements.css.
margin-bottom: 0; margin-bottom: 0;
} }
// FIXME: Remove #searchInput selector when cache has cleared.
.vector-search-box-input,
#searchInput { #searchInput {
padding-left: @size-search-figure; padding-left: @size-search-figure;
// Derived from @padding-input-text in WVUI's Input component. // Derived from @padding-input-text in WVUI's Input component.
@ -68,8 +78,7 @@
} }
// Move & resize search icon to match WVUI. // Move & resize search icon to match WVUI.
#searchButton, .searchButton {
#mw-searchButton {
// T270202: Act like a an inert element instead of a submit button before // T270202: Act like a an inert element instead of a submit button before
// WVUI loads to discourage people clicking on it since it is a submit // WVUI loads to discourage people clicking on it since it is a submit
// button styled to look like WVUI's inert start icon. Note, ideally these // button styled to look like WVUI's inert start icon. Note, ideally these
@ -97,6 +106,8 @@
.p-search--show-thumbnail, .p-search--show-thumbnail,
.vector-search-box-show-thumbnail { .vector-search-box-show-thumbnail {
// Recreate WVUI expanding input. // Recreate WVUI expanding input.
// FIXME: Remove #searchInput selector when cache has cleared.
.vector-search-box-input:focus,
#searchInput:focus { #searchInput:focus {
position: relative; position: relative;
// Use ~ and fixed values to disable the LESS transformation in ResourceLoader LESS implementation. // Use ~ and fixed values to disable the LESS transformation in ResourceLoader LESS implementation.
@ -106,6 +117,8 @@
} }
// Reposition search icon for expanded input. // Reposition search icon for expanded input.
// FIXME: Remove #searchInput selectors when cache has cleared.
.vector-search-box-input:focus ~ .searchButton,
#searchInput:focus ~ #searchButton, #searchInput:focus ~ #searchButton,
#searchInput:focus ~ #mw-searchButton { #searchInput:focus ~ #mw-searchButton {
// Derived from // Derived from
@ -116,7 +129,9 @@
} }
// Update search loader to match width and position of WVUI expanding input. // Update search loader to match width and position of WVUI expanding input.
#simpleSearch.search-form__loader:after { // FIXME: Remove #simpleSearch selector when cache has cleared.
#simpleSearch.search-form__loader:after,
.vector-search-box-inner.search-form__loader:after {
width: ~'calc( 100% + @{size-search-expand} )'; width: ~'calc( 100% + @{size-search-expand} )';
left: ~'calc( -1 * @{size-search-expand} )'; left: ~'calc( -1 * @{size-search-expand} )';
padding-left: @size-search-expand; padding-left: @size-search-expand;

View file

@ -12,7 +12,7 @@
"license-name": "GPL-2.0-or-later", "license-name": "GPL-2.0-or-later",
"type": "skin", "type": "skin",
"requires": { "requires": {
"MediaWiki": ">= 1.37.0" "MediaWiki": ">= 1.38.0"
}, },
"ValidSkinNames": { "ValidSkinNames": {
"vector": { "vector": {
@ -51,6 +51,8 @@
"vector-jumptosearch", "vector-jumptosearch",
"vector-jumptocontent", "vector-jumptocontent",
"search", "search",
"searchbutton",
"searcharticle",
"sitesubtitle", "sitesubtitle",
"sitetitle", "sitetitle",
"tagline" "tagline"

View file

@ -6,18 +6,29 @@ import searchBoxTemplate from '!!raw-loader!../includes/templates/SearchBox.must
import Button from '!!raw-loader!../includes/templates/Button.mustache'; import Button from '!!raw-loader!../includes/templates/Button.mustache';
import { htmlUserLanguageAttributes } from './utils'; import { htmlUserLanguageAttributes } from './utils';
const INPUT_ATTRIBUTES = 'type="search" name="search" placeholder="Search Wikipedia" title="Search Wikipedia [⌃⌥f]" accesskey="f" id="searchInput" autocomplete="off"';
const FULL_TEXT_ATTRIBUTES = 'name="fulltext" title="Search pages for this text" id="mw-searchButton" class="searchButton mw-fallbackSearchButton"';
const GO_ATTRIBUTES = 'name="go" title="Go to a page with this exact name if it exists" id="searchButton" class="searchButton"';
/** /**
* @type {SearchData} * @type {SearchData}
*/ */
const searchBoxData = { const searchBoxData = {
'form-action': '/w/index.php', 'form-action': '/w/index.php',
class: 'vector-search-box vector-search-show-thumbnail', 'form-id': 'searchform',
'is-primary': false,
class: 'vector-search-show-thumbnail',
'html-user-language-attributes': htmlUserLanguageAttributes, 'html-user-language-attributes': htmlUserLanguageAttributes,
'msg-search': 'Search', 'msg-search': 'Search',
'html-input': '<input type="search" name="search" placeholder="Search Wikipedia" title="Search Wikipedia [⌃⌥f]" accesskey="f" id="searchInput" autocomplete="off">', 'html-input': `<input ${INPUT_ATTRIBUTES}>`,
'page-title': 'Special:Search', 'page-title': 'Special:Search',
'html-button-search-fallback': '<input type="submit" name="fulltext" value="Search" title="Search pages for this text" id="mw-searchButton" class="searchButton mw-fallbackSearchButton"/>', 'html-input-attributes': INPUT_ATTRIBUTES,
'html-button-search': '<input type="submit" name="go" value="Go" title="Go to a page with this exact name if it exists" id="searchButton" class="searchButton">' 'html-button-fulltext-attributes': FULL_TEXT_ATTRIBUTES,
'msg-searchbutton': 'Search',
'msg-searcharticle': 'Go',
'html-button-go-attributes': GO_ATTRIBUTES,
'html-button-search-fallback': `<input type="submit" ${FULL_TEXT_ATTRIBUTES} value="Search" />`,
'html-button-search': `<input type="submit" ${GO_ATTRIBUTES} value="Go">`
}; };
/** /**

View file

@ -1,6 +1,6 @@
import mustache from 'mustache'; import mustache from 'mustache';
import '../resources/skins.vector.styles/SearchBox.less'; import '../resources/skins.vector.styles/SearchBox.less';
import '../resources/skins.vector.styles/layouts/screen.less';
import { searchBoxData, searchBoxDataWithCollapsing, searchBoxTemplate, import { searchBoxData, searchBoxDataWithCollapsing, searchBoxTemplate,
SEARCH_TEMPLATE_PARTIALS SEARCH_TEMPLATE_PARTIALS
} from './SearchBox.stories.data'; } from './SearchBox.stories.data';

View file

@ -39,11 +39,18 @@
/** /**
* @typedef {Object} SearchData * @typedef {Object} SearchData
* @property {string|null} msg-search * @property {string|null} msg-search
* @property {string|null} msg-searchbutton
* @property {string|null} msg-searcharticle
* @property {string} [html-user-language-attributes] * @property {string} [html-user-language-attributes]
* @property {boolean} is-primary is this the primary method of search?
* @property {string} form-action URL * @property {string} form-action URL
* @property {string} form-id
* @property {string|null} html-input * @property {string|null} html-input
* @property {string|null} [class] of the menu * @property {string|null} [class] of the menu
* @property {string|null} page-title the title of the search page * @property {string|null} page-title the title of the search page
* @property {string} html-input-attributes
* @property {string} html-button-fulltext-attributes
* @property {string} html-button-go-attributes
* @property {string|null} html-button-search-fallback * @property {string|null} html-button-search-fallback
* @property {string|null} html-button-search * @property {string|null} html-button-search
* @property {string} [input-location] An identifier corresponding the position of the search * @property {string} [input-location] An identifier corresponding the position of the search