ng2-sidebar
Version:
[DEPRECATED] Renamed to ng-sidebar.
372 lines (303 loc) • 10.2 kB
text/typescript
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'
};
export class Sidebar implements AfterContentInit, OnChanges, OnDestroy {
// `openChange` allows for 2-way data binding
open: boolean = false;
openChange: EventEmitter<boolean> = new EventEmitter<boolean>();
keyClose: boolean = false;
keyCode: number = 27;
position: string = SIDEBAR_POSITION.Left;
closeOnClickOutside: boolean = false;
showOverlay: boolean = false;
animate: boolean = true;
defaultStyles: boolean = false;
sidebarClass: string;
overlayClass: string;
ariaLabel: string;
trapFocus: boolean = true;
autoFocus: boolean = true;
onOpen: EventEmitter<null> = new EventEmitter<null>();
onClose: EventEmitter<null> = new EventEmitter<null>();
onAnimationStarted: EventEmitter<AnimationTransitionEvent> =
new EventEmitter<AnimationTransitionEvent>();
onAnimationDone: EventEmitter<AnimationTransitionEvent> =
new EventEmitter<AnimationTransitionEvent>();
/** @internal */
_visibleSidebarState: string;
/** @internal */
_visibleOverlayState: string;
private _elSidebar: ElementRef;
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( 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();
}
}
}