UNPKG

coveo-search-ui

Version:

Coveo JavaScript Search Framework

1,030 lines (941 loc) • 38.1 kB
/// <reference path="../../controllers/HierarchicalFacetQueryController.ts" /> /// <reference path="HierarchicalFacetValuesList.ts" /> /// <reference path="HierarchicalFacetSearch.ts" /> /// <reference path="HierarchicalBreadcrumbValuesList.ts" /> /// <reference path="HierarchicalFacetValueElement.ts" /> import { IFacetOptions } from '../Facet/Facet'; import { FacetValue } from '../Facet/FacetValue'; import { Facet } from '../Facet/Facet'; import { ComponentOptions } from '../Base/ComponentOptions'; import { HierarchicalFacetValuesList } from './HierarchicalFacetValuesList'; import { HierarchicalFacetQueryController } from '../../controllers/HierarchicalFacetQueryController'; import { IComponentBindings } from '../Base/ComponentBindings'; import { IIndexFieldValue } from '../../rest/FieldValue'; import { Utils } from '../../utils/Utils'; import { $$ } from '../../utils/Dom'; import { Defer } from '../../misc/Defer'; import { HierarchicalFacetSearchValuesList } from './HierarchicalFacetSearchValuesList'; import { HierarchicalFacetSearch } from './HierarchicalFacetSearch'; import { HierarchicalBreadcrumbValuesList } from './HierarchicalBreadcrumbValuesList'; import { IQuerySuccessEventArgs } from '../../events/QueryEvents'; import { Assert } from '../../misc/Assert'; import { IPopulateBreadcrumbEventArgs } from '../../events/BreadcrumbEvents'; import { IPopulateOmniboxEventArgs } from '../../events/OmniboxEvents'; import { OmniboxHierarchicalValuesList } from './OmniboxHierarchicalValuesList'; import { HierarchicalFacetValueElement } from './HierarchicalFacetValueElement'; import { Initialization } from '../Base/Initialization'; import { ISearchAlertsPopulateMessageEventArgs } from '../../events/SearchAlertEvents'; import * as _ from 'underscore'; import { exportGlobally } from '../../GlobalExports'; import 'styling/_HierarchicalFacet'; import { SVGIcons } from '../../utils/SVGIcons'; import { SVGDom } from '../../utils/SVGDom'; import { ResponsiveFacetOptions } from '../ResponsiveComponents/ResponsiveFacetOptions'; import { AccessibleButton } from '../../utils/AccessibleButton'; export interface IHierarchicalFacetOptions extends IFacetOptions { delimitingCharacter?: string; levelStart?: number; levelEnd?: number; marginByLevel?: number; } export interface IValueHierarchy { childs?: IValueHierarchy[]; parent?: IValueHierarchy; originalPosition?: number; facetValue: FacetValue; level: number; keepOpened: boolean; hasChildSelected: boolean; allChildShouldBeSelected: boolean; } interface IFlatHierarchy { facetValue: FacetValue; level: number; parent: string; self: string; } /** * @deprecated This component is exposed for legacy reasons. Instead, use the {@link CategoryFacet} component, which is more performant and easier to use. * * The `HierarchicalFacet` component inherits all of its options and behaviors from the [`Facet`]{@link Facet} * component, but is meant to be used to render hierarchical values. * * **Note:** * > The `HierarchicalFacet` component does not support the [`customSort`]{@link Facet.options.customSort} * > `Facet` option. * * The `HierarchicalFacet` component can be used to display files in a file system, or categories for items in a * hierarchy. * * This facet requires a group by field with a special format to work correctly. * * **Example:** * * You have the following files indexed on a file system: * ``` * c:\ * folder1\ * text1.txt * folder2\ * folder3\ * text2.txt * ``` * The `text1.txt` item would have a field with the following format: * `c; c|folder1;` * * The `text2.txt` item would have a field with the following format: * `c; c|folder2; c|folder2|folder3;` * * By default, the `|` character determines the hierarchy (`folder3` inside `folder2` inside `c`). * * Since both items contain the `c` value, selecting it value in the facet would return both items. * * Selecting the `folder3` value in the facet would only return the `text2.txt` item. * * @notSupportedIn salesforcefree * * @deprecatedSince [January 2019 Release (v2.5395.12)](https://docs.coveo.com/en/2763/) */ export class HierarchicalFacet extends Facet implements IComponentBindings { static ID = 'HierarchicalFacet'; static doExport = () => { exportGlobally({ HierarchicalFacet: HierarchicalFacet }); }; /** * The options for the component * @componentOptions */ static options: IHierarchicalFacetOptions = { /** * The character that allows to specify the hierarchical dependency. * * **Example:** * * If your field has the following values: * * `@field: c; c>folder2; c>folder2>folder3;` * * The delimiting character is `>`. * * Default value is `|`. */ delimitingCharacter: ComponentOptions.buildStringOption({ defaultValue: '|' }), /** * Specifies at which level (0-based index) of the hierarchy the `HierarchicalFacet` should start displaying its * values. * * **Example:** * * If you have the following files indexed on a file system: * ``` * c:\ * folder1\ * text1.txt * folder2\ * folder3\ * text2.txt * ``` * Setting `levelStart` to `1` displays `folder1` and `folder2` in the `HierarchicalFacet`, but omits `c:`. * * Default (and minimum) value is `0`. */ levelStart: ComponentOptions.buildNumberOption({ defaultValue: 0, min: 0 }), /** * Specifies at which level (0-based index) of the hierarchy the `HierarchicalFacet` should stop displaying its * values. * * Default value is `undefined`, which means the `HierarchicalFacet` component renders all hierarchical levels. * Minimum value is `0`. */ levelEnd: ComponentOptions.buildNumberOption({ min: 0 }), /** * Specifies the margin (in pixels) to display between each hierarchical level when expanding. * * Default value is `10`. */ marginByLevel: ComponentOptions.buildNumberOption({ defaultValue: 10, min: 0 }), ...ResponsiveFacetOptions }; static parent = Facet; public options: IHierarchicalFacetOptions; public facetValuesList: HierarchicalFacetValuesList; public numberOfValuesToShow: number; public facetQueryController: HierarchicalFacetQueryController; public topLevelHierarchy: IValueHierarchy[]; public shouldReshuffleFacetValuesClientSide = false; public isFieldValueCompatible = false; private valueHierarchy: { [facetValue: string]: IValueHierarchy }; private originalNumberOfValuesToShow: number; private correctLevels: IFlatHierarchy[] = []; /** * Creates a new `HierarchicalFacet` component. * @param element The HTMLElement on which to instantiate the component. * @param options The options for the `HierarchicalFacet` component. * @param bindings The bindings that the component requires to function normally. If not set, these will be * automatically resolved (with a slower execution time). */ public constructor(public element: HTMLElement, options: IHierarchicalFacetOptions, public bindings: IComponentBindings) { super(element, options, bindings, HierarchicalFacet.ID); this.options = ComponentOptions.initComponentOptions(element, HierarchicalFacet, this.options); this.numberOfValuesToShow = this.originalNumberOfValuesToShow = this.options.numberOfValues || 5; this.numberOfValues = Math.max(this.options.numberOfValues, 10000); this.options.injectionDepth = Math.max(this.options.injectionDepth, 10000); this.logger.info('Hierarchy facet: Set number of values very high in order to build hierarchy', this.numberOfValues, this); this.logger.info('Hierarchy facet: Set injection depth very high in order to build hierarchy', this.options.injectionDepth); } /** * Selects a single value. * @param value The value to select. * @param selectChildren Specifies whether to also select all child values (if any). Default value is the opposite of * the [`useAnd`]{@link Facet.options.useAnd} option value set for this `HierarchicalFacet`. */ public selectValue(value: FacetValue | string, selectChildren = !this.options.useAnd) { this.ensureDom(); this.ensureValueHierarchyExists([value]); let valueHierarchy = this.getValueFromHierarchy(value); if (selectChildren) { this.selectChilds(valueHierarchy, valueHierarchy.childs); } this.flagParentForSelection(valueHierarchy); super.selectValue(value); } /** * Selects multiple values * @param values The array of values to select. * @param selectChildren Specifies whether to also select all child values (if any). Default value is the opposite of * the [`useAnd`]{@link Facet.options.useAnd} option value set for this `HierarchicalFacet`. */ public selectMultipleValues(values: FacetValue[] | string[], selectChildren = !this.options.useAnd): void { this.ensureDom(); this.ensureValueHierarchyExists(values); _.each(values as FacetValue[], value => { let valueHierarchy = this.getValueFromHierarchy(value); this.flagParentForSelection(valueHierarchy); if (selectChildren) { _.each(valueHierarchy.childs, child => { this.selectValue(child.facetValue); }); } }); super.selectMultipleValues(values); } /** * Deselects a single value * @param value The value to deselect. * @param deselectChildren Specifies whether to also deselect all child values (if any). Default value is `true`. */ public deselectValue(value: FacetValue | string, deselectChildren = true) { this.ensureDom(); this.ensureValueHierarchyExists([value]); let valueHierarchy = this.getValueFromHierarchy(value); if (deselectChildren) { let hasChilds = valueHierarchy.childs != undefined; if (hasChilds) { let activeChilds = _.filter<IValueHierarchy>(valueHierarchy.childs, child => { let valueToCompare = this.getFacetValueFromHierarchy(child.facetValue); return valueToCompare.selected || valueToCompare.excluded; }); valueHierarchy.hasChildSelected = false; if (activeChilds.length == valueHierarchy.childs.length) { this.deselectChilds(valueHierarchy, valueHierarchy.childs); } } } this.deselectParent(valueHierarchy.parent); this.unflagParentForSelection(valueHierarchy); super.deselectValue(value); } /** * Excludes a single value. * @param value The value to exclude. * @param excludeChildren Specifies whether to also exclude all child values (if any). Default value is the opposite * of the [`useAnd`]{@link Facet.options.useAnd} option value set for this `HierarchicalFacet`. */ public excludeValue(value: FacetValue | string, excludeChildren = !this.options.useAnd): void { this.ensureDom(); this.ensureValueHierarchyExists([value]); let valueHierarchy = this.getValueFromHierarchy(value); if (excludeChildren) { this.excludeChilds(valueHierarchy.childs); } else { this.deselectChilds(valueHierarchy, valueHierarchy.childs); this.close(valueHierarchy); } this.flagParentForSelection(valueHierarchy); super.excludeValue(value); } /** * Un-excludes a single value. * @param value The value to un-exclude. * @param unexludeChildren Specifies whether to also un-exclude all child values (if any). Default value is the * opposite of the [`useAnd`]{@link Facet.options.useAnd} option value set for this `HierarchicalFacet`. */ public unexcludeValue(value: FacetValue | string, unexludeChildren = !this.options.useAnd): void { this.ensureDom(); this.ensureValueHierarchyExists([value]); let valueHierarchy = this.getValueFromHierarchy(value); if (unexludeChildren) { this.unexcludeChilds(valueHierarchy.childs); } this.unflagParentForSelection(valueHierarchy); super.unexcludeValue(value); } /** * Deselects multiple values. * @param values The array of values to deselect. * @param deselectChildren Specifies whether to also deselect all child values (if any). Default value is the opposite * of the [`useAnd`]{@link Facet.options.useAnd} option value set for this `HierarchicalFacet`. */ public deselectMultipleValues(values: FacetValue[] | string[], deselectChildren = !this.options.useAnd): void { this.ensureDom(); this.ensureValueHierarchyExists(values); _.each(values as FacetValue[], value => { let valueHierarchy = this.getValueFromHierarchy(value); valueHierarchy.hasChildSelected = false; this.unflagParentForSelection(valueHierarchy); if (deselectChildren) { _.each(valueHierarchy.childs, child => { let childInHierarchy = this.getValueFromHierarchy(child.facetValue); childInHierarchy.hasChildSelected = false; this.deselectValue(child.facetValue); }); } }); super.deselectMultipleValues(values); } /** * Toggles the selection of a single value (selects value if not selected; deselects value if selected). * @param value The value to select or deselect. */ public toggleSelectValue(value: FacetValue | string): void { this.ensureDom(); this.ensureValueHierarchyExists([value]); if (this.getFacetValueFromHierarchy(value).selected == false) { this.selectValue(value); } else { this.deselectValue(value); } } /** * Toggles the exclusion of a single value (excludes value if not excluded; un-excludes value if excluded). * @param value The value to exclude or un-exclude. */ public toggleExcludeValue(value: FacetValue | string): void { this.ensureDom(); this.ensureValueHierarchyExists([value]); if (this.getFacetValueFromHierarchy(value).excluded == false) { this.excludeValue(value); } else { this.unexcludeValue(value); } } /** * Gets the caption of a single value. * @param facetValue The value whose caption the method should return. * @returns {string} The caption of the value. */ public getValueCaption(facetValue: IIndexFieldValue | FacetValue): string { let stringValue = this.getSelf(facetValue as FacetValue); let ret = stringValue; if (Utils.exists(this.options.valueCaption)) { if (typeof this.options.valueCaption == 'object') { ret = this.options.valueCaption[stringValue] || ret; } if (typeof this.options.valueCaption == 'function') { ret = this.options.valueCaption.call(this, facetValue); } } return ret; } /** * Gets the values that the `HierarchicalFacet` is currently displaying. * @returns {any[]} An array containing all the values that the `HierarchicalFacet` is currently displaying. */ public getDisplayedValues(): string[] { let displayed = _.filter(this.values.getAll(), v => { let valFromHierarchy = this.getValueFromHierarchy(v); if (valFromHierarchy) { let elem = this.getElementFromFacetValueList(v); return !$$(elem).hasClass('coveo-inactive'); } return false; }); return _.pluck(displayed, 'value'); } /** * Updates the sort criteria for the `HierarchicalFacet`. * * See the [`sortCriteria`]{@link IGroupByRequest.sortCriteria} property of the [`IGroupByRequest`] interface for the * list and description of possible values. * * @param criteria The new sort criteria. */ public updateSort(criteria: string) { super.updateSort(criteria); } /** * Opens (expands) a single value and shows all its children. * @param value The value to open. */ public open(value: FacetValue | IValueHierarchy | string) { let getter; if (_.isString(value)) { getter = this.getValueHierarchy(value); } else if (value instanceof FacetValue) { getter = this.getValueHierarchy(value.value); } else { getter = value; } if (getter != undefined) { $$(this.getElementFromFacetValueList(getter.facetValue.value)).addClass('coveo-open'); this.showChilds(getter.childs); if (getter.parent != undefined) { this.open(this.getValueHierarchy(getter.facetValue.value).parent); } this.getValueHierarchy(getter.facetValue.value).keepOpened = true; } } /** * Closes (collapses) a single value and hides all its children. * @param value The value to close. */ public close(value: FacetValue | IValueHierarchy | string) { let getter; if (_.isString(value)) { getter = this.getValueHierarchy(value); } else if (value instanceof FacetValue) { getter = this.getValueHierarchy(value.value); } else { getter = value; } if (getter != undefined) { $$(this.getElementFromFacetValueList(getter.facetValue)).removeClass('coveo-open'); this.hideChilds(getter.childs); _.each(getter.childs, (child: IValueHierarchy) => { this.close(this.getValueHierarchy(child.facetValue.value)); }); this.getValueHierarchy(getter.facetValue.value).keepOpened = false; } } /** * Resets the `HierarchicalFacet` state. */ public reset() { _.each(this.getAllValueHierarchy(), valueHierarchy => { valueHierarchy.hasChildSelected = false; valueHierarchy.allChildShouldBeSelected = false; }); // Need to close all values, otherwise we might end up with orphan(s) // if a parent value, after reset, is no longer visible. _.each(this.getAllValueHierarchy(), valueHierarchy => { this.close(valueHierarchy); }); super.reset(); } public processFacetSearchAllResultsSelected(facetValues: FacetValue[]): void { this.selectMultipleValues(facetValues); this.triggerNewQuery(); } protected triggerUpdateDeltaQuery(facetValues: FacetValue[]) { this.shouldReshuffleFacetValuesClientSide = this.keepDisplayedValuesNextTime; super.triggerUpdateDeltaQuery(facetValues); } protected updateSearchElement(moreValuesAvailable = true) { // We always want to show search for hierarchical facet : // It's useful since child values are folded under their parent most of the time super.updateSearchElement(true); } protected facetValueHasChanged() { this.updateQueryStateModel(); Defer.defer(() => { this.updateAppearanceDependingOnState(); }); } protected initFacetQueryController() { this.facetQueryController = new HierarchicalFacetQueryController(this); } protected initFacetSearch() { this.facetSearch = new HierarchicalFacetSearch(this, HierarchicalFacetSearchValuesList, this.root); this.element.appendChild(this.facetSearch.build()); } protected handleDeferredQuerySuccess(data: IQuerySuccessEventArgs) { this.updateAppearanceDependingOnState(); super.handleDeferredQuerySuccess(data); } protected handlePopulateSearchAlerts(args: ISearchAlertsPopulateMessageEventArgs) { if (this.values.hasSelectedOrExcludedValues()) { args.text.push( new HierarchicalBreadcrumbValuesList( this, this.values.getSelected().concat(this.values.getExcluded()), this.getAllValueHierarchy() ).buildAsString() ); } } protected handlePopulateBreadcrumb(args: IPopulateBreadcrumbEventArgs) { Assert.exists(args); if (this.values.hasSelectedOrExcludedValues()) { let element = new HierarchicalBreadcrumbValuesList( this, this.values.getSelected().concat(this.values.getExcluded()), this.getAllValueHierarchy(), { headingLevel: args.headingLevel } ).build(); args.breadcrumbs.push({ element: element }); } } protected handleOmniboxWithStaticValue(eventArg: IPopulateOmniboxEventArgs) { let regex = eventArg.completeQueryExpression.regex; let match = _.first( _.filter<IValueHierarchy>(this.getAllValueHierarchy(), existingValue => { return regex.test(this.getValueCaption(existingValue.facetValue)); }), this.options.numberOfValuesInOmnibox ); let facetValues = _.compact( _.map(match, gotAMatch => { let fromList = this.getFromFacetValueList(gotAMatch.facetValue); return fromList ? fromList.facetValue : undefined; }) ); let element = new OmniboxHierarchicalValuesList(this, facetValues, eventArg).build(); eventArg.rows.push({ element: element, zIndex: this.omniboxZIndex }); } protected rebuildValueElements() { this.shouldReshuffleFacetValuesClientSide = this.shouldReshuffleFacetValuesClientSide || this.keepDisplayedValuesNextTime; this.numberOfValues = Math.max(this.numberOfValues, 10000); this.processHierarchy(); this.setValueListContent(); super.rebuildValueElements(); this.buildParentChildRelationship(); this.checkForOrphans(); this.checkForNewUnselectedChild(); this.crop(); this.shouldReshuffleFacetValuesClientSide = false; } protected initFacetValuesList() { this.facetValuesList = new HierarchicalFacetValuesList(this, HierarchicalFacetValueElement); this.element.appendChild(this.facetValuesList.build()); } protected updateMoreLess() { let moreValuesAvailable = this.numberOfValuesToShow < this.topLevelHierarchy.length; let lessIsShown = this.numberOfValuesToShow > this.originalNumberOfValuesToShow; super.updateMoreLess(lessIsShown, moreValuesAvailable); } protected handleClickMore(): void { this.numberOfValuesToShow += this.originalNumberOfValuesToShow; this.numberOfValuesToShow = Math.min(this.numberOfValuesToShow, this.values.size()); this.crop(); this.updateMoreLess(); } protected handleClickLess() { this.numberOfValuesToShow = this.originalNumberOfValuesToShow; this.crop(); this.updateMoreLess(); } protected updateNumberOfValues() { this.numberOfValues = Math.max(this.numberOfValues, 10000); } private ensureValueHierarchyExists(facetValues: any) { if (facetValues[0] && typeof facetValues[0] == 'string') { facetValues = _.map(facetValues, (value: string) => { return FacetValue.createFromValue(value); }); } let atLeastOneDoesNotExists = false; _.each(facetValues, (facetValue: FacetValue) => { if (this.getValueHierarchy(facetValue.value) == undefined) { atLeastOneDoesNotExists = true; } }); if (atLeastOneDoesNotExists) { this.processHierarchy(facetValues); } } private crop() { // Partition the top level or the facet to only operate on the values that are not selected or excluded let partition = _.partition(this.topLevelHierarchy, (hierarchy: IValueHierarchy) => { return hierarchy.facetValue.selected || hierarchy.facetValue.excluded || hierarchy.hasChildSelected; }); // Hide and show the partitionned top level values, depending on the numberOfValuesToShow let numberOfValuesLeft = this.numberOfValuesToShow - partition[0].length; _.each(_.last(partition[1], partition[1].length - numberOfValuesLeft), (toHide: IValueHierarchy) => { this.hideFacetValue(toHide); this.hideChilds(toHide.childs); }); _.each(_.first(partition[1], numberOfValuesLeft), (toShow: IValueHierarchy) => { this.showFacetValue(toShow); }); } private placeChildsUnderTheirParent(hierarchy: IValueHierarchy, hierarchyElement: HTMLElement) { let toIterateOver = hierarchy.childs; if (toIterateOver) { let toIterateOverSorted = this.facetValuesList.sortFacetValues(_.pluck(toIterateOver, 'facetValue')).reverse(); _.each(toIterateOverSorted, child => { let childFromHierarchy = this.getValueFromHierarchy(child); if (childFromHierarchy) { let childElement = this.getElementFromFacetValueList(child); $$(childElement).insertAfter(hierarchyElement); if (childFromHierarchy.childs && childFromHierarchy.childs.length != 0) { this.placeChildsUnderTheirParent(childFromHierarchy, childElement); } } }); } if (hierarchy.keepOpened) { this.open(hierarchy); this.showChilds(hierarchy.childs); } else { this.hideChilds(hierarchy.childs); } } private addCssClassToParentAndChilds(hierarchy: IValueHierarchy, hierarchyElement: HTMLElement) { $$(hierarchyElement).addClass('coveo-has-childs'); if (hierarchy.hasChildSelected) { $$(hierarchyElement).addClass('coveo-has-childs-selected'); } const expandChilds = $$('span', { className: 'coveo-hierarchical-facet-expand' }, SVGIcons.icons.facetExpand); const collapseChilds = $$('span', { className: 'coveo-hierarchical-facet-collapse' }, SVGIcons.icons.facetCollapse); SVGDom.addClassToSVGInContainer(expandChilds.el, 'coveo-hierarchical-facet-expand-svg'); SVGDom.addClassToSVGInContainer(collapseChilds.el, 'coveo-hierarchical-facet-collapse-svg'); let openAndCloseChilds = $$( 'div', { className: 'coveo-has-childs-toggle' }, expandChilds.el, collapseChilds.el ).el; new AccessibleButton() .withElement(openAndCloseChilds) .withoutLabelOrTitle() .withSelectAction(() => { $$(hierarchyElement).hasClass('coveo-open') ? this.close(hierarchy) : this.open(hierarchy); }) .build(); $$(hierarchyElement).prepend(openAndCloseChilds); } private buildParentChildRelationship() { let fragment = document.createDocumentFragment(); fragment.appendChild(this.facetValuesList.valueContainer); let sorted = _.map(this.facetValuesList.sortFacetValues(), (facetValue: FacetValue) => { return this.getValueFromHierarchy(facetValue); }); _.each(sorted, (hierarchy: IValueHierarchy) => { let hierarchyElement = this.getElementFromFacetValueList(hierarchy.facetValue); if (Utils.isNonEmptyArray(hierarchy.childs)) { this.placeChildsUnderTheirParent(hierarchy, hierarchyElement); this.addCssClassToParentAndChilds(hierarchy, hierarchyElement); } else { $$(hierarchyElement).addClass('coveo-no-childs'); } hierarchyElement.style.marginLeft = this.options.marginByLevel * (hierarchy.level - this.options.levelStart) + 'px'; }); $$(<any>fragment).insertAfter(this.headerElement); } private setValueListContent() { this.facetValuesList.hierarchyFacetValues = _.map(this.correctLevels, hierarchy => { if (!this.values.contains(hierarchy.facetValue.value)) { hierarchy.facetValue.occurrences = 0; this.values.add(hierarchy.facetValue); } return hierarchy.facetValue; }); } private createHierarchy(valuesToBuildWith: FacetValue[]) { let flatHierarchy = _.map(valuesToBuildWith, (value: FacetValue) => { let parent = this.getParent(value); let self = value.lookupValue || value.value; return { facetValue: value, level: this.getLevel(value), parent: parent, self: self }; }); this.setInHierarchy(flatHierarchy); _.each(this.getAllValueHierarchy(), valueHierarchy => { if (valueHierarchy.facetValue.selected) { this.flagParentForSelection(valueHierarchy); } }); return flatHierarchy; } private processHierarchy(facetValues = this.values.getAll()) { _.each(this.getAllValueHierarchy(), (hierarchy: IValueHierarchy) => { if (this.values.get(hierarchy.facetValue.value) == undefined) { this.deleteValueHierarchy(this.getLookupOrValue(hierarchy.facetValue)); } }); this.createHierarchy(facetValues); } private setInHierarchy(flatHierarchy: IFlatHierarchy[]) { this.correctLevels = _.filter<IFlatHierarchy>(flatHierarchy, hierarchy => { let isCorrectMinimumLevel = this.options.levelStart == undefined || hierarchy.level >= this.options.levelStart; let isCorrectMaximumLevel = this.options.levelEnd == undefined || hierarchy.level < this.options.levelEnd; return isCorrectMinimumLevel && isCorrectMaximumLevel; }); _.each(this.correctLevels, (hierarchy: IFlatHierarchy) => { let childs = _.map( _.filter<IFlatHierarchy>(this.correctLevels, (possibleChild: IFlatHierarchy) => { return possibleChild.parent != null && possibleChild.parent.toLowerCase() == hierarchy.self.toLowerCase(); }), (child): IValueHierarchy => { return { facetValue: child.facetValue, level: child.level, keepOpened: false, hasChildSelected: false, allChildShouldBeSelected: false }; } ); let parent = hierarchy.parent != null ? _.find<IFlatHierarchy>(this.correctLevels, possibleParent => { return possibleParent.self.toLowerCase() == hierarchy.parent.toLowerCase(); }) : null; let hierarchyThatAlreadyExists = this.getValueHierarchy(hierarchy.facetValue.value); if (hierarchyThatAlreadyExists && hierarchyThatAlreadyExists.childs.length != childs.length) { hierarchyThatAlreadyExists.childs = childs; } let hierarchyThatAlreadyExistsAtParent; if (parent) { hierarchyThatAlreadyExistsAtParent = this.getValueHierarchy(parent.facetValue.value); } this.setValueHierarchy(hierarchy.facetValue.value, { childs: childs, parent: parent == undefined ? undefined : { facetValue: parent.facetValue, level: parent.level, keepOpened: hierarchyThatAlreadyExistsAtParent ? hierarchyThatAlreadyExistsAtParent.keepOpened : false, hasChildSelected: hierarchyThatAlreadyExistsAtParent ? hierarchyThatAlreadyExistsAtParent.hasChildSelected : false, originalPosition: hierarchyThatAlreadyExistsAtParent ? hierarchyThatAlreadyExistsAtParent.originalPosition : undefined, allChildShouldBeSelected: hierarchyThatAlreadyExistsAtParent ? hierarchyThatAlreadyExistsAtParent.allChildShouldBeSelected : false }, facetValue: hierarchy.facetValue, level: hierarchy.level, keepOpened: hierarchyThatAlreadyExists ? hierarchyThatAlreadyExists.keepOpened : false, hasChildSelected: hierarchyThatAlreadyExists ? hierarchyThatAlreadyExists.hasChildSelected : false, originalPosition: hierarchyThatAlreadyExists ? hierarchyThatAlreadyExists.originalPosition : undefined, allChildShouldBeSelected: hierarchyThatAlreadyExists ? hierarchyThatAlreadyExists.allChildShouldBeSelected : false }); }); this.topLevelHierarchy = _.chain(this.values.getAll()) .filter((value: FacetValue) => { let fromHierarchy = this.getValueFromHierarchy(value); if (fromHierarchy) { return fromHierarchy.level == (this.options.levelStart || 0); } else { return false; } }) .map((value: FacetValue) => { return this.getValueFromHierarchy(value); }) .value(); } private getParent(value: FacetValue) { let lastIndexOfDelimiting = this.getLookupOrValue(value).lastIndexOf(this.options.delimitingCharacter); if (lastIndexOfDelimiting != -1) { return this.getLookupOrValue(value).substring(0, lastIndexOfDelimiting); } return undefined; } private getSelf(value: FacetValue) { let parent = this.getParent(value); if (parent == undefined) { return this.getLookupOrValue(value); } else { let indexOfParent = this.getLookupOrValue(value).indexOf(parent); return this.getLookupOrValue(value).substring(indexOfParent + parent.length + 1); } } private showFacetValue(value: IValueHierarchy) { $$(this.getElementFromFacetValueList(value.facetValue.value)).removeClass('coveo-inactive'); } private hideFacetValue(value: IValueHierarchy) { $$(this.getElementFromFacetValueList(value.facetValue.value)).addClass('coveo-inactive'); } private hideChilds(children: IValueHierarchy[]) { _.each(children, child => { this.hideFacetValue(child); }); } private showChilds(children: IValueHierarchy[]) { _.each(children, child => { this.showFacetValue(child); }); } private selectChilds(parent: IValueHierarchy, children: IValueHierarchy[]) { this.flagParentForSelection(parent); parent.allChildShouldBeSelected = true; this.selectMultipleValues( _.map(children, child => { return child.facetValue; }) ); } private deselectChilds(parent: IValueHierarchy, children: IValueHierarchy[]) { parent.hasChildSelected = false; parent.allChildShouldBeSelected = false; this.deselectMultipleValues( _.map(children, child => { return child.facetValue; }) ); } private excludeChilds(children: IValueHierarchy[]) { this.excludeMultipleValues( _.map(children, child => { return child.facetValue; }) ); } private unexcludeChilds(children: IValueHierarchy[]) { this.unexcludeMultipleValues( _.map(children, child => { return child.facetValue; }) ); } private deselectParent(parent: IValueHierarchy) { if (parent != undefined) { this.deselectValue(parent.facetValue, false); } } private flagParentForSelection(valueHierarchy: IValueHierarchy) { let parent = valueHierarchy.parent; let current = valueHierarchy; while (parent) { let parentInHierarchy = this.getValueHierarchy(parent.facetValue.value); parentInHierarchy.hasChildSelected = true; let found = _.find(parentInHierarchy.childs, (child: IValueHierarchy) => { return child.facetValue.value.toLowerCase() == current.facetValue.value.toLowerCase(); }); if (found) { if (this.getValueHierarchy(found.facetValue.value).hasChildSelected) { found.hasChildSelected = true; } } parent = parentInHierarchy.parent; current = parentInHierarchy; } } private unflagParentForSelection(valueHierarchy: IValueHierarchy) { let parent = valueHierarchy.parent; while (parent) { let parentInHierarchy = this.getValueHierarchy(parent.facetValue.value); let otherSelectedChilds = _.filter<IValueHierarchy>(parentInHierarchy.childs, child => { let childInHierarchy = this.getValueHierarchy(child.facetValue.value); if (childInHierarchy != undefined) { return ( childInHierarchy.facetValue.value != valueHierarchy.facetValue.value && (childInHierarchy.facetValue.selected || childInHierarchy.facetValue.excluded || childInHierarchy.hasChildSelected) ); } }); if (otherSelectedChilds.length == 0) { parentInHierarchy.hasChildSelected = false; } parentInHierarchy.allChildShouldBeSelected = false; parent = parentInHierarchy.parent; } } public getValueFromHierarchy(value: any): IValueHierarchy { let getter = value instanceof FacetValue ? value.value : value; return this.getValueHierarchy(getter); } private getFacetValueFromHierarchy(value: any): FacetValue { return this.getValueFromHierarchy(value).facetValue; } private getLookupOrValue(value: FacetValue) { return value.lookupValue || value.value; } private getElementFromFacetValueList(value: any) { let ret = this.getFromFacetValueList(value); if (ret) { return ret.renderer.listItem; } else { return $$('div').el; } } private getFromFacetValueList(value: any) { let fromHierarchy = this.getValueFromHierarchy(value); if (fromHierarchy != undefined) { return this.facetValuesList.get(value); } else { return undefined; } } private getLevel(value: FacetValue) { return value.value.split(this.options.delimitingCharacter).length - 1; } public getAllValueHierarchy(): { [facetValue: string]: IValueHierarchy } { if (this.valueHierarchy == null) { this.valueHierarchy = {}; } return this.valueHierarchy; } private deleteValueHierarchy(key: string) { if (this.valueHierarchy != null) { delete this.valueHierarchy[key.toLowerCase()]; } } private getValueHierarchy(key: string): IValueHierarchy { if (this.valueHierarchy == null) { return undefined; } return this.valueHierarchy[key.toLowerCase()]; } private setValueHierarchy(key: string, value: IValueHierarchy) { if (this.valueHierarchy == null) { this.valueHierarchy = {}; } this.valueHierarchy[key.toLowerCase()] = value; } private checkForOrphans() { _.each(this.valueHierarchy, (v: IValueHierarchy) => { if (this.getLevel(v.facetValue) != this.options.levelStart) { if (this.getValueHierarchy(this.getParent(v.facetValue)) == undefined) { this.logger.error( `Orphan value found in HierarchicalFacet : ${v.facetValue.value}`, `Needed : ${this.getParent(v.facetValue)} but not found` ); this.logger.warn(`Removing incoherent facet value : ${v.facetValue.value}`); this.hideFacetValue(v); } } }); } private checkForNewUnselectedChild() { // It's possible that after checking a facet value, the index returns new facet values (because of injection depth); _.each(this.valueHierarchy, (v: IValueHierarchy) => { if (v.allChildShouldBeSelected) { let notAlreadySelected = _.find(v.childs, (child: IValueHierarchy) => { return child.facetValue.selected != true; }); if (notAlreadySelected) { this.selectValue(v.facetValue, true); this.logger.info('Re-executing query with new facet values returned by index'); this.queryController.deferExecuteQuery(); } } }); } } Initialization.registerAutoCreateComponent(HierarchicalFacet);