UNPKG

@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
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;