compact-router 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +120 -2
- package/dist/index.cjs +369 -0
- package/dist/index.d.ts +149 -0
- package/dist/index.mjs +362 -0
- package/index.ts +18 -0
- package/package.json +43 -6
- package/src/Route.ts +295 -0
- package/src/URLState.ts +161 -0
- package/src/types/LinkElement.ts +1 -0
- package/src/types/LocationObject.ts +11 -0
- package/src/types/LocationPattern.ts +6 -0
- package/src/types/LocationValue.ts +6 -0
- package/src/types/MatchHandler.ts +6 -0
- package/src/types/MatchState.ts +22 -0
- package/src/types/NavigationOptions.ts +10 -0
- package/src/types/URLComponents.ts +4 -0
- package/src/types/URLConfig.ts +5 -0
- package/src/types/URLData.ts +10 -0
- package/src/types/URLSchema.ts +4 -0
- package/src/utils/compileURL.ts +27 -0
- package/src/utils/getNavigationOptions.ts +15 -0
- package/src/utils/isLocationObject.ts +11 -0
- package/src/utils/isRouteEvent.ts +11 -0
- package/src/utils/matchURL.ts +99 -0
- package/tsconfig.json +17 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) Alexander Tkačenko
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,121 @@
|
|
|
1
|
-
#
|
|
1
|
+
# compact-router
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Vanilla TS/JS routing
|
|
4
|
+
|
|
5
|
+
Contents: [Route](#route) · [Annotated live demos](#annotated-live-demos)
|
|
6
|
+
|
|
7
|
+
## `Route`
|
|
8
|
+
|
|
9
|
+
`Route` stores the URL and exposes a `window.location`-like API for SPA navigation with a URL matching API.
|
|
10
|
+
|
|
11
|
+
```js
|
|
12
|
+
import { Route } from "compact-router";
|
|
13
|
+
|
|
14
|
+
const route = new Route();
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Navigate to other URLs in a SPA fashion similarly to the browser APIs:
|
|
18
|
+
|
|
19
|
+
```js
|
|
20
|
+
route.href = "/intro";
|
|
21
|
+
route.assign("/intro");
|
|
22
|
+
route.replace("/intro");
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Or in a more fine-grained manner:
|
|
26
|
+
|
|
27
|
+
```js
|
|
28
|
+
route.navigate({ href: "/intro", history: "replace", scroll: "off" });
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Check the current URL value like a regular `string` with `route.href`:
|
|
32
|
+
|
|
33
|
+
```js
|
|
34
|
+
route.href === "/intro";
|
|
35
|
+
route.href.startsWith("/sections/");
|
|
36
|
+
/^\/sections\/\d+\/?/.test(route.href);
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Or, alternatively, with `route.at(url)` returning `true` if the current URL matches `url`, and `false` otherwise:
|
|
40
|
+
|
|
41
|
+
```js
|
|
42
|
+
route.at("/intro"); // `true` at "/intro", `false` otherwise
|
|
43
|
+
route.at(/^\/sections\//); // `true` at "/sections/*"
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Or with the extended form `route.at(url, x, y?)`, which is similar to the ternary conditional operator `atURL ? x : y`:
|
|
47
|
+
|
|
48
|
+
```js
|
|
49
|
+
document.querySelector("header").className = route.at("/", "full", "compact");
|
|
50
|
+
// at "/" ? then "full" : otherwise "compact"
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Use `route.at(url, x, y?)` with dynamic values that require values from the URL pattern's capturing groups:
|
|
54
|
+
|
|
55
|
+
```js
|
|
56
|
+
document.querySelector("h1").textContent = route.at(
|
|
57
|
+
/^\/sections\/(?<id>\d+)\/?/,
|
|
58
|
+
({ params }) => `Section ${params.id}`,
|
|
59
|
+
);
|
|
60
|
+
// at "/sections/:id", "Section <id>" (otherwise `undefined`)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Or, alternatively, with a string URL pattern:
|
|
64
|
+
|
|
65
|
+
```js
|
|
66
|
+
import { url } from "url-shape";
|
|
67
|
+
|
|
68
|
+
document.querySelector("h1").textContent = route.at(
|
|
69
|
+
url("/sections/:id"),
|
|
70
|
+
({ params }) => `Section ${params.id}`,
|
|
71
|
+
);
|
|
72
|
+
// at "/sections/:id", "Section <id>" (otherwise `undefined`)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Use a URL builder like the one from `url-shape` also in conjunction with a URL schema to set up type-safe routes ([example](https://codesandbox.io/p/sandbox/qg7qg3?file=%2Fsrc%2Findex.ts)).
|
|
76
|
+
|
|
77
|
+
Enable SPA navigation with HTML links inside the specified container (or the entire `document`) without any changes to the HTML:
|
|
78
|
+
|
|
79
|
+
```js
|
|
80
|
+
route.observe(document);
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Tweak the links' navigation behavior by adding a relevant combination of the optional `data-` attributes (corresponding to the `route.navigate()` options):
|
|
84
|
+
|
|
85
|
+
```html
|
|
86
|
+
<a href="/intro">Intro</a>
|
|
87
|
+
<a href="/intro" data-history="replace">Intro</a>
|
|
88
|
+
<a href="/intro" data-scroll="off">Intro</a>
|
|
89
|
+
<a href="/intro" data-spa="off">Intro</a>
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Links covered by `route.observe(container, selector?)` also automatically receive the `data-active="true"` attribute whenever their `href` attribute matches the current URL, which can be used for additional styling. If links are added to the DOM asynchronously, call `route.emit("ready")` when the rendering is complete to mark active links.
|
|
93
|
+
|
|
94
|
+
Define what should be done when the URL changes:
|
|
95
|
+
|
|
96
|
+
```js
|
|
97
|
+
route.on("navigationcomplete", ({ href }) => {
|
|
98
|
+
renderContent();
|
|
99
|
+
});
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Define what should be done before the URL changes (in a way effectively similar to routing middleware):
|
|
103
|
+
|
|
104
|
+
```js
|
|
105
|
+
route.on("navigationstart", ({ href }) => {
|
|
106
|
+
if (hasUnsavedInput) return false; // Quit the navigation, prevent the current URL change
|
|
107
|
+
|
|
108
|
+
if (href === "/") {
|
|
109
|
+
route.href = "/intro"; // SPA redirection
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Annotated live demos
|
|
116
|
+
|
|
117
|
+
- [URL-based rendering](https://codesandbox.io/p/sandbox/kt6m5l?file=%252Fsrc%252Findex.ts)
|
|
118
|
+
- [Type-safe URL-based rendering](https://codesandbox.io/p/sandbox/qg7qg3?file=%2Fsrc%2Findex.ts)
|
|
119
|
+
- [SPA redirection](https://codesandbox.io/p/sandbox/rpl3gh?file=%252Fsrc%252Findex.ts)
|
|
120
|
+
|
|
121
|
+
Find also the code of these examples in the repo's [`tests`](https://github.com/axtk/compact-router/tree/main/tests) directory.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
let quasiurl = require("quasiurl");
|
|
3
|
+
let compact_state = require("compact-state");
|
|
4
|
+
// src/URLState.ts
|
|
5
|
+
function isImmediatelyInvokedNavigationEvent(event) {
|
|
6
|
+
return event === "navigationstart" || event === "navigationcomplete";
|
|
7
|
+
}
|
|
8
|
+
function isImmediatelyInvokedEvent(event) {
|
|
9
|
+
return event === "ready";
|
|
10
|
+
}
|
|
11
|
+
var URLState = class extends compact_state.State {
|
|
12
|
+
constructor(href = null, options) {
|
|
13
|
+
super(href ?? "", options);
|
|
14
|
+
}
|
|
15
|
+
_init() {
|
|
16
|
+
super._init();
|
|
17
|
+
if (typeof window === "undefined") return;
|
|
18
|
+
let handleURLChange = () => {
|
|
19
|
+
this.setValue(window.location.href, { source: "popstate" });
|
|
20
|
+
};
|
|
21
|
+
this.on("start", () => {
|
|
22
|
+
window.addEventListener("popstate", handleURLChange);
|
|
23
|
+
this.emit("ready");
|
|
24
|
+
});
|
|
25
|
+
this.on("stop", () => {
|
|
26
|
+
window.removeEventListener("popstate", handleURLChange);
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
on(event, callback, invokeImmediately) {
|
|
30
|
+
if (invokeImmediately !== false) {
|
|
31
|
+
if (isImmediatelyInvokedNavigationEvent(event)) this._call(() => {
|
|
32
|
+
callback({ href: this.getValue() });
|
|
33
|
+
});
|
|
34
|
+
else if (isImmediatelyInvokedEvent(event)) this._call(() => {
|
|
35
|
+
callback();
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
return super.on(event, callback);
|
|
39
|
+
}
|
|
40
|
+
getValue() {
|
|
41
|
+
return this.toValue(this._value);
|
|
42
|
+
}
|
|
43
|
+
setValue(update, options) {
|
|
44
|
+
if (!this._active) return;
|
|
45
|
+
let href = this.toValue(this._resolveValue(update));
|
|
46
|
+
let extendedOptions = {
|
|
47
|
+
...options,
|
|
48
|
+
href,
|
|
49
|
+
referrer: this.getValue()
|
|
50
|
+
};
|
|
51
|
+
if (this.emit("navigationstart", extendedOptions) && this._transition(extendedOptions) !== false) {
|
|
52
|
+
this._assignValue(href);
|
|
53
|
+
this.emit("navigation", extendedOptions);
|
|
54
|
+
if (this.emit("navigationcomplete", extendedOptions)) {
|
|
55
|
+
this._complete(extendedOptions);
|
|
56
|
+
this.emit("ready");
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
_transition(options) {
|
|
61
|
+
if (typeof window === "undefined" || options?.href === void 0 || options?.source === "popstate") return;
|
|
62
|
+
let { href, target, spa, history } = options;
|
|
63
|
+
if (target && target !== "_self") {
|
|
64
|
+
window.open(href, target);
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
let url = new quasiurl.QuasiURL(href);
|
|
68
|
+
if (spa === "off" || !window.history || url.origin !== "" && url.origin !== window.location.origin) {
|
|
69
|
+
window.location[history === "replace" ? "replace" : "assign"](href);
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
window.history[history === "replace" ? "replaceState" : "pushState"]({}, "", href);
|
|
73
|
+
}
|
|
74
|
+
_complete(options) {
|
|
75
|
+
if (typeof window === "undefined" || !options || options.scroll === "off") return;
|
|
76
|
+
let { href, target } = options;
|
|
77
|
+
if (href === void 0 || target && target !== "_self") return;
|
|
78
|
+
let { hash } = new quasiurl.QuasiURL(href);
|
|
79
|
+
requestAnimationFrame(() => {
|
|
80
|
+
let targetElement = hash === "" ? null : document.querySelector(`${hash}, a[name="${hash.slice(1)}"]`);
|
|
81
|
+
if (targetElement) targetElement.scrollIntoView();
|
|
82
|
+
else window.scrollTo(0, 0);
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
toValue(x) {
|
|
86
|
+
if (typeof window === "undefined") return x;
|
|
87
|
+
let url = new quasiurl.QuasiURL(x || window.location.href);
|
|
88
|
+
if (url.origin === window.location.origin) url.origin = "";
|
|
89
|
+
return url.href;
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
// src/utils/isLocationObject.ts
|
|
93
|
+
function isLocationObject(x) {
|
|
94
|
+
return x !== null && typeof x === "object" && "exec" in x && "compile" in x && "_schema" in x;
|
|
95
|
+
}
|
|
96
|
+
// src/utils/compileURL.ts
|
|
97
|
+
function compileURL(urlPattern, data) {
|
|
98
|
+
if (isLocationObject(urlPattern)) return urlPattern.compile(data);
|
|
99
|
+
let url = new quasiurl.QuasiURL(urlPattern ?? "");
|
|
100
|
+
let query = data?.query;
|
|
101
|
+
if (query) url.search = new URLSearchParams(Object.entries(query).reduce((p, [k, v]) => {
|
|
102
|
+
if (v !== null && v !== void 0) p[k] = typeof v === "string" ? v : JSON.stringify(v);
|
|
103
|
+
return p;
|
|
104
|
+
}, {}));
|
|
105
|
+
return url.href;
|
|
106
|
+
}
|
|
107
|
+
// src/utils/getNavigationOptions.ts
|
|
108
|
+
function getNavigationOptions(element) {
|
|
109
|
+
let { id, spa, history, scroll } = element.dataset;
|
|
110
|
+
return {
|
|
111
|
+
href: element.getAttribute("href"),
|
|
112
|
+
target: element.getAttribute("target"),
|
|
113
|
+
spa,
|
|
114
|
+
history,
|
|
115
|
+
scroll,
|
|
116
|
+
id
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
// src/utils/isRouteEvent.ts
|
|
120
|
+
function isRouteEvent(event) {
|
|
121
|
+
return event !== null && typeof event === "object" && (!("button" in event) || event.button === 0) && (!("ctrlKey" in event) || !event.ctrlKey) && (!("shiftKey" in event) || !event.shiftKey) && (!("altKey" in event) || !event.altKey) && (!("metaKey" in event) || !event.metaKey);
|
|
122
|
+
}
|
|
123
|
+
// src/utils/matchURL.ts
|
|
124
|
+
function toObject(x) {
|
|
125
|
+
return x.reduce((p, v, k) => {
|
|
126
|
+
p[String(k)] = v;
|
|
127
|
+
return p;
|
|
128
|
+
}, {});
|
|
129
|
+
}
|
|
130
|
+
function matchPattern(pattern, href) {
|
|
131
|
+
let query = Object.fromEntries(new URLSearchParams(new quasiurl.QuasiURL(href).search));
|
|
132
|
+
if (typeof pattern === "string") return {
|
|
133
|
+
ok: pattern === "*" || pattern === href,
|
|
134
|
+
href,
|
|
135
|
+
params: {},
|
|
136
|
+
query
|
|
137
|
+
};
|
|
138
|
+
if (pattern instanceof RegExp) {
|
|
139
|
+
let matches = pattern.exec(href);
|
|
140
|
+
return {
|
|
141
|
+
ok: matches !== null,
|
|
142
|
+
href,
|
|
143
|
+
params: matches ? {
|
|
144
|
+
...toObject(Array.from(matches).slice(1)),
|
|
145
|
+
...matches.groups
|
|
146
|
+
} : {},
|
|
147
|
+
query
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
if (isLocationObject(pattern)) {
|
|
151
|
+
let result = pattern.exec(href);
|
|
152
|
+
if (result === null) return {
|
|
153
|
+
ok: false,
|
|
154
|
+
href,
|
|
155
|
+
params: {},
|
|
156
|
+
query: {}
|
|
157
|
+
};
|
|
158
|
+
return {
|
|
159
|
+
ok: true,
|
|
160
|
+
href,
|
|
161
|
+
params: result.params ?? {},
|
|
162
|
+
query: result.query ?? {}
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
return {
|
|
166
|
+
ok: false,
|
|
167
|
+
href,
|
|
168
|
+
params: {},
|
|
169
|
+
query: {}
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
function matchURL(pattern, href) {
|
|
173
|
+
if (Array.isArray(pattern)) {
|
|
174
|
+
for (let p of pattern) {
|
|
175
|
+
let result = matchPattern(p, href);
|
|
176
|
+
if (result.ok) return result;
|
|
177
|
+
}
|
|
178
|
+
return {
|
|
179
|
+
ok: false,
|
|
180
|
+
href,
|
|
181
|
+
params: {},
|
|
182
|
+
query: {}
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
return matchPattern(pattern, href);
|
|
186
|
+
}
|
|
187
|
+
// src/Route.ts
|
|
188
|
+
function isElementCollection(x) {
|
|
189
|
+
return Array.isArray(x) || x instanceof NodeList || x instanceof HTMLCollection;
|
|
190
|
+
}
|
|
191
|
+
function isLinkElement(x) {
|
|
192
|
+
return x instanceof HTMLAnchorElement || x instanceof HTMLAreaElement;
|
|
193
|
+
}
|
|
194
|
+
function toggleActive(element, route) {
|
|
195
|
+
if (!isLinkElement(element)) return;
|
|
196
|
+
if (element.hasAttribute("href") && route.match(route.toValue(element.href)).ok) element.dataset.active = "true";
|
|
197
|
+
else delete element.dataset.active;
|
|
198
|
+
}
|
|
199
|
+
var Route = class extends URLState {
|
|
200
|
+
_observeInited = false;
|
|
201
|
+
constructor(href = null, options) {
|
|
202
|
+
super(String(href ?? ""), options);
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Enables SPA navigation with HTML links inside the specified container.
|
|
206
|
+
*
|
|
207
|
+
* @example
|
|
208
|
+
* ```js
|
|
209
|
+
* let route = new Route();
|
|
210
|
+
* route.observe(document);
|
|
211
|
+
* ```
|
|
212
|
+
*/
|
|
213
|
+
observe(container, elements = "a, area") {
|
|
214
|
+
if (typeof window === "undefined") return;
|
|
215
|
+
if (!this._observeInited) {
|
|
216
|
+
let handleClick = (event) => {
|
|
217
|
+
this.emit("documentclick", event);
|
|
218
|
+
};
|
|
219
|
+
this.on("start", () => {
|
|
220
|
+
document.addEventListener("click", handleClick);
|
|
221
|
+
});
|
|
222
|
+
this.on("stop", () => {
|
|
223
|
+
document.removeEventListener("click", handleClick);
|
|
224
|
+
});
|
|
225
|
+
if (this.active) document.addEventListener("click", handleClick);
|
|
226
|
+
this._observeInited = true;
|
|
227
|
+
}
|
|
228
|
+
let resolveParams = () => {
|
|
229
|
+
let resolvedContainer = typeof container === "function" ? container() : container;
|
|
230
|
+
if (!resolvedContainer) return;
|
|
231
|
+
return {
|
|
232
|
+
resolvedContainer,
|
|
233
|
+
targetElements: isElementCollection(elements) ? Array.from(elements) : [elements]
|
|
234
|
+
};
|
|
235
|
+
};
|
|
236
|
+
let removeClickHandlers = this.on("documentclick", (event) => {
|
|
237
|
+
if (event.defaultPrevented || !isRouteEvent(event)) return;
|
|
238
|
+
let resolvedParams = resolveParams();
|
|
239
|
+
if (!resolvedParams) return;
|
|
240
|
+
let { resolvedContainer, targetElements } = resolvedParams;
|
|
241
|
+
let element = null;
|
|
242
|
+
for (let targetElement of targetElements) {
|
|
243
|
+
let target = null;
|
|
244
|
+
if (typeof targetElement === "string") target = event.target instanceof HTMLElement ? event.target.closest(targetElement) : null;
|
|
245
|
+
else target = targetElement;
|
|
246
|
+
if (isLinkElement(target) && resolvedContainer.contains(target)) {
|
|
247
|
+
element = target;
|
|
248
|
+
break;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
if (element) {
|
|
252
|
+
event.preventDefault();
|
|
253
|
+
this.navigate(getNavigationOptions(element));
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
let removeURLChangeHandlers = this.on("ready", () => {
|
|
257
|
+
let resolvedParams = resolveParams();
|
|
258
|
+
if (!resolvedParams) return;
|
|
259
|
+
let { resolvedContainer, targetElements } = resolvedParams;
|
|
260
|
+
for (let targetElement of targetElements) if (typeof targetElement === "string") {
|
|
261
|
+
let targets = resolvedContainer.querySelectorAll(targetElement);
|
|
262
|
+
for (let element of targets) toggleActive(element, this);
|
|
263
|
+
} else if (resolvedContainer.contains(targetElement)) toggleActive(targetElement, this);
|
|
264
|
+
});
|
|
265
|
+
return () => {
|
|
266
|
+
removeClickHandlers();
|
|
267
|
+
removeURLChangeHandlers();
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Navigates to the URL specified with `options.href`.
|
|
272
|
+
*
|
|
273
|
+
* @example
|
|
274
|
+
* ```js
|
|
275
|
+
* let route = new Route();
|
|
276
|
+
* route.navigate({ href: "/intro", history: "replace", scroll: "off" });
|
|
277
|
+
* ```
|
|
278
|
+
*/
|
|
279
|
+
navigate(options) {
|
|
280
|
+
if (!options?.href) return;
|
|
281
|
+
let { href, referrer, ...params } = options;
|
|
282
|
+
let transformedOptions = {
|
|
283
|
+
href: String(href),
|
|
284
|
+
referrer: referrer && String(referrer),
|
|
285
|
+
...params
|
|
286
|
+
};
|
|
287
|
+
this.setValue(transformedOptions.href, transformedOptions);
|
|
288
|
+
}
|
|
289
|
+
assign(url) {
|
|
290
|
+
this.navigate({ href: url });
|
|
291
|
+
}
|
|
292
|
+
replace(url) {
|
|
293
|
+
this.navigate({
|
|
294
|
+
href: url,
|
|
295
|
+
history: "replace"
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
reload() {
|
|
299
|
+
this.assign(this.getValue());
|
|
300
|
+
}
|
|
301
|
+
go(delta) {
|
|
302
|
+
if (typeof window !== "undefined" && window.history) window.history.go(delta);
|
|
303
|
+
}
|
|
304
|
+
back() {
|
|
305
|
+
this.go(-1);
|
|
306
|
+
}
|
|
307
|
+
forward() {
|
|
308
|
+
this.go(1);
|
|
309
|
+
}
|
|
310
|
+
get href() {
|
|
311
|
+
return this.getValue();
|
|
312
|
+
}
|
|
313
|
+
set href(value) {
|
|
314
|
+
this.assign(value);
|
|
315
|
+
}
|
|
316
|
+
get pathname() {
|
|
317
|
+
return new quasiurl.QuasiURL(this.href).pathname;
|
|
318
|
+
}
|
|
319
|
+
set pathname(value) {
|
|
320
|
+
let url = new quasiurl.QuasiURL(this.href);
|
|
321
|
+
url.pathname = String(value);
|
|
322
|
+
this.assign(url.href);
|
|
323
|
+
}
|
|
324
|
+
get search() {
|
|
325
|
+
return new quasiurl.QuasiURL(this.href).search;
|
|
326
|
+
}
|
|
327
|
+
set search(value) {
|
|
328
|
+
let url = new quasiurl.QuasiURL(this.href);
|
|
329
|
+
url.search = value;
|
|
330
|
+
this.assign(url.href);
|
|
331
|
+
}
|
|
332
|
+
get hash() {
|
|
333
|
+
return new quasiurl.QuasiURL(this.href).hash;
|
|
334
|
+
}
|
|
335
|
+
set hash(value) {
|
|
336
|
+
let url = new quasiurl.QuasiURL(this.href);
|
|
337
|
+
url.hash = value;
|
|
338
|
+
this.assign(url.href);
|
|
339
|
+
}
|
|
340
|
+
toString() {
|
|
341
|
+
return this.href;
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Matches the current location against `urlPattern`.
|
|
345
|
+
*/
|
|
346
|
+
match(urlPattern) {
|
|
347
|
+
return matchURL(urlPattern, this.href);
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Compiles `urlPattern` to a URL string by filling out the parameters
|
|
351
|
+
* based on `data`.
|
|
352
|
+
*/
|
|
353
|
+
compile(urlPattern, data) {
|
|
354
|
+
return compileURL(urlPattern, data);
|
|
355
|
+
}
|
|
356
|
+
at(urlPattern, x, y) {
|
|
357
|
+
let result = this.match(urlPattern);
|
|
358
|
+
if (x === void 0 && y === void 0) return result.ok;
|
|
359
|
+
if (!result.ok) return typeof y === "function" ? y(result) : y;
|
|
360
|
+
return typeof x === "function" ? x(result) : x;
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
exports.Route = Route;
|
|
364
|
+
exports.URLState = URLState;
|
|
365
|
+
exports.compileURL = compileURL;
|
|
366
|
+
exports.getNavigationOptions = getNavigationOptions;
|
|
367
|
+
exports.isLocationObject = isLocationObject;
|
|
368
|
+
exports.isRouteEvent = isRouteEvent;
|
|
369
|
+
exports.matchURL = matchURL;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { EventCallback, State, StateOptions, StatePayloadMap, StateUpdate } from "compact-state";
|
|
2
|
+
import { StandardSchemaV1 } from "@standard-schema/spec";
|
|
3
|
+
export type URLComponents = {
|
|
4
|
+
params?: Record<string, unknown> | undefined;
|
|
5
|
+
query?: Record<string, unknown> | undefined;
|
|
6
|
+
};
|
|
7
|
+
export type URLSchema = StandardSchemaV1<URLComponents>;
|
|
8
|
+
export type LocationObject = {
|
|
9
|
+
_schema: URLSchema | undefined;
|
|
10
|
+
exec: (x: string) => URLComponents | null;
|
|
11
|
+
compile: (x: any) => string;
|
|
12
|
+
toString: () => string;
|
|
13
|
+
};
|
|
14
|
+
type URLConfigShape = {
|
|
15
|
+
strict?: boolean;
|
|
16
|
+
};
|
|
17
|
+
export interface URLConfig extends URLConfigShape {}
|
|
18
|
+
export type LocationValue = URLConfig["strict"] extends true ? LocationObject | undefined : string | LocationObject | undefined;
|
|
19
|
+
export type LocationPattern = URLConfig["strict"] extends true ? LocationValue | LocationValue[] : LocationValue | RegExp | (LocationValue | RegExp)[];
|
|
20
|
+
export type MatchState<P extends LocationPattern> = {
|
|
21
|
+
ok: boolean;
|
|
22
|
+
href: string;
|
|
23
|
+
params: P extends {
|
|
24
|
+
_schema: URLSchema;
|
|
25
|
+
} ? StandardSchemaV1.InferOutput<P["_schema"]> extends {
|
|
26
|
+
params?: Record<string, unknown>;
|
|
27
|
+
} ? StandardSchemaV1.InferOutput<P["_schema"]>["params"] : Record<string, never> : Record<string, string | undefined>;
|
|
28
|
+
query: P extends {
|
|
29
|
+
_schema: URLSchema;
|
|
30
|
+
} ? StandardSchemaV1.InferOutput<P["_schema"]> extends {
|
|
31
|
+
query?: Record<string, unknown>;
|
|
32
|
+
} ? StandardSchemaV1.InferOutput<P["_schema"]>["query"] : Record<string, never> : Record<string, string | undefined>;
|
|
33
|
+
};
|
|
34
|
+
export type MatchHandler<P extends LocationPattern, T> = (payload: MatchState<P>) => T;
|
|
35
|
+
export type NavigationOptions<Href = string> = {
|
|
36
|
+
target?: string;
|
|
37
|
+
href?: Href;
|
|
38
|
+
referrer?: Href;
|
|
39
|
+
spa?: "auto" | "off" | undefined;
|
|
40
|
+
history?: "push" | "replace" | undefined;
|
|
41
|
+
scroll?: "auto" | "off" | undefined;
|
|
42
|
+
id?: string;
|
|
43
|
+
source?: "popstate" | undefined;
|
|
44
|
+
};
|
|
45
|
+
export type URLData<T extends LocationValue = LocationValue> = T extends {
|
|
46
|
+
_schema: URLSchema;
|
|
47
|
+
} ? StandardSchemaV1.InferOutput<T["_schema"]> : URLComponents;
|
|
48
|
+
export type URLStatePayloadMap = StatePayloadMap<string> & {
|
|
49
|
+
navigationstart: NavigationOptions;
|
|
50
|
+
navigation: NavigationOptions;
|
|
51
|
+
navigationcomplete: NavigationOptions;
|
|
52
|
+
ready: void;
|
|
53
|
+
};
|
|
54
|
+
export type URLStateOptions = StateOptions;
|
|
55
|
+
export declare class URLState<P extends URLStatePayloadMap = URLStatePayloadMap> extends State<string, P> {
|
|
56
|
+
constructor(href?: string | null, options?: URLStateOptions);
|
|
57
|
+
_init(): void;
|
|
58
|
+
on<E extends string>(event: E, callback: EventCallback<P[E]>, invokeImmediately?: boolean): () => void;
|
|
59
|
+
getValue(): string;
|
|
60
|
+
setValue(update: string | StateUpdate<string>, options?: NavigationOptions): void;
|
|
61
|
+
_transition(options?: NavigationOptions): boolean | void | undefined;
|
|
62
|
+
_complete(options?: NavigationOptions): boolean | void | undefined;
|
|
63
|
+
toValue(x: string): string;
|
|
64
|
+
}
|
|
65
|
+
export type ContainerElement = Document | Element | null | undefined;
|
|
66
|
+
export type ElementCollection = (string | Node)[] | HTMLCollection | NodeList;
|
|
67
|
+
export type ObservedElement = string | Node | (string | Node)[] | HTMLCollection | NodeList;
|
|
68
|
+
export type RoutePayloadMap = URLStatePayloadMap & {
|
|
69
|
+
documentclick: MouseEvent;
|
|
70
|
+
};
|
|
71
|
+
export type RouteOptions = URLStateOptions;
|
|
72
|
+
export declare class Route extends URLState<RoutePayloadMap> {
|
|
73
|
+
_observeInited: boolean;
|
|
74
|
+
constructor(href?: LocationValue | null, options?: RouteOptions);
|
|
75
|
+
/**
|
|
76
|
+
* Enables SPA navigation with HTML links inside the specified container.
|
|
77
|
+
*
|
|
78
|
+
* @example
|
|
79
|
+
* ```js
|
|
80
|
+
* let route = new Route();
|
|
81
|
+
* route.observe(document);
|
|
82
|
+
* ```
|
|
83
|
+
*/
|
|
84
|
+
observe(container: ContainerElement | (() => ContainerElement), elements?: ObservedElement): (() => void) | undefined;
|
|
85
|
+
/**
|
|
86
|
+
* Navigates to the URL specified with `options.href`.
|
|
87
|
+
*
|
|
88
|
+
* @example
|
|
89
|
+
* ```js
|
|
90
|
+
* let route = new Route();
|
|
91
|
+
* route.navigate({ href: "/intro", history: "replace", scroll: "off" });
|
|
92
|
+
* ```
|
|
93
|
+
*/
|
|
94
|
+
navigate(options?: NavigationOptions<LocationValue>): void;
|
|
95
|
+
assign(url: LocationValue): void;
|
|
96
|
+
replace(url: LocationValue): void;
|
|
97
|
+
reload(): void;
|
|
98
|
+
go(delta: number): void;
|
|
99
|
+
back(): void;
|
|
100
|
+
forward(): void;
|
|
101
|
+
get href(): string;
|
|
102
|
+
set href(value: LocationValue);
|
|
103
|
+
get pathname(): string;
|
|
104
|
+
set pathname(value: LocationValue);
|
|
105
|
+
get search(): string;
|
|
106
|
+
set search(value: string | URLSearchParams);
|
|
107
|
+
get hash(): string;
|
|
108
|
+
set hash(value: string);
|
|
109
|
+
toString(): string;
|
|
110
|
+
/**
|
|
111
|
+
* Matches the current location against `urlPattern`.
|
|
112
|
+
*/
|
|
113
|
+
match<P extends LocationPattern>(urlPattern: P): MatchState<P>;
|
|
114
|
+
/**
|
|
115
|
+
* Compiles `urlPattern` to a URL string by filling out the parameters
|
|
116
|
+
* based on `data`.
|
|
117
|
+
*/
|
|
118
|
+
compile<T extends LocationValue>(urlPattern: T, data?: URLData<T>): string;
|
|
119
|
+
/**
|
|
120
|
+
* `at(urlPattern)` returns `true` if `urlPattern` matches the current URL,
|
|
121
|
+
* and `false` otherwise.
|
|
122
|
+
*
|
|
123
|
+
* `at(urlPattern, x, y?)` returns either based on `x` if `urlPattern`
|
|
124
|
+
* matches the current URL, or based on `y` otherwise. (It loosely resembles
|
|
125
|
+
* the ternary conditional operator `matchesURLPattern ? x : y`.)
|
|
126
|
+
*
|
|
127
|
+
* If the current location matches `urlPattern`, `at(urlPattern, x, y)`
|
|
128
|
+
* returns:
|
|
129
|
+
* - `x`, if `x` is not a function;
|
|
130
|
+
* - `x({ params })`, if `x` is a function, with `params` extracted from
|
|
131
|
+
* the current URL.
|
|
132
|
+
*
|
|
133
|
+
* If the current location doesn't match `urlPattern`, `at(urlPattern, x, y)`
|
|
134
|
+
* returns:
|
|
135
|
+
* - `y`, if `y` is not a function;
|
|
136
|
+
* - `y({ params })`, if `y` is a function, with `params` extracted from
|
|
137
|
+
* the current URL.
|
|
138
|
+
*/
|
|
139
|
+
at<P extends LocationPattern>(urlPattern: P): boolean;
|
|
140
|
+
at<P extends LocationPattern, X>(urlPattern: P, x: X | MatchHandler<P, X>): X | undefined;
|
|
141
|
+
at<P extends LocationPattern, X, Y>(urlPattern: P, x: X | MatchHandler<P, X>, y: Y | MatchHandler<P, Y>): X | Y;
|
|
142
|
+
}
|
|
143
|
+
export type LinkElement = HTMLAnchorElement | HTMLAreaElement;
|
|
144
|
+
export declare function compileURL<T extends LocationValue>(urlPattern: T, data?: URLData<T>): string;
|
|
145
|
+
export declare function getNavigationOptions(element: LinkElement): NavigationOptions;
|
|
146
|
+
export declare function isLocationObject(x: unknown): x is LocationObject;
|
|
147
|
+
export declare function isRouteEvent(event: unknown): boolean;
|
|
148
|
+
export declare function matchURL<P extends LocationPattern>(pattern: P, href: string): MatchState<P>;
|
|
149
|
+
export {};
|