@moku-labs/web 1.6.0 → 1.6.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/dist/browser.mjs +58 -14
- package/dist/index.cjs +58 -14
- package/dist/index.mjs +58 -14
- package/package.json +1 -1
package/dist/browser.mjs
CHANGED
|
@@ -3636,15 +3636,28 @@ async function performNavigation(pathname, handlers) {
|
|
|
3636
3636
|
* Run a DOM-mutating swap, optionally wrapped in the View Transitions API when
|
|
3637
3637
|
* enabled and supported (instant swap otherwise — never throws).
|
|
3638
3638
|
*
|
|
3639
|
+
* `beforeCapture` runs synchronously immediately before the swap — for the View
|
|
3640
|
+
* Transitions path that is before `startViewTransition` captures the "old" snapshot.
|
|
3641
|
+
* Scroll restoration belongs HERE, not in the router after the navigation: setting
|
|
3642
|
+
* the destination scroll at this moment means the old and new snapshots share the
|
|
3643
|
+
* same scrollY, so there is no scroll delta for the transition to animate and a
|
|
3644
|
+
* `position: sticky` header never un-pins (the cross-engine flicker, worst on WebKit).
|
|
3645
|
+
* Because the swap is post-fetch, doing it here also avoids the visible "scroll up,
|
|
3646
|
+
* THEN the page loads" pause that scrolling in the router (pre-fetch) produced.
|
|
3647
|
+
*
|
|
3639
3648
|
* @param doSwap - The synchronous DOM mutation to perform.
|
|
3640
3649
|
* @param viewTransitions - Whether to wrap the swap in `startViewTransition`.
|
|
3650
|
+
* @param beforeCapture - Optional hook run synchronously just before the swap/capture
|
|
3651
|
+
* (e.g. scroll to the destination position).
|
|
3641
3652
|
* @example
|
|
3642
|
-
* runSwap(() => current.replaceWith(next), true);
|
|
3653
|
+
* runSwap(() => current.replaceWith(next), true, () => scrollTo({ top: 0, behavior: "instant" }));
|
|
3643
3654
|
*/
|
|
3644
|
-
function runSwap(doSwap, viewTransitions) {
|
|
3655
|
+
function runSwap(doSwap, viewTransitions, beforeCapture) {
|
|
3645
3656
|
const reduced = typeof globalThis.matchMedia === "function" && globalThis.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
3646
3657
|
const docWithVt = document;
|
|
3647
|
-
|
|
3658
|
+
const canUseViewTransitions = viewTransitions && !reduced && typeof docWithVt.startViewTransition === "function";
|
|
3659
|
+
beforeCapture?.();
|
|
3660
|
+
if (canUseViewTransitions) docWithVt.startViewTransition(doSwap);
|
|
3648
3661
|
else doSwap();
|
|
3649
3662
|
}
|
|
3650
3663
|
/**
|
|
@@ -3657,17 +3670,19 @@ function runSwap(doSwap, viewTransitions) {
|
|
|
3657
3670
|
* @param swapSelector - CSS selector for the region to replace.
|
|
3658
3671
|
* @param viewTransitions - Whether to wrap the swap in `startViewTransition`.
|
|
3659
3672
|
* @param onSwapped - Callback run after the DOM mutation (mount/notify/scroll).
|
|
3673
|
+
* @param beforeCapture - Optional hook run synchronously just before the swap/capture
|
|
3674
|
+
* (forwarded to {@link runSwap} — e.g. scroll to the destination position).
|
|
3660
3675
|
* @example
|
|
3661
3676
|
* swapRegion(doc, "main > section", false, () => mountNew());
|
|
3662
3677
|
*/
|
|
3663
|
-
function swapRegion(doc, swapSelector, viewTransitions, onSwapped) {
|
|
3678
|
+
function swapRegion(doc, swapSelector, viewTransitions, onSwapped, beforeCapture) {
|
|
3664
3679
|
const newContent = doc.querySelector(swapSelector);
|
|
3665
3680
|
const currentContent = document.querySelector(swapSelector);
|
|
3666
3681
|
if (!newContent || !currentContent) return;
|
|
3667
3682
|
runSwap(() => {
|
|
3668
3683
|
currentContent.replaceWith(newContent);
|
|
3669
3684
|
onSwapped();
|
|
3670
|
-
}, viewTransitions);
|
|
3685
|
+
}, viewTransitions, beforeCapture);
|
|
3671
3686
|
}
|
|
3672
3687
|
/**
|
|
3673
3688
|
* Resolve a navigable internal URL from a click event, or `undefined` when the
|
|
@@ -3722,7 +3737,7 @@ function attachHistoryFallback(handlers, navigate = (pathname) => performNavigat
|
|
|
3722
3737
|
}
|
|
3723
3738
|
saveScrollPosition(location.pathname);
|
|
3724
3739
|
history.pushState({ scrollY: 0 }, "", url.pathname);
|
|
3725
|
-
navigate(url.pathname).
|
|
3740
|
+
navigate(url.pathname).catch(() => {});
|
|
3726
3741
|
};
|
|
3727
3742
|
/**
|
|
3728
3743
|
* Re-run navigation on back/forward, restoring the saved scroll position.
|
|
@@ -3731,7 +3746,7 @@ function attachHistoryFallback(handlers, navigate = (pathname) => performNavigat
|
|
|
3731
3746
|
* globalThis.addEventListener("popstate", onPopState);
|
|
3732
3747
|
*/
|
|
3733
3748
|
const onPopState = () => {
|
|
3734
|
-
navigate(location.pathname).then(() => restoreScrollPosition(location.pathname)).catch(() => {});
|
|
3749
|
+
navigate(location.pathname, false).then(() => restoreScrollPosition(location.pathname)).catch(() => {});
|
|
3735
3750
|
};
|
|
3736
3751
|
document.addEventListener("click", onClick);
|
|
3737
3752
|
globalThis.addEventListener("popstate", onPopState);
|
|
@@ -3775,9 +3790,10 @@ function attachNavigationApi(navigation, handlers, navigate = (pathname) => perf
|
|
|
3775
3790
|
navEvent.intercept({
|
|
3776
3791
|
scroll: "manual",
|
|
3777
3792
|
handler: async () => {
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
|
|
3793
|
+
if (navEvent.navigationType === "traverse") {
|
|
3794
|
+
await navigate(url.pathname, false);
|
|
3795
|
+
navEvent.scroll();
|
|
3796
|
+
} else await navigate(url.pathname);
|
|
3781
3797
|
}
|
|
3782
3798
|
});
|
|
3783
3799
|
};
|
|
@@ -3936,6 +3952,31 @@ function syncDataHead(route, routeContext) {
|
|
|
3936
3952
|
function createSpaKernel(state, config, emit, deps) {
|
|
3937
3953
|
const resolved = resolveSpaConfig(config);
|
|
3938
3954
|
let progress;
|
|
3955
|
+
let pendingScrollToTop = true;
|
|
3956
|
+
/**
|
|
3957
|
+
* Apply the in-flight navigation's scroll intent — the swap's `beforeCapture` hook.
|
|
3958
|
+
* For a forward nav it scrolls to top BEFORE the snapshot is captured, so the old and
|
|
3959
|
+
* new states share scrollY=0 (no delta → the sticky header never un-pins) and there is
|
|
3960
|
+
* no pre-fetch scroll pause. Traverse (back/forward) sets `pendingScrollToTop = false`
|
|
3961
|
+
* and restores its saved position after the swap instead.
|
|
3962
|
+
*
|
|
3963
|
+
* Scroll behaviour: `"instant"` ONLY when view transitions are enabled — that is what
|
|
3964
|
+
* keeps scrollY=0 in the captured snapshot (a `scroll-behavior: smooth` would otherwise
|
|
3965
|
+
* animate the reset and re-create the delta → sticky-header flicker). With view
|
|
3966
|
+
* transitions OFF there is no snapshot to protect, so it honours the page's
|
|
3967
|
+
* `scroll-behavior` (`"auto"` = use the CSS value, e.g. a smooth scroll-to-top on nav).
|
|
3968
|
+
*
|
|
3969
|
+
* @example
|
|
3970
|
+
* runSwap(renderAndMount, viewTransitions, applyPendingScroll);
|
|
3971
|
+
*/
|
|
3972
|
+
const applyPendingScroll = () => {
|
|
3973
|
+
if (!pendingScrollToTop) return;
|
|
3974
|
+
const behavior = resolved.viewTransitions ? "instant" : "auto";
|
|
3975
|
+
window.scrollTo({
|
|
3976
|
+
top: 0,
|
|
3977
|
+
behavior
|
|
3978
|
+
});
|
|
3979
|
+
};
|
|
3939
3980
|
/**
|
|
3940
3981
|
* Process one navigation: head-sync, unmount, swap, re-mount, emit navigated.
|
|
3941
3982
|
*
|
|
@@ -3951,7 +3992,7 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
3951
3992
|
swapRegion(doc, resolved.swapSelector, resolved.viewTransitions, () => {
|
|
3952
3993
|
scanAndMount(state, emit, resolved.swapSelector);
|
|
3953
3994
|
notifyNavEnd(state);
|
|
3954
|
-
});
|
|
3995
|
+
}, applyPendingScroll);
|
|
3955
3996
|
state.currentUrl = pathname;
|
|
3956
3997
|
progress?.done();
|
|
3957
3998
|
emit("spa:navigated", { url: pathname });
|
|
@@ -4046,7 +4087,7 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
4046
4087
|
*
|
|
4047
4088
|
* @example
|
|
4048
4089
|
* ```ts
|
|
4049
|
-
* runSwap(renderAndMount, resolved.viewTransitions);
|
|
4090
|
+
* runSwap(renderAndMount, resolved.viewTransitions, applyPendingScroll);
|
|
4050
4091
|
* ```
|
|
4051
4092
|
*/
|
|
4052
4093
|
const renderAndMount = () => {
|
|
@@ -4054,7 +4095,7 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
4054
4095
|
scanAndMount(state, emit, resolved.swapSelector);
|
|
4055
4096
|
notifyNavEnd(state);
|
|
4056
4097
|
};
|
|
4057
|
-
runSwap(renderAndMount, resolved.viewTransitions);
|
|
4098
|
+
runSwap(renderAndMount, resolved.viewTransitions, applyPendingScroll);
|
|
4058
4099
|
state.currentUrl = pathname;
|
|
4059
4100
|
progress?.done();
|
|
4060
4101
|
emit("spa:navigated", { url: pathname });
|
|
@@ -4091,11 +4132,14 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
4091
4132
|
* navigation entry point (Navigation API, History, programmatic) goes through it.
|
|
4092
4133
|
*
|
|
4093
4134
|
* @param pathname - The destination pathname.
|
|
4135
|
+
* @param scrollToTop - Whether the swap should scroll to top before its snapshot
|
|
4136
|
+
* (default `true`; forward navs). Traverse passes `false` to keep its restored scroll.
|
|
4094
4137
|
* @returns A promise resolving once the swap (or fallback) is dispatched.
|
|
4095
4138
|
* @example
|
|
4096
4139
|
* await navigate("/en/world/");
|
|
4097
4140
|
*/
|
|
4098
|
-
const navigate = async (pathname) => {
|
|
4141
|
+
const navigate = async (pathname, scrollToTop = true) => {
|
|
4142
|
+
pendingScrollToTop = scrollToTop;
|
|
4099
4143
|
if (deps.router.mode() !== "ssg" && await tryDataRender(pathname)) return;
|
|
4100
4144
|
await performNavigation(pathname, handlers);
|
|
4101
4145
|
};
|
package/dist/index.cjs
CHANGED
|
@@ -10112,15 +10112,28 @@ async function performNavigation(pathname, handlers) {
|
|
|
10112
10112
|
* Run a DOM-mutating swap, optionally wrapped in the View Transitions API when
|
|
10113
10113
|
* enabled and supported (instant swap otherwise — never throws).
|
|
10114
10114
|
*
|
|
10115
|
+
* `beforeCapture` runs synchronously immediately before the swap — for the View
|
|
10116
|
+
* Transitions path that is before `startViewTransition` captures the "old" snapshot.
|
|
10117
|
+
* Scroll restoration belongs HERE, not in the router after the navigation: setting
|
|
10118
|
+
* the destination scroll at this moment means the old and new snapshots share the
|
|
10119
|
+
* same scrollY, so there is no scroll delta for the transition to animate and a
|
|
10120
|
+
* `position: sticky` header never un-pins (the cross-engine flicker, worst on WebKit).
|
|
10121
|
+
* Because the swap is post-fetch, doing it here also avoids the visible "scroll up,
|
|
10122
|
+
* THEN the page loads" pause that scrolling in the router (pre-fetch) produced.
|
|
10123
|
+
*
|
|
10115
10124
|
* @param doSwap - The synchronous DOM mutation to perform.
|
|
10116
10125
|
* @param viewTransitions - Whether to wrap the swap in `startViewTransition`.
|
|
10126
|
+
* @param beforeCapture - Optional hook run synchronously just before the swap/capture
|
|
10127
|
+
* (e.g. scroll to the destination position).
|
|
10117
10128
|
* @example
|
|
10118
|
-
* runSwap(() => current.replaceWith(next), true);
|
|
10129
|
+
* runSwap(() => current.replaceWith(next), true, () => scrollTo({ top: 0, behavior: "instant" }));
|
|
10119
10130
|
*/
|
|
10120
|
-
function runSwap(doSwap, viewTransitions) {
|
|
10131
|
+
function runSwap(doSwap, viewTransitions, beforeCapture) {
|
|
10121
10132
|
const reduced = typeof globalThis.matchMedia === "function" && globalThis.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
10122
10133
|
const docWithVt = document;
|
|
10123
|
-
|
|
10134
|
+
const canUseViewTransitions = viewTransitions && !reduced && typeof docWithVt.startViewTransition === "function";
|
|
10135
|
+
beforeCapture?.();
|
|
10136
|
+
if (canUseViewTransitions) docWithVt.startViewTransition(doSwap);
|
|
10124
10137
|
else doSwap();
|
|
10125
10138
|
}
|
|
10126
10139
|
/**
|
|
@@ -10133,17 +10146,19 @@ function runSwap(doSwap, viewTransitions) {
|
|
|
10133
10146
|
* @param swapSelector - CSS selector for the region to replace.
|
|
10134
10147
|
* @param viewTransitions - Whether to wrap the swap in `startViewTransition`.
|
|
10135
10148
|
* @param onSwapped - Callback run after the DOM mutation (mount/notify/scroll).
|
|
10149
|
+
* @param beforeCapture - Optional hook run synchronously just before the swap/capture
|
|
10150
|
+
* (forwarded to {@link runSwap} — e.g. scroll to the destination position).
|
|
10136
10151
|
* @example
|
|
10137
10152
|
* swapRegion(doc, "main > section", false, () => mountNew());
|
|
10138
10153
|
*/
|
|
10139
|
-
function swapRegion(doc, swapSelector, viewTransitions, onSwapped) {
|
|
10154
|
+
function swapRegion(doc, swapSelector, viewTransitions, onSwapped, beforeCapture) {
|
|
10140
10155
|
const newContent = doc.querySelector(swapSelector);
|
|
10141
10156
|
const currentContent = document.querySelector(swapSelector);
|
|
10142
10157
|
if (!newContent || !currentContent) return;
|
|
10143
10158
|
runSwap(() => {
|
|
10144
10159
|
currentContent.replaceWith(newContent);
|
|
10145
10160
|
onSwapped();
|
|
10146
|
-
}, viewTransitions);
|
|
10161
|
+
}, viewTransitions, beforeCapture);
|
|
10147
10162
|
}
|
|
10148
10163
|
/**
|
|
10149
10164
|
* Resolve a navigable internal URL from a click event, or `undefined` when the
|
|
@@ -10198,7 +10213,7 @@ function attachHistoryFallback(handlers, navigate = (pathname) => performNavigat
|
|
|
10198
10213
|
}
|
|
10199
10214
|
saveScrollPosition(location.pathname);
|
|
10200
10215
|
history.pushState({ scrollY: 0 }, "", url.pathname);
|
|
10201
|
-
navigate(url.pathname).
|
|
10216
|
+
navigate(url.pathname).catch(() => {});
|
|
10202
10217
|
};
|
|
10203
10218
|
/**
|
|
10204
10219
|
* Re-run navigation on back/forward, restoring the saved scroll position.
|
|
@@ -10207,7 +10222,7 @@ function attachHistoryFallback(handlers, navigate = (pathname) => performNavigat
|
|
|
10207
10222
|
* globalThis.addEventListener("popstate", onPopState);
|
|
10208
10223
|
*/
|
|
10209
10224
|
const onPopState = () => {
|
|
10210
|
-
navigate(location.pathname).then(() => restoreScrollPosition(location.pathname)).catch(() => {});
|
|
10225
|
+
navigate(location.pathname, false).then(() => restoreScrollPosition(location.pathname)).catch(() => {});
|
|
10211
10226
|
};
|
|
10212
10227
|
document.addEventListener("click", onClick);
|
|
10213
10228
|
globalThis.addEventListener("popstate", onPopState);
|
|
@@ -10251,9 +10266,10 @@ function attachNavigationApi(navigation, handlers, navigate = (pathname) => perf
|
|
|
10251
10266
|
navEvent.intercept({
|
|
10252
10267
|
scroll: "manual",
|
|
10253
10268
|
handler: async () => {
|
|
10254
|
-
|
|
10255
|
-
|
|
10256
|
-
|
|
10269
|
+
if (navEvent.navigationType === "traverse") {
|
|
10270
|
+
await navigate(url.pathname, false);
|
|
10271
|
+
navEvent.scroll();
|
|
10272
|
+
} else await navigate(url.pathname);
|
|
10257
10273
|
}
|
|
10258
10274
|
});
|
|
10259
10275
|
};
|
|
@@ -10412,6 +10428,31 @@ function syncDataHead(route, routeContext) {
|
|
|
10412
10428
|
function createSpaKernel(state, config, emit, deps) {
|
|
10413
10429
|
const resolved = resolveSpaConfig(config);
|
|
10414
10430
|
let progress;
|
|
10431
|
+
let pendingScrollToTop = true;
|
|
10432
|
+
/**
|
|
10433
|
+
* Apply the in-flight navigation's scroll intent — the swap's `beforeCapture` hook.
|
|
10434
|
+
* For a forward nav it scrolls to top BEFORE the snapshot is captured, so the old and
|
|
10435
|
+
* new states share scrollY=0 (no delta → the sticky header never un-pins) and there is
|
|
10436
|
+
* no pre-fetch scroll pause. Traverse (back/forward) sets `pendingScrollToTop = false`
|
|
10437
|
+
* and restores its saved position after the swap instead.
|
|
10438
|
+
*
|
|
10439
|
+
* Scroll behaviour: `"instant"` ONLY when view transitions are enabled — that is what
|
|
10440
|
+
* keeps scrollY=0 in the captured snapshot (a `scroll-behavior: smooth` would otherwise
|
|
10441
|
+
* animate the reset and re-create the delta → sticky-header flicker). With view
|
|
10442
|
+
* transitions OFF there is no snapshot to protect, so it honours the page's
|
|
10443
|
+
* `scroll-behavior` (`"auto"` = use the CSS value, e.g. a smooth scroll-to-top on nav).
|
|
10444
|
+
*
|
|
10445
|
+
* @example
|
|
10446
|
+
* runSwap(renderAndMount, viewTransitions, applyPendingScroll);
|
|
10447
|
+
*/
|
|
10448
|
+
const applyPendingScroll = () => {
|
|
10449
|
+
if (!pendingScrollToTop) return;
|
|
10450
|
+
const behavior = resolved.viewTransitions ? "instant" : "auto";
|
|
10451
|
+
window.scrollTo({
|
|
10452
|
+
top: 0,
|
|
10453
|
+
behavior
|
|
10454
|
+
});
|
|
10455
|
+
};
|
|
10415
10456
|
/**
|
|
10416
10457
|
* Process one navigation: head-sync, unmount, swap, re-mount, emit navigated.
|
|
10417
10458
|
*
|
|
@@ -10427,7 +10468,7 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
10427
10468
|
swapRegion(doc, resolved.swapSelector, resolved.viewTransitions, () => {
|
|
10428
10469
|
scanAndMount(state, emit, resolved.swapSelector);
|
|
10429
10470
|
notifyNavEnd(state);
|
|
10430
|
-
});
|
|
10471
|
+
}, applyPendingScroll);
|
|
10431
10472
|
state.currentUrl = pathname;
|
|
10432
10473
|
progress?.done();
|
|
10433
10474
|
emit("spa:navigated", { url: pathname });
|
|
@@ -10522,7 +10563,7 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
10522
10563
|
*
|
|
10523
10564
|
* @example
|
|
10524
10565
|
* ```ts
|
|
10525
|
-
* runSwap(renderAndMount, resolved.viewTransitions);
|
|
10566
|
+
* runSwap(renderAndMount, resolved.viewTransitions, applyPendingScroll);
|
|
10526
10567
|
* ```
|
|
10527
10568
|
*/
|
|
10528
10569
|
const renderAndMount = () => {
|
|
@@ -10530,7 +10571,7 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
10530
10571
|
scanAndMount(state, emit, resolved.swapSelector);
|
|
10531
10572
|
notifyNavEnd(state);
|
|
10532
10573
|
};
|
|
10533
|
-
runSwap(renderAndMount, resolved.viewTransitions);
|
|
10574
|
+
runSwap(renderAndMount, resolved.viewTransitions, applyPendingScroll);
|
|
10534
10575
|
state.currentUrl = pathname;
|
|
10535
10576
|
progress?.done();
|
|
10536
10577
|
emit("spa:navigated", { url: pathname });
|
|
@@ -10567,11 +10608,14 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
10567
10608
|
* navigation entry point (Navigation API, History, programmatic) goes through it.
|
|
10568
10609
|
*
|
|
10569
10610
|
* @param pathname - The destination pathname.
|
|
10611
|
+
* @param scrollToTop - Whether the swap should scroll to top before its snapshot
|
|
10612
|
+
* (default `true`; forward navs). Traverse passes `false` to keep its restored scroll.
|
|
10570
10613
|
* @returns A promise resolving once the swap (or fallback) is dispatched.
|
|
10571
10614
|
* @example
|
|
10572
10615
|
* await navigate("/en/world/");
|
|
10573
10616
|
*/
|
|
10574
|
-
const navigate = async (pathname) => {
|
|
10617
|
+
const navigate = async (pathname, scrollToTop = true) => {
|
|
10618
|
+
pendingScrollToTop = scrollToTop;
|
|
10575
10619
|
if (deps.router.mode() !== "ssg" && await tryDataRender(pathname)) return;
|
|
10576
10620
|
await performNavigation(pathname, handlers);
|
|
10577
10621
|
};
|
package/dist/index.mjs
CHANGED
|
@@ -10099,15 +10099,28 @@ async function performNavigation(pathname, handlers) {
|
|
|
10099
10099
|
* Run a DOM-mutating swap, optionally wrapped in the View Transitions API when
|
|
10100
10100
|
* enabled and supported (instant swap otherwise — never throws).
|
|
10101
10101
|
*
|
|
10102
|
+
* `beforeCapture` runs synchronously immediately before the swap — for the View
|
|
10103
|
+
* Transitions path that is before `startViewTransition` captures the "old" snapshot.
|
|
10104
|
+
* Scroll restoration belongs HERE, not in the router after the navigation: setting
|
|
10105
|
+
* the destination scroll at this moment means the old and new snapshots share the
|
|
10106
|
+
* same scrollY, so there is no scroll delta for the transition to animate and a
|
|
10107
|
+
* `position: sticky` header never un-pins (the cross-engine flicker, worst on WebKit).
|
|
10108
|
+
* Because the swap is post-fetch, doing it here also avoids the visible "scroll up,
|
|
10109
|
+
* THEN the page loads" pause that scrolling in the router (pre-fetch) produced.
|
|
10110
|
+
*
|
|
10102
10111
|
* @param doSwap - The synchronous DOM mutation to perform.
|
|
10103
10112
|
* @param viewTransitions - Whether to wrap the swap in `startViewTransition`.
|
|
10113
|
+
* @param beforeCapture - Optional hook run synchronously just before the swap/capture
|
|
10114
|
+
* (e.g. scroll to the destination position).
|
|
10104
10115
|
* @example
|
|
10105
|
-
* runSwap(() => current.replaceWith(next), true);
|
|
10116
|
+
* runSwap(() => current.replaceWith(next), true, () => scrollTo({ top: 0, behavior: "instant" }));
|
|
10106
10117
|
*/
|
|
10107
|
-
function runSwap(doSwap, viewTransitions) {
|
|
10118
|
+
function runSwap(doSwap, viewTransitions, beforeCapture) {
|
|
10108
10119
|
const reduced = typeof globalThis.matchMedia === "function" && globalThis.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
10109
10120
|
const docWithVt = document;
|
|
10110
|
-
|
|
10121
|
+
const canUseViewTransitions = viewTransitions && !reduced && typeof docWithVt.startViewTransition === "function";
|
|
10122
|
+
beforeCapture?.();
|
|
10123
|
+
if (canUseViewTransitions) docWithVt.startViewTransition(doSwap);
|
|
10111
10124
|
else doSwap();
|
|
10112
10125
|
}
|
|
10113
10126
|
/**
|
|
@@ -10120,17 +10133,19 @@ function runSwap(doSwap, viewTransitions) {
|
|
|
10120
10133
|
* @param swapSelector - CSS selector for the region to replace.
|
|
10121
10134
|
* @param viewTransitions - Whether to wrap the swap in `startViewTransition`.
|
|
10122
10135
|
* @param onSwapped - Callback run after the DOM mutation (mount/notify/scroll).
|
|
10136
|
+
* @param beforeCapture - Optional hook run synchronously just before the swap/capture
|
|
10137
|
+
* (forwarded to {@link runSwap} — e.g. scroll to the destination position).
|
|
10123
10138
|
* @example
|
|
10124
10139
|
* swapRegion(doc, "main > section", false, () => mountNew());
|
|
10125
10140
|
*/
|
|
10126
|
-
function swapRegion(doc, swapSelector, viewTransitions, onSwapped) {
|
|
10141
|
+
function swapRegion(doc, swapSelector, viewTransitions, onSwapped, beforeCapture) {
|
|
10127
10142
|
const newContent = doc.querySelector(swapSelector);
|
|
10128
10143
|
const currentContent = document.querySelector(swapSelector);
|
|
10129
10144
|
if (!newContent || !currentContent) return;
|
|
10130
10145
|
runSwap(() => {
|
|
10131
10146
|
currentContent.replaceWith(newContent);
|
|
10132
10147
|
onSwapped();
|
|
10133
|
-
}, viewTransitions);
|
|
10148
|
+
}, viewTransitions, beforeCapture);
|
|
10134
10149
|
}
|
|
10135
10150
|
/**
|
|
10136
10151
|
* Resolve a navigable internal URL from a click event, or `undefined` when the
|
|
@@ -10185,7 +10200,7 @@ function attachHistoryFallback(handlers, navigate = (pathname) => performNavigat
|
|
|
10185
10200
|
}
|
|
10186
10201
|
saveScrollPosition(location.pathname);
|
|
10187
10202
|
history.pushState({ scrollY: 0 }, "", url.pathname);
|
|
10188
|
-
navigate(url.pathname).
|
|
10203
|
+
navigate(url.pathname).catch(() => {});
|
|
10189
10204
|
};
|
|
10190
10205
|
/**
|
|
10191
10206
|
* Re-run navigation on back/forward, restoring the saved scroll position.
|
|
@@ -10194,7 +10209,7 @@ function attachHistoryFallback(handlers, navigate = (pathname) => performNavigat
|
|
|
10194
10209
|
* globalThis.addEventListener("popstate", onPopState);
|
|
10195
10210
|
*/
|
|
10196
10211
|
const onPopState = () => {
|
|
10197
|
-
navigate(location.pathname).then(() => restoreScrollPosition(location.pathname)).catch(() => {});
|
|
10212
|
+
navigate(location.pathname, false).then(() => restoreScrollPosition(location.pathname)).catch(() => {});
|
|
10198
10213
|
};
|
|
10199
10214
|
document.addEventListener("click", onClick);
|
|
10200
10215
|
globalThis.addEventListener("popstate", onPopState);
|
|
@@ -10238,9 +10253,10 @@ function attachNavigationApi(navigation, handlers, navigate = (pathname) => perf
|
|
|
10238
10253
|
navEvent.intercept({
|
|
10239
10254
|
scroll: "manual",
|
|
10240
10255
|
handler: async () => {
|
|
10241
|
-
|
|
10242
|
-
|
|
10243
|
-
|
|
10256
|
+
if (navEvent.navigationType === "traverse") {
|
|
10257
|
+
await navigate(url.pathname, false);
|
|
10258
|
+
navEvent.scroll();
|
|
10259
|
+
} else await navigate(url.pathname);
|
|
10244
10260
|
}
|
|
10245
10261
|
});
|
|
10246
10262
|
};
|
|
@@ -10399,6 +10415,31 @@ function syncDataHead(route, routeContext) {
|
|
|
10399
10415
|
function createSpaKernel(state, config, emit, deps) {
|
|
10400
10416
|
const resolved = resolveSpaConfig(config);
|
|
10401
10417
|
let progress;
|
|
10418
|
+
let pendingScrollToTop = true;
|
|
10419
|
+
/**
|
|
10420
|
+
* Apply the in-flight navigation's scroll intent — the swap's `beforeCapture` hook.
|
|
10421
|
+
* For a forward nav it scrolls to top BEFORE the snapshot is captured, so the old and
|
|
10422
|
+
* new states share scrollY=0 (no delta → the sticky header never un-pins) and there is
|
|
10423
|
+
* no pre-fetch scroll pause. Traverse (back/forward) sets `pendingScrollToTop = false`
|
|
10424
|
+
* and restores its saved position after the swap instead.
|
|
10425
|
+
*
|
|
10426
|
+
* Scroll behaviour: `"instant"` ONLY when view transitions are enabled — that is what
|
|
10427
|
+
* keeps scrollY=0 in the captured snapshot (a `scroll-behavior: smooth` would otherwise
|
|
10428
|
+
* animate the reset and re-create the delta → sticky-header flicker). With view
|
|
10429
|
+
* transitions OFF there is no snapshot to protect, so it honours the page's
|
|
10430
|
+
* `scroll-behavior` (`"auto"` = use the CSS value, e.g. a smooth scroll-to-top on nav).
|
|
10431
|
+
*
|
|
10432
|
+
* @example
|
|
10433
|
+
* runSwap(renderAndMount, viewTransitions, applyPendingScroll);
|
|
10434
|
+
*/
|
|
10435
|
+
const applyPendingScroll = () => {
|
|
10436
|
+
if (!pendingScrollToTop) return;
|
|
10437
|
+
const behavior = resolved.viewTransitions ? "instant" : "auto";
|
|
10438
|
+
window.scrollTo({
|
|
10439
|
+
top: 0,
|
|
10440
|
+
behavior
|
|
10441
|
+
});
|
|
10442
|
+
};
|
|
10402
10443
|
/**
|
|
10403
10444
|
* Process one navigation: head-sync, unmount, swap, re-mount, emit navigated.
|
|
10404
10445
|
*
|
|
@@ -10414,7 +10455,7 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
10414
10455
|
swapRegion(doc, resolved.swapSelector, resolved.viewTransitions, () => {
|
|
10415
10456
|
scanAndMount(state, emit, resolved.swapSelector);
|
|
10416
10457
|
notifyNavEnd(state);
|
|
10417
|
-
});
|
|
10458
|
+
}, applyPendingScroll);
|
|
10418
10459
|
state.currentUrl = pathname;
|
|
10419
10460
|
progress?.done();
|
|
10420
10461
|
emit("spa:navigated", { url: pathname });
|
|
@@ -10509,7 +10550,7 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
10509
10550
|
*
|
|
10510
10551
|
* @example
|
|
10511
10552
|
* ```ts
|
|
10512
|
-
* runSwap(renderAndMount, resolved.viewTransitions);
|
|
10553
|
+
* runSwap(renderAndMount, resolved.viewTransitions, applyPendingScroll);
|
|
10513
10554
|
* ```
|
|
10514
10555
|
*/
|
|
10515
10556
|
const renderAndMount = () => {
|
|
@@ -10517,7 +10558,7 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
10517
10558
|
scanAndMount(state, emit, resolved.swapSelector);
|
|
10518
10559
|
notifyNavEnd(state);
|
|
10519
10560
|
};
|
|
10520
|
-
runSwap(renderAndMount, resolved.viewTransitions);
|
|
10561
|
+
runSwap(renderAndMount, resolved.viewTransitions, applyPendingScroll);
|
|
10521
10562
|
state.currentUrl = pathname;
|
|
10522
10563
|
progress?.done();
|
|
10523
10564
|
emit("spa:navigated", { url: pathname });
|
|
@@ -10554,11 +10595,14 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
10554
10595
|
* navigation entry point (Navigation API, History, programmatic) goes through it.
|
|
10555
10596
|
*
|
|
10556
10597
|
* @param pathname - The destination pathname.
|
|
10598
|
+
* @param scrollToTop - Whether the swap should scroll to top before its snapshot
|
|
10599
|
+
* (default `true`; forward navs). Traverse passes `false` to keep its restored scroll.
|
|
10557
10600
|
* @returns A promise resolving once the swap (or fallback) is dispatched.
|
|
10558
10601
|
* @example
|
|
10559
10602
|
* await navigate("/en/world/");
|
|
10560
10603
|
*/
|
|
10561
|
-
const navigate = async (pathname) => {
|
|
10604
|
+
const navigate = async (pathname, scrollToTop = true) => {
|
|
10605
|
+
pendingScrollToTop = scrollToTop;
|
|
10562
10606
|
if (deps.router.mode() !== "ssg" && await tryDataRender(pathname)) return;
|
|
10563
10607
|
await performNavigation(pathname, handlers);
|
|
10564
10608
|
};
|
package/package.json
CHANGED