diff --git a/website/src/theme/SearchBar/index.js b/website/src/theme/SearchBar/index.js new file mode 100644 index 00000000..98269ded --- /dev/null +++ b/website/src/theme/SearchBar/index.js @@ -0,0 +1,204 @@ +import React, {useState, useRef, useCallback, useMemo} from 'react'; +import {createPortal} from 'react-dom'; +import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; +import {useHistory} from '@docusaurus/router'; +import {useBaseUrlUtils} from '@docusaurus/useBaseUrl'; +import Link from '@docusaurus/Link'; +import Head from '@docusaurus/Head'; +import {isRegexpStringMatch} from '@docusaurus/theme-common'; +import {useSearchPage} from '@docusaurus/theme-common/internal'; +import {DocSearchButton, useDocSearchKeyboardEvents} from '@docsearch/react'; +import Translate from '@docusaurus/Translate'; +import translations from '@theme/SearchTranslations'; +import {useContextualSearchFilters} from '@docusaurus/theme-common'; +// eslint-disable-next-line import/extensions +import {tagToCounter} from '../SearchMetadata'; +let DocSearchModal = null; +function Hit({hit, children}) { + return {children}; +} +function ResultsFooter({state, onClose}) { + const {generateSearchPageLink} = useSearchPage(); + return ( + + + {'See all {count} results'} + + + ); +} +function mergeFacetFilters(f1, f2) { + const normalize = f => { + return typeof f === 'string' ? [f] : f; + }; + return [...normalize(f1), ...normalize(f2)]; +} + +function usePuppeteerSearchFilters() { + const {locale, tags} = useContextualSearchFilters(); + const languageFilter = `language:${locale}`; + return [ + languageFilter, + tags.map(tag => { + return `counter:${tagToCounter.get(tag)}`; + }), + ]; +} + +function DocSearch({contextualSearch, externalUrlRegex, ...props}) { + const {siteMetadata} = useDocusaurusContext(); + const contextualSearchFacetFilters = usePuppeteerSearchFilters(); + const configFacetFilters = props.searchParameters?.facetFilters ?? []; + const facetFilters = contextualSearch + ? // Merge contextual search filters with config filters + mergeFacetFilters(contextualSearchFacetFilters, configFacetFilters) + : // ... or use config facetFilters + configFacetFilters; + // We let user override default searchParameters if she wants to + const searchParameters = { + ...props.searchParameters, + facetFilters, + }; + 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([ + import('@docsearch/react/modal'), + import('@docsearch/react/style'), + 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 => { + importDocSearchModalIfNeeded().then(() => { + setIsOpen(true); + setInitialQuery(event.key); + }); + }, + [importDocSearchModalIfNeeded, setIsOpen, setInitialQuery] + ); + const navigator = useRef({ + navigate({itemUrl}) { + // 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 => { + return items.map(item => { + // If Algolia 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 = useMemo(() => { + return footerProps => { + return ; + }; + }, [onClose]); + const transformSearchClient = useCallback( + searchClient => { + searchClient.addAlgoliaAgent( + 'docusaurus', + siteMetadata.docusaurusVersion + ); + return searchClient; + }, + [siteMetadata.docusaurusVersion] + ); + useDocSearchKeyboardEvents({ + isOpen, + onOpen, + onClose, + onInput, + searchButtonRef, + }); + return ( + <> + + {/* 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. */} + + + + + + {isOpen && + DocSearchModal && + searchContainer.current && + createPortal( + , + searchContainer.current + )} + + ); +} +export default function SearchBar() { + const {siteConfig} = useDocusaurusContext(); + return ; +} diff --git a/website/src/theme/SearchBar/styles.css b/website/src/theme/SearchBar/styles.css new file mode 100644 index 00000000..fdf8dff9 --- /dev/null +++ b/website/src/theme/SearchBar/styles.css @@ -0,0 +1,14 @@ +:root { + --docsearch-primary-color: var(--ifm-color-primary); + --docsearch-text-color: var(--ifm-font-color-base); +} + +.DocSearch-Button { + margin: 0; + transition: all var(--ifm-transition-fast) + var(--ifm-transition-timing-default); +} + +.DocSearch-Container { + z-index: calc(var(--ifm-z-index-fixed) + 1); +} diff --git a/website/src/theme/SearchMetadata/index.js b/website/src/theme/SearchMetadata/index.js index 94ada7b4..3d5a04c8 100644 --- a/website/src/theme/SearchMetadata/index.js +++ b/website/src/theme/SearchMetadata/index.js @@ -4,18 +4,29 @@ import Head from '@docusaurus/Head'; // Tracks the global package version as a local, monotonic counter. This // prevents Algolia from deleting based on differing package versions and // instead delete based on the number of versions we intend to keep documented. -let globalCounter = -1; -const versionToCounter = new Map(); -export default function SearchMetadata({locale, version}) { +class MonotonicCountMap { + #counter = -1; + #map = new Map(); + + get(key) { + console.log(key); + let counter = this.#map.get(key); + if (!counter) { + counter = ++this.#counter; + this.#map.set(key, counter); + } + return counter; + } +} + +export const tagToCounter = new MonotonicCountMap(); + +export default function SearchMetadata({locale, tag}) { const language = locale; let counter; - if (version) { - counter = versionToCounter.get(version); - if (!counter) { - counter = ++globalCounter; - versionToCounter.set(version, counter); - } + if (tag) { + counter = tagToCounter.get(tag); } return ( diff --git a/website/src/theme/SearchPage/index.js b/website/src/theme/SearchPage/index.js index dd2cd539..f8970808 100644 --- a/website/src/theme/SearchPage/index.js +++ b/website/src/theme/SearchPage/index.js @@ -20,6 +20,8 @@ import {useAllDocsData} from '@docusaurus/plugin-content-docs/client'; import Translate, {translate} from '@docusaurus/Translate'; import Layout from '@theme/Layout'; import styles from './styles.module.css'; +// eslint-disable-next-line import/extensions +import {tagToCounter} from '../SearchMetadata'; // Very simple pluralization: probably good enough for now function useDocumentsFoundPlural() { const {selectMessage} = usePluralForm(); @@ -263,11 +265,17 @@ function SearchPageContent() { const makeSearch = useEvent((page = 0) => { algoliaHelper.addDisjunctiveFacetRefinement( 'counter', - document - .querySelector('meta[name="docsearch:counter"]') - .getAttribute('content') + tagToCounter.get('default') ); algoliaHelper.addDisjunctiveFacetRefinement('language', currentLocale); + Object.entries(docsSearchVersionsHelpers.searchVersions).forEach( + ([pluginId, searchVersion]) => { + algoliaHelper.addDisjunctiveFacetRefinement( + 'counter', + tagToCounter.get(`docs-${pluginId}-${searchVersion}`) + ); + } + ); algoliaHelper.setQuery(searchQuery).setPage(page).search(); }); useEffect(() => {