pulse-js-framework 1.4.1 → 1.4.2
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 +414 -414
- package/cli/analyze.js +499 -499
- package/cli/build.js +341 -341
- package/cli/format.js +704 -704
- package/cli/index.js +398 -398
- package/cli/lint.js +642 -642
- package/cli/utils/file-utils.js +298 -298
- package/compiler/lexer.js +766 -766
- package/compiler/parser.js +1797 -1797
- package/compiler/transformer.js +1332 -1332
- package/index.js +1 -1
- package/package.json +68 -68
- package/runtime/router.js +596 -596
package/runtime/router.js
CHANGED
|
@@ -1,596 +1,596 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pulse Router - SPA routing system
|
|
3
|
-
*
|
|
4
|
-
* A simple but powerful router that integrates with Pulse reactivity
|
|
5
|
-
*
|
|
6
|
-
* Features:
|
|
7
|
-
* - Route params and query strings
|
|
8
|
-
* - Nested routes
|
|
9
|
-
* - Route meta fields
|
|
10
|
-
* - Per-route and global guards
|
|
11
|
-
* - Scroll restoration
|
|
12
|
-
* - Lazy-loaded routes
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import { pulse, effect, batch } from './pulse.js';
|
|
16
|
-
import { el } from './dom.js';
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Parse a route pattern into a regex and extract param names
|
|
20
|
-
* Supports: /users/:id, /posts/:id/comments, /files/*path, * (catch-all)
|
|
21
|
-
*/
|
|
22
|
-
function parsePattern(pattern) {
|
|
23
|
-
const paramNames = [];
|
|
24
|
-
|
|
25
|
-
// Handle standalone * as catch-all
|
|
26
|
-
if (pattern === '*') {
|
|
27
|
-
return {
|
|
28
|
-
regex: /^.*$/,
|
|
29
|
-
paramNames: []
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
let regexStr = pattern
|
|
34
|
-
// Escape special regex chars except : and *
|
|
35
|
-
.replace(/[.+?^${}()|[\]\\]/g, '\\$&')
|
|
36
|
-
// Handle wildcard params (*name)
|
|
37
|
-
.replace(/\*([a-zA-Z_][a-zA-Z0-9_]*)/g, (_, name) => {
|
|
38
|
-
paramNames.push(name);
|
|
39
|
-
return '(.*)';
|
|
40
|
-
})
|
|
41
|
-
// Handle named params (:name)
|
|
42
|
-
.replace(/:([a-zA-Z_][a-zA-Z0-9_]*)/g, (_, name) => {
|
|
43
|
-
paramNames.push(name);
|
|
44
|
-
return '([^/]+)';
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
// Ensure exact match
|
|
48
|
-
regexStr = `^${regexStr}$`;
|
|
49
|
-
|
|
50
|
-
return {
|
|
51
|
-
regex: new RegExp(regexStr),
|
|
52
|
-
paramNames
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Normalize route configuration
|
|
58
|
-
* Supports both simple (handler function) and full (object with meta) definitions
|
|
59
|
-
*/
|
|
60
|
-
function normalizeRoute(pattern, config) {
|
|
61
|
-
// Simple format: pattern -> handler
|
|
62
|
-
if (typeof config === 'function') {
|
|
63
|
-
return {
|
|
64
|
-
pattern,
|
|
65
|
-
handler: config,
|
|
66
|
-
meta: {},
|
|
67
|
-
beforeEnter: null,
|
|
68
|
-
children: null
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// Full format: pattern -> { handler, meta, beforeEnter, children }
|
|
73
|
-
return {
|
|
74
|
-
pattern,
|
|
75
|
-
handler: config.handler || config.component,
|
|
76
|
-
meta: config.meta || {},
|
|
77
|
-
beforeEnter: config.beforeEnter || null,
|
|
78
|
-
children: config.children || null,
|
|
79
|
-
redirect: config.redirect || null
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Match a path against a route pattern
|
|
85
|
-
*/
|
|
86
|
-
function matchRoute(pattern, path) {
|
|
87
|
-
const { regex, paramNames } = parsePattern(pattern);
|
|
88
|
-
const match = path.match(regex);
|
|
89
|
-
|
|
90
|
-
if (!match) return null;
|
|
91
|
-
|
|
92
|
-
const params = {};
|
|
93
|
-
paramNames.forEach((name, i) => {
|
|
94
|
-
params[name] = decodeURIComponent(match[i + 1]);
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
return params;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Parse query string into object
|
|
102
|
-
*/
|
|
103
|
-
function parseQuery(search) {
|
|
104
|
-
const params = new URLSearchParams(search);
|
|
105
|
-
const query = {};
|
|
106
|
-
for (const [key, value] of params) {
|
|
107
|
-
if (key in query) {
|
|
108
|
-
// Multiple values for same key
|
|
109
|
-
if (Array.isArray(query[key])) {
|
|
110
|
-
query[key].push(value);
|
|
111
|
-
} else {
|
|
112
|
-
query[key] = [query[key], value];
|
|
113
|
-
}
|
|
114
|
-
} else {
|
|
115
|
-
query[key] = value;
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
return query;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* Create a router instance
|
|
123
|
-
*/
|
|
124
|
-
export function createRouter(options = {}) {
|
|
125
|
-
const {
|
|
126
|
-
routes = {},
|
|
127
|
-
mode = 'history', // 'history' or 'hash'
|
|
128
|
-
base = '',
|
|
129
|
-
scrollBehavior = null // Function to control scroll restoration
|
|
130
|
-
} = options;
|
|
131
|
-
|
|
132
|
-
// Reactive state
|
|
133
|
-
const currentPath = pulse(getPath());
|
|
134
|
-
const currentRoute = pulse(null);
|
|
135
|
-
const currentParams = pulse({});
|
|
136
|
-
const currentQuery = pulse({});
|
|
137
|
-
const currentMeta = pulse({});
|
|
138
|
-
const isLoading = pulse(false);
|
|
139
|
-
|
|
140
|
-
// Scroll positions for history
|
|
141
|
-
const scrollPositions = new Map();
|
|
142
|
-
|
|
143
|
-
// Compile routes (supports nested routes)
|
|
144
|
-
const compiledRoutes = [];
|
|
145
|
-
|
|
146
|
-
function compileRoutes(routeConfig, parentPath = '') {
|
|
147
|
-
for (const [pattern, config] of Object.entries(routeConfig)) {
|
|
148
|
-
const normalized = normalizeRoute(pattern, config);
|
|
149
|
-
const fullPattern = parentPath + pattern;
|
|
150
|
-
|
|
151
|
-
compiledRoutes.push({
|
|
152
|
-
...normalized,
|
|
153
|
-
pattern: fullPattern,
|
|
154
|
-
...parsePattern(fullPattern)
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
// Compile children (nested routes)
|
|
158
|
-
if (normalized.children) {
|
|
159
|
-
compileRoutes(normalized.children, fullPattern);
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
compileRoutes(routes);
|
|
165
|
-
|
|
166
|
-
// Hooks
|
|
167
|
-
const beforeHooks = [];
|
|
168
|
-
const resolveHooks = [];
|
|
169
|
-
const afterHooks = [];
|
|
170
|
-
|
|
171
|
-
/**
|
|
172
|
-
* Get current path based on mode
|
|
173
|
-
*/
|
|
174
|
-
function getPath() {
|
|
175
|
-
if (mode === 'hash') {
|
|
176
|
-
return window.location.hash.slice(1) || '/';
|
|
177
|
-
}
|
|
178
|
-
let path = window.location.pathname;
|
|
179
|
-
if (base && path.startsWith(base)) {
|
|
180
|
-
path = path.slice(base.length) || '/';
|
|
181
|
-
}
|
|
182
|
-
return path;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/**
|
|
186
|
-
* Find matching route
|
|
187
|
-
*/
|
|
188
|
-
function findRoute(path) {
|
|
189
|
-
for (const route of compiledRoutes) {
|
|
190
|
-
const params = matchRoute(route.pattern, path);
|
|
191
|
-
if (params !== null) {
|
|
192
|
-
return { route, params };
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
return null;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
/**
|
|
199
|
-
* Navigate to a path
|
|
200
|
-
*/
|
|
201
|
-
async function navigate(path, options = {}) {
|
|
202
|
-
const { replace = false, query = {}, state = null } = options;
|
|
203
|
-
|
|
204
|
-
// Find matching route first (needed for beforeEnter guard)
|
|
205
|
-
const match = findRoute(path);
|
|
206
|
-
|
|
207
|
-
// Build full path with query
|
|
208
|
-
let fullPath = path;
|
|
209
|
-
const queryString = new URLSearchParams(query).toString();
|
|
210
|
-
if (queryString) {
|
|
211
|
-
fullPath += '?' + queryString;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
// Handle redirect
|
|
215
|
-
if (match?.route?.redirect) {
|
|
216
|
-
const redirectPath = typeof match.route.redirect === 'function'
|
|
217
|
-
? match.route.redirect({ params: match.params, query })
|
|
218
|
-
: match.route.redirect;
|
|
219
|
-
return navigate(redirectPath, { replace: true });
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
// Create navigation context for guards
|
|
223
|
-
const from = {
|
|
224
|
-
path: currentPath.peek(),
|
|
225
|
-
params: currentParams.peek(),
|
|
226
|
-
query: currentQuery.peek(),
|
|
227
|
-
meta: currentMeta.peek()
|
|
228
|
-
};
|
|
229
|
-
|
|
230
|
-
const to = {
|
|
231
|
-
path,
|
|
232
|
-
params: match?.params || {},
|
|
233
|
-
query: parseQuery(queryString),
|
|
234
|
-
meta: match?.route?.meta || {}
|
|
235
|
-
};
|
|
236
|
-
|
|
237
|
-
// Run global beforeEach hooks
|
|
238
|
-
for (const hook of beforeHooks) {
|
|
239
|
-
const result = await hook(to, from);
|
|
240
|
-
if (result === false) return false;
|
|
241
|
-
if (typeof result === 'string') {
|
|
242
|
-
return navigate(result, options);
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
// Run per-route beforeEnter guard
|
|
247
|
-
if (match?.route?.beforeEnter) {
|
|
248
|
-
const result = await match.route.beforeEnter(to, from);
|
|
249
|
-
if (result === false) return false;
|
|
250
|
-
if (typeof result === 'string') {
|
|
251
|
-
return navigate(result, options);
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
// Run beforeResolve hooks (after per-route guards)
|
|
256
|
-
for (const hook of resolveHooks) {
|
|
257
|
-
const result = await hook(to, from);
|
|
258
|
-
if (result === false) return false;
|
|
259
|
-
if (typeof result === 'string') {
|
|
260
|
-
return navigate(result, options);
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
// Save scroll position before leaving
|
|
265
|
-
const currentFullPath = currentPath.peek();
|
|
266
|
-
if (currentFullPath) {
|
|
267
|
-
scrollPositions.set(currentFullPath, {
|
|
268
|
-
x: window.scrollX,
|
|
269
|
-
y: window.scrollY
|
|
270
|
-
});
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
// Update URL
|
|
274
|
-
const url = mode === 'hash' ? `#${fullPath}` : `${base}${fullPath}`;
|
|
275
|
-
const historyState = { path: fullPath, ...(state || {}) };
|
|
276
|
-
|
|
277
|
-
if (replace) {
|
|
278
|
-
window.history.replaceState(historyState, '', url);
|
|
279
|
-
} else {
|
|
280
|
-
window.history.pushState(historyState, '', url);
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
// Update reactive state
|
|
284
|
-
await updateRoute(path, parseQuery(queryString), match);
|
|
285
|
-
|
|
286
|
-
// Handle scroll behavior
|
|
287
|
-
handleScroll(to, from, scrollPositions.get(path));
|
|
288
|
-
|
|
289
|
-
return true;
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
/**
|
|
293
|
-
* Handle scroll behavior after navigation
|
|
294
|
-
*/
|
|
295
|
-
function handleScroll(to, from, savedPosition) {
|
|
296
|
-
if (scrollBehavior) {
|
|
297
|
-
const position = scrollBehavior(to, from, savedPosition);
|
|
298
|
-
if (position) {
|
|
299
|
-
if (position.selector) {
|
|
300
|
-
// Scroll to element
|
|
301
|
-
const el = document.querySelector(position.selector);
|
|
302
|
-
if (el) {
|
|
303
|
-
el.scrollIntoView({ behavior: position.behavior || 'auto' });
|
|
304
|
-
}
|
|
305
|
-
} else if (typeof position.x === 'number' || typeof position.y === 'number') {
|
|
306
|
-
window.scrollTo({
|
|
307
|
-
left: position.x || 0,
|
|
308
|
-
top: position.y || 0,
|
|
309
|
-
behavior: position.behavior || 'auto'
|
|
310
|
-
});
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
} else if (savedPosition) {
|
|
314
|
-
// Default: restore saved position
|
|
315
|
-
window.scrollTo(savedPosition.x, savedPosition.y);
|
|
316
|
-
} else {
|
|
317
|
-
// Default: scroll to top
|
|
318
|
-
window.scrollTo(0, 0);
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
/**
|
|
323
|
-
* Update the current route state
|
|
324
|
-
*/
|
|
325
|
-
async function updateRoute(path, query = {}, match = null) {
|
|
326
|
-
if (!match) {
|
|
327
|
-
match = findRoute(path);
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
batch(() => {
|
|
331
|
-
currentPath.set(path);
|
|
332
|
-
currentQuery.set(query);
|
|
333
|
-
|
|
334
|
-
if (match) {
|
|
335
|
-
currentRoute.set(match.route);
|
|
336
|
-
currentParams.set(match.params);
|
|
337
|
-
currentMeta.set(match.route.meta || {});
|
|
338
|
-
} else {
|
|
339
|
-
currentRoute.set(null);
|
|
340
|
-
currentParams.set({});
|
|
341
|
-
currentMeta.set({});
|
|
342
|
-
}
|
|
343
|
-
});
|
|
344
|
-
|
|
345
|
-
// Run after hooks with full context
|
|
346
|
-
const to = {
|
|
347
|
-
path,
|
|
348
|
-
params: match?.params || {},
|
|
349
|
-
query,
|
|
350
|
-
meta: match?.route?.meta || {}
|
|
351
|
-
};
|
|
352
|
-
|
|
353
|
-
for (const hook of afterHooks) {
|
|
354
|
-
await hook(to);
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
/**
|
|
359
|
-
* Handle browser navigation (back/forward)
|
|
360
|
-
*/
|
|
361
|
-
function handlePopState() {
|
|
362
|
-
const path = getPath();
|
|
363
|
-
const query = parseQuery(window.location.search);
|
|
364
|
-
updateRoute(path, query);
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
/**
|
|
368
|
-
* Start listening to navigation events
|
|
369
|
-
*/
|
|
370
|
-
function start() {
|
|
371
|
-
window.addEventListener('popstate', handlePopState);
|
|
372
|
-
|
|
373
|
-
// Initial route
|
|
374
|
-
const query = parseQuery(window.location.search);
|
|
375
|
-
updateRoute(getPath(), query);
|
|
376
|
-
|
|
377
|
-
return () => {
|
|
378
|
-
window.removeEventListener('popstate', handlePopState);
|
|
379
|
-
};
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
/**
|
|
383
|
-
* Create a link element that uses the router
|
|
384
|
-
*/
|
|
385
|
-
function link(path, content, options = {}) {
|
|
386
|
-
const href = mode === 'hash' ? `#${path}` : `${base}${path}`;
|
|
387
|
-
const a = el('a', content);
|
|
388
|
-
a.href = href;
|
|
389
|
-
|
|
390
|
-
a.addEventListener('click', (e) => {
|
|
391
|
-
// Allow ctrl/cmd+click for new tab
|
|
392
|
-
if (e.ctrlKey || e.metaKey) return;
|
|
393
|
-
|
|
394
|
-
e.preventDefault();
|
|
395
|
-
navigate(path, options);
|
|
396
|
-
});
|
|
397
|
-
|
|
398
|
-
// Add active class when route matches
|
|
399
|
-
effect(() => {
|
|
400
|
-
const current = currentPath.get();
|
|
401
|
-
if (current === path || (options.exact === false && current.startsWith(path))) {
|
|
402
|
-
a.classList.add(options.activeClass || 'active');
|
|
403
|
-
} else {
|
|
404
|
-
a.classList.remove(options.activeClass || 'active');
|
|
405
|
-
}
|
|
406
|
-
});
|
|
407
|
-
|
|
408
|
-
return a;
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
/**
|
|
412
|
-
* Router outlet - renders the current route's component
|
|
413
|
-
*/
|
|
414
|
-
function outlet(container) {
|
|
415
|
-
if (typeof container === 'string') {
|
|
416
|
-
container = document.querySelector(container);
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
let currentView = null;
|
|
420
|
-
let cleanup = null;
|
|
421
|
-
|
|
422
|
-
effect(() => {
|
|
423
|
-
const route = currentRoute.get();
|
|
424
|
-
const params = currentParams.get();
|
|
425
|
-
const query = currentQuery.get();
|
|
426
|
-
|
|
427
|
-
// Cleanup previous view
|
|
428
|
-
if (cleanup) cleanup();
|
|
429
|
-
if (currentView) {
|
|
430
|
-
container.innerHTML = '';
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
if (route && route.handler) {
|
|
434
|
-
// Create context for the route handler
|
|
435
|
-
const ctx = {
|
|
436
|
-
params,
|
|
437
|
-
query,
|
|
438
|
-
path: currentPath.peek(),
|
|
439
|
-
navigate,
|
|
440
|
-
router
|
|
441
|
-
};
|
|
442
|
-
|
|
443
|
-
// Call handler and render result
|
|
444
|
-
const result = typeof route.handler === 'function'
|
|
445
|
-
? route.handler(ctx)
|
|
446
|
-
: route.handler;
|
|
447
|
-
|
|
448
|
-
if (result instanceof Node) {
|
|
449
|
-
container.appendChild(result);
|
|
450
|
-
currentView = result;
|
|
451
|
-
} else if (result && typeof result.then === 'function') {
|
|
452
|
-
// Async component
|
|
453
|
-
isLoading.set(true);
|
|
454
|
-
result.then(component => {
|
|
455
|
-
isLoading.set(false);
|
|
456
|
-
const view = typeof component === 'function' ? component(ctx) : component;
|
|
457
|
-
if (view instanceof Node) {
|
|
458
|
-
container.appendChild(view);
|
|
459
|
-
currentView = view;
|
|
460
|
-
}
|
|
461
|
-
});
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
});
|
|
465
|
-
|
|
466
|
-
return container;
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
/**
|
|
470
|
-
* Add navigation guard
|
|
471
|
-
*/
|
|
472
|
-
function beforeEach(hook) {
|
|
473
|
-
beforeHooks.push(hook);
|
|
474
|
-
return () => {
|
|
475
|
-
const index = beforeHooks.indexOf(hook);
|
|
476
|
-
if (index > -1) beforeHooks.splice(index, 1);
|
|
477
|
-
};
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
/**
|
|
481
|
-
* Add before resolve hook (runs after per-route guards)
|
|
482
|
-
*/
|
|
483
|
-
function beforeResolve(hook) {
|
|
484
|
-
resolveHooks.push(hook);
|
|
485
|
-
return () => {
|
|
486
|
-
const index = resolveHooks.indexOf(hook);
|
|
487
|
-
if (index > -1) resolveHooks.splice(index, 1);
|
|
488
|
-
};
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
/**
|
|
492
|
-
* Add after navigation hook
|
|
493
|
-
*/
|
|
494
|
-
function afterEach(hook) {
|
|
495
|
-
afterHooks.push(hook);
|
|
496
|
-
return () => {
|
|
497
|
-
const index = afterHooks.indexOf(hook);
|
|
498
|
-
if (index > -1) afterHooks.splice(index, 1);
|
|
499
|
-
};
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
/**
|
|
503
|
-
* Check if a route matches the given path
|
|
504
|
-
*/
|
|
505
|
-
function isActive(path, exact = false) {
|
|
506
|
-
const current = currentPath.get();
|
|
507
|
-
if (exact) {
|
|
508
|
-
return current === path;
|
|
509
|
-
}
|
|
510
|
-
return current.startsWith(path);
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
/**
|
|
514
|
-
* Get all matched routes (for nested routes)
|
|
515
|
-
*/
|
|
516
|
-
function getMatchedRoutes(path) {
|
|
517
|
-
const matches = [];
|
|
518
|
-
for (const route of compiledRoutes) {
|
|
519
|
-
const params = matchRoute(route.pattern, path);
|
|
520
|
-
if (params !== null) {
|
|
521
|
-
matches.push({ route, params });
|
|
522
|
-
}
|
|
523
|
-
}
|
|
524
|
-
return matches;
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
/**
|
|
528
|
-
* Go back in history
|
|
529
|
-
*/
|
|
530
|
-
function back() {
|
|
531
|
-
window.history.back();
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
/**
|
|
535
|
-
* Go forward in history
|
|
536
|
-
*/
|
|
537
|
-
function forward() {
|
|
538
|
-
window.history.forward();
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
/**
|
|
542
|
-
* Go to specific history entry
|
|
543
|
-
*/
|
|
544
|
-
function go(delta) {
|
|
545
|
-
window.history.go(delta);
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
const router = {
|
|
549
|
-
// Reactive state (read-only)
|
|
550
|
-
path: currentPath,
|
|
551
|
-
route: currentRoute,
|
|
552
|
-
params: currentParams,
|
|
553
|
-
query: currentQuery,
|
|
554
|
-
meta: currentMeta,
|
|
555
|
-
loading: isLoading,
|
|
556
|
-
|
|
557
|
-
// Methods
|
|
558
|
-
navigate,
|
|
559
|
-
start,
|
|
560
|
-
link,
|
|
561
|
-
outlet,
|
|
562
|
-
beforeEach,
|
|
563
|
-
beforeResolve,
|
|
564
|
-
afterEach,
|
|
565
|
-
back,
|
|
566
|
-
forward,
|
|
567
|
-
go,
|
|
568
|
-
|
|
569
|
-
// Route inspection
|
|
570
|
-
isActive,
|
|
571
|
-
getMatchedRoutes,
|
|
572
|
-
|
|
573
|
-
// Utils
|
|
574
|
-
matchRoute,
|
|
575
|
-
parseQuery
|
|
576
|
-
};
|
|
577
|
-
|
|
578
|
-
return router;
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
/**
|
|
582
|
-
* Create a simple router for quick setup
|
|
583
|
-
*/
|
|
584
|
-
export function simpleRouter(routes, target = '#app') {
|
|
585
|
-
const router = createRouter({ routes });
|
|
586
|
-
router.start();
|
|
587
|
-
router.outlet(target);
|
|
588
|
-
return router;
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
export default {
|
|
592
|
-
createRouter,
|
|
593
|
-
simpleRouter,
|
|
594
|
-
matchRoute,
|
|
595
|
-
parseQuery
|
|
596
|
-
};
|
|
1
|
+
/**
|
|
2
|
+
* Pulse Router - SPA routing system
|
|
3
|
+
*
|
|
4
|
+
* A simple but powerful router that integrates with Pulse reactivity
|
|
5
|
+
*
|
|
6
|
+
* Features:
|
|
7
|
+
* - Route params and query strings
|
|
8
|
+
* - Nested routes
|
|
9
|
+
* - Route meta fields
|
|
10
|
+
* - Per-route and global guards
|
|
11
|
+
* - Scroll restoration
|
|
12
|
+
* - Lazy-loaded routes
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { pulse, effect, batch } from './pulse.js';
|
|
16
|
+
import { el } from './dom.js';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Parse a route pattern into a regex and extract param names
|
|
20
|
+
* Supports: /users/:id, /posts/:id/comments, /files/*path, * (catch-all)
|
|
21
|
+
*/
|
|
22
|
+
function parsePattern(pattern) {
|
|
23
|
+
const paramNames = [];
|
|
24
|
+
|
|
25
|
+
// Handle standalone * as catch-all
|
|
26
|
+
if (pattern === '*') {
|
|
27
|
+
return {
|
|
28
|
+
regex: /^.*$/,
|
|
29
|
+
paramNames: []
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let regexStr = pattern
|
|
34
|
+
// Escape special regex chars except : and *
|
|
35
|
+
.replace(/[.+?^${}()|[\]\\]/g, '\\$&')
|
|
36
|
+
// Handle wildcard params (*name)
|
|
37
|
+
.replace(/\*([a-zA-Z_][a-zA-Z0-9_]*)/g, (_, name) => {
|
|
38
|
+
paramNames.push(name);
|
|
39
|
+
return '(.*)';
|
|
40
|
+
})
|
|
41
|
+
// Handle named params (:name)
|
|
42
|
+
.replace(/:([a-zA-Z_][a-zA-Z0-9_]*)/g, (_, name) => {
|
|
43
|
+
paramNames.push(name);
|
|
44
|
+
return '([^/]+)';
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// Ensure exact match
|
|
48
|
+
regexStr = `^${regexStr}$`;
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
regex: new RegExp(regexStr),
|
|
52
|
+
paramNames
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Normalize route configuration
|
|
58
|
+
* Supports both simple (handler function) and full (object with meta) definitions
|
|
59
|
+
*/
|
|
60
|
+
function normalizeRoute(pattern, config) {
|
|
61
|
+
// Simple format: pattern -> handler
|
|
62
|
+
if (typeof config === 'function') {
|
|
63
|
+
return {
|
|
64
|
+
pattern,
|
|
65
|
+
handler: config,
|
|
66
|
+
meta: {},
|
|
67
|
+
beforeEnter: null,
|
|
68
|
+
children: null
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Full format: pattern -> { handler, meta, beforeEnter, children }
|
|
73
|
+
return {
|
|
74
|
+
pattern,
|
|
75
|
+
handler: config.handler || config.component,
|
|
76
|
+
meta: config.meta || {},
|
|
77
|
+
beforeEnter: config.beforeEnter || null,
|
|
78
|
+
children: config.children || null,
|
|
79
|
+
redirect: config.redirect || null
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Match a path against a route pattern
|
|
85
|
+
*/
|
|
86
|
+
function matchRoute(pattern, path) {
|
|
87
|
+
const { regex, paramNames } = parsePattern(pattern);
|
|
88
|
+
const match = path.match(regex);
|
|
89
|
+
|
|
90
|
+
if (!match) return null;
|
|
91
|
+
|
|
92
|
+
const params = {};
|
|
93
|
+
paramNames.forEach((name, i) => {
|
|
94
|
+
params[name] = decodeURIComponent(match[i + 1]);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
return params;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Parse query string into object
|
|
102
|
+
*/
|
|
103
|
+
function parseQuery(search) {
|
|
104
|
+
const params = new URLSearchParams(search);
|
|
105
|
+
const query = {};
|
|
106
|
+
for (const [key, value] of params) {
|
|
107
|
+
if (key in query) {
|
|
108
|
+
// Multiple values for same key
|
|
109
|
+
if (Array.isArray(query[key])) {
|
|
110
|
+
query[key].push(value);
|
|
111
|
+
} else {
|
|
112
|
+
query[key] = [query[key], value];
|
|
113
|
+
}
|
|
114
|
+
} else {
|
|
115
|
+
query[key] = value;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return query;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Create a router instance
|
|
123
|
+
*/
|
|
124
|
+
export function createRouter(options = {}) {
|
|
125
|
+
const {
|
|
126
|
+
routes = {},
|
|
127
|
+
mode = 'history', // 'history' or 'hash'
|
|
128
|
+
base = '',
|
|
129
|
+
scrollBehavior = null // Function to control scroll restoration
|
|
130
|
+
} = options;
|
|
131
|
+
|
|
132
|
+
// Reactive state
|
|
133
|
+
const currentPath = pulse(getPath());
|
|
134
|
+
const currentRoute = pulse(null);
|
|
135
|
+
const currentParams = pulse({});
|
|
136
|
+
const currentQuery = pulse({});
|
|
137
|
+
const currentMeta = pulse({});
|
|
138
|
+
const isLoading = pulse(false);
|
|
139
|
+
|
|
140
|
+
// Scroll positions for history
|
|
141
|
+
const scrollPositions = new Map();
|
|
142
|
+
|
|
143
|
+
// Compile routes (supports nested routes)
|
|
144
|
+
const compiledRoutes = [];
|
|
145
|
+
|
|
146
|
+
function compileRoutes(routeConfig, parentPath = '') {
|
|
147
|
+
for (const [pattern, config] of Object.entries(routeConfig)) {
|
|
148
|
+
const normalized = normalizeRoute(pattern, config);
|
|
149
|
+
const fullPattern = parentPath + pattern;
|
|
150
|
+
|
|
151
|
+
compiledRoutes.push({
|
|
152
|
+
...normalized,
|
|
153
|
+
pattern: fullPattern,
|
|
154
|
+
...parsePattern(fullPattern)
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// Compile children (nested routes)
|
|
158
|
+
if (normalized.children) {
|
|
159
|
+
compileRoutes(normalized.children, fullPattern);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
compileRoutes(routes);
|
|
165
|
+
|
|
166
|
+
// Hooks
|
|
167
|
+
const beforeHooks = [];
|
|
168
|
+
const resolveHooks = [];
|
|
169
|
+
const afterHooks = [];
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Get current path based on mode
|
|
173
|
+
*/
|
|
174
|
+
function getPath() {
|
|
175
|
+
if (mode === 'hash') {
|
|
176
|
+
return window.location.hash.slice(1) || '/';
|
|
177
|
+
}
|
|
178
|
+
let path = window.location.pathname;
|
|
179
|
+
if (base && path.startsWith(base)) {
|
|
180
|
+
path = path.slice(base.length) || '/';
|
|
181
|
+
}
|
|
182
|
+
return path;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Find matching route
|
|
187
|
+
*/
|
|
188
|
+
function findRoute(path) {
|
|
189
|
+
for (const route of compiledRoutes) {
|
|
190
|
+
const params = matchRoute(route.pattern, path);
|
|
191
|
+
if (params !== null) {
|
|
192
|
+
return { route, params };
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Navigate to a path
|
|
200
|
+
*/
|
|
201
|
+
async function navigate(path, options = {}) {
|
|
202
|
+
const { replace = false, query = {}, state = null } = options;
|
|
203
|
+
|
|
204
|
+
// Find matching route first (needed for beforeEnter guard)
|
|
205
|
+
const match = findRoute(path);
|
|
206
|
+
|
|
207
|
+
// Build full path with query
|
|
208
|
+
let fullPath = path;
|
|
209
|
+
const queryString = new URLSearchParams(query).toString();
|
|
210
|
+
if (queryString) {
|
|
211
|
+
fullPath += '?' + queryString;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Handle redirect
|
|
215
|
+
if (match?.route?.redirect) {
|
|
216
|
+
const redirectPath = typeof match.route.redirect === 'function'
|
|
217
|
+
? match.route.redirect({ params: match.params, query })
|
|
218
|
+
: match.route.redirect;
|
|
219
|
+
return navigate(redirectPath, { replace: true });
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Create navigation context for guards
|
|
223
|
+
const from = {
|
|
224
|
+
path: currentPath.peek(),
|
|
225
|
+
params: currentParams.peek(),
|
|
226
|
+
query: currentQuery.peek(),
|
|
227
|
+
meta: currentMeta.peek()
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
const to = {
|
|
231
|
+
path,
|
|
232
|
+
params: match?.params || {},
|
|
233
|
+
query: parseQuery(queryString),
|
|
234
|
+
meta: match?.route?.meta || {}
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
// Run global beforeEach hooks
|
|
238
|
+
for (const hook of beforeHooks) {
|
|
239
|
+
const result = await hook(to, from);
|
|
240
|
+
if (result === false) return false;
|
|
241
|
+
if (typeof result === 'string') {
|
|
242
|
+
return navigate(result, options);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Run per-route beforeEnter guard
|
|
247
|
+
if (match?.route?.beforeEnter) {
|
|
248
|
+
const result = await match.route.beforeEnter(to, from);
|
|
249
|
+
if (result === false) return false;
|
|
250
|
+
if (typeof result === 'string') {
|
|
251
|
+
return navigate(result, options);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Run beforeResolve hooks (after per-route guards)
|
|
256
|
+
for (const hook of resolveHooks) {
|
|
257
|
+
const result = await hook(to, from);
|
|
258
|
+
if (result === false) return false;
|
|
259
|
+
if (typeof result === 'string') {
|
|
260
|
+
return navigate(result, options);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Save scroll position before leaving
|
|
265
|
+
const currentFullPath = currentPath.peek();
|
|
266
|
+
if (currentFullPath) {
|
|
267
|
+
scrollPositions.set(currentFullPath, {
|
|
268
|
+
x: window.scrollX,
|
|
269
|
+
y: window.scrollY
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Update URL
|
|
274
|
+
const url = mode === 'hash' ? `#${fullPath}` : `${base}${fullPath}`;
|
|
275
|
+
const historyState = { path: fullPath, ...(state || {}) };
|
|
276
|
+
|
|
277
|
+
if (replace) {
|
|
278
|
+
window.history.replaceState(historyState, '', url);
|
|
279
|
+
} else {
|
|
280
|
+
window.history.pushState(historyState, '', url);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Update reactive state
|
|
284
|
+
await updateRoute(path, parseQuery(queryString), match);
|
|
285
|
+
|
|
286
|
+
// Handle scroll behavior
|
|
287
|
+
handleScroll(to, from, scrollPositions.get(path));
|
|
288
|
+
|
|
289
|
+
return true;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Handle scroll behavior after navigation
|
|
294
|
+
*/
|
|
295
|
+
function handleScroll(to, from, savedPosition) {
|
|
296
|
+
if (scrollBehavior) {
|
|
297
|
+
const position = scrollBehavior(to, from, savedPosition);
|
|
298
|
+
if (position) {
|
|
299
|
+
if (position.selector) {
|
|
300
|
+
// Scroll to element
|
|
301
|
+
const el = document.querySelector(position.selector);
|
|
302
|
+
if (el) {
|
|
303
|
+
el.scrollIntoView({ behavior: position.behavior || 'auto' });
|
|
304
|
+
}
|
|
305
|
+
} else if (typeof position.x === 'number' || typeof position.y === 'number') {
|
|
306
|
+
window.scrollTo({
|
|
307
|
+
left: position.x || 0,
|
|
308
|
+
top: position.y || 0,
|
|
309
|
+
behavior: position.behavior || 'auto'
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
} else if (savedPosition) {
|
|
314
|
+
// Default: restore saved position
|
|
315
|
+
window.scrollTo(savedPosition.x, savedPosition.y);
|
|
316
|
+
} else {
|
|
317
|
+
// Default: scroll to top
|
|
318
|
+
window.scrollTo(0, 0);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Update the current route state
|
|
324
|
+
*/
|
|
325
|
+
async function updateRoute(path, query = {}, match = null) {
|
|
326
|
+
if (!match) {
|
|
327
|
+
match = findRoute(path);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
batch(() => {
|
|
331
|
+
currentPath.set(path);
|
|
332
|
+
currentQuery.set(query);
|
|
333
|
+
|
|
334
|
+
if (match) {
|
|
335
|
+
currentRoute.set(match.route);
|
|
336
|
+
currentParams.set(match.params);
|
|
337
|
+
currentMeta.set(match.route.meta || {});
|
|
338
|
+
} else {
|
|
339
|
+
currentRoute.set(null);
|
|
340
|
+
currentParams.set({});
|
|
341
|
+
currentMeta.set({});
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
// Run after hooks with full context
|
|
346
|
+
const to = {
|
|
347
|
+
path,
|
|
348
|
+
params: match?.params || {},
|
|
349
|
+
query,
|
|
350
|
+
meta: match?.route?.meta || {}
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
for (const hook of afterHooks) {
|
|
354
|
+
await hook(to);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Handle browser navigation (back/forward)
|
|
360
|
+
*/
|
|
361
|
+
function handlePopState() {
|
|
362
|
+
const path = getPath();
|
|
363
|
+
const query = parseQuery(window.location.search);
|
|
364
|
+
updateRoute(path, query);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Start listening to navigation events
|
|
369
|
+
*/
|
|
370
|
+
function start() {
|
|
371
|
+
window.addEventListener('popstate', handlePopState);
|
|
372
|
+
|
|
373
|
+
// Initial route
|
|
374
|
+
const query = parseQuery(window.location.search);
|
|
375
|
+
updateRoute(getPath(), query);
|
|
376
|
+
|
|
377
|
+
return () => {
|
|
378
|
+
window.removeEventListener('popstate', handlePopState);
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Create a link element that uses the router
|
|
384
|
+
*/
|
|
385
|
+
function link(path, content, options = {}) {
|
|
386
|
+
const href = mode === 'hash' ? `#${path}` : `${base}${path}`;
|
|
387
|
+
const a = el('a', content);
|
|
388
|
+
a.href = href;
|
|
389
|
+
|
|
390
|
+
a.addEventListener('click', (e) => {
|
|
391
|
+
// Allow ctrl/cmd+click for new tab
|
|
392
|
+
if (e.ctrlKey || e.metaKey) return;
|
|
393
|
+
|
|
394
|
+
e.preventDefault();
|
|
395
|
+
navigate(path, options);
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
// Add active class when route matches
|
|
399
|
+
effect(() => {
|
|
400
|
+
const current = currentPath.get();
|
|
401
|
+
if (current === path || (options.exact === false && current.startsWith(path))) {
|
|
402
|
+
a.classList.add(options.activeClass || 'active');
|
|
403
|
+
} else {
|
|
404
|
+
a.classList.remove(options.activeClass || 'active');
|
|
405
|
+
}
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
return a;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Router outlet - renders the current route's component
|
|
413
|
+
*/
|
|
414
|
+
function outlet(container) {
|
|
415
|
+
if (typeof container === 'string') {
|
|
416
|
+
container = document.querySelector(container);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
let currentView = null;
|
|
420
|
+
let cleanup = null;
|
|
421
|
+
|
|
422
|
+
effect(() => {
|
|
423
|
+
const route = currentRoute.get();
|
|
424
|
+
const params = currentParams.get();
|
|
425
|
+
const query = currentQuery.get();
|
|
426
|
+
|
|
427
|
+
// Cleanup previous view
|
|
428
|
+
if (cleanup) cleanup();
|
|
429
|
+
if (currentView) {
|
|
430
|
+
container.innerHTML = '';
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
if (route && route.handler) {
|
|
434
|
+
// Create context for the route handler
|
|
435
|
+
const ctx = {
|
|
436
|
+
params,
|
|
437
|
+
query,
|
|
438
|
+
path: currentPath.peek(),
|
|
439
|
+
navigate,
|
|
440
|
+
router
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
// Call handler and render result
|
|
444
|
+
const result = typeof route.handler === 'function'
|
|
445
|
+
? route.handler(ctx)
|
|
446
|
+
: route.handler;
|
|
447
|
+
|
|
448
|
+
if (result instanceof Node) {
|
|
449
|
+
container.appendChild(result);
|
|
450
|
+
currentView = result;
|
|
451
|
+
} else if (result && typeof result.then === 'function') {
|
|
452
|
+
// Async component
|
|
453
|
+
isLoading.set(true);
|
|
454
|
+
result.then(component => {
|
|
455
|
+
isLoading.set(false);
|
|
456
|
+
const view = typeof component === 'function' ? component(ctx) : component;
|
|
457
|
+
if (view instanceof Node) {
|
|
458
|
+
container.appendChild(view);
|
|
459
|
+
currentView = view;
|
|
460
|
+
}
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
return container;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Add navigation guard
|
|
471
|
+
*/
|
|
472
|
+
function beforeEach(hook) {
|
|
473
|
+
beforeHooks.push(hook);
|
|
474
|
+
return () => {
|
|
475
|
+
const index = beforeHooks.indexOf(hook);
|
|
476
|
+
if (index > -1) beforeHooks.splice(index, 1);
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Add before resolve hook (runs after per-route guards)
|
|
482
|
+
*/
|
|
483
|
+
function beforeResolve(hook) {
|
|
484
|
+
resolveHooks.push(hook);
|
|
485
|
+
return () => {
|
|
486
|
+
const index = resolveHooks.indexOf(hook);
|
|
487
|
+
if (index > -1) resolveHooks.splice(index, 1);
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Add after navigation hook
|
|
493
|
+
*/
|
|
494
|
+
function afterEach(hook) {
|
|
495
|
+
afterHooks.push(hook);
|
|
496
|
+
return () => {
|
|
497
|
+
const index = afterHooks.indexOf(hook);
|
|
498
|
+
if (index > -1) afterHooks.splice(index, 1);
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Check if a route matches the given path
|
|
504
|
+
*/
|
|
505
|
+
function isActive(path, exact = false) {
|
|
506
|
+
const current = currentPath.get();
|
|
507
|
+
if (exact) {
|
|
508
|
+
return current === path;
|
|
509
|
+
}
|
|
510
|
+
return current.startsWith(path);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Get all matched routes (for nested routes)
|
|
515
|
+
*/
|
|
516
|
+
function getMatchedRoutes(path) {
|
|
517
|
+
const matches = [];
|
|
518
|
+
for (const route of compiledRoutes) {
|
|
519
|
+
const params = matchRoute(route.pattern, path);
|
|
520
|
+
if (params !== null) {
|
|
521
|
+
matches.push({ route, params });
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
return matches;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* Go back in history
|
|
529
|
+
*/
|
|
530
|
+
function back() {
|
|
531
|
+
window.history.back();
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* Go forward in history
|
|
536
|
+
*/
|
|
537
|
+
function forward() {
|
|
538
|
+
window.history.forward();
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* Go to specific history entry
|
|
543
|
+
*/
|
|
544
|
+
function go(delta) {
|
|
545
|
+
window.history.go(delta);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
const router = {
|
|
549
|
+
// Reactive state (read-only)
|
|
550
|
+
path: currentPath,
|
|
551
|
+
route: currentRoute,
|
|
552
|
+
params: currentParams,
|
|
553
|
+
query: currentQuery,
|
|
554
|
+
meta: currentMeta,
|
|
555
|
+
loading: isLoading,
|
|
556
|
+
|
|
557
|
+
// Methods
|
|
558
|
+
navigate,
|
|
559
|
+
start,
|
|
560
|
+
link,
|
|
561
|
+
outlet,
|
|
562
|
+
beforeEach,
|
|
563
|
+
beforeResolve,
|
|
564
|
+
afterEach,
|
|
565
|
+
back,
|
|
566
|
+
forward,
|
|
567
|
+
go,
|
|
568
|
+
|
|
569
|
+
// Route inspection
|
|
570
|
+
isActive,
|
|
571
|
+
getMatchedRoutes,
|
|
572
|
+
|
|
573
|
+
// Utils
|
|
574
|
+
matchRoute,
|
|
575
|
+
parseQuery
|
|
576
|
+
};
|
|
577
|
+
|
|
578
|
+
return router;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* Create a simple router for quick setup
|
|
583
|
+
*/
|
|
584
|
+
export function simpleRouter(routes, target = '#app') {
|
|
585
|
+
const router = createRouter({ routes });
|
|
586
|
+
router.start();
|
|
587
|
+
router.outlet(target);
|
|
588
|
+
return router;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
export default {
|
|
592
|
+
createRouter,
|
|
593
|
+
simpleRouter,
|
|
594
|
+
matchRoute,
|
|
595
|
+
parseQuery
|
|
596
|
+
};
|