UNPKG

ng2-sidebar

Version:

[DEPRECATED] Renamed to ng-sidebar.

372 lines (303 loc) 10.2 kB
import { AfterContentInit, AnimationTransitionEvent, Component, ContentChildren, ElementRef, EventEmitter, Inject, Input, OnChanges, OnDestroy, Output, SimpleChanges, ViewChild, ViewEncapsulation, QueryList, trigger, state, style, transition, animate } from '@angular/core'; import { DOCUMENT } from '@angular/platform-browser'; import { CloseSidebar } from './close.directive'; export const SIDEBAR_POSITION = { Left: 'left', Right: 'right', Top: 'top', Bottom: 'bottom' }; @Component({ selector: 'ng2-sidebar', encapsulation: ViewEncapsulation.None, template: ` <aside #sidebar [@visibleSidebarState]="_visibleSidebarState" (@visibleSidebarState.start)="_animationStarted($event)" (@visibleSidebarState.done)="_animationDone($event)" role="complementary" [attr.aria-hidden]="!open" [attr.aria-label]="ariaLabel" class="ng2-sidebar ng2-sidebar--{{position}}" [class.ng2-sidebar--style]="defaultStyles" [ngClass]="sidebarClass"> <ng-content></ng-content> </aside> <div *ngIf="showOverlay" [@visibleOverlayState]="_visibleOverlayState" aria-hidden="true" class="ng2-sidebar__overlay" [class.ng2-sidebar__overlay--style]="open && defaultStyles" [ngClass]="overlayClass"></div> `, styles: [` .ng2-sidebar { overflow: auto; pointer-events: none; position: fixed; z-index: 99999999; } .ng2-sidebar--left { bottom: 0; left: 0; top: 0; } .ng2-sidebar--right { bottom: 0; right: 0; top: 0; } .ng2-sidebar--top { left: 0; right: 0; top: 0; } .ng2-sidebar--bottom { bottom: 0; left: 0; right: 0; } .ng2-sidebar--style { background: #fff; box-shadow: 0 0 2.5em rgba(85, 85, 85, 0.5); } .ng2-sidebar__overlay { height: 100%; left: 0; pointer-events: none; position: fixed; top: 0; width: 100%; z-index: 99999998; } .ng2-sidebar__overlay--style { background: #000; opacity: 0.75; } `], animations: [ trigger('visibleSidebarState', [ state('expanded', style({ transform: 'none', pointerEvents: 'auto', willChange: 'initial' })), state('expanded--animate', style({ transform: 'none', pointerEvents: 'auto', willChange: 'initial' })), state('collapsed--left', style({ transform: 'translateX(-110%)' })), state('collapsed--right', style({ transform: 'translateX(110%)' })), state('collapsed--top', style({ transform: 'translateY(-110%)' })), state('collapsed--bottom', style({ transform: 'translateY(110%)' })), transition('expanded--animate <=> *', animate('0.3s cubic-bezier(0, 0, 0.3, 1)')) ]), trigger('visibleOverlayState', [ state('visible', style({ pointerEvents: 'auto' })) ]) ] }) export class Sidebar implements AfterContentInit, OnChanges, OnDestroy { // `openChange` allows for 2-way data binding @Input() open: boolean = false; @Output() openChange: EventEmitter<boolean> = new EventEmitter<boolean>(); @Input() keyClose: boolean = false; @Input() keyCode: number = 27; @Input() position: string = SIDEBAR_POSITION.Left; @Input() closeOnClickOutside: boolean = false; @Input() showOverlay: boolean = false; @Input() animate: boolean = true; @Input() defaultStyles: boolean = false; @Input() sidebarClass: string; @Input() overlayClass: string; @Input() ariaLabel: string; @Input() trapFocus: boolean = true; @Input() autoFocus: boolean = true; @Output() onOpen: EventEmitter<null> = new EventEmitter<null>(); @Output() onClose: EventEmitter<null> = new EventEmitter<null>(); @Output() onAnimationStarted: EventEmitter<AnimationTransitionEvent> = new EventEmitter<AnimationTransitionEvent>(); @Output() onAnimationDone: EventEmitter<AnimationTransitionEvent> = new EventEmitter<AnimationTransitionEvent>(); /** @internal */ _visibleSidebarState: string; /** @internal */ _visibleOverlayState: string; @ViewChild('sidebar') private _elSidebar: ElementRef; @ContentChildren(CloseSidebar) private _closeDirectives: QueryList<CloseSidebar>; private _onClickOutsideAttached: boolean = false; private _onKeyDownAttached: boolean = false; private _focusableElementsString: string = 'a[href], area[href], input:not([disabled]), select:not([disabled]),' + 'textarea:not([disabled]), button:not([disabled]), iframe, object, embed, [tabindex], [contenteditable]'; private _focusableElements: Array<HTMLElement>; private _focusedBeforeOpen: HTMLElement; constructor(@Inject(DOCUMENT) private _document /*: HTMLDocument */) { this._manualClose = this._manualClose.bind(this); this._trapFocus = this._trapFocus.bind(this); this._onClickOutside = this._onClickOutside.bind(this); this._onKeyDown = this._onKeyDown.bind(this); } ngAfterContentInit() { if (this._closeDirectives) { this._closeDirectives.forEach((dir: CloseSidebar) => { dir.clicked.subscribe(this._manualClose); }); } } ngOnChanges(changes: SimpleChanges) { if (changes['open']) { if (this.open) { this._open(); } else { this._close(); } this._setVisibleSidebarState(); } if (changes['closeOnClickOutside'] || changes['keyClose']) { this._initCloseListeners(); } if (changes['position']) { this._setVisibleSidebarState(); } } ngOnDestroy() { this._destroyCloseListeners(); if (this._closeDirectives) { this._closeDirectives.forEach((dir: CloseSidebar) => { dir.clicked.unsubscribe(); }); } } // Animation callbacks // ============================================================================================== /** @internal */ _animationStarted(e: AnimationTransitionEvent) { this.onAnimationStarted.emit(e); } /** @internal */ _animationDone(e: AnimationTransitionEvent) { this.onAnimationDone.emit(e); } // Sidebar toggling // ============================================================================================== private _setVisibleSidebarState() { this._visibleSidebarState = this.open ? (this.animate ? 'expanded--animate' : 'expanded') : `collapsed--${this.position}`; this._visibleOverlayState = this.open ? 'visible' : null; } private _open() { this._setFocused(true); this._initCloseListeners(); this.onOpen.emit(null); } private _close() { this._setFocused(false); this._destroyCloseListeners(); this.onClose.emit(null); } private _manualClose() { this.open = false; this.openChange.emit(false); this._close(); } // Focus on open/close // ============================================================================================== private _setFocusToFirstItem() { if (this.autoFocus && this._focusableElements && this._focusableElements.length) { this._focusableElements[0].focus(); } } private _trapFocus(e: FocusEvent) { if (this.open && this.trapFocus && !this._elSidebar.nativeElement.contains(e.target)) { this._setFocusToFirstItem(); } } // Handles the ability to focus sidebar elements when it's open/closed private _setFocused(open: boolean) { this._focusableElements = Array.from( this._elSidebar.nativeElement.querySelectorAll(this._focusableElementsString)) as Array<HTMLElement>; if (open) { this._focusedBeforeOpen = this._document.activeElement as HTMLElement; // Restore focusability, with previous tabindex attributes for (let el of this._focusableElements) { const prevTabIndex = el.getAttribute('__tabindex__'); if (prevTabIndex) { el.setAttribute('tabindex', prevTabIndex); el.removeAttribute('__tabindex__'); } else { el.removeAttribute('tabindex'); } } this._setFocusToFirstItem(); this._document.body.addEventListener('focus', this._trapFocus, true); } else { // Manually make all focusable elements unfocusable, saving existing tabindex attributes for (let el of this._focusableElements) { const existingTabIndex = el.getAttribute('tabindex'); if (existingTabIndex) { el.setAttribute('__tabindex__', existingTabIndex); } el.setAttribute('tabindex', '-1'); } if (this._focusedBeforeOpen) { this._focusedBeforeOpen.focus(); } this._document.body.removeEventListener('focus', this._trapFocus, true); } } // On click outside // ============================================================================================== private _initCloseListeners() { if (this.open && (this.closeOnClickOutside || this.keyClose)) { // In a timeout so that things render first setTimeout(() => { if (this.closeOnClickOutside && !this._onClickOutsideAttached) { this._document.body.addEventListener('click', this._onClickOutside); this._onClickOutsideAttached = true; } if (this.keyClose && !this._onKeyDownAttached) { this._document.body.addEventListener('keydown', this._onKeyDown); this._onKeyDownAttached = true; } }); } } private _destroyCloseListeners() { if (this._onClickOutsideAttached) { this._document.body.removeEventListener('click', this._onClickOutside); this._onClickOutsideAttached = false; } if (this._onKeyDownAttached) { this._document.body.removeEventListener('keydown', this._onKeyDown); this._onKeyDownAttached = false; } } private _onClickOutside(e: MouseEvent) { if (this._onClickOutsideAttached && this._elSidebar && !this._elSidebar.nativeElement.contains(e.target)) { this._manualClose(); } } private _onKeyDown(e: KeyboardEvent | Event) { e = e || window.event; if ((e as KeyboardEvent).keyCode === this.keyCode) { this._manualClose(); } } }