coveo-search-ui
Version:
Coveo JavaScript Search Framework
193 lines (169 loc) • 7.66 kB
text/typescript
import 'styling/_Breadcrumb';
import { each, find, isEmpty } from 'underscore';
import { BreadcrumbEvents, IBreadcrumbItem, IClearBreadcrumbEventArgs, IPopulateBreadcrumbEventArgs } from '../../events/BreadcrumbEvents';
import { InitializationEvents } from '../../events/InitializationEvents';
import { QueryEvents } from '../../events/QueryEvents';
import { exportGlobally } from '../../GlobalExports';
import { l } from '../../strings/Strings';
import { AccessibleButton } from '../../utils/AccessibleButton';
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';
export interface IBreadcrumbOptions {
headingLevel?: number;
}
/**
* The Breadcrumb component displays a summary of the currently active query filters.
*
* For example, when the user selects a {@link Facet} value, the breadcrumbs display this value.
*
* The active filters are obtained by the component by firing an event in the Breadcrumb component.
*
* All other components having an active state can react to this event by providing custom bits of HTML to display
* inside the breadcrumbs.
*
* Thus, it is possible to easily extend the Breadcrumb component using custom code to display information about custom
* states and filters.
*
* See {@link BreadcrumbEvents} for the list of events and parameters sent when a Breadcrumb component is populated.
*/
export class Breadcrumb extends Component {
static ID = 'Breadcrumb';
/**
* The options for the Breadcrumb.
* @componentOptions
*/
static options: IBreadcrumbOptions = {
/**
* The [heading level](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/Heading_Elements) to use for the heading before the breadcrumbs.
*
* A value of 0 will render a [`div`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/div) element instead.
*/
headingLevel: ComponentOptions.buildNumberOption({ min: 0, max: 6 })
};
static doExport = () => {
exportGlobally({
Breadcrumb: Breadcrumb
});
};
private lastBreadcrumbs: IBreadcrumbItem[];
/**
* Creates a new Breadcrumb component. Binds event on {@link QueryEvents.deferredQuerySuccess} to draw the
* breadcrumbs.
* @param element The HTMLElement on which to instantiate the component.
* @param options The options for the Breadcrumb 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?: IBreadcrumbOptions, bindings?: IComponentBindings) {
super(element, Breadcrumb.ID, bindings);
this.options = ComponentOptions.initComponentOptions(element, Breadcrumb, options);
this.bind.oneRootElement(InitializationEvents.afterInitialization, () => this.handleAfterInitialization());
this.bind.onRootElement(BreadcrumbEvents.redrawBreadcrumb, () => this.redrawBreadcrumb());
this.element.style.display = 'none';
this.element.setAttribute('tabindex', '-1');
this.addDefaultAccessibilityAttributes();
}
/**
* Triggers the event to populate the breadcrumbs. Components such as {@link Facet} can populate the breadcrumbs.
*
* This method triggers a {@link BreadcrumbEvents.populateBreadcrumb} event with an
* {@link IPopulateBreadcrumbEventArgs} object (an array) that other components or code can populate.
* @returns {IBreadcrumbItem[]} A populated breadcrumb item list.
*/
public getBreadcrumbs(): IBreadcrumbItem[] {
const args = <IPopulateBreadcrumbEventArgs>{ breadcrumbs: [], headingLevel: this.options.headingLevel };
this.bind.trigger(this.root, BreadcrumbEvents.populateBreadcrumb, args);
this.logger.debug('Retrieved breadcrumbs', args.breadcrumbs);
// If newly received breadcrumbs are empty, and last breadcrumbs were not.
// this means end user removed the last filter:
// We want to shift keyboard focus to the result list container in that case, for ease of use of the interface
// https://coveord.atlassian.net/browse/JSUI-3078
if (isEmpty(args.breadcrumbs) && !isEmpty(this.lastBreadcrumbs)) {
this.focusFirstEnabledResultList();
}
this.lastBreadcrumbs = args.breadcrumbs;
return args.breadcrumbs;
}
/**
* Triggers the event to clear the current breadcrumbs that components such as {@link Facet} can populate.
*
* Also triggers a new query and logs the `breadcrumbResetAll` event in the usage analytics.
*/
public clearBreadcrumbs() {
const args = <IClearBreadcrumbEventArgs>{};
this.bind.trigger(this.root, BreadcrumbEvents.clearBreadcrumb, args);
this.logger.debug('Clearing breadcrumbs');
this.usageAnalytics.logSearchEvent<IAnalyticsNoMeta>(analyticsActionCauseList.breadcrumbResetAll, {});
this.queryController.executeQuery();
}
/**
* Draws the specified breadcrumb items.
* @param breadcrumbs The breadcrumb items to draw.
*/
public drawBreadcrumb(breadcrumbs: IBreadcrumbItem[]) {
$$(this.element).empty();
if (breadcrumbs.length != 0) {
this.element.style.display = '';
} else {
this.element.style.display = 'none';
}
const breadcrumbItems = document.createElement('div');
$$(breadcrumbItems).addClass('coveo-breadcrumb-items');
this.element.appendChild(breadcrumbItems);
each(breadcrumbs, (bcrumb: IBreadcrumbItem) => {
const elem = bcrumb.element;
$$(elem).addClass('coveo-breadcrumb-item');
breadcrumbItems.appendChild(elem);
});
const clearText = $$('div', undefined, l('ClearAllFilters')).el;
const clear = $$(
'div',
{
className: 'coveo-breadcrumb-clear-all'
},
clearText
).el;
new AccessibleButton()
.withElement(clear)
.withSelectAction(() => this.clearBreadcrumbs())
.withOwner(this.bind)
.withoutLabelOrTitle()
.build();
this.element.appendChild(clear);
}
private redrawBreadcrumb() {
this.lastBreadcrumbs ? this.drawBreadcrumb(this.lastBreadcrumbs) : this.drawBreadcrumb(this.getBreadcrumbs());
}
private handleDeferredQuerySuccess() {
this.drawBreadcrumb(this.getBreadcrumbs());
}
private handleQueryError() {
this.drawBreadcrumb(this.getBreadcrumbs());
}
private handleAfterInitialization() {
// We must bind to these events after the initialization to make sure the breadcrumb generation
// is made with updated components. (E.G facet, facetrange, ...)
this.bind.onRootElement(QueryEvents.deferredQuerySuccess, () => this.handleDeferredQuerySuccess());
this.bind.onRootElement(QueryEvents.queryError, () => this.handleQueryError());
}
private focusFirstEnabledResultList() {
const resultLists = this.searchInterface.getComponents<Component>('ResultList');
const firstEnabledResultList = find(resultLists, resultList => resultList.disabled === false);
if (firstEnabledResultList) {
$$(firstEnabledResultList.element).focus(true);
}
}
private addDefaultAccessibilityAttributes() {
if (!this.element.getAttribute('role')) {
this.element.setAttribute('role', 'navigation');
}
if (!this.element.getAttribute('aria-label')) {
this.element.setAttribute('aria-label', l('Breadcrumb'));
}
}
}
Initialization.registerAutoCreateComponent(Breadcrumb);