@qingwu-ui/scroll-fab 0.9.0-beta.1 → 0.9.0-beta.2
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 +96 -0
- package/dist/index.cjs +193 -16
- package/dist/index.d.cts +90 -5
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +90 -5
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +193 -16
- package/dist/index.mjs.map +1 -1
- package/dist/style.css +32 -2
- package/package.json +14 -2
package/dist/index.d.mts
CHANGED
|
@@ -7,6 +7,46 @@ interface ScrollFabPosition {
|
|
|
7
7
|
bottom?: number;
|
|
8
8
|
left?: number;
|
|
9
9
|
}
|
|
10
|
+
/** Lenis 实例的最小结构性类型(组件不硬依赖 lenis 包,按结构识别) */
|
|
11
|
+
interface LenisLike {
|
|
12
|
+
scrollTo: (target: number | string | HTMLElement, options?: Record<string, unknown>) => void;
|
|
13
|
+
raf?: (time: number) => void;
|
|
14
|
+
destroy?: () => void;
|
|
15
|
+
start?: () => void;
|
|
16
|
+
stop?: () => void;
|
|
17
|
+
on?: (event: string, callback: (...args: unknown[]) => void) => void;
|
|
18
|
+
off?: (event: string, callback: (...args: unknown[]) => void) => void;
|
|
19
|
+
isScrolling?: boolean;
|
|
20
|
+
}
|
|
21
|
+
/** 内部自建 Lenis 时的构造配置(lenis 官方选项子集,允许透传其他字段) */
|
|
22
|
+
interface ScrollFabLenisConfig {
|
|
23
|
+
wrapper?: HTMLElement | Window;
|
|
24
|
+
content?: HTMLElement;
|
|
25
|
+
duration?: number;
|
|
26
|
+
easing?: (t: number) => number;
|
|
27
|
+
/** 默认强制 false:自建实例不劫持用户滚轮,只驱动按钮触发的程序滚动;显式置 true 则接管全局 */
|
|
28
|
+
smoothWheel?: boolean;
|
|
29
|
+
smoothTouch?: boolean;
|
|
30
|
+
wheelMultiplier?: number;
|
|
31
|
+
touchMultiplier?: number;
|
|
32
|
+
[key: string]: unknown;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* 异形按钮形状(如银杏叶)。
|
|
36
|
+
* 双 path 才能保证进度描边「完全包裹」:fill 是叶片实体,outline 是外扩一圈的闭合轮廓,
|
|
37
|
+
* SVG 描边居中于路径,单条 path 必有一半笔画压进形状内部。
|
|
38
|
+
* outline 的起点约定在 12 点钟方向(描边顺时针推进)。
|
|
39
|
+
*/
|
|
40
|
+
interface ScrollFabShape {
|
|
41
|
+
/** SVG viewBox,默认 "0 0 48 48";自定义 path 建议按 48×48 绘制 */
|
|
42
|
+
viewBox?: string;
|
|
43
|
+
/** 形状实体 path d(被填充) */
|
|
44
|
+
fill: string;
|
|
45
|
+
/** 外扩进度轮廓 path d(track + 进度描边沿它走);可与 fill 同形再配 outlineTransform 外扩 */
|
|
46
|
+
outline: string;
|
|
47
|
+
/** 作用于 outline path 的 SVG transform,如 "translate(24 24) scale(1.1) translate(-24 -24)" */
|
|
48
|
+
outlineTransform?: string;
|
|
49
|
+
}
|
|
10
50
|
interface ScrollFabOptions {
|
|
11
51
|
/** 滚动容器,null(默认)为 window */
|
|
12
52
|
target?: HTMLElement | null;
|
|
@@ -28,6 +68,18 @@ interface ScrollFabOptions {
|
|
|
28
68
|
className?: string;
|
|
29
69
|
/** 动画开关,关闭或 prefers-reduced-motion 时程序滚动瞬时到位,默认 true */
|
|
30
70
|
animate?: boolean;
|
|
71
|
+
/**
|
|
72
|
+
* Lenis 平滑滚动(lenis 为 optional peerDependency,未安装静默回退内置 rAF):
|
|
73
|
+
* - 传 Lenis 实例:复用宿主实例,组件不负责销毁;
|
|
74
|
+
* - true:点击时动态 import lenis 并自建实例(仅驱动按钮程序滚动,不劫持滚轮);
|
|
75
|
+
* - 传配置对象:自建实例并透传配置(target 容器自动映射为 wrapper);
|
|
76
|
+
* - false / 缺省:内置 rAF 缓动。
|
|
77
|
+
*/
|
|
78
|
+
lenis?: boolean | ScrollFabLenisConfig | LenisLike;
|
|
79
|
+
/** 异形形状(银杏叶等);传入后圆形外壳消失,进度沿外扩轮廓包裹 */
|
|
80
|
+
shape?: ScrollFabShape;
|
|
81
|
+
/** 自建 Lenis 实例就绪回调(外部实例注入时不触发),与 onEditorReady 模式对齐 */
|
|
82
|
+
onLenisReady?: (lenis: LenisLike) => void;
|
|
31
83
|
/** 模式①(滚动到底部)无障碍标签 */
|
|
32
84
|
ariaLabelToBottom?: string;
|
|
33
85
|
/** 模式②(返回顶部)无障碍标签 */
|
|
@@ -39,11 +91,13 @@ interface ScrollFabOptions {
|
|
|
39
91
|
//#region src/scroll-fab.d.ts
|
|
40
92
|
declare class ScrollFab {
|
|
41
93
|
readonly el: HTMLButtonElement;
|
|
42
|
-
private
|
|
43
|
-
private
|
|
94
|
+
private ring;
|
|
95
|
+
private track;
|
|
96
|
+
private fillPath;
|
|
44
97
|
private readonly icon;
|
|
45
|
-
private
|
|
98
|
+
private len;
|
|
46
99
|
private readonly targetEl;
|
|
100
|
+
private readonly shape;
|
|
47
101
|
private readonly ringMs;
|
|
48
102
|
private readonly decayMs;
|
|
49
103
|
private readonly cancelPx;
|
|
@@ -51,6 +105,13 @@ declare class ScrollFab {
|
|
|
51
105
|
private readonly labelBottom;
|
|
52
106
|
private readonly labelTop;
|
|
53
107
|
private readonly onModeChange?;
|
|
108
|
+
private readonly onLenisReady?;
|
|
109
|
+
/** 外部注入:宿主实例,组件永不销毁 */
|
|
110
|
+
private externalLenis;
|
|
111
|
+
/** 自建配置:null = 不走 lenis;true 形态也归一成对象 */
|
|
112
|
+
private ownLenisConfig;
|
|
113
|
+
private ownLenis;
|
|
114
|
+
private ownLenisPromise;
|
|
54
115
|
private mode;
|
|
55
116
|
private progress;
|
|
56
117
|
private hovering;
|
|
@@ -62,10 +123,15 @@ declare class ScrollFab {
|
|
|
62
123
|
private ringRaf;
|
|
63
124
|
private lastTs;
|
|
64
125
|
private scrollRaf;
|
|
126
|
+
private lenisLoopRaf;
|
|
127
|
+
private lenisScrolling;
|
|
128
|
+
private lenisDest;
|
|
65
129
|
private flashTimer;
|
|
66
130
|
private tapResetTimer;
|
|
67
131
|
private touchTap;
|
|
68
132
|
private ro;
|
|
133
|
+
private actionSeq;
|
|
134
|
+
private destroyed;
|
|
69
135
|
constructor(options?: ScrollFabOptions);
|
|
70
136
|
/** 当前模式 */
|
|
71
137
|
get currentMode(): ScrollFabMode;
|
|
@@ -75,6 +141,7 @@ declare class ScrollFab {
|
|
|
75
141
|
scrollToTop(): void;
|
|
76
142
|
destroy(): void;
|
|
77
143
|
private makeCircle;
|
|
144
|
+
private makePath;
|
|
78
145
|
private onEnter;
|
|
79
146
|
private onLeave;
|
|
80
147
|
private onDown;
|
|
@@ -92,20 +159,38 @@ declare class ScrollFab {
|
|
|
92
159
|
private setMode;
|
|
93
160
|
private doAction;
|
|
94
161
|
private animateScroll;
|
|
162
|
+
/** 首次点击时动态 import lenis 并自建实例;未安装(或加载失败)→ null 永久回退 rAF */
|
|
163
|
+
private resolveOwnLenis;
|
|
164
|
+
/**
|
|
165
|
+
* 用 Lenis 执行程序滚动。
|
|
166
|
+
* 自建实例(own):组件自跑 raf loop(Lenis 需外部驱动 raf),保留 wheel/touch/keydown 打断与到底追击;
|
|
167
|
+
* 外部实例:宿主自带 raf loop 与手势处理,组件只发 scrollTo,不追击不打断。
|
|
168
|
+
*/
|
|
169
|
+
private scrollWithLenis;
|
|
170
|
+
/** 自建实例的 raf 驱动 + 懒加载到底追击(对齐内置 rAF 语义) */
|
|
171
|
+
private startLenisLoop;
|
|
172
|
+
private stopLenisLoop;
|
|
173
|
+
/** 打断自建 Lenis 程序滚动:stop 冻结动画(下次发起前 start 复位) */
|
|
174
|
+
private onLenisInterrupt;
|
|
175
|
+
private endLenisScroll;
|
|
176
|
+
private animateRaf;
|
|
95
177
|
private reducedMotion;
|
|
96
178
|
private onInterrupt;
|
|
97
179
|
private interruptSource;
|
|
180
|
+
/** 自建 Lenis 路线的打断监听 */
|
|
98
181
|
private attachInterrupt;
|
|
99
182
|
private detachInterrupt;
|
|
183
|
+
/** 内置 rAF 路线的打断监听 */
|
|
184
|
+
private attachRafInterrupt;
|
|
185
|
+
private detachRafInterrupt;
|
|
100
186
|
private cancelScroll;
|
|
101
187
|
private maxScroll;
|
|
102
188
|
private targetPos;
|
|
103
189
|
private scrollPos;
|
|
104
190
|
private setScrollPos;
|
|
105
|
-
private scrollTo;
|
|
106
191
|
private onRecheck;
|
|
107
192
|
private syncVisible;
|
|
108
193
|
}
|
|
109
194
|
//#endregion
|
|
110
|
-
export { ScrollFab, type ScrollFabMode, type ScrollFabOptions, type ScrollFabPosition };
|
|
195
|
+
export { type LenisLike, ScrollFab, type ScrollFabLenisConfig, type ScrollFabMode, type ScrollFabOptions, type ScrollFabPosition, type ScrollFabShape };
|
|
111
196
|
//# sourceMappingURL=index.d.mts.map
|
package/dist/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/types.ts","../src/scroll-fab.ts"],"mappings":";KAAY;;UAGK;EACf;EACA;EACA;EACA;;UAGe;;EAEf,SAAS;;EAET;;EAEA;;EAEA;;EAEA;;EAEA,mBAAmB;;EAEnB,WAAW;;EAEX;;EAEA;;EAEA;;
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/types.ts","../src/scroll-fab.ts"],"mappings":";KAAY;;UAGK;EACf;EACA;EACA;EACA;;;UAIe;EACf,WACE,0BAA0B,aAC1B,UAAU;EAEZ,OAAO;EACP;EACA;EACA;EACA,MAAM,eAAe,cAAc;EACnC,OAAO,eAAe,cAAc;EACpC;;;UAIe;EACf,UAAU,cAAc;EACxB,UAAU;EACV;EACA,UAAU;;EAEV;EACA;EACA;EACA;GACC;;;;;;;;UASc;;EAEf;;EAEA;;EAEA;;EAEA;;UAGe;;EAEf,SAAS;;EAET;;EAEA;;EAEA;;EAEA;;EAEA,mBAAmB;;EAEnB,WAAW;;EAEX;;EAEA;;EAEA;;;;;;;;EAQA,kBAAkB,uBAAuB;;EAEzC,QAAQ;;EAER,gBAAgB,OAAO;;EAEvB;;EAEA;;EAEA,gBAAgB,MAAM;;;;cCjEX;WACF,IAAI;UAEL;UACA;UACA;mBACS;UACT;mBAES;mBACA;mBACA;mBACA;mBACA;mBACA;mBACA;mBACA;mBACA;mBACA;;UAGT;;UAEA;UACA;UACA;UAEA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;EAER,YAAY,UAAS;;MA2GjB,eAAe;;EAKnB;EAIA;EAIA;EAIA;UAyBQ;UASA;UAaA;UAMA;UAMA;UAeA;UAUA;UAeA;UAOA;UAUA;UAQA;UAIA;UAMA;;UAoBA;UAYA;UAOA;UAUA;UAIA;;UAyBA;;;;;;UA6BA;;UA6BA;UAqBA;;UAQA;UAMA;UASA;UAmCA;UAMA;UAIA;;UAKA;UAMA;;UAOA;UAMA;UAMA;UAeA;UAKA;UAIA;UAKA;UAKA;UAIA"}
|
package/dist/index.mjs
CHANGED
|
@@ -73,19 +73,26 @@ S16("<path d=\"m9 18 6-6-6-6\"/>");
|
|
|
73
73
|
//#endregion
|
|
74
74
|
//#region src/scroll-fab.ts
|
|
75
75
|
/** 青梧UI ScrollFab 悬浮滚动栏:默认“滚动到底部”,桌面悬停/触屏按住绕满描边一圈翻转为“返回顶部”,点击(轻点)直接执行当前模式动作 */
|
|
76
|
+
const SVG_NS = "http://www.w3.org/2000/svg";
|
|
76
77
|
const RADIUS = 21;
|
|
78
|
+
const FALLBACK_PATH_LEN = 100;
|
|
77
79
|
const DEFAULT_LABEL_BOTTOM = "滚动到底部(悬停或按住绕圈可切换为返回顶部)";
|
|
78
80
|
const DEFAULT_LABEL_TOP = "返回顶部(悬停或按住绕圈可切换为滚动到底部)";
|
|
79
81
|
function easeInOutCubic(p) {
|
|
80
82
|
return p < .5 ? 4 * p * p * p : 1 - (-2 * p + 2) ** 3 / 2;
|
|
81
83
|
}
|
|
84
|
+
function isLenisInstance(v) {
|
|
85
|
+
return typeof v === "object" && v !== null && typeof v.scrollTo === "function";
|
|
86
|
+
}
|
|
82
87
|
var ScrollFab = class {
|
|
83
88
|
el;
|
|
84
89
|
ring;
|
|
85
90
|
track;
|
|
91
|
+
fillPath = null;
|
|
86
92
|
icon;
|
|
87
93
|
len;
|
|
88
94
|
targetEl;
|
|
95
|
+
shape;
|
|
89
96
|
ringMs;
|
|
90
97
|
decayMs;
|
|
91
98
|
cancelPx;
|
|
@@ -93,6 +100,13 @@ var ScrollFab = class {
|
|
|
93
100
|
labelBottom;
|
|
94
101
|
labelTop;
|
|
95
102
|
onModeChange;
|
|
103
|
+
onLenisReady;
|
|
104
|
+
/** 外部注入:宿主实例,组件永不销毁 */
|
|
105
|
+
externalLenis = null;
|
|
106
|
+
/** 自建配置:null = 不走 lenis;true 形态也归一成对象 */
|
|
107
|
+
ownLenisConfig = null;
|
|
108
|
+
ownLenis = null;
|
|
109
|
+
ownLenisPromise = null;
|
|
96
110
|
mode = "to-bottom";
|
|
97
111
|
progress = 0;
|
|
98
112
|
hovering = false;
|
|
@@ -104,12 +118,18 @@ var ScrollFab = class {
|
|
|
104
118
|
ringRaf = null;
|
|
105
119
|
lastTs = null;
|
|
106
120
|
scrollRaf = null;
|
|
121
|
+
lenisLoopRaf = null;
|
|
122
|
+
lenisScrolling = false;
|
|
123
|
+
lenisDest = null;
|
|
107
124
|
flashTimer = null;
|
|
108
125
|
tapResetTimer = null;
|
|
109
126
|
touchTap = false;
|
|
110
127
|
ro = null;
|
|
128
|
+
actionSeq = 0;
|
|
129
|
+
destroyed = false;
|
|
111
130
|
constructor(options = {}) {
|
|
112
131
|
this.targetEl = options.target ?? null;
|
|
132
|
+
this.shape = options.shape ?? null;
|
|
113
133
|
this.ringMs = Math.max(100, Math.min(5e3, options.ringDuration ?? 800));
|
|
114
134
|
this.decayMs = Math.max(100, options.decayDuration ?? 300);
|
|
115
135
|
this.cancelPx = Math.max(4, options.cancelThreshold ?? 10);
|
|
@@ -117,10 +137,20 @@ var ScrollFab = class {
|
|
|
117
137
|
this.labelBottom = options.ariaLabelToBottom ?? DEFAULT_LABEL_BOTTOM;
|
|
118
138
|
this.labelTop = options.ariaLabelToTop ?? DEFAULT_LABEL_TOP;
|
|
119
139
|
this.onModeChange = options.onModeChange;
|
|
140
|
+
this.onLenisReady = options.onLenisReady;
|
|
141
|
+
if (isLenisInstance(options.lenis)) this.externalLenis = options.lenis;
|
|
142
|
+
else if (options.lenis != null && options.lenis !== false) {
|
|
143
|
+
const cfg = options.lenis === true ? {} : options.lenis;
|
|
144
|
+
this.ownLenisConfig = {
|
|
145
|
+
smoothWheel: false,
|
|
146
|
+
...cfg
|
|
147
|
+
};
|
|
148
|
+
}
|
|
120
149
|
this.el = document.createElement("button");
|
|
121
150
|
this.el.type = "button";
|
|
122
151
|
this.el.className = `qsf-root${options.className ? ` ${options.className}` : ""}`;
|
|
123
152
|
this.el.dataset.mode = this.mode;
|
|
153
|
+
if (this.shape) this.el.dataset.shape = "on";
|
|
124
154
|
this.el.setAttribute("aria-label", this.labelBottom);
|
|
125
155
|
this.el.style.setProperty("--qsf-size", `${Math.max(44, options.size ?? 48)}px`);
|
|
126
156
|
if (options.zIndex != null) this.el.style.setProperty("--qsf-z", String(options.zIndex));
|
|
@@ -128,17 +158,32 @@ var ScrollFab = class {
|
|
|
128
158
|
if (options.position?.bottom != null) this.el.style.setProperty("--qsf-bottom", `${options.position.bottom}px`);
|
|
129
159
|
if (options.position?.left != null) this.el.style.setProperty("--qsf-left", `${options.position.left}px`);
|
|
130
160
|
if (options.position?.top != null) this.el.style.setProperty("--qsf-top", `${options.position.top}px`);
|
|
131
|
-
const svg = document.createElementNS(
|
|
161
|
+
const svg = document.createElementNS(SVG_NS, "svg");
|
|
132
162
|
svg.setAttribute("class", "qsf-svg");
|
|
133
|
-
svg.setAttribute("viewBox", "0 0 48 48");
|
|
163
|
+
svg.setAttribute("viewBox", this.shape?.viewBox ?? "0 0 48 48");
|
|
134
164
|
svg.setAttribute("aria-hidden", "true");
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
165
|
+
if (this.shape) {
|
|
166
|
+
this.fillPath = document.createElementNS(SVG_NS, "path");
|
|
167
|
+
this.fillPath.setAttribute("class", "qsf-shape-fill");
|
|
168
|
+
this.fillPath.setAttribute("d", this.shape.fill);
|
|
169
|
+
this.track = this.makePath("qsf-track", this.shape.outline);
|
|
170
|
+
this.ring = this.makePath("qsf-bar", this.shape.outline);
|
|
171
|
+
if (this.shape.outlineTransform) {
|
|
172
|
+
this.track.setAttribute("transform", this.shape.outlineTransform);
|
|
173
|
+
this.ring.setAttribute("transform", this.shape.outlineTransform);
|
|
174
|
+
}
|
|
175
|
+
svg.append(this.fillPath, this.track, this.ring);
|
|
176
|
+
} else {
|
|
177
|
+
this.track = this.makeCircle("qsf-track");
|
|
178
|
+
this.ring = this.makeCircle("qsf-bar");
|
|
179
|
+
svg.append(this.track, this.ring);
|
|
180
|
+
}
|
|
181
|
+
let len;
|
|
182
|
+
if (this.shape) len = FALLBACK_PATH_LEN;
|
|
183
|
+
else len = 2 * Math.PI * RADIUS;
|
|
139
184
|
try {
|
|
140
185
|
const measured = this.ring.getTotalLength();
|
|
141
|
-
if (Number.isFinite(measured)) len = measured;
|
|
186
|
+
if (Number.isFinite(measured) && measured > 0) len = measured;
|
|
142
187
|
} catch {}
|
|
143
188
|
this.len = len;
|
|
144
189
|
this.ring.style.strokeDasharray = String(this.len);
|
|
@@ -179,9 +224,10 @@ var ScrollFab = class {
|
|
|
179
224
|
this.animateScroll("bottom");
|
|
180
225
|
}
|
|
181
226
|
scrollToTop() {
|
|
182
|
-
this.
|
|
227
|
+
this.animateScroll("top");
|
|
183
228
|
}
|
|
184
229
|
destroy() {
|
|
230
|
+
this.destroyed = true;
|
|
185
231
|
this.el.removeEventListener("pointerenter", this.onEnter);
|
|
186
232
|
this.el.removeEventListener("pointerleave", this.onLeave);
|
|
187
233
|
this.el.removeEventListener("pointerdown", this.onDown);
|
|
@@ -194,6 +240,9 @@ var ScrollFab = class {
|
|
|
194
240
|
(this.targetEl ?? window).removeEventListener("scroll", this.onRecheck);
|
|
195
241
|
this.ro?.disconnect();
|
|
196
242
|
this.cancelScroll();
|
|
243
|
+
this.stopLenisLoop();
|
|
244
|
+
this.ownLenis?.destroy?.();
|
|
245
|
+
this.ownLenis = null;
|
|
197
246
|
if (this.ringRaf != null) cancelAnimationFrame(this.ringRaf);
|
|
198
247
|
this.ringRaf = null;
|
|
199
248
|
if (this.flashTimer != null) clearTimeout(this.flashTimer);
|
|
@@ -201,13 +250,19 @@ var ScrollFab = class {
|
|
|
201
250
|
this.el.remove();
|
|
202
251
|
}
|
|
203
252
|
makeCircle(cls) {
|
|
204
|
-
const c = document.createElementNS(
|
|
253
|
+
const c = document.createElementNS(SVG_NS, "circle");
|
|
205
254
|
c.setAttribute("class", cls);
|
|
206
255
|
c.setAttribute("cx", "24");
|
|
207
256
|
c.setAttribute("cy", "24");
|
|
208
257
|
c.setAttribute("r", String(RADIUS));
|
|
209
258
|
return c;
|
|
210
259
|
}
|
|
260
|
+
makePath(cls, d) {
|
|
261
|
+
const p = document.createElementNS(SVG_NS, "path");
|
|
262
|
+
p.setAttribute("class", cls);
|
|
263
|
+
p.setAttribute("d", d);
|
|
264
|
+
return p;
|
|
265
|
+
}
|
|
211
266
|
onEnter = (e) => {
|
|
212
267
|
if (e.pointerType !== "mouse") return;
|
|
213
268
|
this.hovering = true;
|
|
@@ -321,12 +376,110 @@ var ScrollFab = class {
|
|
|
321
376
|
this.animateScroll(this.mode === "to-bottom" ? "bottom" : "top");
|
|
322
377
|
}
|
|
323
378
|
animateScroll(dest) {
|
|
379
|
+
++this.actionSeq;
|
|
324
380
|
this.cancelScroll();
|
|
325
381
|
if (!this.animate || this.reducedMotion()) {
|
|
326
382
|
this.setScrollPos(this.targetPos(dest));
|
|
327
383
|
return;
|
|
328
384
|
}
|
|
329
|
-
this.
|
|
385
|
+
if (this.externalLenis) {
|
|
386
|
+
this.scrollWithLenis(this.externalLenis, dest, false);
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
if (this.ownLenisConfig) {
|
|
390
|
+
const seq = ++this.actionSeq;
|
|
391
|
+
this.resolveOwnLenis().then((lenis) => {
|
|
392
|
+
if (this.destroyed || seq !== this.actionSeq) return;
|
|
393
|
+
if (lenis) this.scrollWithLenis(lenis, dest, true);
|
|
394
|
+
else this.animateRaf(dest);
|
|
395
|
+
});
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
this.animateRaf(dest);
|
|
399
|
+
}
|
|
400
|
+
/** 首次点击时动态 import lenis 并自建实例;未安装(或加载失败)→ null 永久回退 rAF */
|
|
401
|
+
resolveOwnLenis() {
|
|
402
|
+
if (this.ownLenis) return Promise.resolve(this.ownLenis);
|
|
403
|
+
if (this.ownLenisPromise) return this.ownLenisPromise;
|
|
404
|
+
this.ownLenisPromise = import("lenis").then((mod) => {
|
|
405
|
+
const LenisCtor = mod.default ?? mod;
|
|
406
|
+
const cfg = { ...this.ownLenisConfig };
|
|
407
|
+
if (this.targetEl) cfg.wrapper = this.targetEl;
|
|
408
|
+
this.ownLenis = new LenisCtor(cfg);
|
|
409
|
+
this.onLenisReady?.(this.ownLenis);
|
|
410
|
+
return this.ownLenis;
|
|
411
|
+
}).catch(() => {
|
|
412
|
+
this.ownLenisConfig = null;
|
|
413
|
+
this.ownLenisPromise = null;
|
|
414
|
+
return null;
|
|
415
|
+
});
|
|
416
|
+
return this.ownLenisPromise;
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* 用 Lenis 执行程序滚动。
|
|
420
|
+
* 自建实例(own):组件自跑 raf loop(Lenis 需外部驱动 raf),保留 wheel/touch/keydown 打断与到底追击;
|
|
421
|
+
* 外部实例:宿主自带 raf loop 与手势处理,组件只发 scrollTo,不追击不打断。
|
|
422
|
+
*/
|
|
423
|
+
scrollWithLenis(lenis, dest, own) {
|
|
424
|
+
const immediate = !this.animate || this.reducedMotion();
|
|
425
|
+
const target = this.targetPos(dest);
|
|
426
|
+
this.lenisDest = dest;
|
|
427
|
+
if (own) {
|
|
428
|
+
lenis.start?.();
|
|
429
|
+
lenis.scrollTo(target, {
|
|
430
|
+
immediate,
|
|
431
|
+
onComplete: () => this.endLenisScroll()
|
|
432
|
+
});
|
|
433
|
+
if (!immediate) {
|
|
434
|
+
this.lenisScrolling = true;
|
|
435
|
+
this.attachInterrupt();
|
|
436
|
+
this.startLenisLoop(lenis, dest);
|
|
437
|
+
} else this.endLenisScroll();
|
|
438
|
+
} else {
|
|
439
|
+
lenis.scrollTo(target, { immediate });
|
|
440
|
+
this.lenisDest = null;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
/** 自建实例的 raf 驱动 + 懒加载到底追击(对齐内置 rAF 语义) */
|
|
444
|
+
startLenisLoop(lenis, dest) {
|
|
445
|
+
let lastEnd = this.targetPos(dest);
|
|
446
|
+
const loop = (time) => {
|
|
447
|
+
if (!this.lenisScrolling) return;
|
|
448
|
+
if (dest === "bottom") {
|
|
449
|
+
const live = this.targetPos("bottom");
|
|
450
|
+
if (Math.abs(live - lastEnd) > 2) {
|
|
451
|
+
lastEnd = live;
|
|
452
|
+
lenis.scrollTo(live, {
|
|
453
|
+
force: true,
|
|
454
|
+
onComplete: () => this.endLenisScroll()
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
lenis.raf?.(time);
|
|
459
|
+
this.lenisLoopRaf = requestAnimationFrame(loop);
|
|
460
|
+
};
|
|
461
|
+
this.lenisLoopRaf = requestAnimationFrame(loop);
|
|
462
|
+
}
|
|
463
|
+
stopLenisLoop() {
|
|
464
|
+
if (this.lenisLoopRaf != null) {
|
|
465
|
+
cancelAnimationFrame(this.lenisLoopRaf);
|
|
466
|
+
this.lenisLoopRaf = null;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
/** 打断自建 Lenis 程序滚动:stop 冻结动画(下次发起前 start 复位) */
|
|
470
|
+
onLenisInterrupt = () => {
|
|
471
|
+
if (!this.lenisScrolling) return;
|
|
472
|
+
this.ownLenis?.stop?.();
|
|
473
|
+
this.endLenisScroll();
|
|
474
|
+
};
|
|
475
|
+
endLenisScroll() {
|
|
476
|
+
this.lenisScrolling = false;
|
|
477
|
+
this.lenisDest = null;
|
|
478
|
+
this.stopLenisLoop();
|
|
479
|
+
this.detachInterrupt();
|
|
480
|
+
}
|
|
481
|
+
animateRaf(dest) {
|
|
482
|
+
this.attachRafInterrupt();
|
|
330
483
|
let init = false;
|
|
331
484
|
let start = 0;
|
|
332
485
|
let end = 0;
|
|
@@ -353,7 +506,7 @@ var ScrollFab = class {
|
|
|
353
506
|
if (p < 1) this.scrollRaf = requestAnimationFrame(step);
|
|
354
507
|
else {
|
|
355
508
|
this.scrollRaf = null;
|
|
356
|
-
this.
|
|
509
|
+
this.detachRafInterrupt();
|
|
357
510
|
}
|
|
358
511
|
};
|
|
359
512
|
this.scrollRaf = requestAnimationFrame(step);
|
|
@@ -367,15 +520,33 @@ var ScrollFab = class {
|
|
|
367
520
|
interruptSource() {
|
|
368
521
|
return this.targetEl ?? window;
|
|
369
522
|
}
|
|
523
|
+
/** 自建 Lenis 路线的打断监听 */
|
|
370
524
|
attachInterrupt() {
|
|
371
525
|
const src = this.interruptSource();
|
|
372
526
|
for (const ev of [
|
|
373
527
|
"wheel",
|
|
374
528
|
"touchstart",
|
|
375
529
|
"keydown"
|
|
376
|
-
]) src.addEventListener(ev, this.
|
|
530
|
+
]) src.addEventListener(ev, this.onLenisInterrupt, { passive: true });
|
|
377
531
|
}
|
|
378
532
|
detachInterrupt() {
|
|
533
|
+
const src = this.interruptSource();
|
|
534
|
+
for (const ev of [
|
|
535
|
+
"wheel",
|
|
536
|
+
"touchstart",
|
|
537
|
+
"keydown"
|
|
538
|
+
]) src.removeEventListener(ev, this.onLenisInterrupt);
|
|
539
|
+
}
|
|
540
|
+
/** 内置 rAF 路线的打断监听 */
|
|
541
|
+
attachRafInterrupt() {
|
|
542
|
+
const src = this.interruptSource();
|
|
543
|
+
for (const ev of [
|
|
544
|
+
"wheel",
|
|
545
|
+
"touchstart",
|
|
546
|
+
"keydown"
|
|
547
|
+
]) src.addEventListener(ev, this.onInterrupt, { passive: true });
|
|
548
|
+
}
|
|
549
|
+
detachRafInterrupt() {
|
|
379
550
|
const src = this.interruptSource();
|
|
380
551
|
for (const ev of [
|
|
381
552
|
"wheel",
|
|
@@ -388,7 +559,16 @@ var ScrollFab = class {
|
|
|
388
559
|
cancelAnimationFrame(this.scrollRaf);
|
|
389
560
|
this.scrollRaf = null;
|
|
390
561
|
}
|
|
391
|
-
this.
|
|
562
|
+
if (this.lenisScrolling) this.endLenisScroll();
|
|
563
|
+
const src = this.interruptSource();
|
|
564
|
+
for (const ev of [
|
|
565
|
+
"wheel",
|
|
566
|
+
"touchstart",
|
|
567
|
+
"keydown"
|
|
568
|
+
]) {
|
|
569
|
+
src.removeEventListener(ev, this.onInterrupt);
|
|
570
|
+
src.removeEventListener(ev, this.onLenisInterrupt);
|
|
571
|
+
}
|
|
392
572
|
}
|
|
393
573
|
maxScroll() {
|
|
394
574
|
if (this.targetEl) return Math.max(0, this.targetEl.scrollHeight - this.targetEl.clientHeight);
|
|
@@ -405,9 +585,6 @@ var ScrollFab = class {
|
|
|
405
585
|
if (this.targetEl) this.targetEl.scrollTop = v;
|
|
406
586
|
else window.scrollTo(0, v);
|
|
407
587
|
}
|
|
408
|
-
scrollTo(v) {
|
|
409
|
-
this.animateScroll(v === 0 ? "top" : "bottom");
|
|
410
|
-
}
|
|
411
588
|
onRecheck = () => {
|
|
412
589
|
this.syncVisible();
|
|
413
590
|
};
|