GlobalSearch.tsx 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142
  1. import React, {
  2. useState, useCallback, useRef, useEffect, useMemo,
  3. } from 'react';
  4. import assert from 'assert';
  5. import { useTranslation } from 'next-i18next';
  6. import { useRouter } from 'next/router';
  7. import { IFocusable } from '~/client/interfaces/focusable';
  8. import { IPageWithSearchMeta } from '~/interfaces/search';
  9. import {
  10. useIsSearchScopeChildrenAsDefault, useIsSearchServiceReachable,
  11. } from '~/stores/context';
  12. import { useCurrentPagePath } from '~/stores/page';
  13. import { useGlobalSearchFormRef } from '~/stores/ui';
  14. import SearchForm from '../SearchForm';
  15. import styles from './GlobalSearch.module.scss';
  16. export type GlobalSearchProps = {
  17. dropup?: boolean,
  18. }
  19. export const GlobalSearch = (props: GlobalSearchProps): JSX.Element => {
  20. const { t } = useTranslation('commons');
  21. const { dropup } = props;
  22. const router = useRouter();
  23. const globalSearchFormRef = useRef<IFocusable>(null);
  24. useGlobalSearchFormRef(globalSearchFormRef);
  25. const { data: isSearchServiceReachable } = useIsSearchServiceReachable();
  26. const { data: isSearchScopeChildrenAsDefault } = useIsSearchScopeChildrenAsDefault();
  27. const { data: currentPagePath } = useCurrentPagePath();
  28. const [text, setText] = useState('');
  29. const [isScopeChildren, setScopeChildren] = useState<boolean|undefined>(isSearchScopeChildrenAsDefault ?? false);
  30. const [isFocused, setFocused] = useState<boolean>(false);
  31. useEffect(() => {
  32. setScopeChildren(isSearchScopeChildrenAsDefault);
  33. }, [isSearchScopeChildrenAsDefault]);
  34. const gotoPage = useCallback((data: IPageWithSearchMeta[]) => {
  35. assert(data.length > 0);
  36. const page = data[0].data; // should be single page selected
  37. // navigate to page
  38. if (page != null) {
  39. router.push(`/${page._id}`);
  40. }
  41. }, [router]);
  42. const search = useCallback(() => {
  43. const url = new URL(window.location.href);
  44. url.pathname = '/_search';
  45. // construct search query
  46. let q = text;
  47. if (isScopeChildren) {
  48. q += ` prefix:${currentPagePath ?? window.location.pathname}`;
  49. }
  50. url.searchParams.append('q', q);
  51. router.push(url.href);
  52. }, [currentPagePath, isScopeChildren, router, text]);
  53. const scopeLabel = isScopeChildren
  54. ? t('header_search_box.label.This tree')
  55. : t('header_search_box.label.All pages');
  56. const isIndicatorShown = !isFocused && (text.length === 0);
  57. if (isScopeChildren == null || isSearchServiceReachable == null) {
  58. return <></>;
  59. }
  60. return (
  61. <div className={`grw-global-search ${styles['grw-global-search']} form-group mb-0 d-print-none ${isSearchServiceReachable ? '' : 'has-error'}`}>
  62. <div className="input-group flex-nowrap">
  63. <div className={`input-group-prepend ${dropup ? 'dropup' : ''}`}>
  64. <button
  65. className="btn btn-secondary dropdown-toggle py-0"
  66. type="button"
  67. data-toggle="dropdown"
  68. aria-haspopup="true"
  69. data-testid="select-search-scope"
  70. >
  71. {scopeLabel}
  72. </button>
  73. <div className="dropdown-menu">
  74. <button
  75. className="dropdown-item"
  76. type="button"
  77. onClick={() => {
  78. setScopeChildren(false);
  79. globalSearchFormRef.current?.focus();
  80. }}
  81. >
  82. { t('header_search_box.item_label.All pages') }
  83. </button>
  84. <button
  85. data-tesid="search-current-tree"
  86. className="dropdown-item"
  87. type="button"
  88. onClick={() => {
  89. setScopeChildren(true);
  90. globalSearchFormRef.current?.focus();
  91. }}
  92. >
  93. { t('header_search_box.item_label.This tree') }
  94. </button>
  95. </div>
  96. </div>
  97. <SearchForm
  98. ref={globalSearchFormRef}
  99. isSearchServiceReachable={isSearchServiceReachable || false}
  100. dropup={dropup}
  101. onChange={gotoPage}
  102. onBlur={() => setFocused(false)}
  103. onFocus={() => setFocused(true)}
  104. onInputChange={text => setText(text)}
  105. onSubmit={search}
  106. />
  107. { isIndicatorShown && (
  108. <span className="grw-shortcut-key-indicator">
  109. <code className="bg-transparent text-muted">/</code>
  110. </span>
  111. ) }
  112. </div>
  113. </div>
  114. );
  115. };