UNPKG

coveo-search-ui

Version:

Coveo JavaScript Search Framework

490 lines (430 loc) • 19.5 kB
import { contains, each, escape, extend, filter, find, isArray, isObject, isString, keys, map, omit } from 'underscore'; import { exportGlobally } from '../../GlobalExports'; import { Assert } from '../../misc/Assert'; import { IQueryResult } from '../../rest/QueryResult'; import { l } from '../../strings/Strings'; import { AccessibleButton } from '../../utils/AccessibleButton'; import { DateUtils, IDateToStringOptions } from '../../utils/DateUtils'; import { $$ } from '../../utils/Dom'; import { StringUtils } from '../../utils/StringUtils'; import { Utils } from '../../utils/Utils'; import { analyticsActionCauseList } from '../Analytics/AnalyticsActionListMeta'; import { Component } from '../Base/Component'; import { IComponentBindings } from '../Base/ComponentBindings'; import { ComponentOptions } from '../Base/ComponentOptions'; import { IComponentOptionsObjectOptionArgs, IFieldConditionOption, IFieldOption } from '../Base/IComponentOptions'; import { Initialization } from '../Base/Initialization'; import { FacetUtils } from '../Facet/FacetUtils'; import { TemplateFieldsEvaluator } from '../Templates/TemplateFieldsEvaluator'; import { TemplateHelpers } from '../Templates/TemplateHelpers'; import { IFieldValueCompatibleFacet, isFacetFieldValueCompatible } from './IFieldValueCompatibleFacet'; import { ComponentsTypes } from '../../utils/ComponentsTypes'; export interface IFieldValueOptions { field?: IFieldOption; facet?: string; dynamicFacet?: string; htmlValue?: boolean; helper?: string; helperOptions?: { [key: string]: any }; splitValues?: boolean; separator?: string; displaySeparator?: string; textCaption?: string; conditions?: IFieldConditionOption[]; } export interface IAnalyticsFieldValueMeta { facetId: string; facetField: string; facetValue?: string; facetTitle?: string; } function showOnlyWithHelper<T>(helpers: string[], options?: T): T { if (options == null) { options = <any>{}; } (<any>options).helpers = helpers; return options; } /** * The FieldValue component displays the value of a field associated to its parent search result. It is normally usable * within a {@link FieldTable}. * * This component is a result template component (see [Result Templates](https://docs.coveo.com/en/413/)). * * A common use of this component is to display a specific field value which also happens to be an existing * {@link Facet.options.field}. When the user clicks on the FieldValue component, it activates the corresponding Facet. */ export class FieldValue extends Component { static ID = 'FieldValue'; static doExport = () => { exportGlobally({ FieldValue: FieldValue }); }; /** * The options for the component * @componentOptions */ static options: IFieldValueOptions = { /** * Specifies the field that the FieldValue should display. * * Specifying a value for this parameter is required in order for the FieldValue component to work. */ field: ComponentOptions.buildFieldOption({ defaultValue: '@field', required: true }), /** * Specifies the {@link Facet} component to toggle when the end user clicks the FieldValue. * * Default value is the value of {@link FieldValue.options.field}. * * **Note:** * > If the target {@link Facet.options.id} is is not the same as its {@link Facet.options.field}), you must specify * > this option manually in order to link to the correct Facet. */ facet: ComponentOptions.buildStringOption({ postProcessing: (value, options) => value || options.field }), /** * Specifies whether the content to display is an HTML element. * * Default value is `false`. */ htmlValue: ComponentOptions.buildBooleanOption({ defaultValue: false }), /** * Specifies whether to split the FieldValue at each {@link FieldValue.options.separator}. * * This is useful for splitting groups using a {@link Facet.options.field}. * * When this option is `true`, the displayed values are split by the {@link FieldValue.options.displaySeparator}. * * Default value is `false`. */ splitValues: ComponentOptions.buildBooleanOption({ defaultValue: false }), /** * If {@link FieldValue.options.splitValues} is `true`, specifies the separator string which separates multi-value * fields in the index. * * See {@link FieldValue.options.displaySeparator}. * * Default value is `";"`. */ separator: ComponentOptions.buildStringOption({ depend: 'splitValues', defaultValue: ';' }), /** * If {@link FieldValue.options.splitValues} is `true`, specifies the string to use when displaying multi-value * fields in the UI. * * The component will insert this string between each value it displays from a multi-value field. * * See also {@link FieldValue.options.separator}. * * Default value is `", "`. */ displaySeparator: ComponentOptions.buildStringOption({ depend: 'splitValues', defaultValue: ', ' }), /** * Specifies the helper that the FieldValue should use to display its content. * * While several helpers exist by default (see {@link ICoreHelpers}), it is also possible for you to create your own * custom helpers (see {@link TemplateHelpers}). */ helper: ComponentOptions.buildHelperOption(), /** * Specifies the options to call on the specified helper. */ helperOptions: ComponentOptions.buildObjectOption(<IComponentOptionsObjectOptionArgs>{ subOptions: { text: ComponentOptions.buildStringOption(showOnlyWithHelper(['anchor'])), target: ComponentOptions.buildStringOption(showOnlyWithHelper(['anchor'])), class: ComponentOptions.buildStringOption(showOnlyWithHelper(['anchor'])), format: ComponentOptions.buildStringOption(showOnlyWithHelper(['number'])), decimals: ComponentOptions.buildNumberOption(showOnlyWithHelper(['currency'], { min: 0 })), symbol: ComponentOptions.buildStringOption(showOnlyWithHelper(['currency'])), useTodayYesterdayAndTomorrow: ComponentOptions.buildBooleanOption( showOnlyWithHelper(['date', 'dateTime', 'emailDateTime', 'time'], { defaultValue: true }) ), useWeekdayIfThisWeek: ComponentOptions.buildBooleanOption( showOnlyWithHelper(['date', 'dateTime', 'emailDateTime', 'time'], { defaultValue: true }) ), omitYearIfCurrentOne: ComponentOptions.buildBooleanOption( showOnlyWithHelper(['date', 'dateTime', 'emailDateTime', 'time'], { defaultValue: true }) ), useLongDateFormat: ComponentOptions.buildBooleanOption( showOnlyWithHelper(['date', 'dateTime', 'emailDateTime', 'time'], { defaultValue: false }) ), includeTimeIfToday: ComponentOptions.buildBooleanOption( showOnlyWithHelper(['date', 'dateTime', 'emailDateTime', 'time'], { defaultValue: true }) ), includeTimeIfThisWeek: ComponentOptions.buildBooleanOption( showOnlyWithHelper(['date', 'dateTime', 'emailDateTime', 'time'], { defaultValue: true }) ), alwaysIncludeTime: ComponentOptions.buildBooleanOption( showOnlyWithHelper(['date', 'dateTime', 'emailDateTime', 'time'], { defaultValue: false }) ), predefinedFormat: ComponentOptions.buildStringOption(showOnlyWithHelper(['date', 'dateTime', 'emailDateTime', 'time'])), companyDomain: ComponentOptions.buildStringOption(showOnlyWithHelper(['email'])), me: ComponentOptions.buildStringOption(showOnlyWithHelper(['email'])), lengthLimit: ComponentOptions.buildNumberOption(showOnlyWithHelper(['email'], { min: 1 })), truncateName: ComponentOptions.buildBooleanOption(showOnlyWithHelper(['email'])), alt: ComponentOptions.buildStringOption(showOnlyWithHelper(['image'])), height: ComponentOptions.buildStringOption(showOnlyWithHelper(['image'])), width: ComponentOptions.buildStringOption(showOnlyWithHelper(['image'])), srcTemplate: ComponentOptions.buildStringOption(showOnlyWithHelper(['image'])), precision: ComponentOptions.buildNumberOption(showOnlyWithHelper(['size'], { min: 0, defaultValue: 2 })), base: ComponentOptions.buildNumberOption(showOnlyWithHelper(['size'], { min: 0, defaultValue: 0 })), isMilliseconds: ComponentOptions.buildBooleanOption(showOnlyWithHelper(['timeSpan'])), length: ComponentOptions.buildNumberOption(showOnlyWithHelper(['shorten', 'shortenPath', 'shortenUri'], { defaultValue: 200 })) } }), /** * Specifies a caption to display before the value. * * Default value is `undefined`. * * @availablesince [January 2017 Release (v1.1865.9)](https://docs.coveo.com/en/396/#january-2017-release-v118659) */ textCaption: ComponentOptions.buildLocalizedStringOption(), /** * A field-based condition that must be satisfied by the query result item for the component to be rendered. * * Note: This option uses a distinctive markup configuration syntax allowing multiple conditions to be expressed. Its underlying logic is the same as that of the field value conditions mechanism used by result templates. * * **Examples:** * Render the component if the query result item's @documenttype field value is Article or Documentation. * ```html * <div class="CoveoFieldValue" data-field="@author" data-condition-field-documenttype="Article, Documentation"></div> * ``` * Render the component if the query result item's @documenttype field value is anything but Case. * ```html * <div class="CoveoFieldValue" data-field="@author" data-condition-field-not-documenttype="Case"></div> * ``` * Render the component if the query result item's @documenttype field value is Article, and if its @author field value is anything but Anonymous. * ```html * <div class="CoveoFieldValue" data-field="@author" data-condition-field-documenttype="Article" data-condition-field-not-author="Anonymous"></div> * ``` * Default value is `undefined`. */ conditions: ComponentOptions.buildFieldConditionOption() }; static simpleOptions = omit(FieldValue.options, 'helperOptions'); static helperOptions = <any>{ helperOptions: FieldValue.options.helperOptions }; /** * Creates a new FieldValue. * @param element The HTMLElement on which to instantiate the component. * @param options The options for the FieldValue 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 result The result to associate the component with. */ constructor( public element: HTMLElement, public options: IFieldValueOptions, bindings?: IComponentBindings, public result?: IQueryResult, fieldValueClassId: string = FieldValue.ID ) { super(element, fieldValueClassId, bindings); this.options = ComponentOptions.initOptions(element, FieldValue.simpleOptions, options, FieldValue.ID); if (this.options.helper != null) { this.normalizeHelperAndOptions(); } this.result = this.result || this.resolveResult(); Assert.exists(this.result); if (TemplateFieldsEvaluator.evaluateFieldsToMatch(this.options.conditions, this.result) && !Utils.isNullOrUndefined(this.getValue())) { this.initialize(); } else if (this.element.parentElement != null) { this.element.parentElement.removeChild(this.element); } } private initialize() { let loadedValueFromComponent = this.getValue(); let values: string[]; if (isArray(loadedValueFromComponent)) { values = loadedValueFromComponent; } else if (this.options.splitValues) { if (isString(loadedValueFromComponent)) { values = map(loadedValueFromComponent.split(this.options.separator), (v: string) => { return v.trim(); }); } } else { loadedValueFromComponent = loadedValueFromComponent.toString(); values = [loadedValueFromComponent]; } if (values.length > 1 && this.isValueHierarchical) { values = values.slice(-1); } this.appendValuesToDom(values); if (this.options.textCaption != null) { this.prependTextCaptionToDom(); } } /** * Gets the current FieldValue from the current {@link IQueryResult}. * * @returns {any} The current FieldValue or `null` if value is and `Object`. */ public getValue() { let value = Utils.getFieldValue(this.result, <string>this.options.field); if (!isArray(value) && isObject(value)) { value = null; } return value; } /** * Renders a value to HTML using all of the current FieldValue component options. * @param value The value to render. * @returns {HTMLElement} The element containing the rendered value. */ public renderOneValue(value: string): HTMLElement { const element = $$('span').el; let toRender = this.getCaption(value); if (this.options.helper) { // Try to resolve and execute version 2 of each helper function if available const helper = TemplateHelpers.getHelper(`${this.options.helper}v2`) || TemplateHelpers.getHelper(`${this.options.helper}`); if (Utils.exists(helper)) { toRender = helper.call(this, value, this.getHelperOptions(), this.result); } else { this.logger.warn( `Helper ${this.options.helper} is not found in available helpers. The list of supported helpers is :`, keys(TemplateHelpers.getHelpers()) ); } const fullDateStr = this.getFullDate(value, this.options.helper); if (fullDateStr) { element.setAttribute('title', fullDateStr); } if (this.options.helper == 'date' || this.options.helper == 'dateTime' || this.options.helper == 'emailDateTime') { toRender = StringUtils.capitalizeFirstLetter(toRender); } } if (this.options.htmlValue) { element.innerHTML = toRender; } else { element.appendChild(document.createTextNode(toRender)); } this.bindEventOnValue(element, value, toRender); return element; } protected getValueContainer() { return this.element; } private normalizeHelperAndOptions() { this.options = ComponentOptions.initOptions(this.element, FieldValue.helperOptions, this.options, FieldValue.ID); const toFilter = keys(FieldValue.options.helperOptions['subOptions']); const toKeep = filter(toFilter, optionKey => { const optionDefinition = FieldValue.options.helperOptions['subOptions'][optionKey]; if (optionDefinition) { const helpers = optionDefinition.helpers; return helpers != null && contains(helpers, this.options.helper); } return false; }); this.options.helperOptions = omit(this.options.helperOptions, (value, key) => { return !contains(toKeep, key); }); } private getHelperOptions() { const inlineOptions = ComponentOptions.loadStringOption(this.element, 'helperOptions', {}); if (Utils.isNonEmptyString(inlineOptions)) { return extend({}, this.options.helperOptions, eval('(' + inlineOptions + ')')); } return this.options.helperOptions; } private getFullDate(date: string, helper: string) { const fullDateOptions: IDateToStringOptions = { useLongDateFormat: true, useTodayYesterdayAndTomorrow: false, useWeekdayIfThisWeek: false, omitYearIfCurrentOne: false }; if (helper == 'date') { return DateUtils.dateToString(new Date(parseInt(date)), fullDateOptions); } else if (helper == 'dateTime' || helper == 'emailDateTime') { return DateUtils.dateTimeToString(new Date(parseInt(date)), fullDateOptions); } return ''; } private appendValuesToDom(values: string[]): void { each(values, (value, index) => { if (value != undefined) { this.getValueContainer().appendChild(this.renderOneValue(value)); if (index !== values.length - 1) { this.getValueContainer().appendChild(document.createTextNode(this.options.displaySeparator)); } } }); } private renderTextCaption(): HTMLElement { const element = $$('span', { className: 'coveo-field-caption' }, escape(this.options.textCaption)); return element.el; } protected prependTextCaptionToDom(): void { const elem = this.getValueContainer(); $$(elem).prepend(this.renderTextCaption()); // Add a class to the container so the value and the caption wrap together. $$(elem).addClass('coveo-with-label'); } private bindEventOnValue(element: HTMLElement, originalFacetValue: string, renderedFacetValue: string) { this.bindFacets(element, originalFacetValue, renderedFacetValue); } private getCaption(value: string) { for (let facet of this.getFacets()) { const caption = facet.getCaptionForStringValue(value); if (caption) { return caption; } } return FacetUtils.tryToGetTranslatedCaption(this.options.field as string, value); } private getFacets() { const facets = ComponentsTypes.getAllFacetsFromSearchInterface(this.searchInterface) .filter(isFacetFieldValueCompatible) .filter(facet => !facet.disabled); const facetsWithMatchingId = facets.filter(facet => facet.options.id === this.options.facet); if (facetsWithMatchingId.length) { return facetsWithMatchingId; } return facets.filter(facet => facet.options.field === this.options.field); } private get isValueHierarchical() { for (let facet of this.getFacets()) { if (facet.isFieldValueHierarchical) { return true; } } return false; } private bindFacets(element: HTMLElement, originalFacetValue: string, renderedFacetValue: string) { const facets = this.getFacets(); if (!facets.length) { return; } const isValueSelected = !!find(facets, facet => facet.hasSelectedValue(originalFacetValue)); const selectAction = () => this.handleFacetSelection(isValueSelected, facets, originalFacetValue); this.buildClickableElement(element, isValueSelected, renderedFacetValue, selectAction); } private buildClickableElement(element: HTMLElement, isSelected: boolean, value: string, selectAction: () => void) { const label = isSelected ? l('RemoveFilterOn', value) : l('FilterOn', value); new AccessibleButton().withTitle(label).withElement(element).withSelectAction(selectAction).build(); if (isSelected) { $$(element).addClass('coveo-selected'); } $$(element).addClass('coveo-clickable'); } private handleFacetSelection(isValueSelected: boolean, facets: IFieldValueCompatibleFacet[], value: string) { facets.forEach(facet => { isValueSelected ? facet.deselectValue(value) : facet.selectValue(value); }); this.executeQuery(value); } private executeQuery(value: string) { this.queryController.deferExecuteQuery({ beforeExecuteQuery: () => this.usageAnalytics.logSearchEvent<IAnalyticsFieldValueMeta>(analyticsActionCauseList.documentField, { facetId: this.options.facet, facetField: this.options.field.toString(), facetValue: value.toLowerCase() }) }); } } Initialization.registerAutoCreateComponent(FieldValue);