import { Component, ElementRef, EventEmitter, HostBinding, HostListener, Input, OnChanges, OnDestroy, OnInit, Output, Renderer2, SimpleChanges, ViewEncapsulation } from '@angular/core'; import { animate, AnimationBuilder, AnimationPlayer, style } from '@angular/animations'; import { FuseDrawerMode, FuseDrawerPosition } from '@fuse/components/drawer/drawer.types'; import { FuseDrawerService } from '@fuse/components/drawer/drawer.service'; import { FuseUtilsService } from '@fuse/services/utils/utils.service'; import { BooleanInput, coerceBooleanProperty } from '@angular/cdk/coercion'; @Component({ selector : 'fuse-drawer', templateUrl : './drawer.component.html', styleUrls : ['./drawer.component.scss'], encapsulation: ViewEncapsulation.None, exportAs : 'fuseDrawer' }) export class FuseDrawerComponent implements OnChanges, OnInit, OnDestroy { /* eslint-disable @typescript-eslint/naming-convention */ static ngAcceptInputType_fixed: BooleanInput; static ngAcceptInputType_opened: BooleanInput; static ngAcceptInputType_transparentOverlay: BooleanInput; /* eslint-enable @typescript-eslint/naming-convention */ @Input() fixed: boolean = false; @Input() mode: FuseDrawerMode = 'side'; @Input() name: string = this._fuseUtilsService.randomId(); @Input() opened: boolean = false; @Input() position: FuseDrawerPosition = 'left'; @Input() transparentOverlay: boolean = false; @Output() readonly fixedChanged: EventEmitter = new EventEmitter(); @Output() readonly modeChanged: EventEmitter = new EventEmitter(); @Output() readonly openedChanged: EventEmitter = new EventEmitter(); @Output() readonly positionChanged: EventEmitter = new EventEmitter(); private _animationsEnabled: boolean = false; private _hovered: boolean = false; private _overlay: HTMLElement; private _player: AnimationPlayer; /** * Constructor */ constructor( private _animationBuilder: AnimationBuilder, private _elementRef: ElementRef, private _renderer2: Renderer2, private _fuseDrawerService: FuseDrawerService, private _fuseUtilsService: FuseUtilsService ) { } // ----------------------------------------------------------------------------------------------------- // @ Accessors // ----------------------------------------------------------------------------------------------------- /** * Host binding for component classes */ @HostBinding('class') get classList(): any { return { 'fuse-drawer-animations-enabled' : this._animationsEnabled, 'fuse-drawer-fixed' : this.fixed, 'fuse-drawer-hover' : this._hovered, [`fuse-drawer-mode-${this.mode}`] : true, 'fuse-drawer-opened' : this.opened, [`fuse-drawer-position-${this.position}`]: true }; } /** * Host binding for component inline styles */ @HostBinding('style') get styleList(): any { return { 'visibility': this.opened ? 'visible' : 'hidden' }; } // ----------------------------------------------------------------------------------------------------- // @ Decorated methods // ----------------------------------------------------------------------------------------------------- /** * On mouseenter * * @private */ @HostListener('mouseenter') private _onMouseenter(): void { // Enable the animations this._enableAnimations(); // Set the hovered this._hovered = true; } /** * On mouseleave * * @private */ @HostListener('mouseleave') private _onMouseleave(): void { // Enable the animations this._enableAnimations(); // Set the hovered this._hovered = false; } // ----------------------------------------------------------------------------------------------------- // @ Lifecycle hooks // ----------------------------------------------------------------------------------------------------- /** * On changes * * @param changes */ ngOnChanges(changes: SimpleChanges): void { // Fixed if ( 'fixed' in changes ) { // Coerce the value to a boolean this.fixed = coerceBooleanProperty(changes.fixed.currentValue); // Execute the observable this.fixedChanged.next(this.fixed); } // Mode if ( 'mode' in changes ) { // Get the previous and current values const previousMode = changes.mode.previousValue; const currentMode = changes.mode.currentValue; // Disable the animations this._disableAnimations(); // If the mode changes: 'over -> side' if ( previousMode === 'over' && currentMode === 'side' ) { // Hide the overlay this._hideOverlay(); } // If the mode changes: 'side -> over' if ( previousMode === 'side' && currentMode === 'over' ) { // If the drawer is opened if ( this.opened ) { // Show the overlay this._showOverlay(); } } // Execute the observable this.modeChanged.next(currentMode); // Enable the animations after a delay // The delay must be bigger than the current transition-duration // to make sure nothing will be animated while the mode is changing setTimeout(() => { this._enableAnimations(); }, 500); } // Opened if ( 'opened' in changes ) { // Coerce the value to a boolean const open = coerceBooleanProperty(changes.opened.currentValue); // Open/close the drawer this._toggleOpened(open); } // Position if ( 'position' in changes ) { // Execute the observable this.positionChanged.next(this.position); } // Transparent overlay if ( 'transparentOverlay' in changes ) { // Coerce the value to a boolean this.transparentOverlay = coerceBooleanProperty(changes.transparentOverlay.currentValue); } } /** * On init */ ngOnInit(): void { // Register the drawer this._fuseDrawerService.registerComponent(this.name, this); } /** * On destroy */ ngOnDestroy(): void { // Finish the animation if ( this._player ) { this._player.finish(); } // Deregister the drawer from the registry this._fuseDrawerService.deregisterComponent(this.name); } // ----------------------------------------------------------------------------------------------------- // @ Public methods // ----------------------------------------------------------------------------------------------------- /** * Open the drawer */ open(): void { // Return if the drawer has already opened if ( this.opened ) { return; } // Open the drawer this._toggleOpened(true); } /** * Close the drawer */ close(): void { // Return if the drawer has already closed if ( !this.opened ) { return; } // Close the drawer this._toggleOpened(false); } /** * Toggle the drawer */ toggle(): void { if ( this.opened ) { this.close(); } else { this.open(); } } // ----------------------------------------------------------------------------------------------------- // @ Private methods // ----------------------------------------------------------------------------------------------------- /** * Enable the animations * * @private */ private _enableAnimations(): void { // Return if the animations are already enabled if ( this._animationsEnabled ) { return; } // Enable the animations this._animationsEnabled = true; } /** * Disable the animations * * @private */ private _disableAnimations(): void { // Return if the animations are already disabled if ( !this._animationsEnabled ) { return; } // Disable the animations this._animationsEnabled = false; } /** * Show the backdrop * * @private */ private _showOverlay(): void { // Create the backdrop element this._overlay = this._renderer2.createElement('div'); // Return if overlay couldn't be create for some reason if ( !this._overlay ) { return; } // Add a class to the backdrop element this._overlay.classList.add('fuse-drawer-overlay'); // Add a class depending on the fixed option if ( this.fixed ) { this._overlay.classList.add('fuse-drawer-overlay-fixed'); } // Add a class depending on the transparentOverlay option if ( this.transparentOverlay ) { this._overlay.classList.add('fuse-drawer-overlay-transparent'); } // Append the backdrop to the parent of the drawer this._renderer2.appendChild(this._elementRef.nativeElement.parentElement, this._overlay); // Create the enter animation and attach it to the player this._player = this._animationBuilder.build([ style({opacity: 0}), animate('300ms cubic-bezier(0.25, 0.8, 0.25, 1)', style({opacity: 1})) ]).create(this._overlay); // Once the animation is done... this._player.onDone(() => { // Destroy the player this._player.destroy(); this._player = null; }); // Play the animation this._player.play(); // Add an event listener to the overlay this._overlay.addEventListener('click', () => { this.close(); }); } /** * Hide the backdrop * * @private */ private _hideOverlay(): void { if ( !this._overlay ) { return; } // Create the leave animation and attach it to the player this._player = this._animationBuilder.build([ animate('300ms cubic-bezier(0.25, 0.8, 0.25, 1)', style({opacity: 0})) ]).create(this._overlay); // Play the animation this._player.play(); // Once the animation is done... this._player.onDone(() => { // Destroy the player this._player.destroy(); this._player = null; // If the backdrop still exists... if ( this._overlay ) { // Remove the backdrop this._overlay.parentNode.removeChild(this._overlay); this._overlay = null; } }); } /** * Open/close the drawer * * @param open * @private */ private _toggleOpened(open: boolean): void { // Set the opened this.opened = open; // Enable the animations this._enableAnimations(); // If the mode is 'over' if ( this.mode === 'over' ) { // If the drawer opens, show the overlay if ( open ) { this._showOverlay(); } // Otherwise, close the overlay else { this._hideOverlay(); } } // Execute the observable this.openedChanged.next(open); } }