UNPKG

coveo-search-ui

Version:

Coveo JavaScript Search Framework

306 lines (265 loc) • 10.7 kB
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);