From ce1122d746b1ec8043617c49c44986c13042fa5b Mon Sep 17 00:00:00 2001 From: RoiArthurB Date: Mon, 11 Jul 2022 11:38:32 +0700 Subject: [PATCH] [SP] Eject swizzling - Fix #3457 Waiting for https://github.com/facebook/docusaurus/pull/7761 merging --- website/src/theme/SearchPage/index.d.ts | 2 + website/src/theme/SearchPage/index.js | 461 +++++++++++++++++- .../src/theme/SearchPage/styles.module.css | 112 +++++ 3 files changed, 567 insertions(+), 8 deletions(-) create mode 100644 website/src/theme/SearchPage/index.d.ts create mode 100644 website/src/theme/SearchPage/styles.module.css diff --git a/website/src/theme/SearchPage/index.d.ts b/website/src/theme/SearchPage/index.d.ts new file mode 100644 index 0000000000..21b47773f6 --- /dev/null +++ b/website/src/theme/SearchPage/index.d.ts @@ -0,0 +1,2 @@ +/// +export default function SearchPage(): JSX.Element; diff --git a/website/src/theme/SearchPage/index.js b/website/src/theme/SearchPage/index.js index 9fea50069d..7459330e96 100644 --- a/website/src/theme/SearchPage/index.js +++ b/website/src/theme/SearchPage/index.js @@ -1,14 +1,459 @@ -import React from 'react'; -import SearchPage from '@theme-original/SearchPage'; +/* eslint-disable jsx-a11y/no-autofocus */ +import React, {useEffect, useState, useReducer, useRef} from 'react'; +import clsx from 'clsx'; +import algoliaSearch from 'algoliasearch/lite'; +import algoliaSearchHelper from 'algoliasearch-helper'; +import Head from '@docusaurus/Head'; +import Link from '@docusaurus/Link'; import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment'; +import { + HtmlClassNameProvider, + usePluralForm, + isRegexpStringMatch, + useEvent, +} from '@docusaurus/theme-common'; +import { + useTitleFormatter, + useSearchPage, +} from '@docusaurus/theme-common/internal'; +import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; +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'; +// Very simple pluralization: probably good enough for now +function useDocumentsFoundPlural() { + const {selectMessage} = usePluralForm(); + return (count) => + selectMessage( + count, + translate( + { + id: 'theme.SearchPage.documentsFound.plurals', + description: + 'Pluralized label for "{count} documents found". Use as much plural forms (separated by "|") as your language support (see https://www.unicode.org/cldr/cldr-aux/charts/34/supplemental/language_plural_rules.html)', + message: 'One document found|{count} documents found', + }, + {count}, + ), + ); +} +function useDocsSearchVersionsHelpers() { + const allDocsData = useAllDocsData(); + // State of the version select menus / algolia facet filters + // docsPluginId -> versionName map + const [searchVersions, setSearchVersions] = useState(() => + Object.entries(allDocsData).reduce( + (acc, [pluginId, pluginData]) => ({ + ...acc, + [pluginId]: pluginData.versions[0].name, + }), + {}, + ), + ); + // Set the value of a single select menu + const setSearchVersion = (pluginId, searchVersion) => + setSearchVersions((s) => ({...s, [pluginId]: searchVersion})); + const versioningEnabled = Object.values(allDocsData).some( + (docsData) => docsData.versions.length > 1, + ); + return { + allDocsData, + versioningEnabled, + searchVersions, + setSearchVersion, + }; +} +// We want to display one select per versioned docs plugin instance +function SearchVersionSelectList({docsSearchVersionsHelpers}) { + const versionedPluginEntries = Object.entries( + docsSearchVersionsHelpers.allDocsData, + ) + // Do not show a version select for unversioned docs plugin instances + .filter(([, docsData]) => docsData.versions.length > 1); + return ( +
+ {versionedPluginEntries.map(([pluginId, docsData]) => { + const labelPrefix = + versionedPluginEntries.length > 1 ? `${pluginId}: ` : ''; + return ( + + ); + })} +
+ ); +} +function SearchPageContent() { + const { + siteConfig: {themeConfig}, + i18n: {currentLocale}, + } = useDocusaurusContext(); + const { + algolia: {appId, apiKey, indexName, externalUrlRegex}, + } = themeConfig; + const documentsFoundPlural = useDocumentsFoundPlural(); + const docsSearchVersionsHelpers = useDocsSearchVersionsHelpers(); + const {searchQuery, setSearchQuery} = useSearchPage(); + const initialSearchResultState = { + items: [], + query: null, + totalResults: null, + totalPages: null, + lastPage: null, + hasMore: null, + loading: null, + }; + const [searchResultState, searchResultStateDispatcher] = useReducer( + (prevState, data) => { + switch (data.type) { + case 'reset': { + return initialSearchResultState; + } + case 'loading': { + return {...prevState, loading: true}; + } + case 'update': { + if (searchQuery !== data.value.query) { + return prevState; + } + return { + ...data.value, + items: + data.value.lastPage === 0 + ? data.value.items + : prevState.items.concat(data.value.items), + }; + } + case 'advance': { + const hasMore = prevState.totalPages > prevState.lastPage + 1; + return { + ...prevState, + lastPage: hasMore ? prevState.lastPage + 1 : prevState.lastPage, + hasMore, + }; + } + default: + return prevState; + } + }, + initialSearchResultState, + ); + const algoliaClient = algoliaSearch(appId, apiKey); + const algoliaHelper = algoliaSearchHelper(algoliaClient, indexName, { + hitsPerPage: 15, + advancedSyntax: true, + disjunctiveFacets: ['language', 'docusaurus_tag'], + }); + algoliaHelper.on( + 'result', + ({results: {query, hits, page, nbHits, nbPages}}) => { + if (query === '' || !Array.isArray(hits)) { + searchResultStateDispatcher({type: 'reset'}); + return; + } + const sanitizeValue = (value) => + value.replace( + /algolia-docsearch-suggestion--highlight/g, + 'search-result-match', + ); + const items = hits.map( + ({ + url, + _highlightResult: {hierarchy}, + _snippetResult: snippet = {}, + }) => { + const parsedURL = new URL(url); + const titles = Object.keys(hierarchy).map((key) => + sanitizeValue(hierarchy[key].value), + ); + return { + title: titles.pop(), + url: isRegexpStringMatch(externalUrlRegex, parsedURL.href) + ? parsedURL.href + : parsedURL.pathname + parsedURL.hash, + summary: snippet.content + ? `${sanitizeValue(snippet.content.value)}...` + : '', + breadcrumbs: titles, + }; + }, + ); + searchResultStateDispatcher({ + type: 'update', + value: { + items, + query, + totalResults: nbHits, + totalPages: nbPages, + lastPage: page, + hasMore: nbPages > page + 1, + loading: false, + }, + }); + }, + ); + const [loaderRef, setLoaderRef] = useState(null); + const prevY = useRef(0); + const observer = useRef( + false && + new IntersectionObserver( + (entries) => { + const { + isIntersecting, + boundingClientRect: {y: currentY}, + } = entries[0]; + if (isIntersecting && prevY.current > currentY) { + searchResultStateDispatcher({type: 'advance'}); + } + prevY.current = currentY; + }, + {threshold: 1}, + ), + ); + const getTitle = () => + searchQuery + ? translate( + { + id: 'theme.SearchPage.existingResultsTitle', + message: 'Search results for "{query}"', + description: 'The search page title for non-empty query', + }, + { + query: searchQuery, + }, + ) + : translate({ + id: 'theme.SearchPage.emptyResultsTitle', + message: 'Search the documentation', + description: 'The search page title for empty query', + }); + const makeSearch = useEvent((page = 0) => { + algoliaHelper.addDisjunctiveFacetRefinement('docusaurus_tag', 'default'); + algoliaHelper.addDisjunctiveFacetRefinement('language', currentLocale); + Object.entries(docsSearchVersionsHelpers.searchVersions).forEach( + ([pluginId, searchVersion]) => { + algoliaHelper.addDisjunctiveFacetRefinement( + 'docusaurus_tag', + `docs-${pluginId}-${searchVersion}`, + ); + }, + ); + algoliaHelper.setQuery(searchQuery).setPage(page).search(); + }); + useEffect(() => { + if (!loaderRef) { + return undefined; + } + const currentObserver = observer.current; + if (currentObserver) { + currentObserver.observe(loaderRef); + return () => currentObserver.unobserve(loaderRef); + } + return () => true; + }, [loaderRef]); + useEffect(() => { + searchResultStateDispatcher({type: 'reset'}); + if (searchQuery) { + searchResultStateDispatcher({type: 'loading'}); + setTimeout(() => { + makeSearch(); + }, 300); + } + }, [searchQuery, docsSearchVersionsHelpers.searchVersions, makeSearch]); + useEffect(() => { + if (!searchResultState.lastPage || searchResultState.lastPage === 0) { + return; + } + makeSearch(searchResultState.lastPage); + }, [makeSearch, searchResultState.lastPage]); + return ( + + + {useTitleFormatter(getTitle())} + {/* + We should not index search pages + See https://github.com/facebook/docusaurus/pull/3233 + */} + + + +
+

{getTitle()}

+ +
e.preventDefault()}> +
+ setSearchQuery(e.target.value)} + value={searchQuery} + autoComplete="off" + autoFocus + /> +
+ + {docsSearchVersionsHelpers.versioningEnabled && ( + + )} + + +
+
+ {!!searchResultState.totalResults && + documentsFoundPlural(searchResultState.totalResults)} +
-ExecutionEnvironment.canUseIntersectionObserver = false; + +
-export default function SearchPageWrapper(props) { - ExecutionEnvironment.canUseIntersectionObserver = false; + {searchResultState.items.length > 0 ? ( +
+ {searchResultState.items.map( + ({title, url, summary, breadcrumbs}, i) => ( +
+

+ +

+ + {breadcrumbs.length > 0 && ( + + )} + + {summary && ( +

+ )} +

+ ), + )} +
+ ) : ( + [ + searchQuery && !searchResultState.loading && ( +

+ + No results were found + +

+ ), + !!searchResultState.loading && ( +
+ ), + ] + )} + + {searchResultState.hasMore && ( +
+ + Fetching new results... + +
+ )} +
+ + ); +} +export default function SearchPage() { return ( - <> - - + + + ); } diff --git a/website/src/theme/SearchPage/styles.module.css b/website/src/theme/SearchPage/styles.module.css new file mode 100644 index 0000000000..57de7498db --- /dev/null +++ b/website/src/theme/SearchPage/styles.module.css @@ -0,0 +1,112 @@ +.searchQueryInput, +.searchVersionInput { + border-radius: var(--ifm-global-radius); + border: 2px solid var(--ifm-toc-border-color); + font: var(--ifm-font-size-base) var(--ifm-font-family-base); + padding: 0.8rem; + width: 100%; + background: var(--docsearch-searchbox-focus-background); + color: var(--docsearch-text-color); + margin-bottom: 0.5rem; + transition: border var(--ifm-transition-fast) ease; +} + +.searchQueryInput:focus, +.searchVersionInput:focus { + border-color: var(--docsearch-primary-color); + outline: none; +} + +.searchQueryInput::placeholder { + color: var(--docsearch-muted-color); +} + +.searchResultsColumn { + font-size: 0.9rem; + font-weight: bold; +} + +.algoliaLogo { + max-width: 150px; +} + +.algoliaLogoPathFill { + fill: var(--ifm-font-color-base); +} + +.searchResultItem { + padding: 1rem 0; + border-bottom: 1px solid var(--ifm-toc-border-color); +} + +.searchResultItemHeading { + font-weight: 400; + margin-bottom: 0; +} + +.searchResultItemPath { + font-size: 0.8rem; + color: var(--ifm-color-content-secondary); + --ifm-breadcrumb-separator-size-multiplier: 1; +} + +.searchResultItemSummary { + margin: 0.5rem 0 0; + font-style: italic; +} + +@media only screen and (max-width: 996px) { + .searchQueryColumn { + max-width: 60% !important; + } + + .searchVersionColumn { + max-width: 40% !important; + } + + .searchResultsColumn { + max-width: 60% !important; + } + + .searchLogoColumn { + max-width: 40% !important; + padding-left: 0 !important; + } +} + +@media screen and (max-width: 576px) { + .searchQueryColumn { + max-width: 100% !important; + } + + .searchVersionColumn { + max-width: 100% !important; + padding-left: var(--ifm-spacing-horizontal) !important; + } +} + +.loadingSpinner { + width: 3rem; + height: 3rem; + border: 0.4em solid #eee; + border-top-color: var(--ifm-color-primary); + border-radius: 50%; + animation: loading-spin 1s linear infinite; + margin: 0 auto; +} + +@keyframes loading-spin { + 100% { + transform: rotate(360deg); + } +} + +.loader { + margin-top: 2rem; +} + +:global(.search-result-match) { + color: var(--docsearch-hit-color); + background: rgb(255 215 142 / 25%); + padding: 0.09em 0; +}