coveo-search-ui
Version:
Coveo JavaScript Search Framework
442 lines (381 loc) • 16.1 kB
text/typescript
/// <reference path="Facet.ts" />
import { IIndexFieldValue } from '../../rest/FieldValue';
import { Facet } from './Facet';
import { $$ } from '../../utils/Dom';
import { Utils } from '../../utils/Utils';
import { InitializationEvents } from '../../events/InitializationEvents';
import { FacetSearchParameters } from './FacetSearchParameters';
import { IAnalyticsFacetMeta, analyticsActionCauseList } from '../Analytics/AnalyticsActionListMeta';
import { IEndpointError } from '../../rest/EndpointError';
import { l } from '../../strings/Strings';
import { Assert } from '../../misc/Assert';
import { FacetValue } from './FacetValue';
import { StringUtils } from '../../utils/StringUtils';
import { IFacetSearchValuesListKlass } from './FacetSearchValuesList';
import { FacetValueElement } from './FacetValueElement';
import { ModalBox } from '../../ExternalModulesShim';
import { SearchInterface } from '../SearchInterface/SearchInterface';
import { ResponsiveComponentsUtils } from '../ResponsiveComponents/ResponsiveComponentsUtils';
import { FacetValuesOrder } from './FacetValuesOrder';
import 'styling/_FacetSearch';
import { each, debounce, map, pluck } from 'underscore';
import { FacetSearchElement } from './FacetSearchElement';
import { IFacetSearch } from './IFacetSearch';
/**
* Used by the {@link Facet} component to render and handle the facet search part of each facet.
*/
export class FacetSearch implements IFacetSearch {
public currentlyDisplayedResults: string[];
public facetSearchElement: FacetSearchElement;
public facetSearchPromise: Promise<IIndexFieldValue[]>;
public moreValuesToFetch = true;
private facetSearchTimeout: number;
private onResize: (...args: any[]) => void;
private onDocumentClick: (e: Event) => void;
private lastSearchWasEmpty = true;
constructor(public facet: Facet, public facetSearchValuesListKlass: IFacetSearchValuesListKlass, private root: HTMLElement) {
this.facetSearchElement = new FacetSearchElement(this);
this.onResize = debounce(() => {
// Mitigate issues in UT where the window in phantom js might get resized in the scope of another test.
// These would point to random instance of a test karma object, and not a real search interface.
if (this.facet instanceof Facet && this.facet.searchInterface instanceof SearchInterface) {
if (this.shouldPositionSearchResults()) {
this.positionSearchResults();
}
}
}, 250);
this.onDocumentClick = (e: Event) => {
this.handleClickElsewhere(e);
};
window.addEventListener('resize', this.onResize);
document.addEventListener('click', (e: Event) => this.onDocumentClick(e));
$$(facet.root).on(InitializationEvents.nuke, () => this.handleNuke());
}
public get facetType() {
return Facet.ID;
}
public get facetTitle() {
return this.facet.options.title || this.facet.options.field.toString();
}
/**
* Build the search component and return an `HTMLElement` which can be appended to the {@link Facet}.
* @returns {HTMLElement}
*/
public build(): HTMLElement {
return this.buildBaseSearch();
}
/**
* Position the search results at the footer of the facet.
*/
public positionSearchResults() {
this.facetSearchElement.positionSearchResults();
}
public fetchMoreValues() {
this.triggerNewFacetSearch(this.buildParamsForFetchingMore());
}
/**
* Dismiss the search results
*/
public dismissSearchResults() {
this.cancelAnyPendingSearchOperation();
this.facet.unfadeInactiveValuesInMainList();
$$(this.searchResults).empty();
this.moreValuesToFetch = true;
$$(this.search).removeClass('coveo-facet-search-no-results');
$$(this.facet.element).removeClass('coveo-facet-searching');
this.facetSearchElement.hideSearchResultsElement();
this.input.value = '';
$$(this.clear).hide();
this.currentlyDisplayedResults = undefined;
}
/**
* Trigger a new facet search, and display the results.
* @param params
*/
public triggerNewFacetSearch(params: FacetSearchParameters) {
this.cancelAnyPendingSearchOperation();
this.facetSearchElement.showFacetSearchWaitingAnimation();
this.facet.logger.info('Triggering new facet search');
this.facetSearchPromise = this.facet.facetQueryController.search(params);
if (this.facetSearchPromise) {
this.facetSearchPromise
.then((fieldValues: IIndexFieldValue[]) => {
this.facet.usageAnalytics.logCustomEvent<IAnalyticsFacetMeta>(
analyticsActionCauseList.facetSearch,
{
facetId: this.facet.options.id,
facetField: this.facet.options.field.toString(),
facetTitle: this.facet.options.title
},
this.facet.root
);
this.facet.logger.debug('Received field values', fieldValues);
this.processNewFacetSearchResults(fieldValues, params);
this.facetSearchElement.hideFacetSearchWaitingAnimation();
this.facetSearchPromise = undefined;
})
.catch((error: IEndpointError) => {
// The request might be normally cancelled if another search is triggered.
// In this case we do not hide the animation to prevent flicking.
if (Utils.exists(error)) {
this.facet.logger.error('Error while retrieving facet values', error);
this.facetSearchElement.hideFacetSearchWaitingAnimation();
}
this.facetSearchPromise = undefined;
return null;
});
}
}
/**
* Trigger the event associated with the focus of the search input.
*/
public focus(): void {
this.facetSearchElement.focus();
}
public get searchResults() {
return this.facetSearchElement.searchResults;
}
public get searchBarIsAnimating() {
return this.facetSearchElement.searchBarIsAnimating;
}
public get search() {
return this.facetSearchElement.search;
}
public setExpandedFacetSearchAccessibilityAttributes(searchResultsElement: HTMLElement) {
this.facet.setExpandedFacetSearchAccessibilityAttributes(searchResultsElement);
}
public setCollapsedFacetSearchAccessibilityAttributes() {
this.facet.setCollapsedFacetSearchAccessibilityAttributes();
}
public keyboardEventDefaultHandler() {
this.moreValuesToFetch = true;
this.highlightCurrentQueryWithinSearchResults();
if (!this.inputIsEmpty()) {
this.lastSearchWasEmpty = false;
this.displayNewValues(this.buildParamsForNormalSearch());
} else if (!this.lastSearchWasEmpty) {
// This normally happen if a user delete the search box content to go back to "empty" state.
this.currentlyDisplayedResults = undefined;
$$(this.searchResults).empty();
this.lastSearchWasEmpty = true;
this.displayNewValues(this.buildParamsForFetchingMore());
}
}
public keyboardNavigationEnterPressed(event: KeyboardEvent) {
if (event.shiftKey) {
this.triggerNewFacetSearch(this.buildParamsForNormalSearch());
} else {
if (this.searchResults.style.display != 'none') {
this.performActionOnCurrentSearchResult();
this.dismissSearchResults();
} else if ($$(this.search).is('.coveo-facet-search-no-results')) {
this.selectAllValuesMatchingSearch();
}
}
}
public keyboardNavigationDeletePressed(event: KeyboardEvent) {
if (event.shiftKey) {
this.performExcludeActionOnCurrentSearchResult();
this.dismissSearchResults();
this.input.value = '';
}
}
public displayNewValues(params: FacetSearchParameters = this.buildParamsForExcludingCurrentlyDisplayedValues()) {
this.cancelAnyPendingSearchOperation();
this.facetSearchTimeout = window.setTimeout(() => {
this.triggerNewFacetSearch(params);
}, this.facet.options.facetSearchDelay);
}
public getCaptions() {
return $$(this.searchResults).findAll('.coveo-facet-value-caption');
}
public getValueInInputForFacetSearch() {
return this.facetSearchElement.getValueInInputForFacetSearch();
}
public updateAriaLive(text: string) {
this.facet.searchInterface.ariaLive.updateText(text);
}
private get input() {
return this.facetSearchElement.input;
}
private get clear() {
return this.facetSearchElement.clear;
}
private shouldPositionSearchResults(): boolean {
return !ResponsiveComponentsUtils.isSmallFacetActivated($$(this.root)) && $$(this.facet.element).hasClass('coveo-facet-searching');
}
private buildBaseSearch(): HTMLElement {
this.facetSearchElement.build(() => this.handleFacetSearchClear());
$$(this.facetSearchElement.input).on('keyup', () => this.showOrHideClearElement());
return this.search;
}
private handleNuke() {
window.removeEventListener('resize', this.onResize);
document.removeEventListener('click', this.onDocumentClick);
}
private handleClickElsewhere(event: Event) {
if (this.currentlyDisplayedResults && this.search != event.target && this.searchResults != event.target && this.input != event.target) {
this.dismissSearchResults();
}
}
private handleFacetSearchClear() {
this.input.value = '';
$$(this.clear).hide();
this.dismissSearchResults();
}
private showOrHideClearElement() {
if (!this.inputIsEmpty()) {
$$(this.clear).show();
} else {
$$(this.clear).hide();
$$(this.search).removeClass('coveo-facet-search-no-results');
}
}
private cancelAnyPendingSearchOperation() {
if (Utils.exists(this.facetSearchTimeout)) {
clearTimeout(this.facetSearchTimeout);
this.facetSearchTimeout = undefined;
}
if (Utils.exists(this.facetSearchPromise)) {
Promise.reject(this.facetSearchPromise).catch(() => {});
this.facetSearchPromise = undefined;
}
this.facetSearchElement.hideFacetSearchWaitingAnimation();
}
private inputIsEmpty() {
return this.input.value.trim() == '';
}
private processNewFacetSearchResults(fieldValues: IIndexFieldValue[], facetSearchParameters: FacetSearchParameters) {
Assert.exists(fieldValues);
fieldValues = new FacetValuesOrder(this.facet, this.facet.facetSort).reorderValues(fieldValues);
if (fieldValues.length > 0) {
$$(this.search).removeClass('coveo-facet-search-no-results');
this.facet.fadeInactiveValuesInMainList(this.facet.options.facetSearchDelay);
this.rebuildSearchResults(fieldValues, facetSearchParameters);
if (!facetSearchParameters.fetchMore) {
this.showSearchResultsElement();
}
this.highlightCurrentQueryWithinSearchResults();
this.makeFirstSearchResultTheCurrentOne();
this.facetSearchElement.updateAriaLiveWithResults(this.input.value, this.currentlyDisplayedResults.length, this.moreValuesToFetch);
} else {
if (facetSearchParameters.fetchMore) {
this.moreValuesToFetch = false;
} else {
$$(this.search).addClass('coveo-facet-search-no-results');
this.showSearchResultsElement();
this.facetSearchElement.emptyAndShowNoResults();
}
}
}
private rebuildSearchResults(fieldValues: IIndexFieldValue[], facetSearchParameters: FacetSearchParameters) {
Assert.exists(fieldValues);
if (!facetSearchParameters.fetchMore) {
$$(this.searchResults).empty();
}
const facetSearchHasQuery = Utils.isNonEmptyString(facetSearchParameters.valueToSearch);
if (facetSearchHasQuery) {
this.appendSelectAllResultsButton();
}
let facetValues = map(fieldValues, fieldValue => {
return FacetValue.create(fieldValue);
});
each(new this.facetSearchValuesListKlass(this.facet, FacetValueElement).build(facetValues), (listElement: HTMLElement) => {
this.facetSearchElement.appendToSearchResults(listElement);
});
if (this.currentlyDisplayedResults) {
this.currentlyDisplayedResults = this.currentlyDisplayedResults.concat(pluck(facetValues, 'value'));
} else {
this.currentlyDisplayedResults = pluck(facetValues, 'value');
}
each($$(this.searchResults).findAll('.coveo-facet-selectable'), (elem: HTMLElement, index: number) => {
$$(elem).setAttribute('id', `coveo-facet-search-${this.facet.options.id}-suggestion-${index}`);
$$(elem).setAttribute('role', 'option');
$$(elem).setAttribute('aria-selected', 'false');
$$(elem).addClass('coveo-facet-search-selectable');
});
}
private appendSelectAllResultsButton() {
const selectAll = document.createElement('li');
$$(selectAll).addClass(['coveo-facet-selectable', 'coveo-facet-search-selectable', 'coveo-facet-search-select-all']);
$$(selectAll).text(l('SelectAll'));
$$(selectAll).on('click', () => this.selectAllValuesMatchingSearch());
this.facetSearchElement.appendToSearchResults(selectAll);
}
private buildParamsForNormalSearch() {
let params = new FacetSearchParameters(this.facet);
params.setValueToSearch(this.getValueInInputForFacetSearch());
params.fetchMore = false;
return params;
}
private buildParamsForFetchingMore() {
let params = this.buildParamsForExcludingCurrentlyDisplayedValues();
params.fetchMore = true;
return params;
}
protected buildParamsForExcludingCurrentlyDisplayedValues() {
let params = new FacetSearchParameters(this.facet);
params.excludeCurrentlyDisplayedValuesInSearch(this.searchResults);
params.setValueToSearch(this.getValueInInputForFacetSearch());
return params;
}
private showSearchResultsElement() {
this.positionSearchResults();
}
private highlightCurrentQueryWithinSearchResults() {
let search = this.getValueInInputForFacetSearch();
let regex = new RegExp('(' + StringUtils.wildcardsToRegex(search, this.facet.options.facetSearchIgnoreAccents) + ')', 'ig');
this.facetSearchElement.highlightCurrentQueryInSearchResults(regex);
}
private makeFirstSearchResultTheCurrentOne() {
this.facetSearchElement.setAsCurrentResult($$(this.getSelectables()[0]));
}
private getSelectables(target = this.searchResults) {
return $$(target).findAll('.coveo-facet-selectable');
}
private performActionOnCurrentSearchResult() {
let current = $$(this.searchResults).find('.coveo-facet-search-current-result');
Assert.check(current != undefined);
const shouldExclude = $$(current).hasClass('coveo-facet-value-will-exclude');
if (shouldExclude) {
const excludeIcon = $$(current).find('.coveo-facet-value-exclude');
excludeIcon.click();
return;
}
const checkbox = <HTMLInputElement>$$(current).find('input[type="checkbox"]');
if (checkbox) {
checkbox.checked = true;
$$(checkbox).trigger('change');
} else {
current.click();
}
}
private performExcludeActionOnCurrentSearchResult() {
let current = $$(this.searchResults).find('.coveo-facet-search-current-result');
Assert.check(current != null);
let valueCaption = $$(current).find('.coveo-facet-value-caption');
let valueElement = this.facet.facetValuesList.get($$(valueCaption).text());
valueElement.toggleExcludeWithUA();
}
protected selectAllValuesMatchingSearch() {
this.facet.showWaitingAnimation();
let searchParameters = new FacetSearchParameters(this.facet);
searchParameters.nbResults = 1000;
searchParameters.setValueToSearch(this.getValueInInputForFacetSearch());
this.facet.facetQueryController.search(searchParameters).then((fieldValues: IIndexFieldValue[]) => {
this.dismissSearchResults();
ModalBox.close(true);
let facetValues = map(fieldValues, fieldValue => {
let facetValue = this.facet.values.get(fieldValue.value);
if (!Utils.exists(facetValue)) {
facetValue = FacetValue.create(fieldValue);
}
facetValue.selected = true;
facetValue.excluded = false;
return facetValue;
});
this.facet.processFacetSearchAllResultsSelected(facetValues);
});
this.dismissSearchResults();
}
}