-
Notifications
You must be signed in to change notification settings - Fork 190
/
Copy pathobservers.ts
127 lines (107 loc) · 3.92 KB
/
observers.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
124
125
126
127
import { ObserverInstanceCallback } from './index';
const ObserverMap = new Map<
string,
{
id: string;
observer: IntersectionObserver;
elements: Map<Element, Array<ObserverInstanceCallback>>;
}
>();
const RootIds: Map<Element, string> = new Map();
let consecutiveRootId = 0;
/**
* Generate a unique ID for the root element
* @param root
*/
function getRootId(root?: Element | null) {
if (!root) return '';
if (RootIds.has(root)) return RootIds.get(root);
consecutiveRootId += 1;
RootIds.set(root, consecutiveRootId.toString());
return RootIds.get(root);
}
/**
* Convert the options to a string Id, based on the values.
* Ensures we can reuse the same observer for, when observer elements with the same options.
* @param options
*/
export function optionsToId(options: IntersectionObserverInit) {
const values = Object.keys(options)
.sort()
.map((key) => {
let value = options[key];
if (key === 'root') {
value = getRootId(options.root);
}
return `${key}_${value}`;
});
return values.join('|');
}
function createObserver(options: IntersectionObserverInit) {
// Create a unique ID for this observer instance, based on the root, root margin and threshold.
let id = optionsToId(options);
let instance = ObserverMap.get(id);
if (!instance) {
// Create a map of elements this observer is going to observe. Each element has a list of callbacks that should be triggered, once it comes into view.
const elements = new Map<Element, Array<ObserverInstanceCallback>>();
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
// While it would be nice if you could just look at isIntersecting to determine if the component is inside the viewport, browsers can't agree on how to use it.
// -Firefox ignores `threshold` when considering `isIntersecting`, so it will never be false again if `threshold` is > 0
const inView = observer.thresholds.some((threshold) => {
return !entry.isIntersecting
? // The intersectionRatio should be more than the threshold to be considered inside the viewport
entry.intersectionRatio > threshold
: // If we're not intersecting, make sure we accept `intersectionRatio` 0 as not inside the viewport
entry.intersectionRatio >= threshold;
});
// @ts-ignore support IntersectionObserver v2
if (options.trackVisibility && typeof entry.isVisible === 'undefined') {
// The browser doesn't support Intersection Observer v2, falling back to v1 behavior.
// @ts-ignore
entry.isVisible = inView;
}
elements.get(entry.target)?.forEach((callback) => {
callback(inView && entry.isIntersecting, entry);
});
});
}, options);
instance = {
id,
observer,
elements,
};
ObserverMap.set(id, instance);
}
return instance;
}
export function observe(
element: Element,
callback: ObserverInstanceCallback,
options: IntersectionObserverInit = {},
) {
if (!element) return () => {};
// An observer with the same options can be reused, so lets use this fact
const { id, observer, elements } = createObserver(options);
// Register the callback listener for this element
let callbacks = elements.get(element) || [];
if (!elements.has(element)) {
elements.set(element, callbacks);
}
callbacks.push(callback);
observer.observe(element);
return function unobserve() {
// Remove the callback from the callback list
callbacks.splice(callbacks.indexOf(callback), 1);
if (callbacks.length === 0) {
// No more callback exists for element, so destroy it
elements.delete(element);
observer.unobserve(element);
}
if (elements.size === 0) {
// No more elements are being observer by this instance, so destroy it
observer.disconnect();
ObserverMap.delete(id);
}
};
}