diff --git a/services/web/frontend/js/features/contact-form/index.js b/services/web/frontend/js/features/contact-form/index.js index 51aff806e3..d0de64f43b 100644 --- a/services/web/frontend/js/features/contact-form/index.js +++ b/services/web/frontend/js/features/contact-form/index.js @@ -9,7 +9,6 @@ document .forEach(el => { el.addEventListener('click', function (e) { e.preventDefault() - $('[data-ol-contact-form-modal="contact-us"]').modal() }) }) @@ -18,7 +17,6 @@ document .forEach(el => { el.addEventListener('click', function (e) { e.preventDefault() - $('[data-ol-contact-form-modal="general"]').modal() }) }) diff --git a/services/web/frontend/js/features/contact-form/search.js b/services/web/frontend/js/features/contact-form/search.js index eaa34bdeeb..7b9c79981b 100644 --- a/services/web/frontend/js/features/contact-form/search.js +++ b/services/web/frontend/js/features/contact-form/search.js @@ -1,17 +1,21 @@ import _ from 'lodash' +import DOMPurify from 'dompurify' import { formatWikiHit, searchWiki } from '../algolia-search/search-wiki' import { sendMB } from '../../infrastructure/event-tracking' import { materialIcon } from '@/features/utils/material-icon' export function setupSearch(formEl) { const inputEl = formEl.querySelector('[name="subject"]') - const resultsEl = formEl.querySelector('[data-ol-search-results]') + const resultsContainerEl = formEl.querySelector( + '[data-ol-search-results-container]' + ) const wrapperEl = formEl.querySelector('[data-ol-search-results-wrapper]') let lastValue = '' function hideResults() { wrapperEl.setAttribute('hidden', '') } + function showResults() { wrapperEl.removeAttribute('hidden') } @@ -25,32 +29,55 @@ export function setupSearch(formEl) { return } + DOMPurify.addHook('uponSanitizeElement', node => { + if (node.nodeName === 'EM') { + const strong = document.createElement('strong') + strong.textContent = node.textContent + node.parentNode?.replaceChild(strong, node) + } + }) + try { const { hits, nbHits } = await searchWiki(value, { hitsPerPage: 3, typoTolerance: 'strict', }) - resultsEl.innerText = '' + + resultsContainerEl.innerHTML = '' for (const hit of hits) { - const { url, pageName } = formatWikiHit(hit) + const { url, rawPageName, sectionName } = formatWikiHit(hit) const liEl = document.createElement('li') const linkEl = document.createElement('a') - linkEl.className = 'contact-suggestion-list-item' + linkEl.className = 'dropdown-item' linkEl.href = url linkEl.target = '_blank' + linkEl.rel = 'noopener noreferrer' + linkEl.setAttribute('role', 'menuitem') liEl.append(linkEl) - const contentEl = document.createElement('span') - contentEl.innerHTML = pageName - linkEl.append(contentEl) + const contentWrapperEl = document.createElement('div') + contentWrapperEl.className = 'dropdown-item-description-container' + linkEl.append(contentWrapperEl) + + const pageNameEl = document.createElement('div') + pageNameEl.innerHTML = DOMPurify.sanitize(rawPageName) + contentWrapperEl.append(pageNameEl) const iconEl = materialIcon('open_in_new') iconEl.classList.add('dropdown-item-trailing-icon') - linkEl.append(iconEl) + iconEl.setAttribute('aria-hidden', 'true') + contentWrapperEl.append(iconEl) - resultsEl.append(liEl) + if (sectionName) { + const sectionEl = document.createElement('span') + sectionEl.className = 'dropdown-item-description' + sectionEl.innerHTML = DOMPurify.sanitize(sectionName) + contentWrapperEl.append(sectionEl) + } + + resultsContainerEl.append(liEl) } if (nbHits > 0) { showResults() @@ -61,10 +88,31 @@ export function setupSearch(formEl) { } catch (e) { hideResults() } + + DOMPurify.removeHook('uponSanitizeElement') } inputEl.addEventListener('input', _.debounce(handleChange, 350)) - // display initial results + function handleClickOutside(event) { + if (!wrapperEl.contains(event.target) && !inputEl.contains(event.target)) { + hideResults() + } + } + + document.addEventListener('click', handleClickOutside) + + function handleKeyDown(event) { + if (event.key === 'Escape') { + if (!wrapperEl.hasAttribute('hidden')) { + hideResults() + event.stopPropagation() + event.preventDefault() + } + } + } + + formEl.addEventListener('keydown', handleKeyDown) + handleChange() } diff --git a/services/web/frontend/js/marketing.ts b/services/web/frontend/js/marketing.ts index df9fd27d83..5fdb663653 100644 --- a/services/web/frontend/js/marketing.ts +++ b/services/web/frontend/js/marketing.ts @@ -10,3 +10,4 @@ import './features/multi-submit' import './features/cookie-banner' import './features/autoplay-video' import './features/mathjax' +import './features/contact-form' diff --git a/services/web/frontend/stylesheets/modals/contact-us-modal.scss b/services/web/frontend/stylesheets/modals/contact-us-modal.scss index a0a373c94b..c03dc0112b 100644 --- a/services/web/frontend/stylesheets/modals/contact-us-modal.scss +++ b/services/web/frontend/stylesheets/modals/contact-us-modal.scss @@ -15,22 +15,6 @@ margin-bottom: var(--spacing-05); } -.contact-suggestions { - @include body-sm; - - margin: 0 calc(-1 * var(--bs-modal-padding)) var(--spacing-05); - padding: var(--spacing-05) 0; - color: var(--content-secondary); - background-color: var(--bg-light-secondary); - border-top: solid 1px var(--border-primary-dark); - border-bottom: solid 1px var(--border-primary-dark); -} - -.contact-suggestion-label { - margin-bottom: var(--spacing-05); - padding: 0 var(--spacing-07); -} - .contact-suggestion-list { padding-left: 0; list-style: none; @@ -64,6 +48,7 @@ } .contact-suggestions-dropdown { + margin-top: $spacing-02; width: calc(100% - 2 * var(--bs-modal-padding)); .dropdown-header {