/
autoclose.ts
65 lines (53 loc) · 3 KB
/
autoclose.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
import {NgZone} from '@angular/core';
import {fromEvent, Observable, race} from 'rxjs';
import {delay, filter, map, takeUntil, tap, withLatestFrom} from 'rxjs/operators';
import {Key} from './key';
import {closest} from './util';
const isContainedIn = (element: HTMLElement, array?: HTMLElement[]) =>
array ? array.some(item => item.contains(element)) : false;
const matchesSelectorIfAny = (element: HTMLElement, selector?: string) =>
!selector || closest(element, selector) != null;
// we have to add a more significant delay to avoid re-opening when handling (click) on a toggling element
// TODO: use proper Angular platform detection when NgbAutoClose becomes a service and we can inject PLATFORM_ID
const isMobile = () => typeof navigator !== 'undefined' ?
!!navigator.userAgent && /iPad|iPhone|iPod|Android/.test(navigator.userAgent) :
false;
// setting 'ngbAutoClose' synchronously on mobile results in immediate popup closing
// when tapping on the triggering element
const wrapAsyncForMobile = fn => isMobile() ? () => setTimeout(() => fn(), 100) : fn;
export function ngbAutoClose(
zone: NgZone, document: any, type: boolean | 'inside' | 'outside', close: () => void, closed$: Observable<any>,
insideElements: HTMLElement[], ignoreElements?: HTMLElement[], insideSelector?: string) {
// closing on ESC and outside clicks
if (type) {
zone.runOutsideAngular(wrapAsyncForMobile(() => {
const shouldCloseOnClick = (event: MouseEvent) => {
const element = event.target as HTMLElement;
if (event.button === 2 || isContainedIn(element, ignoreElements)) {
return false;
}
if (type === 'inside') {
return isContainedIn(element, insideElements) && matchesSelectorIfAny(element, insideSelector);
} else if (type === 'outside') {
return !isContainedIn(element, insideElements);
} else /* if (type === true) */ {
return matchesSelectorIfAny(element, insideSelector) || !isContainedIn(element, insideElements);
}
};
const escapes$ = fromEvent<KeyboardEvent>(document, 'keydown')
.pipe(
takeUntil(closed$),
// tslint:disable-next-line:deprecation
filter(e => e.which === Key.Escape), tap(e => e.preventDefault()));
// we have to pre-calculate 'shouldCloseOnClick' on 'mousedown',
// because on 'mouseup' DOM nodes might be detached
const mouseDowns$ =
fromEvent<MouseEvent>(document, 'mousedown').pipe(map(shouldCloseOnClick), takeUntil(closed$));
const closeableClicks$ = fromEvent<MouseEvent>(document, 'mouseup')
.pipe(
withLatestFrom(mouseDowns$), filter(([_, shouldClose]) => shouldClose), delay(0),
takeUntil(closed$)) as Observable<MouseEvent>;
race<Event>([escapes$, closeableClicks$]).subscribe(() => zone.run(close));
}));
}
}