@xwadex/fesd-next 0.3.4-7.3 → 0.3.4-7.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/TestComponent.js +5 -5
- package/dist/hooks/index.d.ts +2 -0
- package/dist/hooks/index.js +2 -0
- package/dist/hooks/useAnchors.d.ts +40 -20
- package/dist/hooks/useAnchors.js +138 -95
- package/dist/hooks/useHash.d.ts +1 -0
- package/dist/hooks/useHash.js +19 -0
- package/package.json +1 -1
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useAnchors } from "../hooks/index.js";
|
|
4
|
+
import { cn } from "../utils";
|
|
4
5
|
const TestComponent = () => {
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
return (_jsx(_Fragment, { children: "TestComponent content" }));
|
|
6
|
+
const { registerAnchors, registerContainers, scrollToAnchor } = useAnchors();
|
|
7
|
+
return (_jsxs("div", { children: [_jsxs("div", { className: "flex flex-col gap-3 fixed top-0 right-0", children: [_jsx("button", { onClick: () => scrollToAnchor({ anchor: "#", }), children: "Scoll to Top" }), _jsx("button", { onClick: () => scrollToAnchor({ anchor: "50", }), children: "Scoll to 50" }), _jsx("button", { onClick: () => scrollToAnchor({ anchor: "100", }), children: "Scoll to 100" })] }), _jsx("h2", { className: "text-xl", children: "TestComponent content" }), _jsx("div", { className: cn("flex flex-col gap-5"), children: Array.from({ length: 200 }).map((_, index) => _jsx("div", { className: cn("p-3", "bg-indigo-600"), ...registerAnchors(String(index)), children: index }, index)) })] }));
|
|
8
8
|
};
|
|
9
9
|
TestComponent.displayName = "TestComponent";
|
|
10
10
|
export default TestComponent;
|
package/dist/hooks/index.d.ts
CHANGED
package/dist/hooks/index.js
CHANGED
|
@@ -1,41 +1,60 @@
|
|
|
1
1
|
import { Easing } from "motion/react";
|
|
2
2
|
export interface PropsTypes {
|
|
3
3
|
}
|
|
4
|
-
export interface
|
|
5
|
-
anchor?: string;
|
|
6
|
-
container: string;
|
|
7
|
-
offseters?: string | string[];
|
|
8
|
-
direction?: "horizontal" | "vertical";
|
|
4
|
+
export interface AnimationOptions {
|
|
9
5
|
duration?: number;
|
|
10
|
-
offset?: number;
|
|
11
6
|
delay?: number;
|
|
12
7
|
ease?: Easing | Easing[] | undefined;
|
|
13
8
|
onScroll?: () => void;
|
|
14
9
|
onScrolling?: (value: number) => void;
|
|
15
10
|
onScrolled?: () => void;
|
|
16
11
|
}
|
|
12
|
+
export interface AnchorActionsOptions extends AnimationOptions {
|
|
13
|
+
anchor: string;
|
|
14
|
+
container?: string;
|
|
15
|
+
offseters?: string | string[];
|
|
16
|
+
direction?: "x" | "y" | "xy";
|
|
17
|
+
align?: "start" | "center" | "end";
|
|
18
|
+
offset?: number;
|
|
19
|
+
}
|
|
20
|
+
export interface ScrollAnimationsOptions extends AnimationOptions {
|
|
21
|
+
containerDom: HTMLElement | Window;
|
|
22
|
+
scrollValue: {
|
|
23
|
+
x: number;
|
|
24
|
+
y: number;
|
|
25
|
+
};
|
|
26
|
+
containerScrollValue: {
|
|
27
|
+
left: number;
|
|
28
|
+
top: number;
|
|
29
|
+
};
|
|
30
|
+
direction?: AnchorActionsOptions["direction"];
|
|
31
|
+
}
|
|
17
32
|
export declare function useAnchors(): {
|
|
18
33
|
registerAnchors: typeof registerAnchors;
|
|
19
34
|
registerContainers: typeof registerContainers;
|
|
20
35
|
registerOffseters: typeof registerOffseters;
|
|
36
|
+
setStores: (options: SetAnchorStoreOptions) => void;
|
|
21
37
|
getStores: () => AnchorsStores;
|
|
22
|
-
|
|
23
|
-
removeOffseter: (name: string) => void;
|
|
24
|
-
removeContainer: (name: string) => void;
|
|
25
|
-
scrollToAnchor: ({ anchor: anchorName, container: containerName, ...anchorOptions }: AnchorActionsOptions) => void;
|
|
38
|
+
scrollToAnchor: (anchorOptions: AnchorActionsOptions) => void;
|
|
26
39
|
};
|
|
40
|
+
export interface RegistrationProps {
|
|
41
|
+
name: string;
|
|
42
|
+
key: keyof AnchorsStores;
|
|
43
|
+
node: HTMLDivElement | null;
|
|
44
|
+
}
|
|
45
|
+
export declare function registration({ name, key, node }: RegistrationProps): void;
|
|
27
46
|
export declare function registerAnchors(name: string): {
|
|
28
47
|
ref: (node: HTMLDivElement | null) => void;
|
|
29
48
|
"data-anchor": string;
|
|
30
49
|
};
|
|
31
|
-
export declare function registerOffseters(name: string): {
|
|
32
|
-
ref: (node: HTMLDivElement | null) => void;
|
|
33
|
-
"data-anchor-offseter": string;
|
|
34
|
-
};
|
|
35
50
|
export declare function registerContainers(name: string): {
|
|
36
51
|
ref: (node: HTMLDivElement | null) => void;
|
|
37
52
|
"data-anchor-container": string;
|
|
38
53
|
};
|
|
54
|
+
export declare function registerOffseters(name: string): {
|
|
55
|
+
ref: (node: HTMLDivElement | null) => void;
|
|
56
|
+
"data-anchor-offseter": string;
|
|
57
|
+
};
|
|
39
58
|
export type AnchorsStores = {
|
|
40
59
|
anchors: Map<string, HTMLElement | null>;
|
|
41
60
|
containers: Map<string, HTMLElement | null>;
|
|
@@ -43,9 +62,10 @@ export type AnchorsStores = {
|
|
|
43
62
|
};
|
|
44
63
|
export declare const useAnchorsStores: import("zustand").UseBoundStore<import("zustand").StoreApi<AnchorsStores>>;
|
|
45
64
|
export declare const getAnchorsStores: () => AnchorsStores;
|
|
46
|
-
export
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
65
|
+
export interface SetAnchorStoreOptions {
|
|
66
|
+
key: keyof AnchorsStores;
|
|
67
|
+
action: "add" | "remove";
|
|
68
|
+
name: string;
|
|
69
|
+
node?: HTMLElement | null;
|
|
70
|
+
}
|
|
71
|
+
export declare const setAnchorStore: (options: SetAnchorStoreOptions) => void;
|
package/dist/hooks/useAnchors.js
CHANGED
|
@@ -1,122 +1,165 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
// update: 2025.10.
|
|
3
|
-
// version: 0.0.1
|
|
2
|
+
// update: 2025.10.30
|
|
3
|
+
// version: 0.0.1.bate
|
|
4
4
|
// dev: wade
|
|
5
5
|
import { useCallback, useMemo, useRef } from "react";
|
|
6
6
|
import { animate, useMotionValue } from "motion/react";
|
|
7
7
|
import { create } from "zustand";
|
|
8
8
|
export function useAnchors() {
|
|
9
9
|
const motionValue = useMotionValue(0);
|
|
10
|
-
const
|
|
11
|
-
const
|
|
12
|
-
const {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
return;
|
|
22
|
-
const { top: targetTop } = anchorDom.getBoundingClientRect();
|
|
23
|
-
const { top: containerTop } = containerDom.getBoundingClientRect();
|
|
24
|
-
const containerScrollTop = Math.round(containerDom.scrollTop);
|
|
25
|
-
const scrollOffset = Math.round(targetTop - containerTop + containerScrollTop);
|
|
26
|
-
if (scrollOffset == containerScrollTop)
|
|
27
|
-
return;
|
|
28
|
-
if (controlsRef.current)
|
|
29
|
-
controlsRef.current?.stop();
|
|
30
|
-
// console.log("scrollOffset", scrollOffset);
|
|
31
|
-
// console.log("containerScrollTop", containerScrollTop);
|
|
32
|
-
// console.log(scrollOffset == containerScrollTop);
|
|
33
|
-
motionValue.set(containerScrollTop);
|
|
34
|
-
controlsRef.current = animate(motionValue, scrollOffset, {
|
|
10
|
+
const animateRef = useRef(null);
|
|
11
|
+
const scrollAnimations = useCallback(({ containerDom, scrollValue, containerScrollValue, direction = "y", duration = 0.5, delay = 0, ease = [0.215, 0.61, 0.355, 1.0], onScroll, onScrolled, onScrolling }) => {
|
|
12
|
+
const { left: scrollLeft, top: scrollTop } = containerScrollValue;
|
|
13
|
+
const { x: scrollX, y: scrollY } = scrollValue;
|
|
14
|
+
const isScrollX = direction === "x";
|
|
15
|
+
const isScrollY = direction === "y";
|
|
16
|
+
const isBoth = direction === "xy";
|
|
17
|
+
const animationsOptions = {
|
|
18
|
+
duration,
|
|
19
|
+
ease,
|
|
20
|
+
delay,
|
|
35
21
|
onPlay: onScroll,
|
|
22
|
+
onUpdate: onUpdate,
|
|
36
23
|
onComplete: onScrolled,
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
}
|
|
43
|
-
|
|
24
|
+
};
|
|
25
|
+
if ((isBoth || isScrollX) && (scrollX !== scrollLeft)) {
|
|
26
|
+
motionValue.set(scrollLeft);
|
|
27
|
+
animateRef.current = animate(motionValue, scrollX, animationsOptions);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
if ((isBoth || isScrollY) && (scrollY !== scrollTop)) {
|
|
31
|
+
motionValue.set(scrollTop);
|
|
32
|
+
animateRef.current = animate(motionValue, scrollY, animationsOptions);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
function onUpdate(value) {
|
|
36
|
+
if (isScrollX)
|
|
37
|
+
containerDom.scrollTo({ left: value });
|
|
38
|
+
if (isScrollY)
|
|
39
|
+
containerDom.scrollTo({ top: value });
|
|
40
|
+
if (isBoth)
|
|
41
|
+
containerDom.scrollTo({ top: value, left: value });
|
|
42
|
+
onScrolling?.(value);
|
|
43
|
+
}
|
|
44
|
+
}, []);
|
|
45
|
+
const scrollToAnchor = useCallback((anchorOptions) => {
|
|
46
|
+
if (animateRef.current)
|
|
47
|
+
animateRef.current?.stop();
|
|
48
|
+
const { anchor: anchorName, container: containerName, offseters, align, offset = 0, ...options } = anchorOptions;
|
|
49
|
+
const isContainer = containerName && containerName !== "window";
|
|
50
|
+
const stores = getAnchorsStores();
|
|
51
|
+
const containerDom = isContainer
|
|
52
|
+
? stores.containers.get(containerName)
|
|
53
|
+
: window;
|
|
54
|
+
const containerScrollValue = {
|
|
55
|
+
top: Math.round(isContainer
|
|
56
|
+
? containerDom.scrollTop
|
|
57
|
+
: containerDom.pageYOffset),
|
|
58
|
+
left: Math.round(isContainer
|
|
59
|
+
? containerDom.scrollLeft
|
|
60
|
+
: pageXOffset),
|
|
61
|
+
};
|
|
62
|
+
const offsetValue = Array.isArray(offseters)
|
|
63
|
+
? offseters.reduce((init, offseter) => {
|
|
64
|
+
const dom = stores.offseters.get(offseter);
|
|
65
|
+
const width = (init.width ?? 0) + (dom ? dom.clientWidth : 0);
|
|
66
|
+
const height = (init.height ?? 0) + (dom ? dom.clientHeight : 0);
|
|
67
|
+
return { width, height };
|
|
68
|
+
}, {})
|
|
69
|
+
: { width: 0, height: 0 };
|
|
70
|
+
const scrollValue = {
|
|
71
|
+
x: 0 - offsetValue.width,
|
|
72
|
+
y: 0 - offsetValue.height
|
|
73
|
+
};
|
|
74
|
+
const AnimationsOptions = {
|
|
75
|
+
containerDom, containerScrollValue, scrollValue, ...options
|
|
76
|
+
};
|
|
77
|
+
if (anchorName !== "#") {
|
|
78
|
+
const anchorDom = stores.anchors.get(anchorName);
|
|
79
|
+
if (!anchorDom)
|
|
80
|
+
return;
|
|
81
|
+
const anchorRect = anchorDom.getBoundingClientRect();
|
|
82
|
+
const containerRect = {
|
|
83
|
+
left: (isContainer ? containerDom.getBoundingClientRect().left : 0),
|
|
84
|
+
top: (isContainer ? containerDom.getBoundingClientRect().top : 0)
|
|
85
|
+
};
|
|
86
|
+
const offsets = {
|
|
87
|
+
x: offsetValue.width - offset,
|
|
88
|
+
y: offsetValue.height - offset
|
|
89
|
+
};
|
|
90
|
+
AnimationsOptions.scrollValue = {
|
|
91
|
+
x: Math.round(anchorRect.left + containerScrollValue.left - offsets.x - containerRect.left),
|
|
92
|
+
y: Math.round(anchorRect.top + containerScrollValue.top - offsets.y - containerRect.top),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
scrollAnimations(AnimationsOptions);
|
|
96
|
+
}, []);
|
|
44
97
|
const returnsMemo = useMemo(() => ({
|
|
45
98
|
registerAnchors,
|
|
46
99
|
registerContainers,
|
|
47
100
|
registerOffseters,
|
|
101
|
+
setStores: setAnchorStore,
|
|
48
102
|
getStores: getAnchorsStores,
|
|
49
|
-
removeAnchor: removeAnchor,
|
|
50
|
-
removeOffseter: removeOffseter,
|
|
51
|
-
removeContainer: removeContainer,
|
|
52
103
|
scrollToAnchor
|
|
53
104
|
}), [scrollToAnchor]);
|
|
54
105
|
return returnsMemo;
|
|
55
106
|
}
|
|
56
|
-
export function
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
}
|
|
107
|
+
export function registration({ name, key, node }) {
|
|
108
|
+
if (!node)
|
|
109
|
+
return;
|
|
110
|
+
if (name == "#") {
|
|
111
|
+
console.error(`Cannot register ${key} name '#'`);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (node instanceof Window || node.tagName === "BODY" || node.tagName === "HTML") {
|
|
115
|
+
console.error(`Cannot register ${key} 'body' or 'html' `);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
setAnchorStore({ key, name, node, action: node ? "add" : "remove" });
|
|
67
119
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
};
|
|
75
|
-
return {
|
|
76
|
-
ref: register,
|
|
77
|
-
"data-anchor-offseter": name
|
|
78
|
-
};
|
|
120
|
+
const ANCHOR_ATTRIBUTE = "data-anchor";
|
|
121
|
+
const CONTAINER_ATTRIBUTE = "data-anchor-container";
|
|
122
|
+
const OFFSETER_ATTRIBUTE = "data-anchor-offseter";
|
|
123
|
+
export function registerAnchors(name) {
|
|
124
|
+
const register = (node) => registration({ key: "anchors", name, node });
|
|
125
|
+
return { ref: register, [ANCHOR_ATTRIBUTE]: name };
|
|
79
126
|
}
|
|
80
127
|
export function registerContainers(name) {
|
|
81
|
-
const register = (node) => {
|
|
82
|
-
|
|
83
|
-
addContainer(name, node);
|
|
84
|
-
else
|
|
85
|
-
removeContainer(name);
|
|
86
|
-
};
|
|
87
|
-
return {
|
|
88
|
-
ref: register,
|
|
89
|
-
"data-anchor-container": name
|
|
90
|
-
};
|
|
128
|
+
const register = (node) => registration({ key: "containers", name, node });
|
|
129
|
+
return { ref: register, [CONTAINER_ATTRIBUTE]: name };
|
|
91
130
|
}
|
|
92
|
-
|
|
131
|
+
export function registerOffseters(name) {
|
|
132
|
+
const register = (node) => registration({ key: "offseters", name, node });
|
|
133
|
+
return { ref: register, [OFFSETER_ATTRIBUTE]: name };
|
|
134
|
+
}
|
|
135
|
+
export const useAnchorsStores = create()(() => ({
|
|
93
136
|
anchors: new Map(),
|
|
94
137
|
containers: new Map(),
|
|
95
138
|
offseters: new Map(),
|
|
96
|
-
};
|
|
97
|
-
export const useAnchorsStores = create()(() => anchorsStores);
|
|
139
|
+
}));
|
|
98
140
|
export const getAnchorsStores = () => useAnchorsStores.getState();
|
|
99
|
-
export const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
141
|
+
export const setAnchorStore = (options) => useAnchorsStores.setState(stores => {
|
|
142
|
+
const { key, action, name, node } = options;
|
|
143
|
+
switch (key) {
|
|
144
|
+
case "anchors":
|
|
145
|
+
if (action == "add" && node instanceof HTMLElement)
|
|
146
|
+
stores.anchors?.set(name, node);
|
|
147
|
+
if (action == "remove")
|
|
148
|
+
stores.anchors?.delete(name);
|
|
149
|
+
return { anchors: stores.anchors };
|
|
150
|
+
case "containers":
|
|
151
|
+
if (action == "add" && node instanceof HTMLElement)
|
|
152
|
+
stores.containers?.set(name, node);
|
|
153
|
+
if (action == "remove")
|
|
154
|
+
stores.containers?.delete(name);
|
|
155
|
+
return { containers: stores.containers };
|
|
156
|
+
case "offseters":
|
|
157
|
+
if (action == "add" && node instanceof HTMLElement)
|
|
158
|
+
stores.offseters?.set(name, node);
|
|
159
|
+
if (action == "remove")
|
|
160
|
+
stores.offseters?.delete(name);
|
|
161
|
+
return { offseters: stores.offseters };
|
|
162
|
+
default:
|
|
163
|
+
return stores;
|
|
164
|
+
}
|
|
122
165
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function useHash(): string;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
export function useHash() {
|
|
4
|
+
const [hash, setHash] = useState("");
|
|
5
|
+
const updateHash = () => setHash(window.location.hash);
|
|
6
|
+
const hashchangeEvent = (e) => {
|
|
7
|
+
e.preventDefault?.();
|
|
8
|
+
updateHash();
|
|
9
|
+
};
|
|
10
|
+
// 初始化
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
updateHash();
|
|
13
|
+
window.addEventListener("hashchange", hashchangeEvent);
|
|
14
|
+
return () => {
|
|
15
|
+
window.removeEventListener("hashchange", hashchangeEvent);
|
|
16
|
+
};
|
|
17
|
+
}, []);
|
|
18
|
+
return hash;
|
|
19
|
+
}
|