GlobalSearch.tsx 3.7 KB

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