search.ts 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127
  1. import _ from 'lodash'
  2. import DOMPurify from 'dompurify'
  3. import { formatWikiHit, searchWiki } from '../algolia-search/search-wiki'
  4. import { sendMB } from '../../infrastructure/event-tracking'
  5. import { materialIcon } from '@/features/utils/material-icon'
  6. export function setupSearch(formEl: Element) {
  7. const inputEl = formEl.querySelector('[name="subject"]') as HTMLInputElement
  8. const resultsContainerEl = formEl.querySelector(
  9. '[data-ol-search-results-container]'
  10. ) as HTMLElement
  11. const wrapperEl = formEl.querySelector(
  12. '[data-ol-search-results-wrapper]'
  13. ) as HTMLElement
  14. if (!inputEl || !resultsContainerEl || !wrapperEl) {
  15. return
  16. }
  17. let lastValue = ''
  18. function hideResults() {
  19. wrapperEl.setAttribute('hidden', '')
  20. }
  21. function showResults() {
  22. wrapperEl.removeAttribute('hidden')
  23. }
  24. async function handleChange() {
  25. const value = inputEl.value
  26. if (value === lastValue) return
  27. lastValue = value
  28. if (value.length < 3) {
  29. hideResults()
  30. return
  31. }
  32. DOMPurify.addHook('uponSanitizeElement', node => {
  33. if (node.nodeName === 'EM') {
  34. const strong = document.createElement('strong')
  35. strong.textContent = node.textContent
  36. node.parentNode?.replaceChild(strong, node)
  37. }
  38. })
  39. try {
  40. const { hits, nbHits } = await searchWiki(value, {
  41. hitsPerPage: 3,
  42. typoTolerance: 'strict',
  43. })
  44. resultsContainerEl.innerHTML = ''
  45. for (const hit of hits) {
  46. const { url, rawPageName, sectionName } = formatWikiHit(hit)
  47. const liEl = document.createElement('li')
  48. const linkEl = document.createElement('a')
  49. linkEl.className = 'dropdown-item'
  50. linkEl.href = url
  51. linkEl.target = '_blank'
  52. linkEl.rel = 'noopener noreferrer'
  53. linkEl.setAttribute('role', 'menuitem')
  54. liEl.append(linkEl)
  55. const contentWrapperEl = document.createElement('span')
  56. contentWrapperEl.className = 'dropdown-item-description-container'
  57. linkEl.append(contentWrapperEl)
  58. const pageNameEl = document.createElement('span')
  59. pageNameEl.innerHTML = DOMPurify.sanitize(rawPageName)
  60. contentWrapperEl.append(pageNameEl)
  61. if (sectionName) {
  62. const sectionEl = document.createElement('span')
  63. sectionEl.className = 'dropdown-item-description'
  64. sectionEl.innerHTML = DOMPurify.sanitize(sectionName)
  65. contentWrapperEl.append(sectionEl)
  66. }
  67. const iconEl = materialIcon('open_in_new')
  68. iconEl.classList.add('dropdown-item-trailing-icon')
  69. iconEl.setAttribute('aria-hidden', 'true')
  70. linkEl.append(iconEl)
  71. resultsContainerEl.append(liEl)
  72. }
  73. if (nbHits > 0) {
  74. showResults()
  75. sendMB('contact-form-suggestions-shown')
  76. } else {
  77. hideResults()
  78. }
  79. } catch (e) {
  80. hideResults()
  81. }
  82. DOMPurify.removeHook('uponSanitizeElement')
  83. }
  84. inputEl.addEventListener('input', _.debounce(handleChange, 350))
  85. function handleClickOutside(event: Event) {
  86. const target = event.target as Element
  87. if (!wrapperEl.contains(target) && !inputEl.contains(target)) {
  88. hideResults()
  89. }
  90. }
  91. document.addEventListener('click', handleClickOutside)
  92. function handleKeyDown(event: Event) {
  93. const keyboardEvent = event as KeyboardEvent
  94. if (keyboardEvent.key === 'Escape') {
  95. if (!wrapperEl.hasAttribute('hidden')) {
  96. hideResults()
  97. keyboardEvent.stopPropagation()
  98. keyboardEvent.preventDefault()
  99. }
  100. }
  101. }
  102. formEl.addEventListener('keydown', handleKeyDown)
  103. handleChange()
  104. }