mediawiki-skins-Vector/resources/skins.vector.search/App.vue
Roan Kattouw 5bcc1597ca search: Fix unclickable search footer
Clicking the bolded text in the search footer didn't work. This change
fixes that by working around the underlying issue, rewriting the search
footer template to use v-html instead of v-i18n-html. I don't know
exactly know why this works, but it does. Hopefully we'll be able to
fix the underlying issue and undo this workaround in the future.

Bug: T327229
Change-Id: I5d05d6ade1a34b12163bb96aa888ed3cfee78b4d
2023-01-30 18:18:29 +00:00

280 lines
6.8 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>
<input
type="hidden"
name="title"
:value="searchPageTitle"
>
<input
type="hidden"
name="wprov"
:value="wprov"
>
</template>
<!-- eslint-disable-next-line vue/no-template-shadow -->
<template #search-footer-text="{ searchQuery }">
<!--
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>
</template>
</cdx-typeahead-search>
</template>
<script>
/* global AbortableSearchFetch, SearchSubmitEvent */
const { CdxTypeaheadSearch } = require( '@wikimedia/codex-search' ),
{ 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
},
searchPageTitle: {
type: String,
default: 'Special:Search'
},
autofocusInput: {
type: Boolean,
default: false
},
action: {
type: String,
default: ''
},
/** The keyboard shortcut to focus search. */
// eslint-disable-next-line vue/require-default-prop
searchAccessKey: {
type: String
},
/** The access key informational tip for search. */
// eslint-disable-next-line vue/require-default-prop
searchTitle: {
type: String
},
/** The ghost text shown when no search query is entered. */
// eslint-disable-next-line vue/require-default-prop
searchPlaceholder: {
type: String
},
/**
* The search query string taken from the server-side rendered input immediately before
* client render.
*/
// eslint-disable-next-line vue/require-default-prop
searchQuery: {
type: String
},
showThumbnail: {
type: Boolean,
// eslint-disable-next-line vue/no-boolean-default
default: true
},
showDescription: {
type: Boolean,
// eslint-disable-next-line vue/no-boolean-default
default: true
},
highlightQuery: {
type: Boolean,
// eslint-disable-next-line vue/no-boolean-default
default: true
},
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
);
},
/**
* 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;
} );
}
}
} );
</script>