@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
@@ -4,26 +4,29 @@
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
-
8
7
  /* eslint-disable jsx-a11y/no-autofocus */
9
-
10
8
  import React, {useEffect, useState, useReducer, useRef} from 'react';
11
-
9
+ import clsx from 'clsx';
12
10
  import algoliaSearch from 'algoliasearch/lite';
13
11
  import algoliaSearchHelper from 'algoliasearch-helper';
14
- import clsx from 'clsx';
15
-
16
12
  import Head from '@docusaurus/Head';
17
13
  import Link from '@docusaurus/Link';
18
14
  import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment';
19
- import {useTitleFormatter, usePluralForm} from '@docusaurus/theme-common';
15
+ import {
16
+ HtmlClassNameProvider,
17
+ usePluralForm,
18
+ isRegexpStringMatch,
19
+ useEvent,
20
+ } from '@docusaurus/theme-common';
21
+ import {
22
+ useTitleFormatter,
23
+ useSearchPage,
24
+ } from '@docusaurus/theme-common/internal';
20
25
  import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
21
- import {useAllDocsData} from '@theme/hooks/useDocs';
22
- import useSearchQuery from '@theme/hooks/useSearchQuery';
23
- import Layout from '@theme/Layout';
26
+ import {useAllDocsData} from '@docusaurus/plugin-content-docs/client';
24
27
  import Translate, {translate} from '@docusaurus/Translate';
28
+ import Layout from '@theme/Layout';
25
29
  import styles from './styles.module.css';
26
-
27
30
  // Very simple pluralization: probably good enough for now
28
31
  function useDocumentsFoundPlural() {
29
32
  const {selectMessage} = usePluralForm();
@@ -41,26 +44,25 @@ function useDocumentsFoundPlural() {
41
44
  ),
42
45
  );
43
46
  }
