import React , { useState , useRef } from 'react' ; import { menuContent , MenuItemT } from '../../common/menu-content' ; import { Menu , MenuItems , MenuSurfaceAnchor } from '@rmwc/menu' ; import { SimpleListItem } from '@rmwc/list' ; import { TextField } from '@rmwc/textfield' ; import { Link } from 'react-router-dom' ; import { CircularProgress } from '@rmwc/circular-progress' ; import { history } from '../../common/history' ; import styles from './site-search.module.css' ; import { IconPropT } from '@rmwc/types' ; type SiteSearchItemT = { cacheId : string ; displayLink : string ; formattedUrl : string ; htmlSnippet : string ; htmlTitle : string ; kind : string ; link : string ; snippet : string ; title : string ; } ; const componentsList = ( ( ) => { const walkOptions = ( options ) => { return options . reduce < MenuItemT [ ] > ( ( acc , val ) => { if ( val . options ) { acc . push ( ... walkOptions ( val . options ! ) ) ; } else { acc . push ( val ) ; } return acc ; } , [ ] ) ; } ; return walkOptions ( menuContent ) ; } ) ( ) ; const searchComponents = ( val ) => componentsList . filter ( c => { return c . label . toLowerCase ( ) . includes ( val . toLowerCase ( ) ) ; } ) . map ( c => ( { id : c . label , icon : { icon : 'code' , theme : 'primary' } , sectionName : c . label , snippet : `View docs for ${ c . label }` , url : c . url ! } ) ) ; const searchGoogle = async ( val , abortController ) => { const { items = [ ] } = await fetch ( `https://www.googleapis.com/customsearch/v1/siterestrict?key=${ process . env . REACT_APP_CUSTOM_SEARCH_KEY }&cx=${ process . env . REACT_APP_CUSTOM_SEARCH_ID }&q=${ val }` , { signal : abortController . signal } ) . then < { items ? : SiteSearchItemT [ ] ; } > ( res => res . json ( ) ) ; return ( items . filter ( r => ! r . snippet . startsWith ( 'RMWC is a React wrapper' ) ) . map ( r => ( { id : r . cacheId , icon : { icon : 'notes' , theme : 'primary' } , sectionName : ( r . title . split ( '|' ) . pop ( ) || '' ) . trim ( ) , snippet : r . snippet , url : ( ( r . formattedUrl . split ( '?' ) . pop ( ) || '' ) . split ( '&' ) . find ( p => p . startsWith ( 'p=' ) ) || '' ) . slice ( 2 ) } ) ) ) ; } ; export function SiteSearch ( ) { const [ searchVal , _setSearchVal ] = useState ( '' ) ; const [ isSearching , setIsSearching ] = useState ( false ) ; const timerIdRef = useRef < number > ( ) ; const abortControllerRef = useRef < AbortController > ( ) ; const [ results , setResults ] = useState < Array < { id : string ; sectionName : string ; icon : IconPropT ; snippet : string ; url : string ; } > > ( [ ] ) ; const doSearch = ( val ) => { if ( abortControllerRef . current ) { abortControllerRef . current . abort ( ) ; abortControllerRef . current = undefined ; } if ( timerIdRef . current ) { clearTimeout ( timerIdRef . current ) ; } const abortController = new AbortController ( ) ; abortControllerRef . current = abortController ; timerIdRef . current = window . setTimeout ( async ( ) => { const components = searchComponents ( val ) ; setResults ( components ) ; try { const searchResults = await searchGoogle ( val , abortController ) ; abortControllerRef . current = undefined ; setResults ( [ ... components , ... searchResults ] ) ; } catch ( err ) { console . warn ( err ) ; abortControllerRef . current = undefined ; } } , 200 ) ; } ; const setSearchVal = ( val ) => { if ( val . length > 2 ) { ! isSearching && setIsSearching ( true ) ; doSearch ( val ) ; } else { setResults ( [ ] ) ; } _setSearchVal ( val ) ; } ; return ( < MenuSurfaceAnchor className = { styles . siteSearchWrapper } > < TextField placeholder = "Search" icon = "search" trailingIcon = { { icon : searchVal ? 'close' : '' , onClick : ( ) => setSearchVal ( '' ) } } outlined className = { styles . siteSearch } value = { searchVal } onChange = { evt => setSearchVal ( evt . currentTarget . value ) } onFocus = { ( ) => setIsSearching ( true ) } onKeyDown = { evt => { if ( evt . which === 40 ) { const listItem = document . querySelector < HTMLAnchorElement > ( `.${ styles . siteSearchMenu } a` ) ; if ( isSearching && listItem ) { listItem . focus ( ) ; } } } } /> < Menu className = { styles . siteSearchMenu } open = { isSearching && searchVal . length > 2 } anchorCorner = "bottomStart" focusOnOpen = { false } onFocus = { ( ) => setIsSearching ( true ) } onClose = { ( ) => { setIsSearching ( false ) ; } } onSelect = { evt => { window . scrollTo ( 0 , 0 ) ; history . replace ( ( evt . detail . item as HTMLAnchorElement ) . href . split ( '/' ) . pop ( ) || '/' ) ; document . activeElement && ( document . activeElement as HTMLElement ) . blur ( ) ; } } > < MenuItems twoLine > { results . map ( r => ( < SimpleListItem tag = { Link } to = { r . url } graphic = { r . icon } key = { r . id } text = { r . sectionName } secondaryText = { r . snippet } / > ) ) } { ! ! abortControllerRef . current && ( < div className = { styles . loading } > < CircularProgress size = "large" / > < /div>)} { ! abortControllerRef . current && ! results . length && ( < SimpleListItem text = "No results found" / > ) } < /MenuItems> < /Menu> < /MenuSurfaceAnchor>); }