@takazudo/zfb-runtime 0.1.0-next.75 → 0.1.0-next.77
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 +44 -0
- package/dist/client-router/index.d.ts +2 -2
- package/dist/client-router/index.js +2 -1
- package/dist/client-router/index.js.map +1 -1
- package/dist/client-router/prefetch.js +27 -8
- package/dist/client-router/prefetch.js.map +1 -1
- package/dist/client-router/router.d.ts +28 -1
- package/dist/client-router/router.js +158 -11
- package/dist/client-router/router.js.map +1 -1
- package/dist/client-router/swap-functions.js +35 -7
- package/dist/client-router/swap-functions.js.map +1 -1
- package/dist/client-router/types.d.ts +4 -0
- package/dist/client-router/types.js.map +1 -1
- package/dist/client-router.d.ts +18 -1
- package/dist/client-router.js +11 -1
- package/dist/client-router.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/router.d.ts +15 -1
- package/dist/router.js +21 -0
- package/dist/router.js.map +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -204,6 +204,50 @@ on its own. See the [Client-Side Routing concept
|
|
|
204
204
|
guide](https://takazudomodular.com/pj/zudo-front-builder/docs/concepts/client-side-routing/)
|
|
205
205
|
for the full API.
|
|
206
206
|
|
|
207
|
+
#### `navigate()` needs `<ClientRouter />` mounted on the current page
|
|
208
|
+
|
|
209
|
+
The root barrel exports `navigate` and `syncHistoryEntry`, but **not** `init`
|
|
210
|
+
— importing either of those two on their own is not enough to get soft
|
|
211
|
+
navigation. `navigate()` checks for the `<meta
|
|
212
|
+
name="zfb-view-transitions-enabled">` tag that `<ClientRouter />` renders into
|
|
213
|
+
`<head>` before it will do a soft swap; with that tag absent it silently falls
|
|
214
|
+
back to a full `location.href` load. Mount `<ClientRouter />` in the layout
|
|
215
|
+
`<head>` of every page that should be soft-navigable — it both renders that
|
|
216
|
+
meta tag and calls `init()` (click/form interception) as a side effect. `init`
|
|
217
|
+
itself is exported from the `@takazudo/zfb-runtime/client-router` subpath, not
|
|
218
|
+
the root barrel, for the rare case where you want to call it directly instead
|
|
219
|
+
of mounting the component.
|
|
220
|
+
|
|
221
|
+
#### Persisting elements and island state across navigations (`data-zfb-transition-persist`)
|
|
222
|
+
|
|
223
|
+
Add `data-zfb-transition-persist="<id>"` to an element (matched by the same
|
|
224
|
+
`id` on both the outgoing and incoming page) to keep it alive across a soft
|
|
225
|
+
navigation instead of letting it be discarded and re-created — the router
|
|
226
|
+
lifts it out of the old body and reattaches it into the new one. This works
|
|
227
|
+
on plain elements (`<video>`, `<canvas>`) and on island wrapper elements
|
|
228
|
+
(`[data-zfb-island]`) alike; for an island, the live component instance (and
|
|
229
|
+
its internal state) survives too, not just the DOM node.
|
|
230
|
+
|
|
231
|
+
```tsx
|
|
232
|
+
<div data-zfb-island="SidebarTree" data-zfb-transition-persist="sidebar-tree" data-props={props}>
|
|
233
|
+
<SidebarTree {...props} />
|
|
234
|
+
</div>
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
By default, a persisted island still gets its `data-props` refreshed to match
|
|
238
|
+
the incoming page on every swap; if the refreshed props differ from what the
|
|
239
|
+
live instance currently holds, the island is unmounted and remounted fresh
|
|
240
|
+
with the new props (so it can't get stuck showing stale data), otherwise
|
|
241
|
+
nothing happens and the instance's state carries over untouched. Set
|
|
242
|
+
`data-zfb-transition-persist-props` to any value other than `"false"`
|
|
243
|
+
(conventionally `"true"`) to opt OUT of that props refresh and keep the
|
|
244
|
+
island's current props/state exactly as they are, regardless of what the
|
|
245
|
+
incoming page's props would have been — the attribute's absence, or the
|
|
246
|
+
literal string `"false"`, is what makes props refresh (mirrors Astro's
|
|
247
|
+
`data-astro-transition-persist-props`). See the [Client-Side Routing concept
|
|
248
|
+
guide](https://takazudomodular.com/pj/zudo-front-builder/docs/concepts/client-side-routing/)
|
|
249
|
+
for the full walkthrough, including when to reach for the opt-out.
|
|
250
|
+
|
|
207
251
|
### `createPageRouter(options) → PageRouter`
|
|
208
252
|
|
|
209
253
|
Build a fetch-handler that serves the supplied pages. The returned
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
export { ClientRouter, type ClientRouterProps } from "../client-router.js";
|
|
2
2
|
export { TRANSITION_BEFORE_PREPARATION, TRANSITION_AFTER_PREPARATION, TRANSITION_BEFORE_SWAP, TRANSITION_AFTER_SWAP, TRANSITION_PAGE_LOAD, TRANSITION_NAVIGATION_ABORTED, TransitionBeforePreparationEvent, TransitionBeforeSwapEvent, isTransitionBeforePreparationEvent, isTransitionBeforeSwapEvent, } from "./events.js";
|
|
3
|
-
export type { Direction, Fallback, NavigationTypeString, Options } from "./types.js";
|
|
3
|
+
export type { Direction, Fallback, NavigationTypeString, Options, SyncHistoryEntryOptions, } from "./types.js";
|
|
4
4
|
export { swapFunctions, swap } from "./swap-functions.js";
|
|
5
|
-
export { navigate, supportsViewTransitions, transitionEnabledOnThisPage, init } from "./router.js";
|
|
5
|
+
export { navigate, supportsViewTransitions, transitionEnabledOnThisPage, init, syncHistoryEntry, } from "./router.js";
|
|
6
6
|
export type { InitOptions } from "./router.js";
|
|
7
7
|
export { prefetch, init as prefetchInit } from "./prefetch.js";
|
|
8
8
|
export type { PrefetchStrategy, PrefetchInitOptions, PrefetchOptions } from "./prefetch.js";
|
|
@@ -9,7 +9,8 @@ export { swapFunctions, swap } from "./swap-functions.js";
|
|
|
9
9
|
// - W3C1: `supportsViewTransitions`, `transitionEnabledOnThisPage`.
|
|
10
10
|
// - W3C2: `navigate()` (public navigation entry).
|
|
11
11
|
// - W3C3: `init()` (idempotent bootstrap: registers click + form intercept listeners).
|
|
12
|
-
|
|
12
|
+
// - W2 (#1377): `syncHistoryEntry()` (history bookkeeping without navigation).
|
|
13
|
+
export { navigate, supportsViewTransitions, transitionEnabledOnThisPage, init, syncHistoryEntry, } from "./router.js";
|
|
13
14
|
// Prefetch public surface (#276).
|
|
14
15
|
// `init` from prefetch.ts is re-exported as `prefetchInit` to avoid colliding
|
|
15
16
|
// with the router's `init` above.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/client-router/index.ts"],"names":[],"mappings":"AAAA,mDAAmD;AACnD,4EAA4E;AAC5E,4EAA4E;AAE5E,mBAAmB;AACnB,OAAO,EAAE,YAAY,EAA0B,MAAM,qBAAqB,CAAC;AAE3E,OAAO,EACL,6BAA6B,EAC7B,4BAA4B,EAC5B,sBAAsB,EACtB,qBAAqB,EACrB,oBAAoB,EACpB,6BAA6B,EAC7B,gCAAgC,EAChC,yBAAyB,EACzB,kCAAkC,EAClC,2BAA2B,GAC5B,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/client-router/index.ts"],"names":[],"mappings":"AAAA,mDAAmD;AACnD,4EAA4E;AAC5E,4EAA4E;AAE5E,mBAAmB;AACnB,OAAO,EAAE,YAAY,EAA0B,MAAM,qBAAqB,CAAC;AAE3E,OAAO,EACL,6BAA6B,EAC7B,4BAA4B,EAC5B,sBAAsB,EACtB,qBAAqB,EACrB,oBAAoB,EACpB,6BAA6B,EAC7B,gCAAgC,EAChC,yBAAyB,EACzB,kCAAkC,EAClC,2BAA2B,GAC5B,MAAM,aAAa,CAAC;AAUrB,OAAO,EAAE,aAAa,EAAE,IAAI,EAAE,MAAM,qBAAqB,CAAC;AAE1D,yBAAyB;AACzB,sEAAsE;AACtE,oDAAoD;AACpD,yFAAyF;AACzF,iFAAiF;AACjF,OAAO,EACL,QAAQ,EACR,uBAAuB,EACvB,2BAA2B,EAC3B,IAAI,EACJ,gBAAgB,GACjB,MAAM,aAAa,CAAC;AAGrB,kCAAkC;AAClC,8EAA8E;AAC9E,kCAAkC;AAClC,OAAO,EAAE,QAAQ,EAAE,IAAI,IAAI,YAAY,EAAE,MAAM,eAAe,CAAC","sourcesContent":["// Public API surface for the client-router module.\n// This file is the barrel for @takazudo/zfb-runtime's client-router export.\n// W3D adds the <ClientRouter /> component and ClientRouterProps re-exports.\n\n// Component (W3D).\nexport { ClientRouter, type ClientRouterProps } from \"../client-router.js\";\n\nexport {\n TRANSITION_BEFORE_PREPARATION,\n TRANSITION_AFTER_PREPARATION,\n TRANSITION_BEFORE_SWAP,\n TRANSITION_AFTER_SWAP,\n TRANSITION_PAGE_LOAD,\n TRANSITION_NAVIGATION_ABORTED,\n TransitionBeforePreparationEvent,\n TransitionBeforeSwapEvent,\n isTransitionBeforePreparationEvent,\n isTransitionBeforeSwapEvent,\n} from \"./events.js\";\n\nexport type {\n Direction,\n Fallback,\n NavigationTypeString,\n Options,\n SyncHistoryEntryOptions,\n} from \"./types.js\";\n\nexport { swapFunctions, swap } from \"./swap-functions.js\";\n\n// Router public surface.\n// - W3C1: `supportsViewTransitions`, `transitionEnabledOnThisPage`.\n// - W3C2: `navigate()` (public navigation entry).\n// - W3C3: `init()` (idempotent bootstrap: registers click + form intercept listeners).\n// - W2 (#1377): `syncHistoryEntry()` (history bookkeeping without navigation).\nexport {\n navigate,\n supportsViewTransitions,\n transitionEnabledOnThisPage,\n init,\n syncHistoryEntry,\n} from \"./router.js\";\nexport type { InitOptions } from \"./router.js\";\n\n// Prefetch public surface (#276).\n// `init` from prefetch.ts is re-exported as `prefetchInit` to avoid colliding\n// with the router's `init` above.\nexport { prefetch, init as prefetchInit } from \"./prefetch.js\";\nexport type { PrefetchStrategy, PrefetchInitOptions, PrefetchOptions } from \"./prefetch.js\";\n"]}
|
|
@@ -145,14 +145,6 @@ export function prefetch(url, opts = {}) {
|
|
|
145
145
|
return;
|
|
146
146
|
executePrefetch(href, opts).catch(() => { });
|
|
147
147
|
}
|
|
148
|
-
/**
|
|
149
|
-
* Returns a pair of [enterHandler, leaveHandler] that queue and cancel an idle
|
|
150
|
-
* prefetch callback, keyed on the supplied per-trigger cancel-handle map.
|
|
151
|
-
*
|
|
152
|
-
* Both the pointer (hover) and focus triggers use identical queue/cancel logic;
|
|
153
|
-
* the only difference between them is which map tracks their pending handles.
|
|
154
|
-
* Parameterising by the map eliminates that duplication.
|
|
155
|
-
*/
|
|
156
148
|
function makeEnterLeaveHandlers(cancelHandles) {
|
|
157
149
|
function enterHandler(e) {
|
|
158
150
|
const link = e.target.closest("a[href]");
|
|
@@ -160,6 +152,27 @@ function makeEnterLeaveHandlers(cancelHandles) {
|
|
|
160
152
|
return;
|
|
161
153
|
if (!shouldPrefetchLink(link, "hover"))
|
|
162
154
|
return;
|
|
155
|
+
// A handle is already queued for this link — the pointer/focus moved
|
|
156
|
+
// between nested descendants of the SAME link, which re-fires enter on
|
|
157
|
+
// each one. Keep the original debounce window instead of overwriting it
|
|
158
|
+
// with a fresh requestIdleCallback/timeout that a busy pointer could keep
|
|
159
|
+
// resetting indefinitely. #1398.
|
|
160
|
+
if (cancelHandles.has(link))
|
|
161
|
+
return;
|
|
162
|
+
// The pending debounce already FIRED for this link — its idle callback ran
|
|
163
|
+
// prefetch(link.href), so the handle is gone from cancelHandles but the
|
|
164
|
+
// href is now prefetched (or in-flight). A later enter on the SAME link
|
|
165
|
+
// (e.g. an intra-link child->child move whose first callback fired in the
|
|
166
|
+
// gap between the two pointer moves — which the synchronous-dispatch L2
|
|
167
|
+
// suite cannot reproduce but a real browser does) must NOT queue a second,
|
|
168
|
+
// redundant idle callback whose prefetch() would only no-op. This is the
|
|
169
|
+
// fire+requeue companion to the cancel+requeue guard above. A failed
|
|
170
|
+
// prefetch leaves the href in NEITHER set (executePrefetch deletes it from
|
|
171
|
+
// inFlight without adding it to prefetched), so retry-after-error still
|
|
172
|
+
// works. #1398.
|
|
173
|
+
const resolvedHref = resolveHref(link.href);
|
|
174
|
+
if (resolvedHref && (prefetched.has(resolvedHref) || inFlight.has(resolvedHref)))
|
|
175
|
+
return;
|
|
163
176
|
// Use requestIdleCallback if available, else a small timeout.
|
|
164
177
|
const fire = () => {
|
|
165
178
|
cancelHandles.delete(link);
|
|
@@ -178,6 +191,12 @@ function makeEnterLeaveHandlers(cancelHandles) {
|
|
|
178
191
|
const link = e.target.closest("a[href]");
|
|
179
192
|
if (!link)
|
|
180
193
|
return;
|
|
194
|
+
// The pointer/focus is moving to relatedTarget. If that's still inside
|
|
195
|
+
// the same link (a nested descendant), this is NOT a real leave — skip
|
|
196
|
+
// the cancel so the pending prefetch survives intra-link moves. #1398.
|
|
197
|
+
const related = e.relatedTarget;
|
|
198
|
+
if (related instanceof Node && link.contains(related))
|
|
199
|
+
return;
|
|
181
200
|
const handle = cancelHandles.get(link);
|
|
182
201
|
if (handle === undefined)
|
|
183
202
|
return;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"prefetch.js","sourceRoot":"","sources":["../../src/client-router/prefetch.ts"],"names":[],"mappings":"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","sourcesContent":["/// <reference lib=\"dom\" />\n/// <reference lib=\"dom.iterable\" />\n// `@takazudo/zfb-runtime` — prefetch module.\n//\n// Ported from Astro's prefetch module (packages/astro/src/prefetch/index.ts).\n// Source: https://github.com/withastro/astro\n// Issue: Takazudo/zudo-front-builder#276\n//\n// Mechanical renames per W1B §13.5:\n// astro:* event names → zfb:*\n// data-astro-prefetch → data-zfb-prefetch\n// __PREFETCH_DISABLED__ → <meta name=\"zfb-prefetch-disabled\" content=\"true\">\n//\n// DISABLED-FLAG CONTRACT — exact meta tag name and content value are locked:\n// meta[name=\"zfb-prefetch-disabled\"][content=\"true\"]\n// Both this module and sibling sub-issue #272-config pin this verbatim.\n\nexport type PrefetchStrategy = \"hover\" | \"viewport\" | \"load\" | \"tap\";\n\nexport interface PrefetchInitOptions {\n prefetchAll?: boolean;\n defaultStrategy?: PrefetchStrategy;\n}\n\nexport interface PrefetchOptions {\n ignoreSlowConnection?: boolean;\n with?: \"link\" | \"fetch\";\n}\n\n// Module-level state — persists across SPA navigations within a session.\nlet initialized = false;\nlet prefetchAll = false;\nlet defaultStrategy: PrefetchStrategy = \"hover\";\n\n// Set of hrefs that have been successfully prefetched.\nconst prefetched = new Set<string>();\n// Map of in-flight prefetch promises for dedup (also used as the concurrent short-circuit guard).\nconst inFlight = new Map<string, Promise<void>>();\n\n// How long to wait for a <link rel=prefetch>'s load/error before treating\n// the prefetch as settled anyway (see executePrefetch).\nconst LINK_SETTLE_TIMEOUT_MS = 10_000;\n\n// The viewport observer — retained across post-swap re-scans so new elements\n// can be observed without recreating the observer.\nlet viewportObserver: IntersectionObserver | null = null;\n\n// Hover cancel handle — set when a pointerenter idle-callback fires, cleared on\n// pointerleave before the callback executes.\nconst hoverCancelHandles = new Map<Element, ReturnType<typeof setTimeout> | number>();\n\n// Focus cancel handle — set when a focusin idle-callback fires, cleared on\n// focusout before the callback executes.\nconst focusCancelHandles = new Map<Element, ReturnType<typeof setTimeout> | number>();\n\n/**\n * Reset all module-level state. Exported for test isolation only — not part of\n * the public API and not re-exported from any barrel.\n */\nexport function __resetForTests(): void {\n initialized = false;\n prefetchAll = false;\n defaultStrategy = \"hover\";\n prefetched.clear();\n inFlight.clear();\n if (viewportObserver) {\n viewportObserver.disconnect();\n viewportObserver = null;\n }\n hoverCancelHandles.clear();\n focusCancelHandles.clear();\n}\n\n// ---------------------------------------------------------------------------\n// Feature detection\n// ---------------------------------------------------------------------------\n\nfunction supportsLinkPrefetch(): boolean {\n const link = document.createElement(\"link\");\n return link.relList?.supports?.(\"prefetch\") ?? false;\n}\n\nfunction isSlowConnection(): boolean {\n const nav = navigator as Navigator & {\n connection?: { saveData?: boolean; effectiveType?: string };\n };\n return (\n nav.connection?.saveData === true ||\n nav.connection?.effectiveType === \"2g\" ||\n nav.connection?.effectiveType === \"slow-2g\"\n );\n}\n\n// ---------------------------------------------------------------------------\n// Core prefetch logic\n// ---------------------------------------------------------------------------\n\n/**\n * Resolve an href to an absolute URL string keyed against the current origin.\n * Returns null for cross-origin hrefs (these are skipped entirely).\n */\nfunction resolveHref(href: string): string | null {\n try {\n const url = new URL(href, location.href);\n if (url.origin !== location.origin) return null;\n return url.href;\n } catch {\n return null;\n }\n}\n\n/**\n * Execute a single prefetch for the given absolute href.\n * Uses <link rel=\"prefetch\"> (preferred) or fetch() fallback.\n */\nfunction executePrefetch(href: string, opts: PrefetchOptions): Promise<void> {\n const existing = inFlight.get(href);\n if (existing) return existing;\n\n const p: Promise<void> = (async () => {\n try {\n const method = opts.with ?? (supportsLinkPrefetch() ? \"link\" : \"fetch\");\n if (method === \"link\") {\n const link = document.createElement(\"link\");\n link.rel = \"prefetch\";\n link.href = href;\n // Wait for load/error so we only mark success on actual load and allow\n // retry after error — inserting without waiting would mark success\n // immediately even on failure. (Bug: Takazudo/zudo-front-builder#896)\n // Some browsers (e.g. Safari) never fire load/error on rel=prefetch\n // links; without the settle timeout the href would sit in inFlight\n // forever and block every future retry. Timing out counts as\n // success — the resource was requested, which is all prefetch needs.\n await new Promise<void>((resolve, reject) => {\n const timer = setTimeout(() => resolve(), LINK_SETTLE_TIMEOUT_MS);\n link.addEventListener(\n \"load\",\n () => {\n clearTimeout(timer);\n resolve();\n },\n { once: true },\n );\n link.addEventListener(\n \"error\",\n () => {\n clearTimeout(timer);\n // Remove the failed element so retries don't accumulate\n // dead <link> nodes in <head>.\n link.remove();\n reject(new Error(`prefetch link error: ${href}`));\n },\n { once: true },\n );\n document.head.appendChild(link);\n });\n } else {\n await fetch(href, { priority: \"low\" } as RequestInit & { priority?: string });\n }\n prefetched.add(href);\n } finally {\n inFlight.delete(href);\n }\n })();\n\n inFlight.set(href, p);\n return p;\n}\n\n/**\n * Public prefetch function. Idempotent per href.\n */\nexport function prefetch(url: string, opts: PrefetchOptions = {}): void {\n const href = resolveHref(url);\n if (!href) return; // cross-origin — skip\n\n if (opts.ignoreSlowConnection !== true && isSlowConnection()) return;\n\n if (prefetched.has(href) || inFlight.has(href)) return;\n\n executePrefetch(href, opts).catch(() => {});\n}\n\n// ---------------------------------------------------------------------------\n// Shared enter/leave handler factory\n// ---------------------------------------------------------------------------\n\ntype CancelHandleMap = Map<Element, ReturnType<typeof setTimeout> | number>;\n\n/**\n * Returns a pair of [enterHandler, leaveHandler] that queue and cancel an idle\n * prefetch callback, keyed on the supplied per-trigger cancel-handle map.\n *\n * Both the pointer (hover) and focus triggers use identical queue/cancel logic;\n * the only difference between them is which map tracks their pending handles.\n * Parameterising by the map eliminates that duplication.\n */\nfunction makeEnterLeaveHandlers(\n cancelHandles: CancelHandleMap,\n): [enterHandler: (e: Event) => void, leaveHandler: (e: Event) => void] {\n function enterHandler(e: Event): void {\n const link = (e.target as Element).closest(\"a[href]\") as HTMLAnchorElement | null;\n if (!link) return;\n if (!shouldPrefetchLink(link, \"hover\")) return;\n\n // Use requestIdleCallback if available, else a small timeout.\n const fire = () => {\n cancelHandles.delete(link);\n prefetch(link.href);\n };\n\n if (typeof requestIdleCallback !== \"undefined\") {\n const handle = requestIdleCallback(fire);\n cancelHandles.set(link, handle);\n } else {\n const handle = setTimeout(fire, 100);\n cancelHandles.set(link, handle);\n }\n }\n\n function leaveHandler(e: Event): void {\n const link = (e.target as Element).closest(\"a[href]\") as HTMLAnchorElement | null;\n if (!link) return;\n\n const handle = cancelHandles.get(link);\n if (handle === undefined) return;\n\n if (typeof cancelIdleCallback !== \"undefined\") {\n cancelIdleCallback(handle as number);\n } else {\n clearTimeout(handle as ReturnType<typeof setTimeout>);\n }\n cancelHandles.delete(link);\n }\n\n return [enterHandler, leaveHandler];\n}\n\n// ---------------------------------------------------------------------------\n// Trigger: hover (pointerenter / pointerleave)\n// ---------------------------------------------------------------------------\n\nconst [onPointerEnter, onPointerLeave] = makeEnterLeaveHandlers(hoverCancelHandles);\n\n// ---------------------------------------------------------------------------\n// Trigger: focus (focusin / focusout with cancel on focusout)\n// ---------------------------------------------------------------------------\n\nconst [onFocusIn, onFocusOut] = makeEnterLeaveHandlers(focusCancelHandles);\n\n// ---------------------------------------------------------------------------\n// Trigger: tap (touchstart / mousedown)\n// ---------------------------------------------------------------------------\n\nfunction onTap(e: Event): void {\n const link = (e.target as Element).closest(\"a[href]\") as HTMLAnchorElement | null;\n if (!link) return;\n if (!shouldPrefetchLink(link, \"tap\")) return;\n prefetch(link.href);\n}\n\n// ---------------------------------------------------------------------------\n// Trigger: viewport (IntersectionObserver)\n// ---------------------------------------------------------------------------\n\nfunction initViewportObserver(): IntersectionObserver {\n if (viewportObserver) return viewportObserver;\n\n viewportObserver = new IntersectionObserver(\n (entries) => {\n for (const entry of entries) {\n if (entry.isIntersecting) {\n const link = entry.target as HTMLAnchorElement;\n prefetch(link.href);\n // Once observed and in-flight, no need to keep observing.\n viewportObserver?.unobserve(link);\n }\n }\n },\n { threshold: 0 },\n );\n\n return viewportObserver;\n}\n\nfunction observeViewportLinks(): void {\n const observer = initViewportObserver();\n const selector =\n prefetchAll && defaultStrategy === \"viewport\"\n ? \"a[href]:not([data-zfb-prefetch='false'])\"\n : \"a[data-zfb-prefetch='viewport']\";\n\n document.querySelectorAll<HTMLAnchorElement>(selector).forEach((link) => {\n const href = resolveHref(link.href);\n if (href && !prefetched.has(href)) {\n observer.observe(link);\n }\n });\n}\n\n// ---------------------------------------------------------------------------\n// Trigger: load (requestIdleCallback after DOMContentLoaded)\n// ---------------------------------------------------------------------------\n\nfunction prefetchLoadLinks(): void {\n const selector =\n prefetchAll && defaultStrategy === \"load\"\n ? \"a[href]:not([data-zfb-prefetch='false'])\"\n : \"a[data-zfb-prefetch='load']\";\n\n document.querySelectorAll<HTMLAnchorElement>(selector).forEach((link) => {\n const href = resolveHref(link.href);\n if (!href || prefetched.has(href)) return;\n\n if (typeof requestIdleCallback !== \"undefined\") {\n requestIdleCallback(() => prefetch(link.href));\n } else {\n setTimeout(() => prefetch(link.href), 0);\n }\n });\n}\n\n// ---------------------------------------------------------------------------\n// Per-link strategy resolution\n// ---------------------------------------------------------------------------\n\n/**\n * Determine whether a given link should be prefetched for the supplied trigger strategy.\n * Returns false if:\n * - data-zfb-prefetch=\"false\" (always disabled)\n * - The effective strategy for this link doesn't match the active trigger\n */\nfunction shouldPrefetchLink(link: HTMLAnchorElement, triggerStrategy: PrefetchStrategy): boolean {\n const attr = link.dataset[\"zfbPrefetch\"];\n\n // Explicitly disabled.\n if (attr === \"false\") return false;\n\n // Per-link explicit strategy.\n if (attr && attr !== \"false\") {\n return attr === triggerStrategy;\n }\n\n // No explicit attribute — rely on prefetchAll + defaultStrategy.\n if (!prefetchAll) return false;\n\n return defaultStrategy === triggerStrategy;\n}\n\n// ---------------------------------------------------------------------------\n// Post-swap re-scan\n// ---------------------------------------------------------------------------\n\nfunction onAfterSwap(): void {\n // Disconnect the existing observer before re-scanning so detached anchors from\n // the old body are unobserved and don't accumulate across SPA swaps.\n // (Bug: Takazudo/zudo-front-builder#896 — observer leak across SPA swaps)\n if (viewportObserver) {\n viewportObserver.disconnect();\n viewportObserver = null;\n }\n observeViewportLinks();\n prefetchLoadLinks();\n}\n\n// ---------------------------------------------------------------------------\n// init()\n// ---------------------------------------------------------------------------\n\n/**\n * Initialize the prefetch module.\n *\n * Idempotent — multiple calls are safe. The module-level `initialized` flag\n * ensures listeners are registered exactly once.\n *\n * DISABLED-FLAG CONTRACT: if `<meta name=\"zfb-prefetch-disabled\" content=\"true\">`\n * is present in the document at init() time, this function is a no-op.\n */\nexport function init(options?: PrefetchInitOptions): void {\n // Disabled-flag check — exact selector is the contract with #272-config.\n if (document.querySelector('meta[name=\"zfb-prefetch-disabled\"][content=\"true\"]')) {\n return;\n }\n\n if (initialized) return;\n initialized = true;\n\n prefetchAll = options?.prefetchAll ?? false;\n defaultStrategy = options?.defaultStrategy ?? \"hover\";\n\n // Hover + tap — document delegation, picks up SPA-inserted links automatically.\n document.addEventListener(\"pointerenter\", onPointerEnter, { capture: true });\n document.addEventListener(\"pointerleave\", onPointerLeave, { capture: true });\n // Focus — separate cancel map so hover and focus don't share handles.\n document.addEventListener(\"focusin\", onFocusIn, { capture: true });\n document.addEventListener(\"focusout\", onFocusOut, { capture: true });\n document.addEventListener(\"touchstart\", onTap, { capture: true, passive: true });\n document.addEventListener(\"mousedown\", onTap, { capture: true });\n\n // Viewport — observe current links immediately.\n observeViewportLinks();\n\n // Load — queue current links via idle callback.\n const queueLoad = () => prefetchLoadLinks();\n if (document.readyState === \"loading\") {\n document.addEventListener(\"DOMContentLoaded\", queueLoad, { once: true });\n } else {\n queueLoad();\n }\n\n // Post-swap re-scan — re-walk viewport + load links after each SPA navigation.\n document.addEventListener(\"zfb:after-swap\", onAfterSwap);\n}\n"]}
|
|
1
|
+
{"version":3,"file":"prefetch.js","sourceRoot":"","sources":["../../src/client-router/prefetch.ts"],"names":[],"mappings":"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","sourcesContent":["/// <reference lib=\"dom\" />\n/// <reference lib=\"dom.iterable\" />\n// `@takazudo/zfb-runtime` — prefetch module.\n//\n// Ported from Astro's prefetch module (packages/astro/src/prefetch/index.ts).\n// Source: https://github.com/withastro/astro\n// Issue: Takazudo/zudo-front-builder#276\n//\n// Mechanical renames per W1B §13.5:\n// astro:* event names → zfb:*\n// data-astro-prefetch → data-zfb-prefetch\n// __PREFETCH_DISABLED__ → <meta name=\"zfb-prefetch-disabled\" content=\"true\">\n//\n// DISABLED-FLAG CONTRACT — exact meta tag name and content value are locked:\n// meta[name=\"zfb-prefetch-disabled\"][content=\"true\"]\n// Both this module and sibling sub-issue #272-config pin this verbatim.\n\nexport type PrefetchStrategy = \"hover\" | \"viewport\" | \"load\" | \"tap\";\n\nexport interface PrefetchInitOptions {\n prefetchAll?: boolean;\n defaultStrategy?: PrefetchStrategy;\n}\n\nexport interface PrefetchOptions {\n ignoreSlowConnection?: boolean;\n with?: \"link\" | \"fetch\";\n}\n\n// Module-level state — persists across SPA navigations within a session.\nlet initialized = false;\nlet prefetchAll = false;\nlet defaultStrategy: PrefetchStrategy = \"hover\";\n\n// Set of hrefs that have been successfully prefetched.\nconst prefetched = new Set<string>();\n// Map of in-flight prefetch promises for dedup (also used as the concurrent short-circuit guard).\nconst inFlight = new Map<string, Promise<void>>();\n\n// How long to wait for a <link rel=prefetch>'s load/error before treating\n// the prefetch as settled anyway (see executePrefetch).\nconst LINK_SETTLE_TIMEOUT_MS = 10_000;\n\n// The viewport observer — retained across post-swap re-scans so new elements\n// can be observed without recreating the observer.\nlet viewportObserver: IntersectionObserver | null = null;\n\n// Hover cancel handle — set when a pointerenter idle-callback fires, cleared on\n// pointerleave before the callback executes.\nconst hoverCancelHandles = new Map<Element, ReturnType<typeof setTimeout> | number>();\n\n// Focus cancel handle — set when a focusin idle-callback fires, cleared on\n// focusout before the callback executes.\nconst focusCancelHandles = new Map<Element, ReturnType<typeof setTimeout> | number>();\n\n/**\n * Reset all module-level state. Exported for test isolation only — not part of\n * the public API and not re-exported from any barrel.\n */\nexport function __resetForTests(): void {\n initialized = false;\n prefetchAll = false;\n defaultStrategy = \"hover\";\n prefetched.clear();\n inFlight.clear();\n if (viewportObserver) {\n viewportObserver.disconnect();\n viewportObserver = null;\n }\n hoverCancelHandles.clear();\n focusCancelHandles.clear();\n}\n\n// ---------------------------------------------------------------------------\n// Feature detection\n// ---------------------------------------------------------------------------\n\nfunction supportsLinkPrefetch(): boolean {\n const link = document.createElement(\"link\");\n return link.relList?.supports?.(\"prefetch\") ?? false;\n}\n\nfunction isSlowConnection(): boolean {\n const nav = navigator as Navigator & {\n connection?: { saveData?: boolean; effectiveType?: string };\n };\n return (\n nav.connection?.saveData === true ||\n nav.connection?.effectiveType === \"2g\" ||\n nav.connection?.effectiveType === \"slow-2g\"\n );\n}\n\n// ---------------------------------------------------------------------------\n// Core prefetch logic\n// ---------------------------------------------------------------------------\n\n/**\n * Resolve an href to an absolute URL string keyed against the current origin.\n * Returns null for cross-origin hrefs (these are skipped entirely).\n */\nfunction resolveHref(href: string): string | null {\n try {\n const url = new URL(href, location.href);\n if (url.origin !== location.origin) return null;\n return url.href;\n } catch {\n return null;\n }\n}\n\n/**\n * Execute a single prefetch for the given absolute href.\n * Uses <link rel=\"prefetch\"> (preferred) or fetch() fallback.\n */\nfunction executePrefetch(href: string, opts: PrefetchOptions): Promise<void> {\n const existing = inFlight.get(href);\n if (existing) return existing;\n\n const p: Promise<void> = (async () => {\n try {\n const method = opts.with ?? (supportsLinkPrefetch() ? \"link\" : \"fetch\");\n if (method === \"link\") {\n const link = document.createElement(\"link\");\n link.rel = \"prefetch\";\n link.href = href;\n // Wait for load/error so we only mark success on actual load and allow\n // retry after error — inserting without waiting would mark success\n // immediately even on failure. (Bug: Takazudo/zudo-front-builder#896)\n // Some browsers (e.g. Safari) never fire load/error on rel=prefetch\n // links; without the settle timeout the href would sit in inFlight\n // forever and block every future retry. Timing out counts as\n // success — the resource was requested, which is all prefetch needs.\n await new Promise<void>((resolve, reject) => {\n const timer = setTimeout(() => resolve(), LINK_SETTLE_TIMEOUT_MS);\n link.addEventListener(\n \"load\",\n () => {\n clearTimeout(timer);\n resolve();\n },\n { once: true },\n );\n link.addEventListener(\n \"error\",\n () => {\n clearTimeout(timer);\n // Remove the failed element so retries don't accumulate\n // dead <link> nodes in <head>.\n link.remove();\n reject(new Error(`prefetch link error: ${href}`));\n },\n { once: true },\n );\n document.head.appendChild(link);\n });\n } else {\n await fetch(href, { priority: \"low\" } as RequestInit & { priority?: string });\n }\n prefetched.add(href);\n } finally {\n inFlight.delete(href);\n }\n })();\n\n inFlight.set(href, p);\n return p;\n}\n\n/**\n * Public prefetch function. Idempotent per href.\n */\nexport function prefetch(url: string, opts: PrefetchOptions = {}): void {\n const href = resolveHref(url);\n if (!href) return; // cross-origin — skip\n\n if (opts.ignoreSlowConnection !== true && isSlowConnection()) return;\n\n if (prefetched.has(href) || inFlight.has(href)) return;\n\n executePrefetch(href, opts).catch(() => {});\n}\n\n// ---------------------------------------------------------------------------\n// Shared enter/leave handler factory\n// ---------------------------------------------------------------------------\n\ntype CancelHandleMap = Map<Element, ReturnType<typeof setTimeout> | number>;\n\n/**\n * Returns a pair of [enterHandler, leaveHandler] that queue and cancel an idle\n * prefetch callback, keyed on the supplied per-trigger cancel-handle map.\n *\n * Both the pointer (hover) and focus triggers use identical queue/cancel logic;\n * the only difference between them is which map tracks their pending handles.\n * Parameterising by the map eliminates that duplication.\n */\n// pointerenter/pointerleave (and focusin/focusout) fire on every element the\n// pointer/focus enters or leaves, including nested descendants — not just the\n// delegated document listener's eventual `closest(\"a[href]\")` target. For\n// `<a><span>text</span></a>`, moving the pointer between the link's own\n// children fires a leave on the child being left and an enter on the child\n// being entered, even though the pointer never left the LINK itself.\n// `relatedTarget` is where the pointer/focus is going (leave) or came from\n// (enter); reading it lets the two handlers below tell an intra-link move\n// apart from a real leave/first-enter. #1398.\ntype RelatedTargetEvent = Event & { relatedTarget?: EventTarget | null };\n\nfunction makeEnterLeaveHandlers(\n cancelHandles: CancelHandleMap,\n): [enterHandler: (e: Event) => void, leaveHandler: (e: Event) => void] {\n function enterHandler(e: Event): void {\n const link = (e.target as Element).closest(\"a[href]\") as HTMLAnchorElement | null;\n if (!link) return;\n if (!shouldPrefetchLink(link, \"hover\")) return;\n\n // A handle is already queued for this link — the pointer/focus moved\n // between nested descendants of the SAME link, which re-fires enter on\n // each one. Keep the original debounce window instead of overwriting it\n // with a fresh requestIdleCallback/timeout that a busy pointer could keep\n // resetting indefinitely. #1398.\n if (cancelHandles.has(link)) return;\n\n // The pending debounce already FIRED for this link — its idle callback ran\n // prefetch(link.href), so the handle is gone from cancelHandles but the\n // href is now prefetched (or in-flight). A later enter on the SAME link\n // (e.g. an intra-link child->child move whose first callback fired in the\n // gap between the two pointer moves — which the synchronous-dispatch L2\n // suite cannot reproduce but a real browser does) must NOT queue a second,\n // redundant idle callback whose prefetch() would only no-op. This is the\n // fire+requeue companion to the cancel+requeue guard above. A failed\n // prefetch leaves the href in NEITHER set (executePrefetch deletes it from\n // inFlight without adding it to prefetched), so retry-after-error still\n // works. #1398.\n const resolvedHref = resolveHref(link.href);\n if (resolvedHref && (prefetched.has(resolvedHref) || inFlight.has(resolvedHref))) return;\n\n // Use requestIdleCallback if available, else a small timeout.\n const fire = () => {\n cancelHandles.delete(link);\n prefetch(link.href);\n };\n\n if (typeof requestIdleCallback !== \"undefined\") {\n const handle = requestIdleCallback(fire);\n cancelHandles.set(link, handle);\n } else {\n const handle = setTimeout(fire, 100);\n cancelHandles.set(link, handle);\n }\n }\n\n function leaveHandler(e: Event): void {\n const link = (e.target as Element).closest(\"a[href]\") as HTMLAnchorElement | null;\n if (!link) return;\n\n // The pointer/focus is moving to relatedTarget. If that's still inside\n // the same link (a nested descendant), this is NOT a real leave — skip\n // the cancel so the pending prefetch survives intra-link moves. #1398.\n const related = (e as RelatedTargetEvent).relatedTarget;\n if (related instanceof Node && link.contains(related)) return;\n\n const handle = cancelHandles.get(link);\n if (handle === undefined) return;\n\n if (typeof cancelIdleCallback !== \"undefined\") {\n cancelIdleCallback(handle as number);\n } else {\n clearTimeout(handle as ReturnType<typeof setTimeout>);\n }\n cancelHandles.delete(link);\n }\n\n return [enterHandler, leaveHandler];\n}\n\n// ---------------------------------------------------------------------------\n// Trigger: hover (pointerenter / pointerleave)\n// ---------------------------------------------------------------------------\n\nconst [onPointerEnter, onPointerLeave] = makeEnterLeaveHandlers(hoverCancelHandles);\n\n// ---------------------------------------------------------------------------\n// Trigger: focus (focusin / focusout with cancel on focusout)\n// ---------------------------------------------------------------------------\n\nconst [onFocusIn, onFocusOut] = makeEnterLeaveHandlers(focusCancelHandles);\n\n// ---------------------------------------------------------------------------\n// Trigger: tap (touchstart / mousedown)\n// ---------------------------------------------------------------------------\n\nfunction onTap(e: Event): void {\n const link = (e.target as Element).closest(\"a[href]\") as HTMLAnchorElement | null;\n if (!link) return;\n if (!shouldPrefetchLink(link, \"tap\")) return;\n prefetch(link.href);\n}\n\n// ---------------------------------------------------------------------------\n// Trigger: viewport (IntersectionObserver)\n// ---------------------------------------------------------------------------\n\nfunction initViewportObserver(): IntersectionObserver {\n if (viewportObserver) return viewportObserver;\n\n viewportObserver = new IntersectionObserver(\n (entries) => {\n for (const entry of entries) {\n if (entry.isIntersecting) {\n const link = entry.target as HTMLAnchorElement;\n prefetch(link.href);\n // Once observed and in-flight, no need to keep observing.\n viewportObserver?.unobserve(link);\n }\n }\n },\n { threshold: 0 },\n );\n\n return viewportObserver;\n}\n\nfunction observeViewportLinks(): void {\n const observer = initViewportObserver();\n const selector =\n prefetchAll && defaultStrategy === \"viewport\"\n ? \"a[href]:not([data-zfb-prefetch='false'])\"\n : \"a[data-zfb-prefetch='viewport']\";\n\n document.querySelectorAll<HTMLAnchorElement>(selector).forEach((link) => {\n const href = resolveHref(link.href);\n if (href && !prefetched.has(href)) {\n observer.observe(link);\n }\n });\n}\n\n// ---------------------------------------------------------------------------\n// Trigger: load (requestIdleCallback after DOMContentLoaded)\n// ---------------------------------------------------------------------------\n\nfunction prefetchLoadLinks(): void {\n const selector =\n prefetchAll && defaultStrategy === \"load\"\n ? \"a[href]:not([data-zfb-prefetch='false'])\"\n : \"a[data-zfb-prefetch='load']\";\n\n document.querySelectorAll<HTMLAnchorElement>(selector).forEach((link) => {\n const href = resolveHref(link.href);\n if (!href || prefetched.has(href)) return;\n\n if (typeof requestIdleCallback !== \"undefined\") {\n requestIdleCallback(() => prefetch(link.href));\n } else {\n setTimeout(() => prefetch(link.href), 0);\n }\n });\n}\n\n// ---------------------------------------------------------------------------\n// Per-link strategy resolution\n// ---------------------------------------------------------------------------\n\n/**\n * Determine whether a given link should be prefetched for the supplied trigger strategy.\n * Returns false if:\n * - data-zfb-prefetch=\"false\" (always disabled)\n * - The effective strategy for this link doesn't match the active trigger\n */\nfunction shouldPrefetchLink(link: HTMLAnchorElement, triggerStrategy: PrefetchStrategy): boolean {\n const attr = link.dataset[\"zfbPrefetch\"];\n\n // Explicitly disabled.\n if (attr === \"false\") return false;\n\n // Per-link explicit strategy.\n if (attr && attr !== \"false\") {\n return attr === triggerStrategy;\n }\n\n // No explicit attribute — rely on prefetchAll + defaultStrategy.\n if (!prefetchAll) return false;\n\n return defaultStrategy === triggerStrategy;\n}\n\n// ---------------------------------------------------------------------------\n// Post-swap re-scan\n// ---------------------------------------------------------------------------\n\nfunction onAfterSwap(): void {\n // Disconnect the existing observer before re-scanning so detached anchors from\n // the old body are unobserved and don't accumulate across SPA swaps.\n // (Bug: Takazudo/zudo-front-builder#896 — observer leak across SPA swaps)\n if (viewportObserver) {\n viewportObserver.disconnect();\n viewportObserver = null;\n }\n observeViewportLinks();\n prefetchLoadLinks();\n}\n\n// ---------------------------------------------------------------------------\n// init()\n// ---------------------------------------------------------------------------\n\n/**\n * Initialize the prefetch module.\n *\n * Idempotent — multiple calls are safe. The module-level `initialized` flag\n * ensures listeners are registered exactly once.\n *\n * DISABLED-FLAG CONTRACT: if `<meta name=\"zfb-prefetch-disabled\" content=\"true\">`\n * is present in the document at init() time, this function is a no-op.\n */\nexport function init(options?: PrefetchInitOptions): void {\n // Disabled-flag check — exact selector is the contract with #272-config.\n if (document.querySelector('meta[name=\"zfb-prefetch-disabled\"][content=\"true\"]')) {\n return;\n }\n\n if (initialized) return;\n initialized = true;\n\n prefetchAll = options?.prefetchAll ?? false;\n defaultStrategy = options?.defaultStrategy ?? \"hover\";\n\n // Hover + tap — document delegation, picks up SPA-inserted links automatically.\n document.addEventListener(\"pointerenter\", onPointerEnter, { capture: true });\n document.addEventListener(\"pointerleave\", onPointerLeave, { capture: true });\n // Focus — separate cancel map so hover and focus don't share handles.\n document.addEventListener(\"focusin\", onFocusIn, { capture: true });\n document.addEventListener(\"focusout\", onFocusOut, { capture: true });\n document.addEventListener(\"touchstart\", onTap, { capture: true, passive: true });\n document.addEventListener(\"mousedown\", onTap, { capture: true });\n\n // Viewport — observe current links immediately.\n observeViewportLinks();\n\n // Load — queue current links via idle callback.\n const queueLoad = () => prefetchLoadLinks();\n if (document.readyState === \"loading\") {\n document.addEventListener(\"DOMContentLoaded\", queueLoad, { once: true });\n } else {\n queueLoad();\n }\n\n // Post-swap re-scan — re-walk viewport + load links after each SPA navigation.\n document.addEventListener(\"zfb:after-swap\", onAfterSwap);\n}\n"]}
|
|
@@ -1,7 +1,34 @@
|
|
|
1
|
-
import type { Fallback, Options } from "./types.js";
|
|
1
|
+
import type { Fallback, Options, SyncHistoryEntryOptions } from "./types.js";
|
|
2
2
|
export declare const supportsViewTransitions: boolean;
|
|
3
3
|
export declare const transitionEnabledOnThisPage: () => boolean;
|
|
4
4
|
export declare function getFallback(): Fallback;
|
|
5
|
+
/**
|
|
6
|
+
* Write a router-managed history entry (push or replace) WITHOUT any
|
|
7
|
+
* navigation, DOM, or lifecycle side effect. This is the supported path for
|
|
8
|
+
* consumers deep-linking transient UI state (dialogs/modals, a photo
|
|
9
|
+
* viewer's `/photos/<slug>/` URL). Hand-rolled raw `history.pushState`
|
|
10
|
+
* desyncs `originalLocation` and the index bookkeeping that popstate
|
|
11
|
+
* direction detection ({@link derivePopDirection}) and the same-page
|
|
12
|
+
* traverse fast-path depend on; `navigate()` can't be used instead because
|
|
13
|
+
* it forces a fetch. See #1377 / #1374.
|
|
14
|
+
*
|
|
15
|
+
* Never scrolls the viewport itself — but the entry it writes IS stamped
|
|
16
|
+
* with the CURRENT scroll position (`scrollX`/`scrollY` at call time), not
|
|
17
|
+
* `(0, 0)` (issue #1398). This matters only for a later Forward-traversal
|
|
18
|
+
* back to this entry: the same-page traverse fast-path restores whatever
|
|
19
|
+
* scroll position was stamped on the entry, so a same-page push (e.g.
|
|
20
|
+
* opening a dialog) does not snap the underlying page to the top when the
|
|
21
|
+
* dialog is later reopened via Forward.
|
|
22
|
+
*
|
|
23
|
+
* @param url - The URL to write. Cross-origin URLs throw rather than
|
|
24
|
+
* silently falling back to a full-page load.
|
|
25
|
+
* @param options.replace - Use `history.replaceState` instead of
|
|
26
|
+
* `history.pushState` (no new Back-button entry). Default: push.
|
|
27
|
+
* @param options.state - Merged into the entry's `history.state`; the
|
|
28
|
+
* router's own bookkeeping keys (`index`, `scrollX`, `scrollY`) always win
|
|
29
|
+
* on a colliding key.
|
|
30
|
+
*/
|
|
31
|
+
export declare function syncHistoryEntry(url: string | URL, options?: SyncHistoryEntryOptions): void;
|
|
5
32
|
export declare function navigate(href: string, options?: Options): Promise<void>;
|
|
6
33
|
export interface InitOptions {
|
|
7
34
|
/** Reserved for forward-compat with Astro's prefetch integration. Ignored in v1. */
|
|
@@ -66,6 +66,17 @@ const internalFetchHeaders = {};
|
|
|
66
66
|
const inBrowser = typeof document !== "undefined";
|
|
67
67
|
export const supportsViewTransitions = inBrowser && !!document.startViewTransition;
|
|
68
68
|
export const transitionEnabledOnThisPage = () => inBrowser && !!document.querySelector('[name="zfb-view-transitions-enabled"]');
|
|
69
|
+
// A same-page Back/Forward traversal (two history entries sharing pathname +
|
|
70
|
+
// search) is served from the live DOM by the fast-path in transition() — no
|
|
71
|
+
// re-fetch, no re-swap, no lifecycle events. A per-request SSR page
|
|
72
|
+
// (`prerender = false`) whose server output can differ between two visits to
|
|
73
|
+
// the SAME url opts back INTO the fetch by emitting
|
|
74
|
+
// <meta name="zfb-traverse-refetch" content="true"> (via <ClientRouter
|
|
75
|
+
// traverseRefetch />); skipping the fetch on such a page would pin the stale
|
|
76
|
+
// first-render content. Read from the CURRENT document — for a same-page
|
|
77
|
+
// traverse the current document IS the target page. Mirrors the
|
|
78
|
+
// zfb-prefetch-disabled meta reader. (#1376)
|
|
79
|
+
const traverseRefetchOptedOut = () => inBrowser && !!document.querySelector('meta[name="zfb-traverse-refetch"][content="true"]');
|
|
69
80
|
const samePage = (thisLocation, otherLocation) => thisLocation.pathname === otherLocation.pathname && thisLocation.search === otherLocation.search;
|
|
70
81
|
// The previous navigation that might still be in processing
|
|
71
82
|
let mostRecentNavigation;
|
|
@@ -120,6 +131,13 @@ const announce = () => {
|
|
|
120
131
|
}, 60);
|
|
121
132
|
};
|
|
122
133
|
const PERSIST_ATTR = "data-zfb-transition-persist";
|
|
134
|
+
const attrValueSelector = (attr, value) => value === "" ? `[${attr}=""]` : `[${attr}=${CSS.escape(value)}]`;
|
|
135
|
+
const querySelectorWithAttrValue = (root, selectorPrefix, attr, value) => {
|
|
136
|
+
const escapedMatch = root.querySelector(`${selectorPrefix}${attrValueSelector(attr, value)}`);
|
|
137
|
+
if (escapedMatch)
|
|
138
|
+
return escapedMatch;
|
|
139
|
+
return (Array.from(root.querySelectorAll(`${selectorPrefix}[${attr}]`)).find((el) => el.getAttribute(attr) === value) ?? null);
|
|
140
|
+
};
|
|
123
141
|
const DIRECTION_ATTR = "data-zfb-transition";
|
|
124
142
|
const OLD_NEW_ATTR = "data-zfb-transition-fallback";
|
|
125
143
|
let parser;
|
|
@@ -232,13 +250,22 @@ function runScripts() {
|
|
|
232
250
|
}
|
|
233
251
|
// Add a new entry to the browser history. This also sets the new page in the browser address bar.
|
|
234
252
|
// Sets the scroll position according to the hash fragment of the new location.
|
|
235
|
-
const moveToLocation = (to, from, options, pageTitleForBrowserHistory, historyState
|
|
253
|
+
const moveToLocation = (to, from, options, pageTitleForBrowserHistory, historyState,
|
|
254
|
+
// True when transition() already committed a history entry for THIS
|
|
255
|
+
// navigation before the swap ran (the WebKit-workaround early commit
|
|
256
|
+
// below). `to` here may differ from that committed entry's URL if a
|
|
257
|
+
// `zfb:before-swap` listener mutated `event.to` (writable per Astro
|
|
258
|
+
// parity) after the early commit already ran — in that case we must
|
|
259
|
+
// REPLACE the already-committed entry instead of pushing a second one, or
|
|
260
|
+
// a single navigation would leave two history entries (a phantom Back
|
|
261
|
+
// stop). #1398.
|
|
262
|
+
historyCommittedEarly = false) => {
|
|
236
263
|
const intraPage = samePage(from, to);
|
|
237
264
|
const targetPageTitle = document.title;
|
|
238
265
|
document.title = pageTitleForBrowserHistory;
|
|
239
266
|
let scrolledToTop = false;
|
|
240
267
|
if (to.href !== location.href && !historyState) {
|
|
241
|
-
if (options.history === "replace") {
|
|
268
|
+
if (options.history === "replace" || historyCommittedEarly) {
|
|
242
269
|
// Astro reads current.index/scrollX/scrollY directly; `history.state` can be
|
|
243
270
|
// null (page entered without a transition state), which would throw a
|
|
244
271
|
// TypeError. Fall back to a synthesized state from the tracked index/scroll.
|
|
@@ -268,7 +295,12 @@ const moveToLocation = (to, from, options, pageTitleForBrowserHistory, historySt
|
|
|
268
295
|
scrolledToTop = true;
|
|
269
296
|
}
|
|
270
297
|
if (historyState) {
|
|
271
|
-
|
|
298
|
+
// Raw-History consumers may push partial/empty state (`{}`), so the tracked
|
|
299
|
+
// scroll fields can be absent or non-finite. Never scrollTo(undefined, …) —
|
|
300
|
+
// leave scroll where it is rather than jumping to (0,0). #1376.
|
|
301
|
+
if (Number.isFinite(historyState.scrollX) && Number.isFinite(historyState.scrollY)) {
|
|
302
|
+
scrollTo(historyState.scrollX, historyState.scrollY);
|
|
303
|
+
}
|
|
272
304
|
}
|
|
273
305
|
else {
|
|
274
306
|
if (to.hash) {
|
|
@@ -293,15 +325,111 @@ const moveToLocation = (to, from, options, pageTitleForBrowserHistory, historySt
|
|
|
293
325
|
history.scrollRestoration = "manual";
|
|
294
326
|
}
|
|
295
327
|
};
|
|
328
|
+
let syncHistoryEntryOnServerWarned = false;
|
|
329
|
+
/**
|
|
330
|
+
* Write a router-managed history entry (push or replace) WITHOUT any
|
|
331
|
+
* navigation, DOM, or lifecycle side effect. This is the supported path for
|
|
332
|
+
* consumers deep-linking transient UI state (dialogs/modals, a photo
|
|
333
|
+
* viewer's `/photos/<slug>/` URL). Hand-rolled raw `history.pushState`
|
|
334
|
+
* desyncs `originalLocation` and the index bookkeeping that popstate
|
|
335
|
+
* direction detection ({@link derivePopDirection}) and the same-page
|
|
336
|
+
* traverse fast-path depend on; `navigate()` can't be used instead because
|
|
337
|
+
* it forces a fetch. See #1377 / #1374.
|
|
338
|
+
*
|
|
339
|
+
* Never scrolls the viewport itself — but the entry it writes IS stamped
|
|
340
|
+
* with the CURRENT scroll position (`scrollX`/`scrollY` at call time), not
|
|
341
|
+
* `(0, 0)` (issue #1398). This matters only for a later Forward-traversal
|
|
342
|
+
* back to this entry: the same-page traverse fast-path restores whatever
|
|
343
|
+
* scroll position was stamped on the entry, so a same-page push (e.g.
|
|
344
|
+
* opening a dialog) does not snap the underlying page to the top when the
|
|
345
|
+
* dialog is later reopened via Forward.
|
|
346
|
+
*
|
|
347
|
+
* @param url - The URL to write. Cross-origin URLs throw rather than
|
|
348
|
+
* silently falling back to a full-page load.
|
|
349
|
+
* @param options.replace - Use `history.replaceState` instead of
|
|
350
|
+
* `history.pushState` (no new Back-button entry). Default: push.
|
|
351
|
+
* @param options.state - Merged into the entry's `history.state`; the
|
|
352
|
+
* router's own bookkeeping keys (`index`, `scrollX`, `scrollY`) always win
|
|
353
|
+
* on a colliding key.
|
|
354
|
+
*/
|
|
355
|
+
export function syncHistoryEntry(url, options = {}) {
|
|
356
|
+
// SSR guard: no window/history to touch. Mirror navigate()'s server no-op +
|
|
357
|
+
// one-time console warning. Checked at call time via `typeof document` (not the
|
|
358
|
+
// module-eval `inBrowser` const) so the branch is reachable and behavior is
|
|
359
|
+
// identical in a real environment (document is always present in a browser).
|
|
360
|
+
if (typeof document === "undefined") {
|
|
361
|
+
if (!syncHistoryEntryOnServerWarned) {
|
|
362
|
+
// instantiate an error for the stacktrace to show to user.
|
|
363
|
+
const warning = new Error("syncHistoryEntry() was called during a server side render. This may be unintentional as it is expected to be called in response to a client-side user interaction. Please make sure that your usage is correct.");
|
|
364
|
+
warning.name = "Warning";
|
|
365
|
+
// biome-ignore lint/suspicious/noConsole: allowed
|
|
366
|
+
console.warn(warning);
|
|
367
|
+
syncHistoryEntryOnServerWarned = true;
|
|
368
|
+
}
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
const to = new URL(url, location.href);
|
|
372
|
+
// Cross-origin is not ours to manage. The History API would throw a native
|
|
373
|
+
// SecurityError for a cross-origin URL, but we guard explicitly so the failure
|
|
374
|
+
// is a clear, documented contract violation rather than an opaque platform
|
|
375
|
+
// error — and never a silent full-page load. #1377.
|
|
376
|
+
if (to.origin !== location.origin) {
|
|
377
|
+
throw new Error(`syncHistoryEntry(): refusing to write a cross-origin history entry for "${to.href}" (current origin: ${location.origin}).`);
|
|
378
|
+
}
|
|
379
|
+
if (options.replace) {
|
|
380
|
+
// Replace keeps the current entry's index. A consumer migrating from raw
|
|
381
|
+
// history.pushState may have left a missing/invalid index, so fall back to
|
|
382
|
+
// the tracked index rather than stamping NaN/undefined.
|
|
383
|
+
const current = history.state;
|
|
384
|
+
const index = current != null && Number.isFinite(current.index) ? current.index : currentHistoryIndex;
|
|
385
|
+
history.replaceState({
|
|
386
|
+
...options.state,
|
|
387
|
+
index,
|
|
388
|
+
scrollX: Number.isFinite(current?.scrollX) ? current.scrollX : scrollX,
|
|
389
|
+
scrollY: Number.isFinite(current?.scrollY) ? current.scrollY : scrollY,
|
|
390
|
+
}, "", to.href);
|
|
391
|
+
}
|
|
392
|
+
else {
|
|
393
|
+
// Persist the CURRENT (outgoing) entry's latest scroll before pushing, so a
|
|
394
|
+
// later Back restoration can't lose scroll that `scrollend` hasn't flushed
|
|
395
|
+
// yet — the same guard transition() applies before a non-traverse push.
|
|
396
|
+
updateScrollPosition({ scrollX, scrollY });
|
|
397
|
+
// Stamp the CURRENT scroll on the freshly-pushed entry too — NOT (0,0).
|
|
398
|
+
// This is a same-page push (dialog/photo-viewer pattern): the underlying
|
|
399
|
+
// page never actually scrolled anywhere, it just gained an overlay. A
|
|
400
|
+
// (0,0)-stamped entry would make the traverse fast-path (moveToLocation's
|
|
401
|
+
// historyState branch below) scrollTo(0,0) when this entry is later
|
|
402
|
+
// Forward-reopened, snapping the page to the top under the reopened
|
|
403
|
+
// dialog. #1398.
|
|
404
|
+
history.pushState({ ...options.state, index: ++currentHistoryIndex, scrollX, scrollY }, "", to.href);
|
|
405
|
+
}
|
|
406
|
+
// Always re-point originalLocation so the next transition()/onPopState uses the
|
|
407
|
+
// correct "from" URL. Skipping this is exactly what makes a raw pushState
|
|
408
|
+
// desync the router (a same-page Back would miss the traverse fast path).
|
|
409
|
+
originalLocation = to;
|
|
410
|
+
}
|
|
296
411
|
function preloadStyleLinks(newDocument) {
|
|
297
412
|
const links = [];
|
|
298
413
|
for (const el of newDocument.querySelectorAll("head link[rel=stylesheet]")) {
|
|
414
|
+
const persistId = el.getAttribute(PERSIST_ATTR);
|
|
415
|
+
const href = el.getAttribute("href");
|
|
416
|
+
const existingSelectors = [
|
|
417
|
+
persistId !== null ? attrValueSelector(PERSIST_ATTR, persistId) : null,
|
|
418
|
+
href !== null ? `link[rel=stylesheet]${attrValueSelector("href", href)}` : null,
|
|
419
|
+
].filter((selector) => selector !== null);
|
|
420
|
+
const existingLink = (existingSelectors.length ? document.querySelector(existingSelectors.join(", ")) : null) ??
|
|
421
|
+
(persistId !== null
|
|
422
|
+
? querySelectorWithAttrValue(document, "", PERSIST_ATTR, persistId)
|
|
423
|
+
: null) ??
|
|
424
|
+
(href !== null
|
|
425
|
+
? querySelectorWithAttrValue(document, "link[rel=stylesheet]", "href", href)
|
|
426
|
+
: null);
|
|
299
427
|
// Do not preload links that are already on the page.
|
|
300
|
-
if (!
|
|
428
|
+
if (href !== null && !existingLink) {
|
|
301
429
|
const c = document.createElement("link");
|
|
302
430
|
c.setAttribute("rel", "preload");
|
|
303
431
|
c.setAttribute("as", "style");
|
|
304
|
-
c.setAttribute("href",
|
|
432
|
+
c.setAttribute("href", href);
|
|
305
433
|
links.push(new Promise((resolve) => {
|
|
306
434
|
["load", "error"].forEach((evName) => c.addEventListener(evName, resolve));
|
|
307
435
|
document.head.append(c);
|
|
@@ -314,7 +442,9 @@ function preloadStyleLinks(newDocument) {
|
|
|
314
442
|
// if !popstate, update the history entry and scroll position according to toLocation
|
|
315
443
|
// if popState is given, this holds the scroll position for history navigation
|
|
316
444
|
// if fallback === "animate" then simulate view transitions
|
|
317
|
-
async function updateDOM(preparationEvent, options, currentTransition, historyState, fallback
|
|
445
|
+
async function updateDOM(preparationEvent, options, currentTransition, historyState, fallback,
|
|
446
|
+
// Forwarded to moveToLocation() — see its parameter doc. #1398.
|
|
447
|
+
historyCommittedEarly = false) {
|
|
318
448
|
async function animate(phase) {
|
|
319
449
|
function isInfinite(animation) {
|
|
320
450
|
const effect = animation.effect;
|
|
@@ -354,9 +484,15 @@ async function updateDOM(preparationEvent, options, currentTransition, historySt
|
|
|
354
484
|
// trees receive render(null, element) / root.unmount() and their useEffect
|
|
355
485
|
// cleanups fire. Must happen after cancelPendingIslands() and before doSwap()
|
|
356
486
|
// so document.body still points to the old body.
|
|
357
|
-
|
|
487
|
+
//
|
|
488
|
+
// Pass the incoming body so unmountIslands can SKIP islands that swapBodyElement
|
|
489
|
+
// will lift into the new body (a persisted marker matched on both sides). Those
|
|
490
|
+
// nodes are physically moved, not discarded, so their component instance and
|
|
491
|
+
// state must survive the swap — unmounting them here would empty the container
|
|
492
|
+
// before the lift and defeat data-zfb-transition-persist entirely (issue #1389).
|
|
493
|
+
unmountIslands(document.body, preparationEvent.newDocument.body);
|
|
358
494
|
const swapEvent = await doSwap(preparationEvent, currentTransition.viewTransition, animateFallbackOld);
|
|
359
|
-
moveToLocation(swapEvent.to, swapEvent.from, options, pageTitleForBrowserHistory, historyState);
|
|
495
|
+
moveToLocation(swapEvent.to, swapEvent.from, options, pageTitleForBrowserHistory, historyState, historyCommittedEarly);
|
|
360
496
|
triggerEvent("zfb:after-swap");
|
|
361
497
|
// Resolve the finished promise of the simulation's ViewTransition.
|
|
362
498
|
// For 'animate', wait for the new-page animation to complete first.
|
|
@@ -397,7 +533,12 @@ async function transition(direction, from, to, options, historyState, hasUAVisua
|
|
|
397
533
|
updateScrollPosition({ scrollX, scrollY });
|
|
398
534
|
}
|
|
399
535
|
if (samePage(from, to) && !options.formData) {
|
|
400
|
-
if (
|
|
536
|
+
if (
|
|
537
|
+
// Same-page Back/Forward: serve from the live DOM (no re-fetch/re-swap)
|
|
538
|
+
// unless this page opted back into the fetch (per-request SSR). #1374/#1376.
|
|
539
|
+
(navigationType === "traverse" && !traverseRefetchOptedOut()) ||
|
|
540
|
+
(direction !== "back" && to.hash) ||
|
|
541
|
+
(direction === "back" && from.hash)) {
|
|
401
542
|
moveToLocation(to, from, options, document.title, historyState);
|
|
402
543
|
if (currentNavigation === mostRecentNavigation)
|
|
403
544
|
mostRecentNavigation = undefined;
|
|
@@ -536,7 +677,13 @@ async function transition(direction, from, to, options, historyState, hasUAVisua
|
|
|
536
677
|
//
|
|
537
678
|
// Traverse (popstate) navigations carry historyState: the browser has already
|
|
538
679
|
// moved, so they must NOT create a new entry here.
|
|
680
|
+
//
|
|
681
|
+
// Tracks whether this block ran (push OR replace) so moveToLocation (called
|
|
682
|
+
// later, from updateDOM) knows a commit already happened for THIS navigation
|
|
683
|
+
// — see moveToLocation's `historyCommittedEarly` parameter doc. #1398.
|
|
684
|
+
let historyCommittedEarly = false;
|
|
539
685
|
if (!historyState && prepEvent.to.href !== location.href) {
|
|
686
|
+
historyCommittedEarly = true;
|
|
540
687
|
if (options.history === "replace") {
|
|
541
688
|
// Mirror of the moveToLocation replace-path guard: `history.state` can be
|
|
542
689
|
// null here too (page entered without a transition state), so synthesize a
|
|
@@ -560,7 +707,7 @@ async function transition(direction, from, to, options, historyState, hasUAVisua
|
|
|
560
707
|
if (supportsViewTransitions && !hasUAVisualTransition) {
|
|
561
708
|
// This automatically cancels any previous transition
|
|
562
709
|
// We also already took care that the earlier update callback got through
|
|
563
|
-
currentTransition.viewTransition = document.startViewTransition(async () => await updateDOM(prepEvent, options, currentTransition, historyState));
|
|
710
|
+
currentTransition.viewTransition = document.startViewTransition(async () => await updateDOM(prepEvent, options, currentTransition, historyState, undefined, historyCommittedEarly));
|
|
564
711
|
}
|
|
565
712
|
else {
|
|
566
713
|
// Simulation mode requires a bit more manual work.
|
|
@@ -570,7 +717,7 @@ async function transition(direction, from, to, options, historyState, hasUAVisua
|
|
|
570
717
|
const updateDone = (async () => {
|
|
571
718
|
// Immediately paused to set up the ViewTransition object for Fallback mode
|
|
572
719
|
await Promise.resolve(); // hop through the micro task queue
|
|
573
|
-
await updateDOM(prepEvent, options, currentTransition, historyState, hasUAVisualTransition ? "swap" : getFallback());
|
|
720
|
+
await updateDOM(prepEvent, options, currentTransition, historyState, hasUAVisualTransition ? "swap" : getFallback(), historyCommittedEarly);
|
|
574
721
|
return undefined;
|
|
575
722
|
})();
|
|
576
723
|
// When the updateDone promise is settled,
|