elementdrawing 1.0.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/dist/elementdrawing.min.js +3 -0
- package/dist/elementdrawing.min.js.LICENSE.txt +8 -0
- package/dist/elementdrawing.min.js.map +1 -0
- package/dist/index.html +1 -0
- package/package.json +127 -0
- package/src/core/bridge.h +855 -0
- package/src/core/diff.c +900 -0
- package/src/core/element.c +1078 -0
- package/src/core/event.c +813 -0
- package/src/core/fiber.c +1027 -0
- package/src/core/hooks.c +919 -0
- package/src/core/renderer.c +963 -0
- package/src/core/scheduler.c +702 -0
- package/src/core/state.c +803 -0
- package/src/css/animations.css +779 -0
- package/src/css/base.css +615 -0
- package/src/css/components.css +1311 -0
- package/src/css/tailwind.css +370 -0
- package/src/css/themes.css +517 -0
- package/src/css/utilities.css +475 -0
- package/src/index.js +746 -0
- package/src/js/animation.js +655 -0
- package/src/js/dom.js +665 -0
- package/src/js/events.js +585 -0
- package/src/js/http.js +446 -0
- package/src/js/index.js +26 -0
- package/src/js/router.js +483 -0
- package/src/js/store.js +539 -0
- package/src/js/utils.js +593 -0
- package/src/js/validator.js +529 -0
- package/src/jsx/components/Accordion.jsx +210 -0
- package/src/jsx/components/Alert.jsx +169 -0
- package/src/jsx/components/Avatar.jsx +214 -0
- package/src/jsx/components/Badge.jsx +136 -0
- package/src/jsx/components/Breadcrumb.jsx +200 -0
- package/src/jsx/components/Button.jsx +188 -0
- package/src/jsx/components/Card.jsx +192 -0
- package/src/jsx/components/Carousel.jsx +278 -0
- package/src/jsx/components/Checkbox.jsx +215 -0
- package/src/jsx/components/Dialog.jsx +242 -0
- package/src/jsx/components/Drawer.jsx +190 -0
- package/src/jsx/components/Dropdown.jsx +268 -0
- package/src/jsx/components/Form.jsx +274 -0
- package/src/jsx/components/Input.jsx +285 -0
- package/src/jsx/components/Menu.jsx +276 -0
- package/src/jsx/components/Modal.jsx +274 -0
- package/src/jsx/components/Navbar.jsx +292 -0
- package/src/jsx/components/Pagination.jsx +268 -0
- package/src/jsx/components/Progress.jsx +252 -0
- package/src/jsx/components/Radio.jsx +208 -0
- package/src/jsx/components/Select.jsx +397 -0
- package/src/jsx/components/Sidebar.jsx +250 -0
- package/src/jsx/components/Slider.jsx +310 -0
- package/src/jsx/components/Spinner.jsx +198 -0
- package/src/jsx/components/Switch.jsx +201 -0
- package/src/jsx/components/Table.jsx +332 -0
- package/src/jsx/components/Tabs.jsx +227 -0
- package/src/jsx/components/Textarea.jsx +212 -0
- package/src/jsx/components/Toast.jsx +270 -0
- package/src/jsx/components/Tooltip.jsx +178 -0
- package/src/jsx/components/Typography.jsx +299 -0
- package/src/jsx/components/index.jsx +70 -0
- package/src/jsx/core/element.js +3 -0
- package/src/jsx/hooks/index.js +356 -0
- package/src/jsx/hooks/useCallback.js +472 -0
- package/src/jsx/hooks/useContext.js +586 -0
- package/src/jsx/hooks/useEffect.js +704 -0
- package/src/jsx/hooks/useLayoutEffect.js +508 -0
- package/src/jsx/hooks/useMemo.js +689 -0
- package/src/jsx/hooks/useReducer.js +729 -0
- package/src/jsx/hooks/useRef.js +542 -0
- package/src/jsx/hooks/useState.js +854 -0
- package/src/jsx/runtime/commit.js +903 -0
- package/src/jsx/runtime/createElement.js +860 -0
- package/src/jsx/runtime/index.js +356 -0
- package/src/jsx/runtime/reconcile.js +687 -0
- package/src/jsx/runtime/render.js +914 -0
package/src/js/router.js
ADDED
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-Side Router
|
|
3
|
+
* ElementDrawing Framework - Route definition, matching, navigation,
|
|
4
|
+
* History API, hash fallback, guards, lazy loading, nested routes,
|
|
5
|
+
* named routes, transitions, and query string parsing.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
'use strict';
|
|
9
|
+
|
|
10
|
+
// ─── Route Definition ─────────────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Create a route configuration.
|
|
14
|
+
* @param {Object} config
|
|
15
|
+
* @param {string} config.path - URL path pattern
|
|
16
|
+
* @param {Function|string} config.component - Component to render
|
|
17
|
+
* @param {string} [config.name] - Named route
|
|
18
|
+
* @param {Object} [config.meta] - Route metadata
|
|
19
|
+
* @param {Function} [config.redirect] - Redirect function or path
|
|
20
|
+
* @param {Function} [config.beforeEnter] - Navigation guard
|
|
21
|
+
* @param {Array} [config.children] - Child routes
|
|
22
|
+
* @param {Object} [config.props] - Props to pass to component
|
|
23
|
+
* @returns {Object} Route configuration
|
|
24
|
+
*/
|
|
25
|
+
function createRoute(config) {
|
|
26
|
+
return {
|
|
27
|
+
path: config.path,
|
|
28
|
+
component: config.component,
|
|
29
|
+
name: config.name || null,
|
|
30
|
+
meta: config.meta || {},
|
|
31
|
+
redirect: config.redirect || null,
|
|
32
|
+
beforeEnter: config.beforeEnter || null,
|
|
33
|
+
children: (config.children || []).map((child) => {
|
|
34
|
+
// Inherit parent path prefix
|
|
35
|
+
if (config.path && config.path !== '/' && child.path && !child.path.startsWith('/')) {
|
|
36
|
+
child.path = config.path.replace(/\/$/, '') + '/' + child.path;
|
|
37
|
+
}
|
|
38
|
+
return createRoute(child);
|
|
39
|
+
}),
|
|
40
|
+
props: config.props || {},
|
|
41
|
+
_regex: null,
|
|
42
|
+
_paramNames: [],
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ─── Path Matching ────────────────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Compile a path pattern into a regex.
|
|
50
|
+
* Supports :param, :param?, *wildcard
|
|
51
|
+
* @param {string} path
|
|
52
|
+
* @returns {{ regex: RegExp, paramNames: string[] }}
|
|
53
|
+
*/
|
|
54
|
+
function compilePath(path) {
|
|
55
|
+
const paramNames = [];
|
|
56
|
+
let regexStr = '^' + path
|
|
57
|
+
.replace(/\//g, '\\/')
|
|
58
|
+
.replace(/:([^/]+)/g, (_, paramName) => {
|
|
59
|
+
if (paramName.endsWith('?')) {
|
|
60
|
+
paramNames.push(paramName.slice(0, -1));
|
|
61
|
+
return '(?:([^/]+))?';
|
|
62
|
+
}
|
|
63
|
+
paramNames.push(paramName);
|
|
64
|
+
return '([^/]+)';
|
|
65
|
+
})
|
|
66
|
+
.replace(/\*([^/]*)/, '(?:.*)') +
|
|
67
|
+
'(?:\\?.*)?$';
|
|
68
|
+
|
|
69
|
+
return { regex: new RegExp(regexStr), paramNames };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Match a path against a route pattern.
|
|
74
|
+
* @param {string} pattern - Route pattern (e.g., '/users/:id')
|
|
75
|
+
* @param {string} path - Actual path to match
|
|
76
|
+
* @param {boolean} [exact=true] - Require exact match
|
|
77
|
+
* @returns {Object|null} Match result with params, or null
|
|
78
|
+
*/
|
|
79
|
+
function matchPath(pattern, path, exact) {
|
|
80
|
+
exact = exact !== false;
|
|
81
|
+
const { regex, paramNames } = compilePath(pattern);
|
|
82
|
+
const match = path.match(regex);
|
|
83
|
+
|
|
84
|
+
if (!match) return null;
|
|
85
|
+
|
|
86
|
+
const params = {};
|
|
87
|
+
paramNames.forEach((name, i) => {
|
|
88
|
+
params[name] = match[i + 1] !== undefined ? decodeURIComponent(match[i + 1]) : undefined;
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// Check if remaining path exists for non-exact match
|
|
92
|
+
const matchedLength = match[0].length;
|
|
93
|
+
if (exact && matchedLength < path.length) {
|
|
94
|
+
// Check for query string
|
|
95
|
+
if (path[matchedLength] !== '?') return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
path: match[0].replace(/\?.*$/, ''),
|
|
100
|
+
params,
|
|
101
|
+
isExact: matchedLength === path.length || path[matchedLength] === '?',
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Match a path against all registered routes.
|
|
107
|
+
* @param {string} path
|
|
108
|
+
* @param {Array} routes
|
|
109
|
+
* @returns {Object|null} Matched route with params
|
|
110
|
+
*/
|
|
111
|
+
function matchRoute(path, routes) {
|
|
112
|
+
for (let i = 0; i < routes.length; i++) {
|
|
113
|
+
const route = routes[i];
|
|
114
|
+
const match = matchPath(route.path, path, !route.children || route.children.length === 0);
|
|
115
|
+
|
|
116
|
+
if (match) {
|
|
117
|
+
const result = { route, match };
|
|
118
|
+
|
|
119
|
+
// Check children for nested matching
|
|
120
|
+
if (route.children && route.children.length > 0) {
|
|
121
|
+
const remainingPath = path.slice(match.path.length) || '/';
|
|
122
|
+
const childMatch = matchRoute(remainingPath, route.children);
|
|
123
|
+
if (childMatch) {
|
|
124
|
+
result.child = childMatch;
|
|
125
|
+
Object.assign(result.match.params, childMatch.match.params);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return result;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ─── Query String Parsing ─────────────────────────────────────────────────────
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Parse a query string into an object.
|
|
139
|
+
* @param {string} qs
|
|
140
|
+
* @returns {Object}
|
|
141
|
+
*/
|
|
142
|
+
function parseQuery(qs) {
|
|
143
|
+
if (!qs) return {};
|
|
144
|
+
qs = qs.replace(/^\?/, '');
|
|
145
|
+
const params = {};
|
|
146
|
+
qs.split('&').forEach((pair) => {
|
|
147
|
+
const [key, val] = pair.split('=');
|
|
148
|
+
if (!key) return;
|
|
149
|
+
const decoded = decodeURIComponent(val || '');
|
|
150
|
+
if (params[decodeURIComponent(key)]) {
|
|
151
|
+
if (!Array.isArray(params[decodeURIComponent(key)])) {
|
|
152
|
+
params[decodeURIComponent(key)] = [params[decodeURIComponent(key)]];
|
|
153
|
+
}
|
|
154
|
+
params[decodeURIComponent(key)].push(decoded);
|
|
155
|
+
} else {
|
|
156
|
+
params[decodeURIComponent(key)] = decoded;
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
return params;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Stringify an object into a query string.
|
|
164
|
+
* @param {Object} params
|
|
165
|
+
* @returns {string}
|
|
166
|
+
*/
|
|
167
|
+
function stringifyQuery(params) {
|
|
168
|
+
if (!params) return '';
|
|
169
|
+
return Object.keys(params)
|
|
170
|
+
.filter((k) => params[k] !== undefined && params[k] !== null)
|
|
171
|
+
.map((k) => encodeURIComponent(k) + '=' + encodeURIComponent(params[k]))
|
|
172
|
+
.join('&');
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ─── Router Class ─────────────────────────────────────────────────────────────
|
|
176
|
+
|
|
177
|
+
function Router(options) {
|
|
178
|
+
options = options || {};
|
|
179
|
+
this.routes = [];
|
|
180
|
+
this.currentRoute = null;
|
|
181
|
+
this.currentParams = {};
|
|
182
|
+
this.currentQuery = {};
|
|
183
|
+
this.beforeEachGuards = [];
|
|
184
|
+
this.afterEachGuards = [];
|
|
185
|
+
this._listeners = [];
|
|
186
|
+
this._mode = options.mode || 'history'; // 'history' | 'hash'
|
|
187
|
+
this._base = options.base || '';
|
|
188
|
+
this._transition = options.transition || null;
|
|
189
|
+
this._namedRoutes = {};
|
|
190
|
+
|
|
191
|
+
// Register routes
|
|
192
|
+
if (options.routes) {
|
|
193
|
+
options.routes.forEach((r) => this.addRoute(r));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Bind popstate
|
|
197
|
+
this._onPopState = this._onPopState.bind(this);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Add a route to the router.
|
|
202
|
+
* @param {Object} routeConfig
|
|
203
|
+
*/
|
|
204
|
+
Router.prototype.addRoute = function (routeConfig) {
|
|
205
|
+
const route = createRoute(routeConfig);
|
|
206
|
+
this.routes.push(route);
|
|
207
|
+
if (route.name) {
|
|
208
|
+
this._namedRoutes[route.name] = route;
|
|
209
|
+
}
|
|
210
|
+
return this;
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Remove a route by path.
|
|
215
|
+
* @param {string} path
|
|
216
|
+
*/
|
|
217
|
+
Router.prototype.removeRoute = function (path) {
|
|
218
|
+
const idx = this.routes.findIndex((r) => r.path === path);
|
|
219
|
+
if (idx !== -1) this.routes.splice(idx, 1);
|
|
220
|
+
return this;
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
// ─── Navigation ───────────────────────────────────────────────────────────────
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Navigate to a path.
|
|
227
|
+
* @param {string} path
|
|
228
|
+
* @param {Object} [query]
|
|
229
|
+
* @returns {Promise<boolean>}
|
|
230
|
+
*/
|
|
231
|
+
Router.prototype.push = function (path, query) {
|
|
232
|
+
return this._navigate(path, query, 'push');
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Replace current history entry.
|
|
237
|
+
* @param {string} path
|
|
238
|
+
* @param {Object} [query]
|
|
239
|
+
* @returns {Promise<boolean>}
|
|
240
|
+
*/
|
|
241
|
+
Router.prototype.replace = function (path, query) {
|
|
242
|
+
return this._navigate(path, query, 'replace');
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Navigate by history offset.
|
|
247
|
+
* @param {number} offset
|
|
248
|
+
*/
|
|
249
|
+
Router.prototype.go = function (offset) {
|
|
250
|
+
window.history.go(offset);
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Go back one entry.
|
|
255
|
+
*/
|
|
256
|
+
Router.prototype.back = function () { this.go(-1); };
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Go forward one entry.
|
|
260
|
+
*/
|
|
261
|
+
Router.prototype.forward = function () { this.go(1); };
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Navigate to a named route.
|
|
265
|
+
* @param {string} name
|
|
266
|
+
* @param {Object} [params]
|
|
267
|
+
* @param {Object} [query]
|
|
268
|
+
* @returns {Promise<boolean>}
|
|
269
|
+
*/
|
|
270
|
+
Router.prototype.pushByName = function (name, params, query) {
|
|
271
|
+
const route = this._namedRoutes[name];
|
|
272
|
+
if (!route) return Promise.reject(new Error('Route not found: ' + name));
|
|
273
|
+
const path = buildPath(route.path, params);
|
|
274
|
+
return this.push(path, query);
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
// ─── Internal Navigation ──────────────────────────────────────────────────────
|
|
278
|
+
|
|
279
|
+
Router.prototype._navigate = async function (path, query, action) {
|
|
280
|
+
const fullPath = this._base + path + (query ? '?' + stringifyQuery(query) : '');
|
|
281
|
+
|
|
282
|
+
// Run beforeEach guards
|
|
283
|
+
const toRoute = matchRoute(path, this.routes);
|
|
284
|
+
const fromRoute = this.currentRoute;
|
|
285
|
+
|
|
286
|
+
for (const guard of this.beforeEachGuards) {
|
|
287
|
+
const result = await guard(toRoute, fromRoute);
|
|
288
|
+
if (result === false) return false;
|
|
289
|
+
if (typeof result === 'string') {
|
|
290
|
+
return this.push(result);
|
|
291
|
+
}
|
|
292
|
+
if (typeof result === 'object' && result.path) {
|
|
293
|
+
return this.push(result.path, result.query);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Run route-level beforeEnter guard
|
|
298
|
+
if (toRoute && toRoute.route && toRoute.route.beforeEnter) {
|
|
299
|
+
const result = await toRoute.route.beforeEnter(toRoute, fromRoute);
|
|
300
|
+
if (result === false) return false;
|
|
301
|
+
if (typeof result === 'string') return this.push(result);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Update history
|
|
305
|
+
if (this._mode === 'history') {
|
|
306
|
+
if (action === 'push') {
|
|
307
|
+
window.history.pushState({ path: fullPath }, '', fullPath);
|
|
308
|
+
} else {
|
|
309
|
+
window.history.replaceState({ path: fullPath }, '', fullPath);
|
|
310
|
+
}
|
|
311
|
+
} else {
|
|
312
|
+
window.location.hash = path;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Update current state
|
|
316
|
+
const parsedPath = this._parseFullPath(fullPath);
|
|
317
|
+
this.currentRoute = toRoute;
|
|
318
|
+
this.currentParams = toRoute ? toRoute.match.params : {};
|
|
319
|
+
this.currentQuery = parsedPath.query;
|
|
320
|
+
|
|
321
|
+
// Run afterEach guards
|
|
322
|
+
for (const guard of this.afterEachGuards) {
|
|
323
|
+
guard(toRoute, fromRoute);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Notify listeners
|
|
327
|
+
this._notifyListeners(toRoute, fromRoute);
|
|
328
|
+
|
|
329
|
+
return true;
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
Router.prototype._parseFullPath = function (fullPath) {
|
|
333
|
+
const [path, qs] = fullPath.split('?');
|
|
334
|
+
return { path, query: parseQuery(qs) };
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
Router.prototype._onPopState = function () {
|
|
338
|
+
const path = this._mode === 'history'
|
|
339
|
+
? window.location.pathname.slice(this._base.length) || '/'
|
|
340
|
+
: window.location.hash.slice(1) || '/';
|
|
341
|
+
this.replace(path);
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
// ─── Route Guards ─────────────────────────────────────────────────────────────
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Register a beforeEach guard.
|
|
348
|
+
* @param {Function} guard - (to, from) => boolean | string | { path, query }
|
|
349
|
+
* @returns {Function} Remove guard function
|
|
350
|
+
*/
|
|
351
|
+
Router.prototype.beforeEach = function (guard) {
|
|
352
|
+
this.beforeEachGuards.push(guard);
|
|
353
|
+
return () => {
|
|
354
|
+
const idx = this.beforeEachGuards.indexOf(guard);
|
|
355
|
+
if (idx !== -1) this.beforeEachGuards.splice(idx, 1);
|
|
356
|
+
};
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Register an afterEach guard.
|
|
361
|
+
* @param {Function} guard - (to, from) => void
|
|
362
|
+
* @returns {Function} Remove guard function
|
|
363
|
+
*/
|
|
364
|
+
Router.prototype.afterEach = function (guard) {
|
|
365
|
+
this.afterEachGuards.push(guard);
|
|
366
|
+
return () => {
|
|
367
|
+
const idx = this.afterEachGuards.indexOf(guard);
|
|
368
|
+
if (idx !== -1) this.afterEachGuards.splice(idx, 1);
|
|
369
|
+
};
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
// ─── Lazy Loading ─────────────────────────────────────────────────────────────
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Create a lazy-loaded route component.
|
|
376
|
+
* @param {Function} loader - () => Promise<Component>
|
|
377
|
+
* @returns {Function} Lazy component
|
|
378
|
+
*/
|
|
379
|
+
Router.prototype.lazy = function (loader) {
|
|
380
|
+
let cachedComponent = null;
|
|
381
|
+
return function LazyComponent(props) {
|
|
382
|
+
if (cachedComponent) return cachedComponent(props);
|
|
383
|
+
return loader().then((module) => {
|
|
384
|
+
cachedComponent = module.default || module;
|
|
385
|
+
return cachedComponent(props);
|
|
386
|
+
});
|
|
387
|
+
};
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
// ─── Active Route Detection ───────────────────────────────────────────────────
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Check if a path is currently active.
|
|
394
|
+
* @param {string} path
|
|
395
|
+
* @param {boolean} [exact=false]
|
|
396
|
+
* @returns {boolean}
|
|
397
|
+
*/
|
|
398
|
+
Router.prototype.isActive = function (path, exact) {
|
|
399
|
+
if (!this.currentRoute) return false;
|
|
400
|
+
const currentPath = this.currentRoute.match ? this.currentRoute.match.path : '';
|
|
401
|
+
if (exact) return currentPath === path;
|
|
402
|
+
return currentPath.startsWith(path);
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
// ─── Listener Management ──────────────────────────────────────────────────────
|
|
406
|
+
|
|
407
|
+
Router.prototype.onRouteChange = function (callback) {
|
|
408
|
+
this._listeners.push(callback);
|
|
409
|
+
return () => {
|
|
410
|
+
const idx = this._listeners.indexOf(callback);
|
|
411
|
+
if (idx !== -1) this._listeners.splice(idx, 1);
|
|
412
|
+
};
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
Router.prototype._notifyListeners = function (to, from) {
|
|
416
|
+
this._listeners.forEach((cb) => {
|
|
417
|
+
try { cb(to, from); } catch (e) { console.error('[Router] Listener error:', e); }
|
|
418
|
+
});
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
// ─── Start/Stop ───────────────────────────────────────────────────────────────
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Start listening for navigation events.
|
|
425
|
+
*/
|
|
426
|
+
Router.prototype.start = function () {
|
|
427
|
+
if (this._mode === 'history') {
|
|
428
|
+
window.addEventListener('popstate', this._onPopState);
|
|
429
|
+
// Navigate to current URL
|
|
430
|
+
const path = window.location.pathname.slice(this._base.length) || '/';
|
|
431
|
+
this.replace(path);
|
|
432
|
+
} else {
|
|
433
|
+
window.addEventListener('hashchange', this._onPopState);
|
|
434
|
+
const hash = window.location.hash.slice(1) || '/';
|
|
435
|
+
this.replace(hash);
|
|
436
|
+
}
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Stop the router.
|
|
441
|
+
*/
|
|
442
|
+
Router.prototype.stop = function () {
|
|
443
|
+
if (this._mode === 'history') {
|
|
444
|
+
window.removeEventListener('popstate', this._onPopState);
|
|
445
|
+
} else {
|
|
446
|
+
window.removeEventListener('hashchange', this._onPopState);
|
|
447
|
+
}
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
// ─── Helper: Build path from pattern and params ──────────────────────────────
|
|
451
|
+
|
|
452
|
+
function buildPath(pattern, params) {
|
|
453
|
+
params = params || {};
|
|
454
|
+
return pattern.replace(/:([^/?]+)/g, (_, name) => {
|
|
455
|
+
if (name.endsWith('?')) name = name.slice(0, -1);
|
|
456
|
+
return params[name] !== undefined ? encodeURIComponent(params[name]) : '';
|
|
457
|
+
}).replace(/\/+/g, '/').replace(/\/$/, '') || '/';
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// ─── Factory ──────────────────────────────────────────────────────────────────
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Create a new router instance.
|
|
464
|
+
* @param {Object} [options]
|
|
465
|
+
* @returns {Router}
|
|
466
|
+
*/
|
|
467
|
+
function createRouter(options) {
|
|
468
|
+
return new Router(options);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// ─── Exports ──────────────────────────────────────────────────────────────────
|
|
472
|
+
|
|
473
|
+
module.exports = {
|
|
474
|
+
Router,
|
|
475
|
+
createRouter,
|
|
476
|
+
createRoute,
|
|
477
|
+
matchPath,
|
|
478
|
+
matchRoute,
|
|
479
|
+
compilePath,
|
|
480
|
+
parseQuery,
|
|
481
|
+
stringifyQuery,
|
|
482
|
+
buildPath,
|
|
483
|
+
};
|