/** * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import React, {useState, useRef, useCallback, useMemo} from 'react'; import {createPortal} from 'react-dom'; // @ts-ignore import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; // @ts-ignore import {useHistory} from '@docusaurus/router'; // @ts-ignore import {useBaseUrlUtils} from '@docusaurus/useBaseUrl'; // @ts-ignore import Link from '@docusaurus/Link'; // @ts-ignore import Head from '@docusaurus/Head'; // @ts-ignore import {isRegexpStringMatch} from '@docusaurus/theme-common'; // @ts-ignore import {useSearchPage} from '../../hooks/useSearchPage'; import { DocSearchButton, useDocSearchKeyboardEvents, } from 'typesense-docsearch-react'; import {useTypesenseContextualFilters} from '../../client'; // @ts-ignore import Translate, {translate} from '@docusaurus/Translate'; // @ts-ignore import translations from '@theme/SearchTranslations'; import type { DocSearchModal as DocSearchModalType, DocSearchModalProps, } from 'typesense-docsearch-react'; import type { InternalDocSearchHit, StoredDocSearchHit, } from 'typesense-docsearch-react/dist/esm/types'; import type {AutocompleteState} from '@algolia/autocomplete-core'; import {DocsPreferredVersionContextProvider} from '@docusaurus/plugin-content-docs/lib/client/index.js'; type DocSearchProps = Omit< DocSearchModalProps, 'onClose' | 'initialScrollY' > & { contextualSearch?: string; externalUrlRegex?: string; searchPagePath: boolean | string; }; let DocSearchModal: typeof DocSearchModalType | null = null; function Hit({ hit, children, }: { hit: InternalDocSearchHit | StoredDocSearchHit; children: React.ReactNode; }) { return {children}; } type ResultsFooterProps = { state: AutocompleteState; onClose: () => void; }; function ResultsFooter({state, onClose}: ResultsFooterProps) { const {generateSearchPageLink} = useSearchPage(); return ( {'See all {count} results'} ); } function DocSearch({ contextualSearch, externalUrlRegex, ...props }: DocSearchProps) { const {siteMetadata} = useDocusaurusContext(); const contextualSearchFacetFilters = useTypesenseContextualFilters() as string; const configFacetFilters: string = props.typesenseSearchParameters?.filter_by ?? ''; const facetFilters = contextualSearch ? // Merge contextual search filters with config filters [contextualSearchFacetFilters, configFacetFilters] .filter((e) => e) .join(' && ') : // ... or use config facetFilters configFacetFilters; // we let user override default searchParameters if he wants to const typesenseSearchParameters = { filter_by: facetFilters, ...props.typesenseSearchParameters, }; const typesenseServerConfig = props.typesenseServerConfig; const typesenseCollectionName = props.typesenseCollectionName; const {withBaseUrl} = useBaseUrlUtils(); const history = useHistory(); const searchContainer = useRef(null); const searchButtonRef = useRef(null); const [isOpen, setIsOpen] = useState(false); const [initialQuery, setInitialQuery] = useState( undefined, ); const importDocSearchModalIfNeeded = useCallback(() => { if (DocSearchModal) { return Promise.resolve(); } return Promise.all([ // @ts-ignore import('typesense-docsearch-react/modal') as Promise< typeof import('typesense-docsearch-react') >, // @ts-ignore import('typesense-docsearch-react/style'), // @ts-ignore import('./styles.css'), ]).then(([{DocSearchModal: Modal}]) => { DocSearchModal = Modal; }); }, []); const onOpen = useCallback(() => { importDocSearchModalIfNeeded().then(() => { searchContainer.current = document.createElement('div'); document.body.insertBefore( searchContainer.current, document.body.firstChild, ); setIsOpen(true); }); }, [importDocSearchModalIfNeeded, setIsOpen]); const onClose = useCallback(() => { setIsOpen(false); searchContainer.current?.remove(); }, [setIsOpen]); const onInput = useCallback( (event: KeyboardEvent) => { importDocSearchModalIfNeeded().then(() => { setIsOpen(true); setInitialQuery(event.key); }); }, [importDocSearchModalIfNeeded, setIsOpen, setInitialQuery], ); const navigator = useRef({ navigate({itemUrl}: {itemUrl?: string}) { // Algolia results could contain URL's from other domains which cannot // be served through history and should navigate with window.location if (isRegexpStringMatch(externalUrlRegex, itemUrl)) { window.location.href = itemUrl!; } else { history.push(itemUrl!); } }, }).current; const transformItems = useRef( (items) => items.map((item) => { // If result contains a external domain, we should navigate without // relative URL if (isRegexpStringMatch(externalUrlRegex, item.url)) { return item; } // We transform the absolute URL into a relative URL. const url = new URL(item.url); return { ...item, url: withBaseUrl(`${url.pathname}${url.hash}`), }; }), ).current; const resultsFooterComponent: DocSearchProps['resultsFooterComponent'] = useMemo( () => // eslint-disable-next-line react/no-unstable-nested-components (footerProps: Omit): JSX.Element => ( ), [onClose], ); useDocSearchKeyboardEvents({ isOpen, onOpen, onClose, onInput, searchButtonRef, }); return ( <> {isOpen && DocSearchModal && searchContainer.current && createPortal( , searchContainer.current, )} ); } export default function SearchBar(): JSX.Element { const {siteConfig} = useDocusaurusContext(); return ( ); }