{"version":3,"sources":["webpack:///./app/javascript/shared/components/useKeyboardShortcuts.js","webpack:///./app/javascript/utilities/http/request.js","webpack:///./app/javascript/utilities/search/index.js","webpack:///./app/javascript/Search/SearchForm.jsx","webpack:///./app/javascript/Search/Search.jsx","webpack:///./app/javascript/Search/SearchFormSync.jsx","webpack:///./app/javascript/packs/Search.jsx","webpack:///./app/javascript/utilities/http/errors.js"],"names":["isFormField","element","HTMLElement","name","nodeName","toLowerCase","type","getAttribute","isContentEditable","callShortcut","e","keys","chain","shortcuts","shortcut","length","join","code","key","defaultOptions","timeout","useKeyboardShortcuts","eventTarget","window","options","useState","storedShortcuts","keyChain","setKeyChain","mergedOptions","setMergedOptions","useEffect","newOptions","setTimeout","clearTimeout","Object","keyEvent","defaultPrevented","ctrlKey","metaKey","altKey","shiftKey","target","Node","newChain","addEventListener","removeEventListener","KeyboardShortcuts","propTypes","PropTypes","object","isRequired","shape","number","instanceOf","Element","defaultProps","request","url","headers","body","method","csrfToken","getCsrfToken","restOfOptions","jsonifiedBody","JSON","stringify","fetchOptions","Accept","credentials","fetch","getParameters","params","location","href","sanitizedName","replace","results","RegExp","exec","decodeURIComponent","getParameterByName","getFilterParameters","hasInstantClick","instantClick","fixedEncodeURIComponent","value","encodeURIComponent","c","charCodeAt","toString","displaySearchResults","searchTerm","baseUrl","origin","sanitizedQuery","filterParameters","sortParameters","InstantClick","display","getSearchTermFromUrl","querystring","searchParameters","URLSearchParams","query","filterXSS","get","divForDecode","document","createElement","innerHTML","firstChild","nodeValue","preloadSearchResults","encodedQuery","searchUrl","preload","fetchSearch","endpoint","dataHash","searchParams","forEach","Array","isArray","arrayValue","append","createSearchUrl","then","response","json","SearchForm","forwardRef","ref","onSearch","onSubmitSearch","action","role","acceptCharset","onSubmit","className","placeholder","autoComplete","onKeyDown","string","func","Search","props","enableSearchPageChecker","event","preventDefault","classList","toggle","remove","searchBox","searchInputRef","current","focus","select","syncSearchUrlWithInput","bind","createRef","this","setSearchTerm","searchPageChecker","search","on","enableSearchPageListener","currentSearchTerm","globalKeysListener","off","focusOnSearchBox","minimizeHeader","submit","Component","SearchFormSync","mobileSearchContainer","setMobileSearchContainer","syncSearchFormsListener","detail","updatedSearchTerm","getElementById","unmountComponentAtNode","form","querySelector","removeChild","createPortal","root","render","handleFetchAPIErrors","ok","data","Error","error","SyntaxError","statusText"],"mappings":"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","file":"js/Search-803ebae71d31c2f27f0c.chunk.js","sourcesContent":["import { useState, useEffect } from 'preact/hooks';\nimport PropTypes from 'prop-types';\n\n/**\n * Checker that return true if element is a form element\n *\n * @param {node} element to be checked\n *\n * @returns {boolean} isFormField\n */\nfunction isFormField(element) {\n if (element instanceof HTMLElement === false) return false;\n\n const name = element.nodeName.toLowerCase();\n const type = (element.getAttribute('type') || '').toLowerCase();\n return (\n name === 'select' ||\n name === 'textarea' ||\n (name === 'input' &&\n type !== 'submit' &&\n type !== 'reset' &&\n type !== 'checkbox' &&\n type !== 'radio') ||\n element.isContentEditable\n );\n}\n\n/**\n * Function to handle converting key presses to callback functions\n *\n * @param {KeyboardEvent} e Keyboard event\n * @param {String} keys special keys formatted in a string\n * @param {Array} chain array of past keys\n * @param {Object} shortcuts object containing callback functions\n *\n * @returns {Array} New chain\n */\nconst callShortcut = (e, keys, chain, shortcuts) => {\n const shortcut =\n chain && chain.length > 0\n ? shortcuts[`${chain.join('~')}~${e.code}`]\n : shortcuts[`${keys}${e.code}`] ||\n shortcuts[`${keys}${e.key.toLowerCase()}`];\n\n // if a valid shortcut is found call it and reset the chain\n if (shortcut) {\n shortcut(e);\n return [];\n }\n\n // if we have keys don't add to the chain\n if (keys || e.key === 'Shift') {\n return [];\n }\n\n return [...chain, e.code];\n};\n\n// Default options to be used if null\nconst defaultOptions = {\n timeout: 0, // The default is zero as we want no delays between keystrokes by default.\n};\n\n/**\n * hook that can be added to a component to listen\n * for keyboard presses\n *\n * @example\n * const shortcuts = {\n * 'ctrl+alt+KeyG': (e) => {\n * e.preventDefault();\n * alert('Control Alt G has been pressed');\n * },\n * 'KeyG~KeyH': (e) => {\n * e.preventDefault();\n * alert('G has been pressed quickly followed by H');\n * },\n * '?': (e) => {\n * setIsHelpVisible(true);\n * }\n * }\n *\n * useKeyboardShortcuts(shortcuts, someElementOrWindowObject, {timeout: 1500});\n *\n * @param {object} shortcuts List of keyboard shortcuts/event\n * @param {EventTarget} [eventTarget=window] An event target.\n * @param {object} [options = {}] An object for extra options\n *\n */\nexport function useKeyboardShortcuts(\n shortcuts,\n eventTarget = window,\n options = {},\n) {\n const [storedShortcuts] = useState(shortcuts);\n const [keyChain, setKeyChain] = useState([]);\n const [mergedOptions, setMergedOptions] = useState({\n ...defaultOptions,\n ...options,\n });\n\n // update mergedOptions if options prop changes\n useEffect(() => {\n const newOptions = {};\n if (typeof options.timeout === 'number')\n newOptions.timeout = options.timeout;\n setMergedOptions({ ...defaultOptions, ...newOptions });\n }, [options.timeout]);\n\n // clear key chain after timeout is reached\n useEffect(() => {\n if (keyChain.length <= 0) return;\n\n const timeout = window.setTimeout(() => {\n clearTimeout(timeout);\n setKeyChain([]);\n }, mergedOptions.timeout);\n\n return () => clearTimeout(timeout);\n }, [keyChain.length, mergedOptions.timeout]);\n\n // set up event listeners\n useEffect(() => {\n if (!storedShortcuts || Object.keys(storedShortcuts).length === 0) return;\n\n const keyEvent = (e) => {\n if (e.defaultPrevented) return;\n\n // Get special keys\n const keys = `${e.ctrlKey || e.metaKey ? 'ctrl+' : ''}${\n e.altKey ? 'alt+' : ''\n }${(e.ctrlKey || e.metaKey || e.altKey) && e.shiftKey ? 'shift+' : ''}`;\n\n // If no special keys, except shift, are pressed and focus is inside a field return\n if (e.target instanceof Node && isFormField(e.target) && !keys) return;\n\n const newChain = callShortcut(e, keys, keyChain, storedShortcuts);\n\n // update keychain with latest chain\n setKeyChain(newChain);\n };\n\n eventTarget.addEventListener('keydown', keyEvent);\n\n return () => eventTarget.removeEventListener('keydown', keyEvent);\n }, [keyChain, storedShortcuts, eventTarget]);\n}\n\n/**\n * A component that can be added to a component to listen\n * for keyboard presses using the useKeyboardShortcuts hook\n *\n * @example\n * const shortcuts = {\n * 'ctrl+alt+KeyG': (e) => {\n * e.preventDefault();\n * alert('Control Alt G has been pressed')\n * }\n * }\n *\n * \n * \n *\n * @param {object} shortcuts List of keyboard shortcuts/event\n * @param {EventTarget} [eventTarget=window] An event target.\n * @param {object} [options = {}] An object for extra options\n *\n */\nexport function KeyboardShortcuts({ shortcuts, eventTarget, options }) {\n useKeyboardShortcuts(shortcuts, eventTarget, options);\n\n return null;\n}\n\nKeyboardShortcuts.propTypes = {\n shortcuts: PropTypes.object.isRequired,\n options: PropTypes.shape({\n timeout: PropTypes.number,\n }),\n eventTarget: PropTypes.instanceOf(Element),\n};\n\nKeyboardShortcuts.defaultProps = {\n shortcuts: {},\n options: {},\n eventTarget: window,\n};\n","/**\n * Generic request with all the default headers required by the application.\n *\n * @example\n * import { request } from '@utilities/http';\n *\n * const response = await request('/notification_subscriptions/Article/26')\n *\n * Note:\n * The body option will typically be passed in as a JavaScript object.\n * A check is performed for this and automatically convert it to JSON if necessary.\n *\n * Requests send JSON by default but this can be easily overridden by adding\n * the Accept and Content-Type headers to the request options.\n *\n * The default method is GET.\n *\n * @param {string} url The URL to make the request to.\n * @param {RequestInit} [options={}] The request options.\n *\n * @return {Promise} the response\n */\nexport async function request(url, options = {}) {\n const {\n headers,\n body,\n method = 'GET',\n csrfToken = await getCsrfToken(),\n // These are any other options that might be passed in e.g. keepalive\n ...restOfOptions\n } = options;\n\n // There should never be a scenario where null is passed as the body,\n // but if ever there is, this logic should change.\n const jsonifiedBody = {\n body: body && typeof body !== 'string' ? JSON.stringify(body) : body,\n };\n\n const fetchOptions = {\n method,\n headers: {\n Accept: 'application/json',\n 'X-CSRF-Token': csrfToken,\n 'Content-Type': 'application/json',\n ...headers,\n },\n credentials: 'same-origin',\n ...jsonifiedBody,\n ...restOfOptions,\n };\n\n return fetch(url, fetchOptions);\n}\n","// TODO: We should really be using the xss package by installing it in package.json\n// but for now filterXSS is global because of legacy JS\n\nimport { request } from '@utilities/http';\n\nfunction getParameterByName(name, url = window.location.href) {\n const sanitizedName = name.replace(/[[\\]]/g, '\\\\$&');\n const regex = new RegExp(`[?&]${sanitizedName}(=([^&#]*)|&|#|$)`);\n const results = regex.exec(url);\n\n if (!results) {\n return null;\n }\n\n if (!results[2]) {\n return '';\n }\n\n return decodeURIComponent(results[2].replace(/\\+/g, ' '));\n}\n\nfunction getParameters(name, url) {\n const params = getParameterByName(name, url);\n\n if (params) {\n return `&${name}=${params}`;\n }\n\n return '';\n}\n\nfunction getFilterParameters(url) {\n return getParameters('filters', url);\n}\n\nfunction getSortParameters(url) {\n const sortBy = getParameters('sort_by', url);\n const sortDirection = getParameters('sort_direction', url);\n\n return sortBy + sortDirection;\n}\n\n/**\n * Determines whether or not InstantClick is enabled.\n *\n * @returns True if InstantClick is enabled, otherwise false.\n */\nexport function hasInstantClick() {\n return typeof instantClick !== 'undefined';\n}\n\nfunction fixedEncodeURIComponent(value) {\n // from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent\n return encodeURIComponent(value).replace(\n /[!'()*]/g,\n (c) => `%${c.charCodeAt(0).toString(16)}`,\n );\n}\n\nfunction createSearchUrl(dataHash) {\n const searchParams = new URLSearchParams();\n Object.keys(dataHash).forEach((key) => {\n const value = dataHash[key];\n if (Array.isArray(value)) {\n value.forEach((arrayValue) => {\n searchParams.append(`${key}[]`, arrayValue);\n });\n } else {\n searchParams.append(key, value);\n }\n });\n\n return searchParams.toString();\n}\n\n/**\n *\n * @param {*} param0\n */\nexport function displaySearchResults({\n searchTerm,\n location = window.location,\n}) {\n const baseUrl = location.origin;\n const sanitizedQuery = fixedEncodeURIComponent(searchTerm);\n const filterParameters = getFilterParameters(location.href);\n const sortParameters = getSortParameters(location.href);\n\n InstantClick.display(\n `${baseUrl}/search?q=${sanitizedQuery}${filterParameters}${sortParameters}`,\n );\n}\n\n/**\n * Extracts the search term from an URL's query string.\n *\n * @param {string} querystring A URL query string\n *\n * @returns The extracted search term from the query string\n */\nexport function getSearchTermFromUrl(querystring) {\n const searchParameters = new URLSearchParams(querystring);\n const query = filterXSS(searchParameters.get('q')) ?? '';\n const divForDecode = document.createElement('div');\n\n divForDecode.innerHTML = query;\n\n return divForDecode.firstChild !== null\n ? divForDecode.firstChild.nodeValue\n : query;\n}\n\n/**\n * Preloads search results for the given search term\n * @param {string} searchTerm The search term\n * @param {Location} location[window.location] The location (URL) of the object it is linked to.\n * By default it is linked to the Window object.\n */\nexport function preloadSearchResults({\n searchTerm,\n location = window.location,\n}) {\n const encodedQuery = fixedEncodeURIComponent(\n searchTerm.replace(/^[ ]+|[ ]+$/g, ''),\n );\n const searchUrl = `${\n location.origin\n }/search?q=${encodedQuery}${getFilterParameters(location.href)}`;\n InstantClick.preload(searchUrl);\n}\n\n/**\n * A helper method to call /search endpoints.\n *\n * @param {string} endpoint - The search endpoint you want to request (i.e. tags).\n * @param {object} dataHash - A hash with the search params that need to be included in the request.\n *\n * @returns {Promise} A promise object with response formatted as JSON.\n */\nexport function fetchSearch(endpoint, dataHash) {\n const searchUrl = createSearchUrl(dataHash);\n\n return request(`/search/${endpoint}?${searchUrl}`).then((response) =>\n response.json(),\n );\n}\n","import PropTypes from 'prop-types';\nimport { h } from 'preact';\nimport { forwardRef } from 'preact/compat';\nimport i18next from 'i18next';\n\nexport const SearchForm = forwardRef(\n ({ searchTerm, onSearch, onSubmitSearch }, ref) => (\n \n \n \n \n ),\n);\n\nSearchForm.propTypes = {\n searchTerm: PropTypes.string.isRequired,\n onSearch: PropTypes.func.isRequired,\n onSubmitSearch: PropTypes.func.isRequired,\n};\n","import { h, Component, Fragment, createRef } from 'preact';\nimport PropTypes from 'prop-types';\nimport {\n displaySearchResults,\n getSearchTermFromUrl,\n hasInstantClick,\n preloadSearchResults,\n} from '../utilities/search';\nimport { KeyboardShortcuts } from '../shared/components/useKeyboardShortcuts';\nimport { SearchForm } from './SearchForm';\n\nconst GLOBAL_MINIMIZE_KEY = 'Digit0';\nconst GLOBAL_SEARCH_KEY = '/';\nconst ENTER_KEY = 'Enter';\n\nexport class Search extends Component {\n constructor(props) {\n super(props);\n this.enableSearchPageChecker = true;\n this.syncSearchUrlWithInput = this.syncSearchUrlWithInput.bind(this);\n this.searchInputRef = createRef(null);\n }\n\n componentWillMount() {\n const { searchTerm, setSearchTerm } = this.props;\n\n const searchPageChecker = () => {\n if (\n this.enableSearchPageChecker &&\n searchTerm !== '' &&\n /^http(s)?:\\/\\/[^/]+\\/search/.exec(window.location.href) === null\n ) {\n setSearchTerm('');\n }\n\n setTimeout(searchPageChecker, 500);\n };\n\n searchPageChecker();\n }\n\n /**\n * Synchronizes the search input value with the search term defined in the URL.\n */\n syncSearchUrlWithInput() {\n // TODO: Consolidate search functionality.\n // Note that push states for search occur in _search.html.erb\n // in initializeSortingTabs(query)\n const { setSearchTerm } = this.props;\n const searchTerm = getSearchTermFromUrl(window.location.search);\n\n // We set the value outside of React state so that there is no flickering of placeholder\n // to search term.\n const searchBox = this.searchInputRef.current;\n searchBox.value = searchTerm;\n\n // Even though we set the search term directly via the DOM, it still needs to reside\n // in component state.\n setSearchTerm(searchTerm);\n }\n\n componentDidMount() {\n InstantClick.on('change', this.enableSearchPageListener);\n\n window.addEventListener('popstate', this.syncSearchUrlWithInput);\n }\n\n enableSearchPageListener = () => {\n this.enableSearchPageChecker = true;\n };\n\n hasKeyModifiers = (event) => {\n return event.altKey || event.ctrlKey || event.metaKey || event.shiftKey;\n };\n\n submit = (event) => {\n if (hasInstantClick) {\n event.preventDefault();\n }\n };\n\n search(key, searchTerm) {\n const { searchTerm: currentSearchTerm } = this.props;\n this.enableSearchPageChecker = false;\n\n if (\n hasInstantClick() &&\n key === ENTER_KEY &&\n currentSearchTerm !== searchTerm\n ) {\n const { setSearchTerm } = this.props;\n\n setSearchTerm(searchTerm);\n preloadSearchResults({ searchTerm });\n displaySearchResults({ searchTerm });\n }\n }\n\n componentWillUnmount() {\n document.removeEventListener('keydown', this.globalKeysListener);\n window.removeEventListener('popstate', this.syncSearchUrlWithInput);\n InstantClick.off &&\n InstantClick.off('change', this.enableSearchPageListener);\n }\n\n minimizeHeader = (event) => {\n event.preventDefault();\n document.body.classList.toggle('zen-mode');\n };\n\n focusOnSearchBox = (event) => {\n event.preventDefault();\n document.body.classList.remove('zen-mode');\n\n const searchBox = this.searchInputRef.current;\n searchBox.focus();\n searchBox.select();\n };\n\n render({ searchTerm }) {\n return (\n \n \n {\n const {\n key,\n target: { value },\n } = event;\n this.search(key, value);\n }}\n onSubmitSearch={this.submit}\n ref={this.searchInputRef}\n />\n \n );\n }\n}\n\nSearch.propTypes = {\n searchTerm: PropTypes.string.isRequired,\n setSearchTerm: PropTypes.func.isRequired,\n};\n","import { h } from 'preact';\nimport { useEffect, useState } from 'preact/hooks';\nimport { createPortal, Fragment, unmountComponentAtNode } from 'preact/compat';\nimport { Search } from './Search';\nimport { getSearchTermFromUrl } from '@utilities/search';\n\n/**\n * Manages the synchronization of search state between the top search bar (desktop) and\n * mobile (in search results page).\n */\nexport function SearchFormSync() {\n const [searchTerm, setSearchTerm] = useState(() => {\n return getSearchTermFromUrl(location.search);\n });\n const [mobileSearchContainer, setMobileSearchContainer] = useState(null);\n\n /**\n * A listener for handling the synchronization of search forms.\n *\n * @param {CustomEvent<{ querystring: string }>} event A custom event for synching search forms.\n */\n function syncSearchFormsListener(event) {\n const { querystring } = event.detail;\n const updatedSearchTerm = getSearchTermFromUrl(querystring);\n\n // Server-side rendering of search results means the DOM node is destroyed everytime a search is performed,\n // So we need to get the reference every time and use that for the parent in the portal.\n const element = document.getElementById('mobile-search-container');\n\n // The DOM element has changed because server-sde rendering returns new\n // search results which destroys the existing search form in mobile view.\n // Because of this we need to unmount the component at the old element reference\n // i.e. the container for the createPortal call in the render.\n // If we do not unmount, it will result in an unmounting error that will throw as the\n // container element (search form that was wiped out because of the new search results) no longer exists.\n if (mobileSearchContainer && element !== mobileSearchContainer) {\n unmountComponentAtNode(mobileSearchContainer);\n }\n\n // We need to delete the existing server-side rendered form because createPortal only appends to it's container.\n if (element) {\n const form = element.querySelector('form');\n form && element.removeChild(form);\n }\n\n setMobileSearchContainer(element);\n setSearchTerm(updatedSearchTerm);\n }\n\n useEffect(() => {\n window.addEventListener('syncSearchForms', syncSearchFormsListener);\n\n return () => {\n window.removeEventListener('syncSearchForms', syncSearchFormsListener);\n };\n });\n\n return (\n \n \n {mobileSearchContainer &&\n createPortal(\n ,\n mobileSearchContainer,\n )}\n \n );\n}\n","import { h, render } from 'preact';\nimport 'focus-visible';\nimport { SearchFormSync } from '../Search/SearchFormSync';\n\ndocument.addEventListener('DOMContentLoaded', () => {\n const root = document.getElementById('header-search');\n\n render(, root);\n});\n","// eslint-disable-next-line consistent-return\nexport function handleFetchAPIErrors(response) {\n // pass along a correct response\n if (response.ok) {\n return response;\n }\n\n // API errors contain the error message in {\"error\": \"error message\"}\n // but they could be unhandled 500 errors\n try {\n response.json().then((data) => {\n throw new Error(data.error);\n });\n } catch (e) {\n if (e instanceof SyntaxError) {\n throw new Error(response.statusText);\n } else {\n throw e;\n }\n }\n}\n"],"sourceRoot":""}