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/dist/index.mjs
ADDED
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
import { QuasiURL } from "quasiurl";
|
|
2
|
+
import { State } from "compact-state";
|
|
3
|
+
// src/URLState.ts
|
|
4
|
+
function isImmediatelyInvokedNavigationEvent(event) {
|
|
5
|
+
return event === "navigationstart" || event === "navigationcomplete";
|
|
6
|
+
}
|
|
7
|
+
function isImmediatelyInvokedEvent(event) {
|
|
8
|
+
return event === "ready";
|
|
9
|
+
}
|
|
10
|
+
var URLState = class extends State {
|
|
11
|
+
constructor(href = null, options) {
|
|
12
|
+
super(href ?? "", options);
|
|
13
|
+
}
|
|
14
|
+
_init() {
|
|
15
|
+
super._init();
|
|
16
|
+
if (typeof window === "undefined") return;
|
|
17
|
+
let handleURLChange = () => {
|
|
18
|
+
this.setValue(window.location.href, { source: "popstate" });
|
|
19
|
+
};
|
|
20
|
+
this.on("start", () => {
|
|
21
|
+
window.addEventListener("popstate", handleURLChange);
|
|
22
|
+
this.emit("ready");
|
|
23
|
+
});
|
|
24
|
+
this.on("stop", () => {
|
|
25
|
+
window.removeEventListener("popstate", handleURLChange);
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
on(event, callback, invokeImmediately) {
|
|
29
|
+
if (invokeImmediately !== false) {
|
|
30
|
+
if (isImmediatelyInvokedNavigationEvent(event)) this._call(() => {
|
|
31
|
+
callback({ href: this.getValue() });
|
|
32
|
+
});
|
|
33
|
+
else if (isImmediatelyInvokedEvent(event)) this._call(() => {
|
|
34
|
+
callback();
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
return super.on(event, callback);
|
|
38
|
+
}
|
|
39
|
+
getValue() {
|
|
40
|
+
return this.toValue(this._value);
|
|
41
|
+
}
|
|
42
|
+
setValue(update, options) {
|
|
43
|
+
if (!this._active) return;
|
|
44
|
+
let href = this.toValue(this._resolveValue(update));
|
|
45
|
+
let extendedOptions = {
|
|
46
|
+
...options,
|
|
47
|
+
href,
|
|
48
|
+
referrer: this.getValue()
|
|
49
|
+
};
|
|
50
|
+
if (this.emit("navigationstart", extendedOptions) && this._transition(extendedOptions) !== false) {
|
|
51
|
+
this._assignValue(href);
|
|
52
|
+
this.emit("navigation", extendedOptions);
|
|
53
|
+
if (this.emit("navigationcomplete", extendedOptions)) {
|
|
54
|
+
this._complete(extendedOptions);
|
|
55
|
+
this.emit("ready");
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
_transition(options) {
|
|
60
|
+
if (typeof window === "undefined" || options?.href === void 0 || options?.source === "popstate") return;
|
|
61
|
+
let { href, target, spa, history } = options;
|
|
62
|
+
if (target && target !== "_self") {
|
|
63
|
+
window.open(href, target);
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
let url = new QuasiURL(href);
|
|
67
|
+
if (spa === "off" || !window.history || url.origin !== "" && url.origin !== window.location.origin) {
|
|
68
|
+
window.location[history === "replace" ? "replace" : "assign"](href);
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
window.history[history === "replace" ? "replaceState" : "pushState"]({}, "", href);
|
|
72
|
+
}
|
|
73
|
+
_complete(options) {
|
|
74
|
+
if (typeof window === "undefined" || !options || options.scroll === "off") return;
|
|
75
|
+
let { href, target } = options;
|
|
76
|
+
if (href === void 0 || target && target !== "_self") return;
|
|
77
|
+
let { hash } = new QuasiURL(href);
|
|
78
|
+
requestAnimationFrame(() => {
|
|
79
|
+
let targetElement = hash === "" ? null : document.querySelector(`${hash}, a[name="${hash.slice(1)}"]`);
|
|
80
|
+
if (targetElement) targetElement.scrollIntoView();
|
|
81
|
+
else window.scrollTo(0, 0);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
toValue(x) {
|
|
85
|
+
if (typeof window === "undefined") return x;
|
|
86
|
+
let url = new QuasiURL(x || window.location.href);
|
|
87
|
+
if (url.origin === window.location.origin) url.origin = "";
|
|
88
|
+
return url.href;
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
// src/utils/isLocationObject.ts
|
|
92
|
+
function isLocationObject(x) {
|
|
93
|
+
return x !== null && typeof x === "object" && "exec" in x && "compile" in x && "_schema" in x;
|
|
94
|
+
}
|
|
95
|
+
// src/utils/compileURL.ts
|
|
96
|
+
function compileURL(urlPattern, data) {
|
|
97
|
+
if (isLocationObject(urlPattern)) return urlPattern.compile(data);
|
|
98
|
+
let url = new QuasiURL(urlPattern ?? "");
|
|
99
|
+
let query = data?.query;
|
|
100
|
+
if (query) url.search = new URLSearchParams(Object.entries(query).reduce((p, [k, v]) => {
|
|
101
|
+
if (v !== null && v !== void 0) p[k] = typeof v === "string" ? v : JSON.stringify(v);
|
|
102
|
+
return p;
|
|
103
|
+
}, {}));
|
|
104
|
+
return url.href;
|
|
105
|
+
}
|
|
106
|
+
// src/utils/getNavigationOptions.ts
|
|
107
|
+
function getNavigationOptions(element) {
|
|
108
|
+
let { id, spa, history, scroll } = element.dataset;
|
|
109
|
+
return {
|
|
110
|
+
href: element.getAttribute("href"),
|
|
111
|
+
target: element.getAttribute("target"),
|
|
112
|
+
spa,
|
|
113
|
+
history,
|
|
114
|
+
scroll,
|
|
115
|
+
id
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
// src/utils/isRouteEvent.ts
|
|
119
|
+
function isRouteEvent(event) {
|
|
120
|
+
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);
|
|
121
|
+
}
|
|
122
|
+
// src/utils/matchURL.ts
|
|
123
|
+
function toObject(x) {
|
|
124
|
+
return x.reduce((p, v, k) => {
|
|
125
|
+
p[String(k)] = v;
|
|
126
|
+
return p;
|
|
127
|
+
}, {});
|
|
128
|
+
}
|
|
129
|
+
function matchPattern(pattern, href) {
|
|
130
|
+
let query = Object.fromEntries(new URLSearchParams(new QuasiURL(href).search));
|
|
131
|
+
if (typeof pattern === "string") return {
|
|
132
|
+
ok: pattern === "*" || pattern === href,
|
|
133
|
+
href,
|
|
134
|
+
params: {},
|
|
135
|
+
query
|
|
136
|
+
};
|
|
137
|
+
if (pattern instanceof RegExp) {
|
|
138
|
+
let matches = pattern.exec(href);
|
|
139
|
+
return {
|
|
140
|
+
ok: matches !== null,
|
|
141
|
+
href,
|
|
142
|
+
params: matches ? {
|
|
143
|
+
...toObject(Array.from(matches).slice(1)),
|
|
144
|
+
...matches.groups
|
|
145
|
+
} : {},
|
|
146
|
+
query
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
if (isLocationObject(pattern)) {
|
|
150
|
+
let result = pattern.exec(href);
|
|
151
|
+
if (result === null) return {
|
|
152
|
+
ok: false,
|
|
153
|
+
href,
|
|
154
|
+
params: {},
|
|
155
|
+
query: {}
|
|
156
|
+
};
|
|
157
|
+
return {
|
|
158
|
+
ok: true,
|
|
159
|
+
href,
|
|
160
|
+
params: result.params ?? {},
|
|
161
|
+
query: result.query ?? {}
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
return {
|
|
165
|
+
ok: false,
|
|
166
|
+
href,
|
|
167
|
+
params: {},
|
|
168
|
+
query: {}
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
function matchURL(pattern, href) {
|
|
172
|
+
if (Array.isArray(pattern)) {
|
|
173
|
+
for (let p of pattern) {
|
|
174
|
+
let result = matchPattern(p, href);
|
|
175
|
+
if (result.ok) return result;
|
|
176
|
+
}
|
|
177
|
+
return {
|
|
178
|
+
ok: false,
|
|
179
|
+
href,
|
|
180
|
+
params: {},
|
|
181
|
+
query: {}
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
return matchPattern(pattern, href);
|
|
185
|
+
}
|
|
186
|
+
// src/Route.ts
|
|
187
|
+
function isElementCollection(x) {
|
|
188
|
+
return Array.isArray(x) || x instanceof NodeList || x instanceof HTMLCollection;
|
|
189
|
+
}
|
|
190
|
+
function isLinkElement(x) {
|
|
191
|
+
return x instanceof HTMLAnchorElement || x instanceof HTMLAreaElement;
|
|
192
|
+
}
|
|
193
|
+
function toggleActive(element, route) {
|
|
194
|
+
if (!isLinkElement(element)) return;
|
|
195
|
+
if (element.hasAttribute("href") && route.match(route.toValue(element.href)).ok) element.dataset.active = "true";
|
|
196
|
+
else delete element.dataset.active;
|
|
197
|
+
}
|
|
198
|
+
var Route = class extends URLState {
|
|
199
|
+
_observeInited = false;
|
|
200
|
+
constructor(href = null, options) {
|
|
201
|
+
super(String(href ?? ""), options);
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Enables SPA navigation with HTML links inside the specified container.
|
|
205
|
+
*
|
|
206
|
+
* @example
|
|
207
|
+
* ```js
|
|
208
|
+
* let route = new Route();
|
|
209
|
+
* route.observe(document);
|
|
210
|
+
* ```
|
|
211
|
+
*/
|
|
212
|
+
observe(container, elements = "a, area") {
|
|
213
|
+
if (typeof window === "undefined") return;
|
|
214
|
+
if (!this._observeInited) {
|
|
215
|
+
let handleClick = (event) => {
|
|
216
|
+
this.emit("documentclick", event);
|
|
217
|
+
};
|
|
218
|
+
this.on("start", () => {
|
|
219
|
+
document.addEventListener("click", handleClick);
|
|
220
|
+
});
|
|
221
|
+
this.on("stop", () => {
|
|
222
|
+
document.removeEventListener("click", handleClick);
|
|
223
|
+
});
|
|
224
|
+
if (this.active) document.addEventListener("click", handleClick);
|
|
225
|
+
this._observeInited = true;
|
|
226
|
+
}
|
|
227
|
+
let resolveParams = () => {
|
|
228
|
+
let resolvedContainer = typeof container === "function" ? container() : container;
|
|
229
|
+
if (!resolvedContainer) return;
|
|
230
|
+
return {
|
|
231
|
+
resolvedContainer,
|
|
232
|
+
targetElements: isElementCollection(elements) ? Array.from(elements) : [elements]
|
|
233
|
+
};
|
|
234
|
+
};
|
|
235
|
+
let removeClickHandlers = this.on("documentclick", (event) => {
|
|
236
|
+
if (event.defaultPrevented || !isRouteEvent(event)) return;
|
|
237
|
+
let resolvedParams = resolveParams();
|
|
238
|
+
if (!resolvedParams) return;
|
|
239
|
+
let { resolvedContainer, targetElements } = resolvedParams;
|
|
240
|
+
let element = null;
|
|
241
|
+
for (let targetElement of targetElements) {
|
|
242
|
+
let target = null;
|
|
243
|
+
if (typeof targetElement === "string") target = event.target instanceof HTMLElement ? event.target.closest(targetElement) : null;
|
|
244
|
+
else target = targetElement;
|
|
245
|
+
if (isLinkElement(target) && resolvedContainer.contains(target)) {
|
|
246
|
+
element = target;
|
|
247
|
+
break;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
if (element) {
|
|
251
|
+
event.preventDefault();
|
|
252
|
+
this.navigate(getNavigationOptions(element));
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
let removeURLChangeHandlers = this.on("ready", () => {
|
|
256
|
+
let resolvedParams = resolveParams();
|
|
257
|
+
if (!resolvedParams) return;
|
|
258
|
+
let { resolvedContainer, targetElements } = resolvedParams;
|
|
259
|
+
for (let targetElement of targetElements) if (typeof targetElement === "string") {
|
|
260
|
+
let targets = resolvedContainer.querySelectorAll(targetElement);
|
|
261
|
+
for (let element of targets) toggleActive(element, this);
|
|
262
|
+
} else if (resolvedContainer.contains(targetElement)) toggleActive(targetElement, this);
|
|
263
|
+
});
|
|
264
|
+
return () => {
|
|
265
|
+
removeClickHandlers();
|
|
266
|
+
removeURLChangeHandlers();
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Navigates to the URL specified with `options.href`.
|
|
271
|
+
*
|
|
272
|
+
* @example
|
|
273
|
+
* ```js
|
|
274
|
+
* let route = new Route();
|
|
275
|
+
* route.navigate({ href: "/intro", history: "replace", scroll: "off" });
|
|
276
|
+
* ```
|
|
277
|
+
*/
|
|
278
|
+
navigate(options) {
|
|
279
|
+
if (!options?.href) return;
|
|
280
|
+
let { href, referrer, ...params } = options;
|
|
281
|
+
let transformedOptions = {
|
|
282
|
+
href: String(href),
|
|
283
|
+
referrer: referrer && String(referrer),
|
|
284
|
+
...params
|
|
285
|
+
};
|
|
286
|
+
this.setValue(transformedOptions.href, transformedOptions);
|
|
287
|
+
}
|
|
288
|
+
assign(url) {
|
|
289
|
+
this.navigate({ href: url });
|
|
290
|
+
}
|
|
291
|
+
replace(url) {
|
|
292
|
+
this.navigate({
|
|
293
|
+
href: url,
|
|
294
|
+
history: "replace"
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
reload() {
|
|
298
|
+
this.assign(this.getValue());
|
|
299
|
+
}
|
|
300
|
+
go(delta) {
|
|
301
|
+
if (typeof window !== "undefined" && window.history) window.history.go(delta);
|
|
302
|
+
}
|
|
303
|
+
back() {
|
|
304
|
+
this.go(-1);
|
|
305
|
+
}
|
|
306
|
+
forward() {
|
|
307
|
+
this.go(1);
|
|
308
|
+
}
|
|
309
|
+
get href() {
|
|
310
|
+
return this.getValue();
|
|
311
|
+
}
|
|
312
|
+
set href(value) {
|
|
313
|
+
this.assign(value);
|
|
314
|
+
}
|
|
315
|
+
get pathname() {
|
|
316
|
+
return new QuasiURL(this.href).pathname;
|
|
317
|
+
}
|
|
318
|
+
set pathname(value) {
|
|
319
|
+
let url = new QuasiURL(this.href);
|
|
320
|
+
url.pathname = String(value);
|
|
321
|
+
this.assign(url.href);
|
|
322
|
+
}
|
|
323
|
+
get search() {
|
|
324
|
+
return new QuasiURL(this.href).search;
|
|
325
|
+
}
|
|
326
|
+
set search(value) {
|
|
327
|
+
let url = new QuasiURL(this.href);
|
|
328
|
+
url.search = value;
|
|
329
|
+
this.assign(url.href);
|
|
330
|
+
}
|
|
331
|
+
get hash() {
|
|
332
|
+
return new QuasiURL(this.href).hash;
|
|
333
|
+
}
|
|
334
|
+
set hash(value) {
|
|
335
|
+
let url = new QuasiURL(this.href);
|
|
336
|
+
url.hash = value;
|
|
337
|
+
this.assign(url.href);
|
|
338
|
+
}
|
|
339
|
+
toString() {
|
|
340
|
+
return this.href;
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Matches the current location against `urlPattern`.
|
|
344
|
+
*/
|
|
345
|
+
match(urlPattern) {
|
|
346
|
+
return matchURL(urlPattern, this.href);
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Compiles `urlPattern` to a URL string by filling out the parameters
|
|
350
|
+
* based on `data`.
|
|
351
|
+
*/
|
|
352
|
+
compile(urlPattern, data) {
|
|
353
|
+
return compileURL(urlPattern, data);
|
|
354
|
+
}
|
|
355
|
+
at(urlPattern, x, y) {
|
|
356
|
+
let result = this.match(urlPattern);
|
|
357
|
+
if (x === void 0 && y === void 0) return result.ok;
|
|
358
|
+
if (!result.ok) return typeof y === "function" ? y(result) : y;
|
|
359
|
+
return typeof x === "function" ? x(result) : x;
|
|
360
|
+
}
|
|
361
|
+
};
|
|
362
|
+
export { Route, URLState, compileURL, getNavigationOptions, isLocationObject, isRouteEvent, matchURL };
|
package/index.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export * from "./src/Route.ts";
|
|
2
|
+
export * from "./src/types/LinkElement.ts";
|
|
3
|
+
export * from "./src/types/LocationObject.ts";
|
|
4
|
+
export * from "./src/types/LocationPattern.ts";
|
|
5
|
+
export * from "./src/types/LocationValue.ts";
|
|
6
|
+
export * from "./src/types/MatchHandler.ts";
|
|
7
|
+
export * from "./src/types/MatchState.ts";
|
|
8
|
+
export * from "./src/types/NavigationOptions.ts";
|
|
9
|
+
export * from "./src/types/URLComponents.ts";
|
|
10
|
+
export * from "./src/types/URLConfig.ts";
|
|
11
|
+
export * from "./src/types/URLData.ts";
|
|
12
|
+
export * from "./src/types/URLSchema.ts";
|
|
13
|
+
export * from "./src/URLState.ts";
|
|
14
|
+
export * from "./src/utils/compileURL.ts";
|
|
15
|
+
export * from "./src/utils/getNavigationOptions.ts";
|
|
16
|
+
export * from "./src/utils/isLocationObject.ts";
|
|
17
|
+
export * from "./src/utils/isRouteEvent.ts";
|
|
18
|
+
export * from "./src/utils/matchURL.ts";
|
package/package.json
CHANGED
|
@@ -1,6 +1,43 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "compact-router",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "compact-router",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Vanilla TS/JS routing",
|
|
5
|
+
"main": "./dist/index.cjs",
|
|
6
|
+
"module": "./dist/index.mjs",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"type": "module",
|
|
9
|
+
"scripts": {
|
|
10
|
+
"demo": "npx auxsrv tests/URL-based_rendering",
|
|
11
|
+
"preversion": "npx npm-run-all shape test",
|
|
12
|
+
"shape": "npx codeshape",
|
|
13
|
+
"test": "npx playwright test --project=chromium",
|
|
14
|
+
"typecheck": "npx codeshape typecheck"
|
|
15
|
+
},
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/axtk/compact-router.git"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"router",
|
|
22
|
+
"routing"
|
|
23
|
+
],
|
|
24
|
+
"author": "axtk",
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@playwright/test": "^1.64.1",
|
|
28
|
+
"@types/node": "^26.6.6",
|
|
29
|
+
"auxsrv": "^0.3.21",
|
|
30
|
+
"url-shape": "^2.1.6",
|
|
31
|
+
"zod": "^4.6.7"
|
|
32
|
+
},
|
|
33
|
+
"peerDependencies": {
|
|
34
|
+
"compact-state": "^0.6.0"
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"@standard-schema/spec": "^1.1.0",
|
|
38
|
+
"quasiurl": "^1.1.15"
|
|
39
|
+
},
|
|
40
|
+
"allowScripts": {
|
|
41
|
+
"esbuild": true
|
|
42
|
+
}
|
|
43
|
+
}
|