coveo-search-ui
Version:
Coveo JavaScript Search Framework
206 lines (186 loc) • 8.14 kB
text/typescript
import { IComponentBindings } from '../Base/ComponentBindings';
import { exportGlobally } from '../../GlobalExports';
import { ComponentOptions, Initialization, $$, Component, HtmlTemplate } from '../../Core';
import { IQueryResults } from '../../rest/QueryResults';
import 'styling/_QuerySuggestPreview';
import { Template } from '../Templates/Template';
import { TemplateComponentOptions } from '../Base/TemplateComponentOptions';
import { ITemplateToHtml, TemplateToHtml } from '../Templates/TemplateToHtml';
import { ResultLink } from '../ResultLink/ResultLink';
import { ISearchResultPreview } from '../../magicbox/ResultPreviewsManager';
import { ImageFieldValue } from '../FieldImage/ImageFieldValue';
import {
ResultPreviewsManagerEvents,
IPopulateSearchResultPreviewsEventArgs,
IUpdateResultPreviewsManagerOptionsEventArgs,
IBuildingResultPreviewsQueryEventArgs
} from '../../events/ResultPreviewsManagerEvents';
import { IQuery } from '../../rest/Query';
import { Suggestion } from '../../magicbox/SuggestionsManager';
export interface IQuerySuggestPreview {
numberOfPreviewResults?: number;
resultTemplate?: Template;
executeQueryDelay?: number;
}
/**
* This component renders previews of the top query results matching the currently focused query suggestion in the search box.
*
* As such, this component only works when the search interface can
* [provide Coveo Machine Learning query suggestions](https://docs.coveo.com/en/340/#providing-coveo-machine-learning-query-suggestions).
*
* This component should be initialized on a `div` which can be nested anywhere inside the root element of your search interface.
*
* See [Rendering Query Suggestion Result Previews](https://docs.coveo.com/en/340/#rendering-query-suggestion-result-previews).
*
* @availablesince [December 2019 Release (v2.7610)](https://docs.coveo.com/en/3142/)
*/
export class QuerySuggestPreview extends Component implements IComponentBindings {
static ID = 'QuerySuggestPreview';
static doExport = () => {
exportGlobally({
QuerySuggestPreview: QuerySuggestPreview
});
};
/**
* The options for the component
* @componentOptions
*/
static options: IQuerySuggestPreview = {
/**
* The HTML `id` attribute value, or CSS selector of the previously registered
* [result template](https://docs.coveo.com/413/) to apply when rendering the
* query suggestion result previews.
*
* **Examples**
* * Specifying the `id` attribute of the target result template:
* ```html
* <div class="CoveoQuerySuggestPreview" data-result-template-id="myTemplateId"></div>
* ```
* * Specifying an equivalent CSS selector:
* ```html
* <div class="CoveoQuerySuggestPreview" data-result-template-selector="#myTemplateId"></div>
* ```
*
* If you specify no previously registered template through this option, the component uses its default template.
*/
resultTemplate: TemplateComponentOptions.buildTemplateOption(),
/**
* The maximum number of query results to render in the preview.
*/
numberOfPreviewResults: ComponentOptions.buildNumberOption({
defaultValue: 4,
min: 1,
max: 6
}),
/**
* The amount of focus time (in milliseconds) required on a query suggestion before requesting a preview of its top results.
*/
executeQueryDelay: ComponentOptions.buildNumberOption({ defaultValue: 200 })
};
/**
* Creates a new QuerySuggestPreview component.
* @param element The HTMLElement on which to instantiate the component.
* @param options The options for the QuerySuggestPreview 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).
*/
constructor(public element: HTMLElement, public options?: IQuerySuggestPreview, public bindings?: IComponentBindings) {
super(element, QuerySuggestPreview.ID, bindings);
this.options = ComponentOptions.initComponentOptions(element, QuerySuggestPreview, options);
if (!this.options.resultTemplate) {
this.logger.warn(`No template was provided for ${QuerySuggestPreview.ID}, a default template was used instead.`);
this.options.resultTemplate = this.buildDefaultSearchResultPreviewTemplate();
}
this.bind.onRootElement(
ResultPreviewsManagerEvents.updateResultPreviewsManagerOptions,
(args: IUpdateResultPreviewsManagerOptionsEventArgs) =>
(args.displayAfterDuration = Math.max(args.displayAfterDuration || 0, this.options.executeQueryDelay))
);
this.bind.onRootElement(ResultPreviewsManagerEvents.populateSearchResultPreviews, (args: IPopulateSearchResultPreviewsEventArgs) =>
this.populateSearchResultPreviews(args)
);
}
private buildDefaultSearchResultPreviewTemplate() {
return HtmlTemplate.create(
$$(
'div',
{ className: 'result-template' },
$$(
'div',
{ className: 'coveo-result-frame coveo-default-result-preview' },
$$('div', { className: Component.computeCssClassName(ImageFieldValue), 'data-field': '@image' }),
$$('a', { className: Component.computeCssClassName(ResultLink) })
)
).el
);
}
private get templateToHtml() {
const templateToHtmlArgs: ITemplateToHtml = {
searchInterface: this.searchInterface,
queryStateModel: this.queryStateModel,
resultTemplate: this.options.resultTemplate
};
return new TemplateToHtml(templateToHtmlArgs);
}
private populateSearchResultPreviews(args: IPopulateSearchResultPreviewsEventArgs) {
args.previewsQueries.push(this.fetchSearchResultPreviews(args.suggestion));
}
private async fetchSearchResultPreviews(suggestion: Suggestion) {
const query = this.buildQuery(suggestion);
const results = await this.queryController.getEndpoint().search(query);
if (!results) {
return [];
}
return this.buildResultsPreview(suggestion, results);
}
private buildQuery(suggestion: Suggestion) {
const query = this.buildDefaultQuery(suggestion);
$$(this.root).trigger(ResultPreviewsManagerEvents.buildingResultPreviewsQuery, <IBuildingResultPreviewsQueryEventArgs>{ query });
return query;
}
private buildDefaultQuery(suggestion: Suggestion): IQuery {
const { searchHub, pipeline, tab, locale, timezone, context, cq } = this.queryController.getLastQuery();
return {
firstResult: 0,
searchHub,
pipeline,
tab,
locale,
timezone,
context,
cq,
numberOfResults: this.options.numberOfPreviewResults,
q: suggestion.text || suggestion.dom.innerText,
...(suggestion.advancedQuery && { aq: suggestion.advancedQuery })
};
}
private async buildResultsPreview(suggestion: Suggestion, results: IQueryResults) {
const buildResults = await this.templateToHtml.buildResults(results, 'preview', []);
if (!(buildResults.length > 0)) {
return [];
}
return buildResults.map((element, index) => this.buildResultPreview(suggestion, element, index));
}
private buildResultPreview(suggestion: Suggestion, element: HTMLElement, rank: number): ISearchResultPreview {
element.classList.add('coveo-preview-selectable');
const resultLink = element.querySelector(Component.computeSelectorForType(ResultLink.ID)) as HTMLElement;
if (resultLink) {
element.setAttribute('aria-label', resultLink.textContent);
resultLink.setAttribute('role', 'link');
resultLink.removeAttribute('aria-level');
}
return {
element,
onSelect: () => this.handleSelect(suggestion, element, rank)
};
}
private handleSelect(suggestion: Suggestion, element: HTMLElement, rank: number) {
const link = $$(element).find(`.${Component.computeCssClassNameForType('ResultLink')}`);
if (link) {
const resultLink = <ResultLink>Component.get(link);
resultLink.openLinkAsConfigured();
resultLink.openLink();
}
}
}
Initialization.registerAutoCreateComponent(QuerySuggestPreview);