UNPKG

coveo-search-ui

Version:

Coveo JavaScript Search Framework

237 lines (210 loc) • 8.02 kB
import { $$, Dom } from '../utils/Dom'; import { l } from '../strings/Strings'; import { defaults, findIndex } from 'underscore'; import { Component } from '../ui/Base/Component'; import { Direction, Suggestion } from './SuggestionsManager'; import { ResultPreviewsManagerEvents, IPopulateSearchResultPreviewsEventArgs, IUpdateResultPreviewsManagerOptionsEventArgs } from '../events/ResultPreviewsManagerEvents'; import { QueryProcessor, ProcessingStatus } from './QueryProcessor'; import { Utils } from '../utils/Utils'; export interface ISearchResultPreview { element: HTMLElement; onSelect: () => void; } export interface IResultPreviewsManagerOptions { previewClass: string; selectedClass: string; previewHeaderText: string; previewHeaderFieldText: string; timeout: number; } export class ResultPreviewsManager { private options: IResultPreviewsManagerOptions; private suggestionsPreviewContainer: Dom; private resultPreviewsContainer: Dom; private lastQueriedSuggestion: Suggestion; private lastDisplayedSuggestion: Suggestion; private previewsProcessor: QueryProcessor<ISearchResultPreview>; private lastDelay: Promise<void>; private root: HTMLElement; public get previewsOwner() { return this.lastDisplayedSuggestion; } public get hasPreviews() { return !!this.suggestionsPreviewContainer; } public get focusedPreviewElement() { if (!this.hasPreviews) { return null; } const focusedElement = this.suggestionsPreviewContainer.findClass(this.options.selectedClass)[0]; if (!focusedElement || !focusedElement.classList.contains(this.options.previewClass)) { return null; } return focusedElement; } public get previewElements() { if (!this.hasPreviews) { return []; } return this.suggestionsPreviewContainer.findClass(this.options.previewClass); } private get suggestionsListbox() { return $$($$(this.element).findClass('coveo-magicbox-suggestions')[0]); } private get numberOfResultsPerRow() { const previewSelectables = this.suggestionsPreviewContainer.findClass(this.options.previewClass); if (previewSelectables.length === 0) { return 0; } const firstVerticalOffset = previewSelectables[0].offsetTop; const firstIndexOnNextRow = findIndex(previewSelectables, previewSelectable => previewSelectable.offsetTop !== firstVerticalOffset); return firstIndexOnNextRow !== -1 ? firstIndexOnNextRow : previewSelectables.length; } private get previewContainerId() { return `coveo-previews-for-${this.lastDisplayedSuggestion.dom.id}`; } constructor(private element: HTMLElement, options: Partial<IResultPreviewsManagerOptions> = {}) { this.options = defaults(options, <IResultPreviewsManagerOptions>{ previewHeaderText: l('QuerySuggestPreview'), previewHeaderFieldText: l('QuerySuggestPreviewWithField'), previewClass: 'coveo-preview-selectable', selectedClass: 'magic-box-selected' }); this.root = Component.resolveRoot(element); this.previewsProcessor = new QueryProcessor({ timeout: this.options.timeout }); } public async displaySearchResultPreviewsForSuggestion(suggestion: Suggestion) { const externalOptions = this.getExternalOptions(); const currentDelay = (this.lastDelay = Utils.resolveAfter( Utils.isNullOrUndefined(externalOptions.displayAfterDuration) ? 200 : externalOptions.displayAfterDuration )); await currentDelay; if (currentDelay !== this.lastDelay) { return; } const isQueryForSuggestionOngoing = suggestion && this.lastQueriedSuggestion === suggestion; if (isQueryForSuggestionOngoing) { return; } const arePreviewsForSuggestionCurrentlyDisplayed = this.lastDisplayedSuggestion === suggestion; if (arePreviewsForSuggestionCurrentlyDisplayed) { this.previewsProcessor.overrideIfProcessing(); this.lastQueriedSuggestion = null; return; } this.lastQueriedSuggestion = suggestion; if (!suggestion) { this.displaySuggestionPreviews(null, []); return; } const { status, results } = await this.getSearchResultPreviewsQuery(suggestion); if (status === ProcessingStatus.Overriden) { return; } this.lastQueriedSuggestion = null; this.displaySuggestionPreviews(suggestion, results); } public getElementInDirection(direction: Direction) { const previewElements = this.previewElements; const focusedIndex = previewElements.indexOf(this.focusedPreviewElement); if (focusedIndex === -1) { return null; } if (focusedIndex === 0 && direction === Direction.Left) { return null; } return previewElements[(focusedIndex + this.getIncrementInDirection(direction)) % previewElements.length]; } private getIncrementInDirection(direction: Direction) { switch (direction) { case Direction.Left: return -1; case Direction.Right: return 1; case Direction.Up: return -this.numberOfResultsPerRow; case Direction.Down: return this.numberOfResultsPerRow; } } private setHasPreviews(shouldHavePreviews: boolean) { if (this.hasPreviews === !!shouldHavePreviews) { return; } if (shouldHavePreviews) { this.initPreviewForSuggestions(); } else { this.revertPreviewForSuggestions(); } } private initPreviewForSuggestions() { this.suggestionsPreviewContainer = $$( 'div', { className: 'coveo-suggestion-container' }, this.suggestionsListbox.el, this.buildPreviewContainer() ); this.element.appendChild(this.suggestionsPreviewContainer.el); this.suggestionsListbox.setAttribute('aria-controls', this.previewContainerId); } private revertPreviewForSuggestions() { this.element.appendChild(this.suggestionsListbox.el); this.suggestionsPreviewContainer.remove(); this.suggestionsPreviewContainer = null; } private buildPreviewContainer() { return $$( 'div', { className: 'coveo-preview-container', id: this.previewContainerId }, (this.resultPreviewsContainer = $$('div', { className: 'coveo-preview-results', role: 'listbox', 'aria-orientation': 'horizontal' })) ).el; } private getExternalOptions() { const optionsEventArgs: IUpdateResultPreviewsManagerOptionsEventArgs = {}; $$(this.root).trigger(ResultPreviewsManagerEvents.updateResultPreviewsManagerOptions, optionsEventArgs); return optionsEventArgs; } private getSearchResultPreviewsQuery(suggestion: Suggestion) { const populateEventArgs: IPopulateSearchResultPreviewsEventArgs = { suggestion, previewsQueries: [] }; $$(this.root).trigger(ResultPreviewsManagerEvents.populateSearchResultPreviews, populateEventArgs); return this.previewsProcessor.processQueries(populateEventArgs.previewsQueries); } private appendSearchResultPreview(preview: ISearchResultPreview, position: number) { this.resultPreviewsContainer.append(preview.element); preview.element.id = `coveo-result-preview-${position}`; const elementDom = $$(preview.element); elementDom.setAttribute('aria-selected', 'false'); elementDom.setAttribute('role', 'option'); elementDom.on('click', () => preview.onSelect()); elementDom.on('keyboardSelect', () => preview.onSelect()); } private appendSearchResultPreviews(previews: ISearchResultPreview[]) { this.resultPreviewsContainer.empty(); previews.forEach((preview, i) => this.appendSearchResultPreview(preview, i)); } private displaySuggestionPreviews(suggestion: Suggestion, previews: ISearchResultPreview[]) { this.lastDisplayedSuggestion = suggestion; this.setHasPreviews(previews && previews.length > 0); this.element.classList.toggle('magic-box-hasPreviews', this.hasPreviews); if (!this.hasPreviews) { return; } this.appendSearchResultPreviews(previews); } }