import { ChangeDetectorRef, Component, ElementRef, HostBinding, HostListener, Input, OnDestroy, OnInit, Renderer2, RendererStyleFlags2, ViewEncapsulation } from '@angular/core'; import { animate, AnimationBuilder, AnimationPlayer, style } from '@angular/animations'; import { ObservableMedia } from '@angular/flex-layout'; import { Subject } from 'rxjs'; import { FuseSidebarService } from './sidebar.service'; import { FuseMatchMediaService } from '@fuse/services/match-media.service'; import { FuseConfigService } from '@fuse/services/config.service'; import { takeUntil } from 'rxjs/internal/operators'; @Component({ selector : 'fuse-sidebar', templateUrl : './sidebar.component.html', styleUrls : ['./sidebar.component.scss'], encapsulation: ViewEncapsulation.None }) export class FuseSidebarComponent implements OnInit, OnDestroy { // Name @Input() name: string; // Position @Input() position: 'left' | 'right'; // Open @HostBinding('class.open') opened: boolean; // Locked Open @Input() lockedOpen: string; // isLockedOpen @HostBinding('class.locked-open') isLockedOpen: boolean; // Folded unfolded @HostBinding('class.unfolded') unfolded: boolean; // Invisible overlay @Input() invisibleOverlay: boolean; // Private private _folded: boolean; private _fuseConfig: any; private _wasActive: boolean; private _backdrop: HTMLElement | null = null; private _player: AnimationPlayer; private _unsubscribeAll: Subject; @HostBinding('class.animations-enabled') private _animationsEnabled: boolean; /** * Constructor * * @param {AnimationBuilder} _animationBuilder * @param {ChangeDetectorRef} _changeDetectorRef * @param {ElementRef} _elementRef * @param {FuseConfigService} _fuseConfigService * @param {FuseMatchMediaService} _fuseMatchMediaService * @param {FuseSidebarService} _fuseSidebarService * @param {ObservableMedia} _observableMedia * @param {Renderer2} _renderer */ constructor( private _animationBuilder: AnimationBuilder, private _changeDetectorRef: ChangeDetectorRef, private _elementRef: ElementRef, private _fuseConfigService: FuseConfigService, private _fuseMatchMediaService: FuseMatchMediaService, private _fuseSidebarService: FuseSidebarService, private _observableMedia: ObservableMedia, private _renderer: Renderer2 ) { // Set the defaults this.folded = false; this.opened = false; this.position = 'left'; this.invisibleOverlay = false; // Set the private defaults this._animationsEnabled = false; this._unsubscribeAll = new Subject(); } // ----------------------------------------------------------------------------------------------------- // @ Accessors // ----------------------------------------------------------------------------------------------------- // Folded @HostBinding('class.folded') @Input() set folded(value: boolean) { // Only work if the sidebar is not closed if ( !this.opened ) { return; } // Set the folded this._folded = value; // Programmatically add/remove margin to the element // that comes after or before based on the position let sibling, styleRule; const styleValue = '64px'; // Get the sibling and set the style rule if ( this.position === 'left' ) { sibling = this._elementRef.nativeElement.nextElementSibling; styleRule = 'margin-left'; } else { sibling = this._elementRef.nativeElement.previousElementSibling; styleRule = 'margin-right'; } // If there is no sibling, return... if ( !sibling ) { return; } // If folded... if ( value ) { // Set the style this._renderer.setStyle(sibling, styleRule, styleValue, RendererStyleFlags2.Important + RendererStyleFlags2.DashCase); } // If unfolded... else { // Remove the style this._renderer.removeStyle(sibling, styleRule); } } get folded(): boolean { return this._folded; } // ----------------------------------------------------------------------------------------------------- // @ Lifecycle hooks // ----------------------------------------------------------------------------------------------------- /** * On init */ ngOnInit(): void { // Subscribe to config changes this._fuseConfigService.config .pipe(takeUntil(this._unsubscribeAll)) .subscribe((config) => { this._fuseConfig = config; }); // Register the sidebar this._fuseSidebarService.register(this.name, this); // Setup visibility this._setupVisibility(); // Setup position this._setupPosition(); // Setup lockedOpen this._setupLockedOpen(); } /** * On destroy */ ngOnDestroy(): void { // If the sidebar is folded, unfold it to revert modifications if ( this.folded ) { this.unfold(); } // Unregister the sidebar this._fuseSidebarService.unregister(this.name); // Unsubscribe from all subscriptions this._unsubscribeAll.next(); this._unsubscribeAll.complete(); } // ----------------------------------------------------------------------------------------------------- // @ Private methods // ----------------------------------------------------------------------------------------------------- /** * Setup the visibility of the sidebar * * @private */ private _setupVisibility(): void { // Remove the existing box-shadow this._renderer.setStyle(this._elementRef.nativeElement, 'box-shadow', 'none'); // Make the sidebar invisible this._renderer.setStyle(this._elementRef.nativeElement, 'visibility', 'hidden'); } /** * Setup the sidebar position * * @private */ private _setupPosition(): void { // Add the correct class name to the sidebar // element depending on the position attribute if ( this.position === 'right' ) { this._renderer.addClass(this._elementRef.nativeElement, 'right-positioned'); } else { this._renderer.addClass(this._elementRef.nativeElement, 'left-positioned'); } } /** * Setup the lockedOpen handler * * @private */ private _setupLockedOpen(): void { // Return if the lockedOpen wasn't set if ( !this.lockedOpen ) { // Return return; } // Set the wasActive for the first time this._wasActive = false; // Show the sidebar this._showSidebar(); // Act on every media change this._fuseMatchMediaService.onMediaChange .pipe(takeUntil(this._unsubscribeAll)) .subscribe(() => { // Get the active status const isActive = this._observableMedia.isActive(this.lockedOpen); // If the both status are the same, don't act if ( this._wasActive === isActive ) { return; } // Activate the lockedOpen if ( isActive ) { // Set the lockedOpen status this.isLockedOpen = true; // Show the sidebar this._showSidebar(); // Force the the opened status to true this.opened = true; // Read the folded setting from the config // and fold the sidebar if it's true if ( this._fuseConfig.layout.navbar.folded ) { this.fold(); } // Hide the backdrop if any exists this._hideBackdrop(); } // De-Activate the lockedOpen else { // Set the lockedOpen status this.isLockedOpen = false; // Unfold the sidebar in case if it was folded this.unfold(); // Force the the opened status to close this.opened = false; // Hide the sidebar this._hideSidebar(); } // Store the new active status this._wasActive = isActive; }); } /** * Show the backdrop * * @private */ private _showBackdrop(): void { // Create the backdrop element this._backdrop = this._renderer.createElement('div'); // Add a class to the backdrop element this._backdrop.classList.add('fuse-sidebar-overlay'); // Add a class depending on the invisibleOverlay option if ( this.invisibleOverlay ) { this._backdrop.classList.add('fuse-sidebar-overlay-invisible'); } // Append the backdrop to the parent of the sidebar this._renderer.appendChild(this._elementRef.nativeElement.parentElement, this._backdrop); // Create the enter animation and attach it to the player this._player = this._animationBuilder .build([ animate('300ms ease', style({opacity: 1})) ]).create(this._backdrop); // Play the animation this._player.play(); // Add an event listener to the overlay this._backdrop.addEventListener('click', () => { this.close(); } ); // Mark for check this._changeDetectorRef.markForCheck(); } /** * Hide the backdrop * * @private */ private _hideBackdrop(): void { if ( !this._backdrop ) { return; } // Create the leave animation and attach it to the player this._player = this._animationBuilder .build([ animate('300ms ease', style({opacity: 0})) ]).create(this._backdrop); // Play the animation this._player.play(); // Once the animation is done... this._player.onDone(() => { // If the backdrop still exists... if ( this._backdrop ) { // Remove the backdrop this._backdrop.parentNode.removeChild(this._backdrop); this._backdrop = null; } }); // Mark for check this._changeDetectorRef.markForCheck(); } /** * Change some properties of the sidebar * and make it visible * * @private */ private _showSidebar(): void { // Remove the box-shadow style this._renderer.removeStyle(this._elementRef.nativeElement, 'box-shadow'); // Make the sidebar invisible this._renderer.removeStyle(this._elementRef.nativeElement, 'visibility'); // Mark for check this._changeDetectorRef.markForCheck(); } /** * Change some properties of the sidebar * and make it invisible * * @private */ private _hideSidebar(delay = true): void { const delayAmount = delay ? 300 : 0; // Add a delay so close animation can play setTimeout(() => { // Remove the box-shadow this._renderer.setStyle(this._elementRef.nativeElement, 'box-shadow', 'none'); // Make the sidebar invisible this._renderer.setStyle(this._elementRef.nativeElement, 'visibility', 'hidden'); }, delayAmount); // Mark for check this._changeDetectorRef.markForCheck(); } /** * Enable the animations * * @private */ private _enableAnimations(): void { // Return if animations already enabled if ( this._animationsEnabled ) { return; } // Enable the animations this._animationsEnabled = true; // Mark for check this._changeDetectorRef.markForCheck(); } // ----------------------------------------------------------------------------------------------------- // @ Public methods // ----------------------------------------------------------------------------------------------------- /** * Open the sidebar */ open(): void { if ( this.opened || this.isLockedOpen ) { return; } // Enable the animations this._enableAnimations(); // Show the sidebar this._showSidebar(); // Show the backdrop this._showBackdrop(); // Set the opened status this.opened = true; // Mark for check this._changeDetectorRef.markForCheck(); } /** * Close the sidebar */ close(): void { if ( !this.opened || this.isLockedOpen ) { return; } // Enable the animations this._enableAnimations(); // Hide the backdrop this._hideBackdrop(); // Set the opened status this.opened = false; // Hide the sidebar this._hideSidebar(); // Mark for check this._changeDetectorRef.markForCheck(); } /** * Toggle open/close the sidebar */ toggleOpen(): void { if ( this.opened ) { this.close(); } else { this.open(); } } /** * Mouseenter */ @HostListener('mouseenter') onMouseEnter(): void { // Only work if the sidebar is folded if ( !this.folded ) { return; } // Enable the animations this._enableAnimations(); // Unfold the sidebar temporarily this.unfolded = true; // Mark for check this._changeDetectorRef.markForCheck(); } /** * Mouseleave */ @HostListener('mouseleave') onMouseLeave(): void { // Only work if the sidebar is folded if ( !this.folded ) { return; } // Enable the animations this._enableAnimations(); // Fold the sidebar back this.unfolded = false; // Mark for check this._changeDetectorRef.markForCheck(); } /** * Fold the sidebar permanently */ fold(): void { // Only work if the sidebar is not folded if ( this.folded ) { return; } // Enable the animations this._enableAnimations(); // Fold this.folded = true; // Mark for check this._changeDetectorRef.markForCheck(); } /** * Unfold the sidebar permanently */ unfold(): void { // Only work if the sidebar is folded if ( !this.folded ) { return; } // Enable the animations this._enableAnimations(); // Unfold this.folded = false; // Mark for check this._changeDetectorRef.markForCheck(); } /** * Toggle the sidebar fold/unfold permanently */ toggleFold(): void { if ( this.folded ) { this.unfold(); } else { this.fold(); } } }