license import { AnimationEvent } from '@angular/animations' ; import { FocusMonitor , FocusOrigin , FocusTrap , FocusTrapFactory } from '@angular/cdk/a11y' ; import { Directionality } from '@angular/cdk/bidi' ; import { coerceBooleanProperty } from '@angular/cdk/coercion' ; import { ESCAPE } from '@angular/cdk/keycodes' ; import { Platform } from '@angular/cdk/platform' ; import { CdkScrollable , ScrollDispatcher , ViewportRuler } from '@angular/cdk/scrolling' ; import { DOCUMENT } from '@angular/common' ; import { AfterContentChecked , AfterContentInit , ChangeDetectionStrategy , ChangeDetectorRef , Component , ContentChild , ContentChildren , DoCheck , ElementRef , EventEmitter , forwardRef , Inject , InjectionToken , Input , NgZone , OnDestroy , Optional , Output , QueryList , ViewChild , ViewEncapsulation , } from '@angular/core' ; import { fromEvent , merge , Observable , Subject } from 'rxjs' ; import { debounceTime , filter , map , startWith , take , takeUntil , distinctUntilChanged , } from 'rxjs/operators' ; import { matDrawerAnimations } from './drawer-animations' ; import { ANIMATION_MODULE_TYPE } from '@angular/platform-browser/animations' ; docs-private export function throwMatDuplicatedDrawerError ( position ) { throw Error ( `A drawer was already declared for 'position="${ position }"'` ) ; } /** Result of the toggle promise that indicates the state of the drawer. */ export type MatDrawerToggleResult = 'open' | 'close' ; /** Configures whether drawers should use auto sizing by default. */ export const MAT_DRAWER_DEFAULT_AUTOSIZE = new InjectionToken < boolean > ( 'MAT_DRAWER_DEFAULT_AUTOSIZE' , { providedIn : 'root' , factory : MAT_DRAWER_DEFAULT_AUTOSIZE_FACTORY , } ) ; docs-private export function MAT_DRAWER_DEFAULT_AUTOSIZE_FACTORY ( ) { return false ; } @ Component ( { moduleId : module . id , selector : 'mat-drawer-content' , template : '<ng-content></ng-content>' , host : { 'class' : 'mat-drawer-content' , '[style.margin-left.px]' : '_container._contentMargins.left' , '[style.margin-right.px]' : '_container._contentMargins.right' , } , changeDetection : ChangeDetectionStrategy . OnPush , encapsulation : ViewEncapsulation . None , } ) export class MatDrawerContent extends CdkScrollable implements AfterContentInit { constructor ( private _changeDetectorRef , @ Inject ( forwardRef ( ( ) => MatDrawerContainer ) ) public _container , elementRef , scrollDispatcher , ngZone ) { super ( elementRef , scrollDispatcher , ngZone ) ; } ngAfterContentInit ( ) { this . _container . _contentMarginChanges . subscribe ( ( ) => { this . _changeDetectorRef . markForCheck ( ) ; } ) ; } } /**
 * This component corresponds to a drawer that can be opened on the drawer container.
 */ @ Component ( { moduleId : module . id , selector : 'mat-drawer' , exportAs : 'matDrawer' , templateUrl : 'drawer.html' , animations : [ matDrawerAnimations . transformDrawer ] , host : { 'class' : 'mat-drawer' , '[@transform]' : '_animationState' , '(@transform.start)' : '_animationStarted.next($event)' , '(@transform.done)' : '_animationEnd.next($event)' , '[attr.align]' : 'null' , '[class.mat-drawer-end]' : 'position === "end"' , '[class.mat-drawer-over]' : 'mode === "over"' , '[class.mat-drawer-push]' : 'mode === "push"' , '[class.mat-drawer-side]' : 'mode === "side"' , 'tabIndex' : '-1' , } , changeDetection : ChangeDetectionStrategy . OnPush , encapsulation : ViewEncapsulation . None , } ) export class MatDrawer implements AfterContentInit , AfterContentChecked , OnDestroy { private _focusTrap : FocusTrap ; private _elementFocusedBeforeDrawerWasOpened : HTMLElement | null = null ; /** Whether the drawer is initialized. Used for disabling the initial animation. */ private _enableAnimations = false ; /** The side that the drawer is attached to. */ @ Input ( ) get position ( ) : 'start' | 'end' { return this . _position ; } set position ( value ) { value = value === 'end' ? 'end' : 'start' ; if ( value != this . _position ) { this . _position = value ; this . onPositionChanged . emit ( ) ; } } private _position : 'start' | 'end' = 'start' ; /** Mode of the drawer; one of 'over', 'push' or 'side'. */ @ Input ( ) get mode ( ) : 'over' | 'push' | 'side' { return this . _mode ; } set mode ( value ) { this . _mode = value ; this . _modeChanged . next ( ) ; } private _mode : 'over' | 'push' | 'side' = 'over' ; /** Whether the drawer can be closed with the escape key or by clicking on the backdrop. */ @ Input ( ) get disableClose ( ) : boolean { return this . _disableClose ; } set disableClose ( value ) { this . _disableClose = coerceBooleanProperty ( value ) ; } private _disableClose : boolean = false ; /** Whether the drawer should focus the first focusable element automatically when opened. */ @ Input ( ) get autoFocus ( ) : boolean { return this . _autoFocus ; } set autoFocus ( value ) { this . _autoFocus = coerceBooleanProperty ( value ) ; } private _autoFocus : boolean = true ; /** How the sidenav was opened (keypress, mouse click etc.) */ private _openedVia : FocusOrigin | null ; /** Emits whenever the drawer has started animating. */ _animationStarted = new Subject < AnimationEvent > ( ) ; /** Emits whenever the drawer is done animating. */ _animationEnd = new Subject < AnimationEvent > ( ) ; /** Current state of the sidenav animation. */ _animationState : 'open-instant' | 'open' | 'void' = 'void' ; /** Event emitted when the drawer open state is changed. */ @ Output ( ) readonly openedChange : EventEmitter < boolean > = new EventEmitter < boolean > ( true ) ; /** Event emitted when the drawer has been opened. */ @ Output ( 'opened' ) get _openedStream ( ) : Observable < void > { return this . openedChange . pipe ( filter ( o => o ) , map ( ( ) => { } ) ) ; } /** Event emitted when the drawer has started opening. */ @ Output ( ) get openedStart ( ) : Observable < void > { return this . _animationStarted . pipe ( filter ( e => e . fromState !== e . toState && e . toState . indexOf ( 'open' ) === 0 ) , map ( ( ) => { } ) ) ; } /** Event emitted when the drawer has been closed. */ @ Output ( 'closed' ) get _closedStream ( ) : Observable < void > { return this . openedChange . pipe ( filter ( o => ! o ) , map ( ( ) => { } ) ) ; } /** Event emitted when the drawer has started closing. */ @ Output ( ) get closedStart ( ) : Observable < void > { return this . _animationStarted . pipe ( filter ( e => e . fromState !== e . toState && e . toState === 'void' ) , map ( ( ) => { } ) ) ; } /** Emits when the component is destroyed. */ private readonly _destroyed = new Subject < void > ( ) ; /** Event emitted when the drawer's position changes. */ @ Output ( 'positionChanged' ) onPositionChanged : EventEmitter < void > = new EventEmitter < void > ( ) ; /**
     * An observable that emits when the drawer mode changes. This is used by the drawer container to
     * to know when to when the mode changes so it can adapt the margins on the content.
     */ readonly _modeChanged = new Subject ( ) ; get _isFocusTrapEnabled ( ) : boolean { return this . opened && this . mode !== 'side' ; } constructor ( private _elementRef , private _focusTrapFactory , private _focusMonitor , private _platform , private _ngZone , @ Optional ( ) @ Inject ( DOCUMENT ) private _doc ) { this . openedChange . subscribe ( ( opened ) => { if ( opened ) { if ( this . _doc ) { this . _elementFocusedBeforeDrawerWasOpened = this . _doc . activeElement as HTMLElement ; } if ( this . _isFocusTrapEnabled && this . _focusTrap ) { this . _trapFocus ( ) ; } } else { this . _restoreFocus ( ) ; } } ) ; /**
         * Listen to `keydown` events outside the zone so that change detection is not run every
         * time a key is pressed. Instead we re-enter the zone only if the `ESC` key is pressed
         * and we don't have close disabled.
         */ this . _ngZone . runOutsideAngular ( ( ) => { fromEvent < KeyboardEvent > ( this . _elementRef . nativeElement , 'keydown' ) . pipe ( filter ( event => event . keyCode === ESCAPE && ! this . disableClose ) , takeUntil ( this . _destroyed ) ) . subscribe ( event => this . _ngZone . run ( ( ) => { this . close ( ) ; event . stopPropagation ( ) ; } ) ) ; } ) ; this . _animationEnd . pipe ( distinctUntilChanged ( ( x , y ) => { return x . fromState === y . fromState && x . toState === y . toState ; } ) ) . subscribe ( ( event ) => { const { fromState , toState } = event ; if ( ( toState . indexOf ( 'open' ) === 0 && fromState === 'void' ) || ( toState === 'void' && fromState . indexOf ( 'open' ) === 0 ) ) { this . openedChange . emit ( this . _opened ) ; } } ) ; } /** Traps focus inside the drawer. */ private _trapFocus ( ) { if ( ! this . autoFocus ) { return ; } this . _focusTrap . focusInitialElementWhenReady ( ) . then ( hasMovedFocus => { if ( ! hasMovedFocus && typeof this . _elementRef . nativeElement . focus === 'function' ) { this . _elementRef . nativeElement . focus ( ) ; } } ) ; } /**
     * If focus is currently inside the drawer, restores it to where it was before the drawer
     * opened.
     */ private _restoreFocus ( ) { if ( ! this . autoFocus ) { return ; } const activeEl = this . _doc && this . _doc . activeElement ; if ( activeEl && this . _elementRef . nativeElement . contains ( activeEl ) ) { if ( this . _elementFocusedBeforeDrawerWasOpened instanceof HTMLElement ) { this . _focusMonitor . focusVia ( this . _elementFocusedBeforeDrawerWasOpened , this . _openedVia ) ; } else { this . _elementRef . nativeElement . blur ( ) ; } } this . _elementFocusedBeforeDrawerWasOpened = null ; this . _openedVia = null ; } ngAfterContentInit ( ) { this . _focusTrap = this . _focusTrapFactory . create ( this . _elementRef . nativeElement ) ; this . _focusTrap . enabled = this . _isFocusTrapEnabled ; } ngAfterContentChecked ( ) { if ( this . _platform . isBrowser ) { this . _enableAnimations = true ; } } ngOnDestroy ( ) { if ( this . _focusTrap ) { this . _focusTrap . destroy ( ) ; } this . _animationStarted . complete ( ) ; this . _animationEnd . complete ( ) ; this . _destroyed . next ( ) ; this . _destroyed . complete ( ) ; } /**
     * Whether the drawer is opened. We overload this because we trigger an event when it
     * starts or end.
     */ @ Input ( ) get opened ( ) : boolean { return this . _opened ; } set opened ( value ) { this . toggle ( coerceBooleanProperty ( value ) ) ; } private _opened : boolean = false ; param openedVia open ( openedVia ? ) { return this . toggle ( true , openedVia ) ; } /** Close the drawer. */ close ( ) { return this . toggle ( false ) ; } param isOpen param openedVia toggle ( isOpen = ! this . opened , openedVia = 'program' ) { this . _opened = isOpen ; if ( isOpen ) { this . _animationState = this . _enableAnimations ? 'open' : 'open-instant' ; this . _openedVia = openedVia ; } else { this . _animationState = 'void' ; this . _restoreFocus ( ) ; } if ( this . _focusTrap ) { this . _focusTrap . enabled = this . _isFocusTrapEnabled ; } return new Promise < MatDrawerToggleResult > ( resolve => { this . openedChange . pipe ( take ( 1 ) ) . subscribe ( open => resolve ( open ? 'open' : 'close' ) ) ; } ) ; } get _width ( ) : number { return this . _elementRef . nativeElement ? ( this . _elementRef . nativeElement . offsetWidth || 0 ) : 0 ; } } /**
 * `<mat-drawer-container>` component.
 *
 * This is the parent component to one or two `<mat-drawer>`s that validates the state internally
 * and coordinates the backdrop and content styling.
 */ @ Component ( { moduleId : module . id , selector : 'mat-drawer-container' , exportAs : 'matDrawerContainer' , templateUrl : 'drawer-container.html' , styleUrls : [ 'drawer.css' ] , host : { 'class' : 'mat-drawer-container' , '[class.mat-drawer-container-explicit-backdrop]' : '_backdropOverride' , } , changeDetection : ChangeDetectionStrategy . OnPush , encapsulation : ViewEncapsulation . None , } ) export class MatDrawerContainer implements AfterContentInit , DoCheck , OnDestroy { @ ContentChildren ( MatDrawer ) _drawers : QueryList < MatDrawer > ; @ ContentChild ( MatDrawerContent ) _content : MatDrawerContent ; @ ViewChild ( MatDrawerContent ) _userContent : MatDrawerContent ; /** The drawer child with the `start` position. */ get start ( ) : MatDrawer | null { return this . _start ; } /** The drawer child with the `end` position. */ get end ( ) : MatDrawer | null { return this . _end ; } /**
     * Whether to automatically resize the container whenever
     * the size of any of its drawers changes.
     *
     * **Use at your own risk!** Enabling this option can cause layout thrashing by measuring
     * the drawers on every change detection cycle. Can be configured globally via the
     * `MAT_DRAWER_DEFAULT_AUTOSIZE` token.
     */ @ Input ( ) get autosize ( ) : boolean { return this . _autosize ; } set autosize ( value ) { this . _autosize = coerceBooleanProperty ( value ) ; } private _autosize : boolean ; /**
     * Whether the drawer container should have a backdrop while one of the sidenavs is open.
     * If explicitly set to `true`, the backdrop will be enabled for drawers in the `side`
     * mode as well.
     */ @ Input ( ) get hasBackdrop ( ) { if ( this . _backdropOverride == null ) { return ! this . _start || this . _start . mode !== 'side' || ! this . _end || this . _end . mode !== 'side' ; } return this . _backdropOverride ; } set hasBackdrop ( value ) { this . _backdropOverride = value == null ? null : coerceBooleanProperty ( value ) ; } _backdropOverride : boolean | null ; /** Event emitted when the drawer backdrop is clicked. */ @ Output ( ) readonly backdropClick : EventEmitter < void > = new EventEmitter < void > ( ) ; /** The drawer at the start/end position, independent of direction. */ private _start : MatDrawer | null ; private _end : MatDrawer | null ; /**
     * The drawer at the left/right. When direction changes, these will change as well.
     * They're used as aliases for the above to set the left/right style properly.
     * In LTR, _left == _start and _right == _end.
     * In RTL, _left == _end and _right == _start.
     */ private _left : MatDrawer | null ; private _right : MatDrawer | null ; /** Emits when the component is destroyed. */ private readonly _destroyed = new Subject < void > ( ) ; /** Emits on every ngDoCheck. Used for debouncing reflows. */ private readonly _doCheckSubject = new Subject < void > ( ) ; /**
     * Margins to be applied to the content. These are used to push / shrink the drawer content when a
     * drawer is open. We use margin rather than transform even for push mode because transform breaks
     * fixed position elements inside of the transformed element.
     */ _contentMargins : { left : number | null ; right : number | null ; } = { left : null , right : null } ; readonly _contentMarginChanges = new Subject < { left : number | null ; right : number | null ; } > ( ) ; /** Reference to the CdkScrollable instance that wraps the scrollable content. */ get scrollable ( ) : CdkScrollable { return this . _userContent || this . _content ; } constructor ( @ Optional ( ) private _dir , private _element , private _ngZone , private _changeDetectorRef , @ Inject ( MAT_DRAWER_DEFAULT_AUTOSIZE ) defaultAutosize = false , @ Optional ( ) @ Inject ( ANIMATION_MODULE_TYPE ) private _animationMode ? , deprecated breaking-change @ Optional ( ) viewportRuler ? ) { if ( _dir ) { _dir . change . pipe ( takeUntil ( this . _destroyed ) ) . subscribe ( ( ) => { this . _validateDrawers ( ) ; this . _updateContentMargins ( ) ; } ) ; } if ( viewportRuler ) { viewportRuler . change ( ) . pipe ( takeUntil ( this . _destroyed ) ) . subscribe ( ( ) => this . _updateContentMargins ( ) ) ; } this . _autosize = defaultAutosize ; } ngAfterContentInit ( ) { this . _drawers . changes . pipe ( startWith ( null ) ) . subscribe ( ( ) => { this . _validateDrawers ( ) ; this . _drawers . forEach ( ( drawer ) => { this . _watchDrawerToggle ( drawer ) ; this . _watchDrawerPosition ( drawer ) ; this . _watchDrawerMode ( drawer ) ; } ) ; if ( ! this . _drawers . length || this . _isDrawerOpen ( this . _start ) || this . _isDrawerOpen ( this . _end ) ) { this . _updateContentMargins ( ) ; } this . _changeDetectorRef . markForCheck ( ) ; } ) ; this . _doCheckSubject . pipe ( debounceTime ( 10 ) , takeUntil ( this . _destroyed ) ) . subscribe ( ( ) => this . _updateContentMargins ( ) ) ; } ngOnDestroy ( ) { this . _doCheckSubject . complete ( ) ; this . _destroyed . next ( ) ; this . _destroyed . complete ( ) ; } /** Calls `open` of both start and end drawers */ open ( ) { this . _drawers . forEach ( drawer => drawer . open ( ) ) ; } /** Calls `close` of both start and end drawers */ close ( ) { this . _drawers . forEach ( drawer => drawer . close ( ) ) ; } ngDoCheck ( ) { if ( this . _autosize && this . _isPushed ( ) ) { this . _ngZone . runOutsideAngular ( ( ) => this . _doCheckSubject . next ( ) ) ; } } /**
     * Subscribes to drawer events in order to set a class on the main container element when the
     * drawer is open and the backdrop is visible. This ensures any overflow on the container element
     * is properly hidden.
     */ private _watchDrawerToggle ( drawer ) { drawer . _animationStarted . pipe ( filter ( ( event ) => event . fromState !== event . toState ) , takeUntil ( this . _drawers . changes ) ) . subscribe ( ( event ) => { if ( event . toState !== 'open-instant' && this . _animationMode !== 'NoopAnimations' ) { this . _element . nativeElement . classList . add ( 'mat-drawer-transition' ) ; } this . _updateContentMargins ( ) ; this . _changeDetectorRef . markForCheck ( ) ; } ) ; if ( drawer . mode !== 'side' ) { drawer . openedChange . pipe ( takeUntil ( this . _drawers . changes ) ) . subscribe ( ( ) => this . _setContainerClass ( drawer . opened ) ) ; } } /**
     * Subscribes to drawer onPositionChanged event in order to
     * re-validate drawers when the position changes.
     */ private _watchDrawerPosition ( drawer ) { if ( ! drawer ) { return ; } drawer . onPositionChanged . pipe ( takeUntil ( this . _drawers . changes ) ) . subscribe ( ( ) => { this . _ngZone . onMicrotaskEmpty . asObservable ( ) . pipe ( take ( 1 ) ) . subscribe ( ( ) => { this . _validateDrawers ( ) ; } ) ; } ) ; } /** Subscribes to changes in drawer mode so we can run change detection. */ private _watchDrawerMode ( drawer ) { if ( drawer ) { drawer . _modeChanged . pipe ( takeUntil ( merge ( this . _drawers . changes , this . _destroyed ) ) ) . subscribe ( ( ) => { this . _updateContentMargins ( ) ; this . _changeDetectorRef . markForCheck ( ) ; } ) ; } } /** Toggles the 'mat-drawer-opened' class on the main 'mat-drawer-container' element. */ private _setContainerClass ( isAdd ) { if ( isAdd ) { this . _element . nativeElement . classList . add ( 'mat-drawer-opened' ) ; } else { this . _element . nativeElement . classList . remove ( 'mat-drawer-opened' ) ; } } /** Validate the state of the drawer children components. */ private _validateDrawers ( ) { this . _start = this . _end = null ; this . _drawers . forEach ( drawer => { if ( drawer . position == 'end' ) { if ( this . _end != null ) { throwMatDuplicatedDrawerError ( 'end' ) ; } this . _end = drawer ; } else { if ( this . _start != null ) { throwMatDuplicatedDrawerError ( 'start' ) ; } this . _start = drawer ; } } ) ; this . _right = this . _left = null ; if ( this . _dir && this . _dir . value === 'rtl' ) { this . _left = this . _end ; this . _right = this . _start ; } else { this . _left = this . _start ; this . _right = this . _end ; } } /** Whether the container is being pushed to the side by one of the drawers. */ private _isPushed ( ) { return ( this . _isDrawerOpen ( this . _start ) && this . _start . mode != 'over' ) || ( this . _isDrawerOpen ( this . _end ) && this . _end . mode != 'over' ) ; } _onBackdropClicked ( ) { this . backdropClick . emit ( ) ; this . _closeModalDrawer ( ) ; } _closeModalDrawer ( ) { [ this . _start , this . _end ] . filter ( drawer => drawer && ! drawer . disableClose && this . _canHaveBackdrop ( drawer ) ) . forEach ( drawer => drawer ! . close ( ) ) ; } _isShowingBackdrop ( ) { return ( this . _isDrawerOpen ( this . _start ) && this . _canHaveBackdrop ( this . _start ) ) || ( this . _isDrawerOpen ( this . _end ) && this . _canHaveBackdrop ( this . _end ) ) ; } private _canHaveBackdrop ( drawer ) { return drawer . mode !== 'side' || ! ! this . _backdropOverride ; } private _isDrawerOpen ( drawer ) { return drawer != null && drawer . opened ; } /**
     * Recalculates and updates the inline styles for the content. Note that this should be used
     * sparingly, because it causes a reflow.
     */ private _updateContentMargins ( ) { let left = 0 ; let right = 0 ; if ( this . _left && this . _left . opened ) { if ( this . _left . mode == 'side' ) { left += this . _left . _width ; } else if ( this . _left . mode == 'push' ) { let width = this . _left . _width ; left += width ; right -= width ; } } if ( this . _right && this . _right . opened ) { if ( this . _right . mode == 'side' ) { right += this . _right . _width ; } else if ( this . _right . mode == 'push' ) { let width = this . _right . _width ; right += width ; left -= width ; } } left = left || null ! ; right = right || null ! ; if ( left !== this . _contentMargins . left || right !== this . _contentMargins . right ) { this . _contentMargins = { left , right } ; this . _ngZone . run ( ( ) => this . _contentMarginChanges . next ( this . _contentMargins ) ) ; } } }