UNPKG

@reactuses/core

Version:

Collection of 100+ essential React Hooks with TypeScript support, tree-shaking, and SSR compatibility. Sensors, browser APIs, state management, animations, and more.

1,446 lines (1,404 loc) 171 kB
import React, { useLayoutEffect, useEffect, useRef, useReducer, useState, useCallback, useMemo } from 'react'; import { isEqual, debounce, throttle } from 'lodash-es'; import Cookies from 'js-cookie'; import { useSyncExternalStore } from 'use-sync-external-store/shim/index.js'; import screenfull from 'screenfull'; import { fetchEventSource } from '@microsoft/fetch-event-source'; var _window_navigator, _window; function isFunction(val) { return typeof val === 'function'; } function isString(val) { return typeof val === 'string'; } const isDev = process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'test'; const isBrowser = typeof window !== 'undefined'; const isNavigator = typeof navigator !== 'undefined'; function noop$1() {} const isIOS = isBrowser && ((_window = window) == null ? void 0 : (_window_navigator = _window.navigator) == null ? void 0 : _window_navigator.userAgent) && /iP(?:ad|hone|od)/.test(window.navigator.userAgent); const useIsomorphicLayoutEffect = isBrowser ? useLayoutEffect : useEffect; const useLatest = (value)=>{ const ref = useRef(value); useIsomorphicLayoutEffect(()=>{ ref.current = value; }, [ value ]); return ref; }; function on(obj, ...args) { if (obj && obj.addEventListener) { obj.addEventListener(...args); } } function off(obj, ...args) { if (obj && obj.removeEventListener) { obj.removeEventListener(...args); } } const defaultWindow = typeof window !== 'undefined' ? window : undefined; const defaultDocument = typeof document !== 'undefined' ? document : undefined; const defaultOptions$1 = {}; function defaultOnError(e) { console.error(e); } function getTargetElement(target, defaultElement) { if (!isBrowser) { return undefined; } if (!target) { return defaultElement; } let targetElement; if (isFunction(target)) { targetElement = target(); } else if ('current' in target) { targetElement = target.current; } else { targetElement = target; } return targetElement; } const updateReducer = (num)=>(num + 1) % 1000000; function useUpdate() { const [, update] = useReducer(updateReducer, 0); return update; } const useCustomCompareEffect = (effect, deps, depsEqual)=>{ if (process.env.NODE_ENV !== 'production') { if (!Array.isArray(deps) || !deps.length) { console.warn('`useCustomCompareEffect` should not be used with no dependencies. Use React.useEffect instead.'); } if (typeof depsEqual !== 'function') { console.warn('`useCustomCompareEffect` should be used with depsEqual callback for comparing deps list'); } } const ref = useRef(undefined); const forceUpdate = useUpdate(); if (!ref.current) { ref.current = deps; } useIsomorphicLayoutEffect(()=>{ if (!depsEqual(deps, ref.current)) { ref.current = deps; forceUpdate(); } }); // eslint-disable-next-line react-hooks/exhaustive-deps useEffect(effect, ref.current); }; const useDeepCompareEffect = (effect, deps)=>{ if (process.env.NODE_ENV !== 'production') { if (!Array.isArray(deps) || !deps.length) { console.warn('`useDeepCompareEffect` should not be used with no dependencies. Use React.useEffect instead.'); } } useCustomCompareEffect(effect, deps, isEqual); }; /** * Creates a stable identifier for a BasicTarget that can be safely used in effect dependencies. * * This hook solves the problem where passing unstable function references like `() => document` * would cause infinite re-renders when used directly in effect dependency arrays. * * @param target - The target element (ref, function, or direct element) * @param defaultElement - Default element to use if target is undefined * @returns A stable reference that only changes when the actual target element changes * * @example * ```tsx * // For ref objects: returns the ref itself (stable) * const ref = useRef<HTMLDivElement>(null) * const key = useStableTarget(ref) // key === ref (stable) * * // For functions: returns the resolved actual element * const key = useStableTarget(() => document) // key === document (stable) * * // For direct elements: returns the element itself * const key = useStableTarget(divElement) // key === divElement (stable) * ``` */ function useStableTarget(target, defaultElement) { const targetRef = useRef(target); targetRef.current = target; // Calculate stable key without memoization // For ref objects: return the ref itself (always stable) // For functions/direct elements: resolve to the actual element let stableKey; if (!target) { stableKey = defaultElement != null ? defaultElement : null; } else if (typeof target === 'object' && 'current' in target) { // Ref object - use the ref itself as the stable key stableKey = target; } else { // Function or direct element - resolve to actual element stableKey = getTargetElement(target, defaultElement); } return { /** The stable key that can be safely used in effect dependencies */ key: stableKey, /** A ref containing the current target (useful for accessing in effects) */ ref: targetRef }; } function useEventListenerImpl(eventName, handler, element, options = defaultOptions$1) { const savedHandler = useLatest(handler); const { key: elementKey, ref: elementRef } = useStableTarget(element, defaultWindow); useDeepCompareEffect(()=>{ // Call getTargetElement inside effect to support ref-based targets // (ref.current is null during render, only available in commit phase) const targetElement = getTargetElement(elementRef.current, defaultWindow); if (!(targetElement && targetElement.addEventListener)) { return; } const eventListener = (event)=>savedHandler.current(event); on(targetElement, eventName, eventListener, options); return ()=>{ if (!(targetElement && targetElement.removeEventListener)) { return; } off(targetElement, eventName, eventListener); }; }, [ eventName, elementKey, options ]); } function noop() {} const useEventListener = isBrowser ? useEventListenerImpl : noop; const useMount = (fn)=>{ if (isDev) { if (!isFunction(fn)) { console.error(`useMount: parameter \`fn\` expected to be a function, but got "${typeof fn}".`); } } useEffect(()=>{ fn == null ? void 0 : fn(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); }; const useActiveElement = ()=>{ const [active, setActive] = useState(null); const listener = useCallback(()=>{ var _window; setActive((_window = window) == null ? void 0 : _window.document.activeElement); }, []); useEventListener('blur', listener, defaultWindow, true); useEventListener('focus', listener, defaultWindow, true); useMount(()=>{ var _window; setActive((_window = window) == null ? void 0 : _window.document.activeElement); }); return active; }; function useMountedState() { const mountedRef = useRef(false); const get = useCallback(()=>mountedRef.current, []); useEffect(()=>{ mountedRef.current = true; return ()=>{ mountedRef.current = false; }; }, []); return get; } function asyncGeneratorStep$9(gen, resolve, reject, _next, _throw, key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(_next, _throw); } } function _async_to_generator$9(fn) { return function() { var self = this, args = arguments; return new Promise(function(resolve, reject) { var gen = fn.apply(self, args); function _next(value) { asyncGeneratorStep$9(gen, resolve, reject, _next, _throw, "next", value); } function _throw(err) { asyncGeneratorStep$9(gen, resolve, reject, _next, _throw, "throw", err); } _next(undefined); }); }; } const useAsyncEffect = (effect, cleanup = noop$1, deps)=>{ const mounted = useMountedState(); useEffect(()=>{ const execute = ()=>_async_to_generator$9(function*() { if (!mounted()) { return; } yield effect(); })(); execute(); return ()=>{ cleanup(); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, deps); }; const listerOptions = { passive: true }; const useClickOutside = (target, handler, enabled = true)=>{ const savedHandler = useLatest(handler); const listener = (event)=>{ if (!enabled) { return; } const element = getTargetElement(target); if (!element) { return; } const elements = event.composedPath(); if (element === event.target || elements.includes(element)) { return; } savedHandler.current(event); }; useEventListener('mousedown', listener, defaultWindow, listerOptions); useEventListener('touchstart', listener, defaultWindow, listerOptions); }; // Custom DOM event used to keep sibling useCookie instances in the SAME tab in // sync. Cookies fire no native cross-tab event at all, so this `window` event is // the only propagation channel. (`refreshCookie` remains for picking up cookie // changes made outside the hook — e.g. by the server or a direct `document.cookie` // write.) const SAME_TAB_COOKIE_EVENT = 'reactuse:cookie'; function getInitialState$4(key, defaultValue) { // Prevent a React hydration mismatch when a default value is provided. if (defaultValue !== undefined) { return defaultValue; } if (isBrowser) { return Cookies.get(key); } if (process.env.NODE_ENV !== 'production') { console.warn('`useCookie` When server side rendering, defaultValue should be defined to prevent a hydration mismatches.'); } return ''; } const useCookie = (key, options = defaultOptions$1, defaultValue)=>{ const [cookieValue, setCookieValue] = useState(getInitialState$4(key, defaultValue)); // Sync with sibling useCookie instances watching the same key in this tab. // useEventListener keeps the latest handler in a ref, so the `key` comparison // always uses the current key without re-binding (and it's a no-op on the server). useEventListener(SAME_TAB_COOKIE_EVENT, (event)=>{ const { key: evKey, value } = event.detail; if (evKey === key) setCookieValue(value); }); useEffect(()=>{ const getStoredValue = ()=>{ const raw = Cookies.get(key); if (raw !== undefined && raw !== null) { return raw; } else { if (defaultValue === undefined) { Cookies.remove(key); } else { Cookies.set(key, defaultValue, options); } return defaultValue; } }; setCookieValue(getStoredValue()); // eslint-disable-next-line react-hooks/exhaustive-deps }, [ defaultValue, key, JSON.stringify(options) ]); const updateCookie = useCallback((newValue)=>{ const value = isFunction(newValue) ? newValue(cookieValue) : newValue; if (value === undefined) { Cookies.remove(key); } else { Cookies.set(key, value, options); } setCookieValue(value); // Notify sibling instances in this tab. Our own listener also fires and // re-sets the same value, which React bails out on — no self-skip needed. window.dispatchEvent(new CustomEvent(SAME_TAB_COOKIE_EVENT, { detail: { key, value } })); }, // eslint-disable-next-line react-hooks/exhaustive-deps [ key, cookieValue, JSON.stringify(options) ]); const refreshCookie = useCallback(()=>{ const cookieValue = Cookies.get(key); if (isString(cookieValue)) { setCookieValue(cookieValue); } }, [ key ]); return [ cookieValue, updateCookie, refreshCookie ]; }; /** * keep function reference immutable */ const useEvent = (fn)=>{ if (isDev) { if (!isFunction(fn)) { console.error(`useEvent expected parameter is a function, got ${typeof fn}`); } } const handlerRef = useRef(fn); useIsomorphicLayoutEffect(()=>{ handlerRef.current = fn; }, [ fn ]); return useCallback((...args)=>{ const fn = handlerRef.current; return fn(...args); }, []); }; const useInterval = (callback, delay, options = defaultOptions$1)=>{ const { immediate, controls } = options; const savedCallback = useLatest(callback); const isActive = useRef(false); const timer = useRef(null); const clean = ()=>{ timer.current && clearInterval(timer.current); }; const resume = useEvent(()=>{ isActive.current = true; timer.current = setInterval(()=>savedCallback.current(), delay || 0); }); const pause = useEvent(()=>{ isActive.current = false; clean(); }); useEffect(()=>{ if (immediate) { savedCallback.current(); } if (controls) { return; } if (delay !== null) { resume(); return ()=>{ clean(); }; } return undefined; // eslint-disable-next-line react-hooks/exhaustive-deps }, [ delay, immediate ]); return { isActive, pause, resume }; }; function padZero(time) { return `${time}`.length < 2 ? `0${time}` : `${time}`; } function getHMSTime(timeDiff) { if (timeDiff <= 0) { return [ '00', '00', '00' ]; } if (timeDiff > 100 * 3600) { return [ '99', '59', '59' ]; } const hour = Math.floor(timeDiff / 3600); const minute = Math.floor((timeDiff - hour * 3600) / 60); const second = timeDiff - hour * 3600 - minute * 60; return [ padZero(hour), padZero(minute), padZero(second) ]; } const useCountDown = (time, format = getHMSTime, callback)=>{ const [remainTime, setRemainTime] = useState(time); const [delay, setDelay] = useState(1000); useInterval(()=>{ if (remainTime <= 0) { setDelay(null); return; } setRemainTime(remainTime - 1); }, delay); useEffect(()=>{ if (time > 0 && remainTime <= 0) { callback && callback(); } }, [ callback, remainTime, time ]); const [hour, minute, secoud] = format(remainTime); return [ hour, minute, secoud ]; }; const useCounter = (initialValue = 0, max = null, min = null)=>{ // avoid exec init code every render const initFunc = ()=>{ let init = typeof initialValue === 'function' ? initialValue() : initialValue; typeof init !== 'number' && console.error(`initialValue has to be a number, got ${typeof initialValue}`); if (typeof min === 'number') { init = Math.max(init, min); } else if (min !== null) { console.error(`min has to be a number, got ${typeof min}`); } if (typeof max === 'number') { init = Math.min(init, max); } else if (max !== null) { console.error(`max has to be a number, got ${typeof max}`); } return init; }; const [value, setValue] = useState(initFunc); const set = useEvent((newState)=>{ setValue((v)=>{ let nextValue = typeof newState === 'function' ? newState(v) : newState; if (typeof min === 'number') { nextValue = Math.max(nextValue, min); } if (typeof max === 'number') { nextValue = Math.min(nextValue, max); } return nextValue; }); }); const inc = (delta = 1)=>{ set((value)=>value + delta); }; const dec = (delta = 1)=>{ set((value)=>value - delta); }; const reset = ()=>{ set(initFunc); }; return [ value, set, inc, dec, reset ]; }; const defaultOptions = { observe: false }; function getInitialState$3(defaultValue) { // Prevent a React hydration mismatch when a default value is provided. if (defaultValue !== undefined) { return defaultValue; } if (isBrowser) { return ''; } if (process.env.NODE_ENV !== 'production') { console.warn('`useCssVar` When server side rendering, defaultValue should be defined to prevent a hydration mismatches.'); } return ''; } const useCssVar = (prop, target, defaultValue, options = defaultOptions)=>{ const { observe } = options; const [variable, setVariable] = useState(getInitialState$3(defaultValue)); const observerRef = useRef(); const set = useCallback((v)=>{ const element = getTargetElement(target); if (element == null ? void 0 : element.style) { element == null ? void 0 : element.style.setProperty(prop, v); setVariable(v); } }, [ prop, target ]); const updateCssVar = useCallback(()=>{ const element = getTargetElement(target); if (element) { var _window_getComputedStyle_getPropertyValue; const value = (_window_getComputedStyle_getPropertyValue = window.getComputedStyle(element).getPropertyValue(prop)) == null ? void 0 : _window_getComputedStyle_getPropertyValue.trim(); setVariable(value); } }, [ target, prop ]); useEffect(()=>{ var _window_getComputedStyle_getPropertyValue; const element = getTargetElement(target); if (!element) { return; } const value = (_window_getComputedStyle_getPropertyValue = window.getComputedStyle(element).getPropertyValue(prop)) == null ? void 0 : _window_getComputedStyle_getPropertyValue.trim(); /** if var don't has value and defaultValue exist */ if (!value && defaultValue) { set(defaultValue); } else { updateCssVar(); } if (!observe) { return; } observerRef.current = new MutationObserver(updateCssVar); observerRef.current.observe(element, { attributeFilter: [ 'style', 'class' ] }); return ()=>{ if (observerRef.current) { observerRef.current.disconnect(); } }; }, [ observe, target, updateCssVar, set, defaultValue, prop ]); return [ variable, set ]; }; const useCycleList = (list, i = 0)=>{ const [index, setIndex] = useState(i); const set = (i)=>{ const length = list.length; const nextIndex = ((index + i) % length + length) % length; setIndex(nextIndex); }; const next = (i = 1)=>{ set(i); }; const prev = (i = 1)=>{ set(-i); }; return [ list[index], next, prev ]; }; function guessSerializerType(rawInit) { return rawInit == null || rawInit === undefined ? 'any' : rawInit instanceof Set ? 'set' : rawInit instanceof Map ? 'map' : rawInit instanceof Date ? 'date' : typeof rawInit === 'boolean' ? 'boolean' : typeof rawInit === 'string' ? 'string' : typeof rawInit === 'object' ? 'object' : Array.isArray(rawInit) ? 'object' : !Number.isNaN(rawInit) ? 'number' : 'any'; } // Custom DOM event used to keep sibling hook instances in the SAME tab in sync. // The native `storage` event only fires on OTHER tabs/windows (never on the // document that made the change), so each write is re-broadcast under this // custom name and every instance listens for it alongside the native event. // We deliberately do NOT reuse the native `'storage'` name, to avoid notifying // unrelated `storage` listeners in the host application. A `window` event (not a // module-level registry) survives the library being bundled more than once. const SAME_TAB_STORAGE_EVENT = 'reactuse:storage'; function dispatchSameTabStorage(detail) { window.dispatchEvent(new CustomEvent(SAME_TAB_STORAGE_EVENT, { detail })); } const StorageSerializers = { boolean: { read: (v)=>v === 'true', write: (v)=>String(v) }, object: { read: (v)=>JSON.parse(v), write: (v)=>JSON.stringify(v) }, number: { read: (v)=>Number.parseFloat(v), write: (v)=>String(v) }, any: { read: (v)=>v, write: (v)=>String(v) }, string: { read: (v)=>v, write: (v)=>String(v) }, map: { read: (v)=>new Map(JSON.parse(v)), write: (v)=>JSON.stringify(Array.from(v.entries())) }, set: { read: (v)=>new Set(JSON.parse(v)), write: (v)=>JSON.stringify(Array.from(v)) }, date: { read: (v)=>new Date(v), write: (v)=>v.toISOString() } }; function useStorage(key, defaultValue, getStorage = ()=>isBrowser ? sessionStorage : undefined, options = defaultOptions$1) { let storage; const { onError = defaultOnError, effectStorageValue, mountStorageValue, listenToStorageChanges = true } = options; const storageValueRef = useLatest(mountStorageValue != null ? mountStorageValue : effectStorageValue); const onErrorRef = useLatest(onError); try { storage = getStorage(); } catch (err) { onErrorRef.current(err); } const type = guessSerializerType(defaultValue); var _options_serializer; const serializerRef = useLatest((_options_serializer = options.serializer) != null ? _options_serializer : StorageSerializers[type]); // storageRef and defaultValueRef are updated synchronously each render so that // the stable getSnapshot/getServerSnapshot closures always read current values. const storageRef = useRef(storage); storageRef.current = storage; const defaultValueRef = useRef(defaultValue); defaultValueRef.current = defaultValue; // Cache for referential stability of deserialized values. // lastRawRef uses three-state semantics: // undefined → no cached value (initial state or after key change) — absent key yields defaultValue // null → key was explicitly removed (setState(null) or cross-tab) — absent key yields null // string → cached raw string — compared for referential stability const lastRawRef = useRef(undefined); const lastKeyRef = useRef(key); const lastValueRef = useRef(defaultValue != null ? defaultValue : null); // Reset per-key caches when the key changes (runs during render, before snapshot). if (lastKeyRef.current !== key) { lastKeyRef.current = key; lastRawRef.current = undefined; lastValueRef.current = defaultValue != null ? defaultValue : null; } // Internal per-instance subscriber callback stored so updateState can notify it. const notifyRef = useRef(null); const getSnapshot = useRef(()=>{ const currentStorage = storageRef.current; var _defaultValueRef_current; const fallback = (_defaultValueRef_current = defaultValueRef.current) != null ? _defaultValueRef_current : null; if (!currentStorage) { // Storage unavailable — act as an in-memory state holder using the same // three-state lastRawRef semantics so updateState() still works. if (lastRawRef.current === undefined) return fallback; return lastRawRef.current === null ? null : lastValueRef.current; } try { const raw = currentStorage.getItem(lastKeyRef.current); if (raw === null) { // lastRawRef === null means the key was explicitly removed; return null. // lastRawRef !== null means the key is merely absent (e.g. after key change); return defaultValue. return lastRawRef.current === null ? null : fallback; } if (raw === lastRawRef.current) return lastValueRef.current; const deserialized = serializerRef.current.read(raw); lastRawRef.current = raw; lastValueRef.current = deserialized; return deserialized; } catch (e) { onErrorRef.current(e); return fallback; } }).current; const getServerSnapshot = useRef(()=>{ var _defaultValueRef_current; return (_defaultValueRef_current = defaultValueRef.current) != null ? _defaultValueRef_current : null; }).current; // subscribe is stable: it only registers/clears the React-provided callback. // Cross-tab listener management is handled separately in a useEffect so that // changes to listenToStorageChanges are properly reflected after mount. const subscribe = useRef((callback)=>{ notifyRef.current = callback; return ()=>{ notifyRef.current = null; }; }).current; const state = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot); // Shared core for an external change (cross-tab or same-tab). The two listeners // below stay separate but funnel through here so the key/area filtering and the // removal-reset rule live in exactly one place. const applyExternalChange = (evKey, evNewValue, evArea)=>{ // evKey === null ⇒ storage.clear() ⇒ all keys affected, so don't filter by // key. lastKeyRef is updated synchronously during render, so it always holds // the latest key when this async event fires. if (evKey !== null && evKey !== lastKeyRef.current) return; // Identity-scope by storage area so localStorage / sessionStorage / custom // Storage objects never cross-notify (only when the event provides it). if (evArea && evArea !== storageRef.current) return; // newValue === null means the key was removed (removeItem or clear). Reset // the caches so getSnapshot returns null immediately rather than falling back // to defaultValue. if (evNewValue === null) { lastRawRef.current = null; lastValueRef.current = null; } notifyRef.current == null ? void 0 : notifyRef.current.call(notifyRef); }; // Cross-tab: the native `storage` event fires when ANOTHER tab changes Web // Storage (it never fires in the tab that made the change). This is the ONLY // path gated by listenToStorageChanges — that option is specifically about // cross-tab events. useEventListener keeps the latest handler in a ref, so // toggling the option after mount is reflected without re-binding. useEventListener('storage', (e)=>{ if (!listenToStorageChanges) return; applyExternalChange(e.key, e.newValue, e.storageArea); }); // Same-tab: our custom event fires when a SIBLING instance changes the key in // THIS tab. ALWAYS on (NOT gated by listenToStorageChanges) — keeping components // in one tab consistent is core behavior, not an opt-in. SSR-safe via useEventListener. useEventListener(SAME_TAB_STORAGE_EVENT, (e)=>{ applyExternalChange(e.detail.key, e.detail.newValue, e.detail.storageArea); }); // Write mountStorageValue / defaultValue to storage on mount when key is absent. useEffect(()=>{ const serializer = serializerRef.current; const storageValue = storageValueRef.current; var _ref; const data = (_ref = storageValue ? isFunction(storageValue) ? storageValue() : storageValue : defaultValue) != null ? _ref : null; try { const raw = storage == null ? void 0 : storage.getItem(key); if ((raw === null || raw === undefined) && data !== null) { const written = serializer.write(data); storage == null ? void 0 : storage.setItem(key, written); lastRawRef.current = undefined; notifyRef.current == null ? void 0 : notifyRef.current.call(notifyRef); // Let sibling instances that mounted alongside us (e.g. two useColorMode // in a header and footer) pick up the freshly-written default. if (storage) dispatchSameTabStorage({ key, newValue: written, storageArea: storage }); } } catch (e) { onErrorRef.current(e); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [ key, storage ]); const updateState = useEvent((valOrFunc)=>{ const currentState = isFunction(valOrFunc) ? valOrFunc(getSnapshot()) : valOrFunc; // Payload broadcast to sibling instances: the raw written string, or null // when the key is removed (mirrors StorageEvent.newValue semantics). let payload; if (currentState === null) { storage == null ? void 0 : storage.removeItem(key); lastRawRef.current = null; lastValueRef.current = null; payload = null; } else { try { const raw = serializerRef.current.write(currentState); storage == null ? void 0 : storage.setItem(key, raw); lastRawRef.current = raw; lastValueRef.current = currentState; payload = raw; } catch (e) { onErrorRef.current(e); return; } } notifyRef.current == null ? void 0 : notifyRef.current.call(notifyRef); // Propagate to sibling instances in this tab (the native `storage` event // only reaches OTHER tabs). Our own listener also receives this, but // getSnapshot returns the cached value so useSyncExternalStore bails out — // no redundant render, hence no self-skip bookkeeping needed. if (storage) dispatchSameTabStorage({ key, newValue: payload, storageArea: storage }); }); return [ state, updateState ]; } const useColorMode = (options)=>{ const { selector = 'html', attribute = 'class', modes, defaultValue, storageKey = 'reactuses-color-mode', storage = ()=>isBrowser ? localStorage : undefined, initialValueDetector, modeClassNames = {} } = options; const initialValueDetectorRef = useLatest(initialValueDetector); // Validate modes array if (!modes || modes.length === 0) { throw new Error('useColorMode: modes array cannot be empty'); } // Get initial value from detector or use first mode as fallback const getInitialValue = useCallback(()=>{ if (initialValueDetectorRef.current) { const initialValueDetector = initialValueDetectorRef.current; try { const detectedValue = initialValueDetector(); return modes.includes(detectedValue) ? detectedValue : modes[0]; } catch (e) { return modes[0]; } } return defaultValue && modes.includes(defaultValue) ? defaultValue : modes[0]; // eslint-disable-next-line react-hooks/exhaustive-deps }, [ defaultValue, JSON.stringify(modes) ]); const [colorMode, setColorMode] = useStorage(storageKey, defaultValue, storage, { mountStorageValue: getInitialValue }); // Apply color mode to DOM element useDeepCompareEffect(()=>{ var _window; if (!colorMode) return; const element = (_window = window) == null ? void 0 : _window.document.querySelector(selector); if (!element) return; // Remove all existing mode classes/attributes modes.forEach((mode)=>{ const className = modeClassNames[mode] || mode; if (attribute === 'class') { element.classList.remove(className); } else { element.removeAttribute(attribute); } }); // Apply current mode class/attribute const currentClassName = modeClassNames[colorMode] || colorMode; if (attribute === 'class') { element.classList.add(currentClassName); } else { element.setAttribute(attribute, currentClassName); } return ()=>{ // Cleanup: remove current mode class/attribute if (attribute === 'class') { element.classList.remove(currentClassName); } else { element.removeAttribute(attribute); } }; }, [ colorMode, selector, attribute, modes, modeClassNames ]); const cycle = useEvent(()=>{ if (!colorMode) return; const currentIndex = modes.indexOf(colorMode); const nextIndex = (currentIndex + 1) % modes.length; setColorMode(modes[nextIndex]); }); return [ colorMode, setColorMode, cycle ]; }; function getSystemPreference() { return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } const useDarkMode = (options)=>{ const { selector = 'html', attribute = 'class', classNameDark = '', classNameLight = '', storageKey = 'reactuses-color-scheme', storage = ()=>isBrowser ? localStorage : undefined, defaultValue = false } = options; // Convert boolean-based options to string-based options for useColorMode const [colorMode, setColorMode] = useColorMode({ selector, attribute, modes: [ 'light', 'dark' ], defaultValue: defaultValue ? 'dark' : 'light', storageKey, storage, initialValueDetector: getSystemPreference, modeClassNames: { dark: classNameDark, light: classNameLight } }); // Convert string mode back to boolean const dark = colorMode === 'dark'; // Toggle function that switches between dark and light const toggle = useCallback(()=>{ setColorMode(dark ? 'light' : 'dark'); }, [ dark, setColorMode ]); // Set function that accepts boolean value const setDark = useCallback((value)=>{ if (typeof value === 'function') { const currentDark = colorMode === 'dark'; const newDark = value(currentDark); if (newDark !== null) { setColorMode(newDark ? 'dark' : 'light'); } } else if (value !== null) { setColorMode(value ? 'dark' : 'light'); } }, [ colorMode, setColorMode ]); return [ dark, toggle, setDark ]; }; function useUnmount(fn) { if (isDev) { if (!isFunction(fn)) { console.error(`useUnmount expected parameter is a function, got ${typeof fn}`); } } const fnRef = useLatest(fn); useEffect(()=>()=>{ fnRef.current(); }, [ fnRef ]); } const useDebounceFn = (fn, wait, options)=>{ if (isDev) { if (!isFunction(fn)) { console.error(`useDebounceFn expected parameter is a function, got ${typeof fn}`); } } const fnRef = useLatest(fn); const debounced = useMemo(()=>debounce((...args)=>{ return fnRef.current(...args); }, wait, options), // eslint-disable-next-line react-hooks/exhaustive-deps [ JSON.stringify(options), wait ]); useUnmount(()=>{ debounced.cancel(); }); return { run: debounced, cancel: debounced.cancel, flush: debounced.flush }; }; const useDebounce = (value, wait, options)=>{ const [debounced, setDebounced] = useState(value); const { run } = useDebounceFn(()=>{ setDebounced(value); }, wait, options); useEffect(()=>{ run(); }, [ run, value ]); return debounced; }; function getInitialState$2(defaultValue) { // Prevent a React hydration mismatch when a default value is provided. if (defaultValue !== undefined) { return defaultValue; } if (isBrowser) { return document.visibilityState; } if (process.env.NODE_ENV !== 'production') { console.warn('`useDocumentVisibility` When server side rendering, defaultValue should be defined to prevent a hydration mismatches.'); } return 'visible'; } function useDocumentVisibility(defaultValue) { const [visible, setVisible] = useState(getInitialState$2(defaultValue)); useEventListener('visibilitychange', ()=>{ setVisible(document.visibilityState); }, ()=>document); useEffect(()=>{ setVisible(document.visibilityState); }, []); return visible; } const useDoubleClick = ({ target, latency = 300, onSingleClick = ()=>{}, onDoubleClick = ()=>{} })=>{ const handle = useCallback((onSingleClick, onDoubleClick)=>{ let count = 0; return (e)=>{ // prevent ios double click slide if (e.type === 'touchend') { e.stopPropagation(); e.preventDefault(); } count += 1; setTimeout(()=>{ if (count === 1) { onSingleClick(e); } else if (count === 2) { onDoubleClick(e); } count = 0; }, latency); }; }, [ latency ]); const handleClick = handle(onSingleClick, onDoubleClick); const handleTouchEnd = handle(onSingleClick, onDoubleClick); useEventListener('click', handleClick, target); useEventListener('touchend', handleTouchEnd, target, { passive: false }); }; function isScrollX(node) { if (!node) { return false; } return getComputedStyle(node).overflowX === 'auto' || getComputedStyle(node).overflowX === 'scroll'; } function isScrollY(node) { if (!node) { return false; } return getComputedStyle(node).overflowY === 'auto' || getComputedStyle(node).overflowY === 'scroll'; } const useDraggable = (target, options = {})=>{ const { draggingElement, containerElement } = options; var _options_handle; const draggingHandle = (_options_handle = options.handle) != null ? _options_handle : target; var _options_initialValue; const [position, setPositon] = useState((_options_initialValue = options.initialValue) != null ? _options_initialValue : { x: 0, y: 0 }); useDeepCompareEffect(()=>{ var _options_initialValue; setPositon((_options_initialValue = options.initialValue) != null ? _options_initialValue : { x: 0, y: 0 }); }, [ options.initialValue ]); const [pressedDelta, setPressedDelta] = useState(); const filterEvent = (e)=>{ if (options.pointerTypes) { return options.pointerTypes.includes(e.pointerType); } return true; }; const handleEvent = (e)=>{ if (options.preventDefault) { e.preventDefault(); } if (options.stopPropagation) { e.stopPropagation(); } }; const start = (e)=>{ var _container_getBoundingClientRect; const element = getTargetElement(target); if (!filterEvent(e) || !element) { return; } if (options.exact && e.target !== element) { return; } const container = getTargetElement(containerElement); const containerRect = container == null ? void 0 : (_container_getBoundingClientRect = container.getBoundingClientRect) == null ? void 0 : _container_getBoundingClientRect.call(container); const targetRect = element.getBoundingClientRect(); const pos = { x: e.clientX - (container && containerRect ? targetRect.left - (containerRect == null ? void 0 : containerRect.left) + container.scrollLeft : targetRect.left), y: e.clientY - (container && containerRect ? targetRect.top - containerRect.top + container.scrollTop : targetRect.top) }; if ((options.onStart == null ? void 0 : options.onStart.call(options, pos, e)) === false) { return; } setPressedDelta(pos); handleEvent(e); }; const move = (e)=>{ const element = getTargetElement(target); if (!filterEvent(e) || !element) { return; } if (!pressedDelta) { return; } const container = getTargetElement(containerElement); const targetRect = element.getBoundingClientRect(); let { x, y } = position; x = e.clientX - pressedDelta.x; y = e.clientY - pressedDelta.y; if (container) { const containerWidth = isScrollX(container) ? container.scrollWidth : container.clientWidth; const containerHeight = isScrollY(container) ? container.scrollHeight : container.clientHeight; x = Math.min(Math.max(0, x), containerWidth - targetRect.width); y = Math.min(Math.max(0, y), containerHeight - targetRect.height); } setPositon({ x, y }); options.onMove == null ? void 0 : options.onMove.call(options, position, e); handleEvent(e); }; const end = (e)=>{ if (!filterEvent(e)) { return; } if (!pressedDelta) { return; } setPressedDelta(undefined); options.onEnd == null ? void 0 : options.onEnd.call(options, position, e); handleEvent(e); }; useEventListener('pointerdown', start, draggingHandle, true); useEventListener('pointermove', move, draggingElement, true); useEventListener('pointerup', end, draggingElement, true); return [ position.x, position.y, !!pressedDelta, setPositon ]; }; const useDropZone = (target, onDrop)=>{ const [over, setOver] = useState(false); const counter = useRef(0); useEventListener('dragenter', (event)=>{ event.preventDefault(); counter.current += 1; setOver(true); }, target); useEventListener('dragover', (event)=>{ event.preventDefault(); }, target); useEventListener('dragleave', (event)=>{ event.preventDefault(); counter.current -= 1; if (counter.current === 0) { setOver(false); } }, target); useEventListener('drop', (event)=>{ var _event_dataTransfer; event.preventDefault(); counter.current = 0; setOver(false); var _event_dataTransfer_files; const files = Array.from((_event_dataTransfer_files = (_event_dataTransfer = event.dataTransfer) == null ? void 0 : _event_dataTransfer.files) != null ? _event_dataTransfer_files : []); onDrop == null ? void 0 : onDrop(files.length === 0 ? null : files); }, target); return over; }; const useResizeObserver = (target, callback, options = defaultOptions$1)=>{ const savedCallback = useLatest(callback); const observerRef = useRef(); const { key: targetKey, ref: targetRef } = useStableTarget(target); const stop = useCallback(()=>{ if (observerRef.current) { observerRef.current.disconnect(); } }, []); useDeepCompareEffect(()=>{ const element = getTargetElement(targetRef.current); if (!element) { return; } observerRef.current = new ResizeObserver(savedCallback.current); observerRef.current.observe(element, options); return stop; }, [ targetKey, options ]); return stop; }; const useElementBounding = (target, options = defaultOptions$1)=>{ const { reset = true, windowResize = true, windowScroll = true, immediate = true } = options; const [height, setHeight] = useState(0); const [bottom, setBottom] = useState(0); const [left, setLeft] = useState(0); const [right, setRight] = useState(0); const [top, setTop] = useState(0); const [width, setWidth] = useState(0); const [x, setX] = useState(0); const [y, setY] = useState(0); const update = useEvent(()=>{ const element = getTargetElement(target); if (!element) { if (reset) { setHeight(0); setBottom(0); setLeft(0); setRight(0); setTop(0); setWidth(0); setX(0); setY(0); } return; } const rect = element.getBoundingClientRect(); setHeight(rect.height); setBottom(rect.bottom); setLeft(rect.left); setRight(rect.right); setTop(rect.top); setWidth(rect.width); setX(rect.x); setY(rect.y); }); useResizeObserver(target, update); useEffect(()=>{ if (immediate) { update(); } }, [ immediate, update ]); useEffect(()=>{ if (windowScroll) { window.addEventListener('scroll', update, { passive: true }); } if (windowResize) { window.addEventListener('resize', update, { passive: true }); } return ()=>{ if (windowScroll) { window.removeEventListener('scroll', update); } if (windowResize) { window.removeEventListener('resize', update); } }; }, [ update, windowResize, windowScroll ]); return { height, bottom, left, right, top, width, x, y, update }; }; const useElementSize = (target, options = defaultOptions$1)=>{ const { box = 'content-box' } = options; const [width, setWidth] = useState(0); const [height, setHeight] = useState(0); useResizeObserver(target, ([entry])=>{ const boxSize = box === 'border-box' ? entry.borderBoxSize : box === 'content-box' ? entry.contentBoxSize : entry.devicePixelContentBoxSize; if (boxSize) { setWidth(boxSize.reduce((acc, { inlineSize })=>acc + inlineSize, 0)); setHeight(boxSize.reduce((acc, { blockSize })=>acc + blockSize, 0)); } else { // fallback setWidth(entry.contentRect.width); setHeight(entry.contentRect.height); } }, options); return [ width, height ]; }; const useIntersectionObserver = (target, callback, options = defaultOptions$1)=>{ const savedCallback = useLatest(callback); const observerRef = useRef(); const { key: targetKey, ref: targetRef } = useStableTarget(target); const stop = useCallback(()=>{ if (observerRef.current) { observerRef.current.disconnect(); } }, []); useDeepCompareEffect(()=>{ const element = getTargetElement(targetRef.current); if (!element) { return; } observerRef.current = new IntersectionObserver(savedCallback.current, options); observerRef.current.observe(element); return stop; }, [ targetKey, options ]); return stop; }; const useElementVisibility = (target, options = defaultOptions$1)=>{ const [visible, setVisible] = useState(false); const callback = useCallback((entries)=>{ const rect = entries[0].boundingClientRect; setVisible(rect.top <= (window.innerHeight || document.documentElement.clientHeight) && rect.left <= (window.innerWidth || document.documentElement.clientWidth) && rect.bottom >= 0 && rect.right >= 0); }, []); const stop = useIntersectionObserver(target, callback, options); return [ visible, stop ]; }; function useEventEmitter() { const listeners = useRef([]); const _disposed = useRef(false); const _event = useRef((listener)=>{ listeners.current.push(listener); const disposable = { dispose: ()=>{ if (!_disposed.current) { for(let i = 0; i < listeners.current.length; i++){ if (listeners.current[i] === listener) { listeners.current.splice(i, 1); return; } } } } }; return disposable; }); const fire = (arg1, arg2)=>{ const queue = []; for(let i = 0; i < listeners.current.length; i++){ queue.push(listeners.current[i]); } for(let i = 0; i < queue.length; i++){ queue[i].call(undefined, arg1, arg2); } }; const dispose = ()=>{ if (listeners.current.length !== 0) { listeners.current.length = 0; }