@docusaurus/theme-search-algolia 2.0.0-beta.ff31de0ff → 2.0.1

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.
Files changed (37) hide show
  1. package/lib/client/index.d.ts +7 -0
  2. package/lib/client/index.js +7 -0
  3. package/lib/client/useAlgoliaContextualFacetFilters.d.ts +7 -0
  4. package/lib/client/useAlgoliaContextualFacetFilters.js +15 -0
  5. package/lib/index.d.ts +9 -0
  6. package/lib/index.js +90 -0
  7. package/lib/templates/opensearch.d.ts +8 -0
  8. package/lib/templates/opensearch.js +23 -0
  9. package/lib/theme/SearchBar/index.d.ts +8 -0
  10. package/{src → lib}/theme/SearchBar/index.js +51 -60
  11. package/lib/theme/SearchBar/styles.css +21 -0
  12. package/lib/theme/SearchPage/index.d.ts +8 -0
  13. package/{src → lib}/theme/SearchPage/index.js +100 -131
  14. package/lib/theme/SearchPage/styles.module.css +119 -0
  15. package/lib/theme/SearchTranslations/index.d.ts +11 -0
  16. package/lib/theme/SearchTranslations/index.js +167 -0
  17. package/lib/validateThemeConfig.d.ts +15 -0
  18. package/lib/validateThemeConfig.js +44 -0
  19. package/package.json +43 -14
  20. package/src/client/index.ts +8 -0
  21. package/src/{theme/hooks/useAlgoliaContextualFacetFilters.js → client/useAlgoliaContextualFacetFilters.ts} +3 -3
  22. package/src/deps.d.ts +20 -0
  23. package/src/index.ts +116 -0
  24. package/src/templates/{opensearch.js → opensearch.ts} +7 -5
  25. package/src/theme/SearchBar/index.tsx +271 -0
  26. package/src/theme/SearchBar/styles.css +1 -0
  27. package/src/theme/SearchPage/index.tsx +535 -0
  28. package/src/theme/SearchPage/styles.module.css +15 -34
  29. package/src/theme/SearchTranslations/index.ts +172 -0
  30. package/src/theme-search-algolia.d.ts +42 -0
  31. package/src/types.d.ts +10 -0
  32. package/src/validateThemeConfig.ts +53 -0
  33. package/src/__tests__/validateThemeConfig.test.js +0 -121
  34. package/src/index.js +0 -92
  35. package/src/theme/SearchMetadatas/index.js +0 -25
  36. package/src/theme/hooks/useSearchQuery.js +0 -44
  37. package/src/validateThemeConfig.js +0 -45
