@reely/router 0.1.0 → 0.2.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/README.md +6 -6
- package/dist/index.d.ts +10 -0
- package/dist/index.js +25 -9
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -103,7 +103,7 @@ navigate(href(paths.message, { id: '43' }));
|
|
|
103
103
|
navigate('/login?next=/account', { replace: true });
|
|
104
104
|
```
|
|
105
105
|
|
|
106
|
-
- The router takes over a plain left click on a link of this site. A click with a modifier key or another button, a link with `target="_blank"` or `download`, a link to another site, and a link to a place on the page shown stay the browser's.
|
|
106
|
+
- The router takes over a plain left click on a link of this site. A click with a modifier key or another button, a link with `target="_blank"` or `download`, a link marked `rel="external"` (a file or a page the server answers), a link to another site, and a link to a place on the page shown stay the browser's.
|
|
107
107
|
- A new query (`?sort=price`) shows the page again; a new `#place` on the page shown only scrolls to it.
|
|
108
108
|
- `navigate` to the URL already shown adds no history entry.
|
|
109
109
|
|
|
@@ -165,7 +165,7 @@ effect(() => {
|
|
|
165
165
|
});
|
|
166
166
|
```
|
|
167
167
|
|
|
168
|
-
Only the latest move counts: a page still loading when the reader clicks another link is dropped. `currentPath()`
|
|
168
|
+
Only the latest move counts: a page still loading when the reader clicks another link is dropped. `currentPath()` and `pageLoading()` read the same for the browser's router from anywhere in the app, without the router at hand: a progress bar in the header needs only `pageLoading()`.
|
|
169
169
|
|
|
170
170
|
## Scroll and focus
|
|
171
171
|
|
|
@@ -177,7 +177,7 @@ The browser's router opens each page the way the reader expects:
|
|
|
177
177
|
|
|
178
178
|
## A router inside part of the page
|
|
179
179
|
|
|
180
|
-
A widget, a dialog or a help panel can have pages of its own and leave the address bar alone. `memoryHistory(start)` keeps the address in memory; `followLinks(root, navigate)` hands it the plain clicks on the links inside `root`, each read against its own address (so `href="7"` on `/orders/3` goes to `/orders/7`):
|
|
180
|
+
A widget, a dialog or a help panel can have pages of its own and leave the address bar alone. `memoryHistory(start)` keeps the address in memory; `followLinks(root, navigate)` hands it the plain clicks on the links inside `root`, each read against its own address (so `href="7"` on `/orders/3` goes to `/orders/7`). It keeps no Back stack, so `replace` changes nothing there:
|
|
181
181
|
|
|
182
182
|
```ts
|
|
183
183
|
import { defineRoutes, followLinks, href, memoryHistory, startRouter } from '@reely/router';
|
|
@@ -208,7 +208,7 @@ startRouter(
|
|
|
208
208
|
);
|
|
209
209
|
|
|
210
210
|
help.navigate(href(paths.topic, { slug: 'returns' })); // from code, the same way
|
|
211
|
-
console.log(help.path()); // the path shown, read like
|
|
211
|
+
console.log(help.path(), help.loading()); // the path shown and the next page loading, read like signals
|
|
212
212
|
```
|
|
213
213
|
|
|
214
214
|
After a move, the page's `h1` gets the focus, or its first heading of any level when it has none: a widget's pages start at `h2`.
|
|
@@ -291,8 +291,8 @@ export const HelpWidget = (): Node => {
|
|
|
291
291
|
- `startRouter(routes, { show, fail, history })`: shows the page of the current address and keeps following it; returns `{ path, loading, navigate, stop }`.
|
|
292
292
|
- `href(pattern, params)`: a URL from a pattern, typed and encoded.
|
|
293
293
|
- `navigate(to, { replace })`: goes to a URL of this site in the browser.
|
|
294
|
-
- `currentPath()`: the path the browser's router shows, read like
|
|
295
|
-
- `memoryHistory(start)`: a history in memory, for a router inside part of the page; `path()` and `navigate()` on it.
|
|
294
|
+
- `currentPath()` and `pageLoading()`: the path the browser's router shows and whether the next page is loading, read like signals.
|
|
295
|
+
- `memoryHistory(start)`: a history in memory, for a router inside part of the page; `path()`, `loading()` and `navigate()` on it.
|
|
296
296
|
- `followLinks(root, navigate)`: sends the plain clicks on the links inside `root` to `navigate`; returns the stop.
|
|
297
297
|
- Types: `Routes`, `RouteTable`, `ParamsOf`, `HrefParams`, `PageOf`, `Router`, `RouterOptions`, `RouterHistory`, `NavigateOptions`.
|
|
298
298
|
|
package/dist/index.d.ts
CHANGED
|
@@ -47,6 +47,12 @@ export declare interface NavigateOptions {
|
|
|
47
47
|
|
|
48
48
|
declare type NoParams = Record<never, string>;
|
|
49
49
|
|
|
50
|
+
/**
|
|
51
|
+
* Whether the browser's router is loading the next page, read like a signal: the page shown stays
|
|
52
|
+
* until it has loaded, so a progress bar anywhere in the app says one is coming.
|
|
53
|
+
*/
|
|
54
|
+
export declare const pageLoading: () => boolean;
|
|
55
|
+
|
|
50
56
|
/** The pages a route table answers with, loaded. */
|
|
51
57
|
export declare type PageOf<T> = NonNullable<Awaited<T[keyof T]>>;
|
|
52
58
|
|
|
@@ -73,6 +79,10 @@ export declare interface RouterHistory {
|
|
|
73
79
|
url: () => URL;
|
|
74
80
|
/** The path of the page shown, read like a signal. */
|
|
75
81
|
path: () => string;
|
|
82
|
+
/** Whether the next page is loading, read like a signal. */
|
|
83
|
+
loading: () => boolean;
|
|
84
|
+
/** The router says a page for the address now starts loading, or that loading is over. */
|
|
85
|
+
loads: (loading: boolean) => void;
|
|
76
86
|
/** Goes to an address, as a click on a link to it does. */
|
|
77
87
|
navigate: (to: string | URL, options?: NavigateOptions) => void;
|
|
78
88
|
/** Calls `moved` after each move until the stop it returns. */
|
package/dist/index.js
CHANGED
|
@@ -11,6 +11,16 @@ var currentPath = () => shownPath().value;
|
|
|
11
11
|
var showPath = (path) => {
|
|
12
12
|
shownPath().value = path;
|
|
13
13
|
};
|
|
14
|
+
var loading;
|
|
15
|
+
var loadingPage = () => loading ??= signal(false);
|
|
16
|
+
/**
|
|
17
|
+
* Whether the browser's router is loading the next page, read like a signal: the page shown stays
|
|
18
|
+
* until it has loaded, so a progress bar anywhere in the app says one is coming.
|
|
19
|
+
*/
|
|
20
|
+
var pageLoading = () => loadingPage().value;
|
|
21
|
+
var showLoading = (value) => {
|
|
22
|
+
loadingPage().value = value;
|
|
23
|
+
};
|
|
14
24
|
//#endregion
|
|
15
25
|
//#region src/lib/router.href.ts
|
|
16
26
|
function href(pattern, params = {}) {
|
|
@@ -54,7 +64,7 @@ function hasProperty(property, source) {
|
|
|
54
64
|
//#region src/lib/router.links.ts
|
|
55
65
|
var linkOf = (target) => isInstanceOf(Element, target) ? target.closest("a") : null;
|
|
56
66
|
var isPlainClick = (event) => event.button === 0 && !event.defaultPrevented && !(event.altKey || event.ctrlKey || event.metaKey || event.shiftKey);
|
|
57
|
-
var opensHere = (link) => (link.target === "" || link.target === "_self") && !link.hasAttribute("download") && link.origin === location.origin;
|
|
67
|
+
var opensHere = (link) => (link.target === "" || link.target === "_self") && !link.hasAttribute("download") && !link.relList.contains("external") && link.origin === location.origin;
|
|
58
68
|
var takeOverLinks = (root, follow, signal) => {
|
|
59
69
|
root.addEventListener("click", (event) => {
|
|
60
70
|
const link = isInstanceOf(MouseEvent, event) && isPlainClick(event) ? linkOf(event.target) : null;
|
|
@@ -110,10 +120,15 @@ var anyOrigin = "https://reely.invalid";
|
|
|
110
120
|
var memoryHistory = (start = "/") => {
|
|
111
121
|
let now = new URL(start, anyOrigin);
|
|
112
122
|
const shown = signal(now.pathname);
|
|
123
|
+
const loading = signal(false);
|
|
113
124
|
const movers = /* @__PURE__ */ new Set();
|
|
114
125
|
return {
|
|
115
126
|
url: () => new URL(now),
|
|
116
127
|
path: () => shown.value,
|
|
128
|
+
loading: () => loading.value,
|
|
129
|
+
loads: (value) => {
|
|
130
|
+
loading.value = value;
|
|
131
|
+
},
|
|
117
132
|
navigate: (to) => {
|
|
118
133
|
const next = new URL(to, now);
|
|
119
134
|
if (next.href !== now.href) {
|
|
@@ -218,6 +233,8 @@ var browserHistory = () => {
|
|
|
218
233
|
return {
|
|
219
234
|
url: () => new URL(location.href),
|
|
220
235
|
path: currentPath,
|
|
236
|
+
loading: pageLoading,
|
|
237
|
+
loads: showLoading,
|
|
221
238
|
navigate,
|
|
222
239
|
follow: (moved) => {
|
|
223
240
|
const listening = new AbortController();
|
|
@@ -272,7 +289,6 @@ var browserHistory = () => {
|
|
|
272
289
|
* page shown stays until the next one has loaded; only the latest move counts.
|
|
273
290
|
*/
|
|
274
291
|
var startRouter = (routes, { show, fail, history = browserHistory() }) => {
|
|
275
|
-
const loading = signal(true);
|
|
276
292
|
let latest = 0;
|
|
277
293
|
let requested = pageAddress(history.url());
|
|
278
294
|
let shown = "";
|
|
@@ -281,7 +297,7 @@ var startRouter = (routes, { show, fail, history = browserHistory() }) => {
|
|
|
281
297
|
const display = (page, url, moved) => {
|
|
282
298
|
shown = pageAddress(url);
|
|
283
299
|
batch(() => {
|
|
284
|
-
|
|
300
|
+
history.loads(false);
|
|
285
301
|
history.showing();
|
|
286
302
|
});
|
|
287
303
|
try {
|
|
@@ -294,11 +310,11 @@ var startRouter = (routes, { show, fail, history = browserHistory() }) => {
|
|
|
294
310
|
const load = (moved) => {
|
|
295
311
|
const turn = ++latest;
|
|
296
312
|
const url = history.url();
|
|
297
|
-
|
|
313
|
+
history.loads(true);
|
|
298
314
|
Promise.resolve().then(() => routes(pageAddress(url))).then((page) => page ?? Promise.reject(/* @__PURE__ */ new Error(`No route answers ${url.pathname}`))).then(identity, failure).then((page) => {
|
|
299
315
|
if (turn === latest) display(page, url, moved);
|
|
300
316
|
}).catch((error) => {
|
|
301
|
-
if (turn === latest)
|
|
317
|
+
if (turn === latest) history.loads(false);
|
|
302
318
|
reportUncaught(error);
|
|
303
319
|
});
|
|
304
320
|
};
|
|
@@ -307,7 +323,7 @@ var startRouter = (routes, { show, fail, history = browserHistory() }) => {
|
|
|
307
323
|
if (address === shown) {
|
|
308
324
|
latest += 1;
|
|
309
325
|
requested = address;
|
|
310
|
-
|
|
326
|
+
history.loads(false);
|
|
311
327
|
history.arrive(onScreen, true);
|
|
312
328
|
} else if (address !== requested) {
|
|
313
329
|
requested = address;
|
|
@@ -319,18 +335,18 @@ var startRouter = (routes, { show, fail, history = browserHistory() }) => {
|
|
|
319
335
|
unfollow?.();
|
|
320
336
|
unfollow = null;
|
|
321
337
|
latest += 1;
|
|
322
|
-
|
|
338
|
+
history.loads(false);
|
|
323
339
|
};
|
|
324
340
|
onCleanup(stop);
|
|
325
341
|
load(false);
|
|
326
342
|
return {
|
|
327
343
|
path: history.path,
|
|
328
|
-
loading:
|
|
344
|
+
loading: history.loading,
|
|
329
345
|
navigate: history.navigate,
|
|
330
346
|
stop
|
|
331
347
|
};
|
|
332
348
|
};
|
|
333
349
|
//#endregion
|
|
334
|
-
export { currentPath, defineRoutes, followLinks, href, memoryHistory, navigate, startRouter };
|
|
350
|
+
export { currentPath, defineRoutes, followLinks, href, memoryHistory, navigate, pageLoading, startRouter };
|
|
335
351
|
|
|
336
352
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../src/lib/router.current.ts","../src/lib/router.href.ts","../../utils/dist/lib/fp/identity.js","../../utils/dist/lib/objects/isNil.js","../../utils/dist/lib/objects/isInstanceOf.js","../../utils/dist/lib/objects/isValidRecordKey.js","../../utils/dist/lib/objects/isNumber.js","../../utils/dist/lib/objects/hasProperty.js","../src/lib/router.links.ts","../src/lib/router.focus.ts","../src/lib/router.history.ts","../src/lib/router.memory.ts","../src/lib/router.navigate.ts","../src/lib/router.decode.ts","../src/lib/router.routes.ts","../src/lib/router.scroll.ts","../src/lib/router.browser.ts","../src/lib/router.start.ts"],"sourcesContent":["import { signal } from '@reely/signals';\nimport type { Signal } from '@reely/signals';\n\n// one location per document, so one path shown; the signal is made on first use\nlet shown: Signal<string> | undefined;\n\nconst shownPath = (): Signal<string> => (shown ??= signal(location.pathname));\n\n/**\n * The path of the page the browser's router shows, read like a signal: a binding or effect that reads it\n * follows every move, as a menu's `aria-current` does.\n */\nexport const currentPath = (): string => shownPath().value;\n\n// the Router tells `currentPath` the path of each page it shows\nexport const showPath = (path: string): void => {\n shownPath().value = path;\n};\n","import type { ParamsOf } from './router.types';\n\n/** What `href` takes after the pattern: its params, or nothing for a pattern without any. */\nexport type HrefParams<P extends string> = keyof ParamsOf<P> extends never ? [] : [params: ParamsOf<P>];\n\n/**\n * The URL of a route: its pattern with the params filled in and encoded, typed from the pattern,\n * so a link and the route it opens share one path. `*rest` keeps its slashes.\n */\nexport function href<P extends string>(pattern: P, ...params: HrefParams<P>): string;\nexport function href(pattern: string, params: Readonly<Record<string, string>> = {}): string {\n return pattern\n .split('/')\n .map((part) => {\n const value = params[part.slice(1)] ?? '';\n if (part.startsWith(':')) {\n return encodeURIComponent(value);\n }\n return part.startsWith('*') ? value.split('/').map(encodeURIComponent).join('/') : part;\n })\n .join('/');\n}\n","export default function identity(v) {\n return v;\n}\n","export default function isNil(value) {\n return value === null || value === undefined;\n}\n","export default function isInstanceOf(instanceType, instance) {\n return instance instanceof instanceType;\n}\n","export default function isValidRecordKey(source) {\n return typeof source === 'string' || typeof source === 'number' || typeof source === 'symbol';\n}\n","export default function isNumber(source) {\n return typeof source === 'number' || source instanceof Number;\n}\n","import { hasSome, isSomeFunction } from '@reely/basics';\nimport isValidRecordKey from './isValidRecordKey';\nexport function hasProperty(property, source) {\n return (isValidRecordKey(property) &&\n (typeof source === 'object' || isSomeFunction(source)) &&\n hasSome(source) &&\n property in source);\n}\n","import { onCleanup } from '@reely/signals';\nimport { isInstanceOf, isNil } from '@reely/utils';\nimport type { Nullable } from '@reely/utils';\n\nconst linkOf = (target: Nullable<EventTarget>): Nullable<HTMLAnchorElement> =>\n isInstanceOf(Element, target) ? target.closest('a') : null;\n\n// a primary-button click without modifiers: the others open tabs and windows\nconst isPlainClick = (event: MouseEvent): boolean =>\n event.button === 0 && !event.defaultPrevented && !(event.altKey || event.ctrlKey || event.metaKey || event.shiftKey);\n\nconst opensHere = (link: HTMLAnchorElement): boolean =>\n (link.target === '' || link.target === '_self') && !link.hasAttribute('download') && link.origin === location.origin;\n\n// takes over the plain clicks on the links of this site inside `root` that `follow` takes: it\n// answers whether it went to the link's page, or leaves the click to the browser\nexport const takeOverLinks = (\n root: EventTarget,\n follow: (link: HTMLAnchorElement) => boolean,\n signal: AbortSignal\n): void => {\n root.addEventListener(\n 'click',\n (event) => {\n const link = isInstanceOf(MouseEvent, event) && isPlainClick(event) ? linkOf(event.target) : null;\n if (!isNil(link) && opensHere(link) && follow(link)) {\n event.preventDefault();\n }\n },\n { signal }\n );\n};\n\n/**\n * Sends the plain clicks on the links of this site inside `root` to `navigate`, with each link's own\n * `href`: what a `memoryHistory` needs to follow the links of its part of the page. Returns the stop;\n * called under an owner, it stops with it too.\n */\nexport const followLinks = (root: EventTarget, navigate: (to: string) => void): (() => void) => {\n const listening = new AbortController();\n const stop = (): void => listening.abort();\n takeOverLinks(\n root,\n (link) => {\n navigate(link.getAttribute('href') ?? '');\n return true;\n },\n listening.signal\n );\n onCleanup(stop);\n return stop;\n};\n","import { isInstanceOf } from '@reely/utils';\nimport type { Nullable } from '@reely/utils';\n\nimport type { Shown } from './router.history';\n\n\nconst firstOf = (shown: Shown, selector: string): Nullable<Element> => {\n for (const node of isInstanceOf(Node, shown) ? [shown] : shown) {\n const found = isInstanceOf(Element, node) && !node.matches(selector) ? node.querySelector(selector) : node;\n if (isInstanceOf(Element, found) && found.matches(selector)) {\n return found;\n }\n }\n return null;\n};\n\n/**\n * The heading of a page shown, not one the app shows around it: its `h1`, or its first heading of any\n * level when it has none (a widget's pages start at `h2`).\n */\nexport const headingIn = (shown: Shown): Nullable<Element> =>\n firstOf(shown, 'h1') ?? firstOf(shown, 'h1, h2, h3, h4, h5, h6');\n\n// a screen reader starts reading the new page there; the scroll is the router's to set\nexport const focusOn = (target: Nullable<Element>): void => {\n if (isInstanceOf(HTMLElement, target)) {\n if (!target.hasAttribute('tabindex')) {\n target.tabIndex = -1;\n }\n target.focus({ preventScroll: true });\n }\n};\n","import type { NavigateOptions } from './router.navigate';\n\n/** What `show` returns: the element a page is shown in, or the nodes it put on screen. */\nexport type Shown = Node | readonly Node[];\n\n/**\n * Where a router's addresses come from and how it moves between them: the browser's history, or\n * `memoryHistory` for a router inside a part of the page.\n */\nexport interface RouterHistory {\n /** The address now. */\n url: () => URL;\n /** The path of the page shown, read like a signal. */\n path: () => string;\n /** Goes to an address, as a click on a link to it does. */\n navigate: (to: string | URL, options?: NavigateOptions) => void;\n /** Calls `moved` after each move until the stop it returns. */\n follow: (moved: () => void) => () => void;\n /** Takes the address now for the page about to be shown, so the page reads its path while it renders. */\n showing: () => void;\n /** Brings the reader to the page just shown; `moved` is false for the first page. */\n arrive: (shown: Shown, moved: boolean) => void;\n}\n\n// routes answer a path and its query; a new query is a new page as well, a new place on it is not\nexport const pageAddress = ({ pathname, search }: URL | Location | HTMLAnchorElement): string => `${pathname}${search}`;\n\n/** Any address parses against it where only its path and query matter, never its origin. */\nexport const anyOrigin = 'https://reely.invalid';\n","import { forEachSettled } from '@reely/basics';\nimport { signal } from '@reely/signals';\n\nimport { focusOn, headingIn } from './router.focus';\nimport { anyOrigin } from './router.history';\n\nimport type { RouterHistory } from './router.history';\n\n/**\n * A history kept in memory, for a router inside part of the page (a widget, a dialog, a demo): it\n * starts at `start` and leaves the document's URL alone. It follows the links `followLinks` gives it.\n */\nexport const memoryHistory = (start = '/'): RouterHistory => {\n let now = new URL(start, anyOrigin);\n const shown = signal(now.pathname);\n const movers = new Set<() => void>();\n\n return {\n url: () => new URL(now),\n path: () => shown.value,\n navigate: (to): void => {\n const next = new URL(to, now);\n if (next.href !== now.href) {\n now = next;\n forEachSettled(movers, (moved) => moved());\n }\n },\n follow: (moved) => {\n movers.add(moved);\n return (): void => void movers.delete(moved);\n },\n showing: (): void => {\n shown.value = now.pathname;\n },\n arrive: (page, moved): void => {\n if (moved) {\n focusOn(headingIn(page));\n }\n },\n };\n};\n","// a move made by code: the page left is told first, then the new URL; `pushState` fires no `popstate`\nexport const leavingEvent = 'reely:leaving';\nexport const navigationEvent = 'reely:navigate';\n\nexport interface NavigateOptions {\n /** Takes the place of the current history entry, as a redirect does, instead of adding one. */\n replace?: boolean;\n}\n\n/** Goes to a URL of this site without loading the document; the router shows its page. */\nexport const navigate = (to: string | URL, { replace = false }: NavigateOptions = {}): void => {\n const url = new URL(to, location.href);\n if (url.href === location.href) {\n return;\n }\n if (replace) {\n history.replaceState(null, '', url);\n } else {\n window.dispatchEvent(new Event(leavingEvent));\n history.pushState(null, '', url);\n }\n window.dispatchEvent(new Event(navigationEvent));\n};\n","// a part of a URL decoded; one that is not valid percent-encoding stays as it came\nexport const decode = (value: string): string => {\n try {\n return decodeURIComponent(value);\n } catch {\n return value;\n }\n};\n","import { hasSome } from '@reely/basics';\nimport { isNil } from '@reely/utils';\n\nimport { decode } from './router.decode';\nimport { anyOrigin } from './router.history';\n\nimport type { ParamsOf, Routes } from './router.types';\n\ntype Params = Readonly<Record<string, string>>;\n\n/**\n * A route table: each path pattern with the function that answers it from the params the pattern\n * names and the address's query. An answer is a page, a promise of one (`import()`), or nothing to\n * pass the address on to the next route.\n */\nexport type RouteTable<T> = { [P in keyof T & string]: (params: ParamsOf<P>, query: URLSearchParams) => T[P] };\n\n/** The pages a route table answers with, loaded. */\nexport type PageOf<T> = NonNullable<Awaited<T[keyof T]>>;\n\nconst segmentsOf = (path: string): string[] => path.split('/').filter((segment) => segment !== '');\n\n// the params a pattern takes from a pathname's segments, or undefined when it does not match them\nconst matchSegments = (pattern: readonly string[], segments: readonly string[]): Params | undefined => {\n const params: Record<string, string> = {};\n for (const [index, part] of pattern.entries()) {\n if (part.startsWith('*')) {\n params[part.slice(1)] = decode(segments.slice(index).join('/'));\n return params;\n }\n const segment = segments[index];\n if (isNil(segment) || (!part.startsWith(':') && part !== segment)) {\n return undefined;\n }\n if (part.startsWith(':')) {\n params[part.slice(1)] = decode(segment);\n }\n }\n return segments.length === pattern.length ? params : undefined;\n};\n\n/**\n * Turns a table of path patterns into the app's routes: an address gets the page of the first route\n * that matches its path and answers. `:name` takes one segment, `*name` the rest of the path; a route\n * that answers nothing passes the address on to the next one.\n */\nexport function defineRoutes<T>(table: RouteTable<T>): Routes<PageOf<T>>;\nexport function defineRoutes(\n table: Readonly<Record<string, (params: Params, query: URLSearchParams) => unknown>>\n): Routes<unknown> {\n const routes = Object.entries(table).map(([pattern, answer]) => ({ pattern: segmentsOf(pattern), answer }));\n return async (address) => {\n const { pathname, searchParams } = new URL(address, anyOrigin);\n const segments = segmentsOf(pathname);\n for (const { pattern, answer } of routes) {\n const params = matchSegments(pattern, segments);\n const page: unknown = hasSome(params) ? await answer(params, searchParams) : undefined;\n if (hasSome(page)) {\n return page;\n }\n }\n return undefined;\n };\n}\n","import { isPlainObject } from '@reely/basics';\nimport { hasProperty, isNumber } from '@reely/utils';\nimport type { Nullable } from '@reely/utils';\n\nimport { decode } from './router.decode';\n\nconst scrollKey = 'reelyScroll';\n\n/** Where the reader had scrolled the page of the current history entry, kept in its state for a reload too. */\nexport const keptScroll = (): number | undefined => {\n const { state } = history;\n const scrolled = hasProperty(scrollKey, state) ? state[scrollKey] : undefined;\n return isNumber(scrolled) ? scrolled : undefined;\n};\n\n/** Keeps where the page of the current history entry is scrolled, and what else its state held. */\nexport const keepScroll = (): void => {\n history.replaceState({ ...(isPlainObject(history.state) ? history.state : {}), [scrollKey]: window.scrollY }, '');\n};\n\n/** The element the hash of the location names. */\nexport const placeInUrl = (): Nullable<HTMLElement> =>\n location.hash === '' ? null : document.getElementById(decode(location.hash.slice(1)));\n","import { hasSome } from '@reely/basics';\n\nimport { currentPath, showPath } from './router.current';\nimport { focusOn, headingIn } from './router.focus';\nimport { pageAddress } from './router.history';\nimport { takeOverLinks } from './router.links';\nimport { leavingEvent, navigate, navigationEvent } from './router.navigate';\nimport { keepScroll, keptScroll, placeInUrl } from './router.scroll';\n\nimport type { RouterHistory } from './router.history';\n\n/**\n * The browser's own history: links of this site anywhere in the document, back and forward, and\n * `navigate`; each page opens where the reader had scrolled it, at the place its URL names, or at\n * the top.\n */\nexport const browserHistory = (): RouterHistory => {\n let shown = '';\n\n return {\n url: () => new URL(location.href),\n path: currentPath,\n navigate,\n follow: (moved) => {\n const listening = new AbortController();\n const { signal } = listening;\n const { scrollRestoration } = history;\n let saving: ReturnType<typeof setTimeout> | undefined;\n // the page shown keeps where it is scrolled, for a reload and for the way back to it\n const saveScroll = (): void => {\n if (pageAddress(location) === shown) {\n keepScroll();\n }\n };\n // a link to a place on the page shown is the browser's to follow\n takeOverLinks(\n document,\n (link) => {\n if (pageAddress(link) === shown && link.hash !== '') {\n return false;\n }\n navigate(link.href);\n return true;\n },\n signal\n );\n window.addEventListener('popstate', moved, { signal });\n window.addEventListener(navigationEvent, moved, { signal });\n window.addEventListener(\n 'scroll',\n () => {\n clearTimeout(saving);\n saving = setTimeout(saveScroll, 100);\n },\n { signal, passive: true }\n );\n window.addEventListener('pagehide', saveScroll, { signal });\n window.addEventListener(leavingEvent, saveScroll, { signal });\n // pages load after a move, so the browser's own restoring would scroll the page before it is there\n history.scrollRestoration = 'manual';\n return () => {\n listening.abort();\n clearTimeout(saving);\n history.scrollRestoration = scrollRestoration;\n };\n },\n showing: (): void => {\n shown = pageAddress(location);\n showPath(location.pathname);\n },\n arrive: (page, moved): void => {\n const place = placeInUrl();\n const scrolled = keptScroll();\n if (hasSome(scrolled)) {\n window.scrollTo(0, scrolled);\n } else if (hasSome(place)) {\n place.scrollIntoView();\n } else if (moved) {\n window.scrollTo(0, 0);\n }\n focusOn(place ?? (moved ? headingIn(page) : null));\n },\n };\n};\n","import { reportUncaught, toErrorWithMessage } from '@reely/basics';\nimport { batch, onCleanup, signal } from '@reely/signals';\nimport { identity } from '@reely/utils';\nimport type { Nullable } from '@reely/utils';\n\nimport { browserHistory } from './router.browser';\nimport { pageAddress } from './router.history';\n\nimport type { RouterHistory, Shown } from './router.history';\nimport type { NavigateOptions } from './router.navigate';\nimport type { Routes } from './router.types';\n\nexport interface RouterOptions<Page> {\n /** Puts a page on screen; returns the element it is shown in, or its nodes, whose heading a move focuses. */\n show: (page: Page) => Shown;\n /** The page for what went wrong: a route that fails or fails to load, an address no route answers, a page that fails to show. */\n fail: (error: Error) => Page;\n /** Where the addresses come from: the browser's history when left out, or a `memoryHistory`. */\n history?: RouterHistory;\n}\n\nexport interface Router {\n /** The path of the page shown, read like a signal: an effect that reads it follows every move. */\n path: () => string;\n /** Whether a page is loading, read like a signal; the page shown stays until it has loaded. */\n loading: () => boolean;\n /** Goes to an address of this router, as a click on a link to it does. */\n navigate: (to: string | URL, options?: NavigateOptions) => void;\n /** Stops following moves; a router started under an owner stops with it too. */\n stop: () => void;\n}\n\n/**\n * Shows the page of the current address and keeps following it: a click on a link of this site, the\n * browser's back and forward, and `navigate` show the next page without loading the document. The\n * page shown stays until the next one has loaded; only the latest move counts.\n */\nexport const startRouter = <Page>(\n routes: Routes<Page>,\n { show, fail, history = browserHistory() }: RouterOptions<NoInfer<Page>>\n): Router => {\n const loading = signal(true);\n let latest = 0;\n let requested = pageAddress(history.url());\n let shown = '';\n let onScreen: Shown = [];\n\n const failure = (error: unknown): Page => fail(toErrorWithMessage(error));\n\n const display = (page: Page, url: URL, moved: boolean): void => {\n shown = pageAddress(url);\n batch(() => {\n loading.value = false;\n history.showing();\n });\n try {\n onScreen = show(page);\n } catch (error) {\n onScreen = show(failure(error));\n }\n history.arrive(onScreen, moved);\n };\n\n const load = (moved: boolean): void => {\n const turn = ++latest;\n const url = history.url();\n loading.value = true;\n // a route that throws rejects like one whose page fails to load\n Promise.resolve()\n .then(() => routes(pageAddress(url)))\n .then((page) => page ?? Promise.reject(new Error(`No route answers ${url.pathname}`)))\n .then(identity, failure)\n .then((page) => {\n if (turn === latest) {\n display(page, url, moved);\n }\n })\n .catch((error: unknown) => {\n // even the failure page failed: nothing more is coming for this move\n if (turn === latest) {\n loading.value = false;\n }\n reportUncaught(error);\n });\n };\n\n const move = (): void => {\n const address = pageAddress(history.url());\n if (address === shown) {\n // back on the page shown, or a new place on it: a load still pending for another is dropped\n latest += 1;\n requested = address;\n loading.value = false;\n history.arrive(onScreen, true);\n } else if (address !== requested) {\n requested = address;\n load(true);\n }\n };\n\n let unfollow: Nullable<() => void> = history.follow(move);\n const stop = (): void => {\n unfollow?.();\n unfollow = null;\n latest += 1;\n loading.value = false;\n };\n onCleanup(stop);\n load(false);\n\n return { path: history.path, loading: () => loading.value, navigate: history.navigate, stop };\n};\n"],"mappings":";;;AAIA,IAAI;AAEJ,IAAM,kBAAmC,UAAU,OAAO,SAAS,QAAQ;;;;;AAM3E,IAAa,oBAA4B,UAAU,CAAC,CAAC;AAGrD,IAAa,YAAY,SAAuB;CAC9C,UAAU,CAAC,CAAC,QAAQ;AACtB;;;ACPA,SAAgB,KAAK,SAAiB,SAA2C,CAAC,GAAW;CAC3F,OAAO,QACJ,MAAM,GAAG,CAAC,CACV,KAAK,SAAS;EACb,MAAM,QAAQ,OAAO,KAAK,MAAM,CAAC,MAAM;EACvC,IAAI,KAAK,WAAW,GAAG,GACrB,OAAO,mBAAmB,KAAK;EAEjC,OAAO,KAAK,WAAW,GAAG,IAAI,MAAM,MAAM,GAAG,CAAC,CAAC,IAAI,kBAAkB,CAAC,CAAC,KAAK,GAAG,IAAI;CACrF,CAAC,CAAC,CACD,KAAK,GAAG;AACb;;;ACrBA,SAAwB,SAAS,GAAG;CAChC,OAAO;AACX;;;ACFA,SAAwB,MAAM,OAAO;CACjC,OAAO,UAAU,QAAQ,UAAU,KAAA;AACvC;;;ACFA,SAAwB,aAAa,cAAc,UAAU;CACzD,OAAO,oBAAoB;AAC/B;;;ACFA,SAAwB,iBAAiB,QAAQ;CAC7C,OAAO,OAAO,WAAW,YAAY,OAAO,WAAW,YAAY,OAAO,WAAW;AACzF;;;ACFA,SAAwB,SAAS,QAAQ;CACrC,OAAO,OAAO,WAAW,YAAY,kBAAkB;AAC3D;;;ACAA,SAAgB,YAAY,UAAU,QAAQ;CAC1C,OAAQ,iBAAiB,QAAQ,MAC5B,OAAO,WAAW,YAAY,eAAe,MAAM,MACpD,QAAQ,MAAM,KACd,YAAY;AACpB;;;ACHA,IAAM,UAAU,WACd,aAAa,SAAS,MAAM,IAAI,OAAO,QAAQ,GAAG,IAAI;AAGxD,IAAM,gBAAgB,UACpB,MAAM,WAAW,KAAK,CAAC,MAAM,oBAAoB,EAAE,MAAM,UAAU,MAAM,WAAW,MAAM,WAAW,MAAM;AAE7G,IAAM,aAAa,UAChB,KAAK,WAAW,MAAM,KAAK,WAAW,YAAY,CAAC,KAAK,aAAa,UAAU,KAAK,KAAK,WAAW,SAAS;AAIhH,IAAa,iBACX,MACA,QACA,WACS;CACT,KAAK,iBACH,UACC,UAAU;EACT,MAAM,OAAO,aAAa,YAAY,KAAK,KAAK,aAAa,KAAK,IAAI,OAAO,MAAM,MAAM,IAAI;EAC7F,IAAI,CAAC,MAAM,IAAI,KAAK,UAAU,IAAI,KAAK,OAAO,IAAI,GAChD,MAAM,eAAe;CAEzB,GACA,EAAE,OAAO,CACX;AACF;;;;;;AAOA,IAAa,eAAe,MAAmB,aAAiD;CAC9F,MAAM,YAAY,IAAI,gBAAgB;CACtC,MAAM,aAAmB,UAAU,MAAM;CACzC,cACE,OACC,SAAS;EACR,SAAS,KAAK,aAAa,MAAM,KAAK,EAAE;EACxC,OAAO;CACT,GACA,UAAU,MACZ;CACA,UAAU,IAAI;CACd,OAAO;AACT;;;AC7CA,IAAM,WAAW,OAAc,aAAwC;CACrE,KAAK,MAAM,QAAQ,aAAa,MAAM,KAAK,IAAI,CAAC,KAAK,IAAI,OAAO;EAC9D,MAAM,QAAQ,aAAa,SAAS,IAAI,KAAK,CAAC,KAAK,QAAQ,QAAQ,IAAI,KAAK,cAAc,QAAQ,IAAI;EACtG,IAAI,aAAa,SAAS,KAAK,KAAK,MAAM,QAAQ,QAAQ,GACxD,OAAO;CAEX;CACA,OAAO;AACT;;;;;AAMA,IAAa,aAAa,UACxB,QAAQ,OAAO,IAAI,KAAK,QAAQ,OAAO,wBAAwB;AAGjE,IAAa,WAAW,WAAoC;CAC1D,IAAI,aAAa,aAAa,MAAM,GAAG;EACrC,IAAI,CAAC,OAAO,aAAa,UAAU,GACjC,OAAO,WAAW;EAEpB,OAAO,MAAM,EAAE,eAAe,KAAK,CAAC;CACtC;AACF;;;ACNA,IAAa,eAAe,EAAE,UAAU,aAAyD,GAAG,WAAW;;AAG/G,IAAa,YAAY;;;;;;;AChBzB,IAAa,iBAAiB,QAAQ,QAAuB;CAC3D,IAAI,MAAM,IAAI,IAAI,OAAO,SAAS;CAClC,MAAM,QAAQ,OAAO,IAAI,QAAQ;CACjC,MAAM,yBAAS,IAAI,IAAgB;CAEnC,OAAO;EACL,WAAW,IAAI,IAAI,GAAG;EACtB,YAAY,MAAM;EAClB,WAAW,OAAa;GACtB,MAAM,OAAO,IAAI,IAAI,IAAI,GAAG;GAC5B,IAAI,KAAK,SAAS,IAAI,MAAM;IAC1B,MAAM;IACN,eAAe,SAAS,UAAU,MAAM,CAAC;GAC3C;EACF;EACA,SAAS,UAAU;GACjB,OAAO,IAAI,KAAK;GAChB,aAAmB,KAAK,OAAO,OAAO,KAAK;EAC7C;EACA,eAAqB;GACnB,MAAM,QAAQ,IAAI;EACpB;EACA,SAAS,MAAM,UAAgB;GAC7B,IAAI,OACF,QAAQ,UAAU,IAAI,CAAC;EAE3B;CACF;AACF;;;ACvCA,IAAa,eAAe;AAC5B,IAAa,kBAAkB;;AAQ/B,IAAa,YAAY,IAAkB,EAAE,UAAU,UAA2B,CAAC,MAAY;CAC7F,MAAM,MAAM,IAAI,IAAI,IAAI,SAAS,IAAI;CACrC,IAAI,IAAI,SAAS,SAAS,MACxB;CAEF,IAAI,SACF,QAAQ,aAAa,MAAM,IAAI,GAAG;MAC7B;EACL,OAAO,cAAc,IAAI,MAAM,YAAY,CAAC;EAC5C,QAAQ,UAAU,MAAM,IAAI,GAAG;CACjC;CACA,OAAO,cAAc,IAAI,MAAM,eAAe,CAAC;AACjD;;;ACrBA,IAAa,UAAU,UAA0B;CAC/C,IAAI;EACF,OAAO,mBAAmB,KAAK;CACjC,QAAQ;EACN,OAAO;CACT;AACF;;;ACaA,IAAM,cAAc,SAA2B,KAAK,MAAM,GAAG,CAAC,CAAC,QAAQ,YAAY,YAAY,EAAE;AAGjG,IAAM,iBAAiB,SAA4B,aAAoD;CACrG,MAAM,SAAiC,CAAC;CACxC,KAAK,MAAM,CAAC,OAAO,SAAS,QAAQ,QAAQ,GAAG;EAC7C,IAAI,KAAK,WAAW,GAAG,GAAG;GACxB,OAAO,KAAK,MAAM,CAAC,KAAK,OAAO,SAAS,MAAM,KAAK,CAAC,CAAC,KAAK,GAAG,CAAC;GAC9D,OAAO;EACT;EACA,MAAM,UAAU,SAAS;EACzB,IAAI,MAAM,OAAO,KAAM,CAAC,KAAK,WAAW,GAAG,KAAK,SAAS,SACvD;EAEF,IAAI,KAAK,WAAW,GAAG,GACrB,OAAO,KAAK,MAAM,CAAC,KAAK,OAAO,OAAO;CAE1C;CACA,OAAO,SAAS,WAAW,QAAQ,SAAS,SAAS,KAAA;AACvD;AAQA,SAAgB,aACd,OACiB;CACjB,MAAM,SAAS,OAAO,QAAQ,KAAK,CAAC,CAAC,KAAK,CAAC,SAAS,aAAa;EAAE,SAAS,WAAW,OAAO;EAAG;CAAO,EAAE;CAC1G,OAAO,OAAO,YAAY;EACxB,MAAM,EAAE,UAAU,iBAAiB,IAAI,IAAI,SAAS,SAAS;EAC7D,MAAM,WAAW,WAAW,QAAQ;EACpC,KAAK,MAAM,EAAE,SAAS,YAAY,QAAQ;GACxC,MAAM,SAAS,cAAc,SAAS,QAAQ;GAC9C,MAAM,OAAgB,QAAQ,MAAM,IAAI,MAAM,OAAO,QAAQ,YAAY,IAAI,KAAA;GAC7E,IAAI,QAAQ,IAAI,GACd,OAAO;EAEX;CAEF;AACF;;;ACzDA,IAAM,YAAY;;AAGlB,IAAa,mBAAuC;CAClD,MAAM,EAAE,UAAU;CAClB,MAAM,WAAW,YAAY,WAAW,KAAK,IAAI,MAAM,aAAa,KAAA;CACpE,OAAO,SAAS,QAAQ,IAAI,WAAW,KAAA;AACzC;;AAGA,IAAa,mBAAyB;CACpC,QAAQ,aAAa;EAAE,GAAI,cAAc,QAAQ,KAAK,IAAI,QAAQ,QAAQ,CAAC;GAAK,YAAY,OAAO;CAAQ,GAAG,EAAE;AAClH;;AAGA,IAAa,mBACX,SAAS,SAAS,KAAK,OAAO,SAAS,eAAe,OAAO,SAAS,KAAK,MAAM,CAAC,CAAC,CAAC;;;;;;;;ACNtF,IAAa,uBAAsC;CACjD,IAAI,QAAQ;CAEZ,OAAO;EACL,WAAW,IAAI,IAAI,SAAS,IAAI;EAChC,MAAM;EACN;EACA,SAAS,UAAU;GACjB,MAAM,YAAY,IAAI,gBAAgB;GACtC,MAAM,EAAE,WAAW;GACnB,MAAM,EAAE,sBAAsB;GAC9B,IAAI;GAEJ,MAAM,mBAAyB;IAC7B,IAAI,YAAY,QAAQ,MAAM,OAC5B,WAAW;GAEf;GAEA,cACE,WACC,SAAS;IACR,IAAI,YAAY,IAAI,MAAM,SAAS,KAAK,SAAS,IAC/C,OAAO;IAET,SAAS,KAAK,IAAI;IAClB,OAAO;GACT,GACA,MACF;GACA,OAAO,iBAAiB,YAAY,OAAO,EAAE,OAAO,CAAC;GACrD,OAAO,iBAAiB,iBAAiB,OAAO,EAAE,OAAO,CAAC;GAC1D,OAAO,iBACL,gBACM;IACJ,aAAa,MAAM;IACnB,SAAS,WAAW,YAAY,GAAG;GACrC,GACA;IAAE;IAAQ,SAAS;GAAK,CAC1B;GACA,OAAO,iBAAiB,YAAY,YAAY,EAAE,OAAO,CAAC;GAC1D,OAAO,iBAAiB,cAAc,YAAY,EAAE,OAAO,CAAC;GAE5D,QAAQ,oBAAoB;GAC5B,aAAa;IACX,UAAU,MAAM;IAChB,aAAa,MAAM;IACnB,QAAQ,oBAAoB;GAC9B;EACF;EACA,eAAqB;GACnB,QAAQ,YAAY,QAAQ;GAC5B,SAAS,SAAS,QAAQ;EAC5B;EACA,SAAS,MAAM,UAAgB;GAC7B,MAAM,QAAQ,WAAW;GACzB,MAAM,WAAW,WAAW;GAC5B,IAAI,QAAQ,QAAQ,GAClB,OAAO,SAAS,GAAG,QAAQ;QACtB,IAAI,QAAQ,KAAK,GACtB,MAAM,eAAe;QAChB,IAAI,OACT,OAAO,SAAS,GAAG,CAAC;GAEtB,QAAQ,UAAU,QAAQ,UAAU,IAAI,IAAI,KAAK;EACnD;CACF;AACF;;;;;;;;AC9CA,IAAa,eACX,QACA,EAAE,MAAM,MAAM,UAAU,eAAe,QAC5B;CACX,MAAM,UAAU,OAAO,IAAI;CAC3B,IAAI,SAAS;CACb,IAAI,YAAY,YAAY,QAAQ,IAAI,CAAC;CACzC,IAAI,QAAQ;CACZ,IAAI,WAAkB,CAAC;CAEvB,MAAM,WAAW,UAAyB,KAAK,mBAAmB,KAAK,CAAC;CAExE,MAAM,WAAW,MAAY,KAAU,UAAyB;EAC9D,QAAQ,YAAY,GAAG;EACvB,YAAY;GACV,QAAQ,QAAQ;GAChB,QAAQ,QAAQ;EAClB,CAAC;EACD,IAAI;GACF,WAAW,KAAK,IAAI;EACtB,SAAS,OAAO;GACd,WAAW,KAAK,QAAQ,KAAK,CAAC;EAChC;EACA,QAAQ,OAAO,UAAU,KAAK;CAChC;CAEA,MAAM,QAAQ,UAAyB;EACrC,MAAM,OAAO,EAAE;EACf,MAAM,MAAM,QAAQ,IAAI;EACxB,QAAQ,QAAQ;EAEhB,QAAQ,QAAQ,CAAC,CACd,WAAW,OAAO,YAAY,GAAG,CAAC,CAAC,CAAC,CACpC,MAAM,SAAS,QAAQ,QAAQ,uBAAO,IAAI,MAAM,oBAAoB,IAAI,UAAU,CAAC,CAAC,CAAC,CACrF,KAAK,UAAU,OAAO,CAAC,CACvB,MAAM,SAAS;GACd,IAAI,SAAS,QACX,QAAQ,MAAM,KAAK,KAAK;EAE5B,CAAC,CAAC,CACD,OAAO,UAAmB;GAEzB,IAAI,SAAS,QACX,QAAQ,QAAQ;GAElB,eAAe,KAAK;EACtB,CAAC;CACL;CAEA,MAAM,aAAmB;EACvB,MAAM,UAAU,YAAY,QAAQ,IAAI,CAAC;EACzC,IAAI,YAAY,OAAO;GAErB,UAAU;GACV,YAAY;GACZ,QAAQ,QAAQ;GAChB,QAAQ,OAAO,UAAU,IAAI;EAC/B,OAAO,IAAI,YAAY,WAAW;GAChC,YAAY;GACZ,KAAK,IAAI;EACX;CACF;CAEA,IAAI,WAAiC,QAAQ,OAAO,IAAI;CACxD,MAAM,aAAmB;EACvB,WAAW;EACX,WAAW;EACX,UAAU;EACV,QAAQ,QAAQ;CAClB;CACA,UAAU,IAAI;CACd,KAAK,KAAK;CAEV,OAAO;EAAE,MAAM,QAAQ;EAAM,eAAe,QAAQ;EAAO,UAAU,QAAQ;EAAU;CAAK;AAC9F"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/lib/router.current.ts","../src/lib/router.href.ts","../../utils/dist/lib/fp/identity.js","../../utils/dist/lib/objects/isNil.js","../../utils/dist/lib/objects/isInstanceOf.js","../../utils/dist/lib/objects/isValidRecordKey.js","../../utils/dist/lib/objects/isNumber.js","../../utils/dist/lib/objects/hasProperty.js","../src/lib/router.links.ts","../src/lib/router.focus.ts","../src/lib/router.history.ts","../src/lib/router.memory.ts","../src/lib/router.navigate.ts","../src/lib/router.decode.ts","../src/lib/router.routes.ts","../src/lib/router.scroll.ts","../src/lib/router.browser.ts","../src/lib/router.start.ts"],"sourcesContent":["import { signal } from '@reely/signals';\nimport type { Signal } from '@reely/signals';\n\n// one location per document, so one path shown; the signal is made on first use\nlet shown: Signal<string> | undefined;\n\nconst shownPath = (): Signal<string> => (shown ??= signal(location.pathname));\n\n/**\n * The path of the page the browser's router shows, read like a signal: a binding or effect that reads it\n * follows every move, as a menu's `aria-current` does.\n */\nexport const currentPath = (): string => shownPath().value;\n\n// the Router tells `currentPath` the path of each page it shows\nexport const showPath = (path: string): void => {\n shownPath().value = path;\n};\n\n// and one page loading at a time\nlet loading: Signal<boolean> | undefined;\n\nconst loadingPage = (): Signal<boolean> => (loading ??= signal(false));\n\n/**\n * Whether the browser's router is loading the next page, read like a signal: the page shown stays\n * until it has loaded, so a progress bar anywhere in the app says one is coming.\n */\nexport const pageLoading = (): boolean => loadingPage().value;\n\nexport const showLoading = (value: boolean): void => {\n loadingPage().value = value;\n};\n","import type { ParamsOf } from './router.types';\n\n/** What `href` takes after the pattern: its params, or nothing for a pattern without any. */\nexport type HrefParams<P extends string> = keyof ParamsOf<P> extends never ? [] : [params: ParamsOf<P>];\n\n/**\n * The URL of a route: its pattern with the params filled in and encoded, typed from the pattern,\n * so a link and the route it opens share one path. `*rest` keeps its slashes.\n */\nexport function href<P extends string>(pattern: P, ...params: HrefParams<P>): string;\nexport function href(pattern: string, params: Readonly<Record<string, string>> = {}): string {\n return pattern\n .split('/')\n .map((part) => {\n const value = params[part.slice(1)] ?? '';\n if (part.startsWith(':')) {\n return encodeURIComponent(value);\n }\n return part.startsWith('*') ? value.split('/').map(encodeURIComponent).join('/') : part;\n })\n .join('/');\n}\n","export default function identity(v) {\n return v;\n}\n","export default function isNil(value) {\n return value === null || value === undefined;\n}\n","export default function isInstanceOf(instanceType, instance) {\n return instance instanceof instanceType;\n}\n","export default function isValidRecordKey(source) {\n return typeof source === 'string' || typeof source === 'number' || typeof source === 'symbol';\n}\n","export default function isNumber(source) {\n return typeof source === 'number' || source instanceof Number;\n}\n","import { hasSome, isSomeFunction } from '@reely/basics';\nimport isValidRecordKey from './isValidRecordKey';\nexport function hasProperty(property, source) {\n return (isValidRecordKey(property) &&\n (typeof source === 'object' || isSomeFunction(source)) &&\n hasSome(source) &&\n property in source);\n}\n","import { onCleanup } from '@reely/signals';\nimport { isInstanceOf, isNil } from '@reely/utils';\nimport type { Nullable } from '@reely/utils';\n\nconst linkOf = (target: Nullable<EventTarget>): Nullable<HTMLAnchorElement> =>\n isInstanceOf(Element, target) ? target.closest('a') : null;\n\n// a primary-button click without modifiers: the others open tabs and windows\nconst isPlainClick = (event: MouseEvent): boolean =>\n event.button === 0 && !event.defaultPrevented && !(event.altKey || event.ctrlKey || event.metaKey || event.shiftKey);\n\n// `rel=\"external\"` marks a link of this site the server answers (a file, a page of another app)\nconst opensHere = (link: HTMLAnchorElement): boolean =>\n (link.target === '' || link.target === '_self') &&\n !link.hasAttribute('download') &&\n !link.relList.contains('external') &&\n link.origin === location.origin;\n\n// takes over the plain clicks on the links of this site inside `root` that `follow` takes: it\n// answers whether it went to the link's page, or leaves the click to the browser\nexport const takeOverLinks = (\n root: EventTarget,\n follow: (link: HTMLAnchorElement) => boolean,\n signal: AbortSignal\n): void => {\n root.addEventListener(\n 'click',\n (event) => {\n const link = isInstanceOf(MouseEvent, event) && isPlainClick(event) ? linkOf(event.target) : null;\n if (!isNil(link) && opensHere(link) && follow(link)) {\n event.preventDefault();\n }\n },\n { signal }\n );\n};\n\n/**\n * Sends the plain clicks on the links of this site inside `root` to `navigate`, with each link's own\n * `href`: what a `memoryHistory` needs to follow the links of its part of the page. Returns the stop;\n * called under an owner, it stops with it too.\n */\nexport const followLinks = (root: EventTarget, navigate: (to: string) => void): (() => void) => {\n const listening = new AbortController();\n const stop = (): void => listening.abort();\n takeOverLinks(\n root,\n (link) => {\n navigate(link.getAttribute('href') ?? '');\n return true;\n },\n listening.signal\n );\n onCleanup(stop);\n return stop;\n};\n","import { isInstanceOf } from '@reely/utils';\nimport type { Nullable } from '@reely/utils';\n\nimport type { Shown } from './router.history';\n\nconst firstOf = (shown: Shown, selector: string): Nullable<Element> => {\n for (const node of isInstanceOf(Node, shown) ? [shown] : shown) {\n const found = isInstanceOf(Element, node) && !node.matches(selector) ? node.querySelector(selector) : node;\n if (isInstanceOf(Element, found) && found.matches(selector)) {\n return found;\n }\n }\n return null;\n};\n\n/**\n * The heading of a page shown, not one the app shows around it: its `h1`, or its first heading of any\n * level when it has none (a widget's pages start at `h2`).\n */\nexport const headingIn = (shown: Shown): Nullable<Element> =>\n firstOf(shown, 'h1') ?? firstOf(shown, 'h1, h2, h3, h4, h5, h6');\n\n// a screen reader starts reading the new page there; the scroll is the router's to set\nexport const focusOn = (target: Nullable<Element>): void => {\n if (isInstanceOf(HTMLElement, target)) {\n if (!target.hasAttribute('tabindex')) {\n target.tabIndex = -1;\n }\n target.focus({ preventScroll: true });\n }\n};\n","import type { NavigateOptions } from './router.navigate';\n\n/** What `show` returns: the element a page is shown in, or the nodes it put on screen. */\nexport type Shown = Node | readonly Node[];\n\n/**\n * Where a router's addresses come from and how it moves between them: the browser's history, or\n * `memoryHistory` for a router inside a part of the page.\n */\nexport interface RouterHistory {\n /** The address now. */\n url: () => URL;\n /** The path of the page shown, read like a signal. */\n path: () => string;\n /** Whether the next page is loading, read like a signal. */\n loading: () => boolean;\n /** The router says a page for the address now starts loading, or that loading is over. */\n loads: (loading: boolean) => void;\n /** Goes to an address, as a click on a link to it does. */\n navigate: (to: string | URL, options?: NavigateOptions) => void;\n /** Calls `moved` after each move until the stop it returns. */\n follow: (moved: () => void) => () => void;\n /** Takes the address now for the page about to be shown, so the page reads its path while it renders. */\n showing: () => void;\n /** Brings the reader to the page just shown; `moved` is false for the first page. */\n arrive: (shown: Shown, moved: boolean) => void;\n}\n\n// routes answer a path and its query; a new query is a new page as well, a new place on it is not\nexport const pageAddress = ({ pathname, search }: URL | Location | HTMLAnchorElement): string => `${pathname}${search}`;\n\n/** Any address parses against it where only its path and query matter, never its origin. */\nexport const anyOrigin = 'https://reely.invalid';\n","import { forEachSettled } from '@reely/basics';\nimport { signal } from '@reely/signals';\n\nimport { focusOn, headingIn } from './router.focus';\nimport { anyOrigin } from './router.history';\n\nimport type { RouterHistory } from './router.history';\n\n/**\n * A history kept in memory, for a router inside part of the page (a widget, a dialog, a demo): it\n * starts at `start` and leaves the document's URL alone. It follows the links `followLinks` gives it.\n */\nexport const memoryHistory = (start = '/'): RouterHistory => {\n let now = new URL(start, anyOrigin);\n const shown = signal(now.pathname);\n const loading = signal(false);\n const movers = new Set<() => void>();\n\n return {\n url: () => new URL(now),\n path: () => shown.value,\n loading: () => loading.value,\n loads: (value): void => {\n loading.value = value;\n },\n navigate: (to): void => {\n const next = new URL(to, now);\n if (next.href !== now.href) {\n now = next;\n forEachSettled(movers, (moved) => moved());\n }\n },\n follow: (moved) => {\n movers.add(moved);\n return (): void => void movers.delete(moved);\n },\n showing: (): void => {\n shown.value = now.pathname;\n },\n arrive: (page, moved): void => {\n if (moved) {\n focusOn(headingIn(page));\n }\n },\n };\n};\n","// a move made by code: the page left is told first, then the new URL; `pushState` fires no `popstate`\nexport const leavingEvent = 'reely:leaving';\nexport const navigationEvent = 'reely:navigate';\n\nexport interface NavigateOptions {\n /** Takes the place of the current history entry, as a redirect does, instead of adding one. */\n replace?: boolean;\n}\n\n/** Goes to a URL of this site without loading the document; the router shows its page. */\nexport const navigate = (to: string | URL, { replace = false }: NavigateOptions = {}): void => {\n const url = new URL(to, location.href);\n if (url.href === location.href) {\n return;\n }\n if (replace) {\n history.replaceState(null, '', url);\n } else {\n window.dispatchEvent(new Event(leavingEvent));\n history.pushState(null, '', url);\n }\n window.dispatchEvent(new Event(navigationEvent));\n};\n","// a part of a URL decoded; one that is not valid percent-encoding stays as it came\nexport const decode = (value: string): string => {\n try {\n return decodeURIComponent(value);\n } catch {\n return value;\n }\n};\n","import { hasSome } from '@reely/basics';\nimport { isNil } from '@reely/utils';\n\nimport { decode } from './router.decode';\nimport { anyOrigin } from './router.history';\n\nimport type { ParamsOf, Routes } from './router.types';\n\ntype Params = Readonly<Record<string, string>>;\n\n/**\n * A route table: each path pattern with the function that answers it from the params the pattern\n * names and the address's query. An answer is a page, a promise of one (`import()`), or nothing to\n * pass the address on to the next route.\n */\nexport type RouteTable<T> = { [P in keyof T & string]: (params: ParamsOf<P>, query: URLSearchParams) => T[P] };\n\n/** The pages a route table answers with, loaded. */\nexport type PageOf<T> = NonNullable<Awaited<T[keyof T]>>;\n\nconst segmentsOf = (path: string): string[] => path.split('/').filter((segment) => segment !== '');\n\n// the params a pattern takes from a pathname's segments, or undefined when it does not match them\nconst matchSegments = (pattern: readonly string[], segments: readonly string[]): Params | undefined => {\n const params: Record<string, string> = {};\n for (const [index, part] of pattern.entries()) {\n if (part.startsWith('*')) {\n params[part.slice(1)] = decode(segments.slice(index).join('/'));\n return params;\n }\n const segment = segments[index];\n if (isNil(segment) || (!part.startsWith(':') && part !== segment)) {\n return undefined;\n }\n if (part.startsWith(':')) {\n params[part.slice(1)] = decode(segment);\n }\n }\n return segments.length === pattern.length ? params : undefined;\n};\n\n/**\n * Turns a table of path patterns into the app's routes: an address gets the page of the first route\n * that matches its path and answers. `:name` takes one segment, `*name` the rest of the path; a route\n * that answers nothing passes the address on to the next one.\n */\nexport function defineRoutes<T>(table: RouteTable<T>): Routes<PageOf<T>>;\nexport function defineRoutes(\n table: Readonly<Record<string, (params: Params, query: URLSearchParams) => unknown>>\n): Routes<unknown> {\n const routes = Object.entries(table).map(([pattern, answer]) => ({ pattern: segmentsOf(pattern), answer }));\n return async (address) => {\n const { pathname, searchParams } = new URL(address, anyOrigin);\n const segments = segmentsOf(pathname);\n for (const { pattern, answer } of routes) {\n const params = matchSegments(pattern, segments);\n const page: unknown = hasSome(params) ? await answer(params, searchParams) : undefined;\n if (hasSome(page)) {\n return page;\n }\n }\n return undefined;\n };\n}\n","import { isPlainObject } from '@reely/basics';\nimport { hasProperty, isNumber } from '@reely/utils';\nimport type { Nullable } from '@reely/utils';\n\nimport { decode } from './router.decode';\n\nconst scrollKey = 'reelyScroll';\n\n/** Where the reader had scrolled the page of the current history entry, kept in its state for a reload too. */\nexport const keptScroll = (): number | undefined => {\n const { state } = history;\n const scrolled = hasProperty(scrollKey, state) ? state[scrollKey] : undefined;\n return isNumber(scrolled) ? scrolled : undefined;\n};\n\n/** Keeps where the page of the current history entry is scrolled, and what else its state held. */\nexport const keepScroll = (): void => {\n history.replaceState({ ...(isPlainObject(history.state) ? history.state : {}), [scrollKey]: window.scrollY }, '');\n};\n\n/** The element the hash of the location names. */\nexport const placeInUrl = (): Nullable<HTMLElement> =>\n location.hash === '' ? null : document.getElementById(decode(location.hash.slice(1)));\n","import { hasSome } from '@reely/basics';\n\nimport { currentPath, pageLoading, showLoading, showPath } from './router.current';\nimport { focusOn, headingIn } from './router.focus';\nimport { pageAddress } from './router.history';\nimport { takeOverLinks } from './router.links';\nimport { leavingEvent, navigate, navigationEvent } from './router.navigate';\nimport { keepScroll, keptScroll, placeInUrl } from './router.scroll';\n\nimport type { RouterHistory } from './router.history';\n\n/**\n * The browser's own history: links of this site anywhere in the document, back and forward, and\n * `navigate`; each page opens where the reader had scrolled it, at the place its URL names, or at\n * the top.\n */\nexport const browserHistory = (): RouterHistory => {\n let shown = '';\n\n return {\n url: () => new URL(location.href),\n path: currentPath,\n loading: pageLoading,\n loads: showLoading,\n navigate,\n follow: (moved) => {\n const listening = new AbortController();\n const { signal } = listening;\n const { scrollRestoration } = history;\n let saving: ReturnType<typeof setTimeout> | undefined;\n // the page shown keeps where it is scrolled, for a reload and for the way back to it\n const saveScroll = (): void => {\n if (pageAddress(location) === shown) {\n keepScroll();\n }\n };\n // a link to a place on the page shown is the browser's to follow\n takeOverLinks(\n document,\n (link) => {\n if (pageAddress(link) === shown && link.hash !== '') {\n return false;\n }\n navigate(link.href);\n return true;\n },\n signal\n );\n window.addEventListener('popstate', moved, { signal });\n window.addEventListener(navigationEvent, moved, { signal });\n window.addEventListener(\n 'scroll',\n () => {\n clearTimeout(saving);\n saving = setTimeout(saveScroll, 100);\n },\n { signal, passive: true }\n );\n window.addEventListener('pagehide', saveScroll, { signal });\n window.addEventListener(leavingEvent, saveScroll, { signal });\n // pages load after a move, so the browser's own restoring would scroll the page before it is there\n history.scrollRestoration = 'manual';\n return () => {\n listening.abort();\n clearTimeout(saving);\n history.scrollRestoration = scrollRestoration;\n };\n },\n showing: (): void => {\n shown = pageAddress(location);\n showPath(location.pathname);\n },\n arrive: (page, moved): void => {\n const place = placeInUrl();\n const scrolled = keptScroll();\n if (hasSome(scrolled)) {\n window.scrollTo(0, scrolled);\n } else if (hasSome(place)) {\n place.scrollIntoView();\n } else if (moved) {\n window.scrollTo(0, 0);\n }\n focusOn(place ?? (moved ? headingIn(page) : null));\n },\n };\n};\n","import { reportUncaught, toErrorWithMessage } from '@reely/basics';\nimport { batch, onCleanup } from '@reely/signals';\nimport { identity } from '@reely/utils';\nimport type { Nullable } from '@reely/utils';\n\nimport { browserHistory } from './router.browser';\nimport { pageAddress } from './router.history';\n\nimport type { RouterHistory, Shown } from './router.history';\nimport type { NavigateOptions } from './router.navigate';\nimport type { Routes } from './router.types';\n\nexport interface RouterOptions<Page> {\n /** Puts a page on screen; returns the element it is shown in, or its nodes, whose heading a move focuses. */\n show: (page: Page) => Shown;\n /** The page for what went wrong: a route that fails or fails to load, an address no route answers, a page that fails to show. */\n fail: (error: Error) => Page;\n /** Where the addresses come from: the browser's history when left out, or a `memoryHistory`. */\n history?: RouterHistory;\n}\n\nexport interface Router {\n /** The path of the page shown, read like a signal: an effect that reads it follows every move. */\n path: () => string;\n /** Whether a page is loading, read like a signal; the page shown stays until it has loaded. */\n loading: () => boolean;\n /** Goes to an address of this router, as a click on a link to it does. */\n navigate: (to: string | URL, options?: NavigateOptions) => void;\n /** Stops following moves; a router started under an owner stops with it too. */\n stop: () => void;\n}\n\n/**\n * Shows the page of the current address and keeps following it: a click on a link of this site, the\n * browser's back and forward, and `navigate` show the next page without loading the document. The\n * page shown stays until the next one has loaded; only the latest move counts.\n */\nexport const startRouter = <Page>(\n routes: Routes<Page>,\n { show, fail, history = browserHistory() }: RouterOptions<NoInfer<Page>>\n): Router => {\n let latest = 0;\n let requested = pageAddress(history.url());\n let shown = '';\n let onScreen: Shown = [];\n\n const failure = (error: unknown): Page => fail(toErrorWithMessage(error));\n\n const display = (page: Page, url: URL, moved: boolean): void => {\n shown = pageAddress(url);\n batch(() => {\n history.loads(false);\n history.showing();\n });\n try {\n onScreen = show(page);\n } catch (error) {\n onScreen = show(failure(error));\n }\n history.arrive(onScreen, moved);\n };\n\n const load = (moved: boolean): void => {\n const turn = ++latest;\n const url = history.url();\n history.loads(true);\n // a route that throws rejects like one whose page fails to load\n Promise.resolve()\n .then(() => routes(pageAddress(url)))\n .then((page) => page ?? Promise.reject(new Error(`No route answers ${url.pathname}`)))\n .then(identity, failure)\n .then((page) => {\n if (turn === latest) {\n display(page, url, moved);\n }\n })\n .catch((error: unknown) => {\n // even the failure page failed: nothing more is coming for this move\n if (turn === latest) {\n history.loads(false);\n }\n reportUncaught(error);\n });\n };\n\n const move = (): void => {\n const address = pageAddress(history.url());\n if (address === shown) {\n // back on the page shown, or a new place on it: a load still pending for another is dropped\n latest += 1;\n requested = address;\n history.loads(false);\n history.arrive(onScreen, true);\n } else if (address !== requested) {\n requested = address;\n load(true);\n }\n };\n\n let unfollow: Nullable<() => void> = history.follow(move);\n const stop = (): void => {\n unfollow?.();\n unfollow = null;\n latest += 1;\n history.loads(false);\n };\n onCleanup(stop);\n load(false);\n\n return { path: history.path, loading: history.loading, navigate: history.navigate, stop };\n};\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reely/router",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "A small typed router for any web app: routes from path patterns, lazy pages, links and history taken over, scroll and focus kept, no framework.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|