File

projects/angular/directives/ui-click-outside/src/ui-click-outside.directive.ts

Description

A directive that emits when a click event occurs outside of the decorated element.

Metadata

Outputs

uiClickOutside
Type : Observable<MouseEvent>

Emits the original MouseEvent when the click occurs outside of the decorated element.

import type { Observable } from 'rxjs';
import {
    fromEvent,
    Subject,
} from 'rxjs';
import {
    filter,
    share,
    takeUntil,
    throttleTime,
} from 'rxjs/operators';

import { DOCUMENT } from '@angular/common';
import {
    Directive,
    ElementRef,
    Inject,
    Injectable,
    OnDestroy,
    Output,
} from '@angular/core';

/**
 *   Info:
 *
 *   On average a user trying hard enough can reach ~7 click/s
 *   We'll reduce the click event count to half
 *
 *   Benefits:
 *
 *   - drastically reduce misclick emissions
 *
 * @ignore
 */
const MAX_CLICKS_PER_SECOND = 3;

/**
 * A service that shares the `global` events required for the `uiClickOutside` directive.
 * By sharing the event stream, we end up adding only one event listener rather than {N}.
 *
 * @export
 */
@Injectable({
    providedIn: 'root',
})
export class UiClickOutsideService implements OnDestroy {
    /**
     * The `global` event handler for `click` events.
     *
     */
    // eslint-disable-next-line rxjs/finnish
    source: Observable<MouseEvent>;
    private _destroyed$ = new Subject<void>();

    /**
     * @ignore
     */
    constructor(
        @Inject(DOCUMENT)
        document: any,
    ) {
        this.source = fromEvent<MouseEvent>((document as Document).body, 'click', {
            capture: true,
        })
            .pipe(
                throttleTime(1000 / MAX_CLICKS_PER_SECOND),
                takeUntil(this._destroyed$),
                share(),
            );
    }

    /**
     * @ignore
     */
    ngOnDestroy() {
        this._destroyed$.next();
    }
}

/**
 * A directive that emits when a click event occurs outside of the decorated element.
 *
 * @export
 */
@Directive({
    selector: '[uiClickOutside]',
})
export class UiClickOutsideDirective {
    /**
     * Emits the original `MouseEvent` when the click occurs outside of the decorated element.
     *
     */
    @Output()
    // eslint-disable-next-line rxjs/finnish
    uiClickOutside: Observable<MouseEvent>;

    /**
     * @ignore
     */
    constructor(
        ref: ElementRef,
        private _clickService: UiClickOutsideService,
    ) {
        const element: HTMLElement = ref.nativeElement;

        this.uiClickOutside = this._clickService
            .source
            .pipe(
                filter(ev =>
                    !element.contains((ev.target as Element)),
                ),
            );
    }
}

results matching ""

    No results matching ""