UNPKG

coveo-search-ui

Version:

Coveo JavaScript Search Framework

206 lines (186 loc) 8.14 kB
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);