@cajoa/dom 0.10.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +220 -38
- package/dist/index.d.cts +38 -7
- package/dist/index.d.ts +38 -7
- package/dist/index.js +220 -40
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -21,12 +21,17 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
Link: () => Link,
|
|
24
|
+
NavLink: () => NavLink,
|
|
25
|
+
Navigate: () => Navigate,
|
|
26
|
+
Outlet: () => Outlet,
|
|
24
27
|
Route: () => Route,
|
|
25
28
|
Router: () => Router,
|
|
26
29
|
domRenderer: () => domRenderer,
|
|
27
30
|
navigate: () => navigate,
|
|
31
|
+
useLocation: () => useLocation,
|
|
28
32
|
useParams: () => useParams,
|
|
29
|
-
usePath: () => usePath
|
|
33
|
+
usePath: () => usePath,
|
|
34
|
+
useSearchParams: () => useSearchParams
|
|
30
35
|
});
|
|
31
36
|
module.exports = __toCommonJS(index_exports);
|
|
32
37
|
var import_core2 = require("@cajoa/core");
|
|
@@ -130,8 +135,8 @@ function isEvent(el, key) {
|
|
|
130
135
|
return /^on[A-Z]/.test(key) || key.startsWith("on") && key in el;
|
|
131
136
|
}
|
|
132
137
|
function setEvent(el, key, value) {
|
|
133
|
-
const
|
|
134
|
-
const name = EVENT_NAMES[
|
|
138
|
+
const raw2 = key.slice(2).toLowerCase();
|
|
139
|
+
const name = EVENT_NAMES[raw2] ?? raw2;
|
|
135
140
|
let byName = listeners.get(el);
|
|
136
141
|
if (!byName) {
|
|
137
142
|
byName = /* @__PURE__ */ new Map();
|
|
@@ -217,23 +222,106 @@ var domRenderer = {
|
|
|
217
222
|
|
|
218
223
|
// src/router.ts
|
|
219
224
|
var import_core = require("@cajoa/core");
|
|
220
|
-
var
|
|
221
|
-
var
|
|
225
|
+
var inBrowser = typeof window !== "undefined";
|
|
226
|
+
var readRaw = () => inBrowser ? {
|
|
227
|
+
pathname: location.pathname,
|
|
228
|
+
search: location.search,
|
|
229
|
+
hash: location.hash
|
|
230
|
+
} : { pathname: "/", search: "", hash: "" };
|
|
231
|
+
var [base, setBase] = (0, import_core.createSignal)("");
|
|
232
|
+
var [raw, setRaw] = (0, import_core.createSignal)(readRaw());
|
|
233
|
+
function normalizeBase(value) {
|
|
234
|
+
const trimmed = (value ?? "").replace(/^\/+|\/+$/g, "");
|
|
235
|
+
return trimmed ? "/" + trimmed : "";
|
|
236
|
+
}
|
|
237
|
+
function stripBase(pathname, prefix) {
|
|
238
|
+
if (!prefix) return pathname;
|
|
239
|
+
if (pathname === prefix) return "/";
|
|
240
|
+
return pathname.startsWith(prefix + "/") ? pathname.slice(prefix.length) : pathname;
|
|
241
|
+
}
|
|
242
|
+
function withBase(to) {
|
|
243
|
+
const prefix = base();
|
|
244
|
+
return prefix && to.startsWith("/") && !to.startsWith("//") ? prefix + to : to;
|
|
245
|
+
}
|
|
246
|
+
var location$ = (0, import_core.createMemo)(() => {
|
|
247
|
+
const r = raw();
|
|
248
|
+
return {
|
|
249
|
+
pathname: stripBase(r.pathname, base()),
|
|
250
|
+
search: r.search,
|
|
251
|
+
hash: r.hash
|
|
252
|
+
};
|
|
253
|
+
});
|
|
254
|
+
var pathname$ = (0, import_core.createMemo)(() => location$().pathname);
|
|
255
|
+
var useLocation = () => location$;
|
|
256
|
+
var usePath = () => pathname$;
|
|
257
|
+
function sync() {
|
|
258
|
+
if (!inBrowser) return;
|
|
259
|
+
const next = readRaw();
|
|
260
|
+
const prev = (0, import_core.untrack)(raw);
|
|
261
|
+
if (next.pathname !== prev.pathname || next.search !== prev.search || next.hash !== prev.hash) {
|
|
262
|
+
setRaw(next);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
222
265
|
var listening = false;
|
|
223
266
|
function listen() {
|
|
224
|
-
if (listening ||
|
|
267
|
+
if (listening || !inBrowser) return;
|
|
225
268
|
listening = true;
|
|
226
|
-
window.addEventListener("popstate",
|
|
269
|
+
window.addEventListener("popstate", sync);
|
|
227
270
|
}
|
|
228
271
|
function navigate(to, options = {}) {
|
|
229
|
-
if (
|
|
230
|
-
|
|
231
|
-
|
|
272
|
+
if (!inBrowser) return;
|
|
273
|
+
const url = new URL(
|
|
274
|
+
(0, import_core.untrack)(() => withBase(to)),
|
|
275
|
+
location.href
|
|
276
|
+
);
|
|
277
|
+
if (url.origin !== location.origin) {
|
|
278
|
+
location.assign(url.href);
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
if (url.href === location.href) {
|
|
282
|
+
sync();
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
const pathChanged = url.pathname !== location.pathname;
|
|
286
|
+
history[options.replace ? "replaceState" : "pushState"](
|
|
287
|
+
null,
|
|
288
|
+
"",
|
|
289
|
+
url.pathname + url.search + url.hash
|
|
290
|
+
);
|
|
291
|
+
sync();
|
|
292
|
+
if (options.scroll !== false) restoreScroll(url.hash, pathChanged);
|
|
232
293
|
}
|
|
233
|
-
|
|
234
|
-
|
|
294
|
+
function restoreScroll(hash, pathChanged) {
|
|
295
|
+
if (hash) {
|
|
296
|
+
const target = document.getElementById(decode(hash.slice(1)));
|
|
297
|
+
if (target) {
|
|
298
|
+
target.scrollIntoView?.();
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
if (pathChanged) window.scrollTo(0, 0);
|
|
303
|
+
}
|
|
304
|
+
function useSearchParams() {
|
|
305
|
+
const params = () => new URLSearchParams(location$().search);
|
|
306
|
+
const setParams = (values, options = {}) => {
|
|
307
|
+
if (!inBrowser) return;
|
|
308
|
+
const next = new URLSearchParams(location.search);
|
|
309
|
+
for (const [key, value] of Object.entries(values)) {
|
|
310
|
+
if (value === null || value === void 0) next.delete(key);
|
|
311
|
+
else next.set(key, String(value));
|
|
312
|
+
}
|
|
313
|
+
const search = next.toString();
|
|
314
|
+
const { pathname, hash } = (0, import_core.untrack)(location$);
|
|
315
|
+
navigate(pathname + (search ? "?" + search : "") + hash, {
|
|
316
|
+
replace: options.replace,
|
|
317
|
+
scroll: false
|
|
318
|
+
});
|
|
319
|
+
};
|
|
320
|
+
return [params, setParams];
|
|
321
|
+
}
|
|
322
|
+
var ParamsContext = (0, import_core.createContext)({});
|
|
235
323
|
function useParams() {
|
|
236
|
-
return (0, import_core.useContext)(
|
|
324
|
+
return (0, import_core.useContext)(ParamsContext);
|
|
237
325
|
}
|
|
238
326
|
var split = (s) => s.split("/").filter(Boolean);
|
|
239
327
|
function decode(s) {
|
|
@@ -243,47 +331,136 @@ function decode(s) {
|
|
|
243
331
|
return s;
|
|
244
332
|
}
|
|
245
333
|
}
|
|
246
|
-
function
|
|
247
|
-
if (pattern === "*") return {};
|
|
248
|
-
const p = split(pattern);
|
|
249
|
-
const u = split(pathname);
|
|
250
|
-
if (p.length !== u.length) return null;
|
|
334
|
+
function matchSegments(pattern, segments) {
|
|
251
335
|
const params = {};
|
|
252
|
-
for (let i = 0; i <
|
|
253
|
-
|
|
254
|
-
|
|
336
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
337
|
+
const part = pattern[i];
|
|
338
|
+
if (part === "*") {
|
|
339
|
+
params["*"] = segments.slice(i).map(decode).join("/");
|
|
340
|
+
return { params, rest: [] };
|
|
341
|
+
}
|
|
342
|
+
if (i >= segments.length) return null;
|
|
343
|
+
if (part.startsWith(":")) params[part.slice(1)] = decode(segments[i]);
|
|
344
|
+
else if (part !== decode(segments[i])) return null;
|
|
255
345
|
}
|
|
256
|
-
return params;
|
|
346
|
+
return { params, rest: segments.slice(pattern.length) };
|
|
257
347
|
}
|
|
348
|
+
var nextRouteId = 1;
|
|
349
|
+
var isRoute = (value) => typeof value === "object" && value !== null && value.__route === true;
|
|
350
|
+
var collectRoutes = (children) => [children].flat(Infinity).filter(isRoute);
|
|
258
351
|
function Route(props) {
|
|
259
|
-
return {
|
|
352
|
+
return {
|
|
353
|
+
__route: true,
|
|
354
|
+
id: nextRouteId++,
|
|
355
|
+
path: props.path,
|
|
356
|
+
component: props.component,
|
|
357
|
+
children: collectRoutes(props.children)
|
|
358
|
+
};
|
|
260
359
|
}
|
|
261
|
-
function
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
(
|
|
265
|
-
|
|
360
|
+
function matchRoutes(routes, segments, inherited) {
|
|
361
|
+
for (const route of routes) {
|
|
362
|
+
const hit = matchSegments(split(route.path), segments);
|
|
363
|
+
if (!hit) continue;
|
|
364
|
+
const params = { ...inherited, ...hit.params };
|
|
365
|
+
if (route.children.length === 0) {
|
|
366
|
+
if (hit.rest.length === 0) return [{ route, params }];
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
const inner = matchRoutes(route.children, hit.rest, params);
|
|
370
|
+
if (inner) return [{ route, params }, ...inner];
|
|
371
|
+
}
|
|
372
|
+
return null;
|
|
373
|
+
}
|
|
374
|
+
var OutletContext = (0, import_core.createContext)(null);
|
|
375
|
+
function Outlet() {
|
|
376
|
+
const render = (0, import_core.useContext)(OutletContext);
|
|
377
|
+
return render ? render() : null;
|
|
378
|
+
}
|
|
379
|
+
function renderLevel(matches, level) {
|
|
380
|
+
const key = (0, import_core.createMemo)(() => {
|
|
381
|
+
const match = matches()[level];
|
|
382
|
+
return match ? `${match.route.id}:${JSON.stringify(match.params)}` : "";
|
|
383
|
+
});
|
|
266
384
|
return () => {
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
385
|
+
key();
|
|
386
|
+
const match = (0, import_core.untrack)(() => matches()[level]);
|
|
387
|
+
if (!match) return null;
|
|
388
|
+
const component = match.route.component;
|
|
389
|
+
if (!component) {
|
|
390
|
+
return (0, import_core.provide)(
|
|
391
|
+
ParamsContext,
|
|
392
|
+
match.params,
|
|
393
|
+
() => renderLevel(matches, level + 1)()
|
|
394
|
+
);
|
|
273
395
|
}
|
|
274
|
-
return
|
|
396
|
+
return (0, import_core.untrack)(
|
|
397
|
+
() => (0, import_core.provide)(
|
|
398
|
+
ParamsContext,
|
|
399
|
+
match.params,
|
|
400
|
+
() => (0, import_core.provide)(
|
|
401
|
+
OutletContext,
|
|
402
|
+
() => renderLevel(matches, level + 1),
|
|
403
|
+
() => component()
|
|
404
|
+
)
|
|
405
|
+
)
|
|
406
|
+
);
|
|
275
407
|
};
|
|
276
408
|
}
|
|
409
|
+
function Router(props) {
|
|
410
|
+
setBase(normalizeBase(props.base));
|
|
411
|
+
(0, import_core.onCleanup)(() => setBase(""));
|
|
412
|
+
listen();
|
|
413
|
+
sync();
|
|
414
|
+
const routes = collectRoutes(props.children);
|
|
415
|
+
const matches = (0, import_core.createMemo)(
|
|
416
|
+
() => matchRoutes(routes, split(pathname$()), {}) ?? []
|
|
417
|
+
);
|
|
418
|
+
return renderLevel(matches, 0);
|
|
419
|
+
}
|
|
277
420
|
function Link(props) {
|
|
278
421
|
const { href, children, ...rest } = props;
|
|
422
|
+
const external = /^[a-z][a-z0-9+.-]*:|^\/\//i.test(href);
|
|
279
423
|
const onClick = (e) => {
|
|
280
|
-
if (
|
|
424
|
+
if (external) return;
|
|
281
425
|
if (e.defaultPrevented || e.button !== 0) return;
|
|
282
426
|
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
|
|
427
|
+
const target = rest.target;
|
|
428
|
+
if (target && target !== "_self" || rest.download !== void 0) return;
|
|
283
429
|
e.preventDefault();
|
|
284
430
|
navigate(href);
|
|
285
431
|
};
|
|
286
|
-
return (0, import_core.h)(
|
|
432
|
+
return (0, import_core.h)(
|
|
433
|
+
"a",
|
|
434
|
+
{ ...rest, href: external ? href : () => withBase(href), onClick },
|
|
435
|
+
children
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
function isActive(current, href, end) {
|
|
439
|
+
const target = split(href.split(/[?#]/)[0]);
|
|
440
|
+
const here = split(current);
|
|
441
|
+
if (end && target.length !== here.length) return false;
|
|
442
|
+
return target.length <= here.length && target.every((segment, i) => segment === here[i]);
|
|
443
|
+
}
|
|
444
|
+
function NavLink(props) {
|
|
445
|
+
const {
|
|
446
|
+
href,
|
|
447
|
+
activeClass = "active",
|
|
448
|
+
end = false,
|
|
449
|
+
class: cls,
|
|
450
|
+
children,
|
|
451
|
+
...rest
|
|
452
|
+
} = props;
|
|
453
|
+
const active = () => isActive(pathname$(), href, end);
|
|
454
|
+
return Link({
|
|
455
|
+
...rest,
|
|
456
|
+
href,
|
|
457
|
+
children,
|
|
458
|
+
class: () => [cls, active() ? activeClass : ""].filter(Boolean).join(" ")
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
function Navigate(props) {
|
|
462
|
+
(0, import_core.onMount)(() => navigate(props.to, { replace: props.replace ?? true }));
|
|
463
|
+
return null;
|
|
287
464
|
}
|
|
288
465
|
|
|
289
466
|
// src/index.ts
|
|
@@ -291,10 +468,15 @@ function Link(props) {
|
|
|
291
468
|
// Annotate the CommonJS export names for ESM import in node:
|
|
292
469
|
0 && (module.exports = {
|
|
293
470
|
Link,
|
|
471
|
+
NavLink,
|
|
472
|
+
Navigate,
|
|
473
|
+
Outlet,
|
|
294
474
|
Route,
|
|
295
475
|
Router,
|
|
296
476
|
domRenderer,
|
|
297
477
|
navigate,
|
|
478
|
+
useLocation,
|
|
298
479
|
useParams,
|
|
299
|
-
usePath
|
|
480
|
+
usePath,
|
|
481
|
+
useSearchParams
|
|
300
482
|
});
|
package/dist/index.d.cts
CHANGED
|
@@ -2,21 +2,40 @@ import { Renderer } from '@cajoa/core';
|
|
|
2
2
|
|
|
3
3
|
declare const domRenderer: Renderer<Node>;
|
|
4
4
|
|
|
5
|
-
declare
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
declare const useLocation: () => () => {
|
|
6
|
+
pathname: string;
|
|
7
|
+
search: string;
|
|
8
|
+
hash: string;
|
|
9
|
+
};
|
|
8
10
|
declare const usePath: () => () => string;
|
|
11
|
+
interface NavigateOptions {
|
|
12
|
+
replace?: boolean;
|
|
13
|
+
scroll?: boolean;
|
|
14
|
+
}
|
|
15
|
+
declare function navigate(to: string, options?: NavigateOptions): void;
|
|
16
|
+
type SearchParamValue = string | number | boolean | null | undefined;
|
|
17
|
+
declare function useSearchParams(): [
|
|
18
|
+
() => URLSearchParams,
|
|
19
|
+
(values: Record<string, SearchParamValue>, options?: {
|
|
20
|
+
replace?: boolean;
|
|
21
|
+
}) => void
|
|
22
|
+
];
|
|
9
23
|
declare function useParams(): Record<string, string>;
|
|
10
24
|
interface RouteDef {
|
|
11
25
|
__route: true;
|
|
26
|
+
id: number;
|
|
12
27
|
path: string;
|
|
13
|
-
component
|
|
28
|
+
component?: () => unknown;
|
|
29
|
+
children: RouteDef[];
|
|
14
30
|
}
|
|
15
31
|
declare function Route(props: {
|
|
16
32
|
path: string;
|
|
17
|
-
component
|
|
33
|
+
component?: () => unknown;
|
|
34
|
+
children?: unknown;
|
|
18
35
|
}): RouteDef;
|
|
36
|
+
declare function Outlet(): unknown;
|
|
19
37
|
declare function Router(props: {
|
|
38
|
+
base?: string;
|
|
20
39
|
children?: unknown;
|
|
21
40
|
}): () => unknown;
|
|
22
41
|
declare function Link(props: {
|
|
@@ -24,6 +43,18 @@ declare function Link(props: {
|
|
|
24
43
|
children?: unknown;
|
|
25
44
|
[key: string]: unknown;
|
|
26
45
|
}): object;
|
|
46
|
+
declare function NavLink(props: {
|
|
47
|
+
href: string;
|
|
48
|
+
activeClass?: string;
|
|
49
|
+
end?: boolean;
|
|
50
|
+
class?: string;
|
|
51
|
+
children?: unknown;
|
|
52
|
+
[key: string]: unknown;
|
|
53
|
+
}): object;
|
|
54
|
+
declare function Navigate(props: {
|
|
55
|
+
to: string;
|
|
56
|
+
replace?: boolean;
|
|
57
|
+
}): null;
|
|
27
58
|
|
|
28
|
-
export { Link, Route, Router, domRenderer, navigate, useParams, usePath };
|
|
29
|
-
export type { RouteDef };
|
|
59
|
+
export { Link, NavLink, Navigate, Outlet, Route, Router, domRenderer, navigate, useLocation, useParams, usePath, useSearchParams };
|
|
60
|
+
export type { NavigateOptions, RouteDef, SearchParamValue };
|
package/dist/index.d.ts
CHANGED
|
@@ -2,21 +2,40 @@ import { Renderer } from '@cajoa/core';
|
|
|
2
2
|
|
|
3
3
|
declare const domRenderer: Renderer<Node>;
|
|
4
4
|
|
|
5
|
-
declare
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
declare const useLocation: () => () => {
|
|
6
|
+
pathname: string;
|
|
7
|
+
search: string;
|
|
8
|
+
hash: string;
|
|
9
|
+
};
|
|
8
10
|
declare const usePath: () => () => string;
|
|
11
|
+
interface NavigateOptions {
|
|
12
|
+
replace?: boolean;
|
|
13
|
+
scroll?: boolean;
|
|
14
|
+
}
|
|
15
|
+
declare function navigate(to: string, options?: NavigateOptions): void;
|
|
16
|
+
type SearchParamValue = string | number | boolean | null | undefined;
|
|
17
|
+
declare function useSearchParams(): [
|
|
18
|
+
() => URLSearchParams,
|
|
19
|
+
(values: Record<string, SearchParamValue>, options?: {
|
|
20
|
+
replace?: boolean;
|
|
21
|
+
}) => void
|
|
22
|
+
];
|
|
9
23
|
declare function useParams(): Record<string, string>;
|
|
10
24
|
interface RouteDef {
|
|
11
25
|
__route: true;
|
|
26
|
+
id: number;
|
|
12
27
|
path: string;
|
|
13
|
-
component
|
|
28
|
+
component?: () => unknown;
|
|
29
|
+
children: RouteDef[];
|
|
14
30
|
}
|
|
15
31
|
declare function Route(props: {
|
|
16
32
|
path: string;
|
|
17
|
-
component
|
|
33
|
+
component?: () => unknown;
|
|
34
|
+
children?: unknown;
|
|
18
35
|
}): RouteDef;
|
|
36
|
+
declare function Outlet(): unknown;
|
|
19
37
|
declare function Router(props: {
|
|
38
|
+
base?: string;
|
|
20
39
|
children?: unknown;
|
|
21
40
|
}): () => unknown;
|
|
22
41
|
declare function Link(props: {
|
|
@@ -24,6 +43,18 @@ declare function Link(props: {
|
|
|
24
43
|
children?: unknown;
|
|
25
44
|
[key: string]: unknown;
|
|
26
45
|
}): object;
|
|
46
|
+
declare function NavLink(props: {
|
|
47
|
+
href: string;
|
|
48
|
+
activeClass?: string;
|
|
49
|
+
end?: boolean;
|
|
50
|
+
class?: string;
|
|
51
|
+
children?: unknown;
|
|
52
|
+
[key: string]: unknown;
|
|
53
|
+
}): object;
|
|
54
|
+
declare function Navigate(props: {
|
|
55
|
+
to: string;
|
|
56
|
+
replace?: boolean;
|
|
57
|
+
}): null;
|
|
27
58
|
|
|
28
|
-
export { Link, Route, Router, domRenderer, navigate, useParams, usePath };
|
|
29
|
-
export type { RouteDef };
|
|
59
|
+
export { Link, NavLink, Navigate, Outlet, Route, Router, domRenderer, navigate, useLocation, useParams, usePath, useSearchParams };
|
|
60
|
+
export type { NavigateOptions, RouteDef, SearchParamValue };
|
package/dist/index.js
CHANGED
|
@@ -100,8 +100,8 @@ function isEvent(el, key) {
|
|
|
100
100
|
return /^on[A-Z]/.test(key) || key.startsWith("on") && key in el;
|
|
101
101
|
}
|
|
102
102
|
function setEvent(el, key, value) {
|
|
103
|
-
const
|
|
104
|
-
const name = EVENT_NAMES[
|
|
103
|
+
const raw2 = key.slice(2).toLowerCase();
|
|
104
|
+
const name = EVENT_NAMES[raw2] ?? raw2;
|
|
105
105
|
let byName = listeners.get(el);
|
|
106
106
|
if (!byName) {
|
|
107
107
|
byName = /* @__PURE__ */ new Map();
|
|
@@ -187,30 +187,116 @@ var domRenderer = {
|
|
|
187
187
|
|
|
188
188
|
// src/router.ts
|
|
189
189
|
import {
|
|
190
|
-
createSignal,
|
|
191
190
|
createContext,
|
|
191
|
+
createMemo,
|
|
192
|
+
createSignal,
|
|
193
|
+
h,
|
|
194
|
+
onCleanup,
|
|
195
|
+
onMount,
|
|
192
196
|
provide,
|
|
193
|
-
useContext,
|
|
194
197
|
untrack,
|
|
195
|
-
|
|
198
|
+
useContext
|
|
196
199
|
} from "@cajoa/core";
|
|
197
|
-
var
|
|
198
|
-
var
|
|
200
|
+
var inBrowser = typeof window !== "undefined";
|
|
201
|
+
var readRaw = () => inBrowser ? {
|
|
202
|
+
pathname: location.pathname,
|
|
203
|
+
search: location.search,
|
|
204
|
+
hash: location.hash
|
|
205
|
+
} : { pathname: "/", search: "", hash: "" };
|
|
206
|
+
var [base, setBase] = createSignal("");
|
|
207
|
+
var [raw, setRaw] = createSignal(readRaw());
|
|
208
|
+
function normalizeBase(value) {
|
|
209
|
+
const trimmed = (value ?? "").replace(/^\/+|\/+$/g, "");
|
|
210
|
+
return trimmed ? "/" + trimmed : "";
|
|
211
|
+
}
|
|
212
|
+
function stripBase(pathname, prefix) {
|
|
213
|
+
if (!prefix) return pathname;
|
|
214
|
+
if (pathname === prefix) return "/";
|
|
215
|
+
return pathname.startsWith(prefix + "/") ? pathname.slice(prefix.length) : pathname;
|
|
216
|
+
}
|
|
217
|
+
function withBase(to) {
|
|
218
|
+
const prefix = base();
|
|
219
|
+
return prefix && to.startsWith("/") && !to.startsWith("//") ? prefix + to : to;
|
|
220
|
+
}
|
|
221
|
+
var location$ = createMemo(() => {
|
|
222
|
+
const r = raw();
|
|
223
|
+
return {
|
|
224
|
+
pathname: stripBase(r.pathname, base()),
|
|
225
|
+
search: r.search,
|
|
226
|
+
hash: r.hash
|
|
227
|
+
};
|
|
228
|
+
});
|
|
229
|
+
var pathname$ = createMemo(() => location$().pathname);
|
|
230
|
+
var useLocation = () => location$;
|
|
231
|
+
var usePath = () => pathname$;
|
|
232
|
+
function sync() {
|
|
233
|
+
if (!inBrowser) return;
|
|
234
|
+
const next = readRaw();
|
|
235
|
+
const prev = untrack(raw);
|
|
236
|
+
if (next.pathname !== prev.pathname || next.search !== prev.search || next.hash !== prev.hash) {
|
|
237
|
+
setRaw(next);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
199
240
|
var listening = false;
|
|
200
241
|
function listen() {
|
|
201
|
-
if (listening ||
|
|
242
|
+
if (listening || !inBrowser) return;
|
|
202
243
|
listening = true;
|
|
203
|
-
window.addEventListener("popstate",
|
|
244
|
+
window.addEventListener("popstate", sync);
|
|
204
245
|
}
|
|
205
246
|
function navigate(to, options = {}) {
|
|
206
|
-
if (
|
|
207
|
-
|
|
208
|
-
|
|
247
|
+
if (!inBrowser) return;
|
|
248
|
+
const url = new URL(
|
|
249
|
+
untrack(() => withBase(to)),
|
|
250
|
+
location.href
|
|
251
|
+
);
|
|
252
|
+
if (url.origin !== location.origin) {
|
|
253
|
+
location.assign(url.href);
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
if (url.href === location.href) {
|
|
257
|
+
sync();
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
const pathChanged = url.pathname !== location.pathname;
|
|
261
|
+
history[options.replace ? "replaceState" : "pushState"](
|
|
262
|
+
null,
|
|
263
|
+
"",
|
|
264
|
+
url.pathname + url.search + url.hash
|
|
265
|
+
);
|
|
266
|
+
sync();
|
|
267
|
+
if (options.scroll !== false) restoreScroll(url.hash, pathChanged);
|
|
268
|
+
}
|
|
269
|
+
function restoreScroll(hash, pathChanged) {
|
|
270
|
+
if (hash) {
|
|
271
|
+
const target = document.getElementById(decode(hash.slice(1)));
|
|
272
|
+
if (target) {
|
|
273
|
+
target.scrollIntoView?.();
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
if (pathChanged) window.scrollTo(0, 0);
|
|
278
|
+
}
|
|
279
|
+
function useSearchParams() {
|
|
280
|
+
const params = () => new URLSearchParams(location$().search);
|
|
281
|
+
const setParams = (values, options = {}) => {
|
|
282
|
+
if (!inBrowser) return;
|
|
283
|
+
const next = new URLSearchParams(location.search);
|
|
284
|
+
for (const [key, value] of Object.entries(values)) {
|
|
285
|
+
if (value === null || value === void 0) next.delete(key);
|
|
286
|
+
else next.set(key, String(value));
|
|
287
|
+
}
|
|
288
|
+
const search = next.toString();
|
|
289
|
+
const { pathname, hash } = untrack(location$);
|
|
290
|
+
navigate(pathname + (search ? "?" + search : "") + hash, {
|
|
291
|
+
replace: options.replace,
|
|
292
|
+
scroll: false
|
|
293
|
+
});
|
|
294
|
+
};
|
|
295
|
+
return [params, setParams];
|
|
209
296
|
}
|
|
210
|
-
var
|
|
211
|
-
var Params = createContext({});
|
|
297
|
+
var ParamsContext = createContext({});
|
|
212
298
|
function useParams() {
|
|
213
|
-
return useContext(
|
|
299
|
+
return useContext(ParamsContext);
|
|
214
300
|
}
|
|
215
301
|
var split = (s) => s.split("/").filter(Boolean);
|
|
216
302
|
function decode(s) {
|
|
@@ -220,57 +306,151 @@ function decode(s) {
|
|
|
220
306
|
return s;
|
|
221
307
|
}
|
|
222
308
|
}
|
|
223
|
-
function
|
|
224
|
-
if (pattern === "*") return {};
|
|
225
|
-
const p = split(pattern);
|
|
226
|
-
const u = split(pathname);
|
|
227
|
-
if (p.length !== u.length) return null;
|
|
309
|
+
function matchSegments(pattern, segments) {
|
|
228
310
|
const params = {};
|
|
229
|
-
for (let i = 0; i <
|
|
230
|
-
|
|
231
|
-
|
|
311
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
312
|
+
const part = pattern[i];
|
|
313
|
+
if (part === "*") {
|
|
314
|
+
params["*"] = segments.slice(i).map(decode).join("/");
|
|
315
|
+
return { params, rest: [] };
|
|
316
|
+
}
|
|
317
|
+
if (i >= segments.length) return null;
|
|
318
|
+
if (part.startsWith(":")) params[part.slice(1)] = decode(segments[i]);
|
|
319
|
+
else if (part !== decode(segments[i])) return null;
|
|
232
320
|
}
|
|
233
|
-
return params;
|
|
321
|
+
return { params, rest: segments.slice(pattern.length) };
|
|
234
322
|
}
|
|
323
|
+
var nextRouteId = 1;
|
|
324
|
+
var isRoute = (value) => typeof value === "object" && value !== null && value.__route === true;
|
|
325
|
+
var collectRoutes = (children) => [children].flat(Infinity).filter(isRoute);
|
|
235
326
|
function Route(props) {
|
|
236
|
-
return {
|
|
327
|
+
return {
|
|
328
|
+
__route: true,
|
|
329
|
+
id: nextRouteId++,
|
|
330
|
+
path: props.path,
|
|
331
|
+
component: props.component,
|
|
332
|
+
children: collectRoutes(props.children)
|
|
333
|
+
};
|
|
237
334
|
}
|
|
238
|
-
function
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
(
|
|
242
|
-
|
|
335
|
+
function matchRoutes(routes, segments, inherited) {
|
|
336
|
+
for (const route of routes) {
|
|
337
|
+
const hit = matchSegments(split(route.path), segments);
|
|
338
|
+
if (!hit) continue;
|
|
339
|
+
const params = { ...inherited, ...hit.params };
|
|
340
|
+
if (route.children.length === 0) {
|
|
341
|
+
if (hit.rest.length === 0) return [{ route, params }];
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
const inner = matchRoutes(route.children, hit.rest, params);
|
|
345
|
+
if (inner) return [{ route, params }, ...inner];
|
|
346
|
+
}
|
|
347
|
+
return null;
|
|
348
|
+
}
|
|
349
|
+
var OutletContext = createContext(null);
|
|
350
|
+
function Outlet() {
|
|
351
|
+
const render = useContext(OutletContext);
|
|
352
|
+
return render ? render() : null;
|
|
353
|
+
}
|
|
354
|
+
function renderLevel(matches, level) {
|
|
355
|
+
const key = createMemo(() => {
|
|
356
|
+
const match = matches()[level];
|
|
357
|
+
return match ? `${match.route.id}:${JSON.stringify(match.params)}` : "";
|
|
358
|
+
});
|
|
243
359
|
return () => {
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
360
|
+
key();
|
|
361
|
+
const match = untrack(() => matches()[level]);
|
|
362
|
+
if (!match) return null;
|
|
363
|
+
const component = match.route.component;
|
|
364
|
+
if (!component) {
|
|
365
|
+
return provide(
|
|
366
|
+
ParamsContext,
|
|
367
|
+
match.params,
|
|
368
|
+
() => renderLevel(matches, level + 1)()
|
|
369
|
+
);
|
|
250
370
|
}
|
|
251
|
-
return
|
|
371
|
+
return untrack(
|
|
372
|
+
() => provide(
|
|
373
|
+
ParamsContext,
|
|
374
|
+
match.params,
|
|
375
|
+
() => provide(
|
|
376
|
+
OutletContext,
|
|
377
|
+
() => renderLevel(matches, level + 1),
|
|
378
|
+
() => component()
|
|
379
|
+
)
|
|
380
|
+
)
|
|
381
|
+
);
|
|
252
382
|
};
|
|
253
383
|
}
|
|
384
|
+
function Router(props) {
|
|
385
|
+
setBase(normalizeBase(props.base));
|
|
386
|
+
onCleanup(() => setBase(""));
|
|
387
|
+
listen();
|
|
388
|
+
sync();
|
|
389
|
+
const routes = collectRoutes(props.children);
|
|
390
|
+
const matches = createMemo(
|
|
391
|
+
() => matchRoutes(routes, split(pathname$()), {}) ?? []
|
|
392
|
+
);
|
|
393
|
+
return renderLevel(matches, 0);
|
|
394
|
+
}
|
|
254
395
|
function Link(props) {
|
|
255
396
|
const { href, children, ...rest } = props;
|
|
397
|
+
const external = /^[a-z][a-z0-9+.-]*:|^\/\//i.test(href);
|
|
256
398
|
const onClick = (e) => {
|
|
257
|
-
if (
|
|
399
|
+
if (external) return;
|
|
258
400
|
if (e.defaultPrevented || e.button !== 0) return;
|
|
259
401
|
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
|
|
402
|
+
const target = rest.target;
|
|
403
|
+
if (target && target !== "_self" || rest.download !== void 0) return;
|
|
260
404
|
e.preventDefault();
|
|
261
405
|
navigate(href);
|
|
262
406
|
};
|
|
263
|
-
return h(
|
|
407
|
+
return h(
|
|
408
|
+
"a",
|
|
409
|
+
{ ...rest, href: external ? href : () => withBase(href), onClick },
|
|
410
|
+
children
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
function isActive(current, href, end) {
|
|
414
|
+
const target = split(href.split(/[?#]/)[0]);
|
|
415
|
+
const here = split(current);
|
|
416
|
+
if (end && target.length !== here.length) return false;
|
|
417
|
+
return target.length <= here.length && target.every((segment, i) => segment === here[i]);
|
|
418
|
+
}
|
|
419
|
+
function NavLink(props) {
|
|
420
|
+
const {
|
|
421
|
+
href,
|
|
422
|
+
activeClass = "active",
|
|
423
|
+
end = false,
|
|
424
|
+
class: cls,
|
|
425
|
+
children,
|
|
426
|
+
...rest
|
|
427
|
+
} = props;
|
|
428
|
+
const active = () => isActive(pathname$(), href, end);
|
|
429
|
+
return Link({
|
|
430
|
+
...rest,
|
|
431
|
+
href,
|
|
432
|
+
children,
|
|
433
|
+
class: () => [cls, active() ? activeClass : ""].filter(Boolean).join(" ")
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
function Navigate(props) {
|
|
437
|
+
onMount(() => navigate(props.to, { replace: props.replace ?? true }));
|
|
438
|
+
return null;
|
|
264
439
|
}
|
|
265
440
|
|
|
266
441
|
// src/index.ts
|
|
267
442
|
setRenderer(domRenderer);
|
|
268
443
|
export {
|
|
269
444
|
Link,
|
|
445
|
+
NavLink,
|
|
446
|
+
Navigate,
|
|
447
|
+
Outlet,
|
|
270
448
|
Route,
|
|
271
449
|
Router,
|
|
272
450
|
domRenderer,
|
|
273
451
|
navigate,
|
|
452
|
+
useLocation,
|
|
274
453
|
useParams,
|
|
275
|
-
usePath
|
|
454
|
+
usePath,
|
|
455
|
+
useSearchParams
|
|
276
456
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cajoa/dom",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "Cajoa for the browser: DOM renderer and router.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
],
|
|
27
27
|
"sideEffects": true,
|
|
28
28
|
"peerDependencies": {
|
|
29
|
-
"@cajoa/core": "^0.
|
|
29
|
+
"@cajoa/core": "^0.11.0"
|
|
30
30
|
},
|
|
31
31
|
"scripts": {
|
|
32
32
|
"build": "tsup src/index.ts --format esm,cjs --dts --clean",
|