@moku-labs/web 1.6.0 → 1.6.1
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 +52 -14
- package/dist/index.cjs +52 -14
- package/dist/index.mjs +52 -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,25 @@ 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. `behavior: "instant"` defeats a page-level
|
|
3961
|
+
* `scroll-behavior: smooth` that would otherwise animate the reset and re-create the
|
|
3962
|
+
* delta. Traverse (back/forward) sets `pendingScrollToTop = false` and restores its
|
|
3963
|
+
* saved position after the swap instead.
|
|
3964
|
+
*
|
|
3965
|
+
* @example
|
|
3966
|
+
* runSwap(renderAndMount, viewTransitions, applyPendingScroll);
|
|
3967
|
+
*/
|
|
3968
|
+
const applyPendingScroll = () => {
|
|
3969
|
+
if (pendingScrollToTop) window.scrollTo({
|
|
3970
|
+
top: 0,
|
|
3971
|
+
behavior: "instant"
|
|
3972
|
+
});
|
|
3973
|
+
};
|
|
3939
3974
|
/**
|
|
3940
3975
|
* Process one navigation: head-sync, unmount, swap, re-mount, emit navigated.
|
|
3941
3976
|
*
|
|
@@ -3951,7 +3986,7 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
3951
3986
|
swapRegion(doc, resolved.swapSelector, resolved.viewTransitions, () => {
|
|
3952
3987
|
scanAndMount(state, emit, resolved.swapSelector);
|
|
3953
3988
|
notifyNavEnd(state);
|
|
3954
|
-
});
|
|
3989
|
+
}, applyPendingScroll);
|
|
3955
3990
|
state.currentUrl = pathname;
|
|
3956
3991
|
progress?.done();
|
|
3957
3992
|
emit("spa:navigated", { url: pathname });
|
|
@@ -4046,7 +4081,7 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
4046
4081
|
*
|
|
4047
4082
|
* @example
|
|
4048
4083
|
* ```ts
|
|
4049
|
-
* runSwap(renderAndMount, resolved.viewTransitions);
|
|
4084
|
+
* runSwap(renderAndMount, resolved.viewTransitions, applyPendingScroll);
|
|
4050
4085
|
* ```
|
|
4051
4086
|
*/
|
|
4052
4087
|
const renderAndMount = () => {
|
|
@@ -4054,7 +4089,7 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
4054
4089
|
scanAndMount(state, emit, resolved.swapSelector);
|
|
4055
4090
|
notifyNavEnd(state);
|
|
4056
4091
|
};
|
|
4057
|
-
runSwap(renderAndMount, resolved.viewTransitions);
|
|
4092
|
+
runSwap(renderAndMount, resolved.viewTransitions, applyPendingScroll);
|
|
4058
4093
|
state.currentUrl = pathname;
|
|
4059
4094
|
progress?.done();
|
|
4060
4095
|
emit("spa:navigated", { url: pathname });
|
|
@@ -4091,11 +4126,14 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
4091
4126
|
* navigation entry point (Navigation API, History, programmatic) goes through it.
|
|
4092
4127
|
*
|
|
4093
4128
|
* @param pathname - The destination pathname.
|
|
4129
|
+
* @param scrollToTop - Whether the swap should scroll to top before its snapshot
|
|
4130
|
+
* (default `true`; forward navs). Traverse passes `false` to keep its restored scroll.
|
|
4094
4131
|
* @returns A promise resolving once the swap (or fallback) is dispatched.
|
|
4095
4132
|
* @example
|
|
4096
4133
|
* await navigate("/en/world/");
|
|
4097
4134
|
*/
|
|
4098
|
-
const navigate = async (pathname) => {
|
|
4135
|
+
const navigate = async (pathname, scrollToTop = true) => {
|
|
4136
|
+
pendingScrollToTop = scrollToTop;
|
|
4099
4137
|
if (deps.router.mode() !== "ssg" && await tryDataRender(pathname)) return;
|
|
4100
4138
|
await performNavigation(pathname, handlers);
|
|
4101
4139
|
};
|
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,25 @@ 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. `behavior: "instant"` defeats a page-level
|
|
10437
|
+
* `scroll-behavior: smooth` that would otherwise animate the reset and re-create the
|
|
10438
|
+
* delta. Traverse (back/forward) sets `pendingScrollToTop = false` and restores its
|
|
10439
|
+
* saved position after the swap instead.
|
|
10440
|
+
*
|
|
10441
|
+
* @example
|
|
10442
|
+
* runSwap(renderAndMount, viewTransitions, applyPendingScroll);
|
|
10443
|
+
*/
|
|
10444
|
+
const applyPendingScroll = () => {
|
|
10445
|
+
if (pendingScrollToTop) window.scrollTo({
|
|
10446
|
+
top: 0,
|
|
10447
|
+
behavior: "instant"
|
|
10448
|
+
});
|
|
10449
|
+
};
|
|
10415
10450
|
/**
|
|
10416
10451
|
* Process one navigation: head-sync, unmount, swap, re-mount, emit navigated.
|
|
10417
10452
|
*
|
|
@@ -10427,7 +10462,7 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
10427
10462
|
swapRegion(doc, resolved.swapSelector, resolved.viewTransitions, () => {
|
|
10428
10463
|
scanAndMount(state, emit, resolved.swapSelector);
|
|
10429
10464
|
notifyNavEnd(state);
|
|
10430
|
-
});
|
|
10465
|
+
}, applyPendingScroll);
|
|
10431
10466
|
state.currentUrl = pathname;
|
|
10432
10467
|
progress?.done();
|
|
10433
10468
|
emit("spa:navigated", { url: pathname });
|
|
@@ -10522,7 +10557,7 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
10522
10557
|
*
|
|
10523
10558
|
* @example
|
|
10524
10559
|
* ```ts
|
|
10525
|
-
* runSwap(renderAndMount, resolved.viewTransitions);
|
|
10560
|
+
* runSwap(renderAndMount, resolved.viewTransitions, applyPendingScroll);
|
|
10526
10561
|
* ```
|
|
10527
10562
|
*/
|
|
10528
10563
|
const renderAndMount = () => {
|
|
@@ -10530,7 +10565,7 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
10530
10565
|
scanAndMount(state, emit, resolved.swapSelector);
|
|
10531
10566
|
notifyNavEnd(state);
|
|
10532
10567
|
};
|
|
10533
|
-
runSwap(renderAndMount, resolved.viewTransitions);
|
|
10568
|
+
runSwap(renderAndMount, resolved.viewTransitions, applyPendingScroll);
|
|
10534
10569
|
state.currentUrl = pathname;
|
|
10535
10570
|
progress?.done();
|
|
10536
10571
|
emit("spa:navigated", { url: pathname });
|
|
@@ -10567,11 +10602,14 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
10567
10602
|
* navigation entry point (Navigation API, History, programmatic) goes through it.
|
|
10568
10603
|
*
|
|
10569
10604
|
* @param pathname - The destination pathname.
|
|
10605
|
+
* @param scrollToTop - Whether the swap should scroll to top before its snapshot
|
|
10606
|
+
* (default `true`; forward navs). Traverse passes `false` to keep its restored scroll.
|
|
10570
10607
|
* @returns A promise resolving once the swap (or fallback) is dispatched.
|
|
10571
10608
|
* @example
|
|
10572
10609
|
* await navigate("/en/world/");
|
|
10573
10610
|
*/
|
|
10574
|
-
const navigate = async (pathname) => {
|
|
10611
|
+
const navigate = async (pathname, scrollToTop = true) => {
|
|
10612
|
+
pendingScrollToTop = scrollToTop;
|
|
10575
10613
|
if (deps.router.mode() !== "ssg" && await tryDataRender(pathname)) return;
|
|
10576
10614
|
await performNavigation(pathname, handlers);
|
|
10577
10615
|
};
|
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,25 @@ 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. `behavior: "instant"` defeats a page-level
|
|
10424
|
+
* `scroll-behavior: smooth` that would otherwise animate the reset and re-create the
|
|
10425
|
+
* delta. Traverse (back/forward) sets `pendingScrollToTop = false` and restores its
|
|
10426
|
+
* saved position after the swap instead.
|
|
10427
|
+
*
|
|
10428
|
+
* @example
|
|
10429
|
+
* runSwap(renderAndMount, viewTransitions, applyPendingScroll);
|
|
10430
|
+
*/
|
|
10431
|
+
const applyPendingScroll = () => {
|
|
10432
|
+
if (pendingScrollToTop) window.scrollTo({
|
|
10433
|
+
top: 0,
|
|
10434
|
+
behavior: "instant"
|
|
10435
|
+
});
|
|
10436
|
+
};
|
|
10402
10437
|
/**
|
|
10403
10438
|
* Process one navigation: head-sync, unmount, swap, re-mount, emit navigated.
|
|
10404
10439
|
*
|
|
@@ -10414,7 +10449,7 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
10414
10449
|
swapRegion(doc, resolved.swapSelector, resolved.viewTransitions, () => {
|
|
10415
10450
|
scanAndMount(state, emit, resolved.swapSelector);
|
|
10416
10451
|
notifyNavEnd(state);
|
|
10417
|
-
});
|
|
10452
|
+
}, applyPendingScroll);
|
|
10418
10453
|
state.currentUrl = pathname;
|
|
10419
10454
|
progress?.done();
|
|
10420
10455
|
emit("spa:navigated", { url: pathname });
|
|
@@ -10509,7 +10544,7 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
10509
10544
|
*
|
|
10510
10545
|
* @example
|
|
10511
10546
|
* ```ts
|
|
10512
|
-
* runSwap(renderAndMount, resolved.viewTransitions);
|
|
10547
|
+
* runSwap(renderAndMount, resolved.viewTransitions, applyPendingScroll);
|
|
10513
10548
|
* ```
|
|
10514
10549
|
*/
|
|
10515
10550
|
const renderAndMount = () => {
|
|
@@ -10517,7 +10552,7 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
10517
10552
|
scanAndMount(state, emit, resolved.swapSelector);
|
|
10518
10553
|
notifyNavEnd(state);
|
|
10519
10554
|
};
|
|
10520
|
-
runSwap(renderAndMount, resolved.viewTransitions);
|
|
10555
|
+
runSwap(renderAndMount, resolved.viewTransitions, applyPendingScroll);
|
|
10521
10556
|
state.currentUrl = pathname;
|
|
10522
10557
|
progress?.done();
|
|
10523
10558
|
emit("spa:navigated", { url: pathname });
|
|
@@ -10554,11 +10589,14 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
10554
10589
|
* navigation entry point (Navigation API, History, programmatic) goes through it.
|
|
10555
10590
|
*
|
|
10556
10591
|
* @param pathname - The destination pathname.
|
|
10592
|
+
* @param scrollToTop - Whether the swap should scroll to top before its snapshot
|
|
10593
|
+
* (default `true`; forward navs). Traverse passes `false` to keep its restored scroll.
|
|
10557
10594
|
* @returns A promise resolving once the swap (or fallback) is dispatched.
|
|
10558
10595
|
* @example
|
|
10559
10596
|
* await navigate("/en/world/");
|
|
10560
10597
|
*/
|
|
10561
|
-
const navigate = async (pathname) => {
|
|
10598
|
+
const navigate = async (pathname, scrollToTop = true) => {
|
|
10599
|
+
pendingScrollToTop = scrollToTop;
|
|
10562
10600
|
if (deps.router.mode() !== "ssg" && await tryDataRender(pathname)) return;
|
|
10563
10601
|
await performNavigation(pathname, handlers);
|
|
10564
10602
|
};
|
package/package.json
CHANGED