227 lines
4.8 KiB
TypeScript
227 lines
4.8 KiB
TypeScript
import {
|
|
Directive,
|
|
ElementRef,
|
|
EventEmitter,
|
|
Inject,
|
|
Injectable,
|
|
Input,
|
|
OnChanges,
|
|
OnDestroy,
|
|
OnInit,
|
|
Output,
|
|
PLATFORM_ID,
|
|
SimpleChanges,
|
|
NgZone
|
|
} from '@angular/core';
|
|
import { isPlatformBrowser } from '@angular/common';
|
|
|
|
@Directive({ selector: '[ucapClickOutside]' })
|
|
export class ClickOutsideDirective implements OnInit, OnChanges, OnDestroy {
|
|
@Input()
|
|
clickOutsideEnabled = true;
|
|
|
|
@Input()
|
|
attachOutsideOnClick = false;
|
|
|
|
@Input()
|
|
delayClickOutsideInit = false;
|
|
|
|
@Input()
|
|
emitOnBlur = false;
|
|
|
|
@Input()
|
|
exclude = '';
|
|
|
|
@Input()
|
|
excludeBeforeClick = false;
|
|
|
|
@Input()
|
|
clickOutsideEvents = '';
|
|
|
|
@Output()
|
|
ucapClickOutside: EventEmitter<Event> = new EventEmitter<Event>();
|
|
|
|
private nodesExcluded: Array<HTMLElement> = [];
|
|
private events: Array<string> = ['click', 'contextmenu'];
|
|
|
|
constructor(
|
|
private elementRef: ElementRef,
|
|
private ngZone: NgZone,
|
|
@Inject(PLATFORM_ID) private platformId: object
|
|
) {
|
|
this._initOnClickBody = this._initOnClickBody.bind(this);
|
|
this._onClickBody = this._onClickBody.bind(this);
|
|
this._onWindowBlur = this._onWindowBlur.bind(this);
|
|
}
|
|
|
|
ngOnInit() {
|
|
if (!isPlatformBrowser(this.platformId)) {
|
|
return;
|
|
}
|
|
|
|
this._init();
|
|
}
|
|
|
|
ngOnDestroy() {
|
|
if (!isPlatformBrowser(this.platformId)) {
|
|
return;
|
|
}
|
|
|
|
this._removeClickOutsideListener();
|
|
this._removeAttachOutsideOnClickListener();
|
|
this._removeWindowBlurListener();
|
|
}
|
|
|
|
ngOnChanges(changes: SimpleChanges) {
|
|
if (!isPlatformBrowser(this.platformId)) {
|
|
return;
|
|
}
|
|
|
|
if (changes.attachOutsideOnClick || changes.exclude || changes.emitOnBlur) {
|
|
this._init();
|
|
}
|
|
}
|
|
|
|
private _init() {
|
|
if (this.clickOutsideEvents !== '') {
|
|
this.events = this.clickOutsideEvents.split(',').map(e => e.trim());
|
|
}
|
|
|
|
this._excludeCheck();
|
|
|
|
if (this.attachOutsideOnClick) {
|
|
this._initAttachOutsideOnClickListener();
|
|
} else {
|
|
this._initOnClickBody();
|
|
}
|
|
|
|
if (this.emitOnBlur) {
|
|
this._initWindowBlurListener();
|
|
}
|
|
}
|
|
|
|
private _initOnClickBody() {
|
|
if (this.delayClickOutsideInit) {
|
|
setTimeout(this._initClickOutsideListener.bind(this));
|
|
} else {
|
|
this._initClickOutsideListener();
|
|
}
|
|
}
|
|
|
|
private _excludeCheck() {
|
|
if (this.exclude) {
|
|
try {
|
|
const nodes = Array.from(
|
|
document.querySelectorAll(this.exclude)
|
|
) as Array<HTMLElement>;
|
|
if (nodes) {
|
|
this.nodesExcluded = nodes;
|
|
}
|
|
} catch (err) {
|
|
console.error(
|
|
'[ng-click-outside] Check your exclude selector syntax.',
|
|
err
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
private _onClickBody(ev: Event) {
|
|
if (!this.clickOutsideEnabled) {
|
|
return;
|
|
}
|
|
|
|
if (this.excludeBeforeClick) {
|
|
this._excludeCheck();
|
|
}
|
|
|
|
if (
|
|
!this.elementRef.nativeElement.contains(ev.target) &&
|
|
!this._shouldExclude(ev.target)
|
|
) {
|
|
this._emit(ev);
|
|
|
|
if (this.attachOutsideOnClick) {
|
|
this._removeClickOutsideListener();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Resolves problem with outside click on iframe
|
|
* @see https://github.com/arkon/ng-click-outside/issues/32
|
|
*/
|
|
private _onWindowBlur(ev: Event) {
|
|
setTimeout(() => {
|
|
if (!document.hidden) {
|
|
this._emit(ev);
|
|
}
|
|
});
|
|
}
|
|
|
|
private _emit(ev: Event) {
|
|
if (!this.clickOutsideEnabled) {
|
|
return;
|
|
}
|
|
|
|
this.ngZone.run(() => this.ucapClickOutside.emit(ev));
|
|
}
|
|
|
|
private _shouldExclude(target): boolean {
|
|
for (const excludedNode of this.nodesExcluded) {
|
|
if (excludedNode.contains(target)) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private _initClickOutsideListener() {
|
|
this.ngZone.runOutsideAngular(() => {
|
|
this.events.forEach(e =>
|
|
document.body.addEventListener(e, this._onClickBody)
|
|
);
|
|
});
|
|
}
|
|
|
|
private _removeClickOutsideListener() {
|
|
this.ngZone.runOutsideAngular(() => {
|
|
this.events.forEach(e =>
|
|
document.body.removeEventListener(e, this._onClickBody)
|
|
);
|
|
});
|
|
}
|
|
|
|
private _initAttachOutsideOnClickListener() {
|
|
this.ngZone.runOutsideAngular(() => {
|
|
this.events.forEach(e =>
|
|
this.elementRef.nativeElement.addEventListener(e, this._initOnClickBody)
|
|
);
|
|
});
|
|
}
|
|
|
|
private _removeAttachOutsideOnClickListener() {
|
|
this.ngZone.runOutsideAngular(() => {
|
|
this.events.forEach(e =>
|
|
this.elementRef.nativeElement.removeEventListener(
|
|
e,
|
|
this._initOnClickBody
|
|
)
|
|
);
|
|
});
|
|
}
|
|
|
|
private _initWindowBlurListener() {
|
|
this.ngZone.runOutsideAngular(() => {
|
|
window.addEventListener('blur', this._onWindowBlur);
|
|
});
|
|
}
|
|
|
|
private _removeWindowBlurListener() {
|
|
this.ngZone.runOutsideAngular(() => {
|
|
window.removeEventListener('blur', this._onWindowBlur);
|
|
});
|
|
}
|
|
}
|