@rivetjs/router 0.0.0-stage → 0.1.0-alpha.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/LICENSE +21 -0
- package/README.md +10 -2
- package/adapters/assemble.d.ts +32 -0
- package/adapters/assemble.js +32 -0
- package/adapters/build.d.ts +24 -0
- package/adapters/build.js +43 -0
- package/adapters/bun.d.ts +18 -0
- package/adapters/bun.js +44 -0
- package/adapters/cloudflare.d.ts +5 -0
- package/adapters/cloudflare.js +8 -0
- package/adapters/cluster.d.ts +7 -0
- package/adapters/cluster.js +130 -0
- package/adapters/csr.d.ts +12 -0
- package/adapters/csr.js +11 -0
- package/adapters/deno.d.ts +13 -0
- package/adapters/deno.js +35 -0
- package/adapters/fetch.d.ts +55 -0
- package/adapters/fetch.js +72 -0
- package/adapters/files.d.ts +20 -0
- package/adapters/files.js +155 -0
- package/adapters/idle-memory.d.ts +23 -0
- package/adapters/idle-memory.js +91 -0
- package/adapters/netlify.d.ts +13 -0
- package/adapters/netlify.js +15 -0
- package/adapters/node.d.ts +180 -0
- package/adapters/node.js +986 -0
- package/adapters/pack.d.ts +39 -0
- package/adapters/pack.js +360 -0
- package/adapters/server-target.d.ts +17 -0
- package/adapters/server-target.js +8 -0
- package/adapters/static.d.ts +84 -0
- package/adapters/static.js +231 -0
- package/adapters/target-build.d.ts +95 -0
- package/adapters/target-build.js +118 -0
- package/adapters/vercel-edge.d.ts +7 -0
- package/adapters/vercel-edge.js +9 -0
- package/adapters/vercel.d.ts +35 -0
- package/adapters/vercel.js +45 -0
- package/build/codegen.d.ts +97 -0
- package/build/codegen.js +1037 -0
- package/build/dev-errors.d.ts +23 -0
- package/build/dev-errors.js +247 -0
- package/build/lazy-rows.d.ts +19 -0
- package/build/lazy-rows.js +59 -0
- package/build/migrate.d.ts +31 -0
- package/build/migrate.js +385 -0
- package/build/reach.d.ts +60 -0
- package/build/reach.js +302 -0
- package/build/route-css.d.ts +54 -0
- package/build/route-css.js +216 -0
- package/build/routes.d.ts +55 -0
- package/build/routes.js +219 -0
- package/build/sync.d.ts +20 -0
- package/build/sync.js +16 -0
- package/build/vite.d.ts +208 -0
- package/build/vite.js +861 -0
- package/cli/rivet.d.ts +2 -0
- package/cli/rivet.js +101 -0
- package/client/active.d.ts +6 -0
- package/client/active.js +22 -0
- package/client/announce.d.ts +2 -0
- package/client/announce.js +21 -0
- package/client/dev.d.ts +41 -0
- package/client/dev.js +91 -0
- package/client/index.d.ts +156 -0
- package/client/index.js +714 -0
- package/client/layouts.d.ts +110 -0
- package/client/layouts.js +310 -0
- package/client/leave.d.ts +40 -0
- package/client/leave.js +75 -0
- package/client/link.d.ts +48 -0
- package/client/link.jsx +65 -0
- package/client/live.d.ts +57 -0
- package/client/live.js +170 -0
- package/client/navigate.d.ts +71 -0
- package/client/navigate.js +120 -0
- package/client/navigation.d.ts +28 -0
- package/client/navigation.js +14 -0
- package/client/params.d.ts +16 -0
- package/client/params.js +28 -0
- package/client/route-context.d.ts +32 -0
- package/client/route-context.js +21 -0
- package/client/scroll.d.ts +16 -0
- package/client/scroll.js +80 -0
- package/client/search.d.ts +8 -0
- package/client/search.js +17 -0
- package/client/tailwind.css +8 -0
- package/client/transition-helpers.d.ts +28 -0
- package/client/transition-helpers.js +63 -0
- package/client/transitions.css +90 -0
- package/client/transitions.d.ts +21 -0
- package/client/transitions.js +156 -0
- package/index.d.ts +135 -0
- package/index.js +8 -0
- package/package.json +156 -4
- package/server/app.d.ts +175 -0
- package/server/app.js +335 -0
- package/server/body.d.ts +20 -0
- package/server/body.js +8 -0
- package/server/deferred.d.ts +12 -0
- package/server/deferred.js +60 -0
- package/server/early-hints.d.ts +34 -0
- package/server/early-hints.js +63 -0
- package/server/handler.d.ts +240 -0
- package/server/handler.js +661 -0
- package/server/index.d.ts +9 -0
- package/server/index.js +6 -0
- package/server/inline-styles.d.ts +21 -0
- package/server/inline-styles.js +28 -0
- package/server/module.d.ts +14 -0
- package/server/module.js +96 -0
- package/server/render.d.ts +96 -0
- package/server/render.js +492 -0
- package/server/route-cache.d.ts +245 -0
- package/server/route-cache.js +813 -0
- package/server/security.d.ts +18 -0
- package/server/security.js +47 -0
- package/server/shell-build.d.ts +3 -0
- package/server/shell-build.js +2 -0
- package/server/shell-cache.d.ts +8 -0
- package/server/shell-cache.js +836 -0
- package/server/strict-csp.d.ts +7 -0
- package/server/strict-csp.js +18 -0
- package/server-only.d.ts +1 -0
- package/server-only.js +3 -0
- package/shared/actions.d.ts +10 -0
- package/shared/actions.js +18 -0
- package/shared/base.d.ts +8 -0
- package/shared/base.js +20 -0
- package/shared/cookie.d.ts +7 -0
- package/shared/cookie.js +32 -0
- package/shared/csp-directives.d.ts +9 -0
- package/shared/csp-directives.js +85 -0
- package/shared/csp-hash.d.ts +39 -0
- package/shared/csp-hash.js +121 -0
- package/shared/csp.d.ts +26 -0
- package/shared/csp.js +13 -0
- package/shared/data.d.ts +175 -0
- package/shared/data.js +22 -0
- package/shared/errors.d.ts +31 -0
- package/shared/errors.js +51 -0
- package/shared/head.d.ts +57 -0
- package/shared/head.js +242 -0
- package/shared/nonce.d.ts +2 -0
- package/shared/nonce.js +23 -0
- package/shared/prefetch-fallback.min.d.ts +1 -0
- package/shared/prefetch-fallback.min.js +2 -0
- package/shared/prefetch.d.ts +54 -0
- package/shared/prefetch.js +74 -0
- package/shared/redirect.d.ts +6 -0
- package/shared/redirect.js +25 -0
- package/shared/server-exports.d.ts +78 -0
- package/shared/server-exports.js +77 -0
- package/shared/slash.d.ts +8 -0
- package/shared/slash.js +24 -0
package/client/index.js
ADDED
|
@@ -0,0 +1,714 @@
|
|
|
1
|
+
// Client side of file routes: route data over the data endpoint, link interception, intent
|
|
2
|
+
// prefetch, scroll restoration and focus. Every export is a no-op or server-safe outside a browser.
|
|
3
|
+
import { effect, onCleanup, useContext } from "@rivetjs/reactivity";
|
|
4
|
+
import { _decodeWire } from "@rivetjs/runtime/resume-format";
|
|
5
|
+
import { compileRoutes, _popGate } from "@rivetjs/runtime/router";
|
|
6
|
+
import { HttpError } from "../shared/errors.js";
|
|
7
|
+
import { underBase } from "../shared/base.js";
|
|
8
|
+
import { DATA_PARAM, KEEP_PAGE, KEEP_PARAM, withoutRivet } from "../shared/data.js";
|
|
9
|
+
import { Navigation, _searchSync } from "./navigation.js";
|
|
10
|
+
import { _guard } from "./leave.js";
|
|
11
|
+
import { _take as _requests } from "./navigate.js";
|
|
12
|
+
import { announce } from "./announce.js";
|
|
13
|
+
import { enter, focusOn, fragmentTarget, leave, restoreScroll, reveal } from "./scroll.js";
|
|
14
|
+
import { _building, _bump, _lastMatch, _depsLoaded, _invalidating, _keepIds, _plan, _refreshed, _stale, } from "./layouts.js";
|
|
15
|
+
export { _layouts, _level, _enter, _take, _landed, _sync, _keepIds } from "./layouts.js";
|
|
16
|
+
export { beforeLeave } from "./leave.js";
|
|
17
|
+
export { Navigation, navigation, IDLE } from "./navigation.js";
|
|
18
|
+
// The router's typed navigation component (also `@rivetjs/router/link`).
|
|
19
|
+
export { Link } from "./link.jsx";
|
|
20
|
+
export { currentOf, _markActive } from "./active.js";
|
|
21
|
+
export { navigate, invalidate, revalidate, setSearchParams, _go } from "./navigate.js";
|
|
22
|
+
export { useParams, useSearchParams, RouteParamsContext } from "./params.js";
|
|
23
|
+
export { SearchParams } from "./search.js";
|
|
24
|
+
export { _live, _livePage, _liveRouter, _armLive, _liveIntent, _liveLevels, _serveLive } from "./live.js";
|
|
25
|
+
// Server render input and action results (no `.tsx` imports: Node loads it in tests).
|
|
26
|
+
import { ServerData } from "./route-context.js";
|
|
27
|
+
export { ServerData, actionResult } from "./route-context.js";
|
|
28
|
+
const PREFETCH_TTL = 5_000, PREFETCH_MAX = 32;
|
|
29
|
+
const prefetched = new Map();
|
|
30
|
+
// Drop warmed requests (all, or all but `except`'s), aborting them: unread bodies are released.
|
|
31
|
+
function dropPrefetch(except, now) {
|
|
32
|
+
for (const [url, warm] of prefetched)
|
|
33
|
+
if (url !== except && (now === undefined || now - warm.at >= PREFETCH_TTL))
|
|
34
|
+
(prefetched.delete(url), warm.stop.abort());
|
|
35
|
+
}
|
|
36
|
+
/** URL of a page's data for client navigation; `keep`: ids of layouts whose data the client has. */
|
|
37
|
+
export function dataURL(url, source = "server", keep) {
|
|
38
|
+
if (source === "static")
|
|
39
|
+
return url.pathname.replace(/\/?$/, "/") + "_data.json";
|
|
40
|
+
// The page's query as written (not re-serialized), the router's params appended; never the page
|
|
41
|
+
// URL's own `_rivet_keep`: only what this client keeps.
|
|
42
|
+
const search = withoutRivet(url.search);
|
|
43
|
+
return url.pathname + (search ? search + "&" : "?") + DATA_PARAM + "=data" + (keep ? "&" + KEEP_PARAM + "=" + encodeURIComponent(keep) : "");
|
|
44
|
+
}
|
|
45
|
+
const key = (url) => url.pathname + url.search;
|
|
46
|
+
// Fresh data handed to `invalidate(fresh)` (an action's single-flight answer), for the next load of its URL.
|
|
47
|
+
let seeded = null;
|
|
48
|
+
function request(url, source, signal, keep) {
|
|
49
|
+
const cached = prefetched.get(key(url));
|
|
50
|
+
prefetched.delete(key(url));
|
|
51
|
+
// Warmed with another keep list (layout state changed since the intent): its answer would be wrong.
|
|
52
|
+
if (cached && Date.now() - cached.at < PREFETCH_TTL && cached.keep === (keep ?? ""))
|
|
53
|
+
return cached.response;
|
|
54
|
+
cached?.stop.abort();
|
|
55
|
+
return fetch(dataURL(url, source, keep), { signal, headers: { accept: "application/json" }, credentials: "same-origin" });
|
|
56
|
+
}
|
|
57
|
+
/** `promise`, or a rejection with the signal's reason once it aborts (whichever comes first). */
|
|
58
|
+
function settled(promise, signal) {
|
|
59
|
+
if (signal.aborted)
|
|
60
|
+
return Promise.reject(signal.reason);
|
|
61
|
+
return new Promise((resolve, reject) => {
|
|
62
|
+
const stop = () => reject(signal.reason);
|
|
63
|
+
signal.addEventListener("abort", stop, { once: true });
|
|
64
|
+
promise.then(resolve, reject).finally(() => signal.removeEventListener("abort", stop));
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
/** Decode a data-endpoint body (resume table encoding; built-in entries load their decoders first, `x`). */
|
|
68
|
+
export function decodeData(text) {
|
|
69
|
+
return _decodeWire(text);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* @internal Server render of a generated route level: the handler's loader results (`ServerData`) through the
|
|
73
|
+
* route's result chain synchronously (`ok` / `fail` as the `then` steps of the browser chain, then `land`) — no
|
|
74
|
+
* `Promise.all` / `then` hops per request, no page import (the lazy page view loads itself, rendering
|
|
75
|
+
* synchronously once its module is in). Browser (and no `ServerData`): undefined, the generated chain runs.
|
|
76
|
+
*/
|
|
77
|
+
export function _ssrLevel(work, ok, fail, land) {
|
|
78
|
+
if (typeof document !== "undefined")
|
|
79
|
+
return undefined;
|
|
80
|
+
const server = work.get(ServerData);
|
|
81
|
+
if (!server)
|
|
82
|
+
return undefined;
|
|
83
|
+
let result;
|
|
84
|
+
try {
|
|
85
|
+
result = server.error ? fail(new HttpError(server.error.status, server.error.message)) : ok(server);
|
|
86
|
+
}
|
|
87
|
+
catch (cause) {
|
|
88
|
+
result = fail(cause);
|
|
89
|
+
}
|
|
90
|
+
return land(result);
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Route `load` for generated routes. On the server the handler already ran guards and loaders and
|
|
94
|
+
* provides them through `ServerData`; in the browser it fetches the data endpoint, which runs the
|
|
95
|
+
* same guards. Redirects navigate (replace); failures throw `HttpError` for the route's error view.
|
|
96
|
+
*/
|
|
97
|
+
const used = new WeakSet();
|
|
98
|
+
/** Load a generated route's data: from `ServerData` on the server, from the data endpoint in the browser. */
|
|
99
|
+
export async function routeData(work, match, router, source = "server", keep) {
|
|
100
|
+
const server = work.get(ServerData);
|
|
101
|
+
// In the browser (dev mounts) the provided data answers the first load only; later ones fetch.
|
|
102
|
+
if (server && !used.has(server)) {
|
|
103
|
+
if (typeof document !== "undefined")
|
|
104
|
+
(used.add(server), _depsLoaded(match.url, server.data.length, server.deps));
|
|
105
|
+
if (server.error)
|
|
106
|
+
throw new HttpError(server.error.status, server.error.message);
|
|
107
|
+
return server;
|
|
108
|
+
}
|
|
109
|
+
if (source === "none")
|
|
110
|
+
return { data: [] };
|
|
111
|
+
let body;
|
|
112
|
+
// `invalidate(fresh)` / `revalidate(fresh)` (an action's single-flight answer): no request.
|
|
113
|
+
const seed = seeded;
|
|
114
|
+
seeded = null;
|
|
115
|
+
if (seed && seed.key === key(match.url))
|
|
116
|
+
body = seed.body;
|
|
117
|
+
else {
|
|
118
|
+
// A warmed request is not tied to this load's signal: race it, so a superseded load ends now.
|
|
119
|
+
const response = await settled(request(match.url, source, work.signal, keep), work.signal);
|
|
120
|
+
if (source === "static" && response.status === 404)
|
|
121
|
+
throw new HttpError(404, "Not found");
|
|
122
|
+
// Not the data endpoint's JSON (a proxy error page, an HTML fallback): the document load shows it.
|
|
123
|
+
// Malformed JSON stays an error (the route's error view; the dev overlay).
|
|
124
|
+
body = /json/.test(response.headers.get("content-type") ?? "json")
|
|
125
|
+
? (await decodeData(await settled(response.text(), work.signal)))
|
|
126
|
+
: { reload: true };
|
|
127
|
+
}
|
|
128
|
+
// The user navigated on meanwhile: no side effects (title, redirects, document loads) for this one.
|
|
129
|
+
work.signal.throwIfAborted();
|
|
130
|
+
// A guard or request handler answered with a non-redirect Response: load the document instead.
|
|
131
|
+
if (body.reload) {
|
|
132
|
+
location.assign(match.url.href);
|
|
133
|
+
return new Promise(() => { });
|
|
134
|
+
}
|
|
135
|
+
if (body.redirect !== undefined) {
|
|
136
|
+
const target = new URL(body.redirect, location.href);
|
|
137
|
+
// A target no page route matches (an endpoint, another app on the origin) loads its document.
|
|
138
|
+
if (target.origin === location.origin && underBase(target.pathname) && router.navigate && (!routable || routable(target))) {
|
|
139
|
+
started({ from: current?.from ?? new URL(committed), to: target, type: "replace", trigger: "redirect" });
|
|
140
|
+
go(router, target, true);
|
|
141
|
+
}
|
|
142
|
+
else
|
|
143
|
+
// Only HTTP(S) targets (never `javascript:`/`data:`): anything else loads the page's document,
|
|
144
|
+
// where the browser applies the server's real answer.
|
|
145
|
+
location.assign(/^https?:$/.test(target.protocol) ? target.href : match.url.href);
|
|
146
|
+
// The navigation replaces this region; never settle.
|
|
147
|
+
return new Promise(() => { });
|
|
148
|
+
}
|
|
149
|
+
if (body.error)
|
|
150
|
+
throw new HttpError(body.error.status, body.error.message);
|
|
151
|
+
_depsLoaded(match.url, body.data?.length ?? 0, body.deps, body.keep);
|
|
152
|
+
if (body.head?.title !== undefined)
|
|
153
|
+
document.title = body.head.title;
|
|
154
|
+
return {
|
|
155
|
+
data: body.data ?? [],
|
|
156
|
+
...(body.head ? { head: body.head } : {}),
|
|
157
|
+
...(body.keep ? { kept: body.keep } : {}),
|
|
158
|
+
...(body.byURL ? { byURL: body.byURL } : {}),
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Same-origin, same-document-eligible anchor for a click/hover event, else null. With the app's page
|
|
163
|
+
* `modules`, a URL no page route matches (an endpoint, a file, another app on the origin) is not
|
|
164
|
+
* routable either: the browser loads its document (no data request, no chunk prefetch).
|
|
165
|
+
*/
|
|
166
|
+
export function routableAnchor(event, modules) {
|
|
167
|
+
if (typeof Element === "undefined" || !(event.target instanceof Element))
|
|
168
|
+
return null;
|
|
169
|
+
const anchor = event.target.closest("a");
|
|
170
|
+
if (!anchor || !anchor.href || anchor.hasAttribute("download") || anchor.hasAttribute("data-rivet-reload"))
|
|
171
|
+
return null;
|
|
172
|
+
if ((anchor.target && anchor.target !== "_self") || /(^|\s)external(\s|$)/.test(anchor.rel))
|
|
173
|
+
return null;
|
|
174
|
+
const url = new URL(anchor.href);
|
|
175
|
+
// Outside the app's URL base path: the browser loads it.
|
|
176
|
+
if (url.origin !== location.origin || !/^https?:$/.test(url.protocol) || !underBase(url.pathname))
|
|
177
|
+
return null;
|
|
178
|
+
if (!modules)
|
|
179
|
+
return anchor;
|
|
180
|
+
routable ??= (to) => isPage(to, modules);
|
|
181
|
+
return isPage(url, modules) ? anchor : null;
|
|
182
|
+
}
|
|
183
|
+
let traversal = false;
|
|
184
|
+
// `rivetRouter({ announce: false })`: no route announcer.
|
|
185
|
+
let quiet = false;
|
|
186
|
+
/** @internal Hooks installed by `@rivetjs/router/transitions` once it loads. */
|
|
187
|
+
export const _transitions = {};
|
|
188
|
+
const listeners = new Set();
|
|
189
|
+
let current = null;
|
|
190
|
+
let afters = [];
|
|
191
|
+
// The URL the routes region shows, and the history index of the current entry (`history.state.i`).
|
|
192
|
+
let committed = typeof location === "undefined" ? "" : location.href;
|
|
193
|
+
const entryIndex = () => history.state?.i ?? 0;
|
|
194
|
+
let index = typeof history === "undefined" ? 0 : entryIndex();
|
|
195
|
+
// Back/forward guards (`beforeLeave`): a cancelled popstate moves neither the runtime router nor this client.
|
|
196
|
+
const popBlocked = (event) => !!_guard.blocked?.(event, committed, index);
|
|
197
|
+
_popGate.blocked = popBlocked;
|
|
198
|
+
/**
|
|
199
|
+
* Run `fn(nav)` when a client navigation starts (link, back/forward, redirect); a function it
|
|
200
|
+
* returns runs once the new page is in the document. Returns an unsubscribe function.
|
|
201
|
+
*/
|
|
202
|
+
export function onNavigate(fn) {
|
|
203
|
+
listeners.add(fn);
|
|
204
|
+
return () => void listeners.delete(fn);
|
|
205
|
+
}
|
|
206
|
+
function started(nav) {
|
|
207
|
+
if (recovering === undefined)
|
|
208
|
+
((recovering = false), addEventListener("error", recover), addEventListener("unhandledrejection", recover));
|
|
209
|
+
// Warmed data for other URLs is no longer wanted: abort it (the target's own stays for its load).
|
|
210
|
+
dropPrefetch(key(nav.to));
|
|
211
|
+
keep = 0;
|
|
212
|
+
revalidating = false;
|
|
213
|
+
current = nav;
|
|
214
|
+
afters = [];
|
|
215
|
+
for (const fn of listeners) {
|
|
216
|
+
const after = fn(nav);
|
|
217
|
+
if (typeof after === "function")
|
|
218
|
+
afters.push(after);
|
|
219
|
+
}
|
|
220
|
+
_transitions.start?.(nav);
|
|
221
|
+
}
|
|
222
|
+
/** @internal Dev (`dev.ts`): the error overlay shows a failed navigation's error before its document loads. */
|
|
223
|
+
export const _recover = {};
|
|
224
|
+
// Listening (`false`, from the first navigation) / loading the document (`true`); `undefined`: not listening yet.
|
|
225
|
+
let recovering;
|
|
226
|
+
// The URL a navigation just committed (`swapRoute`; until the next frame): its page renders after the commit.
|
|
227
|
+
let rendering;
|
|
228
|
+
/**
|
|
229
|
+
* A client navigation whose render or commit throws (an uncaught error after the history push: before the routes
|
|
230
|
+
* region committed the URL, or inside its swap) loads the URL's document instead: URL and page never diverge. `location.replace`: the
|
|
231
|
+
* history entry is already there. A failing document load does not re-enter: once per URL within 10 s (session storage).
|
|
232
|
+
*/
|
|
233
|
+
function recover() {
|
|
234
|
+
const href = location.href, at = Date.now();
|
|
235
|
+
if (recovering || (href !== rendering && (!current || href === committed)))
|
|
236
|
+
return;
|
|
237
|
+
try {
|
|
238
|
+
// `<13-digit ms><href>` of the last recovery.
|
|
239
|
+
const last = sessionStorage["rivet:recover"];
|
|
240
|
+
if (last?.slice(13) === href && at - +last.slice(0, 13) < 1e4)
|
|
241
|
+
return;
|
|
242
|
+
sessionStorage["rivet:recover"] = at + href;
|
|
243
|
+
}
|
|
244
|
+
catch {
|
|
245
|
+
// No session storage: recover without the cross-document loop guard.
|
|
246
|
+
}
|
|
247
|
+
recovering = true;
|
|
248
|
+
(_recover.defer ?? ((load) => load()))(() => location.replace(href));
|
|
249
|
+
}
|
|
250
|
+
/** @internal The navigation in flight (read by the transitions module when it installs). */
|
|
251
|
+
export const _current = () => current;
|
|
252
|
+
/** @internal Run the `onNavigate` callbacks' after-swap functions. */
|
|
253
|
+
export function _afterSwap() {
|
|
254
|
+
const list = afters;
|
|
255
|
+
afters = [];
|
|
256
|
+
for (const fn of list)
|
|
257
|
+
fn();
|
|
258
|
+
committedSearch();
|
|
259
|
+
}
|
|
260
|
+
/** @internal Generated load: remember the loaded page module's `transition` export for the swap. */
|
|
261
|
+
export function _routeModule(module) {
|
|
262
|
+
if (current)
|
|
263
|
+
current.route = module?.transition;
|
|
264
|
+
}
|
|
265
|
+
/** Navigation state accessor: idle unless `transitions` is on. Resumed regions reading it update in place. */
|
|
266
|
+
export function useNavigation() {
|
|
267
|
+
return useContext(Navigation);
|
|
268
|
+
}
|
|
269
|
+
// Navigate and number the new history entry (merged into its state), so back/forward know their direction.
|
|
270
|
+
function go(router, url, replace) {
|
|
271
|
+
router.navigate(url, { replace });
|
|
272
|
+
if (!replace)
|
|
273
|
+
index++;
|
|
274
|
+
enter(index);
|
|
275
|
+
}
|
|
276
|
+
/** Delegated click handler: route same-origin links through the router (`modules`: only URLs a page route matches). */
|
|
277
|
+
export function interceptClick(event, router, modules) {
|
|
278
|
+
if (!router.navigate || event.defaultPrevented || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey)
|
|
279
|
+
return;
|
|
280
|
+
const anchor = routableAnchor(event, modules);
|
|
281
|
+
if (!anchor)
|
|
282
|
+
return;
|
|
283
|
+
const url = new URL(anchor.href);
|
|
284
|
+
// Same page, another fragment (or the same one again): no route work, scroll to the target.
|
|
285
|
+
if (url.pathname === location.pathname && url.search === location.search && (url.hash || location.hash)) {
|
|
286
|
+
event.preventDefault();
|
|
287
|
+
if (url.href !== location.href) {
|
|
288
|
+
if (!current)
|
|
289
|
+
leave(true, index);
|
|
290
|
+
go(router, url, anchor.hasAttribute("data-rivet-replace"));
|
|
291
|
+
}
|
|
292
|
+
// During a route navigation its commit scrolls to the fragment.
|
|
293
|
+
if (!current) {
|
|
294
|
+
committed = location.href;
|
|
295
|
+
reveal(url);
|
|
296
|
+
}
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
event.preventDefault();
|
|
300
|
+
if (url.href === location.href)
|
|
301
|
+
return;
|
|
302
|
+
const replace = anchor.hasAttribute("data-rivet-replace");
|
|
303
|
+
const nav = {
|
|
304
|
+
from: new URL(location.href),
|
|
305
|
+
to: url,
|
|
306
|
+
type: replace ? "replace" : "push",
|
|
307
|
+
trigger: "link",
|
|
308
|
+
anchor,
|
|
309
|
+
transition: anchor.getAttribute("data-transition"),
|
|
310
|
+
};
|
|
311
|
+
// Cancelable (unsaved-changes guards: `beforeLeave`, or the event): the page stays.
|
|
312
|
+
if (_guard.cancelled?.(url) || !document.dispatchEvent(new CustomEvent("rivet:navigate", { cancelable: true, detail: nav })))
|
|
313
|
+
return;
|
|
314
|
+
leave(true, index);
|
|
315
|
+
traversal = false;
|
|
316
|
+
started(nav);
|
|
317
|
+
go(router, url, replace);
|
|
318
|
+
}
|
|
319
|
+
/** Back/forward: the next settled route restores its saved position. */
|
|
320
|
+
export function notePopstate(event) {
|
|
321
|
+
if (event && popBlocked(event))
|
|
322
|
+
return;
|
|
323
|
+
traversal = true;
|
|
324
|
+
const i = entryIndex();
|
|
325
|
+
const from = new URL(committed), to = new URL(location.href);
|
|
326
|
+
const nav = {
|
|
327
|
+
from,
|
|
328
|
+
to,
|
|
329
|
+
type: i < index ? "back" : "forward",
|
|
330
|
+
trigger: "popstate",
|
|
331
|
+
visual: !!event?.hasUAVisualTransition,
|
|
332
|
+
};
|
|
333
|
+
index = i;
|
|
334
|
+
// A synchronous route may have committed already (the router's own listener ran first).
|
|
335
|
+
if (to.href === committed)
|
|
336
|
+
return;
|
|
337
|
+
// The page still shows the entry being left: save its position, then key the new one.
|
|
338
|
+
leave(false, i);
|
|
339
|
+
enter(i);
|
|
340
|
+
if (!current && to.pathname === from.pathname && to.search === from.search) {
|
|
341
|
+
// Hash-only traversal: the route stays; restore the entry's position, else reveal its target.
|
|
342
|
+
traversal = false;
|
|
343
|
+
committed = to.href;
|
|
344
|
+
if (!restoreScroll())
|
|
345
|
+
reveal(to);
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
started(nav);
|
|
349
|
+
}
|
|
350
|
+
/** Router `swap` of generated apps: the view transition when installed, then scroll and focus. */
|
|
351
|
+
export function swapRoute(apply, next) {
|
|
352
|
+
// A nested level's first commit, inside the outer level's render: part of that commit.
|
|
353
|
+
if (_building())
|
|
354
|
+
return apply();
|
|
355
|
+
const href = location.href;
|
|
356
|
+
if (revalidating && href === committed)
|
|
357
|
+
return revalidated(apply);
|
|
358
|
+
let nav = current;
|
|
359
|
+
current = null;
|
|
360
|
+
if (!nav && href !== committed) {
|
|
361
|
+
nav = { from: new URL(committed), to: new URL(href), type: "push", trigger: "api" };
|
|
362
|
+
// Programmatic navigation: the old page is still shown; save its position, key the new entry.
|
|
363
|
+
leave(false, index);
|
|
364
|
+
enter((index = history.state?.i ?? index + 1));
|
|
365
|
+
}
|
|
366
|
+
committed = href;
|
|
367
|
+
// Until the next frame's first task (the routes region renders the new page after the swap, up to a frame later), an
|
|
368
|
+
// uncaught error is this navigation's (`recover`).
|
|
369
|
+
if (nav)
|
|
370
|
+
((rendering = href), requestAnimationFrame(() => setTimeout(() => rendering === href && (rendering = undefined))));
|
|
371
|
+
if (nav && _transitions.swap)
|
|
372
|
+
return _transitions.swap(apply, next, nav);
|
|
373
|
+
apply();
|
|
374
|
+
settle(new URL(href));
|
|
375
|
+
_afterSwap();
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* @internal live.ts: a search-only navigation of a live page committed in place (no route render): the
|
|
379
|
+
* navigation in flight ends here, scroll and focus stay, `onNavigate` after-swap callbacks and waiters run.
|
|
380
|
+
*/
|
|
381
|
+
export function _inPlace() {
|
|
382
|
+
const href = location.href;
|
|
383
|
+
if (!current && href !== committed) {
|
|
384
|
+
leave(false, index);
|
|
385
|
+
enter((index = history.state?.i ?? index + 1));
|
|
386
|
+
}
|
|
387
|
+
current = null;
|
|
388
|
+
committed = href;
|
|
389
|
+
traversal = false;
|
|
390
|
+
keep = 0;
|
|
391
|
+
_afterSwap();
|
|
392
|
+
}
|
|
393
|
+
/** `swapRoute` without the route announcer (generated apps of `rivetRouter({ announce: false })`). */
|
|
394
|
+
export function swapRouteQuiet(apply, next) {
|
|
395
|
+
quiet = true;
|
|
396
|
+
swapRoute(apply, next);
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* Router `hold` view of generated apps: on a resumed page's first client navigation the server
|
|
400
|
+
* rendered route content stays visible (inert) until the new route commits, instead of a blank region.
|
|
401
|
+
*/
|
|
402
|
+
export function heldRoutes(level) {
|
|
403
|
+
if (typeof document === "undefined" || _building())
|
|
404
|
+
return null;
|
|
405
|
+
// Nested levels: the Router region inside the persisted layout of the level above.
|
|
406
|
+
const host = document.querySelector(level ? `[data-rivet-outlet="${level}"]` : "[data-rivet-routes]");
|
|
407
|
+
const first = host?.firstChild, last = host?.lastChild;
|
|
408
|
+
if (!first || !last || first === last || first.nodeType !== 8 || last.nodeType !== 8 || !/^r\d*\[$/.test(first.data))
|
|
409
|
+
return null;
|
|
410
|
+
const held = document.createDocumentFragment();
|
|
411
|
+
while (first.nextSibling !== last)
|
|
412
|
+
held.append(first.nextSibling);
|
|
413
|
+
return held;
|
|
414
|
+
}
|
|
415
|
+
/** Warm a route's data on intent (pointer over / focus); `keep` as for `dataURL`. */
|
|
416
|
+
export function prefetchData(url, source = "server", keep) {
|
|
417
|
+
if (source === "none")
|
|
418
|
+
return;
|
|
419
|
+
// A form submit may change what loaders return: warmed data from before it is stale.
|
|
420
|
+
if (!watching)
|
|
421
|
+
((watching = true), addEventListener("submit", _clearPrefetch, true));
|
|
422
|
+
const at = Date.now(), id = key(url);
|
|
423
|
+
const existing = prefetched.get(id);
|
|
424
|
+
if (existing && at - existing.at < PREFETCH_TTL && existing.keep === (keep ?? ""))
|
|
425
|
+
return;
|
|
426
|
+
// Expired entries go first; past the cap, the oldest.
|
|
427
|
+
dropPrefetch(undefined, at);
|
|
428
|
+
existing?.stop.abort();
|
|
429
|
+
prefetched.delete(id);
|
|
430
|
+
if (prefetched.size >= PREFETCH_MAX) {
|
|
431
|
+
const [oldest, warm] = prefetched.entries().next().value;
|
|
432
|
+
prefetched.delete(oldest);
|
|
433
|
+
warm.stop.abort();
|
|
434
|
+
}
|
|
435
|
+
const stop = new AbortController();
|
|
436
|
+
const warm = {
|
|
437
|
+
at,
|
|
438
|
+
keep: keep ?? "",
|
|
439
|
+
stop,
|
|
440
|
+
response: fetch(dataURL(url, source, keep), { signal: stop.signal, headers: { accept: "application/json" }, credentials: "same-origin" }),
|
|
441
|
+
};
|
|
442
|
+
warm.response.catch(() => prefetched.get(id) === warm && prefetched.delete(id));
|
|
443
|
+
prefetched.set(id, warm);
|
|
444
|
+
}
|
|
445
|
+
const matchers = new WeakMap();
|
|
446
|
+
/** @internal Generated `modules` row importer of an endpoint (its URLs load documents). */
|
|
447
|
+
export const _endpoint = () => { };
|
|
448
|
+
/** Import the page module of the route matching `url` (the import is shared with the render). */
|
|
449
|
+
export function loadModule(url, modules) {
|
|
450
|
+
let match = matchers.get(modules);
|
|
451
|
+
// Intent (every pointerover), the click and the navigation re-match the same path: remember the last answer.
|
|
452
|
+
if (!match)
|
|
453
|
+
matchers.set(modules, (match = _lastMatch(compileRoutes(modules.map(([path, load]) => ({ path, render: load }))))));
|
|
454
|
+
const hit = match(url);
|
|
455
|
+
return hit ? Promise.resolve(hit.route.render(undefined, hit)) : undefined;
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* A page route of `modules` matches `url` (not an endpoint row). The first call builds the matcher
|
|
459
|
+
* through `loadModule` (for a page URL that starts the import the click / intent starts next anyway).
|
|
460
|
+
*/
|
|
461
|
+
function isPage(url, modules) {
|
|
462
|
+
if (!matchers.has(modules))
|
|
463
|
+
loadModule(url, modules)?.catch(() => { });
|
|
464
|
+
const hit = matchers.get(modules)(url);
|
|
465
|
+
return !!hit && hit.route.render !== _endpoint;
|
|
466
|
+
}
|
|
467
|
+
// Set by the first link check (apps without client routing carry none of it): data redirects to a
|
|
468
|
+
// URL no page route matches load the document.
|
|
469
|
+
let routable;
|
|
470
|
+
/**
|
|
471
|
+
* Page loader of generated apps: import the page module and ready its lazy component (browser
|
|
472
|
+
* `lazyComponent(...).load`), so the route renders synchronously once loaded. Resolves the module.
|
|
473
|
+
*/
|
|
474
|
+
export function preloadPage(component, importer) {
|
|
475
|
+
// One load per page module (a failed one retries): the server renders this route on every request, and
|
|
476
|
+
// a dynamic `import()` per call costs the module loader's resolve + job lookup each time.
|
|
477
|
+
let loading = preloaded.get(importer);
|
|
478
|
+
if (!loading) {
|
|
479
|
+
const load = component.load;
|
|
480
|
+
const started = load ? Promise.all([importer(), load()]).then(([module]) => module) : importer();
|
|
481
|
+
preloaded.set(importer, (loading = started));
|
|
482
|
+
started.catch(() => preloaded.get(importer) === started && preloaded.delete(importer));
|
|
483
|
+
}
|
|
484
|
+
return loading;
|
|
485
|
+
}
|
|
486
|
+
const preloaded = new WeakMap();
|
|
487
|
+
/** Load the page chunk of the route matching `url` on intent; false when no page route matches (nothing to prefetch). */
|
|
488
|
+
export function prefetchModule(url, modules) {
|
|
489
|
+
const loading = loadModule(url, modules);
|
|
490
|
+
loading?.catch(() => { });
|
|
491
|
+
return !!loading;
|
|
492
|
+
}
|
|
493
|
+
// A CSR app's first render is the page load, not a navigation: leave scroll and focus alone.
|
|
494
|
+
let initial = typeof location === "undefined" ? null : location.href;
|
|
495
|
+
let settles = 0;
|
|
496
|
+
/** After a client-rendered route settles: restore or reset scroll, honour #hash, move focus, announce. */
|
|
497
|
+
export function settle(url) {
|
|
498
|
+
if (typeof document === "undefined")
|
|
499
|
+
return;
|
|
500
|
+
const first = initial;
|
|
501
|
+
initial = null;
|
|
502
|
+
if (first === url.href)
|
|
503
|
+
return;
|
|
504
|
+
const restore = traversal;
|
|
505
|
+
traversal = false;
|
|
506
|
+
const flags = keep;
|
|
507
|
+
keep = 0;
|
|
508
|
+
// Routes commit with their page chunk loaded: the heading is usually there already; nested lazy
|
|
509
|
+
// components can still bring it a few frames later. Without one, wait while the document keeps
|
|
510
|
+
// changing (a frame with no mutation settles), at most 30 frames.
|
|
511
|
+
let frames = 0, changed = true, watch;
|
|
512
|
+
const id = ++settles;
|
|
513
|
+
const run = () => {
|
|
514
|
+
// A newer navigation settled meanwhile: it owns scroll and focus now.
|
|
515
|
+
if (id !== settles)
|
|
516
|
+
return void watch?.disconnect();
|
|
517
|
+
const heading = document.querySelector("main h1, h1");
|
|
518
|
+
if (!heading && !url.hash && changed && ++frames < 30) {
|
|
519
|
+
if (!watch && typeof MutationObserver === "function")
|
|
520
|
+
(watch = new MutationObserver(() => (changed = true))).observe(document.body, { childList: true, subtree: true });
|
|
521
|
+
// No observer: frames only (the 30-frame bound).
|
|
522
|
+
changed = !watch;
|
|
523
|
+
return void requestAnimationFrame(run);
|
|
524
|
+
}
|
|
525
|
+
watch?.disconnect();
|
|
526
|
+
const target = fragmentTarget(url);
|
|
527
|
+
if (!(restore && restoreScroll()) && !(flags & KEEP_SCROLL)) {
|
|
528
|
+
if (target)
|
|
529
|
+
target.scrollIntoView();
|
|
530
|
+
else
|
|
531
|
+
scrollTo(0, 0);
|
|
532
|
+
}
|
|
533
|
+
if (!restore && !(flags & KEEP_FOCUS)) {
|
|
534
|
+
const focus = target ?? heading ?? document.querySelector("main");
|
|
535
|
+
if (focus)
|
|
536
|
+
focusOn(focus);
|
|
537
|
+
}
|
|
538
|
+
if (!quiet)
|
|
539
|
+
announce();
|
|
540
|
+
};
|
|
541
|
+
run();
|
|
542
|
+
}
|
|
543
|
+
/**
|
|
544
|
+
* @internal Generated `App` of apps using `@rivetjs/forms` (codegen `forms`): every router handler (link click /
|
|
545
|
+
* intent, popstate, `rivet:go`) first installs the router under `Symbol.for("rivet.router")` — `clear` (drop warmed
|
|
546
|
+
* route data) and `go` (`navigate.ts` `_go`) — so forms reach it without importing this module (their chunks share
|
|
547
|
+
* no router code with the App). The first install dispatches `rivet:router` on the document (forms waiting for the
|
|
548
|
+
* router to boot). Assigned each call: a re-instantiated router module (dev HMR) replaces the old entry points.
|
|
549
|
+
*/
|
|
550
|
+
export function _brand(go) {
|
|
551
|
+
const key = Symbol.for("rivet.router"), first = !globalThis[key];
|
|
552
|
+
globalThis[key] = { clear: _clearPrefetch, go };
|
|
553
|
+
if (first)
|
|
554
|
+
document.dispatchEvent(new Event("rivet:router"));
|
|
555
|
+
}
|
|
556
|
+
let watching = false;
|
|
557
|
+
/** Drop warmed route data (form submits and `invalidate()` do; call it after other mutations). */
|
|
558
|
+
export function _clearPrefetch() {
|
|
559
|
+
dropPrefetch();
|
|
560
|
+
// Layout data may be stale too: the next data request reloads every layout.
|
|
561
|
+
_stale();
|
|
562
|
+
}
|
|
563
|
+
// `navigate()` / `invalidate()` (navigate.ts) served by the app router: the navigation in flight
|
|
564
|
+
// keeps scroll / focus (`keep`), invalidations re-render in place, `waiting` resolves on commit.
|
|
565
|
+
/** Navigation flag: keep the scroll position. */
|
|
566
|
+
export const KEEP_SCROLL = 1,
|
|
567
|
+
/** Navigation flag: keep focus where it is. */
|
|
568
|
+
KEEP_FOCUS = 2;
|
|
569
|
+
let keep = 0;
|
|
570
|
+
let revalidating = false;
|
|
571
|
+
let waiting = [];
|
|
572
|
+
function committedSearch() {
|
|
573
|
+
_searchSync.shown = location.search;
|
|
574
|
+
_searchSync.search?.();
|
|
575
|
+
const list = waiting;
|
|
576
|
+
waiting = [];
|
|
577
|
+
for (const done of list)
|
|
578
|
+
done();
|
|
579
|
+
}
|
|
580
|
+
// The page level reloads for the same URL (loaders the keep list leaves out rerun), re-rendering in place.
|
|
581
|
+
function rerun(router, url, done) {
|
|
582
|
+
_bump();
|
|
583
|
+
revalidating = true;
|
|
584
|
+
waiting.push(done);
|
|
585
|
+
router.navigate(url, { revalidate: true });
|
|
586
|
+
}
|
|
587
|
+
// `invalidate(key)` matching layout loaders only: one data request keeping every other loader (the
|
|
588
|
+
// page's too); the layouts that reran update in place, the page is not re-rendered.
|
|
589
|
+
async function refresh(router, state, table, url, plan, done) {
|
|
590
|
+
_invalidating(url, plan.drop);
|
|
591
|
+
const keep = _keepIds(state, table, url);
|
|
592
|
+
// A redirect or a navigation meanwhile commits another page: that commit resolves `done`.
|
|
593
|
+
waiting.push(done);
|
|
594
|
+
let result;
|
|
595
|
+
try {
|
|
596
|
+
// Detached from any navigation (created here: a module-level controller would survive tree-shaking).
|
|
597
|
+
const work = { signal: new AbortController().signal, get: () => null };
|
|
598
|
+
result = await routeData(work, { url }, router, "server", keep ? keep + "," + KEEP_PAGE : KEEP_PAGE);
|
|
599
|
+
}
|
|
600
|
+
catch {
|
|
601
|
+
// A failed loader: the page level reruns as well and renders the route's error view.
|
|
602
|
+
waiting.splice(waiting.indexOf(done), 1);
|
|
603
|
+
_invalidating(url, plan.drop);
|
|
604
|
+
return rerun(router, url, done);
|
|
605
|
+
}
|
|
606
|
+
if (current || key(new URL(location.href)) !== key(url))
|
|
607
|
+
return;
|
|
608
|
+
_refreshed(state, table, url, result);
|
|
609
|
+
waiting.splice(waiting.indexOf(done), 1);
|
|
610
|
+
done();
|
|
611
|
+
}
|
|
612
|
+
// Same page, fresh data: no transition, scroll or focus move; the focused control (by id) keeps focus.
|
|
613
|
+
function revalidated(apply) {
|
|
614
|
+
revalidating = false;
|
|
615
|
+
const focused = document.activeElement?.id;
|
|
616
|
+
apply();
|
|
617
|
+
if (focused && document.activeElement?.id !== focused)
|
|
618
|
+
document.getElementById(focused)?.focus({ preventScroll: true });
|
|
619
|
+
committedSearch();
|
|
620
|
+
}
|
|
621
|
+
const armed = new WeakSet();
|
|
622
|
+
/**
|
|
623
|
+
* @internal Generated `App`: start its route effects (nested level sync, `<Link active>` re-marking) once per
|
|
624
|
+
* router, from the first navigation entry point (link click, popstate, `rivet:go`) — not at load, so a resumed
|
|
625
|
+
* page loads no router code before a navigation (Z1). Each runs as an effect owned by the caller's owner.
|
|
626
|
+
*/
|
|
627
|
+
export function _arm(router, ...effects) {
|
|
628
|
+
if (armed.has(router))
|
|
629
|
+
return;
|
|
630
|
+
armed.add(router);
|
|
631
|
+
for (const fn of effects)
|
|
632
|
+
effect(() => {
|
|
633
|
+
const cleanup = fn();
|
|
634
|
+
if (typeof cleanup === "function")
|
|
635
|
+
onCleanup(cleanup);
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
/**
|
|
639
|
+
* @internal Generated `App`: serve queued `navigate()` / `invalidate()` requests (`rivet:go` event).
|
|
640
|
+
* Server apps with layouts pass their layout state and table (`invalidate(key)` keeps loaders).
|
|
641
|
+
*/
|
|
642
|
+
export function _serve(router, state, table) {
|
|
643
|
+
for (const request of _requests())
|
|
644
|
+
perform(router, request, state, table);
|
|
645
|
+
}
|
|
646
|
+
// `_serveLive` (live.ts) serves the requests it does not refresh in place through these.
|
|
647
|
+
export { perform as _perform, dropPrefetch as _dropPrefetch };
|
|
648
|
+
function perform(router, request, state, table) {
|
|
649
|
+
const { url, done } = request;
|
|
650
|
+
if (!router.navigate)
|
|
651
|
+
return location.assign(url.href);
|
|
652
|
+
const target = request.invalidate;
|
|
653
|
+
if (typeof target === "function") {
|
|
654
|
+
// `invalidate(key)`: warmed data may be stale (all of it is dropped); only matching loaders rerun.
|
|
655
|
+
dropPrefetch();
|
|
656
|
+
const plan = _plan(state, table, url, target);
|
|
657
|
+
if (plan && !plan.any)
|
|
658
|
+
return done();
|
|
659
|
+
if (plan && !plan.page && state && table)
|
|
660
|
+
return void refresh(router, state, table, url, plan, done);
|
|
661
|
+
// Keys unknown: like `invalidate()`, every loader reruns.
|
|
662
|
+
if (plan)
|
|
663
|
+
_invalidating(url, plan.drop);
|
|
664
|
+
else
|
|
665
|
+
_stale();
|
|
666
|
+
return rerun(router, url, done);
|
|
667
|
+
}
|
|
668
|
+
if (target) {
|
|
669
|
+
_clearPrefetch();
|
|
670
|
+
const fresh = target === true ? undefined : target;
|
|
671
|
+
if (fresh?.data)
|
|
672
|
+
seeded = { key: key(url), body: { data: [...fresh.data], ...(fresh.head ? { head: fresh.head } : {}), ...(fresh.deps ? { deps: fresh.deps } : {}) } };
|
|
673
|
+
return rerun(router, url, done);
|
|
674
|
+
}
|
|
675
|
+
const replace = !!request.replace;
|
|
676
|
+
const push = () => {
|
|
677
|
+
router.navigate(url, { replace });
|
|
678
|
+
if (request.state)
|
|
679
|
+
history.replaceState({ ...request.state }, "");
|
|
680
|
+
if (!replace)
|
|
681
|
+
index++;
|
|
682
|
+
enter(index);
|
|
683
|
+
};
|
|
684
|
+
// Same page, another fragment: no route work (like a hash link).
|
|
685
|
+
if (url.pathname === location.pathname && url.search === location.search && (url.hash || location.hash)) {
|
|
686
|
+
if (url.href !== location.href) {
|
|
687
|
+
if (!current)
|
|
688
|
+
leave(true, index);
|
|
689
|
+
push();
|
|
690
|
+
}
|
|
691
|
+
if (!current) {
|
|
692
|
+
committed = location.href;
|
|
693
|
+
if (request.scroll !== false)
|
|
694
|
+
reveal(url);
|
|
695
|
+
}
|
|
696
|
+
return done();
|
|
697
|
+
}
|
|
698
|
+
if (url.href === location.href) {
|
|
699
|
+
if (request.state)
|
|
700
|
+
history.replaceState({ ...history.state, ...request.state }, "");
|
|
701
|
+
return done();
|
|
702
|
+
}
|
|
703
|
+
const transition = request.transition === false ? "none" : typeof request.transition === "string" ? request.transition : (request.transition?.join(" ") ?? null);
|
|
704
|
+
const nav = { from: new URL(location.href), to: url, type: replace ? "replace" : "push", trigger: "api", transition };
|
|
705
|
+
// Same guards as a link click (`beforeLeave`, the cancelable event).
|
|
706
|
+
if (_guard.cancelled?.(url) || !document.dispatchEvent(new CustomEvent("rivet:navigate", { cancelable: true, detail: nav })))
|
|
707
|
+
return done();
|
|
708
|
+
leave(true, index);
|
|
709
|
+
traversal = false;
|
|
710
|
+
started(nav);
|
|
711
|
+
keep = (request.scroll === false ? KEEP_SCROLL : 0) | (request.focus === false ? KEEP_FOCUS : 0);
|
|
712
|
+
waiting.push(done);
|
|
713
|
+
push();
|
|
714
|
+
}
|