DocSearchModal.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375
  1. var _excluded = ["footer", "searchBox"];
  2. function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
  3. function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; }
  4. function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; }
  5. function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
  6. function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest(); }
  7. function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
  8. function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); }
  9. function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; }
  10. function _iterableToArrayLimit(arr, i) { var _i = arr == null ? null : typeof Symbol !== "undefined" && arr[Symbol.iterator] || arr["@@iterator"]; if (_i == null) return; var _arr = []; var _n = true; var _d = false; var _s, _e; try { for (_i = _i.call(arr); !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"] != null) _i["return"](); } finally { if (_d) throw _e; } } return _arr; }
  11. function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; }
  12. function _objectWithoutProperties(source, excluded) { if (source == null) return {}; var target = _objectWithoutPropertiesLoose(source, excluded); var key, i; if (Object.getOwnPropertySymbols) { var sourceSymbolKeys = Object.getOwnPropertySymbols(source); for (i = 0; i < sourceSymbolKeys.length; i++) { key = sourceSymbolKeys[i]; if (excluded.indexOf(key) >= 0) continue; if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue; target[key] = source[key]; } } return target; }
  13. function _objectWithoutPropertiesLoose(source, excluded) { if (source == null) return {}; var target = {}; var sourceKeys = Object.keys(source); var key, i; for (i = 0; i < sourceKeys.length; i++) { key = sourceKeys[i]; if (excluded.indexOf(key) >= 0) continue; target[key] = source[key]; } return target; }
  14. import { createAutocomplete } from '@algolia/autocomplete-core';
  15. import React from 'react';
  16. import { MAX_QUERY_SIZE } from './constants';
  17. import { Footer } from './Footer';
  18. import { Hit } from './Hit';
  19. import { ScreenState } from './ScreenState';
  20. import { SearchBox } from './SearchBox';
  21. import { createStoredSearches } from './stored-searches';
  22. import { useSearchClient } from './useSearchClient';
  23. import { useTouchEvents } from './useTouchEvents';
  24. import { useTrapFocus } from './useTrapFocus';
  25. import { groupBy, identity, noop, removeHighlightTags } from './utils';
  26. export function DocSearchModal(_ref) {
  27. var appId = _ref.appId,
  28. apiKey = _ref.apiKey,
  29. indexName = _ref.indexName,
  30. _ref$placeholder = _ref.placeholder,
  31. placeholder = _ref$placeholder === void 0 ? 'Search docs' : _ref$placeholder,
  32. searchParameters = _ref.searchParameters,
  33. _ref$onClose = _ref.onClose,
  34. onClose = _ref$onClose === void 0 ? noop : _ref$onClose,
  35. _ref$transformItems = _ref.transformItems,
  36. transformItems = _ref$transformItems === void 0 ? identity : _ref$transformItems,
  37. _ref$hitComponent = _ref.hitComponent,
  38. hitComponent = _ref$hitComponent === void 0 ? Hit : _ref$hitComponent,
  39. _ref$resultsFooterCom = _ref.resultsFooterComponent,
  40. resultsFooterComponent = _ref$resultsFooterCom === void 0 ? function () {
  41. return null;
  42. } : _ref$resultsFooterCom,
  43. navigator = _ref.navigator,
  44. _ref$initialScrollY = _ref.initialScrollY,
  45. initialScrollY = _ref$initialScrollY === void 0 ? 0 : _ref$initialScrollY,
  46. _ref$transformSearchC = _ref.transformSearchClient,
  47. transformSearchClient = _ref$transformSearchC === void 0 ? identity : _ref$transformSearchC,
  48. _ref$disableUserPerso = _ref.disableUserPersonalization,
  49. disableUserPersonalization = _ref$disableUserPerso === void 0 ? false : _ref$disableUserPerso,
  50. _ref$initialQuery = _ref.initialQuery,
  51. initialQueryFromProp = _ref$initialQuery === void 0 ? '' : _ref$initialQuery,
  52. _ref$translations = _ref.translations,
  53. translations = _ref$translations === void 0 ? {} : _ref$translations,
  54. getMissingResultsUrl = _ref.getMissingResultsUrl;
  55. var footerTranslations = translations.footer,
  56. searchBoxTranslations = translations.searchBox,
  57. screenStateTranslations = _objectWithoutProperties(translations, _excluded);
  58. var _React$useState = React.useState({
  59. query: '',
  60. collections: [],
  61. completion: null,
  62. context: {},
  63. isOpen: false,
  64. activeItemId: null,
  65. status: 'idle'
  66. }),
  67. _React$useState2 = _slicedToArray(_React$useState, 2),
  68. state = _React$useState2[0],
  69. setState = _React$useState2[1];
  70. var containerRef = React.useRef(null);
  71. var modalRef = React.useRef(null);
  72. var formElementRef = React.useRef(null);
  73. var dropdownRef = React.useRef(null);
  74. var inputRef = React.useRef(null);
  75. var snippetLength = React.useRef(10);
  76. var initialQueryFromSelection = React.useRef(typeof window !== 'undefined' ? window.getSelection().toString().slice(0, MAX_QUERY_SIZE) : '').current;
  77. var initialQuery = React.useRef(initialQueryFromProp || initialQueryFromSelection).current;
  78. var searchClient = useSearchClient(appId, apiKey, transformSearchClient);
  79. var favoriteSearches = React.useRef(createStoredSearches({
  80. key: "__DOCSEARCH_FAVORITE_SEARCHES__".concat(indexName),
  81. limit: 10
  82. })).current;
  83. var recentSearches = React.useRef(createStoredSearches({
  84. key: "__DOCSEARCH_RECENT_SEARCHES__".concat(indexName),
  85. // We display 7 recent searches and there's no favorites, but only
  86. // 4 when there are favorites.
  87. limit: favoriteSearches.getAll().length === 0 ? 7 : 4
  88. })).current;
  89. var saveRecentSearch = React.useCallback(function saveRecentSearch(item) {
  90. if (disableUserPersonalization) {
  91. return;
  92. } // We don't store `content` record, but their parent if available.
  93. var search = item.type === 'content' ? item.__docsearch_parent : item; // We save the recent search only if it's not favorited.
  94. if (search && favoriteSearches.getAll().findIndex(function (x) {
  95. return x.objectID === search.objectID;
  96. }) === -1) {
  97. recentSearches.add(search);
  98. }
  99. }, [favoriteSearches, recentSearches, disableUserPersonalization]);
  100. var autocomplete = React.useMemo(function () {
  101. return createAutocomplete({
  102. id: 'docsearch',
  103. defaultActiveItemId: 0,
  104. placeholder: placeholder,
  105. openOnFocus: true,
  106. initialState: {
  107. query: initialQuery,
  108. context: {
  109. searchSuggestions: []
  110. }
  111. },
  112. navigator: navigator,
  113. onStateChange: function onStateChange(props) {
  114. setState(props.state);
  115. },
  116. getSources: function getSources(_ref2) {
  117. var query = _ref2.query,
  118. sourcesState = _ref2.state,
  119. setContext = _ref2.setContext,
  120. setStatus = _ref2.setStatus;
  121. if (!query) {
  122. if (disableUserPersonalization) {
  123. return [];
  124. }
  125. return [{
  126. sourceId: 'recentSearches',
  127. onSelect: function onSelect(_ref3) {
  128. var item = _ref3.item,
  129. event = _ref3.event;
  130. saveRecentSearch(item);
  131. if (!event.shiftKey && !event.ctrlKey && !event.metaKey) {
  132. onClose();
  133. }
  134. },
  135. getItemUrl: function getItemUrl(_ref4) {
  136. var item = _ref4.item;
  137. return item.url;
  138. },
  139. getItems: function getItems() {
  140. return recentSearches.getAll();
  141. }
  142. }, {
  143. sourceId: 'favoriteSearches',
  144. onSelect: function onSelect(_ref5) {
  145. var item = _ref5.item,
  146. event = _ref5.event;
  147. saveRecentSearch(item);
  148. if (!event.shiftKey && !event.ctrlKey && !event.metaKey) {
  149. onClose();
  150. }
  151. },
  152. getItemUrl: function getItemUrl(_ref6) {
  153. var item = _ref6.item;
  154. return item.url;
  155. },
  156. getItems: function getItems() {
  157. return favoriteSearches.getAll();
  158. }
  159. }];
  160. }
  161. return searchClient.search([{
  162. query: query,
  163. indexName: indexName,
  164. params: _objectSpread({
  165. attributesToRetrieve: ['hierarchy.lvl0', 'hierarchy.lvl1', 'hierarchy.lvl2', 'hierarchy.lvl3', 'hierarchy.lvl4', 'hierarchy.lvl5', 'hierarchy.lvl6', 'content', 'type', 'url'],
  166. attributesToSnippet: ["hierarchy.lvl1:".concat(snippetLength.current), "hierarchy.lvl2:".concat(snippetLength.current), "hierarchy.lvl3:".concat(snippetLength.current), "hierarchy.lvl4:".concat(snippetLength.current), "hierarchy.lvl5:".concat(snippetLength.current), "hierarchy.lvl6:".concat(snippetLength.current), "content:".concat(snippetLength.current)],
  167. snippetEllipsisText: '…',
  168. highlightPreTag: '<mark>',
  169. highlightPostTag: '</mark>',
  170. hitsPerPage: 20
  171. }, searchParameters)
  172. }]).catch(function (error) {
  173. // The Algolia `RetryError` happens when all the servers have
  174. // failed, meaning that there's no chance the response comes
  175. // back. This is the right time to display an error.
  176. // See https://github.com/algolia/algoliasearch-client-javascript/blob/2ffddf59bc765cd1b664ee0346b28f00229d6e12/packages/transporter/src/errors/createRetryError.ts#L5
  177. if (error.name === 'RetryError') {
  178. setStatus('error');
  179. }
  180. throw error;
  181. }).then(function (_ref7) {
  182. var results = _ref7.results;
  183. var _results$ = results[0],
  184. hits = _results$.hits,
  185. nbHits = _results$.nbHits;
  186. var sources = groupBy(hits, function (hit) {
  187. return removeHighlightTags(hit);
  188. }); // We store the `lvl0`s to display them as search suggestions
  189. // in the "no results" screen.
  190. if (sourcesState.context.searchSuggestions.length < Object.keys(sources).length) {
  191. setContext({
  192. searchSuggestions: Object.keys(sources)
  193. });
  194. }
  195. setContext({
  196. nbHits: nbHits
  197. });
  198. return Object.values(sources).map(function (items, index) {
  199. return {
  200. sourceId: "hits".concat(index),
  201. onSelect: function onSelect(_ref8) {
  202. var item = _ref8.item,
  203. event = _ref8.event;
  204. saveRecentSearch(item);
  205. if (!event.shiftKey && !event.ctrlKey && !event.metaKey) {
  206. onClose();
  207. }
  208. },
  209. getItemUrl: function getItemUrl(_ref9) {
  210. var item = _ref9.item;
  211. return item.url;
  212. },
  213. getItems: function getItems() {
  214. return Object.values(groupBy(items, function (item) {
  215. return item.hierarchy.lvl1;
  216. })).map(transformItems).map(function (groupedHits) {
  217. return groupedHits.map(function (item) {
  218. return _objectSpread(_objectSpread({}, item), {}, {
  219. __docsearch_parent: item.type !== 'lvl1' && groupedHits.find(function (siblingItem) {
  220. return siblingItem.type === 'lvl1' && siblingItem.hierarchy.lvl1 === item.hierarchy.lvl1;
  221. })
  222. });
  223. });
  224. }).flat();
  225. }
  226. };
  227. });
  228. });
  229. }
  230. });
  231. }, [indexName, searchParameters, searchClient, onClose, recentSearches, favoriteSearches, saveRecentSearch, initialQuery, placeholder, navigator, transformItems, disableUserPersonalization]);
  232. var getEnvironmentProps = autocomplete.getEnvironmentProps,
  233. getRootProps = autocomplete.getRootProps,
  234. refresh = autocomplete.refresh;
  235. useTouchEvents({
  236. getEnvironmentProps: getEnvironmentProps,
  237. panelElement: dropdownRef.current,
  238. formElement: formElementRef.current,
  239. inputElement: inputRef.current
  240. });
  241. useTrapFocus({
  242. container: containerRef.current
  243. });
  244. React.useEffect(function () {
  245. document.body.classList.add('DocSearch--active');
  246. return function () {
  247. var _window$scrollTo, _window;
  248. document.body.classList.remove('DocSearch--active'); // IE11 doesn't support `scrollTo` so we check that the method exists
  249. // first.
  250. (_window$scrollTo = (_window = window).scrollTo) === null || _window$scrollTo === void 0 ? void 0 : _window$scrollTo.call(_window, 0, initialScrollY);
  251. }; // eslint-disable-next-line react-hooks/exhaustive-deps
  252. }, []);
  253. React.useEffect(function () {
  254. var isMobileMediaQuery = window.matchMedia('(max-width: 750px)');
  255. if (isMobileMediaQuery.matches) {
  256. snippetLength.current = 5;
  257. }
  258. }, []);
  259. React.useEffect(function () {
  260. if (dropdownRef.current) {
  261. dropdownRef.current.scrollTop = 0;
  262. }
  263. }, [state.query]); // We don't focus the input when there's an initial query (i.e. Selection
  264. // Search) because users rather want to see the results directly, without the
  265. // keyboard appearing.
  266. // We therefore need to refresh the autocomplete instance to load all the
  267. // results, which is usually triggered on focus.
  268. React.useEffect(function () {
  269. if (initialQuery.length > 0) {
  270. refresh();
  271. if (inputRef.current) {
  272. inputRef.current.focus();
  273. }
  274. }
  275. }, [initialQuery, refresh]); // We rely on a CSS property to set the modal height to the full viewport height
  276. // because all mobile browsers don't compute their height the same way.
  277. // See https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
  278. React.useEffect(function () {
  279. function setFullViewportHeight() {
  280. if (modalRef.current) {
  281. var vh = window.innerHeight * 0.01;
  282. modalRef.current.style.setProperty('--docsearch-vh', "".concat(vh, "px"));
  283. }
  284. }
  285. setFullViewportHeight();
  286. window.addEventListener('resize', setFullViewportHeight);
  287. return function () {
  288. window.removeEventListener('resize', setFullViewportHeight);
  289. };
  290. }, []);
  291. return /*#__PURE__*/React.createElement("div", _extends({
  292. ref: containerRef
  293. }, getRootProps({
  294. 'aria-expanded': true
  295. }), {
  296. className: ['DocSearch', 'DocSearch-Container', state.status === 'stalled' && 'DocSearch-Container--Stalled', state.status === 'error' && 'DocSearch-Container--Errored'].filter(Boolean).join(' '),
  297. role: "button",
  298. tabIndex: 0,
  299. onMouseDown: function onMouseDown(event) {
  300. if (event.target === event.currentTarget) {
  301. onClose();
  302. }
  303. }
  304. }), /*#__PURE__*/React.createElement("div", {
  305. className: "DocSearch-Modal",
  306. ref: modalRef
  307. }, /*#__PURE__*/React.createElement("header", {
  308. className: "DocSearch-SearchBar",
  309. ref: formElementRef
  310. }, /*#__PURE__*/React.createElement(SearchBox, _extends({}, autocomplete, {
  311. state: state,
  312. autoFocus: initialQuery.length === 0,
  313. inputRef: inputRef,
  314. isFromSelection: Boolean(initialQuery) && initialQuery === initialQueryFromSelection,
  315. translations: searchBoxTranslations,
  316. onClose: onClose
  317. }))), /*#__PURE__*/React.createElement("div", {
  318. className: "DocSearch-Dropdown",
  319. ref: dropdownRef
  320. }, /*#__PURE__*/React.createElement(ScreenState, _extends({}, autocomplete, {
  321. indexName: indexName,
  322. state: state,
  323. hitComponent: hitComponent,
  324. resultsFooterComponent: resultsFooterComponent,
  325. disableUserPersonalization: disableUserPersonalization,
  326. recentSearches: recentSearches,
  327. favoriteSearches: favoriteSearches,
  328. inputRef: inputRef,
  329. translations: screenStateTranslations,
  330. getMissingResultsUrl: getMissingResultsUrl,
  331. onItemClick: function onItemClick(item) {
  332. saveRecentSearch(item);
  333. onClose();
  334. }
  335. }))), /*#__PURE__*/React.createElement("footer", {
  336. className: "DocSearch-Footer"
  337. }, /*#__PURE__*/React.createElement(Footer, {
  338. translations: footerTranslations
  339. }))));
  340. }