2020-11-19 17:12:53 +00:00
|
|
|
<template>
|
2022-02-01 20:52:16 +00:00
|
|
|
<cdx-typeahead-search
|
2021-09-09 21:40:06 +00:00
|
|
|
:id="id"
|
2021-04-21 12:48:02 +00:00
|
|
|
ref="searchForm"
|
2022-09-01 18:04:12 +00:00
|
|
|
class="vector-typeahead-search"
|
2022-02-01 20:52:16 +00:00
|
|
|
:class="rootClasses"
|
|
|
|
:search-results-label="$i18n( 'searchresults' ).text()"
|
2021-04-21 12:48:02 +00:00
|
|
|
:accesskey="searchAccessKey"
|
2022-10-26 20:22:06 +00:00
|
|
|
:autocapitalize="autocapitalizeValue"
|
2021-04-21 12:48:02 +00:00
|
|
|
:title="searchTitle"
|
|
|
|
:placeholder="searchPlaceholder"
|
|
|
|
:aria-label="searchPlaceholder"
|
|
|
|
:initial-input-value="searchQuery"
|
2021-05-26 21:13:43 +00:00
|
|
|
:button-label="$i18n( 'searchbutton' ).text()"
|
2021-04-21 12:48:02 +00:00
|
|
|
:form-action="action"
|
|
|
|
:show-thumbnail="showThumbnail"
|
2021-05-25 10:40:49 +00:00
|
|
|
:highlight-query="highlightQuery"
|
2021-12-16 03:13:56 +00:00
|
|
|
:auto-expand-width="autoExpandWidth"
|
2022-02-01 20:52:16 +00:00
|
|
|
:search-results="suggestions"
|
|
|
|
:search-footer-url="searchFooterUrl"
|
2022-10-24 12:10:46 +00:00
|
|
|
:visible-item-limit="visibleItemLimit"
|
|
|
|
@load-more="onLoadMore"
|
2022-02-01 20:52:16 +00:00
|
|
|
@input="onInput"
|
|
|
|
@search-result-click="instrumentation.onSuggestionClick"
|
2021-05-13 14:25:53 +00:00
|
|
|
@submit="onSubmit"
|
2022-09-01 18:04:12 +00:00
|
|
|
@focus="onFocus"
|
|
|
|
@blur="onBlur"
|
2021-04-21 12:48:02 +00:00
|
|
|
>
|
2021-09-30 20:52:16 +00:00
|
|
|
<template #default>
|
2022-05-16 13:25:03 +00:00
|
|
|
<input
|
|
|
|
type="hidden"
|
2021-09-30 20:52:16 +00:00
|
|
|
name="title"
|
|
|
|
:value="searchPageTitle"
|
|
|
|
>
|
2022-05-16 13:25:03 +00:00
|
|
|
<input
|
|
|
|
type="hidden"
|
2021-09-30 20:52:16 +00:00
|
|
|
name="wprov"
|
|
|
|
:value="wprov"
|
|
|
|
>
|
|
|
|
</template>
|
2022-05-16 13:25:03 +00:00
|
|
|
<!-- eslint-disable-next-line vue/no-template-shadow -->
|
2022-03-16 23:40:42 +00:00
|
|
|
<template #search-footer-text="{ searchQuery }">
|
2023-01-28 00:03:09 +00:00
|
|
|
<!--
|
|
|
|
Normally we'd use v-i18n-html here, like this:
|
|
|
|
<span v-i18n-html:vector-searchsuggest-containing="[ searchQuery ]"></span>
|
|
|
|
but that causes strange rerendering issues and makes the <strong> tag rendered
|
|
|
|
by the message unclickable, see T327229.
|
|
|
|
-->
|
|
|
|
<!-- eslint-disable-next-line max-len, vue/no-v-html -->
|
|
|
|
<span v-html="$i18n( 'vector-searchsuggest-containing' ).params( [ searchQuery ] ).parse()"></span>
|
2021-09-30 20:52:16 +00:00
|
|
|
</template>
|
2022-02-01 20:52:16 +00:00
|
|
|
</cdx-typeahead-search>
|
2020-11-19 17:12:53 +00:00
|
|
|
</template>
|
|
|
|
|
|
|
|
<script>
|
2022-10-24 12:10:46 +00:00
|
|
|
/* global AbortableSearchFetch, SearchSubmitEvent */
|
2022-02-01 20:52:16 +00:00
|
|
|
const { CdxTypeaheadSearch } = require( '@wikimedia/codex-search' ),
|
|
|
|
{ defineComponent, nextTick } = require( 'vue' ),
|
2022-02-02 21:17:56 +00:00
|
|
|
client = require( './restSearchClient.js' ),
|
2022-02-01 20:52:16 +00:00
|
|
|
restClient = client( mw.config ),
|
|
|
|
urlGenerator = require( './urlGenerator.js' )( mw.config ),
|
2020-11-24 22:16:03 +00:00
|
|
|
instrumentation = require( './instrumentation.js' );
|
2020-11-19 17:12:53 +00:00
|
|
|
|
2022-05-16 13:25:03 +00:00
|
|
|
// @vue/component
|
2022-02-01 20:52:16 +00:00
|
|
|
module.exports = exports = defineComponent( {
|
2020-11-19 17:12:53 +00:00
|
|
|
name: 'App',
|
2022-07-19 23:23:06 +00:00
|
|
|
compatConfig: {
|
|
|
|
MODE: 3
|
|
|
|
},
|
|
|
|
compilerOptions: {
|
|
|
|
whitespace: 'condense'
|
|
|
|
},
|
2022-02-01 20:52:16 +00:00
|
|
|
components: { CdxTypeaheadSearch },
|
2020-11-19 17:12:53 +00:00
|
|
|
props: {
|
2021-09-09 21:40:06 +00:00
|
|
|
id: {
|
|
|
|
type: String,
|
|
|
|
required: true
|
|
|
|
},
|
2022-10-26 20:22:06 +00:00
|
|
|
autocapitalizeValue: {
|
|
|
|
type: String
|
|
|
|
},
|
2021-07-28 16:15:57 +00:00
|
|
|
searchPageTitle: {
|
|
|
|
type: String,
|
|
|
|
default: 'Special:Search'
|
|
|
|
},
|
2020-11-19 17:12:53 +00:00
|
|
|
autofocusInput: {
|
|
|
|
type: Boolean,
|
|
|
|
default: false
|
|
|
|
},
|
|
|
|
action: {
|
|
|
|
type: String,
|
|
|
|
default: ''
|
|
|
|
},
|
|
|
|
/** The keyboard shortcut to focus search. */
|
2022-05-16 13:25:03 +00:00
|
|
|
// eslint-disable-next-line vue/require-default-prop
|
2020-11-19 17:12:53 +00:00
|
|
|
searchAccessKey: {
|
2022-03-16 23:40:42 +00:00
|
|
|
type: String
|
2020-11-19 17:12:53 +00:00
|
|
|
},
|
|
|
|
/** The access key informational tip for search. */
|
2022-05-16 13:25:03 +00:00
|
|
|
// eslint-disable-next-line vue/require-default-prop
|
2020-11-19 17:12:53 +00:00
|
|
|
searchTitle: {
|
2022-03-16 23:40:42 +00:00
|
|
|
type: String
|
2020-11-19 17:12:53 +00:00
|
|
|
},
|
|
|
|
/** The ghost text shown when no search query is entered. */
|
2022-05-16 13:25:03 +00:00
|
|
|
// eslint-disable-next-line vue/require-default-prop
|
2020-11-19 17:12:53 +00:00
|
|
|
searchPlaceholder: {
|
2022-03-16 23:40:42 +00:00
|
|
|
type: String
|
2020-11-19 17:12:53 +00:00
|
|
|
},
|
|
|
|
/**
|
|
|
|
* The search query string taken from the server-side rendered input immediately before
|
|
|
|
* client render.
|
|
|
|
*/
|
2022-05-16 13:25:03 +00:00
|
|
|
// eslint-disable-next-line vue/require-default-prop
|
2020-11-19 17:12:53 +00:00
|
|
|
searchQuery: {
|
2022-03-16 23:40:42 +00:00
|
|
|
type: String
|
2020-11-26 00:58:10 +00:00
|
|
|
},
|
|
|
|
showThumbnail: {
|
2022-03-16 23:40:42 +00:00
|
|
|
type: Boolean,
|
2022-05-16 13:25:03 +00:00
|
|
|
// eslint-disable-next-line vue/no-boolean-default
|
2022-03-16 23:40:42 +00:00
|
|
|
default: true
|
2020-11-26 00:58:10 +00:00
|
|
|
},
|
|
|
|
showDescription: {
|
2022-03-16 23:40:42 +00:00
|
|
|
type: Boolean,
|
2022-05-16 13:25:03 +00:00
|
|
|
// eslint-disable-next-line vue/no-boolean-default
|
2022-03-16 23:40:42 +00:00
|
|
|
default: true
|
2021-05-25 10:40:49 +00:00
|
|
|
},
|
|
|
|
highlightQuery: {
|
2022-03-16 23:40:42 +00:00
|
|
|
type: Boolean,
|
2022-05-16 13:25:03 +00:00
|
|
|
// eslint-disable-next-line vue/no-boolean-default
|
2022-03-16 23:40:42 +00:00
|
|
|
default: true
|
2021-12-16 03:13:56 +00:00
|
|
|
},
|
|
|
|
autoExpandWidth: {
|
2022-03-16 23:40:42 +00:00
|
|
|
type: Boolean,
|
|
|
|
default: false
|
2020-11-19 17:12:53 +00:00
|
|
|
}
|
2020-11-24 22:16:03 +00:00
|
|
|
},
|
2022-01-31 21:43:32 +00:00
|
|
|
data() {
|
2020-11-24 22:16:03 +00:00
|
|
|
return {
|
2022-02-01 20:52:16 +00:00
|
|
|
// -1 here is the default "active suggestion index".
|
2020-11-24 22:16:03 +00:00
|
|
|
wprov: instrumentation.getWprovFromResultIndex( -1 ),
|
|
|
|
|
2022-02-01 20:52:16 +00:00
|
|
|
// Suggestions to be shown in the TypeaheadSearch menu.
|
|
|
|
suggestions: [],
|
|
|
|
|
|
|
|
// Link to the search page for the current search query.
|
|
|
|
searchFooterUrl: '',
|
|
|
|
|
2022-10-18 17:48:11 +00:00
|
|
|
// The current search query. Used to detect whether a fetch response is stale.
|
|
|
|
currentSearchQuery: '',
|
|
|
|
|
2022-02-01 20:52:16 +00:00
|
|
|
// Whether to apply a CSS class that disables the CSS transitions on the text input
|
|
|
|
disableTransitions: this.autofocusInput,
|
|
|
|
|
2022-09-01 18:04:12 +00:00
|
|
|
instrumentation: instrumentation.listeners,
|
|
|
|
|
|
|
|
isFocused: false
|
2020-11-24 22:16:03 +00:00
|
|
|
};
|
|
|
|
},
|
2022-05-16 13:25:03 +00:00
|
|
|
computed: {
|
2022-02-01 20:52:16 +00:00
|
|
|
rootClasses() {
|
|
|
|
return {
|
2022-09-01 18:04:12 +00:00
|
|
|
'vector-search-box-disable-transitions': this.disableTransitions,
|
|
|
|
'vector-typeahead-search--active': this.isFocused
|
2022-02-01 20:52:16 +00:00
|
|
|
};
|
2022-10-24 12:10:46 +00:00
|
|
|
},
|
|
|
|
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;
|
2022-02-01 20:52:16 +00:00
|
|
|
}
|
|
|
|
},
|
|
|
|
methods: {
|
2022-05-16 13:25:03 +00:00
|
|
|
/**
|
2022-02-01 20:52:16 +00:00
|
|
|
* Fetch suggestions when new input is received.
|
2022-05-16 13:25:03 +00:00
|
|
|
*
|
2022-02-01 20:52:16 +00:00
|
|
|
* @param {string} value
|
2022-05-16 13:25:03 +00:00
|
|
|
*/
|
2022-02-01 20:52:16 +00:00
|
|
|
onInput: function ( value ) {
|
2022-11-25 13:52:19 +00:00
|
|
|
const query = value.trim();
|
2022-02-01 20:52:16 +00:00
|
|
|
|
2022-10-18 17:48:11 +00:00
|
|
|
this.currentSearchQuery = query;
|
|
|
|
|
2022-02-01 20:52:16 +00:00
|
|
|
if ( query === '' ) {
|
|
|
|
this.suggestions = [];
|
|
|
|
this.searchFooterUrl = '';
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2022-10-24 12:10:46 +00:00
|
|
|
this.updateUIWithSearchClientResult(
|
|
|
|
restClient.fetchByTitle( query, 10, this.showDescription ),
|
|
|
|
true
|
|
|
|
);
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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;
|
2022-02-01 20:52:16 +00:00
|
|
|
instrumentation.listeners.onFetchStart();
|
|
|
|
|
2022-10-24 12:10:46 +00:00
|
|
|
search.fetch
|
2022-02-01 20:52:16 +00:00
|
|
|
.then( ( data ) => {
|
2022-10-18 17:48:11 +00:00
|
|
|
// 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 ) {
|
2022-10-24 12:10:46 +00:00
|
|
|
if ( replaceResults ) {
|
|
|
|
this.suggestions = [];
|
|
|
|
}
|
|
|
|
this.suggestions.push(
|
|
|
|
...instrumentation.addWprovToSearchResultUrls( data.results, this.suggestions.length )
|
|
|
|
);
|
2022-10-18 17:48:11 +00:00
|
|
|
this.searchFooterUrl = urlGenerator.generateUrl( query );
|
|
|
|
}
|
2022-02-01 20:52:16 +00:00
|
|
|
|
|
|
|
const event = {
|
|
|
|
numberOfResults: data.results.length,
|
|
|
|
query: query
|
|
|
|
};
|
|
|
|
instrumentation.listeners.onFetchEnd( event );
|
|
|
|
} )
|
|
|
|
.catch( () => {
|
|
|
|
// TODO: error handling
|
|
|
|
} );
|
2022-05-16 13:25:03 +00:00
|
|
|
},
|
2022-02-01 20:52:16 +00:00
|
|
|
|
2020-11-24 22:16:03 +00:00
|
|
|
/**
|
2022-02-01 20:52:16 +00:00
|
|
|
* @param {SearchSubmitEvent} event
|
2020-11-24 22:16:03 +00:00
|
|
|
*/
|
2022-01-31 21:43:32 +00:00
|
|
|
onSubmit( event ) {
|
2020-11-24 22:16:03 +00:00
|
|
|
this.wprov = instrumentation.getWprovFromResultIndex( event.index );
|
|
|
|
|
|
|
|
instrumentation.listeners.onSubmit( event );
|
2022-09-01 18:04:12 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
onFocus() {
|
|
|
|
this.isFocused = true;
|
|
|
|
},
|
|
|
|
|
|
|
|
onBlur() {
|
|
|
|
this.isFocused = false;
|
2020-11-24 22:16:03 +00:00
|
|
|
}
|
2022-05-16 13:25:03 +00:00
|
|
|
},
|
|
|
|
mounted() {
|
|
|
|
if ( this.autofocusInput ) {
|
2022-02-01 20:52:16 +00:00
|
|
|
this.$refs.searchForm.focus();
|
|
|
|
nextTick( () => {
|
|
|
|
this.disableTransitions = false;
|
|
|
|
} );
|
2022-05-16 13:25:03 +00:00
|
|
|
}
|
2020-11-19 17:12:53 +00:00
|
|
|
}
|
2022-02-01 20:52:16 +00:00
|
|
|
} );
|
2020-11-19 17:12:53 +00:00
|
|
|
</script>
|