@@ -0,0 +1,271 @@
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 React, {useState, useRef, useCallback, useMemo} from 'react';
9
+ import {createPortal} from 'react-dom';
10
+ import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
11
+ import {useHistory} from '@docusaurus/router';
12
+ import {useBaseUrlUtils} from '@docusaurus/useBaseUrl';
13
+ import Link from '@docusaurus/Link';
14
+ import Head from '@docusaurus/Head';
15
+ import {isRegexpStringMatch} from '@docusaurus/theme-common';
16
+ import {useSearchPage} from '@docusaurus/theme-common/internal';
17
+ import {DocSearchButton, useDocSearchKeyboardEvents} from '@docsearch/react';
18
+ import {useAlgoliaContextualFacetFilters} from '@docusaurus/theme-search-algolia/client';
19
+ import Translate from '@docusaurus/Translate';
20
+ import translations from '@theme/SearchTranslations';
21
+
22
+ import type {
23
+ DocSearchModal as DocSearchModalType,
24
+ DocSearchModalProps,
25
+ } from '@docsearch/react';
26
+ import type {
27
+ InternalDocSearchHit,
28
+ StoredDocSearchHit,
29
+ } from '@docsearch/react/dist/esm/types';
30
+ import type {SearchClient} from 'algoliasearch/lite';
31
+ import type {AutocompleteState} from '@algolia/autocomplete-core';
32
+
33
+ type DocSearchProps = Omit<
34
+ DocSearchModalProps,
35
+ 'onClose' | 'initialScrollY'
36
+ > & {
37
+ contextualSearch?: string;
38
+ externalUrlRegex?: string;
39
+ searchPagePath: boolean | string;
40
+ };
41
+
42
+ let DocSearchModal: typeof DocSearchModalType | null = null;
43
+
44
+ function Hit({
45
+ hit,
46
+ children,
47
+ }: {
48
+ hit: InternalDocSearchHit | StoredDocSearchHit;
49
+ children: React.ReactNode;
50
+ }) {
51
+ return <Link to={hit.url}>{children}</Link>;
52
+ }
53
+
54
+ type ResultsFooterProps = {
55
+ state: AutocompleteState<InternalDocSearchHit>;
56
+ onClose: () => void;
57
+ };
58
+
59
+ function ResultsFooter({state, onClose}: ResultsFooterProps) {
60
+ const {generateSearchPageLink} = useSearchPage();
61
+
62
+ return (
63
+ <Link to={generateSearchPageLink(state.query)} onClick={onClose}>
64
+ <Translate
65
+ id="theme.SearchBar.seeAll"
66
+ values={{count: state.context.nbHits}}>
67
+ {'See all {count} results'}
68
+ </Translate>
69
+ </Link>
70
+ );
71
+ }
72
+
73
+ type FacetFilters = Required<
74
+ Required<DocSearchProps>['searchParameters']
75
+ >['facetFilters'];
76
+
77
+ function mergeFacetFilters(f1: FacetFilters, f2: FacetFilters): FacetFilters {
78
+ const normalize = (
79
+ f: FacetFilters,
80
+ ): readonly string[] | readonly (string | readonly string[])[] =>
81
+ typeof f === 'string' ? [f] : f;
82
+ return [...normalize(f1), ...normalize(f2)] as FacetFilters;
83
+ }
84
+
85
+ function DocSearch({
86
+ contextualSearch,
87
+ externalUrlRegex,
88
+ ...props
89
+ }: DocSearchProps) {
90
+ const {siteMetadata} = useDocusaurusContext();
91
+
92
+ const contextualSearchFacetFilters =
93
+ useAlgoliaContextualFacetFilters() as FacetFilters;
94
+
95
+ const configFacetFilters: FacetFilters =
96
+ props.searchParameters?.facetFilters ?? [];
97
+
98
+ const facetFilters: FacetFilters = contextualSearch
99
+ ? // Merge contextual search filters with config filters
100
+ mergeFacetFilters(contextualSearchFacetFilters, configFacetFilters)
101
+ : // ... or use config facetFilters
102
+ configFacetFilters;
103
+
104
+ // We let user override default searchParameters if she wants to
105
+ const searchParameters: DocSearchProps['searchParameters'] = {
106
+ ...props.searchParameters,
107
+ facetFilters,
108
+ };
109
+
110
+ const {withBaseUrl} = useBaseUrlUtils();
111
+ const history = useHistory();
112
+ const searchContainer = useRef<HTMLDivElement | null>(null);
113
+ const searchButtonRef = useRef<HTMLButtonElement>(null);
114
+ const [isOpen, setIsOpen] = useState(false);
115
+ const [initialQuery, setInitialQuery] = useState<string | undefined>(
116
+ undefined,
117
+ );
118
+
119
+ const importDocSearchModalIfNeeded = useCallback(() => {
120
+ if (DocSearchModal) {
121
+ return Promise.resolve();
122
+ }
123
+
124
+ return Promise.all([
125
+ import('@docsearch/react/modal') as Promise<
126
+ typeof import('@docsearch/react')
127
+ >,
128
+ import('@docsearch/react/style'),
129
+ import('./styles.css'),
130
+ ]).then(([{DocSearchModal: Modal}]) => {
131
+ DocSearchModal = Modal;
132
+ });
133
+ }, []);
134
+
135
+ const onOpen = useCallback(() => {
136
+ importDocSearchModalIfNeeded().then(() => {
137
+ searchContainer.current = document.createElement('div');
138
+ document.body.insertBefore(
139
+ searchContainer.current,
140
+ document.body.firstChild,
141
+ );
142
+ setIsOpen(true);
143
+ });
144
+ }, [importDocSearchModalIfNeeded, setIsOpen]);
145
+
146
+ const onClose = useCallback(() => {
147
+ setIsOpen(false);
148
+ searchContainer.current?.remove();
149
+ }, [setIsOpen]);
150
+
151
+ const onInput = useCallback(
152
+ (event: KeyboardEvent) => {
153
+ importDocSearchModalIfNeeded().then(() => {
154
+ setIsOpen(true);
155
+ setInitialQuery(event.key);
156
+ });
157
+ },
158
+ [importDocSearchModalIfNeeded, setIsOpen, setInitialQuery],
159
+ );
160
+
161
+ const navigator = useRef({
162
+ navigate({itemUrl}: {itemUrl?: string}) {
163
+ // Algolia results could contain URL's from other domains which cannot
164
+ // be served through history and should navigate with window.location
165
+ if (isRegexpStringMatch(externalUrlRegex, itemUrl)) {
166
+ window.location.href = itemUrl!;
167
+ } else {
168
+ history.push(itemUrl!);
169
+ }
170
+ },
171
+ }).current;
172
+
173
+ const transformItems = useRef<DocSearchModalProps['transformItems']>(
174
+ (items) =>
175
+ items.map((item) => {
176
+ // If Algolia contains a external domain, we should navigate without
177
+ // relative URL
178
+ if (isRegexpStringMatch(externalUrlRegex, item.url)) {
179
+ return item;
180
+ }
181
+
182
+ // We transform the absolute URL into a relative URL.
183
+ const url = new URL(item.url);
184
+ return {
185
+ ...item,
186
+ url: withBaseUrl(`${url.pathname}${url.hash}`),
187
+ };
188
+ }),
189
+ ).current;
190
+
191
+ const resultsFooterComponent: DocSearchProps['resultsFooterComponent'] =
192
+ useMemo(
193
+ () =>
194
+ // eslint-disable-next-line react/no-unstable-nested-components
195
+ (footerProps: Omit<ResultsFooterProps, 'onClose'>): JSX.Element =>
196
+ <ResultsFooter {...footerProps} onClose={onClose} />,
197
+ [onClose],
198
+ );
199
+
200
+ const transformSearchClient = useCallback(
201
+ (searchClient: SearchClient) => {
202
+ searchClient.addAlgoliaAgent(
203
+ 'docusaurus',
204
+ siteMetadata.docusaurusVersion,
205
+ );
206
+
207
+ return searchClient;
208
+ },
209
+ [siteMetadata.docusaurusVersion],
210
+ );
211
+
212
+ useDocSearchKeyboardEvents({
213
+ isOpen,
214
+ onOpen,
215
+ onClose,
216
+ onInput,
217
+ searchButtonRef,
218
+ });
219
+
220
+ return (
221
+ <>
222
+ <Head>
223
+ {/* This hints the browser that the website will load data from Algolia,
224
+ and allows it to preconnect to the DocSearch cluster. It makes the first
225
+ query faster, especially on mobile. */}
226
+ <link
227
+ rel="preconnect"
228
+ href={`https://${props.appId}-dsn.algolia.net`}
229
+ crossOrigin="anonymous"
230
+ />
231
+ </Head>
232
+
233
+ <DocSearchButton
234
+ onTouchStart={importDocSearchModalIfNeeded}
235
+ onFocus={importDocSearchModalIfNeeded}
236
+ onMouseOver={importDocSearchModalIfNeeded}
237
+ onClick={onOpen}
238
+ ref={searchButtonRef}
239
+ translations={translations.button}
240
+ />
241
+
242
+ {isOpen &&
243
+ DocSearchModal &&
244
+ searchContainer.current &&
245
+ createPortal(
246
+ <DocSearchModal
247
+ onClose={onClose}
248
+ initialScrollY={window.scrollY}
249
+ initialQuery={initialQuery}
250
+ navigator={navigator}
251
+ transformItems={transformItems}
252
+ hitComponent={Hit}
253
+ transformSearchClient={transformSearchClient}
254
+ {...(props.searchPagePath && {
255
+ resultsFooterComponent,
256
+ })}
257
+ {...props}
258
+ searchParameters={searchParameters}
259
+ placeholder={translations.placeholder}
260
+ translations={translations.modal}
261
+ />,
262
+ searchContainer.current,
263
+ )}
264
+ </>
265
+ );
266
+ }
267
+
268
+ export default function SearchBar(): JSX.Element {
269
+ const {siteConfig} = useDocusaurusContext();
270
+ return <DocSearch {...(siteConfig.themeConfig.algolia as DocSearchProps)} />;
271
+ }
@@ -11,6 +11,7 @@
11
11
  }
12
12
 
13
13
  .DocSearch-Button {
14
+ margin: 0;
14
15
  transition: all var(--ifm-transition-fast)
15
16
  var(--ifm-transition-timing-default);
16
17
  }