domet 1.0.1 → 1.0.3
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.md +5 -3
- package/dist/{useDomet.d.ts → cjs/index.d.ts} +14 -12
- package/dist/{useScrowl.js → cjs/index.js} +196 -177
- package/dist/{useScrowl.d.ts → es/index.d.mts} +14 -12
- package/dist/{useDomet.js → es/index.mjs} +167 -154
- package/package.json +20 -15
- package/LICENSE +0 -21
- package/dist/index.d.ts +0 -3
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -1
- package/dist/useDomet.d.ts.map +0 -1
- package/dist/useScrowl.d.ts.map +0 -1
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
1
|
+
import { RefObject } from 'react';
|
|
2
|
+
|
|
3
|
+
type SectionBounds = {
|
|
3
4
|
top: number;
|
|
4
5
|
bottom: number;
|
|
5
6
|
height: number;
|
|
6
7
|
};
|
|
7
|
-
|
|
8
|
+
type ScrollState = {
|
|
8
9
|
y: number;
|
|
9
10
|
progress: number;
|
|
10
11
|
direction: "up" | "down" | null;
|
|
@@ -14,15 +15,15 @@ export type ScrollState = {
|
|
|
14
15
|
viewportHeight: number;
|
|
15
16
|
offset: number;
|
|
16
17
|
};
|
|
17
|
-
|
|
18
|
+
type SectionState = {
|
|
18
19
|
bounds: SectionBounds;
|
|
19
20
|
visibility: number;
|
|
20
21
|
progress: number;
|
|
21
22
|
isInViewport: boolean;
|
|
22
23
|
isActive: boolean;
|
|
23
24
|
};
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
type ScrollBehavior = "smooth" | "instant" | "auto";
|
|
26
|
+
type DometOptions = {
|
|
26
27
|
offset?: number;
|
|
27
28
|
offsetRatio?: number;
|
|
28
29
|
debounceMs?: number;
|
|
@@ -35,17 +36,17 @@ export type DometOptions = {
|
|
|
35
36
|
onScrollStart?: () => void;
|
|
36
37
|
onScrollEnd?: () => void;
|
|
37
38
|
};
|
|
38
|
-
|
|
39
|
+
type SectionProps = {
|
|
39
40
|
id: string;
|
|
40
41
|
ref: (el: HTMLElement | null) => void;
|
|
41
42
|
"data-domet": string;
|
|
42
43
|
};
|
|
43
|
-
|
|
44
|
+
type NavProps = {
|
|
44
45
|
onClick: () => void;
|
|
45
46
|
"aria-current": "page" | undefined;
|
|
46
47
|
"data-active": boolean;
|
|
47
48
|
};
|
|
48
|
-
|
|
49
|
+
type UseDometReturn = {
|
|
49
50
|
activeId: string | null;
|
|
50
51
|
activeIndex: number;
|
|
51
52
|
scroll: ScrollState;
|
|
@@ -55,6 +56,7 @@ export type UseDometReturn = {
|
|
|
55
56
|
sectionProps: (id: string) => SectionProps;
|
|
56
57
|
navProps: (id: string) => NavProps;
|
|
57
58
|
};
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
59
|
+
declare function useDomet(sectionIds: string[], containerRef?: RefObject<HTMLElement> | null, options?: DometOptions): UseDometReturn;
|
|
60
|
+
|
|
61
|
+
export { useDomet as default, useDomet };
|
|
62
|
+
export type { DometOptions, NavProps, ScrollBehavior, ScrollState, SectionBounds, SectionProps, SectionState, UseDometReturn };
|
|
@@ -1,19 +1,24 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useMemo, useState, useRef, useCallback, useLayoutEffect, useEffect } from 'react';
|
|
2
|
+
|
|
2
3
|
const DEFAULT_VISIBILITY_THRESHOLD = 0.6;
|
|
3
4
|
const DEFAULT_HYSTERESIS_MARGIN = 150;
|
|
4
5
|
const SCROLL_IDLE_MS = 100;
|
|
5
6
|
const useIsomorphicLayoutEffect = typeof window !== "undefined" ? useLayoutEffect : useEffect;
|
|
6
|
-
|
|
7
|
-
const { offset = 0, offsetRatio = 0.08, debounceMs = 10, visibilityThreshold = DEFAULT_VISIBILITY_THRESHOLD, hysteresisMargin = DEFAULT_HYSTERESIS_MARGIN, behavior = "auto", onActiveChange, onSectionEnter, onSectionLeave, onScrollStart, onScrollEnd
|
|
8
|
-
|
|
9
|
-
const stableSectionIds = useMemo(()
|
|
10
|
-
|
|
7
|
+
function useDomet(sectionIds, containerRef = null, options = {}) {
|
|
8
|
+
const { offset = 0, offsetRatio = 0.08, debounceMs = 10, visibilityThreshold = DEFAULT_VISIBILITY_THRESHOLD, hysteresisMargin = DEFAULT_HYSTERESIS_MARGIN, behavior = "auto", onActiveChange, onSectionEnter, onSectionLeave, onScrollStart, onScrollEnd } = options;
|
|
9
|
+
JSON.stringify(sectionIds);
|
|
10
|
+
const stableSectionIds = useMemo(()=>sectionIds, [
|
|
11
|
+
sectionIds
|
|
12
|
+
]);
|
|
13
|
+
const sectionIndexMap = useMemo(()=>{
|
|
11
14
|
const map = new Map();
|
|
12
|
-
for
|
|
15
|
+
for(let i = 0; i < stableSectionIds.length; i++){
|
|
13
16
|
map.set(stableSectionIds[i], i);
|
|
14
17
|
}
|
|
15
18
|
return map;
|
|
16
|
-
}, [
|
|
19
|
+
}, [
|
|
20
|
+
stableSectionIds
|
|
21
|
+
]);
|
|
17
22
|
const [activeId, setActiveId] = useState(stableSectionIds[0] || null);
|
|
18
23
|
const [scroll, setScroll] = useState({
|
|
19
24
|
y: 0,
|
|
@@ -23,7 +28,7 @@ export function useDomet(sectionIds, containerRef = null, options = {}) {
|
|
|
23
28
|
isScrolling: false,
|
|
24
29
|
maxScroll: 0,
|
|
25
30
|
viewportHeight: 0,
|
|
26
|
-
offset: 0
|
|
31
|
+
offset: 0
|
|
27
32
|
});
|
|
28
33
|
const [sections, setSections] = useState({});
|
|
29
34
|
const [containerElement, setContainerElement] = useState(null);
|
|
@@ -41,56 +46,61 @@ export function useDomet(sectionIds, containerRef = null, options = {}) {
|
|
|
41
46
|
const isScrollingRef = useRef(false);
|
|
42
47
|
const scrollIdleTimeoutRef = useRef(null);
|
|
43
48
|
const prevSectionsInViewport = useRef(new Set());
|
|
44
|
-
const recalculateRef = useRef(()
|
|
49
|
+
const recalculateRef = useRef(()=>{});
|
|
45
50
|
const scrollCleanupRef = useRef(null);
|
|
46
51
|
const callbackRefs = useRef({
|
|
47
52
|
onActiveChange,
|
|
48
53
|
onSectionEnter,
|
|
49
54
|
onSectionLeave,
|
|
50
55
|
onScrollStart,
|
|
51
|
-
onScrollEnd
|
|
56
|
+
onScrollEnd
|
|
52
57
|
});
|
|
53
58
|
callbackRefs.current = {
|
|
54
59
|
onActiveChange,
|
|
55
60
|
onSectionEnter,
|
|
56
61
|
onSectionLeave,
|
|
57
62
|
onScrollStart,
|
|
58
|
-
onScrollEnd
|
|
63
|
+
onScrollEnd
|
|
59
64
|
};
|
|
60
|
-
const getEffectiveOffset = useCallback(()
|
|
65
|
+
const getEffectiveOffset = useCallback(()=>{
|
|
61
66
|
return offset;
|
|
62
|
-
}, [
|
|
63
|
-
|
|
67
|
+
}, [
|
|
68
|
+
offset
|
|
69
|
+
]);
|
|
70
|
+
const getScrollBehavior = useCallback(()=>{
|
|
64
71
|
if (behavior === "auto") {
|
|
65
|
-
if (typeof window === "undefined")
|
|
66
|
-
return "instant";
|
|
72
|
+
if (typeof window === "undefined") return "instant";
|
|
67
73
|
const prefersReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
68
74
|
return prefersReducedMotion ? "instant" : "smooth";
|
|
69
75
|
}
|
|
70
76
|
return behavior;
|
|
71
|
-
}, [
|
|
72
|
-
|
|
73
|
-
|
|
77
|
+
}, [
|
|
78
|
+
behavior
|
|
79
|
+
]);
|
|
80
|
+
useIsomorphicLayoutEffect(()=>{
|
|
81
|
+
var _ref;
|
|
82
|
+
const nextContainer = (_ref = containerRef == null ? void 0 : containerRef.current) != null ? _ref : null;
|
|
74
83
|
if (nextContainer !== containerElement) {
|
|
75
84
|
setContainerElement(nextContainer);
|
|
76
85
|
}
|
|
77
|
-
}, [
|
|
78
|
-
|
|
86
|
+
}, [
|
|
87
|
+
containerRef,
|
|
88
|
+
containerElement
|
|
89
|
+
]);
|
|
90
|
+
const registerRef = useCallback((id)=>{
|
|
79
91
|
const existing = refCallbacks.current[id];
|
|
80
|
-
if (existing)
|
|
81
|
-
|
|
82
|
-
const callback = (el) => {
|
|
92
|
+
if (existing) return existing;
|
|
93
|
+
const callback = (el)=>{
|
|
83
94
|
if (el) {
|
|
84
95
|
refs.current[id] = el;
|
|
85
|
-
}
|
|
86
|
-
else {
|
|
96
|
+
} else {
|
|
87
97
|
delete refs.current[id];
|
|
88
98
|
}
|
|
89
99
|
};
|
|
90
100
|
refCallbacks.current[id] = callback;
|
|
91
101
|
return callback;
|
|
92
102
|
}, []);
|
|
93
|
-
const scrollToSection = useCallback((id)
|
|
103
|
+
const scrollToSection = useCallback((id)=>{
|
|
94
104
|
if (!stableSectionIds.includes(id)) {
|
|
95
105
|
if (process.env.NODE_ENV !== "production") {
|
|
96
106
|
console.warn(`[domet] scrollToSection: id "${id}" not in sectionIds`);
|
|
@@ -98,12 +108,11 @@ export function useDomet(sectionIds, containerRef = null, options = {}) {
|
|
|
98
108
|
return;
|
|
99
109
|
}
|
|
100
110
|
const element = refs.current[id];
|
|
101
|
-
if (!element)
|
|
102
|
-
return;
|
|
111
|
+
if (!element) return;
|
|
103
112
|
if (programmaticScrollTimeoutId.current) {
|
|
104
113
|
clearTimeout(programmaticScrollTimeoutId.current);
|
|
105
114
|
}
|
|
106
|
-
scrollCleanupRef.current
|
|
115
|
+
scrollCleanupRef.current == null ? void 0 : scrollCleanupRef.current.call(scrollCleanupRef);
|
|
107
116
|
isProgrammaticScrolling.current = true;
|
|
108
117
|
activeIdRef.current = id;
|
|
109
118
|
setActiveId(id);
|
|
@@ -111,19 +120,19 @@ export function useDomet(sectionIds, containerRef = null, options = {}) {
|
|
|
111
120
|
const elementRect = element.getBoundingClientRect();
|
|
112
121
|
const effectiveOffset = getEffectiveOffset() + 10;
|
|
113
122
|
const scrollTarget = container || window;
|
|
114
|
-
const unlockScroll = ()
|
|
123
|
+
const unlockScroll = ()=>{
|
|
115
124
|
isProgrammaticScrolling.current = false;
|
|
116
125
|
if (programmaticScrollTimeoutId.current) {
|
|
117
126
|
clearTimeout(programmaticScrollTimeoutId.current);
|
|
118
127
|
programmaticScrollTimeoutId.current = null;
|
|
119
128
|
}
|
|
120
|
-
requestAnimationFrame(()
|
|
129
|
+
requestAnimationFrame(()=>{
|
|
121
130
|
recalculateRef.current();
|
|
122
131
|
});
|
|
123
132
|
};
|
|
124
133
|
let debounceTimer = null;
|
|
125
134
|
let isUnlocked = false;
|
|
126
|
-
const cleanup = ()
|
|
135
|
+
const cleanup = ()=>{
|
|
127
136
|
if (debounceTimer) {
|
|
128
137
|
clearTimeout(debounceTimer);
|
|
129
138
|
debounceTimer = null;
|
|
@@ -134,31 +143,30 @@ export function useDomet(sectionIds, containerRef = null, options = {}) {
|
|
|
134
143
|
}
|
|
135
144
|
scrollCleanupRef.current = null;
|
|
136
145
|
};
|
|
137
|
-
const doUnlock = ()
|
|
138
|
-
if (isUnlocked)
|
|
139
|
-
return;
|
|
146
|
+
const doUnlock = ()=>{
|
|
147
|
+
if (isUnlocked) return;
|
|
140
148
|
isUnlocked = true;
|
|
141
149
|
cleanup();
|
|
142
150
|
unlockScroll();
|
|
143
151
|
};
|
|
144
|
-
const resetDebounce = ()
|
|
152
|
+
const resetDebounce = ()=>{
|
|
145
153
|
if (debounceTimer) {
|
|
146
154
|
clearTimeout(debounceTimer);
|
|
147
155
|
}
|
|
148
156
|
debounceTimer = setTimeout(doUnlock, SCROLL_IDLE_MS);
|
|
149
157
|
};
|
|
150
|
-
const handleScrollActivity = ()
|
|
158
|
+
const handleScrollActivity = ()=>{
|
|
151
159
|
resetDebounce();
|
|
152
160
|
};
|
|
153
|
-
const handleScrollEnd = ()
|
|
161
|
+
const handleScrollEnd = ()=>{
|
|
154
162
|
doUnlock();
|
|
155
163
|
};
|
|
156
164
|
scrollTarget.addEventListener("scroll", handleScrollActivity, {
|
|
157
|
-
passive: true
|
|
165
|
+
passive: true
|
|
158
166
|
});
|
|
159
167
|
if ("onscrollend" in scrollTarget) {
|
|
160
168
|
scrollTarget.addEventListener("scrollend", handleScrollEnd, {
|
|
161
|
-
once: true
|
|
169
|
+
once: true
|
|
162
170
|
});
|
|
163
171
|
}
|
|
164
172
|
scrollCleanupRef.current = cleanup;
|
|
@@ -168,121 +176,117 @@ export function useDomet(sectionIds, containerRef = null, options = {}) {
|
|
|
168
176
|
const relativeTop = elementRect.top - containerRect.top + container.scrollTop;
|
|
169
177
|
container.scrollTo({
|
|
170
178
|
top: relativeTop - effectiveOffset,
|
|
171
|
-
behavior: scrollBehavior
|
|
179
|
+
behavior: scrollBehavior
|
|
172
180
|
});
|
|
173
|
-
}
|
|
174
|
-
else {
|
|
181
|
+
} else {
|
|
175
182
|
const absoluteTop = elementRect.top + window.scrollY;
|
|
176
183
|
window.scrollTo({
|
|
177
184
|
top: absoluteTop - effectiveOffset,
|
|
178
|
-
behavior: scrollBehavior
|
|
185
|
+
behavior: scrollBehavior
|
|
179
186
|
});
|
|
180
187
|
}
|
|
181
188
|
if (scrollBehavior === "instant") {
|
|
182
189
|
doUnlock();
|
|
183
|
-
}
|
|
184
|
-
else {
|
|
190
|
+
} else {
|
|
185
191
|
resetDebounce();
|
|
186
192
|
}
|
|
187
|
-
}, [
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
193
|
+
}, [
|
|
194
|
+
stableSectionIds,
|
|
195
|
+
containerElement,
|
|
196
|
+
getEffectiveOffset,
|
|
197
|
+
getScrollBehavior
|
|
198
|
+
]);
|
|
199
|
+
const sectionProps = useCallback((id)=>({
|
|
200
|
+
id,
|
|
201
|
+
ref: registerRef(id),
|
|
202
|
+
"data-domet": id
|
|
203
|
+
}), [
|
|
204
|
+
registerRef
|
|
205
|
+
]);
|
|
206
|
+
const navProps = useCallback((id)=>({
|
|
207
|
+
onClick: ()=>scrollToSection(id),
|
|
208
|
+
"aria-current": activeId === id ? "page" : undefined,
|
|
209
|
+
"data-active": activeId === id
|
|
210
|
+
}), [
|
|
211
|
+
activeId,
|
|
212
|
+
scrollToSection
|
|
213
|
+
]);
|
|
214
|
+
useEffect(()=>{
|
|
215
|
+
var _stableSectionIds_;
|
|
199
216
|
const idsSet = new Set(stableSectionIds);
|
|
200
|
-
for (const id of Object.keys(refs.current))
|
|
217
|
+
for (const id of Object.keys(refs.current)){
|
|
201
218
|
if (!idsSet.has(id)) {
|
|
202
219
|
delete refs.current[id];
|
|
203
220
|
}
|
|
204
221
|
}
|
|
205
|
-
for (const id of Object.keys(refCallbacks.current))
|
|
222
|
+
for (const id of Object.keys(refCallbacks.current)){
|
|
206
223
|
if (!idsSet.has(id)) {
|
|
207
224
|
delete refCallbacks.current[id];
|
|
208
225
|
}
|
|
209
226
|
}
|
|
210
227
|
const currentActive = activeIdRef.current;
|
|
211
|
-
const nextActive = currentActive && idsSet.has(currentActive)
|
|
212
|
-
? currentActive
|
|
213
|
-
: (stableSectionIds[0] ?? null);
|
|
228
|
+
const nextActive = currentActive && idsSet.has(currentActive) ? currentActive : (_stableSectionIds_ = stableSectionIds[0]) != null ? _stableSectionIds_ : null;
|
|
214
229
|
if (nextActive !== currentActive) {
|
|
215
230
|
activeIdRef.current = nextActive;
|
|
216
231
|
}
|
|
217
|
-
setActiveId((prev)
|
|
218
|
-
}, [
|
|
219
|
-
|
|
232
|
+
setActiveId((prev)=>prev !== nextActive ? nextActive : prev);
|
|
233
|
+
}, [
|
|
234
|
+
stableSectionIds
|
|
235
|
+
]);
|
|
236
|
+
const getSectionBounds = useCallback(()=>{
|
|
220
237
|
const container = containerElement;
|
|
221
238
|
const scrollTop = container ? container.scrollTop : window.scrollY;
|
|
222
239
|
const containerTop = container ? container.getBoundingClientRect().top : 0;
|
|
223
|
-
return stableSectionIds
|
|
224
|
-
.map((id) => {
|
|
240
|
+
return stableSectionIds.map((id)=>{
|
|
225
241
|
const el = refs.current[id];
|
|
226
|
-
if (!el)
|
|
227
|
-
return null;
|
|
242
|
+
if (!el) return null;
|
|
228
243
|
const rect = el.getBoundingClientRect();
|
|
229
|
-
const relativeTop = container
|
|
230
|
-
? rect.top - containerTop + scrollTop
|
|
231
|
-
: rect.top + window.scrollY;
|
|
244
|
+
const relativeTop = container ? rect.top - containerTop + scrollTop : rect.top + window.scrollY;
|
|
232
245
|
return {
|
|
233
246
|
id,
|
|
234
247
|
top: relativeTop,
|
|
235
248
|
bottom: relativeTop + rect.height,
|
|
236
|
-
height: rect.height
|
|
249
|
+
height: rect.height
|
|
237
250
|
};
|
|
238
|
-
})
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
251
|
+
}).filter((bounds)=>bounds !== null);
|
|
252
|
+
}, [
|
|
253
|
+
stableSectionIds,
|
|
254
|
+
containerElement
|
|
255
|
+
]);
|
|
256
|
+
const calculateActiveSection = useCallback(()=>{
|
|
257
|
+
if (isProgrammaticScrolling.current) return;
|
|
244
258
|
const container = containerElement;
|
|
245
259
|
const currentActiveId = activeIdRef.current;
|
|
246
260
|
const now = Date.now();
|
|
247
261
|
const scrollY = container ? container.scrollTop : window.scrollY;
|
|
248
|
-
const viewportHeight = container
|
|
249
|
-
|
|
250
|
-
: window.innerHeight;
|
|
251
|
-
const scrollHeight = container
|
|
252
|
-
? container.scrollHeight
|
|
253
|
-
: document.documentElement.scrollHeight;
|
|
262
|
+
const viewportHeight = container ? container.clientHeight : window.innerHeight;
|
|
263
|
+
const scrollHeight = container ? container.scrollHeight : document.documentElement.scrollHeight;
|
|
254
264
|
const maxScroll = Math.max(0, scrollHeight - viewportHeight);
|
|
255
265
|
const scrollProgress = maxScroll > 0 ? scrollY / maxScroll : 0;
|
|
256
|
-
const scrollDirection = scrollY === lastScrollY.current
|
|
257
|
-
? null
|
|
258
|
-
: scrollY > lastScrollY.current
|
|
259
|
-
? "down"
|
|
260
|
-
: "up";
|
|
266
|
+
const scrollDirection = scrollY === lastScrollY.current ? null : scrollY > lastScrollY.current ? "down" : "up";
|
|
261
267
|
const deltaTime = now - lastScrollTime.current;
|
|
262
268
|
const deltaY = scrollY - lastScrollY.current;
|
|
263
269
|
const velocity = deltaTime > 0 ? Math.abs(deltaY) / deltaTime : 0;
|
|
264
270
|
lastScrollY.current = scrollY;
|
|
265
271
|
lastScrollTime.current = now;
|
|
266
272
|
const sectionBounds = getSectionBounds();
|
|
267
|
-
if (sectionBounds.length === 0)
|
|
268
|
-
return;
|
|
273
|
+
if (sectionBounds.length === 0) return;
|
|
269
274
|
const baseOffset = getEffectiveOffset();
|
|
270
275
|
const effectiveOffset = Math.max(baseOffset, viewportHeight * offsetRatio);
|
|
271
276
|
const triggerLine = scrollY + effectiveOffset;
|
|
272
277
|
const viewportTop = scrollY;
|
|
273
278
|
const viewportBottom = scrollY + viewportHeight;
|
|
274
|
-
const scores = sectionBounds.map((section)
|
|
279
|
+
const scores = sectionBounds.map((section)=>{
|
|
280
|
+
var _sectionIndexMap_get;
|
|
275
281
|
const visibleTop = Math.max(section.top, viewportTop);
|
|
276
282
|
const visibleBottom = Math.min(section.bottom, viewportBottom);
|
|
277
283
|
const visibleHeight = Math.max(0, visibleBottom - visibleTop);
|
|
278
284
|
const visibilityRatio = section.height > 0 ? visibleHeight / section.height : 0;
|
|
279
285
|
const visibleInViewportRatio = viewportHeight > 0 ? visibleHeight / viewportHeight : 0;
|
|
280
286
|
const isInViewport = section.bottom > viewportTop && section.top < viewportBottom;
|
|
281
|
-
const sectionProgress = (()
|
|
282
|
-
if (section.height === 0)
|
|
283
|
-
return 0;
|
|
287
|
+
const sectionProgress = (()=>{
|
|
288
|
+
if (section.height === 0) return 0;
|
|
284
289
|
const entryPoint = viewportBottom;
|
|
285
|
-
const _exitPoint = viewportTop;
|
|
286
290
|
const totalTravel = viewportHeight + section.height;
|
|
287
291
|
const traveled = entryPoint - section.top;
|
|
288
292
|
return Math.max(0, Math.min(1, traveled / totalTravel));
|
|
@@ -290,15 +294,11 @@ export function useDomet(sectionIds, containerRef = null, options = {}) {
|
|
|
290
294
|
let score = 0;
|
|
291
295
|
if (visibilityRatio >= visibilityThreshold) {
|
|
292
296
|
score += 1000 + visibilityRatio * 500;
|
|
293
|
-
}
|
|
294
|
-
else if (isInViewport) {
|
|
297
|
+
} else if (isInViewport) {
|
|
295
298
|
score += visibleInViewportRatio * 800;
|
|
296
299
|
}
|
|
297
|
-
const sectionIndex = sectionIndexMap.get(section.id)
|
|
298
|
-
if (scrollDirection &&
|
|
299
|
-
isInViewport &&
|
|
300
|
-
section.top <= triggerLine &&
|
|
301
|
-
section.bottom > triggerLine) {
|
|
300
|
+
const sectionIndex = (_sectionIndexMap_get = sectionIndexMap.get(section.id)) != null ? _sectionIndexMap_get : 0;
|
|
301
|
+
if (scrollDirection && isInViewport && section.top <= triggerLine && section.bottom > triggerLine) {
|
|
302
302
|
score += 200;
|
|
303
303
|
}
|
|
304
304
|
score -= sectionIndex * 0.1;
|
|
@@ -308,7 +308,7 @@ export function useDomet(sectionIds, containerRef = null, options = {}) {
|
|
|
308
308
|
visibilityRatio,
|
|
309
309
|
isInViewport,
|
|
310
310
|
bounds: section,
|
|
311
|
-
progress: sectionProgress
|
|
311
|
+
progress: sectionProgress
|
|
312
312
|
};
|
|
313
313
|
});
|
|
314
314
|
const hasScroll = maxScroll > 10;
|
|
@@ -317,39 +317,34 @@ export function useDomet(sectionIds, containerRef = null, options = {}) {
|
|
|
317
317
|
let newActiveId = null;
|
|
318
318
|
if (isAtBottom && stableSectionIds.length > 0) {
|
|
319
319
|
newActiveId = stableSectionIds[stableSectionIds.length - 1];
|
|
320
|
-
}
|
|
321
|
-
else if (isAtTop && stableSectionIds.length > 0) {
|
|
320
|
+
} else if (isAtTop && stableSectionIds.length > 0) {
|
|
322
321
|
newActiveId = stableSectionIds[0];
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
const visibleScores = scores.filter((s) => s.isInViewport);
|
|
322
|
+
} else {
|
|
323
|
+
const visibleScores = scores.filter((s)=>s.isInViewport);
|
|
326
324
|
const candidates = visibleScores.length > 0 ? visibleScores : scores;
|
|
327
|
-
candidates.sort((a, b)
|
|
325
|
+
candidates.sort((a, b)=>b.score - a.score);
|
|
328
326
|
if (candidates.length > 0) {
|
|
329
327
|
const bestCandidate = candidates[0];
|
|
330
|
-
const currentScore = scores.find((s)
|
|
331
|
-
const shouldSwitch = !currentScore ||
|
|
332
|
-
!currentScore.isInViewport ||
|
|
333
|
-
bestCandidate.score > currentScore.score + hysteresisMargin ||
|
|
334
|
-
bestCandidate.id === currentActiveId;
|
|
328
|
+
const currentScore = scores.find((s)=>s.id === currentActiveId);
|
|
329
|
+
const shouldSwitch = !currentScore || !currentScore.isInViewport || bestCandidate.score > currentScore.score + hysteresisMargin || bestCandidate.id === currentActiveId;
|
|
335
330
|
newActiveId = shouldSwitch ? bestCandidate.id : currentActiveId;
|
|
336
331
|
}
|
|
337
332
|
}
|
|
338
333
|
if (newActiveId !== currentActiveId) {
|
|
339
334
|
activeIdRef.current = newActiveId;
|
|
340
335
|
setActiveId(newActiveId);
|
|
341
|
-
callbackRefs.current.onActiveChange
|
|
336
|
+
callbackRefs.current.onActiveChange == null ? void 0 : callbackRefs.current.onActiveChange.call(callbackRefs.current, newActiveId, currentActiveId);
|
|
342
337
|
}
|
|
343
|
-
const currentInViewport = new Set(scores.filter((s)
|
|
338
|
+
const currentInViewport = new Set(scores.filter((s)=>s.isInViewport).map((s)=>s.id));
|
|
344
339
|
const prevInViewport = prevSectionsInViewport.current;
|
|
345
|
-
for (const id of currentInViewport)
|
|
340
|
+
for (const id of currentInViewport){
|
|
346
341
|
if (!prevInViewport.has(id)) {
|
|
347
|
-
callbackRefs.current.onSectionEnter
|
|
342
|
+
callbackRefs.current.onSectionEnter == null ? void 0 : callbackRefs.current.onSectionEnter.call(callbackRefs.current, id);
|
|
348
343
|
}
|
|
349
344
|
}
|
|
350
|
-
for (const id of prevInViewport)
|
|
345
|
+
for (const id of prevInViewport){
|
|
351
346
|
if (!currentInViewport.has(id)) {
|
|
352
|
-
callbackRefs.current.onSectionLeave
|
|
347
|
+
callbackRefs.current.onSectionLeave == null ? void 0 : callbackRefs.current.onSectionLeave.call(callbackRefs.current, id);
|
|
353
348
|
}
|
|
354
349
|
}
|
|
355
350
|
prevSectionsInViewport.current = currentInViewport;
|
|
@@ -361,20 +356,20 @@ export function useDomet(sectionIds, containerRef = null, options = {}) {
|
|
|
361
356
|
isScrolling: isScrollingRef.current,
|
|
362
357
|
maxScroll,
|
|
363
358
|
viewportHeight,
|
|
364
|
-
offset: effectiveOffset
|
|
359
|
+
offset: effectiveOffset
|
|
365
360
|
};
|
|
366
361
|
const newSections = {};
|
|
367
|
-
for (const s of scores)
|
|
362
|
+
for (const s of scores){
|
|
368
363
|
newSections[s.id] = {
|
|
369
364
|
bounds: {
|
|
370
365
|
top: s.bounds.top,
|
|
371
366
|
bottom: s.bounds.bottom,
|
|
372
|
-
height: s.bounds.height
|
|
367
|
+
height: s.bounds.height
|
|
373
368
|
},
|
|
374
369
|
visibility: Math.round(s.visibilityRatio * 100) / 100,
|
|
375
370
|
progress: Math.round(s.progress * 100) / 100,
|
|
376
371
|
isInViewport: s.isInViewport,
|
|
377
|
-
isActive: s.id === newActiveId
|
|
372
|
+
isActive: s.id === newActiveId
|
|
378
373
|
};
|
|
379
374
|
}
|
|
380
375
|
setScroll(newScrollState);
|
|
@@ -387,31 +382,37 @@ export function useDomet(sectionIds, containerRef = null, options = {}) {
|
|
|
387
382
|
visibilityThreshold,
|
|
388
383
|
hysteresisMargin,
|
|
389
384
|
getSectionBounds,
|
|
390
|
-
containerElement
|
|
385
|
+
containerElement
|
|
391
386
|
]);
|
|
392
387
|
recalculateRef.current = calculateActiveSection;
|
|
393
|
-
useEffect(()
|
|
388
|
+
useEffect(()=>{
|
|
394
389
|
const container = containerElement;
|
|
395
390
|
const scrollTarget = container || window;
|
|
396
|
-
const scheduleCalculate = ()
|
|
391
|
+
const scheduleCalculate = ()=>{
|
|
397
392
|
if (rafId.current) {
|
|
398
393
|
cancelAnimationFrame(rafId.current);
|
|
399
394
|
}
|
|
400
|
-
rafId.current = requestAnimationFrame(()
|
|
395
|
+
rafId.current = requestAnimationFrame(()=>{
|
|
401
396
|
rafId.current = null;
|
|
402
397
|
calculateActiveSection();
|
|
403
398
|
});
|
|
404
399
|
};
|
|
405
|
-
const handleScrollEnd = ()
|
|
400
|
+
const handleScrollEnd = ()=>{
|
|
406
401
|
isScrollingRef.current = false;
|
|
407
|
-
setScroll((prev)
|
|
408
|
-
|
|
402
|
+
setScroll((prev)=>({
|
|
403
|
+
...prev,
|
|
404
|
+
isScrolling: false
|
|
405
|
+
}));
|
|
406
|
+
callbackRefs.current.onScrollEnd == null ? void 0 : callbackRefs.current.onScrollEnd.call(callbackRefs.current);
|
|
409
407
|
};
|
|
410
|
-
const handleScroll = ()
|
|
408
|
+
const handleScroll = ()=>{
|
|
411
409
|
if (!isScrollingRef.current) {
|
|
412
410
|
isScrollingRef.current = true;
|
|
413
|
-
setScroll((prev)
|
|
414
|
-
|
|
411
|
+
setScroll((prev)=>({
|
|
412
|
+
...prev,
|
|
413
|
+
isScrolling: true
|
|
414
|
+
}));
|
|
415
|
+
callbackRefs.current.onScrollStart == null ? void 0 : callbackRefs.current.onScrollStart.call(callbackRefs.current);
|
|
415
416
|
}
|
|
416
417
|
if (scrollIdleTimeoutRef.current) {
|
|
417
418
|
clearTimeout(scrollIdleTimeoutRef.current);
|
|
@@ -427,7 +428,7 @@ export function useDomet(sectionIds, containerRef = null, options = {}) {
|
|
|
427
428
|
clearTimeout(throttleTimeoutId.current);
|
|
428
429
|
}
|
|
429
430
|
scheduleCalculate();
|
|
430
|
-
throttleTimeoutId.current = setTimeout(()
|
|
431
|
+
throttleTimeoutId.current = setTimeout(()=>{
|
|
431
432
|
isThrottled.current = false;
|
|
432
433
|
throttleTimeoutId.current = null;
|
|
433
434
|
if (hasPendingScroll.current) {
|
|
@@ -436,16 +437,20 @@ export function useDomet(sectionIds, containerRef = null, options = {}) {
|
|
|
436
437
|
}
|
|
437
438
|
}, debounceMs);
|
|
438
439
|
};
|
|
439
|
-
const handleResize = ()
|
|
440
|
+
const handleResize = ()=>{
|
|
440
441
|
scheduleCalculate();
|
|
441
442
|
};
|
|
442
443
|
calculateActiveSection();
|
|
443
|
-
const deferredRecalcId = setTimeout(()
|
|
444
|
+
const deferredRecalcId = setTimeout(()=>{
|
|
444
445
|
calculateActiveSection();
|
|
445
446
|
}, 0);
|
|
446
|
-
scrollTarget.addEventListener("scroll", handleScroll, {
|
|
447
|
-
|
|
448
|
-
|
|
447
|
+
scrollTarget.addEventListener("scroll", handleScroll, {
|
|
448
|
+
passive: true
|
|
449
|
+
});
|
|
450
|
+
window.addEventListener("resize", handleResize, {
|
|
451
|
+
passive: true
|
|
452
|
+
});
|
|
453
|
+
return ()=>{
|
|
449
454
|
clearTimeout(deferredRecalcId);
|
|
450
455
|
scrollTarget.removeEventListener("scroll", handleScroll);
|
|
451
456
|
window.removeEventListener("resize", handleResize);
|
|
@@ -465,18 +470,25 @@ export function useDomet(sectionIds, containerRef = null, options = {}) {
|
|
|
465
470
|
clearTimeout(scrollIdleTimeoutRef.current);
|
|
466
471
|
scrollIdleTimeoutRef.current = null;
|
|
467
472
|
}
|
|
468
|
-
scrollCleanupRef.current
|
|
473
|
+
scrollCleanupRef.current == null ? void 0 : scrollCleanupRef.current.call(scrollCleanupRef);
|
|
469
474
|
isThrottled.current = false;
|
|
470
475
|
hasPendingScroll.current = false;
|
|
471
476
|
isProgrammaticScrolling.current = false;
|
|
472
477
|
isScrollingRef.current = false;
|
|
473
478
|
};
|
|
474
|
-
}, [
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
479
|
+
}, [
|
|
480
|
+
calculateActiveSection,
|
|
481
|
+
debounceMs,
|
|
482
|
+
containerElement
|
|
483
|
+
]);
|
|
484
|
+
const activeIndex = useMemo(()=>{
|
|
485
|
+
var _sectionIndexMap_get;
|
|
486
|
+
if (!activeId) return -1;
|
|
487
|
+
return (_sectionIndexMap_get = sectionIndexMap.get(activeId)) != null ? _sectionIndexMap_get : -1;
|
|
488
|
+
}, [
|
|
489
|
+
activeId,
|
|
490
|
+
sectionIndexMap
|
|
491
|
+
]);
|
|
480
492
|
return {
|
|
481
493
|
activeId,
|
|
482
494
|
activeIndex,
|
|
@@ -485,7 +497,8 @@ export function useDomet(sectionIds, containerRef = null, options = {}) {
|
|
|
485
497
|
registerRef,
|
|
486
498
|
scrollToSection,
|
|
487
499
|
sectionProps,
|
|
488
|
-
navProps
|
|
500
|
+
navProps
|
|
489
501
|
};
|
|
490
502
|
}
|
|
491
|
-
|
|
503
|
+
|
|
504
|
+
export { useDomet as default, useDomet };
|