Register
Login
Resources
Docs Blog Datasets Glossary Case Studies Tutorials & Webinars
Product
Data Engine LLMs Platform Enterprise
Pricing Explore
Connect to our Discord channel

search.ts 9.7 KB

You have to be logged in to leave a comment. Sign In
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
  1. import { tags } from './hyperscript'
  2. import { sendEvent, EventType } from './events'
  3. import searchWithYourKeyboard from 'search-with-your-keyboard'
  4. let $searchInputContainer: HTMLElement | null | undefined
  5. let $searchResultsContainer: HTMLElement | null | undefined
  6. let $searchOverlay: HTMLElement | null | undefined
  7. let $searchInput: HTMLInputElement | null | undefined
  8. let isExplorerPage: boolean
  9. // This is our default placeholder, but it can be localized with a <meta> tag
  10. let placeholder = 'Search topics, products...'
  11. let version: string
  12. let language: string
  13. export default function search() {
  14. // @ts-ignore
  15. if (window.IS_NEXTJS_PAGE) return
  16. // We don't want to mess with query params intended for the GraphQL Explorer
  17. isExplorerPage = Boolean(document.getElementById('graphiql'))
  18. // First, only initialize search if the elements are on the page
  19. $searchInputContainer = document.getElementById('search-input-container')
  20. $searchResultsContainer = document.getElementById('search-results-container')
  21. if (!$searchInputContainer || !$searchResultsContainer) return
  22. // This overlay exists so if you click off the search, it closes
  23. $searchOverlay = document.querySelector('.search-overlay-desktop') as HTMLElement
  24. // There's an index for every version/language combination
  25. const { languages, versions, nonEnterpriseDefaultVersion } = JSON.parse(
  26. (document.getElementById('expose') as HTMLScriptElement)?.text || ''
  27. ).searchOptions
  28. version = deriveVersionFromPath(versions, nonEnterpriseDefaultVersion)
  29. language = deriveLanguageCodeFromPath(languages)
  30. // Find search placeholder text in a <meta> tag, falling back to a default
  31. const $placeholderMeta = document.querySelector(
  32. 'meta[name="site.data.ui.search.placeholder"]'
  33. ) as HTMLMetaElement
  34. if ($placeholderMeta) {
  35. placeholder = $placeholderMeta.content
  36. }
  37. // Write the search form into its container
  38. $searchInputContainer.append(tmplSearchInput())
  39. $searchInput = $searchInputContainer.querySelector('input') as HTMLInputElement
  40. // Prevent 'enter' from refreshing the page
  41. ;($searchInputContainer.querySelector('form') as HTMLFormElement).addEventListener(
  42. 'submit',
  43. (evt) => evt.preventDefault()
  44. )
  45. // Search when the user finished typing
  46. $searchInput.addEventListener('keyup', debounce(onSearch))
  47. // Adds ability to navigate search results with keyboard (up, down, enter, esc)
  48. searchWithYourKeyboard('#search-input-container input', '.ais-Hits-item')
  49. // If the user already has a query in the URL, parse it and search away
  50. if (!isExplorerPage) {
  51. parseExistingSearch()
  52. }
  53. // If not on home page, decide if search panel should be open
  54. toggleSearchDisplay() // must come after parseExistingSearch
  55. }
  56. // The home page and 404 pages have a standalone search
  57. function hasStandaloneSearch() {
  58. return document.getElementById('landing') || document.querySelector('body.error-404') !== null
  59. }
  60. function toggleSearchDisplay() {
  61. // Clear/close search, if ESC is clicked
  62. document.addEventListener('keyup', (e) => {
  63. if (e.key === 'Escape') {
  64. closeSearch()
  65. }
  66. })
  67. // If not on homepage...
  68. if (hasStandaloneSearch()) return
  69. // Open panel if input is clicked
  70. $searchInput?.addEventListener('focus', openSearch)
  71. // Close panel if overlay is clicked
  72. if ($searchOverlay) {
  73. $searchOverlay.addEventListener('click', closeSearch)
  74. }
  75. // Open panel if page loads with query in the params/input
  76. if ($searchInput?.value) {
  77. openSearch()
  78. }
  79. }
  80. // On most pages, opens the search panel
  81. function openSearch() {
  82. $searchInput?.classList.add('js-open')
  83. $searchResultsContainer?.classList.add('js-open')
  84. $searchOverlay?.classList.add('js-open')
  85. }
  86. // Close panel if not on homepage
  87. function closeSearch() {
  88. if (!hasStandaloneSearch()) {
  89. $searchInput?.classList.remove('js-open')
  90. $searchResultsContainer?.classList.remove('js-open')
  91. $searchOverlay?.classList.remove('js-open')
  92. }
  93. if ($searchInput) $searchInput.value = ''
  94. onSearch()
  95. }
  96. function deriveLanguageCodeFromPath(languageCodes: Array<string>) {
  97. let languageCode = location.pathname.split('/')[1]
  98. if (!languageCodes.includes(languageCode)) languageCode = 'en'
  99. return languageCode
  100. }
  101. function deriveVersionFromPath(
  102. allVersions: Record<string, string>,
  103. nonEnterpriseDefaultVersion: string
  104. ) {
  105. // fall back to the non-enterprise default version (FPT currently) on the homepage, 404 page, etc.
  106. const versionStr = location.pathname.split('/')[2] || nonEnterpriseDefaultVersion
  107. return allVersions[versionStr] || allVersions[nonEnterpriseDefaultVersion]
  108. }
  109. // Wait for the event to stop triggering for X milliseconds before responding
  110. function debounce(fn: Function, delay = 300) {
  111. let timer: number
  112. return (...args: Array<any>) => {
  113. clearTimeout(timer)
  114. timer = window.setTimeout(() => fn.apply(null, args), delay)
  115. }
  116. }
  117. // When the user finishes typing, update the results
  118. async function onSearch() {
  119. const query = $searchInput?.value || ''
  120. // Update the URL with the search parameters in the query string
  121. // UNLESS this is the GraphQL Explorer page, where a query in the URL is a GraphQL query
  122. const pushUrl = new URL(location.toString())
  123. pushUrl.search = query && !isExplorerPage ? new URLSearchParams({ query }).toString() : ''
  124. history.pushState({}, '', pushUrl.toString())
  125. // If there's a query, call the endpoint
  126. // Otherwise, there's no results by default
  127. let results = []
  128. if (query.trim()) {
  129. const endpointUrl = new URL(location.origin)
  130. endpointUrl.pathname = '/search'
  131. endpointUrl.search = new URLSearchParams({ language, version, query }).toString()
  132. const response = await fetch(endpointUrl.toString(), {
  133. method: 'GET',
  134. headers: {
  135. 'Content-Type': 'application/json',
  136. },
  137. })
  138. results = response.ok ? await response.json() : []
  139. }
  140. // Either way, update the display
  141. $searchResultsContainer?.querySelectorAll('*').forEach((el) => el.remove())
  142. $searchResultsContainer?.append(tmplSearchResults(results))
  143. toggleStandaloneSearch()
  144. // Analytics tracking
  145. if (query.trim()) {
  146. sendEvent({
  147. type: EventType.search,
  148. search_query: query,
  149. // search_context
  150. })
  151. }
  152. }
  153. // If on homepage, toggle results container if query is present
  154. function toggleStandaloneSearch() {
  155. if (!hasStandaloneSearch()) return
  156. const query = $searchInput?.value
  157. const queryPresent = Boolean(query && query.length > 0)
  158. const $results = document.querySelector('.ais-Hits') as HTMLElement
  159. // Primer classNames for showing and hiding the results container
  160. const activeClass = $searchResultsContainer?.getAttribute('data-active-class')
  161. const inactiveClass = $searchResultsContainer?.getAttribute('data-inactive-class')
  162. if (!activeClass) {
  163. console.error(
  164. 'container is missing required `data-active-class` attribute',
  165. $searchResultsContainer
  166. )
  167. return
  168. }
  169. if (!inactiveClass) {
  170. console.error(
  171. 'container is missing required `data-inactive-class` attribute',
  172. $searchResultsContainer
  173. )
  174. return
  175. }
  176. // hide the container when no query is present
  177. $searchResultsContainer?.classList.toggle(activeClass, queryPresent)
  178. $searchResultsContainer?.classList.toggle(inactiveClass, !queryPresent)
  179. if (queryPresent && $results) $results.style.display = 'block'
  180. }
  181. // If the user shows up with a query in the URL, go ahead and search for it
  182. function parseExistingSearch() {
  183. const params = new URLSearchParams(location.search)
  184. if (!params.has('query')) return
  185. if ($searchInput) $searchInput.value = params.get('query') || ''
  186. onSearch()
  187. }
  188. /** * Template functions ***/
  189. function tmplSearchInput() {
  190. // only autofocus on the homepage, and only if no #hash is present in the URL
  191. const autofocus = (hasStandaloneSearch() && !location.hash.length) || null
  192. const { div, form, input, button } = tags
  193. return div(
  194. { class: 'ais-SearchBox' },
  195. form(
  196. { role: 'search', class: 'ais-SearchBox-form', novalidate: true },
  197. input({
  198. class: 'ais-SearchBox-input',
  199. type: 'search',
  200. placeholder,
  201. autofocus,
  202. autocomplete: 'off',
  203. autocorrect: 'off',
  204. autocapitalize: 'off',
  205. spellcheck: 'false',
  206. maxlength: '512',
  207. }),
  208. button({
  209. class: 'ais-SearchBox-submit',
  210. type: 'submit',
  211. title: 'Submit the search query.',
  212. hidden: true,
  213. })
  214. )
  215. )
  216. }
  217. type SearchResult = {
  218. url: string
  219. breadcrumbs: string
  220. heading: string
  221. title: string
  222. content: string
  223. }
  224. function tmplSearchResults(items: Array<SearchResult>) {
  225. const { div, ol, li } = tags
  226. return div(
  227. { class: 'ais-Hits', style: 'display:block' },
  228. ol(
  229. { class: 'ais-Hits-list' },
  230. items.map((item) => li({ class: 'ais-Hits-item' }, tmplSearchResult(item)))
  231. )
  232. )
  233. }
  234. function tmplSearchResult({ url, breadcrumbs, heading, title, content }: SearchResult) {
  235. const { div, a } = tags
  236. return div(
  237. { class: 'search-result border-top color-border-secondary py-3 px-2' },
  238. a(
  239. { href: url, class: 'no-underline' },
  240. div(
  241. {
  242. class: 'search-result-breadcrumbs d-block color-text-primary opacity-60 text-small pb-1',
  243. },
  244. // Breadcrumbs in search records don't include the page title
  245. markify(breadcrumbs || '')
  246. ),
  247. div(
  248. { class: 'search-result-title d-block h4-mktg color-text-primary' },
  249. // Display page title and heading (if present exists)
  250. markify(heading ? `${title}: ${heading}` : title)
  251. ),
  252. div({ class: 'search-result-content d-block color-text-secondary' }, markify(content))
  253. )
  254. )
  255. }
  256. // Convert mark tags in search responses
  257. function markify(text: string) {
  258. const { mark } = tags
  259. return text.split(/<\/?mark>/g).map((el, i) => (i % 2 ? mark(el) : el))
  260. }
Tip!

Press p or to see the previous file or, n or to see the next file

Comments

Loading...