coveo-search-ui
Version:
Coveo JavaScript Search Framework
621 lines (569 loc) • 26.3 kB
text/typescript
import { Initialization, LazyInitialization, EagerInitialization } from './Initialization';
import { Assert } from '../../misc/Assert';
import { QueryController } from '../../controllers/QueryController';
import { QueryStateModel, setState } from '../../models/QueryStateModel';
import { IQueryResult } from '../../rest/QueryResult';
import { IQueryResults } from '../../rest/QueryResults';
import { IAnalyticsClient } from '../Analytics/AnalyticsClient';
import { InitializationEvents } from '../../events/InitializationEvents';
import { $$ } from '../../utils/Dom';
import { IAnalyticsActionCause } from '../Analytics/AnalyticsActionListMeta';
import { IStringMap } from '../../rest/GenericParam';
import { BaseComponent } from '../Base/BaseComponent';
import { Component } from '../Base/Component';
import { IStandaloneSearchInterfaceOptions, SearchInterface } from '../SearchInterface/SearchInterface';
import { IRecommendationOptions } from '../Recommendation/Recommendation';
import * as _ from 'underscore';
import { PublicPathUtils } from '../../utils/PublicPathUtils';
import { Logger } from '../../misc/Logger';
import { Analytics } from '../Analytics/Analytics';
import { AnalyticsUtils } from '../../utils/AnalyticsUtils';
let registeredNamedMethodsLogger = new Logger('RegisteredNamedMethods');
/**
* Initialize the framework with a basic search interface. Calls {@link Initialization.initSearchInterface}.
*
* If using the jQuery extension, this is called using <code>$('#root').coveo('init');</code>.
* @param element The root of the interface to initialize.
* @param options JSON options for the framework (e.g.: <code>{Searchbox : {enableSearchAsYouType : true}}</code>).
* @returns {Promise<{elem: HTMLElement}>}
*/
export function init(element: HTMLElement, options: any = {}) {
return Initialization.initializeFramework(element, options, () => {
return Initialization.initSearchInterface(element, options);
});
}
Initialization.registerNamedMethod('init', (element: HTMLElement, options: any = {}) => {
return init(element, options);
});
/**
* Initialize the framework with a standalone search box. Calls {@link Initialize.initStandaloneSearchInterface}.
*
* If using the jQuery extension, this is called using <code>$('#root').coveo('initSearchbox');</code>.
* @param element The root of the interface to initialize.
* @param searchPageUri The search page on which to redirect when there is a query.
* @param options JSON options for the framework (e.g.: <code>{Searchbox : {enableSearchAsYouType : true}}</code>).
* @returns {Promise<{elem: HTMLElement}>}
*/
export function initSearchbox(element: HTMLElement, searchPageUri: string, options: any = {}) {
Assert.isNonEmptyString(searchPageUri);
var searchInterfaceOptions = <IStandaloneSearchInterfaceOptions>{};
searchInterfaceOptions.searchPageUri = searchPageUri;
searchInterfaceOptions.autoTriggerQuery = false;
searchInterfaceOptions.enableHistory = false;
searchInterfaceOptions = { ...searchInterfaceOptions, ...options.StandaloneSearchInterface };
options = _.extend({}, options, { StandaloneSearchInterface: searchInterfaceOptions });
return Initialization.initializeFramework(element, options, () => {
return Initialization.initStandaloneSearchInterface(element, options);
});
}
Initialization.registerNamedMethod('initSearchbox', (element: HTMLElement, searchPageUri: string, options: any = {}) => {
initSearchbox(element, searchPageUri, options);
});
/**
* Initialize the framework with a recommendation interface. Calls {@link Initialization.initRecommendationInterface}.
*
* If using the jQuery extension, this is called using <code>$('#root').coveo('initRecommendation');</code>.
* @param element The root of the interface to initialize.
* @param mainSearchInterface The search interface to link with the recommendation interface (see {@link Recommendation}).
* @param userContext The user context to pass with the query generated in the recommendation interface (see {@link Recommendation}).
* @param options JSON options for the framework (e.g.: <code>{Searchbox : {enableSearchAsYouType: true}}</code>).
* @returns {Promise<{elem: HTMLElement}>}
*/
export function initRecommendation(
element: HTMLElement,
mainSearchInterface?: HTMLElement,
userContext?: { [name: string]: any },
options: any = {}
) {
var recommendationOptions = <IRecommendationOptions>{};
recommendationOptions.mainSearchInterface = mainSearchInterface;
recommendationOptions.userContext = userContext;
recommendationOptions.enableHistory = false;
options = _.extend({}, options, { Recommendation: recommendationOptions });
// Recommendation component is special : It is not explicitely registered like other "basic" components since it's a full search interface.
// Since it's not done using the "standard" path, we need to register this manually here
// This ensure that we can always call `getLazyRegisteredComponent`, no matter if it was loaded from eager or lazy mode.
if (window['Coveo']['Recommendation'] != null) {
LazyInitialization.registerLazyComponent('Recommendation', () => Promise.resolve(window['Coveo']['Recommendation']));
EagerInitialization.eagerlyLoadedComponents['Recommendation'] = window['Coveo']['Recommendation'];
}
return LazyInitialization.getLazyRegisteredComponent('Recommendation').then(() => {
return Initialization.initializeFramework(element, options, () => {
return Initialization.initRecommendationInterface(element, options);
});
});
}
Initialization.registerNamedMethod(
'initRecommendation',
(element: HTMLElement, mainSearchInterface: HTMLElement, userContext: any = {}, options: any = {}) => {
initRecommendation(element, mainSearchInterface, userContext, options);
}
);
/**
* Execute a standard query. Active component in the interface will react to events/ push data in the query / handle the query success or failure as needed.
*
* It triggers a standard query flow for which the standard component will perform their expected behavior.
*
* If you wish to only perform a query on the index to retrieve results (without the component reacting), look into {@link SearchInterface} instead.
*
* Calling this method is the same as calling {@link QueryController.executeQuery}.
*
* @param element The root of the interface to initialize.
* @returns {Promise<IQueryResults>}
*/
export function executeQuery(element: HTMLElement): Promise<IQueryResults> {
Assert.exists(element);
var queryController = <QueryController>Component.resolveBinding(element, QueryController);
Assert.exists(queryController);
return queryController.executeQuery();
}
Initialization.registerNamedMethod('executeQuery', (element: HTMLElement) => {
return executeQuery(element);
});
/**
* Performs read and write operations on the [`QueryStateModel`]{@link QueryStateModel} instance of the search
* interface. See [State](https://docs.coveo.com/en/344/).
*
* Can perform the following actions:
*
* - Get the `QueryStateModel` instance:
* ```javascript
* Coveo.state(element);
* ```
*
* - Get the value of a single state attribute from the `QueryStateModel` instance:
* ```javascript
* // You can replace `q` with any registered state attribute.
* Coveo.state(element, "q");
* ```
*
* - Set the value of a single state attribute in the `QueryStateModel` instance:
* ```javascript
* // You can replace `q` with any registered state attribute.
* Coveo.state(element, "q", "my new value");
* ```
*
* - Set multiple state attribute values in the `QueryStateModel` instance:
* ```javascript
* // You can replace `q` and `sort` with any registered state attributes.
* Coveo.state(element, {
* "q" : "my new value",
* "sort" : "relevancy"
* });
* ```
*
* **Note:**
* > When setting one or several state attribute values with this function, you can pass an additional argument to set
* > the `silent` attribute to `true` in order to prevent the state change from triggering state change events.
* >
* > **Example:**
* > ```javascript
* > Coveo.state(element, "q", "my new value", { "silent" : true });
* > ```
*
* @param element The root of the interface whose `QueryStateModel` instance the function should interact with.
* @param args The arguments that determine the action to perform on the `QueryStateModel` instance.
* @returns {any} Depends on the action performed.
*/
export function state(element: HTMLElement, ...args: any[]): any {
Assert.exists(element);
var model = <QueryStateModel>Component.resolveBinding(element, QueryStateModel);
return setState(model, args);
}
Initialization.registerNamedMethod('state', (element: HTMLElement, ...args: any[]): any => {
if (args.length != 0) {
return state.apply(undefined, [element].concat(args));
} else {
return state.apply(undefined, [element]);
}
});
/**
* Get the component bound on the given `HTMLElement`.
* @param element The `HTMLElement` for which to get the component instance.
* @param componentClass If multiple components are bound to a single `HTMLElement`, you need to specify which components you wish to get.
* @param noThrow By default, the GET method will throw if there is no component bound, or if there are multiple component and no `componentClass` is specified. This deletes the error if set to true.
* @returns {Component}
*/
export function get(element: HTMLElement, componentClass?, noThrow?: boolean): BaseComponent {
Assert.exists(element);
return Component.get(element, componentClass, noThrow);
}
Initialization.registerNamedMethod(
'get',
(element: HTMLElement, componentClass?: any, noThrow?: boolean): BaseComponent => {
return get(element, componentClass, noThrow);
}
);
export function result(element: HTMLElement, noThrow?: boolean): IQueryResult {
Assert.exists(element);
return Component.getResult(element, noThrow);
}
Initialization.registerNamedMethod(
'result',
(element: HTMLElement, noThrow?: boolean): IQueryResult => {
return result(element, noThrow);
}
);
function getCoveoAnalyticsClient(element: HTMLElement): IAnalyticsClient {
var analytics = getCoveoAnalytics(element);
if (analytics) {
return analytics.client;
} else {
return undefined;
}
}
function getCoveoAnalytics(element: HTMLElement) {
var analyticsElement = element.classList.contains(Component.computeCssClassNameForType('Analytics'))
? element
: $$(element).find(Component.computeSelectorForType(`Analytics`));
if (analyticsElement) {
return <Analytics>Component.resolveBinding(analyticsElement, Analytics);
} else {
return undefined;
}
}
/**
* Finds the [`Analytics`]{@link Analytics} component instance, and uses it to log a `Custom` usage analytics event.
*
* You can use `Custom` events to create custom reports, or to track events which are neither queries nor item
* views.
*
* @param element The root of the search interface which contains the [`Analytics`]{@link Analytics} component.
* @param customEventCause The cause of the event.
* @param metadata The metadata you want to use to create custom dimensions. Metadata can contain as many key-value
* pairs as you need. Each key must contain only alphanumeric characters and underscores. The Coveo Usage Analytics
* service automatically converts white spaces to underscores, and uppercase characters to lowercase characters in key
* names. Each value must be a simple string. If you do not need to log metadata, you can simply pass an empty JSON
* ( `{}` ).
* @param result The query result that relates to the custom event, if applicable.
*/
export function logCustomEvent<TMeta extends IStringMap<any>>(
element: HTMLElement,
customEventCause: IAnalyticsActionCause,
metadata: TMeta,
result?: IQueryResult
) {
var client = getCoveoAnalyticsClient(element);
if (client) {
client.logCustomEvent<TMeta>(customEventCause, metadata, element, result);
}
}
Initialization.registerNamedMethod(
'logCustomEvent',
(element: HTMLElement, customEventCause: IAnalyticsActionCause, metadata: any, result?: IQueryResult) => {
logCustomEvent(element, customEventCause, metadata, result);
}
);
/**
* Finds the [`Analytics`]{@link Analytics} component instance, and uses it to log a `Search` usage analytics event.
*
* A `Search` event is actually sent to the Coveo Usage Analytics service only after the query successfully returns (not
* immediately after calling this method). Therefore, it is important to call this method **before** executing the
* query. Otherwise, the `Search` event will not be logged, and you will get a warning message in the console. See
* [Logging Your Own Search Events](https://docs.coveo.com/en/2726/#logging-your-own-search-events).
*
* @param element The root of the search interface which contains the [`Analytics`]{@link Analytics} component.
* @param searchEventCause The cause of the event.
* @param metadata The metadata you want to use to create custom dimensions. Metadata can contain as many key-value
* pairs as you need. Each key must contain only alphanumeric characters and underscores. The Coveo Usage Analytics
* service automatically converts white spaces to underscores, and uppercase characters to lowercase characters in key
* names. Each value must be a simple string. If you do not need to log metadata, you can simply pass an empty JSON
* ( `{}` ).
*/
export function logSearchEvent(element: HTMLElement, searchEventCause: IAnalyticsActionCause, metadata: IStringMap<string>) {
var client = getCoveoAnalyticsClient(element);
if (client) {
client.logSearchEvent<any>(searchEventCause, metadata);
}
}
Initialization.registerNamedMethod(
'logSearchEvent',
(element: HTMLElement, searchEventCause: IAnalyticsActionCause, metadata: IStringMap<string>) => {
logSearchEvent(element, searchEventCause, metadata);
}
);
/**
* Finds the [`Analytics`]{@link Analytics} component instance, and uses it to log a `SearchAsYouType` usage analytics
* event.
*
* This function is very similar to the `logSearchEvent` function, except that `logSearchAsYouTypeEvent` should, by
* definition, be called more frequently. Consequently, in order to avoid logging every single partial query, the
* `PendingSearchAsYouTypeEvent` takes care of logging only the "relevant" last event: an event that occurs after 5
* seconds have elapsed without any event being logged, or an event that occurs after another part of the interface
* triggers a search event.
*
* It is important to call this method **before** executing the query. Otherwise, no `SearchAsYouType` event will be
* logged, and you will get a warning message in the console. See
* [Logging Your Own Search Events](https://docs.coveo.com/en/2726/#logging-your-own-search-events).
*
* @param element The root of the search interface which contains the [`Analytics`]{@link Analytics} component.
* @param searchAsYouTypeEventCause The cause of the event.
* @param metadata The metadata you want to use to create custom dimensions. Metadata can contain as many key-value
* pairs as you need. Each key must contain only alphanumeric characters and underscores. The Coveo Usage Analytics
* service automatically converts white spaces to underscores, and uppercase characters to lowercase characters in key
* names. Each value must be a simple string. If you do not need to log metadata, you can simply pass an empty JSON
* ( `{}` ).
*/
export function logSearchAsYouTypeEvent<TMeta extends IStringMap<any>>(
element: HTMLElement,
searchAsYouTypeEventCause: IAnalyticsActionCause,
metadata: TMeta
) {
var client = getCoveoAnalyticsClient(element);
if (client) {
client.logSearchAsYouType<TMeta>(searchAsYouTypeEventCause, metadata);
}
}
Initialization.registerNamedMethod(
'logSearchAsYouTypeEvent',
(element: HTMLElement, searchAsYouTypeEventCause: IAnalyticsActionCause, metadata: IStringMap<string>) => {
logSearchAsYouTypeEvent(element, searchAsYouTypeEventCause, metadata);
}
);
/**
* Finds the [`Analytics`]{@link Analytics} component instance, and uses it to log a `Click` usage analytics event.
*
* A `Click` event corresponds to an item view (e.g., clicking on a {@link ResultLink} or opening a {@link Quickview}).
*
* `Click` events are immediately sent to the Coveo Usage Analytics service.
*
* @param element The root of the search interface which contains the [`Analytics`]{@link Analytics} component.
* @param clickEventCause The cause of the event.
* @param metadata The metadata you want to use to create custom dimensions. Metadata can contain as many key-value
* pairs as you need. Each key must contain only alphanumeric characters and underscores. The Coveo Usage Analytics
* service automatically converts white spaces to underscores, and uppercase characters to lowercase characters in key
* names. Each value must be a simple string. If you do not need to log metadata, you can simply pass an empty JSON
* ( `{}` ).
* @param result The result that was clicked.
*/
export function logClickEvent<TMeta extends IStringMap<any>>(
element: HTMLElement,
clickEventCause: IAnalyticsActionCause,
metadata: TMeta,
result: IQueryResult
) {
var client = getCoveoAnalyticsClient(element);
if (client) {
client.logClickEvent<TMeta>(clickEventCause, metadata, result, element);
}
}
Initialization.registerNamedMethod(
'logClickEvent',
(element: HTMLElement, clickEventCause: IAnalyticsActionCause, metadata: IStringMap<string>, result: IQueryResult) => {
logClickEvent(element, clickEventCause, metadata, result);
}
);
/**
* Pass options to the framework, before it is initialized ({@link init}).<br/>
* All the options passed with this calls will be merged together on initialization.
* @param element The root of the interface for which you wish to set options.
* @param optionsToSet JSON options for the framework (e.g.: <code>{Searchbox : {enableSearchAsYouType: true}}</code>).
*/
export function options(element: HTMLElement, optionsToSet: any = {}) {
Initialization.registerDefaultOptions(element, optionsToSet);
}
Initialization.registerNamedMethod('options', (element: HTMLElement, optionsToSet: any = {}) => {
options(element, optionsToSet);
});
/**
* Patch the given `methodName` on an instance of a component bound to an `HTMLElement` with a new handler.
* @param element
* @param methodName
* @param handler
*/
export function patch(element: HTMLElement, methodName: string, handler: (...args: any[]) => any) {
Initialization.monkeyPatchComponentMethod(methodName, element, handler);
}
Initialization.registerNamedMethod('patch', (element?: HTMLElement, methodName?: string, handler?: (...args: any[]) => any) => {
patch(element, methodName, handler);
});
export function initBox(element: HTMLElement, ...args: any[]) {
var type,
options: any = {},
injectMarkup;
// This means : initBox, no type (no injection) and no options
if (args.length == 0) {
type = 'Standard';
injectMarkup = false;
} else if (args.length == 1) {
// 1 arg, might be options or type
// This mean a type (with injection) and no options
if (typeof args[0] == 'string') {
type = args[0];
injectMarkup = true;
} else if (typeof args[0] == 'object') {
// This means no type(no injection) and with options
type = 'Standard';
injectMarkup = false;
options = args[0];
} else {
Assert.fail('Invalid parameters to init a box');
}
} else if (args.length == 2) {
// 2 args means both options and type (with injection);
type = args[0];
options = args[1];
injectMarkup = true;
}
var merged: any = {};
merged[type || 'Container'] = _.extend({}, options.SearchInterface, options[type]);
options = _.extend({}, options, merged);
Initialization.initializeFramework(element, options, () => {
return Initialization.initBoxInterface(element, options, type, injectMarkup);
});
}
Initialization.registerNamedMethod('initBox', (element?: HTMLElement, ...args: any[]) => {
initBox(element, args);
});
export function nuke(element: HTMLElement) {
$$(element).trigger(InitializationEvents.nuke);
}
Initialization.registerNamedMethod('nuke', (element: HTMLElement) => {
nuke(element);
});
/**
* Sets the path from where the chunks used for lazy loading will be loaded. In some cases, in IE11, we cannot automatically detect it, use this instead.
* @param path This should be the path of the Coveo script. It should also have a trailing slash.
*/
export function configureResourceRoot(path: string) {
PublicPathUtils.configureResourceRoot(path);
}
Initialization.registerNamedMethod('configureResourceRoot', (path: string) => {
configureResourceRoot(path);
});
/**
* Re-enables an [`Analytics`]{@link Analytics} component if it was previously disabled.
* @param searchRoot
* The element to scan for an Analytics component.
* This can be an element onto which a component instance is bound
* (e.g., document.querySelector(".CoveoAnalytics"),
* or an ancestor of such an element (e.g., document.getElementById("search").
*
* @availablesince [October 2019 Release (v2.7219)](https://docs.coveo.com/en/3084/)
*/
export function enableAnalytics(searchRoot = document.querySelector(Component.computeSelectorForType(SearchInterface.ID)) as HTMLElement) {
const analytics = getCoveoAnalytics(searchRoot) as Analytics;
if (!analytics) {
return registeredNamedMethodsLogger.warn(
'Could not enable analytics because no Analytics component could be found in the specified searchRoot.'
);
}
analytics.enable();
}
Initialization.registerNamedMethod('enableAnalytics', () => {
enableAnalytics();
});
/**
* Removes all session information stored in the browser (e.g., analytics visitor cookies, action history, etc.)
* @param searchRoot
* The element to scan for an Analytics component.
* This can be an element onto which a component instance is bound
* (e.g., document.querySelector(".CoveoAnalytics"),
* or an ancestor of such an element (e.g., document.getElementById("search").
*/
export function clearLocalData(searchRoot = document.querySelector(Component.computeSelectorForType(SearchInterface.ID)) as HTMLElement) {
const analytics = getCoveoAnalytics(searchRoot) as Analytics;
if (!analytics) {
return registeredNamedMethodsLogger.warn(
'Could not clear local data because no Analytics component could be found in the specified searchRoot.'
);
}
analytics.clearLocalData();
}
Initialization.registerNamedMethod('clearLocalData', () => {
clearLocalData();
});
/**
* Disables an [`Analytics`]{@link Analytics} component and clears local data.
* @param searchRoot
* The element to scan for an Analytics component.
* This can be an element onto which a component instance is bound
* (e.g., document.querySelector(".CoveoAnalytics"),
* or an ancestor of such an element (e.g., document.getElementById("search").
*
* @availablesince [October 2019 Release (v2.7219)](https://docs.coveo.com/en/3084/)
*/
export function disableAnalytics(searchRoot = document.querySelector(Component.computeSelectorForType(SearchInterface.ID)) as HTMLElement) {
const analytics = getCoveoAnalytics(searchRoot) as Analytics;
if (!analytics) {
return registeredNamedMethodsLogger.warn(
'Could not disable analytics because no Analytics component could be found in the specified searchRoot.'
);
}
analytics.disable();
}
Initialization.registerNamedMethod('disableAnalytics', () => {
disableAnalytics();
});
/**
* Adds a new analytics action cause to the ActionCauseList.
* Adding a new actionCause allows to specify a custom user-action when triggering a search event.
* @param newActionCause
* (e.g.,
* {
* Name: "newActionCause",
* Type: "exampleType"
* },
*/
export function addActionCauseToList(newActionCause: IAnalyticsActionCause) {
AnalyticsUtils.addActionCauseToList(newActionCause);
}
Initialization.registerNamedMethod('addActionCauseToList', (newActionCause: IAnalyticsActionCause) => {
addActionCauseToList(newActionCause);
});
/**
* Removes an actionCause from the ActionCauseList.
* @param actionCauseToRemoveName
*/
export function removeActionCauseFromList(actionCauseToRemoveName: string) {
AnalyticsUtils.removeActionCauseFromList(actionCauseToRemoveName);
}
Initialization.registerNamedMethod('removeActionCauseFromList', (actionCauseToRemoveName: string) => {
removeActionCauseFromList(actionCauseToRemoveName);
});
/**
* Asynchronously loads a module, or chunk.
*
* This is especially useful when you want to extend a base component, and make sure the lazy component loading process
* recognizes it (see [Lazy Versus Eager Component Loading](https://docs.coveo.com/en/295/)).
*
* **Example:**
*
* ```typescript
* export function lazyCustomFacet() {
* return Coveo.load<Facet>('Facet').then((Facet) => {
* class CustomFacet extends Facet {
* [ ... ]
* };
* Coveo.Initialization.registerAutoCreateComponent(CustomFacet);
* return CustomFacet;
* });
* };
*
* Coveo.LazyInitialization.registerLazyComponent('CustomFacet', lazyCustomFacet);
* ```
*
* You can also use this function to assert a component is fully loaded in your page before executing any code relating
* to it.
*
* **Example:**
*
* > You could do `Coveo.load('Searchbox').then((Searchbox) => {})` to load the [`Searchbox`]{@link Searchbox}
* > component, if it is not already loaded in your search page.
*
* @param id The identifier of the module you wish to load. In the case of components, this identifier is the component
* name (e.g., `Facet`, `Searchbox`).
* @returns {Promise} A Promise of the module, or chunk.
*/
export function load<T>(id: string): Promise<T> {
if (LazyInitialization.lazyLoadedComponents[id] != null) {
return <Promise<T>>(<any>LazyInitialization.getLazyRegisteredComponent(id));
} else if (LazyInitialization.lazyLoadedModule[id] != null) {
return <Promise<T>>LazyInitialization.getLazyRegisteredModule(id);
} else {
return Promise.reject(`Module ${id} is not available`);
}
}
Initialization.registerNamedMethod('require', (modules: string) => {
return load(modules);
});