@fluixi/core 1.0.0-alpha.94 → 1.0.0-alpha.96
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 +1 -1
- package/dist/cdn/core.global.js +2 -2
- package/dist/src/cdn/{chunk-7XM7Q5VR.mjs → chunk-RKBFEFZP.mjs} +125 -10
- package/dist/src/cdn/core.cjs +132 -12
- package/dist/src/cdn/core.mjs +4 -4
- package/dist/src/cdn/{router-VOY43YNM.mjs → router-V2THK27H.mjs} +3 -3
- package/dist/src/index.cjs +122 -7
- package/dist/src/index.mjs +122 -7
- package/dist/src/lib/client/{chunk-TSXM2PNY.mjs → chunk-RNRJHQFG.mjs} +1 -1
- package/dist/src/lib/client/index.cjs +133 -13
- package/dist/src/lib/client/index.d.ts.map +1 -1
- package/dist/src/lib/client/index.js +4 -1
- package/dist/src/lib/client/index.mjs +5 -5
- package/dist/src/lib/client/{router-BDVPLLYX.mjs → router-F3Y4BIVJ.mjs} +127 -12
- package/dist/src/lib/core.cjs +121 -6
- package/dist/src/lib/core.mjs +121 -6
- package/dist/src/lib/i18n/index.cjs +1 -1
- package/dist/src/lib/i18n/index.mjs +1 -1
- package/dist/src/lib/index.cjs +1 -1
- package/dist/src/lib/index.mjs +1 -1
- package/dist/src/lib/isomorphic.cjs +1 -1
- package/dist/src/lib/isomorphic.d.ts +5 -0
- package/dist/src/lib/isomorphic.d.ts.map +1 -1
- package/dist/src/lib/isomorphic.js +1 -1
- package/dist/src/lib/isomorphic.mjs +1 -1
- package/dist/src/lib/jsx-runtime/index.cjs +1 -1
- package/dist/src/lib/jsx-runtime/index.mjs +1 -1
- package/dist/src/lib/jsx-runtime/jsx-dev-runtime.cjs +1 -1
- package/dist/src/lib/jsx-runtime/jsx-dev-runtime.mjs +1 -1
- package/dist/src/lib/plugins/index.cjs +1 -1
- package/dist/src/lib/plugins/index.mjs +1 -1
- package/dist/src/lib/render/index.cjs +1 -1
- package/dist/src/lib/render/index.mjs +1 -1
- package/dist/src/lib/render/versions.cjs +1 -1
- package/dist/src/lib/render/versions.mjs +1 -1
- package/dist/src/lib/router/data.cjs +4 -4
- package/dist/src/lib/router/data.d.ts.map +1 -1
- package/dist/src/lib/router/data.js +9 -8
- package/dist/src/lib/router/data.mjs +4 -4
- package/dist/src/lib/router/index.cjs +126 -11
- package/dist/src/lib/router/index.d.ts +8 -5
- package/dist/src/lib/router/index.d.ts.map +1 -1
- package/dist/src/lib/router/index.js +94 -7
- package/dist/src/lib/router/index.mjs +126 -11
- package/dist/src/lib/router/scroll.cjs +79 -0
- package/dist/src/lib/router/scroll.d.ts +19 -0
- package/dist/src/lib/router/scroll.d.ts.map +1 -0
- package/dist/src/lib/router/scroll.js +68 -0
- package/dist/src/lib/router/scroll.mjs +58 -0
- package/dist/src/lib/server/index.cjs +1 -1
- package/dist/src/lib/server/index.mjs +1 -1
- package/dist/src/lib/storage/index.cjs +1 -1
- package/dist/src/lib/storage/index.mjs +1 -1
- package/dist/src/routes.cjs +1 -1
- package/dist/src/routes.mjs +1 -1
- package/dist/src/utils.cjs +1 -1
- package/dist/src/utils.mjs +1 -1
- package/dist/src/version.generated.cjs +1 -1
- package/dist/src/version.generated.d.ts +1 -1
- package/dist/src/version.generated.js +1 -1
- package/dist/src/version.generated.mjs +1 -1
- package/dist/tsconfig.lib.tsbuildinfo +1 -1
- package/package.json +16 -16
|
@@ -19,6 +19,7 @@ import { insert, createComponent, createNativeElement, setAttribute, } from '@fl
|
|
|
19
19
|
import { createContext, useContext } from '@fluixi/reactive/signal';
|
|
20
20
|
import { get } from '@fluixi/utils/object';
|
|
21
21
|
import { deepEqual } from '@fluixi/utils/compare';
|
|
22
|
+
import { keepScroll } from './scroll.js';
|
|
22
23
|
import { createRouter as createCoreRouter, createBrowserHistory, matchRoutes, matchPath, } from '@fluixi/router';
|
|
23
24
|
const RouterContext = createContext(undefined, 'router');
|
|
24
25
|
const OutletContext = createContext(undefined, 'outlet');
|
|
@@ -55,12 +56,56 @@ function renderLayer(m, depth, base) {
|
|
|
55
56
|
});
|
|
56
57
|
});
|
|
57
58
|
};
|
|
59
|
+
// Whether the loader looked at the location: only then does a change of query string,
|
|
60
|
+
// which keeps this layer, concern it. Unknown until it has run here: a page hydrated from
|
|
61
|
+
// the server got its data without running it.
|
|
62
|
+
let readsLocation;
|
|
58
63
|
const [resource, { refetch }] = createResource(() => {
|
|
59
|
-
|
|
64
|
+
// The params and location this load is for, fixed now. Accessors read later would see
|
|
65
|
+
// the next navigation's: a lazy route reads them after its module resolves, and a
|
|
66
|
+
// reload a revalidation started just before a navigation then ran with the next page's
|
|
67
|
+
// params (an empty id).
|
|
68
|
+
const params = untrack(base.params);
|
|
69
|
+
const location = untrack(base.location);
|
|
70
|
+
readsLocation = false;
|
|
71
|
+
const result = loader({
|
|
72
|
+
params: () => params,
|
|
73
|
+
location: () => {
|
|
74
|
+
readsLocation = true;
|
|
75
|
+
return location;
|
|
76
|
+
},
|
|
77
|
+
}, owner, () => cancelled, trackDeps);
|
|
60
78
|
return router ? router.trackLoad(result) : result;
|
|
61
79
|
});
|
|
62
80
|
// A revalidated cache entry: load again. The page stays on screen with its current data.
|
|
63
|
-
|
|
81
|
+
// Not a page on its way out (another route matched at this depth, kept until the next one
|
|
82
|
+
// has its data): its loader would run with the params of the page replacing it.
|
|
83
|
+
onCleanup(onRevalidate(() => {
|
|
84
|
+
if (untrack(() => base.matched()[depth]?.route) !== m.route)
|
|
85
|
+
return;
|
|
86
|
+
void refetch();
|
|
87
|
+
}));
|
|
88
|
+
// A navigation that changes only this layer's params, or only the query string, keeps the
|
|
89
|
+
// layer, so nothing else would load its data for them. Params compared by value: `matched`
|
|
90
|
+
// is rebuilt on every navigation. The query counts only for a loader that read the
|
|
91
|
+
// location; the hash never does.
|
|
92
|
+
let loadedFor = untrack(() => JSON.stringify(base.matched()[depth]?.params ?? {}));
|
|
93
|
+
let searchedFor = untrack(() => base.location().search);
|
|
94
|
+
createEffect(() => {
|
|
95
|
+
const now = base.matched()[depth];
|
|
96
|
+
const search = base.location().search;
|
|
97
|
+
// Another route at this depth: this page is on its way out, kept only until the next
|
|
98
|
+
// one has its data, and the new params are not for it.
|
|
99
|
+
if (now?.route !== m.route)
|
|
100
|
+
return;
|
|
101
|
+
const params = JSON.stringify(now.params ?? {});
|
|
102
|
+
const paramsMoved = params !== loadedFor;
|
|
103
|
+
const searchMoved = readsLocation !== false && search !== searchedFor;
|
|
104
|
+
loadedFor = params;
|
|
105
|
+
searchedFor = search;
|
|
106
|
+
if (paramsMoved || searchMoved)
|
|
107
|
+
untrack(() => void refetch());
|
|
108
|
+
});
|
|
64
109
|
// A failed load throws where the page reads it, so the nearest ErrorBoundary shows it.
|
|
65
110
|
// A resource reads as undefined once it has failed, which left the page empty and gave
|
|
66
111
|
// nothing a chance to catch the error.
|
|
@@ -68,6 +113,12 @@ function renderLayer(m, depth, base) {
|
|
|
68
113
|
const error = resource.error;
|
|
69
114
|
if (error !== undefined && error !== null)
|
|
70
115
|
throw error;
|
|
116
|
+
// Loading again (new params, a revalidation): the page keeps its current data. Reading
|
|
117
|
+
// the resource itself would suspend, and a Suspense boundary above would take the page
|
|
118
|
+
// out of the document until the data arrives, losing what the reader had in it.
|
|
119
|
+
const r = resource;
|
|
120
|
+
if (r.state === 'refreshing')
|
|
121
|
+
return r.latest;
|
|
71
122
|
return resource();
|
|
72
123
|
};
|
|
73
124
|
Object.defineProperty(data, 'loading', { get: () => resource.loading });
|
|
@@ -250,6 +301,11 @@ export function Router(props) {
|
|
|
250
301
|
* the page would show B.
|
|
251
302
|
*/
|
|
252
303
|
let navigationId = 0;
|
|
304
|
+
// The window's scroll, for a browser history: where each navigation lands once its page is
|
|
305
|
+
// shown. `arrived` is the navigation whose page was last committed.
|
|
306
|
+
const scroll = history.window ? keepScroll(router.state().location) : undefined;
|
|
307
|
+
let arriving = scroll ? { id: 0, location: router.state().location, from: undefined } : null;
|
|
308
|
+
const [arrived, setArrived] = createSignal(0);
|
|
253
309
|
/**
|
|
254
310
|
* How to present the next commit, set by `useNavigate` immediately before it calls
|
|
255
311
|
* into the core router.
|
|
@@ -268,6 +324,10 @@ export function Router(props) {
|
|
|
268
324
|
const id = ++navigationId;
|
|
269
325
|
const mode = takeMode();
|
|
270
326
|
setNavigating(true);
|
|
327
|
+
if (scroll) {
|
|
328
|
+
scroll.leave();
|
|
329
|
+
arriving = { id, location: next.location, from: untrack(state).location };
|
|
330
|
+
}
|
|
271
331
|
// Load the modules the next route needs before anything is torn down, so a lazy
|
|
272
332
|
// route renders straight through instead of suspending into a fallback with the
|
|
273
333
|
// old page already gone.
|
|
@@ -280,6 +340,8 @@ export function Router(props) {
|
|
|
280
340
|
const flushed = flush();
|
|
281
341
|
if (id === navigationId)
|
|
282
342
|
setNavigating(false);
|
|
343
|
+
if (scroll)
|
|
344
|
+
setArrived(id);
|
|
283
345
|
return flushed;
|
|
284
346
|
};
|
|
285
347
|
if (!prepared) {
|
|
@@ -296,6 +358,7 @@ export function Router(props) {
|
|
|
296
358
|
});
|
|
297
359
|
onCleanup(() => {
|
|
298
360
|
unsub();
|
|
361
|
+
scroll?.dispose();
|
|
299
362
|
router.destroy();
|
|
300
363
|
// Anything still preparing is now stale, so nothing can commit into a torn-down
|
|
301
364
|
// router.
|
|
@@ -319,6 +382,27 @@ export function Router(props) {
|
|
|
319
382
|
return result;
|
|
320
383
|
},
|
|
321
384
|
};
|
|
385
|
+
// Scroll once the page is on screen: after its modules and its data, which is when the
|
|
386
|
+
// router stops routing, and a frame later, before that frame is painted. The page's own
|
|
387
|
+
// effects fill it in after the router's (an article set through innerHTML), and a scroll
|
|
388
|
+
// into a page not yet filled is cut short to its height. A newer navigation takes over
|
|
389
|
+
// from one still loading.
|
|
390
|
+
if (scroll) {
|
|
391
|
+
const nextFrame = typeof requestAnimationFrame === 'function' ? requestAnimationFrame : (fn) => setTimeout(fn, 0);
|
|
392
|
+
createEffect(() => {
|
|
393
|
+
const id = arrived();
|
|
394
|
+
if (ctx.isRouting())
|
|
395
|
+
return;
|
|
396
|
+
const next = arriving;
|
|
397
|
+
if (!next || next.id !== id || id !== navigationId)
|
|
398
|
+
return;
|
|
399
|
+
arriving = null;
|
|
400
|
+
nextFrame(() => {
|
|
401
|
+
if (id === navigationId)
|
|
402
|
+
scroll.arrive(next.location, next.from);
|
|
403
|
+
});
|
|
404
|
+
});
|
|
405
|
+
}
|
|
322
406
|
// Render depth 0 (a virtual parent at depth -1 so the root reuses the same path).
|
|
323
407
|
const root = {
|
|
324
408
|
depth: -1,
|
|
@@ -650,7 +734,7 @@ export function Link(props) {
|
|
|
650
734
|
if (props.slot)
|
|
651
735
|
setAttribute(a, 'slot', read(props.slot));
|
|
652
736
|
// Forward any other author attributes (data-*, aria-*, style, id, title, role, ...)
|
|
653
|
-
// onto the <a>, the way <
|
|
737
|
+
// onto the <a>, the way <ActionForm> does. Without this a component composed via
|
|
654
738
|
// `as={Link}` (polymorphic) silently loses its styling hooks: e.g. @fluixi-ui
|
|
655
739
|
// parts carry `data-*` slots + inline styles that would otherwise be dropped, so
|
|
656
740
|
// the element renders unstyled. Own-props handled elsewhere are skipped:
|
|
@@ -728,17 +812,20 @@ export function Link(props) {
|
|
|
728
812
|
export const A = Link;
|
|
729
813
|
import { onRevalidate } from './data.js';
|
|
730
814
|
/**
|
|
731
|
-
* A `<form>` wired to
|
|
815
|
+
* A `<form>` wired to a router action: on submit it builds `FormData` from the form and calls
|
|
732
816
|
* `action(formData)` (which tracks submission state via `useSubmission`), without a full
|
|
733
817
|
* page navigation. The markup carries `method="post"` + `action={action.url}` so it's
|
|
734
818
|
* progressive-enhancement-ready.
|
|
735
819
|
*
|
|
736
|
-
* <
|
|
820
|
+
* <ActionForm action={save}>
|
|
737
821
|
* <input name="title" />
|
|
738
822
|
* <button>Save</button>
|
|
739
|
-
* </
|
|
823
|
+
* </ActionForm>
|
|
824
|
+
*
|
|
825
|
+
* It sends what the fields hold and checks nothing. For a form whose fields are validated
|
|
826
|
+
* first, use `@fluixi-ui/form`'s `Form` with an `action`.
|
|
740
827
|
*/
|
|
741
|
-
export function
|
|
828
|
+
export function ActionForm(props) {
|
|
742
829
|
const action = props.action;
|
|
743
830
|
const cls = (props.className ?? props.class); // compiler passes class -> className
|
|
744
831
|
const url = action.url ?? '';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! @fluixi/core v1.0.0-alpha.
|
|
1
|
+
/*! @fluixi/core v1.0.0-alpha.96 | (c) 2026 Ibrahima Touré and Fluixi contributors | MIT */
|
|
2
2
|
|
|
3
3
|
// src/lib/router/index.ts
|
|
4
4
|
import {
|
|
@@ -72,6 +72,64 @@ import {
|
|
|
72
72
|
import { createContext as createContext2, useContext as useContext2 } from "@fluixi/reactive/signal";
|
|
73
73
|
import { get } from "@fluixi/utils/object";
|
|
74
74
|
import { deepEqual } from "@fluixi/utils/compare";
|
|
75
|
+
|
|
76
|
+
// src/lib/router/scroll.ts
|
|
77
|
+
var STORE = "fx:scroll";
|
|
78
|
+
var KEEP = 100;
|
|
79
|
+
function keepScroll(initial) {
|
|
80
|
+
const read2 = () => {
|
|
81
|
+
try {
|
|
82
|
+
return JSON.parse(sessionStorage.getItem(STORE) ?? "{}");
|
|
83
|
+
} catch {
|
|
84
|
+
return {};
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
const positions = read2();
|
|
88
|
+
const write = () => {
|
|
89
|
+
const keys = Object.keys(positions);
|
|
90
|
+
for (const key of keys.slice(0, Math.max(0, keys.length - KEEP))) delete positions[key];
|
|
91
|
+
try {
|
|
92
|
+
sessionStorage.setItem(STORE, JSON.stringify(positions));
|
|
93
|
+
} catch {
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
let current = initial.key;
|
|
97
|
+
const save = () => {
|
|
98
|
+
if (current === void 0) return;
|
|
99
|
+
delete positions[current];
|
|
100
|
+
positions[current] = window.scrollY;
|
|
101
|
+
write();
|
|
102
|
+
};
|
|
103
|
+
const restoration = history.scrollRestoration;
|
|
104
|
+
history.scrollRestoration = "manual";
|
|
105
|
+
window.addEventListener("pagehide", save);
|
|
106
|
+
const to = (top) => window.scrollTo({ top, left: 0, behavior: "instant" });
|
|
107
|
+
const toHash = (hash) => {
|
|
108
|
+
if (!hash || hash === "#") return false;
|
|
109
|
+
const target = document.getElementById(decodeURIComponent(hash.slice(1)));
|
|
110
|
+
target?.scrollIntoView({ behavior: "instant" });
|
|
111
|
+
return target !== null;
|
|
112
|
+
};
|
|
113
|
+
return {
|
|
114
|
+
leave: save,
|
|
115
|
+
arrive(location, from) {
|
|
116
|
+
current = location.key;
|
|
117
|
+
if (location.action === "pop" || from === void 0) {
|
|
118
|
+
const y = location.key === void 0 ? void 0 : positions[location.key];
|
|
119
|
+
if (y !== void 0) to(y);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (location.action === "replace" && location.pathname === from.pathname) return;
|
|
123
|
+
if (!toHash(location.hash)) to(0);
|
|
124
|
+
},
|
|
125
|
+
dispose() {
|
|
126
|
+
window.removeEventListener("pagehide", save);
|
|
127
|
+
history.scrollRestoration = restoration;
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// src/lib/router/index.ts
|
|
75
133
|
import {
|
|
76
134
|
createRouter as createCoreRouter,
|
|
77
135
|
createBrowserHistory,
|
|
@@ -203,7 +261,8 @@ function getSubmissionsSignal(url) {
|
|
|
203
261
|
}
|
|
204
262
|
function action(fn, name, options = {}) {
|
|
205
263
|
const actionUrl = name || fn.name || "action";
|
|
206
|
-
const submit = async (
|
|
264
|
+
const submit = async (...args) => {
|
|
265
|
+
const input = args.length > 1 ? args : args[0];
|
|
207
266
|
const [, setSubmission] = getSubmissionSignal(actionUrl);
|
|
208
267
|
const [, setSubmissions] = getSubmissionsSignal(actionUrl);
|
|
209
268
|
let entry;
|
|
@@ -218,11 +277,10 @@ function action(fn, name, options = {}) {
|
|
|
218
277
|
};
|
|
219
278
|
const retry = () => {
|
|
220
279
|
drop2();
|
|
221
|
-
return submit(
|
|
280
|
+
return submit(...args);
|
|
222
281
|
};
|
|
223
282
|
entry = {
|
|
224
283
|
input,
|
|
225
|
-
// Approximate type for tracking
|
|
226
284
|
pending: true,
|
|
227
285
|
clear,
|
|
228
286
|
retry
|
|
@@ -230,7 +288,7 @@ function action(fn, name, options = {}) {
|
|
|
230
288
|
setSubmission(entry);
|
|
231
289
|
setSubmissions((list) => [...list, entry]);
|
|
232
290
|
try {
|
|
233
|
-
const result = await fn(
|
|
291
|
+
const result = await fn(...args);
|
|
234
292
|
setSubmission({
|
|
235
293
|
input,
|
|
236
294
|
result,
|
|
@@ -358,14 +416,49 @@ function renderLayer(m, depth, base) {
|
|
|
358
416
|
});
|
|
359
417
|
});
|
|
360
418
|
};
|
|
419
|
+
let readsLocation;
|
|
361
420
|
const [resource, { refetch }] = createResource3(() => {
|
|
362
|
-
const
|
|
421
|
+
const params = untrack2(base.params);
|
|
422
|
+
const location = untrack2(base.location);
|
|
423
|
+
readsLocation = false;
|
|
424
|
+
const result = loader(
|
|
425
|
+
{
|
|
426
|
+
params: () => params,
|
|
427
|
+
location: () => {
|
|
428
|
+
readsLocation = true;
|
|
429
|
+
return location;
|
|
430
|
+
}
|
|
431
|
+
},
|
|
432
|
+
owner,
|
|
433
|
+
() => cancelled,
|
|
434
|
+
trackDeps
|
|
435
|
+
);
|
|
363
436
|
return router ? router.trackLoad(result) : result;
|
|
364
437
|
});
|
|
365
|
-
onCleanup(
|
|
438
|
+
onCleanup(
|
|
439
|
+
onRevalidate(() => {
|
|
440
|
+
if (untrack2(() => base.matched()[depth]?.route) !== m.route) return;
|
|
441
|
+
void refetch();
|
|
442
|
+
})
|
|
443
|
+
);
|
|
444
|
+
let loadedFor = untrack2(() => JSON.stringify(base.matched()[depth]?.params ?? {}));
|
|
445
|
+
let searchedFor = untrack2(() => base.location().search);
|
|
446
|
+
createEffect(() => {
|
|
447
|
+
const now = base.matched()[depth];
|
|
448
|
+
const search = base.location().search;
|
|
449
|
+
if (now?.route !== m.route) return;
|
|
450
|
+
const params = JSON.stringify(now.params ?? {});
|
|
451
|
+
const paramsMoved = params !== loadedFor;
|
|
452
|
+
const searchMoved = readsLocation !== false && search !== searchedFor;
|
|
453
|
+
loadedFor = params;
|
|
454
|
+
searchedFor = search;
|
|
455
|
+
if (paramsMoved || searchMoved) untrack2(() => void refetch());
|
|
456
|
+
});
|
|
366
457
|
data = () => {
|
|
367
458
|
const error = resource.error;
|
|
368
459
|
if (error !== void 0 && error !== null) throw error;
|
|
460
|
+
const r = resource;
|
|
461
|
+
if (r.state === "refreshing") return r.latest;
|
|
369
462
|
return resource();
|
|
370
463
|
};
|
|
371
464
|
Object.defineProperty(data, "loading", { get: () => resource.loading });
|
|
@@ -470,16 +563,19 @@ function buildDefaultNotFound() {
|
|
|
470
563
|
}
|
|
471
564
|
var pendingTransitionMode;
|
|
472
565
|
function Router(props) {
|
|
473
|
-
const
|
|
566
|
+
const history2 = props.history ?? createBrowserHistory();
|
|
474
567
|
const router = createCoreRouter({
|
|
475
568
|
routes: props.routes,
|
|
476
569
|
base: props.base,
|
|
477
|
-
history
|
|
570
|
+
history: history2
|
|
478
571
|
});
|
|
479
572
|
const [state, setState] = createSignal2(router.state());
|
|
480
573
|
const [navigating, setNavigating] = createSignal2(false);
|
|
481
574
|
const [loads, setLoads] = createSignal2(0);
|
|
482
575
|
let navigationId = 0;
|
|
576
|
+
const scroll = history2.window ? keepScroll(router.state().location) : void 0;
|
|
577
|
+
let arriving = scroll ? { id: 0, location: router.state().location, from: void 0 } : null;
|
|
578
|
+
const [arrived, setArrived] = createSignal2(0);
|
|
483
579
|
const takeMode = () => {
|
|
484
580
|
const mode = pendingTransitionMode ?? (props.transition ?? "auto");
|
|
485
581
|
pendingTransitionMode = void 0;
|
|
@@ -489,6 +585,10 @@ function Router(props) {
|
|
|
489
585
|
const id = ++navigationId;
|
|
490
586
|
const mode = takeMode();
|
|
491
587
|
setNavigating(true);
|
|
588
|
+
if (scroll) {
|
|
589
|
+
scroll.leave();
|
|
590
|
+
arriving = { id, location: next.location, from: untrack2(state).location };
|
|
591
|
+
}
|
|
492
592
|
const prepared = prepareComponents(
|
|
493
593
|
(next.match?.matched ?? []).map((m) => m.component).filter(Boolean)
|
|
494
594
|
);
|
|
@@ -496,6 +596,7 @@ function Router(props) {
|
|
|
496
596
|
setState(next);
|
|
497
597
|
const flushed = flush();
|
|
498
598
|
if (id === navigationId) setNavigating(false);
|
|
599
|
+
if (scroll) setArrived(id);
|
|
499
600
|
return flushed;
|
|
500
601
|
};
|
|
501
602
|
if (!prepared) {
|
|
@@ -509,6 +610,7 @@ function Router(props) {
|
|
|
509
610
|
});
|
|
510
611
|
onCleanup(() => {
|
|
511
612
|
unsub();
|
|
613
|
+
scroll?.dispose();
|
|
512
614
|
router.destroy();
|
|
513
615
|
navigationId++;
|
|
514
616
|
});
|
|
@@ -531,6 +633,19 @@ function Router(props) {
|
|
|
531
633
|
return result;
|
|
532
634
|
}
|
|
533
635
|
};
|
|
636
|
+
if (scroll) {
|
|
637
|
+
const nextFrame = typeof requestAnimationFrame === "function" ? requestAnimationFrame : (fn) => setTimeout(fn, 0);
|
|
638
|
+
createEffect(() => {
|
|
639
|
+
const id = arrived();
|
|
640
|
+
if (ctx.isRouting()) return;
|
|
641
|
+
const next = arriving;
|
|
642
|
+
if (!next || next.id !== id || id !== navigationId) return;
|
|
643
|
+
arriving = null;
|
|
644
|
+
nextFrame(() => {
|
|
645
|
+
if (id === navigationId) scroll.arrive(next.location, next.from);
|
|
646
|
+
});
|
|
647
|
+
});
|
|
648
|
+
}
|
|
534
649
|
const root = {
|
|
535
650
|
depth: -1,
|
|
536
651
|
matched,
|
|
@@ -746,7 +861,7 @@ function Link(props) {
|
|
|
746
861
|
return a;
|
|
747
862
|
}
|
|
748
863
|
var A = Link;
|
|
749
|
-
function
|
|
864
|
+
function ActionForm(props) {
|
|
750
865
|
const action2 = props.action;
|
|
751
866
|
const cls = props.className ?? props.class;
|
|
752
867
|
const url = action2.url ?? "";
|
|
@@ -786,7 +901,7 @@ async function preloadRoutes(routes, pathname) {
|
|
|
786
901
|
}
|
|
787
902
|
export {
|
|
788
903
|
A,
|
|
789
|
-
|
|
904
|
+
ActionForm,
|
|
790
905
|
Link,
|
|
791
906
|
Outlet,
|
|
792
907
|
Redirect,
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/lib/router/scroll.ts
|
|
21
|
+
var scroll_exports = {};
|
|
22
|
+
__export(scroll_exports, {
|
|
23
|
+
keepScroll: () => keepScroll
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(scroll_exports);
|
|
26
|
+
var STORE = "fx:scroll";
|
|
27
|
+
var KEEP = 100;
|
|
28
|
+
function keepScroll(initial) {
|
|
29
|
+
const read = () => {
|
|
30
|
+
try {
|
|
31
|
+
return JSON.parse(sessionStorage.getItem(STORE) ?? "{}");
|
|
32
|
+
} catch {
|
|
33
|
+
return {};
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
const positions = read();
|
|
37
|
+
const write = () => {
|
|
38
|
+
const keys = Object.keys(positions);
|
|
39
|
+
for (const key of keys.slice(0, Math.max(0, keys.length - KEEP))) delete positions[key];
|
|
40
|
+
try {
|
|
41
|
+
sessionStorage.setItem(STORE, JSON.stringify(positions));
|
|
42
|
+
} catch {
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
let current = initial.key;
|
|
46
|
+
const save = () => {
|
|
47
|
+
if (current === void 0) return;
|
|
48
|
+
delete positions[current];
|
|
49
|
+
positions[current] = window.scrollY;
|
|
50
|
+
write();
|
|
51
|
+
};
|
|
52
|
+
const restoration = history.scrollRestoration;
|
|
53
|
+
history.scrollRestoration = "manual";
|
|
54
|
+
window.addEventListener("pagehide", save);
|
|
55
|
+
const to = (top) => window.scrollTo({ top, left: 0, behavior: "instant" });
|
|
56
|
+
const toHash = (hash) => {
|
|
57
|
+
if (!hash || hash === "#") return false;
|
|
58
|
+
const target = document.getElementById(decodeURIComponent(hash.slice(1)));
|
|
59
|
+
target?.scrollIntoView({ behavior: "instant" });
|
|
60
|
+
return target !== null;
|
|
61
|
+
};
|
|
62
|
+
return {
|
|
63
|
+
leave: save,
|
|
64
|
+
arrive(location, from) {
|
|
65
|
+
current = location.key;
|
|
66
|
+
if (location.action === "pop" || from === void 0) {
|
|
67
|
+
const y = location.key === void 0 ? void 0 : positions[location.key];
|
|
68
|
+
if (y !== void 0) to(y);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (location.action === "replace" && location.pathname === from.pathname) return;
|
|
72
|
+
if (!toHash(location.hash)) to(0);
|
|
73
|
+
},
|
|
74
|
+
dispose() {
|
|
75
|
+
window.removeEventListener("pagehide", save);
|
|
76
|
+
history.scrollRestoration = restoration;
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The window's scroll across client navigations, as a full page load would leave it: a new
|
|
3
|
+
* page starts at the top, or at its `#hash`; back and forward return to where the reader
|
|
4
|
+
* was; a reload stays where it was.
|
|
5
|
+
*
|
|
6
|
+
* The router keeps the current page on screen until the next one has loaded its data, so
|
|
7
|
+
* the browser's own restoration, applied when the URL changes, would scroll the page being
|
|
8
|
+
* left. The router turns it off and scrolls once the next page is shown.
|
|
9
|
+
*/
|
|
10
|
+
import type { RouterLocation } from '@fluixi/router';
|
|
11
|
+
export interface ScrollKeeper {
|
|
12
|
+
/** A navigation started: remember where the entry being left was scrolled to. */
|
|
13
|
+
leave(): void;
|
|
14
|
+
/** The page for `location` is on screen. */
|
|
15
|
+
arrive(location: RouterLocation, from: RouterLocation | undefined): void;
|
|
16
|
+
dispose(): void;
|
|
17
|
+
}
|
|
18
|
+
export declare function keepScroll(initial: RouterLocation): ScrollKeeper;
|
|
19
|
+
//# sourceMappingURL=scroll.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scroll.d.ts","sourceRoot":"","sources":["../../../../src/lib/router/scroll.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAMrD,MAAM,WAAW,YAAY;IAC3B,iFAAiF;IACjF,KAAK,IAAI,IAAI,CAAC;IACd,4CAA4C;IAC5C,MAAM,CAAC,QAAQ,EAAE,cAAc,EAAE,IAAI,EAAE,cAAc,GAAG,SAAS,GAAG,IAAI,CAAC;IACzE,OAAO,IAAI,IAAI,CAAC;CACjB;AAED,wBAAgB,UAAU,CAAC,OAAO,EAAE,cAAc,GAAG,YAAY,CA4DhE"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
const STORE = 'fx:scroll';
|
|
2
|
+
// Positions kept for this many entries, the most recent ones.
|
|
3
|
+
const KEEP = 100;
|
|
4
|
+
export function keepScroll(initial) {
|
|
5
|
+
const read = () => {
|
|
6
|
+
try {
|
|
7
|
+
return JSON.parse(sessionStorage.getItem(STORE) ?? '{}');
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
return {};
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
const positions = read();
|
|
14
|
+
const write = () => {
|
|
15
|
+
const keys = Object.keys(positions);
|
|
16
|
+
for (const key of keys.slice(0, Math.max(0, keys.length - KEEP)))
|
|
17
|
+
delete positions[key];
|
|
18
|
+
try {
|
|
19
|
+
sessionStorage.setItem(STORE, JSON.stringify(positions));
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
// Storage full or refused: positions still hold for this page's life.
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
let current = initial.key;
|
|
26
|
+
const save = () => {
|
|
27
|
+
if (current === undefined)
|
|
28
|
+
return;
|
|
29
|
+
delete positions[current];
|
|
30
|
+
positions[current] = window.scrollY;
|
|
31
|
+
write();
|
|
32
|
+
};
|
|
33
|
+
const restoration = history.scrollRestoration;
|
|
34
|
+
history.scrollRestoration = 'manual';
|
|
35
|
+
// A reload or a tab closed and reopened: the entry keeps its position.
|
|
36
|
+
window.addEventListener('pagehide', save);
|
|
37
|
+
// Instant, as a page load lands, whatever `scroll-behavior` the page's CSS sets: a smooth
|
|
38
|
+
// scroll would animate from where the last page was.
|
|
39
|
+
const to = (top) => window.scrollTo({ top, left: 0, behavior: 'instant' });
|
|
40
|
+
const toHash = (hash) => {
|
|
41
|
+
if (!hash || hash === '#')
|
|
42
|
+
return false;
|
|
43
|
+
const target = document.getElementById(decodeURIComponent(hash.slice(1)));
|
|
44
|
+
target?.scrollIntoView({ behavior: 'instant' });
|
|
45
|
+
return target !== null;
|
|
46
|
+
};
|
|
47
|
+
return {
|
|
48
|
+
leave: save,
|
|
49
|
+
arrive(location, from) {
|
|
50
|
+
current = location.key;
|
|
51
|
+
if (location.action === 'pop' || from === undefined) {
|
|
52
|
+
const y = location.key === undefined ? undefined : positions[location.key];
|
|
53
|
+
if (y !== undefined)
|
|
54
|
+
to(y);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
// A replace on the same page (a filter in the query) leaves the reader where they are.
|
|
58
|
+
if (location.action === 'replace' && location.pathname === from.pathname)
|
|
59
|
+
return;
|
|
60
|
+
if (!toHash(location.hash))
|
|
61
|
+
to(0);
|
|
62
|
+
},
|
|
63
|
+
dispose() {
|
|
64
|
+
window.removeEventListener('pagehide', save);
|
|
65
|
+
history.scrollRestoration = restoration;
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// src/lib/router/scroll.ts
|
|
2
|
+
var STORE = "fx:scroll";
|
|
3
|
+
var KEEP = 100;
|
|
4
|
+
function keepScroll(initial) {
|
|
5
|
+
const read = () => {
|
|
6
|
+
try {
|
|
7
|
+
return JSON.parse(sessionStorage.getItem(STORE) ?? "{}");
|
|
8
|
+
} catch {
|
|
9
|
+
return {};
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
const positions = read();
|
|
13
|
+
const write = () => {
|
|
14
|
+
const keys = Object.keys(positions);
|
|
15
|
+
for (const key of keys.slice(0, Math.max(0, keys.length - KEEP))) delete positions[key];
|
|
16
|
+
try {
|
|
17
|
+
sessionStorage.setItem(STORE, JSON.stringify(positions));
|
|
18
|
+
} catch {
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
let current = initial.key;
|
|
22
|
+
const save = () => {
|
|
23
|
+
if (current === void 0) return;
|
|
24
|
+
delete positions[current];
|
|
25
|
+
positions[current] = window.scrollY;
|
|
26
|
+
write();
|
|
27
|
+
};
|
|
28
|
+
const restoration = history.scrollRestoration;
|
|
29
|
+
history.scrollRestoration = "manual";
|
|
30
|
+
window.addEventListener("pagehide", save);
|
|
31
|
+
const to = (top) => window.scrollTo({ top, left: 0, behavior: "instant" });
|
|
32
|
+
const toHash = (hash) => {
|
|
33
|
+
if (!hash || hash === "#") return false;
|
|
34
|
+
const target = document.getElementById(decodeURIComponent(hash.slice(1)));
|
|
35
|
+
target?.scrollIntoView({ behavior: "instant" });
|
|
36
|
+
return target !== null;
|
|
37
|
+
};
|
|
38
|
+
return {
|
|
39
|
+
leave: save,
|
|
40
|
+
arrive(location, from) {
|
|
41
|
+
current = location.key;
|
|
42
|
+
if (location.action === "pop" || from === void 0) {
|
|
43
|
+
const y = location.key === void 0 ? void 0 : positions[location.key];
|
|
44
|
+
if (y !== void 0) to(y);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (location.action === "replace" && location.pathname === from.pathname) return;
|
|
48
|
+
if (!toHash(location.hash)) to(0);
|
|
49
|
+
},
|
|
50
|
+
dispose() {
|
|
51
|
+
window.removeEventListener("pagehide", save);
|
|
52
|
+
history.scrollRestoration = restoration;
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
export {
|
|
57
|
+
keepScroll
|
|
58
|
+
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! @fluixi/core v1.0.0-alpha.
|
|
1
|
+
/*! @fluixi/core v1.0.0-alpha.96 | (c) 2026 Ibrahima Touré and Fluixi contributors | MIT */
|
|
2
2
|
"use strict";
|
|
3
3
|
var __defProp = Object.defineProperty;
|
|
4
4
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! @fluixi/core v1.0.0-alpha.
|
|
1
|
+
/*! @fluixi/core v1.0.0-alpha.96 | (c) 2026 Ibrahima Touré and Fluixi contributors | MIT */
|
|
2
2
|
|
|
3
3
|
// src/lib/server/hydration.ts
|
|
4
4
|
import { isServer as domIsServer } from "@fluixi/dom";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! @fluixi/core v1.0.0-alpha.
|
|
1
|
+
/*! @fluixi/core v1.0.0-alpha.96 | (c) 2026 Ibrahima Touré and Fluixi contributors | MIT */
|
|
2
2
|
"use strict";
|
|
3
3
|
var __defProp = Object.defineProperty;
|
|
4
4
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! @fluixi/core v1.0.0-alpha.
|
|
1
|
+
/*! @fluixi/core v1.0.0-alpha.96 | (c) 2026 Ibrahima Touré and Fluixi contributors | MIT */
|
|
2
2
|
|
|
3
3
|
// src/lib/storage/index.ts
|
|
4
4
|
import { appendResponseHeader, getRequestContext, getServerData } from "@fluixi/dom";
|
package/dist/src/routes.cjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! @fluixi/core v1.0.0-alpha.
|
|
1
|
+
/*! @fluixi/core v1.0.0-alpha.96 | (c) 2026 Ibrahima Touré and Fluixi contributors | MIT */
|
|
2
2
|
"use strict";
|
|
3
3
|
var __defProp = Object.defineProperty;
|
|
4
4
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|