coveo-search-ui
Version:
Coveo JavaScript Search Framework
627 lines (544 loc) • 22.4 kB
text/typescript
import Popper from 'popper.js';
import 'styling/_FacetSettings';
import { compact, contains, each, escape, filter, find, findWhere, map } from 'underscore';
import { InitializationEvents } from '../../events/InitializationEvents';
import { QueryStateModel } from '../../models/QueryStateModel';
import { l } from '../../strings/Strings';
import { AccessibleButton } from '../../utils/AccessibleButton';
import { $$ } from '../../utils/Dom';
import { KEYBOARD, KeyboardUtils } from '../../utils/KeyboardUtils';
import { LocalStorageUtils } from '../../utils/LocalStorageUtils';
import { SVGDom } from '../../utils/SVGDom';
import { SVGIcons } from '../../utils/SVGIcons';
import { Utils } from '../../utils/Utils';
import { analyticsActionCauseList, IAnalyticsFacetMeta } from '../Analytics/AnalyticsActionListMeta';
import { Facet } from './Facet';
import { FacetSort, IFacetSortDescription } from './FacetSort';
export interface IFacetSettingsKlass {
new (sorts: string[], facet: Facet): FacetSettings;
}
export interface IFacetState {
included: string[];
excluded: string[];
operator: string;
}
/**
* Handle the rendering of the {@link Facet} settings menu (typically the ... in the facet header).
*/
export class FacetSettings extends FacetSort {
public loadedFromSettings: { [attribute: string]: any };
public settingsButton: HTMLElement;
public settingsPopup: HTMLElement;
private facetStateLocalStorage: LocalStorageUtils<IFacetState>;
private includedStateAttribute: string;
private excludedStateAttribute: string;
private operatorStateAttribute: string;
private directionSection: HTMLElement[];
private saveStateSection: HTMLElement;
private clearStateSection: HTMLElement;
private hideSection: HTMLElement;
private showSection: HTMLElement;
private sortSection: { element: HTMLElement; sortItems: HTMLElement[] };
private customSortDirectionChange = false;
private onDocumentClick = () => this.close();
private closeTimeout: number;
private enabledSortsIgnoreRenderBecauseOfPairs: IFacetSortDescription[] = [];
private get isExpanded() {
return this.settingsButton && this.settingsButton.getAttribute('aria-expanded') === `${true}`;
}
private set isExpanded(expanded: boolean) {
this.settingsButton.setAttribute('aria-expanded', `${expanded}`);
}
private get firstFocusablePopupElement() {
return find($$(this.settingsPopup).findAll('[tabindex]'), element => element.tabIndex >= 0);
}
constructor(public sorts: string[], public facet: Facet) {
super(sorts, facet);
this.filterDuplicateForRendering();
}
/**
* Build the menu, hook click events.
* @returns {HTMLElement}
*/
public build() {
this.buildSettingsButton();
this.buildSettingsPopup();
if (Utils.isNonEmptyArray(this.enabledSorts)) {
this.sortSection = this.buildSortSection();
if (this.enabledSortsAllowDirection()) {
this.directionSection = this.buildDirectionSection();
}
}
if (this.facet.options.enableSettingsFacetState) {
this.saveStateSection = this.buildSaveStateSection();
this.clearStateSection = this.buildClearStateSection();
}
if (this.facet.options.enableCollapse) {
this.hideSection = this.buildHideSection();
this.showSection = this.buildShowSection();
}
const appendCommon = () => {
this.appendIfNotUndefined(this.saveStateSection);
this.appendIfNotUndefined(this.clearStateSection);
this.appendIfNotUndefined(this.hideSection);
this.appendIfNotUndefined(this.showSection);
};
this.addOnDocumentClickHandler();
this.addOnNukeHandler();
if (Utils.isNonEmptyArray(this.enabledSorts)) {
this.settingsPopup.appendChild(this.sortSection.element);
each(this.directionSection, d => {
this.appendIfNotUndefined(d);
});
appendCommon();
} else {
appendCommon();
}
return this.settingsButton;
}
/**
* Restore the facet state from local storage, and apply it in the query state model.
*/
public loadSavedState() {
if (this.facetStateLocalStorage) {
// set the state from the settings only if there is nothing
// in the query state model for the current facet
const state = this.facetStateLocalStorage.load();
const currentStateIncluded = this.facet.queryStateModel.get(this.includedStateAttribute);
const currentStateExcluded = this.facet.queryStateModel.get(this.excludedStateAttribute);
const currentStateOperator = this.facet.queryStateModel.get(this.operatorStateAttribute);
if (
!Utils.isNullOrUndefined(state) &&
Utils.isEmptyArray(currentStateIncluded) &&
Utils.isEmptyArray(currentStateExcluded) &&
!Utils.isNonEmptyString(currentStateOperator)
) {
const toSet: { [key: string]: any } = {};
toSet[this.includedStateAttribute] = state.included;
toSet[this.excludedStateAttribute] = state.excluded;
toSet[this.operatorStateAttribute] = state.operator;
this.facet.queryStateModel.setMultiple(toSet);
this.loadedFromSettings = toSet;
}
} else {
this.facet.logger.info('Facet state local storage not enabled : See Facet.options.enableSettingsFacetState');
}
}
/**
* Take the current state of the facet and save it in the local storage.
*/
public saveState() {
if (this.facetStateLocalStorage) {
this.facetStateLocalStorage.save({
included: this.facet.queryStateModel.get(this.includedStateAttribute),
excluded: this.facet.queryStateModel.get(this.excludedStateAttribute),
operator: this.facet.queryStateModel.get(this.operatorStateAttribute)
});
} else {
this.facet.logger.info('Facet state local storage not enabled : See Facet.options.enableSettingsFacetState');
}
}
/**
* Close the settings menu
*/
public close() {
if (!this.isExpanded) {
return;
}
this.isExpanded = false;
$$(this.settingsPopup).detach();
}
/**
* Open the settings menu
*/
public open() {
$$(this.settingsPopup).insertAfter(this.settingsButton);
new Popper(this.settingsButton, this.settingsPopup);
this.isExpanded = true;
if (this.hideSection && this.showSection) {
$$(this.hideSection).toggle(!$$(this.facet.element).hasClass('coveo-facet-collapsed'));
$$(this.showSection).toggle($$(this.facet.element).hasClass('coveo-facet-collapsed'));
}
if (this.facet.options.enableSettingsFacetState) {
$$(this.clearStateSection).toggle(!Utils.isNullOrUndefined(this.facetStateLocalStorage.load()));
}
each(this.enabledSorts, (criteria: IFacetSortDescription, i) => {
if (this.activeSort.name == criteria.name.toLowerCase()) {
this.selectItem(this.getSortItem(criteria.name));
} else {
this.unselectItem(this.getSortItem(criteria.name));
}
});
const elementToFocus = this.firstFocusablePopupElement;
if (elementToFocus) {
elementToFocus.focus();
}
}
public getSortItem(sortName: string): HTMLElement {
return find(this.sortSection.sortItems, sortItem => {
return $$(sortItem).getAttribute('data-sort-name').toLowerCase() == sortName.replace('ascending|descending', '').toLowerCase();
});
}
public get button() {
return this.settingsButton;
}
private buildSettingsButton() {
this.settingsButton = $$('div', { className: 'coveo-facet-header-settings', 'aria-haspopup': 'true' }).el;
this.settingsButton.innerHTML = SVGIcons.icons.more;
SVGDom.addClassToSVGInContainer(this.settingsButton, 'coveo-facet-settings-more-svg');
this.hideElementOnMouseEnterLeave(this.settingsButton);
this.isExpanded = false;
new AccessibleButton()
.withElement(this.settingsButton)
.withLabel(l('Settings'))
.withClickAction(e => this.handleSettingsButtonClick(e))
.withEnterKeyboardAction(e => this.handleSettingsButtonClick(e))
.build();
}
private hideElementOnMouseEnterLeave(el: HTMLElement) {
const mouseLeave = () => (this.closeTimeout = window.setTimeout(() => this.close(), 300));
const mouseEnter = () => clearTimeout(this.closeTimeout);
$$(el).on('mouseleave', mouseLeave);
$$(el).on('mouseenter', mouseEnter);
$$(el).on(
'keyup',
KeyboardUtils.keypressAction(KEYBOARD.ESCAPE, () => this.handleKeyboardClose())
);
}
private handleKeyboardClose() {
this.close();
this.settingsButton.focus();
}
private buildSettingsPopup() {
this.settingsPopup = $$('div', { className: 'coveo-facet-settings-popup' }).el;
this.hideElementOnMouseEnterLeave(this.settingsPopup);
$$(this.settingsPopup).on('focusout', (e: FocusEvent) => {
if (e.relatedTarget && this.settingsPopup.contains(e.relatedTarget as Node)) {
return;
}
this.close();
});
}
private buildSortSection() {
const sortSection = this.buildSection('coveo-facet-settings-section-sort');
const sortSectionIcon = this.buildIcon('coveo-facet-settings-section-sort-svg', SVGIcons.icons.sort);
const sortSectionItems = this.buildItems();
const sortTitle = document.createElement('div');
$$(sortTitle).addClass('coveo-facet-settings-section-sort-title');
$$(sortTitle).text(l('SortBy') + ' :');
sortSectionItems.appendChild(sortTitle);
const sortItems = this.buildSortSectionItems();
each(sortItems, s => {
sortSectionItems.appendChild(s);
});
sortSection.appendChild(sortSectionIcon);
sortSection.appendChild(sortSectionItems);
return { element: sortSection, sortItems: sortItems };
}
private buildSortSectionItems() {
let elems = map(this.enabledSorts, enabledSort => {
if (contains(this.enabledSortsIgnoreRenderBecauseOfPairs, enabledSort)) {
return undefined;
} else {
const elem = this.buildItem(l(enabledSort.label), enabledSort.description);
$$(elem).setAttribute('data-sort-name', enabledSort.name.toLowerCase().replace('ascending|descending', ''));
new AccessibleButton()
.withElement(elem)
.withSelectAction((e: Event) => this.handleClickSortButton(e, enabledSort))
.withLabel(enabledSort.label)
.build();
return elem;
}
});
elems = compact(elems);
return elems;
}
private closePopupAndUpdateSort() {
this.close();
if (this.activeSort.name != 'custom') {
this.facet.updateSort(this.activeSort.name);
} else {
this.facet.updateSort('nosort');
if (this.customSortDirectionChange) {
this.customSortDirectionChange = false;
this.facet.queryController.executeQuery();
}
}
}
private enabledSortsAllowDirection() {
const allEnabledSortsWithPossibleDirectionToggle = filter(this.enabledSorts, (facetSortDescription: IFacetSortDescription) => {
return facetSortDescription.directionToggle;
});
const allowToggle = filter(allEnabledSortsWithPossibleDirectionToggle, possibleDirectionToggle => {
return findWhere(this.enabledSorts, { name: possibleDirectionToggle.relatedSort }) != undefined;
});
return allowToggle.length > 0;
}
private buildDirectionSection() {
const directionAscendingSection = this.buildAscendingOrDescendingSection('Ascending');
const iconAscending = this.buildIcon('coveo-facet-settings-section-direction-ascending-svg', SVGIcons.icons.ascending);
const iconDescending = this.buildIcon('coveo-facet-settings-section-direction-descending-svg', SVGIcons.icons.descending);
const directionItemsAscending = this.buildItems();
const ascending = this.buildAscendingOrDescending('Ascending');
directionItemsAscending.appendChild(ascending);
directionAscendingSection.appendChild(iconAscending);
directionAscendingSection.appendChild(directionItemsAscending);
new AccessibleButton()
.withElement(ascending)
.withoutLabelOrTitle()
.withSelectAction(() => this.handleDirectionClick(ascending, 'ascending'))
.build();
this.unselectSection(directionAscendingSection);
const directionDescendingSection = this.buildAscendingOrDescendingSection('Descending');
const directionItemsDescending = this.buildItems();
const descending = this.buildAscendingOrDescending('Descending');
directionItemsDescending.appendChild(descending);
directionDescendingSection.appendChild(iconDescending);
directionDescendingSection.appendChild(directionItemsDescending);
new AccessibleButton()
.withElement(descending)
.withoutLabelOrTitle()
.withSelectAction(() => this.handleDirectionClick(descending, 'descending'))
.build();
this.unselectSection(directionDescendingSection);
if (!this.activeSort.directionToggle) {
$$(directionAscendingSection).addClass('coveo-facet-settings-disabled');
$$(directionDescendingSection).addClass('coveo-facet-settings-disabled');
} else {
this.selectItem(this.getItems(directionAscendingSection)[0]);
}
return [directionAscendingSection, directionDescendingSection];
}
private buildSaveStateSection() {
const saveStateSection = this.buildSection('coveo-facet-settings-section-save-state');
const icon = this.buildIcon('coveo-facet-settings-section-save-state-svg', SVGIcons.icons.dropdownMore);
const saveStateItems = this.buildItems();
this.facetStateLocalStorage = new LocalStorageUtils<IFacetState>('facet-state-' + this.facet.options.id);
this.includedStateAttribute = QueryStateModel.getFacetId(this.facet.options.id);
this.excludedStateAttribute = QueryStateModel.getFacetId(this.facet.options.id, false);
this.operatorStateAttribute = QueryStateModel.getFacetOperator(this.facet.options.id);
const saveStateItem = document.createElement('div');
$$(saveStateItem).addClass('coveo-facet-settings-item');
saveStateItem.setAttribute('title', l('SaveFacetState'));
$$(saveStateItem).text(l('SaveFacetState'));
saveStateItems.appendChild(saveStateItem);
saveStateSection.appendChild(icon);
saveStateSection.appendChild(saveStateItems);
new AccessibleButton()
.withElement(saveStateSection)
.withSelectAction(() => this.handleSaveStateClick())
.withoutLabelOrTitle()
.build();
return saveStateSection;
}
private buildClearStateSection() {
const clearStateSection = this.buildSection('coveo-facet-settings-section-clear-state');
const icon = this.buildIcon('coveo-facet-settings-section-clear-state-svg', SVGIcons.icons.dropdownLess);
const clearStateItems = this.buildItems();
const clearStateItem = this.buildItem(l('ClearFacetState'));
clearStateItems.appendChild(clearStateItem);
clearStateSection.appendChild(icon);
clearStateSection.appendChild(clearStateItems);
new AccessibleButton()
.withElement(clearStateSection)
.withSelectAction(() => this.handleClearStateClick())
.withoutLabelOrTitle()
.build();
return clearStateSection;
}
private buildHideSection() {
const hideSection = this.buildSection('coveo-facet-settings-section-hide');
const icon = this.buildIcon('coveo-facet-settings-section-hide-svg', SVGIcons.icons.facetCollapse);
const hideItems = this.buildItems();
const hideItem = this.buildItem(l('Collapse'));
hideItems.appendChild(hideItem);
hideSection.appendChild(icon);
hideSection.appendChild(hideItems);
new AccessibleButton()
.withElement(hideSection)
.withSelectAction(() => {
this.facet.facetHeader.collapseFacet();
this.close();
})
.withLabel(l('CollapseFacet', this.facet.options.title))
.build();
return hideSection;
}
private buildShowSection() {
const showSection = this.buildSection('coveo-facet-settings-section-show');
const icon = this.buildIcon('coveo-facet-settings-section-show-svg', SVGIcons.icons.facetExpand);
const showItems = this.buildItems();
const showItem = this.buildItem(l('Expand'));
showItems.appendChild(showItem);
showSection.appendChild(icon);
showSection.appendChild(showItems);
new AccessibleButton()
.withElement(showSection)
.withSelectAction(() => {
this.facet.facetHeader.expandFacet();
this.close();
})
.withLabel(l('ExpandFacet', this.facet.options.title))
.build();
return showSection;
}
private buildIcon(iconClass?: string, svgIcon?: string) {
if (iconClass && svgIcon) {
const icon = $$('div', { className: 'coveo-icon-container' }, svgIcon);
SVGDom.addClassToSVGInContainer(icon.el, iconClass);
return icon.el;
} else {
return $$('div', { className: 'coveo-icon' }).el;
}
}
private buildAscendingOrDescending(direction: string) {
const elem = this.buildItem(l(direction));
elem.setAttribute('aria-disabled', this.activeSort.directionToggle ? 'false' : 'true');
elem.setAttribute('data-direction', direction.toLowerCase());
return elem;
}
private buildAscendingOrDescendingSection(direction: string) {
return this.buildSection('coveo-facet-settings-section-direction-' + direction.toLowerCase());
}
private buildItem(label: string, title = label) {
return $$(
'div',
{
className: 'coveo-facet-settings-item',
title: title
},
escape(label)
).el;
}
private buildItems() {
const elem = document.createElement('div');
$$(elem).addClass('coveo-facet-settings-items');
return elem;
}
private buildSection(className: string) {
const section = document.createElement('div');
$$(section).addClass(['coveo-facet-settings-section', className]);
return section;
}
private handleSettingsButtonClick(event: Event) {
event.stopPropagation();
const settingsPopupIsOpen = !Utils.isNullOrUndefined(this.settingsPopup.parentElement);
settingsPopupIsOpen ? this.close() : this.open();
}
private handleClickSortButton(e: Event, enabledSort: IFacetSortDescription) {
if (this.activeSort != enabledSort && this.activeSort.relatedSort != enabledSort.name) {
this.activeSort = enabledSort;
if (enabledSort.directionToggle && contains(this.enabledSorts, FacetSettings.availableSorts[this.activeSort.relatedSort])) {
this.activateDirectionSection();
} else {
this.disableDirectionSection();
}
this.unselectSection(this.sortSection.element);
this.selectItem(<HTMLElement>e.target);
this.closePopupAndUpdateSort();
}
}
private handleDirectionClick(item: HTMLElement, direction: string) {
if (item.getAttribute('aria-disabled') === 'false' && this.activeSort.name.indexOf(direction) === -1) {
this.activeSort = FacetSettings.availableSorts[this.activeSort.relatedSort];
each(this.directionSection, d => {
this.unselectSection(d);
});
this.selectItem(item);
if (this.activeSort.name == 'custom' && this.customSortDirection != direction) {
this.customSortDirection = direction;
this.customSortDirectionChange = true;
}
this.closePopupAndUpdateSort();
}
}
private handleSaveStateClick() {
this.saveState();
this.close();
}
private handleClearStateClick() {
this.facetStateLocalStorage.remove();
this.facet.reset();
this.close();
this.facet.triggerNewQuery(() =>
this.facet.usageAnalytics.logSearchEvent<IAnalyticsFacetMeta>(analyticsActionCauseList.facetClearAll, {
facetId: this.facet.options.id,
facetField: this.facet.options.field.toString(),
facetTitle: this.facet.options.title
})
);
}
private addOnNukeHandler() {
$$(this.facet.root).on(InitializationEvents.nuke, () => this.handleNuke());
}
private addOnDocumentClickHandler() {
document.addEventListener('click', () => this.onDocumentClick());
}
public getCurrentDirectionItem(directionSection = this.directionSection) {
let found: HTMLElement;
each(directionSection, direction => {
if (!found) {
found = find(this.getItems(direction), (direction: HTMLElement) => {
return this.activeSort.name.indexOf(direction.getAttribute('data-direction')) != -1;
});
}
});
if (!found) {
found = directionSection[0];
}
return found;
}
private activateDirectionSection() {
each(this.directionSection, direction => {
$$(direction).removeClass('coveo-facet-settings-disabled');
$$(direction).find('.coveo-facet-settings-item').setAttribute('aria-disabled', 'false');
this.unselectSection(direction);
});
this.selectItem(this.getCurrentDirectionItem());
}
private disableDirectionSection() {
each(this.directionSection, direction => {
$$(direction).addClass('coveo-facet-settings-disabled');
$$(direction).find('.coveo-facet-settings-item').setAttribute('aria-disabled', 'true');
this.unselectSection(direction);
});
}
private getItems(section: HTMLElement) {
return $$(section).findAll('.coveo-facet-settings-item');
}
private unselectSection(section: HTMLElement) {
each(this.getItems(section), i => this.unselectItem(i));
}
private selectItem(item: HTMLElement) {
if (item) {
$$(item).addClass('coveo-selected');
item.setAttribute('aria-pressed', 'true');
}
}
private unselectItem(item: HTMLElement) {
if (item) {
$$(item).removeClass('coveo-selected');
item.setAttribute('aria-pressed', 'false');
}
}
private filterDuplicateForRendering() {
each(this.enabledSorts, (enabledSort: IFacetSortDescription, i: number) => {
if (enabledSort.relatedSort != null) {
for (let j = i + 1; j < this.enabledSorts.length; j++) {
if (this.enabledSorts[j].name == enabledSort.relatedSort) {
this.enabledSortsIgnoreRenderBecauseOfPairs.push(this.enabledSorts[j]);
break;
}
}
}
});
}
private appendIfNotUndefined(toAppend: HTMLElement) {
if (!Utils.isNullOrUndefined(toAppend)) {
this.settingsPopup.appendChild(toAppend);
}
}
private handleNuke() {
document.removeEventListener('click', this.onDocumentClick);
}
}