UNPKG

coveo-search-ui

Version:

Coveo JavaScript Search Framework

627 lines (544 loc) • 22.4 kB
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); } }