Skip to content

Commit 74cf9cb

Browse files
steven008thePunderWoman
authored andcommitted
refactor(migrations): moved onInteraction, onHover (angular#61342)
, onViewport to core/primitives PR Close angular#61342
1 parent 4554c09 commit 74cf9cb

3 files changed

Lines changed: 275 additions & 161 deletions

File tree

Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
2+
/** Configuration object used to register passive and capturing events. */
3+
const eventListenerOptions: AddEventListenerOptions = {
4+
passive: true,
5+
capture: true,
6+
};
7+
8+
/** Keeps track of the currently-registered `on hover` triggers. */
9+
const hoverTriggers = new WeakMap<Element, DeferEventEntry>();
10+
11+
/** Keeps track of the currently-registered `on interaction` triggers. */
12+
const interactionTriggers = new WeakMap<Element, DeferEventEntry>();
13+
14+
/** Currently-registered `viewport` triggers. */
15+
const viewportTriggers = new WeakMap<Element, DeferEventEntry>();
16+
17+
/** Names of the events considered as interaction events. */
18+
export const interactionEventNames = ['click', 'keydown'] as const;
19+
20+
/** Names of the events considered as hover events. */
21+
export const hoverEventNames = ['mouseenter', 'mouseover', 'focusin'] as const;
22+
23+
/** `IntersectionObserver` used to observe `viewport` triggers. */
24+
let intersectionObserver: IntersectionObserver | null = null;
25+
26+
/** Number of elements currently observed with `viewport` triggers. */
27+
let observedViewportElements = 0;
28+
29+
/** Object keeping track of registered callbacks for a deferred block trigger. */
30+
class DeferEventEntry {
31+
callbacks = new Set<VoidFunction>();
32+
33+
listener = () => {
34+
for (const callback of this.callbacks) {
35+
callback();
36+
}
37+
};
38+
}
39+
40+
export function getViewportTriggers() {
41+
return viewportTriggers;
42+
}
43+
44+
/**
45+
* Registers an interaction trigger.
46+
* @param trigger Element that is the trigger.
47+
* @param callback Callback to be invoked when the trigger is interacted with.
48+
*/
49+
export function onInteraction(trigger: Element, callback: VoidFunction): VoidFunction {
50+
let entry = interactionTriggers.get(trigger);
51+
52+
// If this is the first entry for this element, add the listeners.
53+
if (!entry) {
54+
// Note that managing events centrally like this lends itself well to using global
55+
// event delegation. It currently does delegation at the element level, rather than the
56+
// document level, because:
57+
// 1. Global delegation is the most effective when there are a lot of events being registered
58+
// at the same time. Deferred blocks are unlikely to be used in such a way.
59+
// 2. Matching events to their target isn't free. For each `click` and `keydown` event we
60+
// would have look through all the triggers and check if the target either is the element
61+
// itself or it's contained within the element. Given that `click` and `keydown` are some
62+
// of the most common events, this may end up introducing a lot of runtime overhead.
63+
// 3. We're still registering only two events per element, no matter how many deferred blocks
64+
// are referencing it.
65+
entry = new DeferEventEntry();
66+
interactionTriggers.set(trigger, entry);
67+
68+
for (const name of interactionEventNames) {
69+
trigger.addEventListener(name, entry!.listener, eventListenerOptions);
70+
}
71+
}
72+
73+
entry.callbacks.add(callback);
74+
75+
return () => {
76+
const {callbacks, listener} = entry!;
77+
callbacks.delete(callback);
78+
79+
if (callbacks.size === 0) {
80+
interactionTriggers.delete(trigger);
81+
82+
for (const name of interactionEventNames) {
83+
trigger.removeEventListener(name, listener, eventListenerOptions);
84+
}
85+
}
86+
};
87+
}
88+
89+
90+
/**
91+
* Registers a hover trigger.
92+
* @param trigger Element that is the trigger.
93+
* @param callback Callback to be invoked when the trigger is hovered over.
94+
*/
95+
export function onHover(trigger: Element, callback: VoidFunction): VoidFunction {
96+
let entry = hoverTriggers.get(trigger);
97+
98+
// If this is the first entry for this element, add the listener.
99+
if (!entry) {
100+
entry = new DeferEventEntry();
101+
hoverTriggers.set(trigger, entry);
102+
103+
for (const name of hoverEventNames) {
104+
trigger.addEventListener(name, entry!.listener, eventListenerOptions);
105+
}
106+
}
107+
108+
entry.callbacks.add(callback);
109+
110+
return () => {
111+
const {callbacks, listener} = entry!;
112+
callbacks.delete(callback);
113+
114+
if (callbacks.size === 0) {
115+
for (const name of hoverEventNames) {
116+
trigger.removeEventListener(name, listener, eventListenerOptions);
117+
}
118+
hoverTriggers.delete(trigger);
119+
}
120+
};
121+
}
122+
123+
export interface Observer {
124+
observe: (target: Element) => void,
125+
unobserve: (target: Element) => void,
126+
disconnect: () => void;
127+
}
128+
129+
/**
130+
* Registers a viewport trigger.
131+
* @param trigger Element that is the trigger.
132+
* @param callback Callback to be invoked when the trigger comes into the viewport.
133+
* @param observer Observer interface which provides a way to observe changes to target element
134+
*/
135+
export function onViewport(
136+
trigger: Element,
137+
callback: VoidFunction,
138+
observer: Observer,
139+
): VoidFunction {
140+
let entry = viewportTriggers.get(trigger);
141+
142+
if (!entry) {
143+
entry = new DeferEventEntry();
144+
observer.observe(trigger);
145+
viewportTriggers.set(trigger, entry);
146+
observedViewportElements++;
147+
}
148+
149+
entry.callbacks.add(callback);
150+
151+
return () => {
152+
// It's possible that a different cleanup callback fully removed this element already.
153+
if (!viewportTriggers.has(trigger)) {
154+
return;
155+
}
156+
157+
entry!.callbacks.delete(callback);
158+
159+
if (entry!.callbacks.size === 0) {
160+
observer.unobserve(trigger);
161+
viewportTriggers.delete(trigger);
162+
observedViewportElements--;
163+
}
164+
165+
if (observedViewportElements === 0) {
166+
observer.disconnect();
167+
}
168+
};
169+
}
170+
171+
172+
// function createIntersectionObserver(ngZone: NgZone | undefined): IntersectionObserver {
173+
// return new IntersectionObserver((entries) => {
174+
// for (const current of entries) {
175+
// // Only invoke the callbacks if the specific element is intersecting.
176+
// if (current.isIntersecting && viewportTriggers.has(current.target)) {
177+
// if (ngZone) {
178+
// ngZone!.run(viewportTriggers.get(current.target)!.listener);
179+
// } else {
180+
// viewportTriggers.get(current.target)!.listener();
181+
// }
182+
// }
183+
// }
184+
// })
185+
// }
186+
187+
// /**
188+
// * Registers a viewport trigger.
189+
// * @param trigger Element that is the trigger.
190+
// * @param callback Callback to be invoked when the trigger comes into the viewport.
191+
// * @param injector Injector that can be used by the trigger to resolve DI tokens.
192+
// */
193+
// export function onViewport(
194+
// trigger: Element,
195+
// callback: VoidFunction,
196+
// injector?: Injector,
197+
// ): VoidFunction {
198+
// const ngZone = injector?.get(NgZone);
199+
// let entry = viewportTriggers.get(trigger);
200+
201+
// if (!intersectionObserver) {
202+
// if (injector) {
203+
// intersectionObserver = ngZone!.runOutsideAngular(() => {
204+
// return createIntersectionObserver(ngZone);
205+
// });
206+
// } else {
207+
// intersectionObserver = createIntersectionObserver(ngZone);
208+
// }
209+
// }
210+
211+
// if (!entry) {
212+
// entry = new DeferEventEntry();
213+
// if (ngZone) {
214+
// ngZone.runOutsideAngular(() => intersectionObserver!.observe(trigger));
215+
// } else {
216+
// intersectionObserver!.observe(trigger);
217+
// }
218+
// viewportTriggers.set(trigger, entry);
219+
// observedViewportElements++;
220+
// }
221+
222+
// entry.callbacks.add(callback);
223+
224+
// return () => {
225+
// // It's possible that a different cleanup callback fully removed this element already.
226+
// if (!viewportTriggers.has(trigger)) {
227+
// return;
228+
// }
229+
230+
// entry!.callbacks.delete(callback);
231+
232+
// if (entry!.callbacks.size === 0) {
233+
// intersectionObserver?.unobserve(trigger);
234+
// viewportTriggers.delete(trigger);
235+
// observedViewportElements--;
236+
// }
237+
238+
// if (observedViewportElements === 0) {
239+
// intersectionObserver?.disconnect();
240+
// intersectionObserver = null;
241+
// }
242+
// };
243+
// }
244+

0 commit comments

Comments
 (0)