TelegramPWA/src/hooks/useIntersectionObserver.ts

180 lines
4.6 KiB
TypeScript

import { RefObject } from 'react';
import {
useEffect, useRef, useCallback, useState,
} from '../lib/teact/teact';
import { throttle, debounce } from '../util/schedulers';
import useHeavyAnimationCheck from './useHeavyAnimationCheck';
type TargetCallback = (entry: IntersectionObserverEntry) => void;
type RootCallback = (entries: IntersectionObserverEntry[]) => void;
type ObserveCleanup = NoneToVoidFunction;
export type ObserveFn = (target: HTMLElement, targetCallback?: TargetCallback) => ObserveCleanup;
interface IntersectionController {
observer: IntersectionObserver;
callbacks: Map<HTMLElement, TargetCallback>;
}
interface Response {
observe: ObserveFn;
freeze: NoneToVoidFunction;
unfreeze: NoneToVoidFunction;
}
export function useIntersectionObserver({
rootRef,
throttleMs,
debounceMs,
shouldSkipFirst,
margin,
threshold,
isDisabled,
}: {
rootRef: RefObject<HTMLDivElement>;
throttleMs?: number;
debounceMs?: number;
shouldSkipFirst?: boolean;
margin?: number;
threshold?: number | number[];
isDisabled?: boolean;
}, rootCallback?: RootCallback): Response {
const controllerRef = useRef<IntersectionController>();
const rootCallbackRef = useRef<RootCallback>();
const freezeFlagsRef = useRef(0);
const onUnfreezeRef = useRef<NoneToVoidFunction>();
rootCallbackRef.current = rootCallback;
const freeze = useCallback(() => {
freezeFlagsRef.current++;
}, []);
const unfreeze = useCallback(() => {
if (!freezeFlagsRef.current) {
return;
}
freezeFlagsRef.current--;
if (!freezeFlagsRef.current && onUnfreezeRef.current) {
onUnfreezeRef.current();
onUnfreezeRef.current = undefined;
}
}, []);
useHeavyAnimationCheck(freeze, unfreeze);
useEffect(() => {
if (isDisabled) {
return undefined;
}
return () => {
if (controllerRef.current) {
controllerRef.current.observer.disconnect();
controllerRef.current.callbacks.clear();
controllerRef.current = undefined;
}
};
}, [isDisabled]);
function initController() {
const callbacks = new Map();
const entriesAccumulator = new Map<Element, IntersectionObserverEntry>();
const observerCallbackSync = () => {
const entries = Array.from(entriesAccumulator.values());
entries.forEach((entry: IntersectionObserverEntry) => {
const callback = callbacks.get(entry.target);
if (callback) {
callback!(entry, entries);
}
});
if (rootCallbackRef.current) {
rootCallbackRef.current(entries);
}
entriesAccumulator.clear();
};
const scheduler = throttleMs ? throttle : debounceMs ? debounce : undefined;
const observerCallback = scheduler
? scheduler(observerCallbackSync, (throttleMs || debounceMs)!, !shouldSkipFirst)
: observerCallbackSync;
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
entriesAccumulator.set(entry.target, entry);
});
if (freezeFlagsRef.current) {
onUnfreezeRef.current = () => {
observerCallback();
};
} else {
observerCallback();
}
},
{
root: rootRef.current,
rootMargin: margin ? `${margin}px` : undefined,
threshold,
},
);
controllerRef.current = { observer, callbacks };
}
const observe = useCallback((target, targetCallback) => {
if (!controllerRef.current) {
initController();
}
const controller = controllerRef.current!;
controller.observer.observe(target);
if (targetCallback) {
controller.callbacks.set(target, targetCallback);
}
return () => {
if (targetCallback) {
controller.callbacks.delete(target);
}
controller.observer.unobserve(target);
};
// Arguments should never change
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isDisabled]);
return { observe, freeze, unfreeze };
}
export function useOnIntersect(
targetRef: RefObject<HTMLDivElement>, observe?: ObserveFn, callback?: TargetCallback,
) {
useEffect(() => {
return observe ? observe(targetRef.current!, callback) : undefined;
// Arguments should never change
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
}
export function useIsIntersecting(
targetRef: RefObject<HTMLDivElement>, observe?: ObserveFn, callback?: TargetCallback,
) {
const [isIntersecting, setIsIntersecting] = useState(!observe);
useOnIntersect(targetRef, observe, (entry) => {
setIsIntersecting(entry.isIntersecting);
if (callback) {
callback(entry);
}
});
return isIntersecting;
}