@jbpark/use-hooks 2.0.2 → 2.1.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/README.ko.md +18 -10
- package/README.md +16 -10
- package/dist/hooks/index.d.mts +12 -0
- package/dist/hooks/index.mjs +14 -0
- package/dist/hooks/useBodyScrollLock/index.d.mts +5 -0
- package/dist/hooks/useBodyScrollLock/index.mjs +115 -0
- package/dist/hooks/useBodyScrollLock/index.mjs.map +1 -0
- package/dist/hooks/useDebounce/index.d.mts +12 -0
- package/dist/hooks/useDebounce/index.mjs +41 -0
- package/dist/hooks/useDebounce/index.mjs.map +1 -0
- package/dist/hooks/useElementPosition/index.d.mts +8 -0
- package/dist/hooks/useElementPosition/index.mjs +32 -0
- package/dist/hooks/useElementPosition/index.mjs.map +1 -0
- package/dist/hooks/useElementScroll/index.d.mts +15 -0
- package/dist/hooks/useElementScroll/index.mjs +68 -0
- package/dist/hooks/useElementScroll/index.mjs.map +1 -0
- package/dist/hooks/useImage/index.d.mts +14 -0
- package/dist/hooks/useImage/index.mjs +56 -0
- package/dist/hooks/useImage/index.mjs.map +1 -0
- package/dist/hooks/useLocalStorage/index.d.mts +5 -0
- package/dist/hooks/useLocalStorage/index.mjs +40 -0
- package/dist/hooks/useLocalStorage/index.mjs.map +1 -0
- package/dist/hooks/useRecursiveTimeout/index.d.mts +5 -0
- package/dist/hooks/useRecursiveTimeout/index.mjs +27 -0
- package/dist/hooks/useRecursiveTimeout/index.mjs.map +1 -0
- package/dist/hooks/useResponsiveSize/index.d.mts +26 -0
- package/dist/hooks/useResponsiveSize/index.mjs +108 -0
- package/dist/hooks/useResponsiveSize/index.mjs.map +1 -0
- package/dist/hooks/useScrollToElements/index.d.mts +14 -0
- package/dist/hooks/useScrollToElements/index.mjs +34 -0
- package/dist/hooks/useScrollToElements/index.mjs.map +1 -0
- package/dist/hooks/useThrottle/index.d.mts +5 -0
- package/dist/hooks/useThrottle/index.mjs +42 -0
- package/dist/hooks/useThrottle/index.mjs.map +1 -0
- package/dist/hooks/useViewport/index.d.mts +18 -0
- package/dist/hooks/useViewport/index.mjs +87 -0
- package/dist/hooks/useViewport/index.mjs.map +1 -0
- package/dist/hooks/useWindowScroll/index.d.mts +12 -0
- package/dist/hooks/useWindowScroll/index.mjs +60 -0
- package/dist/hooks/useWindowScroll/index.mjs.map +1 -0
- package/dist/index.d.mts +14 -0
- package/dist/index.mjs +15 -0
- package/package.json +7 -8
- package/dist/index.cjs +0 -1
- package/dist/index.d.ts +0 -106
- package/dist/index.js +0 -424
- package/dist/vite.svg +0 -1
package/README.ko.md
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
## 기능
|
|
13
13
|
|
|
14
|
-
- 📦 **
|
|
14
|
+
- 📦 **12개 프로덕션 레디 훅** - 스크롤, 뷰포트, 스토리지 등 다양한 유틸리티
|
|
15
15
|
- 🎯 **TypeScript 지원** - 완전한 타입 지원으로 더 나은 개발 경험
|
|
16
16
|
- ⚡ **트리 셰이킹 지원** - 필요한 것만 임포트하세요
|
|
17
17
|
- 🔒 **SSR 안전** - window/document 전역 변수에 대한 보호
|
|
@@ -34,8 +34,9 @@ pnpm add @jbpark/use-hooks
|
|
|
34
34
|
|
|
35
35
|
```tsx
|
|
36
36
|
import {
|
|
37
|
-
useElementSize,
|
|
38
37
|
useLocalStorage,
|
|
38
|
+
useResponsiveSize,
|
|
39
|
+
useThrottle,
|
|
39
40
|
useWindowScroll,
|
|
40
41
|
} from '@jbpark/use-hooks';
|
|
41
42
|
|
|
@@ -47,13 +48,17 @@ function MyComponent() {
|
|
|
47
48
|
const { y, percent } = useWindowScroll();
|
|
48
49
|
|
|
49
50
|
// 브레이크포인트를 포함한 요소 크기 모니터링
|
|
50
|
-
const { size, breakpoint, ref } =
|
|
51
|
+
const { size, breakpoint, ref } = useResponsiveSize();
|
|
52
|
+
|
|
53
|
+
// 너비 업데이트를 스로틀링
|
|
54
|
+
const throttledWidth = useThrottle(size.width, 200);
|
|
51
55
|
|
|
52
56
|
return (
|
|
53
57
|
<div ref={ref}>
|
|
54
58
|
<p>Count: {count}</p>
|
|
55
59
|
<p>Scroll: {percent.y}%</p>
|
|
56
60
|
<p>Breakpoint: {breakpoint.current}</p>
|
|
61
|
+
<p>Throttled width: {throttledWidth}</p>
|
|
57
62
|
<button onClick={() => setCount(count + 1)}>증가</button>
|
|
58
63
|
</div>
|
|
59
64
|
);
|
|
@@ -66,14 +71,16 @@ function MyComponent() {
|
|
|
66
71
|
| --------------------- | --------------------------------------------------------------- |
|
|
67
72
|
| `useLocalStorage` | 에러 핸들링이 포함된 JSON 기반 영속 상태 (SSR 안전) |
|
|
68
73
|
| `useWindowScroll` | 윈도우 스크롤 위치 및 백분율 추적 (iOS visualViewport 대응) |
|
|
69
|
-
| `
|
|
74
|
+
| `useElementScroll` | ResizeObserver를 사용한 특정 요소의 스크롤 상태 추적 |
|
|
70
75
|
| `useElementPosition` | 스크롤/리사이즈 시 요소의 바운딩 렉트 모니터링 (요소 참조 지원) |
|
|
71
|
-
| `
|
|
76
|
+
| `useResponsiveSize` | Tailwind 유사 브레이크포인트를 포함한 요소 크기 추적 (debounce) |
|
|
72
77
|
| `useBodyScrollLock` | 스타일 보존을 포함한 바디 스크롤 잠금/해제 (iOS 특별 처리) |
|
|
73
78
|
| `useScrollToElements` | 인덱스별로 특정 요소로 스크롤 (오프셋 조절 가능) |
|
|
74
79
|
| `useImage` | 이미지 사전로드 및 로딩/에러 상태 노출 |
|
|
75
80
|
| `useRecursiveTimeout` | 비동기/동기 콜백을 재귀적으로 스케줄링 |
|
|
76
81
|
| `useViewport` | visualViewport 지원, 인앱 모드 옵션, debounce 포함 |
|
|
82
|
+
| `useDebounce` | 함수 실행을 지연해 과도한 업데이트를 방지 (autoInvoke 지원) |
|
|
83
|
+
| `useThrottle` | 값 업데이트를 일정 간격으로 제한 |
|
|
77
84
|
|
|
78
85
|
## 개발
|
|
79
86
|
|
|
@@ -100,6 +107,7 @@ pnpm exec prettier --write .
|
|
|
100
107
|
src/
|
|
101
108
|
├── hooks/ # 개별 훅 구현
|
|
102
109
|
│ ├── useBodyScrollLock/
|
|
110
|
+
│ ├── useDebounce/
|
|
103
111
|
│ ├── useElementPosition/
|
|
104
112
|
│ ├── useElementScroll/
|
|
105
113
|
│ ├── useImage/
|
|
@@ -107,12 +115,13 @@ src/
|
|
|
107
115
|
│ ├── useRecursiveTimeout/
|
|
108
116
|
│ ├── useResponsiveSize/
|
|
109
117
|
│ ├── useScrollToElements/
|
|
118
|
+
│ ├── useThrottle/
|
|
110
119
|
│ ├── useViewport/
|
|
111
120
|
│ ├── useWindowScroll/
|
|
112
121
|
│ └── index.ts # 배럴 익스포트
|
|
113
122
|
└── index.ts # 패키지 진입점
|
|
114
123
|
|
|
115
|
-
dist/ # 빌드된 라이브러리 (
|
|
124
|
+
dist/ # 빌드된 라이브러리 (ESM + types)
|
|
116
125
|
.changeset/ # 버저닝을 위한 Changesets
|
|
117
126
|
```
|
|
118
127
|
|
|
@@ -136,18 +145,17 @@ git push --follow-tags
|
|
|
136
145
|
|
|
137
146
|
라이브러리는 다음과 같이 빌드됩니다:
|
|
138
147
|
|
|
139
|
-
- **ES Module**: `dist/index.
|
|
140
|
-
- **CommonJS**: `dist/index.cjs`
|
|
148
|
+
- **ES Module**: `dist/index.mjs`
|
|
141
149
|
- **타입 정의**: `dist/index.d.ts`
|
|
142
150
|
|
|
143
151
|
## 주요 패턴
|
|
144
152
|
|
|
145
153
|
- **Window 보호**: `window`/`document`에 접근하는 훅은 SSR 안전성을 위해 `typeof window` 체크 (예: `useLocalStorage`)
|
|
146
154
|
- **이벤트 리스너**: 모든 스크롤/리사이즈 리스너는 가능한 한 passive 플래그 사용
|
|
147
|
-
- **ResizeObserver**: `
|
|
155
|
+
- **ResizeObserver**: `useResponsiveSize`와 `useElementPosition`에서 사용하여 성능 최적화
|
|
148
156
|
- **requestAnimationFrame**: 스크롤/리사이즈 콜백에서 레이아웃 스래싱 방지
|
|
149
157
|
- **iOS 대응**: `useBodyScrollLock`, `useWindowScroll`, `useViewport`에서 iOS의 visualViewport 특성 처리
|
|
150
|
-
- **Debounce**: `
|
|
158
|
+
- **Debounce**: `useResponsiveSize`와 `useViewport`에서 리사이즈 이벤트 디바운싱 지원
|
|
151
159
|
|
|
152
160
|
## 브라우저 지원
|
|
153
161
|
|
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ A collection of reusable React 19 hooks for common UI and interaction patterns.
|
|
|
11
11
|
|
|
12
12
|
## Features
|
|
13
13
|
|
|
14
|
-
- 📦 **
|
|
14
|
+
- 📦 **12 Production-Ready Hooks** - Utilities for scrolling, viewport, storage, and more
|
|
15
15
|
- 🎯 **Full TypeScript Support** - Complete type definitions for better development experience
|
|
16
16
|
- ⚡ **Tree-Shakeable** - Import only what you need
|
|
17
17
|
- 🔒 **SSR-Safe** - Built-in protection for window/document globals
|
|
@@ -34,8 +34,9 @@ pnpm add @jbpark/use-hooks
|
|
|
34
34
|
|
|
35
35
|
```tsx
|
|
36
36
|
import {
|
|
37
|
-
useElementSize,
|
|
38
37
|
useLocalStorage,
|
|
38
|
+
useResponsiveSize,
|
|
39
|
+
useThrottle,
|
|
39
40
|
useWindowScroll,
|
|
40
41
|
} from '@jbpark/use-hooks';
|
|
41
42
|
|
|
@@ -47,13 +48,17 @@ function MyComponent() {
|
|
|
47
48
|
const { y, percent } = useWindowScroll();
|
|
48
49
|
|
|
49
50
|
// Monitor element size with breakpoints
|
|
50
|
-
const { size, breakpoint, ref } =
|
|
51
|
+
const { size, breakpoint, ref } = useResponsiveSize();
|
|
52
|
+
|
|
53
|
+
// Throttled width update
|
|
54
|
+
const throttledWidth = useThrottle(size.width, 200);
|
|
51
55
|
|
|
52
56
|
return (
|
|
53
57
|
<div ref={ref}>
|
|
54
58
|
<p>Count: {count}</p>
|
|
55
59
|
<p>Scroll: {percent.y}%</p>
|
|
56
60
|
<p>Breakpoint: {breakpoint.current}</p>
|
|
61
|
+
<p>Throttled width: {throttledWidth}</p>
|
|
57
62
|
<button onClick={() => setCount(count + 1)}>Increment</button>
|
|
58
63
|
</div>
|
|
59
64
|
);
|
|
@@ -66,15 +71,16 @@ function MyComponent() {
|
|
|
66
71
|
| --------------------- | --------------------------------------------------------------------------- |
|
|
67
72
|
| `useLocalStorage` | JSON-based persistent state with error handling (SSR-safe) |
|
|
68
73
|
| `useWindowScroll` | Track window scroll position and percentage (iOS visualViewport compatible) |
|
|
69
|
-
| `
|
|
74
|
+
| `useElementScroll` | Monitor scroll state of specific elements using ResizeObserver |
|
|
70
75
|
| `useElementPosition` | Monitor element bounding rect on scroll/resize (element ref support) |
|
|
71
|
-
| `
|
|
76
|
+
| `useResponsiveSize` | Track element size with Tailwind-like breakpoints (debounced) |
|
|
72
77
|
| `useBodyScrollLock` | Lock/unlock body scroll with style preservation (iOS-specific handling) |
|
|
73
78
|
| `useScrollToElements` | Scroll to specific elements by index (adjustable offset) |
|
|
74
79
|
| `useImage` | Preload images and expose loading/error states |
|
|
75
80
|
| `useRecursiveTimeout` | Recursively schedule async/sync callbacks |
|
|
76
81
|
| `useViewport` | visualViewport support with in-app mode option and debounce |
|
|
77
82
|
| `useDebounce` | Delay function execution to prevent excessive updates (autoInvoke support) |
|
|
83
|
+
| `useThrottle` | Throttle value updates to a fixed interval |
|
|
78
84
|
|
|
79
85
|
## Development
|
|
80
86
|
|
|
@@ -109,12 +115,13 @@ src/
|
|
|
109
115
|
│ ├── useRecursiveTimeout/
|
|
110
116
|
│ ├── useResponsiveSize/
|
|
111
117
|
│ ├── useScrollToElements/
|
|
118
|
+
│ ├── useThrottle/
|
|
112
119
|
│ ├── useViewport/
|
|
113
120
|
│ ├── useWindowScroll/
|
|
114
121
|
│ └── index.ts # Barrel export
|
|
115
122
|
└── index.ts # Package entry point
|
|
116
123
|
|
|
117
|
-
dist/ # Built library (
|
|
124
|
+
dist/ # Built library (ESM + types)
|
|
118
125
|
.changeset/ # Changesets for versioning
|
|
119
126
|
```
|
|
120
127
|
|
|
@@ -138,18 +145,17 @@ git push --follow-tags
|
|
|
138
145
|
|
|
139
146
|
The library is built as:
|
|
140
147
|
|
|
141
|
-
- **ES Module**: `dist/index.
|
|
142
|
-
- **CommonJS**: `dist/index.cjs`
|
|
148
|
+
- **ES Module**: `dist/index.mjs`
|
|
143
149
|
- **Type Definitions**: `dist/index.d.ts`
|
|
144
150
|
|
|
145
151
|
## Key Patterns
|
|
146
152
|
|
|
147
153
|
- **Window Protection**: Hooks accessing `window`/`document` check `typeof window` for SSR safety (e.g., `useLocalStorage`)
|
|
148
154
|
- **Event Listeners**: All scroll/resize listeners use passive flag when possible
|
|
149
|
-
- **ResizeObserver**: Used in `
|
|
155
|
+
- **ResizeObserver**: Used in `useResponsiveSize` and `useElementPosition` for performance
|
|
150
156
|
- **requestAnimationFrame**: Prevents layout thrashing in scroll/resize callbacks
|
|
151
157
|
- **iOS Compatibility**: Special handling of iOS visualViewport in `useBodyScrollLock`, `useWindowScroll`, and `useViewport`
|
|
152
|
-
- **Debounce**: Optional debouncing for resize events in `
|
|
158
|
+
- **Debounce**: Optional debouncing for resize events in `useResponsiveSize` and `useViewport`
|
|
153
159
|
|
|
154
160
|
## Browser Support
|
|
155
161
|
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { useDebounce } from "./useDebounce/index.mjs";
|
|
2
|
+
import { useBodyScrollLock } from "./useBodyScrollLock/index.mjs";
|
|
3
|
+
import { useElementPosition } from "./useElementPosition/index.mjs";
|
|
4
|
+
import { useElementScroll } from "./useElementScroll/index.mjs";
|
|
5
|
+
import { useResponsiveSize } from "./useResponsiveSize/index.mjs";
|
|
6
|
+
import { useImage } from "./useImage/index.mjs";
|
|
7
|
+
import { useLocalStorage } from "./useLocalStorage/index.mjs";
|
|
8
|
+
import { useRecursiveTimeout } from "./useRecursiveTimeout/index.mjs";
|
|
9
|
+
import { useScrollToElements } from "./useScrollToElements/index.mjs";
|
|
10
|
+
import { useThrottle } from "./useThrottle/index.mjs";
|
|
11
|
+
import { useWindowScroll } from "./useWindowScroll/index.mjs";
|
|
12
|
+
import { useViewport } from "./useViewport/index.mjs";
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import useDebounce from "./useDebounce/index.mjs";
|
|
2
|
+
import useBodyScrollLock from "./useBodyScrollLock/index.mjs";
|
|
3
|
+
import useElementPosition from "./useElementPosition/index.mjs";
|
|
4
|
+
import useElementScroll from "./useElementScroll/index.mjs";
|
|
5
|
+
import useResponsiveSize from "./useResponsiveSize/index.mjs";
|
|
6
|
+
import useImage from "./useImage/index.mjs";
|
|
7
|
+
import useLocalStorage from "./useLocalStorage/index.mjs";
|
|
8
|
+
import useRecursiveTimeout from "./useRecursiveTimeout/index.mjs";
|
|
9
|
+
import useScrollToElements from "./useScrollToElements/index.mjs";
|
|
10
|
+
import useThrottle from "./useThrottle/index.mjs";
|
|
11
|
+
import useWindowScroll from "./useWindowScroll/index.mjs";
|
|
12
|
+
import useViewport from "./useViewport/index.mjs";
|
|
13
|
+
|
|
14
|
+
export { };
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { useEffect } from "react";
|
|
2
|
+
|
|
3
|
+
//#region src/hooks/useBodyScrollLock/index.ts
|
|
4
|
+
const useBodyScrollLock = (enabled = true) => {
|
|
5
|
+
useEffect(() => {
|
|
6
|
+
if (!enabled) return;
|
|
7
|
+
const scrollY = window.scrollY;
|
|
8
|
+
const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent);
|
|
9
|
+
const originalStyles = {
|
|
10
|
+
documentElement: {
|
|
11
|
+
overflow: document.documentElement.style.overflow,
|
|
12
|
+
height: document.documentElement.style.height,
|
|
13
|
+
position: document.documentElement.style.position,
|
|
14
|
+
width: document.documentElement.style.width
|
|
15
|
+
},
|
|
16
|
+
body: {
|
|
17
|
+
overflow: document.body.style.overflow,
|
|
18
|
+
height: document.body.style.height,
|
|
19
|
+
position: document.body.style.position,
|
|
20
|
+
top: document.body.style.top,
|
|
21
|
+
left: document.body.style.left,
|
|
22
|
+
right: document.body.style.right,
|
|
23
|
+
width: document.body.style.width,
|
|
24
|
+
webkitOverflowScrolling: document.body.style.getPropertyValue("-webkit-overflow-scrolling")
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
document.documentElement.style.overflow = "hidden";
|
|
28
|
+
document.documentElement.style.height = "100%";
|
|
29
|
+
document.documentElement.style.position = "fixed";
|
|
30
|
+
document.documentElement.style.width = "100%";
|
|
31
|
+
document.body.style.overflow = "hidden";
|
|
32
|
+
document.body.style.height = "100%";
|
|
33
|
+
document.body.style.position = "fixed";
|
|
34
|
+
document.body.style.top = `-${scrollY}px`;
|
|
35
|
+
document.body.style.left = "0";
|
|
36
|
+
document.body.style.right = "0";
|
|
37
|
+
document.body.style.width = "100%";
|
|
38
|
+
if (isIOS) {
|
|
39
|
+
document.body.style.setProperty("-webkit-overflow-scrolling", "touch");
|
|
40
|
+
const preventAll = (e) => {
|
|
41
|
+
if (e.target === document.body || e.target === document.documentElement || e.target === window) {
|
|
42
|
+
e.preventDefault();
|
|
43
|
+
e.stopPropagation();
|
|
44
|
+
e.stopImmediatePropagation();
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
const preventScroll = () => {
|
|
48
|
+
window.scrollTo(0, 0);
|
|
49
|
+
document.body.scrollTop = 0;
|
|
50
|
+
document.documentElement.scrollTop = 0;
|
|
51
|
+
};
|
|
52
|
+
const events = [
|
|
53
|
+
"scroll",
|
|
54
|
+
"touchmove",
|
|
55
|
+
"touchstart",
|
|
56
|
+
"touchend"
|
|
57
|
+
];
|
|
58
|
+
events.forEach((event) => {
|
|
59
|
+
window.addEventListener(event, preventAll, {
|
|
60
|
+
passive: false,
|
|
61
|
+
capture: true
|
|
62
|
+
});
|
|
63
|
+
document.addEventListener(event, preventAll, {
|
|
64
|
+
passive: false,
|
|
65
|
+
capture: true
|
|
66
|
+
});
|
|
67
|
+
document.body.addEventListener(event, preventAll, {
|
|
68
|
+
passive: false,
|
|
69
|
+
capture: true
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
const scrollInterval = setInterval(preventScroll, 16);
|
|
73
|
+
return () => {
|
|
74
|
+
clearInterval(scrollInterval);
|
|
75
|
+
events.forEach((event) => {
|
|
76
|
+
window.removeEventListener(event, preventAll, { capture: true });
|
|
77
|
+
document.removeEventListener(event, preventAll, { capture: true });
|
|
78
|
+
document.body.removeEventListener(event, preventAll, { capture: true });
|
|
79
|
+
});
|
|
80
|
+
document.documentElement.style.overflow = originalStyles.documentElement.overflow;
|
|
81
|
+
document.documentElement.style.height = originalStyles.documentElement.height;
|
|
82
|
+
document.documentElement.style.position = originalStyles.documentElement.position;
|
|
83
|
+
document.documentElement.style.width = originalStyles.documentElement.width;
|
|
84
|
+
document.body.style.overflow = originalStyles.body.overflow;
|
|
85
|
+
document.body.style.height = originalStyles.body.height;
|
|
86
|
+
document.body.style.position = originalStyles.body.position;
|
|
87
|
+
document.body.style.top = originalStyles.body.top;
|
|
88
|
+
document.body.style.left = originalStyles.body.left;
|
|
89
|
+
document.body.style.right = originalStyles.body.right;
|
|
90
|
+
document.body.style.width = originalStyles.body.width;
|
|
91
|
+
if (originalStyles.body.webkitOverflowScrolling) document.body.style.setProperty("-webkit-overflow-scrolling", originalStyles.body.webkitOverflowScrolling);
|
|
92
|
+
else document.body.style.removeProperty("-webkit-overflow-scrolling");
|
|
93
|
+
window.scrollTo(0, scrollY);
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
return () => {
|
|
97
|
+
document.documentElement.style.overflow = originalStyles.documentElement.overflow;
|
|
98
|
+
document.documentElement.style.height = originalStyles.documentElement.height;
|
|
99
|
+
document.documentElement.style.position = originalStyles.documentElement.position;
|
|
100
|
+
document.documentElement.style.width = originalStyles.documentElement.width;
|
|
101
|
+
document.body.style.overflow = originalStyles.body.overflow;
|
|
102
|
+
document.body.style.height = originalStyles.body.height;
|
|
103
|
+
document.body.style.position = originalStyles.body.position;
|
|
104
|
+
document.body.style.top = originalStyles.body.top;
|
|
105
|
+
document.body.style.left = originalStyles.body.left;
|
|
106
|
+
document.body.style.right = originalStyles.body.right;
|
|
107
|
+
document.body.style.width = originalStyles.body.width;
|
|
108
|
+
window.scrollTo(0, scrollY);
|
|
109
|
+
};
|
|
110
|
+
}, [enabled]);
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
//#endregion
|
|
114
|
+
export { useBodyScrollLock as default };
|
|
115
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../../src/hooks/useBodyScrollLock/index.ts"],"sourcesContent":["import { useEffect } from 'react';\n\nconst useBodyScrollLock = (enabled: boolean = true) => {\n useEffect(() => {\n if (!enabled) {\n return;\n }\n const scrollY = window.scrollY;\n\n const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent);\n\n const originalStyles = {\n documentElement: {\n overflow: document.documentElement.style.overflow,\n height: document.documentElement.style.height,\n position: document.documentElement.style.position,\n width: document.documentElement.style.width,\n },\n body: {\n overflow: document.body.style.overflow,\n height: document.body.style.height,\n position: document.body.style.position,\n top: document.body.style.top,\n left: document.body.style.left,\n right: document.body.style.right,\n width: document.body.style.width,\n webkitOverflowScrolling: document.body.style.getPropertyValue(\n '-webkit-overflow-scrolling',\n ),\n },\n };\n\n document.documentElement.style.overflow = 'hidden';\n document.documentElement.style.height = '100%';\n document.documentElement.style.position = 'fixed';\n document.documentElement.style.width = '100%';\n\n document.body.style.overflow = 'hidden';\n document.body.style.height = '100%';\n document.body.style.position = 'fixed';\n document.body.style.top = `-${scrollY}px`;\n document.body.style.left = '0';\n document.body.style.right = '0';\n document.body.style.width = '100%';\n\n if (isIOS) {\n document.body.style.setProperty('-webkit-overflow-scrolling', 'touch');\n\n const preventAll = (e: Event) => {\n if (\n e.target === document.body ||\n e.target === document.documentElement ||\n e.target === window\n ) {\n e.preventDefault();\n e.stopPropagation();\n e.stopImmediatePropagation();\n }\n };\n\n const preventScroll = () => {\n window.scrollTo(0, 0);\n document.body.scrollTop = 0;\n document.documentElement.scrollTop = 0;\n };\n\n const events = ['scroll', 'touchmove', 'touchstart', 'touchend'];\n events.forEach(event => {\n window.addEventListener(event, preventAll, {\n passive: false,\n capture: true,\n });\n document.addEventListener(event, preventAll, {\n passive: false,\n capture: true,\n });\n document.body.addEventListener(event, preventAll, {\n passive: false,\n capture: true,\n });\n });\n\n const scrollInterval = setInterval(preventScroll, 16);\n\n return () => {\n clearInterval(scrollInterval);\n events.forEach(event => {\n window.removeEventListener(event, preventAll, { capture: true });\n document.removeEventListener(event, preventAll, { capture: true });\n document.body.removeEventListener(event, preventAll, {\n capture: true,\n });\n });\n\n document.documentElement.style.overflow =\n originalStyles.documentElement.overflow;\n document.documentElement.style.height =\n originalStyles.documentElement.height;\n document.documentElement.style.position =\n originalStyles.documentElement.position;\n document.documentElement.style.width =\n originalStyles.documentElement.width;\n\n document.body.style.overflow = originalStyles.body.overflow;\n document.body.style.height = originalStyles.body.height;\n document.body.style.position = originalStyles.body.position;\n document.body.style.top = originalStyles.body.top;\n document.body.style.left = originalStyles.body.left;\n document.body.style.right = originalStyles.body.right;\n document.body.style.width = originalStyles.body.width;\n\n if (originalStyles.body.webkitOverflowScrolling) {\n document.body.style.setProperty(\n '-webkit-overflow-scrolling',\n originalStyles.body.webkitOverflowScrolling,\n );\n } else {\n document.body.style.removeProperty('-webkit-overflow-scrolling');\n }\n\n window.scrollTo(0, scrollY);\n };\n }\n\n return () => {\n document.documentElement.style.overflow =\n originalStyles.documentElement.overflow;\n document.documentElement.style.height =\n originalStyles.documentElement.height;\n document.documentElement.style.position =\n originalStyles.documentElement.position;\n document.documentElement.style.width =\n originalStyles.documentElement.width;\n\n document.body.style.overflow = originalStyles.body.overflow;\n document.body.style.height = originalStyles.body.height;\n document.body.style.position = originalStyles.body.position;\n document.body.style.top = originalStyles.body.top;\n document.body.style.left = originalStyles.body.left;\n document.body.style.right = originalStyles.body.right;\n document.body.style.width = originalStyles.body.width;\n\n window.scrollTo(0, scrollY);\n };\n }, [enabled]);\n};\n\nexport default useBodyScrollLock;\n"],"mappings":"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"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
//#region src/hooks/useDebounce/index.d.ts
|
|
2
|
+
interface Options {
|
|
3
|
+
delay?: number;
|
|
4
|
+
autoInvoke?: boolean;
|
|
5
|
+
}
|
|
6
|
+
declare const useDebounce: <T extends (...args: unknown[]) => unknown>(callback: T, {
|
|
7
|
+
delay,
|
|
8
|
+
autoInvoke
|
|
9
|
+
}: Options, deps?: React.DependencyList) => T;
|
|
10
|
+
//#endregion
|
|
11
|
+
export { useDebounce };
|
|
12
|
+
//# sourceMappingURL=index.d.mts.map
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { useEffect, useRef } from "react";
|
|
2
|
+
|
|
3
|
+
//#region src/hooks/useDebounce/index.ts
|
|
4
|
+
const useDebounce = (callback, { delay = 100, autoInvoke = true }, deps = []) => {
|
|
5
|
+
const timeoutRef = useRef(null);
|
|
6
|
+
const callbackRef = useRef(callback);
|
|
7
|
+
const depsRef = useRef(deps);
|
|
8
|
+
const prevDeps = useRef(void 0);
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
callbackRef.current = callback;
|
|
11
|
+
});
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
depsRef.current = deps;
|
|
14
|
+
});
|
|
15
|
+
const stableDebouncedCallback = useRef(null);
|
|
16
|
+
if (!stableDebouncedCallback.current) stableDebouncedCallback.current = ((...args) => {
|
|
17
|
+
if (timeoutRef.current) clearTimeout(timeoutRef.current);
|
|
18
|
+
timeoutRef.current = setTimeout(() => {
|
|
19
|
+
callbackRef.current(...args);
|
|
20
|
+
}, delay);
|
|
21
|
+
});
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
const depsChanged = prevDeps.current === void 0 || prevDeps.current.length !== deps.length || prevDeps.current.some((dep, i) => dep !== deps[i]);
|
|
24
|
+
if (depsChanged && timeoutRef.current) clearTimeout(timeoutRef.current);
|
|
25
|
+
if (autoInvoke && depsChanged) {
|
|
26
|
+
if (prevDeps.current === void 0) callbackRef.current();
|
|
27
|
+
else if (stableDebouncedCallback.current) stableDebouncedCallback.current();
|
|
28
|
+
}
|
|
29
|
+
prevDeps.current = deps;
|
|
30
|
+
});
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
return () => {
|
|
33
|
+
if (timeoutRef.current) clearTimeout(timeoutRef.current);
|
|
34
|
+
};
|
|
35
|
+
}, []);
|
|
36
|
+
return stableDebouncedCallback.current;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
//#endregion
|
|
40
|
+
export { useDebounce as default };
|
|
41
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../../src/hooks/useDebounce/index.ts"],"sourcesContent":["import { useEffect, useRef } from 'react';\n\ninterface Options {\n delay?: number;\n autoInvoke?: boolean;\n}\n\nconst useDebounce = <T extends (...args: unknown[]) => unknown>(\n callback: T,\n { delay = 100, autoInvoke = true }: Options,\n deps: React.DependencyList = [],\n): T => {\n const timeoutRef = useRef<NodeJS.Timeout | null>(null);\n const callbackRef = useRef(callback);\n const depsRef = useRef(deps);\n const prevDeps = useRef<React.DependencyList | undefined>(undefined);\n\n useEffect(() => {\n callbackRef.current = callback;\n });\n\n useEffect(() => {\n depsRef.current = deps;\n });\n\n const stableDebouncedCallback = useRef<T | null>(null);\n\n if (!stableDebouncedCallback.current) {\n stableDebouncedCallback.current = ((...args: Parameters<T>) => {\n if (timeoutRef.current) {\n clearTimeout(timeoutRef.current);\n }\n\n timeoutRef.current = setTimeout(() => {\n callbackRef.current(...args);\n }, delay);\n }) as T;\n }\n\n useEffect(() => {\n const depsChanged =\n prevDeps.current === undefined ||\n prevDeps.current.length !== deps.length ||\n prevDeps.current.some((dep, i) => dep !== deps[i]);\n\n if (depsChanged && timeoutRef.current) {\n clearTimeout(timeoutRef.current);\n }\n\n if (autoInvoke && depsChanged) {\n const isFirstRender = prevDeps.current === undefined;\n if (isFirstRender) {\n callbackRef.current();\n } else if (stableDebouncedCallback.current) {\n stableDebouncedCallback.current();\n }\n }\n\n prevDeps.current = deps;\n });\n\n useEffect(() => {\n return () => {\n if (timeoutRef.current) {\n clearTimeout(timeoutRef.current);\n }\n };\n }, []);\n\n return stableDebouncedCallback.current;\n};\n\nexport default useDebounce;\n"],"mappings":";;;AAOA,MAAM,eACJ,UACA,EAAE,QAAQ,KAAK,aAAa,QAC5B,OAA6B,EAAE,KACzB;CACN,MAAM,aAAa,OAA8B,KAAK;CACtD,MAAM,cAAc,OAAO,SAAS;CACpC,MAAM,UAAU,OAAO,KAAK;CAC5B,MAAM,WAAW,OAAyC,OAAU;AAEpE,iBAAgB;AACd,cAAY,UAAU;GACtB;AAEF,iBAAgB;AACd,UAAQ,UAAU;GAClB;CAEF,MAAM,0BAA0B,OAAiB,KAAK;AAEtD,KAAI,CAAC,wBAAwB,QAC3B,yBAAwB,YAAY,GAAG,SAAwB;AAC7D,MAAI,WAAW,QACb,cAAa,WAAW,QAAQ;AAGlC,aAAW,UAAU,iBAAiB;AACpC,eAAY,QAAQ,GAAG,KAAK;KAC3B,MAAM;;AAIb,iBAAgB;EACd,MAAM,cACJ,SAAS,YAAY,UACrB,SAAS,QAAQ,WAAW,KAAK,UACjC,SAAS,QAAQ,MAAM,KAAK,MAAM,QAAQ,KAAK,GAAG;AAEpD,MAAI,eAAe,WAAW,QAC5B,cAAa,WAAW,QAAQ;AAGlC,MAAI,cAAc,aAEhB;OADsB,SAAS,YAAY,OAEzC,aAAY,SAAS;YACZ,wBAAwB,QACjC,yBAAwB,SAAS;;AAIrC,WAAS,UAAU;GACnB;AAEF,iBAAgB;AACd,eAAa;AACX,OAAI,WAAW,QACb,cAAa,WAAW,QAAQ;;IAGnC,EAAE,CAAC;AAEN,QAAO,wBAAwB"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { RefObject } from "react";
|
|
2
|
+
|
|
3
|
+
//#region src/hooks/useElementPosition/index.d.ts
|
|
4
|
+
type ElementReference<T> = string | RefObject<T>;
|
|
5
|
+
declare const useElementPosition: <T>(elementRef: ElementReference<T>) => DOMRect | null;
|
|
6
|
+
//#endregion
|
|
7
|
+
export { useElementPosition };
|
|
8
|
+
//# sourceMappingURL=index.d.mts.map
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
|
|
3
|
+
//#region src/hooks/useElementPosition/index.ts
|
|
4
|
+
const useElementPosition = (elementRef) => {
|
|
5
|
+
const [rect, setRect] = useState(null);
|
|
6
|
+
useEffect(() => {
|
|
7
|
+
const getElement = (ref) => {
|
|
8
|
+
if (typeof ref === "string") return document.querySelector(ref);
|
|
9
|
+
return ref.current;
|
|
10
|
+
};
|
|
11
|
+
const updateRect = () => {
|
|
12
|
+
const element = getElement(elementRef);
|
|
13
|
+
if (element) setRect(element.getBoundingClientRect());
|
|
14
|
+
else setRect(null);
|
|
15
|
+
};
|
|
16
|
+
const onUpdate = () => {
|
|
17
|
+
requestAnimationFrame(updateRect);
|
|
18
|
+
};
|
|
19
|
+
updateRect();
|
|
20
|
+
window.addEventListener("scroll", onUpdate, { passive: true });
|
|
21
|
+
window.addEventListener("resize", onUpdate, { passive: true });
|
|
22
|
+
return () => {
|
|
23
|
+
window.removeEventListener("scroll", onUpdate);
|
|
24
|
+
window.removeEventListener("resize", onUpdate);
|
|
25
|
+
};
|
|
26
|
+
}, [elementRef]);
|
|
27
|
+
return rect;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
//#endregion
|
|
31
|
+
export { useElementPosition as default };
|
|
32
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../../src/hooks/useElementPosition/index.ts"],"sourcesContent":["import { type RefObject, useEffect, useState } from 'react';\n\ntype ElementReference<T> = string | RefObject<T>;\n\nconst useElementPosition = <T>(elementRef: ElementReference<T>) => {\n const [rect, setRect] = useState<DOMRect | null>(null);\n\n useEffect(() => {\n const getElement = (ref: ElementReference<T>): T | null => {\n if (typeof ref === 'string') {\n return document.querySelector(ref) as T;\n }\n return ref.current;\n };\n\n const updateRect = () => {\n const element = getElement(elementRef) as HTMLElement | null;\n if (element) {\n setRect(element.getBoundingClientRect());\n } else {\n setRect(null);\n }\n };\n\n const onUpdate = () => {\n requestAnimationFrame(updateRect);\n };\n\n updateRect();\n\n window.addEventListener('scroll', onUpdate, { passive: true });\n window.addEventListener('resize', onUpdate, { passive: true });\n\n return () => {\n window.removeEventListener('scroll', onUpdate);\n window.removeEventListener('resize', onUpdate);\n };\n }, [elementRef]);\n\n return rect;\n};\n\nexport default useElementPosition;\n"],"mappings":";;;AAIA,MAAM,sBAAyB,eAAoC;CACjE,MAAM,CAAC,MAAM,WAAW,SAAyB,KAAK;AAEtD,iBAAgB;EACd,MAAM,cAAc,QAAuC;AACzD,OAAI,OAAO,QAAQ,SACjB,QAAO,SAAS,cAAc,IAAI;AAEpC,UAAO,IAAI;;EAGb,MAAM,mBAAmB;GACvB,MAAM,UAAU,WAAW,WAAW;AACtC,OAAI,QACF,SAAQ,QAAQ,uBAAuB,CAAC;OAExC,SAAQ,KAAK;;EAIjB,MAAM,iBAAiB;AACrB,yBAAsB,WAAW;;AAGnC,cAAY;AAEZ,SAAO,iBAAiB,UAAU,UAAU,EAAE,SAAS,MAAM,CAAC;AAC9D,SAAO,iBAAiB,UAAU,UAAU,EAAE,SAAS,MAAM,CAAC;AAE9D,eAAa;AACX,UAAO,oBAAoB,UAAU,SAAS;AAC9C,UAAO,oBAAoB,UAAU,SAAS;;IAE/C,CAAC,WAAW,CAAC;AAEhB,QAAO"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
//#region src/hooks/useElementScroll/index.d.ts
|
|
2
|
+
declare const useElementScroll: () => {
|
|
3
|
+
element: HTMLElement | null;
|
|
4
|
+
setRef: (el: HTMLElement | null) => void;
|
|
5
|
+
scrollY: number;
|
|
6
|
+
scrollPercentage: number;
|
|
7
|
+
isAtTop: boolean;
|
|
8
|
+
isAtBottom: boolean;
|
|
9
|
+
scrollableHeight: number;
|
|
10
|
+
clientHeight: number;
|
|
11
|
+
scrollHeight: number;
|
|
12
|
+
};
|
|
13
|
+
//#endregion
|
|
14
|
+
export { useElementScroll };
|
|
15
|
+
//# sourceMappingURL=index.d.mts.map
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from "react";
|
|
2
|
+
|
|
3
|
+
//#region src/hooks/useElementScroll/index.ts
|
|
4
|
+
const useElementScroll = () => {
|
|
5
|
+
const [element, setElement] = useState(null);
|
|
6
|
+
const [scrollPosition, setScrollPosition] = useState({
|
|
7
|
+
scrollY: 0,
|
|
8
|
+
scrollPercentage: 0,
|
|
9
|
+
isAtTop: true,
|
|
10
|
+
isAtBottom: false,
|
|
11
|
+
scrollableHeight: 0,
|
|
12
|
+
clientHeight: 0,
|
|
13
|
+
scrollHeight: 0
|
|
14
|
+
});
|
|
15
|
+
const setRef = useCallback((el) => {
|
|
16
|
+
setElement(el);
|
|
17
|
+
}, []);
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
if (!element) return;
|
|
20
|
+
const updateScrollPosition = () => {
|
|
21
|
+
const { scrollTop, scrollHeight, clientHeight } = element;
|
|
22
|
+
const scrollableHeight = scrollHeight - clientHeight;
|
|
23
|
+
if (scrollableHeight <= 0) {
|
|
24
|
+
setScrollPosition({
|
|
25
|
+
scrollY: 0,
|
|
26
|
+
scrollPercentage: 0,
|
|
27
|
+
isAtTop: true,
|
|
28
|
+
isAtBottom: true,
|
|
29
|
+
scrollableHeight: 0,
|
|
30
|
+
clientHeight,
|
|
31
|
+
scrollHeight
|
|
32
|
+
});
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
setScrollPosition({
|
|
36
|
+
scrollY: scrollTop,
|
|
37
|
+
scrollPercentage: Math.min(100, Math.max(0, scrollTop / scrollableHeight * 100)),
|
|
38
|
+
isAtTop: scrollTop <= 0,
|
|
39
|
+
isAtBottom: scrollTop >= scrollableHeight - 1,
|
|
40
|
+
scrollableHeight,
|
|
41
|
+
clientHeight,
|
|
42
|
+
scrollHeight
|
|
43
|
+
});
|
|
44
|
+
};
|
|
45
|
+
updateScrollPosition();
|
|
46
|
+
const onScroll = () => {
|
|
47
|
+
updateScrollPosition();
|
|
48
|
+
};
|
|
49
|
+
element.addEventListener("scroll", onScroll, { passive: true });
|
|
50
|
+
const resizeObserver = new ResizeObserver(() => {
|
|
51
|
+
updateScrollPosition();
|
|
52
|
+
});
|
|
53
|
+
resizeObserver.observe(element);
|
|
54
|
+
return () => {
|
|
55
|
+
element.removeEventListener("scroll", onScroll);
|
|
56
|
+
resizeObserver.unobserve(element);
|
|
57
|
+
};
|
|
58
|
+
}, [element]);
|
|
59
|
+
return {
|
|
60
|
+
...scrollPosition,
|
|
61
|
+
element,
|
|
62
|
+
setRef
|
|
63
|
+
};
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
//#endregion
|
|
67
|
+
export { useElementScroll as default };
|
|
68
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../../src/hooks/useElementScroll/index.ts"],"sourcesContent":["import { useCallback, useEffect, useState } from 'react';\n\ninterface ScrollPosition {\n scrollY: number;\n scrollPercentage: number;\n isAtTop: boolean;\n isAtBottom: boolean;\n scrollableHeight: number;\n clientHeight: number;\n scrollHeight: number;\n}\n\nconst useElementScroll = () => {\n const [element, setElement] = useState<HTMLElement | null>(null);\n const [scrollPosition, setScrollPosition] = useState<ScrollPosition>({\n scrollY: 0,\n scrollPercentage: 0,\n isAtTop: true,\n isAtBottom: false,\n scrollableHeight: 0,\n clientHeight: 0,\n scrollHeight: 0,\n });\n\n const setRef = useCallback((el: HTMLElement | null) => {\n setElement(el);\n }, []);\n\n useEffect(() => {\n if (!element) {\n return;\n }\n\n const updateScrollPosition = () => {\n const { scrollTop, scrollHeight, clientHeight } = element;\n const scrollableHeight = scrollHeight - clientHeight;\n\n if (scrollableHeight <= 0) {\n setScrollPosition({\n scrollY: 0,\n scrollPercentage: 0,\n isAtTop: true,\n isAtBottom: true,\n scrollableHeight: 0,\n clientHeight,\n scrollHeight,\n });\n return;\n }\n\n const percentage = Math.min(\n 100,\n Math.max(0, (scrollTop / scrollableHeight) * 100),\n );\n\n setScrollPosition({\n scrollY: scrollTop,\n scrollPercentage: percentage,\n isAtTop: scrollTop <= 0,\n isAtBottom: scrollTop >= scrollableHeight - 1,\n scrollableHeight,\n clientHeight,\n scrollHeight,\n });\n };\n\n updateScrollPosition();\n\n const onScroll = () => {\n updateScrollPosition();\n };\n\n element.addEventListener('scroll', onScroll, { passive: true });\n\n const resizeObserver = new ResizeObserver(() => {\n updateScrollPosition();\n });\n\n resizeObserver.observe(element);\n\n return () => {\n element.removeEventListener('scroll', onScroll);\n resizeObserver.unobserve(element);\n };\n }, [element]);\n\n return { ...scrollPosition, element, setRef };\n};\n\nexport default useElementScroll;\n"],"mappings":";;;AAYA,MAAM,yBAAyB;CAC7B,MAAM,CAAC,SAAS,cAAc,SAA6B,KAAK;CAChE,MAAM,CAAC,gBAAgB,qBAAqB,SAAyB;EACnE,SAAS;EACT,kBAAkB;EAClB,SAAS;EACT,YAAY;EACZ,kBAAkB;EAClB,cAAc;EACd,cAAc;EACf,CAAC;CAEF,MAAM,SAAS,aAAa,OAA2B;AACrD,aAAW,GAAG;IACb,EAAE,CAAC;AAEN,iBAAgB;AACd,MAAI,CAAC,QACH;EAGF,MAAM,6BAA6B;GACjC,MAAM,EAAE,WAAW,cAAc,iBAAiB;GAClD,MAAM,mBAAmB,eAAe;AAExC,OAAI,oBAAoB,GAAG;AACzB,sBAAkB;KAChB,SAAS;KACT,kBAAkB;KAClB,SAAS;KACT,YAAY;KACZ,kBAAkB;KAClB;KACA;KACD,CAAC;AACF;;AAQF,qBAAkB;IAChB,SAAS;IACT,kBAPiB,KAAK,IACtB,KACA,KAAK,IAAI,GAAI,YAAY,mBAAoB,IAAI,CAClD;IAKC,SAAS,aAAa;IACtB,YAAY,aAAa,mBAAmB;IAC5C;IACA;IACA;IACD,CAAC;;AAGJ,wBAAsB;EAEtB,MAAM,iBAAiB;AACrB,yBAAsB;;AAGxB,UAAQ,iBAAiB,UAAU,UAAU,EAAE,SAAS,MAAM,CAAC;EAE/D,MAAM,iBAAiB,IAAI,qBAAqB;AAC9C,yBAAsB;IACtB;AAEF,iBAAe,QAAQ,QAAQ;AAE/B,eAAa;AACX,WAAQ,oBAAoB,UAAU,SAAS;AAC/C,kBAAe,UAAU,QAAQ;;IAElC,CAAC,QAAQ,CAAC;AAEb,QAAO;EAAE,GAAG;EAAgB;EAAS;EAAQ"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
//#region src/hooks/useImage/index.d.ts
|
|
2
|
+
interface Options {
|
|
3
|
+
retryCount?: number;
|
|
4
|
+
retryDelay?: number;
|
|
5
|
+
}
|
|
6
|
+
declare const useImage: (src: string, options?: Options) => {
|
|
7
|
+
loading: boolean;
|
|
8
|
+
error: string | Event | null;
|
|
9
|
+
loaded: boolean;
|
|
10
|
+
retry: () => void;
|
|
11
|
+
};
|
|
12
|
+
//#endregion
|
|
13
|
+
export { useImage };
|
|
14
|
+
//# sourceMappingURL=index.d.mts.map
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from "react";
|
|
2
|
+
|
|
3
|
+
//#region src/hooks/useImage/index.ts
|
|
4
|
+
const useImage = (src, options = {}) => {
|
|
5
|
+
const { retryCount = 0, retryDelay = 1e3 } = options;
|
|
6
|
+
const [loading, setLoading] = useState(true);
|
|
7
|
+
const [error, setError] = useState(null);
|
|
8
|
+
const [loaded, setLoaded] = useState(false);
|
|
9
|
+
const [attemptCount, setAttemptCount] = useState(0);
|
|
10
|
+
const loadImage = useCallback(() => {
|
|
11
|
+
if (!src) {
|
|
12
|
+
setLoading(false);
|
|
13
|
+
setLoaded(false);
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
setLoading(true);
|
|
17
|
+
setError(null);
|
|
18
|
+
const img = new Image();
|
|
19
|
+
img.src = src;
|
|
20
|
+
img.onload = () => {
|
|
21
|
+
setLoading(false);
|
|
22
|
+
setLoaded(true);
|
|
23
|
+
setError(null);
|
|
24
|
+
setAttemptCount(0);
|
|
25
|
+
};
|
|
26
|
+
img.onerror = (err) => {
|
|
27
|
+
setLoading(false);
|
|
28
|
+
setLoaded(false);
|
|
29
|
+
setError(err);
|
|
30
|
+
if (attemptCount < retryCount) setTimeout(() => {
|
|
31
|
+
setAttemptCount((prev) => prev + 1);
|
|
32
|
+
}, retryDelay);
|
|
33
|
+
};
|
|
34
|
+
}, [
|
|
35
|
+
src,
|
|
36
|
+
attemptCount,
|
|
37
|
+
retryCount,
|
|
38
|
+
retryDelay
|
|
39
|
+
]);
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
loadImage();
|
|
42
|
+
}, [loadImage]);
|
|
43
|
+
return {
|
|
44
|
+
loading,
|
|
45
|
+
error,
|
|
46
|
+
loaded,
|
|
47
|
+
retry: useCallback(() => {
|
|
48
|
+
setAttemptCount(0);
|
|
49
|
+
loadImage();
|
|
50
|
+
}, [loadImage])
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
//#endregion
|
|
55
|
+
export { useImage as default };
|
|
56
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../../src/hooks/useImage/index.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<string | Event | null>(null);\n const [loaded, setLoaded] = useState(false);\n const [attemptCount, setAttemptCount] = useState(0);\n\n const loadImage = useCallback(() => {\n if (!src) {\n setLoading(false);\n setLoaded(false);\n return;\n }\n\n setLoading(true);\n setError(null);\n\n const img = new Image();\n img.src = src;\n\n img.onload = () => {\n setLoading(false);\n setLoaded(true);\n setError(null);\n setAttemptCount(0);\n };\n\n img.onerror = err => {\n setLoading(false);\n setLoaded(false);\n setError(err);\n\n if (attemptCount < retryCount) {\n setTimeout(() => {\n setAttemptCount(prev => prev + 1);\n }, retryDelay);\n }\n };\n }, [src, attemptCount, retryCount, retryDelay]);\n\n useEffect(() => {\n loadImage();\n }, [loadImage]);\n\n const retry = useCallback(() => {\n setAttemptCount(0);\n loadImage();\n }, [loadImage]);\n\n return {\n loading,\n error,\n loaded,\n retry,\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,SAAgC,KAAK;CAC/D,MAAM,CAAC,QAAQ,aAAa,SAAS,MAAM;CAC3C,MAAM,CAAC,cAAc,mBAAmB,SAAS,EAAE;CAEnD,MAAM,YAAY,kBAAkB;AAClC,MAAI,CAAC,KAAK;AACR,cAAW,MAAM;AACjB,aAAU,MAAM;AAChB;;AAGF,aAAW,KAAK;AAChB,WAAS,KAAK;EAEd,MAAM,MAAM,IAAI,OAAO;AACvB,MAAI,MAAM;AAEV,MAAI,eAAe;AACjB,cAAW,MAAM;AACjB,aAAU,KAAK;AACf,YAAS,KAAK;AACd,mBAAgB,EAAE;;AAGpB,MAAI,WAAU,QAAO;AACnB,cAAW,MAAM;AACjB,aAAU,MAAM;AAChB,YAAS,IAAI;AAEb,OAAI,eAAe,WACjB,kBAAiB;AACf,qBAAgB,SAAQ,OAAO,EAAE;MAChC,WAAW;;IAGjB;EAAC;EAAK;EAAc;EAAY;EAAW,CAAC;AAE/C,iBAAgB;AACd,aAAW;IACV,CAAC,UAAU,CAAC;AAOf,QAAO;EACL;EACA;EACA;EACA,OATY,kBAAkB;AAC9B,mBAAgB,EAAE;AAClB,cAAW;KACV,CAAC,UAAU,CAAC;EAOd"}
|