@ng-matero/extensions
Version:
Angular Material Extensions
390 lines (382 loc) • 23.3 kB
JavaScript
import { BidiModule } from '@angular/cdk/bidi';
import { CdkDialogContainer, Dialog, DialogConfig, DialogModule } from '@angular/cdk/dialog';
import { CdkPortalOutlet, PortalModule } from '@angular/cdk/portal';
import * as i0 from '@angular/core';
import { EventEmitter, ViewChild, ViewEncapsulation, ChangeDetectionStrategy, Component, InjectionToken, inject, Injectable, NgModule } from '@angular/core';
import { coerceCssPixelValue } from '@angular/cdk/coercion';
import { Overlay } from '@angular/cdk/overlay';
import { Subject, merge, defer } from 'rxjs';
import { filter, take, startWith } from 'rxjs/operators';
import { _animationsDisabled } from '@angular/material/core';
import { ESCAPE, hasModifierKey } from '@angular/cdk/keycodes';
/**
* Configuration used when opening a drawer.
*/
class MtxDrawerConfig {
constructor() {
/** The ARIA role of the dialog element. */
this.role = 'dialog';
/** Data being injected into the child component. */
this.data = null;
/** Whether the drawer has a backdrop. */
this.hasBackdrop = true;
/** Whether the user can use escape or clicking outside to close the drawer. */
this.disableClose = false;
/** Aria label to assign to the drawer element. */
this.ariaLabel = null;
/**
* Whether this is a modal dialog. Used to set the `aria-modal` attribute. Off by default,
* because it can interfere with other overlay-based components (e.g. `mat-select`) and because
* it is redundant since the dialog marks all outside content as `aria-hidden` anyway.
*/
this.ariaModal = false;
/**
* Whether the drawer should close when the user goes backwards/forwards in history.
* Note that this usually doesn't include clicking on links (unless the user is using
* the `HashLocationStrategy`).
*/
this.closeOnNavigation = true;
/**
* Where the drawer should focus on open.
* @breaking-change 14.0.0 Remove boolean option from autoFocus. Use string or
* AutoFocusTarget instead.
*/
this.autoFocus = 'first-tabbable';
/**
* Whether the drawer should restore focus to the
* previously-focused element, after it's closed.
*/
this.restoreFocus = true;
/** Position of the drawer. */
this.position = 'right';
}
}
const ENTER_ANIMATION = '_mtx-drawer-enter';
const EXIT_ANIMATION = '_mtx-drawer-exit';
/**
* Internal component that wraps user-provided drawer content.
* @docs-private
*/
class MtxDrawerContainer extends CdkDialogContainer {
constructor() {
super(...arguments);
this._animationsDisabled = _animationsDisabled();
/** The state of the drawer animations. */
this._animationState = 'void';
/** Emits whenever the state of the animation changes. */
this._animationStateChanged = new EventEmitter();
/** Whether the component has been destroyed. */
this._destroyed = false;
}
get _drawerPosition() {
return `mtx-drawer-${this._config.position}`;
}
_contentAttached() {
// Delegate to the original dialog-container initialization (i.e. saving the
// previous element, setting up the focus trap and moving focus to the container).
super._contentAttached();
this.enter();
}
/** Begin animation of bottom sheet entrance into view. */
enter() {
if (!this._destroyed) {
this._animationState = 'visible';
this._changeDetectorRef.markForCheck();
this._changeDetectorRef.detectChanges();
if (this._animationsDisabled) {
this._simulateAnimation(ENTER_ANIMATION);
}
}
}
/** Begin animation of the bottom sheet exiting from view. */
exit() {
if (!this._destroyed) {
this._animationState = 'hidden';
this._changeDetectorRef.markForCheck();
if (this._animationsDisabled) {
this._simulateAnimation(EXIT_ANIMATION);
}
}
}
ngOnDestroy() {
super.ngOnDestroy();
this._destroyed = true;
}
_simulateAnimation(name) {
this._ngZone.run(() => {
this._handleAnimationEvent(true, name);
setTimeout(() => this._handleAnimationEvent(false, name));
});
}
_handleAnimationEvent(isStart, animationName) {
const isEnter = animationName === ENTER_ANIMATION;
const isExit = animationName === EXIT_ANIMATION;
if (isEnter) {
this._trapFocus();
}
if (isEnter || isExit) {
this._animationStateChanged.emit({
toState: isEnter ? 'visible' : 'hidden',
phase: isStart ? 'start' : 'done',
});
}
}
_captureInitialFocus() { }
/** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: MtxDrawerContainer, deps: null, target: i0.ɵɵFactoryTarget.Component }); }
/** @nocollapse */ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.5", type: MtxDrawerContainer, isStandalone: true, selector: "mtx-drawer-container", host: { attributes: { "tabindex": "-1" }, listeners: { "animationstart": "_handleAnimationEvent(true, $event.animationName)", "animationend": "_handleAnimationEvent(false, $event.animationName)", "animationcancel": "_handleAnimationEvent(false, $event.animationName)" }, properties: { "class": "_drawerPosition", "class.mtx-drawer-container-animations-enabled": "!_animationsDisabled", "class.mtx-drawer-container-enter": "_animationState === \"visible\"", "class.mtx-drawer-container-exit": "_animationState === \"hidden\"", "id": "_config.id", "attr.role": "_config.role", "attr.aria-modal": "_config.ariaModal", "attr.aria-label": "_config.ariaLabel" }, classAttribute: "mtx-drawer-container" }, viewQueries: [{ propertyName: "_portalOutlet", first: true, predicate: CdkPortalOutlet, descendants: true, static: true }], usesInheritance: true, ngImport: i0, template: "<ng-template cdkPortalOutlet></ng-template>\n", styles: ["@keyframes _mtx-drawer-enter{0%{box-shadow:none;visibility:hidden}to{transform:none;visibility:visible}}@keyframes _mtx-drawer-exit{0%{transform:none;visibility:visible}to{box-shadow:none;visibility:hidden}}.mtx-drawer-container{display:block;width:100%;padding:8px 16px;overflow:auto;outline:0;box-sizing:border-box;position:relative;background-color:var(--mtx-drawer-container-background-color, var(--mat-sys-surface));color:var(--mtx-drawer-container-text-color, var(--mat-sys-on-surface-variant));box-shadow:var(--mtx-drawer-container-elevation-shadow, 0px 3px 1px -2px rgba(0, 0, 0, .2), 0px 2px 2px 0px rgba(0, 0, 0, .14), 0px 1px 5px 0px rgba(0, 0, 0, .12))}@media(forced-colors:active){.mtx-drawer-container{outline:1px solid}}.mtx-drawer-container-animations-enabled.mtx-drawer-container-enter{animation:_mtx-drawer-enter .15s cubic-bezier(0,0,.2,1) forwards}.mtx-drawer-container-animations-enabled.mtx-drawer-container-exit{animation:_mtx-drawer-exit .4s cubic-bezier(.25,.8,.25,1) backwards}.mtx-drawer-container-animations-enabled.mtx-drawer-right{transform:translate(100%)}.mtx-drawer-container-animations-enabled.mtx-drawer-left{transform:translate(-100%)}.mtx-drawer-container-animations-enabled.mtx-drawer-bottom{transform:translateY(100%)}.mtx-drawer-container-animations-enabled.mtx-drawer-top{transform:translateY(-100%)}.mtx-drawer-right{border-top-left-radius:var(--mtx-drawer-container-shape, var(--mat-sys-corner-large));border-bottom-left-radius:var(--mtx-drawer-container-shape, var(--mat-sys-corner-large))}.mtx-drawer-left{border-top-right-radius:var(--mtx-drawer-container-shape, var(--mat-sys-corner-large));border-bottom-right-radius:var(--mtx-drawer-container-shape, var(--mat-sys-corner-large))}.mtx-drawer-bottom{border-top-left-radius:var(--mtx-drawer-container-shape, var(--mat-sys-corner-large));border-top-right-radius:var(--mtx-drawer-container-shape, var(--mat-sys-corner-large))}.mtx-drawer-top{border-bottom-left-radius:var(--mtx-drawer-container-shape, var(--mat-sys-corner-large));border-bottom-right-radius:var(--mtx-drawer-container-shape, var(--mat-sys-corner-large))}\n"], dependencies: [{ kind: "directive", type: CdkPortalOutlet, selector: "[cdkPortalOutlet]", inputs: ["cdkPortalOutlet"], outputs: ["attached"], exportAs: ["cdkPortalOutlet"] }], changeDetection: i0.ChangeDetectionStrategy.Eager, encapsulation: i0.ViewEncapsulation.None }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: MtxDrawerContainer, decorators: [{
type: Component,
args: [{ selector: 'mtx-drawer-container', changeDetection: ChangeDetectionStrategy.Default, encapsulation: ViewEncapsulation.None, host: {
'class': 'mtx-drawer-container',
'[class]': '_drawerPosition',
'[class.mtx-drawer-container-animations-enabled]': '!_animationsDisabled',
'[class.mtx-drawer-container-enter]': '_animationState === "visible"',
'[class.mtx-drawer-container-exit]': '_animationState === "hidden"',
'tabindex': '-1',
'[id]': '_config.id',
'[attr.role]': '_config.role',
'[attr.aria-modal]': '_config.ariaModal',
'[attr.aria-label]': '_config.ariaLabel',
'(animationstart)': '_handleAnimationEvent(true, $event.animationName)',
'(animationend)': '_handleAnimationEvent(false, $event.animationName)',
'(animationcancel)': '_handleAnimationEvent(false, $event.animationName)',
}, imports: [CdkPortalOutlet], template: "<ng-template cdkPortalOutlet></ng-template>\n", styles: ["@keyframes _mtx-drawer-enter{0%{box-shadow:none;visibility:hidden}to{transform:none;visibility:visible}}@keyframes _mtx-drawer-exit{0%{transform:none;visibility:visible}to{box-shadow:none;visibility:hidden}}.mtx-drawer-container{display:block;width:100%;padding:8px 16px;overflow:auto;outline:0;box-sizing:border-box;position:relative;background-color:var(--mtx-drawer-container-background-color, var(--mat-sys-surface));color:var(--mtx-drawer-container-text-color, var(--mat-sys-on-surface-variant));box-shadow:var(--mtx-drawer-container-elevation-shadow, 0px 3px 1px -2px rgba(0, 0, 0, .2), 0px 2px 2px 0px rgba(0, 0, 0, .14), 0px 1px 5px 0px rgba(0, 0, 0, .12))}@media(forced-colors:active){.mtx-drawer-container{outline:1px solid}}.mtx-drawer-container-animations-enabled.mtx-drawer-container-enter{animation:_mtx-drawer-enter .15s cubic-bezier(0,0,.2,1) forwards}.mtx-drawer-container-animations-enabled.mtx-drawer-container-exit{animation:_mtx-drawer-exit .4s cubic-bezier(.25,.8,.25,1) backwards}.mtx-drawer-container-animations-enabled.mtx-drawer-right{transform:translate(100%)}.mtx-drawer-container-animations-enabled.mtx-drawer-left{transform:translate(-100%)}.mtx-drawer-container-animations-enabled.mtx-drawer-bottom{transform:translateY(100%)}.mtx-drawer-container-animations-enabled.mtx-drawer-top{transform:translateY(-100%)}.mtx-drawer-right{border-top-left-radius:var(--mtx-drawer-container-shape, var(--mat-sys-corner-large));border-bottom-left-radius:var(--mtx-drawer-container-shape, var(--mat-sys-corner-large))}.mtx-drawer-left{border-top-right-radius:var(--mtx-drawer-container-shape, var(--mat-sys-corner-large));border-bottom-right-radius:var(--mtx-drawer-container-shape, var(--mat-sys-corner-large))}.mtx-drawer-bottom{border-top-left-radius:var(--mtx-drawer-container-shape, var(--mat-sys-corner-large));border-top-right-radius:var(--mtx-drawer-container-shape, var(--mat-sys-corner-large))}.mtx-drawer-top{border-bottom-left-radius:var(--mtx-drawer-container-shape, var(--mat-sys-corner-large));border-bottom-right-radius:var(--mtx-drawer-container-shape, var(--mat-sys-corner-large))}\n"] }]
}], propDecorators: { _portalOutlet: [{
type: ViewChild,
args: [CdkPortalOutlet, { static: true }]
}] } });
/**
* Reference to a drawer dispatched from the drawer service.
*/
class MtxDrawerRef {
/** Instance of the component making up the content of the drawer. */
get instance() {
return this._ref.componentInstance;
}
/**
* `ComponentRef` of the component opened into the drawer. Will be
* null when the drawer is opened using a `TemplateRef`.
*/
get componentRef() {
return this._ref.componentRef;
}
constructor(_ref, config, containerInstance) {
this._ref = _ref;
/** Subject for notifying the user that the drawer has been dismissed. */
this._afterDismissed = new Subject();
/** Subject for notifying the user that the drawer has opened and appeared. */
this._afterOpened = new Subject();
this.containerInstance = containerInstance;
this.disableClose = config.disableClose;
this.id = _ref.id;
// Emit when opening animation completes
containerInstance._animationStateChanged
.pipe(filter(event => event.phase === 'done' && event.toState === 'visible'), take(1))
.subscribe(() => {
this._afterOpened.next();
this._afterOpened.complete();
});
// Dispose overlay when closing animation is complete
containerInstance._animationStateChanged
.pipe(filter(event => event.phase === 'done' && event.toState === 'hidden'), take(1))
.subscribe(() => {
clearTimeout(this._closeFallbackTimeout);
this._ref.close(this._result);
});
_ref.overlayRef.detachments().subscribe(() => {
this._ref.close(this._result);
});
merge(this.backdropClick(), this.keydownEvents().pipe(filter(event => event.keyCode === ESCAPE))).subscribe(event => {
if (!this.disableClose &&
(event.type !== 'keydown' || !hasModifierKey(event))) {
event.preventDefault();
this.dismiss();
}
});
}
/**
* Dismisses the drawer.
* @param result Data to be passed back to the drawer opener.
*/
dismiss(result) {
if (this.containerInstance && !this._afterDismissed.closed) {
// Transition the backdrop in parallel to the drawer.
this.containerInstance._animationStateChanged
.pipe(filter(event => event.phase === 'start'), take(1))
.subscribe(event => {
// The logic that disposes of the overlay depends on the exit animation completing, however
// it isn't guaranteed if the parent view is destroyed while it's running. Add a fallback
// timeout which will clean everything up if the animation hasn't fired within the specified
// amount of time plus 100ms. We don't need to run this outside the NgZone, because for the
// vast majority of cases the timeout will have been cleared before it has fired.
this._closeFallbackTimeout = setTimeout(() => this._ref.close(this._result), 500);
this._ref.overlayRef.detachBackdrop();
});
this._result = result;
this.containerInstance.exit();
this.containerInstance = null;
}
}
/** Gets an observable that is notified when the drawer is finished closing. */
afterDismissed() {
return this._ref.closed;
}
/** Gets an observable that is notified when the drawer has opened and appeared. */
afterOpened() {
return this._afterOpened;
}
/**
* Gets an observable that emits when the overlay's backdrop has been clicked.
*/
backdropClick() {
return this._ref.backdropClick;
}
/**
* Gets an observable that emits when keydown events are targeted on the overlay.
*/
keydownEvents() {
return this._ref.keydownEvents;
}
}
/** Injection token that can be used to access the data that was passed in to a drawer. */
const MTX_DRAWER_DATA = new InjectionToken('MtxDrawerData');
/** Injection token that can be used to specify default drawer options. */
const MTX_DRAWER_DEFAULT_OPTIONS = new InjectionToken('mtx-drawer-default-options');
// Counter for unique drawer ids.
let uniqueId = 0;
/**
* Service to trigger Material Design bottom sheets.
*/
class MtxDrawer {
constructor() {
this._overlay = inject(Overlay);
this._parentDrawer = inject(MtxDrawer, { optional: true, skipSelf: true });
this._defaultOptions = inject(MTX_DRAWER_DEFAULT_OPTIONS, { optional: true });
this._openDrawersAtThisLevel = [];
this._afterAllDismissedAtThisLevel = new Subject();
this._afterOpenedAtThisLevel = new Subject();
this._dialog = inject(Dialog);
/**
* Stream that emits when all open drawer have finished closing.
* Will emit on subscribe if there are no open drawers to begin with.
*/
this.afterAllDismissed = defer(() => this.openDrawers.length
? this._getAfterAllDismissed()
: this._getAfterAllDismissed().pipe(startWith(undefined)));
}
/** Keeps track of the currently-open dialogs. */
get openDrawers() {
return this._parentDrawer ? this._parentDrawer.openDrawers : this._openDrawersAtThisLevel;
}
/** Stream that emits when a drawer has been opened. */
get afterOpened() {
return this._parentDrawer ? this._parentDrawer.afterOpened : this._afterOpenedAtThisLevel;
}
_getAfterAllDismissed() {
const parent = this._parentDrawer;
return parent ? parent._getAfterAllDismissed() : this._afterAllDismissedAtThisLevel;
}
open(componentOrTemplateRef, config) {
let drawerRef;
const _config = { ...(this._defaultOptions || new MtxDrawerConfig()), ...config };
_config.id = _config.id || `mtx-drawer-${uniqueId++}`;
_config.width =
_config.position === 'left' || _config.position === 'right'
? coerceCssPixelValue(_config.width)
: '100vw';
_config.height =
_config.position === 'top' || _config.position === 'bottom'
? coerceCssPixelValue(_config.height)
: '100vh';
this._dialog.open(componentOrTemplateRef, {
..._config,
// Disable closing since we need to sync it up to the animation ourselves.
disableClose: true,
// Disable closing on detachments so that we can sync up the animation.
closeOnOverlayDetachments: false,
container: {
type: MtxDrawerContainer,
providers: () => [
// Provide our config as the CDK config as well since it has the same interface as the
// CDK one, but it contains the actual values passed in by the user for things like
// `disableClose` which we disable for the CDK dialog since we handle it ourselves.
{ provide: MtxDrawerConfig, useValue: _config },
{ provide: DialogConfig, useValue: _config },
],
},
scrollStrategy: _config.scrollStrategy || this._overlay.scrollStrategies.block(),
positionStrategy: this._overlay.position().global()[_config.position]('0'),
templateContext: () => ({ drawerRef }),
providers: (cdkRef, _cdkConfig, container) => {
drawerRef = new MtxDrawerRef(cdkRef, _config, container);
return [
{ provide: MtxDrawerRef, useValue: drawerRef },
{ provide: MTX_DRAWER_DATA, useValue: _config.data },
];
},
});
this.openDrawers.push(drawerRef);
this.afterOpened.next(drawerRef);
drawerRef.afterDismissed().subscribe(() => {
const index = this.openDrawers.indexOf(drawerRef);
if (index > -1) {
this.openDrawers.splice(index, 1);
if (!this.openDrawers.length) {
this._getAfterAllDismissed().next();
}
}
});
return drawerRef;
}
/**
* Dismisses all of the currently-open drawers.
*/
dismissAll() {
this._dismissDrawers(this.openDrawers);
}
/**
* Finds an open drawer by its id.
* @param id ID to use when looking up the drawer.
*/
getDrawerById(id) {
return this.openDrawers.find(drawer => drawer.id === id);
}
ngOnDestroy() {
// Only dismiss the drawers at this level on destroy
// since the parent service may still be active.
this._dismissDrawers(this._openDrawersAtThisLevel);
this._afterAllDismissedAtThisLevel.complete();
this._afterOpenedAtThisLevel.complete();
}
_dismissDrawers(drawers) {
let i = drawers.length;
while (i--) {
drawers[i].dismiss();
}
}
/** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: MtxDrawer, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
/** @nocollapse */ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: MtxDrawer, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: MtxDrawer, decorators: [{
type: Injectable,
args: [{ providedIn: 'root' }]
}] });
class MtxDrawerModule {
/** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: MtxDrawerModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
/** @nocollapse */ static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "21.2.5", ngImport: i0, type: MtxDrawerModule, imports: [DialogModule, PortalModule, MtxDrawerContainer], exports: [MtxDrawerContainer, BidiModule] }); }
/** @nocollapse */ static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: MtxDrawerModule, providers: [MtxDrawer], imports: [DialogModule, PortalModule, BidiModule] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: MtxDrawerModule, decorators: [{
type: NgModule,
args: [{
imports: [DialogModule, PortalModule, MtxDrawerContainer],
exports: [MtxDrawerContainer, BidiModule],
providers: [MtxDrawer],
}]
}] });
/**
* Generated bundle index. Do not edit.
*/
export { MTX_DRAWER_DATA, MTX_DRAWER_DEFAULT_OPTIONS, MtxDrawer, MtxDrawerConfig, MtxDrawerContainer, MtxDrawerModule, MtxDrawerRef };
//# sourceMappingURL=mtxDrawer.mjs.map