coveo-search-ui
Version:
Coveo JavaScript Search Framework
237 lines (210 loc) • 8.02 kB
text/typescript
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);
}
}