@cajoa/dom 0.10.1 → 0.12.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 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 raw = key.slice(2).toLowerCase();
134
- const name = EVENT_NAMES[raw] ?? raw;
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 read = () => typeof location === "undefined" ? "/" : location.pathname;
221
- var [path, setPath] = (0, import_core.createSignal)(read());
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 || typeof window === "undefined") return;
267
+ if (listening || !inBrowser) return;
225
268
  listening = true;
226
- window.addEventListener("popstate", () => setPath(read()));
269
+ window.addEventListener("popstate", sync);
227
270
  }
228
271
  function navigate(to, options = {}) {
229
- if (new URL(to, location.href).href === location.href) return;
230
- history[options.replace ? "replaceState" : "pushState"](null, "", to);
231
- setPath(read());
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
- var usePath = () => path;
234
- var Params = (0, import_core.createContext)({});
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)(Params);
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 match(pattern, pathname) {
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 < p.length; i++) {
253
- if (p[i].startsWith(":")) params[p[i].slice(1)] = decode(u[i]);
254
- else if (p[i] !== u[i]) return null;
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 { __route: true, path: props.path, component: props.component };
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 Router(props) {
262
- listen();
263
- const routes = [props.children].flat(Infinity).filter(
264
- (r) => r?.__route === true
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
- const current = path();
268
- for (const route of routes) {
269
- const params = match(route.path, current);
270
- if (params) {
271
- return (0, import_core.untrack)(() => (0, import_core.provide)(Params, params, () => route.component()));
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 null;
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 (/^[a-z][a-z0-9+.-]*:|^\/\//i.test(href)) return;
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)("a", { ...rest, href, onClick }, children);
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 function navigate(to: string, options?: {
6
- replace?: boolean;
7
- }): void;
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: () => unknown;
28
+ component?: () => unknown;
29
+ children: RouteDef[];
14
30
  }
15
31
  declare function Route(props: {
16
32
  path: string;
17
- component: () => unknown;
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 function navigate(to: string, options?: {
6
- replace?: boolean;
7
- }): void;
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: () => unknown;
28
+ component?: () => unknown;
29
+ children: RouteDef[];
14
30
  }
15
31
  declare function Route(props: {
16
32
  path: string;
17
- component: () => unknown;
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 raw = key.slice(2).toLowerCase();
104
- const name = EVENT_NAMES[raw] ?? raw;
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
- h
198
+ useContext
196
199
  } from "@cajoa/core";
197
- var read = () => typeof location === "undefined" ? "/" : location.pathname;
198
- var [path, setPath] = createSignal(read());
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 || typeof window === "undefined") return;
242
+ if (listening || !inBrowser) return;
202
243
  listening = true;
203
- window.addEventListener("popstate", () => setPath(read()));
244
+ window.addEventListener("popstate", sync);
204
245
  }
205
246
  function navigate(to, options = {}) {
206
- if (new URL(to, location.href).href === location.href) return;
207
- history[options.replace ? "replaceState" : "pushState"](null, "", to);
208
- setPath(read());
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 usePath = () => path;
211
- var Params = createContext({});
297
+ var ParamsContext = createContext({});
212
298
  function useParams() {
213
- return useContext(Params);
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 match(pattern, pathname) {
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 < p.length; i++) {
230
- if (p[i].startsWith(":")) params[p[i].slice(1)] = decode(u[i]);
231
- else if (p[i] !== u[i]) return null;
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 { __route: true, path: props.path, component: props.component };
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 Router(props) {
239
- listen();
240
- const routes = [props.children].flat(Infinity).filter(
241
- (r) => r?.__route === true
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
- const current = path();
245
- for (const route of routes) {
246
- const params = match(route.path, current);
247
- if (params) {
248
- return untrack(() => provide(Params, params, () => route.component()));
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 null;
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 (/^[a-z][a-z0-9+.-]*:|^\/\//i.test(href)) return;
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("a", { ...rest, href, onClick }, children);
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.10.1",
3
+ "version": "0.12.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.10.1"
29
+ "@cajoa/core": "^0.12.0"
30
30
  },
31
31
  "scripts": {
32
32
  "build": "tsup src/index.ts --format esm,cjs --dts --clean",