white-label-router 6.1.2 → 6.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 +64 -17
- package/dist/index.d.ts +96 -28
- package/dist/index.js +252 -82
- package/dist/index.js.map +1 -1
- package/package.json +13 -7
package/README.md
CHANGED
|
@@ -88,7 +88,18 @@ Public navigation should remain a real link:
|
|
|
88
88
|
<a href="/products/42" data-pushstate>View product 42</a>
|
|
89
89
|
```
|
|
90
90
|
|
|
91
|
-
`data-pushstate` opts the link into client-side enhancement.
|
|
91
|
+
`data-pushstate` opts the link into client-side enhancement. Router intercepts a click on an `<a href data-pushstate>` (HTML or SVG; Router reads the `href` attribute, resolved against `document.baseURI`) only when it can actually handle it. These clicks stay native:
|
|
92
|
+
|
|
93
|
+
- links without `data-pushstate`, or clicks whose default action was already prevented;
|
|
94
|
+
- non-left clicks and clicks with Meta, Ctrl, Shift, or Alt held;
|
|
95
|
+
- links with `download`, or a `target` other than `_self`;
|
|
96
|
+
- links to a `#fragment` of the page already shown (same path and query), which the browser handles as in-page fragment navigation; the resulting `popstate` follows the [hash-only rule](#browser-history-behavior);
|
|
97
|
+
- cross-origin or unparseable URLs;
|
|
98
|
+
- URLs that match no route, or only the placeholder `defaultRoute` the constructor installs (a `defaultRoute` you configure yourself does intercept unmatched links);
|
|
99
|
+
- routes with no runnable view behavior;
|
|
100
|
+
- links whose guard returns anything but `true`, or throws (the guard's error still propagates).
|
|
101
|
+
|
|
102
|
+
Once a route has been committed, the click's default action is cancelled even if the route's view throws, so the browser does not navigate a second time.
|
|
92
103
|
|
|
93
104
|
This preserves keyboard behavior, context menus, no-JavaScript navigation, and crawler discovery. Public routes should have directly requestable URLs that return meaningful HTML and appropriate title, description, canonical, and robots metadata.
|
|
94
105
|
|
|
@@ -145,7 +156,28 @@ router.routes = {
|
|
|
145
156
|
};
|
|
146
157
|
```
|
|
147
158
|
|
|
148
|
-
|
|
159
|
+
### Guards
|
|
160
|
+
|
|
161
|
+
`secure(scope, location)` runs synchronously with the destination location, before anything is torn down. It must return exactly `true` to allow navigation; any other value (including a Promise) rejects it and `navigate()` returns `false`. An error thrown by a guard propagates to the caller and leaves Router state unchanged. Client guards are interface behavior, not server authorization.
|
|
162
|
+
|
|
163
|
+
### Lifecycle order
|
|
164
|
+
|
|
165
|
+
A successful navigation runs these steps in order:
|
|
166
|
+
|
|
167
|
+
1. Match the route and run its guard. Rejection stops here with no state change.
|
|
168
|
+
2. Commit `router.url`, `router.route`, and `router.locationData` for the destination.
|
|
169
|
+
3. Call the previous route's `destroy(scope, nextLocation)`, if it had one.
|
|
170
|
+
4. Run the new route: a function route, a `view` function, or `view.initialize(scope, location)`.
|
|
171
|
+
5. Apply the route's title and focus (browser only; see [Title and focus](#title-and-focus)).
|
|
172
|
+
6. Set `router.previousRoute`, then push a history entry (browser only; skipped for `initialize()` and back/forward).
|
|
173
|
+
|
|
174
|
+
Router keeps the `destroy` callback of the lifecycle that actually initialized, so replacing or mutating the route table later does not change which teardown runs. That lifecycle stays owned even if `initialize()` throws, or title/focus work throws after it: the route remains committed, the error is rethrown to the caller, and its `destroy()` still runs before the next successful route starts. Title, focus, and history are skipped for a route whose `initialize()` threw.
|
|
175
|
+
|
|
176
|
+
If the previous route's `destroy()` throws, its error propagates out of `navigate()` and that teardown is never called again: Router releases it before calling it. The destination stays committed (`router.url`, `router.route`, `router.locationData`), but its view does not run, so it owns no `destroy()`, and title, focus, `previousRoute`, and history are skipped. The next navigation therefore starts without a teardown; `navigate()` with no URL re-runs the committed destination to initialize it.
|
|
177
|
+
|
|
178
|
+
### Navigating from inside a route
|
|
179
|
+
|
|
180
|
+
`navigate()` is re-entrant. If `initialize()`, `destroy()`, a focus handler, or a mediator event starts another navigation, the newer navigation wins: the outer one stops at that point, applies no title, focus, or history of its own, and returns the router. For example, navigating to `/two` from inside `/one`'s `initialize()` runs `/one`'s `destroy()` and then `/two`'s `initialize()`, and leaves `/two` as the active route.
|
|
149
181
|
|
|
150
182
|
## Matching
|
|
151
183
|
|
|
@@ -163,6 +195,10 @@ router.routes = {
|
|
|
163
195
|
|
|
164
196
|
`/products/sale` selects `saleRoute` even though `/products` was defined first. When no prefix matches, `defaultRoute` runs if configured.
|
|
165
197
|
|
|
198
|
+
Route keys are encoded the same way URL parsing encodes paths, so `'/café'` matches a request for `/caf%C3%A9`. Keys that don't start with `/`, or that contain `?` or `#`, can never match a pathname and are ignored (apart from `defaultRoute`). If two keys normalize to the same path (for example `/x/../same` and `/same`), the key already in normalized form wins, otherwise the alphabetically lowest; insertion order never decides.
|
|
199
|
+
|
|
200
|
+
A new `Router` starts with a placeholder `defaultRoute` that does nothing, so programmatic navigation to an unmatched URL succeeds until you replace `routes`. That placeholder never intercepts link clicks.
|
|
201
|
+
|
|
166
202
|
## Location contract
|
|
167
203
|
|
|
168
204
|
Routes receive the configured `scope` and a location object:
|
|
@@ -178,7 +214,12 @@ Routes receive the configured `scope` and a location object:
|
|
|
178
214
|
}
|
|
179
215
|
```
|
|
180
216
|
|
|
181
|
-
`
|
|
217
|
+
- `url` is the path, query, and hash (`/products/42?color=blue#reviews`), never a full absolute URL. This holds on the server too: `initialize('https://example.com/products/42?color=blue')` produces `url: '/products/42?color=blue'`. `router.url` holds the same value.
|
|
218
|
+
- `data.url` contains the URI-decoded path segments after the matched route prefix (all segments for `defaultRoute`). A segment with malformed percent-encoding is passed through undecoded.
|
|
219
|
+
- `data.query` contains query values decoded with `URLSearchParams`; for duplicate keys the last value wins.
|
|
220
|
+
- `data.mediator` contains navigation data passed to `navigate()` or through `CustomEvent.detail`. It is `{}` for link clicks, `initialize()`, and back/forward, and `undefined` when `navigate()` is called without data.
|
|
221
|
+
|
|
222
|
+
Applications must still validate these untrusted values.
|
|
182
223
|
|
|
183
224
|
## Programmatic navigation
|
|
184
225
|
|
|
@@ -192,7 +233,7 @@ In browsers, successful navigation updates history unless the call represents `p
|
|
|
192
233
|
|
|
193
234
|
Rejected navigation is atomic from Router's perspective: malformed or wrong-type URL candidates, cross-origin browser URLs, URLs with no matching route/default route, failed guards, and routes without runnable view behavior return `false` without destroying the current route, pushing browser history, or replacing the last successful URL, selected route, or location payload.
|
|
194
235
|
|
|
195
|
-
Calling `navigate()` without a URL
|
|
236
|
+
Calling `navigate()` without a URL re-runs Router's last committed URL. In a browser, that re-run adds a history entry only when the address bar does not already show that URL, so it never duplicates the current entry. Before anything has been committed it uses the current browser URL, or `/` on a server; server applications should normally supply a fully qualified request URL explicitly. Errors thrown by a guard or a route callback propagate out of `navigate()`.
|
|
196
237
|
|
|
197
238
|
## Mediator integration
|
|
198
239
|
|
|
@@ -214,7 +255,7 @@ mediator.dispatchEvent(new CustomEvent('router:navigate', {
|
|
|
214
255
|
}));
|
|
215
256
|
```
|
|
216
257
|
|
|
217
|
-
The `CustomEvent.detail`
|
|
258
|
+
The `CustomEvent.detail` object becomes `location.data.mediator`, and its `url` (if any) is the destination. A detail without `url`, or a `null`/missing detail, re-runs the current URL without adding a duplicate history entry; a detail that is not a plain object (a string, number, or array) is ignored. Mediator navigations to a `url` push browser history like `navigate()`. `destroy()` removes the listener owned by Router. Repeated listener initialization is idempotent. Reassigning `router.mediator` while listeners are active moves the `router:navigate` subscription to the replacement.
|
|
218
259
|
|
|
219
260
|
Router does not import or require `white-label-mediator`; the integration is structural so another compatible EventTarget can be used instead.
|
|
220
261
|
|
|
@@ -259,12 +300,14 @@ router.routes = {
|
|
|
259
300
|
};
|
|
260
301
|
```
|
|
261
302
|
|
|
262
|
-
|
|
303
|
+
After a route renders, Router sets `document.title` from `title` and focuses the first element matching `focus` (default `main h1`), adding `tabindex="-1"` if the element has no `tabindex`. Set `focus: false` for an in-page refinement that should preserve current focus. The first dispatch from `initialize()` sets the title but does not move focus, because the browser already owns focus on a freshly loaded page; later navigations do. Server runtimes retain `pageTitle` as route state but do not mutate a document or focus target.
|
|
263
304
|
|
|
264
305
|
## Browser history behavior
|
|
265
306
|
|
|
266
307
|
Initialization dispatches the current URL without adding a duplicate history entry. Back/forward navigation reads `window.location`—including path, query, and hash—even when history state is null or belongs to another application. History state is not treated as the authoritative URL. Rejected navigation does not add a history entry.
|
|
267
308
|
|
|
309
|
+
When back/forward, or a click on a link to a `#fragment` of the current page, changes only the hash (footnotes, skip links), Router updates `router.url` and `locationData.url` but does not re-run the route, call `destroy()`, or move focus. A change to the path or query re-runs the route as usual.
|
|
310
|
+
|
|
268
311
|
## Public API
|
|
269
312
|
|
|
270
313
|
| Member | Behavior | Returns |
|
|
@@ -277,12 +320,14 @@ Initialization dispatches the current URL without adding a duplicate history ent
|
|
|
277
320
|
| `navigate(url?, data?, isPopState?)` | Match and run a route; update browser history when appropriate. Invalid/malformed URL candidates and other rejected navigation preserve current route state. | The same Router on success; `false` for rejected navigation. |
|
|
278
321
|
| `addListeners()` | Attach browser and optional mediator listeners once. | The same `Router` instance. |
|
|
279
322
|
| `removeListeners()` | Release listeners owned by this router. | The same `Router` instance. |
|
|
280
|
-
| `destroy()` | Release routing listeners. | The same `Router` instance after cleanup. |
|
|
323
|
+
| `destroy()` | Release routing listeners. Does not call the active route's `destroy()`. | The same `Router` instance after cleanup. |
|
|
281
324
|
| `parseQueryString(query)` | Decode a query string using `URLSearchParams`. | A plain object containing the last value for each query key. |
|
|
282
325
|
| `setLocationData(data?)` | Rebuild the current route location payload. | `undefined`; updates `locationData` in place. |
|
|
283
326
|
| `applyPageContext(route)` | Update route title/focus context when applicable. | The same `Router` instance. |
|
|
284
|
-
| `eventPushStateClick(event)` | Handle an eligible browser push-state click. | The same Router after handled navigation; `true` when native/default handling should continue. |
|
|
285
|
-
| `eventPopState()` | Dispatch the current browser history location
|
|
327
|
+
| `eventPushStateClick(event)` | Handle an eligible browser push-state click; same-page `#fragment` links stay native (see [Progressive enhancement first](#progressive-enhancement-first)). | The same Router after handled navigation; `true` when native/default handling should continue. |
|
|
328
|
+
| `eventPopState()` | Dispatch the current browser history location; a hash-only change just updates `url`. | The same `Router` instance. |
|
|
329
|
+
|
|
330
|
+
Router records navigation state on public properties that applications should treat as read-only: `url`, `route`, `previousRoute`, `locationData`, and `pageTitle`.
|
|
286
331
|
|
|
287
332
|
## TypeScript
|
|
288
333
|
|
|
@@ -304,17 +349,19 @@ router.initialize();
|
|
|
304
349
|
|
|
305
350
|
```sh
|
|
306
351
|
npm ci --ignore-scripts
|
|
307
|
-
npm run lint
|
|
308
|
-
npm run typecheck
|
|
309
|
-
npm test
|
|
310
|
-
npm run coverage
|
|
311
|
-
npm run audit
|
|
312
|
-
npm pack --dry-run
|
|
352
|
+
npm run lint && npm run coverage && npm run audit
|
|
313
353
|
```
|
|
314
354
|
|
|
315
|
-
|
|
355
|
+
`npm run coverage` builds (which typechecks), runs the consumer type checks, and runs every Node test, so `npm run typecheck` and `npm test` don't need separate runs. For a quick loop while iterating, use `npm run build && node --test test/<file>.test.js`. `npm run lint` covers `src/`, `test/`, and `benchmark/`. `dist/` is generated and not committed; `npm run build` (also run by `prepare` for git installs) creates it.
|
|
356
|
+
|
|
357
|
+
After `npm run build`:
|
|
358
|
+
|
|
359
|
+
- `npm run test:browser` runs `test/browser/` in real Chromium through Playwright (real link clicks, back/forward, fragments, focus, `navigationRoot`) against pages served by a local `node:http` server. It is not part of `npm test`. Install a browser once with `npx playwright install chromium`, or point `PLAYWRIGHT_CHROMIUM_EXECUTABLE` at an existing Chromium.
|
|
360
|
+
- `npm run benchmark` times `matchRoute`, `navigate`, and link clicks with 10 and 1,000 routes. `npm run bench:ci` is a short version that fails if any operation is more than 3x slower than `benchmark/baseline.json` or a deterministic counter differs. Refresh the baseline after an intended performance change with `node benchmark/router.bench.js --ci --update-baseline` and commit the result.
|
|
361
|
+
|
|
362
|
+
Tests cover browser behavior, scoped progressive navigation, DOM-free server routing, atomic rejected navigation, mediator reassignment, and parity of the shared route contract. Coverage enforces 100% statements, branches, functions, and lines per implementation file. CI verifies the documented Node minimum and primary Node line, runs the short benchmark and the Chromium browser tests, audits dependencies, packs the package, and verifies the public API across npm, Yarn, and pnpm.
|
|
316
363
|
|
|
317
|
-
Edit `src
|
|
364
|
+
Edit `src/index.ts`; `dist` is build output and is not edited or committed. See [`CONTRIBUTING.md`](CONTRIBUTING.md).
|
|
318
365
|
|
|
319
366
|
## Design boundary
|
|
320
367
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/** Extra data passed with a navigation. `url`, if present, is the destination; any other fields are yours. */
|
|
2
2
|
interface NavigationData {
|
|
3
3
|
url?: string;
|
|
4
4
|
[key: string]: unknown;
|
|
5
5
|
}
|
|
6
|
-
/**
|
|
6
|
+
/**
|
|
7
|
+
* What a route callback receives about the destination.
|
|
8
|
+
* `url` is the path, query, and hash; `data.url` holds the decoded path segments after the matched route key;
|
|
9
|
+
* `data.query` holds decoded query values; `data.mediator` is the navigation data, if any.
|
|
10
|
+
*/
|
|
7
11
|
interface LocationData {
|
|
8
12
|
url: string;
|
|
9
13
|
data: {
|
|
@@ -12,12 +16,17 @@ interface LocationData {
|
|
|
12
16
|
query: Record<string, string>;
|
|
13
17
|
};
|
|
14
18
|
}
|
|
19
|
+
/** A route callback. It receives `router.scope` and the destination location. */
|
|
15
20
|
type RouteHandler = (scope: Element | null, location: LocationData) => unknown;
|
|
21
|
+
/** A view with setup and teardown. `destroy` receives the location being navigated to. */
|
|
16
22
|
interface RouteViewLifecycle {
|
|
17
23
|
initialize?: RouteHandler;
|
|
18
24
|
destroy?: RouteHandler;
|
|
19
25
|
}
|
|
20
|
-
/**
|
|
26
|
+
/**
|
|
27
|
+
* A route with optional page context and a guard.
|
|
28
|
+
* `secure` must return exactly `true` to allow navigation; `focus: false` leaves focus where it is.
|
|
29
|
+
*/
|
|
21
30
|
interface RouteObject {
|
|
22
31
|
title?: string;
|
|
23
32
|
focus?: string | false;
|
|
@@ -25,12 +34,15 @@ interface RouteObject {
|
|
|
25
34
|
view?: RouteHandler | RouteViewLifecycle;
|
|
26
35
|
}
|
|
27
36
|
type Route = RouteHandler | RouteObject;
|
|
28
|
-
/**
|
|
37
|
+
/** Any EventTarget-like object; Router only adds and removes a `router:navigate` listener on it. */
|
|
29
38
|
interface NavigationMediator {
|
|
30
39
|
addEventListener(type: string, callback: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
|
|
31
40
|
removeEventListener(type: string, callback: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
|
|
32
41
|
}
|
|
33
|
-
/**
|
|
42
|
+
/**
|
|
43
|
+
* Maps URLs to route callbacks. In a browser it enhances `data-pushstate` links and back/forward with the History API;
|
|
44
|
+
* on a server it dispatches the request URL you pass, with no DOM required.
|
|
45
|
+
*/
|
|
34
46
|
declare class Router {
|
|
35
47
|
routes: Record<string, Route>;
|
|
36
48
|
url: string;
|
|
@@ -48,57 +60,113 @@ declare class Router {
|
|
|
48
60
|
private currentNavigationRoot;
|
|
49
61
|
private boundNavigationRoot;
|
|
50
62
|
private activeDestroy;
|
|
51
|
-
|
|
63
|
+
private compiledRoutes;
|
|
64
|
+
private navigationToken;
|
|
65
|
+
private initialDispatch;
|
|
66
|
+
private suppressFocus;
|
|
67
|
+
/** Create a router with a placeholder `defaultRoute` and no listeners attached yet. */
|
|
52
68
|
constructor();
|
|
69
|
+
/** Event target that can send `router:navigate` events, or `false` for none. */
|
|
53
70
|
get mediator(): NavigationMediator | false;
|
|
54
|
-
/**
|
|
71
|
+
/** Set the mediator. If listeners are active, the `router:navigate` listener moves from the old one to the new one. */
|
|
55
72
|
set mediator(value: NavigationMediator | false);
|
|
73
|
+
/** Element or document whose `data-pushstate` link clicks this router handles; `null` means the whole document. */
|
|
56
74
|
get navigationRoot(): Document | Element | null;
|
|
57
|
-
/**
|
|
75
|
+
/** Set the click root. If listeners are active, the click listener moves to the new root. */
|
|
58
76
|
set navigationRoot(value: Document | Element | null);
|
|
59
|
-
/** True when
|
|
77
|
+
/** True when `window` and `document` exist, i.e. browser-only effects should run. */
|
|
60
78
|
private isBrowserRuntime;
|
|
61
|
-
/**
|
|
79
|
+
/** Origin used as the base for URL parsing: the page's origin in a browser, a dummy one on a server. */
|
|
62
80
|
private getOrigin;
|
|
63
|
-
/**
|
|
81
|
+
/** Current path, query, and hash: from `window.location` in a browser, or the last committed URL (default `/`) on a server. */
|
|
64
82
|
private getCurrentUrl;
|
|
65
|
-
/** Parse a
|
|
83
|
+
/** Parse a URL relative to the current page (or the dummy server origin). Returns `null` if it can't be parsed. */
|
|
66
84
|
private parseNavigationUrl;
|
|
67
|
-
/**
|
|
85
|
+
/** In a browser, reduce a URL to its path, query, and hash, or `null` if it is cross-origin or invalid. */
|
|
68
86
|
private normalizeBrowserUrl;
|
|
69
|
-
/**
|
|
87
|
+
/** True if the route has something to run: a function, a `view` function, or a `view.initialize`. */
|
|
70
88
|
private isRunnableRoute;
|
|
71
|
-
/**
|
|
89
|
+
/** Build (or reuse) the lookup tables for the current route keys. Reused while the own key list is unchanged. */
|
|
90
|
+
private compileRoutes;
|
|
91
|
+
/** Find the route key with the longest matching path prefix, else `'defaultRoute'` if one is set, else `null`. */
|
|
72
92
|
private matchRoute;
|
|
73
|
-
/**
|
|
93
|
+
/** Length of the normalized path for a route key, used to slice the matched prefix off `data.url`; 0 for `defaultRoute`. */
|
|
94
|
+
private routePrefixLength;
|
|
95
|
+
/** Parse a URL Router committed earlier. A path is prefixed with the origin first, so '//x' stays a path, not a host. */
|
|
96
|
+
private parseStoredUrl;
|
|
97
|
+
/** Build the location object passed to route callbacks. Path segments that fail to decode are kept as-is. */
|
|
74
98
|
private buildLocationData;
|
|
75
99
|
/**
|
|
76
|
-
*
|
|
77
|
-
*
|
|
100
|
+
* Attach listeners and dispatch the starting URL. Does not add a history entry or move focus.
|
|
101
|
+
* @param url - Request URL on a server. In a browser, omit it to use `window.location`.
|
|
102
|
+
* @returns This router.
|
|
103
|
+
* @throws Whatever a guard or route callback throws.
|
|
78
104
|
*/
|
|
79
105
|
initialize(url?: string): this;
|
|
80
|
-
/**
|
|
106
|
+
/**
|
|
107
|
+
* Attach the click and popstate listeners (browser only) and the mediator listener. Safe to call more than once.
|
|
108
|
+
* @returns This router.
|
|
109
|
+
*/
|
|
81
110
|
addListeners(): this;
|
|
82
|
-
/**
|
|
111
|
+
/**
|
|
112
|
+
* Remove this router's listeners. Does not call the active route's `destroy()`.
|
|
113
|
+
* @returns This router.
|
|
114
|
+
*/
|
|
83
115
|
destroy(): this;
|
|
84
|
-
/**
|
|
116
|
+
/**
|
|
117
|
+
* Remove the listeners `addListeners()` attached. Subclasses can override this to release more.
|
|
118
|
+
* @returns This router.
|
|
119
|
+
*/
|
|
85
120
|
removeListeners(): this;
|
|
86
|
-
/**
|
|
121
|
+
/**
|
|
122
|
+
* Click handler for `<a href data-pushstate>` links. Takes over only when Router can handle the link:
|
|
123
|
+
* a plain left click on a same-origin link (no `download`, no other `target`) whose route is app-defined,
|
|
124
|
+
* runnable, and allowed by its guard, and that is not just a `#fragment` of the current page. Anything else is left
|
|
125
|
+
* to the browser.
|
|
126
|
+
* @param e - The click event.
|
|
127
|
+
* @returns This router if it handled the click, otherwise `true`.
|
|
128
|
+
* @throws Whatever a guard or route callback throws.
|
|
129
|
+
*/
|
|
87
130
|
eventPushStateClick(e: MouseEvent): true | this;
|
|
88
|
-
/**
|
|
131
|
+
/**
|
|
132
|
+
* Popstate handler for back/forward. Runs the route for the new URL without adding a history entry.
|
|
133
|
+
* A hash-only change just updates `url` and `locationData.url`.
|
|
134
|
+
* @returns This router.
|
|
135
|
+
*/
|
|
89
136
|
eventPopState(): this;
|
|
90
|
-
/**
|
|
137
|
+
/**
|
|
138
|
+
* Decode a query string into a plain object. For repeated keys the last value wins.
|
|
139
|
+
* @param queryString - Query string, with or without the leading `?`.
|
|
140
|
+
*/
|
|
91
141
|
parseQueryString(queryString: string): {
|
|
92
142
|
[k: string]: string;
|
|
93
143
|
};
|
|
94
|
-
/**
|
|
144
|
+
/**
|
|
145
|
+
* Rebuild `locationData` from the current `url` and `route`. Leaves it unchanged if the URL can't be parsed.
|
|
146
|
+
* @param mediatorData - Value to expose as `locationData.data.mediator`.
|
|
147
|
+
*/
|
|
95
148
|
setLocationData(mediatorData?: NavigationData): void;
|
|
96
|
-
/**
|
|
149
|
+
/**
|
|
150
|
+
* Record the route's title in `pageTitle` and, in a browser, set `document.title` and move focus to the route's
|
|
151
|
+
* `focus` selector (default `main h1`), adding `tabindex="-1"` if needed. `focus: false` skips focus.
|
|
152
|
+
* @param route - The route that just ran.
|
|
153
|
+
* @returns This router.
|
|
154
|
+
*/
|
|
97
155
|
applyPageContext(route: Route): this;
|
|
98
|
-
/**
|
|
156
|
+
/**
|
|
157
|
+
* Navigate to a URL: match the route, run its guard, commit the new state, call the previous route's `destroy()`,
|
|
158
|
+
* run the new route, apply title and focus, then push a history entry (browser only, unless `isPopState`).
|
|
159
|
+
* If route code starts another navigation part-way through, that one wins and this call stops early.
|
|
160
|
+
* @param url - Destination URL. Omit to re-run the last committed URL (or the current browser URL / `/` at first).
|
|
161
|
+
* @param mediatorData - Value exposed to the route as `location.data.mediator`.
|
|
162
|
+
* @param isPopState - `true` to skip adding a history entry.
|
|
163
|
+
* @returns This router, or `false` if the navigation was rejected (bad or cross-origin URL, no matching route,
|
|
164
|
+
* guard refused, or nothing to run). A rejected navigation changes nothing.
|
|
165
|
+
* @throws Whatever a guard or route callback throws. A throwing guard changes nothing; a throwing route stays committed.
|
|
166
|
+
*/
|
|
99
167
|
navigate(url?: string, mediatorData?: NavigationData, isPopState?: boolean): false | this;
|
|
100
168
|
}
|
|
101
|
-
/**
|
|
169
|
+
/** Types for route definitions, location data, and navigation data. */
|
|
102
170
|
declare namespace Router {
|
|
103
171
|
type Handler = RouteHandler;
|
|
104
172
|
type Route = RouteHandler | RouteObject;
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
/**
|
|
2
|
+
/** The do-nothing `defaultRoute` objects each constructor installs, so link clicks can tell them apart from app routes. */
|
|
3
|
+
const builtInDefaultRoutes = new WeakSet();
|
|
4
|
+
/** A dummy origin that lets route keys and server URLs go through the standard URL parser. */
|
|
5
|
+
const routeKeyBase = 'http://white-label.invalid';
|
|
6
|
+
/** Encode a route key the way the URL parser encodes paths, so `'/café'` matches `/caf%C3%A9`. */
|
|
7
|
+
function normalizeRouteKey(key) {
|
|
8
|
+
// A key that doesn't start with '/' or contains '?' or '#' can never equal a pathname, so leave it as is (it won't match).
|
|
9
|
+
if (!key.startsWith('/') || /[?#]/.test(key)) {
|
|
10
|
+
return key;
|
|
11
|
+
}
|
|
12
|
+
return new URL(`${routeKeyBase}${key}`).pathname;
|
|
13
|
+
}
|
|
14
|
+
/** Strip the `#fragment`, leaving the path and query. */
|
|
15
|
+
function withoutHash(url) {
|
|
16
|
+
const index = url.indexOf('#');
|
|
17
|
+
return index === -1 ? url : url.slice(0, index);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Maps URLs to route callbacks. In a browser it enhances `data-pushstate` links and back/forward with the History API;
|
|
21
|
+
* on a server it dispatches the request URL you pass, with no DOM required.
|
|
22
|
+
*/
|
|
3
23
|
class Router {
|
|
4
24
|
routes;
|
|
5
25
|
url;
|
|
@@ -17,16 +37,24 @@ class Router {
|
|
|
17
37
|
currentNavigationRoot = null;
|
|
18
38
|
boundNavigationRoot = null;
|
|
19
39
|
activeDestroy;
|
|
20
|
-
|
|
40
|
+
compiledRoutes;
|
|
41
|
+
// Bumped by each navigation that passes its guard. If it changes while a navigation is calling out to app code,
|
|
42
|
+
// a newer navigation has started, and the older one stops before touching title, focus, or history.
|
|
43
|
+
navigationToken = 0;
|
|
44
|
+
// True only for the first navigate() call made by initialize(), so that dispatch leaves focus alone.
|
|
45
|
+
initialDispatch = false;
|
|
46
|
+
// Tells applyPageContext() to skip focus for the current call.
|
|
47
|
+
suppressFocus = false;
|
|
48
|
+
/** Create a router with a placeholder `defaultRoute` and no listeners attached yet. */
|
|
21
49
|
constructor() {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
destroy: () => true
|
|
27
|
-
}
|
|
50
|
+
const defaultRoute = {
|
|
51
|
+
view: {
|
|
52
|
+
initialize: () => true,
|
|
53
|
+
destroy: () => true
|
|
28
54
|
}
|
|
29
55
|
};
|
|
56
|
+
builtInDefaultRoutes.add(defaultRoute);
|
|
57
|
+
this.routes = { defaultRoute };
|
|
30
58
|
this.url = '';
|
|
31
59
|
this.route = null;
|
|
32
60
|
this.scope = null;
|
|
@@ -48,8 +76,9 @@ class Router {
|
|
|
48
76
|
};
|
|
49
77
|
this.listenersInitialized = false;
|
|
50
78
|
}
|
|
79
|
+
/** Event target that can send `router:navigate` events, or `false` for none. */
|
|
51
80
|
get mediator() { return this.currentMediator; }
|
|
52
|
-
/**
|
|
81
|
+
/** Set the mediator. If listeners are active, the `router:navigate` listener moves from the old one to the new one. */
|
|
53
82
|
set mediator(value) {
|
|
54
83
|
if (value === this.currentMediator) {
|
|
55
84
|
return;
|
|
@@ -64,8 +93,9 @@ class Router {
|
|
|
64
93
|
this.boundMediator = value;
|
|
65
94
|
}
|
|
66
95
|
}
|
|
96
|
+
/** Element or document whose `data-pushstate` link clicks this router handles; `null` means the whole document. */
|
|
67
97
|
get navigationRoot() { return this.currentNavigationRoot; }
|
|
68
|
-
/**
|
|
98
|
+
/** Set the click root. If listeners are active, the click listener moves to the new root. */
|
|
69
99
|
set navigationRoot(value) {
|
|
70
100
|
if (value === this.currentNavigationRoot) {
|
|
71
101
|
return;
|
|
@@ -80,22 +110,22 @@ class Router {
|
|
|
80
110
|
}
|
|
81
111
|
this.currentNavigationRoot = value;
|
|
82
112
|
}
|
|
83
|
-
/** True when
|
|
113
|
+
/** True when `window` and `document` exist, i.e. browser-only effects should run. */
|
|
84
114
|
isBrowserRuntime() {
|
|
85
115
|
return typeof window !== 'undefined' && typeof document !== 'undefined';
|
|
86
116
|
}
|
|
87
|
-
/**
|
|
117
|
+
/** Origin used as the base for URL parsing: the page's origin in a browser, a dummy one on a server. */
|
|
88
118
|
getOrigin() {
|
|
89
119
|
return this.isBrowserRuntime() ? window.location.origin : 'http://white-label.invalid';
|
|
90
120
|
}
|
|
91
|
-
/**
|
|
121
|
+
/** Current path, query, and hash: from `window.location` in a browser, or the last committed URL (default `/`) on a server. */
|
|
92
122
|
getCurrentUrl() {
|
|
93
123
|
if (!this.isBrowserRuntime()) {
|
|
94
124
|
return this.url || '/';
|
|
95
125
|
}
|
|
96
126
|
return window.location.pathname + window.location.search + (window.location.hash || '');
|
|
97
127
|
}
|
|
98
|
-
/** Parse a
|
|
128
|
+
/** Parse a URL relative to the current page (or the dummy server origin). Returns `null` if it can't be parsed. */
|
|
99
129
|
parseNavigationUrl(url) {
|
|
100
130
|
try {
|
|
101
131
|
const base = this.isBrowserRuntime() ? window.location.href || `${window.location.origin}/` : this.getOrigin();
|
|
@@ -105,7 +135,7 @@ class Router {
|
|
|
105
135
|
return null;
|
|
106
136
|
}
|
|
107
137
|
}
|
|
108
|
-
/**
|
|
138
|
+
/** In a browser, reduce a URL to its path, query, and hash, or `null` if it is cross-origin or invalid. */
|
|
109
139
|
normalizeBrowserUrl(url) {
|
|
110
140
|
if (!this.isBrowserRuntime()) {
|
|
111
141
|
return url;
|
|
@@ -116,35 +146,81 @@ class Router {
|
|
|
116
146
|
}
|
|
117
147
|
return `${parsedUrl.pathname}${parsedUrl.search}${parsedUrl.hash}`;
|
|
118
148
|
}
|
|
119
|
-
/**
|
|
149
|
+
/** True if the route has something to run: a function, a `view` function, or a `view.initialize`. */
|
|
120
150
|
isRunnableRoute(route) {
|
|
121
151
|
return typeof route === 'function' || typeof route.view === 'function' ||
|
|
122
152
|
Boolean(route.view && typeof route.view.initialize === 'function');
|
|
123
153
|
}
|
|
124
|
-
/**
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
154
|
+
/** Build (or reuse) the lookup tables for the current route keys. Reused while the own key list is unchanged. */
|
|
155
|
+
compileRoutes() {
|
|
156
|
+
const keys = Object.keys(this.routes);
|
|
157
|
+
const cached = this.compiledRoutes;
|
|
158
|
+
if (cached && cached.keys.length === keys.length && cached.keys.every((key, index) => key === keys[index])) {
|
|
159
|
+
return cached;
|
|
160
|
+
}
|
|
161
|
+
const prefixes = new Map();
|
|
162
|
+
const prefixFor = new Map();
|
|
163
|
+
for (const key of keys) {
|
|
164
|
+
if (key === 'defaultRoute') {
|
|
129
165
|
continue;
|
|
130
166
|
}
|
|
131
|
-
|
|
132
|
-
|
|
167
|
+
const prefix = normalizeRouteKey(key);
|
|
168
|
+
prefixFor.set(key, prefix);
|
|
169
|
+
const existing = prefixes.get(prefix);
|
|
170
|
+
// When two keys normalize to the same path, prefer the one already in normalized form, then the alphabetically
|
|
171
|
+
// lowest, so the winner never depends on insertion order.
|
|
172
|
+
if (existing === undefined || (existing !== prefix && (key === prefix || key < existing))) {
|
|
173
|
+
prefixes.set(prefix, key);
|
|
133
174
|
}
|
|
134
175
|
}
|
|
135
|
-
|
|
176
|
+
this.compiledRoutes = { keys, prefixes, prefixFor };
|
|
177
|
+
return this.compiledRoutes;
|
|
178
|
+
}
|
|
179
|
+
/** Find the route key with the longest matching path prefix, else `'defaultRoute'` if one is set, else `null`. */
|
|
180
|
+
matchRoute(pathname) {
|
|
181
|
+
const { prefixes } = this.compileRoutes();
|
|
182
|
+
// Try the full path, then drop one '/segment' at a time; the first hit is the longest match.
|
|
183
|
+
// Cutting only at '/' means '/products' matches '/products/42' but not '/products-old'.
|
|
184
|
+
let candidateRoute = prefixes.get(pathname);
|
|
185
|
+
let index = pathname.lastIndexOf('/');
|
|
186
|
+
while (candidateRoute === undefined && index >= 0) {
|
|
187
|
+
candidateRoute = prefixes.get(pathname.slice(0, index));
|
|
188
|
+
index = index > 0 ? pathname.lastIndexOf('/', index - 1) : -1;
|
|
189
|
+
}
|
|
190
|
+
if (candidateRoute !== undefined) {
|
|
191
|
+
return candidateRoute;
|
|
192
|
+
}
|
|
193
|
+
if (Object.prototype.hasOwnProperty.call(this.routes, 'defaultRoute') && this.routes.defaultRoute) {
|
|
136
194
|
return 'defaultRoute';
|
|
137
195
|
}
|
|
138
|
-
return
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
/** Length of the normalized path for a route key, used to slice the matched prefix off `data.url`; 0 for `defaultRoute`. */
|
|
199
|
+
routePrefixLength(route) {
|
|
200
|
+
if (!route || route === 'defaultRoute') {
|
|
201
|
+
return 0;
|
|
202
|
+
}
|
|
203
|
+
return (this.compileRoutes().prefixFor.get(route) ?? normalizeRouteKey(route)).length;
|
|
139
204
|
}
|
|
140
|
-
/**
|
|
205
|
+
/** Parse a URL Router committed earlier. A path is prefixed with the origin first, so '//x' stays a path, not a host. */
|
|
206
|
+
parseStoredUrl(url) {
|
|
207
|
+
if (!url.startsWith('/')) {
|
|
208
|
+
return this.parseNavigationUrl(url);
|
|
209
|
+
}
|
|
210
|
+
try {
|
|
211
|
+
return new URL(`${this.getOrigin()}${url}`);
|
|
212
|
+
}
|
|
213
|
+
catch (_error) {
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
/** Build the location object passed to route callbacks. Path segments that fail to decode are kept as-is. */
|
|
141
218
|
buildLocationData(url, route, mediatorData, parsedUrl) {
|
|
142
|
-
const matchedRoute = route && route !== 'defaultRoute' ? route : '';
|
|
143
219
|
return {
|
|
144
220
|
url,
|
|
145
221
|
data: {
|
|
146
222
|
url: parsedUrl.pathname
|
|
147
|
-
.slice(
|
|
223
|
+
.slice(this.routePrefixLength(route))
|
|
148
224
|
.split('/')
|
|
149
225
|
.filter(Boolean)
|
|
150
226
|
.map((fragment) => {
|
|
@@ -161,15 +237,27 @@ class Router {
|
|
|
161
237
|
};
|
|
162
238
|
}
|
|
163
239
|
/**
|
|
164
|
-
*
|
|
165
|
-
*
|
|
240
|
+
* Attach listeners and dispatch the starting URL. Does not add a history entry or move focus.
|
|
241
|
+
* @param url - Request URL on a server. In a browser, omit it to use `window.location`.
|
|
242
|
+
* @returns This router.
|
|
243
|
+
* @throws Whatever a guard or route callback throws.
|
|
166
244
|
*/
|
|
167
245
|
initialize(url) {
|
|
168
246
|
this.addListeners();
|
|
169
|
-
|
|
247
|
+
// The browser has already placed focus on the page it just loaded, so only later navigations move it.
|
|
248
|
+
this.initialDispatch = true;
|
|
249
|
+
try {
|
|
250
|
+
this.navigate(url ?? this.getCurrentUrl(), {}, true);
|
|
251
|
+
}
|
|
252
|
+
finally {
|
|
253
|
+
this.initialDispatch = false;
|
|
254
|
+
}
|
|
170
255
|
return this;
|
|
171
256
|
}
|
|
172
|
-
/**
|
|
257
|
+
/**
|
|
258
|
+
* Attach the click and popstate listeners (browser only) and the mediator listener. Safe to call more than once.
|
|
259
|
+
* @returns This router.
|
|
260
|
+
*/
|
|
173
261
|
addListeners() {
|
|
174
262
|
if (this.listenersInitialized) {
|
|
175
263
|
return this;
|
|
@@ -187,12 +275,18 @@ class Router {
|
|
|
187
275
|
this.listenersInitialized = true;
|
|
188
276
|
return this;
|
|
189
277
|
}
|
|
190
|
-
/**
|
|
278
|
+
/**
|
|
279
|
+
* Remove this router's listeners. Does not call the active route's `destroy()`.
|
|
280
|
+
* @returns This router.
|
|
281
|
+
*/
|
|
191
282
|
destroy() {
|
|
192
283
|
this.removeListeners();
|
|
193
284
|
return this;
|
|
194
285
|
}
|
|
195
|
-
/**
|
|
286
|
+
/**
|
|
287
|
+
* Remove the listeners `addListeners()` attached. Subclasses can override this to release more.
|
|
288
|
+
* @returns This router.
|
|
289
|
+
*/
|
|
196
290
|
removeListeners() {
|
|
197
291
|
if (!this.listenersInitialized) {
|
|
198
292
|
return this;
|
|
@@ -209,7 +303,15 @@ class Router {
|
|
|
209
303
|
this.listenersInitialized = false;
|
|
210
304
|
return this;
|
|
211
305
|
}
|
|
212
|
-
/**
|
|
306
|
+
/**
|
|
307
|
+
* Click handler for `<a href data-pushstate>` links. Takes over only when Router can handle the link:
|
|
308
|
+
* a plain left click on a same-origin link (no `download`, no other `target`) whose route is app-defined,
|
|
309
|
+
* runnable, and allowed by its guard, and that is not just a `#fragment` of the current page. Anything else is left
|
|
310
|
+
* to the browser.
|
|
311
|
+
* @param e - The click event.
|
|
312
|
+
* @returns This router if it handled the click, otherwise `true`.
|
|
313
|
+
* @throws Whatever a guard or route callback throws.
|
|
314
|
+
*/
|
|
213
315
|
eventPushStateClick(e) {
|
|
214
316
|
if (typeof window === 'undefined') {
|
|
215
317
|
return true;
|
|
@@ -227,7 +329,8 @@ class Router {
|
|
|
227
329
|
if (anchor.hasAttribute('download') || (target && target.toLowerCase() !== '_self')) {
|
|
228
330
|
return true;
|
|
229
331
|
}
|
|
230
|
-
|
|
332
|
+
// Read the attribute, because on an SVG <a> the href property is an SVGAnimatedString, not a URL.
|
|
333
|
+
const href = anchor.getAttribute('href') ?? '';
|
|
231
334
|
const baseUrl = typeof document !== 'undefined' && document.baseURI
|
|
232
335
|
? document.baseURI
|
|
233
336
|
: window.location.href || `${window.location.origin}/`;
|
|
@@ -241,41 +344,74 @@ class Router {
|
|
|
241
344
|
if (location.origin !== window.location.origin) {
|
|
242
345
|
return true;
|
|
243
346
|
}
|
|
347
|
+
// A link to a #fragment of the page already shown is native fragment navigation: let the browser scroll to it.
|
|
348
|
+
// Its popstate then only updates `url`, matching the fragment-only back/forward rule.
|
|
349
|
+
if (location.href.includes('#') && location.pathname === window.location.pathname && location.search === window.location.search) {
|
|
350
|
+
return true;
|
|
351
|
+
}
|
|
244
352
|
const candidateRoute = this.matchRoute(location.pathname);
|
|
245
353
|
const selected = candidateRoute ? this.routes[candidateRoute] : undefined;
|
|
246
|
-
if (!selected || !this.isRunnableRoute(selected)) {
|
|
354
|
+
if (!selected || !this.isRunnableRoute(selected) || builtInDefaultRoutes.has(selected)) {
|
|
247
355
|
return true;
|
|
248
356
|
}
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
357
|
+
// Guards are synchronous, so try the navigation first and cancel the browser's default only if Router took it.
|
|
358
|
+
// A rejected (or throwing) guard therefore falls back to a normal page load. If the route committed and then
|
|
359
|
+
// threw, the token has moved, so the default is still cancelled and the browser doesn't navigate twice.
|
|
360
|
+
const token = this.navigationToken;
|
|
361
|
+
let handled = false;
|
|
362
|
+
try {
|
|
363
|
+
handled = this.navigate(`${location.pathname}${location.search}${location.hash}`, {}, false) !== false;
|
|
364
|
+
}
|
|
365
|
+
finally {
|
|
366
|
+
if (handled || this.navigationToken !== token) {
|
|
367
|
+
e.preventDefault();
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
return handled ? this : true;
|
|
252
371
|
}
|
|
253
|
-
/**
|
|
372
|
+
/**
|
|
373
|
+
* Popstate handler for back/forward. Runs the route for the new URL without adding a history entry.
|
|
374
|
+
* A hash-only change just updates `url` and `locationData.url`.
|
|
375
|
+
* @returns This router.
|
|
376
|
+
*/
|
|
254
377
|
eventPopState() {
|
|
255
378
|
if (typeof window === 'undefined') {
|
|
256
379
|
return this;
|
|
257
380
|
}
|
|
258
|
-
this.
|
|
381
|
+
const currentUrl = this.getCurrentUrl();
|
|
382
|
+
// Moving between #fragments on the same page (footnotes, skip links) keeps the current view and focus.
|
|
383
|
+
if (this.route !== null && withoutHash(currentUrl) === withoutHash(this.url)) {
|
|
384
|
+
this.url = currentUrl;
|
|
385
|
+
this.locationData = { ...this.locationData, url: currentUrl };
|
|
386
|
+
return this;
|
|
387
|
+
}
|
|
388
|
+
this.navigate(currentUrl, {}, true);
|
|
259
389
|
return this;
|
|
260
390
|
}
|
|
261
|
-
/**
|
|
391
|
+
/**
|
|
392
|
+
* Decode a query string into a plain object. For repeated keys the last value wins.
|
|
393
|
+
* @param queryString - Query string, with or without the leading `?`.
|
|
394
|
+
*/
|
|
262
395
|
parseQueryString(queryString) {
|
|
263
396
|
return Object.fromEntries(new URLSearchParams(queryString));
|
|
264
397
|
}
|
|
265
|
-
/**
|
|
398
|
+
/**
|
|
399
|
+
* Rebuild `locationData` from the current `url` and `route`. Leaves it unchanged if the URL can't be parsed.
|
|
400
|
+
* @param mediatorData - Value to expose as `locationData.data.mediator`.
|
|
401
|
+
*/
|
|
266
402
|
setLocationData(mediatorData) {
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
try {
|
|
270
|
-
const value = this.isBrowserRuntime() ? `${window.location.origin}${url}` : url;
|
|
271
|
-
parsedUrl = new URL(value, this.getOrigin());
|
|
272
|
-
}
|
|
273
|
-
catch (_error) {
|
|
403
|
+
const parsedUrl = this.parseStoredUrl(this.url || '/');
|
|
404
|
+
if (!parsedUrl) {
|
|
274
405
|
return;
|
|
275
406
|
}
|
|
276
407
|
this.locationData = this.buildLocationData(this.url, this.route, mediatorData, parsedUrl);
|
|
277
408
|
}
|
|
278
|
-
/**
|
|
409
|
+
/**
|
|
410
|
+
* Record the route's title in `pageTitle` and, in a browser, set `document.title` and move focus to the route's
|
|
411
|
+
* `focus` selector (default `main h1`), adding `tabindex="-1"` if needed. `focus: false` skips focus.
|
|
412
|
+
* @param route - The route that just ran.
|
|
413
|
+
* @returns This router.
|
|
414
|
+
*/
|
|
279
415
|
applyPageContext(route) {
|
|
280
416
|
if (typeof route === 'function') {
|
|
281
417
|
this.pageTitle = null;
|
|
@@ -288,7 +424,7 @@ class Router {
|
|
|
288
424
|
if (this.pageTitle) {
|
|
289
425
|
document.title = this.pageTitle;
|
|
290
426
|
}
|
|
291
|
-
if (route.focus !== false) {
|
|
427
|
+
if (route.focus !== false && !this.suppressFocus) {
|
|
292
428
|
const target = document.querySelector(route.focus || 'main h1');
|
|
293
429
|
if (target) {
|
|
294
430
|
if (!target.hasAttribute('tabindex')) {
|
|
@@ -299,25 +435,39 @@ class Router {
|
|
|
299
435
|
}
|
|
300
436
|
return this;
|
|
301
437
|
}
|
|
302
|
-
/**
|
|
438
|
+
/**
|
|
439
|
+
* Navigate to a URL: match the route, run its guard, commit the new state, call the previous route's `destroy()`,
|
|
440
|
+
* run the new route, apply title and focus, then push a history entry (browser only, unless `isPopState`).
|
|
441
|
+
* If route code starts another navigation part-way through, that one wins and this call stops early.
|
|
442
|
+
* @param url - Destination URL. Omit to re-run the last committed URL (or the current browser URL / `/` at first).
|
|
443
|
+
* @param mediatorData - Value exposed to the route as `location.data.mediator`.
|
|
444
|
+
* @param isPopState - `true` to skip adding a history entry.
|
|
445
|
+
* @returns This router, or `false` if the navigation was rejected (bad or cross-origin URL, no matching route,
|
|
446
|
+
* guard refused, or nothing to run). A rejected navigation changes nothing.
|
|
447
|
+
* @throws Whatever a guard or route callback throws. A throwing guard changes nothing; a throwing route stays committed.
|
|
448
|
+
*/
|
|
303
449
|
navigate(url, mediatorData, isPopState = false) {
|
|
450
|
+
const skipFocus = this.initialDispatch;
|
|
451
|
+
this.initialDispatch = false;
|
|
304
452
|
if (url !== undefined && typeof url !== 'string') {
|
|
305
453
|
return false;
|
|
306
454
|
}
|
|
307
|
-
|
|
455
|
+
const candidateInput = url !== undefined ? url : this.url;
|
|
456
|
+
let parsedUrl;
|
|
308
457
|
if (!candidateInput) {
|
|
309
|
-
|
|
458
|
+
parsedUrl = this.parseNavigationUrl(this.getCurrentUrl());
|
|
459
|
+
}
|
|
460
|
+
else {
|
|
461
|
+
parsedUrl = url === undefined ? this.parseStoredUrl(candidateInput) : this.parseNavigationUrl(candidateInput);
|
|
310
462
|
}
|
|
311
|
-
const parsedUrl = this.parseNavigationUrl(candidateInput);
|
|
312
463
|
if (!parsedUrl) {
|
|
313
464
|
return false;
|
|
314
465
|
}
|
|
315
466
|
if (this.isBrowserRuntime() && parsedUrl.origin !== window.location.origin) {
|
|
316
467
|
return false;
|
|
317
468
|
}
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
: candidateInput;
|
|
469
|
+
// Store only path, query, and hash, even when a server passed an absolute URL.
|
|
470
|
+
const candidateUrl = `${parsedUrl.pathname}${parsedUrl.search}${parsedUrl.hash}`;
|
|
321
471
|
const candidateRoute = this.matchRoute(parsedUrl.pathname);
|
|
322
472
|
if (!candidateRoute) {
|
|
323
473
|
return false;
|
|
@@ -328,34 +478,54 @@ class Router {
|
|
|
328
478
|
!this.isRunnableRoute(selected)) {
|
|
329
479
|
return false;
|
|
330
480
|
}
|
|
481
|
+
const token = ++this.navigationToken;
|
|
331
482
|
this.url = candidateUrl;
|
|
332
483
|
this.route = candidateRoute;
|
|
333
484
|
this.locationData = candidateLocationData;
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
if (
|
|
341
|
-
|
|
485
|
+
// Clear the stored teardown before calling it, so a navigation started inside destroy() can't call it again,
|
|
486
|
+
// and a destroy() that throws is not retried: its error propagates with the destination committed but not run.
|
|
487
|
+
const previous = this.activeDestroy;
|
|
488
|
+
this.activeDestroy = undefined;
|
|
489
|
+
if (previous) {
|
|
490
|
+
previous.callback.call(previous.receiver, this.scope, candidateLocationData);
|
|
491
|
+
if (token !== this.navigationToken) {
|
|
492
|
+
return this;
|
|
342
493
|
}
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
this.
|
|
353
|
-
|
|
354
|
-
|
|
494
|
+
}
|
|
495
|
+
const selectedView = typeof selected !== 'function' && typeof selected.view === 'object'
|
|
496
|
+
? selected.view
|
|
497
|
+
: undefined;
|
|
498
|
+
// Store the teardown before initialize() runs, so it still runs later if initialize() throws or navigates away.
|
|
499
|
+
this.activeDestroy = selectedView?.destroy
|
|
500
|
+
? { receiver: selectedView, callback: selectedView.destroy }
|
|
501
|
+
: undefined;
|
|
502
|
+
if (typeof selected === 'function') {
|
|
503
|
+
selected(this.scope, candidateLocationData);
|
|
504
|
+
}
|
|
505
|
+
else if (typeof selected.view === 'function') {
|
|
506
|
+
selected.view(this.scope, candidateLocationData);
|
|
507
|
+
}
|
|
508
|
+
else {
|
|
509
|
+
selectedView.initialize(this.scope, candidateLocationData);
|
|
510
|
+
}
|
|
511
|
+
// If initialize() started another navigation, that one has already applied its own title, focus, and history.
|
|
512
|
+
if (token !== this.navigationToken) {
|
|
513
|
+
return this;
|
|
514
|
+
}
|
|
515
|
+
this.suppressFocus = skipFocus;
|
|
516
|
+
try {
|
|
355
517
|
this.applyPageContext(selected);
|
|
356
|
-
this.previousRoute = this.route;
|
|
357
518
|
}
|
|
358
|
-
|
|
519
|
+
finally {
|
|
520
|
+
this.suppressFocus = false;
|
|
521
|
+
}
|
|
522
|
+
if (token !== this.navigationToken) {
|
|
523
|
+
return this;
|
|
524
|
+
}
|
|
525
|
+
this.previousRoute = this.route;
|
|
526
|
+
// Re-running the current URL (no `url` given, e.g. a mediator event without a destination) must not add a
|
|
527
|
+
// duplicate history entry for the address already shown.
|
|
528
|
+
if (!isPopState && this.isBrowserRuntime() && !(url === undefined && parsedUrl.href === window.location.href)) {
|
|
359
529
|
window.history.pushState(this.url, this.pageTitle || '', parsedUrl.href);
|
|
360
530
|
}
|
|
361
531
|
return this;
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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","sourcesContent":["/** Navigation payload, including caller-defined metadata. */\ninterface NavigationData {url?: string; [key: string]: unknown}\n/** URL-derived values supplied to route handlers. */\ninterface LocationData {url: string; data: {url: string[]; mediator: NavigationData | undefined; query: Record<string, string>}}\ntype RouteHandler = (scope: Element | null, location: LocationData) => unknown;\ninterface RouteViewLifecycle {initialize?: RouteHandler; destroy?: RouteHandler}\n/** An object route can own a function or a view lifecycle. */\ninterface RouteObject {title?: string; focus?: string | false; secure?: RouteHandler; view?: RouteHandler | RouteViewLifecycle}\ntype Route = RouteHandler | RouteObject;\ninterface ActiveDestroy {receiver: RouteViewLifecycle; callback: RouteHandler}\n/** Minimal standards-based event target contract required by the router. */\ninterface NavigationMediator {\n addEventListener(type: string, callback: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;\n removeEventListener(type: string, callback: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;\n}\n/** History API router that also dispatches request URLs in non-browser runtimes. */\nclass Router {\n routes: Record<string, Route>;\n url: string;\n route: string | null;\n scope: Element | null;\n previousRoute: string | null;\n pageTitle: string | null;\n boundPushStateClick: (event: MouseEvent) => unknown;\n boundPopState: (event: PopStateEvent) => unknown;\n boundMediatorNavigate: EventListener;\n listenersInitialized: boolean;\n locationData: LocationData = {url: '', data: {url: [], mediator: undefined, query: {}}};\n private currentMediator: NavigationMediator | false = false;\n private boundMediator: NavigationMediator | false = false;\n private currentNavigationRoot: Document | Element | null = null;\n private boundNavigationRoot: Document | Element | null = null;\n private activeDestroy: ActiveDestroy | undefined;\n\n /** Create an instance with its own state and listener references. */\n constructor() {\n this.routes = {\n defaultRoute: {\n view: {\n initialize: () => true,\n destroy: () => true\n }\n }\n };\n this.url = '';\n this.route = null;\n this.scope = null;\n this.previousRoute = null;\n this.pageTitle = null;\n this.boundPushStateClick = this.eventPushStateClick.bind(this);\n this.boundPopState = this.eventPopState.bind(this);\n this.boundMediatorNavigate = (event: Event) => {\n const detail = (event as CustomEvent<unknown>).detail;\n if (detail === undefined || detail === null) {\n this.navigate(undefined, {}, false);\n return;\n }\n if (typeof detail !== 'object' || Array.isArray(detail)) {return;}\n const data = detail as NavigationData;\n this.navigate(data.url, data, false);\n };\n this.listenersInitialized = false;\n }\n\n get mediator(): NavigationMediator | false {return this.currentMediator;}\n\n /** Move an active router:navigate subscription when the mediator changes. */\n set mediator(value: NavigationMediator | false) {\n if (value === this.currentMediator) {return;}\n if (this.listenersInitialized && this.boundMediator) {\n this.boundMediator.removeEventListener('router:navigate', this.boundMediatorNavigate);\n this.boundMediator = false;\n }\n this.currentMediator = value;\n if (this.listenersInitialized && value) {\n value.addEventListener('router:navigate', this.boundMediatorNavigate);\n this.boundMediator = value;\n }\n }\n\n get navigationRoot(): Document | Element | null {return this.currentNavigationRoot;}\n\n /** Scope progressive link interception and move an active click listener when the root changes. */\n set navigationRoot(value: Document | Element | null) {\n if (value === this.currentNavigationRoot) {return;}\n if (this.listenersInitialized && this.isBrowserRuntime()) {\n this.boundNavigationRoot?.removeEventListener('click', this.boundPushStateClick as EventListener);\n this.currentNavigationRoot = value;\n const nextRoot = value || document;\n nextRoot.addEventListener('click', this.boundPushStateClick as EventListener);\n this.boundNavigationRoot = nextRoot;\n return;\n }\n this.currentNavigationRoot = value;\n }\n\n /** True when browser navigation APIs are available. */\n private isBrowserRuntime() {\n return typeof window !== 'undefined' && typeof document !== 'undefined';\n }\n\n /** Stable base URL for WHATWG URL parsing in browser and server runtimes. */\n private getOrigin() {\n return this.isBrowserRuntime() ? window.location.origin : 'http://white-label.invalid';\n }\n\n /** Read the authoritative browser URL, or retain the supplied server URL. */\n private getCurrentUrl() {\n if (!this.isBrowserRuntime()) {return this.url || '/';}\n return window.location.pathname + window.location.search + (window.location.hash || '');\n }\n\n /** Parse a navigation candidate without changing Router state. */\n private parseNavigationUrl(url: string) {\n try {\n const base = this.isBrowserRuntime() ? window.location.href || `${window.location.origin}/` : this.getOrigin();\n return new URL(url || '/', base);\n } catch (_error) {\n return null;\n }\n }\n\n /** Normalize browser navigation to a same-origin path, query, and hash. */\n private normalizeBrowserUrl(url: string) {\n if (!this.isBrowserRuntime()) {return url;}\n const parsedUrl = this.parseNavigationUrl(url);\n if (!parsedUrl || parsedUrl.origin !== window.location.origin) {return null;}\n return `${parsedUrl.pathname}${parsedUrl.search}${parsedUrl.hash}`;\n }\n\n /** Return whether a selected route has runnable view behavior. */\n private isRunnableRoute(route: Route): boolean {\n return typeof route === 'function' || typeof route.view === 'function' ||\n Boolean(route.view && typeof route.view.initialize === 'function');\n }\n /** Return the longest matching own route-table prefix, falling back to defaultRoute when configured. */\n private matchRoute(pathname: string): string | null {\n let candidateRoute: string | null = null;\n for (const route of Object.keys(this.routes)) {\n if (route === 'defaultRoute' || (pathname !== route && !pathname.startsWith(`${route}/`))) {continue;}\n if (!candidateRoute || route.length > candidateRoute.length) {candidateRoute = route;}\n }\n if (!candidateRoute && Object.prototype.hasOwnProperty.call(this.routes, 'defaultRoute') && this.routes.defaultRoute) {\n return 'defaultRoute';\n }\n return candidateRoute;\n }\n\n\n /** Build decoded path, query, and mediator data for a navigation candidate. */\n private buildLocationData(url: string, route: string | null, mediatorData: NavigationData | undefined, parsedUrl: URL): LocationData {\n const matchedRoute = route && route !== 'defaultRoute' ? route : '';\n return {\n url,\n data: {\n url: parsedUrl.pathname\n .slice(matchedRoute.length)\n .split('/')\n .filter(Boolean)\n .map((fragment) => {\n try {return decodeURIComponent(fragment);} catch (_error) {return fragment;}\n }),\n mediator: mediatorData,\n query: this.parseQueryString(parsedUrl.search)\n }\n };\n }\n\n /**\n * Start this instance and return it for lifecycle chaining.\n * In a browser the URL is optional and defaults to window.location. On a server pass the request URL.\n */\n initialize(url?: string) {\n this.addListeners();\n this.navigate(url ?? this.getCurrentUrl(), {}, true);\n return this;\n }\n\n /** Register browser and optional mediator handlers once. */\n addListeners() {\n if (this.listenersInitialized) {return this;}\n if (this.isBrowserRuntime()) {\n const navigationRoot = this.currentNavigationRoot || document;\n navigationRoot.addEventListener('click', this.boundPushStateClick as EventListener);\n this.boundNavigationRoot = navigationRoot;\n window.addEventListener('popstate', this.boundPopState);\n }\n if (this.currentMediator) {\n this.currentMediator.addEventListener('router:navigate', this.boundMediatorNavigate);\n this.boundMediator = this.currentMediator;\n }\n this.listenersInitialized = true;\n return this;\n }\n\n /** Release owned state and listeners so the instance can leave the application lifecycle. */\n destroy() {\n this.removeListeners();\n return this;\n }\n\n /** Release listeners owned by this instance; subclasses may extend the lifecycle hook. */\n removeListeners() {\n if (!this.listenersInitialized) {return this;}\n if (this.isBrowserRuntime()) {\n this.boundNavigationRoot?.removeEventListener('click', this.boundPushStateClick as EventListener);\n this.boundNavigationRoot = null;\n window.removeEventListener('popstate', this.boundPopState);\n }\n if (this.boundMediator) {\n this.boundMediator.removeEventListener('router:navigate', this.boundMediatorNavigate);\n this.boundMediator = false;\n }\n this.listenersInitialized = false;\n return this;\n }\n\n /** Intercept eligible same-origin push-state links while preserving normal browser actions. */\n eventPushStateClick(e: MouseEvent) {\n if (typeof window === 'undefined') {return true;}\n if (e.defaultPrevented || (e.button !== undefined && e.button !== 0) || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {\n return true;\n }\n const eventTarget = e.target as Element | null;\n const source = eventTarget && (typeof eventTarget.closest === 'function' ? eventTarget : eventTarget.parentElement);\n const anchor = source && typeof source.closest === 'function' ? source.closest('a[href][data-pushstate]') : null;\n if (!anchor) {return true;}\n const target = anchor.getAttribute('target');\n if (anchor.hasAttribute('download') || (target && target.toLowerCase() !== '_self')) {return true;}\n const href = (anchor as HTMLAnchorElement).href || anchor.getAttribute('href') || '';\n const baseUrl = typeof document !== 'undefined' && document.baseURI\n ? document.baseURI\n : window.location.href || `${window.location.origin}/`;\n let location: URL;\n try {location = new URL(href, baseUrl);} catch {return true;}\n if (location.origin !== window.location.origin) {return true;}\n const candidateRoute = this.matchRoute(location.pathname);\n const selected = candidateRoute ? this.routes[candidateRoute] : undefined;\n if (!selected || !this.isRunnableRoute(selected)) {return true;}\n e.preventDefault();\n this.navigate(`${location.pathname}${location.search}${location.hash}`, {}, false);\n return this;\n }\n\n /** Restore browser history state without adding a new history entry. */\n eventPopState() {\n if (typeof window === 'undefined') {return this;}\n this.navigate(this.getCurrentUrl(), {}, true);\n return this;\n }\n\n /** Decode URL query parameters, keeping the last value for duplicate keys. */\n parseQueryString(queryString: string) {\n return Object.fromEntries(new URLSearchParams(queryString));\n }\n\n /** Build decoded path, query, and mediator data for the selected route. */\n setLocationData(mediatorData?: NavigationData) {\n const url = this.url || '/';\n let parsedUrl: URL;\n try {\n const value = this.isBrowserRuntime() ? `${window.location.origin}${url}` : url;\n parsedUrl = new URL(value, this.getOrigin());\n } catch (_error) {\n return;\n }\n this.locationData = this.buildLocationData(this.url, this.route, mediatorData, parsedUrl);\n }\n\n /** Update page context after a route renders. DOM effects are browser-only. */\n applyPageContext(route: Route) {\n if (typeof route === 'function') {\n this.pageTitle = null;\n return this;\n }\n this.pageTitle = typeof route.title === 'string' ? route.title : null;\n if (!this.isBrowserRuntime()) {return this;}\n if (this.pageTitle) {document.title = this.pageTitle;}\n if (route.focus !== false) {\n const target = document.querySelector<HTMLElement>(route.focus || 'main h1');\n if (target) {\n if (!target.hasAttribute('tabindex')) {target.setAttribute('tabindex', '-1');}\n target.focus();\n }\n }\n return this;\n }\n\n /** Select a route, enforce its guard, transition view lifecycles, and update browser history when available. */\n navigate(url?: string, mediatorData?: NavigationData, isPopState = false) {\n if (url !== undefined && typeof url !== 'string') {return false;}\n\n let candidateInput = url !== undefined ? url : this.url;\n if (!candidateInput) {candidateInput = this.getCurrentUrl();}\n const parsedUrl = this.parseNavigationUrl(candidateInput);\n if (!parsedUrl) {return false;}\n if (this.isBrowserRuntime() && parsedUrl.origin !== window.location.origin) {return false;}\n\n const candidateUrl = this.isBrowserRuntime()\n ? `${parsedUrl.pathname}${parsedUrl.search}${parsedUrl.hash}`\n : candidateInput;\n const candidateRoute = this.matchRoute(parsedUrl.pathname);\n if (!candidateRoute) {return false;}\n\n const candidateLocationData = this.buildLocationData(candidateUrl, candidateRoute, mediatorData, parsedUrl);\n const selected = this.routes[candidateRoute];\n if (!selected || (typeof selected !== 'function' && selected.secure && selected.secure(this.scope, candidateLocationData) !== true) ||\n !this.isRunnableRoute(selected)) {\n return false;\n }\n\n this.url = candidateUrl;\n this.route = candidateRoute;\n this.locationData = candidateLocationData;\n if (selected) {\n if (this.activeDestroy) {\n const {receiver, callback} = this.activeDestroy;\n callback.call(receiver, this.scope, this.locationData);\n this.activeDestroy = undefined;\n }\n if (typeof selected === 'function') {\n selected(this.scope, this.locationData);\n } else if (typeof selected.view === 'function') {\n selected.view(this.scope, this.locationData);\n } else {\n selected.view!.initialize!(this.scope, this.locationData);\n }\n const selectedView = typeof selected !== 'function' && typeof selected.view === 'object'\n ? selected.view\n : undefined;\n this.activeDestroy = selectedView?.destroy\n ? {receiver: selectedView, callback: selectedView.destroy}\n : undefined;\n this.applyPageContext(selected);\n this.previousRoute = this.route;\n }\n if (!isPopState && this.isBrowserRuntime()) {\n window.history.pushState(this.url, this.pageTitle || '', parsedUrl.href);\n }\n return this;\n }\n}\n\n/** Public route configuration and navigation payload types. */\nnamespace Router {\n export type Handler = RouteHandler;\n export type Route = RouteHandler | RouteObject;\n export type Location = LocationData;\n export type Navigation = NavigationData;\n}\nexport = Router;\n"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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","sourcesContent":["/** Extra data passed with a navigation. `url`, if present, is the destination; any other fields are yours. */\ninterface NavigationData {url?: string; [key: string]: unknown}\n/**\n * What a route callback receives about the destination.\n * `url` is the path, query, and hash; `data.url` holds the decoded path segments after the matched route key;\n * `data.query` holds decoded query values; `data.mediator` is the navigation data, if any.\n */\ninterface LocationData {url: string; data: {url: string[]; mediator: NavigationData | undefined; query: Record<string, string>}}\n/** A route callback. It receives `router.scope` and the destination location. */\ntype RouteHandler = (scope: Element | null, location: LocationData) => unknown;\n/** A view with setup and teardown. `destroy` receives the location being navigated to. */\ninterface RouteViewLifecycle {initialize?: RouteHandler; destroy?: RouteHandler}\n/**\n * A route with optional page context and a guard.\n * `secure` must return exactly `true` to allow navigation; `focus: false` leaves focus where it is.\n */\ninterface RouteObject {title?: string; focus?: string | false; secure?: RouteHandler; view?: RouteHandler | RouteViewLifecycle}\ntype Route = RouteHandler | RouteObject;\n/** The teardown captured from the view that actually initialized, so later route-table edits can't change it. */\ninterface ActiveDestroy {receiver: RouteViewLifecycle; callback: RouteHandler}\n/** Any EventTarget-like object; Router only adds and removes a `router:navigate` listener on it. */\ninterface NavigationMediator {\n addEventListener(type: string, callback: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;\n removeEventListener(type: string, callback: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;\n}\n/**\n * Route keys prepared for fast matching. `prefixes` maps a normalized path to the route key that owns it;\n * `prefixFor` maps each key to its normalized path. Rebuilt only when the list of own keys changes.\n */\ninterface CompiledRoutes {keys: string[]; prefixes: Map<string, string>; prefixFor: Map<string, string>}\n\n/** The do-nothing `defaultRoute` objects each constructor installs, so link clicks can tell them apart from app routes. */\nconst builtInDefaultRoutes = new WeakSet<object>();\n/** A dummy origin that lets route keys and server URLs go through the standard URL parser. */\nconst routeKeyBase = 'http://white-label.invalid';\n\n/** Encode a route key the way the URL parser encodes paths, so `'/café'` matches `/caf%C3%A9`. */\nfunction normalizeRouteKey(key: string) {\n // A key that doesn't start with '/' or contains '?' or '#' can never equal a pathname, so leave it as is (it won't match).\n if (!key.startsWith('/') || /[?#]/.test(key)) {return key;}\n return new URL(`${routeKeyBase}${key}`).pathname;\n}\n\n/** Strip the `#fragment`, leaving the path and query. */\nfunction withoutHash(url: string) {\n const index = url.indexOf('#');\n return index === -1 ? url : url.slice(0, index);\n}\n\n/**\n * Maps URLs to route callbacks. In a browser it enhances `data-pushstate` links and back/forward with the History API;\n * on a server it dispatches the request URL you pass, with no DOM required.\n */\nclass Router {\n routes: Record<string, Route>;\n url: string;\n route: string | null;\n scope: Element | null;\n previousRoute: string | null;\n pageTitle: string | null;\n boundPushStateClick: (event: MouseEvent) => unknown;\n boundPopState: (event: PopStateEvent) => unknown;\n boundMediatorNavigate: EventListener;\n listenersInitialized: boolean;\n locationData: LocationData = {url: '', data: {url: [], mediator: undefined, query: {}}};\n private currentMediator: NavigationMediator | false = false;\n private boundMediator: NavigationMediator | false = false;\n private currentNavigationRoot: Document | Element | null = null;\n private boundNavigationRoot: Document | Element | null = null;\n private activeDestroy: ActiveDestroy | undefined;\n private compiledRoutes: CompiledRoutes | undefined;\n // Bumped by each navigation that passes its guard. If it changes while a navigation is calling out to app code,\n // a newer navigation has started, and the older one stops before touching title, focus, or history.\n private navigationToken = 0;\n // True only for the first navigate() call made by initialize(), so that dispatch leaves focus alone.\n private initialDispatch = false;\n // Tells applyPageContext() to skip focus for the current call.\n private suppressFocus = false;\n\n /** Create a router with a placeholder `defaultRoute` and no listeners attached yet. */\n constructor() {\n const defaultRoute: RouteObject = {\n view: {\n initialize: () => true,\n destroy: () => true\n }\n };\n builtInDefaultRoutes.add(defaultRoute);\n this.routes = {defaultRoute};\n this.url = '';\n this.route = null;\n this.scope = null;\n this.previousRoute = null;\n this.pageTitle = null;\n this.boundPushStateClick = this.eventPushStateClick.bind(this);\n this.boundPopState = this.eventPopState.bind(this);\n this.boundMediatorNavigate = (event: Event) => {\n const detail = (event as CustomEvent<unknown>).detail;\n if (detail === undefined || detail === null) {\n this.navigate(undefined, {}, false);\n return;\n }\n if (typeof detail !== 'object' || Array.isArray(detail)) {return;}\n const data = detail as NavigationData;\n this.navigate(data.url, data, false);\n };\n this.listenersInitialized = false;\n }\n\n /** Event target that can send `router:navigate` events, or `false` for none. */\n get mediator(): NavigationMediator | false {return this.currentMediator;}\n\n /** Set the mediator. If listeners are active, the `router:navigate` listener moves from the old one to the new one. */\n set mediator(value: NavigationMediator | false) {\n if (value === this.currentMediator) {return;}\n if (this.listenersInitialized && this.boundMediator) {\n this.boundMediator.removeEventListener('router:navigate', this.boundMediatorNavigate);\n this.boundMediator = false;\n }\n this.currentMediator = value;\n if (this.listenersInitialized && value) {\n value.addEventListener('router:navigate', this.boundMediatorNavigate);\n this.boundMediator = value;\n }\n }\n\n /** Element or document whose `data-pushstate` link clicks this router handles; `null` means the whole document. */\n get navigationRoot(): Document | Element | null {return this.currentNavigationRoot;}\n\n /** Set the click root. If listeners are active, the click listener moves to the new root. */\n set navigationRoot(value: Document | Element | null) {\n if (value === this.currentNavigationRoot) {return;}\n if (this.listenersInitialized && this.isBrowserRuntime()) {\n this.boundNavigationRoot?.removeEventListener('click', this.boundPushStateClick as EventListener);\n this.currentNavigationRoot = value;\n const nextRoot = value || document;\n nextRoot.addEventListener('click', this.boundPushStateClick as EventListener);\n this.boundNavigationRoot = nextRoot;\n return;\n }\n this.currentNavigationRoot = value;\n }\n\n /** True when `window` and `document` exist, i.e. browser-only effects should run. */\n private isBrowserRuntime() {\n return typeof window !== 'undefined' && typeof document !== 'undefined';\n }\n\n /** Origin used as the base for URL parsing: the page's origin in a browser, a dummy one on a server. */\n private getOrigin() {\n return this.isBrowserRuntime() ? window.location.origin : 'http://white-label.invalid';\n }\n\n /** Current path, query, and hash: from `window.location` in a browser, or the last committed URL (default `/`) on a server. */\n private getCurrentUrl() {\n if (!this.isBrowserRuntime()) {return this.url || '/';}\n return window.location.pathname + window.location.search + (window.location.hash || '');\n }\n\n /** Parse a URL relative to the current page (or the dummy server origin). Returns `null` if it can't be parsed. */\n private parseNavigationUrl(url: string) {\n try {\n const base = this.isBrowserRuntime() ? window.location.href || `${window.location.origin}/` : this.getOrigin();\n return new URL(url || '/', base);\n } catch (_error) {\n return null;\n }\n }\n\n /** In a browser, reduce a URL to its path, query, and hash, or `null` if it is cross-origin or invalid. */\n private normalizeBrowserUrl(url: string) {\n if (!this.isBrowserRuntime()) {return url;}\n const parsedUrl = this.parseNavigationUrl(url);\n if (!parsedUrl || parsedUrl.origin !== window.location.origin) {return null;}\n return `${parsedUrl.pathname}${parsedUrl.search}${parsedUrl.hash}`;\n }\n\n /** True if the route has something to run: a function, a `view` function, or a `view.initialize`. */\n private isRunnableRoute(route: Route): boolean {\n return typeof route === 'function' || typeof route.view === 'function' ||\n Boolean(route.view && typeof route.view.initialize === 'function');\n }\n /** Build (or reuse) the lookup tables for the current route keys. Reused while the own key list is unchanged. */\n private compileRoutes(): CompiledRoutes {\n const keys = Object.keys(this.routes);\n const cached = this.compiledRoutes;\n if (cached && cached.keys.length === keys.length && cached.keys.every((key, index) => key === keys[index])) {\n return cached;\n }\n const prefixes = new Map<string, string>();\n const prefixFor = new Map<string, string>();\n for (const key of keys) {\n if (key === 'defaultRoute') {continue;}\n const prefix = normalizeRouteKey(key);\n prefixFor.set(key, prefix);\n const existing = prefixes.get(prefix);\n // When two keys normalize to the same path, prefer the one already in normalized form, then the alphabetically\n // lowest, so the winner never depends on insertion order.\n if (existing === undefined || (existing !== prefix && (key === prefix || key < existing))) {prefixes.set(prefix, key);}\n }\n this.compiledRoutes = {keys, prefixes, prefixFor};\n return this.compiledRoutes;\n }\n\n /** Find the route key with the longest matching path prefix, else `'defaultRoute'` if one is set, else `null`. */\n private matchRoute(pathname: string): string | null {\n const {prefixes} = this.compileRoutes();\n // Try the full path, then drop one '/segment' at a time; the first hit is the longest match.\n // Cutting only at '/' means '/products' matches '/products/42' but not '/products-old'.\n let candidateRoute = prefixes.get(pathname);\n let index = pathname.lastIndexOf('/');\n while (candidateRoute === undefined && index >= 0) {\n candidateRoute = prefixes.get(pathname.slice(0, index));\n index = index > 0 ? pathname.lastIndexOf('/', index - 1) : -1;\n }\n if (candidateRoute !== undefined) {return candidateRoute;}\n if (Object.prototype.hasOwnProperty.call(this.routes, 'defaultRoute') && this.routes.defaultRoute) {\n return 'defaultRoute';\n }\n return null;\n }\n\n /** Length of the normalized path for a route key, used to slice the matched prefix off `data.url`; 0 for `defaultRoute`. */\n private routePrefixLength(route: string | null) {\n if (!route || route === 'defaultRoute') {return 0;}\n return (this.compileRoutes().prefixFor.get(route) ?? normalizeRouteKey(route)).length;\n }\n\n /** Parse a URL Router committed earlier. A path is prefixed with the origin first, so '//x' stays a path, not a host. */\n private parseStoredUrl(url: string) {\n if (!url.startsWith('/')) {return this.parseNavigationUrl(url);}\n try {\n return new URL(`${this.getOrigin()}${url}`);\n } catch (_error) {\n return null;\n }\n }\n\n /** Build the location object passed to route callbacks. Path segments that fail to decode are kept as-is. */\n private buildLocationData(url: string, route: string | null, mediatorData: NavigationData | undefined, parsedUrl: URL): LocationData {\n return {\n url,\n data: {\n url: parsedUrl.pathname\n .slice(this.routePrefixLength(route))\n .split('/')\n .filter(Boolean)\n .map((fragment) => {\n try {return decodeURIComponent(fragment);} catch (_error) {return fragment;}\n }),\n mediator: mediatorData,\n query: this.parseQueryString(parsedUrl.search)\n }\n };\n }\n\n /**\n * Attach listeners and dispatch the starting URL. Does not add a history entry or move focus.\n * @param url - Request URL on a server. In a browser, omit it to use `window.location`.\n * @returns This router.\n * @throws Whatever a guard or route callback throws.\n */\n initialize(url?: string) {\n this.addListeners();\n // The browser has already placed focus on the page it just loaded, so only later navigations move it.\n this.initialDispatch = true;\n try {\n this.navigate(url ?? this.getCurrentUrl(), {}, true);\n } finally {\n this.initialDispatch = false;\n }\n return this;\n }\n\n /**\n * Attach the click and popstate listeners (browser only) and the mediator listener. Safe to call more than once.\n * @returns This router.\n */\n addListeners() {\n if (this.listenersInitialized) {return this;}\n if (this.isBrowserRuntime()) {\n const navigationRoot = this.currentNavigationRoot || document;\n navigationRoot.addEventListener('click', this.boundPushStateClick as EventListener);\n this.boundNavigationRoot = navigationRoot;\n window.addEventListener('popstate', this.boundPopState);\n }\n if (this.currentMediator) {\n this.currentMediator.addEventListener('router:navigate', this.boundMediatorNavigate);\n this.boundMediator = this.currentMediator;\n }\n this.listenersInitialized = true;\n return this;\n }\n\n /**\n * Remove this router's listeners. Does not call the active route's `destroy()`.\n * @returns This router.\n */\n destroy() {\n this.removeListeners();\n return this;\n }\n\n /**\n * Remove the listeners `addListeners()` attached. Subclasses can override this to release more.\n * @returns This router.\n */\n removeListeners() {\n if (!this.listenersInitialized) {return this;}\n if (this.isBrowserRuntime()) {\n this.boundNavigationRoot?.removeEventListener('click', this.boundPushStateClick as EventListener);\n this.boundNavigationRoot = null;\n window.removeEventListener('popstate', this.boundPopState);\n }\n if (this.boundMediator) {\n this.boundMediator.removeEventListener('router:navigate', this.boundMediatorNavigate);\n this.boundMediator = false;\n }\n this.listenersInitialized = false;\n return this;\n }\n\n /**\n * Click handler for `<a href data-pushstate>` links. Takes over only when Router can handle the link:\n * a plain left click on a same-origin link (no `download`, no other `target`) whose route is app-defined,\n * runnable, and allowed by its guard, and that is not just a `#fragment` of the current page. Anything else is left\n * to the browser.\n * @param e - The click event.\n * @returns This router if it handled the click, otherwise `true`.\n * @throws Whatever a guard or route callback throws.\n */\n eventPushStateClick(e: MouseEvent) {\n if (typeof window === 'undefined') {return true;}\n if (e.defaultPrevented || (e.button !== undefined && e.button !== 0) || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {\n return true;\n }\n const eventTarget = e.target as Element | null;\n const source = eventTarget && (typeof eventTarget.closest === 'function' ? eventTarget : eventTarget.parentElement);\n const anchor = source && typeof source.closest === 'function' ? source.closest('a[href][data-pushstate]') : null;\n if (!anchor) {return true;}\n const target = anchor.getAttribute('target');\n if (anchor.hasAttribute('download') || (target && target.toLowerCase() !== '_self')) {return true;}\n // Read the attribute, because on an SVG <a> the href property is an SVGAnimatedString, not a URL.\n const href = anchor.getAttribute('href') ?? '';\n const baseUrl = typeof document !== 'undefined' && document.baseURI\n ? document.baseURI\n : window.location.href || `${window.location.origin}/`;\n let location: URL;\n try {location = new URL(href, baseUrl);} catch {return true;}\n if (location.origin !== window.location.origin) {return true;}\n // A link to a #fragment of the page already shown is native fragment navigation: let the browser scroll to it.\n // Its popstate then only updates `url`, matching the fragment-only back/forward rule.\n if (location.href.includes('#') && location.pathname === window.location.pathname && location.search === window.location.search) {\n return true;\n }\n const candidateRoute = this.matchRoute(location.pathname);\n const selected = candidateRoute ? this.routes[candidateRoute] : undefined;\n if (!selected || !this.isRunnableRoute(selected) || builtInDefaultRoutes.has(selected)) {return true;}\n // Guards are synchronous, so try the navigation first and cancel the browser's default only if Router took it.\n // A rejected (or throwing) guard therefore falls back to a normal page load. If the route committed and then\n // threw, the token has moved, so the default is still cancelled and the browser doesn't navigate twice.\n const token = this.navigationToken;\n let handled = false;\n try {\n handled = this.navigate(`${location.pathname}${location.search}${location.hash}`, {}, false) !== false;\n } finally {\n if (handled || this.navigationToken !== token) {e.preventDefault();}\n }\n return handled ? this : true;\n }\n\n /**\n * Popstate handler for back/forward. Runs the route for the new URL without adding a history entry.\n * A hash-only change just updates `url` and `locationData.url`.\n * @returns This router.\n */\n eventPopState() {\n if (typeof window === 'undefined') {return this;}\n const currentUrl = this.getCurrentUrl();\n // Moving between #fragments on the same page (footnotes, skip links) keeps the current view and focus.\n if (this.route !== null && withoutHash(currentUrl) === withoutHash(this.url)) {\n this.url = currentUrl;\n this.locationData = {...this.locationData, url: currentUrl};\n return this;\n }\n this.navigate(currentUrl, {}, true);\n return this;\n }\n\n /**\n * Decode a query string into a plain object. For repeated keys the last value wins.\n * @param queryString - Query string, with or without the leading `?`.\n */\n parseQueryString(queryString: string) {\n return Object.fromEntries(new URLSearchParams(queryString));\n }\n\n /**\n * Rebuild `locationData` from the current `url` and `route`. Leaves it unchanged if the URL can't be parsed.\n * @param mediatorData - Value to expose as `locationData.data.mediator`.\n */\n setLocationData(mediatorData?: NavigationData) {\n const parsedUrl = this.parseStoredUrl(this.url || '/');\n if (!parsedUrl) {return;}\n this.locationData = this.buildLocationData(this.url, this.route, mediatorData, parsedUrl);\n }\n\n /**\n * Record the route's title in `pageTitle` and, in a browser, set `document.title` and move focus to the route's\n * `focus` selector (default `main h1`), adding `tabindex=\"-1\"` if needed. `focus: false` skips focus.\n * @param route - The route that just ran.\n * @returns This router.\n */\n applyPageContext(route: Route) {\n if (typeof route === 'function') {\n this.pageTitle = null;\n return this;\n }\n this.pageTitle = typeof route.title === 'string' ? route.title : null;\n if (!this.isBrowserRuntime()) {return this;}\n if (this.pageTitle) {document.title = this.pageTitle;}\n if (route.focus !== false && !this.suppressFocus) {\n const target = document.querySelector<HTMLElement>(route.focus || 'main h1');\n if (target) {\n if (!target.hasAttribute('tabindex')) {target.setAttribute('tabindex', '-1');}\n target.focus();\n }\n }\n return this;\n }\n\n /**\n * Navigate to a URL: match the route, run its guard, commit the new state, call the previous route's `destroy()`,\n * run the new route, apply title and focus, then push a history entry (browser only, unless `isPopState`).\n * If route code starts another navigation part-way through, that one wins and this call stops early.\n * @param url - Destination URL. Omit to re-run the last committed URL (or the current browser URL / `/` at first).\n * @param mediatorData - Value exposed to the route as `location.data.mediator`.\n * @param isPopState - `true` to skip adding a history entry.\n * @returns This router, or `false` if the navigation was rejected (bad or cross-origin URL, no matching route,\n * guard refused, or nothing to run). A rejected navigation changes nothing.\n * @throws Whatever a guard or route callback throws. A throwing guard changes nothing; a throwing route stays committed.\n */\n navigate(url?: string, mediatorData?: NavigationData, isPopState = false) {\n const skipFocus = this.initialDispatch;\n this.initialDispatch = false;\n if (url !== undefined && typeof url !== 'string') {return false;}\n\n const candidateInput = url !== undefined ? url : this.url;\n let parsedUrl: URL | null;\n if (!candidateInput) {\n parsedUrl = this.parseNavigationUrl(this.getCurrentUrl());\n } else {\n parsedUrl = url === undefined ? this.parseStoredUrl(candidateInput) : this.parseNavigationUrl(candidateInput);\n }\n if (!parsedUrl) {return false;}\n if (this.isBrowserRuntime() && parsedUrl.origin !== window.location.origin) {return false;}\n\n // Store only path, query, and hash, even when a server passed an absolute URL.\n const candidateUrl = `${parsedUrl.pathname}${parsedUrl.search}${parsedUrl.hash}`;\n const candidateRoute = this.matchRoute(parsedUrl.pathname);\n if (!candidateRoute) {return false;}\n\n const candidateLocationData = this.buildLocationData(candidateUrl, candidateRoute, mediatorData, parsedUrl);\n const selected = this.routes[candidateRoute];\n if (!selected || (typeof selected !== 'function' && selected.secure && selected.secure(this.scope, candidateLocationData) !== true) ||\n !this.isRunnableRoute(selected)) {\n return false;\n }\n\n const token = ++this.navigationToken;\n this.url = candidateUrl;\n this.route = candidateRoute;\n this.locationData = candidateLocationData;\n // Clear the stored teardown before calling it, so a navigation started inside destroy() can't call it again,\n // and a destroy() that throws is not retried: its error propagates with the destination committed but not run.\n const previous = this.activeDestroy;\n this.activeDestroy = undefined;\n if (previous) {\n previous.callback.call(previous.receiver, this.scope, candidateLocationData);\n if (token !== this.navigationToken) {return this;}\n }\n const selectedView = typeof selected !== 'function' && typeof selected.view === 'object'\n ? selected.view\n : undefined;\n // Store the teardown before initialize() runs, so it still runs later if initialize() throws or navigates away.\n this.activeDestroy = selectedView?.destroy\n ? {receiver: selectedView, callback: selectedView.destroy}\n : undefined;\n if (typeof selected === 'function') {\n selected(this.scope, candidateLocationData);\n } else if (typeof selected.view === 'function') {\n selected.view(this.scope, candidateLocationData);\n } else {\n selectedView!.initialize!(this.scope, candidateLocationData);\n }\n // If initialize() started another navigation, that one has already applied its own title, focus, and history.\n if (token !== this.navigationToken) {return this;}\n this.suppressFocus = skipFocus;\n try {\n this.applyPageContext(selected);\n } finally {\n this.suppressFocus = false;\n }\n if (token !== this.navigationToken) {return this;}\n this.previousRoute = this.route;\n // Re-running the current URL (no `url` given, e.g. a mediator event without a destination) must not add a\n // duplicate history entry for the address already shown.\n if (!isPopState && this.isBrowserRuntime() && !(url === undefined && parsedUrl.href === window.location.href)) {\n window.history.pushState(this.url, this.pageTitle || '', parsedUrl.href);\n }\n return this;\n }\n}\n\n/** Types for route definitions, location data, and navigation data. */\nnamespace Router {\n export type Handler = RouteHandler;\n export type Route = RouteHandler | RouteObject;\n export type Location = LocationData;\n export type Navigation = NavigationData;\n}\nexport = Router;\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "white-label-router",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.2.0",
|
|
4
4
|
"description": "Framework-independent TypeScript router for progressive enhancement in browser, server-rendered, and Node.js applications, with scoped links, guards, and accessible navigation.",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -54,21 +54,25 @@
|
|
|
54
54
|
"audit": "npm audit --audit-level=high",
|
|
55
55
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
56
56
|
"test:unit": "node --test test/*.test.js",
|
|
57
|
+
"test:browser": "node --test test/browser/*.test.js",
|
|
57
58
|
"coverage": "node --run build && node --run test:types && c8 node --run test:unit",
|
|
59
|
+
"benchmark": "node benchmark/router.bench.js",
|
|
60
|
+
"bench:ci": "node benchmark/router.bench.js --ci",
|
|
58
61
|
"prepare": "node --run build",
|
|
59
62
|
"test:types": "tsc -p tsconfig.consumer.json",
|
|
60
|
-
"lint": "eslint \"src/**/*.ts\" --max-warnings=0",
|
|
63
|
+
"lint": "eslint \"src/**/*.ts\" \"test/**/*.js\" \"benchmark/**/*.js\" --max-warnings=0",
|
|
61
64
|
"lint:fix": "node --run lint -- --fix"
|
|
62
65
|
},
|
|
63
66
|
"devDependencies": {
|
|
64
|
-
"@types/node": "24.13.3",
|
|
65
|
-
"c8": "12.0.0",
|
|
66
|
-
"typescript": "7.0.2",
|
|
67
67
|
"@babel/core": "8.0.5",
|
|
68
68
|
"@babel/eslint-parser": "8.0.5",
|
|
69
69
|
"@babel/preset-typescript": "8.0.1",
|
|
70
70
|
"@eslint/js": "10.0.1",
|
|
71
|
-
"
|
|
71
|
+
"@types/node": "24.13.3",
|
|
72
|
+
"c8": "12.0.0",
|
|
73
|
+
"eslint": "10.10.0",
|
|
74
|
+
"playwright": "1.64.1",
|
|
75
|
+
"typescript": "7.0.2"
|
|
72
76
|
},
|
|
73
77
|
"types": "dist/index.d.ts",
|
|
74
78
|
"c8": {
|
|
@@ -89,7 +93,9 @@
|
|
|
89
93
|
"branches": 100,
|
|
90
94
|
"statements": 100,
|
|
91
95
|
"perFile": true,
|
|
92
|
-
"exclude": [
|
|
96
|
+
"exclude": [
|
|
97
|
+
"**/*.d.ts"
|
|
98
|
+
]
|
|
93
99
|
},
|
|
94
100
|
"exports": {
|
|
95
101
|
".": {
|