coveo-search-ui
Version:
Coveo JavaScript Search Framework
336 lines (282 loc) • 11.2 kB
text/typescript
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();
}
}