@coin-voyage/paykit
Version:
Seamless crypto payments. Onboard users from any chain, any coin into your app with one click.
111 lines (110 loc) • 4.26 kB
JavaScript
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
// Suppress `useLayoutEffect` warning on SSR
const useIsoLayoutEffect = typeof window !== "undefined" && window.document && window.document.createElement !== undefined
? useLayoutEffect
: useEffect;
const useFitText = ({ maxFontSize = 100, minFontSize = 20, onFinish, onStart, resolution = 5 } = {}) => {
const onStartRef = useRef(null);
const onFinishRef = useRef(null);
useEffect(() => {
onStartRef.current = onStart;
}, [onStart]);
useEffect(() => {
onFinishRef.current = onFinish;
}, [onFinish]);
const initState = useCallback(() => ({
calcKey: 0,
fontSize: maxFontSize,
fontSizePrev: minFontSize,
fontSizeMax: maxFontSize,
fontSizeMin: minFontSize,
}), [maxFontSize, minFontSize]);
const ref = useRef(null);
const innerHtmlPrevRef = useRef(null);
const isCalculatingRef = useRef(false);
const [state, setState] = useState(initState);
const { calcKey, fontSize, fontSizeMax, fontSizeMin, fontSizePrev } = state;
const animationFrameIdRef = useRef(null);
const roRef = useRef(null);
// Initialize ResizeObserver
useEffect(() => {
roRef.current = new ResizeObserver(() => {
animationFrameIdRef.current = window.requestAnimationFrame(() => {
if (isCalculatingRef.current)
return;
onStartRef.current?.();
isCalculatingRef.current = true;
setState((prev) => ({ ...initState(), calcKey: prev.calcKey + 1 }));
});
});
if (ref.current)
roRef.current.observe(ref.current);
return () => {
if (animationFrameIdRef.current)
window.cancelAnimationFrame(animationFrameIdRef.current);
roRef.current?.disconnect();
};
}, [initState]);
// Recalculate when innerHTML changes
useEffect(() => {
if (!ref.current)
return;
const innerHtml = ref.current.innerHTML;
if (calcKey === 0 || isCalculatingRef.current)
return;
if (innerHtml !== innerHtmlPrevRef.current) {
onStartRef.current?.();
window.requestAnimationFrame(() => {
setState((prev) => ({ ...initState(), calcKey: prev.calcKey + 1 }));
});
}
innerHtmlPrevRef.current = innerHtml;
}, [calcKey, initState]);
// Adjust font size
useIsoLayoutEffect(() => {
if (calcKey === 0 || !ref.current)
return;
const container = ref.current;
const isOverflow = container.scrollHeight > container.offsetHeight || container.scrollWidth > container.offsetWidth;
const isWithinResolution = Math.abs(fontSize - fontSizePrev) <= resolution;
const isFailed = isOverflow && fontSize === fontSizePrev;
const isAsc = fontSize > fontSizePrev;
if (isWithinResolution) {
if (isFailed) {
isCalculatingRef.current = false;
}
else if (isOverflow) {
setState((prev) => ({
...prev,
fontSize: isAsc ? fontSizePrev : fontSizeMin,
}));
}
else {
isCalculatingRef.current = false;
onFinishRef.current?.(fontSize);
}
return;
}
// Binary search adjustment
let delta;
let newMax = fontSizeMax;
let newMin = fontSizeMin;
if (isOverflow) {
delta = isAsc ? fontSizePrev - fontSize : fontSizeMin - fontSize;
newMax = Math.min(fontSizeMax, fontSize);
}
else {
delta = isAsc ? fontSizeMax - fontSize : fontSizePrev - fontSize;
newMin = Math.max(fontSizeMin, fontSize);
}
setState({
calcKey,
fontSize: fontSize + delta / 2,
fontSizeMax: newMax,
fontSizeMin: newMin,
fontSizePrev: fontSize,
});
}, [calcKey, fontSize, fontSizeMax, fontSizeMin, fontSizePrev, resolution]);
return { fontSize, ref };
};
export default useFitText;