UNPKG

coveo-search-ui

Version:

Coveo JavaScript Search Framework

511 lines (463 loc) • 19.1 kB
import PopperJs from 'popper.js'; import 'styling/_Quickview'; import { QuickviewEvents } from '../../events/QuickviewEvents'; import { ResultListEvents } from '../../events/ResultListEvents'; import { ModalBox as ModalBoxModule } from '../../ExternalModulesShim'; import { exportGlobally } from '../../GlobalExports'; import { QueryStateModel } from '../../models/QueryStateModel'; import { IQueryResult } from '../../rest/QueryResult'; import { l } from '../../strings/Strings'; import { AccessibleButton } from '../../utils/AccessibleButton'; import { $$, Dom } from '../../utils/Dom'; import { DomUtils } from '../../utils/DomUtils'; import { StringUtils } from '../../utils/StringUtils'; import { SVGDom } from '../../utils/SVGDom'; import { SVGIcons } from '../../utils/SVGIcons'; import { Utils } from '../../utils/Utils'; import { analyticsActionCauseList } from '../Analytics/AnalyticsActionListMeta'; import { Component } from '../Base/Component'; import { ComponentOptions } from '../Base/ComponentOptions'; import { ComponentOptionsType } from '../Base/IComponentOptions'; import { IInitializationParameters, Initialization } from '../Base/Initialization'; import { IResultsComponentBindings } from '../Base/ResultsComponentBindings'; import { TemplateComponentOptions } from '../Base/TemplateComponentOptions'; import { Template } from '../Templates/Template'; import { DefaultQuickviewTemplate } from './DefaultQuickviewTemplate'; import { QuickviewDocument } from './QuickviewDocument'; import { AccessibleModal } from '../../utils/AccessibleModal'; /** * The allowed [`Quickview`]{@link Quickview} [`tooltipPlacement`]{@link Quickview.options.tooltipPlacement} option values. The `-start` and `-end` variations indicate relative alignement. Horizontally (`top`, `bottom`), `-start` means _left_ and `-end` means _right_. Vertically (`left`, `right`), `-start` means _top_ and `-end` means _bottom_. No variation means _center_. */ export type ValidTooltipPlacement = | 'auto-start' | 'auto' | 'auto-end' | 'top-start' | 'top' | 'top-end' | 'right-start' | 'right' | 'right-end' | 'bottom-end' | 'bottom' | 'bottom-start' | 'left-end' | 'left' | 'left-start'; export interface IQuickviewOptions { title?: string; showDate?: boolean; contentTemplate?: Template; enableLoadingAnimation?: boolean; loadingAnimation?: HTMLElement | Promise<HTMLElement>; alwaysShow?: boolean; tooltipPlacement?: ValidTooltipPlacement; } interface IQuickviewOpenerObject { content: Promise<Dom>; loadingAnimation: HTMLElement | Promise<HTMLElement>; } /** * The `Quickview` component renders a button/link which the end user can click to open a modal box containing certain * information about a result. Most of the time, this component references a * [`QuickviewDocument`]{@link QuickviewDocument} in its [`contentTemplate`]{@link Quickview.options.contentTemplate}. * * **Notes:** * > - `Quickview` is not meant to replace a [ResultLink]{@link ResultLink} to access an item; it has certain limitations (e.g., custom styles and embedded * images/links may not work as expected in a `Quickview`). * > - You can change the appearance of the `Quickview` link/button by adding elements in the inner HTML of its `div`. * > - You can change the content of the `Quickview` modal box link by specifying a template `id` or CSS selector (see * > the [`contentTemplate`]{@link Quickview.options.contentTemplate} option). * > - When using Coveo for Salesforce 3.16, in an environment compliant with LockerService, ensure you use `CoveoSalesforceQuickview` (see [Changing the Default Quick View in Coveo for Salesforce](https://docs.coveo.com/en/1234/)). * * **Example:** * ```html * [ ... ] * * <script class='result-template' type='text/underscore' id='myContentTemplateId'> * <div> * <span>This content will be displayed when then end user opens the quickview modal box. It could also include other Coveo component, and use core helpers.</span> * <table class="CoveoFieldTable"> * <tr data-field="@liboardshorttitle" data-caption="Board" /> * <tr data-field="@licategoryshorttitle" data-caption="Category" /> * <tr data-field="@sysauthor" data-caption="Author" /> * </table> * </div> * </script> * * [ ... ] * * <div class='CoveoResultList'> * <script class='result-template' type='text/underscore' id='myResultTemplateId'> * * [ ... ] * * <!-- The `myContentTemplateId` template applies when displaying content in the quickview modal box. --> * <div class='CoveoQuickview' data-template-id='myContentTemplateId'> * <!-- This changes the appearance of the Quickview button itself in the results --> * <span>Click here for a quickview</span> * </div> * </script> * * [ ... ] * * <!-- Note that simply including `<div class='CoveoQuickview'></div>` in the markup will be enough most of the time, since the component includes a default template and a default button appearance. --> * ``` * * This component is a result template component (see [Result Templates](https://docs.coveo.com/en/413/)). */ export class Quickview extends Component { static ID = 'Quickview'; static doExport = () => { exportGlobally({ Quickview: Quickview, QuickviewDocument: QuickviewDocument }); }; /** * @componentOptions */ static options: IQuickviewOptions = { /** * Specifies whether to always show the `Quickview` button/link, even when the index body of an item is empty. * * In such cases, the [`contentTemplate`]{@link Quickview.options.contentTemplate} defines what appears in the * `Quickview` modal box. Consequently, if there is no quickview for the item, you *MUST* specify a custom * `contentTemplate`, otherwise the component will throw an error when opened. * * Default value is `false`. * * @availablesince [September 2015 Release (v1.0.59)](https://docs.coveo.com/en/289/#september-2015-release-v1059) */ alwaysShow: ComponentOptions.buildBooleanOption({ defaultValue: false }), /** * Specifies the title that should appear in the `Quickview` modal box header. * * Default value is undefined, which is equivalent to the empty string. */ title: ComponentOptions.buildStringOption(), /** * Specifies whether to display the item date in the `Quickview` modal box header. * * Default value is `true`. */ showDate: ComponentOptions.buildBooleanOption({ defaultValue: true }), /** * Specifies whether to enable the loading animation. * * See also [`loadingAnimation`]{Quickview.options.loadingAnimation}. * * Default value is `true`. */ enableLoadingAnimation: ComponentOptions.buildBooleanOption({ defaultValue: true }), /** * Specifies a custom template to use when displaying content in the `Quickview` modal box. * * **Note:** * > You can use [`CoreHelpers`]{@link ICoreHelpers} methods in your content template. * * You can specify a previously registered template to use either by referring to its HTML `id` attribute or to a * CSS selector (see [`TemplateCache`]{@link TemplateCache}). * * **Example:** * * * Specifying a previously registered template by referring to its HTML `id` attribute: * * ```html * <div class="CoveoQuickview" data-template-id="myContentTemplateId"></div> * ``` * * * Specifying a previously registered template by referring to a CSS selector: * * ```html * <div class='CoveoQuickview' data-template-selector=".myContentTemplateSelector"></div> * ``` * * If you do not specify a custom content template, the component uses its default template. */ contentTemplate: TemplateComponentOptions.buildTemplateOption({ selectorAttr: 'data-template-selector', idAttr: 'data-template-id' }), /** * If [`enableLoadingAnimation`]{@link Quickview.options.enableLoadingAnimation} is `true`, specifies a custom * animation to display while the content of the quickview modal window is loading. You can either specify the CSS * selector of the HTML element you wish to display, or the `id` of a previously registered template (see * [`TemplateCache`]{@link TemplateCache}). * * **Examples:** * * * Specifying the CSS selector of the HTML element to display: * * ```html * <div class="CoveoQuickview" data-loading-animation-selector=".my-loading-animation"></div> * ``` * * * Specifying the `id` of a previously registered template: * * ```html * <div class="CoveoQuickview" data-loading-animation-template-id="my-loading-animation-template"></div> * ``` * * By default, the loading animation is a Coveo animation, which you can customize with CSS (see * [Styling the Coveo JavaScript Search Framework](https://docs.coveo.com/en/423/)). */ loadingAnimation: ComponentOptions.buildOption<HTMLElement | Promise<HTMLElement>>( ComponentOptionsType.NONE, (element: HTMLElement) => { const loadingAnimationSelector = element.getAttribute('data-loading-animation-selector'); if (loadingAnimationSelector != null) { const loadingAnimation = $$(document.documentElement).find(loadingAnimationSelector); if (loadingAnimation != null) { $$(loadingAnimation).detach(); return loadingAnimation; } } const id = element.getAttribute('data-loading-animation-template-id'); if (id != null) { const loadingAnimationTemplate = TemplateComponentOptions.loadResultTemplateFromId(id); if (loadingAnimationTemplate) { return loadingAnimationTemplate.instantiateToElement(undefined, { checkCondition: false }); } } return DomUtils.getBasicLoadingAnimation(); } ), /** * Specifies the emplacement of the tooltip in relation to the `Quickview` HTML element. * * **Example:** * > Setting this option to `top-start` will make the tooltip appear on top of the `Quickview` button, aligned to the left. * * @availablesince [January 2019 Release (v2.5395.12)](https://docs.coveo.com/en/2938/) */ tooltipPlacement: ComponentOptions.buildCustomOption<ValidTooltipPlacement>((value: ValidTooltipPlacement) => value, { defaultValue: 'bottom' }) }; public static resultCurrentlyBeingRendered: IQueryResult = null; private modalbox: AccessibleModal; /** * Creates a new `Quickview` component. * @param element The HTMLElement on which to instantiate the component. * @param options The options for the `Quickview` 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. * @param ModalBox The quickview modal box. */ constructor( public element: HTMLElement, public options?: IQuickviewOptions, public bindings?: IResultsComponentBindings, public result?: IQueryResult, ModalBox: Coveo.ModalBox.ModalBox = ModalBoxModule ) { super(element, Quickview.ID, bindings); this.options = ComponentOptions.initComponentOptions(element, Quickview, options); if (this.options.contentTemplate == null) { this.options.contentTemplate = new DefaultQuickviewTemplate(); } // If there is no content inside the Quickview div, // we need to add something that will show up in the result template itself if (/^\s*$/.test(this.element.innerHTML)) { this.buildContent(); } this.bindClick(result); if (this.bindings.resultElement) { this.bind.on(this.bindings.resultElement, ResultListEvents.openQuickview, (event?: Event) => { event && event.stopPropagation(); this.open(); }); } this.modalbox = new AccessibleModal('coveo-quick-view', this.searchInterface.options.modalContainer, ModalBox); } private buildContent() { const icon = this.buildIcon(); const caption = this.buildCaption(); const content = $$('div'); content.append(icon); content.append(caption); $$(this.element).append(content.el); this.buildTooltipIfNotInCardLayout(icon, caption); } private buildIcon() { const icon = $$('div', { className: 'coveo-icon-for-quickview' }, SVGIcons.icons.quickview).el; SVGDom.addClassToSVGInContainer(icon, 'coveo-icon-for-quickview-svg'); return icon; } private buildCaption() { return $$('div', { className: 'coveo-caption-for-icon' }, 'Quickview'.toLocaleString()).el; } private buildTooltipIfNotInCardLayout(icon: HTMLElement, caption: HTMLElement) { if (this.resultsAreInCardLayout) { return; } const arrow = $$('div').el; caption.appendChild(arrow); this.buildPopper(icon, caption, arrow); } private get resultsAreInCardLayout() { return this.queryStateModel.get(QueryStateModel.attributesEnum.layout) === 'card'; } private buildPopper(icon: HTMLElement, caption: HTMLElement, arrow: HTMLElement) { const popperReference = new PopperJs(icon, caption, { placement: this.options.tooltipPlacement, modifiers: { preventOverflow: { boundariesElement: $$(this.root).el, padding: 0 }, arrow: { element: arrow }, // X,Y offset of the tooltip relative to the icon offset: { offset: '0,8' } }, eventsEnabled: false }); $$(this.element).on('mouseover', () => { popperReference.update(); }); } /** * Opens the `Quickview` modal box. */ public open() { if (!this.modalbox.isOpen) { // To prevent the keyboard from opening on mobile if the search bar has focus Quickview.resultCurrentlyBeingRendered = this.result; // activeElement does not exist in LockerService if (document.activeElement && document.activeElement instanceof HTMLElement) { $$(document.activeElement as HTMLElement).trigger('blur'); } const openerObject = this.prepareOpenQuickviewObject(); return this.createModalBox(openerObject).then(() => { this.bindQuickviewEvents(openerObject); this.animateAndOpen(); this.logUsageAnalyticsEvent(); this.queryStateModel.set(QueryStateModel.attributesEnum.quickview, this.getHashId()); Quickview.resultCurrentlyBeingRendered = null; }); } } /** * Closes the `Quickview` modal box. */ public close() { if (this.modalbox.isOpen) { this.modalbox.close(); } } public getHashId() { return this.result.queryUid + '.' + this.result.index + '.' + StringUtils.hashCode(this.result.uniqueId); } private logUsageAnalyticsEvent() { this.usageAnalytics.logClickEvent( analyticsActionCauseList.documentQuickview, { author: Utils.getFieldValue(this.result, 'author'), documentURL: this.result.clickUri, documentTitle: this.result.title }, this.result, this.element ); } private bindClick(result: IQueryResult) { if (typeof result.hasHtmlVersion == 'undefined' || result.hasHtmlVersion || this.options.alwaysShow) { const clickAction = () => this.open(); new AccessibleButton().withElement(this.element).withSelectAction(clickAction).withLabel(l('Quickview')).withOwner(this.bind).build(); } else { this.element.style.display = 'none'; } } private bindQuickviewEvents(openerObject: IQuickviewOpenerObject) { $$(this.modalbox.content).on(QuickviewEvents.quickviewLoaded, async () => { const anim = await openerObject.loadingAnimation; $$(anim).remove(); }); } private animateAndOpen() { const quickviewDocument = $$(this.modalbox.element).find('.' + Component.computeCssClassName(QuickviewDocument)); if (quickviewDocument) { Initialization.dispatchNamedMethodCallOrComponentCreation('open', quickviewDocument, null); } } private createModalBox(openerObject: IQuickviewOpenerObject) { const computedModalBoxContent = $$('div'); computedModalBoxContent.addClass('coveo-computed-modal-box-content'); return openerObject.content.then(builtContent => { computedModalBoxContent.append(builtContent.el); this.modalbox.openResult({ result: this.result, options: { showDate: this.options.showDate, title: this.options.title }, bindings: this.bindings, content: computedModalBoxContent.el, validation: () => { this.closeQuickview(); return true; }, origin: this.element }); return computedModalBoxContent; }); } private prepareOpenQuickviewObject() { const loadingAnimation = this.options.loadingAnimation; return { loadingAnimation: loadingAnimation, content: this.prepareQuickviewContent(loadingAnimation) }; } private async prepareQuickviewContent(loadingAnimation: HTMLElement | Promise<HTMLElement>): Promise<Dom> { const domContent = await this.instantiateTemplateToDom(); const initOptions = this.searchInterface.options; const initParameters: IInitializationParameters = { options: initOptions, bindings: this.getBindings(), result: this.result }; await Initialization.automaticallyCreateComponentsInside(domContent.el, initParameters).initResult; const containsQuickviewDocumentAndCustomAnimation = () => domContent.find(`.${Component.computeCssClassName(QuickviewDocument)}`) != undefined && this.options.enableLoadingAnimation; if (containsQuickviewDocumentAndCustomAnimation()) { if (loadingAnimation instanceof HTMLElement) { domContent.prepend(loadingAnimation); } else if (loadingAnimation instanceof Promise) { loadingAnimation.then(anim => { domContent.prepend(anim); }); } } return domContent; } private async instantiateTemplateToDom(): Promise<Dom> { let templateInstantiated: HTMLElement; try { templateInstantiated = await this.options.contentTemplate.instantiateToElement(this.result); } catch (e) { this.logger.warn(e); } finally { if (!templateInstantiated) { this.logger.warn( 'An unexpected error happened while trying to render a custom template quickview, fallbacking on default quickview template...', this.options.contentTemplate ); templateInstantiated = await new DefaultQuickviewTemplate().instantiateToElement(this.result); } } return $$(templateInstantiated); } private closeQuickview() { this.queryStateModel.set(QueryStateModel.attributesEnum.quickview, ''); } } Initialization.registerAutoCreateComponent(Quickview);