Add search to sticky header
Per T289724#7342741, server renders an anchor tag pointing to #p-search into the "button-start" bucket of the sticky header. In the future after T289718, this anchor will then acts as a button when the search module is loaded and searchToggle executes. * skins.vector.search was modified to accomodate instantiating multiple search components (one in the main header and one in the sticky header). * searchToggle.js was modified to accept a searchToggle element as a param which the caller can then instantiate when ideal. For the sticky header toggle, this needs to happen *after* the search module loads. Before then, the toggle will act as a link. * Drops one jQuery usage from searchToggle so that it can be jQuery free. Because the native .closest method is used, IE11 support is also dropped. However, the script feature detects and returns early if the API isn't available. * Makes App.vue accept an `id` prop so that multiple instances of it can be created. Bug: T289724 Change-Id: I1c5e6eee75918a0d06562d07c31fdcbd5a4ed6d5
This commit is contained in:
parent
f271c86238
commit
93745e4800
|
@ -65,6 +65,7 @@ class SkinVector extends SkinMustache {
|
||||||
'is-quiet' => true,
|
'is-quiet' => true,
|
||||||
'class' => 'sticky-header-icon'
|
'class' => 'sticky-header-icon'
|
||||||
];
|
];
|
||||||
|
private const SEARCH_EXPANDING_CLASS = 'vector-search-box-show-thumbnail';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* T243281: Code used to track clicks to opt-out link.
|
* T243281: Code used to track clicks to opt-out link.
|
||||||
|
@ -325,7 +326,16 @@ class SkinVector extends SkinMustache {
|
||||||
private function getStickyHeaderData() {
|
private function getStickyHeaderData() {
|
||||||
return [
|
return [
|
||||||
'data-primary-action' => !$this->shouldHideLanguages() ? $this->getULSButtonData() : '',
|
'data-primary-action' => !$this->shouldHideLanguages() ? $this->getULSButtonData() : '',
|
||||||
'data-button-start' => self::NO_ICON,
|
'data-button-start' => [
|
||||||
|
'href' => '#p-search',
|
||||||
|
'label' => $this->msg( 'search' ),
|
||||||
|
'icon' => 'wikimedia-search',
|
||||||
|
'is-quiet' => true,
|
||||||
|
'class' => 'vector-sticky-header-search-toggle',
|
||||||
|
],
|
||||||
|
'data-search' => [
|
||||||
|
'class' => $this->shouldSearchExpand() ? self::SEARCH_EXPANDING_CLASS : '',
|
||||||
|
],
|
||||||
'data-buttons' => [
|
'data-buttons' => [
|
||||||
self::TALK_ICON, self::HISTORY_ICON, self::NO_ICON, self::NO_ICON
|
self::TALK_ICON, self::HISTORY_ICON, self::NO_ICON, self::NO_ICON
|
||||||
]
|
]
|
||||||
|
@ -423,7 +433,7 @@ class SkinVector extends SkinMustache {
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( $this->shouldSearchExpand() ) {
|
if ( $this->shouldSearchExpand() ) {
|
||||||
$searchClass .= ' vector-search-box-show-thumbnail';
|
$searchClass .= " " . self::SEARCH_EXPANDING_CLASS;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Annotate search box with a component class.
|
// Annotate search box with a component class.
|
||||||
|
|
|
@ -6,6 +6,11 @@
|
||||||
{{>Button}}
|
{{>Button}}
|
||||||
{{/data-button-start}}
|
{{/data-button-start}}
|
||||||
</div>
|
</div>
|
||||||
|
{{#data-search}}
|
||||||
|
<div class="vector-search-box {{class}}">
|
||||||
|
<div class="vector-secondary-search" id="vector-sticky-header-search"></div>
|
||||||
|
</div>
|
||||||
|
{{/data-search}}
|
||||||
<div class="vector-sticky-header-context-bar">
|
<div class="vector-sticky-header-context-bar">
|
||||||
<div class="vector-sticky-header-context-bar-primary">{{html-title}}</div>
|
<div class="vector-sticky-header-context-bar-primary">{{html-title}}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -22,6 +22,7 @@
|
||||||
"Element": "https://developer.mozilla.org/docs/Web/API/Element",
|
"Element": "https://developer.mozilla.org/docs/Web/API/Element",
|
||||||
"Event": "https://developer.mozilla.org/docs/Web/API/Event",
|
"Event": "https://developer.mozilla.org/docs/Web/API/Event",
|
||||||
"HTMLElement": "https://developer.mozilla.org/docs/Web/API/HTMLElement",
|
"HTMLElement": "https://developer.mozilla.org/docs/Web/API/HTMLElement",
|
||||||
|
"NodeList": "https://developer.mozilla.org/docs/Web/API/NodeList",
|
||||||
"HTMLInputElement": "https://developer.mozilla.org/docs/Web/API/HTMLInputElement",
|
"HTMLInputElement": "https://developer.mozilla.org/docs/Web/API/HTMLInputElement",
|
||||||
"\"removeEventListener\"": "https://developer.mozilla.org/docs/Web/API/EventTarget/removeEventListener",
|
"\"removeEventListener\"": "https://developer.mozilla.org/docs/Web/API/EventTarget/removeEventListener",
|
||||||
"Window": "https://developer.mozilla.org/docs/Web/API/Window",
|
"Window": "https://developer.mozilla.org/docs/Web/API/Window",
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
var
|
var
|
||||||
HEADER_SELECTOR = '.mw-header',
|
HEADER_SELECTOR = 'header',
|
||||||
SEARCH_TOGGLE_SELECTOR = '.search-toggle',
|
SEARCH_BOX_SELECTOR = '.vector-search-box',
|
||||||
SEARCH_BOX_ID = 'p-search',
|
|
||||||
SEARCH_VISIBLE_CLASS = 'vector-header-search-toggled';
|
SEARCH_VISIBLE_CLASS = 'vector-header-search-toggled';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -22,7 +21,9 @@ function bindSearchBoxHandler( searchBox, header ) {
|
||||||
// Check if the click target was a suggestion link. WVUI clears the
|
// Check if the click target was a suggestion link. WVUI clears the
|
||||||
// suggestion elements from the DOM when a suggestion is clicked so we
|
// suggestion elements from the DOM when a suggestion is clicked so we
|
||||||
// can't test if the suggestion is a child of the searchBox.
|
// can't test if the suggestion is a child of the searchBox.
|
||||||
!$( ev.target ).closest( '.wvui-typeahead-suggestion' ).length &&
|
//
|
||||||
|
// Note: The .closest API is feature detected in `initSearchToggle`.
|
||||||
|
!ev.target.closest( '.wvui-typeahead-suggestion' ) &&
|
||||||
!searchBox.contains( ev.target )
|
!searchBox.contains( ev.target )
|
||||||
) {
|
) {
|
||||||
// eslint-disable-next-line mediawiki/class-doc
|
// eslint-disable-next-line mediawiki/class-doc
|
||||||
|
@ -53,15 +54,20 @@ function bindToggleClickHandler( searchBox, header, searchToggle ) {
|
||||||
// from the page when clicked.
|
// from the page when clicked.
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
|
|
||||||
bindSearchBoxHandler( searchBox, header );
|
|
||||||
|
|
||||||
// eslint-disable-next-line mediawiki/class-doc
|
// eslint-disable-next-line mediawiki/class-doc
|
||||||
header.classList.add( SEARCH_VISIBLE_CLASS );
|
header.classList.add( SEARCH_VISIBLE_CLASS );
|
||||||
|
|
||||||
// Defer focusing the input to another task in the event loop. At the time
|
// Defer binding the search box handler until after the event bubbles to the
|
||||||
|
// top of the document so that the handler isn't called when the user clicks
|
||||||
|
// the search toggle. Event bubbled callbacks execute within the same task
|
||||||
|
// in the event loop.
|
||||||
|
//
|
||||||
|
// Also, defer focusing the input to another task in the event loop. At the time
|
||||||
// of this writing, Safari 14.0.3 has trouble changing the visibility of the
|
// of this writing, Safari 14.0.3 has trouble changing the visibility of the
|
||||||
// element and focusing the input within the same task.
|
// element and focusing the input within the same task.
|
||||||
setTimeout( function () {
|
setTimeout( function () {
|
||||||
|
bindSearchBoxHandler( searchBox, header );
|
||||||
|
|
||||||
var searchInput = /** @type {HTMLInputElement|null} */ ( searchBox.querySelector( 'input[type="search"]' ) );
|
var searchInput = /** @type {HTMLInputElement|null} */ ( searchBox.querySelector( 'input[type="search"]' ) );
|
||||||
|
|
||||||
if ( searchInput ) {
|
if ( searchInput ) {
|
||||||
|
@ -73,14 +79,34 @@ function bindToggleClickHandler( searchBox, header, searchToggle ) {
|
||||||
searchToggle.addEventListener( 'click', handler );
|
searchToggle.addEventListener( 'click', handler );
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = function initSearchToggle() {
|
/**
|
||||||
var
|
* Enables search toggling behavior in a header given a toggle element (e.g.
|
||||||
header = /** @type {HTMLElement|null} */ ( document.querySelector( HEADER_SELECTOR ) ),
|
* search icon). When the toggle element is clicked, a class,
|
||||||
searchBox = /** @type {HTMLElement|null} */ ( document.getElementById( SEARCH_BOX_ID ) ),
|
* `SEARCH_VISIBLE_CLASS`, will be applied to a header matching the selector
|
||||||
searchToggle =
|
* `HEADER_SELECTOR` and the input inside the element, SEARCH_BOX_SELECTOR, will
|
||||||
/** @type {HTMLElement|null} */ ( document.querySelector( SEARCH_TOGGLE_SELECTOR ) );
|
* be focused. This class can be used in CSS to show/hide the necessary
|
||||||
|
* elements. When the user clicks outside of SEARCH_BOX_SELECTOR, the class will
|
||||||
|
* be removed.
|
||||||
|
*
|
||||||
|
* @param {HTMLElement|null} searchToggle
|
||||||
|
*/
|
||||||
|
module.exports = function initSearchToggle( searchToggle ) {
|
||||||
|
// Check if .closest API is available (IE11 does not support it).
|
||||||
|
if ( !searchToggle || !searchToggle.closest ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if ( !( searchBox && searchToggle && header ) ) {
|
var header =
|
||||||
|
/** @type {HTMLElement|null} */ ( searchToggle.closest( HEADER_SELECTOR ) );
|
||||||
|
|
||||||
|
if ( !header ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var searchBox =
|
||||||
|
/** @type {HTMLElement|null} */ ( header.querySelector( SEARCH_BOX_SELECTOR ) );
|
||||||
|
|
||||||
|
if ( !searchBox ) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -50,7 +50,9 @@ function main( window ) {
|
||||||
dropdownMenus();
|
dropdownMenus();
|
||||||
vector.init();
|
vector.init();
|
||||||
initSearchLoader( document );
|
initSearchLoader( document );
|
||||||
searchToggle();
|
// Initialize the search toggle for the main header only. The sticky header
|
||||||
|
// toggle is initialized after wvui search loads.
|
||||||
|
searchToggle( document.querySelector( '.mw-header .search-toggle' ) );
|
||||||
languageButton();
|
languageButton();
|
||||||
stickyHeader();
|
stickyHeader();
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,8 @@ var
|
||||||
FIRST_HEADING_ID = 'firstHeading',
|
FIRST_HEADING_ID = 'firstHeading',
|
||||||
USER_MENU_ID = 'p-personal',
|
USER_MENU_ID = 'p-personal',
|
||||||
VECTOR_USER_LINKS_SELECTOR = '.vector-user-links',
|
VECTOR_USER_LINKS_SELECTOR = '.vector-user-links',
|
||||||
VECTOR_MENU_CONTENT_LIST_SELECTOR = '.vector-menu-content-list';
|
VECTOR_MENU_CONTENT_LIST_SELECTOR = '.vector-menu-content-list',
|
||||||
|
SEARCH_TOGGLE_SELECTOR = '.vector-sticky-header-search-toggle';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Copies attribute from an element to another.
|
* Copies attribute from an element to another.
|
||||||
|
@ -141,6 +142,31 @@ function makeStickyHeaderFunctional(
|
||||||
stickyObserver.observe( stickyIntersection );
|
stickyObserver.observe( stickyIntersection );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {HTMLElement} header
|
||||||
|
*/
|
||||||
|
function setupSearchIfNeeded( header ) {
|
||||||
|
var
|
||||||
|
searchToggle = header.querySelector( SEARCH_TOGGLE_SELECTOR );
|
||||||
|
|
||||||
|
if ( !(
|
||||||
|
searchToggle &&
|
||||||
|
window.fetch &&
|
||||||
|
document.body.classList.contains( 'skin-vector-search-vue' )
|
||||||
|
) ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load the `skins.vector.search` module here or setup an event handler to
|
||||||
|
// load it depending on the outcome of T289718. After it loads, initialize the
|
||||||
|
// search toggle.
|
||||||
|
//
|
||||||
|
// Example:
|
||||||
|
// mw.loader.using( 'skins.vector.search', function () {
|
||||||
|
// initSearchToggle( searchToggle );
|
||||||
|
// } );
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = function initStickyHeader() {
|
module.exports = function initStickyHeader() {
|
||||||
var header = document.getElementById( STICKY_HEADER_ID ),
|
var header = document.getElementById( STICKY_HEADER_ID ),
|
||||||
stickyIntersection = document.getElementById(
|
stickyIntersection = document.getElementById(
|
||||||
|
@ -162,4 +188,5 @@ module.exports = function initStickyHeader() {
|
||||||
}
|
}
|
||||||
|
|
||||||
makeStickyHeaderFunctional( header, stickyIntersection, userMenu, userMenuStickyContainer );
|
makeStickyHeaderFunctional( header, stickyIntersection, userMenu, userMenuStickyContainer );
|
||||||
|
setupSearchIfNeeded( header );
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<wvui-typeahead-search
|
<wvui-typeahead-search
|
||||||
id="searchform"
|
:id="id"
|
||||||
ref="searchForm"
|
ref="searchForm"
|
||||||
:client="getClient"
|
:client="getClient"
|
||||||
:domain="domain"
|
:domain="domain"
|
||||||
|
@ -48,10 +48,6 @@ module.exports = {
|
||||||
var wvuiSearchForm = this.$refs.searchForm.$el;
|
var wvuiSearchForm = this.$refs.searchForm.$el;
|
||||||
|
|
||||||
if ( this.autofocusInput ) {
|
if ( this.autofocusInput ) {
|
||||||
// TODO: The wvui-typeahead-search component accepts an id prop but does not
|
|
||||||
// display that value as an HTML attribute on the form element.
|
|
||||||
wvuiSearchForm.querySelector( 'form' ).setAttribute( 'id', 'searchform' );
|
|
||||||
|
|
||||||
// TODO: The wvui-typeahead-search component does not accept an autofocus parameter
|
// TODO: The wvui-typeahead-search component does not accept an autofocus parameter
|
||||||
// or directive. This can be removed when its does.
|
// or directive. This can be removed when its does.
|
||||||
wvuiSearchForm.querySelector( 'input' ).focus();
|
wvuiSearchForm.querySelector( 'input' ).focus();
|
||||||
|
@ -75,6 +71,10 @@ module.exports = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
|
id: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
searchPageTitle: {
|
searchPageTitle: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'Special:Search'
|
default: 'Special:Search'
|
||||||
|
|
|
@ -6,37 +6,55 @@ var
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {HTMLElement} searchForm
|
* @param {HTMLElement} searchForm
|
||||||
|
* @param {NodeList} secondarySearchElements
|
||||||
* @param {HTMLInputElement} search
|
* @param {HTMLInputElement} search
|
||||||
* @param {string|null} searchPageTitle title of page used for searching e.g. Special:Search
|
* @param {string|null} searchPageTitle title of page used for searching e.g. Special:Search
|
||||||
* If null then this will default to Special:Search.
|
* If null then this will default to Special:Search.
|
||||||
* @return {void}
|
* @return {void}
|
||||||
*/
|
*/
|
||||||
function initApp( searchForm, search, searchPageTitle ) {
|
function initApp( searchForm, secondarySearchElements, search, searchPageTitle ) {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @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
|
// eslint-disable-next-line no-new
|
||||||
new Vue( {
|
new Vue( {
|
||||||
el: searchForm,
|
el: searchForm,
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param {Function} createElement
|
|
||||||
* @return {Vue.VNode}
|
|
||||||
*/
|
|
||||||
render: function ( createElement ) {
|
render: function ( createElement ) {
|
||||||
return createElement( App, {
|
return renderFn( createElement, 'searchform' );
|
||||||
props: $.extend( {
|
|
||||||
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
|
|
||||||
)
|
|
||||||
} );
|
|
||||||
}
|
}
|
||||||
} );
|
} );
|
||||||
|
|
||||||
|
// Initialize secondary search elements like the search in the sticky header.
|
||||||
|
Array.prototype.forEach.call( secondarySearchElements, function ( secondarySearchElement ) {
|
||||||
|
// eslint-disable-next-line no-new
|
||||||
|
new Vue( {
|
||||||
|
el: secondarySearchElement,
|
||||||
|
render: function ( createElement ) {
|
||||||
|
return renderFn( createElement, secondarySearchElement.id );
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
} );
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* @param {Document} document
|
* @param {Document} document
|
||||||
|
@ -48,9 +66,12 @@ function main( document ) {
|
||||||
titleInput = /** @type {HTMLInputElement|null} */ (
|
titleInput = /** @type {HTMLInputElement|null} */ (
|
||||||
searchForm.querySelector( 'input[name=title]' )
|
searchForm.querySelector( 'input[name=title]' )
|
||||||
),
|
),
|
||||||
search = /** @type {HTMLInputElement|null} */ ( document.getElementById( 'searchInput' ) );
|
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 ) {
|
if ( search && searchForm ) {
|
||||||
initApp( searchForm, search, titleInput && titleInput.value );
|
initApp( searchForm, secondarySearchElements, search, titleInput && titleInput.value );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
main( document );
|
main( document );
|
||||||
|
|
|
@ -46,6 +46,7 @@
|
||||||
&-start {
|
&-start {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
&-end {
|
&-end {
|
||||||
|
@ -88,6 +89,29 @@
|
||||||
background-image: linear-gradient( to right, rgba( 255, 255, 255, 0 ), rgba( 255, 255, 255, 1 ) 20px );
|
background-image: linear-gradient( to right, rgba( 255, 255, 255, 0 ), rgba( 255, 255, 255, 1 ) 20px );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.vector-search-box {
|
||||||
|
// Hide the search box until the user toggles it.
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.vector-header-search-toggled {
|
||||||
|
.vector-sticky-header-search-toggle,
|
||||||
|
.vector-sticky-header-context-bar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vector-search-box {
|
||||||
|
display: block;
|
||||||
|
flex-basis: unit( 500px / @font-size-browser / @font-size-base, em );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Increase the start margin of the search box to account for the input
|
||||||
|
// expanding on focus.
|
||||||
|
.vector-search-box-show-thumbnail {
|
||||||
|
margin-left: @size-search-expand;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-nojs .vector-sticky-header {
|
.client-nojs .vector-sticky-header {
|
||||||
|
|
|
@ -18,7 +18,16 @@ const data = {
|
||||||
label: '196 languages',
|
label: '196 languages',
|
||||||
'html-vector-button-icon': `<span class="mw-ui-icon mw-ui-icon-wikimedia-language"></span>`
|
'html-vector-button-icon': `<span class="mw-ui-icon mw-ui-icon-wikimedia-language"></span>`
|
||||||
},
|
},
|
||||||
'data-button-start': NO_ICON,
|
'data-search': {
|
||||||
|
class: ''
|
||||||
|
},
|
||||||
|
'data-button-start': {
|
||||||
|
icon: 'wikimedia-search',
|
||||||
|
href: '#',
|
||||||
|
class: 'search-toggle',
|
||||||
|
'is-quiet': true,
|
||||||
|
label: 'Search'
|
||||||
|
},
|
||||||
'data-button-end': NO_ICON,
|
'data-button-end': NO_ICON,
|
||||||
'data-buttons': [
|
'data-buttons': [
|
||||||
NO_ICON, NO_ICON, NO_ICON, NO_ICON
|
NO_ICON, NO_ICON, NO_ICON, NO_ICON
|
||||||
|
|
Loading…
Reference in New Issue