From c1b2cf1b158dcdf9a06910fe0edefe8ccad98c81 Mon Sep 17 00:00:00 2001 From: Kr0san89 Date: Mon, 26 Feb 2024 17:37:10 +0100 Subject: [PATCH] feat: put code for emit on blur into dedicated directive (#55) * feat: put code for emit on blur into dedicated directive BREAKING CHANGE: emitOnBlur property is now an Directive NgClickOutsideEmitOnBlurDirective --- .gitignore | 1 + README.md | 16 +++--- projects/ng-click-outside2/package.json | 2 +- ...ick-outside-emit-on-blur.directive.spec.ts | 54 +++++++++++++++++++ ...ng-click-outside-emit-on-blur.directive.ts | 47 ++++++++++++++++ ...ng-click-outside-exclude.directive.spec.ts | 4 +- .../src/lib/ng-click-outside.directive.ts | 36 ------------- 7 files changed, 113 insertions(+), 47 deletions(-) create mode 100644 projects/ng-click-outside2/src/lib/ng-click-outside-emit-on-blur.directive.spec.ts create mode 100644 projects/ng-click-outside2/src/lib/ng-click-outside-emit-on-blur.directive.ts diff --git a/.gitignore b/.gitignore index 105c00f..e1f6bab 100644 --- a/.gitignore +++ b/.gitignore @@ -44,3 +44,4 @@ testem.log # System Files .DS_Store Thumbs.db +/.nx/ diff --git a/README.md b/README.md index e85f1f3..88d7d19 100644 --- a/README.md +++ b/README.md @@ -42,14 +42,14 @@ If you use Angular <= 12 please use the original package. https://www.npmjs.com/ ### Options -| Property name | Type | Default | Description | -| ------------- | ---- | ------- |---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `attachOutsideOnClick` | boolean | `false` | By default, the outside click event handler is automatically attached. Explicitely setting this to `true` sets the handler after the element is clicked. The outside click event handler will then be removed after a click outside has occurred. | -| `clickOutsideEnabled` | boolean | `true` | Enables directive. | -| `clickOutsideEvents` | string | `'click'` | A comma-separated list of events to cause the trigger. For example, for additional mobile support: `[clickOutsideEvents]="'click,touchstart'"`. | -| `delayClickOutsideInit` | boolean | `false` | Delays the initialization of the click outside handler. This may help for items that are conditionally shown ([see issue #13](https://github.com/arkon/ng-click-outside/issues/13)). | -| `emitOnBlur` | boolean | `false` | If enabled, emits an event when user clicks outside of applications' window while it's visible. Especially useful if page contains iframes. | -| `clickOutsideExclude` | string | | A comma-separated string of DOM element queries to exclude when clicking outside of the element. (Import NgClickOutsideExcludeDirective) For example: `[clickOutsideExclude]="'button,.btn-primary'"`. | +| Property name | Type | Default | Description | +| ------------- |---------|-----------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `attachOutsideOnClick` | boolean | `false` | By default, the outside click event handler is automatically attached. Explicitely setting this to `true` sets the handler after the element is clicked. The outside click event handler will then be removed after a click outside has occurred. | +| `clickOutsideEnabled` | boolean | `true` | Enables directive. | +| `clickOutsideEvents` | string | `'click'` | A comma-separated list of events to cause the trigger. For example, for additional mobile support: `[clickOutsideEvents]="'click,touchstart'"`. | +| `delayClickOutsideInit` | boolean | `false` | Delays the initialization of the click outside handler. This may help for items that are conditionally shown ([see issue #13](https://github.com/arkon/ng-click-outside/issues/13)). | +| `clickOutsideEmitOnBlur` | - | - | If enabled, emits an `blurWindow` event when user clicks outside of applications' window while it's visible. Especially useful if page contains iframes. (Import `NgClickOutsideEmitOnBlurDirective`) | +| `clickOutsideExclude` | string | | A comma-separated string of DOM element queries to exclude when clicking outside of the element. (Import NgClickOutsideExcludeDirective) For example: `[clickOutsideExclude]="'button,.btn-primary'"`. | ## Example Usage diff --git a/projects/ng-click-outside2/package.json b/projects/ng-click-outside2/package.json index 791e3da..4850392 100644 --- a/projects/ng-click-outside2/package.json +++ b/projects/ng-click-outside2/package.json @@ -1,6 +1,6 @@ { "name": "ng-click-outside2", - "version": "13.0.0", + "version": "14.0.0", "description": "Angular directive for handling click events outside an element.", "license": "MIT", "keywords": [ diff --git a/projects/ng-click-outside2/src/lib/ng-click-outside-emit-on-blur.directive.spec.ts b/projects/ng-click-outside2/src/lib/ng-click-outside-emit-on-blur.directive.spec.ts new file mode 100644 index 0000000..d3da3d8 --- /dev/null +++ b/projects/ng-click-outside2/src/lib/ng-click-outside-emit-on-blur.directive.spec.ts @@ -0,0 +1,54 @@ +import {NgClickOutsideEmitOnBlurDirective} from './ng-click-outside-emit-on-blur.directive'; +import {ComponentFixture, TestBed} from "@angular/core/testing"; +import {DOCUMENT} from "@angular/common"; +import {Component, ViewChild} from "@angular/core"; +import {By} from "@angular/platform-browser"; + + +@Component({ + // eslint-disable-next-line @angular-eslint/component-selector + selector: 'test-click', + standalone: true, + imports: [NgClickOutsideEmitOnBlurDirective], + template: ` + + ` +}) +class TestClickOutsideComponent { + @ViewChild(NgClickOutsideEmitOnBlurDirective) ngClickOutsideEmitOnBlurDirective?: NgClickOutsideEmitOnBlurDirective + blurWindowButton1 = 0; + clickButton1 = 0; +} + + +describe('NgClickOutsideEmitOnBlurDirective', () => { + let component: TestClickOutsideComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [TestClickOutsideComponent], + providers: [{provider: DOCUMENT, useValue: document}] + }).compileComponents(); + + fixture = TestBed.createComponent(TestClickOutsideComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }) + + it('should create an instance', () => { + expect(component.ngClickOutsideEmitOnBlurDirective).toBeDefined(); + }); + + it('should emit on window blur', () => { + const button1 = fixture.debugElement.query(By.css('#b-1')); + + button1.nativeElement.click(); + document.defaultView!.dispatchEvent(new Event('blur')); + + expect(component.clickButton1).toBe(1); + expect(component.blurWindowButton1).toBe(1); + }); +}); diff --git a/projects/ng-click-outside2/src/lib/ng-click-outside-emit-on-blur.directive.ts b/projects/ng-click-outside2/src/lib/ng-click-outside-emit-on-blur.directive.ts new file mode 100644 index 0000000..407cc7f --- /dev/null +++ b/projects/ng-click-outside2/src/lib/ng-click-outside-emit-on-blur.directive.ts @@ -0,0 +1,47 @@ +import {afterNextRender, Directive, EventEmitter, inject, NgZone, OnDestroy, Output} from '@angular/core'; +import {DOCUMENT} from "@angular/common"; + +/** + * emits an event when user clicks outside of applications' window while it's visible. + * Especially useful if page contains iframes. + */ +@Directive({ + selector: '[clickOutsideEmitOnBlur]', + standalone: true +}) +export class NgClickOutsideEmitOnBlurDirective implements OnDestroy { + private _ngZone = inject(NgZone); + private document = inject(DOCUMENT); + @Output() blurWindow: EventEmitter = new EventEmitter(); + + constructor() { + this._onWindowBlur = this._onWindowBlur.bind(this); + afterNextRender(() => this._initWindowBlurListener()) + } + + ngOnDestroy() { + this._removeWindowBlurListener(); + } + + private _initWindowBlurListener() { + this._ngZone.runOutsideAngular(() => { + this.document.defaultView?.addEventListener('blur', this._onWindowBlur); + }); + } + + /** + * Resolves problem with outside click on iframe + * @see https://github.com/arkon/ng-click-outside/issues/32 + */ + private _onWindowBlur(ev: Event) { + if (!this.document.hidden) { + this._ngZone.run(() => this.blurWindow.emit(ev)); + } + } + + private _removeWindowBlurListener() { + this._ngZone.runOutsideAngular(() => { + this.document.defaultView?.removeEventListener('blur', this._onWindowBlur); + }); + } +} diff --git a/projects/ng-click-outside2/src/lib/ng-click-outside-exclude.directive.spec.ts b/projects/ng-click-outside2/src/lib/ng-click-outside-exclude.directive.spec.ts index 264ffa8..0710f9d 100644 --- a/projects/ng-click-outside2/src/lib/ng-click-outside-exclude.directive.spec.ts +++ b/projects/ng-click-outside2/src/lib/ng-click-outside-exclude.directive.spec.ts @@ -53,6 +53,6 @@ describe('NgClickOutsideExcludeDirective', () => { button3.nativeElement.click(); expect(component.clickButton3).toBe(1); expect(component.clickButton2).toBe(0); - expect(component.clickOutsideButton2).toBe(0); }); - + expect(component.clickOutsideButton2).toBe(0); + }); }); diff --git a/projects/ng-click-outside2/src/lib/ng-click-outside.directive.ts b/projects/ng-click-outside2/src/lib/ng-click-outside.directive.ts index 633a301..1ebcb2c 100644 --- a/projects/ng-click-outside2/src/lib/ng-click-outside.directive.ts +++ b/projects/ng-click-outside2/src/lib/ng-click-outside.directive.ts @@ -54,11 +54,6 @@ export class NgClickOutsideDirective implements OnChanges, OnDestroy { * This may help for items that are conditionally shown ([see issue #13](https://github.com/arkon/ng-click-outside/issues/13)). */ @Input() delayClickOutsideInit = false; - /** - * If enabled, emits an event when user clicks outside of applications' window while it's visible. - * Especially useful if page contains iframes. - */ - @Input() emitOnBlur = false; /** * A comma-separated list of events to cause the trigger. @@ -81,14 +76,12 @@ export class NgClickOutsideDirective implements OnChanges, OnDestroy { constructor() { this._initOnClickBody = this._initOnClickBody.bind(this); this._onClickBody = this._onClickBody.bind(this); - this._onWindowBlur = this._onWindowBlur.bind(this); afterNextRender(() => this._init()) } ngOnDestroy() { this._removeClickOutsideListener(); this._removeAttachOutsideOnClickListener(); - this._removeWindowBlurListener(); } ngOnChanges(changes: SimpleChanges) { @@ -107,10 +100,6 @@ export class NgClickOutsideDirective implements OnChanges, OnDestroy { } else { this._initOnClickBody(); } - - if (this.emitOnBlur) { - this._initWindowBlurListener(); - } } private _initOnClickBody() { @@ -136,18 +125,6 @@ export class NgClickOutsideDirective implements OnChanges, OnDestroy { } } - /** - * Resolves problem with outside click on iframe - * @see https://github.com/arkon/ng-click-outside/issues/32 - */ - private _onWindowBlur(ev: Event) { - setTimeout(() => { - if (!this.document.hidden) { - this._emit(ev); - } - }); - } - private _emit(ev: Event) { if (!this.clickOutsideEnabled) { return; @@ -156,7 +133,6 @@ export class NgClickOutsideDirective implements OnChanges, OnDestroy { this._ngZone.run(() => this.clickOutside.emit(ev)); } - private _initClickOutsideListener() { this._ngZone.runOutsideAngular(() => { this._events.forEach(e => this.document.addEventListener(e, this._onClickBody)); @@ -180,16 +156,4 @@ export class NgClickOutsideDirective implements OnChanges, OnDestroy { this._events.forEach(e => this._el.nativeElement.removeEventListener(e, this._initOnClickBody)); }); } - - private _initWindowBlurListener() { - this._ngZone.runOutsideAngular(() => { - this.document.defaultView?.addEventListener('blur', this._onWindowBlur); - }); - } - - private _removeWindowBlurListener() { - this._ngZone.runOutsideAngular(() => { - this.document.defaultView?.removeEventListener('blur', this._onWindowBlur); - }); - } }