From d6bfce15c5534791d2f7c4275f5781d2b2d8f6dc Mon Sep 17 00:00:00 2001 From: alistair3149 Date: Wed, 22 May 2024 00:36:16 -0400 Subject: [PATCH] =?UTF-8?q?refactor(search):=20=E2=99=BB=EF=B8=8F=20clean?= =?UTF-8?q?=20up=20searchResults=20functions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../skins.citizen.search/searchResults.js | 87 ++++++++++++++----- resources/skins.citizen.search/typeahead.js | 48 ++-------- 2 files changed, 74 insertions(+), 61 deletions(-) diff --git a/resources/skins.citizen.search/searchResults.js b/resources/skins.citizen.search/searchResults.js index b82eea69f..9b8de156c 100644 --- a/resources/skins.citizen.search/searchResults.js +++ b/resources/skins.citizen.search/searchResults.js @@ -1,25 +1,37 @@ // const config = require( './config.json' ); -// const htmlHelper = require( './htmlHelper.js' )(); +const htmlHelper = require( './htmlHelper.js' )(); const searchAction = require( './searchAction.js' )(); /** - * Returns an object with methods related to search results handling. + * Returns an object with methods for handling search results in a citizen search context. + * It includes methods for getting redirect labels, highlighting titles, getting placeholder HTML, + * getting results HTML, fetching search results, rendering search actions, and clearing search results. * - * @return {Object} An object with the following methods: - * - getRedirectLabel: A function that generates HTML for a search result label with redirection information. - * - highlightTitle: A function that highlights a matched title within a given text. - * - fetch: A function that fetches search results based on a query value using an active search client. - * - render: A function that renders search results in a specified typeahead element. - * - clear: A function that clears search results from a typeahead element. + * Methods: + * - getRedirectLabel: Returns a redirect label for a matched title based on query value. + * - highlightTitle: Highlights a title based on a matching query. + * - getPlaceholderHTML: Returns HTML for a placeholder when no search results are found. + * - getResultsHTML: Returns HTML for displaying search results. + * - fetch: Fetches search results based on a query value using an active search client. + * - render: Renders search actions on a typeahead element with a search query. + * - clear: Clears search results from a typeahead element. + * + * @return {Object} An object with methods for handling search results. */ function searchResults() { + const textCache = {}; + const redirectMessageCache = {}; + const regexCache = {}; + return { getRedirectLabel: function ( title, matchedTitle, queryValue ) { const normalizeText = ( text ) => { - return text.replace( /[-\s]/g, ( match ) => match.toLowerCase() ).toLowerCase(); + if ( !textCache[ text ] ) { + textCache[ text ] = text.replace( /[-\s]/g, ( match ) => match.toLowerCase() ).toLowerCase(); + } + return textCache[ text ]; }; - const redirectMessageCache = {}; const getRedirectMessage = () => { if ( !redirectMessageCache[ matchedTitle ] ) { redirectMessageCache[ matchedTitle ] = mw.message( 'search-redirect', matchedTitle ).plain(); @@ -60,19 +72,52 @@ function searchResults() { return html; }, - highlightTitle: ( function () { - const regexCache = {}; - return function ( title, match ) { - if ( !match ) { - return title; - } - if ( !regexCache[ match ] ) { - regexCache[ match ] = new RegExp( mw.util.escapeRegExp( match ), 'i' ); + highlightTitle: function ( title, match ) { + if ( !match ) { + return title; + } + if ( !regexCache[ match ] ) { + regexCache[ match ] = new RegExp( mw.util.escapeRegExp( match ), 'i' ); + } + const regex = regexCache[ match ]; + return title.replace( regex, '$&' ); + }, + getPlaceholderHTML: function ( queryValue ) { + const data = { + icon: 'articleNotFound', + type: 'placeholder', + size: 'lg', + title: mw.message( 'citizen-search-noresults-title', queryValue ).text(), + desc: mw.message( 'citizen-search-noresults-desc' ).text() + }; + return htmlHelper.getItemElement( data ); + }, + getResultsHTML: function ( results, queryValue ) { + const createSuggestionItem = ( result ) => { + const data = { + type: 'page', + size: 'md', + link: result.url, + title: this.highlightTitle( result.title, queryValue ), + desc: result.description + }; + data.label = this.getRedirectLabel( result.title, result.label ); + if ( result.thumbnail ) { + data.thumbnail = result.thumbnail.url; + } else { + // Thumbnail placeholder icon + data.icon = 'image'; } - const regex = regexCache[ match ]; - return title.replace( regex, '$&' ); + return data; }; - }() ), + + const items = results.map( ( result ) => createSuggestionItem( result ) ); + const itemGroupData = { + id: 'suggestion', + items: items + }; + return htmlHelper.getItemGroupElement( itemGroupData ); + }, fetch: function ( queryValue, activeSearchClient ) { return activeSearchClient.fetchByTitle( queryValue ); }, diff --git a/resources/skins.citizen.search/typeahead.js b/resources/skins.citizen.search/typeahead.js index 15adcf5cc..f0021215d 100644 --- a/resources/skins.citizen.search/typeahead.js +++ b/resources/skins.citizen.search/typeahead.js @@ -344,40 +344,10 @@ async function getSuggestions() { const renderSuggestions = ( results ) => { const fragment = document.createDocumentFragment(); if ( results.length > 0 ) { - // Create suggestion items - const itemGroupData = { - id: 'suggestion', - items: [] - }; - const items = results.map( ( result ) => { - const data = { - type: 'page', - size: 'md', - link: result.url, - title: searchResults.highlightTitle( result.title, searchQuery.valueHtml ), - desc: result.description - }; - data.label = searchResults.getRedirectLabel( result.title, result.label ); - if ( result.thumbnail ) { - data.thumbnail = result.thumbnail.url; - } else { - // Thumbnail placeholder icon - data.icon = 'image'; - } - return data; - } ); - itemGroupData.items.push( ...items ); - fragment.append( htmlHelper.getItemGroupElement( itemGroupData ) ); + fragment.append( searchResults.getResultsHTML( results, searchQuery.valueHtml ) ); } else { // Update placeholder with no result content - const data = { - icon: 'articleNotFound', - type: 'placeholder', - size: 'lg', - title: mw.message( 'citizen-search-noresults-title', searchQuery.valueHtml ).text(), - desc: mw.message( 'citizen-search-noresults-desc' ).text() - }; - fragment.append( htmlHelper.getItemElement( data ) ); + fragment.append( searchResults.getPlaceholderHTML( searchQuery.valueHtml ) ); } htmlHelper.removeItemGroup( typeahead.element, 'suggestion' ); @@ -394,19 +364,17 @@ async function getSuggestions() { const { abort, fetch } = searchResults.fetch( searchQuery.value, searchClient.active.client ); - // Abort fetch if the input is detected - // So that fetch request won't be queued up - typeaheadInputElement.addEventListener( 'input', abort, { once: true } ); + const inputEventListener = () => { + abort(); + typeaheadInputElement.removeEventListener( 'input', inputEventListener ); + }; + typeaheadInputElement.addEventListener( 'input', inputEventListener, { once: true } ); try { const response = await fetch; - typeaheadInputElement.removeEventListener( 'input', abort ); typeahead.suggestions.clear(); - if ( response.results !== null ) { - renderSuggestions( response.results ); - } + renderSuggestions( response.results ); } catch ( error ) { - typeaheadInputElement.removeEventListener( 'input', abort ); typeahead.form.setLoadingState( false ); // User can trigger the abort when the fetch event is pending // There is no need for an error