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/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.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
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
+ }