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

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

Comments

Loading...