coveo-search-ui
Version:
Coveo JavaScript Search Framework
511 lines (463 loc) • 19.1 kB
text/typescript
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);