@xingwangzhe/stalux 1.55.2 → 1.55.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/package.json +1 -1
- package/src/scripts/swup-router.ts +28 -24
- package/src/scripts/toc-scrollspy.ts +29 -1
package/package.json
CHANGED
|
@@ -438,25 +438,20 @@ export function installSwupRouter(): Swup {
|
|
|
438
438
|
// then watch it jump to the entry offset and slide back, which reads as
|
|
439
439
|
// the page shaking. Put the scroll at its final position and start the
|
|
440
440
|
// cue in this same synchronous block, before the scripts run, so the
|
|
441
|
-
// compositor already owns the cue while the main thread is busy.
|
|
442
|
-
//
|
|
443
|
-
//
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
context.
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
const top = position?.top ?? 0;
|
|
456
|
-
const left = position?.left ?? 0;
|
|
457
|
-
if (window.scrollY !== top || window.scrollX !== left)
|
|
458
|
-
window.scrollTo({ top, left, behavior: "instant" });
|
|
459
|
-
startMotion(visit);
|
|
441
|
+
// compositor already owns the cue while the main thread is busy. Hash
|
|
442
|
+
// targets and history traversals are settled in content:scroll below.
|
|
443
|
+
// Entering a new document from a link always starts at its top. The
|
|
444
|
+
// motion preset (reduced motion / hidden tab) and the ScrollPlugin's
|
|
445
|
+
// reset flag are visual concerns, not navigation ones; neither may opt
|
|
446
|
+
// out of the reset. A cached offset belongs to history traversals only
|
|
447
|
+
// (handled in content:scroll) and must never pull a fresh page back
|
|
448
|
+
// into the middle of the previous article.
|
|
449
|
+
const startsFresh =
|
|
450
|
+
!visit.history.popstate && context.navigationType !== "traverse" && !context.to.hash;
|
|
451
|
+
if (startsFresh) {
|
|
452
|
+
if (window.scrollY !== 0 || window.scrollX !== 0)
|
|
453
|
+
window.scrollTo({ top: 0, left: 0, behavior: "instant" });
|
|
454
|
+
if (context.preset !== "none" && visit.scroll?.reset !== false) startMotion(visit);
|
|
460
455
|
}
|
|
461
456
|
context.scriptsReady = context.executeScripts?.(context.controller.signal);
|
|
462
457
|
});
|
|
@@ -464,15 +459,16 @@ export function installSwupRouter(): Swup {
|
|
|
464
459
|
swup.hooks.replace(
|
|
465
460
|
"content:scroll",
|
|
466
461
|
async (visit, args, defaultScroll) => {
|
|
462
|
+
const context = contexts.get(visit);
|
|
467
463
|
const saved = visit.history.popstate
|
|
468
464
|
? historyPositions.get(Number(window.history.state?.index))
|
|
469
465
|
: undefined;
|
|
470
466
|
if (visit.history.popstate) {
|
|
471
|
-
//
|
|
472
|
-
//
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
window.scrollTo({ top:
|
|
467
|
+
// Every route change starts at the top. Restoring the exact
|
|
468
|
+
// mid-article offset on back/forward reads as "the navigation
|
|
469
|
+
// never scrolled"; keep only the focused link so keyboard users
|
|
470
|
+
// retain their place in the navigation.
|
|
471
|
+
window.scrollTo({ top: 0, left: 0, behavior: "instant" });
|
|
476
472
|
scroll.restoreScrollContainers(visit.to.url);
|
|
477
473
|
if (saved?.focus) {
|
|
478
474
|
const link = [...document.querySelectorAll<HTMLAnchorElement>("a[href]")].find(
|
|
@@ -487,6 +483,14 @@ export function installSwupRouter(): Swup {
|
|
|
487
483
|
// ScrollPlugin scrolls in rAF; finish it before heading visibility checks.
|
|
488
484
|
if (!document.hidden)
|
|
489
485
|
await new Promise<void>((resolve) => requestAnimationFrame(() => resolve()));
|
|
486
|
+
// That deferred frame may have restored a cached mid-article offset.
|
|
487
|
+
// A link navigation always wins: land the new document at its top.
|
|
488
|
+
if (
|
|
489
|
+
context?.navigationType !== "traverse" &&
|
|
490
|
+
!context?.to.hash &&
|
|
491
|
+
(window.scrollY !== 0 || window.scrollX !== 0)
|
|
492
|
+
)
|
|
493
|
+
window.scrollTo({ top: 0, left: 0, behavior: "instant" });
|
|
490
494
|
startMotion(visit);
|
|
491
495
|
return true;
|
|
492
496
|
},
|
|
@@ -1,8 +1,36 @@
|
|
|
1
1
|
import { initTocScrollSpy } from "../utils/toc-scrollspy";
|
|
2
2
|
import { registerPageLifecycle } from "./page-runtime";
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* 目录高亮初始化会读取全部标题位置,强制一次完整布局:线上文章页实测
|
|
6
|
+
* 226ms 长任务(JS 仅 21ms,其余是 Style & Layout),正好落在 TBT 统计窗口内。
|
|
7
|
+
* 而读者进入文章后通常先看开头,滚到需要高亮的时机远晚于首屏,
|
|
8
|
+
* 因此把它放到空闲时段;2s 兜底保证页面持续繁忙时仍会初始化。
|
|
9
|
+
*/
|
|
4
10
|
registerPageLifecycle("toc-scrollspy", () => {
|
|
5
11
|
const container = document.querySelector<HTMLElement>("[data-toc-active-class]");
|
|
6
12
|
const activeClass = container?.dataset.tocActiveClass;
|
|
7
|
-
|
|
13
|
+
if (!activeClass) return;
|
|
14
|
+
let cleanup: (() => void) | undefined;
|
|
15
|
+
let disposed = false;
|
|
16
|
+
const run = () => {
|
|
17
|
+
if (!disposed) cleanup = initTocScrollSpy(activeClass);
|
|
18
|
+
};
|
|
19
|
+
// 通过可选属性读取,避免 TS 把初始化值收窄成「必定存在」而报常真判断。
|
|
20
|
+
const idle = (
|
|
21
|
+
globalThis as {
|
|
22
|
+
requestIdleCallback?: (
|
|
23
|
+
callback: IdleRequestCallback,
|
|
24
|
+
options?: IdleRequestOptions,
|
|
25
|
+
) => number;
|
|
26
|
+
}
|
|
27
|
+
).requestIdleCallback;
|
|
28
|
+
const handle = idle ? idle(run, { timeout: 2000 }) : setTimeout(run, 300);
|
|
29
|
+
return () => {
|
|
30
|
+
disposed = true;
|
|
31
|
+
// requestIdleCallback 与 cancelIdleCallback 在支持后者的实现里总是成对出现。
|
|
32
|
+
if (idle) globalThis.cancelIdleCallback(handle as number);
|
|
33
|
+
else clearTimeout(handle as unknown as ReturnType<typeof setTimeout>);
|
|
34
|
+
cleanup?.();
|
|
35
|
+
};
|
|
8
36
|
});
|