import { SearchIcon } from '@chakra-ui/icons' import { Box, Center, Flex, Modal, ModalBody, ModalContent, ModalOverlay, chakra, useDisclosure, useEventListener, useUpdateEffect, } from '@chakra-ui/react' import { findAll } from 'highlight-words-core' import { matchSorter } from 'match-sorter' import Link from 'next/link' import { useRouter } from 'next/router' import * as React from 'react' import MultiRef from 'react-multi-ref' import scrollIntoView from 'scroll-into-view-if-needed' import { SearchButton } from './algolia-search' import searchData from 'configs/search-meta.json' interface OptionTextProps { searchWords: string[] textToHighlight: string } function OptionText({ searchWords, textToHighlight }: OptionTextProps) { const chunks = findAll({ searchWords, textToHighlight, autoEscape: true, }) const highlightedText = chunks.map((chunk, id) => { const { end, highlight, start } = chunk const text = textToHighlight.substr(start, end - start) if (highlight) { return ( {text} ) } else { return text } }) return highlightedText } function DocIcon(props) { return ( ) } function EnterIcon(props) { return ( ) } function HashIcon(props) { return ( ) } function OmniSearch() { const router = useRouter() const [query, setQuery] = React.useState('') const [active, setActive] = React.useState(0) const [shouldCloseModal, setShouldCloseModal] = React.useState(true) const menu = useDisclosure() const modal = useDisclosure() const [menuNodes] = React.useState(() => new MultiRef()) const menuRef = React.useRef(null) const eventRef = React.useRef<'mouse' | 'keyboard'>(null) React.useEffect(() => { router.events.on('routeChangeComplete', modal.onClose) return () => { router.events.off('routeChangeComplete', modal.onClose) } // eslint-disable-next-line react-hooks/exhaustive-deps }, []) // @ts-expect-error @segunadebayo not sure what this should be?! useEventListener('keydown', (event) => { const isMac = /(Mac|iPhone|iPod|iPad)/i.test(navigator?.platform) const hotkey = isMac ? 'metaKey' : 'ctrlKey' if (event?.key?.toLowerCase() === 'k' && event[hotkey]) { event.preventDefault() modal.isOpen ? modal.onClose() : modal.onOpen() } }) React.useEffect(() => { if (modal.isOpen && query.length > 0) { setQuery('') } // eslint-disable-next-line react-hooks/exhaustive-deps }, [modal.isOpen]) const results = React.useMemo( function getResults() { if (query.length < 2) return [] return matchSorter(searchData, query, { keys: ['hierarchy.lvl1', 'hierarchy.lvl2', 'hierarchy.lvl3', 'content'], }).slice(0, 20) // There is probably a filter needed to filter for current locale }, [query], ) const onKeyDown = React.useCallback( (e: React.KeyboardEvent) => { eventRef.current = 'keyboard' switch (e.key) { case 'ArrowDown': { e.preventDefault() if (active + 1 < results.length) { setActive(active + 1) } break } case 'ArrowUp': { e.preventDefault() if (active - 1 >= 0) { setActive(active - 1) } break } case 'Control': case 'Alt': case 'Shift': { e.preventDefault() setShouldCloseModal(true) break } case 'Enter': { if (results?.length <= 0) { break } modal.onClose() router.push(results[active].url) break } } }, [active, modal, results, router], ) const onKeyUp = React.useCallback((e: React.KeyboardEvent) => { eventRef.current = 'keyboard' switch (e.key) { case 'Control': case 'Alt': case 'Shift': { e.preventDefault() setShouldCloseModal(false) } } }, []) useUpdateEffect(() => { setActive(0) }, [query]) useUpdateEffect(() => { if (!menuRef.current || eventRef.current === 'mouse') return const node = menuNodes.map.get(active) if (!node) return scrollIntoView(node, { scrollMode: 'if-needed', block: 'nearest', inline: 'nearest', boundary: menuRef.current, }) }, [active]) const open = menu.isOpen && results.length > 0 return ( <> { setQuery(e.target.value) menu.onOpen() }} onKeyDown={onKeyDown} onKeyUp={onKeyUp} />
{open && ( {results.map((item, index) => { const selected = index === active const isLvl1 = item.type === 'lvl1' return ( { setActive(index) eventRef.current = 'mouse' }} onClick={() => { if (shouldCloseModal) { modal.onClose() } }} ref={menuNodes.ref(index)} role='option' key={item.url} sx={{ display: 'flex', alignItems: 'center', minH: 16, mt: 2, px: 4, py: 2, rounded: 'lg', bg: 'gray.100', '.chakra-ui-dark &': { bg: 'gray.600' }, _selected: { bg: 'teal.500', color: 'white', mark: { color: 'white', textDecoration: 'underline', }, }, }} > {isLvl1 ? ( ) : ( )} {!isLvl1 && ( {item.hierarchy.lvl1} )} ) })} )}
) } export default OmniSearch