diff --git a/src/click-outside.directive.ts b/src/click-outside.directive.ts index 568182c..e4bb4c4 100644 --- a/src/click-outside.directive.ts +++ b/src/click-outside.directive.ts @@ -12,8 +12,13 @@ import { PLATFORM_ID, SimpleChanges, NgZone, + Renderer2, } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; +import { Observable } from 'rxjs/Observable'; +import { Subject } from 'rxjs/Subject'; +import { merge } from 'rxjs/observable/merge'; +import { takeUntil } from 'rxjs/operators/takeUntil'; @Injectable() @Directive({ selector: '[clickOutside]' }) @@ -23,15 +28,23 @@ export class ClickOutsideDirective implements OnInit, OnChanges, OnDestroy { @Input() delayClickOutsideInit: boolean = false; @Input() exclude: string = ''; @Input() excludeBeforeClick: boolean = false; - @Input() clickOutsideEvents: string = ''; + @Input() set clickOutsideEvents(events: string) { + this._events = events.split(',').map(e => e.trim()); + } @Input() clickOutsideEnabled: boolean = true; @Output() clickOutside: EventEmitter = new EventEmitter(); private _nodesExcluded: Array = []; private _events: Array = ['click']; + private _isPlatformBrowser: boolean = isPlatformBrowser(this.platformId); + + private _beforeInit: Subject = new Subject(); + private _onDestroy: Subject = new Subject(); + private _onOutsideClick: Subject = new Subject(); constructor(private _el: ElementRef, + private _renderer2: Renderer2, private _ngZone: NgZone, @Inject(PLATFORM_ID) private platformId: Object) { this._initOnClickBody = this._initOnClickBody.bind(this); @@ -39,23 +52,23 @@ export class ClickOutsideDirective implements OnInit, OnChanges, OnDestroy { } ngOnInit() { - if (!isPlatformBrowser(this.platformId)) { return; } + if (!this._isPlatformBrowser) { return; } this._init(); } ngOnDestroy() { - if (!isPlatformBrowser(this.platformId)) { return; } + if (!this._isPlatformBrowser) { return; } - if (this.attachOutsideOnClick) { - this._events.forEach(e => this._el.nativeElement.removeEventListener(e, this._initOnClickBody)); - } + this._onDestroy.next(); + this._onDestroy.complete(); - this._events.forEach(e => document.body.removeEventListener(e, this._onClickBody)); + this._onOutsideClick.complete(); + this._beforeInit.complete(); } ngOnChanges(changes: SimpleChanges) { - if (!isPlatformBrowser(this.platformId)) { return; } + if (!this._isPlatformBrowser) { return; } if (changes['attachOutsideOnClick'] || changes['exclude']) { this._init(); @@ -63,15 +76,14 @@ export class ClickOutsideDirective implements OnInit, OnChanges, OnDestroy { } private _init() { - if (this.clickOutsideEvents !== '') { - this._events = this.clickOutsideEvents.split(',').map(e => e.trim()); - } + this._beforeInit.next(); this._excludeCheck(); if (this.attachOutsideOnClick) { this._ngZone.runOutsideAngular(() => { - this._events.forEach(e => this._el.nativeElement.addEventListener(e, this._initOnClickBody)); + this._listenAll(this._el.nativeElement, ...this._events) + .subscribe(this._initOnClickBody); }); } else { this._initOnClickBody(); @@ -88,7 +100,11 @@ export class ClickOutsideDirective implements OnInit, OnChanges, OnDestroy { private _initClickListeners() { this._ngZone.runOutsideAngular(() => { - this._events.forEach(e => document.body.addEventListener(e, this._onClickBody)); + this._listenAll('body', ...this._events) + .pipe( + takeUntil(this._onOutsideClick), + ) + .subscribe(this._onClickBody); }); } @@ -118,18 +134,24 @@ export class ClickOutsideDirective implements OnInit, OnChanges, OnDestroy { this._ngZone.run(() => this.clickOutside.emit(ev)); if (this.attachOutsideOnClick) { - this._events.forEach(e => document.body.removeEventListener(e, this._onClickBody)); + this._onOutsideClick.next(); } } } private _shouldExclude(target): boolean { - for (let excludedNode of this._nodesExcluded) { - if (excludedNode.contains(target)) { - return true; - } - } + return this._nodesExcluded.some(excludedNode => excludedNode.contains(target)); + } + + private _listenAll(target: 'window' | 'document' | 'body' | any, ...eventNames: string[]): Observable { + const sources = eventNames.map(eventName => { + return new Observable(observer => this._renderer2.listen(target, eventName, ev => observer.next(ev))); + }); - return false; + return merge(...sources) + .pipe( + takeUntil(this._beforeInit), + takeUntil(this._onDestroy), + ); } }