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