UNPKG

coveo-search-ui

Version:

Coveo JavaScript Search Framework

336 lines (282 loc) • 11.2 kB
import { $$, Dom } from '../../utils/Dom'; import { SVGIcons } from '../../utils/SVGIcons'; import { SVGDom } from '../../utils/SVGDom'; import { Component } from '../Base/Component'; import { l } from '../../strings/Strings'; import { EventsUtils } from '../../utils/EventsUtils'; import { IFacetSearch } from './IFacetSearch'; import { FacetSearchUserInputHandler } from './FacetSearchUserInputHandler'; import { uniqueId } from 'underscore'; import { ISearchDropdownNavigator, ISearchDropdownConfig } from './FacetSearchDropdownNavigation/DefaultSearchDropdownNavigator'; import { SearchDropdownNavigatorFactory } from './FacetSearchDropdownNavigation/SearchDropdownNavigatorFactory'; import { KEYBOARD } from '../../utils/KeyboardUtils'; export class FacetSearchElement { public search: HTMLElement | undefined; public magnifier: HTMLElement | undefined; public wait: HTMLElement | undefined; public clear: HTMLElement | undefined; public input: HTMLInputElement | undefined; public combobox: HTMLElement | undefined; public searchBarIsAnimating: boolean = false; public searchResults: HTMLElement; public facetSearchUserInputHandler: FacetSearchUserInputHandler; private triggeredScroll = false; private facetSearchId = uniqueId('coveo-facet-search-results'); private facetValueNotFoundId = uniqueId('coveo-facet-value-not-found'); private searchDropdownNavigator: ISearchDropdownNavigator; constructor(private facetSearch: IFacetSearch) { this.facetSearchUserInputHandler = new FacetSearchUserInputHandler(this.facetSearch); this.initSearchResults(); } public build(handleFacetSearchClear?: () => void) { this.search = document.createElement('div'); $$(this.search).addClass('coveo-facet-search'); this.magnifier = this.buildMagnifierIcon(); this.search.appendChild(this.magnifier); this.wait = this.buildWaitIcon(); this.search.appendChild(this.wait); this.hideFacetSearchWaitingAnimation(); this.clear = $$('div', { className: 'coveo-facet-search-clear', title: l('Clear', l('Search')) }, SVGIcons.icons.clear).el; SVGDom.addClassToSVGInContainer(this.clear, 'coveo-facet-search-clear-svg'); this.clear.style.display = 'none'; this.search.appendChild(this.clear); this.combobox = this.buildSearchInputWrapper(); this.search.appendChild(this.combobox); this.input = this.buildInputElement(); Component.pointElementsToDummyForm(this.input); this.combobox.appendChild(this.input); $$(this.input).on('keyup', (e: KeyboardEvent) => { this.facetSearchUserInputHandler.handleKeyboardEvent(e); }); $$(this.clear).on('click', (e: Event) => { handleFacetSearchClear && handleFacetSearchClear(); }); $$(this.input).on('focus', (e: Event) => { this.handleFacetSearchFocus(); }); $$(this.input).on('blur', (e: FocusEvent) => this.onInputBlur(e)); this.detectSearchBarAnimation(); this.initSearchDropdownNavigator(); return this.search; } private initSearchResults() { this.searchResults = $$('ul', { id: this.facetSearchId, className: 'coveo-facet-search-results', role: 'listbox' }).el; $$(this.searchResults).on('scroll', () => this.handleScrollEvent()); $$(this.searchResults).on('keyup', (e: KeyboardEvent) => { if (e.which === KEYBOARD.ESCAPE) { this.facetSearch.dismissSearchResults(); } }); $$(this.searchResults).on('focusout', (event: FocusEvent) => this.onSearchResultsFocusOut(event)); $$(this.searchResults).hide(); } private onInputBlur(e: FocusEvent) { const target = e.relatedTarget as HTMLElement; const focusedOnSearchResult = this.searchResults.contains(target); const focusedOnFacet = this.search.parentElement.contains(target); if (focusedOnFacet && !focusedOnSearchResult) { this.facetSearch.dismissSearchResults(); } } private onSearchResultsFocusOut(e: FocusEvent) { const target = e.relatedTarget as HTMLElement; const isUsingKeyboard = !!target; const focusedOnInput = this.input.contains(target); const focusedOnSearchResult = this.searchResults.contains(target); if (isUsingKeyboard && !focusedOnInput && !focusedOnSearchResult) { this.facetSearch.dismissSearchResults(); } } private initSearchDropdownNavigator() { const config: ISearchDropdownConfig = { input: this.input, searchResults: this.searchResults, setScrollTrigger: (val: boolean) => (this.triggeredScroll = val) }; this.searchDropdownNavigator = SearchDropdownNavigatorFactory(this.facetSearch, config); } private buildSearchInputWrapper() { return $$('div', { className: 'coveo-facet-search-middle' }).el; } public showFacetSearchWaitingAnimation() { this.magnifier && $$(this.magnifier).hide(); $$(this.wait).show(); } public getValueInInputForFacetSearch() { return this.input.value.trim(); } public hideFacetSearchWaitingAnimation() { $$(this.magnifier).show(); $$(this.wait).hide(); } public detectSearchBarAnimation() { EventsUtils.addPrefixedEvent(this.search, 'AnimationStart', event => { if (event.animationName == 'grow') { this.searchBarIsAnimating = true; } }); EventsUtils.addPrefixedEvent(this.search, 'AnimationEnd', event => { if (event.animationName == 'grow') { this.searchBarIsAnimating = false; } }); } public positionSearchResults() { if (this.searchResults != null) { $$(this.searchResults).insertAfter(this.search); $$(this.searchResults).show(); if ($$(this.searchResults).css('display') == 'none') { this.searchResults.style.display = ''; } let searchBar = $$(this.search); if (searchBar.css('display') == 'none' || this.searchBarIsAnimating) { if ($$(this.searchResults).css('display') == 'none') { this.searchResults.style.display = ''; } EventsUtils.addPrefixedEvent(this.search, 'AnimationEnd', () => { EventsUtils.removePrefixedEvent(this.search, 'AnimationEnd', this); }); } } this.addAriaAttributes(); } public setAsCurrentResult(toSet: Dom) { this.searchDropdownNavigator.setAsCurrentResult(toSet); } public get currentResult() { return this.searchDropdownNavigator.currentResult; } public moveCurrentResultDown() { this.searchDropdownNavigator.focusNextElement(); } public moveCurrentResultUp() { this.searchDropdownNavigator.focusPreviousElement(); } public highlightCurrentQueryInSearchResults(regex: RegExp) { const captions = this.facetSearch.getCaptions(); captions.forEach(caption => { caption.innerHTML = $$(caption).text().replace(regex, '<span class="coveo-highlight">$1</span>'); }); } public appendToSearchResults(el: HTMLElement) { this.searchResults.appendChild(el); this.setupFacetSearchResultsEvents(el); } public emptyAndShowNoResults() { $$(this.searchResults).empty(); this.searchResults.appendChild( $$( 'li', { id: this.facetValueNotFoundId, className: 'coveo-facet-value-not-found', role: 'option', ariaSelected: 'true', tabindex: 0 }, l('NoValuesFound') ).el ); this.input.setAttribute('aria-activedescendant', this.facetValueNotFoundId); } public updateAriaLiveWithResults(inputValue: string, numberOfResults: number, moreValuesToFetch: boolean) { let ariaLiveText = inputValue === '' ? l('ShowingResults', numberOfResults, inputValue, numberOfResults) : l('ShowingResultsWithQuery', numberOfResults, inputValue, numberOfResults); if (moreValuesToFetch) { ariaLiveText = `${ariaLiveText} (${l('MoreValuesAvailable')})`; } this.facetSearch.updateAriaLive(ariaLiveText); } public focus() { this.input.focus(); this.handleFacetSearchFocus(); } private handleFacetSearchFocus() { if (this.facetSearch.currentlyDisplayedResults == null) { this.facetSearch.displayNewValues(); this.addAriaAttributes(); } } private setupFacetSearchResultsEvents(el: HTMLElement) { $$(el).on('mousemove', () => { this.setAsCurrentResult($$(el)); }); // Prevent closing the search results on the end of a touch drag let touchDragging = false; let mouseDragging = false; $$(el).on('mousedown', () => (mouseDragging = false)); $$(el).on('mousemove', () => (mouseDragging = true)); $$(el).on('touchmove', () => (touchDragging = true)); $$(el).on('mouseup touchend', () => { if (!touchDragging && !mouseDragging) { setTimeout(() => { this.facetSearch.dismissSearchResults(); }, 0); // setTimeout is to give time to trigger the click event before hiding the search menu. } touchDragging = false; mouseDragging = false; }); } public hideSearchResultsElement() { this.removeAriaAttributes(); $$(this.searchResults).hide(); $$(this.searchResults).remove(); } public clearSearchInput() { if (this.input) { this.input.value = ''; } } private buildMagnifierIcon() { const magnifier = document.createElement('div'); magnifier.innerHTML = SVGIcons.icons.search; $$(magnifier).addClass('coveo-facet-search-magnifier'); SVGDom.addClassToSVGInContainer(magnifier, 'coveo-facet-search-magnifier-svg'); this.search.appendChild(magnifier); return magnifier; } private buildWaitIcon() { const wait = document.createElement('div'); wait.innerHTML = SVGIcons.icons.loading; $$(wait).addClass('coveo-facet-search-wait-animation'); SVGDom.addClassToSVGInContainer(wait, 'coveo-facet-search-wait-animation-svg'); return wait; } private buildInputElement() { return <HTMLInputElement>$$('input', { className: 'coveo-facet-search-input', type: 'text', autocapitalize: 'off', autocorrect: 'off', ariaLabel: l('SearchFacetResults', this.facetSearch.facetTitle), ariaHaspopup: 'true', ariaAutocomplete: 'list' }).el; } private handleScrollEvent() { if (this.triggeredScroll) { this.triggeredScroll = false; } else { this.facetSearchUserInputHandler.handleFacetSearchResultsScroll(); } } private addAriaAttributes() { if (!this.input || !this.combobox) { return; } this.input.setAttribute('role', 'combobox'); this.input.setAttribute('aria-owns', this.facetSearchId); this.input.setAttribute('aria-controls', this.facetSearchId); this.input.setAttribute('aria-expanded', 'true'); this.facetSearch.setExpandedFacetSearchAccessibilityAttributes(this.searchResults); } private removeAriaAttributes() { if (!this.input || !this.combobox) { return; } this.input.removeAttribute('role'); this.input.removeAttribute('aria-owns'); this.input.removeAttribute('aria-controls'); this.input.removeAttribute('aria-activedescendant'); this.input.setAttribute('aria-expanded', 'false'); this.facetSearch.setCollapsedFacetSearchAccessibilityAttributes(); } }