coveo-search-ui
Version:
Coveo JavaScript Search Framework
306 lines (265 loc) • 10.7 kB
text/typescript
import 'styling/_ResultsPerPage';
import * as _ from 'underscore';
import { InitializationEvents } from '../../events/InitializationEvents';
import { INoResultsEventArgs, IQuerySuccessEventArgs, QueryEvents } from '../../events/QueryEvents';
import { ResultListEvents } from '../../events/ResultListEvents';
import { exportGlobally } from '../../GlobalExports';
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 { AccessibleButton } from '../../utils/AccessibleButton';
import { DeviceUtils } from '../../utils/DeviceUtils';
import { $$ } from '../../utils/Dom';
import { ResultListUtils } from '../../utils/ResultListUtils';
import { analyticsActionCauseList, IAnalyticsActionCause, IAnalyticsResultsPerPageMeta } from '../Analytics/AnalyticsActionListMeta';
import { Component } from '../Base/Component';
import { IComponentBindings } from '../Base/ComponentBindings';
import { ComponentOptions } from '../Base/ComponentOptions';
import { Initialization } from '../Base/Initialization';
import { uniqueId } from 'underscore';
export interface IResultsPerPageOptions {
choicesDisplayed?: number[];
initialChoice?: number;
}
/**
* The ResultsPerPage component attaches itself to a `div` and allows the end user to choose how many results to
* display per page.
*
* **Note:** Adding a ResultPerPage component to your page overrides the value of
* {@link SearchInterface.options.resultsPerPage}.
*/
export class ResultsPerPage extends Component {
static ID = 'ResultsPerPage';
static doExport = () => {
exportGlobally({
ResultsPerPage: ResultsPerPage
});
};
/**
* The options for the ResultsPerPage
* @componentOptions
*/
static options: IResultsPerPageOptions = {
/**
* Specifies the possible values of number of results to display per page that the end user can select from.
*
* See also {@link ResultsPerPage.options.initialChoice}.
*
* Default value is `[10, 25, 50, 100]`.
*/
choicesDisplayed: ComponentOptions.buildCustomListOption<number[]>(
function (list: string[]) {
const values = _.map(list, function (value) {
return parseInt(value, 10);
});
return values.length == 0 ? null : values;
},
{
defaultFunction: () => {
if (DeviceUtils.isMobileDevice()) {
return [10, 25, 50];
} else {
return [10, 25, 50, 100];
}
}
}
),
/**
* Specifies the value to select by default for the number of results to display per page.
*
* Default value is the first value of {@link ResultsPerPage.options.choicesDisplayed}.
*/
initialChoice: ComponentOptions.buildNumberOption()
};
private currentResultsPerPage: number;
private span: HTMLElement;
private list: HTMLElement;
/**
* Creates a new ResultsPerPage. The component renders itself on every query success.
* @param element The HTMLElement on which to instantiate the component (normally a `div`).
* @param options The options for the ResultsPerPage 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?: IResultsPerPageOptions, bindings?: IComponentBindings) {
super(element, ResultsPerPage.ID, bindings);
this.options = ComponentOptions.initComponentOptions(element, ResultsPerPage, options);
this.bind.onRootElement(InitializationEvents.afterInitialization, () => this.resolveInitialState());
this.bind.onRootElement(QueryEvents.querySuccess, (args: IQuerySuccessEventArgs) => this.handleQuerySuccess(args));
this.bind.onRootElement(QueryEvents.queryError, () => this.handleQueryError());
this.bind.onRootElement(QueryEvents.noResults, (args: INoResultsEventArgs) => this.handleNoResults());
this.bind.onQueryState(MODEL_EVENTS.CHANGE_ONE, QUERY_STATE_ATTRIBUTES.NUMBER_OF_RESULTS, (args: IAttributeChangedEventArg) =>
this.handleQueryStateModelChanged(args)
);
this.addAlwaysActiveListeners();
this.initComponent();
}
/**
* Sets the current number of results per page, then executes a query.
*
* Also logs an event in the usage analytics (`pagerResize` by default) with the new current number of results per
* page as meta data.
* @param resultsPerPage The new number of results per page to select.
* @param analyticCause The event to log in the usage analytics.
*/
public setResultsPerPage(resultsPerPage: number, analyticCause: IAnalyticsActionCause = analyticsActionCauseList.pagerResize) {
Assert.exists(resultsPerPage);
Assert.check(this.isValidChoice(resultsPerPage), 'The specified number of results is not available in the options.');
this.updateResultsPerPage(resultsPerPage);
this.updateQueryStateModelResultsPerPage();
this.logAnalyticsEvent(analyticCause);
this.executeQuery();
}
/**
* Returns the current number of results per page.
*/
public get resultsPerPage() {
return this.currentResultsPerPage;
}
private updateResultsPerPage(resultsPerPage: number) {
this.queryController.options.resultsPerPage = resultsPerPage;
this.searchInterface.resultsPerPage = resultsPerPage;
this.currentResultsPerPage = resultsPerPage;
}
private updateQueryStateModelResultsPerPage() {
this.queryStateModel.set(QueryStateModel.attributesEnum.numberOfResults, this.currentResultsPerPage);
}
private logAnalyticsEvent(analyticCause: IAnalyticsActionCause) {
this.usageAnalytics.logCustomEvent<IAnalyticsResultsPerPageMeta>(
analyticCause,
{ currentResultsPerPage: this.currentResultsPerPage },
this.element
);
}
private executeQuery() {
this.queryController.executeQuery({
ignoreWarningSearchEvent: true,
keepLastSearchUid: true,
origin: this
});
}
private handleQueryStateModelChanged(args: IAttributeChangedEventArg) {
const valueToSet = args.value;
if (!this.isValidChoice(valueToSet)) {
this.logInvalidConfiguredChoiceWarning(valueToSet);
this.resolveInitialState();
} else {
this.updateResultsPerPage(valueToSet);
}
}
private addAlwaysActiveListeners() {
this.searchInterface.element.addEventListener(ResultListEvents.newResultsDisplayed, () =>
ResultListUtils.hideIfInfiniteScrollEnabled(this)
);
}
private resolveInitialState() {
this.updateResultsPerPage(this.getInitialChoice());
this.updateQueryStateModelResultsPerPage();
}
private getInitialChoice(): number {
const firstDisplayedChoice = this.options.choicesDisplayed[0];
const configuredChoice = this.options.initialChoice;
const queryStateModelChoice = this.queryStateModel.get(QueryStateModel.attributesEnum.numberOfResults);
const queryStateModelChoiceIsNotDefault = queryStateModelChoice !== QueryStateModel.defaultAttributes.numberOfResults;
if (queryStateModelChoiceIsNotDefault && this.isValidChoice(queryStateModelChoice)) {
return queryStateModelChoice;
}
if (configuredChoice !== undefined) {
if (this.isValidChoice(configuredChoice)) {
return configuredChoice;
}
this.logInvalidConfiguredChoiceWarning(configuredChoice);
}
return firstDisplayedChoice;
}
private isValidChoice(choice: number) {
return this.options.choicesDisplayed.indexOf(choice) !== -1;
}
private logInvalidConfiguredChoiceWarning(configuredChoice: number) {
const validChoices = this.options.choicesDisplayed;
this.logger.warn(
`The choice ${configuredChoice} is not within the choices displayed. Consider setting a value that is valid: ${validChoices}. The first choice will be selected instead.`
);
}
private initComponent() {
this.span = $$(
'span',
{
id: uniqueId('coveo-results-per-page-text-'),
className: 'coveo-results-per-page-text'
},
l('ResultsPerPage')
).el;
this.element.appendChild(this.span);
this.list = $$('ul', {
className: 'coveo-results-per-page-list',
'aria-labelledby': this.span.id
}).el;
this.element.appendChild(this.list);
}
private render() {
$$(this.span).removeClass('coveo-results-per-page-no-results');
const numResultsList: number[] = this.options.choicesDisplayed;
for (var i = 0; i < numResultsList.length; i++) {
const listItem = $$('li', {
className: 'coveo-results-per-page-list-item'
}).el;
const resultsPerPage = numResultsList[i];
const isActive = resultsPerPage === this.currentResultsPerPage;
if (isActive) {
$$(listItem).addClass('coveo-active');
}
const clickAction = e => {
e.stopPropagation();
this.handleClickPage(resultsPerPage);
};
const button = $$(
'span',
{
className: 'coveo-results-per-page-list-item-text',
tabindex: 0,
ariaPressed: isActive.toString()
},
numResultsList[i].toString()
).el;
listItem.appendChild(button);
listItem.addEventListener('click', e => clickAction(e));
new AccessibleButton()
.withElement(button)
.withLabel(l('DisplayResultsPerPage', numResultsList[i].toString()))
.withClickAction(clickAction)
.withEnterKeyboardAction(clickAction)
.build();
this.list.appendChild(listItem);
}
}
private handleQueryError() {
this.reset();
}
private handleNoResults() {
this.reset();
}
private handleQuerySuccess(data: IQuerySuccessEventArgs) {
if (this.searchInterface.isResultsPerPageModifiedByPipeline) {
this.logger.info('Results per page was modified by backend code (query pipeline). ResultsPerPage component will be hidden', this);
this.reset();
const resultsPerPage = this.getInitialChoice();
this.updateResultsPerPage(resultsPerPage);
return;
}
if (data.results.results.length != 0) {
this.reset();
this.render();
}
}
private handleClickPage(resultsPerPage: number) {
Assert.exists(resultsPerPage);
this.setResultsPerPage(resultsPerPage);
}
private reset() {
$$(this.span).addClass('coveo-results-per-page-no-results');
$$(this.list).empty();
}
}
Initialization.registerAutoCreateComponent(ResultsPerPage);