coveo-search-ui
Version:
Coveo JavaScript Search Framework
399 lines (360 loc) • 16.1 kB
text/typescript
import 'styling/_Querybox';
import { IBuildingQueryEventArgs, QueryEvents } from '../../events/QueryEvents';
import { StandaloneSearchInterfaceEvents } from '../../events/StandaloneSearchInterfaceEvents';
import { exportGlobally } from '../../GlobalExports';
import { Grammar } from '../../magicbox/Grammar';
import { createMagicBox, MagicBoxInstance } from '../../magicbox/MagicBox';
import { Result } from '../../magicbox/Result/Result';
import { Assert } from '../../misc/Assert';
import { IAttributeChangedEventArg, MODEL_EVENTS } from '../../models/Model';
import { QueryStateModel, QUERY_STATE_ATTRIBUTES } from '../../models/QueryStateModel';
import { l } from '../../strings/Strings';
import { $$ } from '../../utils/Dom';
import { analyticsActionCauseList, IAnalyticsNoMeta } from '../Analytics/AnalyticsActionListMeta';
import { Component } from '../Base/Component';
import { IComponentBindings } from '../Base/ComponentBindings';
import { ComponentOptions } from '../Base/ComponentOptions';
import { Initialization } from '../Base/Initialization';
import { QueryboxOptionsProcessing } from './QueryboxOptionsProcessing';
import { QueryboxQueryParameters } from './QueryboxQueryParameters';
export interface IQueryboxOptions {
enableSearchAsYouType?: boolean;
searchAsYouTypeDelay?: number;
enableQuerySyntax?: boolean;
enableWildcards?: boolean;
enableQuestionMarks?: boolean;
enableLowercaseOperators?: boolean;
enablePartialMatch?: boolean;
partialMatchKeywords?: number;
partialMatchThreshold?: string;
placeholder?: string;
triggerQueryOnClear?: boolean;
}
/**
* The `Querybox` component renders an input which the end user can interact with to enter and submit queries.
*
* When the end user submits a search request, the `Querybox` component triggers a query and logs the corresponding
* usage analytics data.
*
* For technical reasons, it is necessary to instantiate this component on a `div` element rather than on an `input`
* element.
*
* See also the [`Searchbox`]{@link Searchbox} component, which can automatically instantiate a `Querybox` along with an
* optional [`SearchButton`]{@link SearchButton} component.
*/
export class Querybox extends Component {
static ID = 'Querybox';
static doExport = () => {
exportGlobally({
Querybox: Querybox,
QueryboxQueryParameters: QueryboxQueryParameters
});
};
/**
* The options for the Querybox.
* @componentOptions
*/
public static options: IQueryboxOptions = {
/**
* Whether to enable the search-as-you-type feature.
*
* **Note:** Enabling this feature can consume lots of queries per month (QPM), especially if the [`searchAsYouTypeDelay`]{@link Querybox.options.searchAsYouTypeDelay} option is set to a low value.
*
* Default value is `false`.
*/
enableSearchAsYouType: ComponentOptions.buildBooleanOption({ defaultValue: false, section: 'Advanced Options' }),
/**
* If the [`enableSearchAsYouType`]{@link Querybox.options.enableSearchAsYouType} option is `true`, specifies how
* long to wait (in milliseconds) between each key press before triggering a new query.
*
* Default value is `50`. Minimum value is `0`
*/
searchAsYouTypeDelay: ComponentOptions.buildNumberOption({ defaultValue: 50, min: 0, section: 'Advanced Options' }),
/**
* Specifies whether to interpret special query syntax (e.g., `@objecttype=message`) when the end user types
* a query in the `Querybox` (see
* [Coveo Query Syntax Reference](https://docs.coveo.com/en/1552/searching-with-coveo/coveo-cloud-query-syntax)). Setting this
* option to `true` also causes the `Querybox` to highlight any query syntax.
*
* Regardless of the value of this option, the Coveo Cloud REST Search API always interprets expressions surrounded
* by double quotes (`"`) as exact phrase match requests.
*
* See also [`enableLowercaseOperators`]{@link Querybox.options.enableLowercaseOperators}.
*
* **Notes:**
* > * End user preferences can override the value you specify for this option.
* >
* > If the end user selects a value other than **Automatic** for the **Enable query syntax** setting (see
* > the [`enableQuerySyntax`]{@link ResultsPreferences.options.enableQuerySyntax} option of the
* > [`ResultsPreferences`]{@link ResultsPreferences} component), the end user preference takes precedence over this
* > option.
* >
* > * On-premises versions of the Coveo Search API require this option to be set to `true` in order to interpret
* > expressions surrounded by double quotes (`"`) as exact phrase match requests.
*
* Default value is `false`.
*/
enableQuerySyntax: ComponentOptions.buildBooleanOption({ defaultValue: false, section: 'Advanced Options' }),
/**
* Specifies whether to expand basic expression keywords containing wildcards characters (`*`) to the possible
* matching keywords in order to broaden the query (see
* [Using Wildcards in Queries](https://docs.coveo.com/en/1580/)).
*
* See also [`enableQuestionMarks`]{@link Querybox.options.enableQuestionMarks}.
*
* **Note:**
* > If you are using an on-premises version of the Coveo Search API, you need to set the
* > [`enableQuerySyntax`]{@link Querybox.options.enableQuerySyntax} option to `true` to be able to set
* > `enableWildcards` to `true`.
*
* Default value is `false`.
*/
enableWildcards: ComponentOptions.buildBooleanOption({ defaultValue: false, section: 'Advanced Options' }),
/**
* If [`enableWildcards`]{@link Querybox.options.enableWildcards} is `true`, specifies whether to expand basic
* expression keywords containing question mark characters (`?`) to the possible matching keywords in order to
* broaden the query (see
* [Using Wildcards in Queries](https://docs.coveo.com/en/1580/)).
*
* **Note:**
* > If you are using an on-premises version of the Coveo Search API, you also need to set the
* > [`enableQuerySyntax`]{@link Querybox.options.enableQuerySyntax} option to `true` in order to be able to set
* > `enableQuestionMarks` to `true`.
*
* Default value is `false`.
*/
enableQuestionMarks: ComponentOptions.buildBooleanOption({ defaultValue: false, depend: 'enableWildcards' }),
/**
* If the [`enableQuerySyntax`]{@link Querybox.options.enableQuerySyntax} option is `true`, specifies whether to
* interpret the `AND`, `NOT`, `OR`, and `NEAR` keywords in the `Querybox` as query operators in the query, even if
* the end user types those keywords in lowercase.
*
* This option applies to all query operators (see
* [Coveo Query Syntax Reference](https://docs.coveo.com/en/1552/searching-with-coveo/coveo-cloud-query-syntax)).
*
* **Example:**
* > If this option and the `enableQuerySyntax` option are both `true`, the Coveo Platform interprets the `near`
* > keyword in a query such as `service center near me` as the `NEAR` query operator (not as a query term).
*
* > Otherwise, if the `enableQuerySyntax` option is `true` and this option is `false`, the end user has to type the
* > `NEAR` keyword in uppercase for the Coveo Platform to interpret it as a query operator.
*
* Default value is `false`.
*/
enableLowercaseOperators: ComponentOptions.buildBooleanOption({ defaultValue: false, depend: 'enableQuerySyntax' }),
/**
* Whether to convert a basic expression containing at least a certain number of keywords (see the
* [`partialMatchKeywords`]{@link Querybox.options.partialMatchKeywords} option) to *partial match expression*, so
* that items containing at least a certain number of those keywords (see the
* [`partialMatchThreshold`]{@link Querybox.options.partialMatchThreshold} option) will match the expression.
*
* **Notes:**
* - Only the basic expression of the query (see [`q`]{@link q}) can be converted to a partial match expression.
* - When the [`enableQuerySyntax`]{@link Querybox.options.enableQuerySyntax} option is set to `true`, this feature has no effect if the basic expression contains advanced query syntax (field expressions, operators, etc.).
*
* @notSupportedIn salesforcefree
*/
enablePartialMatch: ComponentOptions.buildBooleanOption({ defaultValue: false }),
/**
* The minimum number of keywords that need to be present in the basic expression to convert it to a partial match expression.
*
* See also the [`partialMatchThreshold`]{@link Querybox.options.partialMatchThreshold} option.
*
* **Notes:**
*
* - Repeated keywords count as a single keyword.
* - Thesaurus expansions count towards the `partialMatchKeywords` count.
* - Stemming expansions **do not** count towards the `partialMatchKeywords` count.
*
* @notSupportedIn salesforcefree
*/
partialMatchKeywords: ComponentOptions.buildNumberOption({ defaultValue: 5, min: 1, depend: 'enablePartialMatch' }),
/**
* An absolute or relative value indicating the minimum number of partial match expression keywords an item must contain to match the expression.
*
* See also the [`partialMatchKeywords`]{@link Querybox.options.partialMatchKeywords} option.
*
* **Notes:**
* - A keyword and its stemming expansions count as a single keyword when evaluating whether an item meets the `partialMatchThreshold`.
* - When a relative `partialMatchThreshold` does not yield a whole integer, the fractional part is truncated (e.g., `3.6` becomes `3`).
*
* @notSupportedIn salesforcefree
*/
partialMatchThreshold: ComponentOptions.buildStringOption({ defaultValue: '50%', depend: 'enablePartialMatch' }),
/**
* Whether to trigger a query when clearing the `Querybox`.
*
* Default value is `false`.
*/
triggerQueryOnClear: ComponentOptions.buildBooleanOption({ defaultValue: false })
};
MagicBoxImpl;
public magicBox: MagicBoxInstance;
private lastQuery: string;
private searchAsYouTypeTimeout: number;
/**
* Creates a new `Querybox component`. Creates a new `Coveo.Magicbox` instance and wraps the Magicbox methods
* (`onblur`, `onsubmit` etc.). Binds event on `buildingQuery` and before redirection (for standalone box).
* @param element The HTMLElement on which to instantiate the component. This cannot be an HTMLInputElement for
* technical reasons.
* @param options The options for the `Querybox` 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?: IQueryboxOptions, public bindings?: IComponentBindings) {
super(element, Querybox.ID, bindings);
if (element instanceof HTMLInputElement) {
this.logger.error('Querybox cannot be used on an HTMLInputElement');
}
this.options = ComponentOptions.initComponentOptions(element, Querybox, options);
new QueryboxOptionsProcessing(this).postProcess();
$$(this.element).toggleClass('coveo-query-syntax-disabled', this.options.enableQuerySyntax == false);
this.magicBox = createMagicBox(
element,
new Grammar('Query', {
Query: '[Term*][Spaces?]',
Term: '[Spaces?][Word]',
Spaces: / +/,
Word: /[^ ]+/
}),
{
inline: true
}
);
const input = $$(this.magicBox.element).find('input');
if (input) {
$$(input).setAttribute('aria-label', this.options.placeholder || l('Search'));
}
this.bind.onRootElement(QueryEvents.buildingQuery, (args: IBuildingQueryEventArgs) => this.handleBuildingQuery(args));
this.bind.onRootElement(StandaloneSearchInterfaceEvents.beforeRedirect, () => this.updateQueryState());
this.bind.onQueryState(MODEL_EVENTS.CHANGE_ONE, QUERY_STATE_ATTRIBUTES.Q, (args: IAttributeChangedEventArg) =>
this.handleQueryStateChanged(args)
);
if (this.options.enableSearchAsYouType) {
$$(this.element).addClass('coveo-search-as-you-type');
this.magicBox.onchange = () => {
this.searchAsYouType();
};
}
this.magicBox.onsubmit = () => {
this.submit();
};
this.magicBox.onblur = () => {
this.updateQueryState();
};
this.magicBox.onclear = () => {
this.updateQueryState();
if (this.options.triggerQueryOnClear) {
this.usageAnalytics.logSearchEvent<IAnalyticsNoMeta>(analyticsActionCauseList.searchboxClear, {});
this.triggerNewQuery(false);
}
};
}
/**
* Adds the current content of the input to the query and triggers a query if the current content of the input has
* changed since last submit.
*
* Also logs the `serachboxSubmit` event in the usage analytics.
*/
public submit(): void {
this.magicBox.clearSuggestion();
this.updateQueryState();
this.usageAnalytics.logSearchEvent<IAnalyticsNoMeta>(analyticsActionCauseList.searchboxSubmit, {});
this.triggerNewQuery(false);
}
/**
* Sets the content of the input.
*
* @param text The string to set in the input.
*/
public setText(text: string): void {
this.magicBox.setText(text);
this.updateQueryState();
}
/**
* Clears the content of the input.
*
* See also the [`triggerQueryOnClear`]{@link Querybox.options.triggerQueryOnClear} option.
*/
public clear(): void {
this.magicBox.clear();
}
/**
* Gets the content of the input.
*
* @returns {string} The content of the input.
*/
public getText(): string {
return this.magicBox.getText();
}
/**
* Gets the result from the input.
*
* @returns {Result} The result.
*/
public getResult() {
return this.magicBox.getResult();
}
/**
* Gets the displayed result from the input.
*
* @returns {Result} The displayed result.
*/
public getDisplayedResult(): Result {
return this.magicBox.getDisplayedResult();
}
/**
* Gets the current cursor position in the input.
*
* @returns {number} The cursor position (index starts at 0).
*/
public getCursor(): number {
return this.magicBox.getCursor();
}
/**
* Gets the result at cursor position.
*
* @param match {string | { (result): boolean }} The match condition.
*
* @returns {Result[]} The result.
*/
public resultAtCursor(match?: string | { (result): boolean }) {
return this.magicBox.resultAtCursor(match);
}
private handleBuildingQuery(args: IBuildingQueryEventArgs): void {
Assert.exists(args);
Assert.exists(args.queryBuilder);
this.updateQueryState();
this.lastQuery = this.magicBox.getText();
new QueryboxQueryParameters(this.options).addParameters(args.queryBuilder, this.lastQuery);
}
private triggerNewQuery(searchAsYouType: boolean): void {
clearTimeout(this.searchAsYouTypeTimeout);
let text = this.magicBox.getText();
if (this.lastQuery != text && text != null) {
this.lastQuery = text;
this.queryController.executeQuery({
searchAsYouType: searchAsYouType,
logInActionsHistory: true
});
}
}
private updateQueryState(): void {
this.queryStateModel.set(QueryStateModel.attributesEnum.q, this.magicBox.getText());
}
private handleQueryStateChanged(args: IAttributeChangedEventArg): void {
Assert.exists(args);
let q = <string>args.value;
if (q != this.magicBox.getText()) {
this.magicBox.setText(q);
}
}
private searchAsYouType(): void {
clearTimeout(this.searchAsYouTypeTimeout);
this.searchAsYouTypeTimeout = window.setTimeout(() => {
this.usageAnalytics.logSearchAsYouType<IAnalyticsNoMeta>(analyticsActionCauseList.searchboxAsYouType, {});
this.triggerNewQuery(true);
}, this.options.searchAsYouTypeDelay);
}
}
Initialization.registerAutoCreateComponent(Querybox);