UNPKG

@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
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,