From 38ba1601e5614d102827c1536f3a2d51907fd1e4 Mon Sep 17 00:00:00 2001
From: saihaj <44710980+saihaj@users.noreply.github.com>
Date: Thu, 16 Jul 2020 17:57:27 -0500
Subject: [PATCH] refactor(frontend/controller): split up the search component
Split search component so that results are their own component
---
app/frontend/src/Controller/Search.js | 408 ------------------
app/frontend/src/Controller/Search/Results.js | 233 ++++++++++
.../{Search.css => Search/index.css} | 0
app/frontend/src/Controller/Search/index.js | 227 ++++++++++
4 files changed, 460 insertions(+), 408 deletions(-)
delete mode 100644 app/frontend/src/Controller/Search.js
create mode 100644 app/frontend/src/Controller/Search/Results.js
rename app/frontend/src/Controller/{Search.css => Search/index.css} (100%)
create mode 100644 app/frontend/src/Controller/Search/index.js
diff --git a/app/frontend/src/Controller/Search.js b/app/frontend/src/Controller/Search.js
deleted file mode 100644
index 6c753b0e..00000000
--- a/app/frontend/src/Controller/Search.js
+++ /dev/null
@@ -1,408 +0,0 @@
-import React, { useRef, useState, useEffect, useCallback, useContext } from 'react'
-import { func, string, oneOfType, number, instanceOf, shape } from 'prop-types'
-import { useLocation, useHistory } from 'react-router-dom'
-import classNames from 'classnames'
-
-import {
- Input,
- InputAdornment,
- List,
- ListItem,
- IconButton,
-} from '@material-ui/core'
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
-import { faTimes } from '@fortawesome/free-solid-svg-icons'
-
-import { stringify } from 'querystring'
-import { firstLetters, stripVishraams, stripAccents, toUnicode, toAscii } from 'gurmukhi-utils'
-
-import {
- SEARCH_TYPES,
- SEARCH_CHARS,
- LANGUAGE_NAMES,
- SEARCH_ANCHORS,
- MIN_SEARCH_CHARS,
- SOURCE_ABBREVIATIONS,
-} from '../lib/consts'
-import {
- getUrlState,
- getTranslation,
- getTransliteration,
- customiseLine,
-} from '../lib/utils'
-import { WritersContext, RecommendedSourcesContext, SettingsContext } from '../lib/contexts'
-import controller from '../lib/controller'
-
-import { withNavigationHotkeys } from '../shared/NavigationHotkeys'
-
-import './Search.css'
-
-// Generate the regex for capturing anchor chars, optionally
-const searchRegex = new RegExp( `^([${Object.keys( SEARCH_ANCHORS ).map( anchor => `\\${anchor}` ).join( '' )}])?(.*)` )
-
-const getSearchParams = searchQuery => {
- // Extract anchors and search query
- const [ , anchor, query ] = searchQuery.match( searchRegex )
-
- const inputValue = query
-
- // Get search type from anchor char, if any
- const type = SEARCH_ANCHORS[ anchor ] || SEARCH_TYPES.firstLetter
-
- const value = type === SEARCH_TYPES.firstLetter
- ? inputValue.slice().replace( new RegExp( SEARCH_CHARS.wildcard, 'g' ), '_' )
- : inputValue
-
- return { anchor, value, type }
-}
-
-const highlightFullWordMatches = ( line, query ) => {
- const sanitisedQuery = query.trim()
-
- const foundPosition = line.search( sanitisedQuery )
- const matchStartPosition = line.lastIndexOf( ' ', foundPosition )
-
- const wordEndPosition = line.indexOf( ' ', foundPosition + sanitisedQuery.length )
- // If the match finishes in the last word, no space will be deteced, and wordEndPosition
- // will be -1. In this case, we want to end at the last position in the line.
- const matchEndPosition = wordEndPosition === -1 ? line.length - 1 : wordEndPosition
-
- return [
- line.substring( 0, matchStartPosition ),
- line.substring( matchStartPosition, matchEndPosition ),
- line.substring( matchEndPosition ),
- ]
-}
-
-const highlightFirstLetterMatches = ( line, query ) => {
- const baseLine = stripVishraams( line )
-
- const letters = toAscii( firstLetters( stripAccents( toUnicode( baseLine ) ) ) )
- const words = baseLine.split( ' ' )
-
- const startPosition = letters.search( stripAccents( query ) )
- const endPosition = startPosition + query.length
-
- return [
- `${words.slice( 0, startPosition ).join( ' ' )} `,
- `${words.slice( startPosition, endPosition ).join( ' ' )} `,
- `${words.slice( endPosition ).join( ' ' )} `,
- ]
-}
-
-/**
- * Separates the line into words before the first match, the first match, and after the match.
- * @param value The full line.
- * @param input The string inputted by the user.
- * @param mode The type of search being performed, either first word or full word.
- * @return An array of [ beforeMatch, match, afterMatch ],
- * with `match` being the highlighted section.`.
- */
-const highlightMatches = gurmukhi => ( value, input, mode ) => {
- if ( !value ) return [ '', '', '' ]
-
- // Account for wildcard characters
- const sanitizedInput = input.replace( new RegExp( '_', 'g' ), '.' )
-
- return mode === SEARCH_TYPES.fullWord
- ? highlightFullWordMatches( gurmukhi, sanitizedInput )
- : highlightFirstLetterMatches( value, sanitizedInput )
-}
-
-/**
- * Search Component.
- * Converts ASCII to unicode on input.
- * Displays results.
- */
-const Search = ( { updateFocus, register, focused } ) => {
- const { local: {
- sources,
- search: {
- showResultCitations,
- resultTransliterationLanguage,
- resultTranslationLanguage,
- lineEnding,
- },
- } = {} } = useContext( SettingsContext )
-
- // Set the initial search query from URL
- const history = useHistory()
- const { search } = useLocation()
- const { query = '' } = getUrlState( search )
-
- const [ searchedValue, setSearchedValue ] = useState( '' )
-
- const { anchor: initialAnchor, value: initialInputValue } = getSearchParams( query )
- const inputValue = useRef( initialInputValue )
- const [ anchor, setAnchor ] = useState( initialAnchor )
-
- const [ results, setResults ] = useState( [] )
-
- const [ isInputFocused, setInputFocused ] = useState( false )
-
- const inputRef = useRef( null )
-
- /**
- * Set the received results and update the searched vale.
- * @param {Object[]} results An array of the returned results.
- */
- const onResults = useCallback( results => {
- setSearchedValue( inputValue.current )
- setResults( results )
-
- updateFocus( 0 )
- }, [ updateFocus ] )
- /**
- * Run on change of value in the search box.
- * Converts ascii to unicode if need be.
- * Sends the search through to the controller.
- * @param {string} value The new value of the search box.
- */
- const onChange = useCallback( ( { target: { value } } ) => {
- const { anchor, type: searchType, value: searchValue } = getSearchParams( value )
-
- // Search if enough letters
- const doSearch = searchValue.length >= MIN_SEARCH_CHARS
-
- if ( doSearch ) {
- controller.search( searchValue, searchType, {
- translations: !!resultTranslationLanguage,
- transliterations: !!resultTransliterationLanguage,
- citations: !!showResultCitations,
- } )
- } else setResults( [] )
-
- inputValue.current = searchValue
- setAnchor( anchor )
-
- // Update URL with search
- history.push( { search: `?${stringify( {
- ...getUrlState( search ),
- query: value,
- } )}` } )
- }, [
- history,
- search,
- resultTranslationLanguage,
- resultTransliterationLanguage,
- showResultCitations,
- ] )
-
- const writers = useContext( WritersContext )
- const recommendedSources = useContext( RecommendedSourcesContext )
-
- /**
- * Renders a single result, highlighting the match.
- * @param {string} gurmukhi The shabad line to display.
- * @param {int} typeId The type id of line.
- * @param {string} lineId The id of the line.
- * @param {string} shabadId The id of the shabad.
- * @param {Component} ref The ref to the component.
- * @param {int} sourceId The id of source.
- * @param {Object} shabad The object containng section information and other metadata.
- * @param {int} sourcePage The page number of shabad in source.
- * @param {string} translations The translations of shabad line to display.
- * @param {string} transliterations The transliterations of shabad line to display.
- */
- const Result = ( {
- gurmukhi,
- typeId,
- id: lineId,
- shabadId,
- ref,
- focused,
- sourceId,
- shabad,
- sourcePage,
- translations,
- transliterations,
- } ) => {
- const transliteration = resultTransliterationLanguage && transliterations && customiseLine(
- getTransliteration(
- { transliterations },
- resultTransliterationLanguage,
- ),
- { lineEnding, typeId },
- )
-
- const translation = resultTranslationLanguage && translations && customiseLine(
- getTranslation( {
- line: { translations },
- shabad: { sourceId },
- recommendedSources,
- sources,
- languageId: resultTranslationLanguage,
- } ),
- { lineEnding, typeId },
- )
-
- // Grab the search mode or assume it's first letter
- const mode = SEARCH_ANCHORS[ anchor ] || SEARCH_TYPES.firstLetter
-
- // Separate the line into words before the match, the match, and after the match
- const getMatches = highlightMatches( gurmukhi )
-
- const [ beforeMatch, match, afterMatch ] = getMatches(
- gurmukhi,
- searchedValue,
- mode,
- )
- const [ translitBeforeMatch, translitMatch, translitAfterMatch ] = getMatches(
- transliteration,
- searchedValue,
- mode,
- )
-
- // Send the shabad id and line id to the server on click
- const onClick = () => controller.shabad( { shabadId, lineId } )
-
- // Helper render functions for citation
- const showCitation = showResultCitations && shabad && shabad.section
- const getEnglish = ( { nameEnglish } ) => nameEnglish
- const getWriterName = () => getEnglish( writers[ shabad.writerId ] )
- const getPageName = () => recommendedSources[ shabad.sourceId ].pageNameEnglish
-
- return (
-
-
-
- {beforeMatch ? {beforeMatch} : null}
- {match ? {match} : null}
- {afterMatch ? {afterMatch} : null}
-
-
-
-
- {translation && (
-
- {translation}
-
- )}
-
- {transliteration && (
-
- {translitBeforeMatch ? {translitBeforeMatch} : null}
- {translitMatch ? {translitMatch} : null}
- {translitAfterMatch ? {translitAfterMatch} : null}
-
- )}
-
-
-
- {showCitation && (
-
- {[
- getWriterName(),
- SOURCE_ABBREVIATIONS[ sourceId ],
- `${getPageName()} ${sourcePage}`,
- ].reduce( ( prev, curr ) => [ prev, ' - ', curr ] )}
-
- )}
-
-
-
- )
- }
-
- Result.propTypes = {
- gurmukhi: string.isRequired,
- id: string.isRequired,
- typeId: string.isRequired,
- shabadId: string.isRequired,
- ref: instanceOf( Result ).isRequired,
- sourceId: number.isRequired,
- shabad: shape( { } ).isRequired,
- sourcePage: number.isRequired,
- translations: string.isRequired,
- transliterations: string.isRequired,
- }
-
- const filterInputKeys = event => {
- const ignoreKeys = [ 'ArrowUp', 'ArrowDown' ]
-
- if ( ignoreKeys.includes( event.key ) ) event.preventDefault()
- }
-
- const refocus = ( { target } ) => {
- setInputFocused( false )
- target.focus()
- }
-
- const highlightSearch = () => inputRef.current.select()
-
- useEffect( () => {
- controller.on( 'results', onResults )
- return () => controller.off( 'results', onResults )
- }, [ onResults ] )
-
- useEffect( () => {
- if ( inputValue.current ) onChange( { target: { value: `${anchor || ''}${inputValue.current}` } } )
- }, [
- onChange,
- anchor,
- resultTransliterationLanguage,
- resultTranslationLanguage,
- showResultCitations,
- ] )
-
- useEffect( () => { highlightSearch() }, [] )
-
- return (
-
- setInputFocused( true )}
- onChange={onChange}
- value={`${anchor || ''}${inputValue.current}`}
- placeholder="Koj"
- disableUnderline
- autoFocus
- endAdornment={inputValue.current && (
-
- onChange( { target: { value: '' } } )}>
-
-
-
- )}
- inputProps={{
- spellCheck: false,
- autoCapitalize: 'off',
- autoCorrect: 'off',
- autoComplete: 'off',
- }}
- />
-
- {results
- ? results
- .map( ( props, i ) => Result( {
- ...props,
- ref: c => register( i, c ),
- focused: focused === i,
- } ) )
- : ''}
-
-
- )
-}
-
-Search.propTypes = {
- focused: oneOfType( [ string, number ] ),
- register: func.isRequired,
- updateFocus: func.isRequired,
-}
-
-Search.defaultProps = {
- focused: undefined,
-}
-
-export default withNavigationHotkeys( {
- keymap: {
- next: [ 'down', 'tab' ],
- previous: [ 'up', 'shift+tab' ],
- first: null,
- last: null,
- },
-} )( Search )
diff --git a/app/frontend/src/Controller/Search/Results.js b/app/frontend/src/Controller/Search/Results.js
new file mode 100644
index 00000000..9b674281
--- /dev/null
+++ b/app/frontend/src/Controller/Search/Results.js
@@ -0,0 +1,233 @@
+import React from 'react'
+import classNames from 'classnames'
+import { ListItem } from '@material-ui/core'
+import { string, oneOfType, number, instanceOf, shape, bool } from 'prop-types'
+import { firstLetters, stripVishraams, stripAccents, toUnicode, toAscii } from 'gurmukhi-utils'
+
+import controller from '../../lib/controller'
+import {
+ SEARCH_TYPES,
+ LANGUAGE_NAMES,
+ SEARCH_ANCHORS,
+ SOURCE_ABBREVIATIONS,
+} from '../../lib/consts'
+import {
+ getTranslation,
+ getTransliteration,
+ customiseLine,
+} from '../../lib/utils'
+
+const highlightFullWordMatches = ( line, query ) => {
+ const sanitisedQuery = query.trim()
+
+ const foundPosition = line.search( sanitisedQuery )
+ const matchStartPosition = line.lastIndexOf( ' ', foundPosition )
+
+ const wordEndPosition = line.indexOf( ' ', foundPosition + sanitisedQuery.length )
+ // If the match finishes in the last word, no space will be deteced, and wordEndPosition
+ // will be -1. In this case, we want to end at the last position in the line.
+ const matchEndPosition = wordEndPosition === -1 ? line.length - 1 : wordEndPosition
+
+ return [
+ line.substring( 0, matchStartPosition ),
+ line.substring( matchStartPosition, matchEndPosition ),
+ line.substring( matchEndPosition ),
+ ]
+}
+
+const highlightFirstLetterMatches = ( line, query ) => {
+ const baseLine = stripVishraams( line )
+
+ const letters = toAscii( firstLetters( stripAccents( toUnicode( baseLine ) ) ) )
+ const words = baseLine.split( ' ' )
+
+ const startPosition = letters.search( stripAccents( query ) )
+ const endPosition = startPosition + query.length
+
+ return [
+ `${words.slice( 0, startPosition ).join( ' ' )} `,
+ `${words.slice( startPosition, endPosition ).join( ' ' )} `,
+ `${words.slice( endPosition ).join( ' ' )} `,
+ ]
+}
+
+/**
+ * Separates the line into words before the first match, the first match, and after the match.
+ * @param value The full line.
+ * @param input The string inputted by the user.
+ * @param mode The type of search being performed, either first word or full word.
+ * @return An array of [ beforeMatch, match, afterMatch ],
+ * with `match` being the highlighted section.`.
+ */
+const highlightMatches = gurmukhi => ( value, input, mode ) => {
+ if ( !value ) return [ '', '', '' ]
+
+ // Account for wildcard characters
+ const sanitizedInput = input.replace( new RegExp( '_', 'g' ), '.' )
+
+ return mode === SEARCH_TYPES.fullWord
+ ? highlightFullWordMatches( gurmukhi, sanitizedInput )
+ : highlightFirstLetterMatches( value, sanitizedInput )
+}
+
+/**
+ * Renders a single result, highlighting the match.
+ * @param {string} gurmukhi The shabad line to display.
+ * @param {int} typeId The type id of line.
+ * @param {string} lineId The id of the line.
+ * @param {string} shabadId The id of the shabad.
+ * @param {Component} ref The ref to the component.
+ * @param {string|int} focused
+ * @param {int} sourceId The id of source.
+ * @param {Object} shabad The object containng section information and other metadata.
+ * @param {int} sourcePage The page number of shabad in source.
+ * @param {string} translations The translations of shabad line to display.
+ * @param {string} transliterations The transliterations of shabad line to display.
+ * @param {string} searchedValue The input to search.
+ * @param {string} anchor Anchor for search mode.
+ * @param {Object} writers From the SettingsContext.
+ * @param {Object} sources From the ContentContext.
+ * @param {Object} recommendedSources From the RecommendedSourcesContext.
+ * @param {int|bool} resultTransliterationLanguage Language code for translits (SettingsContext).
+ * @param {int|bool} resultTranslationLanguage Language code for translations (SettingsContext).
+ * @param {bool} showResultCitations To show citations or not (SettingsContext).
+ * @param {bool} lineEnding To strip line endings or not (SettingsContext).
+ */
+const Result = ( {
+ gurmukhi,
+ typeId,
+ id: lineId,
+ shabadId,
+ ref,
+ focused,
+ sourceId,
+ shabad,
+ sourcePage,
+ translations,
+ transliterations,
+ searchedValue,
+ anchor,
+ writers,
+ sources,
+ recommendedSources,
+ resultTransliterationLanguage,
+ resultTranslationLanguage,
+ showResultCitations,
+ lineEnding,
+} ) => {
+ const transliteration = resultTransliterationLanguage && transliterations && customiseLine(
+ getTransliteration(
+ { transliterations },
+ resultTransliterationLanguage,
+ ),
+ { lineEnding, typeId },
+ )
+
+ const translation = resultTranslationLanguage && translations && customiseLine(
+ getTranslation( {
+ line: { translations },
+ shabad: { sourceId },
+ recommendedSources,
+ sources,
+ languageId: resultTranslationLanguage,
+ } ),
+ { lineEnding, typeId },
+ )
+
+ // Grab the search mode or assume it's first letter
+ const mode = SEARCH_ANCHORS[ anchor ] || SEARCH_TYPES.firstLetter
+
+ // Separate the line into words before the match, the match, and after the match
+ const getMatches = highlightMatches( gurmukhi )
+
+ const [ beforeMatch, match, afterMatch ] = getMatches(
+ gurmukhi,
+ searchedValue,
+ mode,
+ )
+ const [ translitBeforeMatch, translitMatch, translitAfterMatch ] = getMatches(
+ transliteration,
+ searchedValue,
+ mode,
+ )
+
+ // Send the shabad id and line id to the server on click
+ const onClick = () => controller.shabad( { shabadId, lineId } )
+
+ // Helper render functions for citation
+ const showCitation = showResultCitations && shabad && shabad.section
+ const getEnglish = ( { nameEnglish } ) => nameEnglish
+ const getWriterName = () => getEnglish( writers[ shabad.writerId ] )
+ const getPageName = () => recommendedSources[ shabad.sourceId ].pageNameEnglish
+
+ return (
+
+
+
+
+ {beforeMatch ? {beforeMatch} : null}
+ {match ? {match} : null}
+ {afterMatch ? {afterMatch} : null}
+
+
+
+
+ {translation && (
+
+ {translation}
+
+ )}
+
+ {transliteration && (
+
+ {translitBeforeMatch ? {translitBeforeMatch} : null}
+ {translitMatch ? {translitMatch} : null}
+ {translitAfterMatch ? {translitAfterMatch} : null}
+
+ )}
+
+
+
+ {showCitation && (
+
+ {[
+ getWriterName(),
+ SOURCE_ABBREVIATIONS[ sourceId ],
+ `${getPageName()} ${sourcePage}`,
+ ].reduce( ( prev, curr ) => [ prev, ' - ', curr ] )}
+
+ )}
+
+
+
+ )
+}
+
+Result.propTypes = {
+ gurmukhi: string.isRequired,
+ id: string.isRequired,
+ typeId: string.isRequired,
+ shabadId: string.isRequired,
+ ref: instanceOf( Result ).isRequired,
+ sourceId: number.isRequired,
+ shabad: shape( { } ).isRequired,
+ sourcePage: number.isRequired,
+ translations: string.isRequired,
+ transliterations: string.isRequired,
+ focused: oneOfType( [ string, number ] ),
+ searchedValue: string.isRequired,
+ anchor: string.isRequired,
+ writers: shape( {} ).isRequired,
+ sources: shape( {} ).isRequired,
+ recommendedSources: shape( {} ).isRequired,
+ resultTransliterationLanguage: oneOfType( [ bool, number ] ).isRequired,
+ resultTranslationLanguage: oneOfType( [ bool, number ] ).isRequired,
+ showResultCitations: bool.isRequired,
+ lineEnding: bool.isRequired,
+}
+
+Result.defaultProps = {
+ focused: undefined,
+}
+
+export default Result
diff --git a/app/frontend/src/Controller/Search.css b/app/frontend/src/Controller/Search/index.css
similarity index 100%
rename from app/frontend/src/Controller/Search.css
rename to app/frontend/src/Controller/Search/index.css
diff --git a/app/frontend/src/Controller/Search/index.js b/app/frontend/src/Controller/Search/index.js
new file mode 100644
index 00000000..556a11f0
--- /dev/null
+++ b/app/frontend/src/Controller/Search/index.js
@@ -0,0 +1,227 @@
+import React, { useRef, useState, useEffect, useCallback, useContext } from 'react'
+import classNames from 'classnames'
+import { func, string, oneOfType, number } from 'prop-types'
+import { useLocation, useHistory } from 'react-router-dom'
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
+import { faTimes } from '@fortawesome/free-solid-svg-icons'
+import { stringify } from 'querystring'
+import {
+ Input,
+ InputAdornment,
+ List,
+ IconButton,
+} from '@material-ui/core'
+
+import { getUrlState } from '../../lib/utils'
+import { WritersContext, RecommendedSourcesContext, SettingsContext } from '../../lib/contexts'
+import controller from '../../lib/controller'
+import { withNavigationHotkeys } from '../../shared/NavigationHotkeys'
+import {
+ SEARCH_TYPES,
+ SEARCH_CHARS,
+ SEARCH_ANCHORS,
+ MIN_SEARCH_CHARS,
+} from '../../lib/consts'
+
+import Result from './Results'
+import './index.css'
+
+// Generate the regex for capturing anchor chars, optionally
+const searchRegex = new RegExp( `^([${Object.keys( SEARCH_ANCHORS ).map( anchor => `\\${anchor}` ).join( '' )}])?(.*)` )
+
+const getSearchParams = searchQuery => {
+ // Extract anchors and search query
+ const [ , anchor, query ] = searchQuery.match( searchRegex )
+
+ const inputValue = query
+
+ // Get search type from anchor char, if any
+ const type = SEARCH_ANCHORS[ anchor ] || SEARCH_TYPES.firstLetter
+
+ const value = type === SEARCH_TYPES.firstLetter
+ ? inputValue.slice().replace( new RegExp( SEARCH_CHARS.wildcard, 'g' ), '_' )
+ : inputValue
+
+ return { anchor, value, type }
+}
+
+/**
+ * Search Component.
+ * Converts ASCII to unicode on input.
+ * Displays results.
+ */
+const Search = ( { updateFocus, register, focused } ) => {
+ const { local: {
+ sources,
+ search: {
+ showResultCitations,
+ resultTransliterationLanguage,
+ resultTranslationLanguage,
+ lineEnding,
+ },
+ } = {} } = useContext( SettingsContext )
+
+ const writers = useContext( WritersContext )
+ const recommendedSources = useContext( RecommendedSourcesContext )
+
+ // Set the initial search query from URL
+ const history = useHistory()
+ const { search } = useLocation()
+ const { query = '' } = getUrlState( search )
+
+ const [ searchedValue, setSearchedValue ] = useState( '' )
+
+ const { anchor: initialAnchor, value: initialInputValue } = getSearchParams( query )
+ const inputValue = useRef( initialInputValue )
+ const [ anchor, setAnchor ] = useState( initialAnchor )
+
+ const [ results, setResults ] = useState( [] )
+
+ const [ isInputFocused, setInputFocused ] = useState( false )
+
+ const inputRef = useRef( null )
+
+ /**
+ * Set the received results and update the searched vale.
+ * @param {Object[]} results An array of the returned results.
+ */
+ const onResults = useCallback( results => {
+ setSearchedValue( inputValue.current )
+ setResults( results )
+
+ updateFocus( 0 )
+ }, [ updateFocus ] )
+ /**
+ * Run on change of value in the search box.
+ * Converts ascii to unicode if need be.
+ * Sends the search through to the controller.
+ * @param {string} value The new value of the search box.
+ */
+ const onChange = useCallback( ( { target: { value } } ) => {
+ const { anchor, type: searchType, value: searchValue } = getSearchParams( value )
+
+ // Search if enough letters
+ const doSearch = searchValue.length >= MIN_SEARCH_CHARS
+
+ if ( doSearch ) {
+ controller.search( searchValue, searchType, {
+ translations: !!resultTranslationLanguage,
+ transliterations: !!resultTransliterationLanguage,
+ citations: !!showResultCitations,
+ } )
+ } else setResults( [] )
+
+ inputValue.current = searchValue
+ setAnchor( anchor )
+
+ // Update URL with search
+ history.push( { search: `?${stringify( {
+ ...getUrlState( search ),
+ query: value,
+ } )}` } )
+ }, [
+ history,
+ search,
+ resultTranslationLanguage,
+ resultTransliterationLanguage,
+ showResultCitations,
+ ] )
+
+ const filterInputKeys = event => {
+ const ignoreKeys = [ 'ArrowUp', 'ArrowDown' ]
+
+ if ( ignoreKeys.includes( event.key ) ) event.preventDefault()
+ }
+
+ const refocus = ( { target } ) => {
+ setInputFocused( false )
+ target.focus()
+ }
+
+ const highlightSearch = () => inputRef.current.select()
+
+ useEffect( () => {
+ controller.on( 'results', onResults )
+ return () => controller.off( 'results', onResults )
+ }, [ onResults ] )
+
+ useEffect( () => {
+ if ( inputValue.current ) onChange( { target: { value: `${anchor || ''}${inputValue.current}` } } )
+ }, [
+ onChange,
+ anchor,
+ resultTransliterationLanguage,
+ resultTranslationLanguage,
+ showResultCitations,
+ ] )
+
+ useEffect( () => { highlightSearch() }, [] )
+
+ return (
+
+ setInputFocused( true )}
+ onChange={onChange}
+ value={`${anchor || ''}${inputValue.current}`}
+ placeholder="Koj"
+ disableUnderline
+ autoFocus
+ endAdornment={inputValue.current && (
+
+ onChange( { target: { value: '' } } )}>
+
+
+
+ )}
+ inputProps={{
+ spellCheck: false,
+ autoCapitalize: 'off',
+ autoCorrect: 'off',
+ autoComplete: 'off',
+ }}
+ />
+
+ {results
+ ? results
+ .map( ( props, i ) => Result( {
+ ...props,
+ searchedValue,
+ anchor,
+ sources,
+ writers,
+ recommendedSources,
+ resultTransliterationLanguage,
+ resultTranslationLanguage,
+ showResultCitations,
+ lineEnding,
+ ref: c => register( i, c ),
+ focused: focused === i,
+ } ) )
+ : ''}
+
+
+ )
+}
+
+Search.propTypes = {
+ focused: oneOfType( [ string, number ] ),
+ register: func.isRequired,
+ updateFocus: func.isRequired,
+}
+
+Search.defaultProps = {
+ focused: undefined,
+}
+
+export default withNavigationHotkeys( {
+ keymap: {
+ next: [ 'down', 'tab' ],
+ previous: [ 'up', 'shift+tab' ],
+ first: null,
+ last: null,
+ },
+} )( Search )