docbrown/app/javascript/utilities/search/index.js
Nick Taylor df31a94f2e
Prep work to sync mobile search in #10424 (#11525)
* Prep work to sync mobile search in #10424

* Added a comment about the custom search event.

* Refactor to fix already defined error caused by const and InstantClick.

* Moved <SearchFormSync /> out of the pack file.

* Fixed broken tests.

* Renamed getInitialSearchTerm utility function to getSearchTermFromUrl

* Added some API docs to getSearchTermFromUrl

* Fixed a typo.

* Added some more API docs.

* Switched to a custom event so a custom payload can be passed.

* Added some API docs.

* Fixed bad destructuring statement.

* wip for some more tests.

* Refactored getSearchTermFromUrl to use URLSearchParams

* lazy loaded state for search term initial value.

* test broken still. Need to think on it.

* filterXSS is now set up in testSetup.js

* Fixed tests.

* Added the @utilities alias to Storybooks webpack config.

* Almost 100% coverage with two useful tests.

* Added some comments explaining document.body usage in the tests.

* Fixed issue where search attemps to get results on first load.

* Small refactor for setting the window.location in tests.

* Clarified an explanation in a comment.
2020-11-30 06:13:06 -05:00

146 lines
3.8 KiB
JavaScript

// TODO: We should really be using the xss package by installing it in package.json
// but for now filterXSS is global because of legacy JS
import { request } from '@utilities/http';
function getParameterByName(name, url = window.location.href) {
const sanitizedName = name.replace(/[[\]]/g, '\\$&');
const regex = new RegExp(`[?&]${sanitizedName}(=([^&#]*)|&|#|$)`);
const results = regex.exec(url);
if (!results) {
return null;
}
if (!results[2]) {
return '';
}
return decodeURIComponent(results[2].replace(/\+/g, ' '));
}
function getParameters(name, url) {
const params = getParameterByName(name, url);
if (params) {
return `&${name}=${params}`;
}
return '';
}
function getFilterParameters(url) {
return getParameters('filters', url);
}
function getSortParameters(url) {
const sortBy = getParameters('sort_by', url);
const sortDirection = getParameters('sort_direction', url);
return sortBy + sortDirection;
}
/**
* Determines whether or not InstantClick is enabled.
*
* @returns True if InstantClick is enabled, otherwise false.
*/
export function hasInstantClick() {
return typeof instantClick !== 'undefined';
}
function fixedEncodeURIComponent(value) {
// from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent
return encodeURIComponent(value).replace(
/[!'()*]/g,
(c) => `%${c.charCodeAt(0).toString(16)}`,
);
}
function createSearchUrl(dataHash) {
const searchParams = new URLSearchParams();
Object.keys(dataHash).forEach((key) => {
const value = dataHash[key];
if (Array.isArray(value)) {
value.forEach((arrayValue) => {
searchParams.append(`${key}[]`, arrayValue);
});
} else {
searchParams.append(key, value);
}
});
return searchParams.toString();
}
/**
*
* @param {*} param0
*/
export function displaySearchResults({
searchTerm,
location = window.location,
}) {
const baseUrl = location.origin;
const sanitizedQuery = fixedEncodeURIComponent(searchTerm);
const filterParameters = getFilterParameters(location.href);
const sortParameters = getSortParameters(location.href);
InstantClick.display(
`${baseUrl}/search?q=${sanitizedQuery}${filterParameters}${sortParameters}`,
);
}
/**
* Extracts the search term from an URL's query string.
*
* @param {string} querystring A URL query string
*
* @returns The extracted search term from the query string
*/
export function getSearchTermFromUrl(querystring) {
const searchParameters = new URLSearchParams(querystring);
const query = filterXSS(searchParameters.get('q')) ?? '';
const divForDecode = document.createElement('div');
divForDecode.innerHTML = query;
return divForDecode.firstChild !== null
? divForDecode.firstChild.nodeValue
: query;
}
/**
* Preloads search results for the given search term
* @param {string} searchTerm The search term
* @param {Location} location[window.location] The location (URL) of the object it is linked to.
* By default it is linked to the Window object.
*/
export function preloadSearchResults({
searchTerm,
location = window.location,
}) {
const encodedQuery = fixedEncodeURIComponent(
searchTerm.replace(/^[ ]+|[ ]+$/g, ''),
);
const searchUrl = `${
location.origin
}/search?q=${encodedQuery}${getFilterParameters(location.href)}`;
InstantClick.preload(searchUrl);
}
/**
* A helper method to call /search endpoints.
*
* @param {string} endpoint - The search endpoint you want to request (i.e. tags).
* @param {object} dataHash - A hash with the search params that need to be included in the request.
*
* @returns {Promise} A promise object with response formatted as JSON.
*/
export function fetchSearch(endpoint, dataHash) {
const searchUrl = createSearchUrl(dataHash);
return request(`/search/${endpoint}?${searchUrl}`).then((response) =>
response.json(),
);
}