/
observer-size.ts
123 lines (109 loc) · 3.12 KB
/
observer-size.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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
import {
AfterViewInit,
Directive,
ElementRef,
EventEmitter,
Injectable,
NgModule,
NgZone,
OnDestroy,
Output
} from '@angular/core';
import { Observable, Observer, Subject, Subscription } from 'rxjs';
@Injectable({ providedIn: 'root' })
export class SizeObserver implements OnDestroy {
private _observedElements = new Map<
Element,
{
observer: MutationObserver | null;
readonly stream: Subject<MutationRecord[]>;
count: number;
}
>();
ngOnDestroy(): void {
this._observedElements.forEach((_, element) => this._cleanupObserver(element));
}
observe(element: Element): Observable<MutationRecord[]> {
return new Observable((observer: Observer<MutationRecord[]>) => {
const stream = this._observeElement(element);
const subscription = stream.subscribe(observer);
return () => {
subscription.unsubscribe();
this._unobserveElement(element);
};
});
}
private _observeElement(element: Element): Subject<MutationRecord[]> {
if (!this._observedElements.has(element)) {
const stream = new Subject<MutationRecord[]>();
let observer: MutationObserver | null = null;
if (typeof MutationObserver !== 'undefined') {
observer = new MutationObserver(mutations => stream.next(mutations));
observer.observe(element, {
attributes: true,
attributeOldValue: true,
attributeFilter: ['width', 'height', 'style']
});
}
this._observedElements.set(element, { observer, stream, count: 1 });
} else {
this._observedElements.get(element)!.count++;
}
return this._observedElements.get(element)!.stream;
}
private _unobserveElement(element: Element): void {
if (this._observedElements.has(element)) {
this._observedElements.get(element)!.count--;
if (!this._observedElements.get(element)!.count) {
this._cleanupObserver(element);
}
}
}
private _cleanupObserver(element: Element): void {
if (this._observedElements.has(element)) {
const { observer, stream } = this._observedElements.get(element)!;
if (observer) {
observer.disconnect();
}
stream.complete();
this._observedElements.delete(element);
}
}
}
@Directive({
selector: '[observeSize]',
exportAs: 'observeSize',
standalone: true
})
export class ObserverSize implements AfterViewInit, OnDestroy {
private _sub$: Subscription | null = null;
@Output('observeSize') readonly event = new EventEmitter<MutationRecord[]>();
constructor(
private _obs: SizeObserver,
private el: ElementRef<HTMLElement>,
private ngZone: NgZone
) {}
ngAfterViewInit(): void {
if (!this._sub$) {
this._sub();
}
}
private _sub(): void {
this._unsub();
const stream = this._obs.observe(this.el.nativeElement);
this.ngZone.runOutsideAngular(() => {
this._sub$ = stream.subscribe(this.event);
});
}
private _unsub(): void {
this._sub$?.unsubscribe();
}
ngOnDestroy(): void {
this._unsub();
}
}
@NgModule({
exports: [ObserverSize],
imports: [ObserverSize]
})
export class ObserversModule {}