From 3235b60ddaf68f7bfa29888b9a7e2843fee30293 Mon Sep 17 00:00:00 2001
From: jrandolf <101637635+jrandolf@users.noreply.github.com>
Date: Fri, 16 Dec 2022 17:26:01 +0100
Subject: [PATCH] docs: use counter for algolia search (#9428)
The component was [swizzled](https://docusaurus.io/docs/swizzling) from
the Algolia plugin. The main logic that was changed is on lines 263 to
272.
---
website/src/theme/SearchPage/index.js | 488 ++++++++++++++++++
.../src/theme/SearchPage/styles.module.css | 112 ++++
2 files changed, 600 insertions(+)
create mode 100644 website/src/theme/SearchPage/index.js
create mode 100644 website/src/theme/SearchPage/styles.module.css
diff --git a/website/src/theme/SearchPage/index.js b/website/src/theme/SearchPage/index.js
new file mode 100644
index 00000000..dd2cd539
--- /dev/null
+++ b/website/src/theme/SearchPage/index.js
@@ -0,0 +1,488 @@
+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 => {
+ return 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(() => {
+ return Object.entries(allDocsData).reduce((acc, [pluginId, pluginData]) => {
+ return {
+ ...acc,
+ [pluginId]: pluginData.versions[0].name,
+ };
+ }, {});
+ });
+ // Set the value of a single select menu
+ const setSearchVersion = (pluginId, searchVersion) => {
+ return setSearchVersions(s => {
+ return {...s, [pluginId]: searchVersion};
+ });
+ };
+ const versioningEnabled = Object.values(allDocsData).some(docsData => {
+ return 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]) => {
+ return 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', 'counter'],
+ });
+ algoliaHelper.on(
+ 'result',
+ ({results: {query, hits, page, nbHits, nbPages}}) => {
+ if (query === '' || !Array.isArray(hits)) {
+ searchResultStateDispatcher({type: 'reset'});
+ return;
+ }
+ const sanitizeValue = value => {
+ return 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 => {
+ return 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(
+ ExecutionEnvironment.canUseIntersectionObserver &&
+ 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 = () => {
+ return 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(
+ 'counter',
+ document
+ .querySelector('meta[name="docsearch:counter"]')
+ .getAttribute('content')
+ );
+ algoliaHelper.addDisjunctiveFacetRefinement('language', currentLocale);
+ algoliaHelper.setQuery(searchQuery).setPage(page).search();
+ });
+ useEffect(() => {
+ if (!loaderRef) {
+ return undefined;
+ }
+ const currentObserver = observer.current;
+ if (currentObserver) {
+ currentObserver.observe(loaderRef);
+ return () => {
+ return currentObserver.unobserve(loaderRef);
+ };
+ }
+ return () => {
+ 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()}
+
+
+
+
+
+ {!!searchResultState.totalResults &&
+ documentsFoundPlural(searchResultState.totalResults)}
+
+
+
+
+
+ {searchResultState.items.length > 0 ? (
+
+ {searchResultState.items.map(
+ ({title, url, summary, breadcrumbs}, i) => {
+ return (
+
+
+
+
+
+ {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 00000000..57de7498
--- /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;
+}