Files
AFFiNE-Mirror/packages/frontend/component/src/utils/observe-intersection.ts
2024-12-16 16:55:49 +00:00

78 lines
2.2 KiB
TypeScript

type ObserveIntersection = {
callback: (entity: IntersectionObserverEntry) => void;
dispose: () => void;
};
let _intersectionObserver: IntersectionObserver | null = null;
const elementsMap = new WeakMap<Element, Array<ObserveIntersection>>();
// for debugging
if (typeof window !== 'undefined') {
(window as any)._intersectionObserverElementsMap = elementsMap;
}
/**
* @internal get or initialize the IntersectionObserver instance
*/
const getIntersectionObserver = () =>
(_intersectionObserver ??= new IntersectionObserver(entries => {
entries.forEach(entry => {
const listeners = elementsMap.get(entry.target) ?? [];
listeners.forEach(({ callback }) => callback(entry));
});
}));
/**
* @internal remove element's specific listener
*/
const removeListener = (element: Element, listener: ObserveIntersection) => {
if (!element) return;
const listeners = elementsMap.get(element) ?? [];
const observer = getIntersectionObserver();
// remove the listener from the element
if (listeners.includes(listener)) {
elementsMap.set(
element,
listeners.filter(l => l !== listener)
);
}
// if no more listeners, unobserve the element
if (elementsMap.get(element)?.length === 0) {
observer.unobserve(element);
elementsMap.delete(element);
}
};
/**
* A function to observe the intersection of an element use global IntersectionObserver.
*
* ```ts
* useEffect(() => {
* const dispose1 = observeIntersection(elRef1.current, (entry) => {});
* const dispose2 = observeIntersection(elRef2.current, (entry) => {});
*
* return () => {
* dispose1();
* dispose2();
* };
* }, [])
* ```
* @return A function to dispose the observer.
*/
export const observeIntersection = (
element: Element,
callback: ObserveIntersection['callback']
) => {
const observer = getIntersectionObserver();
if (!elementsMap.has(element)) {
observer.observe(element);
}
const prevListeners = elementsMap.get(element) ?? [];
const listener = { callback, dispose: () => {} };
listener.dispose = () => removeListener(element, listener);
elementsMap.set(element, [...prevListeners, listener]);
return listener.dispose;
};