import { AnimationEvent } from '@angular/animations' ; import { ConfigurableFocusTrapFactory , FocusMonitor , FocusOrigin , FocusTrap , } from '@angular/cdk/a11y' ; import { ESCAPE , hasModifierKey } from '@angular/cdk/keycodes' ; import { BreakpointObserver } from '@angular/cdk/layout' ; import { Platform } from '@angular/cdk/platform' ; import { ScrollDispatcher , ViewportRuler } from '@angular/cdk/scrolling' ; import { DOCUMENT } from '@angular/common' ; import { AfterContentChecked , AfterContentInit , ChangeDetectionStrategy , ChangeDetectorRef , Component , ContentChild , ContentChildren , ElementRef , EventEmitter , forwardRef , HostBinding , HostListener , Inject , NgZone , OnDestroy , Optional , Output , QueryList , ViewChild , ViewEncapsulation , } from '@angular/core' ; import { ANIMATION_MODULE_TYPE } from '@angular/platform-browser/animations' ; import { NavigationStart , Router } from '@angular/router' ; import { fromEvent , merge , NEVER , Observable , Subject } from 'rxjs' ; import { distinctUntilChanged , filter , map , mapTo , startWith , take , takeUntil , } from 'rxjs/operators' ; import { SbbSidebarBase , SbbSidebarContainerBase , SbbSidebarContentBase , SbbSidebarMobileCapableContainer , SBB_SIDEBAR_CONTAINER , } from '../sidebar-base' ; import { sbbSidebarAnimations } from './sidebar-animations' ; /** Result of the toggle promise that indicates the state of the sidebar. */ export type SbbSidebarToggleResult = 'open' | 'close' ; /** Sidebar display modes. */ export type SbbSidebarMode = 'over' | 'side' ; @ Component ( { selector : 'sbb-sidebar-content' , template : '<ng-content></ng-content>' , host : { class : 'sbb-sidebar-content sbb-scrollbar' , '[style.margin-left.px]' : '_container._contentMargins.left' , } , changeDetection : ChangeDetectionStrategy . OnPush , encapsulation : ViewEncapsulation . None , } ) export class SbbSidebarContent extends SbbSidebarContentBase implements AfterContentInit { constructor ( private _changeDetectorRef , @ Inject ( forwardRef ( ( ) => SbbSidebarContainer ) ) public _container , elementRef , scrollDispatcher , ngZone ) { super ( elementRef , scrollDispatcher , ngZone ) ; } ngAfterContentInit ( ) { this . _container . _contentMarginChanges . subscribe ( ( ) => { this . _changeDetectorRef . markForCheck ( ) ; } ) ; } } @ Component ( { selector : 'sbb-sidebar' , exportAs : 'sbbSidebar' , templateUrl : './sidebar.html' , animations : [ sbbSidebarAnimations . transformSidebar ] , host : { class : 'sbb-sidebar' , tabIndex : '-1' , '[attr.align]' : 'null' , '[class.sbb-sidebar-over]' : 'mode === "over"' , '[class.sbb-sidebar-side]' : 'mode === "side"' , '[class.sbb-sidebar-opened]' : 'opened' , } , changeDetection : ChangeDetectionStrategy . OnPush , encapsulation : ViewEncapsulation . None , } ) export class SbbSidebar extends SbbSidebarBase implements AfterContentInit , AfterContentChecked , OnDestroy { _labelCloseSidebar : string = typeof $localize === 'function' ? $localize `:Button label to close the sidebar@@sbbSidebarCloseSidebar:Close Sidebar` : 'Close Sidebar' ; /** Whether the sidebar is initialized. Used for disabling the initial animation. */ private _enableAnimations = false ; /** Mode of the sidebar; one of 'over', or 'side'. */ get mode ( ) : SbbSidebarMode { return this . _mode ; } set mode ( value ) { this . _mode = value ; this . _updateFocusTrapState ( ) ; this . _modeChanged . next ( ) ; } /**
     * Whether the sidebar is opened. We overload this because we trigger an event when it
     * starts or end.
     */ get opened ( ) : boolean { return this . _opened ; } /** Event emitted when the sidebar 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 sidebar has started closing. */ @ Output ( ) get closedStart ( ) : Observable < void > { return this . _animationStarted . pipe ( filter ( ( e ) => e . fromState !== e . toState && e . toState === 'void' ) , map ( ( ) => { } ) ) ; } private _focusTrap : FocusTrap ; private _elementFocusedBeforeSidebarWasOpened : HTMLElement | null = null ; private _mode : SbbSidebarMode = 'side' ; private _opened : boolean = true ; /** How the sidebar was opened (keypress, mouse click etc.) */ private _openedVia : FocusOrigin | null ; /** Emits whenever the sidebar has started animating. */ _animationStarted : Subject < AnimationEvent > = new Subject < AnimationEvent > ( ) ; /** Emits whenever the sidebar is done animating. */ _animationEnd : Subject < AnimationEvent > = new Subject < AnimationEvent > ( ) ; /** Current state of the sidebar animation. */ @ HostBinding ( '@transform' ) _animationState : 'open-instant' | 'open' | 'void' = 'void' ; /** Event emitted when the sidebar open state is changed. */ @ Output ( ) readonly openedChange : EventEmitter < boolean > = new EventEmitter < boolean > ( true ) ; /** Event emitted when the sidebar has been opened. */ @ Output ( 'opened' ) _openedStream : Observable < void > = this . openedChange . pipe ( filter ( ( o ) => o ) , map ( ( ) => { } ) ) ; /** Event emitted when the sidebar has been closed. */ @ Output ( 'closed' ) _closedStream : Observable < void > = this . openedChange . pipe ( filter ( ( o ) => ! o ) , map ( ( ) => { } ) ) ; /** Emits when the component is destroyed. */ private readonly _destroyed = new Subject < void > ( ) ; /**
     * An observable that emits when the sidebar mode changes. This is used by the sidebar container to
     * to know when to when the mode changes so it can adapt the margins on the content.
     */ readonly _modeChanged = new Subject < void > ( ) ; constructor ( private _elementRef , private _focusTrapFactory , private _focusMonitor , private _platform , private _ngZone , @ Optional ( ) @ Inject ( DOCUMENT ) private _doc , @ Inject ( SBB_SIDEBAR_CONTAINER ) public override _container , @ Optional ( ) private _router ) { super ( _container ) ; this . openedChange . subscribe ( ( opened ) => { if ( opened ) { if ( this . _doc ) { this . _elementFocusedBeforeSidebarWasOpened = this . _doc . activeElement as HTMLElement ; } this . _takeFocus ( ) ; } else if ( this . _isFocusWithinSidebar ( ) ) { 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.
         * Additionally listen to router navigation start events to close the sidebar.
         */ this . _ngZone . runOutsideAngular ( ( ) => { merge ( ( fromEvent ( this . _elementRef . nativeElement , 'keydown' ) as Observable < KeyboardEvent > ) . pipe ( filter ( ( event ) => { return event . keyCode === ESCAPE && ! hasModifierKey ( event ) ; } ) ) , this . _router ? this . _router . events . pipe ( filter ( ( e ) => e instanceof NavigationStart ) , mapTo ( null ) ) : NEVER ) . pipe ( takeUntil ( this . _destroyed ) ) . subscribe ( ( event ) => this . _ngZone . run ( ( ) => { if ( this . mode === 'side' ) { return ; } this . close ( ) ; if ( ! event ) { return ; } event . stopPropagation ( ) ; event . preventDefault ( ) ; } ) ) ; } ) ; 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 ) ; } } ) ; } /**
     * Moves focus into the sidebar. Note that this works even if
     * the focus trap is disabled in `side` mode.
     */ private _takeFocus ( ) { if ( this . mode === 'side' || ! this . _focusTrap ) { return ; } this . _focusTrap . focusInitialElementWhenReady ( ) . then ( ( hasMovedFocus ) => { if ( ! hasMovedFocus && typeof this . _elementRef . nativeElement . focus === 'function' ) { this . _elementRef . nativeElement . focus ( ) ; } } ) ; } /**
     * Restores focus to the element that was originally focused when the sidebar opened.
     * If no element was focused at that time, the focus will be restored to the sidebar.
     */ private _restoreFocus ( ) { if ( this . mode === 'side' ) { return ; } if ( this . _elementFocusedBeforeSidebarWasOpened ) { this . _focusMonitor . focusVia ( this . _elementFocusedBeforeSidebarWasOpened , this . _openedVia ) ; } else { this . _elementRef . nativeElement . blur ( ) ; } this . _elementFocusedBeforeSidebarWasOpened = null ; this . _openedVia = null ; } /** Whether focus is currently within the sidebar. */ private _isFocusWithinSidebar ( ) { const activeEl = this . _doc ?. activeElement ; return ! ! activeEl && this . _elementRef . nativeElement . contains ( activeEl ) ; } ngAfterContentInit ( ) { this . _focusTrap = this . _focusTrapFactory . create ( this . _elementRef . nativeElement ) ; this . _updateFocusTrapState ( ) ; } ngAfterContentChecked ( ) { if ( this . _platform . isBrowser ) { this . _enableAnimations = true ; } } ngOnDestroy ( ) { if ( this . _focusTrap ) { this . _focusTrap . destroy ( ) ; } this . _animationStarted . complete ( ) ; this . _animationEnd . complete ( ) ; this . _modeChanged . complete ( ) ; this . _destroyed . next ( ) ; this . _destroyed . complete ( ) ; } param openedVia open ( openedVia ? ) { return this . toggle ( true , openedVia ) ; } /** Close the sidebar. */ close ( ) { return this . toggle ( false ) ; } /** Closes the sidebar with context that the backdrop was clicked. */ _closeViaBackdropClick ( ) { return this . _setOpen ( false , true ) ; } param isOpen param openedVia toggle ( isOpen = ! this . opened , openedVia ? ) { return this . _setOpen ( isOpen , ! isOpen && this . _isFocusWithinSidebar ( ) , openedVia ) ; } param isOpen param restoreFocus param openedVia private _setOpen ( isOpen , restoreFocus , openedVia = 'program' ) { this . _opened = isOpen ; if ( isOpen ) { this . _animationState = this . _enableAnimations ? 'open' : 'open-instant' ; this . _openedVia = openedVia ; } else { this . _animationState = 'void' ; if ( restoreFocus ) { this . _restoreFocus ( ) ; } } this . _updateFocusTrapState ( ) ; return new Promise < SbbSidebarToggleResult > ( ( resolve ) => { this . openedChange . pipe ( take ( 1 ) ) . subscribe ( ( open ) => resolve ( open ? 'open' : 'close' ) ) ; } ) ; } _getWidth ( ) { return this . _elementRef . nativeElement ? this . _elementRef . nativeElement . offsetWidth || 0 : 0 ; } /** Updates the enabled state of the focus trap. */ private _updateFocusTrapState ( ) { if ( this . _focusTrap ) { this . _focusTrap . enabled = this . opened && this . mode !== 'side' ; } } @ HostListener ( '@transform.start' , [ '$event' ] ) _animationStartListener ( event ) { this . _animationStarted . next ( event ) ; } @ HostListener ( '@transform.done' , [ '$event' ] ) _animationDoneListener ( event ) { this . _animationEnd . next ( event ) ; } _mobileChanged ( mobile ) { Promise . resolve ( ) . then ( ( ) => { const wasAnimationsEnabled = this . _enableAnimations ; this . _enableAnimations = false ; if ( mobile ) { this . close ( ) ; this . mode = 'over' ; } else { this . mode = 'side' ; this . open ( ) ; } this . _enableAnimations = wasAnimationsEnabled ; } ) ; } } @ Component ( { selector : 'sbb-sidebar-container' , exportAs : 'sbbSidebarContainer' , templateUrl : './sidebar-container.html' , styleUrls : [ './sidebar.css' ] , host : { class : 'sbb-sidebar-container' , '[class.sbb-sidebar-container-mobile]' : '_mobile' , } , changeDetection : ChangeDetectionStrategy . OnPush , encapsulation : ViewEncapsulation . None , providers : [ { provide : SBB_SIDEBAR_CONTAINER , useExisting : SbbSidebarContainer , } , ] , } ) export class SbbSidebarContainer extends SbbSidebarContainerBase < SbbSidebar > implements AfterContentInit , SbbSidebarMobileCapableContainer , OnDestroy { _labelOpenSidebar : string = typeof $localize === 'function' ? $localize `:Button label to open the sidebar@@sbbSidebarOpenSidebar:Open Sidebar` : 'Open Sidebar' ; /** The sidebar child */ override get sidebar ( ) : SbbSidebar | null { return this . _sidebar ; } /** Whether the sidebar container should have a backdrop while one of the sidebars is open. */ get hasBackdrop ( ) { return ! this . sidebar || this . sidebar . mode !== 'side' ; } constructor ( private _element , ngZone , changeDetectorRef , viewportRuler , breakPointObserver , @ Optional ( ) @ Inject ( ANIMATION_MODULE_TYPE ) private _animationMode ? ) { super ( ngZone , changeDetectorRef , breakPointObserver ) ; viewportRuler . change ( ) . pipe ( takeUntil ( this . _destroyed ) ) . subscribe ( ( ) => this . updateContentMargins ( ) ) ; } /** All sidebars in the container. Includes sidebars from inside nested containers. */ @ ContentChildren ( SbbSidebar , { descendants : true , } ) override _allSidebars : QueryList < SbbSidebar > ; @ ContentChild ( SbbSidebarContent ) override _content : SbbSidebarContent ; @ ViewChild ( SbbSidebarContent ) override _userContent : SbbSidebarContent ; /** Event emitted when the sidebar backdrop is clicked. */ @ Output ( ) readonly backdropClick : EventEmitter < void > = new EventEmitter < void > ( ) ; /** The sidebar */ protected override _sidebar : SbbSidebar | null ; /**
     * Margins to be applied to the content. These are used to shrink the sidebar content when a
     * sidebar is open. We use margin rather than transform because transform breaks
     * fixed position elements inside of the transformed element.
     */ _contentMargins : { left : number | null ; } = { left : null } ; readonly _contentMarginChanges = new Subject < { left : number | null ; } > ( ) ; override ngAfterContentInit ( ) { super . ngAfterContentInit ( ) ; this . _sidebars . changes . pipe ( startWith ( null ) ) . subscribe ( ( ) => { this . _validateSidebars ( ) ; this . _sidebars . forEach ( ( sidebar ) => { this . _watchSidebarToggle ( sidebar ) ; this . _watchSidebarMode ( sidebar ) ; } ) ; if ( ! this . _sidebars . length || this . _isSidebarOpen ( this . _sidebar ) ) { this . updateContentMargins ( ) ; } this . _changeDetectorRef . markForCheck ( ) ; } ) ; this . _watchBreakpointObserver ( ) ; } /** Calls `open` of the sidebar */ open ( ) { this . sidebar ?. open ( ) ; } /** Calls `close` of the sidebar */ close ( ) { this . sidebar ?. close ( ) ; } /**
     * Recalculates and updates the inline styles for the content. Note that this should be used
     * sparingly, because it causes a reflow.
     */ updateContentMargins ( ) { let left = 0 ; if ( this . _sidebar && this . _sidebar . opened && this . _sidebar . mode === 'side' ) { left += this . _sidebar . _getWidth ( ) ; } left = left || null ! ; if ( left !== this . _contentMargins . left ) { this . _contentMargins = { left } ; this . _ngZone . run ( ( ) => this . _contentMarginChanges . next ( this . _contentMargins ) ) ; } } /**
     * Subscribes to sidebar events in order to set a class on the main container element when the
     * sidebar is open and the backdrop is visible. This ensures any overflow on the container element
     * is properly hidden.
     */ private _watchSidebarToggle ( sidebar ) { sidebar . _animationStarted . pipe ( filter ( ( event ) => event . fromState !== event . toState ) , takeUntil ( this . _sidebars . changes ) ) . subscribe ( ( event ) => { if ( event . toState !== 'open-instant' && this . _animationMode !== 'NoopAnimations' ) { this . _element . nativeElement . classList . add ( 'sbb-sidebar-transition' ) ; } this . updateContentMargins ( ) ; this . _changeDetectorRef . markForCheck ( ) ; } ) ; sidebar . openedChange . pipe ( takeUntil ( this . _sidebars . changes ) ) . subscribe ( ( ) => this . _setContainerClass ( sidebar . opened ) ) ; } /** Subscribes to changes in sidebar mode so we can run change detection. */ private _watchSidebarMode ( sidebar ) { if ( sidebar ) { sidebar . _modeChanged . pipe ( takeUntil ( merge ( this . _sidebars . changes , this . _destroyed ) ) ) . subscribe ( ( ) => { this . updateContentMargins ( ) ; this . _changeDetectorRef . markForCheck ( ) ; } ) ; } } /** Toggles the 'sbb-sidebar-opened' class on the main 'sbb-sidebar-container' element. */ private _setContainerClass ( isAdd ) { const classList = this . _element . nativeElement . classList ; const className = 'sbb-sidebar-container-has-open' ; if ( isAdd && this . sidebar ?. mode !== 'side' ) { classList . add ( className ) ; } else { classList . remove ( className ) ; } } _onBackdropClicked ( ) { this . backdropClick . emit ( ) ; this . _closeModalSidebarsViaBackdrop ( ) ; } _closeModalSidebarsViaBackdrop ( ) { if ( this . _sidebar && this . _canHaveBackdrop ( this . _sidebar ) ) { this . _sidebar ! . _closeViaBackdropClick ( ) ; } } _isShowingBackdrop ( ) { return this . _isSidebarOpen ( this . _sidebar ) && this . _canHaveBackdrop ( this . _sidebar ) ; } private _canHaveBackdrop ( sidebar ) { return sidebar . mode !== 'side' ; } private _isSidebarOpen ( sidebar ) { return sidebar != null && sidebar . opened ; } toggleSidebar ( ) { this . _sidebar ?. toggle ( ) ; } override ngOnDestroy ( ) { super . ngOnDestroy ( ) ; this . _contentMarginChanges . complete ( ) ; } }