GlobalSearch.tsx 4.4 KB

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