@solidjs/router 0.17.0-next.6 → 1.0.0-next.10
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 +526 -716
- package/dist/claims.d.ts +21 -0
- package/dist/claims.js +115 -0
- package/dist/data/action.d.ts +15 -0
- package/dist/data/action.js +165 -25
- package/dist/data/events.d.ts +8 -0
- package/dist/data/events.js +23 -22
- package/dist/data/flash.d.ts +1 -5
- package/dist/data/flash.js +10 -18
- package/dist/data/flashCookie.d.ts +7 -0
- package/dist/data/flashCookie.js +20 -0
- package/dist/data/serverForms.d.ts +1 -0
- package/dist/data/serverForms.js +5 -0
- package/dist/index.d.ts +5 -3
- package/dist/index.js +1896 -1156
- package/dist/index.jsx +2 -2
- package/dist/lifecycle.d.ts +29 -4
- package/dist/lifecycle.js +40 -37
- package/dist/paths.d.ts +117 -0
- package/dist/paths.js +41 -0
- package/dist/routers/components.d.ts +10 -21
- package/dist/routers/components.jsx +29 -51
- package/dist/routers/factory.d.ts +66 -0
- package/dist/routers/factory.jsx +151 -0
- package/dist/routers/history.d.ts +26 -0
- package/dist/routers/history.js +180 -0
- package/dist/routers/index.d.ts +4 -11
- package/dist/routers/index.js +2 -6
- package/dist/routers/scrollRestoration.d.ts +33 -0
- package/dist/routers/scrollRestoration.js +135 -0
- package/dist/routing.d.ts +79 -52
- package/dist/routing.js +304 -128
- package/dist/server.d.ts +31 -17
- package/dist/server.js +68 -12
- package/dist/types.d.ts +74 -5
- package/dist/utils.d.ts +2 -0
- package/dist/utils.js +2 -0
- package/package.json +6 -6
- package/dist/components.d.ts +0 -31
- package/dist/components.jsx +0 -46
- package/dist/routers/HashRouter.d.ts +0 -9
- package/dist/routers/HashRouter.js +0 -41
- package/dist/routers/MemoryRouter.d.ts +0 -24
- package/dist/routers/MemoryRouter.js +0 -57
- package/dist/routers/Router.d.ts +0 -9
- package/dist/routers/Router.js +0 -45
- package/dist/routers/StaticRouter.d.ts +0 -6
- package/dist/routers/StaticRouter.js +0 -15
- package/dist/routers/createRouter.d.ts +0 -10
- package/dist/routers/createRouter.js +0 -40
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/*@refresh skip*/
|
|
2
|
+
import { createSignal, getOwner, onCleanup, sharedConfig, untrack } from "solid-js";
|
|
3
|
+
import { getRequestEvent, isServer } from "@solidjs/web";
|
|
4
|
+
import { setupLinkClaims } from "../claims.js";
|
|
5
|
+
import { setupNativeEvents } from "../data/events.js";
|
|
6
|
+
import { createPathsProxy } from "../paths.js";
|
|
7
|
+
import { createBranches, createRouterContext, getRouteMatches, registerFlightRouter, RouterContextObj, trackLazySubtrees, useOptionalContext } from "../routing.js";
|
|
8
|
+
import { mockBase } from "../utils.js";
|
|
9
|
+
import { Root, Routes } from "./components.jsx";
|
|
10
|
+
import { browserHistory } from "./history.js";
|
|
11
|
+
import { createScrollRestoration, withScrollRestoration } from "./scrollRestoration.js";
|
|
12
|
+
/**
|
|
13
|
+
* Identity helper that preserves literal types when the route tree is
|
|
14
|
+
* declared as a separate variable. `createRouter` infers literally from
|
|
15
|
+
* inline arrays, but an extracted `const routes = [...]` widens paths to
|
|
16
|
+
* `string` and silently degrades `paths` and the typed hooks unless it is
|
|
17
|
+
* declared `as const` — `defineRoutes` makes that impossible to forget.
|
|
18
|
+
*/
|
|
19
|
+
export function defineRoutes(routes) {
|
|
20
|
+
return routes;
|
|
21
|
+
}
|
|
22
|
+
/** Wraps a history adapter in the integration signal the router core consumes. Must run under a reactive owner. */
|
|
23
|
+
function createIntegration(history) {
|
|
24
|
+
let ignore = false;
|
|
25
|
+
const wrap = (value) => (typeof value === "string" ? { value } : value);
|
|
26
|
+
const [read, write] = createSignal(wrap(history.get()), {
|
|
27
|
+
equals: (a, b) => a.value === b.value && a.state === b.state,
|
|
28
|
+
ownedWrite: true
|
|
29
|
+
});
|
|
30
|
+
const signal = [
|
|
31
|
+
read,
|
|
32
|
+
(next) => {
|
|
33
|
+
!ignore && history.set(next);
|
|
34
|
+
if (sharedConfig.registry && !sharedConfig.done)
|
|
35
|
+
sharedConfig.done = true;
|
|
36
|
+
write(next);
|
|
37
|
+
}
|
|
38
|
+
];
|
|
39
|
+
history.init &&
|
|
40
|
+
onCleanup(history.init((value = history.get()) => {
|
|
41
|
+
ignore = true;
|
|
42
|
+
signal[1](wrap(value));
|
|
43
|
+
ignore = false;
|
|
44
|
+
}));
|
|
45
|
+
return { signal, utils: history.utils };
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Server default: a static view of the request URL — no signal machinery, a
|
|
49
|
+
* server render never navigates. The request event (when the harness scopes
|
|
50
|
+
* one) wins; the provider's `url` prop is the fallback for renders outside a
|
|
51
|
+
* request scope (SSG scripts, server-side tests, runtimes without
|
|
52
|
+
* `node:async_hooks`). History adapters are a client navigation concern and
|
|
53
|
+
* play no part in locating a server render.
|
|
54
|
+
*/
|
|
55
|
+
function staticIntegration(url, utils) {
|
|
56
|
+
const e = getRequestEvent();
|
|
57
|
+
const source = e ? e.request.url : url;
|
|
58
|
+
let value = "";
|
|
59
|
+
if (source) {
|
|
60
|
+
const u = new URL(source, mockBase);
|
|
61
|
+
value = u.pathname + u.search;
|
|
62
|
+
}
|
|
63
|
+
const obj = { value };
|
|
64
|
+
return { signal: [() => obj, next => Object.assign(obj, next)], utils };
|
|
65
|
+
}
|
|
66
|
+
export function createRouter(config) {
|
|
67
|
+
const basePath = config.base || "";
|
|
68
|
+
// Routes are immutable per instance, so compilation is shared by every
|
|
69
|
+
// mount, request, and `match()` call — recompiled only when a lazy subtree
|
|
70
|
+
// resolves (append-only: resolution, not mutation). Reading the version
|
|
71
|
+
// inside a computation subscribes it; plain calls just see current state.
|
|
72
|
+
let compiled;
|
|
73
|
+
let compiledVersion = -1;
|
|
74
|
+
const branches = () => {
|
|
75
|
+
const version = trackLazySubtrees();
|
|
76
|
+
if (!compiled || compiledVersion !== version) {
|
|
77
|
+
compiled = createBranches(config.routes, basePath);
|
|
78
|
+
compiledVersion = version;
|
|
79
|
+
}
|
|
80
|
+
return compiled;
|
|
81
|
+
};
|
|
82
|
+
const renderPath = (config.history && config.history.utils && config.history.utils.renderPath) || undefined;
|
|
83
|
+
function RouterComponent(props) {
|
|
84
|
+
// One router per app: the session (location, history, delegation, link
|
|
85
|
+
// claims, preloading) has a single owner, and a second instance would
|
|
86
|
+
// fight it — stale content on click navigations, conflicting link
|
|
87
|
+
// attributes. Compose route trees instead; lazy subtrees are the planned
|
|
88
|
+
// answer for definitions unknown at build time.
|
|
89
|
+
if (useOptionalContext(RouterContextObj)) {
|
|
90
|
+
console.warn("Mounting a router inside another router is not supported. " +
|
|
91
|
+
"Compose route trees in one createRouter config instead.");
|
|
92
|
+
}
|
|
93
|
+
const root = untrack(() => props.children);
|
|
94
|
+
let restoration;
|
|
95
|
+
let history = config.history;
|
|
96
|
+
if (!isServer && (config.scrollRestoration ?? !history)) {
|
|
97
|
+
restoration = createScrollRestoration();
|
|
98
|
+
history = withScrollRestoration(history || browserHistory(), restoration);
|
|
99
|
+
}
|
|
100
|
+
const integration = isServer
|
|
101
|
+
? staticIntegration(props.url, config.history && config.history.utils)
|
|
102
|
+
: createIntegration(history || browserHistory());
|
|
103
|
+
let context;
|
|
104
|
+
const routerState = createRouterContext(integration, branches, () => context, {
|
|
105
|
+
base: basePath,
|
|
106
|
+
singleFlight: config.singleFlight,
|
|
107
|
+
transformUrl: config.transformUrl
|
|
108
|
+
});
|
|
109
|
+
if (!isServer) {
|
|
110
|
+
setupNativeEvents({
|
|
111
|
+
preload: config.preloadLinks,
|
|
112
|
+
explicitLinks: config.explicitLinks,
|
|
113
|
+
actionBase: config.actionBase,
|
|
114
|
+
transformUrl: config.transformUrl
|
|
115
|
+
})(routerState);
|
|
116
|
+
setupLinkClaims(routerState, config.explicitLinks);
|
|
117
|
+
if (routerState.singleFlight)
|
|
118
|
+
onCleanup(registerFlightRouter(routerState));
|
|
119
|
+
restoration && restoration.create(routerState);
|
|
120
|
+
}
|
|
121
|
+
return (<RouterContextObj value={routerState}>
|
|
122
|
+
<Root routerState={routerState} root={root} preload={config.preload}>
|
|
123
|
+
{(context = getOwner()) && null}
|
|
124
|
+
<Routes routerState={routerState} branches={branches}/>
|
|
125
|
+
</Root>
|
|
126
|
+
</RouterContextObj>);
|
|
127
|
+
}
|
|
128
|
+
const instance = Object.assign(RouterComponent, {
|
|
129
|
+
routes: config.routes,
|
|
130
|
+
config,
|
|
131
|
+
match(url) {
|
|
132
|
+
const u = new URL(url, mockBase);
|
|
133
|
+
const pathname = config.transformUrl ? config.transformUrl(u.pathname) : u.pathname;
|
|
134
|
+
return getRouteMatches(branches(), pathname).map(({ route, path, params }) => ({
|
|
135
|
+
path: route.originalPath,
|
|
136
|
+
pattern: route.pattern,
|
|
137
|
+
match: path,
|
|
138
|
+
params,
|
|
139
|
+
info: route.info
|
|
140
|
+
}));
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
// Built on first access (a getter via Object.assign would run during the
|
|
144
|
+
// copy) so runtimes without Proxy — some older TVs — can still route as
|
|
145
|
+
// long as they never touch typed paths.
|
|
146
|
+
let paths;
|
|
147
|
+
Object.defineProperty(instance, "paths", {
|
|
148
|
+
get: () => paths || (paths = createPathsProxy(renderPath, basePath))
|
|
149
|
+
});
|
|
150
|
+
return instance;
|
|
151
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { LocationChange, RouterUtils } from "../types.js";
|
|
2
|
+
export declare function bindEvent(target: EventTarget, type: string, handler: EventListener): () => void;
|
|
3
|
+
export declare function saveCurrentDepth(): void;
|
|
4
|
+
/**
|
|
5
|
+
* A history adapter: the source of truth for the current URL and how
|
|
6
|
+
* navigations write back to it. Adapters are plain imported values so
|
|
7
|
+
* unused ones never enter the bundle — `createRouter` defaults to browser
|
|
8
|
+
* history on the client and the request URL on the server.
|
|
9
|
+
*/
|
|
10
|
+
export interface RouterHistory {
|
|
11
|
+
get: () => string | LocationChange;
|
|
12
|
+
set: (next: LocationChange) => void;
|
|
13
|
+
init?: (notify: (value?: string | LocationChange) => void) => () => void;
|
|
14
|
+
utils?: Partial<RouterUtils>;
|
|
15
|
+
}
|
|
16
|
+
export declare function browserHistory(): RouterHistory;
|
|
17
|
+
export declare function hashParser(str: string): string;
|
|
18
|
+
export declare function hashHistory(): RouterHistory;
|
|
19
|
+
export interface MemoryHistoryAdapter extends RouterHistory {
|
|
20
|
+
get: () => string;
|
|
21
|
+
go: (delta: number) => void;
|
|
22
|
+
back: () => void;
|
|
23
|
+
forward: () => void;
|
|
24
|
+
listen: (listener: (value: string) => void) => () => void;
|
|
25
|
+
}
|
|
26
|
+
export declare function memoryHistory(initial?: string): MemoryHistoryAdapter;
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { isServer } from "@solidjs/web";
|
|
2
|
+
export function bindEvent(target, type, handler) {
|
|
3
|
+
target.addEventListener(type, handler);
|
|
4
|
+
return () => target.removeEventListener(type, handler);
|
|
5
|
+
}
|
|
6
|
+
// Depth stamping supports blocking browser-initiated navigation (back/forward)
|
|
7
|
+
// for `useBeforeLeave`. It stays always-on — a couple of history.state writes —
|
|
8
|
+
// so blocking stays exact no matter when the first guard subscribes, while the
|
|
9
|
+
// guard machinery itself lives behind the lazy `beforeLeave` slot.
|
|
10
|
+
let depth;
|
|
11
|
+
export function saveCurrentDepth() {
|
|
12
|
+
if (!window.history.state || window.history.state._depth == null) {
|
|
13
|
+
window.history.replaceState({ ...window.history.state, _depth: window.history.length - 1 }, "");
|
|
14
|
+
}
|
|
15
|
+
depth = window.history.state._depth;
|
|
16
|
+
}
|
|
17
|
+
function keepDepth(state) {
|
|
18
|
+
return {
|
|
19
|
+
...state,
|
|
20
|
+
_depth: window.history.state && window.history.state._depth
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
function notifyIfNotBlocked(notify, block) {
|
|
24
|
+
let ignore = false;
|
|
25
|
+
return () => {
|
|
26
|
+
const prevDepth = depth;
|
|
27
|
+
saveCurrentDepth();
|
|
28
|
+
const delta = prevDepth == null ? null : depth - prevDepth;
|
|
29
|
+
if (ignore) {
|
|
30
|
+
ignore = false;
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (delta && block(delta)) {
|
|
34
|
+
ignore = true;
|
|
35
|
+
window.history.go(-delta);
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
notify();
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function scrollToHash(hash, fallbackTop) {
|
|
43
|
+
const el = hash && document.getElementById(hash);
|
|
44
|
+
if (el) {
|
|
45
|
+
el.scrollIntoView();
|
|
46
|
+
}
|
|
47
|
+
else if (fallbackTop) {
|
|
48
|
+
window.scrollTo(0, 0);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
export function browserHistory() {
|
|
52
|
+
const getSource = () => {
|
|
53
|
+
const url = window.location.pathname + window.location.search;
|
|
54
|
+
const state = window.history.state &&
|
|
55
|
+
window.history.state._depth &&
|
|
56
|
+
Object.keys(window.history.state).length === 1
|
|
57
|
+
? undefined
|
|
58
|
+
: window.history.state;
|
|
59
|
+
return {
|
|
60
|
+
value: url + window.location.hash,
|
|
61
|
+
state
|
|
62
|
+
};
|
|
63
|
+
};
|
|
64
|
+
const beforeLeave = {};
|
|
65
|
+
if (!isServer)
|
|
66
|
+
saveCurrentDepth();
|
|
67
|
+
return {
|
|
68
|
+
get: getSource,
|
|
69
|
+
set({ value, replace, scroll, state }) {
|
|
70
|
+
if (replace) {
|
|
71
|
+
window.history.replaceState(keepDepth(state), "", value);
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
window.history.pushState(state, "", value);
|
|
75
|
+
}
|
|
76
|
+
scrollToHash(decodeURIComponent(window.location.hash.slice(1)), scroll);
|
|
77
|
+
saveCurrentDepth();
|
|
78
|
+
},
|
|
79
|
+
init: notify => bindEvent(window, "popstate", notifyIfNotBlocked(notify, delta => {
|
|
80
|
+
const guard = beforeLeave.current;
|
|
81
|
+
if (!guard)
|
|
82
|
+
return false;
|
|
83
|
+
if (delta) {
|
|
84
|
+
return !guard.confirm(delta);
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
const s = getSource();
|
|
88
|
+
return !guard.confirm(s.value, { state: s.state });
|
|
89
|
+
}
|
|
90
|
+
})),
|
|
91
|
+
utils: {
|
|
92
|
+
go: delta => window.history.go(delta),
|
|
93
|
+
beforeLeave
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
export function hashParser(str) {
|
|
98
|
+
const to = str.replace(/^.*?#/, "");
|
|
99
|
+
// Hash-only hrefs like `#foo` from plain anchors will come in as `/#foo` whereas a link to
|
|
100
|
+
// `/foo` will be `/#/foo`. Check if the to starts with a `/` and if not append it as a hash
|
|
101
|
+
// to the current path so we can handle these in-page anchors correctly.
|
|
102
|
+
if (!to.startsWith("/")) {
|
|
103
|
+
const [, path = "/"] = window.location.hash.split("#", 2);
|
|
104
|
+
return `${path}#${to}`;
|
|
105
|
+
}
|
|
106
|
+
return to;
|
|
107
|
+
}
|
|
108
|
+
export function hashHistory() {
|
|
109
|
+
const getSource = () => window.location.hash.slice(1);
|
|
110
|
+
const beforeLeave = {};
|
|
111
|
+
if (!isServer)
|
|
112
|
+
saveCurrentDepth();
|
|
113
|
+
return {
|
|
114
|
+
get: getSource,
|
|
115
|
+
set({ value, replace, scroll, state }) {
|
|
116
|
+
if (replace) {
|
|
117
|
+
window.history.replaceState(keepDepth(state), "", "#" + value);
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
window.history.pushState(state, "", "#" + value);
|
|
121
|
+
}
|
|
122
|
+
const hashIndex = value.indexOf("#");
|
|
123
|
+
const hash = hashIndex >= 0 ? value.slice(hashIndex + 1) : "";
|
|
124
|
+
scrollToHash(hash, scroll);
|
|
125
|
+
saveCurrentDepth();
|
|
126
|
+
},
|
|
127
|
+
init: notify => bindEvent(window, "hashchange", notifyIfNotBlocked(notify, delta => {
|
|
128
|
+
const guard = beforeLeave.current;
|
|
129
|
+
return !!guard && !guard.confirm(delta && delta < 0 ? delta : getSource());
|
|
130
|
+
})),
|
|
131
|
+
utils: {
|
|
132
|
+
go: delta => window.history.go(delta),
|
|
133
|
+
renderPath: path => `#${path}`,
|
|
134
|
+
parsePath: hashParser,
|
|
135
|
+
beforeLeave
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
export function memoryHistory(initial = "/") {
|
|
140
|
+
const entries = [initial];
|
|
141
|
+
let index = 0;
|
|
142
|
+
const listeners = [];
|
|
143
|
+
const go = (n) => {
|
|
144
|
+
// https://github.com/remix-run/react-router/blob/682810ca929d0e3c64a76f8d6e465196b7a2ac58/packages/router/history.ts#L245
|
|
145
|
+
index = Math.max(0, Math.min(index + n, entries.length - 1));
|
|
146
|
+
const value = entries[index];
|
|
147
|
+
listeners.forEach(listener => listener(value));
|
|
148
|
+
};
|
|
149
|
+
const listen = (listener) => {
|
|
150
|
+
listeners.push(listener);
|
|
151
|
+
return () => {
|
|
152
|
+
const i = listeners.indexOf(listener);
|
|
153
|
+
listeners.splice(i, 1);
|
|
154
|
+
};
|
|
155
|
+
};
|
|
156
|
+
return {
|
|
157
|
+
get: () => entries[index],
|
|
158
|
+
set: ({ value, scroll, replace }) => {
|
|
159
|
+
if (replace) {
|
|
160
|
+
entries[index] = value;
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
entries.splice(index + 1, entries.length - index, value);
|
|
164
|
+
index++;
|
|
165
|
+
}
|
|
166
|
+
listeners.forEach(listener => listener(value));
|
|
167
|
+
setTimeout(() => {
|
|
168
|
+
if (scroll) {
|
|
169
|
+
scrollToHash(value.split("#")[1] || "", true);
|
|
170
|
+
}
|
|
171
|
+
}, 0);
|
|
172
|
+
},
|
|
173
|
+
back: () => go(-1),
|
|
174
|
+
forward: () => go(1),
|
|
175
|
+
go,
|
|
176
|
+
listen,
|
|
177
|
+
init: listen,
|
|
178
|
+
utils: { go }
|
|
179
|
+
};
|
|
180
|
+
}
|
package/dist/routers/index.d.ts
CHANGED
|
@@ -1,11 +1,4 @@
|
|
|
1
|
-
export {
|
|
2
|
-
export type {
|
|
3
|
-
export {
|
|
4
|
-
export {
|
|
5
|
-
export type { RouterProps } from "./Router.js";
|
|
6
|
-
export { HashRouter } from "./HashRouter.js";
|
|
7
|
-
export type { HashRouterProps } from "./HashRouter.js";
|
|
8
|
-
export { MemoryRouter, createMemoryHistory } from "./MemoryRouter.js";
|
|
9
|
-
export type { MemoryRouterProps, MemoryHistory } from "./MemoryRouter.js";
|
|
10
|
-
export { StaticRouter } from "./StaticRouter.js";
|
|
11
|
-
export type { StaticRouterProps } from "./StaticRouter.js";
|
|
1
|
+
export { createRouter, defineRoutes } from "./factory.jsx";
|
|
2
|
+
export type { RouterConfig, RouterInstance, RouterProps } from "./factory.jsx";
|
|
3
|
+
export { browserHistory, hashHistory, memoryHistory } from "./history.js";
|
|
4
|
+
export type { RouterHistory, MemoryHistoryAdapter } from "./history.js";
|
package/dist/routers/index.js
CHANGED
|
@@ -1,6 +1,2 @@
|
|
|
1
|
-
export {
|
|
2
|
-
export {
|
|
3
|
-
export { Router } from "./Router.js";
|
|
4
|
-
export { HashRouter } from "./HashRouter.js";
|
|
5
|
-
export { MemoryRouter, createMemoryHistory } from "./MemoryRouter.js";
|
|
6
|
-
export { StaticRouter } from "./StaticRouter.js";
|
|
1
|
+
export { createRouter, defineRoutes } from "./factory.jsx";
|
|
2
|
+
export { browserHistory, hashHistory, memoryHistory } from "./history.js";
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { RouterContext } from "../types.js";
|
|
2
|
+
import type { RouterHistory } from "./history.js";
|
|
3
|
+
/**
|
|
4
|
+
* Explicit scroll restoration for back/forward navigation. The browser's
|
|
5
|
+
* native same-document heuristic is unreliable for suspense-driven rendering:
|
|
6
|
+
* if the destination route forces a layout while the document is still short,
|
|
7
|
+
* the saved offset for the previous entry is clamped and lost (#577).
|
|
8
|
+
*
|
|
9
|
+
* Positions are captured continuously from the scroll event, keyed by the
|
|
10
|
+
* `_depth` the router already stamps on every history entry — capturing at
|
|
11
|
+
* scroll time (rather than at exit) stays correct through `useBeforeLeave`
|
|
12
|
+
* blocked/reverted traversals. The map persists to sessionStorage on pagehide
|
|
13
|
+
* so restoration survives reloads, which `scrollRestoration = "manual"`
|
|
14
|
+
* otherwise disables. Restoration runs once routing settles; if the document
|
|
15
|
+
* is still shorter than the target (a boundary below the fold hasn't
|
|
16
|
+
* resolved), a ResizeObserver retries as content grows, cancelled by the
|
|
17
|
+
* first user scroll.
|
|
18
|
+
*/
|
|
19
|
+
export declare function createScrollRestoration(): {
|
|
20
|
+
/** When the adapter notifies a traversal: mark the target for restoration. */
|
|
21
|
+
onPop(): void;
|
|
22
|
+
/** After a push: forward entries died, and this depth may be reused. */
|
|
23
|
+
onPush(): void;
|
|
24
|
+
create(router: RouterContext): void;
|
|
25
|
+
};
|
|
26
|
+
export type ScrollRestoration = ReturnType<typeof createScrollRestoration>;
|
|
27
|
+
/**
|
|
28
|
+
* Threads restoration through a history adapter: pushes prune dead forward
|
|
29
|
+
* entries, and adapter notifications (unblocked pops) mark the traversal
|
|
30
|
+
* target. Notification runs after the adapter's depth bookkeeping, so the
|
|
31
|
+
* marked depth is the entry being restored to.
|
|
32
|
+
*/
|
|
33
|
+
export declare function withScrollRestoration(history: RouterHistory, restoration: ScrollRestoration): RouterHistory;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { createEffect, onCleanup } from "solid-js";
|
|
2
|
+
import { bindEvent, saveCurrentDepth } from "./history.js";
|
|
3
|
+
const STORAGE_KEY = "solid-router:scroll";
|
|
4
|
+
/**
|
|
5
|
+
* Explicit scroll restoration for back/forward navigation. The browser's
|
|
6
|
+
* native same-document heuristic is unreliable for suspense-driven rendering:
|
|
7
|
+
* if the destination route forces a layout while the document is still short,
|
|
8
|
+
* the saved offset for the previous entry is clamped and lost (#577).
|
|
9
|
+
*
|
|
10
|
+
* Positions are captured continuously from the scroll event, keyed by the
|
|
11
|
+
* `_depth` the router already stamps on every history entry — capturing at
|
|
12
|
+
* scroll time (rather than at exit) stays correct through `useBeforeLeave`
|
|
13
|
+
* blocked/reverted traversals. The map persists to sessionStorage on pagehide
|
|
14
|
+
* so restoration survives reloads, which `scrollRestoration = "manual"`
|
|
15
|
+
* otherwise disables. Restoration runs once routing settles; if the document
|
|
16
|
+
* is still shorter than the target (a boundary below the fold hasn't
|
|
17
|
+
* resolved), a ResizeObserver retries as content grows, cancelled by the
|
|
18
|
+
* first user scroll.
|
|
19
|
+
*/
|
|
20
|
+
export function createScrollRestoration() {
|
|
21
|
+
window.history.scrollRestoration = "manual";
|
|
22
|
+
// the current entry needs its depth stamp for captures to have a key, even
|
|
23
|
+
// if something replaced history.state after the adapter stamped it
|
|
24
|
+
saveCurrentDepth();
|
|
25
|
+
let positions = {};
|
|
26
|
+
try {
|
|
27
|
+
positions = JSON.parse(sessionStorage.getItem(STORAGE_KEY)) || {};
|
|
28
|
+
}
|
|
29
|
+
catch { }
|
|
30
|
+
const depth = () => window.history.state && window.history.state._depth;
|
|
31
|
+
let programmatic = false;
|
|
32
|
+
let pending;
|
|
33
|
+
let disconnect;
|
|
34
|
+
const cancelGuard = () => {
|
|
35
|
+
disconnect && disconnect();
|
|
36
|
+
disconnect = undefined;
|
|
37
|
+
};
|
|
38
|
+
const unbind = [
|
|
39
|
+
bindEvent(window, "scroll", () => {
|
|
40
|
+
const d = depth();
|
|
41
|
+
if (d != null)
|
|
42
|
+
positions[d] = window.scrollY;
|
|
43
|
+
if (!programmatic) {
|
|
44
|
+
// the user took over — a pending or chasing restore would yank them
|
|
45
|
+
pending = undefined;
|
|
46
|
+
cancelGuard();
|
|
47
|
+
}
|
|
48
|
+
}),
|
|
49
|
+
bindEvent(window, "pagehide", () => {
|
|
50
|
+
try {
|
|
51
|
+
sessionStorage.setItem(STORAGE_KEY, JSON.stringify(positions));
|
|
52
|
+
}
|
|
53
|
+
catch { }
|
|
54
|
+
})
|
|
55
|
+
];
|
|
56
|
+
const restore = () => {
|
|
57
|
+
if (pending == null)
|
|
58
|
+
return;
|
|
59
|
+
const y = positions[pending];
|
|
60
|
+
pending = undefined;
|
|
61
|
+
if (y == null)
|
|
62
|
+
return;
|
|
63
|
+
cancelGuard();
|
|
64
|
+
const attempt = () => {
|
|
65
|
+
programmatic = true;
|
|
66
|
+
window.scrollTo(0, y);
|
|
67
|
+
programmatic = false;
|
|
68
|
+
// reachable once the document is tall enough to hold the offset
|
|
69
|
+
return document.documentElement.scrollHeight - window.innerHeight >= y;
|
|
70
|
+
};
|
|
71
|
+
if (!attempt() && typeof ResizeObserver !== "undefined") {
|
|
72
|
+
const observer = new ResizeObserver(() => attempt() && cancelGuard());
|
|
73
|
+
observer.observe(document.documentElement);
|
|
74
|
+
disconnect = () => observer.disconnect();
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
return {
|
|
78
|
+
/** When the adapter notifies a traversal: mark the target for restoration. */
|
|
79
|
+
onPop() {
|
|
80
|
+
pending = depth();
|
|
81
|
+
},
|
|
82
|
+
/** After a push: forward entries died, and this depth may be reused. */
|
|
83
|
+
onPush() {
|
|
84
|
+
const d = depth();
|
|
85
|
+
if (d != null)
|
|
86
|
+
for (const k in positions)
|
|
87
|
+
+k >= d && delete positions[k];
|
|
88
|
+
},
|
|
89
|
+
create(router) {
|
|
90
|
+
// Restore once the traversal has settled: key on the location (a fully
|
|
91
|
+
// synchronous pop commits without isRouting ever flipping) and on
|
|
92
|
+
// isRouting, which reports in-flight transitions — native pops
|
|
93
|
+
// included — and holds the restore until they commit. restore() no-ops
|
|
94
|
+
// unless a traversal marked a target, so push navigations are inert.
|
|
95
|
+
createEffect(() => ({
|
|
96
|
+
url: router.location.pathname + router.location.search + router.location.hash,
|
|
97
|
+
routing: router.isRouting()
|
|
98
|
+
}), current => {
|
|
99
|
+
if (!current.routing)
|
|
100
|
+
restore();
|
|
101
|
+
});
|
|
102
|
+
onCleanup(() => {
|
|
103
|
+
unbind.forEach(u => u());
|
|
104
|
+
cancelGuard();
|
|
105
|
+
});
|
|
106
|
+
// reload/back_forward document loads land on an existing entry (a fresh
|
|
107
|
+
// navigation starts a new one and belongs at the top); the effect's
|
|
108
|
+
// initial run performs the restore after first render
|
|
109
|
+
const [nav] = (performance.getEntriesByType &&
|
|
110
|
+
performance.getEntriesByType("navigation"));
|
|
111
|
+
if (nav && nav.type !== "navigate")
|
|
112
|
+
pending = depth();
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Threads restoration through a history adapter: pushes prune dead forward
|
|
118
|
+
* entries, and adapter notifications (unblocked pops) mark the traversal
|
|
119
|
+
* target. Notification runs after the adapter's depth bookkeeping, so the
|
|
120
|
+
* marked depth is the entry being restored to.
|
|
121
|
+
*/
|
|
122
|
+
export function withScrollRestoration(history, restoration) {
|
|
123
|
+
return {
|
|
124
|
+
...history,
|
|
125
|
+
set(next) {
|
|
126
|
+
history.set(next);
|
|
127
|
+
next.replace || restoration.onPush();
|
|
128
|
+
},
|
|
129
|
+
init: history.init &&
|
|
130
|
+
(notify => history.init(value => {
|
|
131
|
+
restoration.onPop();
|
|
132
|
+
notify(value);
|
|
133
|
+
}))
|
|
134
|
+
};
|
|
135
|
+
}
|