@spartacus/storefront
Version:
Spartacus Storefront is a package that you can include in your application, which allows you to add default storefront features.
1,097 lines (1,078 loc) • 1.25 MB
JavaScript
import * as i3 from '@angular/common';
import { isPlatformBrowser, CommonModule, DOCUMENT, isPlatformServer, formatCurrency, getCurrencySymbol } from '@angular/common';
import * as i0 from '@angular/core';
import { Injectable, Directive, Input, HostBinding, HostListener, EventEmitter, Output, PLATFORM_ID, Inject, APP_INITIALIZER, NgModule, isDevMode, Component, ApplicationRef, Injector, ComponentFactory, TemplateRef, InjectionToken, Optional, ComponentFactoryResolver, ViewChild, ChangeDetectionStrategy, ElementRef, Pipe, forwardRef, InjectFlags, ChangeDetectorRef, SecurityContext, ViewChildren, inject, APP_BOOTSTRAP_LISTENER } from '@angular/core';
import * as i1 from '@spartacus/core';
import { Config, provideDefaultConfig, ConfigModule, resolveApplicable, isNotNullable, DeferLoadingStrategy, ANONYMOUS_CONSENT_STATUS, I18nModule, FeaturesConfigModule, provideConfig, PromotionLocation, isNotUndefined, GlobalMessageType, AuthGuard, UrlModule, isObject, LANGUAGE_CONTEXT_ID, CURRENCY_CONTEXT_ID, SiteContextModule, ContextServiceMap, EMAIL_PATTERN, PASSWORD_PATTERN, CartValidationStatusCode, CartActions, ConfigChunk, DefaultConfigChunk, deepMerge, CxEvent, PageType, CartModule, RoutingModule as RoutingModule$1, WindowRef, LanguageService, UserAddressService, getLastValueSync, PageMetaModule, OCC_USER_ID_ANONYMOUS, NotificationType, OAuthFlow, UrlMatcherService, DEFAULT_URL_MATCHER, createFrom, FeatureConfigService, THEME_CONTEXT_ID, BaseCoreModule } from '@spartacus/core';
import * as i1$1 from '@angular/router';
import { NavigationStart, RouterModule, NavigationEnd, NavigationCancel, Router, Scroll } from '@angular/router';
import { map, distinctUntilChanged, filter, take, tap, first, flatMap, withLatestFrom, startWith, switchMap, shareReplay, switchMapTo, skipWhile, scan, delayWhen, catchError, mapTo, share, finalize, endWith, debounceTime, pluck, observeOn, skip, pairwise } from 'rxjs/operators';
import { of, BehaviorSubject, combineLatest, Subscription, Observable, ReplaySubject, concat, Subject, timer, defer, forkJoin, EMPTY, isObservable, from, using, asapScheduler, interval } from 'rxjs';
import { __decorate, __param } from 'tslib';
import * as i1$2 from '@angular/platform-browser';
import * as i3$1 from '@angular/forms';
import { FormGroup, FormControl, FormsModule, ReactiveFormsModule, NG_VALUE_ACCESSOR, Validators, FormArray } from '@angular/forms';
import * as i1$3 from '@ng-bootstrap/ng-bootstrap';
import { NgbModalRef, NgbModule } from '@ng-bootstrap/ng-bootstrap';
import * as i1$4 from '@ng-select/ng-select';
import { NgSelectModule } from '@ng-select/ng-select';
import { ofType } from '@ngrx/effects';
import * as i1$5 from '@ngrx/store';
import * as i1$6 from '@angular/service-worker';
import { SwRegistrationOptions, ServiceWorkerModule } from '@angular/service-worker';
import * as i6 from 'ngx-infinite-scroll';
import { InfiniteScrollModule } from 'ngx-infinite-scroll';
import { HttpUrlEncodingCodec } from '@angular/common/http';
import { ROUTER_NAVIGATED } from '@ngrx/router-store';
/** The element attribute used to store the focus state */
const FOCUS_ATTR = 'data-cx-focus';
/** The element attribute used to store the focus group state */
const FOCUS_GROUP_ATTR = 'data-cx-focus-group';
var TrapFocus;
(function (TrapFocus) {
/**
* Will trap the focus at the start of the focus group.
*/
TrapFocus["start"] = "start";
/**
* Will trap the focus only at the end of the focus group.
*/
TrapFocus["end"] = "end";
/**
* Will not trap the focus in both directions. This is actually not are
* a great example of focus trap, but it will give the benefit of keyboard
* tabbing by arrows.
*/
TrapFocus["both"] = "both";
})(TrapFocus || (TrapFocus = {}));
class BaseFocusService {
}
BaseFocusService.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "12.0.5", ngImport: i0, type: BaseFocusService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
BaseFocusService.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "12.0.5", ngImport: i0, type: BaseFocusService, providedIn: 'root' });
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "12.0.5", ngImport: i0, type: BaseFocusService, decorators: [{
type: Injectable,
args: [{
providedIn: 'root',
}]
}] });
/**
* Abstract directive that provides a common interface for all focus directives:
* - Block Focus
* - Persist Focus
* - Escape Focus
* - Auto Focus
* - Tab Focus
* - Trap Focus
* - Lock Focus
*/
class BaseFocusDirective {
constructor(elementRef, service) {
this.elementRef = elementRef;
this.service = service;
/**
* A default config can be provided for each directive if a specific focus directive
* is used directly. i.e. `<div cxAutoFocus></div>`
*/
this.defaultConfig = {};
}
ngOnInit() {
this.setDefaultConfiguration();
this.requiredTabindex = -1;
}
// empty, but sub classes might have an implementation
ngOnChanges(_changes) { }
/**
* Override the (input) config if it undefined or an empty string, with the
* `defaultConfig`. The `defaultConfig` might be specified for each directive
* differently. If a specific directive is used (i.e. `cxAutoFocus`), the
* specific (inherited) defaultConfig will be used.
*/
setDefaultConfiguration() {
if ((!this.config || this.config === '') && this.defaultConfig) {
this.config = this.defaultConfig;
}
}
/**
* Helper method to return the host element for the directive
* given by the `elementRef`.
*/
get host() {
return this.elementRef.nativeElement;
}
/**
* Force a tabindex on the host element if it is _required_ to make the element
* focusable. If the element is focusable by nature or by a given tabindex, the
* `tabindex` is not applied.
*
* Buttons, active links, etc. do no need an explicit tabindex to receive focus.
*/
set requiredTabindex(tabindex) {
if (this.requiresExplicitTabIndex) {
this.tabindex = tabindex;
}
}
/**
* Returns true if the host element does not have a tabindex defined
* and it also doesn't get focus by browsers nature (i.e. button or
* active link).
*/
get requiresExplicitTabIndex() {
return (this.tabindex === undefined &&
['button', 'input', 'select', 'textarea'].indexOf(this.host.tagName.toLowerCase()) === -1 &&
!(this.host.tagName === 'A' &&
(this.host.hasAttribute('href') ||
this.host.hasAttribute('routerlink') ||
this.host.getAttribute('ng-reflect-router-link'))));
}
}
BaseFocusDirective.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "12.0.5", ngImport: i0, type: BaseFocusDirective, deps: [{ token: i0.ElementRef }, { token: BaseFocusService }], target: i0.ɵɵFactoryTarget.Directive });
BaseFocusDirective.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "12.0.0", version: "12.0.5", type: BaseFocusDirective, inputs: { tabindex: "tabindex" }, host: { properties: { "attr.tabindex": "this.tabindex" } }, usesOnChanges: true, ngImport: i0 });
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "12.0.5", ngImport: i0, type: BaseFocusDirective, decorators: [{
type: Directive
}], ctorParameters: function () { return [{ type: i0.ElementRef }, { type: BaseFocusService }]; }, propDecorators: { tabindex: [{
type: Input
}, {
type: HostBinding,
args: ['attr.tabindex']
}] } });
/**
* Directive implementation that adds a CSS class to the host element
* when the moused is used to focus an element. As soon as the keyboard
* is used, the class is removed.
*
* This feature must be explicitly enabled with the `disableMouseFocus` config.
*
* The appearance of the visual focus depends on the CSS implementation to
* begin with. Spartacus styles add a blue border around each focusable element.
* This can be considered annoying by keyboard users, as they won't need such a
* strong indication of the selected element.
*/
class VisibleFocusDirective extends BaseFocusDirective {
constructor() {
super(...arguments);
this.defaultConfig = {
disableMouseFocus: true,
};
/** Controls a css class to hide focus visible CSS rules */
this.mouseFocus = false;
}
handleMousedown() {
if (this.shouldFocusVisible) {
this.mouseFocus = true;
}
}
handleKeydown(event) {
if (this.shouldFocusVisible) {
this.mouseFocus = !this.isNavigating(event);
}
}
/**
* Indicates whether the configurations setup to disable visual focus.
*/
get shouldFocusVisible() {
var _a;
return (_a = this.config) === null || _a === void 0 ? void 0 : _a.disableMouseFocus;
}
/**
* Indicates whether the event is used to navigate the storefront. Some keyboard events
* are used by mouse users to fill a form or interact with the OS or browser.
*/
isNavigating(event) {
// when the cmd or ctrl keys are used, the user doesn't navigate the storefront
if (event.metaKey) {
return false;
}
// when the tab key is used, users are for navigating away from the current (form) element
if (event.code === 'Tab') {
return true;
}
// If the user fill in a form, we don't considering it part of storefront navigation.
if (['INPUT', 'TEXTAREA'].includes(event.target.tagName)) {
return false;
}
return true;
}
}
VisibleFocusDirective.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "12.0.5", ngImport: i0, type: VisibleFocusDirective, deps: null, target: i0.ɵɵFactoryTarget.Directive });
VisibleFocusDirective.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "12.0.0", version: "12.0.5", type: VisibleFocusDirective, host: { listeners: { "mousedown": "handleMousedown()", "keydown": "handleKeydown($event)" }, properties: { "class.mouse-focus": "this.mouseFocus" } }, usesInheritance: true, ngImport: i0 });
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "12.0.5", ngImport: i0, type: VisibleFocusDirective, decorators: [{
type: Directive
}], propDecorators: { mouseFocus: [{
type: HostBinding,
args: ['class.mouse-focus']
}], handleMousedown: [{
type: HostListener,
args: ['mousedown']
}], handleKeydown: [{
type: HostListener,
args: ['keydown', ['$event']]
}] } });
// { selector: '[cxBlockFocus]' }
class BlockFocusDirective extends VisibleFocusDirective {
constructor(elementRef, service) {
super(elementRef, service);
this.elementRef = elementRef;
this.service = service;
this.defaultConfig = { block: true };
// @Input('cxBlockFocus')
this.config = {};
}
ngOnInit() {
super.ngOnInit();
if (this.config.block) {
this.tabindex = -1;
}
}
}
BlockFocusDirective.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "12.0.5", ngImport: i0, type: BlockFocusDirective, deps: [{ token: i0.ElementRef }, { token: BaseFocusService }], target: i0.ɵɵFactoryTarget.Directive });
BlockFocusDirective.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "12.0.0", version: "12.0.5", type: BlockFocusDirective, usesInheritance: true, ngImport: i0 });
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "12.0.5", ngImport: i0, type: BlockFocusDirective, decorators: [{
type: Directive
}], ctorParameters: function () { return [{ type: i0.ElementRef }, { type: BaseFocusService }]; } });
const GLOBAL_GROUP = '_g_';
/**
* Shared service to persist the focus for an element or a group
* of elements. The persisted element focus can be used to persist
* the focus for a DOM tree, so that the focus remains after a repaint
* or reoccurs when a DOM tree is "unlocked".
*/
class PersistFocusService extends BaseFocusService {
constructor() {
super(...arguments);
// this is going to fail as we have sub services. They will al have their own map.
// We must bring this to a singleton map.
this.focus = new Map();
}
get(group) {
return this.focus.get(group || GLOBAL_GROUP);
}
/**
* Persist the keyboard focus state for the given key. The focus is stored globally
* or for the given group.
*/
set(key, group) {
if (key) {
this.focus.set(group || GLOBAL_GROUP, key);
}
}
/**
* Clears the persisted keyboard focus state globally or for the given group.
*/
clear(group) {
this.focus.delete(group || GLOBAL_GROUP);
}
/**
* Returns the group for the host element based on the configured group or
* by the `data-cx-focus-group` attribute stored on the host.
*/
getPersistenceGroup(host, config) {
return (config === null || config === void 0 ? void 0 : config.group) ? config.group : host.getAttribute(FOCUS_GROUP_ATTR);
}
}
PersistFocusService.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "12.0.5", ngImport: i0, type: PersistFocusService, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
PersistFocusService.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "12.0.5", ngImport: i0, type: PersistFocusService, providedIn: 'root' });
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "12.0.5", ngImport: i0, type: PersistFocusService, decorators: [{
type: Injectable,
args: [{
providedIn: 'root',
}]
}] });
/**
* Directive that provides persistence of the focused state. This is useful
* when a group of focusable elements got refocused or even recreated. That
* happens often when the DOM is constructed with an `*ngIf` or `*ngFor`.
*
* The focus state is based on a configured _key_, which can be passed in the
* config input, either by using a string primitive or `PersistFocusConfig.key`:
*
* ```html
* <button cxPersistFocus="myKey"></button>
* <button cxFocus="myKey"></button>
* <button [cxFocus]="{{key:'myKey'}"></button>
* ```
*
* The focus state can be part of a focus _group_, so that the state is shared
* and remember for the given group. In order to detect the persistence for a
* given element, we store the persistence key as a data attribute (`data-cx-focus`):
*
* ```html
* <button data-cx-focus="myKey"></button>
* ```
*
* Other keyboard focus directives can read the key to understand whether the element
* should retrieve focus.
*
*/
class PersistFocusDirective extends BlockFocusDirective {
constructor(elementRef, service) {
super(elementRef, service);
this.elementRef = elementRef;
this.service = service;
this.defaultConfig = {};
/**
* The persistence key can be passed directly or through the `FocusConfig.key`.
* While this could be considered a global key, the likeliness of conflicts
* is very small since the key is cleared when the focus is changed.
*/
// @Input('cxPersistFocus')
this.config = {};
}
handleFocus(event) {
this.service.set(this.key, this.group);
event === null || event === void 0 ? void 0 : event.preventDefault();
event === null || event === void 0 ? void 0 : event.stopPropagation();
}
ngOnInit() {
super.ngOnInit();
this.attr = this.key ? this.key : undefined;
}
setDefaultConfiguration() {
if (typeof this.config === 'string' && this.config !== '') {
this.config = { key: this.config };
}
super.setDefaultConfiguration();
}
/**
* Focus the element explicitly if it was focused before.
*/
ngAfterViewInit() {
if (this.isPersisted) {
this.host.focus({ preventScroll: true });
}
}
get isPersisted() {
return !!this.key && this.service.get(this.group) === this.key;
}
/**
* Returns the key for the host element, which is used to persist the
* focus state. This is useful in cases where the DOM is rebuild.
*/
get key() {
var _a;
return (_a = this.config) === null || _a === void 0 ? void 0 : _a.key;
}
/**
* returns the persistence group (if any) for the focusable elements.
*/
get group() {
return this.service.getPersistenceGroup(this.host, this.config);
}
}
PersistFocusDirective.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "12.0.5", ngImport: i0, type: PersistFocusDirective, deps: [{ token: i0.ElementRef }, { token: PersistFocusService }], target: i0.ɵɵFactoryTarget.Directive });
PersistFocusDirective.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "12.0.0", version: "12.0.5", type: PersistFocusDirective, host: { listeners: { "focus": "handleFocus($event)" }, properties: { "attr.data-cx-focus": "this.attr" } }, usesInheritance: true, ngImport: i0 });
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "12.0.5", ngImport: i0, type: PersistFocusDirective, decorators: [{
type: Directive
}], ctorParameters: function () { return [{ type: i0.ElementRef }, { type: PersistFocusService }]; }, propDecorators: { attr: [{
type: HostBinding,
args: [`attr.${FOCUS_ATTR}`]
}], handleFocus: [{
type: HostListener,
args: ['focus', ['$event']]
}] } });
class SelectFocusUtility {
constructor() {
/**
* Query selectors used to query focusable child elements of the host element.
* The selectors are supplemented with `:not([disabled])` and `:not([hidden])`.
*/
this.focusableSelectors = [
'a[href]',
'button',
'[tabindex]',
'input',
'select',
'textarea',
];
// like to leave out the following as we don't use it, and make this list exensible.
// `[contentEditable=true]`, // very unlikely to suport as we're not a business tool
// `iframe`, // we really don't like iframes...
// `area[href]`, // very debatable!
this.focusableSelectorSuffix = ':not([disabled]):not([hidden])';
}
query(host, selector) {
if (!selector || selector === '') {
return [];
}
return Array.from(host.querySelectorAll(selector));
}
findFirstFocusable(host, config = { autofocus: true }) {
const selector = typeof (config === null || config === void 0 ? void 0 : config.autofocus) === 'string' ? config.autofocus : '[autofocus]';
// fallback to first focusable
return (this.query(host, selector).find((el) => !this.isHidden(el)) ||
this.findFocusable(host).find((el) => Boolean(el)));
}
/**
* returns all focusable child elements of the host element. The element selectors
* are build from the `focusableSelectors`.
*
* @param host the `HTMLElement` used to query focusable elements
* @param locked indicates whether inactive (`tabindex="-1"`) focusable elements should be returned
* @param invisible indicates whether hidden focusable elements should be returned
*/
findFocusable(host, locked = false, invisible = false) {
let suffix = this.focusableSelectorSuffix;
if (!locked) {
suffix += `:not([tabindex='-1'])`;
}
const selector = this.focusableSelectors
.map((s) => (s += suffix))
.join(',');
return this.query(host, selector).filter((el) => !invisible ? !this.isHidden(el) : Boolean(el));
}
/**
* Indicates whether the element is hidden by CSS. There are various CSS rules and
* HTML structures which can lead to an hidden or invisible element. An `offsetParent`
* of null indicates that the element or any of it's decendants is hidden (`display:none`).
*
* Oother techniques use the visibility (`visibility: hidden`), opacity (`opacity`) or
* phyisical location on the element itself or any of it's anchestor elements. Those
* technique require to work with the _computed styles_, which will cause a performance
* downgrade. We don't do this in the standard implementaton.
*/
isHidden(el) {
return el.offsetParent === null;
}
}
SelectFocusUtility.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "12.0.5", ngImport: i0, type: SelectFocusUtility, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
SelectFocusUtility.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "12.0.5", ngImport: i0, type: SelectFocusUtility, providedIn: 'root' });
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "12.0.5", ngImport: i0, type: SelectFocusUtility, decorators: [{
type: Injectable,
args: [{
providedIn: 'root',
}]
}] });
class EscapeFocusService extends PersistFocusService {
constructor(selectFocusUtil) {
super();
this.selectFocusUtil = selectFocusUtil;
}
shouldFocus(config) {
return !!(config === null || config === void 0 ? void 0 : config.focusOnEscape);
}
handleEscape(host, config, event) {
var _a;
if (this.shouldFocus(config)) {
if (host !== event.target) {
host.focus({ preventScroll: true });
event.preventDefault();
event.stopPropagation();
}
else {
if (config === null || config === void 0 ? void 0 : config.focusOnDoubleEscape) {
(_a = this.selectFocusUtil
.findFirstFocusable(host, { autofocus: true })) === null || _a === void 0 ? void 0 : _a.focus();
}
}
}
}
}
EscapeFocusService.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "12.0.5", ngImport: i0, type: EscapeFocusService, deps: [{ token: SelectFocusUtility }], target: i0.ɵɵFactoryTarget.Injectable });
EscapeFocusService.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "12.0.5", ngImport: i0, type: EscapeFocusService, providedIn: 'root' });
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "12.0.5", ngImport: i0, type: EscapeFocusService, decorators: [{
type: Injectable,
args: [{
providedIn: 'root',
}]
}], ctorParameters: function () { return [{ type: SelectFocusUtility }]; } });
/**
* Directive to focus the host element whenever the `escape` key is captured.
* UiEvents bubble up by nature, which is why the `cxEscGroup` can be used
* on a tree of elements. Each time the escape key is used, the focus will
* move up in the DOM tree.
*
*/
class EscapeFocusDirective extends PersistFocusDirective {
constructor(elementRef, service) {
super(elementRef, service);
this.elementRef = elementRef;
this.service = service;
this.defaultConfig = { focusOnEscape: true };
this.esc = new EventEmitter();
}
/**
* Handles the escape key event.
* @param event the native keyboard event which contains the escape keydown event
*/
handleEscape(event) {
if (this.service.shouldFocus(this.config)) {
this.service.handleEscape(this.host, this.config, event);
}
this.esc.emit(this.service.shouldFocus(this.config));
}
ngOnInit() {
if (this.service.shouldFocus(this.config)) {
this.requiredTabindex = -1;
}
super.ngOnInit();
}
}
EscapeFocusDirective.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "12.0.5", ngImport: i0, type: EscapeFocusDirective, deps: [{ token: i0.ElementRef }, { token: EscapeFocusService }], target: i0.ɵɵFactoryTarget.Directive });
EscapeFocusDirective.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "12.0.0", version: "12.0.5", type: EscapeFocusDirective, outputs: { esc: "esc" }, host: { listeners: { "keydown.escape": "handleEscape($event)" } }, usesInheritance: true, ngImport: i0 });
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "12.0.5", ngImport: i0, type: EscapeFocusDirective, decorators: [{
type: Directive
}], ctorParameters: function () { return [{ type: i0.ElementRef }, { type: EscapeFocusService }]; }, propDecorators: { esc: [{
type: Output
}], handleEscape: [{
type: HostListener,
args: ['keydown.escape', ['$event']]
}] } });
class AutoFocusService extends EscapeFocusService {
/**
* Returns the first focusable child element of the host element.
*/
findFirstFocusable(host, config = { autofocus: true }) {
if ((config === null || config === void 0 ? void 0 : config.autofocus) === ':host') {
return host;
}
else if (this.hasPersistedFocus(host, config)) {
return this.getPersisted(host, this.getPersistenceGroup(host, config));
}
else {
return this.selectFocusUtil.findFirstFocusable(host, config) || host;
}
}
/**
* Indicates whether any of the focusable child elements is focused.
*/
hasPersistedFocus(host, config) {
return !!this.getPersisted(host, this.getPersistenceGroup(host, config));
}
/**
* Returns the element that has a persisted focus state.
*
* @param host the `HTMLElement` used to query for focusable children
* @param group the optional group for the persistent state, to separate different focus
* groups and remain the persistence
*/
getPersisted(host, group) {
if (!this.get(group)) {
return;
}
const focussed = Array.from(host.querySelectorAll(`[${FOCUS_ATTR}='${this.get(group)}']`));
return focussed.length > 0 ? focussed[0] : null;
}
}
AutoFocusService.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "12.0.5", ngImport: i0, type: AutoFocusService, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
AutoFocusService.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "12.0.5", ngImport: i0, type: AutoFocusService, providedIn: 'root' });
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "12.0.5", ngImport: i0, type: AutoFocusService, decorators: [{
type: Injectable,
args: [{
providedIn: 'root',
}]
}] });
/**
* Directive that focus the first nested _focusable_ element based on state and configuration:
*
* 1. focusable element that was left in a focused state (aka _persisted_ focus)
* 2. focusable element selected by configured CSS selector (i.e. 'button[type=submit]')
* 3. focusable element marked with the native HTML5 `autofocus` attribute
* 4. first focusable element
* 5. the host element, in case the configured CSS selector is `:host`.
*
* Example configurations:
*
* `<div cxAutoFocus>[...]</div>`
*
* `<div [cxAutoFocus]="{autofocus: false}">[...]</div>`
*
* `<div [cxAutoFocus]="{autofocus: 'button.active'}">[...]</div>`
*
* `<div [cxAutoFocus]="{autofocus: ':host'}">[...]</div>`
*
* When your element is added dynamically (ie. by using an *ngIf or after a DOM change), the
* focus can be refreshed by using the refreshFocus configuration.
*/
class AutoFocusDirective extends EscapeFocusDirective {
constructor(elementRef, service) {
super(elementRef, service);
this.elementRef = elementRef;
this.service = service;
/** The AutoFocusDirective will be using autofocus by default */
this.defaultConfig = { autofocus: true };
}
/**
* Focus the element explicitly if it was focussed before.
*/
ngAfterViewInit() {
if (this.shouldAutofocus) {
this.handleFocus();
}
if (!this.shouldAutofocus || this.hasPersistedFocus) {
super.ngAfterViewInit();
}
}
ngOnChanges(changes) {
var _a;
// responsible for refresh focus based on the configured refresh property name
if (!!((_a = changes.config.currentValue) === null || _a === void 0 ? void 0 : _a.refreshFocus)) {
// ensure the autofocus when it's to provided initially
if (!this.config.autofocus) {
this.config.autofocus = true;
}
this.handleFocus();
}
super.ngOnChanges(changes);
}
/**
* Mimic the focus without setting the actual focus on the host. The first
* focusable child element will be focussed.
*/
handleFocus(event) {
var _a;
if (this.shouldAutofocus) {
if (!(event === null || event === void 0 ? void 0 : event.target) || event.target === this.host) {
(_a = this.firstFocusable) === null || _a === void 0 ? void 0 : _a.focus();
}
else {
event.target.focus();
}
}
super.handleFocus(event);
}
/**
* Helper function to get the first focusable child element
*/
get hasPersistedFocus() {
return this.service.hasPersistedFocus(this.host, this.config);
}
/**
* Helper function to indicate whether we should use autofocus for the
* child elements.
*/
get shouldAutofocus() {
var _a;
return !!((_a = this.config) === null || _a === void 0 ? void 0 : _a.autofocus);
}
/**
* Helper function to get the first focusable child element.
*
* We keep this private to not pollute the API.
*/
get firstFocusable() {
return this.service.findFirstFocusable(this.host, this.config);
}
}
AutoFocusDirective.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "12.0.5", ngImport: i0, type: AutoFocusDirective, deps: [{ token: i0.ElementRef }, { token: AutoFocusService }], target: i0.ɵɵFactoryTarget.Directive });
AutoFocusDirective.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "12.0.0", version: "12.0.5", type: AutoFocusDirective, usesInheritance: true, usesOnChanges: true, ngImport: i0 });
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "12.0.5", ngImport: i0, type: AutoFocusDirective, decorators: [{
type: Directive
}], ctorParameters: function () { return [{ type: i0.ElementRef }, { type: AutoFocusService }]; } });
class TabFocusService extends AutoFocusService {
/**
* Moves to the next (or previous) tab.
*/
moveTab(host, config, increment, event) {
if (config === null || config === void 0 ? void 0 : config.tab) {
const next = config.tab === 'scroll'
? this.findNextScrollable(host, config, increment)
: this.findNext(host, config, increment);
next === null || next === void 0 ? void 0 : next.focus();
event.preventDefault();
event.stopPropagation();
}
}
/**
* builds out virtual slides out of the full scrollable area, to allow
* for maximum flexibility for the underlying layout without using hardcoded
* slide sizes.
*/
findNextScrollable(host, config, increment) {
var _a;
const active = this.getActiveChild(host, config);
if (!active) {
return;
}
// slide count
const virtualSlideCount = Math.round(host.scrollWidth / host.clientWidth);
// find current virtual slide
const currentVirtualSlide = Math.round(active.offsetLeft / (host.scrollWidth / virtualSlideCount));
let nextVirtualSlide = currentVirtualSlide + increment;
if (increment === 1 /* NEXT */ &&
nextVirtualSlide >= virtualSlideCount) {
nextVirtualSlide = 0;
}
if (increment === -1 /* PREV */ && nextVirtualSlide < 0) {
nextVirtualSlide = virtualSlideCount - 1;
}
const firstItemOnNextSlide = (_a = this.getChildren(host, config)) === null || _a === void 0 ? void 0 : _a.find((tab) => tab.offsetLeft >=
(host.scrollWidth / virtualSlideCount) * nextVirtualSlide);
return firstItemOnNextSlide;
}
findNext(host, config, increment) {
const childs = this.getChildren(host, config);
let activeIndex = childs === null || childs === void 0 ? void 0 : childs.findIndex((c) => c === this.getActiveChild(host, config));
if (!activeIndex || activeIndex === -1) {
activeIndex = 0;
}
activeIndex += increment;
if (increment === 1 /* NEXT */ && activeIndex >= (childs === null || childs === void 0 ? void 0 : childs.length)) {
activeIndex = childs.length - 1;
}
if (increment === -1 /* PREV */ && activeIndex < 0) {
activeIndex = 0;
}
return childs ? childs[activeIndex] : undefined;
}
/**
* Returns the active focusable child element. If there's no active
* focusable child element, the first focusable child is returned.
*/
getActiveChild(host, config) {
const persisted = this.getPersisted(host, config === null || config === void 0 ? void 0 : config.group);
if (persisted) {
return persisted;
}
const children = this.getChildren(host, config);
let index = children.findIndex((tab) => this.isActive(tab));
if (!index || index === -1) {
index = 0;
}
return children[index];
}
getChildren(host, config) {
if (typeof config.tab === 'string' && config.tab !== 'scroll') {
return this.selectFocusUtil.query(host, config.tab);
}
else {
return this.findFocusable(host, true);
}
}
/**
* Returns all focusable child elements of the host element.
*
* @param host The host element is used to query child focusable elements.
* @param locked Indicates if locked elements (tabindex=-1) should be returned, defaults to false.
* @param invisible Indicates if invisible child elements should be returned, defaults to false.
*/
findFocusable(host, locked = false, invisible = false) {
return this.selectFocusUtil.findFocusable(host, locked, invisible);
}
isActive(el) {
const child = document.activeElement;
const selector = child.tagName;
return (el === child ||
!!Array.from(el.querySelectorAll(selector)).find((e) => e === child));
}
}
TabFocusService.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "12.0.5", ngImport: i0, type: TabFocusService, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
TabFocusService.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "12.0.5", ngImport: i0, type: TabFocusService, providedIn: 'root' });
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "12.0.5", ngImport: i0, type: TabFocusService, decorators: [{
type: Injectable,
args: [{
providedIn: 'root',
}]
}] });
/**
* Directive to move the focus of ("locked") child elements. This is useful
* for a nested list of tabs, carousel slides or any group of elements that
* requires horizontal navigation.
*/
class TabFocusDirective extends AutoFocusDirective {
constructor(elementRef, service) {
super(elementRef, service);
this.elementRef = elementRef;
this.service = service;
/** `tab` defaults to true if the directive `cxTabFocus` is used. */
this.defaultConfig = { tab: true };
// @Input('cxTabFocus')
this.config = {};
}
handleNextTab(event) {
var _a;
if ((_a = this.config) === null || _a === void 0 ? void 0 : _a.tab) {
this.service.moveTab(this.host, this.config, 1 /* NEXT */, event);
}
}
handlePreviousTab(event) {
var _a;
if ((_a = this.config) === null || _a === void 0 ? void 0 : _a.tab) {
this.service.moveTab(this.host, this.config, -1 /* PREV */, event);
}
}
}
TabFocusDirective.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "12.0.5", ngImport: i0, type: TabFocusDirective, deps: [{ token: i0.ElementRef }, { token: TabFocusService }], target: i0.ɵɵFactoryTarget.Directive });
TabFocusDirective.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "12.0.0", version: "12.0.5", type: TabFocusDirective, host: { listeners: { "keydown.arrowRight": "handleNextTab($event)", "keydown.arrowLeft": "handlePreviousTab($event)" } }, usesInheritance: true, ngImport: i0 });
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "12.0.5", ngImport: i0, type: TabFocusDirective, decorators: [{
type: Directive
}], ctorParameters: function () { return [{ type: i0.ElementRef }, { type: TabFocusService }]; }, propDecorators: { handleNextTab: [{
type: HostListener,
args: ['keydown.arrowRight', ['$event']]
}], handlePreviousTab: [{
type: HostListener,
args: ['keydown.arrowLeft', ['$event']]
}] } });
class TrapFocusService extends TabFocusService {
/**
* Indicates whether any of the child elements of the host are focusable.
*
* @param host `HTMLElement` that is used to query the focusable elements.
*/
hasFocusableChildren(host) {
return this.findFocusable(host).length > 0;
}
/**
* Focus the next or previous element of all available focusable elements.
* The focus is _trapped_ in case there's no next or previous available element.
* The focus will automatically move the start or end of the list.
*/
moveFocus(host, config, increment, event) {
const focusable = this.findFocusable(host);
let index = focusable.findIndex((v) => v === event.target) + increment;
const shouldMoveFocus = (index >= 0 && index < focusable.length) ||
(index < 0 && this.getTrapStart(config.trap)) ||
(index >= focusable.length && this.getTrapEnd(config.trap));
if (shouldMoveFocus) {
if (index >= focusable.length) {
index = 0;
}
if (index < 0) {
index = focusable.length - 1;
}
event.preventDefault();
event.stopPropagation();
const el = focusable[index];
el.focus();
}
}
getTrapStart(trap) {
return trap === true || trap === TrapFocus.start;
}
getTrapEnd(trap) {
return trap === true || trap === TrapFocus.end;
}
}
TrapFocusService.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "12.0.5", ngImport: i0, type: TrapFocusService, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
TrapFocusService.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "12.0.5", ngImport: i0, type: TrapFocusService, providedIn: 'root' });
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "12.0.5", ngImport: i0, type: TrapFocusService, decorators: [{
type: Injectable,
args: [{
providedIn: 'root',
}]
}] });
/**
* Directive that keeps the focus inside the focusable child elements,
* also known as a _focus trap_.
*/
class TrapFocusDirective extends TabFocusDirective {
constructor(elementRef, service) {
super(elementRef, service);
this.elementRef = elementRef;
this.service = service;
this.defaultConfig = { trap: true };
// @Input('cxTrapFocus')
this.config = {};
this.handleTrapDown = (event) => {
if (!!this.config.trap) {
this.moveFocus(event, 1 /* NEXT */);
}
};
this.handleTrapUp = (event) => {
if (!!this.config.trap) {
this.moveFocus(event, -1 /* PREV */);
}
};
}
/**
* Moves the focus of the element reference up or down, depending on the increment.
* The focus of the element is trapped to avoid it from going out of the group.
*
* @param event UIEvent that is used to get the target element. The event is blocked
* from standard execution and further bubbling.
* @param increment indicates whether the next or previous is focussed.
*/
moveFocus(event, increment) {
if (this.service.hasFocusableChildren(this.host)) {
this.service.moveFocus(this.host, this.config, increment, event);
}
}
}
TrapFocusDirective.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "12.0.5", ngImport: i0, type: TrapFocusDirective, deps: [{ token: i0.ElementRef }, { token: TrapFocusService }], target: i0.ɵɵFactoryTarget.Directive });
TrapFocusDirective.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "12.0.0", version: "12.0.5", type: TrapFocusDirective, host: { listeners: { "keydown.arrowdown": "handleTrapDown($event)", "keydown.tab": "handleTrapDown($event)", "keydown.arrowup": "handleTrapUp($event)", "keydown.shift.tab": "handleTrapUp($event)" } }, usesInheritance: true, ngImport: i0 });
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "12.0.5", ngImport: i0, type: TrapFocusDirective, decorators: [{
type: Directive
}], ctorParameters: function () { return [{ type: i0.ElementRef }, { type: TrapFocusService }]; }, propDecorators: { handleTrapDown: [{
type: HostListener,
args: ['keydown.arrowdown', ['$event']]
}, {
type: HostListener,
args: ['keydown.tab', ['$event']]
}], handleTrapUp: [{
type: HostListener,
args: ['keydown.arrowup', ['$event']]
}, {
type: HostListener,
args: ['keydown.shift.tab', ['$event']]
}] } });
class LockFocusService extends TrapFocusService {
}
LockFocusService.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "12.0.5", ngImport: i0, type: LockFocusService, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
LockFocusService.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "12.0.5", ngImport: i0, type: LockFocusService, providedIn: 'root' });
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "12.0.5", ngImport: i0, type: LockFocusService, decorators: [{
type: Injectable,
args: [{
providedIn: 'root',
}]
}] });
/**
* Focusable elements exclude hidden elements by default, but this contradicts with
* unlocking (hidden) elements.
*/
const UNLOCK_HIDDEN_ELEMENTS = true;
/**
* Directive that adds persistence for focussed element in case
* the elements are being rebuild. This happens often when change
* detection kicks in because of new data set from the backend.
*/
class LockFocusDirective extends TrapFocusDirective {
constructor(elementRef, service, renderer) {
super(elementRef, service);
this.elementRef = elementRef;
this.service = service;
this.renderer = renderer;
this.defaultConfig = { lock: true };
// @Input('cxLockFocus')
this.config = {};
/**
* Emits an event when the host is unlocked.
*/
this.unlock = new EventEmitter();
}
/**
* When the user selects enter or space, the focusable childs are
* unlocked, which means that the tabindex is set to 0.
*/
handleEnter(event) {
if (this.shouldLock && this.host === event.target) {
this.unlockFocus(event);
event.preventDefault();
event.stopPropagation();
}
}
/**
* In case any of the children elements is touched by the mouse,
* we unlock the group to not break the mouse-experience.
*/
handleClick(event) {
if (this.shouldLock && this.isLocked) {
this.unlockFocus(event);
event.stopPropagation();
}
}
lockFocus() {
this.addTabindexToChildren(-1);
}
unlockFocus(event) {
this.unlock.emit(true);
this.addTabindexToChildren(0);
// we focus the host if the event was triggered from a child
if ((event === null || event === void 0 ? void 0 : event.target) === this.host) {
// we wait a few milliseconds, mainly because firefox will otherwise apply
// the mouse event on the new focused child element
setTimeout(() => {
super.handleFocus(event);
}, 100);
}
}
ngOnInit() {
var _a, _b;
super.ngOnInit();
this.shouldLock = (_a = this.config) === null || _a === void 0 ? void 0 : _a.lock;
if (this.shouldLock) {
this.tabindex = 0;
// Locked elements will be set to `autofocus` by default if it's not
// been configured. This will ensure that autofocus kicks in upon unlock.
if (!this.config.hasOwnProperty('autofocus')) {
this.config.autofocus = true;
}
// Locked elements will be set to `focusOnEscape` by default if it's not
// been configured. This will ensure that the host gets locked again when
// `escape` is pressed.
if (!this.config.hasOwnProperty('focusOnEscape')) {
this.config.focusOnEscape = !(((_b = this.config) === null || _b === void 0 ? void 0 : _b.focusOnEscape) === false);
}
}
}
ngAfterViewInit() {
if (this.shouldLock) {
/**
* If the component hosts a group of focusable children elements,
* we persist the group key to the children, so that they can taken this
* into account when they persist their focus state.
*/
if (!!this.group) {
this.service.findFocusable(this.host).forEach((el) =>
// we must do this in after view init as
this.renderer.setAttribute(el, FOCUS_GROUP_ATTR, this.group));
}
if (this.shouldAutofocus) {
this.handleFocus();
}
}
super.ngAfterViewInit();
}
handleFocus(event) {
if (this.shouldLock) {
if (this.shouldUnlockAfterAutofocus(event)) {
// Delay unlocking in case the host is using `ChangeDetectionStrategy.Default`
setTimeout(() => this.unlockFocus(event));
}
else {
setTimeout(() => this.lockFocus());
event === null || event === void 0 ? void 0 : event.stopPropagation();
return;
}
}
super.handleFocus(event);
}
handleEscape(event) {
if (this.shouldLock) {
this.service.clear(this.config.group);
}
super.handleEscape(event);
}
/**
* When the handleFocus is called without an actual event, it's coming from Autofocus.
* In this case we unlock the focusable children in case there's a focusable child that
* was unlocked before.
*
* We keep this private to not polute the API.
*/
shouldUnlockAfterAutofocus(event) {
return !event && this.service.hasPersistedFocus(this.host, this.config);
}
/**
* Add the tabindex attribute to the focusable children elements
*/
addTabindexToChildren(i = 0) {
if (this.shouldLock) {
this.isLocked = i === -1;
if (!(this.hasFocusableChildren && i === 0) || i === 0) {
this.focusable.forEach((el) => this.renderer.setAttribute(el, 'tabindex', i.toString()));
}
}
}
/**
* Utility method, returns all focusable children for the host element.
*
* We keep this private to not polute the API.
*/
get hasFocusableChildren() {
return this.service.hasFocusableChildren(this.host);
}
/**
* Returns the focusable children of the host element. If the host element
* is configured to be locked, the query is restricted to child elements
* with a tabindex !== `-1`.
*
* We keep this private to not polute the API.
*/
get focusable() {
return this.service.findFocusable(this.host, this.shouldLock, UNLOCK_HIDDEN_ELEMENTS);
}
}
LockFocusDirective.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "12.0.5", ngImport: i0, type: LockFocusDirective, deps: [{ token: i0.ElementRef }, { token: LockFocusService }, { token: i0.Renderer2 }], target: i0.ɵɵFactoryTarget.Directive });
LockFocusDirective.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "12.0.0", version: "12.0.5", type: LockFocusDirective, outputs: { unlock: "unlock" }, host: { listeners: { "keydown.enter": "handleEnter($event)", "keydown.space": "handleEnter($event)", "click": "handleClick($event)" }, properties: { "class.focus-lock": "this.shouldLock", "class.is-locked": "this.isLocked" } }, usesInheritance: true, ngImport: i0 });
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "12.0.5", ngImport: i0, type: LockFocusDirective, decorators: [{
type: Directive
}], ctorParameters: function () { return [{ type: i0.ElementRef }, { type: LockFocusService }, { type: i0.Renderer2 }]; }, propDecorators: { shouldLock: [{
type: HostBinding,