@qingwu-ui/scroll-fab 0.9.0-beta.1 → 0.9.0-beta.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 +115 -0
- package/dist/index.cjs +264 -37
- package/dist/index.d.cts +114 -6
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +114 -6
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +264 -37
- package/dist/index.mjs.map +1 -1
- package/dist/style.css +32 -2
- package/package.json +14 -2
package/dist/index.mjs
CHANGED
|
@@ -73,19 +73,28 @@ 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 = "返回顶部(悬停或按住绕圈可切换为滚动到底部)";
|
|
81
|
+
const STATIC_LABEL_BOTTOM = "滚动到底部";
|
|
82
|
+
const STATIC_LABEL_TOP = "返回顶部";
|
|
79
83
|
function easeInOutCubic(p) {
|
|
80
84
|
return p < .5 ? 4 * p * p * p : 1 - (-2 * p + 2) ** 3 / 2;
|
|
81
85
|
}
|
|
86
|
+
function isLenisInstance(v) {
|
|
87
|
+
return typeof v === "object" && v !== null && typeof v.scrollTo === "function";
|
|
88
|
+
}
|
|
82
89
|
var ScrollFab = class {
|
|
83
90
|
el;
|
|
84
91
|
ring;
|
|
85
92
|
track;
|
|
93
|
+
fillPath = null;
|
|
86
94
|
icon;
|
|
87
95
|
len;
|
|
88
96
|
targetEl;
|
|
97
|
+
shape;
|
|
89
98
|
ringMs;
|
|
90
99
|
decayMs;
|
|
91
100
|
cancelPx;
|
|
@@ -93,7 +102,18 @@ var ScrollFab = class {
|
|
|
93
102
|
labelBottom;
|
|
94
103
|
labelTop;
|
|
95
104
|
onModeChange;
|
|
96
|
-
|
|
105
|
+
onLenisReady;
|
|
106
|
+
onScroll;
|
|
107
|
+
modes;
|
|
108
|
+
single;
|
|
109
|
+
threshold;
|
|
110
|
+
/** 外部注入:宿主实例,组件永不销毁 */
|
|
111
|
+
externalLenis = null;
|
|
112
|
+
/** 自建配置:null = 不走 lenis;true 形态也归一成对象 */
|
|
113
|
+
ownLenisConfig = null;
|
|
114
|
+
ownLenis = null;
|
|
115
|
+
ownLenisPromise = null;
|
|
116
|
+
mode;
|
|
97
117
|
progress = 0;
|
|
98
118
|
hovering = false;
|
|
99
119
|
pressing = false;
|
|
@@ -104,41 +124,79 @@ var ScrollFab = class {
|
|
|
104
124
|
ringRaf = null;
|
|
105
125
|
lastTs = null;
|
|
106
126
|
scrollRaf = null;
|
|
127
|
+
lenisLoopRaf = null;
|
|
128
|
+
lenisScrolling = false;
|
|
129
|
+
lenisDest = null;
|
|
107
130
|
flashTimer = null;
|
|
108
131
|
tapResetTimer = null;
|
|
109
132
|
touchTap = false;
|
|
110
133
|
ro = null;
|
|
134
|
+
actionSeq = 0;
|
|
135
|
+
destroyed = false;
|
|
136
|
+
emitRaf = null;
|
|
137
|
+
lastPct = -1;
|
|
111
138
|
constructor(options = {}) {
|
|
112
139
|
this.targetEl = options.target ?? null;
|
|
140
|
+
this.shape = options.shape ?? null;
|
|
113
141
|
this.ringMs = Math.max(100, Math.min(5e3, options.ringDuration ?? 800));
|
|
114
142
|
this.decayMs = Math.max(100, options.decayDuration ?? 300);
|
|
115
143
|
this.cancelPx = Math.max(4, options.cancelThreshold ?? 10);
|
|
116
144
|
this.animate = options.animate ?? true;
|
|
117
|
-
this.
|
|
118
|
-
this.labelTop = options.ariaLabelToTop ?? DEFAULT_LABEL_TOP;
|
|
145
|
+
this.threshold = Math.max(0, options.showThreshold ?? 0);
|
|
119
146
|
this.onModeChange = options.onModeChange;
|
|
147
|
+
this.onLenisReady = options.onLenisReady;
|
|
148
|
+
this.onScroll = options.onScroll;
|
|
149
|
+
this.modes = this.normalizeModes(options.modes);
|
|
150
|
+
this.single = this.modes.length === 1;
|
|
151
|
+
this.mode = this.modes[0];
|
|
152
|
+
this.labelBottom = options.ariaLabelToBottom ?? (this.single ? STATIC_LABEL_BOTTOM : DEFAULT_LABEL_BOTTOM);
|
|
153
|
+
this.labelTop = options.ariaLabelToTop ?? (this.single ? STATIC_LABEL_TOP : DEFAULT_LABEL_TOP);
|
|
154
|
+
if (isLenisInstance(options.lenis)) this.externalLenis = options.lenis;
|
|
155
|
+
else if (options.lenis != null && options.lenis !== false) {
|
|
156
|
+
const cfg = options.lenis === true ? {} : options.lenis;
|
|
157
|
+
this.ownLenisConfig = {
|
|
158
|
+
smoothWheel: false,
|
|
159
|
+
...cfg
|
|
160
|
+
};
|
|
161
|
+
}
|
|
120
162
|
this.el = document.createElement("button");
|
|
121
163
|
this.el.type = "button";
|
|
122
164
|
this.el.className = `qsf-root${options.className ? ` ${options.className}` : ""}`;
|
|
123
165
|
this.el.dataset.mode = this.mode;
|
|
124
|
-
this.el.
|
|
166
|
+
if (this.shape) this.el.dataset.shape = "on";
|
|
167
|
+
this.el.setAttribute("aria-label", this.mode === "to-bottom" ? this.labelBottom : this.labelTop);
|
|
125
168
|
this.el.style.setProperty("--qsf-size", `${Math.max(44, options.size ?? 48)}px`);
|
|
126
169
|
if (options.zIndex != null) this.el.style.setProperty("--qsf-z", String(options.zIndex));
|
|
127
170
|
if (options.position?.right != null) this.el.style.setProperty("--qsf-right", `${options.position.right}px`);
|
|
128
171
|
if (options.position?.bottom != null) this.el.style.setProperty("--qsf-bottom", `${options.position.bottom}px`);
|
|
129
172
|
if (options.position?.left != null) this.el.style.setProperty("--qsf-left", `${options.position.left}px`);
|
|
130
173
|
if (options.position?.top != null) this.el.style.setProperty("--qsf-top", `${options.position.top}px`);
|
|
131
|
-
const svg = document.createElementNS(
|
|
174
|
+
const svg = document.createElementNS(SVG_NS, "svg");
|
|
132
175
|
svg.setAttribute("class", "qsf-svg");
|
|
133
|
-
svg.setAttribute("viewBox", "0 0 48 48");
|
|
176
|
+
svg.setAttribute("viewBox", this.shape?.viewBox ?? "0 0 48 48");
|
|
134
177
|
svg.setAttribute("aria-hidden", "true");
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
178
|
+
if (this.shape) {
|
|
179
|
+
this.fillPath = document.createElementNS(SVG_NS, "path");
|
|
180
|
+
this.fillPath.setAttribute("class", "qsf-shape-fill");
|
|
181
|
+
this.fillPath.setAttribute("d", this.shape.fill);
|
|
182
|
+
this.track = this.makePath("qsf-track", this.shape.outline);
|
|
183
|
+
this.ring = this.makePath("qsf-bar", this.shape.outline);
|
|
184
|
+
if (this.shape.outlineTransform) {
|
|
185
|
+
this.track.setAttribute("transform", this.shape.outlineTransform);
|
|
186
|
+
this.ring.setAttribute("transform", this.shape.outlineTransform);
|
|
187
|
+
}
|
|
188
|
+
svg.append(this.fillPath, this.track, this.ring);
|
|
189
|
+
} else {
|
|
190
|
+
this.track = this.makeCircle("qsf-track");
|
|
191
|
+
this.ring = this.makeCircle("qsf-bar");
|
|
192
|
+
svg.append(this.track, this.ring);
|
|
193
|
+
}
|
|
194
|
+
let len;
|
|
195
|
+
if (this.shape) len = FALLBACK_PATH_LEN;
|
|
196
|
+
else len = 2 * Math.PI * RADIUS;
|
|
139
197
|
try {
|
|
140
198
|
const measured = this.ring.getTotalLength();
|
|
141
|
-
if (Number.isFinite(measured)) len = measured;
|
|
199
|
+
if (Number.isFinite(measured) && measured > 0) len = measured;
|
|
142
200
|
} catch {}
|
|
143
201
|
this.len = len;
|
|
144
202
|
this.ring.style.strokeDasharray = String(this.len);
|
|
@@ -151,12 +209,14 @@ var ScrollFab = class {
|
|
|
151
209
|
else this.icon.innerHTML = ICON_CHEVRON_DOWN;
|
|
152
210
|
this.el.append(svg, this.icon);
|
|
153
211
|
document.body.append(this.el);
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
212
|
+
if (!this.single) {
|
|
213
|
+
this.el.addEventListener("pointerenter", this.onEnter);
|
|
214
|
+
this.el.addEventListener("pointerleave", this.onLeave);
|
|
215
|
+
this.el.addEventListener("pointerdown", this.onDown);
|
|
216
|
+
this.el.addEventListener("pointermove", this.onMove);
|
|
217
|
+
this.el.addEventListener("pointerup", this.onUp);
|
|
218
|
+
this.el.addEventListener("pointercancel", this.onCancel);
|
|
219
|
+
}
|
|
160
220
|
this.el.addEventListener("keydown", this.onKey);
|
|
161
221
|
this.el.addEventListener("click", this.onClick);
|
|
162
222
|
window.addEventListener("resize", this.onRecheck);
|
|
@@ -166,48 +226,73 @@ var ScrollFab = class {
|
|
|
166
226
|
this.ro.observe(this.targetEl ?? document.documentElement);
|
|
167
227
|
}
|
|
168
228
|
this.syncVisible();
|
|
229
|
+
this.emitScroll();
|
|
169
230
|
}
|
|
170
231
|
/** 当前模式 */
|
|
171
232
|
get currentMode() {
|
|
172
233
|
return this.mode;
|
|
173
234
|
}
|
|
174
|
-
/** 内容懒加载等外部高度变化后可显式触发重估(滚动/resize
|
|
235
|
+
/** 内容懒加载等外部高度变化后可显式触发重估(滚动/resize 时也会自动重估),并强制补发一次进度 */
|
|
175
236
|
refresh() {
|
|
176
|
-
this.
|
|
237
|
+
this.syncVisible();
|
|
238
|
+
this.lastPct = -1;
|
|
239
|
+
this.emitScroll();
|
|
177
240
|
}
|
|
178
241
|
scrollToBottom() {
|
|
179
242
|
this.animateScroll("bottom");
|
|
180
243
|
}
|
|
181
244
|
scrollToTop() {
|
|
182
|
-
this.
|
|
245
|
+
this.animateScroll("top");
|
|
183
246
|
}
|
|
184
247
|
destroy() {
|
|
185
|
-
this.
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
248
|
+
this.destroyed = true;
|
|
249
|
+
if (!this.single) {
|
|
250
|
+
this.el.removeEventListener("pointerenter", this.onEnter);
|
|
251
|
+
this.el.removeEventListener("pointerleave", this.onLeave);
|
|
252
|
+
this.el.removeEventListener("pointerdown", this.onDown);
|
|
253
|
+
this.el.removeEventListener("pointermove", this.onMove);
|
|
254
|
+
this.el.removeEventListener("pointerup", this.onUp);
|
|
255
|
+
this.el.removeEventListener("pointercancel", this.onCancel);
|
|
256
|
+
}
|
|
191
257
|
this.el.removeEventListener("keydown", this.onKey);
|
|
192
258
|
this.el.removeEventListener("click", this.onClick);
|
|
193
259
|
window.removeEventListener("resize", this.onRecheck);
|
|
194
260
|
(this.targetEl ?? window).removeEventListener("scroll", this.onRecheck);
|
|
195
261
|
this.ro?.disconnect();
|
|
196
262
|
this.cancelScroll();
|
|
263
|
+
this.stopLenisLoop();
|
|
264
|
+
this.ownLenis?.destroy?.();
|
|
265
|
+
this.ownLenis = null;
|
|
197
266
|
if (this.ringRaf != null) cancelAnimationFrame(this.ringRaf);
|
|
198
267
|
this.ringRaf = null;
|
|
268
|
+
if (this.emitRaf != null) cancelAnimationFrame(this.emitRaf);
|
|
269
|
+
this.emitRaf = null;
|
|
199
270
|
if (this.flashTimer != null) clearTimeout(this.flashTimer);
|
|
200
271
|
if (this.tapResetTimer != null) clearTimeout(this.tapResetTimer);
|
|
201
272
|
this.el.remove();
|
|
202
273
|
}
|
|
274
|
+
/** 规范化 modes:缺省双模式;去重、过滤非法值;结果为空直接抛错 */
|
|
275
|
+
normalizeModes(input) {
|
|
276
|
+
if (!input) return ["to-bottom", "to-top"];
|
|
277
|
+
const out = [];
|
|
278
|
+
for (const m of input) if ((m === "to-bottom" || m === "to-top") && !out.includes(m)) out.push(m);
|
|
279
|
+
if (out.length === 0) throw new TypeError("ScrollFab: modes 至少需要一个有效模式('to-bottom' | 'to-top')");
|
|
280
|
+
return out;
|
|
281
|
+
}
|
|
203
282
|
makeCircle(cls) {
|
|
204
|
-
const c = document.createElementNS(
|
|
283
|
+
const c = document.createElementNS(SVG_NS, "circle");
|
|
205
284
|
c.setAttribute("class", cls);
|
|
206
285
|
c.setAttribute("cx", "24");
|
|
207
286
|
c.setAttribute("cy", "24");
|
|
208
287
|
c.setAttribute("r", String(RADIUS));
|
|
209
288
|
return c;
|
|
210
289
|
}
|
|
290
|
+
makePath(cls, d) {
|
|
291
|
+
const p = document.createElementNS(SVG_NS, "path");
|
|
292
|
+
p.setAttribute("class", cls);
|
|
293
|
+
p.setAttribute("d", d);
|
|
294
|
+
return p;
|
|
295
|
+
}
|
|
211
296
|
onEnter = (e) => {
|
|
212
297
|
if (e.pointerType !== "mouse") return;
|
|
213
298
|
this.hovering = true;
|
|
@@ -267,9 +352,12 @@ var ScrollFab = class {
|
|
|
267
352
|
this.doAction();
|
|
268
353
|
};
|
|
269
354
|
onKey = (e) => {
|
|
270
|
-
if (e.repeat || e.key !== "
|
|
355
|
+
if (e.repeat || e.key !== "ArrowUp" && e.key !== "ArrowDown") return;
|
|
356
|
+
if (this.single) return;
|
|
357
|
+
const want = e.key === "ArrowUp" ? "to-top" : "to-bottom";
|
|
358
|
+
if (want === this.mode) return;
|
|
271
359
|
e.preventDefault();
|
|
272
|
-
this.setMode(
|
|
360
|
+
this.setMode(want);
|
|
273
361
|
};
|
|
274
362
|
holding() {
|
|
275
363
|
return this.hovering || this.pressing;
|
|
@@ -321,12 +409,110 @@ var ScrollFab = class {
|
|
|
321
409
|
this.animateScroll(this.mode === "to-bottom" ? "bottom" : "top");
|
|
322
410
|
}
|
|
323
411
|
animateScroll(dest) {
|
|
412
|
+
++this.actionSeq;
|
|
324
413
|
this.cancelScroll();
|
|
325
414
|
if (!this.animate || this.reducedMotion()) {
|
|
326
415
|
this.setScrollPos(this.targetPos(dest));
|
|
327
416
|
return;
|
|
328
417
|
}
|
|
329
|
-
this.
|
|
418
|
+
if (this.externalLenis) {
|
|
419
|
+
this.scrollWithLenis(this.externalLenis, dest, false);
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
if (this.ownLenisConfig) {
|
|
423
|
+
const seq = ++this.actionSeq;
|
|
424
|
+
this.resolveOwnLenis().then((lenis) => {
|
|
425
|
+
if (this.destroyed || seq !== this.actionSeq) return;
|
|
426
|
+
if (lenis) this.scrollWithLenis(lenis, dest, true);
|
|
427
|
+
else this.animateRaf(dest);
|
|
428
|
+
});
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
this.animateRaf(dest);
|
|
432
|
+
}
|
|
433
|
+
/** 首次点击时动态 import lenis 并自建实例;未安装(或加载失败)→ null 永久回退 rAF */
|
|
434
|
+
resolveOwnLenis() {
|
|
435
|
+
if (this.ownLenis) return Promise.resolve(this.ownLenis);
|
|
436
|
+
if (this.ownLenisPromise) return this.ownLenisPromise;
|
|
437
|
+
this.ownLenisPromise = import("lenis").then((mod) => {
|
|
438
|
+
const LenisCtor = mod.default ?? mod;
|
|
439
|
+
const cfg = { ...this.ownLenisConfig };
|
|
440
|
+
if (this.targetEl) cfg.wrapper = this.targetEl;
|
|
441
|
+
this.ownLenis = new LenisCtor(cfg);
|
|
442
|
+
this.onLenisReady?.(this.ownLenis);
|
|
443
|
+
return this.ownLenis;
|
|
444
|
+
}).catch(() => {
|
|
445
|
+
this.ownLenisConfig = null;
|
|
446
|
+
this.ownLenisPromise = null;
|
|
447
|
+
return null;
|
|
448
|
+
});
|
|
449
|
+
return this.ownLenisPromise;
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* 用 Lenis 执行程序滚动。
|
|
453
|
+
* 自建实例(own):组件自跑 raf loop(Lenis 需外部驱动 raf),保留 wheel/touch/keydown 打断与到底追击;
|
|
454
|
+
* 外部实例:宿主自带 raf loop 与手势处理,组件只发 scrollTo,不追击不打断。
|
|
455
|
+
*/
|
|
456
|
+
scrollWithLenis(lenis, dest, own) {
|
|
457
|
+
const immediate = !this.animate || this.reducedMotion();
|
|
458
|
+
const target = this.targetPos(dest);
|
|
459
|
+
this.lenisDest = dest;
|
|
460
|
+
if (own) {
|
|
461
|
+
lenis.start?.();
|
|
462
|
+
lenis.scrollTo(target, {
|
|
463
|
+
immediate,
|
|
464
|
+
onComplete: () => this.endLenisScroll()
|
|
465
|
+
});
|
|
466
|
+
if (!immediate) {
|
|
467
|
+
this.lenisScrolling = true;
|
|
468
|
+
this.attachInterrupt();
|
|
469
|
+
this.startLenisLoop(lenis, dest);
|
|
470
|
+
} else this.endLenisScroll();
|
|
471
|
+
} else {
|
|
472
|
+
lenis.scrollTo(target, { immediate });
|
|
473
|
+
this.lenisDest = null;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
/** 自建实例的 raf 驱动 + 懒加载到底追击(对齐内置 rAF 语义) */
|
|
477
|
+
startLenisLoop(lenis, dest) {
|
|
478
|
+
let lastEnd = this.targetPos(dest);
|
|
479
|
+
const loop = (time) => {
|
|
480
|
+
if (!this.lenisScrolling) return;
|
|
481
|
+
if (dest === "bottom") {
|
|
482
|
+
const live = this.targetPos("bottom");
|
|
483
|
+
if (Math.abs(live - lastEnd) > 2) {
|
|
484
|
+
lastEnd = live;
|
|
485
|
+
lenis.scrollTo(live, {
|
|
486
|
+
force: true,
|
|
487
|
+
onComplete: () => this.endLenisScroll()
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
lenis.raf?.(time);
|
|
492
|
+
this.lenisLoopRaf = requestAnimationFrame(loop);
|
|
493
|
+
};
|
|
494
|
+
this.lenisLoopRaf = requestAnimationFrame(loop);
|
|
495
|
+
}
|
|
496
|
+
stopLenisLoop() {
|
|
497
|
+
if (this.lenisLoopRaf != null) {
|
|
498
|
+
cancelAnimationFrame(this.lenisLoopRaf);
|
|
499
|
+
this.lenisLoopRaf = null;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
/** 打断自建 Lenis 程序滚动:stop 冻结动画(下次发起前 start 复位) */
|
|
503
|
+
onLenisInterrupt = () => {
|
|
504
|
+
if (!this.lenisScrolling) return;
|
|
505
|
+
this.ownLenis?.stop?.();
|
|
506
|
+
this.endLenisScroll();
|
|
507
|
+
};
|
|
508
|
+
endLenisScroll() {
|
|
509
|
+
this.lenisScrolling = false;
|
|
510
|
+
this.lenisDest = null;
|
|
511
|
+
this.stopLenisLoop();
|
|
512
|
+
this.detachInterrupt();
|
|
513
|
+
}
|
|
514
|
+
animateRaf(dest) {
|
|
515
|
+
this.attachRafInterrupt();
|
|
330
516
|
let init = false;
|
|
331
517
|
let start = 0;
|
|
332
518
|
let end = 0;
|
|
@@ -353,7 +539,7 @@ var ScrollFab = class {
|
|
|
353
539
|
if (p < 1) this.scrollRaf = requestAnimationFrame(step);
|
|
354
540
|
else {
|
|
355
541
|
this.scrollRaf = null;
|
|
356
|
-
this.
|
|
542
|
+
this.detachRafInterrupt();
|
|
357
543
|
}
|
|
358
544
|
};
|
|
359
545
|
this.scrollRaf = requestAnimationFrame(step);
|
|
@@ -367,15 +553,33 @@ var ScrollFab = class {
|
|
|
367
553
|
interruptSource() {
|
|
368
554
|
return this.targetEl ?? window;
|
|
369
555
|
}
|
|
556
|
+
/** 自建 Lenis 路线的打断监听 */
|
|
370
557
|
attachInterrupt() {
|
|
371
558
|
const src = this.interruptSource();
|
|
372
559
|
for (const ev of [
|
|
373
560
|
"wheel",
|
|
374
561
|
"touchstart",
|
|
375
562
|
"keydown"
|
|
376
|
-
]) src.addEventListener(ev, this.
|
|
563
|
+
]) src.addEventListener(ev, this.onLenisInterrupt, { passive: true });
|
|
377
564
|
}
|
|
378
565
|
detachInterrupt() {
|
|
566
|
+
const src = this.interruptSource();
|
|
567
|
+
for (const ev of [
|
|
568
|
+
"wheel",
|
|
569
|
+
"touchstart",
|
|
570
|
+
"keydown"
|
|
571
|
+
]) src.removeEventListener(ev, this.onLenisInterrupt);
|
|
572
|
+
}
|
|
573
|
+
/** 内置 rAF 路线的打断监听 */
|
|
574
|
+
attachRafInterrupt() {
|
|
575
|
+
const src = this.interruptSource();
|
|
576
|
+
for (const ev of [
|
|
577
|
+
"wheel",
|
|
578
|
+
"touchstart",
|
|
579
|
+
"keydown"
|
|
580
|
+
]) src.addEventListener(ev, this.onInterrupt, { passive: true });
|
|
581
|
+
}
|
|
582
|
+
detachRafInterrupt() {
|
|
379
583
|
const src = this.interruptSource();
|
|
380
584
|
for (const ev of [
|
|
381
585
|
"wheel",
|
|
@@ -388,7 +592,16 @@ var ScrollFab = class {
|
|
|
388
592
|
cancelAnimationFrame(this.scrollRaf);
|
|
389
593
|
this.scrollRaf = null;
|
|
390
594
|
}
|
|
391
|
-
this.
|
|
595
|
+
if (this.lenisScrolling) this.endLenisScroll();
|
|
596
|
+
const src = this.interruptSource();
|
|
597
|
+
for (const ev of [
|
|
598
|
+
"wheel",
|
|
599
|
+
"touchstart",
|
|
600
|
+
"keydown"
|
|
601
|
+
]) {
|
|
602
|
+
src.removeEventListener(ev, this.onInterrupt);
|
|
603
|
+
src.removeEventListener(ev, this.onLenisInterrupt);
|
|
604
|
+
}
|
|
392
605
|
}
|
|
393
606
|
maxScroll() {
|
|
394
607
|
if (this.targetEl) return Math.max(0, this.targetEl.scrollHeight - this.targetEl.clientHeight);
|
|
@@ -405,14 +618,28 @@ var ScrollFab = class {
|
|
|
405
618
|
if (this.targetEl) this.targetEl.scrollTop = v;
|
|
406
619
|
else window.scrollTo(0, v);
|
|
407
620
|
}
|
|
408
|
-
scrollTo(v) {
|
|
409
|
-
this.animateScroll(v === 0 ? "top" : "bottom");
|
|
410
|
-
}
|
|
411
621
|
onRecheck = () => {
|
|
412
622
|
this.syncVisible();
|
|
623
|
+
this.scheduleScroll();
|
|
413
624
|
};
|
|
414
625
|
syncVisible() {
|
|
415
|
-
this.el.hidden = this.maxScroll() <= 1;
|
|
626
|
+
this.el.hidden = this.maxScroll() <= 1 || this.scrollPos() < this.threshold;
|
|
627
|
+
}
|
|
628
|
+
/** rAF 节流:一帧内多次重估只吐一次;按钮隐藏期间照常调度 */
|
|
629
|
+
scheduleScroll() {
|
|
630
|
+
if (this.emitRaf != null || this.destroyed) return;
|
|
631
|
+
this.emitRaf = requestAnimationFrame(() => {
|
|
632
|
+
this.emitRaf = null;
|
|
633
|
+
this.emitScroll();
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
emitScroll() {
|
|
637
|
+
if (this.destroyed) return;
|
|
638
|
+
const max = this.maxScroll();
|
|
639
|
+
const pct = max <= 0 ? 0 : Math.min(1, Math.max(0, this.scrollPos() / max));
|
|
640
|
+
if (pct === this.lastPct) return;
|
|
641
|
+
this.lastPct = pct;
|
|
642
|
+
this.onScroll?.(pct);
|
|
416
643
|
}
|
|
417
644
|
};
|
|
418
645
|
//#endregion
|