@docusaurus/theme-search-algolia 2.0.0-beta.1 → 2.0.0-beta.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/copyUntypedFiles.js +20 -0
- package/lib/index.d.ts +11 -0
- package/lib/index.js +85 -0
- package/lib/templates/opensearch.d.ts +8 -0
- package/lib/templates/opensearch.js +23 -0
- package/lib/theme/SearchBar/index.d.ts +9 -0
- package/lib/theme/SearchBar/index.js +153 -0
- package/lib/theme/SearchBar/styles.css +21 -0
- package/lib/theme/SearchBar/styles.module.css +20 -0
- package/lib/theme/SearchMetadata/index.d.ts +10 -0
- package/lib/theme/SearchMetadata/index.js +20 -0
- package/lib/theme/SearchPage/index.d.ts +9 -0
- package/lib/theme/SearchPage/index.js +299 -0
- package/lib/theme/SearchPage/styles.module.css +119 -0
- package/lib/theme/hooks/useAlgoliaContextualFacetFilters.d.ts +8 -0
- package/lib/theme/hooks/useAlgoliaContextualFacetFilters.js +15 -0
- package/lib/theme/hooks/useSearchQuery.d.ts +9 -0
- package/lib/theme/hooks/useSearchQuery.js +43 -0
- package/lib/validateThemeConfig.d.ts +13 -0
- package/lib/validateThemeConfig.js +39 -0
- package/package.json +24 -12
- package/src/__tests__/validateThemeConfig.test.js +14 -0
- package/src/{index.js → index.ts} +37 -19
- package/src/templates/{opensearch.js → opensearch.ts} +4 -2
- package/src/theme/SearchBar/{index.js → index.tsx} +78 -26
- package/src/theme/{SearchMetadatas/index.js → SearchMetadata/index.tsx} +9 -2
- package/src/theme/SearchPage/{index.js → index.tsx} +81 -51
- package/src/theme/hooks/{useAlgoliaContextualFacetFilters.js → useAlgoliaContextualFacetFilters.ts} +4 -3
- package/src/theme/hooks/useSearchQuery.ts +63 -0
- package/src/theme-search-algolia.d.ts +47 -0
- package/src/types.d.ts +10 -0
- package/src/{validateThemeConfig.js → validateThemeConfig.ts} +10 -7
- package/tsconfig.browser.json +8 -0
- package/tsconfig.json +9 -0
- package/tsconfig.server.json +4 -0
- package/src/theme/hooks/useSearchQuery.js +0 -44
|
@@ -5,29 +5,38 @@
|
|
|
5
5
|
* LICENSE file in the root directory of this source tree.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import fs from 'fs';
|
|
10
|
+
import {defaultConfig, compile} from 'eta';
|
|
11
|
+
import {normalizeUrl, getSwizzledComponent} from '@docusaurus/utils';
|
|
12
|
+
import {readDefaultCodeTranslationMessages} from '@docusaurus/theme-translations';
|
|
13
|
+
import openSearchTemplate from './templates/opensearch';
|
|
14
|
+
import {memoize} from 'lodash';
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
return eta.compile(openSearchTemplate.trim());
|
|
18
|
-
});
|
|
16
|
+
import type {DocusaurusContext, Plugin} from '@docusaurus/types';
|
|
19
17
|
|
|
20
|
-
|
|
18
|
+
const getCompiledOpenSearchTemplate = memoize(() =>
|
|
19
|
+
compile(openSearchTemplate.trim()),
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
function renderOpenSearchTemplate(data: {
|
|
23
|
+
title: string;
|
|
24
|
+
url: string;
|
|
25
|
+
favicon: string | null;
|
|
26
|
+
}) {
|
|
21
27
|
const compiled = getCompiledOpenSearchTemplate();
|
|
22
|
-
return compiled(data,
|
|
28
|
+
return compiled(data, defaultConfig);
|
|
23
29
|
}
|
|
24
30
|
|
|
25
31
|
const OPEN_SEARCH_FILENAME = 'opensearch.xml';
|
|
26
32
|
|
|
27
|
-
function theme(
|
|
33
|
+
export default function theme(
|
|
34
|
+
context: DocusaurusContext & {baseUrl: string},
|
|
35
|
+
): Plugin<void> {
|
|
28
36
|
const {
|
|
29
37
|
baseUrl,
|
|
30
38
|
siteConfig: {title, url, favicon},
|
|
39
|
+
i18n: {currentLocale},
|
|
31
40
|
} = context;
|
|
32
41
|
const pageComponent = './theme/SearchPage/index.js';
|
|
33
42
|
const pagePath =
|
|
@@ -37,12 +46,23 @@ function theme(context) {
|
|
|
37
46
|
return {
|
|
38
47
|
name: 'docusaurus-theme-search-algolia',
|
|
39
48
|
|
|
49
|
+
getPathsToWatch() {
|
|
50
|
+
return [pagePath];
|
|
51
|
+
},
|
|
52
|
+
|
|
40
53
|
getThemePath() {
|
|
41
54
|
return path.resolve(__dirname, './theme');
|
|
42
55
|
},
|
|
43
56
|
|
|
44
|
-
|
|
45
|
-
return
|
|
57
|
+
getTypeScriptThemePath() {
|
|
58
|
+
return path.resolve(__dirname, '..', 'src', 'theme');
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
getDefaultCodeTranslationMessages() {
|
|
62
|
+
return readDefaultCodeTranslationMessages({
|
|
63
|
+
locale: currentLocale,
|
|
64
|
+
name: 'theme-search-algolia',
|
|
65
|
+
});
|
|
46
66
|
},
|
|
47
67
|
|
|
48
68
|
async contentLoaded({actions: {addRoute}}) {
|
|
@@ -60,7 +80,7 @@ function theme(context) {
|
|
|
60
80
|
renderOpenSearchTemplate({
|
|
61
81
|
title,
|
|
62
82
|
url: url + baseUrl,
|
|
63
|
-
favicon: normalizeUrl([url, baseUrl, favicon]),
|
|
83
|
+
favicon: favicon ? normalizeUrl([url, baseUrl, favicon]) : null,
|
|
64
84
|
}),
|
|
65
85
|
);
|
|
66
86
|
} catch (err) {
|
|
@@ -87,6 +107,4 @@ function theme(context) {
|
|
|
87
107
|
};
|
|
88
108
|
}
|
|
89
109
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
theme.validateThemeConfig = validateThemeConfig;
|
|
110
|
+
export {validateThemeConfig} from './validateThemeConfig';
|
|
@@ -5,14 +5,16 @@
|
|
|
5
5
|
* LICENSE file in the root directory of this source tree.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
export default `
|
|
9
9
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
10
10
|
<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/"
|
|
11
11
|
xmlns:moz="http://www.mozilla.org/2006/browser/search/">
|
|
12
12
|
<ShortName><%= it.title %></ShortName>
|
|
13
13
|
<Description>Search <%= it.title %></Description>
|
|
14
14
|
<InputEncoding>UTF-8</InputEncoding>
|
|
15
|
-
|
|
15
|
+
<% if (it.favicon) { _%>
|
|
16
|
+
<Image width="16" height="16" type="image/x-icon"><%= it.favicon %></Image>
|
|
17
|
+
<% } _%>
|
|
16
18
|
<Url type="text/html" method="get" template="<%= it.url %>search?q={searchTerms}"/>
|
|
17
19
|
<Url type="application/opensearchdescription+xml" rel="self" template="<%= it.url %>opensearch.xml" />
|
|
18
20
|
<moz:SearchForm><%= it.url %></moz:SearchForm>
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* This source code is licensed under the MIT license found in the
|
|
5
5
|
* LICENSE file in the root directory of this source tree.
|
|
6
6
|
*/
|
|
7
|
+
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
|
7
8
|
|
|
8
9
|
import React, {useState, useRef, useCallback, useMemo} from 'react';
|
|
9
10
|
import {createPortal} from 'react-dom';
|
|
@@ -13,18 +14,48 @@ import {useBaseUrlUtils} from '@docusaurus/useBaseUrl';
|
|
|
13
14
|
import Link from '@docusaurus/Link';
|
|
14
15
|
import Head from '@docusaurus/Head';
|
|
15
16
|
import useSearchQuery from '@theme/hooks/useSearchQuery';
|
|
17
|
+
import {isRegexpStringMatch} from '@docusaurus/theme-common';
|
|
16
18
|
import {DocSearchButton, useDocSearchKeyboardEvents} from '@docsearch/react';
|
|
17
19
|
import useAlgoliaContextualFacetFilters from '@theme/hooks/useAlgoliaContextualFacetFilters';
|
|
18
20
|
import {translate} from '@docusaurus/Translate';
|
|
19
21
|
import styles from './styles.module.css';
|
|
20
22
|
|
|
21
|
-
|
|
23
|
+
import type {
|
|
24
|
+
DocSearchModal as DocSearchModalType,
|
|
25
|
+
DocSearchModalProps,
|
|
26
|
+
} from '@docsearch/react';
|
|
27
|
+
import type {
|
|
28
|
+
InternalDocSearchHit,
|
|
29
|
+
StoredDocSearchHit,
|
|
30
|
+
} from '@docsearch/react/dist/esm/types';
|
|
31
|
+
import type {AutocompleteState} from '@algolia/autocomplete-core';
|
|
22
32
|
|
|
23
|
-
|
|
33
|
+
type DocSearchProps = Omit<
|
|
34
|
+
DocSearchModalProps,
|
|
35
|
+
'onClose' | 'initialScrollY'
|
|
36
|
+
> & {
|
|
37
|
+
contextualSearch?: string;
|
|
38
|
+
externalUrlRegex?: string;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
let DocSearchModal: typeof DocSearchModalType | null = null;
|
|
42
|
+
|
|
43
|
+
function Hit({
|
|
44
|
+
hit,
|
|
45
|
+
children,
|
|
46
|
+
}: {
|
|
47
|
+
hit: InternalDocSearchHit | StoredDocSearchHit;
|
|
48
|
+
children: React.ReactNode;
|
|
49
|
+
}) {
|
|
24
50
|
return <Link to={hit.url}>{children}</Link>;
|
|
25
51
|
}
|
|
26
52
|
|
|
27
|
-
|
|
53
|
+
type ResultsFooterProps = {
|
|
54
|
+
state: AutocompleteState<InternalDocSearchHit>;
|
|
55
|
+
onClose: () => void;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
function ResultsFooter({state, onClose}: ResultsFooterProps) {
|
|
28
59
|
const {generateSearchPageLink} = useSearchQuery();
|
|
29
60
|
|
|
30
61
|
return (
|
|
@@ -34,7 +65,11 @@ function ResultsFooter({state, onClose}) {
|
|
|
34
65
|
);
|
|
35
66
|
}
|
|
36
67
|
|
|
37
|
-
function DocSearch({
|
|
68
|
+
function DocSearch({
|
|
69
|
+
contextualSearch,
|
|
70
|
+
externalUrlRegex,
|
|
71
|
+
...props
|
|
72
|
+
}: DocSearchProps) {
|
|
38
73
|
const {siteMetadata} = useDocusaurusContext();
|
|
39
74
|
|
|
40
75
|
const contextualSearchFacetFilters = useAlgoliaContextualFacetFilters();
|
|
@@ -55,10 +90,12 @@ function DocSearch({contextualSearch, ...props}) {
|
|
|
55
90
|
|
|
56
91
|
const {withBaseUrl} = useBaseUrlUtils();
|
|
57
92
|
const history = useHistory();
|
|
58
|
-
const searchContainer = useRef(null);
|
|
59
|
-
const searchButtonRef = useRef(null);
|
|
93
|
+
const searchContainer = useRef<HTMLDivElement | null>(null);
|
|
94
|
+
const searchButtonRef = useRef<HTMLButtonElement>(null);
|
|
60
95
|
const [isOpen, setIsOpen] = useState(false);
|
|
61
|
-
const [initialQuery, setInitialQuery] = useState(
|
|
96
|
+
const [initialQuery, setInitialQuery] = useState<string | undefined>(
|
|
97
|
+
undefined,
|
|
98
|
+
);
|
|
62
99
|
|
|
63
100
|
const importDocSearchModalIfNeeded = useCallback(() => {
|
|
64
101
|
if (DocSearchModal) {
|
|
@@ -66,7 +103,9 @@ function DocSearch({contextualSearch, ...props}) {
|
|
|
66
103
|
}
|
|
67
104
|
|
|
68
105
|
return Promise.all([
|
|
106
|
+
// @ts-ignore
|
|
69
107
|
import('@docsearch/react/modal'),
|
|
108
|
+
// @ts-ignore
|
|
70
109
|
import('@docsearch/react/style'),
|
|
71
110
|
import('./styles.css'),
|
|
72
111
|
]).then(([{DocSearchModal: Modal}]) => {
|
|
@@ -87,7 +126,7 @@ function DocSearch({contextualSearch, ...props}) {
|
|
|
87
126
|
|
|
88
127
|
const onClose = useCallback(() => {
|
|
89
128
|
setIsOpen(false);
|
|
90
|
-
searchContainer.current
|
|
129
|
+
searchContainer.current?.remove();
|
|
91
130
|
}, [setIsOpen]);
|
|
92
131
|
|
|
93
132
|
const onInput = useCallback(
|
|
@@ -101,28 +140,38 @@ function DocSearch({contextualSearch, ...props}) {
|
|
|
101
140
|
);
|
|
102
141
|
|
|
103
142
|
const navigator = useRef({
|
|
104
|
-
navigate({itemUrl}) {
|
|
105
|
-
|
|
143
|
+
navigate({itemUrl}: {itemUrl?: string}) {
|
|
144
|
+
// Algolia results could contain URL's from other domains which cannot
|
|
145
|
+
// be served through history and should navigate with window.location
|
|
146
|
+
if (isRegexpStringMatch(externalUrlRegex, itemUrl)) {
|
|
147
|
+
window.location.href = itemUrl!;
|
|
148
|
+
} else {
|
|
149
|
+
history.push(itemUrl!);
|
|
150
|
+
}
|
|
106
151
|
},
|
|
107
152
|
}).current;
|
|
108
153
|
|
|
109
|
-
const transformItems = useRef(
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
154
|
+
const transformItems = useRef<DocSearchModalProps['transformItems']>(
|
|
155
|
+
(items) =>
|
|
156
|
+
items.map((item) => {
|
|
157
|
+
// If Algolia contains a external domain, we should navigate without relative URL
|
|
158
|
+
if (isRegexpStringMatch(externalUrlRegex, item.url)) {
|
|
159
|
+
return item;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// We transform the absolute URL into a relative URL.
|
|
163
|
+
const url = new URL(item.url);
|
|
164
|
+
return {
|
|
165
|
+
...item,
|
|
166
|
+
url: withBaseUrl(`${url.pathname}${url.hash}`),
|
|
167
|
+
};
|
|
168
|
+
}),
|
|
169
|
+
).current;
|
|
123
170
|
|
|
124
171
|
const resultsFooterComponent = useMemo(
|
|
125
|
-
|
|
172
|
+
// eslint-disable-next-line react/no-unstable-nested-components
|
|
173
|
+
() => (footerProps: ResultsFooterProps) =>
|
|
174
|
+
<ResultsFooter {...footerProps} onClose={onClose} />,
|
|
126
175
|
[onClose],
|
|
127
176
|
);
|
|
128
177
|
|
|
@@ -180,6 +229,8 @@ function DocSearch({contextualSearch, ...props}) {
|
|
|
180
229
|
</div>
|
|
181
230
|
|
|
182
231
|
{isOpen &&
|
|
232
|
+
DocSearchModal &&
|
|
233
|
+
searchContainer.current &&
|
|
183
234
|
createPortal(
|
|
184
235
|
<DocSearchModal
|
|
185
236
|
onClose={onClose}
|
|
@@ -199,8 +250,9 @@ function DocSearch({contextualSearch, ...props}) {
|
|
|
199
250
|
);
|
|
200
251
|
}
|
|
201
252
|
|
|
202
|
-
function SearchBar() {
|
|
253
|
+
function SearchBar(): JSX.Element {
|
|
203
254
|
const {siteConfig} = useDocusaurusContext();
|
|
255
|
+
// @ts-ignore
|
|
204
256
|
return <DocSearch {...siteConfig.themeConfig.algolia} />;
|
|
205
257
|
}
|
|
206
258
|
|
|
@@ -8,9 +8,14 @@
|
|
|
8
8
|
import React from 'react';
|
|
9
9
|
|
|
10
10
|
import Head from '@docusaurus/Head';
|
|
11
|
+
import type {SearchMetadataProps} from '@theme/SearchMetadata';
|
|
11
12
|
|
|
12
|
-
// Override default/agnostic SearchMetas to use Algolia-specific
|
|
13
|
-
|
|
13
|
+
// Override default/agnostic SearchMetas to use Algolia-specific metadata
|
|
14
|
+
function SearchMetadata({
|
|
15
|
+
locale,
|
|
16
|
+
version,
|
|
17
|
+
tag,
|
|
18
|
+
}: SearchMetadataProps): JSX.Element {
|
|
14
19
|
// Seems safe to consider here the locale is the language,
|
|
15
20
|
// as the existing docsearch:language filter is afaik a regular string-based filter
|
|
16
21
|
const language = locale;
|
|
@@ -23,3 +28,5 @@ export default function AlgoliaSearchMetadatas({locale, version, tag}) {
|
|
|
23
28
|
</Head>
|
|
24
29
|
);
|
|
25
30
|
}
|
|
31
|
+
|
|
32
|
+
export default SearchMetadata;
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
/* eslint-disable jsx-a11y/no-autofocus */
|
|
9
|
+
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
|
9
10
|
|
|
10
11
|
import React, {useEffect, useState, useReducer, useRef} from 'react';
|
|
11
12
|
|
|
@@ -16,7 +17,12 @@ import clsx from 'clsx';
|
|
|
16
17
|
import Head from '@docusaurus/Head';
|
|
17
18
|
import Link from '@docusaurus/Link';
|
|
18
19
|
import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment';
|
|
19
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
useTitleFormatter,
|
|
22
|
+
usePluralForm,
|
|
23
|
+
isRegexpStringMatch,
|
|
24
|
+
useDynamicCallback,
|
|
25
|
+
} from '@docusaurus/theme-common';
|
|
20
26
|
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
|
|
21
27
|
import {useAllDocsData} from '@theme/hooks/useDocs';
|
|
22
28
|
import useSearchQuery from '@theme/hooks/useSearchQuery';
|
|
@@ -27,7 +33,7 @@ import styles from './styles.module.css';
|
|
|
27
33
|
// Very simple pluralization: probably good enough for now
|
|
28
34
|
function useDocumentsFoundPlural() {
|
|
29
35
|
const {selectMessage} = usePluralForm();
|
|
30
|
-
return (count) =>
|
|
36
|
+
return (count: number) =>
|
|
31
37
|
selectMessage(
|
|
32
38
|
count,
|
|
33
39
|
translate(
|
|
@@ -47,14 +53,19 @@ function useDocsSearchVersionsHelpers() {
|
|
|
47
53
|
|
|
48
54
|
// State of the version select menus / algolia facet filters
|
|
49
55
|
// docsPluginId -> versionName map
|
|
50
|
-
const [searchVersions, setSearchVersions] = useState(
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
56
|
+
const [searchVersions, setSearchVersions] = useState<Record<string, string>>(
|
|
57
|
+
() =>
|
|
58
|
+
Object.entries(allDocsData).reduce(
|
|
59
|
+
(acc, [pluginId, pluginData]) => ({
|
|
60
|
+
...acc,
|
|
61
|
+
[pluginId]: pluginData.versions[0].name,
|
|
62
|
+
}),
|
|
63
|
+
{},
|
|
64
|
+
),
|
|
65
|
+
);
|
|
55
66
|
|
|
56
67
|
// Set the value of a single select menu
|
|
57
|
-
const setSearchVersion = (pluginId, searchVersion) =>
|
|
68
|
+
const setSearchVersion = (pluginId: string, searchVersion: string) =>
|
|
58
69
|
setSearchVersions((s) => ({...s, [pluginId]: searchVersion}));
|
|
59
70
|
|
|
60
71
|
const versioningEnabled = Object.values(allDocsData).some(
|
|
@@ -70,7 +81,11 @@ function useDocsSearchVersionsHelpers() {
|
|
|
70
81
|
}
|
|
71
82
|
|
|
72
83
|
// We want to display one select per versioned docs plugin instance
|
|
73
|
-
|
|
84
|
+
function SearchVersionSelectList({
|
|
85
|
+
docsSearchVersionsHelpers,
|
|
86
|
+
}: {
|
|
87
|
+
docsSearchVersionsHelpers: ReturnType<typeof useDocsSearchVersionsHelpers>;
|
|
88
|
+
}) {
|
|
74
89
|
const versionedPluginEntries = Object.entries(
|
|
75
90
|
docsSearchVersionsHelpers.allDocsData,
|
|
76
91
|
)
|
|
@@ -111,13 +126,35 @@ const SearchVersionSelectList = ({docsSearchVersionsHelpers}) => {
|
|
|
111
126
|
})}
|
|
112
127
|
</div>
|
|
113
128
|
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
type ResultDispatcherState = {
|
|
132
|
+
items: {
|
|
133
|
+
title: string;
|
|
134
|
+
url: string;
|
|
135
|
+
summary: string;
|
|
136
|
+
breadcrumbs: string[];
|
|
137
|
+
}[];
|
|
138
|
+
query: string | null;
|
|
139
|
+
totalResults: number | null;
|
|
140
|
+
totalPages: number | null;
|
|
141
|
+
lastPage: number | null;
|
|
142
|
+
hasMore: boolean | null;
|
|
143
|
+
loading: boolean | null;
|
|
114
144
|
};
|
|
115
145
|
|
|
116
|
-
|
|
146
|
+
type ResultDispatcher =
|
|
147
|
+
| {type: 'reset'; value?: undefined}
|
|
148
|
+
| {type: 'loading'; value?: undefined}
|
|
149
|
+
| {type: 'update'; value: ResultDispatcherState}
|
|
150
|
+
| {type: 'advance'; value?: undefined};
|
|
151
|
+
|
|
152
|
+
function SearchPage(): JSX.Element {
|
|
117
153
|
const {
|
|
118
154
|
siteConfig: {
|
|
119
155
|
themeConfig: {
|
|
120
|
-
|
|
156
|
+
// @ts-ignore
|
|
157
|
+
algolia: {appId, apiKey, indexName, externalUrlRegex},
|
|
121
158
|
},
|
|
122
159
|
},
|
|
123
160
|
i18n: {currentLocale},
|
|
@@ -125,9 +162,8 @@ function SearchPage() {
|
|
|
125
162
|
const documentsFoundPlural = useDocumentsFoundPlural();
|
|
126
163
|
|
|
127
164
|
const docsSearchVersionsHelpers = useDocsSearchVersionsHelpers();
|
|
128
|
-
const {
|
|
129
|
-
const
|
|
130
|
-
const initialSearchResultState = {
|
|
165
|
+
const {searchQuery, setSearchQuery} = useSearchQuery();
|
|
166
|
+
const initialSearchResultState: ResultDispatcherState = {
|
|
131
167
|
items: [],
|
|
132
168
|
query: null,
|
|
133
169
|
totalResults: null,
|
|
@@ -137,8 +173,8 @@ function SearchPage() {
|
|
|
137
173
|
loading: null,
|
|
138
174
|
};
|
|
139
175
|
const [searchResultState, searchResultStateDispatcher] = useReducer(
|
|
140
|
-
(prevState
|
|
141
|
-
switch (type) {
|
|
176
|
+
(prevState: ResultDispatcherState, data: ResultDispatcher) => {
|
|
177
|
+
switch (data.type) {
|
|
142
178
|
case 'reset': {
|
|
143
179
|
return initialSearchResultState;
|
|
144
180
|
}
|
|
@@ -146,24 +182,24 @@ function SearchPage() {
|
|
|
146
182
|
return {...prevState, loading: true};
|
|
147
183
|
}
|
|
148
184
|
case 'update': {
|
|
149
|
-
if (searchQuery !==
|
|
185
|
+
if (searchQuery !== data.value.query) {
|
|
150
186
|
return prevState;
|
|
151
187
|
}
|
|
152
188
|
|
|
153
189
|
return {
|
|
154
|
-
...
|
|
190
|
+
...data.value,
|
|
155
191
|
items:
|
|
156
|
-
|
|
157
|
-
?
|
|
158
|
-
: prevState.items.concat(
|
|
192
|
+
data.value.lastPage === 0
|
|
193
|
+
? data.value.items
|
|
194
|
+
: prevState.items.concat(data.value.items),
|
|
159
195
|
};
|
|
160
196
|
}
|
|
161
197
|
case 'advance': {
|
|
162
|
-
const hasMore = prevState.totalPages > prevState.lastPage + 1;
|
|
198
|
+
const hasMore = prevState.totalPages! > prevState.lastPage! + 1;
|
|
163
199
|
|
|
164
200
|
return {
|
|
165
201
|
...prevState,
|
|
166
|
-
lastPage: hasMore ? prevState.lastPage + 1 : prevState.lastPage,
|
|
202
|
+
lastPage: hasMore ? prevState.lastPage! + 1 : prevState.lastPage,
|
|
167
203
|
hasMore,
|
|
168
204
|
};
|
|
169
205
|
}
|
|
@@ -173,6 +209,7 @@ function SearchPage() {
|
|
|
173
209
|
},
|
|
174
210
|
initialSearchResultState,
|
|
175
211
|
);
|
|
212
|
+
|
|
176
213
|
const algoliaClient = algoliaSearch(appId, apiKey);
|
|
177
214
|
const algoliaHelper = algoliaSearchHelper(algoliaClient, indexName, {
|
|
178
215
|
hitsPerPage: 15,
|
|
@@ -188,12 +225,11 @@ function SearchPage() {
|
|
|
188
225
|
return;
|
|
189
226
|
}
|
|
190
227
|
|
|
191
|
-
const sanitizeValue = (value) =>
|
|
192
|
-
|
|
228
|
+
const sanitizeValue = (value: string) =>
|
|
229
|
+
value.replace(
|
|
193
230
|
/algolia-docsearch-suggestion--highlight/g,
|
|
194
231
|
'search-result-match',
|
|
195
232
|
);
|
|
196
|
-
};
|
|
197
233
|
|
|
198
234
|
const items = hits.map(
|
|
199
235
|
({
|
|
@@ -201,14 +237,16 @@ function SearchPage() {
|
|
|
201
237
|
_highlightResult: {hierarchy},
|
|
202
238
|
_snippetResult: snippet = {},
|
|
203
239
|
}) => {
|
|
204
|
-
const
|
|
205
|
-
const titles = Object.keys(hierarchy).map((key) =>
|
|
206
|
-
|
|
207
|
-
|
|
240
|
+
const parsedURL = new URL(url);
|
|
241
|
+
const titles = Object.keys(hierarchy).map((key) =>
|
|
242
|
+
sanitizeValue(hierarchy[key].value),
|
|
243
|
+
);
|
|
208
244
|
|
|
209
245
|
return {
|
|
210
|
-
title: titles.pop()
|
|
211
|
-
url:
|
|
246
|
+
title: titles.pop()!,
|
|
247
|
+
url: isRegexpStringMatch(externalUrlRegex, parsedURL.href)
|
|
248
|
+
? parsedURL.href
|
|
249
|
+
: parsedURL.pathname + parsedURL.hash,
|
|
212
250
|
summary: snippet.content
|
|
213
251
|
? `${sanitizeValue(snippet.content.value)}...`
|
|
214
252
|
: '',
|
|
@@ -232,7 +270,7 @@ function SearchPage() {
|
|
|
232
270
|
},
|
|
233
271
|
);
|
|
234
272
|
|
|
235
|
-
const [loaderRef, setLoaderRef] = useState(null);
|
|
273
|
+
const [loaderRef, setLoaderRef] = useState<HTMLDivElement | null>(null);
|
|
236
274
|
const prevY = useRef(0);
|
|
237
275
|
const observer = useRef(
|
|
238
276
|
ExecutionEnvironment.canUseDOM &&
|
|
@@ -271,7 +309,7 @@ function SearchPage() {
|
|
|
271
309
|
description: 'The search page title for empty query',
|
|
272
310
|
});
|
|
273
311
|
|
|
274
|
-
const makeSearch = (page = 0) => {
|
|
312
|
+
const makeSearch = useDynamicCallback((page: number = 0) => {
|
|
275
313
|
algoliaHelper.addDisjunctiveFacetRefinement('docusaurus_tag', 'default');
|
|
276
314
|
algoliaHelper.addDisjunctiveFacetRefinement('language', currentLocale);
|
|
277
315
|
|
|
@@ -285,23 +323,21 @@ function SearchPage() {
|
|
|
285
323
|
);
|
|
286
324
|
|
|
287
325
|
algoliaHelper.setQuery(searchQuery).setPage(page).search();
|
|
288
|
-
};
|
|
326
|
+
});
|
|
289
327
|
|
|
290
328
|
useEffect(() => {
|
|
291
329
|
if (!loaderRef) {
|
|
292
330
|
return undefined;
|
|
293
331
|
}
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
332
|
+
const currentObserver = observer.current;
|
|
333
|
+
if (currentObserver) {
|
|
334
|
+
currentObserver.observe(loaderRef);
|
|
335
|
+
return () => currentObserver.unobserve(loaderRef);
|
|
336
|
+
}
|
|
337
|
+
return () => true;
|
|
300
338
|
}, [loaderRef]);
|
|
301
339
|
|
|
302
340
|
useEffect(() => {
|
|
303
|
-
updateSearchPath(searchQuery);
|
|
304
|
-
|
|
305
341
|
searchResultStateDispatcher({type: 'reset'});
|
|
306
342
|
|
|
307
343
|
if (searchQuery) {
|
|
@@ -311,7 +347,7 @@ function SearchPage() {
|
|
|
311
347
|
makeSearch();
|
|
312
348
|
}, 300);
|
|
313
349
|
}
|
|
314
|
-
}, [searchQuery, docsSearchVersionsHelpers.searchVersions]);
|
|
350
|
+
}, [searchQuery, docsSearchVersionsHelpers.searchVersions, makeSearch]);
|
|
315
351
|
|
|
316
352
|
useEffect(() => {
|
|
317
353
|
if (!searchResultState.lastPage || searchResultState.lastPage === 0) {
|
|
@@ -319,13 +355,7 @@ function SearchPage() {
|
|
|
319
355
|
}
|
|
320
356
|
|
|
321
357
|
makeSearch(searchResultState.lastPage);
|
|
322
|
-
}, [searchResultState.lastPage]);
|
|
323
|
-
|
|
324
|
-
useEffect(() => {
|
|
325
|
-
if (searchValue && searchValue !== searchQuery) {
|
|
326
|
-
setSearchQuery(searchValue);
|
|
327
|
-
}
|
|
328
|
-
}, [searchValue]);
|
|
358
|
+
}, [makeSearch, searchResultState.lastPage]);
|
|
329
359
|
|
|
330
360
|
return (
|
|
331
361
|
<Layout wrapperClassName="search-page-wrapper">
|
package/src/theme/hooks/{useAlgoliaContextualFacetFilters.js → useAlgoliaContextualFacetFilters.ts}
RENAMED
|
@@ -5,13 +5,14 @@
|
|
|
5
5
|
* LICENSE file in the root directory of this source tree.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import
|
|
8
|
+
import type {useAlgoliaContextualFacetFiltersReturns} from '@theme/hooks/useAlgoliaContextualFacetFilters';
|
|
9
|
+
import {useContextualSearchFilters} from '@docusaurus/theme-common';
|
|
9
10
|
|
|
10
11
|
// Translate search-engine agnostic search filters to Algolia search filters
|
|
11
|
-
export default function useAlgoliaContextualFacetFilters() {
|
|
12
|
+
export default function useAlgoliaContextualFacetFilters(): useAlgoliaContextualFacetFiltersReturns {
|
|
12
13
|
const {locale, tags} = useContextualSearchFilters();
|
|
13
14
|
|
|
14
|
-
// seems safe to convert locale->language, see
|
|
15
|
+
// seems safe to convert locale->language, see AlgoliaSearchMetadata comment
|
|
15
16
|
const languageFilter = `language:${locale}`;
|
|
16
17
|
|
|
17
18
|
const tagsFilter = tags.map((tag) => `docusaurus_tag:${tag}`);
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {useHistory} from '@docusaurus/router';
|
|
9
|
+
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
|
|
10
|
+
import {useCallback, useEffect, useState} from 'react';
|
|
11
|
+
import type {SearchQuery} from '@theme/hooks/useSearchQuery';
|
|
12
|
+
|
|
13
|
+
const SEARCH_PARAM_QUERY = 'q';
|
|
14
|
+
|
|
15
|
+
function useSearchQuery(): SearchQuery {
|
|
16
|
+
const history = useHistory();
|
|
17
|
+
const {
|
|
18
|
+
siteConfig: {baseUrl},
|
|
19
|
+
} = useDocusaurusContext();
|
|
20
|
+
|
|
21
|
+
const [searchQuery, setSearchQueryState] = useState('');
|
|
22
|
+
|
|
23
|
+
// Init search query just after React hydration
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
const searchQueryStringValue =
|
|
26
|
+
new URLSearchParams(window.location.search).get(SEARCH_PARAM_QUERY) ?? '';
|
|
27
|
+
|
|
28
|
+
setSearchQueryState(searchQueryStringValue);
|
|
29
|
+
}, []);
|
|
30
|
+
|
|
31
|
+
const setSearchQuery = useCallback(
|
|
32
|
+
(newSearchQuery: string) => {
|
|
33
|
+
const searchParams = new URLSearchParams(window.location.search);
|
|
34
|
+
|
|
35
|
+
if (newSearchQuery) {
|
|
36
|
+
searchParams.set(SEARCH_PARAM_QUERY, newSearchQuery);
|
|
37
|
+
} else {
|
|
38
|
+
searchParams.delete(SEARCH_PARAM_QUERY);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
history.replace({
|
|
42
|
+
search: searchParams.toString(),
|
|
43
|
+
});
|
|
44
|
+
setSearchQueryState(newSearchQuery);
|
|
45
|
+
},
|
|
46
|
+
[history],
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
const generateSearchPageLink = useCallback(
|
|
50
|
+
(targetSearchQuery: string) =>
|
|
51
|
+
// Refer to https://github.com/facebook/docusaurus/pull/2838
|
|
52
|
+
`${baseUrl}search?q=${encodeURIComponent(targetSearchQuery)}`,
|
|
53
|
+
[baseUrl],
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
searchQuery,
|
|
58
|
+
setSearchQuery,
|
|
59
|
+
generateSearchPageLink,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export default useSearchQuery;
|