@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 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()` reads the path the browser's router shows from anywhere in the app, without the router at hand.
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 a signal
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 a signal.
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
- loading.value = false;
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
- loading.value = true;
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) loading.value = false;
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
- loading.value = false;
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
- loading.value = false;
338
+ history.loads(false);
323
339
  };
324
340
  onCleanup(stop);
325
341
  load(false);
326
342
  return {
327
343
  path: history.path,
328
- loading: () => loading.value,
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":"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"}
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":";;;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;AAGA,IAAI;AAEJ,IAAM,oBAAsC,YAAY,OAAO,KAAK;;;;;AAMpE,IAAa,oBAA6B,YAAY,CAAC,CAAC;AAExD,IAAa,eAAe,UAAyB;CACnD,YAAY,CAAC,CAAC,QAAQ;AACxB;;;ACtBA,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;AAG7G,IAAM,aAAa,UAChB,KAAK,WAAW,MAAM,KAAK,WAAW,YACvC,CAAC,KAAK,aAAa,UAAU,KAC7B,CAAC,KAAK,QAAQ,SAAS,UAAU,KACjC,KAAK,WAAW,SAAS;AAI3B,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;;;AClDA,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;;;ACDA,IAAa,eAAe,EAAE,UAAU,aAAyD,GAAG,WAAW;;AAG/G,IAAa,YAAY;;;;;;;ACpBzB,IAAa,iBAAiB,QAAQ,QAAuB;CAC3D,IAAI,MAAM,IAAI,IAAI,OAAO,SAAS;CAClC,MAAM,QAAQ,OAAO,IAAI,QAAQ;CACjC,MAAM,UAAU,OAAO,KAAK;CAC5B,MAAM,yBAAS,IAAI,IAAgB;CAEnC,OAAO;EACL,WAAW,IAAI,IAAI,GAAG;EACtB,YAAY,MAAM;EAClB,eAAe,QAAQ;EACvB,QAAQ,UAAgB;GACtB,QAAQ,QAAQ;EAClB;EACA,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;;;AC5CA,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,SAAS;EACT,OAAO;EACP;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;;;;;;;;AChDA,IAAa,eACX,QACA,EAAE,MAAM,MAAM,UAAU,eAAe,QAC5B;CACX,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,MAAM,KAAK;GACnB,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,MAAM,IAAI;EAElB,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,MAAM,KAAK;GAErB,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,MAAM,KAAK;GACnB,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,MAAM,KAAK;CACrB;CACA,UAAU,IAAI;CACd,KAAK,KAAK;CAEV,OAAO;EAAE,MAAM,QAAQ;EAAM,SAAS,QAAQ;EAAS,UAAU,QAAQ;EAAU;CAAK;AAC1F"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reely/router",
3
- "version": "0.1.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",