@jbpark/use-hooks 2.11.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/hooks/index.d.mts +12 -1
- package/dist/hooks/index.mjs +12 -1
- package/dist/hooks/use-click-outside.d.mts +9 -2
- package/dist/hooks/use-click-outside.mjs +27 -11
- package/dist/hooks/use-click-outside.mjs.map +1 -1
- package/dist/hooks/use-debounced-value.d.mts +5 -0
- package/dist/hooks/use-debounced-value.mjs +20 -0
- package/dist/hooks/use-debounced-value.mjs.map +1 -0
- package/dist/hooks/use-event-listener.d.mts +21 -0
- package/dist/hooks/use-event-listener.mjs +37 -0
- package/dist/hooks/use-event-listener.mjs.map +1 -0
- package/dist/hooks/use-file-drop.d.mts +14 -0
- package/dist/hooks/use-file-drop.mjs +85 -0
- package/dist/hooks/use-file-drop.mjs.map +1 -0
- package/dist/hooks/use-image.d.mts +2 -1
- package/dist/hooks/use-image.mjs +4 -3
- package/dist/hooks/use-image.mjs.map +1 -1
- package/dist/hooks/use-intersection-observer.d.mts +8 -1
- package/dist/hooks/use-intersection-observer.mjs +37 -5
- package/dist/hooks/use-intersection-observer.mjs.map +1 -1
- package/dist/hooks/use-interval.d.mts +5 -0
- package/dist/hooks/use-interval.mjs +20 -0
- package/dist/hooks/use-interval.mjs.map +1 -0
- package/dist/hooks/use-key-press.d.mts +14 -0
- package/dist/hooks/use-key-press.mjs +63 -0
- package/dist/hooks/use-key-press.mjs.map +1 -0
- package/dist/hooks/use-merged-ref.d.mts +8 -0
- package/dist/hooks/use-merged-ref.mjs +25 -0
- package/dist/hooks/use-merged-ref.mjs.map +1 -0
- package/dist/hooks/use-mutation-observer.d.mts +11 -0
- package/dist/hooks/use-mutation-observer.mjs +33 -0
- package/dist/hooks/use-mutation-observer.mjs.map +1 -0
- package/dist/hooks/use-previous.d.mts +5 -0
- package/dist/hooks/use-previous.mjs +14 -0
- package/dist/hooks/use-previous.mjs.map +1 -0
- package/dist/hooks/use-resize-observer.d.mts +12 -0
- package/dist/hooks/use-resize-observer.mjs +29 -0
- package/dist/hooks/use-resize-observer.mjs.map +1 -0
- package/dist/hooks/use-scroll-to-elements.d.mts +7 -5
- package/dist/hooks/use-scroll-to-elements.mjs +52 -27
- package/dist/hooks/use-scroll-to-elements.mjs.map +1 -1
- package/dist/hooks/use-throttle.d.mts +5 -1
- package/dist/hooks/use-throttle.mjs +6 -32
- package/dist/hooks/use-throttle.mjs.map +1 -1
- package/dist/hooks/use-throttled-callback.d.mts +12 -0
- package/dist/hooks/use-throttled-callback.mjs +54 -0
- package/dist/hooks/use-throttled-callback.mjs.map +1 -0
- package/dist/hooks/use-timeout.d.mts +8 -0
- package/dist/hooks/use-timeout.mjs +38 -0
- package/dist/hooks/use-timeout.mjs.map +1 -0
- package/dist/hooks/use-toggle.d.mts +5 -0
- package/dist/hooks/use-toggle.mjs +17 -0
- package/dist/hooks/use-toggle.mjs.map +1 -0
- package/dist/index.d.mts +13 -2
- package/dist/index.mjs +13 -2
- package/package.json +1 -1
- package/dist/hooks/use-timeline.d.mts +0 -50
- package/dist/hooks/use-timeline.mjs +0 -211
- package/dist/hooks/use-timeline.mjs.map +0 -1
package/dist/hooks/index.d.mts
CHANGED
|
@@ -1,19 +1,30 @@
|
|
|
1
1
|
import { useDebounce } from "./use-debounce.mjs";
|
|
2
|
+
import { useDebouncedValue } from "./use-debounced-value.mjs";
|
|
2
3
|
import { useBodyScrollLock } from "./use-body-scroll-lock.mjs";
|
|
3
4
|
import { useClickOutside } from "./use-click-outside.mjs";
|
|
4
5
|
import { useControllableState } from "./use-controllable-state.mjs";
|
|
5
6
|
import { useElementPosition } from "./use-element-position.mjs";
|
|
6
7
|
import { useElementScroll } from "./use-element-scroll.mjs";
|
|
8
|
+
import { useEventListener } from "./use-event-listener.mjs";
|
|
9
|
+
import { useFileDrop } from "./use-file-drop.mjs";
|
|
7
10
|
import { useFileToDataUrl } from "./use-file-to-data-url.mjs";
|
|
8
11
|
import { useHistoryState } from "./use-history-state.mjs";
|
|
9
12
|
import { useIntersectionObserver } from "./use-intersection-observer.mjs";
|
|
13
|
+
import { useInterval } from "./use-interval.mjs";
|
|
10
14
|
import { useResponsiveSize } from "./use-responsive-size.mjs";
|
|
11
15
|
import { useImage } from "./use-image.mjs";
|
|
16
|
+
import { useKeyPress } from "./use-key-press.mjs";
|
|
12
17
|
import { useLocalStorage } from "./use-local-storage.mjs";
|
|
18
|
+
import { useMergedRef } from "./use-merged-ref.mjs";
|
|
13
19
|
import { useMultiSelect } from "./use-multi-select.mjs";
|
|
20
|
+
import { useMutationObserver } from "./use-mutation-observer.mjs";
|
|
21
|
+
import { usePrevious } from "./use-previous.mjs";
|
|
14
22
|
import { useRecursiveTimeout } from "./use-recursive-timeout.mjs";
|
|
23
|
+
import { useResizeObserver } from "./use-resize-observer.mjs";
|
|
15
24
|
import { useScrollToElements } from "./use-scroll-to-elements.mjs";
|
|
16
25
|
import { useThrottle } from "./use-throttle.mjs";
|
|
17
|
-
import {
|
|
26
|
+
import { useThrottledCallback } from "./use-throttled-callback.mjs";
|
|
27
|
+
import { useTimeout } from "./use-timeout.mjs";
|
|
28
|
+
import { useToggle } from "./use-toggle.mjs";
|
|
18
29
|
import { useWindowScroll } from "./use-window-scroll.mjs";
|
|
19
30
|
import { useViewport } from "./use-viewport.mjs";
|
package/dist/hooks/index.mjs
CHANGED
|
@@ -1,20 +1,31 @@
|
|
|
1
1
|
import useDebounce from "./use-debounce.mjs";
|
|
2
|
+
import useDebouncedValue from "./use-debounced-value.mjs";
|
|
2
3
|
import useBodyScrollLock from "./use-body-scroll-lock.mjs";
|
|
3
4
|
import useClickOutside from "./use-click-outside.mjs";
|
|
4
5
|
import useControllableState from "./use-controllable-state.mjs";
|
|
5
6
|
import useElementPosition from "./use-element-position.mjs";
|
|
6
7
|
import useElementScroll from "./use-element-scroll.mjs";
|
|
8
|
+
import useEventListener from "./use-event-listener.mjs";
|
|
9
|
+
import useFileDrop from "./use-file-drop.mjs";
|
|
7
10
|
import useFileToDataUrl from "./use-file-to-data-url.mjs";
|
|
8
11
|
import useHistoryState from "./use-history-state.mjs";
|
|
9
12
|
import useIntersectionObserver from "./use-intersection-observer.mjs";
|
|
13
|
+
import useInterval from "./use-interval.mjs";
|
|
10
14
|
import useResponsiveSize from "./use-responsive-size.mjs";
|
|
11
15
|
import useImage from "./use-image.mjs";
|
|
16
|
+
import useKeyPress from "./use-key-press.mjs";
|
|
12
17
|
import useLocalStorage from "./use-local-storage.mjs";
|
|
18
|
+
import useMergedRef from "./use-merged-ref.mjs";
|
|
13
19
|
import useMultiSelect from "./use-multi-select.mjs";
|
|
20
|
+
import useMutationObserver from "./use-mutation-observer.mjs";
|
|
21
|
+
import usePrevious from "./use-previous.mjs";
|
|
14
22
|
import useRecursiveTimeout from "./use-recursive-timeout.mjs";
|
|
23
|
+
import useResizeObserver from "./use-resize-observer.mjs";
|
|
15
24
|
import useScrollToElements from "./use-scroll-to-elements.mjs";
|
|
25
|
+
import useThrottledCallback from "./use-throttled-callback.mjs";
|
|
16
26
|
import useThrottle from "./use-throttle.mjs";
|
|
17
|
-
import
|
|
27
|
+
import useTimeout from "./use-timeout.mjs";
|
|
28
|
+
import useToggle from "./use-toggle.mjs";
|
|
18
29
|
import useWindowScroll from "./use-window-scroll.mjs";
|
|
19
30
|
import useViewport from "./use-viewport.mjs";
|
|
20
31
|
|
|
@@ -1,7 +1,14 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { RefObject } from "react";
|
|
2
2
|
|
|
3
3
|
//#region src/hooks/use-click-outside.d.ts
|
|
4
|
-
|
|
4
|
+
type ClickOutsideRef = RefObject<HTMLElement | null> | null | undefined;
|
|
5
|
+
type ClickOutsideEvent = PointerEvent | MouseEvent | TouchEvent | KeyboardEvent;
|
|
6
|
+
interface Options {
|
|
7
|
+
enabled?: boolean;
|
|
8
|
+
events?: ('pointerdown' | 'mousedown' | 'touchstart')[];
|
|
9
|
+
escape?: boolean;
|
|
10
|
+
}
|
|
11
|
+
declare const useClickOutside: (refs: ClickOutsideRef | ClickOutsideRef[], handler: (event: ClickOutsideEvent) => void, options?: Options) => void;
|
|
5
12
|
//#endregion
|
|
6
13
|
export { useClickOutside };
|
|
7
14
|
//# sourceMappingURL=use-click-outside.d.mts.map
|
|
@@ -1,27 +1,43 @@
|
|
|
1
1
|
import { useEffect, useRef } from "react";
|
|
2
2
|
|
|
3
3
|
//#region src/hooks/use-click-outside.ts
|
|
4
|
-
const useClickOutside = (handler,
|
|
5
|
-
const
|
|
4
|
+
const useClickOutside = (refs, handler, options = {}) => {
|
|
5
|
+
const { enabled = true, events = ["pointerdown"], escape = true } = options;
|
|
6
6
|
const handlerRef = useRef(handler);
|
|
7
7
|
useEffect(() => {
|
|
8
8
|
handlerRef.current = handler;
|
|
9
9
|
});
|
|
10
|
+
const refList = Array.isArray(refs) ? refs : [refs];
|
|
11
|
+
const refListRef = useRef(refList);
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
refListRef.current = refList;
|
|
14
|
+
});
|
|
10
15
|
useEffect(() => {
|
|
11
16
|
if (!enabled || typeof document === "undefined") return;
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
17
|
+
const isInside = (target) => refListRef.current.some((ref) => ref?.current?.contains(target));
|
|
18
|
+
const onPointerEvent = (event) => {
|
|
19
|
+
const target = event.target;
|
|
20
|
+
if (!target || isInside(target)) return;
|
|
15
21
|
handlerRef.current(event);
|
|
16
22
|
};
|
|
17
|
-
|
|
18
|
-
|
|
23
|
+
const onKeyDown = (event) => {
|
|
24
|
+
if (event.key === "Escape") handlerRef.current(event);
|
|
25
|
+
};
|
|
26
|
+
events.forEach((eventName) => {
|
|
27
|
+
document.addEventListener(eventName, onPointerEvent);
|
|
28
|
+
});
|
|
29
|
+
if (escape) document.addEventListener("keydown", onKeyDown);
|
|
19
30
|
return () => {
|
|
20
|
-
|
|
21
|
-
|
|
31
|
+
events.forEach((eventName) => {
|
|
32
|
+
document.removeEventListener(eventName, onPointerEvent);
|
|
33
|
+
});
|
|
34
|
+
if (escape) document.removeEventListener("keydown", onKeyDown);
|
|
22
35
|
};
|
|
23
|
-
}, [
|
|
24
|
-
|
|
36
|
+
}, [
|
|
37
|
+
enabled,
|
|
38
|
+
escape,
|
|
39
|
+
events.join(",")
|
|
40
|
+
]);
|
|
25
41
|
};
|
|
26
42
|
|
|
27
43
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-click-outside.mjs","names":[],"sources":["../../src/hooks/use-click-outside.ts"],"sourcesContent":["import { useEffect, useRef } from 'react';\n\
|
|
1
|
+
{"version":3,"file":"use-click-outside.mjs","names":[],"sources":["../../src/hooks/use-click-outside.ts"],"sourcesContent":["import { type RefObject, useEffect, useRef } from 'react';\n\ntype ClickOutsideRef = RefObject<HTMLElement | null> | null | undefined;\n\ntype ClickOutsideEvent = PointerEvent | MouseEvent | TouchEvent | KeyboardEvent;\n\ninterface Options {\n enabled?: boolean;\n events?: ('pointerdown' | 'mousedown' | 'touchstart')[];\n escape?: boolean;\n}\n\n// Takes the \"inside\" ref(s) as an argument instead of creating and\n// returning one — lets a caller pass multiple exclusion targets (e.g. a\n// dropdown's trigger button *and* its portaled panel, which live in\n// different DOM subtrees) instead of only ever watching one element.\nconst useClickOutside = (\n refs: ClickOutsideRef | ClickOutsideRef[],\n handler: (event: ClickOutsideEvent) => void,\n options: Options = {},\n) => {\n const { enabled = true, events = ['pointerdown'], escape = true } = options;\n\n const handlerRef = useRef(handler);\n\n useEffect(() => {\n handlerRef.current = handler;\n });\n\n const refList = Array.isArray(refs) ? refs : [refs];\n // `refs` is almost always an inline array literal at the call site, so\n // depending on it directly would tear down/rebuild the listeners on\n // every render — read the latest value from a ref instead.\n const refListRef = useRef(refList);\n\n useEffect(() => {\n refListRef.current = refList;\n });\n\n const eventsKey = events.join(',');\n\n useEffect(() => {\n if (!enabled || typeof document === 'undefined') {\n return;\n }\n\n const isInside = (target: Node) =>\n refListRef.current.some(ref => ref?.current?.contains(target));\n\n const onPointerEvent = (event: Event) => {\n const target = event.target as Node | null;\n\n if (!target || isInside(target)) {\n return;\n }\n\n handlerRef.current(event as ClickOutsideEvent);\n };\n\n const onKeyDown = (event: KeyboardEvent) => {\n if (event.key === 'Escape') {\n handlerRef.current(event);\n }\n };\n\n events.forEach(eventName => {\n document.addEventListener(eventName, onPointerEvent);\n });\n\n if (escape) {\n document.addEventListener('keydown', onKeyDown);\n }\n\n return () => {\n events.forEach(eventName => {\n document.removeEventListener(eventName, onPointerEvent);\n });\n\n if (escape) {\n document.removeEventListener('keydown', onKeyDown);\n }\n };\n // `events` is covered by `eventsKey` below instead of the array\n // itself, which (like `refs`) is usually a fresh literal every render.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [enabled, escape, eventsKey]);\n};\n\nexport default useClickOutside;\n"],"mappings":";;;AAgBA,MAAM,mBACJ,MACA,SACA,UAAmB,EAAE,KAClB;CACH,MAAM,EAAE,UAAU,MAAM,SAAS,CAAC,cAAc,EAAE,SAAS,SAAS;CAEpE,MAAM,aAAa,OAAO,QAAQ;AAElC,iBAAgB;AACd,aAAW,UAAU;GACrB;CAEF,MAAM,UAAU,MAAM,QAAQ,KAAK,GAAG,OAAO,CAAC,KAAK;CAInD,MAAM,aAAa,OAAO,QAAQ;AAElC,iBAAgB;AACd,aAAW,UAAU;GACrB;AAIF,iBAAgB;AACd,MAAI,CAAC,WAAW,OAAO,aAAa,YAClC;EAGF,MAAM,YAAY,WAChB,WAAW,QAAQ,MAAK,QAAO,KAAK,SAAS,SAAS,OAAO,CAAC;EAEhE,MAAM,kBAAkB,UAAiB;GACvC,MAAM,SAAS,MAAM;AAErB,OAAI,CAAC,UAAU,SAAS,OAAO,CAC7B;AAGF,cAAW,QAAQ,MAA2B;;EAGhD,MAAM,aAAa,UAAyB;AAC1C,OAAI,MAAM,QAAQ,SAChB,YAAW,QAAQ,MAAM;;AAI7B,SAAO,SAAQ,cAAa;AAC1B,YAAS,iBAAiB,WAAW,eAAe;IACpD;AAEF,MAAI,OACF,UAAS,iBAAiB,WAAW,UAAU;AAGjD,eAAa;AACX,UAAO,SAAQ,cAAa;AAC1B,aAAS,oBAAoB,WAAW,eAAe;KACvD;AAEF,OAAI,OACF,UAAS,oBAAoB,WAAW,UAAU;;IAMrD;EAAC;EAAS;EA9CK,OAAO,KAAK,IAAI;EA8CH,CAAC"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from "react";
|
|
2
|
+
|
|
3
|
+
//#region src/hooks/use-debounced-value.ts
|
|
4
|
+
const useDebouncedValue = (value, delay = 100) => {
|
|
5
|
+
const [debouncedValue, setDebouncedValue] = useState(value);
|
|
6
|
+
const timeoutRef = useRef(null);
|
|
7
|
+
useEffect(() => {
|
|
8
|
+
timeoutRef.current = setTimeout(() => {
|
|
9
|
+
setDebouncedValue(value);
|
|
10
|
+
}, delay);
|
|
11
|
+
return () => {
|
|
12
|
+
if (timeoutRef.current) clearTimeout(timeoutRef.current);
|
|
13
|
+
};
|
|
14
|
+
}, [value, delay]);
|
|
15
|
+
return debouncedValue;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
//#endregion
|
|
19
|
+
export { useDebouncedValue as default };
|
|
20
|
+
//# sourceMappingURL=use-debounced-value.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-debounced-value.mjs","names":[],"sources":["../../src/hooks/use-debounced-value.ts"],"sourcesContent":["import { useEffect, useRef, useState } from 'react';\n\n// The value-shaped counterpart to useDebounce (which is callback-shaped),\n// symmetric with useThrottle's `(value, delay) => value` signature. The\n// common \"just give me the debounced value\" case otherwise needs two\n// pieces of state and an effect built on useDebounce by hand.\nconst useDebouncedValue = <T>(value: T, delay = 100): T => {\n const [debouncedValue, setDebouncedValue] = useState(value);\n const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n useEffect(() => {\n timeoutRef.current = setTimeout(() => {\n setDebouncedValue(value);\n }, delay);\n\n return () => {\n if (timeoutRef.current) {\n clearTimeout(timeoutRef.current);\n }\n };\n }, [value, delay]);\n\n return debouncedValue;\n};\n\nexport default useDebouncedValue;\n"],"mappings":";;;AAMA,MAAM,qBAAwB,OAAU,QAAQ,QAAW;CACzD,MAAM,CAAC,gBAAgB,qBAAqB,SAAS,MAAM;CAC3D,MAAM,aAAa,OAA6C,KAAK;AAErE,iBAAgB;AACd,aAAW,UAAU,iBAAiB;AACpC,qBAAkB,MAAM;KACvB,MAAM;AAET,eAAa;AACX,OAAI,WAAW,QACb,cAAa,WAAW,QAAQ;;IAGnC,CAAC,OAAO,MAAM,CAAC;AAElB,QAAO"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { RefObject } from "react";
|
|
2
|
+
|
|
3
|
+
//#region src/hooks/use-event-listener.d.ts
|
|
4
|
+
interface Options {
|
|
5
|
+
capture?: boolean;
|
|
6
|
+
passive?: boolean;
|
|
7
|
+
once?: boolean;
|
|
8
|
+
enabled?: boolean;
|
|
9
|
+
}
|
|
10
|
+
declare function useEventListener<K extends keyof WindowEventMap>(type: K, handler: (event: WindowEventMap[K]) => void, options?: Options & {
|
|
11
|
+
target?: Window | null;
|
|
12
|
+
}): void;
|
|
13
|
+
declare function useEventListener<K extends keyof DocumentEventMap>(type: K, handler: (event: DocumentEventMap[K]) => void, options: Options & {
|
|
14
|
+
target: Document | RefObject<Document | null>;
|
|
15
|
+
}): void;
|
|
16
|
+
declare function useEventListener<T extends HTMLElement, K extends keyof HTMLElementEventMap>(type: K, handler: (event: HTMLElementEventMap[K]) => void, options: Options & {
|
|
17
|
+
target: RefObject<T | null>;
|
|
18
|
+
}): void;
|
|
19
|
+
//#endregion
|
|
20
|
+
export { useEventListener };
|
|
21
|
+
//# sourceMappingURL=use-event-listener.d.mts.map
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { useEffect, useRef } from "react";
|
|
2
|
+
|
|
3
|
+
//#region src/hooks/use-event-listener.ts
|
|
4
|
+
function useEventListener(type, handler, options = {}) {
|
|
5
|
+
const { target, capture, passive, once, enabled = true } = options;
|
|
6
|
+
const handlerRef = useRef(handler);
|
|
7
|
+
useEffect(() => {
|
|
8
|
+
handlerRef.current = handler;
|
|
9
|
+
});
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
if (!enabled) return;
|
|
12
|
+
const resolvedTarget = target && "current" in target ? target.current : target ?? (typeof window === "undefined" ? null : window);
|
|
13
|
+
if (!resolvedTarget) return;
|
|
14
|
+
const listener = (event) => {
|
|
15
|
+
handlerRef.current(event);
|
|
16
|
+
};
|
|
17
|
+
resolvedTarget.addEventListener(type, listener, {
|
|
18
|
+
capture,
|
|
19
|
+
passive,
|
|
20
|
+
once
|
|
21
|
+
});
|
|
22
|
+
return () => {
|
|
23
|
+
resolvedTarget.removeEventListener(type, listener, { capture });
|
|
24
|
+
};
|
|
25
|
+
}, [
|
|
26
|
+
type,
|
|
27
|
+
target,
|
|
28
|
+
capture,
|
|
29
|
+
passive,
|
|
30
|
+
once,
|
|
31
|
+
enabled
|
|
32
|
+
]);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
//#endregion
|
|
36
|
+
export { useEventListener as default };
|
|
37
|
+
//# sourceMappingURL=use-event-listener.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-event-listener.mjs","names":[],"sources":["../../src/hooks/use-event-listener.ts"],"sourcesContent":["import { type RefObject, useEffect, useRef } from 'react';\n\ninterface Options {\n capture?: boolean;\n passive?: boolean;\n once?: boolean;\n enabled?: boolean;\n}\n\ntype EventListenerTarget = EventTarget | RefObject<EventTarget | null> | null;\n\nfunction useEventListener<K extends keyof WindowEventMap>(\n type: K,\n handler: (event: WindowEventMap[K]) => void,\n options?: Options & { target?: Window | null },\n): void;\nfunction useEventListener<K extends keyof DocumentEventMap>(\n type: K,\n handler: (event: DocumentEventMap[K]) => void,\n options: Options & { target: Document | RefObject<Document | null> },\n): void;\nfunction useEventListener<\n T extends HTMLElement,\n K extends keyof HTMLElementEventMap,\n>(\n type: K,\n handler: (event: HTMLElementEventMap[K]) => void,\n options: Options & { target: RefObject<T | null> },\n): void;\n\n// Resolves `target` (default `window`, or a raw EventTarget, or a\n// RefObject pointing at one) and (de)registers `type` on it — the pair of\n// addEventListener/removeEventListener calls duplicated across ui-kit\n// (marquee item) and live-editor (error handlers). `handler` is read\n// through a ref so a fresh function every render doesn't tear down and\n// re-add the listener.\nfunction useEventListener(\n type: string,\n handler: (event: Event) => void,\n options: Options & { target?: EventListenerTarget } = {},\n) {\n const { target, capture, passive, once, enabled = true } = options;\n\n const handlerRef = useRef(handler);\n\n useEffect(() => {\n handlerRef.current = handler;\n });\n\n useEffect(() => {\n if (!enabled) {\n return;\n }\n\n const resolvedTarget =\n target && 'current' in target\n ? target.current\n : (target ?? (typeof window === 'undefined' ? null : window));\n\n if (!resolvedTarget) {\n return;\n }\n\n const listener = (event: Event) => {\n handlerRef.current(event);\n };\n\n resolvedTarget.addEventListener(type, listener, { capture, passive, once });\n\n return () => {\n resolvedTarget.removeEventListener(type, listener, { capture });\n };\n }, [type, target, capture, passive, once, enabled]);\n}\n\nexport default useEventListener;\n"],"mappings":";;;AAoCA,SAAS,iBACP,MACA,SACA,UAAsD,EAAE,EACxD;CACA,MAAM,EAAE,QAAQ,SAAS,SAAS,MAAM,UAAU,SAAS;CAE3D,MAAM,aAAa,OAAO,QAAQ;AAElC,iBAAgB;AACd,aAAW,UAAU;GACrB;AAEF,iBAAgB;AACd,MAAI,CAAC,QACH;EAGF,MAAM,iBACJ,UAAU,aAAa,SACnB,OAAO,UACN,WAAW,OAAO,WAAW,cAAc,OAAO;AAEzD,MAAI,CAAC,eACH;EAGF,MAAM,YAAY,UAAiB;AACjC,cAAW,QAAQ,MAAM;;AAG3B,iBAAe,iBAAiB,MAAM,UAAU;GAAE;GAAS;GAAS;GAAM,CAAC;AAE3E,eAAa;AACX,kBAAe,oBAAoB,MAAM,UAAU,EAAE,SAAS,CAAC;;IAEhE;EAAC;EAAM;EAAQ;EAAS;EAAS;EAAM;EAAQ,CAAC"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
//#region src/hooks/use-file-drop.d.ts
|
|
2
|
+
interface Options {
|
|
3
|
+
onDrop?: (files: File[]) => void;
|
|
4
|
+
accept?: string;
|
|
5
|
+
multiple?: boolean;
|
|
6
|
+
disabled?: boolean;
|
|
7
|
+
}
|
|
8
|
+
declare const useFileDrop: <T extends HTMLElement = HTMLElement>(options?: Options) => {
|
|
9
|
+
dropRef: (node: T | null) => void;
|
|
10
|
+
isDragging: boolean;
|
|
11
|
+
};
|
|
12
|
+
//#endregion
|
|
13
|
+
export { useFileDrop };
|
|
14
|
+
//# sourceMappingURL=use-file-drop.d.mts.map
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
|
+
|
|
3
|
+
//#region src/hooks/use-file-drop.ts
|
|
4
|
+
const matchesAccept = (file, accept) => {
|
|
5
|
+
if (!accept) return true;
|
|
6
|
+
const patterns = accept.split(",").map((pattern) => pattern.trim()).filter(Boolean);
|
|
7
|
+
if (patterns.length === 0) return true;
|
|
8
|
+
return patterns.some((pattern) => {
|
|
9
|
+
if (pattern.startsWith(".")) return file.name.toLowerCase().endsWith(pattern.toLowerCase());
|
|
10
|
+
if (pattern.endsWith("/*")) return file.type.startsWith(pattern.slice(0, -1));
|
|
11
|
+
return file.type === pattern;
|
|
12
|
+
});
|
|
13
|
+
};
|
|
14
|
+
const useFileDrop = (options = {}) => {
|
|
15
|
+
const { onDrop, accept, multiple = true, disabled = false } = options;
|
|
16
|
+
const [isDragging, setIsDragging] = useState(false);
|
|
17
|
+
const dragCounterRef = useRef(0);
|
|
18
|
+
const onDropRef = useRef(onDrop);
|
|
19
|
+
const acceptRef = useRef(accept);
|
|
20
|
+
const multipleRef = useRef(multiple);
|
|
21
|
+
const disabledRef = useRef(disabled);
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
onDropRef.current = onDrop;
|
|
24
|
+
acceptRef.current = accept;
|
|
25
|
+
multipleRef.current = multiple;
|
|
26
|
+
disabledRef.current = disabled;
|
|
27
|
+
});
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
if (disabled) {
|
|
30
|
+
dragCounterRef.current = 0;
|
|
31
|
+
setIsDragging(false);
|
|
32
|
+
}
|
|
33
|
+
}, [disabled]);
|
|
34
|
+
const cleanupRef = useRef(null);
|
|
35
|
+
return {
|
|
36
|
+
dropRef: useCallback((node) => {
|
|
37
|
+
cleanupRef.current?.();
|
|
38
|
+
cleanupRef.current = null;
|
|
39
|
+
if (!node) return;
|
|
40
|
+
const onDragEnter = (event) => {
|
|
41
|
+
if (disabledRef.current) return;
|
|
42
|
+
event.preventDefault();
|
|
43
|
+
dragCounterRef.current += 1;
|
|
44
|
+
if (dragCounterRef.current === 1) setIsDragging(true);
|
|
45
|
+
};
|
|
46
|
+
const onDragLeave = (event) => {
|
|
47
|
+
if (disabledRef.current) return;
|
|
48
|
+
event.preventDefault();
|
|
49
|
+
dragCounterRef.current = Math.max(0, dragCounterRef.current - 1);
|
|
50
|
+
if (dragCounterRef.current === 0) setIsDragging(false);
|
|
51
|
+
};
|
|
52
|
+
const onDragOver = (event) => {
|
|
53
|
+
if (disabledRef.current) return;
|
|
54
|
+
event.preventDefault();
|
|
55
|
+
};
|
|
56
|
+
const onDrop = (event) => {
|
|
57
|
+
if (disabledRef.current) return;
|
|
58
|
+
event.preventDefault();
|
|
59
|
+
dragCounterRef.current = 0;
|
|
60
|
+
setIsDragging(false);
|
|
61
|
+
const fileList = event.dataTransfer?.files;
|
|
62
|
+
if (!fileList || fileList.length === 0) return;
|
|
63
|
+
let files = Array.from(fileList).filter((file) => matchesAccept(file, acceptRef.current));
|
|
64
|
+
if (!multipleRef.current) files = files.slice(0, 1);
|
|
65
|
+
if (files.length > 0) onDropRef.current?.(files);
|
|
66
|
+
};
|
|
67
|
+
node.addEventListener("dragenter", onDragEnter);
|
|
68
|
+
node.addEventListener("dragleave", onDragLeave);
|
|
69
|
+
node.addEventListener("dragover", onDragOver);
|
|
70
|
+
node.addEventListener("drop", onDrop);
|
|
71
|
+
cleanupRef.current = () => {
|
|
72
|
+
node.removeEventListener("dragenter", onDragEnter);
|
|
73
|
+
node.removeEventListener("dragleave", onDragLeave);
|
|
74
|
+
node.removeEventListener("dragover", onDragOver);
|
|
75
|
+
node.removeEventListener("drop", onDrop);
|
|
76
|
+
dragCounterRef.current = 0;
|
|
77
|
+
};
|
|
78
|
+
}, []),
|
|
79
|
+
isDragging
|
|
80
|
+
};
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
//#endregion
|
|
84
|
+
export { useFileDrop as default };
|
|
85
|
+
//# sourceMappingURL=use-file-drop.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-file-drop.mjs","names":[],"sources":["../../src/hooks/use-file-drop.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from 'react';\n\ninterface Options {\n onDrop?: (files: File[]) => void;\n // Comma-separated list, same format as the native <input accept>\n // attribute: extensions ('.png'), MIME types ('image/png'), or\n // wildcard subtypes ('image/*'). Unset accepts everything.\n accept?: string;\n multiple?: boolean;\n disabled?: boolean;\n}\n\nconst matchesAccept = (file: File, accept?: string): boolean => {\n if (!accept) {\n return true;\n }\n\n const patterns = accept\n .split(',')\n .map(pattern => pattern.trim())\n .filter(Boolean);\n\n if (patterns.length === 0) {\n return true;\n }\n\n return patterns.some(pattern => {\n if (pattern.startsWith('.')) {\n return file.name.toLowerCase().endsWith(pattern.toLowerCase());\n }\n\n if (pattern.endsWith('/*')) {\n return file.type.startsWith(pattern.slice(0, -1));\n }\n\n return file.type === pattern;\n });\n};\n\n// Pairs with useFileToDataUrl to cover a drag-and-drop upload area\n// end to end: this gets from \"user dropped something\" to a filtered\n// File[], useFileToDataUrl takes it from there to a data URL.\nconst useFileDrop = <T extends HTMLElement = HTMLElement>(\n options: Options = {},\n) => {\n const { onDrop, accept, multiple = true, disabled = false } = options;\n\n const [isDragging, setIsDragging] = useState(false);\n // `dragleave` also fires when the pointer moves onto a child element\n // (which re-fires `dragenter` on the way back out), not just when it\n // truly leaves the drop target — an enter/leave counter (rather than a\n // plain boolean) is what avoids `isDragging` flickering off and back on\n // as the pointer crosses child element boundaries.\n const dragCounterRef = useRef(0);\n\n const onDropRef = useRef(onDrop);\n const acceptRef = useRef(accept);\n const multipleRef = useRef(multiple);\n const disabledRef = useRef(disabled);\n\n useEffect(() => {\n onDropRef.current = onDrop;\n acceptRef.current = accept;\n multipleRef.current = multiple;\n disabledRef.current = disabled;\n });\n\n useEffect(() => {\n if (disabled) {\n dragCounterRef.current = 0;\n setIsDragging(false);\n }\n }, [disabled]);\n\n const cleanupRef = useRef<(() => void) | null>(null);\n\n const dropRef = useCallback((node: T | null) => {\n cleanupRef.current?.();\n cleanupRef.current = null;\n\n if (!node) {\n return;\n }\n\n const onDragEnter = (event: DragEvent) => {\n if (disabledRef.current) {\n return;\n }\n event.preventDefault();\n dragCounterRef.current += 1;\n if (dragCounterRef.current === 1) {\n setIsDragging(true);\n }\n };\n\n const onDragLeave = (event: DragEvent) => {\n if (disabledRef.current) {\n return;\n }\n event.preventDefault();\n dragCounterRef.current = Math.max(0, dragCounterRef.current - 1);\n if (dragCounterRef.current === 0) {\n setIsDragging(false);\n }\n };\n\n // `preventDefault` here is what tells the browser this is a valid\n // drop target at all — without it, `drop` never fires.\n const onDragOver = (event: DragEvent) => {\n if (disabledRef.current) {\n return;\n }\n event.preventDefault();\n };\n\n const onDrop = (event: DragEvent) => {\n if (disabledRef.current) {\n return;\n }\n event.preventDefault();\n dragCounterRef.current = 0;\n setIsDragging(false);\n\n const fileList = event.dataTransfer?.files;\n\n if (!fileList || fileList.length === 0) {\n return;\n }\n\n let files = Array.from(fileList).filter(file =>\n matchesAccept(file, acceptRef.current),\n );\n\n if (!multipleRef.current) {\n files = files.slice(0, 1);\n }\n\n if (files.length > 0) {\n onDropRef.current?.(files);\n }\n };\n\n node.addEventListener('dragenter', onDragEnter);\n node.addEventListener('dragleave', onDragLeave);\n node.addEventListener('dragover', onDragOver);\n node.addEventListener('drop', onDrop);\n\n cleanupRef.current = () => {\n node.removeEventListener('dragenter', onDragEnter);\n node.removeEventListener('dragleave', onDragLeave);\n node.removeEventListener('dragover', onDragOver);\n node.removeEventListener('drop', onDrop);\n dragCounterRef.current = 0;\n };\n }, []);\n\n return { dropRef, isDragging };\n};\n\nexport default useFileDrop;\n"],"mappings":";;;AAYA,MAAM,iBAAiB,MAAY,WAA6B;AAC9D,KAAI,CAAC,OACH,QAAO;CAGT,MAAM,WAAW,OACd,MAAM,IAAI,CACV,KAAI,YAAW,QAAQ,MAAM,CAAC,CAC9B,OAAO,QAAQ;AAElB,KAAI,SAAS,WAAW,EACtB,QAAO;AAGT,QAAO,SAAS,MAAK,YAAW;AAC9B,MAAI,QAAQ,WAAW,IAAI,CACzB,QAAO,KAAK,KAAK,aAAa,CAAC,SAAS,QAAQ,aAAa,CAAC;AAGhE,MAAI,QAAQ,SAAS,KAAK,CACxB,QAAO,KAAK,KAAK,WAAW,QAAQ,MAAM,GAAG,GAAG,CAAC;AAGnD,SAAO,KAAK,SAAS;GACrB;;AAMJ,MAAM,eACJ,UAAmB,EAAE,KAClB;CACH,MAAM,EAAE,QAAQ,QAAQ,WAAW,MAAM,WAAW,UAAU;CAE9D,MAAM,CAAC,YAAY,iBAAiB,SAAS,MAAM;CAMnD,MAAM,iBAAiB,OAAO,EAAE;CAEhC,MAAM,YAAY,OAAO,OAAO;CAChC,MAAM,YAAY,OAAO,OAAO;CAChC,MAAM,cAAc,OAAO,SAAS;CACpC,MAAM,cAAc,OAAO,SAAS;AAEpC,iBAAgB;AACd,YAAU,UAAU;AACpB,YAAU,UAAU;AACpB,cAAY,UAAU;AACtB,cAAY,UAAU;GACtB;AAEF,iBAAgB;AACd,MAAI,UAAU;AACZ,kBAAe,UAAU;AACzB,iBAAc,MAAM;;IAErB,CAAC,SAAS,CAAC;CAEd,MAAM,aAAa,OAA4B,KAAK;AAkFpD,QAAO;EAAE,SAhFO,aAAa,SAAmB;AAC9C,cAAW,WAAW;AACtB,cAAW,UAAU;AAErB,OAAI,CAAC,KACH;GAGF,MAAM,eAAe,UAAqB;AACxC,QAAI,YAAY,QACd;AAEF,UAAM,gBAAgB;AACtB,mBAAe,WAAW;AAC1B,QAAI,eAAe,YAAY,EAC7B,eAAc,KAAK;;GAIvB,MAAM,eAAe,UAAqB;AACxC,QAAI,YAAY,QACd;AAEF,UAAM,gBAAgB;AACtB,mBAAe,UAAU,KAAK,IAAI,GAAG,eAAe,UAAU,EAAE;AAChE,QAAI,eAAe,YAAY,EAC7B,eAAc,MAAM;;GAMxB,MAAM,cAAc,UAAqB;AACvC,QAAI,YAAY,QACd;AAEF,UAAM,gBAAgB;;GAGxB,MAAM,UAAU,UAAqB;AACnC,QAAI,YAAY,QACd;AAEF,UAAM,gBAAgB;AACtB,mBAAe,UAAU;AACzB,kBAAc,MAAM;IAEpB,MAAM,WAAW,MAAM,cAAc;AAErC,QAAI,CAAC,YAAY,SAAS,WAAW,EACnC;IAGF,IAAI,QAAQ,MAAM,KAAK,SAAS,CAAC,QAAO,SACtC,cAAc,MAAM,UAAU,QAAQ,CACvC;AAED,QAAI,CAAC,YAAY,QACf,SAAQ,MAAM,MAAM,GAAG,EAAE;AAG3B,QAAI,MAAM,SAAS,EACjB,WAAU,UAAU,MAAM;;AAI9B,QAAK,iBAAiB,aAAa,YAAY;AAC/C,QAAK,iBAAiB,aAAa,YAAY;AAC/C,QAAK,iBAAiB,YAAY,WAAW;AAC7C,QAAK,iBAAiB,QAAQ,OAAO;AAErC,cAAW,gBAAgB;AACzB,SAAK,oBAAoB,aAAa,YAAY;AAClD,SAAK,oBAAoB,aAAa,YAAY;AAClD,SAAK,oBAAoB,YAAY,WAAW;AAChD,SAAK,oBAAoB,QAAQ,OAAO;AACxC,mBAAe,UAAU;;KAE1B,EAAE,CAAC;EAEY;EAAY"}
|
|
@@ -5,9 +5,10 @@ interface Options {
|
|
|
5
5
|
}
|
|
6
6
|
declare const useImage: (src: string, options?: Options) => {
|
|
7
7
|
loading: boolean;
|
|
8
|
-
error:
|
|
8
|
+
error: Error | null;
|
|
9
9
|
loaded: boolean;
|
|
10
10
|
retry: () => void;
|
|
11
|
+
attemptCount: number;
|
|
11
12
|
};
|
|
12
13
|
//#endregion
|
|
13
14
|
export { useImage };
|
package/dist/hooks/use-image.mjs
CHANGED
|
@@ -26,11 +26,11 @@ const useImage = (src, options = {}) => {
|
|
|
26
26
|
setLoaded(true);
|
|
27
27
|
setError(null);
|
|
28
28
|
};
|
|
29
|
-
img.onerror = (
|
|
29
|
+
img.onerror = (event) => {
|
|
30
30
|
if (cancelled) return;
|
|
31
31
|
setLoading(false);
|
|
32
32
|
setLoaded(false);
|
|
33
|
-
setError(
|
|
33
|
+
setError(new Error(`Failed to load image: ${src}`, { cause: event }));
|
|
34
34
|
if (attemptCount < retryCount) retryTimeoutId = setTimeout(() => {
|
|
35
35
|
if (!cancelled) setAttemptCount((prev) => prev + 1);
|
|
36
36
|
}, retryDelay);
|
|
@@ -55,7 +55,8 @@ const useImage = (src, options = {}) => {
|
|
|
55
55
|
retry: useCallback(() => {
|
|
56
56
|
setAttemptCount(0);
|
|
57
57
|
setReloadToken((token) => token + 1);
|
|
58
|
-
}, [])
|
|
58
|
+
}, []),
|
|
59
|
+
attemptCount
|
|
59
60
|
};
|
|
60
61
|
};
|
|
61
62
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-image.mjs","names":[],"sources":["../../src/hooks/use-image.ts"],"sourcesContent":["import { useCallback, useEffect, useState } from 'react';\n\ninterface Options {\n retryCount?: number;\n retryDelay?: number;\n}\n\nconst useImage = (src: string, options: Options = {}) => {\n const { retryCount = 0, retryDelay = 1000 } = options;\n\n const [loading, setLoading] = useState(true);\n const [error, setError] = useState<
|
|
1
|
+
{"version":3,"file":"use-image.mjs","names":[],"sources":["../../src/hooks/use-image.ts"],"sourcesContent":["import { useCallback, useEffect, useState } from 'react';\n\ninterface Options {\n retryCount?: number;\n retryDelay?: number;\n}\n\nconst useImage = (src: string, options: Options = {}) => {\n const { retryCount = 0, retryDelay = 1000 } = options;\n\n const [loading, setLoading] = useState(true);\n const [error, setError] = useState<Error | null>(null);\n const [loaded, setLoaded] = useState(false);\n const [attemptCount, setAttemptCount] = useState(0);\n const [reloadToken, setReloadToken] = useState(0);\n\n useEffect(() => {\n if (!src) {\n setLoading(false);\n setLoaded(false);\n return;\n }\n\n let cancelled = false;\n let retryTimeoutId: ReturnType<typeof setTimeout> | undefined;\n\n setLoading(true);\n setError(null);\n\n const img = new Image();\n img.src = src;\n\n img.onload = () => {\n if (cancelled) {\n return;\n }\n setLoading(false);\n setLoaded(true);\n setError(null);\n };\n\n img.onerror = event => {\n if (cancelled) {\n return;\n }\n setLoading(false);\n setLoaded(false);\n // `img.onerror`'s handler type is `OnErrorEventHandler`, so `event`\n // is typed `Event | string` even though the browser only ever\n // passes an `Event` here — that `Event` carries no useful failure\n // reason itself, so it's kept as `cause` rather than surfaced\n // directly as `error`.\n setError(new Error(`Failed to load image: ${src}`, { cause: event }));\n\n if (attemptCount < retryCount) {\n retryTimeoutId = setTimeout(() => {\n if (!cancelled) {\n setAttemptCount(prev => prev + 1);\n }\n }, retryDelay);\n }\n };\n\n return () => {\n cancelled = true;\n img.onload = null;\n img.onerror = null;\n if (retryTimeoutId) {\n clearTimeout(retryTimeoutId);\n }\n };\n }, [src, attemptCount, retryCount, retryDelay, reloadToken]);\n\n const retry = useCallback(() => {\n setAttemptCount(0);\n setReloadToken(token => token + 1);\n }, []);\n\n return {\n loading,\n error,\n loaded,\n retry,\n attemptCount,\n };\n};\n\nexport default useImage;\n"],"mappings":";;;AAOA,MAAM,YAAY,KAAa,UAAmB,EAAE,KAAK;CACvD,MAAM,EAAE,aAAa,GAAG,aAAa,QAAS;CAE9C,MAAM,CAAC,SAAS,cAAc,SAAS,KAAK;CAC5C,MAAM,CAAC,OAAO,YAAY,SAAuB,KAAK;CACtD,MAAM,CAAC,QAAQ,aAAa,SAAS,MAAM;CAC3C,MAAM,CAAC,cAAc,mBAAmB,SAAS,EAAE;CACnD,MAAM,CAAC,aAAa,kBAAkB,SAAS,EAAE;AAEjD,iBAAgB;AACd,MAAI,CAAC,KAAK;AACR,cAAW,MAAM;AACjB,aAAU,MAAM;AAChB;;EAGF,IAAI,YAAY;EAChB,IAAI;AAEJ,aAAW,KAAK;AAChB,WAAS,KAAK;EAEd,MAAM,MAAM,IAAI,OAAO;AACvB,MAAI,MAAM;AAEV,MAAI,eAAe;AACjB,OAAI,UACF;AAEF,cAAW,MAAM;AACjB,aAAU,KAAK;AACf,YAAS,KAAK;;AAGhB,MAAI,WAAU,UAAS;AACrB,OAAI,UACF;AAEF,cAAW,MAAM;AACjB,aAAU,MAAM;AAMhB,YAAS,IAAI,MAAM,yBAAyB,OAAO,EAAE,OAAO,OAAO,CAAC,CAAC;AAErE,OAAI,eAAe,WACjB,kBAAiB,iBAAiB;AAChC,QAAI,CAAC,UACH,kBAAgB,SAAQ,OAAO,EAAE;MAElC,WAAW;;AAIlB,eAAa;AACX,eAAY;AACZ,OAAI,SAAS;AACb,OAAI,UAAU;AACd,OAAI,eACF,cAAa,eAAe;;IAG/B;EAAC;EAAK;EAAc;EAAY;EAAY;EAAY,CAAC;AAO5D,QAAO;EACL;EACA;EACA;EACA,OATY,kBAAkB;AAC9B,mBAAgB,EAAE;AAClB,mBAAe,UAAS,QAAQ,EAAE;KACjC,EAAE,CAAC;EAOJ;EACD"}
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
//#region src/hooks/use-intersection-observer.d.ts
|
|
2
|
-
|
|
2
|
+
interface Options extends IntersectionObserverInit {
|
|
3
|
+
freezeOnceVisible?: boolean;
|
|
4
|
+
}
|
|
5
|
+
interface Result {
|
|
6
|
+
entry: IntersectionObserverEntry | null;
|
|
7
|
+
isIntersecting: boolean;
|
|
8
|
+
}
|
|
9
|
+
declare const useIntersectionObserver: <T extends Element = Element>(options?: Options) => [(node: T | null) => void, Result];
|
|
3
10
|
//#endregion
|
|
4
11
|
export { useIntersectionObserver };
|
|
5
12
|
//# sourceMappingURL=use-intersection-observer.d.mts.map
|
|
@@ -1,21 +1,53 @@
|
|
|
1
|
-
import { useCallback, useRef, useState } from "react";
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
2
|
|
|
3
3
|
//#region src/hooks/use-intersection-observer.ts
|
|
4
4
|
const useIntersectionObserver = (options) => {
|
|
5
|
+
const { freezeOnceVisible = false, ...observerInit } = options ?? {};
|
|
5
6
|
const [entry, setEntry] = useState(null);
|
|
6
7
|
const observerRef = useRef(null);
|
|
7
|
-
|
|
8
|
+
const nodeRef = useRef(null);
|
|
9
|
+
const frozenRef = useRef(false);
|
|
10
|
+
const observerInitKey = JSON.stringify(observerInit);
|
|
11
|
+
const observerInitRef = useRef(observerInit);
|
|
12
|
+
const freezeRef = useRef(freezeOnceVisible);
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
observerInitRef.current = observerInit;
|
|
15
|
+
freezeRef.current = freezeOnceVisible;
|
|
16
|
+
});
|
|
17
|
+
const connect = useCallback(() => {
|
|
8
18
|
if (observerRef.current) {
|
|
9
19
|
observerRef.current.disconnect();
|
|
10
20
|
observerRef.current = null;
|
|
11
21
|
}
|
|
12
|
-
|
|
22
|
+
const node = nodeRef.current;
|
|
23
|
+
if (!node || typeof IntersectionObserver === "undefined" || frozenRef.current) return;
|
|
13
24
|
const observer = new IntersectionObserver(([observedEntry]) => {
|
|
25
|
+
if (!observedEntry) return;
|
|
14
26
|
setEntry(observedEntry);
|
|
15
|
-
|
|
27
|
+
if (observedEntry.isIntersecting && freezeRef.current) {
|
|
28
|
+
frozenRef.current = true;
|
|
29
|
+
observer.disconnect();
|
|
30
|
+
}
|
|
31
|
+
}, observerInitRef.current);
|
|
16
32
|
observer.observe(node);
|
|
17
33
|
observerRef.current = observer;
|
|
18
|
-
}, [])
|
|
34
|
+
}, []);
|
|
35
|
+
const ref = useCallback((node) => {
|
|
36
|
+
nodeRef.current = node;
|
|
37
|
+
connect();
|
|
38
|
+
}, []);
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
connect();
|
|
41
|
+
}, [observerInitKey]);
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
return () => {
|
|
44
|
+
observerRef.current?.disconnect();
|
|
45
|
+
};
|
|
46
|
+
}, []);
|
|
47
|
+
return [ref, {
|
|
48
|
+
entry,
|
|
49
|
+
isIntersecting: entry?.isIntersecting ?? false
|
|
50
|
+
}];
|
|
19
51
|
};
|
|
20
52
|
|
|
21
53
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-intersection-observer.mjs","names":[],"sources":["../../src/hooks/use-intersection-observer.ts"],"sourcesContent":["import { useCallback, useRef, useState } from 'react';\n\nconst useIntersectionObserver = <T extends Element = Element>(\n options?:
|
|
1
|
+
{"version":3,"file":"use-intersection-observer.mjs","names":[],"sources":["../../src/hooks/use-intersection-observer.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from 'react';\n\ninterface Options extends IntersectionObserverInit {\n // Disconnects for good the first time the target becomes intersecting\n // — the common \"seen once, that's enough\" case (lazy loading, entrance\n // animations, infinite-scroll triggers) that otherwise needs the\n // consumer to track their own \"already fired\" flag.\n freezeOnceVisible?: boolean;\n}\n\ninterface Result {\n entry: IntersectionObserverEntry | null;\n isIntersecting: boolean;\n}\n\nconst useIntersectionObserver = <T extends Element = Element>(\n options?: Options,\n): [(node: T | null) => void, Result] => {\n const { freezeOnceVisible = false, ...observerInit } = options ?? {};\n\n const [entry, setEntry] = useState<IntersectionObserverEntry | null>(null);\n const observerRef = useRef<IntersectionObserver | null>(null);\n const nodeRef = useRef<T | null>(null);\n const frozenRef = useRef(false);\n\n // `options` is typically a fresh object literal at the call site —\n // serialize the IntersectionObserverInit part into a stable key so\n // reconnecting only happens when it actually changes, not on every\n // render (the previous version didn't reconnect on options changes at\n // all, so threshold/rootMargin could never be updated at runtime).\n const observerInitKey = JSON.stringify(observerInit);\n const observerInitRef = useRef(observerInit);\n const freezeRef = useRef(freezeOnceVisible);\n\n useEffect(() => {\n observerInitRef.current = observerInit;\n freezeRef.current = freezeOnceVisible;\n });\n\n const connect = useCallback(() => {\n if (observerRef.current) {\n observerRef.current.disconnect();\n observerRef.current = null;\n }\n\n const node = nodeRef.current;\n\n if (\n !node ||\n typeof IntersectionObserver === 'undefined' ||\n frozenRef.current\n ) {\n return;\n }\n\n const observer = new IntersectionObserver(([observedEntry]) => {\n if (!observedEntry) {\n return;\n }\n\n setEntry(observedEntry);\n\n if (observedEntry.isIntersecting && freezeRef.current) {\n frozenRef.current = true;\n observer.disconnect();\n }\n }, observerInitRef.current);\n\n observer.observe(node);\n observerRef.current = observer;\n }, []);\n\n const ref = useCallback((node: T | null) => {\n nodeRef.current = node;\n connect();\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n useEffect(() => {\n connect();\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [observerInitKey]);\n\n useEffect(() => {\n return () => {\n observerRef.current?.disconnect();\n };\n }, []);\n\n return [ref, { entry, isIntersecting: entry?.isIntersecting ?? false }];\n};\n\nexport default useIntersectionObserver;\n"],"mappings":";;;AAeA,MAAM,2BACJ,YACuC;CACvC,MAAM,EAAE,oBAAoB,OAAO,GAAG,iBAAiB,WAAW,EAAE;CAEpE,MAAM,CAAC,OAAO,YAAY,SAA2C,KAAK;CAC1E,MAAM,cAAc,OAAoC,KAAK;CAC7D,MAAM,UAAU,OAAiB,KAAK;CACtC,MAAM,YAAY,OAAO,MAAM;CAO/B,MAAM,kBAAkB,KAAK,UAAU,aAAa;CACpD,MAAM,kBAAkB,OAAO,aAAa;CAC5C,MAAM,YAAY,OAAO,kBAAkB;AAE3C,iBAAgB;AACd,kBAAgB,UAAU;AAC1B,YAAU,UAAU;GACpB;CAEF,MAAM,UAAU,kBAAkB;AAChC,MAAI,YAAY,SAAS;AACvB,eAAY,QAAQ,YAAY;AAChC,eAAY,UAAU;;EAGxB,MAAM,OAAO,QAAQ;AAErB,MACE,CAAC,QACD,OAAO,yBAAyB,eAChC,UAAU,QAEV;EAGF,MAAM,WAAW,IAAI,sBAAsB,CAAC,mBAAmB;AAC7D,OAAI,CAAC,cACH;AAGF,YAAS,cAAc;AAEvB,OAAI,cAAc,kBAAkB,UAAU,SAAS;AACrD,cAAU,UAAU;AACpB,aAAS,YAAY;;KAEtB,gBAAgB,QAAQ;AAE3B,WAAS,QAAQ,KAAK;AACtB,cAAY,UAAU;IACrB,EAAE,CAAC;CAEN,MAAM,MAAM,aAAa,SAAmB;AAC1C,UAAQ,UAAU;AAClB,WAAS;IAER,EAAE,CAAC;AAEN,iBAAgB;AACd,WAAS;IAER,CAAC,gBAAgB,CAAC;AAErB,iBAAgB;AACd,eAAa;AACX,eAAY,SAAS,YAAY;;IAElC,EAAE,CAAC;AAEN,QAAO,CAAC,KAAK;EAAE;EAAO,gBAAgB,OAAO,kBAAkB;EAAO,CAAC"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { useEffect, useRef } from "react";
|
|
2
|
+
|
|
3
|
+
//#region src/hooks/use-interval.ts
|
|
4
|
+
const useInterval = (callback, delay) => {
|
|
5
|
+
const callbackRef = useRef(callback);
|
|
6
|
+
useEffect(() => {
|
|
7
|
+
callbackRef.current = callback;
|
|
8
|
+
});
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
if (delay === null) return;
|
|
11
|
+
const id = setInterval(() => {
|
|
12
|
+
callbackRef.current();
|
|
13
|
+
}, delay);
|
|
14
|
+
return () => clearInterval(id);
|
|
15
|
+
}, [delay]);
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
//#endregion
|
|
19
|
+
export { useInterval as default };
|
|
20
|
+
//# sourceMappingURL=use-interval.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-interval.mjs","names":[],"sources":["../../src/hooks/use-interval.ts"],"sourcesContent":["import { useEffect, useRef } from 'react';\n\n// Dan Abramov's useInterval pattern: the callback lives in a ref so a\n// fresh function every render doesn't reset the interval — only `delay`\n// actually changing does that. `delay === null` pauses it (same rule as\n// useTimeout/useRecursiveTimeout); toggling back to a number resumes on\n// a fresh interval rather than trying to pick up mid-tick.\nconst useInterval = (callback: () => void, delay: number | null) => {\n const callbackRef = useRef(callback);\n\n useEffect(() => {\n callbackRef.current = callback;\n });\n\n useEffect(() => {\n if (delay === null) {\n return;\n }\n\n const id = setInterval(() => {\n callbackRef.current();\n }, delay);\n\n return () => clearInterval(id);\n }, [delay]);\n};\n\nexport default useInterval;\n"],"mappings":";;;AAOA,MAAM,eAAe,UAAsB,UAAyB;CAClE,MAAM,cAAc,OAAO,SAAS;AAEpC,iBAAgB;AACd,cAAY,UAAU;GACtB;AAEF,iBAAgB;AACd,MAAI,UAAU,KACZ;EAGF,MAAM,KAAK,kBAAkB;AAC3B,eAAY,SAAS;KACpB,MAAM;AAET,eAAa,cAAc,GAAG;IAC7B,CAAC,MAAM,CAAC"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { RefObject } from "react";
|
|
2
|
+
|
|
3
|
+
//#region src/hooks/use-key-press.d.ts
|
|
4
|
+
type KeyPressTarget = RefObject<HTMLElement | null> | Window | Document | HTMLElement | null | undefined;
|
|
5
|
+
interface Options {
|
|
6
|
+
target?: KeyPressTarget;
|
|
7
|
+
enabled?: boolean;
|
|
8
|
+
preventDefault?: boolean;
|
|
9
|
+
ignore?: string;
|
|
10
|
+
}
|
|
11
|
+
declare const useKeyPress: (combo: string | string[], handler: (event: KeyboardEvent) => void, options?: Options) => void;
|
|
12
|
+
//#endregion
|
|
13
|
+
export { useKeyPress };
|
|
14
|
+
//# sourceMappingURL=use-key-press.d.mts.map
|