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

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

Comments

Loading...