GlobalSearch.tsx 3.6 KB

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