coveo-search-ui
Version:
Coveo JavaScript Search Framework
327 lines (294 loc) • 12.6 kB
text/typescript
import 'styling/_FieldSuggestions';
import * as _ from 'underscore';
import { IPopulateOmniboxEventArgs, OmniboxEvents } from '../../events/OmniboxEvents';
import { exportGlobally } from '../../GlobalExports';
import { Assert } from '../../misc/Assert';
import { QueryStateModel } from '../../models/QueryStateModel';
import { IIndexFieldValue } from '../../rest/FieldValue';
import { IStringMap } from '../../rest/GenericParam';
import { IListFieldValuesRequest } from '../../rest/ListFieldValuesRequest';
import { $$ } from '../../utils/Dom';
import { Utils } from '../../utils/Utils';
import { analyticsActionCauseList, IAnalyticsNoMeta } from '../Analytics/AnalyticsActionListMeta';
import { Component } from '../Base/Component';
import { IComponentBindings } from '../Base/ComponentBindings';
import { ComponentOptions } from '../Base/ComponentOptions';
import { IFieldOption, IQueryExpression } from '../Base/IComponentOptions';
import { Initialization } from '../Base/Initialization';
import {
ISuggestionForOmniboxOptions,
ISuggestionForOmniboxOptionsOnSelect,
ISuggestionForOmniboxTemplate,
SuggestionForOmnibox
} from '../Misc/SuggestionForOmnibox';
export interface IFieldSuggestionsOptions extends ISuggestionForOmniboxOptions {
field?: IFieldOption;
queryOverride?: IQueryExpression;
}
/**
* The `FieldSuggestions` component provides query suggestions based on a particular facet field. For example, you could
* use this component to provide auto-complete suggestions while the end user is typing the title of an item.
*
* The query suggestions provided by this component appear in the [`Omnibox`]{@link Omnibox} component.
*
* **Note:** Consider [providing Coveo ML query suggestions](https://docs.coveo.com/en/340/#providing-coveo-machine-learning-query-suggestions)
* rather than field suggestions, as the former yields better performance and relevance.
*/
export class FieldSuggestions extends Component {
static ID = 'FieldSuggestions';
static doExport = () => {
exportGlobally({
FieldSuggestions: FieldSuggestions
});
};
/**
* @componentOptions
*/
static options: IFieldSuggestionsOptions = {
/**
* Specifies the facet field from which to provide suggestions.
*
* Specifying a value for this option is required for the `FieldSuggestions` component to work.
*/
field: ComponentOptions.buildFieldOption({ required: true }),
/**
* Specifies a query override to apply when retrieving suggestions. You can use any valid query expression (see
* [Coveo Query Syntax Reference](https://docs.coveo.com/en/1552/searching-with-coveo/coveo-cloud-query-syntax)).
*
* Default value is the empty string, and the component applies no query override.
*/
queryOverride: ComponentOptions.buildQueryExpressionOption({ defaultValue: '' }),
/**
* Specifies the z-index position at which the suggestions render themselves in the [`Omnibox`]{@link Omnibox}.
*
* When there are multiple suggestion providers, components with higher `omniboxZIndex` values render themselves
* first.
*
* Default value is `51`. Minimum value is `0`.
*/
omniboxZIndex: ComponentOptions.buildNumberOption({ defaultValue: 51, min: 0 }),
/**
* Specifies the title of the result suggestions group in the [`Omnibox`]{@link Omnibox} component.
* If not provided, the component will simply not output any title.
*
* Default value is `null`.
*/
headerTitle: ComponentOptions.buildLocalizedStringOption(),
/**
* Specifies the number of suggestions to render in the [`Omnibox`]{@link Omnibox}.
*
* Default value is `5`. Minimum value is `1`.
*/
numberOfSuggestions: ComponentOptions.buildNumberOption({ defaultValue: 5, min: 1 }),
/**
* Specifies the event handler function to execute when the end user selects a suggested value in the
* [`Omnibox`]{@link Omnibox}. By default, the query box text is replaced by what the end user selected and a new
* query is executed. You can, however, replace this default behavior by providing a callback function to execute
* when the value is selected.
*
* **Note:**
* > You cannot set this option directly in the component markup as an HTML attribute. You must either set it in the
* > [`init`]{@link init} call of your search interface (see
* > [Passing Component Options in the init Call](https://docs.coveo.com/en/346/#passing-component-options-in-the-init-call)),
* > or before the `init` call, using the `options` top-level function (see
* > [Passing Component Options Before the init Call](https://docs.coveo.com/en/346/#passing-component-options-before-the-init-call)).
*
* **Example:**
*
* ```javascript
*
* var myOnSelectFunction = function(selectedValue, populateOmniboxEventArgs) {
*
* // Close the suggestion list when the user clicks a suggestion.
* populateOmniboxEventArgs.closeOmnibox();
*
* // Search for matching title results in the default endpoint.
* Coveo.SearchEndpoint.endpoints["default"].search({
* q: "@title==" + selectedValue
* }).done(function(results) {
*
* // If more than one result is found, select a result that matches the selected title.
* var foundResult = Coveo._.find(results.results, function(result) {
* return selectedValue == result.raw.title;
* });
*
* // Open the found result in the current window, or log an error.
* if (foundResult) {
* window.location = foundResult.clickUri;
* }
* else {
* new Coveo.Logger.warn("Selected suggested result '" + selectedValue + "' not found.");
* }
* });
* };
*
* // You can set the option in the 'init' call:
* Coveo.init(document.querySelector("#search"), {
* FieldSuggestions : {
* onSelect : myOnSelectFunction
* }
* });
*
* // Or before the 'init' call, using the 'options' top-level function:
* // Coveo.options(document.querySelector("#search"), {
* // FieldSuggestions : {
* // onSelect : myOnSelectFunction
* // }
* // });
* ```
*/
onSelect: ComponentOptions.buildCustomOption<ISuggestionForOmniboxOptionsOnSelect>(() => {
return null;
})
};
private suggestionForOmnibox: SuggestionForOmnibox;
private currentlyDisplayedSuggestions: { [suggestion: string]: { element: HTMLElement; pos: number } };
/**
* Creates a new `FieldSuggestions` component.
* @param element The HTMLElement on which to instantiate the component.
* @param options The options for the `FieldSuggestions` 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(element: HTMLElement, public options: IFieldSuggestionsOptions, bindings?: IComponentBindings) {
super(element, FieldSuggestions.ID, bindings);
if (this.options && 'omniboxSuggestionOptions' in this.options) {
this.options = _.extend(this.options, this.options['omniboxSuggestionOptions']);
}
this.options = ComponentOptions.initComponentOptions(element, FieldSuggestions, options);
Assert.check(Utils.isCoveoField(<string>this.options.field), this.options.field + ' is not a valid field');
this.options.onSelect = this.options.onSelect || this.onRowSelection;
let rowTemplate = (toRender: IStringMap<string>) => {
let rowElement = $$('div', {
className: 'magic-box-suggestion coveo-omnibox-selectable coveo-top-field-suggestion-row'
});
if (toRender['data']) {
rowElement.el.innerHTML = toRender['data'];
}
return rowElement.el.outerHTML;
};
let suggestionStructure: ISuggestionForOmniboxTemplate;
if (this.options.headerTitle == null) {
suggestionStructure = {
row: rowTemplate
};
} else {
let headerTemplate = () => {
let headerElement = $$('div', {
className: 'coveo-top-field-suggestion-header'
});
let iconElement = $$('span', {
className: 'coveo-icon-top-field'
});
let captionElement = $$('span', {
className: 'coveo-caption'
});
if (this.options.headerTitle) {
captionElement.text(this.options.headerTitle);
}
headerElement.append(iconElement.el);
headerElement.append(captionElement.el);
return headerElement.el.outerHTML;
};
suggestionStructure = {
header: { template: headerTemplate, title: this.options.headerTitle },
row: rowTemplate
};
}
this.suggestionForOmnibox = new SuggestionForOmnibox(
suggestionStructure,
(value: string, args: IPopulateOmniboxEventArgs) => {
this.options.onSelect.call(this, value, args);
},
(value: string, args: IPopulateOmniboxEventArgs) => {
this.onRowTab(value, args);
}
);
this.bind.onRootElement(OmniboxEvents.populateOmnibox, (args: IPopulateOmniboxEventArgs) => this.handlePopulateOmnibox(args));
}
public selectSuggestion(suggestion: number);
public selectSuggestion(suggestion: string);
/**
* Selects a currently displayed query suggestion. This implies that at least one suggestion must have been returned
* at least once.
* @param suggestion Either a number (0-based index position of the query suggestion to select) or a string that
* matches the suggestion to select.
*/
public selectSuggestion(suggestion: any) {
if (this.currentlyDisplayedSuggestions) {
if (isNaN(suggestion)) {
if (this.currentlyDisplayedSuggestions[suggestion]) {
$$(this.currentlyDisplayedSuggestions[suggestion].element).trigger('click');
}
} else {
let currentlySuggested = <{ element: HTMLElement; pos: number }>_.findWhere(<any>this.currentlyDisplayedSuggestions, {
pos: suggestion
});
if (currentlySuggested) {
$$(currentlySuggested.element).trigger('click');
}
}
}
}
private handlePopulateOmnibox(args: IPopulateOmniboxEventArgs) {
Assert.exists(args);
let valueToSearch = args.completeQueryExpression.word;
let promise = new Promise(resolve => {
this.queryController
.getEndpoint()
.listFieldValues(this.buildListFieldValueRequest(valueToSearch))
.then((results: IIndexFieldValue[]) => {
let element = this.suggestionForOmnibox.buildOmniboxElement(results, args);
this.currentlyDisplayedSuggestions = {};
if (element) {
_.map($$(element).findAll('.coveo-omnibox-selectable'), (selectable, i?) => {
this.currentlyDisplayedSuggestions[$$(selectable).text()] = {
element: selectable,
pos: i
};
});
resolve({
element: element,
zIndex: this.options.omniboxZIndex
});
} else {
resolve({
element: undefined
});
}
})
.catch(() => {
resolve({
element: undefined
});
});
});
args.rows.push({
deferred: promise
});
}
private onRowSelection(value: string, args: IPopulateOmniboxEventArgs) {
args.closeOmnibox();
this.queryStateModel.set(QueryStateModel.attributesEnum.q, value);
this.usageAnalytics.logSearchEvent<IAnalyticsNoMeta>(analyticsActionCauseList.omniboxField, {});
this.queryController.executeQuery();
}
private onRowTab(value: string, args: IPopulateOmniboxEventArgs) {
args.clear();
args.closeOmnibox();
this.queryStateModel.set(QueryStateModel.attributesEnum.q, `${value}`);
this.usageAnalytics.logCustomEvent(analyticsActionCauseList.omniboxField, {}, this.element);
}
private buildListFieldValueRequest(valueToSearch: string): IListFieldValuesRequest {
return {
field: <string>this.options.field,
ignoreAccents: true,
sortCriteria: 'occurrences',
maximumNumberOfValues: this.options.numberOfSuggestions,
patternType: 'Wildcards',
pattern: '*' + valueToSearch + '*',
queryOverride: this.options.queryOverride
};
}
}
Initialization.registerAutoCreateComponent(FieldSuggestions);