44
-
45
47
  function useDocsSearchVersionsHelpers() {
46
48
  const allDocsData = useAllDocsData();
47
-
48
49
  // State of the version select menus / algolia facet filters
49
50
  // docsPluginId -> versionName map
50
- const [searchVersions, setSearchVersions] = useState(() => {
51
- return Object.entries(allDocsData).reduce((acc, [pluginId, pluginData]) => {
52
- return {...acc, [pluginId]: pluginData.versions[0].name};
53
- }, {});
54
- });
55
-
51
+ const [searchVersions, setSearchVersions] = useState(() =>
52
+ Object.entries(allDocsData).reduce(
53
+ (acc, [pluginId, pluginData]) => ({
54
+ ...acc,
55
+ [pluginId]: pluginData.versions[0].name,
56
+ }),
57
+ {},
58
+ ),
59
+ );
56
60
  // Set the value of a single select menu
57
61
  const setSearchVersion = (pluginId, searchVersion) =>
58
62
  setSearchVersions((s) => ({...s, [pluginId]: searchVersion}));
59
-
60
63
  const versioningEnabled = Object.values(allDocsData).some(
61
64
  (docsData) => docsData.versions.length > 1,
62
65
  );
63
-
64
66
  return {
65
67
  allDocsData,
66
68
  versioningEnabled,
@@ -68,15 +70,13 @@ function useDocsSearchVersionsHelpers() {
68
70
  setSearchVersion,
69
71
  };
70
72
  }
71
-
72
73
  // We want to display one select per versioned docs plugin instance
73
- const SearchVersionSelectList = ({docsSearchVersionsHelpers}) => {
74
+ function SearchVersionSelectList({docsSearchVersionsHelpers}) {
74
75
  const versionedPluginEntries = Object.entries(
75
76
  docsSearchVersionsHelpers.allDocsData,
76
77
  )
77
78
  // Do not show a version select for unversioned docs plugin instances
78
79
  .filter(([, docsData]) => docsData.versions.length > 1);
79
-
80
80
  return (
81
81
  <div
82
82
  className={clsx(
@@ -111,22 +111,18 @@ const SearchVersionSelectList = ({docsSearchVersionsHelpers}) => {
111
111
  })}
112
112
  </div>
113
113
  );
114
- };
115
-
116
- function SearchPage() {
114
+ }
115
+ function SearchPageContent() {
117
116
  const {
118
- siteConfig: {
119
- themeConfig: {
120
- algolia: {appId, apiKey, indexName},
121
- },
122
- },
117
+ siteConfig: {themeConfig},
123
118
  i18n: {currentLocale},
124
119
  } = useDocusaurusContext();
120
+ const {
121
+ algolia: {appId, apiKey, indexName, externalUrlRegex},
122
+ } = themeConfig;
125
123
  const documentsFoundPlural = useDocumentsFoundPlural();
126
-
127
124
  const docsSearchVersionsHelpers = useDocsSearchVersionsHelpers();
128
- const {searchValue, updateSearchPath} = useSearchQuery();
129
- const [searchQuery, setSearchQuery] = useState(searchValue);
125
+ const {searchQuery, setSearchQuery} = useSearchPage();
130
126
  const initialSearchResultState = {
131
127
  items: [],
132
128
  query: null,
@@ -137,8 +133,8 @@ function SearchPage() {
137
133
  loading: null,
138
134
  };
139
135
  const [searchResultState, searchResultStateDispatcher] = useReducer(
140
- (prevState, {type, value: state}) => {
141
- switch (type) {
136
+ (prevState, data) => {
137
+ switch (data.type) {
142
138
  case 'reset': {
143
139
  return initialSearchResultState;
144
140
  }
@@ -146,21 +142,19 @@ function SearchPage() {
146
142
  return {...prevState, loading: true};
147
143
  }
148
144
  case 'update': {
149
- if (searchQuery !== state.query) {
145
+ if (searchQuery !== data.value.query) {
150
146
  return prevState;
151
147
  }
152
-
153
148
  return {
154
- ...state,
149
+ ...data.value,
155
150
  items:
156
- state.lastPage === 0
157
- ? state.items
158
- : prevState.items.concat(state.items),
151
+ data.value.lastPage === 0
152
+ ? data.value.items
153
+ : prevState.items.concat(data.value.items),
159
154
  };
160
155
  }
161
156
  case 'advance': {
162
157
  const hasMore = prevState.totalPages > prevState.lastPage + 1;
163
-
164
158
  return {
165
159
  ...prevState,
166
160
  lastPage: hasMore ? prevState.lastPage + 1 : prevState.lastPage,
@@ -179,36 +173,33 @@ function SearchPage() {
179
173
  advancedSyntax: true,
180
174
  disjunctiveFacets: ['language', 'docusaurus_tag'],
181
175
  });
182
-
183
176
  algoliaHelper.on(
184
177
  'result',
185
178
  ({results: {query, hits, page, nbHits, nbPages}}) => {
186
- if (query === '' || !(hits instanceof Array)) {
179
+ if (query === '' || !Array.isArray(hits)) {
187
180
  searchResultStateDispatcher({type: 'reset'});
188
181
  return;
189
182
  }
190
-
191
- const sanitizeValue = (value) => {
192
- return value.replace(
183
+ const sanitizeValue = (value) =>
184
+ value.replace(
193
185
  /algolia-docsearch-suggestion--highlight/g,
194
186
  'search-result-match',
195
187
  );
196
- };
197
-
198
188
  const items = hits.map(
199
189
  ({
200
190
  url,
201
191
  _highlightResult: {hierarchy},
202
192
  _snippetResult: snippet = {},
203
193
  }) => {
204
- const {pathname, hash} = new URL(url);
205
- const titles = Object.keys(hierarchy).map((key) => {
206
- return sanitizeValue(hierarchy[key].value);
207
- });
208
-
194
+ const parsedURL = new URL(url);
195
+ const titles = Object.keys(hierarchy).map((key) =>
196
+ sanitizeValue(hierarchy[key].value),
197
+ );
209
198
  return {
210
199
  title: titles.pop(),
211
- url: pathname + hash,
200
+ url: isRegexpStringMatch(externalUrlRegex, parsedURL.href)
201
+ ? parsedURL.href
202
+ : parsedURL.pathname + parsedURL.hash,
212
203
  summary: snippet.content
213
204
  ? `${sanitizeValue(snippet.content.value)}...`
214
205
  : '',
@@ -216,7 +207,6 @@ function SearchPage() {
216
207
  };
217
208
  },
218
209
  );
219
-
220
210
  searchResultStateDispatcher({
221
211
  type: 'update',
222
212
  value: {
@@ -231,28 +221,24 @@ function SearchPage() {
231
221
  });
232
222
  },
233
223
  );
234
-
235
224
  const [loaderRef, setLoaderRef] = useState(null);
236
225
  const prevY = useRef(0);
237
226
  const observer = useRef(
238
- ExecutionEnvironment.canUseDOM &&
227
+ ExecutionEnvironment.canUseIntersectionObserver &&
239
228
  new IntersectionObserver(
240
229
  (entries) => {
241
230
  const {
242
231
  isIntersecting,
243
232
  boundingClientRect: {y: currentY},
244
233
  } = entries[0];
245
-
246
234
  if (isIntersecting && prevY.current > currentY) {
247
235
  searchResultStateDispatcher({type: 'advance'});
248
236
  }
249
-
250
237
  prevY.current = currentY;
251
238
  },
252
239
  {threshold: 1},
253
240
  ),
254
241
  );
255
-
256
242
  const getTitle = () =>
257
243
  searchQuery
258
244
  ? translate(
@@ -270,11 +256,9 @@ function SearchPage() {
270
256
  message: 'Search the documentation',
271
257
  description: 'The search page title for empty query',
272
258
  });
273
-
274
- const makeSearch = (page = 0) => {
259
+ const makeSearch = useEvent((page = 0) => {
275
260
  algoliaHelper.addDisjunctiveFacetRefinement('docusaurus_tag', 'default');
276
261
  algoliaHelper.addDisjunctiveFacetRefinement('language', currentLocale);
277
-
278
262
  Object.entries(docsSearchVersionsHelpers.searchVersions).forEach(
279
263
  ([pluginId, searchVersion]) => {
280
264
  algoliaHelper.addDisjunctiveFacetRefinement(
@@ -283,52 +267,36 @@ function SearchPage() {
283
267
  );
284
268
  },
285
269
  );
286
-
287
270
  algoliaHelper.setQuery(searchQuery).setPage(page).search();
288
- };
289
-
271
+ });
290
272
  useEffect(() => {
291
273
  if (!loaderRef) {
292
274
  return undefined;
293
275
  }
294
-
295
- observer.current.observe(loaderRef);
296
-
297
- return () => {
298
- observer.current.unobserve(loaderRef);
299
- };
276
+ const currentObserver = observer.current;
277
+ if (currentObserver) {
278
+ currentObserver.observe(loaderRef);
279
+ return () => currentObserver.unobserve(loaderRef);
280
+ }
281
+ return () => true;
300
282
  }, [loaderRef]);
301
-
302
283
  useEffect(() => {
303
- updateSearchPath(searchQuery);
304
-
305
284
  searchResultStateDispatcher({type: 'reset'});
306
-
307
285
  if (searchQuery) {
308
286
  searchResultStateDispatcher({type: 'loading'});
309
-
310
287
  setTimeout(() => {
311
288
  makeSearch();
312
289
  }, 300);
313
290
  }
314
- }, [searchQuery, docsSearchVersionsHelpers.searchVersions]);
315
-
291
+ }, [searchQuery, docsSearchVersionsHelpers.searchVersions, makeSearch]);
316
292
  useEffect(() => {
317
293
  if (!searchResultState.lastPage || searchResultState.lastPage === 0) {
318
294
  return;
319
295
  }
320
-
321
296
  makeSearch(searchResultState.lastPage);
322
- }, [searchResultState.lastPage]);
323
-
324
- useEffect(() => {
325
- if (searchValue && searchValue !== searchQuery) {
326
- setSearchQuery(searchValue);
327
- }
328
- }, [searchValue]);
329
-
297
+ }, [makeSearch, searchResultState.lastPage]);
330
298
  return (
331
- <Layout wrapperClassName="search-page-wrapper">
299
+ <Layout>
332
300
  <Head>
333
301
  <title>{useTitleFormatter(getTitle())}</title>
334
302
  {/*
@@ -375,16 +343,19 @@ function SearchPage() {
375
343
  )}
376
344
  </form>
377
345
 
378
- <div className={clsx('row', 'margin-vert--sm')}>
346
+ <div className="row">
379
347
  <div className={clsx('col', 'col--8', styles.searchResultsColumn)}>
380
- {!!searchResultState.totalResults && (
381
- <strong>
382
- {documentsFoundPlural(searchResultState.totalResults)}
383
- </strong>
384
- )}
348
+ {!!searchResultState.totalResults &&
349
+ documentsFoundPlural(searchResultState.totalResults)}
385
350
  </div>
386
351
 
387
- <div className={clsx('col', 'col--4', styles.searchLogoColumn)}>
352
+ <div
353
+ className={clsx(
354
+ 'col',
355
+ 'col--4',
356
+ 'text--right',
357
+ styles.searchLogoColumn,
358
+ )}>
388
359
  <a
389
360
  target="_blank"
390
361
  rel="noopener noreferrer"
@@ -394,10 +365,7 @@ function SearchPage() {
394
365
  message: 'Search by Algolia',
395
366
  description: 'The ARIA label for Algolia mention',
396
367
  })}>
397
- <svg
398
- viewBox="0 0 168 24"
399
- className={styles.algoliaLogo}
400
- xmlns="http://www.w3.org/2000/svg">
368
+ <svg viewBox="0 0 168 24" className={styles.algoliaLogo}>
401
369
  <g fill="none">
402
370
  <path
403
371
  className={styles.algoliaLogoPathFill}
@@ -418,34 +386,32 @@ function SearchPage() {
418
386
  </div>
419
387
 
420
388
  {searchResultState.items.length > 0 ? (
421
- <section>
389
+ <main>
422
390
  {searchResultState.items.map(
423
391
  ({title, url, summary, breadcrumbs}, i) => (
424
392
  <article key={i} className={styles.searchResultItem}>
425
- <Link
426
- to={url}
427
- className={styles.searchResultItemHeading}
428
- dangerouslySetInnerHTML={{__html: title}}
429
- />
393
+ <h2 className={styles.searchResultItemHeading}>
394
+ <Link to={url} dangerouslySetInnerHTML={{__html: title}} />
395
+ </h2>
430
396
 
431
397
  {breadcrumbs.length > 0 && (
432
- <span className={styles.searchResultItemPath}>
433
- {breadcrumbs.map((html, index) => (
434
- <React.Fragment key={index}>
435
- {index !== 0 && (
436
- <span
437
- className={styles.searchResultItemPathSeparator}>
438
-
439
- </span>
440
- )}
441
- <span
398
+ <nav aria-label="breadcrumbs">
399
+ <ul
400
+ className={clsx(
401
+ 'breadcrumbs',
402
+ styles.searchResultItemPath,
403
+ )}>
404
+ {breadcrumbs.map((html, index) => (
405
+ <li
406
+ key={index}
407
+ className="breadcrumbs__item"
442
408
  // Developer provided the HTML, so assume it's safe.
443
409
  // eslint-disable-next-line react/no-danger
444
410
  dangerouslySetInnerHTML={{__html: html}}
445
411
  />
446
- </React.Fragment>
447
- ))}
448
- </span>
412
+ ))}
413
+ </ul>
414
+ </nav>
449
415
  )}
450
416
 
451
417
  {summary && (
@@ -459,7 +425,7 @@ function SearchPage() {
459
425
  </article>
460
426
  ),
461
427
  )}
462
- </section>
428
+ </main>
463
429
  ) : (
464
430
  [
465
431
  searchQuery && !searchResultState.loading && (
@@ -479,18 +445,21 @@ function SearchPage() {
479
445
 
480
446
  {searchResultState.hasMore && (
481
447
  <div className={styles.loader} ref={setLoaderRef}>
482
- <span>
483
- <Translate
484
- id="theme.SearchPage.fetchingNewResults"
485
- description="The paragraph for fetching new search results">
486
- Fetching new results...
487
- </Translate>
488
- </span>
448
+ <Translate
449
+ id="theme.SearchPage.fetchingNewResults"
450
+ description="The paragraph for fetching new search results">
451
+ Fetching new results...
452
+ </Translate>
489
453
  </div>
490
454
  )}
491
455
  </div>
492
456
  </Layout>
493
457
  );
494
458
  }
495
-
496
- export default SearchPage;
459
+ export default function SearchPage() {
460
+ return (
461
+ <HtmlClassNameProvider className="search-page-wrapper">
462
+ <SearchPageContent />
463
+ </HtmlClassNameProvider>
464
+ );
465
+ }
@@ -0,0 +1,119 @@
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
+ .searchQueryInput,
9
+ .searchVersionInput {
10
+ border-radius: var(--ifm-global-radius);
11
+ border: 2px solid var(--ifm-toc-border-color);
12
+ font: var(--ifm-font-size-base) var(--ifm-font-family-base);
13
+ padding: 0.8rem;
14
+ width: 100%;
15
+ background: var(--docsearch-searchbox-focus-background);
16
+ color: var(--docsearch-text-color);
17
+ margin-bottom: 0.5rem;
18
+ transition: border var(--ifm-transition-fast) ease;
19
+ }
20
+
21
+ .searchQueryInput:focus,
22
+ .searchVersionInput:focus {
23
+ border-color: var(--docsearch-primary-color);
24
+ outline: none;
25
+ }
26
+
27
+ .searchQueryInput::placeholder {
28
+ color: var(--docsearch-muted-color);
29
+ }
30
+
31
+ .searchResultsColumn {
32
+ font-size: 0.9rem;
33
+ font-weight: bold;
34
+ }
35
+
36
+ .algoliaLogo {
37
+ max-width: 150px;
38
+ }
39
+
40
+ .algoliaLogoPathFill {
41
+ fill: var(--ifm-font-color-base);
42
+ }
43
+
44
+ .searchResultItem {
45
+ padding: 1rem 0;
46
+ border-bottom: 1px solid var(--ifm-toc-border-color);
47
+ }
48
+
49
+ .searchResultItemHeading {
50
+ font-weight: 400;
51
+ margin-bottom: 0;
52
+ }
53
+
54
+ .searchResultItemPath {
55
+ font-size: 0.8rem;
56
+ color: var(--ifm-color-content-secondary);
57
+ --ifm-breadcrumb-separator-size-multiplier: 1;
58
+ }
59
+
60
+ .searchResultItemSummary {
61
+ margin: 0.5rem 0 0;
62
+ font-style: italic;
63
+ }
64
+
65
+ @media only screen and (max-width: 996px) {
66
+ .searchQueryColumn {
67
+ max-width: 60% !important;
68
+ }
69
+
70
+ .searchVersionColumn {
71
+ max-width: 40% !important;
72
+ }
73
+
74
+ .searchResultsColumn {
75
+ max-width: 60% !important;
76
+ }
77
+
78
+ .searchLogoColumn {
79
+ max-width: 40% !important;
80
+ padding-left: 0 !important;
81
+ }
82
+ }
83
+
84
+ @media screen and (max-width: 576px) {
85
+ .searchQueryColumn {
86
+ max-width: 100% !important;
87
+ }
88
+
89
+ .searchVersionColumn {
90
+ max-width: 100% !important;
91
+ padding-left: var(--ifm-spacing-horizontal) !important;
92
+ }
93
+ }
94
+
95
+ .loadingSpinner {
96
+ width: 3rem;
97
+ height: 3rem;
98
+ border: 0.4em solid #eee;
99
+ border-top-color: var(--ifm-color-primary);
100
+ border-radius: 50%;
101
+ animation: loading-spin 1s linear infinite;
102
+ margin: 0 auto;
103
+ }
104
+
105
+ @keyframes loading-spin {
106
+ 100% {
107
+ transform: rotate(360deg);
108
+ }
109
+ }
110
+
111
+ .loader {
112
+ margin-top: 2rem;
113
+ }
114
+
115
+ :global(.search-result-match) {
116
+ color: var(--docsearch-hit-color);
117
+ background: rgb(255 215 142 / 25%);
118
+ padding: 0.09em 0;
119
+ }
@@ -0,0 +1,11 @@
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
+ import type { DocSearchTranslations } from '@docsearch/react';
8
+ declare const translations: DocSearchTranslations & {
9
+ placeholder: string;
10
+ };
11
+ export default translations;