UNPKG

addsearch-search-ui

Version:

JavaScript library to develop Search UIs for the web

291 lines (251 loc) 8.01 kB
/* global window, history */ import { WARMUP_QUERY_PREFIX, MATCH_ALL_QUERY } from '../index'; import { search } from '../actions/search'; import { setKeyword } from '../actions/keyword'; import { setPage } from '../actions/pagination'; import {setActiveFilters, setActiveFacets, setActiveRangeFacets} from '../actions/filters'; import {sortBy} from "../actions/sortby"; export const HISTORY_PARAMETERS = { SEARCH: 'search', FILTERS: 'search_filters', FACETS: 'search_facets', PAGE: 'search_page', SORTBY: 'search_sort', RANGE_FACETS: 'range_facets' }; const SET_HISTORY_DEBOUNCE_TIME = 1500; let setHistoryDebounceTimeout = null; // Set history right away or after a debounce delay export function setHistory(parameter, value, debounce, store) { const state = store.getState(); if (state && state.configuration && state.configuration.updateBrowserHistory === false) { return; } // Debounce for search-as-you-type if (debounce) { if (setHistoryDebounceTimeout) { clearTimeout(setHistoryDebounceTimeout); } setHistoryDebounceTimeout = setTimeout(() => { doSetHistory(parameter, value); }, SET_HISTORY_DEBOUNCE_TIME); } // No debounce else { doSetHistory(parameter, value); } } // Set the actual history state function doSetHistory(parameter, value) { // ignore warmup search query if (parameter === HISTORY_PARAMETERS.SEARCH && value && value.indexOf(WARMUP_QUERY_PREFIX) === 0) { return; } let url = window.location.href; if (url.indexOf('#') !== -1) { url = url.substring(0, url.indexOf('#')); } const hash = window.location.hash || ''; const params = queryParamsToObject(url); // If pagination parameter and page=1, don't add to URL if (parameter === HISTORY_PARAMETERS.PAGE && value == 1) { delete params[parameter]; } // If search=*, don't add to URL else if (parameter === HISTORY_PARAMETERS.SEARCH && value == MATCH_ALL_QUERY) { delete params[parameter]; } // Add value to URL if an value exists and it's not empty else if (parameter && value !== null && value !== '') { params[parameter] = value; } // No value. Delete query parameter else if (parameter) { delete params[parameter]; } let stateUrl = url; if (url.indexOf('?') !== -1) { stateUrl = url.substring(0, url.indexOf('?')); } if (JSON.stringify(params) !== JSON.stringify({})) { stateUrl = stateUrl + '?' + objectToQueryParams(params); } // Put hash back to URL stateUrl = stateUrl + hash; // Firt time called if (history.state === null) { history.replaceState(params, '', stateUrl); } // Update history if it has changed else if (JSON.stringify(history.state) !== JSON.stringify(params)) { history.pushState(params, '', stateUrl); } } export function getQueryParam(url, param) { const name = param.replace(/[\[\]]/g, "\\$&"); const regex = new RegExp("[?&]" + name + "(=([^&#]*)|&|#|$)"); const results = regex.exec(url); if (!results) return null; if (!results[2]) return ''; return decodeURIComponent(results[2].replace(/\+/g, " ")); } export function initFromURL(client, reduxStore, createFilterObjectFunction, searchFunction, hasMatchAllQuery, baseFilters) { // Initial load const url = window.location.href; const qs = queryParamsToObject(url); handleURLParams(client, reduxStore, qs, createFilterObjectFunction, searchFunction, false, baseFilters); // Browser back button. Re-handle URL window.onpopstate = (e) => { const qs = queryParamsToObject(window.location.href); handleURLParams(client, reduxStore, qs, createFilterObjectFunction, searchFunction, hasMatchAllQuery, baseFilters); } } function handleURLParams(client, store, qs, createFilterObjectFunction, searchFunction, hasMatchAllQuery, baseFilters) { let hasFacetsOrFilters = false; if (qs[HISTORY_PARAMETERS.FILTERS]) { // Take active filters from URL const filtersJson = urlParamToJSON(qs[HISTORY_PARAMETERS.FILTERS]); store.dispatch(setActiveFilters(filtersJson)); hasFacetsOrFilters = true; } if (qs[HISTORY_PARAMETERS.FACETS]) { // Take active facets from URL const facetsJson = urlParamToJSON(qs[HISTORY_PARAMETERS.FACETS]); store.dispatch(setActiveFacets(facetsJson)); hasFacetsOrFilters = true; } if (qs[HISTORY_PARAMETERS.RANGE_FACETS]) { const rangeFacetsJson = urlParamToJSON(qs[HISTORY_PARAMETERS.RANGE_FACETS]); store.dispatch(setActiveRangeFacets(rangeFacetsJson)); } // Has facets or filters. Update client state if (hasFacetsOrFilters) { const filterState = store.getState().filters; const filterObject = createFilterObjectFunction(filterState, baseFilters); client.setFilterObject(filterObject); } // No facets or filters else { store.dispatch(setActiveFilters(null)); store.dispatch(setActiveFacets(null)); client.setFilterObject(baseFilters || null); } // Has sort method if (qs[HISTORY_PARAMETERS.SORTBY]) { // Take sort method from URL const sortMethod = urlParamToJSON(qs[HISTORY_PARAMETERS.SORTBY]); store.dispatch(sortBy(client, sortMethod.field, sortMethod.order)); } if (qs[HISTORY_PARAMETERS.PAGE]) { store.dispatch(setPage(client, parseInt(qs[HISTORY_PARAMETERS.PAGE], 10), null, store)); } else { store.dispatch(setPage(client, 1, null, store)); } if (qs[HISTORY_PARAMETERS.SEARCH]) { const keyword = decodeURIComponent(qs[HISTORY_PARAMETERS.SEARCH]); store.dispatch(setKeyword(keyword, true, null, true)); searchFunction(keyword); } else if (hasMatchAllQuery === true) { store.dispatch(setKeyword(MATCH_ALL_QUERY, true, null, true)); searchFunction(MATCH_ALL_QUERY); } } /** * Pick up query parameters from an URL and return them as a JSON object * * Example: ?foo=bar&baz=x returns * { foo: 'bar', baz: 'x' } */ export function queryParamsToObject(url) { if (url.indexOf('?') === -1) { return {}; } let qs = url.substring(url.indexOf('?') + 1); if (qs === '') { return {}; } if (qs.indexOf('#') !== -1) { qs = qs.substring(0, qs.indexOf('#')); } let obj = {}; const qsArr = qs.split('&'); qsArr.forEach(v => { const kv = v.split('='); if (kv[0] && kv[0].length > 0) { let value = null; if (kv.length > 1) { value = kv[1] || ''; value = value.replace(/\+/g, '%20'); value = decodeURIComponent(value); } obj[kv[0]] = value; } }); return obj; } /** * Example: { foo: 'bar', baz: 'x' } returns * foo=bar&baz=x */ export function objectToQueryParams(obj) { let qs = ''; for (let key in obj) { if (obj.hasOwnProperty(key)) { if (qs !== '') { qs = qs + '&'; } let value = ''; if (obj[key] !== null && obj[key] !== undefined) { value = '=' + encodeURIComponent(obj[key]); } qs = qs + key + value; } } return qs; } /** * Transfer a URL parameter to a JSON object */ export function urlParamToJSON(urlParameter) { try { return JSON.parse(urlParameter); } catch(error) {} return null; } /** * Transfer JSON object to a string suitable for URL param */ export function jsonToUrlParam(json) { if (Object.keys(json).length > 0) { delete json['v']; return JSON.stringify(json); } return null; } export function rangeFacetsJsonToUrlParam(json) { const fieldKeysList = {}; for (let field in json) { if (field !== 'v') { fieldKeysList[field] = []; for (let key in json[field]) { fieldKeysList[field].push(key); } } } return JSON.stringify(fieldKeysList); } /** * Redirect to search results page */ export function redirectToSearchResultsPage(url, keyword) { if (url.indexOf('?') === -1) { window.location.href = url + '?' + HISTORY_PARAMETERS.SEARCH + '=' + encodeURIComponent(keyword); } else { window.location.href = url + '&' + HISTORY_PARAMETERS.SEARCH + '=' + encodeURIComponent(keyword); } }