| import React, {useCallback, useMemo, useRef, useState} from 'react'; |
| import {createPortal} from 'react-dom'; |
| import {DocSearchButton, useDocSearchKeyboardEvents} from '@docsearch/react'; |
| import Head from '@docusaurus/Head'; |
| import Link from '@docusaurus/Link'; |
| import {useHistory} from '@docusaurus/router'; |
| import { |
| isRegexpStringMatch, |
| useSearchLinkCreator, |
| } from '@docusaurus/theme-common'; |
| import { |
| useAlgoliaContextualFacetFilters, |
| useSearchResultUrlProcessor, |
| } from '@docusaurus/theme-search-algolia/client'; |
| import Translate from '@docusaurus/Translate'; |
| import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; |
| import translations from '@theme/SearchTranslations'; |
| let DocSearchModal = null; |
| function Hit({hit, children}) { |
| if (hit.url.includes('/api_reference/')) { |
| return ( |
| <a href={ |
| hit.url |
| }> |
| {children} |
| </a> |
| ) |
| } |
| return <Link to={hit.url}>{children}</Link>; |
| } |
| function ResultsFooter({state, onClose}) { |
| const createSearchLink = useSearchLinkCreator(); |
| return ( |
| <Link to={createSearchLink(state.query)} onClick={onClose}> |
| <Translate |
| id="theme.SearchBar.seeAll" |
| values={{count: state.context.nbHits}}> |
| {'See all {count} results'} |
| </Translate> |
| </Link> |
| ); |
| } |
| function mergeFacetFilters(f1, f2) { |
| const normalize = (f) => (typeof f === 'string' ? [f] : f); |
| return [...normalize(f1), ...normalize(f2)]; |
| } |
| function DocSearch({contextualSearch, externalUrlRegex, ...props}) { |
| const {siteMetadata} = useDocusaurusContext(); |
| const processSearchResultUrl = useSearchResultUrlProcessor(); |
| const contextualSearchFacetFilters = useAlgoliaContextualFacetFilters(); |
| const configFacetFilters = props.searchParameters?.facetFilters ?? []; |
| const facetFilters = contextualSearch |
| ? |
| mergeFacetFilters(contextualSearchFacetFilters, configFacetFilters) |
| : |
| configFacetFilters; |
| |
| const searchParameters = { |
| ...props.searchParameters, |
| facetFilters, |
| }; |
| 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([ |
| import('@docsearch/react/modal'), |
| import('@docsearch/react/style'), |
| import('./styles.css'), |
| ]).then(([{DocSearchModal: Modal}]) => { |
| DocSearchModal = Modal; |
| }); |
| }, []); |
| const prepareSearchContainer = useCallback(() => { |
| if (!searchContainer.current) { |
| const divElement = document.createElement('div'); |
| searchContainer.current = divElement; |
| document.body.insertBefore(divElement, document.body.firstChild); |
| } |
| }, []); |
| const openModal = useCallback(() => { |
| prepareSearchContainer(); |
| importDocSearchModalIfNeeded().then(() => setIsOpen(true)); |
| }, [importDocSearchModalIfNeeded, prepareSearchContainer]); |
| const closeModal = useCallback(() => { |
| setIsOpen(false); |
| searchButtonRef.current?.focus(); |
| }, []); |
| const handleInput = useCallback( |
| (event) => { |
| if (event.key === 'f' && (event.metaKey || event.ctrlKey)) { |
| |
| return; |
| } |
| |
| event.preventDefault(); |
| setInitialQuery(event.key); |
| openModal(); |
| }, |
| [openModal], |
| ); |
| const navigator = useRef({ |
| navigate({itemUrl}) { |
| |
| |
| if (isRegexpStringMatch(externalUrlRegex, itemUrl) || itemUrl.includes('/api_reference/')) { |
| window.location.href = itemUrl; |
| } else { |
| history.push(itemUrl); |
| } |
| }, |
| }).current; |
| const transformItems = useRef((items) => |
| props.transformItems |
| ? |
| props.transformItems(items) |
| : |
| items.map((item) => ({ |
| ...item, |
| url: processSearchResultUrl(item.url), |
| })), |
| ).current; |
| const resultsFooterComponent = useMemo( |
| () => |
| |
| (footerProps) => |
| <ResultsFooter {...footerProps} onClose={closeModal} />, |
| [closeModal], |
| ); |
| const transformSearchClient = useCallback( |
| (searchClient) => { |
| searchClient.addAlgoliaAgent( |
| 'docusaurus', |
| siteMetadata.docusaurusVersion, |
| ); |
| return searchClient; |
| }, |
| [siteMetadata.docusaurusVersion], |
| ); |
| useDocSearchKeyboardEvents({ |
| isOpen, |
| onOpen: openModal, |
| onClose: closeModal, |
| onInput: handleInput, |
| searchButtonRef, |
| }); |
| return ( |
| <> |
| <Head> |
| {/* This hints the browser that the website will load data from Algolia, |
| and allows it to preconnect to the DocSearch cluster. It makes the first |
| query faster, especially on mobile. */} |
| <link |
| rel="preconnect" |
| href={`https://${props.appId}-dsn.algolia.net`} |
| crossOrigin="anonymous" |
| /> |
| </Head> |
| |
| <DocSearchButton |
| onTouchStart={importDocSearchModalIfNeeded} |
| onFocus={importDocSearchModalIfNeeded} |
| onMouseOver={importDocSearchModalIfNeeded} |
| onClick={openModal} |
| ref={searchButtonRef} |
| translations={translations.button} |
| /> |
| |
| {isOpen && |
| DocSearchModal && |
| searchContainer.current && |
| createPortal( |
| <DocSearchModal |
| onClose={closeModal} |
| initialScrollY={window.scrollY} |
| initialQuery={initialQuery} |
| navigator={navigator} |
| transformItems={transformItems} |
| hitComponent={Hit} |
| transformSearchClient={transformSearchClient} |
| {...(props.searchPagePath && { |
| resultsFooterComponent, |
| })} |
| {...props} |
| searchParameters={searchParameters} |
| placeholder={translations.placeholder} |
| translations={translations.modal} |
| />, |
| searchContainer.current, |
| )} |
| </> |
| ); |
| } |
| export default function SearchBar() { |
| const {siteConfig} = useDocusaurusContext(); |
| return <DocSearch {...siteConfig.themeConfig.algolia} />; |
| } |
|
|