UNPKG

react-hooks-and-com

Version:

一个现代化的 React 自定义 Hooks 库,提供 20+ 个实用的自定义 Hooks。使用 TypeScript 和 Tailwind CSS 构建,完全类型安全。

1,548 lines (1,524 loc) 46.9 kB
import React, { useState, useCallback, useRef, useEffect, useLayoutEffect, useMemo } from 'react'; const useCounter = (options = {}) => { const { initialValue = 0, min = -Infinity, max = Infinity, step = 1 } = options; const [count, setCountState] = useState(initialValue); const setCount = useCallback((value) => { const clampedValue = Math.min(Math.max(value, min), max); setCountState(clampedValue); }, [min, max]); const increment = useCallback(() => { setCount(count + step); }, [count, step, setCount]); const decrement = useCallback(() => { setCount(count - step); }, [count, step, setCount]); const reset = useCallback(() => { setCountState(initialValue); }, [initialValue]); const isMin = count <= min; const isMax = count >= max; return { count, increment, decrement, reset, setCount, isMin, isMax }; }; const usePrevious = (value) => { const ref = useRef(void 0); useEffect(() => { ref.current = value; }); return ref.current; }; const useToggle = (options = {}) => { const { initialValue = false, trueValue = true, falseValue = false } = options; const [value, setValue] = useState(initialValue); const toggle = useCallback(() => { setValue((prev) => prev === trueValue ? falseValue : trueValue); }, [trueValue, falseValue]); const setTrue = useCallback(() => { setValue(trueValue); }, [trueValue]); const setFalse = useCallback(() => { setValue(falseValue); }, [falseValue]); return { value, toggle, setValue, setTrue, setFalse }; }; const useWindowSize = (options = {}) => { const { debounceMs = 100, initialSize = { width: 0, height: 0 } } = options; const [size, setSize] = useState(() => { if (typeof window !== "undefined") { return { width: window.innerWidth, height: window.innerHeight }; } return initialSize; }); useEffect(() => { if (typeof window === "undefined") { return; } let timeoutId; const handleResize = () => { clearTimeout(timeoutId); timeoutId = setTimeout(() => { setSize({ width: window.innerWidth, height: window.innerHeight }); }, debounceMs); }; const observer = new ResizeObserver((entries) => { for (const entry of entries) { if (entry.target === document.documentElement) { handleResize(); break; } } }); window.addEventListener("resize", handleResize); observer.observe(document.documentElement); return () => { clearTimeout(timeoutId); window.removeEventListener("resize", handleResize); observer.disconnect(); }; }, [debounceMs]); return size; }; const useLocalStorage = (key, initialValue, options = {}) => { const { serializer = { stringify: JSON.stringify, parse: JSON.parse }, onError = (error) => console.error("useLocalStorage error:", error) } = options; const [storedValue, setStoredValue] = useState(() => { try { if (typeof window === "undefined") { return initialValue; } const item = window.localStorage.getItem(key); return item ? serializer.parse(item) : initialValue; } catch (error) { onError(error); return initialValue; } }); const [isStored, setIsStored] = useState(() => { try { if (typeof window === "undefined") { return false; } return window.localStorage.getItem(key) !== null; } catch { return false; } }); const setValue = useCallback((value) => { try { const valueToStore = value instanceof Function ? value(storedValue) : value; setStoredValue(valueToStore); setIsStored(true); if (typeof window !== "undefined") { window.localStorage.setItem(key, serializer.stringify(valueToStore)); } } catch (error) { onError(error); } }, [key, storedValue, serializer, onError]); const removeValue = useCallback(() => { try { setStoredValue(initialValue); setIsStored(false); if (typeof window !== "undefined") { window.localStorage.removeItem(key); } } catch (error) { onError(error); } }, [key, initialValue, onError]); return { value: storedValue, setValue, removeValue, isStored }; }; const useDebounce = (initialValue, options = {}) => { const { delay = 500, immediate = false } = options; const [value, setValue] = useState(initialValue); const [debouncedValue, setDebouncedValue] = useState(initialValue); const [isPending, setIsPending] = useState(false); useEffect(() => { if (immediate) { setDebouncedValue(value); setIsPending(false); return; } setIsPending(true); const timer = setTimeout(() => { setDebouncedValue(value); setIsPending(false); }, delay); return () => { clearTimeout(timer); }; }, [value, delay, immediate]); return { value, setValue, debouncedValue, isPending }; }; const useThrottle = (initialValue, interval = 500, options = {}) => { const { leading = true, trailing = true } = options; const [value, setValue] = useState(initialValue); const [throttledValue, setThrottledValue] = useState(initialValue); const [isThrottled, setIsThrottled] = useState(false); const lastUpdated = useRef(null); const timeoutRef = useRef(null); useEffect(() => { const now = Date.now(); if (leading && lastUpdated.current === null) { lastUpdated.current = now; setThrottledValue(value); setIsThrottled(false); return; } if (lastUpdated.current && now >= lastUpdated.current + interval) { lastUpdated.current = now; setThrottledValue(value); setIsThrottled(false); if (timeoutRef.current) { clearTimeout(timeoutRef.current); timeoutRef.current = null; } } else if (trailing) { setIsThrottled(true); if (timeoutRef.current) { clearTimeout(timeoutRef.current); } timeoutRef.current = setTimeout(() => { lastUpdated.current = Date.now(); setThrottledValue(value); setIsThrottled(false); timeoutRef.current = null; }, interval - (now - (lastUpdated.current || 0))); } }, [value, interval, leading, trailing]); useEffect(() => { return () => { if (timeoutRef.current) { clearTimeout(timeoutRef.current); } }; }, []); return { value, setValue, throttledValue, isThrottled }; }; const useClickAway = (options = {}) => { const { enabled = true, events = ["mousedown", "touchstart"], onClickAway } = options; const ref = useRef(null); const callbackRef = useRef(onClickAway); useLayoutEffect(() => { callbackRef.current = onClickAway; }, [onClickAway]); useEffect(() => { if (!enabled || !callbackRef.current) return; const handler = (event) => { if (ref.current && !ref.current.contains(event.target)) { callbackRef.current?.(event); } }; events.forEach((eventType) => { document.addEventListener(eventType, handler); }); return () => { events.forEach((eventType) => { document.removeEventListener(eventType, handler); }); }; }, [enabled, events]); return { ref }; }; const useInView = (options = {}) => { const { threshold = 0, rootMargin = "0px", root, triggerOnce = false, onEnter, onLeave } = options; const ref = useRef(null); const [inView, setInView] = useState(false); const [entryCount, setEntryCount] = useState(0); const [hasEntered, setHasEntered] = useState(false); useEffect(() => { const element = ref.current; if (!element) return; const observer = new IntersectionObserver( ([entry]) => { const isIntersecting = entry.isIntersecting; if (isIntersecting) { setInView(true); setEntryCount((prev) => prev + 1); if (!hasEntered) { setHasEntered(true); onEnter?.(); } } else { setInView(false); if (!triggerOnce) { onLeave?.(); } } }, { threshold, rootMargin, root } ); observer.observe(element); return () => { observer.unobserve(element); observer.disconnect(); }; }, [threshold, rootMargin, root, triggerOnce, onEnter, onLeave, hasEntered]); return { ref, inView, entryCount, hasEntered }; }; const CHINESE_UNITS = { second: "\u79D2", minute: "\u5206\u949F", hour: "\u5C0F\u65F6", day: "\u5929", week: "\u5468", month: "\u4E2A\u6708", year: "\u5E74" }; const ENGLISH_UNITS = { second: "second", minute: "minute", hour: "hour", day: "day", week: "week", month: "month", year: "year" }; const useTimeAgo = (timestamp, options = {}) => { const { interval = 6e4, locale = "zh", relative = true, minUnit = "minute", autoUpdate = true } = options; const [timeAgo, setTimeAgo] = useState(""); const [isUpdating, setIsUpdating] = useState(false); const calculateTimeAgo = useCallback(() => { const now = Date.now(); const date2 = timestamp instanceof Date ? timestamp : new Date(timestamp); const diffInMs2 = now - date2.getTime(); const diffInSeconds = Math.floor(diffInMs2 / 1e3); const diffInMinutes = Math.floor(diffInSeconds / 60); const diffInHours = Math.floor(diffInMinutes / 60); const diffInDays = Math.floor(diffInHours / 24); const diffInWeeks = Math.floor(diffInDays / 7); const diffInMonths = Math.floor(diffInDays / 30); const diffInYears = Math.floor(diffInDays / 365); const formatCustom = (value, unit) => { const units = locale === "zh" ? CHINESE_UNITS : ENGLISH_UNITS; if (locale === "zh") { if (unit === "second" && value === 0) return "\u521A\u521A"; if (unit === "second") return `${value}\u79D2\u524D`; if (unit === "minute") return `${value}\u5206\u949F\u524D`; if (unit === "hour") return `${value}\u5C0F\u65F6\u524D`; if (unit === "day") return `${value}\u5929\u524D`; if (unit === "week") return `${value}\u5468\u524D`; if (unit === "month") return `${value}\u4E2A\u6708\u524D`; if (unit === "year") return `${value}\u5E74\u524D`; } if (locale === "en") { if (value === 1) return `1 ${units[unit]} ago`; return `${value} ${units[unit]}s ago`; } return `${value} ${units[unit]}${locale === "zh" ? "\u524D" : "s ago"}`; }; const formatWithIntl = (value, unit) => { try { const rtf = new Intl.RelativeTimeFormat(locale, { numeric: "auto" }); return rtf.format(-value, unit); } catch (e) { return formatCustom(value, unit); } }; if (minUnit === "year" || diffInYears > 0 && minUnit !== "second") { return relative ? formatWithIntl(diffInYears, "year") : formatCustom(diffInYears, "year"); } else if (minUnit === "month" || diffInMonths > 0 && minUnit !== "second" && minUnit !== "minute") { return relative ? formatWithIntl(diffInMonths, "month") : formatCustom(diffInMonths, "month"); } else if (minUnit === "week" || diffInWeeks > 0 && minUnit !== "second" && minUnit !== "minute" && minUnit !== "hour") { return relative ? formatWithIntl(diffInWeeks, "week") : formatCustom(diffInWeeks, "week"); } else if (minUnit === "day" || diffInDays > 0 && minUnit !== "second" && minUnit !== "minute") { return relative ? formatWithIntl(diffInDays, "day") : formatCustom(diffInDays, "day"); } else if (minUnit === "hour" || diffInHours > 0 && minUnit !== "second") { return relative ? formatWithIntl(diffInHours, "hour") : formatCustom(diffInHours, "hour"); } else if (minUnit === "minute" || diffInMinutes > 0) { return relative ? formatWithIntl(diffInMinutes, "minute") : formatCustom(diffInMinutes, "minute"); } else { return relative ? formatWithIntl(diffInSeconds, "second") : formatCustom(diffInSeconds, "second"); } }, [timestamp, locale, relative, minUnit]); useEffect(() => { setTimeAgo(calculateTimeAgo()); }, [calculateTimeAgo]); useEffect(() => { if (!autoUpdate) return; const intervalId = setInterval(() => { setIsUpdating(true); setTimeAgo(calculateTimeAgo()); setIsUpdating(false); }, interval); return () => { clearInterval(intervalId); }; }, [interval, calculateTimeAgo, autoUpdate]); const date = timestamp instanceof Date ? timestamp : new Date(timestamp); const diffInMs = Date.now() - date.getTime(); return { timeAgo, timestamp: date, diffInMs, isUpdating }; }; const useQueue = (options = {}) => { const { initialItems = [], maxSize, overflowStrategy = "drop" } = options; const [queue, setQueue] = useState(initialItems); const enqueue = useCallback((item) => { setQueue((prevQueue) => { if (maxSize && prevQueue.length >= maxSize) { switch (overflowStrategy) { case "error": throw new Error("Queue is full"); case "shift": return [...prevQueue.slice(1), item]; case "drop": default: return prevQueue; } } return [...prevQueue, item]; }); }, [maxSize, overflowStrategy]); const enqueueMany = useCallback((items) => { setQueue((prevQueue) => { if (!maxSize) { return [...prevQueue, ...items]; } const availableSpace = maxSize - prevQueue.length; if (availableSpace <= 0) { switch (overflowStrategy) { case "error": throw new Error("Queue is full"); case "shift": const itemsToAdd2 = items.slice(-maxSize); return [...prevQueue.slice(items.length), ...itemsToAdd2]; case "drop": default: return prevQueue; } } const itemsToAdd = items.slice(0, availableSpace); return [...prevQueue, ...itemsToAdd]; }); }, [maxSize, overflowStrategy]); const dequeue = useCallback(() => { let dequeuedItem; setQueue((prevQueue) => { if (prevQueue.length === 0) return prevQueue; dequeuedItem = prevQueue[0]; return prevQueue.slice(1); }); return dequeuedItem; }, []); const peek = useCallback(() => { return queue[0]; }, [queue]); const peekLast = useCallback(() => { return queue[queue.length - 1]; }, [queue]); const clear = useCallback(() => { setQueue([]); }, []); const isEmpty = useCallback(() => { return queue.length === 0; }, [queue]); const size = useCallback(() => { return queue.length; }, [queue]); const isFull = useCallback(() => { return maxSize ? queue.length >= maxSize : false; }, [queue, maxSize]); const queueString = useMemo(() => { return JSON.stringify(queue); }, [queue]); return { queue, enqueue, enqueueMany, dequeue, peek, peekLast, clear, isEmpty, size, isFull, queueString, full: isFull(), length: queue.length }; }; const usePolling = (callback, options) => { const { interval, immediate = false, enabled = true, maxRetries = -1, onError, onSuccess } = options; const [isPolling, setIsPolling] = useState(false); const [retryCount, setRetryCount] = useState(0); const [error, setError] = useState(null); const [data, setData] = useState(null); const intervalRef = useRef(null); const callbackRef = useRef(callback); const optionsRef = useRef({ maxRetries, onError, onSuccess }); const isPollingRef = useRef(false); useEffect(() => { callbackRef.current = callback; optionsRef.current = { maxRetries, onError, onSuccess }; }, [callback, maxRetries, onError, onSuccess]); const executeCallback = useCallback(async () => { if (!isPollingRef.current) return; try { setError(null); const result = await callbackRef.current(); setData(result); setRetryCount(0); optionsRef.current.onSuccess?.(result); } catch (err) { const error2 = err instanceof Error ? err : new Error(String(err)); setError(error2); setRetryCount((prev) => { const newCount = prev + 1; if (optionsRef.current.maxRetries > 0 && newCount >= optionsRef.current.maxRetries) { if (intervalRef.current) { clearInterval(intervalRef.current); intervalRef.current = null; } setIsPolling(false); isPollingRef.current = false; } return newCount; }); optionsRef.current.onError?.(error2); } }, []); const stop = useCallback(() => { if (intervalRef.current) { clearInterval(intervalRef.current); intervalRef.current = null; } setIsPolling(false); isPollingRef.current = false; }, []); const start = useCallback(() => { if (isPollingRef.current) return; setIsPolling(true); isPollingRef.current = true; setRetryCount(0); setError(null); executeCallback(); intervalRef.current = setInterval(() => { executeCallback(); }, interval); }, [interval, executeCallback]); const restart = useCallback(() => { stop(); setTimeout(() => start(), 0); }, [stop, start]); useEffect(() => { if (immediate && enabled) { start(); } return () => { if (intervalRef.current) { clearInterval(intervalRef.current); } }; }, []); useEffect(() => { return () => { if (intervalRef.current) { clearInterval(intervalRef.current); } }; }, []); return { isPolling, start, stop, restart, retryCount, error, data }; }; const useMouse = (options = {}) => { const { enabled = true, target = typeof window !== "undefined" ? window : null, onMouseMove, onMouseEnter, onMouseLeave } = options; const [position, setPosition] = useState({ x: 0, y: 0, pageX: 0, pageY: 0, screenX: 0, screenY: 0, clientX: 0, clientY: 0 }); const [isInside, setIsInside] = useState(false); const [isMoving, setIsMoving] = useState(false); const updateMousePosition = useCallback((event) => { const mouseEvent = event; const newPosition = { x: mouseEvent.clientX, y: mouseEvent.clientY, pageX: mouseEvent.pageX, pageY: mouseEvent.pageY, screenX: mouseEvent.screenX, screenY: mouseEvent.screenY, clientX: mouseEvent.clientX, clientY: mouseEvent.clientY }; setPosition(newPosition); setIsMoving(true); onMouseMove?.(newPosition); setTimeout(() => setIsMoving(false), 100); }, [onMouseMove]); const handleMouseEnter = useCallback((event) => { const mouseEvent = event; setIsInside(true); const newPosition = { x: mouseEvent.clientX, y: mouseEvent.clientY, pageX: mouseEvent.pageX, pageY: mouseEvent.pageY, screenX: mouseEvent.screenX, screenY: mouseEvent.screenY, clientX: mouseEvent.clientX, clientY: mouseEvent.clientY }; onMouseEnter?.(newPosition); }, [onMouseEnter]); const handleMouseLeave = useCallback((event) => { const mouseEvent = event; setIsInside(false); const newPosition = { x: mouseEvent.clientX, y: mouseEvent.clientY, pageX: mouseEvent.pageX, pageY: mouseEvent.pageY, screenX: mouseEvent.screenX, screenY: mouseEvent.screenY, clientX: mouseEvent.clientX, clientY: mouseEvent.clientY }; onMouseLeave?.(newPosition); }, [onMouseLeave]); const reset = useCallback(() => { setPosition({ x: 0, y: 0, pageX: 0, pageY: 0, screenX: 0, screenY: 0, clientX: 0, clientY: 0 }); setIsInside(false); setIsMoving(false); }, []); useEffect(() => { if (!enabled || !target) return; target.addEventListener("mousemove", updateMousePosition); target.addEventListener("mouseenter", handleMouseEnter); target.addEventListener("mouseleave", handleMouseLeave); return () => { target.removeEventListener("mousemove", updateMousePosition); target.removeEventListener("mouseenter", handleMouseEnter); target.removeEventListener("mouseleave", handleMouseLeave); }; }, [enabled, target, updateMousePosition, handleMouseEnter, handleMouseLeave]); return { position, isInside, isMoving, reset }; }; const useFullscreen = (options = {}) => { const { onEnter, onExit, background, enabled = true } = options; const [isFullscreen, setIsFullscreen] = useState(false); const [error, setError] = useState(null); const elementRef = useRef(null); const originalStylesRef = useRef({}); const isSupported = typeof window !== "undefined" && !!(document.fullscreenEnabled || document.webkitFullscreenEnabled || document.mozFullScreenEnabled || document.msFullscreenEnabled); const handleFullscreenChange = useCallback(() => { const isFull = !!(document.fullscreenElement || document.webkitFullscreenElement || document.mozFullScreenElement || document.msFullscreenElement); setIsFullscreen(isFull); if (!isFull) { if (elementRef.current) { const element = elementRef.current; if (originalStylesRef.current.background !== void 0) { element.style.background = originalStylesRef.current.background; } else { element.style.removeProperty("background"); } } if (originalStylesRef.current.overflow !== void 0) { document.body.style.overflow = originalStylesRef.current.overflow; } else { document.body.style.removeProperty("overflow"); } } }, []); const enterFullscreen = useCallback(async () => { if (!enabled || !isSupported || !elementRef.current) { throw new Error("Fullscreen not supported or element not available"); } try { setError(null); const element = elementRef.current; originalStylesRef.current = { background: element.style.background || "", overflow: document.body.style.overflow || "" }; if (background) { element.style.background = background; } document.body.style.overflow = "hidden"; if (element.requestFullscreen) { await element.requestFullscreen(); } else if (element.webkitRequestFullscreen) { await element.webkitRequestFullscreen(); } else if (element.mozRequestFullScreen) { await element.mozRequestFullScreen(); } else if (element.msRequestFullscreen) { await element.msRequestFullscreen(); } else { throw new Error("Fullscreen API not supported"); } onEnter?.(); } catch (err) { const error2 = err instanceof Error ? err : new Error(String(err)); setError(error2); throw error2; } }, [enabled, isSupported, background, onEnter]); const exitFullscreen = useCallback(async () => { if (!enabled || !isSupported) { throw new Error("Fullscreen not supported"); } try { setError(null); if (document.exitFullscreen) { await document.exitFullscreen(); } else if (document.webkitExitFullscreen) { await document.webkitExitFullscreen(); } else if (document.mozCancelFullScreen) { await document.mozCancelFullScreen(); } else if (document.msExitFullscreen) { await document.msExitFullscreen(); } else { throw new Error("Exit fullscreen API not supported"); } onExit?.(); } catch (err) { const error2 = err instanceof Error ? err : new Error(String(err)); setError(error2); throw error2; } }, [enabled, isSupported, onExit]); const toggleFullscreen = useCallback(async () => { if (isFullscreen) { await exitFullscreen(); } else { await enterFullscreen(); } }, [isFullscreen, enterFullscreen, exitFullscreen]); useEffect(() => { if (!enabled || !isSupported) return; document.addEventListener("fullscreenchange", handleFullscreenChange); document.addEventListener("webkitfullscreenchange", handleFullscreenChange); document.addEventListener("mozfullscreenchange", handleFullscreenChange); document.addEventListener("MSFullscreenChange", handleFullscreenChange); return () => { document.removeEventListener("fullscreenchange", handleFullscreenChange); document.removeEventListener("webkitfullscreenchange", handleFullscreenChange); document.removeEventListener("mozfullscreenchange", handleFullscreenChange); document.removeEventListener("MSFullscreenChange", handleFullscreenChange); }; }, [enabled, isSupported, handleFullscreenChange]); return { elementRef, isFullscreen, enterFullscreen, exitFullscreen, toggleFullscreen, isSupported, error }; }; const globalEventsMap = /* @__PURE__ */ new Map(); const useEventBus = (eventName, options = {}) => { const { autoCleanup = true, eventBus = globalEventsMap } = options; const eventsRef = useRef(eventBus); const cleanupRefs = useRef(/* @__PURE__ */ new Set()); const getEventListeners = useCallback(() => { if (!eventsRef.current.has(eventName)) { eventsRef.current.set(eventName, /* @__PURE__ */ new Set()); } return eventsRef.current.get(eventName); }, [eventName]); const on = useCallback((callback) => { const listeners = getEventListeners(); listeners.add(callback); cleanupRefs.current.add(callback); return () => { listeners.delete(callback); cleanupRefs.current.delete(callback); }; }, [getEventListeners]); const once = useCallback((callback) => { const onceCallback = (data) => { callback(data); off(onceCallback); }; return on(onceCallback); }, [on]); const emit = useCallback((data) => { const listeners = getEventListeners(); listeners.forEach((callback) => { try { callback(data); } catch (error) { console.error("EventBus callback error:", error); } }); }, [getEventListeners]); const off = useCallback((callback) => { const listeners = getEventListeners(); listeners.delete(callback); cleanupRefs.current.delete(callback); }, [getEventListeners]); const reset = useCallback(() => { const listeners = getEventListeners(); listeners.clear(); cleanupRefs.current.clear(); }, [getEventListeners]); const listenerCount = useCallback(() => { const listeners = getEventListeners(); return listeners.size; }, [getEventListeners]); useEffect(() => { if (!autoCleanup) return; return () => { const listeners = getEventListeners(); cleanupRefs.current.forEach((callback) => { listeners.delete(callback); }); cleanupRefs.current.clear(); }; }, [autoCleanup, getEventListeners]); return { on, once, emit, off, reset, listenerCount }; }; const useElementSize = (options = {}) => { const { enabled = false, includeBorder = false, includePadding = false, onSizeChange, debounceMs = 0 } = options; const ref = useRef(null); const [size, setSize] = useState({ width: 0, height: 0, contentWidth: 0, contentHeight: 0, borderWidth: 0, borderHeight: 0, paddingWidth: 0, paddingHeight: 0 }); const [isObserving, setIsObserving] = useState(false); const observerRef = useRef(null); const debounceTimerRef = useRef(null); const optionsRef = useRef({ includeBorder, includePadding }); useEffect(() => { optionsRef.current = { includeBorder, includePadding }; }, [includeBorder, includePadding]); const calculateSize = useCallback((element) => { const rect = element.getBoundingClientRect(); const computedStyle = window.getComputedStyle(element); const borderLeft = parseFloat(computedStyle.borderLeftWidth) || 0; const borderRight = parseFloat(computedStyle.borderRightWidth) || 0; const borderTop = parseFloat(computedStyle.borderTopWidth) || 0; const borderBottom = parseFloat(computedStyle.borderBottomWidth) || 0; const paddingLeft = parseFloat(computedStyle.paddingLeft) || 0; const paddingRight = parseFloat(computedStyle.paddingRight) || 0; const paddingTop = parseFloat(computedStyle.paddingTop) || 0; const paddingBottom = parseFloat(computedStyle.paddingBottom) || 0; const borderWidth = borderLeft + borderRight; const borderHeight = borderTop + borderBottom; const paddingWidth = paddingLeft + paddingRight; const paddingHeight = paddingTop + paddingBottom; const contentWidth = rect.width - borderWidth - paddingWidth; const contentHeight = rect.height - borderHeight - paddingHeight; return { width: optionsRef.current.includeBorder ? rect.width : contentWidth + paddingWidth, height: optionsRef.current.includeBorder ? rect.height : contentHeight + paddingHeight, contentWidth, contentHeight, borderWidth, borderHeight, paddingWidth, paddingHeight }; }, []); const updateSize = useCallback(() => { const element = ref.current; if (!element) return; const newSize = calculateSize(element); setSize(newSize); onSizeChange?.(newSize); }, [calculateSize, onSizeChange]); const debouncedUpdateSize = useCallback(() => { if (debounceTimerRef.current) { clearTimeout(debounceTimerRef.current); } if (debounceMs > 0) { debounceTimerRef.current = setTimeout(updateSize, debounceMs); } else { updateSize(); } }, [updateSize, debounceMs]); const startObserving = useCallback(() => { const element = ref.current; if (!element || isObserving) return; updateSize(); if (typeof ResizeObserver !== "undefined") { observerRef.current = new ResizeObserver(() => { debouncedUpdateSize(); }); observerRef.current.observe(element); setIsObserving(true); } }, [isObserving, updateSize, debouncedUpdateSize]); const stopObserving = useCallback(() => { if (observerRef.current) { observerRef.current.disconnect(); observerRef.current = null; } setIsObserving(false); if (debounceTimerRef.current) { clearTimeout(debounceTimerRef.current); debounceTimerRef.current = null; } }, []); useEffect(() => { return () => { if (observerRef.current) { observerRef.current.disconnect(); } if (debounceTimerRef.current) { clearTimeout(debounceTimerRef.current); } }; }, []); return { ref, size, isObserving, startObserving, stopObserving, updateSize }; }; const useClipboard = (options = {}) => { const { showSuccess = true, successMessage = "\u5DF2\u590D\u5236\u5230\u526A\u8D34\u677F", errorMessage = "\u590D\u5236\u5931\u8D25", onSuccess, onError, timeout = 3e3 } = options; const [isCopying, setIsCopying] = useState(false); const [isCopied, setIsCopied] = useState(false); const [copiedText, setCopiedText] = useState(null); const [error, setError] = useState(null); const canUseClipboardApi = typeof navigator !== "undefined" && navigator.clipboard && typeof navigator.clipboard.writeText === "function"; const copyToClipboard = useCallback(async (text, copyOptions = {}) => { const { showSuccess: showSuccessOverride = showSuccess, successMessage: successMessageOverride = successMessage, errorMessage: errorMessageOverride = errorMessage, onSuccess: onSuccessOverride = onSuccess, onError: onErrorOverride = onError, timeout: timeoutOverride = timeout } = copyOptions; try { setIsCopying(true); setError(null); setIsCopied(false); let copySuccessful = false; if (canUseClipboardApi) { try { await navigator.clipboard.writeText(text); copySuccessful = true; } catch (err) { console.warn("Clipboard API failed, trying fallback:", err); } } if (!copySuccessful) { const textarea = document.createElement("textarea"); textarea.value = text; textarea.style.position = "fixed"; textarea.style.opacity = "0"; textarea.style.pointerEvents = "none"; textarea.style.left = "-9999px"; textarea.style.top = "-9999px"; document.body.appendChild(textarea); textarea.select(); textarea.setSelectionRange(0, textarea.value.length); const successful = document.execCommand("copy"); document.body.removeChild(textarea); if (successful) { copySuccessful = true; } else { throw new Error("Failed to copy via execCommand"); } } if (!copySuccessful) { throw new Error("All copy methods failed"); } setCopiedText(text); setIsCopied(true); if (showSuccessOverride) { if ("Notification" in window && Notification.permission === "granted") { new Notification(successMessageOverride); } else { console.log(successMessageOverride); } } onSuccessOverride?.(text); } catch (err) { const copyError = err instanceof Error ? err : new Error(String(err)); console.error("Failed to copy text:", copyError); setError(copyError); if (showSuccessOverride) { if ("Notification" in window && Notification.permission === "granted") { new Notification(errorMessageOverride); } else { console.error(errorMessageOverride); } } onErrorOverride?.(copyError); } finally { setIsCopying(false); setTimeout(() => { setIsCopied(false); setCopiedText(null); }, timeoutOverride); } }, [canUseClipboardApi, showSuccess, successMessage, errorMessage, onSuccess, onError, timeout]); const readFromClipboard = useCallback(async () => { try { setError(null); if (canUseClipboardApi && navigator.clipboard.readText) { return await navigator.clipboard.readText(); } else { throw new Error("Clipboard read API not supported"); } } catch (err) { const readError = err instanceof Error ? err : new Error(String(err)); setError(readError); throw readError; } }, [canUseClipboardApi]); const reset = useCallback(() => { setIsCopying(false); setIsCopied(false); setCopiedText(null); setError(null); }, []); return { copyToClipboard, readFromClipboard, isCopying, isCopied, copiedText, error, canUseClipboardApi, reset }; }; const useHover = (options = {}) => { const { onEnter, onLeave, enabled = true, delayEnter = 0, delayLeave = 0 } = options; const [isHovered, setIsHovered] = useState(false); const ref = useRef(null); const enterTimerRef = useRef(null); const leaveTimerRef = useRef(null); const clearTimers = useCallback(() => { if (enterTimerRef.current) { clearTimeout(enterTimerRef.current); enterTimerRef.current = null; } if (leaveTimerRef.current) { clearTimeout(leaveTimerRef.current); leaveTimerRef.current = null; } }, []); const handleMouseEnter = useCallback(() => { if (!enabled) return; clearTimers(); if (delayEnter > 0) { enterTimerRef.current = setTimeout(() => { setIsHovered(true); onEnter?.(); }, delayEnter); } else { setIsHovered(true); onEnter?.(); } }, [enabled, delayEnter, onEnter, clearTimers]); const handleMouseLeave = useCallback(() => { if (!enabled) return; clearTimers(); if (delayLeave > 0) { leaveTimerRef.current = setTimeout(() => { setIsHovered(false); onLeave?.(); }, delayLeave); } else { setIsHovered(false); onLeave?.(); } }, [enabled, delayLeave, onLeave, clearTimers]); const setHovered = useCallback((hovered) => { clearTimers(); setIsHovered(hovered); if (hovered) { onEnter?.(); } else { onLeave?.(); } }, [onEnter, onLeave, clearTimers]); useEffect(() => { const element = ref.current; if (element) { if (enabled) { element.addEventListener("mouseenter", handleMouseEnter); element.addEventListener("mouseleave", handleMouseLeave); } else { element.removeEventListener("mouseenter", handleMouseEnter); element.removeEventListener("mouseleave", handleMouseLeave); } } return () => { if (element) { element.removeEventListener("mouseenter", handleMouseEnter); element.removeEventListener("mouseleave", handleMouseLeave); } clearTimers(); }; }, [enabled, handleMouseEnter, handleMouseLeave, clearTimers]); return { ref, isHovered, setHovered }; }; const useScrolling = (options = {}) => { const { onScrollStart, onScrollEnd, onScroll, enabled = true, scrollEndDelay = 150, horizontal = true, vertical = true } = options; const [isScrolling, setIsScrolling] = useState(false); const [scrollDirection, setScrollDirection] = useState(null); const [scrollPosition, setScrollPosition] = useState({ scrollTop: 0, scrollLeft: 0, scrollWidth: 0, scrollHeight: 0, clientWidth: 0, clientHeight: 0 }); const ref = useRef(null); const scrollEndTimerRef = useRef(null); const lastScrollTopRef = useRef(0); const lastScrollLeftRef = useRef(0); const isScrollingRef = useRef(false); const callbacksRef = useRef({ onScrollStart, onScrollEnd, onScroll, enabled, scrollEndDelay, horizontal, vertical }); useEffect(() => { callbacksRef.current = { onScrollStart, onScrollEnd, onScroll, enabled, scrollEndDelay, horizontal, vertical }; }); const clearScrollEndTimer = useCallback(() => { if (scrollEndTimerRef.current) { clearTimeout(scrollEndTimerRef.current); scrollEndTimerRef.current = null; } }, []); const handleScroll = useCallback((event) => { const { enabled: enabled2, onScrollStart: onScrollStart2, onScrollEnd: onScrollEnd2, onScroll: onScroll2, scrollEndDelay: scrollEndDelay2, horizontal: horizontal2, vertical: vertical2 } = callbacksRef.current; if (!enabled2) return; const element = event.target; if (!element) return; const newScrollPosition = { scrollTop: element.scrollTop, scrollLeft: element.scrollLeft, scrollWidth: element.scrollWidth, scrollHeight: element.scrollHeight, clientWidth: element.clientWidth, clientHeight: element.clientHeight }; setScrollPosition(newScrollPosition); const currentScrollTop = element.scrollTop; const currentScrollLeft = element.scrollLeft; const topChanged = currentScrollTop !== lastScrollTopRef.current; const leftChanged = currentScrollLeft !== lastScrollLeftRef.current; let direction = null; if (topChanged && leftChanged) { direction = "both"; } else if (topChanged && vertical2) { direction = "vertical"; } else if (leftChanged && horizontal2) { direction = "horizontal"; } if (direction) { setScrollDirection(direction); } lastScrollTopRef.current = element.scrollTop; lastScrollLeftRef.current = element.scrollLeft; if (!isScrollingRef.current) { isScrollingRef.current = true; setIsScrolling(true); onScrollStart2?.(); } clearScrollEndTimer(); scrollEndTimerRef.current = setTimeout(() => { isScrollingRef.current = false; setIsScrolling(false); setScrollDirection(null); onScrollEnd2?.(); }, scrollEndDelay2); onScroll2?.(event); }, [clearScrollEndTimer]); const handleMouseLeave = useCallback(() => { const { enabled: enabled2, onScrollEnd: onScrollEnd2 } = callbacksRef.current; if (!enabled2) return; clearScrollEndTimer(); isScrollingRef.current = false; setIsScrolling(false); setScrollDirection(null); onScrollEnd2?.(); }, [clearScrollEndTimer]); const setScrolling = useCallback((scrolling) => { const { onScrollStart: onScrollStart2, onScrollEnd: onScrollEnd2 } = callbacksRef.current; clearScrollEndTimer(); isScrollingRef.current = scrolling; setIsScrolling(scrolling); if (!scrolling) { setScrollDirection(null); onScrollEnd2?.(); } else { onScrollStart2?.(); } }, [clearScrollEndTimer]); useEffect(() => { const element = ref.current; if (!element) return; const position = { scrollTop: element.scrollTop, scrollLeft: element.scrollLeft, scrollWidth: element.scrollWidth, scrollHeight: element.scrollHeight, clientWidth: element.clientWidth, clientHeight: element.clientHeight }; setScrollPosition(position); lastScrollTopRef.current = position.scrollTop; lastScrollLeftRef.current = position.scrollLeft; element.addEventListener("scroll", handleScroll, { passive: true }); element.addEventListener("mouseleave", handleMouseLeave); return () => { element.removeEventListener("scroll", handleScroll); element.removeEventListener("mouseleave", handleMouseLeave); clearScrollEndTimer(); }; }, []); return { ref, isScrolling, scrollDirection, scrollPosition, setScrolling }; }; const useUpdateEffect = (effect, deps = []) => { const isFirstRender = useRef(true); useEffect(() => { if (isFirstRender.current) { isFirstRender.current = false; return; } return effect(); }, [effect, ...deps]); }; const useWatermark = (options = {}) => { const { text = "\u6C34\u5370", image, fontSize = 16, color = "#999", opacity = 0.3, rotate = -15, gap = [100, 100], offset = [0, 0], zIndex = 1e3, fontFamily = "Arial, sans-serif", fontWeight = "normal", width = 200, height = 100, enabled = true } = options; const ref = useRef(null); const watermarkRef = useRef(null); const optionsRef = useRef(options); useEffect(() => { optionsRef.current = { ...options }; }); const createWatermark = useCallback(() => { if (!ref.current || !enabled) return; if (watermarkRef.current) { watermarkRef.current.remove(); watermarkRef.current = null; } const element = ref.current; element.getBoundingClientRect(); const watermark = document.createElement("div"); watermark.style.position = "absolute"; watermark.style.top = "0"; watermark.style.left = "0"; watermark.style.width = "100%"; watermark.style.height = "100%"; watermark.style.pointerEvents = "none"; watermark.style.zIndex = zIndex.toString(); watermark.style.overflow = "hidden"; const canvas = document.createElement("canvas"); const ctx = canvas.getContext("2d"); if (!ctx) return; canvas.width = width; canvas.height = height; ctx.font = `${fontWeight} ${fontSize}px ${fontFamily}`; ctx.fillStyle = color; ctx.globalAlpha = opacity; ctx.textAlign = "center"; ctx.textBaseline = "middle"; if (image) { const img = new Image(); img.crossOrigin = "anonymous"; img.onload = () => { ctx.save(); ctx.translate(width / 2, height / 2); ctx.rotate(rotate * Math.PI / 180); ctx.drawImage(img, -img.width / 2, -img.height / 2); ctx.restore(); createWatermarkPattern(); }; img.src = image; } else { ctx.save(); ctx.translate(width / 2, height / 2); ctx.rotate(rotate * Math.PI / 180); ctx.fillText(text, 0, 0); ctx.restore(); createWatermarkPattern(); } function createWatermarkPattern() { const dataURL = canvas.toDataURL(); const pattern = `url(${dataURL})`; watermark.style.backgroundImage = pattern; watermark.style.backgroundRepeat = "repeat"; watermark.style.backgroundSize = `${gap[0]}px ${gap[1]}px`; watermark.style.backgroundPosition = `${offset[0]}px ${offset[1]}px`; element.style.position = "relative"; element.appendChild(watermark); watermarkRef.current = watermark; } }, [text, image, fontSize, color, opacity, rotate, gap, offset, zIndex, fontFamily, fontWeight, width, height, enabled]); const clear = useCallback(() => { if (watermarkRef.current) { watermarkRef.current.remove(); watermarkRef.current = null; } }, []); const setOptions = useCallback((newOptions) => { Object.assign(optionsRef.current, newOptions); createWatermark(); }, [createWatermark]); useEffect(() => { if (!ref.current) return; const element = ref.current; createWatermark(); const resizeObserver = new ResizeObserver(() => { createWatermark(); }); resizeObserver.observe(element); return () => { resizeObserver.disconnect(); clear(); }; }, [createWatermark, clear]); return { ref, setOptions, clear, options: optionsRef.current }; }; const Button = ({ children, variant = "primary", size = "md", disabled = false, onClick, className = "" }) => { const baseClasses = "inline-flex items-center justify-center rounded-md font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2"; const variantClasses = { primary: "bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500", secondary: "bg-gray-600 text-white hover:bg-gray-700 focus:ring-gray-500", outline: "border border-gray-300 bg-transparent text-gray-700 hover:bg-gray-50 focus:ring-gray-500" }; const sizeClasses = { sm: "px-3 py-1.5 text-sm", md: "px-4 py-2 text-base", lg: "px-6 py-3 text-lg" }; const disabledClasses = disabled ? "opacity-50 cursor-not-allowed" : "cursor-pointer"; const classes = `${baseClasses} ${variantClasses[variant]} ${sizeClasses[size]} ${disabledClasses} ${className}`; return /* @__PURE__ */ React.createElement( "button", { className: classes, disabled, onClick }, children ); }; export { Button, useClickAway, useClipboard, useCounter, useDebounce, useElementSize, useEventBus, useFullscreen, useHover, useInView, useLocalStorage, useMouse, usePolling, usePrevious, useQueue, useScrolling, useThrottle, useTimeAgo, useToggle, useUpdateEffect, useWatermark, useWindowSize }; //# sourceMappingURL=index.esm.js.map