mirror of
https://github.com/StarCitizenTools/mediawiki-skins-Citizen.git
synced 2024-09-23 10:19:43 +00:00
refactor(search): ♻️ reimplement and clean up search clients
This should make the search clients more readable and easier to expand
This commit is contained in:
parent
c2431ebeba
commit
431599cb59
|
@ -19,6 +19,7 @@
|
|||
"es-x/no-array-prototype-includes": "warn",
|
||||
"es-x/no-optional-chaining": "warn",
|
||||
"es-x/no-nullish-coalescing-operators": "warn",
|
||||
"es-x/no-rest-spread-properties": "warn",
|
||||
"es-x/no-symbol-prototype-description": "warn",
|
||||
"compat/compat": "warn",
|
||||
"mediawiki/class-doc": "off"
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
"devDependencies": {
|
||||
"@commitlint/cli": "^17.6.7",
|
||||
"@commitlint/config-conventional": "^17.6.7",
|
||||
"@wikimedia/types-wikimedia": "0.4.1",
|
||||
"devmoji": "^2.3.0",
|
||||
"eslint-config-wikimedia": "0.25.1",
|
||||
"grunt-banana-checker": "0.11.0",
|
||||
|
|
|
@ -1,89 +0,0 @@
|
|||
/**
|
||||
* @typedef {Object} Results
|
||||
* @property {string} id The page ID of the page
|
||||
* @property {string} title The title of the page.
|
||||
* @property {string} desc The description of the page.
|
||||
* @property {string} thumbnail The url of the thumbnail of the page.
|
||||
*
|
||||
* @global
|
||||
*/
|
||||
|
||||
const fetchJson = require( '../fetch.js' );
|
||||
const defaultGatewayName = require( '../config.json' ).wgCitizenSearchGateway;
|
||||
|
||||
/**
|
||||
* Setup the gateway based on the name provided
|
||||
*
|
||||
* @param {string} gatewayName
|
||||
* @return {module}
|
||||
*/
|
||||
function getGateway( gatewayName ) {
|
||||
switch ( gatewayName ) {
|
||||
case 'mwActionApi':
|
||||
return require( './mwActionApi.js' );
|
||||
case 'mwRestApi':
|
||||
return require( './mwRestApi.js' );
|
||||
case 'smwAskApi':
|
||||
return require( './smwAskApi.js' );
|
||||
default:
|
||||
throw new Error( 'Unknown search gateway' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch suggestion from gateway and return the results object
|
||||
*
|
||||
* @param {string} searchQuery
|
||||
* @return {Object} Results
|
||||
*/
|
||||
function getResults( searchQuery ) {
|
||||
let gateway = getGateway( defaultGatewayName );
|
||||
|
||||
/*
|
||||
* Multi-gateway search experiment
|
||||
* This is a rough proof of concept for allowing multiple search gateway
|
||||
* We are using SMW Ask as an experiment
|
||||
*
|
||||
* TODO:
|
||||
* - Clean up gateway into JSON data perhaps
|
||||
* - Implement UI support (initial states, search syntax suggestions)
|
||||
* - Search query needs to be trimmed earlier so that query in the UI does not show the command
|
||||
*/
|
||||
const gatewayMap = new Map( [
|
||||
[ 'action', 'mwActionApi' ],
|
||||
[ 'ask', 'smwAskApi' ],
|
||||
[ 'rest', 'mwRestApi' ]
|
||||
] );
|
||||
|
||||
for ( const [ command, gatewayName ] of gatewayMap ) {
|
||||
if ( searchQuery.startsWith( `/${command}` ) ) {
|
||||
gateway = getGateway( gatewayName );
|
||||
/* Remove command (e.g. /smw) from query */
|
||||
searchQuery = searchQuery.slice( command.length + 1 );
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/* Abort early if there are no search query */
|
||||
if ( searchQuery.length === 0 ) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const result = fetchJson( gateway.getUrl( searchQuery ), {
|
||||
headers: {
|
||||
accept: 'application/json'
|
||||
}
|
||||
} );
|
||||
const searchResponsePromise = result.fetch
|
||||
.then( ( /** @type {RestResponse} */ res ) => {
|
||||
return gateway.convertDataToResults( res );
|
||||
} );
|
||||
return {
|
||||
abort: result.abort,
|
||||
fetch: searchResponsePromise
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getResults: getResults
|
||||
};
|
|
@ -1,119 +0,0 @@
|
|||
const config = require( '../config.json' ),
|
||||
descriptionSource = config.wgCitizenSearchDescriptionSource;
|
||||
|
||||
/**
|
||||
* Build URL used for fetch request
|
||||
*
|
||||
* @param {string} input
|
||||
* @return {string} url
|
||||
*/
|
||||
function getUrl( input ) {
|
||||
const endpoint = config.wgScriptPath + '/api.php?format=json',
|
||||
cacheExpiry = config.wgSearchSuggestCacheExpiry,
|
||||
maxResults = config.wgCitizenMaxSearchResults,
|
||||
query = {
|
||||
action: 'query',
|
||||
smaxage: cacheExpiry,
|
||||
maxage: cacheExpiry,
|
||||
generator: 'prefixsearch',
|
||||
prop: 'pageprops|pageimages',
|
||||
redirects: '',
|
||||
ppprop: 'displaytitle',
|
||||
piprop: 'thumbnail',
|
||||
pithumbsize: 200,
|
||||
pilimit: maxResults,
|
||||
gpssearch: input,
|
||||
gpsnamespace: 0,
|
||||
gpslimit: maxResults
|
||||
};
|
||||
|
||||
switch ( descriptionSource ) {
|
||||
case 'wikidata':
|
||||
query.prop += '|description';
|
||||
break;
|
||||
case 'textextracts':
|
||||
query.prop += '|extracts';
|
||||
query.exchars = '60';
|
||||
query.exintro = '1';
|
||||
query.exlimit = maxResults;
|
||||
query.explaintext = '1';
|
||||
break;
|
||||
case 'pagedescription':
|
||||
query.prop += '|pageprops';
|
||||
query.ppprop = 'description';
|
||||
break;
|
||||
}
|
||||
|
||||
let queryString = '';
|
||||
for ( const property in query ) {
|
||||
queryString += '&' + property + '=' + query[ property ];
|
||||
}
|
||||
|
||||
return endpoint + queryString;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map raw response to Results object
|
||||
*
|
||||
* @param {Object} data
|
||||
* @return {Object} Results
|
||||
*/
|
||||
function convertDataToResults( data ) {
|
||||
const getDisplayTitle = ( item ) => {
|
||||
if ( item.pageprops && item.pageprops.displaytitle ) {
|
||||
return item.pageprops.displaytitle;
|
||||
} else {
|
||||
return item.title;
|
||||
}
|
||||
};
|
||||
|
||||
const getDescription = ( item ) => {
|
||||
switch ( descriptionSource ) {
|
||||
case 'wikidata':
|
||||
/* eslint-disable-next-line es-x/no-symbol-prototype-description */
|
||||
return item.description || '';
|
||||
case 'textextracts':
|
||||
return item.extract || '';
|
||||
case 'pagedescription':
|
||||
/* eslint-disable es-x/no-symbol-prototype-description */
|
||||
if ( item.pageprops && item.pageprops.description ) {
|
||||
return item.pageprops.description.slice( 0, 60 ) + '...';
|
||||
/* eslint-enable es-x/no-symbol-prototype-description */
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const results = [];
|
||||
|
||||
if ( typeof ( ( data || [] ).query || [] ).pages === 'undefined' ) {
|
||||
return [];
|
||||
}
|
||||
|
||||
data = Object.values( data.query.pages );
|
||||
|
||||
// Sort the data with the index property since it is not in order
|
||||
data.sort( ( a, b ) => {
|
||||
return a.index - b.index;
|
||||
} );
|
||||
|
||||
for ( let i = 0; i < data.length; i++ ) {
|
||||
results[ i ] = {
|
||||
id: data[ i ].pageid,
|
||||
key: data[ i ].title,
|
||||
title: getDisplayTitle( data[ i ] ),
|
||||
desc: getDescription( data[ i ] )
|
||||
};
|
||||
if ( data[ i ].thumbnail && data[ i ].thumbnail.source ) {
|
||||
results[ i ].thumbnail = data[ i ].thumbnail.source;
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getUrl: getUrl,
|
||||
convertDataToResults: convertDataToResults
|
||||
};
|
|
@ -1,52 +0,0 @@
|
|||
const config = require( '../config.json' );
|
||||
|
||||
/**
|
||||
* Build URL used for fetch request
|
||||
*
|
||||
* @param {string} input
|
||||
* @return {string} url
|
||||
*/
|
||||
function getUrl( input ) {
|
||||
const endpoint = config.wgScriptPath + '/rest.php/v1/search/title?q=',
|
||||
query = '&limit=' + config.wgCitizenMaxSearchResults;
|
||||
|
||||
return endpoint + input + query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map raw response to Results object
|
||||
*
|
||||
* @param {Object} data
|
||||
* @return {Object} Results
|
||||
*/
|
||||
function convertDataToResults( data ) {
|
||||
const results = [];
|
||||
|
||||
data = ( data || [] ).pages || [];
|
||||
|
||||
for ( let i = 0; i < data.length; i++ ) {
|
||||
results[ i ] = {
|
||||
id: data[ i ].id,
|
||||
key: data[ i ].key,
|
||||
title: data[ i ].title,
|
||||
/* eslint-disable-next-line es-x/no-symbol-prototype-description */
|
||||
desc: data[ i ].description
|
||||
};
|
||||
// Redirect title
|
||||
// Since 1.38
|
||||
if ( data[ i ].matched_title ) {
|
||||
results[ i ].matchedTitle = data[ i ].matched_title;
|
||||
}
|
||||
|
||||
if ( data[ i ].thumbnail && data[ i ].thumbnail.url ) {
|
||||
results[ i ].thumbnail = data[ i ].thumbnail.url;
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getUrl: getUrl,
|
||||
convertDataToResults: convertDataToResults
|
||||
};
|
195
resources/skins.citizen.search/searchClients/mwActionApi.js
Normal file
195
resources/skins.citizen.search/searchClients/mwActionApi.js
Normal file
|
@ -0,0 +1,195 @@
|
|||
/** @module mwActionApiSearchClient */
|
||||
|
||||
const fetchJson = require( '../fetch.js' );
|
||||
const urlGenerator = require( '../urlGenerator.js' );
|
||||
|
||||
/**
|
||||
* @typedef {Object} ActionResponse
|
||||
* @property {ActionQuery[]} query
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} ActionQuery
|
||||
* @property {ActionRedirects[] | null} redirects
|
||||
* @property {ActionResult[]} pages
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} ActionRedirects
|
||||
* @property {string} from
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} ActionResult
|
||||
* @property {number} pageid
|
||||
* @property {number} index
|
||||
* @property {string} title
|
||||
* @property {ActionThumbnail | null} [thumbnail]
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} ActionThumbnail
|
||||
* @property {string} source
|
||||
* @property {number | null} [width]
|
||||
* @property {number | null} [height]
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} SearchResponse
|
||||
* @property {string} query
|
||||
* @property {SearchResult[]} results
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {MwMap} config
|
||||
* @param {string} query
|
||||
* @param {Object} response
|
||||
* @param {boolean} showDescription
|
||||
* @return {SearchResponse}
|
||||
*/
|
||||
function adaptApiResponse( config, query, response, showDescription ) {
|
||||
const urlGeneratorInstance = urlGenerator( config );
|
||||
const getDescription = ( page ) => {
|
||||
switch ( config.wgCitizenSearchDescriptionSource ) {
|
||||
case 'wikidata':
|
||||
return page?.description;
|
||||
case 'textextracts':
|
||||
return page?.extract;
|
||||
case 'pagedescription':
|
||||
return page?.pageprops?.description?.slice( 0, 60 );
|
||||
}
|
||||
};
|
||||
|
||||
response = response.query;
|
||||
|
||||
// Merge redirects array into pages array if avaliable
|
||||
// So the from key can be used for matched title
|
||||
if ( response.redirects ) {
|
||||
response.pages = Object.values(
|
||||
[ ...response.redirects, ...response.pages ].reduce( ( acc, curr ) => {
|
||||
const index = curr.index;
|
||||
acc[ index ] = { ...acc[ index ], ...curr };
|
||||
return acc;
|
||||
}, [] )
|
||||
);
|
||||
}
|
||||
|
||||
// Sort pages by index key instead of page id
|
||||
response.pages.sort( ( a, b ) => a.index - b.index );
|
||||
|
||||
return {
|
||||
query,
|
||||
results: response.pages.map( ( page ) => {
|
||||
const thumbnail = page.thumbnail;
|
||||
return {
|
||||
id: page.pageid,
|
||||
label: page.from,
|
||||
key: page.title.replace( / /g, '_' ),
|
||||
title: page.title,
|
||||
description: showDescription ? getDescription( page ) : undefined,
|
||||
url: urlGeneratorInstance.generateUrl( page ),
|
||||
thumbnail: thumbnail ? {
|
||||
url: thumbnail.source,
|
||||
width: thumbnail.width ?? undefined,
|
||||
height: thumbnail.height ?? undefined
|
||||
} : undefined
|
||||
};
|
||||
} )
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {Object} AbortableSearchFetch
|
||||
* @property {Promise<SearchResponse>} fetch
|
||||
* @property {Function} abort
|
||||
*/
|
||||
|
||||
/**
|
||||
* @callback fetchByTitle
|
||||
* @param {string} query The search term.
|
||||
* @param {number} [limit] Maximum number of results.
|
||||
* @return {AbortableSearchFetch}
|
||||
*/
|
||||
|
||||
/**
|
||||
* @callback loadMore
|
||||
* @param {string} query The search term.
|
||||
* @param {number} offset The number of search results that were already loaded.
|
||||
* @param {number} [limit] How many further search results to load (at most).
|
||||
* @return {AbortableSearchFetch}
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} SearchClient
|
||||
* @property {fetchByTitle} fetchByTitle
|
||||
* @property {loadMore} [loadMore]
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {MwMap} config
|
||||
* @return {SearchClient}
|
||||
*/
|
||||
function mwActionApiSearchClient( config ) {
|
||||
return {
|
||||
/**
|
||||
* @type {fetchByTitle}
|
||||
*/
|
||||
fetchByTitle: ( q, limit = config.wgCitizenMaxSearchResults, showDescription = true ) => {
|
||||
const cacheExpiry = config.wgSearchSuggestCacheExpiry;
|
||||
const descriptionSource = config.wgCitizenSearchDescriptionSource;
|
||||
|
||||
const searchApiUrl = config.wgScriptPath + '/api.php';
|
||||
const params = {
|
||||
format: 'json',
|
||||
formatversion: '2',
|
||||
action: 'query',
|
||||
smaxage: cacheExpiry,
|
||||
maxage: cacheExpiry,
|
||||
generator: 'prefixsearch',
|
||||
prop: 'pageprops|pageimages',
|
||||
redirects: '',
|
||||
ppprop: 'displaytitle',
|
||||
pilicense: 'any',
|
||||
piprop: 'thumbnail',
|
||||
pithumbsize: 200,
|
||||
pilimit: limit.toString(),
|
||||
gpssearch: q,
|
||||
gpslimit: limit.toString()
|
||||
};
|
||||
switch ( descriptionSource ) {
|
||||
case 'wikidata':
|
||||
params.prop += '|description';
|
||||
params.descprefersource = 'local';
|
||||
break;
|
||||
case 'textextracts':
|
||||
params.prop += '|extracts';
|
||||
params.exchars = '60';
|
||||
params.exintro = '1';
|
||||
params.exlimit = limit.toString();
|
||||
params.explaintext = '1';
|
||||
break;
|
||||
case 'pagedescription':
|
||||
params.prop += '|pageprops';
|
||||
params.ppprop = 'description';
|
||||
break;
|
||||
}
|
||||
const search = new URLSearchParams( params );
|
||||
const url = `${searchApiUrl}?${search.toString()}`;
|
||||
const result = fetchJson( url, {
|
||||
headers: {
|
||||
accept: 'application/json'
|
||||
}
|
||||
} );
|
||||
const searchResponsePromise = result.fetch
|
||||
.then( ( /** @type {ActionResponse} */ res ) => {
|
||||
return adaptApiResponse( config, q, res, showDescription );
|
||||
} );
|
||||
return {
|
||||
abort: result.abort,
|
||||
fetch: searchResponsePromise
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = mwActionApiSearchClient;
|
122
resources/skins.citizen.search/searchClients/mwRestApi.js
Normal file
122
resources/skins.citizen.search/searchClients/mwRestApi.js
Normal file
|
@ -0,0 +1,122 @@
|
|||
/** @module mwRestApiSearchClient */
|
||||
|
||||
const fetchJson = require( '../fetch.js' );
|
||||
const urlGenerator = require( '../urlGenerator.js' );
|
||||
|
||||
/**
|
||||
* @typedef {Object} RestResponse
|
||||
* @property {RestResult[]} pages
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} RestResult
|
||||
* @property {number} id
|
||||
* @property {string} key
|
||||
* @property {string} title
|
||||
* @property {string | null } matched_title
|
||||
* @property {string} [description]
|
||||
* @property {RestThumbnail | null} [thumbnail]
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} RestThumbnail
|
||||
* @property {string} url
|
||||
* @property {number | null} [width]
|
||||
* @property {number | null} [height]
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} SearchResponse
|
||||
* @property {string} query
|
||||
* @property {SearchResult[]} results
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {MwMap} config
|
||||
* @param {string} query
|
||||
* @param {Object} response
|
||||
* @param {boolean} showDescription
|
||||
* @return {SearchResponse}
|
||||
*/
|
||||
function adaptApiResponse( config, query, response, showDescription ) {
|
||||
const urlGeneratorInstance = urlGenerator( config );
|
||||
return {
|
||||
query,
|
||||
results: response.pages.map( ( page ) => {
|
||||
const thumbnail = page.thumbnail;
|
||||
return {
|
||||
id: page.id,
|
||||
label: page.matched_title,
|
||||
key: page.key,
|
||||
title: page.title,
|
||||
description: showDescription ? page.description : undefined,
|
||||
url: urlGeneratorInstance.generateUrl( page ),
|
||||
thumbnail: thumbnail ? {
|
||||
url: thumbnail.url,
|
||||
width: thumbnail.width ?? undefined,
|
||||
height: thumbnail.height ?? undefined
|
||||
} : undefined
|
||||
};
|
||||
} )
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {Object} AbortableSearchFetch
|
||||
* @property {Promise<SearchResponse>} fetch
|
||||
* @property {Function} abort
|
||||
*/
|
||||
|
||||
/**
|
||||
* @callback fetchByTitle
|
||||
* @param {string} query The search term.
|
||||
* @param {number} [limit] Maximum number of results.
|
||||
* @return {AbortableSearchFetch}
|
||||
*/
|
||||
|
||||
/**
|
||||
* @callback loadMore
|
||||
* @param {string} query The search term.
|
||||
* @param {number} offset The number of search results that were already loaded.
|
||||
* @param {number} [limit] How many further search results to load (at most).
|
||||
* @return {AbortableSearchFetch}
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} SearchClient
|
||||
* @property {fetchByTitle} fetchByTitle
|
||||
* @property {loadMore} [loadMore]
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {MwMap} config
|
||||
* @return {SearchClient}
|
||||
*/
|
||||
function mwRestApiSearchClient( config ) {
|
||||
return {
|
||||
/**
|
||||
* @type {fetchByTitle}
|
||||
*/
|
||||
fetchByTitle: ( q, limit = config.wgCitizenMaxSearchResults, showDescription = true ) => {
|
||||
const searchApiUrl = config.wgScriptPath + '/rest.php';
|
||||
const params = { q, limit: limit.toString() };
|
||||
const search = new URLSearchParams( params );
|
||||
const url = `${searchApiUrl}/v1/search/title?${search.toString()}`;
|
||||
const result = fetchJson( url, {
|
||||
headers: {
|
||||
accept: 'application/json'
|
||||
}
|
||||
} );
|
||||
const searchResponsePromise = result.fetch
|
||||
.then( ( /** @type {RestResponse} */ res ) => {
|
||||
return adaptApiResponse( config, q, res, showDescription );
|
||||
} );
|
||||
return {
|
||||
abort: result.abort,
|
||||
fetch: searchResponsePromise
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = mwRestApiSearchClient;
|
|
@ -0,0 +1,17 @@
|
|||
[
|
||||
{
|
||||
"id": "mwActionApi",
|
||||
"name": "MediaWiki search",
|
||||
"command": "action"
|
||||
},
|
||||
{
|
||||
"id": "mwRestApi",
|
||||
"name": "MediaWiki search",
|
||||
"command": "rest"
|
||||
},
|
||||
{
|
||||
"id": "smwAskApi",
|
||||
"name": "Semantic search",
|
||||
"command": "ask"
|
||||
}
|
||||
]
|
|
@ -1,3 +1,6 @@
|
|||
/*
|
||||
* TODO: This is going to be refactored soon as it is using the old standard
|
||||
*/
|
||||
const config = require( '../config.json' );
|
||||
|
||||
/**
|
|
@ -5,11 +5,27 @@ const
|
|||
ACTIVE_CLASS = `${ITEM_CLASS}--active`,
|
||||
HIDDEN_CLASS = `${ITEM_CLASS}--hidden`;
|
||||
|
||||
/**
|
||||
* Config object from getCitizenSearchResourceLoaderConfig()
|
||||
*/
|
||||
// Config object from getCitizenSearchResourceLoaderConfig()
|
||||
const config = require( './config.json' );
|
||||
const gateway = require( './gateway/gateway.js' );
|
||||
|
||||
// Search clients definition
|
||||
const searchClients = require( './searchClients/searchClients.json' );
|
||||
|
||||
const searchClient = {
|
||||
active: null,
|
||||
getData: function ( id ) {
|
||||
const data = Object.values( searchClients ).find( ( item ) => item.id === id );
|
||||
return data;
|
||||
},
|
||||
setActive: function ( id ) {
|
||||
const data = this.getData( id );
|
||||
if ( data ) {
|
||||
const client = require( `./searchClients/${data.id}.js` );
|
||||
this.active = data;
|
||||
this.active.client = client( config );
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const activeIndex = {
|
||||
index: -1,
|
||||
|
@ -165,9 +181,7 @@ function clearSuggestions() {
|
|||
function getSuggestions( searchQuery, htmlSafeSearchQuery, placeholder ) {
|
||||
const renderSuggestions = ( results ) => {
|
||||
if ( results.length > 0 ) {
|
||||
const
|
||||
fragment = document.createDocumentFragment(),
|
||||
suggestionLinkPrefix = `${config.wgScriptPath}/index.php?title=Special:Search&search=`;
|
||||
const fragment = document.createDocumentFragment();
|
||||
/**
|
||||
* Return the redirect title with search query highlight
|
||||
*
|
||||
|
@ -225,16 +239,15 @@ function getSuggestions( searchQuery, htmlSafeSearchQuery, placeholder ) {
|
|||
id: `${PREFIX}-suggestion-${index}`,
|
||||
type: 'page',
|
||||
size: 'md',
|
||||
link: suggestionLinkPrefix + encodeURIComponent( result.key ),
|
||||
link: result.url,
|
||||
title: highlightTitle( result.title ),
|
||||
// Just to be safe, not sure if the default API is HTML escaped
|
||||
desc: result.desc
|
||||
desc: result.description
|
||||
};
|
||||
if ( result.matchedTitle ) {
|
||||
data.label = getRedirectLabel( result.title, result.matchedTitle );
|
||||
if ( result.label ) {
|
||||
data.label = getRedirectLabel( result.title, result.label );
|
||||
}
|
||||
if ( result.thumbnail ) {
|
||||
data.thumbnail = result.thumbnail;
|
||||
data.thumbnail = result.thumbnail.url;
|
||||
} else {
|
||||
// Thumbnail placeholder icon
|
||||
data.icon = 'image';
|
||||
|
@ -263,17 +276,17 @@ function getSuggestions( searchQuery, htmlSafeSearchQuery, placeholder ) {
|
|||
// Add loading animation
|
||||
searchInput.parentNode.classList.add( SEARCH_LOADING_CLASS );
|
||||
|
||||
const { abort, fetch } = gateway.getResults( searchQuery );
|
||||
const { abort, fetch } = searchClient.active.client.fetchByTitle( searchQuery );
|
||||
|
||||
// Abort fetch if the input is detected
|
||||
// So that fetch request won't be queued up
|
||||
searchInput.addEventListener( 'input', abort, { once: true } );
|
||||
|
||||
fetch?.then( ( results ) => {
|
||||
fetch?.then( ( response ) => {
|
||||
searchInput.removeEventListener( 'input', abort );
|
||||
clearSuggestions();
|
||||
if ( results !== null ) {
|
||||
renderSuggestions( results );
|
||||
if ( response.results !== null ) {
|
||||
renderSuggestions( response.results );
|
||||
updateActiveIndex();
|
||||
}
|
||||
} ).catch( ( error ) => {
|
||||
|
@ -498,6 +511,9 @@ function initTypeahead( searchForm, input ) {
|
|||
|
||||
// Init the value in case of undef error
|
||||
updateActiveIndex();
|
||||
// Set default active search client
|
||||
searchClient.setActive( config.wgCitizenSearchGateway );
|
||||
|
||||
// Since searchInput is focused before the event listener is set up
|
||||
onFocus();
|
||||
searchInput.addEventListener( 'focus', onFocus );
|
||||
|
|
17
resources/skins.citizen.search/types.js
Normal file
17
resources/skins.citizen.search/types.js
Normal file
|
@ -0,0 +1,17 @@
|
|||
/**
|
||||
* @typedef {Object} SearchResult
|
||||
* @property {number} id
|
||||
* @property {string} key
|
||||
* @property {string} title
|
||||
* @property {string} [description]
|
||||
* @property {SearchResultThumbnail} [thumbnail]
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} SearchResultThumbnail
|
||||
* @property {string} url
|
||||
* @property {number} [width]
|
||||
* @property {number} [height]
|
||||
*/
|
||||
|
||||
/* exported SearchResult */
|
67
resources/skins.citizen.search/urlGenerator.js
Normal file
67
resources/skins.citizen.search/urlGenerator.js
Normal file
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* Adapted from Vector
|
||||
* All credits go to the developers behind Vector
|
||||
* @see https://github.com/wikimedia/mediawiki-skins-Vector/blob/master/resources/skins.vector.search/urlGenerator.js
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Record<string,string>} UrlParams
|
||||
* @param {string} title
|
||||
* @param {string} fulltext
|
||||
*/
|
||||
|
||||
/**
|
||||
* @callback generateUrl
|
||||
* @param {SearchResult|string} searchResult
|
||||
* @param {UrlParams} [params]
|
||||
* @param {string} [articlePath]
|
||||
* @return {string}
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} UrlGenerator
|
||||
* @property {generateUrl} generateUrl
|
||||
*/
|
||||
|
||||
/**
|
||||
* Generates URLs for suggestions like those in MediaWiki's mediawiki.searchSuggest implementation.
|
||||
*
|
||||
* @param {MwMap} config
|
||||
* @return {UrlGenerator}
|
||||
*/
|
||||
function urlGenerator( config ) {
|
||||
return {
|
||||
/**
|
||||
* @param {SearchResult|string} suggestion
|
||||
* @param {UrlParams} params
|
||||
* @param {string} articlePath
|
||||
* @return {string}
|
||||
*/
|
||||
generateUrl(
|
||||
suggestion,
|
||||
params = {
|
||||
title: 'Special:Search'
|
||||
},
|
||||
articlePath = config.wgScript
|
||||
) {
|
||||
if ( typeof suggestion !== 'string' ) {
|
||||
suggestion = suggestion.title;
|
||||
} else {
|
||||
// Add `fulltext` query param to search within pages and for navigation
|
||||
// to the search results page (prevents being redirected to a certain
|
||||
// article).
|
||||
params = Object.assign( {}, params, {
|
||||
fulltext: '1'
|
||||
} );
|
||||
}
|
||||
|
||||
const searchParams = new URLSearchParams(
|
||||
Object.assign( {}, params, { search: suggestion } )
|
||||
);
|
||||
return `${articlePath}?${searchParams.toString()}`;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** @module urlGenerator */
|
||||
module.exports = urlGenerator;
|
|
@ -143,6 +143,7 @@
|
|||
]
|
||||
},
|
||||
"skins.citizen.search": {
|
||||
"es6": true,
|
||||
"templates": [
|
||||
"resources/skins.citizen.search/templates/typeahead.mustache"
|
||||
],
|
||||
|
@ -157,10 +158,10 @@
|
|||
},
|
||||
"resources/skins.citizen.search/typeahead.js",
|
||||
"resources/skins.citizen.search/fetch.js",
|
||||
"resources/skins.citizen.search/gateway/gateway.js",
|
||||
"resources/skins.citizen.search/gateway/mwActionApi.js",
|
||||
"resources/skins.citizen.search/gateway/mwRestApi.js",
|
||||
"resources/skins.citizen.search/gateway/smwAskApi.js"
|
||||
"resources/skins.citizen.search/urlGenerator.js",
|
||||
"resources/skins.citizen.search/searchClients/searchClients.json",
|
||||
"resources/skins.citizen.search/searchClients/mwActionApi.js",
|
||||
"resources/skins.citizen.search/searchClients/mwRestApi.js"
|
||||
],
|
||||
"messages": [
|
||||
"citizen-search-fulltext",
|
||||
|
|
Loading…
Reference in a new issue