coveo-search-ui
Version:
Coveo JavaScript Search Framework
382 lines (326 loc) • 13.9 kB
text/typescript
import { Template } from '../Templates/Template';
import { Component } from '../Base/Component';
import { IComponentBindings } from '../Base/ComponentBindings';
import { ComponentOptions } from '../Base/ComponentOptions';
import { DefaultFoldingTemplate } from './DefaultFoldingTemplate';
import { IQueryResult } from '../../rest/QueryResult';
import { Utils } from '../../utils/Utils';
import { QueryUtils } from '../../utils/QueryUtils';
import { Initialization } from '../Base/Initialization';
import { Assert } from '../../misc/Assert';
import { $$, Win } from '../../utils/Dom';
import { l } from '../../strings/Strings';
import { each, map } from 'underscore';
import { exportGlobally } from '../../GlobalExports';
import { analyticsActionCauseList, IAnalyticsDocumentViewMeta } from '../Analytics/AnalyticsActionListMeta';
import { Logger } from '../../misc/Logger';
import 'styling/_ResultFolding';
import { SVGIcons } from '../../utils/SVGIcons';
import { SVGDom } from '../../utils/SVGDom';
import { TemplateComponentOptions } from '../Base/TemplateComponentOptions';
import { AccessibleButton } from '../../utils/AccessibleButton';
export interface IResultFoldingOptions {
resultTemplate?: Template;
normalCaption?: string;
expandedCaption?: string;
moreCaption?: string;
lessCaption?: string;
oneResultCaption?: string;
}
/**
* The `ResultFolding` component renders folded result sets. It is usable inside a result template when there is an
* active [`Folding`]{@link Folding} component in the page. This component takes care of rendering the parent result and
* its child results in a coherent manner.
*
* This component is a result template component (see [Result Templates](https://docs.coveo.com/en/413/)).
*
* See [Folding Results](https://docs.coveo.com/en/428/).
*/
export class ResultFolding extends Component {
static ID = 'ResultFolding';
static doExport = () => {
exportGlobally({
ResultFolding: ResultFolding,
DefaultFoldingTemplate: DefaultFoldingTemplate
});
};
/**
* The options for the component
* @componentOptions
*/
static options: IResultFoldingOptions = {
/**
* Specifies the template to use to render each of the child results for a top result.
*
* You can specify a previously registered template to use either by referring to its HTML `id` attribute or to a
* CSS selector (see {@link TemplateCache}).
*
* **Example:**
*
* Specifying a previously registered template by referring to its HTML `id` attribute:
*
* ```html
* <span class="CoveoResultFolding" data-result-template-id="Foo"></span>
* ```
*
* Specifying a previously registered template by referring to a CSS selector:
*
* ```html
* <span class='CoveoResultFolding' data-result-template-selector="#Foo"></span>
* ```
*
* If you do not specify a custom folding template, the component uses the default result folding template.
*/
resultTemplate: TemplateComponentOptions.buildTemplateOption({ defaultFunction: () => new DefaultFoldingTemplate() }),
/**
* Specifies the caption to display at the top of the child results when the folding result set is not expanded.
*
* Default value is `undefined`, which displays no caption.
*/
normalCaption: ComponentOptions.buildLocalizedStringOption(),
/**
* Specifies the caption to display at the top of the child results when the folding result set is expanded.
*
* Default value is `undefined`, which displays no caption.
*/
expandedCaption: ComponentOptions.buildLocalizedStringOption(),
/**
* Specifies the caption to display on the link to expand / show child results.
*
* Default value is the localized string for `ShowMore`.
*/
moreCaption: ComponentOptions.buildLocalizedStringOption({ postProcessing: value => value || l('ShowMore') }),
/**
* Specifies the caption to display on the link to shrink the loaded folding result set back to only the top result.
*
* Default value is the localized string for `ShowLess`.
*/
lessCaption: ComponentOptions.buildLocalizedStringOption({ postProcessing: value => value || l('ShowLess') }),
/**
* Specifies the caption to display when there is only one result in a folding result set.
*
* Default value is the localized string for `DisplayingTheOnlyMessage`
*/
oneResultCaption: ComponentOptions.buildLocalizedStringOption({
postProcessing: value => value || l('DisplayingTheOnlyMessage')
})
};
private normalCaption: HTMLElement;
private expandedCaption: HTMLElement;
private oneResultCaption: HTMLElement;
private results: HTMLElement;
private showMore: HTMLElement;
private showLess: HTMLElement;
private waitAnimation: HTMLElement;
private moreResultsPromise: Promise<IQueryResult[]>;
private showingMoreResults = false;
public childResults: IQueryResult[];
/**
* Creates a new ResultFolding component.
* @param options The options for the ResultFolding 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?: IResultFoldingOptions,
bindings?: IComponentBindings,
public result?: IQueryResult
) {
super(element, ResultFolding.ID, bindings);
this.options = ComponentOptions.initComponentOptions(this.element, ResultFolding, options);
Assert.exists(result);
this.buildElements();
this.renderElements();
}
/**
* Show more results by fetching additional results from the index, which match the current folded conversation.
* This is the equivalent of clicking "Show all conversation".
* @returns {Promise<IQueryResult[]>}
*/
public async showMoreResults() {
Assert.exists(this.result.moreResults);
this.cancelAnyPendingShowMore();
this.moreResultsPromise = this.result.moreResults();
this.waitAnimation = $$('div', { className: 'coveo-loading-spinner' }).el;
this.results.appendChild(this.waitAnimation);
this.updateElementVisibility();
const results = await this.moreResultsPromise;
this.childResults = results;
this.showingMoreResults = true;
this.usageAnalytics.logClickEvent<IAnalyticsDocumentViewMeta>(
analyticsActionCauseList.foldingShowMore,
this.getAnalyticsMetadata(),
this.result,
this.element
);
try {
await this.displayThoseResults(results);
this.updateElementVisibility(results.length);
} catch (e) {
const logger = new Logger(this);
logger.warn('An error occured when trying to display more results');
}
this.moreResultsPromise = undefined;
$$(this.waitAnimation).detach();
this.waitAnimation = undefined;
return results;
}
/**
* Show less results for a given conversation. This is the equivalent of clicking "Show less"
*/
public async showLessResults() {
this.cancelAnyPendingShowMore();
this.showingMoreResults = false;
this.usageAnalytics.logCustomEvent<IAnalyticsDocumentViewMeta>(
analyticsActionCauseList.foldingShowLess,
this.getAnalyticsMetadata(),
this.element
);
await this.displayThoseResults(this.result.childResults);
this.updateElementVisibility();
this.scrollToResultElement();
}
private buildElements() {
this.buildHeader();
this.buildResults();
this.buildFooter();
}
private async renderElements() {
await this.displayThoseResults(this.result.childResults);
this.updateElementVisibility();
if ($$(this.element.parentElement).hasClass('CoveoCardOverlay')) {
this.bindOverlayEvents();
}
if (this.result.childResults.length == 0 && !this.result.moreResults) {
$$(this.element).hide();
}
}
private buildHeader() {
let header = $$('div', { className: 'coveo-folding-header' }).el;
this.element.appendChild(header);
if (this.options.normalCaption != undefined && this.options.expandedCaption != undefined) {
this.normalCaption = $$('div', { className: 'coveo-folding-normal-caption' }, this.options.normalCaption).el;
header.appendChild(this.normalCaption);
this.expandedCaption = $$('div', { className: 'coveo-folding-expanded-caption' }, this.options.expandedCaption).el;
header.appendChild(this.expandedCaption);
}
this.oneResultCaption = $$('div', { className: 'coveo-folding-oneresult-caption' }, this.options.oneResultCaption).el;
header.appendChild(this.oneResultCaption);
}
private buildResults() {
this.results = $$('div', { className: 'coveo-folding-results' }).el;
this.element.appendChild(this.results);
}
private buildFooter() {
const footer = $$('div', { className: 'coveo-folding-footer' }).el;
this.element.parentElement.appendChild(footer);
if (this.result.moreResults) {
this.showMore = $$('div', { className: 'coveo-folding-footer-section-for-less' }).el;
footer.appendChild(this.showMore);
this.showLess = $$('div', { className: 'coveo-folding-footer-section-for-more' }).el;
footer.appendChild(this.showLess);
const footerIconShowMore = $$(
'div',
{ className: 'coveo-folding-more' },
$$('span', { className: 'coveo-folding-footer-icon' }, SVGIcons.icons.arrowDown).el
).el;
SVGDom.addClassToSVGInContainer(footerIconShowMore, 'coveo-folding-more-svg');
const footerIconShowLess = $$(
'div',
{ className: 'coveo-folding-less' },
$$('span', { className: 'coveo-folding-footer-icon' }, SVGIcons.icons.arrowUp).el
).el;
SVGDom.addClassToSVGInContainer(footerIconShowLess, 'coveo-folding-less-svg');
const showMoreLink = $$('a', { className: 'coveo-folding-show-more' }, this.options.moreCaption).el;
const showLessLink = $$('a', { className: 'coveo-folding-show-less' }, this.options.lessCaption).el;
new AccessibleButton()
.withElement(this.showMore)
.withLabel(this.options.moreCaption)
.withSelectAction(() => this.showMoreResults())
.build();
new AccessibleButton()
.withElement(this.showLess)
.withLabel(this.options.lessCaption)
.withSelectAction(() => this.showLessResults())
.build();
this.showMore.appendChild(showMoreLink);
this.showLess.appendChild(showLessLink);
this.showMore.appendChild(footerIconShowMore);
this.showLess.appendChild(footerIconShowLess);
}
}
private updateElementVisibility(subResultsLength?: number) {
if (this.normalCaption) {
$$(this.normalCaption).toggle(!this.showingMoreResults && this.result.childResults.length > 0);
}
if (this.expandedCaption) {
$$(this.expandedCaption).toggle(this.showingMoreResults);
}
$$(this.oneResultCaption).toggleClass('coveo-hidden', !(subResultsLength && subResultsLength == 1));
if (this.showMore) {
$$(this.showMore).toggleClass('coveo-visible', !this.showingMoreResults && !Utils.exists(this.moreResultsPromise));
$$(this.showLess).toggleClass('coveo-visible', this.showingMoreResults);
}
let showIfNormal = $$(this.element).find('.coveo-show-if-normal');
if (showIfNormal) {
$$(showIfNormal).toggle(!this.showingMoreResults);
}
let showIfExpanded = $$(this.element).find('.coveo-show-if-expanded');
if (showIfExpanded) {
$$(showIfExpanded).toggle(this.showingMoreResults);
}
}
private scrollToResultElement() {
let resultElem = $$(this.element).closest('CoveoResult');
window.scrollTo(0, new Win(window).scrollY() + resultElem.getBoundingClientRect().top);
}
private async displayThoseResults(results: IQueryResult[]): Promise<boolean> {
const childResultsPromises = map(results, result => {
return this.renderChildResult(result);
});
const childsToAppend: HTMLElement[] = await Promise.all(childResultsPromises);
$$(this.results).empty();
each(childsToAppend, oneChild => {
this.results.appendChild(oneChild);
});
return true;
}
private async renderChildResult(childResult: IQueryResult): Promise<HTMLElement> {
QueryUtils.setStateObjectOnQueryResult(this.queryStateModel.get(), childResult);
QueryUtils.setSearchInterfaceObjectOnQueryResult(this.searchInterface, childResult);
const oneChild: HTMLElement = await this.options.resultTemplate.instantiateToElement(childResult, {
wrapInDiv: false,
checkCondition: false,
responsiveComponents: this.searchInterface.responsiveComponents
});
$$(oneChild).addClass('coveo-result-folding-child-result');
$$(oneChild).toggleClass('coveo-normal-child-result', !this.showingMoreResults);
$$(oneChild).toggleClass('coveo-expanded-child-result', this.showingMoreResults);
await Initialization.automaticallyCreateComponentsInsideResult(oneChild, childResult).initResult;
return oneChild;
}
private cancelAnyPendingShowMore() {
if (this.moreResultsPromise) {
this.moreResultsPromise = undefined;
}
Assert.doesNotExists(this.moreResultsPromise);
Assert.doesNotExists(this.waitAnimation);
}
private bindOverlayEvents() {
this.bind.one(this.element.parentElement, 'openCardOverlay', () => {
if (this.result.moreResults) {
this.showMoreResults();
}
});
}
private getAnalyticsMetadata(): IAnalyticsDocumentViewMeta {
return {
documentURL: this.result.clickUri,
documentTitle: this.result.title,
author: Utils.getFieldValue(this.result, 'author')
};
}
}
Initialization.registerAutoCreateComponent(ResultFolding);