coveo-search-ui
Version:
Coveo JavaScript Search Framework
1,243 lines (1,186 loc) • 82.2 kB
text/typescript
import 'styling/_Facet';
import 'styling/_FacetFooter';
import * as _ from 'underscore';
import { FacetQueryController } from '../../controllers/FacetQueryController';
import { BreadcrumbEvents, IClearBreadcrumbEventArgs, IPopulateBreadcrumbEventArgs } from '../../events/BreadcrumbEvents';
import { IPopulateOmniboxEventArgs, OmniboxEvents } from '../../events/OmniboxEvents';
import { IDoneBuildingQueryEventArgs, INewQueryEventArgs, IQuerySuccessEventArgs, QueryEvents } from '../../events/QueryEvents';
import { ISearchAlertsPopulateMessageEventArgs, SearchAlertsEvents } from '../../events/SearchAlertEvents';
import { exportGlobally } from '../../GlobalExports';
import { Assert } from '../../misc/Assert';
import { Defer } from '../../misc/Defer';
import { IAttributesChangedEventArg, MODEL_EVENTS } from '../../models/Model';
import { IQueryStateExcludedAttribute, IQueryStateIncludedAttribute, QueryStateModel } from '../../models/QueryStateModel';
import { IIndexFieldValue } from '../../rest/FieldValue';
import { IStringMap } from '../../rest/GenericParam';
import { IGroupByResult } from '../../rest/GroupByResult';
import { IGroupByValue } from '../../rest/GroupByValue';
import { IQueryResults } from '../../rest/QueryResults';
import { ISearchEndpoint } from '../../rest/SearchEndpointInterface';
import { l } from '../../strings/Strings';
import { AccessibleButton } from '../../utils/AccessibleButton';
import { ComponentsTypes } from '../../utils/ComponentsTypes';
import {
DependsOnManager,
IDependentFacet,
IDependsOnCompatibleFacetOptions,
IDependentFacetCondition
} from '../../utils/DependsOnManager';
import { DeviceUtils } from '../../utils/DeviceUtils';
import { $$, Win } from '../../utils/Dom';
import { SVGDom } from '../../utils/SVGDom';
import { SVGIcons } from '../../utils/SVGIcons';
import { Utils } from '../../utils/Utils';
import {
analyticsActionCauseList,
IAnalyticsActionCause,
IAnalyticsFacetMeta,
IAnalyticsFacetSortMeta
} from '../Analytics/AnalyticsActionListMeta';
import { Component } from '../Base/Component';
import { IComponentBindings } from '../Base/ComponentBindings';
import { ComponentOptions } from '../Base/ComponentOptions';
import { IFieldOption, IQueryExpression } from '../Base/IComponentOptions';
import { Initialization } from '../Base/Initialization';
import { IOmniboxDataRow } from '../Omnibox/OmniboxInterface';
import { IResponsiveComponentOptions } from '../ResponsiveComponents/ResponsiveComponentsManager';
import { ResponsiveFacetOptions } from '../ResponsiveComponents/ResponsiveFacetOptions';
import { ResponsiveFacets } from '../ResponsiveComponents/ResponsiveFacets';
import { BreadcrumbValueElement } from './BreadcrumbValueElement';
import { BreadcrumbValueList } from './BreadcrumbValuesList';
import { FacetHeader } from './FacetHeader';
import { FacetSearch } from './FacetSearch';
import { FacetSearchParameters } from './FacetSearchParameters';
import { FacetSearchValuesList } from './FacetSearchValuesList';
import { FacetSettings } from './FacetSettings';
import { FacetSort } from './FacetSort';
import { FacetSortCriterion } from './FacetSortCriterion';
import { FacetUtils } from './FacetUtils';
import { FacetValueElement } from './FacetValueElement';
import { FacetValue } from './FacetValue';
import { FacetValues, ISortFacetValuesOptions } from './FacetValues';
import { FacetValuesList } from './FacetValuesList';
import { FacetValuesOrder } from './FacetValuesOrder';
import { OmniboxValueElement } from './OmniboxValueElement';
import { OmniboxValuesList } from './OmniboxValuesList';
import { ValueElement } from './ValueElement';
import { ValueElementRenderer } from './ValueElementRenderer';
import { IFieldValueCompatibleFacet } from '../FieldValue/IFieldValueCompatibleFacet';
type ComputedFieldOperation = 'sum' | 'average' | 'minimum' | 'maximum';
type ComputedFieldFormat = 'c0' | 'n0' | 'n2';
export interface IFacetOptions extends IResponsiveComponentOptions, IDependsOnCompatibleFacetOptions {
title?: string;
field?: IFieldOption;
isMultiValueField?: boolean;
numberOfValues?: number;
pageSize?: number;
sortCriteria?: string;
availableSorts?: string[];
injectionDepth?: number;
showIcon?: boolean;
useAnd?: boolean;
enableCollapse?: boolean;
enableTogglingOperator?: boolean;
enableMoreLess?: boolean;
valueCaption?: any;
lookupField?: IFieldOption;
enableFacetSearch?: boolean;
facetSearchDelay?: number;
facetSearchIgnoreAccents?: boolean;
numberOfValuesInFacetSearch?: number;
includeInBreadcrumb?: boolean;
includeInOmnibox?: boolean;
numberOfValuesInOmnibox?: number;
numberOfValuesInBreadcrumb?: number;
computedField?: IFieldOption;
computedFieldOperation?: string;
computedFieldFormat?: string;
computedFieldCaption?: string;
preservePosition?: boolean;
scrollContainer?: HTMLElement;
paddingContainer?: HTMLElement;
customSort?: string[];
enableSettings?: boolean;
enableSettingsFacetState?: boolean;
allowedValues?: string[];
headerIcon?: string;
valueIcon?: (facetValue: FacetValue) => string;
additionalFilter?: IQueryExpression;
useWildcardsInFacetSearch?: boolean;
}
/**
* **Note:**
* > We recommend using the simpler and more powerful [`DynamicFacet`]{@link DynamicFacet} component instead.
*
* The `Facet` component displays a *facet* of the results for the current query. A facet is a list of values for a
* certain field occurring in the results, ordered using a configurable criteria (e.g., number of occurrences).
*
* The list of values is obtained using a [`GroupByRequest`]{@link IGroupByRequest} operation performed at the same time
* as the main query.
*
* The `Facet` component allows the end user to drill down inside a result set by restricting the result to certain
* field values. It also allows filtering out values from the facet itself, and can provide a search box to look for
* specific values inside larger sets.
*
* This is probably the most complex component in the Coveo JavaScript Search Framework and as such, it allows for many
* configuration options.
*
* See also the [`FacetRange`]{@link FacetRange} and [`TimespanFacet`]{@link TimespanFacet} components (which
* extend this component), and the [`FacetSlider`]{@link FacetSlider} and [`CategoryFacet`]{@link CategoryFacet} components (which do not extend this
* component, but are very similar).
*/
export class Facet extends Component implements IFieldValueCompatibleFacet {
static ID = 'Facet';
static omniboxIndex = 50;
static doExport = () => {
exportGlobally({
Facet: Facet,
FacetHeader: FacetHeader,
FacetSearchValuesList: FacetSearchValuesList,
FacetSettings: FacetSettings,
FacetSort: FacetSort,
FacetUtils: FacetUtils,
FacetValueElement: FacetValueElement,
FacetValue: FacetValue
});
};
/**
* The possible options for a facet
* @componentOptions
*/
static options: IFacetOptions = {
/**
* Specifies the title to display at the top of the facet.
*
* Default value is the localized string for `NoTitle`.
*/
title: ComponentOptions.buildLocalizedStringOption({
localizedString: () => l('NoTitle'),
section: 'CommonOptions',
priority: 10
}),
/**
* Specifies the index field whose values the facet should use.
*
* The field must of the `string` type.
*
* This requires the given field to be configured correctly in the index as a *Facet field* (see
* [Add or Edit Fields](https://docs.coveo.com/en/1982/)).
*
* Specifying a value for this option is required for the `Facet` component to work.
*/
field: ComponentOptions.buildFieldOption({ required: true, groupByField: true, section: 'CommonOptions' }),
headerIcon: ComponentOptions.buildStringOption({
deprecated: 'This option is exposed for legacy reasons, and the recommendation is to not use this option.'
}),
/**
* Specifies a unique identifier for the facet. Among other things, this identifier serves the purpose of saving
* the facet state in the URL hash.
*
* If you have two facets with the same field on the same page, you should specify an `id` value for at least one of
* those two facets. This `id` must be unique among the facets.
*
* Non-word characters except - @ $ _ . + ! * ' ( ) , , ( `^a-zA-Z0-9-@$_.+!*'(),,]+` ) are automatically removed from the `id` value.
*
* Default value is the [`field`]{@link Facet.options.field} option value.
*/
id: ComponentOptions.buildStringOption({
postProcessing: (value: string, options: IFacetOptions) => {
if (value) {
// All non-word characters, except @ (the default character that specifies a field in the index)
// and characters that do no need to be encoded in the URL : - @ $ _ . + ! * ' ( ) , ,
const modified = value.replace(/[^a-zA-Z0-9-@$_.+!*'(),,]+/g, '');
if (Utils.isNullOrEmptyString(modified)) {
return options.field as string;
}
return modified;
}
return options.field as string;
}
}),
/**
* Specifies whether the facet [`field`]{@link Facet.options.field} is configured in the index as a multi-value
* field (semicolon separated values such as `abc;def;ghi`).
*
* Default value is `false`.
*/
isMultiValueField: ComponentOptions.buildBooleanOption({ defaultValue: false, section: 'CommonOptions' }),
/**
* Specifies the field whose values the Facet should display.
*
* @deprecated This option is exposed for legacy reasons. It is not recommended to use this option.
*/
lookupField: ComponentOptions.buildFieldOption({
deprecated: 'This option is exposed for legacy reasons. It is not recommended to use this option.'
}),
/**
* Specifies whether to display the facet **Settings** menu.
*
* See also the [`enableSettingsFacetState`]{@link Facet.options.enableSettingsFacetState},
* [`availableSorts`]{@link Facet.options.availableSorts}, and
* [`enableCollapse`]{@link Facet.options.enableCollapse} options.
*
* **Note:**
* > The [`FacetRange`]{@link FacetRange} component does not support this option.
*
* Default value is `true`.
*/
enableSettings: ComponentOptions.buildBooleanOption({ defaultValue: true, section: 'Sorting', priority: 9 }),
/**
* If the [`enableSettings`]{@link Facet.options.enableSettings} option is `true`, specifies whether the
* **Save state** menu option is available in the facet **Settings** menu.
*
* **Note:**
* > The [`FacetRange`]{@link FacetRange} component does not support this option.
*
* Default value is `false`.
*/
enableSettingsFacetState: ComponentOptions.buildBooleanOption({ defaultValue: false, depend: 'enableSettings' }),
/**
* If the [`enableSettings`]{@link Facet.options.enableSettings} option is `true`, specifies the sort criteria
* options to display in the facet **Settings** menu.
*
* Possible values are:
* - `"occurrences"`
* - `"score"`
* - `"alphaascending"`
* - `"alphadescending"`
* - `"computedfieldascending"`
* - `"computedfielddescending"`
* - `"custom"`
*
* See {@link IGroupByRequest.sortCriteria} for a description of each possible value.
*
* **Notes:**
* > * The [`FacetRange`]{@link FacetRange} component does not support this option.
*
* > * Using value captions will disable alphabetical sorts (see the [valueCaption]{@link Facet.options.valueCaption} option).
*
* Default value is `occurrences,score,alphaascending,alphadescending`.
*/
availableSorts: ComponentOptions.buildListOption<FacetSortCriterion>({
defaultValue: ['occurrences', 'score', 'alphaascending', 'alphadescending'],
section: 'Sorting',
depend: 'enableSettings',
values: [
'occurrences',
'score',
'alphaascending',
'alphadescending',
'computedfieldascending',
'computedfielddescending',
'chisquare',
'nosort'
]
}),
/**
* Specifies the criteria to use to sort the facet values.
*
* See {@link IGroupByRequest.sortCriteria} for the list and description of possible values.
*
* Default value is the first sort criteria specified in the [`availableSorts`]{@link Facet.options.availableSorts}
* option, or `occurrences` if no sort criteria is specified.
*/
sortCriteria: ComponentOptions.buildStringOption<FacetSortCriterion>({
postProcessing: (value, options: IFacetOptions) =>
value || (options.availableSorts.length > 0 ? (options.availableSorts[0] as FacetSortCriterion) : 'occurrences'),
section: 'Sorting'
}),
/**
* Specifies a custom order by which to sort the facet values.
*
* **Example:**
*
* You could use this option to specify a logical order for support tickets, such as:
* ```html
* <div class="CoveoFacet" data-field="@ticketstatus" data-title="Ticket Status" data-tab="All" data-custom-sort="New,Opened,Feedback,Resolved"></div>
* ```
*
* **Note:**
* > The [`FacetRange`]{@link FacetRange} component does not support this option.
*/
customSort: ComponentOptions.buildListOption<string>({ section: 'Sorting' }),
/**
* Specifies the maximum number of field values to display by default in the facet before the user
* clicks the arrow to show more.
*
* See also the [`enableMoreLess`]{@link Facet.options.enableMoreLess} option.
*/
numberOfValues: ComponentOptions.buildNumberOption({ defaultValue: 5, min: 0, section: 'CommonOptions' }),
/**
* Specifies the *injection depth* to use for the [`GroupByRequest`]{@link IGroupByRequest} operation.
*
* The injection depth determines how many results to scan in the index to ensure that the facet lists all potential
* facet values. Increasing this value enhances the accuracy of the listed values at the cost of performance.
*
* Default value is `1000`. Minimum value is `1000`.
* @notSupportedIn salesforcefree
*
* @examples 1500
*/
injectionDepth: ComponentOptions.buildNumberOption({ defaultValue: 1000, min: 1000 }),
showIcon: ComponentOptions.buildBooleanOption({
defaultValue: false,
deprecated: 'This option is exposed for legacy reasons. It is not recommended to use this option.'
}),
/**
* Specifies whether to use the `AND` operator in the resulting filter when multiple values are selected in the
* facet.
*
* Setting this option to `true` means that items must have all of the selected values to match the resulting
* query.
*
* Default value is `false`, which means that the filter uses the `OR` operator. Thus, by default, items must
* have at least one of the selected values to match the query.
*/
useAnd: ComponentOptions.buildBooleanOption({ defaultValue: false, section: 'Filtering' }),
/**
* Specifies whether to allow the user to toggle between the `OR` and `AND` modes in the facet.
*
* Setting this option to `true` displays an icon in the top right corner of the facet. The user can click this icon
* to toggle between between the two modes.
*
* Default value is `false`.
*/
enableTogglingOperator: ComponentOptions.buildBooleanOption({
defaultValue: false,
alias: 'allowTogglingOperator'
}),
/**
* Specifies whether to display a search box at the bottom of the facet for searching among the available facet
* [`field`]{@link Facet.options.field} values.
*
* See also the [`facetSearchDelay`]{@link Facet.options.facetSearchDelay},
* [`facetSearchIgnoreAccents`]{@link Facet.options.facetSearchIgnoreAccents}, and
* [`numberOfValuesInFacetSearch`]{@link Facet.options.numberOfValuesInFacetSearch} options.
*
* **Note:**
* > The [`FacetRange`]{@link FacetRange} component does not support this option.
*
* Default value is `true`.
*/
enableFacetSearch: ComponentOptions.buildBooleanOption({ defaultValue: true, section: 'FacetSearch', priority: 8 }),
/**
* If the [`enableFacetSearch`]{@link Facet.options.enableFacetSearch} option is `true`, specifies the delay (in
* milliseconds) before sending a search request to the server when the user starts typing in the facet search box.
*
* Specifying a smaller value makes results appear faster. However, chances of having to cancel many requests
* sent to the server increase as the user keeps on typing new characters.
*
* **Note:**
* > The [`FacetRange`]{@link FacetRange} component does not support this option.
*
* Default value is `100`. Minimum value is `0`.
*/
facetSearchDelay: ComponentOptions.buildNumberOption({ defaultValue: 100, min: 0, depend: 'enableFacetSearch' }),
/**
* If the [`enableFacetSearch`]{@link Facet.options.enableFacetSearch} option is `true`, specifies whether to ignore
* accents in the facet search box.
*
* **Note:**
* > The [`FacetRange`]{@link FacetRange} component does not support this option.
*
* Default value is `false`.
*/
facetSearchIgnoreAccents: ComponentOptions.buildBooleanOption({ defaultValue: false, depend: 'enableFacetSearch' }),
/**
* If the [`enableFacetSearch`]{@link Facet.options.enableFacetSearch} option is `true`, specifies the number of v
* alues to display in the facet search results popup.
*
* **Note:**
* > The [`FacetRange`]{@link FacetRange} component does not support this option.
*
* Default value is `15`. Minimum value is `1`.
*/
numberOfValuesInFacetSearch: ComponentOptions.buildNumberOption({ defaultValue: 15, min: 1, section: 'FacetSearch' }),
/**
* Specifies whether [wildcards]{@link AllowedValuesPatternType.wildcards} are used as the [allowedValuesPatternType]{@link IGroupByRequest.allowedValuesPatternType}
* in the [groupBy]{@link IGroupByRequest} for the facet search.
*
* Enabling this option returns results that end with the entered value. For example, searching for `veo` would match with `Coveo`.
*
* **Note:**
* > If you are experiencing slow facet search and/or timeouts when this option is set to `true`, consider enabling the **Use cache for nested queries**
* > option on your facet [field]{@link Facet.options.field} in the Coveo Cloud Admninistration Console (see [Add or Edit Fields]{@link https://docs.coveo.com/en/1982/}).
*/
useWildcardsInFacetSearch: ComponentOptions.buildBooleanOption({ defaultValue: false, section: 'FacetSearch' }),
/**
* Specifies whether the facet should push data to the [`Breadcrumb`]{@link Breadcrumb} component.
*
* See also the [`numberOfValuesInBreadcrumb`]{@link Facet.options.numberOfValuesInBreadcrumb} option.
*
* Default value is `true`.
*/
includeInBreadcrumb: ComponentOptions.buildBooleanOption({ defaultValue: true }),
/**
* If the [`includeInBreadcrumb`]{@link Facet.options.includeInBreadcrumb} option is `true`, specifies the maximum
* number of values that the facet should display in the [`Breadcrumb`]{@link Breadcrumb} before outputting a
* **more...** button.
*
* Default value is `5` on a desktop computer and `3` on a mobile device. Minimum value is `0`.
*/
numberOfValuesInBreadcrumb: ComponentOptions.buildNumberOption({
defaultFunction: () => (DeviceUtils.isMobileDevice() ? 3 : 5),
min: 0,
depend: 'includeInBreadcrumb'
}),
/**
* Specifies whether the Facet should push data to the {@link Omnibox} component.
*
* See also {@link Facet.options.numberOfValuesInOmnibox}.
*
* Default value is `false`.
*
* @deprecated This option is exposed for legacy reasons. It is not recommended to use this option.
*/
includeInOmnibox: ComponentOptions.buildBooleanOption({
defaultValue: false,
deprecated: 'This option is exposed for legacy reasons. It is not recommended to use this option.'
}),
/**
* When {@link Facet.options.includeInOmnibox} is `true`, specifies the number of values to populate the
* {@link Breadcrumb} with.
*
* Default value is `5` on desktop computer and `3` on a mobile device. Minimum value is `0`.
*
* @deprecated This option is exposed for legacy reasons. It is not recommended to use this option.
*/
numberOfValuesInOmnibox: ComponentOptions.buildNumberOption({
defaultFunction: () => (DeviceUtils.isMobileDevice() ? 3 : 5),
min: 0,
depend: 'includeInOmnibox',
deprecated: 'This option is exposed for legacy reasons. It is not recommended to use this option.'
}),
/**
* Specifies the name of a field on which to execute an aggregate operation for all distinct values of the facet
* [`field`]{@link Facet.options.field}.
*
* The facet displays the result of the operation along with the number of occurrences for each value.
*
* You can use this option to compute the sum of a field (like a money amount) for each listed facet value.
*
* Works in conjunction with the [`computedFieldOperation`]{@link Facet.options.computedFieldOperation},
* [`computedFieldFormat`]{@link Facet.options.computedFieldFormat}, and
* [`computedFieldCaption`]{@link Facet.options.computedFieldCaption} options.
* @notSupportedIn salesforcefree
*/
computedField: ComponentOptions.buildFieldOption({ priority: 7 }),
/**
* Specifies the type of aggregate operation to perform on the [`computedField`]{@link Facet.options.computedField}.
*
* The possible values are:
* - `sum` - Computes the sum of the computed field values.
* - `average` - Computes the average of the computed field values.
* - `minimum` - Finds the minimum value of the computed field values.
* - `maximum` - Finds the maximum value of the computed field values.
*
* Default value is `sum`.
* @notSupportedIn salesforcefree
*/
computedFieldOperation: ComponentOptions.buildStringOption<ComputedFieldOperation>({
defaultValue: 'sum',
section: 'ComputedField'
}),
/**
* Specifies how to format the values resulting from a
* [`computedFieldOperation`]{@link Facet.options.computedFieldOperation}.
*
* The Globalize library defines all available formats (see
* [Globalize](https://github.com/klaaspieter/jquery-global#globalizeformat-value-format-culture-)).
*
* The most commonly used formats are:
* - `c0` - Formats the value as a currency.
* - `n0` - Formats the value as an integer.
* - `n2` - Formats the value as a floating point with 2 decimal digits.
*
* Default value is `"c0"`.
* @notSupportedIn salesforcefree
*/
computedFieldFormat: ComponentOptions.buildStringOption<ComputedFieldFormat>({
defaultValue: 'c0',
section: 'ComputedField'
}),
/**
* Specifies what the caption of the [`computedField`]{@link Facet.options.computedField} should be in the facet
* **Settings** menu for sorting.
*
* For example, setting this option to `"Money"` will display `"Money Ascending"` for computed field ascending.
*
* **Note:**
* > The [`FacetRange`]{@link FacetRange} component does not support this option.
*
* Default value is the localized string for `ComputedField`.
* @notSupportedIn salesforcefree
*/
computedFieldCaption: ComponentOptions.buildLocalizedStringOption({
localizedString: () => l('ComputedField'),
section: 'ComputedField'
}),
/**
* Specifies whether the facet should remain stable in its current position in the viewport while the mouse cursor
* is over it.
*
* Whenever the value selection changes in a facet, the search interface automatically performs a query. This new
* query might cause other elements in the page to resize themselves (typically, other facets above or below the
* one the user is interacting with).
*
* This option is responsible for adding the `<div class='coveo-topSpace'>` and
* `<div class='coveo-bottomSpace'>` around the Facet container. The Facet adjusts the scroll amount of the page to
* ensure that it does not move relatively to the mouse when the results are updated.
*
* In some cases, the facet also adds margins to the `scrollContainer`, if scrolling alone is not enough to
* preserve position.
*
* See also the [`paddingContainer`]{@link Facet.options.paddingContainer}, and
* [`scrollContainer`]{@link Facet.options.scrollContainer} options.
*
* Default value is `true`.
*/
preservePosition: ComponentOptions.buildBooleanOption({ defaultValue: true }),
/**
* Specifies the parent container of the facets.
*
* Used by the [`preservePosition`]{@link Facet.options.preservePosition} option.
*
* Default value is `element.parentElement`.
*/
paddingContainer: ComponentOptions.buildSelectorOption({
defaultFunction: element => {
const standardColumn = $$(element).parent('coveo-facet-column');
if (standardColumn != null) {
return standardColumn;
}
return element.parentElement;
}
}),
/**
* Specifies the HTML element (through a CSS selector) whose scroll amount the facet should adjust to preserve its
* position when results are updated.
*
* Used by the [`preservePosition`]{@link Facet.options.preservePosition} option.
*
* Default value is `document.body`.
*/
scrollContainer: ComponentOptions.buildSelectorOption({ defaultFunction: element => document.body }),
/**
* Specifies whether to enable the **More** and **Less** buttons in the Facet.
*
* See also the [`pageSize`]{@link Facet.options.pageSize} option.
*
* **Note:**
* > The [`FacetRange`]{@link FacetRange} component does not support this option.
*
* Default value is `true`.
*/
enableMoreLess: ComponentOptions.buildBooleanOption({ defaultValue: true }),
/**
* If the [`enableMoreLess`]{@link Facet.options.enableMoreLess} option is `true`, specifies the number of
* additional results to fetch when clicking the **More** button.
*
* **Note:**
* > The [`FacetRange`]{@link FacetRange} component does not support this option.
*
* Default value is `10`. Minimum value is `1`.
*/
pageSize: ComponentOptions.buildNumberOption({ defaultValue: 10, min: 1, depend: 'enableMoreLess' }),
/**
* If the [`enableSettings`]{@link Facet.options.enableSettings} option is `true`, specifies whether the
* **Collapse \ Expand** menu option is available in the facet **Settings** menu.
*
* **Note:**
* > The [`FacetRange`]{@link FacetRange} component does not support this option.
*
* Default value is `true`.
*/
enableCollapse: ComponentOptions.buildBooleanOption({ defaultValue: true, depend: 'enableSettings' }),
/**
* Specifies an explicit list of [`allowedValues`]{@link IGroupByRequest.allowedValues} in the
* [`GroupByRequest`]{@link IGroupByRequest}.
*
* If you specify a list of values for this option, the facet uses only these values (if they are available in
* the current result set).
*
* **Example:**
*
* The following facet only uses the `Contact`, `Account`, and `File` values of the `@objecttype` field. Even if the
* current result set contains other `@objecttype` values, such as `Message`, or `Product`, the facet does not use
* those other values.
*
* ```html
*
* <div class="CoveoFacet" data-field="@objecttype" data-title="Object Type" data-tab="All" data-allowed-values="Contact,Account,File"></div>
* ```
*
* Default value is `undefined`, and the facet uses all available values for its
* [`field`]{@link Facet.options.field} in the current result set.
*/
allowedValues: ComponentOptions.buildListOption<string>(),
/**
* Specifies an additional query expression (query override) to add to each
* [`GroupByRequest`]{@link IGroupByRequest} that this facet performs.
*
* Example: `@date>=2014/01/01`
* @notSupportedIn salesforcefree
*/
additionalFilter: ComponentOptions.buildQueryExpressionOption(),
/**
* Specifies whether this facet only appears when a value is selected in its "parent" facet.
*
* To specify the parent facet, use its [`id`]{@link Facet.options.id}.
*
* Remember that by default, a facet `id` value is the same as its [`field`]{@link Facet.options.field} option
* value.
*
* **Examples:**
*
* First case: the "parent" facet has no custom `id`:
* ```html
* <!-- "Parent" Facet: -->
* <div class='CoveoFacet' data-field='@myfield' data-title='My Parent Facet'></div>
*
* <!-- The "dependent" Facet must refer to the default `id` of its "parent" Facet, which is the name of its field. -->
* <div class='CoveoFacet' data-field='@myotherfield' data-title='My Dependent Facet' data-depends-on='@myfield'></div>
* ```
*
* Second case: the "parent" facet has a custom `id`:
* ```html
* <!-- "Parent" Facet: -->
* <div class='CoveoFacet' data-field='@myfield' data-title='My Parent Facet' data-id='myParentCustomId'></div>
*
* <!-- The "dependent" Facet must refer to the custom `id` of its "parent" Facet, which is 'myParentCustomId'. -->
* <div class='CoveoFacet' data-field='@myotherfield' data-title='My Dependent Facet' data-depends-on='myParentCustomId'></div>
* ```
*
* Default value is `undefined`
*/
dependsOn: ComponentOptions.buildStringOption(),
/**
* A function that verifies whether the current state of the `dependsOn` facet allows the dependent facet to be displayed.
*
* If specified, the function receives a reference to the resolved `dependsOn` facet component instance as an argument, and must return a boolean.
* The function's argument should typically be treated as read-only.
*
* By default, the dependent facet is displayed whenever one or more values are selected in its `dependsOn` facet.
*
* @externaldocs [Defining Dependent Facets](https://docs.coveo.com/3210/)
*/
dependsOnCondition: ComponentOptions.buildCustomOption<IDependentFacetCondition>(
() => {
return null;
},
{ depend: 'dependsOn', section: 'CommonOptions' }
),
/**
* Specifies a JSON object describing a mapping of facet values to their desired captions. See
* [Normalizing Facet Value Captions](https://docs.coveo.com/en/368/).
*
* **Examples:**
*
* You can set the option in the ['init']{@link init} call:
* ```javascript
* var myValueCaptions = {
* "txt" : "Text files",
* "html" : "Web page",
* [ ... ]
* };
*
* Coveo.init(document.querySelector("#search"), {
* Facet : {
* valueCaption : myValueCaptions
* }
* });
* ```
*
* Or before the `init` call, using the ['options']{@link options} top-level function:
* ```javascript
* Coveo.options(document.querySelector("#search"), {
* Facet : {
* valueCaption : myValueCaptions
* }
* });
* ```
*
* Or directly in the markup:
* ```html
* <!-- Ensure that the double quotes are properly handled in data-value-caption. -->
* <div class='CoveoFacet' data-field='@myotherfield' data-value-caption='{"txt":"Text files","html":"Web page"}'></div>
* ```
*
* **Note:**
* > Using value captions will disable alphabetical sorts (see the [availableSorts]{@link Facet.options.availableSorts} option).
*/
valueCaption: ComponentOptions.buildJsonOption<IStringMap<string>>(),
...ResponsiveFacetOptions
};
public facetQueryController: FacetQueryController;
public keepDisplayedValuesNextTime: boolean = false;
public values = new FacetValues();
public currentPage: number = 0;
public numberOfValues: number;
public firstQuery = true;
public operatorAttributeId: string;
public isFieldValueCompatible = true;
public isFieldValueHierarchical = false;
/**
* Renders and handles the facet **Search** part of the component.
*/
public facetSearch: FacetSearch;
/**
* Renders and handles the facet **Settings** part of the component
*/
public facetSettings: FacetSettings;
public facetSort: FacetSort;
public facetValuesList: FacetValuesList;
public facetHeader: FacetHeader;
public searchContainer: ValueElementRenderer;
public dependsOnManager: DependsOnManager;
protected omniboxZIndex;
protected moreElement: HTMLElement;
protected lessElement: HTMLElement;
protected headerElement: HTMLElement;
protected footerElement: HTMLElement;
private canFetchMore: boolean = true;
private nbAvailableValues: number;
private showingWaitAnimation = false;
private pinnedViewportPosition: number;
private unpinnedViewportPosition: number;
private pinnedTopSpace: HTMLElement;
private pinnedBottomSpace: HTMLElement;
private componentStateId: string;
private includedAttributeId: string;
private excludedAttributeId: string;
private lookupValueAttributeId: string;
private listenToQueryStateChange = true;
/**
* Creates a new `Facet` component. Binds multiple query events as well.
* @param element The HTMLElement on which to instantiate the component.
* @param options The options for the `Facet` 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).
* @param facetClassId The ID to use for this facet (as `Facet` inherited from by other component
* (e.g., [`FacetRange`]{@link FacetRange}). Default value is `Facet`.
*/
constructor(public element: HTMLElement, public options: IFacetOptions, bindings?: IComponentBindings, facetClassId: string = Facet.ID) {
super(element, facetClassId, bindings);
this.options = ComponentOptions.initComponentOptions(element, Facet, options);
if (this.options.valueCaption != null) {
this.options.availableSorts = _.filter(this.options.availableSorts, (sort: string) => !/^alpha.*$/.test(sort));
this.logger.warn(`Because the ${this.options.field} facet is using value captions, alphabetical sorts are disabled.`);
}
ResponsiveFacets.init(this.root, this, this.options);
// Serves as a way to render facet in the omnibox in the order in which they are instantiated
this.omniboxZIndex = Facet.omniboxIndex;
Facet.omniboxIndex--;
this.checkForComputedFieldAndSort();
this.checkForValueCaptionType();
this.checkForCustomSort();
this.initDependsOnManager();
this.initFacetQueryController();
this.initQueryEvents();
this.initQueryStateEvents();
this.initComponentStateEvents();
this.initOmniboxEvents();
this.initBreadCrumbEvents();
this.initSearchAlertEvents();
this.updateNumberOfValues();
this.bind.oneRootElement(QueryEvents.querySuccess, () => {
this.firstQuery = false;
});
}
public setExpandedFacetSearchAccessibilityAttributes(searchResultsElement: HTMLElement) {
if (!this.searchContainer) {
return;
}
Assert.exists(searchResultsElement);
const { accessibleElement } = this.searchContainer;
accessibleElement.setAttribute('aria-expanded', true.toString());
}
public setCollapsedFacetSearchAccessibilityAttributes() {
if (!this.searchContainer) {
return;
}
this.searchContainer.accessibleElement.setAttribute('aria-expanded', false.toString());
}
public isCurrentlyDisplayed() {
if (!$$(this.element).isVisible()) {
return false;
}
if ($$(this.element).hasClass('coveo-active')) {
return true;
}
if ($$(this.element).hasClass('coveo-facet-empty')) {
return false;
}
return true;
}
public createDom() {
this.initBottomAndTopSpacer();
this.buildFacetContent();
this.updateAppearanceDependingOnState();
// After the facet has been created (and before the first query is applied)
// Try to load a state from the setting, if it's available
// Execute only _.once (only the first query, or the first time the user switch to a tab that contains a newly set of active facet)
if (this.facetSettings && this.options.enableSettingsFacetState) {
const loadOnce = <(args: INewQueryEventArgs) => any>_.once(() => {
this.facetSettings.loadSavedState.apply(this.facetSettings);
});
this.bind.onRootElement(QueryEvents.newQuery, loadOnce);
}
}
/**
* Selects a single value.
*
* Does not trigger a query automatically.
*
* @param value Can be a [`FacetValue`]{@link FacetValue} or a string (e.g., `selectValue('foobar')` or
* `selectValue(new FacetValue('foobar'))`).
*/
public selectValue(value: FacetValue | string): void {
Assert.exists(value);
this.ensureDom();
this.logger.info('Selecting facet value', this.facetValuesList.select(value));
this.facetValueHasChanged();
}
/**
* Selects multiple values.
*
* Does not trigger a query automatically.
*
* @param values Can be an array of [`FacetValue`]{@link FacetValue} or an array of strings.
*/
public selectMultipleValues(values: FacetValue[] | string[]): void {
Assert.exists(values);
this.ensureDom();
_.each(values as FacetValue[], value => {
this.logger.info('Selecting facet value', this.facetValuesList.select(value));
});
this.facetValueHasChanged();
}
/**
* Deselects a single value.
*
* Does not trigger a query automatically.
*
* @param value Can be a [`FacetValue`]{@link FacetValue} or a string (e.g., `deselectValue('foobar')` or
* `deselectValue(new FacetValue('foobar'))`).
*/
public deselectValue(value: FacetValue | string): void {
Assert.exists(value);
this.ensureDom();
this.logger.info('Deselecting facet value', this.facetValuesList.unselect(value));
this.facetValueHasChanged();
}
/**
* Deselects multiple values.
*
* Does not trigger a query automatically.
*
* @param values Can be an array of [`FacetValue`]{@link FacetValue} or an array of strings.
*/
public deselectMultipleValues(values: FacetValue[] | string[]): void {
Assert.exists(values);
this.ensureDom();
_.each(values as FacetValue[], value => {
this.logger.info('Deselecting facet value', this.facetValuesList.unselect(value));
});
this.facetValueHasChanged();
}
/**
* Excludes a single value.
*
* Does not trigger a query automatically.
*
* @param value Can be a [`FacetValue`]{@link FacetValue} or a string (e.g., `excludeValue('foobar')` or
* `excludeValue(new FacetValue('foobar'))`).
*/
public excludeValue(value: FacetValue | string): void {
Assert.exists(value);
this.ensureDom();
this.logger.info('Excluding facet value', this.facetValuesList.exclude(value));
this.facetValueHasChanged();
}
/**
* Excludes multiple values.
*
* Does not trigger a query automatically.
*
* @param values Can be an array of [`FacetValue`]{@link FacetValue} or an array of strings.
*/
public excludeMultipleValues(values: FacetValue[] | string[]): void {
Assert.exists(values);
this.ensureDom();
_.each(values as FacetValue[], value => {
this.logger.info('Excluding facet value', this.facetValuesList.exclude(value));
});
this.facetValueHasChanged();
}
/**
* Unexcludes a single value.
*
* Does not trigger a query automatically.
*
* @param value Can be a [`FacetValue`]{@link FacetValue} or a string.
*/
public unexcludeValue(value: FacetValue | string): void {
Assert.exists(value);
this.ensureDom();
this.logger.info('Unexcluding facet value', this.facetValuesList.unExclude(value));
this.facetValueHasChanged();
}
/**
* Unexcludes multiple values.
*
* Does not trigger a query automatically.
*
* @param values Can be an array of [`FacetValue`]{@link FacetValue} or an array of strings.
*/
public unexcludeMultipleValues(values: FacetValue[] | string[]): void {
Assert.exists(values);
this.ensureDom();
_.each(values as FacetValue[], value => {
this.logger.info('Unexcluding facet value', this.facetValuesList.unExclude(value));
});
this.facetValueHasChanged();
}
/**
* Toggles the selection state of a single value (selects the value if it is not already selected; un-selects the
* value if it is already selected).
*
* Does not trigger a query automatically.
* @param value Can be a [`FacetValue`]{@link FacetValue} or a string.
*/
public toggleSelectValue(value: FacetValue | string): void {
Assert.exists(value);
this.ensureDom();
this.logger.info('Toggle select facet value', this.facetValuesList.toggleSelect(value));
this.facetValueHasChanged();
}
/**
* Toggles the exclusion state of a single value (excludes the value if it is not already excluded; un-excludes the
* value if it is already excluded).
*
* Does not trigger a query automatically.
*
* @param value Can be a [`FacetValue`]{@link FacetValue} or a string.
*/
public toggleExcludeValue(value: FacetValue | string): void {
Assert.exists(value);
this.ensureDom();
this.logger.info('Toggle exclude facet value', this.facetValuesList.toggleExclude(value));
this.facetValueHasChanged();
}
/**
* Returns the currently displayed values as an array of strings.
*
* @returns {any[]} The currently displayed values.
*/
public getDisplayedValues(): string[] {
return _.pluck(this.getDisplayedFacetValues(), 'value');
}
/**
* Returns the currently displayed values as an array of [`FacetValue`]{@link FacetValue}.
*
* @returns {T[]} The currently displayed values.
*/
public getDisplayedFacetValues(): FacetValue[] {
this.ensureDom();
const displayed = this.facetValuesList.getAllCurrentlyDisplayed();
return _.map(displayed, (value: ValueElement) => {
return value.facetValue;
});
}
/**
* Returns the currently selected values as an array of strings.
* @returns {string[]} The currently selected values.
*/
public getSelectedValues(): string[] {
this.ensureDom();
return _.map(this.values.getSelected(), (value: FacetValue) => value.value);
}
/**
* Determines whether the specified value is selected in the facet.
* @param value The name of the facet value to verify.
*/
public hasSelectedValue(value: string) {
const facetValue = this.values.get(value);
return facetValue && facetValue.selected;
}
/**
* Returns the currently excluded values as an array of strings.
* @returns {string[]} The currently excluded values.
*/
public getExcludedValues(): string[] {
this.ensureDom();
return _.map(this.values.getExcluded(), (value: FacetValue) => value.value);
}
/**
* Resets the facet by un-selecting all values, un-excluding all values, and redrawing the facet.
*/
public reset(): void {
this.ensureDom();
this.values.reset();
this.rebuildValueElements();
this.updateAppearanceDependingOnState();
this.updateQueryStateModel();
}
/**
* Switches the facet to `AND` mode.
*
* See the [`useAnd`]{@link Facet.options.useAnd}, and
* [`enableTogglingOperator`]{@link Facet.options.enableTogglingOperator} options.
*/
public switchToAnd(): void {
this.ensureDom();
this.logger.info('Switching to AND');
this.facetHeader.switchToAnd();
}
/**
* Switches the facet to `OR` mode.
*
* See the [`useAnd`]{@link Facet.options.useAnd}, and
* [`enableTogglingOperator`]{@link Facet.options.enableTogglingOperator} options.
*/
public switchToOr(): void {
this.ensureDom();
this.logger.info('Switching to OR');
this.facetHeader.switchToOr();
}
/**
* Returns the endpoint for the facet.
* @returns {ISearchEndpoint} The endpoint for the facet.
*/
public getEndpoint(): ISearchEndpoint {
return this.queryController.getEndpoint();
}
/**
* Changes the sort parameter for the facet.
*
* See {@link Facet.options.availableSorts} for the list of possible values.
*
* Also triggers a new query.
*
* @param criteria The new sort parameter for the facet.
*/
public updateSort(criteria: string): void {
this.ensureDom();
if (this.options.sortCriteria != criteria) {
this.options.sortCriteria = criteria;
this.usageAnalytics.logCustomEvent<IAnalyticsFacetSortMeta>(
analyticsActionCauseList.facetUpdateSort,
{
criteria,
facetId: this.options.id,
facetField: this.options.field.toString(),
facetTitle: this.options.title
},
this.element
);
this.triggerNewQuery();
}
}
public unfadeInactiveValuesInMainList(): void {
$$(this.element).removeClass('coveo-facet-fade');
}
public fadeInactiveValuesInMainList(delay: number): void {
$$(this.element).addClass('coveo-facet-fade');
}
/**
* Shows a waiting animation in the facet header (a spinner).
*/
public showWaitingAnimation() {
this.ensureDom();
if (!this.showingWaitAnimation) {
$$(this.headerElement).find('.coveo-facet-header-wait-animation').style.visibility = 'visible';
this.showingWaitAnimation = true;
}
}
/**
* Hides the waiting animation in the facet header.
*/
public hideWaitingAnimation(): void {
this.ensureDom();
if (this.showingWaitAnimation) {
$$(this.headerElement).find('.coveo-facet-header-wait-animation').style.visibility = 'hidden';
this.showingWaitAnimation = false;
}
}
public processFacetSearchAllResultsSelected(facetValues: FacetValue[]): void {
const valuesForAnalytics = [];
_.each(facetValues, facetValue => {
this.ensureFacetValueIsInList(facetValue);
valuesForAnalytics.push(facetValue.value);
});
// Calculate the correct number of values from the current selected/excluded values (those will stay no matter what next rendering)
// add the new one that will be selected (and are not already selected in the facet)
// The minimum number of values is the number of values set in the option
const valuesThatStays = this.values.getSelected().concat(this.values.getExcluded());
this.numberOfValues = valuesThatStays.length + _.difference(valuesThatStays, facetValues).length;
this.numberOfValues = Math.max(this.numberOfValues, this.options.numberOfValues);
this.updateQueryStateModel();
this.triggerNewQuery(() =>
this.usageAnalytics.logSearchEvent<IAnalyticsFacetMeta>(analyticsActionCauseList.facetSelectAll, {
facetId: this.options.id,
facetField: this.options.field.toString(),
facetTitle: this.options.title
})
);
}
public pinFacetPosition() {
if (this.options.preservePosition) {
this.pinnedViewportPosition = this.element.getBoundingClientRect().top;
}
}
/**
* Returns the configured caption for the given [`FacetValue`]{@link FacetValue}.
*
* @param facetValue The `FacetValue` whose caption the method should return.
*/
public getValueCaption(facetValue: IIndexFieldValue | FacetValue | string): string {
Assert.exists(facetValue);
const lookupValue = typeof facetValue === 'string' ? facetValue : facetValue.lookupValue || facetValue.value;
let ret = FacetUtils.tryToGetTranslatedCaption(<string>this.options.field, lookupValue);
if (Utils.exists(this.options.valueCaption)) {
if (typeof this.options.valueCaption == 'object') {
ret = this.options.valueCaption[lookupValue] || ret;
}
if (typeof this.options.valueCaption == 'function') {
const fv = facetValue instanceof FacetValue ? facetValue : FacetValue.create(facetValue);
const valueFromList = this.facetValuesList.get(fv).facetValue;
ret = this.options.valueCaption.call(this, valueFromList);
}
}
return ret;
}
/**
* Returns the configured caption for a desired facet value.
*
* @param value The string facet value whose caption the method should return.
*/
public getCaptionForStringValue(value: string) {
Assert.exists(value);
return this.getValueCaption(value);
}
/**
* Shows the next page of results in the facet.
*
* See the [`enableMoreLess`]{@link Facet.options.enableMoreLess}, and [`pageSize`]{@link Facet.options.pageSize}
* options.
*
* Triggers a query if needed, or displays the already available values.
*/
public showMore() {
this.currentPage = Math.floor((this.numberOfValues - this.options.numberOfValues) / this.options.pageSize) + 1;
this.updateNumberOfValues();
if (this.nbAvailableValues >= this.numberOfValues || !this.canFetchMore) {
this.rebuildValueElements();
} else {
this.triggerMoreQuery();
}
}
/**
* Shows less elements in the Facet (up to the original number of values).
*
* See the [`enableMoreLess`]{@link Facet.options.enableMoreLess}, and
* [`numberOfValues`]{@link Facet.options.numberOfValues} options.
*/
public showLess(