mirror of
https://github.com/facebook/docusaurus.git
synced 2025-05-03 20:27:20 +02:00
refactor(theme-search-algolia): migrate package to TS (#5935)
This commit is contained in:
parent
284cdabb0a
commit
425144afc7
20 changed files with 301 additions and 91 deletions
|
@ -14,6 +14,7 @@ packages/docusaurus/lib/
|
||||||
packages/docusaurus-*/lib/*
|
packages/docusaurus-*/lib/*
|
||||||
packages/docusaurus-*/lib-next/
|
packages/docusaurus-*/lib-next/
|
||||||
packages/docusaurus-plugin-ideal-image/copyUntypedFiles.js
|
packages/docusaurus-plugin-ideal-image/copyUntypedFiles.js
|
||||||
|
packages/docusaurus-theme-search-algolia/copyUntypedFiles.js
|
||||||
|
|
||||||
packages/create-docusaurus/lib/*
|
packages/create-docusaurus/lib/*
|
||||||
packages/create-docusaurus/templates/facebook/.eslintrc.js
|
packages/create-docusaurus/templates/facebook/.eslintrc.js
|
||||||
|
|
|
@ -15,6 +15,6 @@ const srcDir = path.resolve(__dirname, 'src');
|
||||||
const libDir = path.resolve(__dirname, 'lib');
|
const libDir = path.resolve(__dirname, 'lib');
|
||||||
fs.copySync(srcDir, libDir, {
|
fs.copySync(srcDir, libDir, {
|
||||||
filter(filepath) {
|
filter(filepath) {
|
||||||
return !/__tests__/.test(filepath) && !/\.ts$/.test(filepath);
|
return !/__tests__/.test(filepath) && !/\.tsx?$/.test(filepath);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -15,7 +15,7 @@ const CodeDirPaths = [
|
||||||
path.join(__dirname, 'lib-next'),
|
path.join(__dirname, 'lib-next'),
|
||||||
// TODO other themes should rather define their own translations in the future?
|
// TODO other themes should rather define their own translations in the future?
|
||||||
path.join(__dirname, '..', 'docusaurus-theme-common', 'lib'),
|
path.join(__dirname, '..', 'docusaurus-theme-common', 'lib'),
|
||||||
path.join(__dirname, '..', 'docusaurus-theme-search-algolia', 'src', 'theme'),
|
path.join(__dirname, '..', 'docusaurus-theme-search-algolia', 'lib', 'theme'),
|
||||||
path.join(__dirname, '..', 'docusaurus-theme-live-codeblock', 'src', 'theme'),
|
path.join(__dirname, '..', 'docusaurus-theme-live-codeblock', 'src', 'theme'),
|
||||||
path.join(__dirname, '..', 'docusaurus-plugin-pwa', 'src', 'theme'),
|
path.join(__dirname, '..', 'docusaurus-plugin-pwa', 'src', 'theme'),
|
||||||
];
|
];
|
||||||
|
|
|
@ -22,6 +22,8 @@ export {createStorageSlot, listStorageKeys} from './utils/storageUtils';
|
||||||
|
|
||||||
export {useAlternatePageUtils} from './utils/useAlternatePageUtils';
|
export {useAlternatePageUtils} from './utils/useAlternatePageUtils';
|
||||||
|
|
||||||
|
export {useContextualSearchFilters} from './utils/useContextualSearchFilters';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
parseCodeBlockTitle,
|
parseCodeBlockTitle,
|
||||||
parseLanguage,
|
parseLanguage,
|
||||||
|
|
|
@ -5,21 +5,18 @@
|
||||||
* LICENSE file in the root directory of this source tree.
|
* LICENSE file in the root directory of this source tree.
|
||||||
*/
|
*/
|
||||||
import {useAllDocsData, useActivePluginAndVersion} from '@theme/hooks/useDocs';
|
import {useAllDocsData, useActivePluginAndVersion} from '@theme/hooks/useDocs';
|
||||||
import {
|
import {useDocsPreferredVersionByPluginId} from './docsPreferredVersion/useDocsPreferredVersion';
|
||||||
useDocsPreferredVersionByPluginId,
|
import {docVersionSearchTag, DEFAULT_SEARCH_TAG} from './searchUtils';
|
||||||
DEFAULT_SEARCH_TAG,
|
|
||||||
docVersionSearchTag,
|
|
||||||
} from '@docusaurus/theme-common';
|
|
||||||
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
|
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
|
||||||
|
|
||||||
type ContextualSearchFilters = {
|
export type useContextualSearchFiltersReturns = {
|
||||||
locale: string;
|
locale: string;
|
||||||
tags: string[];
|
tags: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
// We may want to support multiple search engines, don't couple that to Algolia/DocSearch
|
// We may want to support multiple search engines, don't couple that to Algolia/DocSearch
|
||||||
// Maybe users will want to use its own search engine solution
|
// Maybe users will want to use its own search engine solution
|
||||||
export default function useContextualSearchFilters(): ContextualSearchFilters {
|
export function useContextualSearchFilters(): useContextualSearchFiltersReturns {
|
||||||
const {i18n} = useDocusaurusContext();
|
const {i18n} = useDocusaurusContext();
|
||||||
const allDocsData = useAllDocsData();
|
const allDocsData = useAllDocsData();
|
||||||
const activePluginAndVersion = useActivePluginAndVersion();
|
const activePluginAndVersion = useActivePluginAndVersion();
|
20
packages/docusaurus-theme-search-algolia/copyUntypedFiles.js
Normal file
20
packages/docusaurus-theme-search-algolia/copyUntypedFiles.js
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const path = require('path');
|
||||||
|
const fs = require('fs-extra');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copy all untyped and static assets files to lib.
|
||||||
|
*/
|
||||||
|
const srcDir = path.resolve(__dirname, 'src');
|
||||||
|
const libDir = path.resolve(__dirname, 'lib');
|
||||||
|
fs.copySync(srcDir, libDir, {
|
||||||
|
filter(filepath) {
|
||||||
|
return !/__tests__/.test(filepath) && !/\.tsx?$/.test(filepath);
|
||||||
|
},
|
||||||
|
});
|
|
@ -2,7 +2,8 @@
|
||||||
"name": "@docusaurus/theme-search-algolia",
|
"name": "@docusaurus/theme-search-algolia",
|
||||||
"version": "2.0.0-beta.9",
|
"version": "2.0.0-beta.9",
|
||||||
"description": "Algolia search component for Docusaurus.",
|
"description": "Algolia search component for Docusaurus.",
|
||||||
"main": "src/index.js",
|
"main": "lib/index.js",
|
||||||
|
"types": "src/theme-search-algolia.d.ts",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"access": "public"
|
"access": "public"
|
||||||
},
|
},
|
||||||
|
@ -12,6 +13,12 @@
|
||||||
"directory": "packages/docusaurus-theme-search-algolia"
|
"directory": "packages/docusaurus-theme-search-algolia"
|
||||||
},
|
},
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"scripts": {
|
||||||
|
"build": "yarn build:server && yarn build:browser && yarn build:copy",
|
||||||
|
"build:server": "tsc --project tsconfig.server.json",
|
||||||
|
"build:browser": "tsc --project tsconfig.browser.json",
|
||||||
|
"build:copy": "node copyUntypedFiles.js"
|
||||||
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@docsearch/react": "^3.0.0-alpha.39",
|
"@docsearch/react": "^3.0.0-alpha.39",
|
||||||
"@docusaurus/core": "2.0.0-beta.9",
|
"@docusaurus/core": "2.0.0-beta.9",
|
||||||
|
@ -24,6 +31,10 @@
|
||||||
"eta": "^1.12.3",
|
"eta": "^1.12.3",
|
||||||
"lodash": "^4.17.20"
|
"lodash": "^4.17.20"
|
||||||
},
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@docusaurus/module-type-aliases": "2.0.0-beta.9",
|
||||||
|
"fs-extra": "^10.0.0"
|
||||||
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react": "^16.8.4 || ^17.0.0",
|
"react": "^16.8.4 || ^17.0.0",
|
||||||
"react-dom": "^16.8.4 || ^17.0.0"
|
"react-dom": "^16.8.4 || ^17.0.0"
|
||||||
|
|
|
@ -5,26 +5,33 @@
|
||||||
* LICENSE file in the root directory of this source tree.
|
* LICENSE file in the root directory of this source tree.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const path = require('path');
|
import path from 'path';
|
||||||
const fs = require('fs');
|
import fs from 'fs';
|
||||||
const eta = require('eta');
|
import {defaultConfig, compile} from 'eta';
|
||||||
const {normalizeUrl, getSwizzledComponent} = require('@docusaurus/utils');
|
import {normalizeUrl, getSwizzledComponent} from '@docusaurus/utils';
|
||||||
const openSearchTemplate = require('./templates/opensearch');
|
import openSearchTemplate from './templates/opensearch';
|
||||||
const {validateThemeConfig} = require('./validateThemeConfig');
|
import {memoize} from 'lodash';
|
||||||
const {memoize} = require('lodash');
|
|
||||||
|
import type {DocusaurusContext, Plugin} from '@docusaurus/types';
|
||||||
|
|
||||||
const getCompiledOpenSearchTemplate = memoize(() => {
|
const getCompiledOpenSearchTemplate = memoize(() => {
|
||||||
return eta.compile(openSearchTemplate.trim());
|
return compile(openSearchTemplate.trim());
|
||||||
});
|
});
|
||||||
|
|
||||||
function renderOpenSearchTemplate(data) {
|
function renderOpenSearchTemplate(data: {
|
||||||
|
title: string;
|
||||||
|
url: string;
|
||||||
|
favicon: string | null;
|
||||||
|
}) {
|
||||||
const compiled = getCompiledOpenSearchTemplate();
|
const compiled = getCompiledOpenSearchTemplate();
|
||||||
return compiled(data, eta.defaultConfig);
|
return compiled(data, defaultConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
const OPEN_SEARCH_FILENAME = 'opensearch.xml';
|
const OPEN_SEARCH_FILENAME = 'opensearch.xml';
|
||||||
|
|
||||||
function theme(context) {
|
export default function theme(
|
||||||
|
context: DocusaurusContext & {baseUrl: string},
|
||||||
|
): Plugin<void> {
|
||||||
const {
|
const {
|
||||||
baseUrl,
|
baseUrl,
|
||||||
siteConfig: {title, url, favicon},
|
siteConfig: {title, url, favicon},
|
||||||
|
@ -37,12 +44,16 @@ function theme(context) {
|
||||||
return {
|
return {
|
||||||
name: 'docusaurus-theme-search-algolia',
|
name: 'docusaurus-theme-search-algolia',
|
||||||
|
|
||||||
|
getPathsToWatch() {
|
||||||
|
return [pagePath];
|
||||||
|
},
|
||||||
|
|
||||||
getThemePath() {
|
getThemePath() {
|
||||||
return path.resolve(__dirname, './theme');
|
return path.resolve(__dirname, './theme');
|
||||||
},
|
},
|
||||||
|
|
||||||
getPathsToWatch() {
|
getTypeScriptThemePath() {
|
||||||
return [pagePath];
|
return path.resolve(__dirname, '..', 'src', 'theme');
|
||||||
},
|
},
|
||||||
|
|
||||||
async contentLoaded({actions: {addRoute}}) {
|
async contentLoaded({actions: {addRoute}}) {
|
||||||
|
@ -87,6 +98,4 @@ function theme(context) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = theme;
|
export {validateThemeConfig} from './validateThemeConfig';
|
||||||
|
|
||||||
theme.validateThemeConfig = validateThemeConfig;
|
|
|
@ -5,7 +5,7 @@
|
||||||
* LICENSE file in the root directory of this source tree.
|
* LICENSE file in the root directory of this source tree.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
module.exports = `
|
export default `
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/"
|
<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/"
|
||||||
xmlns:moz="http://www.mozilla.org/2006/browser/search/">
|
xmlns:moz="http://www.mozilla.org/2006/browser/search/">
|
49
packages/docusaurus-theme-search-algolia/src/theme-search-algolia.d.ts
vendored
Normal file
49
packages/docusaurus-theme-search-algolia/src/theme-search-algolia.d.ts
vendored
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare module '@docusaurus/theme-search-algolia' {
|
||||||
|
export type Options = never;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '@theme/hooks/useSearchQuery' {
|
||||||
|
export interface SearchQuery {
|
||||||
|
searchQuery: string;
|
||||||
|
|
||||||
|
setSearchQuery(newSearchQuery: string): void;
|
||||||
|
|
||||||
|
generateSearchPageLink(targetSearchQuery: string): string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function useSearchQuery(): SearchQuery;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '@theme/hooks/useAlgoliaContextualFacetFilters' {
|
||||||
|
export type useAlgoliaContextualFacetFiltersReturns = [string, string[]];
|
||||||
|
|
||||||
|
export default function useAlgoliaContextualFacetFilters(): useAlgoliaContextualFacetFiltersReturns;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '@theme/SearchPage' {
|
||||||
|
const SearchPage: () => JSX.Element;
|
||||||
|
export default SearchPage;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '@theme/SearchMetadata' {
|
||||||
|
export type SearchMetadataProps = {
|
||||||
|
readonly locale?: string;
|
||||||
|
readonly version?: string;
|
||||||
|
readonly tag?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SearchMetadata: (props: SearchMetadataProps) => JSX.Element;
|
||||||
|
export default SearchMetadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '@theme/SearchBar' {
|
||||||
|
const SearchBar: () => JSX.Element;
|
||||||
|
export default SearchBar;
|
||||||
|
}
|
|
@ -4,6 +4,7 @@
|
||||||
* This source code is licensed under the MIT license found in the
|
* This source code is licensed under the MIT license found in the
|
||||||
* LICENSE file in the root directory of this source tree.
|
* LICENSE file in the root directory of this source tree.
|
||||||
*/
|
*/
|
||||||
|
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
||||||
|
|
||||||
import React, {useState, useRef, useCallback, useMemo} from 'react';
|
import React, {useState, useRef, useCallback, useMemo} from 'react';
|
||||||
import {createPortal} from 'react-dom';
|
import {createPortal} from 'react-dom';
|
||||||
|
@ -19,13 +20,42 @@ import useAlgoliaContextualFacetFilters from '@theme/hooks/useAlgoliaContextualF
|
||||||
import {translate} from '@docusaurus/Translate';
|
import {translate} from '@docusaurus/Translate';
|
||||||
import styles from './styles.module.css';
|
import styles from './styles.module.css';
|
||||||
|
|
||||||
let DocSearchModal = null;
|
import type {
|
||||||
|
DocSearchModal as DocSearchModalType,
|
||||||
|
DocSearchModalProps,
|
||||||
|
} from '@docsearch/react';
|
||||||
|
import type {
|
||||||
|
InternalDocSearchHit,
|
||||||
|
StoredDocSearchHit,
|
||||||
|
} from '@docsearch/react/dist/esm/types';
|
||||||
|
import type {AutocompleteState} from '@algolia/autocomplete-core';
|
||||||
|
|
||||||
function Hit({hit, children}) {
|
type DocSearchProps = Omit<
|
||||||
|
DocSearchModalProps,
|
||||||
|
'onClose' | 'initialScrollY'
|
||||||
|
> & {
|
||||||
|
contextualSearch?: string;
|
||||||
|
externalUrlRegex?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
let DocSearchModal: typeof DocSearchModalType | null = null;
|
||||||
|
|
||||||
|
function Hit({
|
||||||
|
hit,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
hit: InternalDocSearchHit | StoredDocSearchHit;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
return <Link to={hit.url}>{children}</Link>;
|
return <Link to={hit.url}>{children}</Link>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ResultsFooter({state, onClose}) {
|
type ResultsFooterProps = {
|
||||||
|
state: AutocompleteState<InternalDocSearchHit>;
|
||||||
|
onClose: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
function ResultsFooter({state, onClose}: ResultsFooterProps) {
|
||||||
const {generateSearchPageLink} = useSearchQuery();
|
const {generateSearchPageLink} = useSearchQuery();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -35,7 +65,11 @@ function ResultsFooter({state, onClose}) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DocSearch({contextualSearch, externalUrlRegex, ...props}) {
|
function DocSearch({
|
||||||
|
contextualSearch,
|
||||||
|
externalUrlRegex,
|
||||||
|
...props
|
||||||
|
}: DocSearchProps) {
|
||||||
const {siteMetadata} = useDocusaurusContext();
|
const {siteMetadata} = useDocusaurusContext();
|
||||||
|
|
||||||
const contextualSearchFacetFilters = useAlgoliaContextualFacetFilters();
|
const contextualSearchFacetFilters = useAlgoliaContextualFacetFilters();
|
||||||
|
@ -56,10 +90,12 @@ function DocSearch({contextualSearch, externalUrlRegex, ...props}) {
|
||||||
|
|
||||||
const {withBaseUrl} = useBaseUrlUtils();
|
const {withBaseUrl} = useBaseUrlUtils();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const searchContainer = useRef(null);
|
const searchContainer = useRef<HTMLDivElement | null>(null);
|
||||||
const searchButtonRef = useRef(null);
|
const searchButtonRef = useRef<HTMLButtonElement>(null);
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [initialQuery, setInitialQuery] = useState(null);
|
const [initialQuery, setInitialQuery] = useState<string | undefined>(
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
|
||||||
const importDocSearchModalIfNeeded = useCallback(() => {
|
const importDocSearchModalIfNeeded = useCallback(() => {
|
||||||
if (DocSearchModal) {
|
if (DocSearchModal) {
|
||||||
|
@ -67,7 +103,9 @@ function DocSearch({contextualSearch, externalUrlRegex, ...props}) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
|
// @ts-ignore
|
||||||
import('@docsearch/react/modal'),
|
import('@docsearch/react/modal'),
|
||||||
|
// @ts-ignore
|
||||||
import('@docsearch/react/style'),
|
import('@docsearch/react/style'),
|
||||||
import('./styles.css'),
|
import('./styles.css'),
|
||||||
]).then(([{DocSearchModal: Modal}]) => {
|
]).then(([{DocSearchModal: Modal}]) => {
|
||||||
|
@ -88,7 +126,7 @@ function DocSearch({contextualSearch, externalUrlRegex, ...props}) {
|
||||||
|
|
||||||
const onClose = useCallback(() => {
|
const onClose = useCallback(() => {
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
searchContainer.current.remove();
|
searchContainer.current?.remove();
|
||||||
}, [setIsOpen]);
|
}, [setIsOpen]);
|
||||||
|
|
||||||
const onInput = useCallback(
|
const onInput = useCallback(
|
||||||
|
@ -102,35 +140,38 @@ function DocSearch({contextualSearch, externalUrlRegex, ...props}) {
|
||||||
);
|
);
|
||||||
|
|
||||||
const navigator = useRef({
|
const navigator = useRef({
|
||||||
navigate({itemUrl}) {
|
navigate({itemUrl}: {itemUrl?: string}) {
|
||||||
// Algolia results could contain URL's from other domains which cannot
|
// Algolia results could contain URL's from other domains which cannot
|
||||||
// be served through history and should navigate with window.location
|
// be served through history and should navigate with window.location
|
||||||
if (isRegexpStringMatch(externalUrlRegex, itemUrl)) {
|
if (isRegexpStringMatch(externalUrlRegex, itemUrl)) {
|
||||||
window.location.href = itemUrl;
|
window.location.href = itemUrl!;
|
||||||
} else {
|
} else {
|
||||||
history.push(itemUrl);
|
history.push(itemUrl!);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}).current;
|
}).current;
|
||||||
|
|
||||||
const transformItems = useRef((items) => {
|
const transformItems = useRef<DocSearchModalProps['transformItems']>(
|
||||||
return items.map((item) => {
|
(items) => {
|
||||||
// If Algolia contains a external domain, we should navigate without relative URL
|
return items.map((item) => {
|
||||||
if (isRegexpStringMatch(externalUrlRegex, item.url)) {
|
// If Algolia contains a external domain, we should navigate without relative URL
|
||||||
return item;
|
if (isRegexpStringMatch(externalUrlRegex, item.url)) {
|
||||||
}
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
// We transform the absolute URL into a relative URL.
|
// We transform the absolute URL into a relative URL.
|
||||||
const url = new URL(item.url);
|
const url = new URL(item.url);
|
||||||
return {
|
return {
|
||||||
...item,
|
...item,
|
||||||
url: withBaseUrl(`${url.pathname}${url.hash}`),
|
url: withBaseUrl(`${url.pathname}${url.hash}`),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}).current;
|
},
|
||||||
|
).current;
|
||||||
|
|
||||||
const resultsFooterComponent = useMemo(
|
const resultsFooterComponent = useMemo(
|
||||||
() => (footerProps) => <ResultsFooter {...footerProps} onClose={onClose} />,
|
() => (footerProps: ResultsFooterProps) =>
|
||||||
|
<ResultsFooter {...footerProps} onClose={onClose} />,
|
||||||
[onClose],
|
[onClose],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -188,6 +229,8 @@ function DocSearch({contextualSearch, externalUrlRegex, ...props}) {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isOpen &&
|
{isOpen &&
|
||||||
|
DocSearchModal &&
|
||||||
|
searchContainer.current &&
|
||||||
createPortal(
|
createPortal(
|
||||||
<DocSearchModal
|
<DocSearchModal
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
|
@ -209,6 +252,7 @@ function DocSearch({contextualSearch, externalUrlRegex, ...props}) {
|
||||||
|
|
||||||
function SearchBar() {
|
function SearchBar() {
|
||||||
const {siteConfig} = useDocusaurusContext();
|
const {siteConfig} = useDocusaurusContext();
|
||||||
|
// @ts-ignore
|
||||||
return <DocSearch {...siteConfig.themeConfig.algolia} />;
|
return <DocSearch {...siteConfig.themeConfig.algolia} />;
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,9 +8,14 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import Head from '@docusaurus/Head';
|
import Head from '@docusaurus/Head';
|
||||||
|
import type {SearchMetadataProps} from '@theme/SearchMetadata';
|
||||||
|
|
||||||
// Override default/agnostic SearchMetas to use Algolia-specific metadata
|
// Override default/agnostic SearchMetas to use Algolia-specific metadata
|
||||||
export default function AlgoliaSearchMetadata({locale, version, tag}) {
|
function SearchMetadata({
|
||||||
|
locale,
|
||||||
|
version,
|
||||||
|
tag,
|
||||||
|
}: SearchMetadataProps): JSX.Element {
|
||||||
// Seems safe to consider here the locale is the language,
|
// Seems safe to consider here the locale is the language,
|
||||||
// as the existing docsearch:language filter is afaik a regular string-based filter
|
// as the existing docsearch:language filter is afaik a regular string-based filter
|
||||||
const language = locale;
|
const language = locale;
|
||||||
|
@ -23,3 +28,5 @@ export default function AlgoliaSearchMetadata({locale, version, tag}) {
|
||||||
</Head>
|
</Head>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default SearchMetadata;
|
|
@ -6,6 +6,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/* eslint-disable jsx-a11y/no-autofocus */
|
/* eslint-disable jsx-a11y/no-autofocus */
|
||||||
|
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
||||||
|
|
||||||
import React, {useEffect, useState, useReducer, useRef} from 'react';
|
import React, {useEffect, useState, useReducer, useRef} from 'react';
|
||||||
|
|
||||||
|
@ -32,7 +33,7 @@ import styles from './styles.module.css';
|
||||||
// Very simple pluralization: probably good enough for now
|
// Very simple pluralization: probably good enough for now
|
||||||
function useDocumentsFoundPlural() {
|
function useDocumentsFoundPlural() {
|
||||||
const {selectMessage} = usePluralForm();
|
const {selectMessage} = usePluralForm();
|
||||||
return (count) =>
|
return (count: number) =>
|
||||||
selectMessage(
|
selectMessage(
|
||||||
count,
|
count,
|
||||||
translate(
|
translate(
|
||||||
|
@ -52,14 +53,19 @@ function useDocsSearchVersionsHelpers() {
|
||||||
|
|
||||||
// State of the version select menus / algolia facet filters
|
// State of the version select menus / algolia facet filters
|
||||||
// docsPluginId -> versionName map
|
// docsPluginId -> versionName map
|
||||||
const [searchVersions, setSearchVersions] = useState(() => {
|
const [searchVersions, setSearchVersions] = useState<Record<string, string>>(
|
||||||
return Object.entries(allDocsData).reduce((acc, [pluginId, pluginData]) => {
|
() => {
|
||||||
return {...acc, [pluginId]: pluginData.versions[0].name};
|
return Object.entries(allDocsData).reduce(
|
||||||
}, {});
|
(acc, [pluginId, pluginData]) => {
|
||||||
});
|
return {...acc, [pluginId]: pluginData.versions[0].name};
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// Set the value of a single select menu
|
// Set the value of a single select menu
|
||||||
const setSearchVersion = (pluginId, searchVersion) =>
|
const setSearchVersion = (pluginId: string, searchVersion: string) =>
|
||||||
setSearchVersions((s) => ({...s, [pluginId]: searchVersion}));
|
setSearchVersions((s) => ({...s, [pluginId]: searchVersion}));
|
||||||
|
|
||||||
const versioningEnabled = Object.values(allDocsData).some(
|
const versioningEnabled = Object.values(allDocsData).some(
|
||||||
|
@ -75,7 +81,11 @@ function useDocsSearchVersionsHelpers() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// We want to display one select per versioned docs plugin instance
|
// We want to display one select per versioned docs plugin instance
|
||||||
const SearchVersionSelectList = ({docsSearchVersionsHelpers}) => {
|
const SearchVersionSelectList = ({
|
||||||
|
docsSearchVersionsHelpers,
|
||||||
|
}: {
|
||||||
|
docsSearchVersionsHelpers: ReturnType<typeof useDocsSearchVersionsHelpers>;
|
||||||
|
}) => {
|
||||||
const versionedPluginEntries = Object.entries(
|
const versionedPluginEntries = Object.entries(
|
||||||
docsSearchVersionsHelpers.allDocsData,
|
docsSearchVersionsHelpers.allDocsData,
|
||||||
)
|
)
|
||||||
|
@ -118,10 +128,32 @@ const SearchVersionSelectList = ({docsSearchVersionsHelpers}) => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
function SearchPage() {
|
type ResultDispatcherState = {
|
||||||
|
items: {
|
||||||
|
title: string;
|
||||||
|
url: string;
|
||||||
|
summary: string;
|
||||||
|
breadcrumbs: string[];
|
||||||
|
}[];
|
||||||
|
query: string | null;
|
||||||
|
totalResults: number | null;
|
||||||
|
totalPages: number | null;
|
||||||
|
lastPage: number | null;
|
||||||
|
hasMore: boolean | null;
|
||||||
|
loading: boolean | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ResultDispatcher =
|
||||||
|
| {type: 'reset'; value?: undefined}
|
||||||
|
| {type: 'loading'; value?: undefined}
|
||||||
|
| {type: 'update'; value: ResultDispatcherState}
|
||||||
|
| {type: 'advance'; value?: undefined};
|
||||||
|
|
||||||
|
function SearchPage(): JSX.Element {
|
||||||
const {
|
const {
|
||||||
siteConfig: {
|
siteConfig: {
|
||||||
themeConfig: {
|
themeConfig: {
|
||||||
|
// @ts-ignore
|
||||||
algolia: {appId, apiKey, indexName, externalUrlRegex},
|
algolia: {appId, apiKey, indexName, externalUrlRegex},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -131,7 +163,7 @@ function SearchPage() {
|
||||||
|
|
||||||
const docsSearchVersionsHelpers = useDocsSearchVersionsHelpers();
|
const docsSearchVersionsHelpers = useDocsSearchVersionsHelpers();
|
||||||
const {searchQuery, setSearchQuery} = useSearchQuery();
|
const {searchQuery, setSearchQuery} = useSearchQuery();
|
||||||
const initialSearchResultState = {
|
const initialSearchResultState: ResultDispatcherState = {
|
||||||
items: [],
|
items: [],
|
||||||
query: null,
|
query: null,
|
||||||
totalResults: null,
|
totalResults: null,
|
||||||
|
@ -141,8 +173,8 @@ function SearchPage() {
|
||||||
loading: null,
|
loading: null,
|
||||||
};
|
};
|
||||||
const [searchResultState, searchResultStateDispatcher] = useReducer(
|
const [searchResultState, searchResultStateDispatcher] = useReducer(
|
||||||
(prevState, {type, value: state}) => {
|
(prevState: ResultDispatcherState, data: ResultDispatcher) => {
|
||||||
switch (type) {
|
switch (data.type) {
|
||||||
case 'reset': {
|
case 'reset': {
|
||||||
return initialSearchResultState;
|
return initialSearchResultState;
|
||||||
}
|
}
|
||||||
|
@ -150,24 +182,24 @@ function SearchPage() {
|
||||||
return {...prevState, loading: true};
|
return {...prevState, loading: true};
|
||||||
}
|
}
|
||||||
case 'update': {
|
case 'update': {
|
||||||
if (searchQuery !== state.query) {
|
if (searchQuery !== data.value.query) {
|
||||||
return prevState;
|
return prevState;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...data.value,
|
||||||
items:
|
items:
|
||||||
state.lastPage === 0
|
data.value.lastPage === 0
|
||||||
? state.items
|
? data.value.items
|
||||||
: prevState.items.concat(state.items),
|
: prevState.items.concat(data.value.items),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case 'advance': {
|
case 'advance': {
|
||||||
const hasMore = prevState.totalPages > prevState.lastPage + 1;
|
const hasMore = prevState.totalPages! > prevState.lastPage! + 1;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...prevState,
|
...prevState,
|
||||||
lastPage: hasMore ? prevState.lastPage + 1 : prevState.lastPage,
|
lastPage: hasMore ? prevState.lastPage! + 1 : prevState.lastPage,
|
||||||
hasMore,
|
hasMore,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -193,7 +225,7 @@ function SearchPage() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const sanitizeValue = (value) => {
|
const sanitizeValue = (value: string) => {
|
||||||
return value.replace(
|
return value.replace(
|
||||||
/algolia-docsearch-suggestion--highlight/g,
|
/algolia-docsearch-suggestion--highlight/g,
|
||||||
'search-result-match',
|
'search-result-match',
|
||||||
|
@ -212,7 +244,7 @@ function SearchPage() {
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: titles.pop(),
|
title: titles.pop()!,
|
||||||
url: isRegexpStringMatch(externalUrlRegex, parsedURL.href)
|
url: isRegexpStringMatch(externalUrlRegex, parsedURL.href)
|
||||||
? parsedURL.href
|
? parsedURL.href
|
||||||
: parsedURL.pathname + parsedURL.hash,
|
: parsedURL.pathname + parsedURL.hash,
|
||||||
|
@ -239,7 +271,7 @@ function SearchPage() {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const [loaderRef, setLoaderRef] = useState(null);
|
const [loaderRef, setLoaderRef] = useState<HTMLDivElement | null>(null);
|
||||||
const prevY = useRef(0);
|
const prevY = useRef(0);
|
||||||
const observer = useRef(
|
const observer = useRef(
|
||||||
ExecutionEnvironment.canUseDOM &&
|
ExecutionEnvironment.canUseDOM &&
|
||||||
|
@ -278,7 +310,7 @@ function SearchPage() {
|
||||||
description: 'The search page title for empty query',
|
description: 'The search page title for empty query',
|
||||||
});
|
});
|
||||||
|
|
||||||
const makeSearch = useDynamicCallback((page = 0) => {
|
const makeSearch = useDynamicCallback((page: number = 0) => {
|
||||||
algoliaHelper.addDisjunctiveFacetRefinement('docusaurus_tag', 'default');
|
algoliaHelper.addDisjunctiveFacetRefinement('docusaurus_tag', 'default');
|
||||||
algoliaHelper.addDisjunctiveFacetRefinement('language', currentLocale);
|
algoliaHelper.addDisjunctiveFacetRefinement('language', currentLocale);
|
||||||
|
|
||||||
|
@ -299,9 +331,11 @@ function SearchPage() {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
const currentObserver = observer.current;
|
const currentObserver = observer.current;
|
||||||
|
if (currentObserver) {
|
||||||
currentObserver.observe(loaderRef);
|
currentObserver.observe(loaderRef);
|
||||||
return () => currentObserver.unobserve(loaderRef);
|
return () => currentObserver.unobserve(loaderRef);
|
||||||
|
}
|
||||||
|
return () => true;
|
||||||
}, [loaderRef]);
|
}, [loaderRef]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
|
@ -5,10 +5,11 @@
|
||||||
* LICENSE file in the root directory of this source tree.
|
* LICENSE file in the root directory of this source tree.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import useContextualSearchFilters from '@theme/hooks/useContextualSearchFilters';
|
import type {useAlgoliaContextualFacetFiltersReturns} from '@theme/hooks/useAlgoliaContextualFacetFilters';
|
||||||
|
import {useContextualSearchFilters} from '@docusaurus/theme-common';
|
||||||
|
|
||||||
// Translate search-engine agnostic search filters to Algolia search filters
|
// Translate search-engine agnostic search filters to Algolia search filters
|
||||||
export default function useAlgoliaContextualFacetFilters() {
|
export default function useAlgoliaContextualFacetFilters(): useAlgoliaContextualFacetFiltersReturns {
|
||||||
const {locale, tags} = useContextualSearchFilters();
|
const {locale, tags} = useContextualSearchFilters();
|
||||||
|
|
||||||
// seems safe to convert locale->language, see AlgoliaSearchMetadata comment
|
// seems safe to convert locale->language, see AlgoliaSearchMetadata comment
|
|
@ -8,10 +8,11 @@
|
||||||
import {useHistory} from '@docusaurus/router';
|
import {useHistory} from '@docusaurus/router';
|
||||||
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
|
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
|
||||||
import {useCallback, useEffect, useState} from 'react';
|
import {useCallback, useEffect, useState} from 'react';
|
||||||
|
import type {SearchQuery} from '@theme/hooks/useSearchQuery';
|
||||||
|
|
||||||
const SEARCH_PARAM_QUERY = 'q';
|
const SEARCH_PARAM_QUERY = 'q';
|
||||||
|
|
||||||
function useSearchQuery() {
|
function useSearchQuery(): SearchQuery {
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const {
|
const {
|
||||||
siteConfig: {baseUrl},
|
siteConfig: {baseUrl},
|
||||||
|
@ -28,7 +29,7 @@ function useSearchQuery() {
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const setSearchQuery = useCallback(
|
const setSearchQuery = useCallback(
|
||||||
(newSearchQuery) => {
|
(newSearchQuery: string) => {
|
||||||
const searchParams = new URLSearchParams(window.location.search);
|
const searchParams = new URLSearchParams(window.location.search);
|
||||||
|
|
||||||
if (newSearchQuery) {
|
if (newSearchQuery) {
|
||||||
|
@ -46,7 +47,7 @@ function useSearchQuery() {
|
||||||
);
|
);
|
||||||
|
|
||||||
const generateSearchPageLink = useCallback(
|
const generateSearchPageLink = useCallback(
|
||||||
(targetSearchQuery) => {
|
(targetSearchQuery: string) => {
|
||||||
// Refer to https://github.com/facebook/docusaurus/pull/2838
|
// Refer to https://github.com/facebook/docusaurus/pull/2838
|
||||||
return `${baseUrl}search?q=${encodeURIComponent(targetSearchQuery)}`;
|
return `${baseUrl}search?q=${encodeURIComponent(targetSearchQuery)}`;
|
||||||
},
|
},
|
10
packages/docusaurus-theme-search-algolia/src/types.d.ts
vendored
Normal file
10
packages/docusaurus-theme-search-algolia/src/types.d.ts
vendored
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/// <reference types="@docusaurus/module-type-aliases" />
|
||||||
|
/// <reference types="@docusaurus/theme-common" />
|
||||||
|
/// <reference types="@docusaurus/theme-classic" />
|
|
@ -5,7 +5,8 @@
|
||||||
* LICENSE file in the root directory of this source tree.
|
* LICENSE file in the root directory of this source tree.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const {Joi} = require('@docusaurus/utils-validation');
|
import {Joi} from '@docusaurus/utils-validation';
|
||||||
|
import type {ThemeConfig, Validate, ValidationResult} from '@docusaurus/types';
|
||||||
|
|
||||||
const DEFAULT_CONFIG = {
|
const DEFAULT_CONFIG = {
|
||||||
contextualSearch: false, // future: maybe we want to enable this by default
|
contextualSearch: false, // future: maybe we want to enable this by default
|
||||||
|
@ -18,7 +19,7 @@ const DEFAULT_CONFIG = {
|
||||||
};
|
};
|
||||||
exports.DEFAULT_CONFIG = DEFAULT_CONFIG;
|
exports.DEFAULT_CONFIG = DEFAULT_CONFIG;
|
||||||
|
|
||||||
const Schema = Joi.object({
|
export const Schema = Joi.object({
|
||||||
algolia: Joi.object({
|
algolia: Joi.object({
|
||||||
// Docusaurus attributes
|
// Docusaurus attributes
|
||||||
contextualSearch: Joi.boolean().default(DEFAULT_CONFIG.contextualSearch),
|
contextualSearch: Joi.boolean().default(DEFAULT_CONFIG.contextualSearch),
|
||||||
|
@ -35,11 +36,13 @@ const Schema = Joi.object({
|
||||||
.required()
|
.required()
|
||||||
.unknown(), // DocSearch 3 is still alpha: don't validate the rest for now
|
.unknown(), // DocSearch 3 is still alpha: don't validate the rest for now
|
||||||
});
|
});
|
||||||
exports.Schema = Schema;
|
|
||||||
|
|
||||||
exports.validateThemeConfig = function validateThemeConfig({
|
export function validateThemeConfig({
|
||||||
validate,
|
validate,
|
||||||
themeConfig,
|
themeConfig,
|
||||||
}) {
|
}: {
|
||||||
|
validate: Validate<ThemeConfig>;
|
||||||
|
themeConfig: ThemeConfig;
|
||||||
|
}): ValidationResult<ThemeConfig> {
|
||||||
return validate(Schema, themeConfig);
|
return validate(Schema, themeConfig);
|
||||||
};
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "esnext",
|
||||||
|
"jsx": "react-native"
|
||||||
|
},
|
||||||
|
"include": ["src/theme/", "src/*.d.ts"]
|
||||||
|
}
|
9
packages/docusaurus-theme-search-algolia/tsconfig.json
Normal file
9
packages/docusaurus-theme-search-algolia/tsconfig.json
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"extends": "../../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"lib": ["DOM", "ES2019"],
|
||||||
|
"rootDir": "src",
|
||||||
|
"baseUrl": "src",
|
||||||
|
"outDir": "lib"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"include": ["src/*.ts", "src/templates/*.ts"]
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue