mirror of
https://gerrit.wikimedia.org/r/mediawiki/skins/Vector.git
synced 2024-12-18 10:51:10 +00:00
98bee1c71f
Add hook that renders when search displays for first time to allow experimentation. Code can call ``` mw.hook( 'search.display' ).add((node)=> { node.innerHTML = 'hello world' } ); Bug: T371294 Change-Id: Ib3ec73b8ed66877c11e0d8d290a6b564a013702b
281 lines
6.5 KiB
Vue
281 lines
6.5 KiB
Vue
<template>
|
|
<cdx-typeahead-search
|
|
:id="id"
|
|
ref="searchForm"
|
|
class="vector-typeahead-search"
|
|
:class="rootClasses"
|
|
:search-results-label="$i18n( 'searchresults' ).text()"
|
|
:accesskey="searchAccessKey"
|
|
:autocapitalize="autocapitalizeValue"
|
|
:title="searchTitle"
|
|
:placeholder="searchPlaceholder"
|
|
:aria-label="searchPlaceholder"
|
|
:initial-input-value="searchQuery"
|
|
:button-label="$i18n( 'searchbutton' ).text()"
|
|
:form-action="action"
|
|
:show-thumbnail="showThumbnail"
|
|
:highlight-query="highlightQuery"
|
|
:auto-expand-width="autoExpandWidth"
|
|
:search-results="suggestions"
|
|
:search-footer-url="searchFooterUrl"
|
|
:visible-item-limit="visibleItemLimit"
|
|
@load-more="onLoadMore"
|
|
@input="onInput"
|
|
@search-result-click="instrumentation.onSuggestionClick"
|
|
@submit="onSubmit"
|
|
@focus="onFocus"
|
|
@blur="onBlur"
|
|
>
|
|
<template #default>
|
|
<div ref="emptyState">empty</div>
|
|
<input
|
|
type="hidden"
|
|
name="title"
|
|
:value="searchPageTitle"
|
|
>
|
|
<input
|
|
type="hidden"
|
|
name="wprov"
|
|
:value="wprov"
|
|
>
|
|
</template>
|
|
<template #search-results-pending>
|
|
{{ $i18n( 'vector-search-loader' ).text() }}
|
|
</template>
|
|
<!-- eslint-disable-next-line vue/no-template-shadow -->
|
|
<template #search-footer-text="{ searchQuery }">
|
|
<span v-i18n-html:vector-searchsuggest-containing="[ searchQuery ]"></span>
|
|
</template>
|
|
</cdx-typeahead-search>
|
|
</template>
|
|
|
|
<script>
|
|
const { CdxTypeaheadSearch } = mw.loader.require( 'skins.vector.search.codex.scripts' ),
|
|
{ defineComponent, nextTick } = require( 'vue' ),
|
|
client = require( './restSearchClient.js' ),
|
|
restClient = client( mw.config ),
|
|
urlGenerator = require( './urlGenerator.js' )( mw.config ),
|
|
instrumentation = require( './instrumentation.js' );
|
|
|
|
// @vue/component
|
|
module.exports = exports = defineComponent( {
|
|
name: 'App',
|
|
compatConfig: {
|
|
MODE: 3
|
|
},
|
|
compilerOptions: {
|
|
whitespace: 'condense'
|
|
},
|
|
components: { CdxTypeaheadSearch },
|
|
props: {
|
|
id: {
|
|
type: String,
|
|
required: true
|
|
},
|
|
autocapitalizeValue: {
|
|
type: String,
|
|
default: undefined
|
|
},
|
|
searchPageTitle: {
|
|
type: String,
|
|
default: 'Special:Search'
|
|
},
|
|
autofocusInput: {
|
|
type: Boolean,
|
|
default: false
|
|
},
|
|
action: {
|
|
type: String,
|
|
default: ''
|
|
},
|
|
/** The keyboard shortcut to focus search. */
|
|
searchAccessKey: {
|
|
type: String,
|
|
default: undefined
|
|
},
|
|
/** The access key informational tip for search. */
|
|
searchTitle: {
|
|
type: String,
|
|
default: undefined
|
|
},
|
|
/** The ghost text shown when no search query is entered. */
|
|
searchPlaceholder: {
|
|
type: String,
|
|
default: undefined
|
|
},
|
|
/**
|
|
* The search query string taken from the server-side rendered input immediately before
|
|
* client render.
|
|
*/
|
|
searchQuery: {
|
|
type: String,
|
|
default: undefined
|
|
},
|
|
showThumbnail: {
|
|
type: Boolean,
|
|
required: true,
|
|
default: false
|
|
},
|
|
showDescription: {
|
|
type: Boolean,
|
|
default: false
|
|
},
|
|
highlightQuery: {
|
|
type: Boolean,
|
|
default: false
|
|
},
|
|
autoExpandWidth: {
|
|
type: Boolean,
|
|
default: false
|
|
}
|
|
},
|
|
data() {
|
|
return {
|
|
// -1 here is the default "active suggestion index".
|
|
wprov: instrumentation.getWprovFromResultIndex( -1 ),
|
|
|
|
// Suggestions to be shown in the TypeaheadSearch menu.
|
|
suggestions: [],
|
|
|
|
// Link to the search page for the current search query.
|
|
searchFooterUrl: '',
|
|
|
|
// The current search query. Used to detect whether a fetch response is stale.
|
|
currentSearchQuery: '',
|
|
|
|
// Whether to apply a CSS class that disables the CSS transitions on the text input
|
|
disableTransitions: this.autofocusInput,
|
|
|
|
instrumentation: instrumentation.listeners,
|
|
|
|
isFocused: false
|
|
};
|
|
},
|
|
computed: {
|
|
rootClasses() {
|
|
return {
|
|
'vector-search-box-disable-transitions': this.disableTransitions,
|
|
'vector-typeahead-search--active': this.isFocused
|
|
};
|
|
},
|
|
visibleItemLimit() {
|
|
// if the search client supports loading more results,
|
|
// show 7 out of 10 results at first (arbitrary number),
|
|
// so that scroll events are fired and trigger onLoadMore()
|
|
return restClient.loadMore ? 7 : null;
|
|
}
|
|
},
|
|
methods: {
|
|
/**
|
|
* Fetch suggestions when new input is received.
|
|
*
|
|
* @param {string} value
|
|
*/
|
|
onInput: function ( value ) {
|
|
const query = value.trim();
|
|
|
|
this.currentSearchQuery = query;
|
|
|
|
if ( query === '' ) {
|
|
this.suggestions = [];
|
|
this.searchFooterUrl = '';
|
|
return;
|
|
}
|
|
|
|
this.updateUIWithSearchClientResult(
|
|
restClient.fetchByTitle( query, 10, this.showDescription ),
|
|
true
|
|
);
|
|
this.$refs.emptyState.innerHTML = '';
|
|
},
|
|
|
|
/**
|
|
* Fetch additional suggestions.
|
|
*
|
|
* This should only be called if visibleItemLimit is non-null,
|
|
* i.e. if the search client supports loading more results.
|
|
*/
|
|
onLoadMore() {
|
|
if ( !restClient.loadMore ) {
|
|
mw.log.warn( 'onLoadMore() should not have been called for this search client' );
|
|
return;
|
|
}
|
|
|
|
this.updateUIWithSearchClientResult(
|
|
restClient.loadMore(
|
|
this.currentSearchQuery,
|
|
this.suggestions.length,
|
|
10,
|
|
this.showDescription
|
|
),
|
|
false
|
|
);
|
|
},
|
|
|
|
/**
|
|
* @param {AbortableSearchFetch} search
|
|
* @param {boolean} replaceResults
|
|
*/
|
|
updateUIWithSearchClientResult( search, replaceResults ) {
|
|
const query = this.currentSearchQuery;
|
|
instrumentation.listeners.onFetchStart();
|
|
|
|
search.fetch
|
|
.then( ( data ) => {
|
|
// Only use these results if they're still relevant
|
|
// If currentSearchQuery !== query, these results are for a previous search
|
|
// and we shouldn't show them.
|
|
if ( this.currentSearchQuery === query ) {
|
|
if ( replaceResults ) {
|
|
this.suggestions = [];
|
|
}
|
|
this.suggestions.push(
|
|
...instrumentation.addWprovToSearchResultUrls(
|
|
data.results, this.suggestions.length
|
|
)
|
|
);
|
|
this.searchFooterUrl = urlGenerator.generateUrl( query );
|
|
}
|
|
|
|
const event = {
|
|
numberOfResults: data.results.length,
|
|
query: query
|
|
};
|
|
instrumentation.listeners.onFetchEnd( event );
|
|
} )
|
|
.catch( () => {
|
|
// TODO: error handling
|
|
} );
|
|
},
|
|
|
|
/**
|
|
* @param {SearchSubmitEvent} event
|
|
*/
|
|
onSubmit( event ) {
|
|
this.wprov = instrumentation.getWprovFromResultIndex( event.index );
|
|
|
|
instrumentation.listeners.onSubmit( event );
|
|
},
|
|
|
|
onFocus() {
|
|
this.isFocused = true;
|
|
},
|
|
|
|
onBlur() {
|
|
this.isFocused = false;
|
|
}
|
|
},
|
|
mounted() {
|
|
if ( this.autofocusInput ) {
|
|
this.$refs.searchForm.focus();
|
|
nextTick( () => {
|
|
this.disableTransitions = false;
|
|
} );
|
|
}
|
|
if ( this.$refs.emptyState ) {
|
|
mw.hook( 'search.display' ).fire( this.$refs.emptyState );
|
|
}
|
|
}
|
|
} );
|
|
</script>
|