coveo-search-ui
Version:
Coveo JavaScript Search Framework
285 lines (255 loc) • 11.5 kB
text/typescript
import * as _ from 'underscore';
import { IPopulateOmniboxEventArgs, OmniboxEvents } from '../../events/OmniboxEvents';
import { QueryEvents } from '../../events/QueryEvents';
import { exportGlobally } from '../../GlobalExports';
import { Assert } from '../../misc/Assert';
import { QueryStateModel } from '../../models/QueryStateModel';
import { IStringMap } from '../../rest/GenericParam';
import { l } from '../../strings/Strings';
import { AccessibleButton } from '../../utils/AccessibleButton';
import { $$ } from '../../utils/Dom';
import { analyticsActionCauseList, IAnalyticsTopSuggestionMeta } from '../Analytics/AnalyticsActionListMeta';
import { Component } from '../Base/Component';
import { IComponentBindings } from '../Base/ComponentBindings';
import { ComponentOptions } from '../Base/ComponentOptions';
import { Initialization } from '../Base/Initialization';
import { ISuggestionForOmniboxOptions, ISuggestionForOmniboxTemplate, SuggestionForOmnibox } from '../Misc/SuggestionForOmnibox';
import { StandaloneSearchInterface } from '../SearchInterface/SearchInterface';
export interface IAnalyticsSuggestionsOptions extends ISuggestionForOmniboxOptions {}
/**
* The AnalyticsSuggestion component provides query suggestions based on the queries that a Coveo Analytics service most
* commonly logs (see [`topQueries`](https://platform.cloud.coveo.com/docs?urls.primaryName=Usage%20Analytics%20Read#/Statistics%20API%20-%20Version%2015/get__v15_stats_topQueries)).
*
* This component orders possible query suggestions by their respective number of successful item views, thus
* prioritizing the most relevant query suggestions. Consequently, when better options are available, this component
* does not suggest queries resulting in no clicks from users or requiring refinements.
*
* The query suggestions appear in the {@link Omnibox} Component. The AnalyticsSuggestion component strongly
* relates to the {@link Analytics} component. While a user is typing in a query box, the AnalyticsSuggestion component
* allows them to see and select the most commonly used and relevant queries.
*
* @deprecated This component is exposed for legacy reasons. If possible, you should avoid using this component.
* Instead, you should use the [`Omnibox`]{@link Omnibox}
* [`enableQuerySuggestAddon`]{@link Omnibox.options.enableQuerySuggestAddon} option.
*/
export class AnalyticsSuggestions extends Component {
static ID = 'AnalyticsSuggestions';
static doExport() {
exportGlobally({
AnalyticsSuggestions: AnalyticsSuggestions
});
}
/**
* The options for the component
* @componentOptions
*/
static options: IAnalyticsSuggestionsOptions = {
/**
* Specifies the z-index position at which the query suggestions render themselves in the {@link Omnibox}
* component. Higher values are placed first.
*
* Default value is `52` and minimum value is `0`.
*/
omniboxZIndex: ComponentOptions.buildNumberOption({ defaultValue: 52, min: 0 }),
/**
* Specifies the title of the query suggestions group in the {@link Omnibox} component. This option is not available
* when using the default Lightning Friendly Theme.
*
* Default value is the localized string for `"Suggested Queries"`.
*/
headerTitle: ComponentOptions.buildLocalizedStringOption({
localizedString: () => l('SuggestedQueries')
}),
/**
* Specifies the number of query suggestions to request and display in the {@link Omnibox} component.
*
* Default value is `5` and minimum value is `1`.
*/
numberOfSuggestions: ComponentOptions.buildNumberOption({ defaultValue: 5, min: 1 })
};
private suggestionForOmnibox: SuggestionForOmnibox;
private partialQueries: string[] = [];
private lastSuggestions: string[] = [];
private resultsToBuildWith = [];
private currentlyDisplayedSuggestions: { [suggestion: string]: { element: HTMLElement; pos: number } };
/**
* Creates a new AnalyticsSuggestions component.
*
* Also binds event handlers so that when a user selects a suggestion, an `omniboxFromLink` usage analytics event is
* logged if the suggestion comes from a standalone search box, or an `omniboxAnalytics` usage analytics
* event is logged otherwise.
*
* @param element The HTMLElement on which to instantiate the component.
* @param options The options for the AnalyticsSuggestions 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: IAnalyticsSuggestionsOptions, bindings?: IComponentBindings) {
super(element, AnalyticsSuggestions.ID, bindings);
if (this.options && 'omniboxSuggestionOptions' in this.options) {
this.options = _.extend(this.options, this.options['omniboxSuggestionOptions']);
}
this.options = ComponentOptions.initComponentOptions(element, AnalyticsSuggestions, this.options);
const rowTemplate = (toRender: IStringMap<any>) => {
const rowElement = $$('div', {
className: 'magic-box-suggestion coveo-omnibox-selectable coveo-top-analytics-suggestion-row'
});
new AccessibleButton()
.withElement(rowElement)
.withLabel(toRender.rawValue)
.build();
if (toRender['data']) {
rowElement.el.innerHTML = toRender['data'];
}
return rowElement.el.outerHTML;
};
this.options.onSelect = this.options.onSelect || this.onRowSelection;
const suggestionStructure: ISuggestionForOmniboxTemplate = {
row: rowTemplate
};
this.suggestionForOmnibox = new SuggestionForOmnibox(
suggestionStructure,
(value: string, args: IPopulateOmniboxEventArgs) => {
this.options.onSelect.call(this, value, args);
},
(value: string, args: IPopulateOmniboxEventArgs) => {
this.onRowTab.call(this, value, args);
}
);
this.bind.onRootElement(OmniboxEvents.populateOmnibox, (args: IPopulateOmniboxEventArgs) => this.handlePopulateOmnibox(args));
this.bind.onRootElement(QueryEvents.querySuccess, () => (this.partialQueries = []));
}
/**
* Selects a currently displayed query suggestion. This implies that at least one suggestion must have been returned
* at least once. The suggestion parameter can either be a number (0-based index position of the query suggestion to
* select) or a string that matches the suggestion.
*
* @param suggestion
*/
public selectSuggestion(suggestion: number);
public selectSuggestion(suggestion: string);
public selectSuggestion(suggestion: any) {
if (this.currentlyDisplayedSuggestions) {
if (isNaN(suggestion)) {
if (this.currentlyDisplayedSuggestions[suggestion]) {
$$(this.currentlyDisplayedSuggestions[suggestion].element).trigger('click');
}
} else {
const currentlySuggested = <{ element: HTMLElement; pos: number }>_.findWhere(<any>this.currentlyDisplayedSuggestions, {
pos: suggestion
});
if (currentlySuggested) {
$$(currentlySuggested.element).trigger('click');
}
}
}
}
private handlePopulateOmnibox(args: IPopulateOmniboxEventArgs) {
Assert.exists(args);
var promise = new Promise((resolve, reject) => {
const searchPromise = this.usageAnalytics.getTopQueries({
pageSize: this.options.numberOfSuggestions,
queryText: args.completeQueryExpression.word
});
searchPromise.then((results: string[]) => {
this.resultsToBuildWith = _.map(results, result => {
return {
value: result
};
});
this.lastSuggestions = results;
if (!_.isEmpty(this.resultsToBuildWith) && args.completeQueryExpression.word != '') {
this.partialQueries.push(args.completeQueryExpression.word);
}
const element = this.suggestionForOmnibox.buildOmniboxElement(this.resultsToBuildWith, 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
});
});
searchPromise.catch(() => {
resolve({
element: undefined
});
});
});
args.rows.push({ deferred: promise });
}
private onRowSelection(value: string, args: IPopulateOmniboxEventArgs) {
args.clear();
args.closeOmnibox();
this.queryStateModel.set(QueryStateModel.attributesEnum.q, value);
this.usageAnalytics.logSearchEvent<IAnalyticsTopSuggestionMeta>(this.getOmniboxAnalyticsEventCause(), {
partialQueries: this.cleanCustomData(this.partialQueries),
suggestionRanking: _.indexOf(_.pluck(this.resultsToBuildWith, 'value'), value),
suggestions: this.cleanCustomData(this.lastSuggestions),
partialQuery: args.completeQueryExpression.word
});
this.queryController.executeQuery();
}
private onRowTab(value: string, args: IPopulateOmniboxEventArgs) {
args.clear();
args.closeOmnibox();
this.queryStateModel.set(QueryStateModel.attributesEnum.q, `${value}`);
this.usageAnalytics.logCustomEvent<IAnalyticsTopSuggestionMeta>(
this.getOmniboxAnalyticsEventCause(),
{
partialQueries: this.cleanCustomData(this.partialQueries),
suggestionRanking: _.indexOf(_.pluck(this.resultsToBuildWith, 'value'), value),
suggestions: this.cleanCustomData(this.lastSuggestions),
partialQuery: args.completeQueryExpression.word
},
this.element
);
}
private cleanCustomData(toClean: string[], rejectLength = 256) {
// Filter out only consecutive values that are the identical
toClean = _.compact(
_.filter(toClean, (partial: string, pos?: number, array?: string[]) => {
return pos === 0 || partial !== array[pos - 1];
})
);
// Custom dimensions cannot be an array in analytics service: Send a string joined by ; instead.
// Need to replace ;
toClean = _.map(toClean, partial => {
return partial.replace(/;/g, '');
});
// Reduce right to get the last X words that adds to less then rejectLength
const reducedToRejectLengthOrLess = [];
_.reduceRight(
toClean,
(memo: number, partial: string) => {
const totalSoFar = memo + partial.length;
if (totalSoFar <= rejectLength) {
reducedToRejectLengthOrLess.push(partial);
}
return totalSoFar;
},
0
);
toClean = reducedToRejectLengthOrLess.reverse();
const ret = toClean.join(';');
// analytics service can store max 256 char in a custom event
// if we're over that, call cleanup again with an arbitrary 10 less char accepted
if (ret.length >= 256) {
return this.cleanCustomData(toClean, rejectLength - 10);
}
return toClean.join(';');
}
private getOmniboxAnalyticsEventCause() {
if (this.searchInterface instanceof StandaloneSearchInterface) {
return analyticsActionCauseList.omniboxFromLink;
}
return analyticsActionCauseList.omniboxAnalytics;
}
}
Initialization.registerAutoCreateComponent(AnalyticsSuggestions);