import { useState, useCallback, useRef, createContext, useContext, useEffect } from 'react' import { createPortal } from 'react-dom' import Link from 'next/link' import Head from 'next/head' import { useRouter } from 'next/router' import { DocSearchModal } from '@docsearch/react' import clsx from 'clsx' import { useActionKey } from '~/hooks/useActionKey' // [Joshen] We're currently using this over Algolia search for the time being // Refer to the comment in Navigation/AlgoliaSearch for more information about this const INDEX_NAME = process.env.NEXT_PUBLIC_ALGOLIA_INDEX_NAME const API_KEY = process.env.NEXT_PUBLIC_ALGOLIA_SEARCH_API_KEY const APP_ID = process.env.NEXT_PUBLIC_ALGOLIA_APP_ID const SearchContext = createContext(null) export function SearchProvider({ children }: any) { const router = useRouter() const [isOpen, setIsOpen] = useState(false) const [initialQuery, setInitialQuery] = useState(null) const onOpen = useCallback(() => { setIsOpen(true) }, [setIsOpen]) const onClose = useCallback(() => { setIsOpen(false) }, [setIsOpen]) const onInput = useCallback( (e) => { setIsOpen(true) setInitialQuery(e.key) }, [setIsOpen, setInitialQuery] ) useDocSearchKeyboardEvents({ isOpen, onOpen, onClose, // @ts-ignore onInput, }) return ( <> {children} {isOpen && createPortal( { return items.map((item, index) => { // We transform the absolute URL into a relative URL to // leverage Next's preloading. const a = document.createElement('a') a.href = item.url const hash = a.hash === '#content-wrapper' ? '' : a.hash return { ...item, url: `${a.pathname}${hash}`, __is_result: () => true, __is_parent: () => item.type === 'lvl1' && items.length > 1 && index === 0, __is_child: () => item.type !== 'lvl1' && items.length > 1 && items[0].type === 'lvl1' && index !== 0, __is_first: () => index === 1, __is_last: () => index === items.length - 1 && index !== 0, } }) }} />, document.body )} ) } // @ts-ignore function Hit({ hit, children }) { return ( {children} ) } // @ts-ignore export function SearchButton({ children, ...props }) { let searchButtonRef = useRef() let actionKey = useActionKey() // @ts-ignore let { onOpen, onInput } = useContext(SearchContext) useEffect(() => { // @ts-ignore function onKeyDown(event) { if (searchButtonRef && searchButtonRef.current === document.activeElement && onInput) { if (/[a-zA-Z0-9]/.test(String.fromCharCode(event.keyCode))) { onInput(event) } } } window.addEventListener('keydown', onKeyDown) return () => { window.removeEventListener('keydown', onKeyDown) } }, [onInput, searchButtonRef]) return ( // @ts-ignore ) } // @ts-ignore function useDocSearchKeyboardEvents({ isOpen, onOpen, onClose }) { useEffect(() => { // @ts-ignore function onKeyDown(event) { function open() { // We check that no other DocSearch modal is showing before opening // another one. if (!document.body.classList.contains('DocSearch--active')) { onOpen() } } if ( (event.keyCode === 27 && isOpen) || (event.key === 'k' && (event.metaKey || event.ctrlKey)) || (!isEditingContent(event) && event.key === '/' && !isOpen) ) { event.preventDefault() if (isOpen) { onClose() } else if (!document.body.classList.contains('DocSearch--active')) { open() } } } window.addEventListener('keydown', onKeyDown) return () => { window.removeEventListener('keydown', onKeyDown) } }, [isOpen, onOpen, onClose]) } // @ts-ignore function isEditingContent(event) { let element = event.target let tagName = element.tagName return ( element.isContentEditable || tagName === 'INPUT' || tagName === 'SELECT' || tagName === 'TEXTAREA' ) }