mirror of
https://gerrit.wikimedia.org/r/mediawiki/skins/Vector.git
synced 2024-11-24 23:55:53 +00:00
Merge "Allow multiple search components on the same page"
This commit is contained in:
commit
84023ff39c
|
@ -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",
|
||||||
|
|
|
@ -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).
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 = {
|
||||||
|
|
|
@ -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).
|
||||||
|
|
|
@ -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 );
|
||||||
|
|
|
@ -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 );
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 );
|
||||||
//
|
//
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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">`
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue