onelaraveljs 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.
Files changed (67) hide show
  1. package/README.md +87 -0
  2. package/docs/integration_analysis.md +116 -0
  3. package/docs/onejs_analysis.md +108 -0
  4. package/docs/optimization_implementation_group2.md +458 -0
  5. package/docs/optimization_plan.md +130 -0
  6. package/index.js +16 -0
  7. package/package.json +13 -0
  8. package/src/app.js +61 -0
  9. package/src/core/API.js +72 -0
  10. package/src/core/ChildrenRegistry.js +410 -0
  11. package/src/core/DOMBatcher.js +207 -0
  12. package/src/core/ErrorBoundary.js +226 -0
  13. package/src/core/EventDelegator.js +416 -0
  14. package/src/core/Helper.js +817 -0
  15. package/src/core/LoopContext.js +97 -0
  16. package/src/core/OneDOM.js +246 -0
  17. package/src/core/OneMarkup.js +444 -0
  18. package/src/core/Router.js +996 -0
  19. package/src/core/SEOConfig.js +321 -0
  20. package/src/core/SectionEngine.js +75 -0
  21. package/src/core/TemplateEngine.js +83 -0
  22. package/src/core/View.js +273 -0
  23. package/src/core/ViewConfig.js +229 -0
  24. package/src/core/ViewController.js +1410 -0
  25. package/src/core/ViewControllerOptimized.js +164 -0
  26. package/src/core/ViewIdentifier.js +361 -0
  27. package/src/core/ViewLoader.js +272 -0
  28. package/src/core/ViewManager.js +1962 -0
  29. package/src/core/ViewState.js +761 -0
  30. package/src/core/ViewSystem.js +301 -0
  31. package/src/core/ViewTemplate.js +4 -0
  32. package/src/core/helpers/BindingHelper.js +239 -0
  33. package/src/core/helpers/ConfigHelper.js +37 -0
  34. package/src/core/helpers/EventHelper.js +172 -0
  35. package/src/core/helpers/LifecycleHelper.js +17 -0
  36. package/src/core/helpers/ReactiveHelper.js +169 -0
  37. package/src/core/helpers/RenderHelper.js +15 -0
  38. package/src/core/helpers/ResourceHelper.js +89 -0
  39. package/src/core/helpers/TemplateHelper.js +11 -0
  40. package/src/core/managers/BindingManager.js +671 -0
  41. package/src/core/managers/ConfigurationManager.js +136 -0
  42. package/src/core/managers/EventManager.js +309 -0
  43. package/src/core/managers/LifecycleManager.js +356 -0
  44. package/src/core/managers/ReactiveManager.js +334 -0
  45. package/src/core/managers/RenderEngine.js +292 -0
  46. package/src/core/managers/ResourceManager.js +441 -0
  47. package/src/core/managers/ViewHierarchyManager.js +258 -0
  48. package/src/core/managers/ViewTemplateManager.js +127 -0
  49. package/src/core/reactive/ReactiveComponent.js +592 -0
  50. package/src/core/services/EventService.js +418 -0
  51. package/src/core/services/HttpService.js +106 -0
  52. package/src/core/services/LoggerService.js +57 -0
  53. package/src/core/services/StateService.js +512 -0
  54. package/src/core/services/StorageService.js +856 -0
  55. package/src/core/services/StoreService.js +258 -0
  56. package/src/core/services/TemplateDetectorService.js +361 -0
  57. package/src/core/services/Test.js +18 -0
  58. package/src/helpers/devWarnings.js +205 -0
  59. package/src/helpers/performance.js +226 -0
  60. package/src/helpers/utils.js +287 -0
  61. package/src/init.js +343 -0
  62. package/src/plugins/auto-plugin.js +34 -0
  63. package/src/services/Test.js +18 -0
  64. package/src/types/index.js +193 -0
  65. package/src/utils/date-helper.js +51 -0
  66. package/src/utils/helpers.js +39 -0
  67. package/src/utils/validation.js +32 -0
@@ -0,0 +1,996 @@
1
+ /**
2
+ * router Module
3
+ * ES6 Module for Blade Compiler
4
+ */
5
+
6
+ // import { Application } from "../app.js";
7
+ import { View } from "./View.js";
8
+
9
+ class ActiveRoute {
10
+ constructor(route, urlPath, params, query = {}, fragment = {}) {
11
+ this.$route = route;
12
+ this.$urlPath = urlPath;
13
+ this.$params = params || {};
14
+ this.$query = query || {};
15
+ this.$fragment = fragment || {};
16
+ this.$paramKeys = [...Object.keys(params || {})];
17
+ Object.keys(params).forEach(key => {
18
+ Object.defineProperty(this, key, {
19
+ get: () => this.$params[key],
20
+ set: (value) => {
21
+ this.$params[key] = value;
22
+ },
23
+ enumerable: true,
24
+ configurable: false,
25
+ });
26
+ });
27
+ }
28
+
29
+ getPath() {
30
+ return this.$urlPath || null;
31
+ }
32
+ getUrlPath() {
33
+ return this.$urlPath || null;
34
+ }
35
+
36
+ getParams() {
37
+ return this.$params || {};
38
+ }
39
+ getParam(name) {
40
+ return this.$params[name] || null;
41
+ }
42
+ getQuery() {
43
+ return this.$query || {};
44
+ }
45
+ getFragment() {
46
+ return this.$fragment || {};
47
+ }
48
+ }
49
+
50
+ export class Router {
51
+ static activeRoute = null;
52
+ static containers = {};
53
+ constructor(App = null) {
54
+ /**
55
+ * @type {Application}
56
+ */
57
+ this.App = App;
58
+ this.routes = [];
59
+ this.currentRoute = null;
60
+ this.mode = 'history';
61
+ this.base = '';
62
+ this._beforeEach = null;
63
+ this._afterEach = null;
64
+ this.defaultRoute = '/';
65
+
66
+ // Bind methods
67
+ this.handleRoute = this.handleRoute.bind(this);
68
+ this.handlePopState = this.handlePopState.bind(this);
69
+
70
+ /**
71
+ * @type {Object<string, {path: string, view: string, params: object}>}
72
+ */
73
+ this.routeConfigs = {};
74
+ this.currentUri = window.location.pathname + window.location.search;
75
+ }
76
+
77
+ setApp(app) {
78
+ this.App = app;
79
+ return this;
80
+ }
81
+
82
+ addRouteConfig(routeConfig) {
83
+ this.routeConfigs[routeConfig.name] = routeConfig;
84
+ }
85
+
86
+ setAllRoutes(routes) {
87
+ for (const route of routes) {
88
+ this.addRouteConfig(route);
89
+ }
90
+ }
91
+
92
+
93
+ getURL(name, params = {}) {
94
+ const routeConfig = this.routeConfigs[name];
95
+ if (!routeConfig) {
96
+ return null;
97
+ }
98
+ let url = this.generateUrl(routeConfig.path, params);
99
+ if (!(url.startsWith('/') || url.startsWith('http:') || url.startsWith('https:'))) {
100
+ url = this.base + url;
101
+ }
102
+ return url;
103
+ }
104
+
105
+ addRoute(path, view, options = {}) {
106
+ this.routes.push({
107
+ path: this.normalizePath(path),
108
+ view: view,
109
+ options: options
110
+ });
111
+ }
112
+
113
+ setMode(mode) {
114
+ this.mode = mode;
115
+ }
116
+
117
+ setBase(base) {
118
+ this.base = base;
119
+ }
120
+
121
+ setDefaultRoute(route) {
122
+ this.defaultRoute = route;
123
+ }
124
+
125
+ beforeEach(callback) {
126
+ this._beforeEach = callback;
127
+ }
128
+
129
+ afterEach(callback) {
130
+ this._afterEach = callback;
131
+ }
132
+
133
+ normalizePath(path) {
134
+ // Remove leading slash if present, then add it back to ensure consistency
135
+ let normalized = path.startsWith('/') ? path : '/' + path;
136
+
137
+ // Remove trailing slash except for root path
138
+ if (normalized.length > 1 && normalized.endsWith('/')) {
139
+ normalized = normalized.slice(0, -1);
140
+ }
141
+
142
+ // Handle empty path as root
143
+ if (normalized === '') {
144
+ normalized = '/';
145
+ }
146
+
147
+ return normalized;
148
+ }
149
+
150
+ /**
151
+ * So sánh xem chuỗi str có khớp với format không.
152
+ * @param {string} str - Chuỗi đầu vào cần kiểm tra
153
+ * @param {string} format - Định dạng, ví dụ: {abc}, {abc}-def, demo-{key}, test-demo-{key}.xyz, ...
154
+ * @returns {boolean} true nếu khớp, false nếu không
155
+ */
156
+ matchFormat(str, format) {
157
+ // Chuyển format thành regex
158
+ // Thay thế {param} thành ([^\/\-.]+) để match 1 đoạn không chứa /, -, .
159
+ // Giữ lại các ký tự đặc biệt như -, ., ...
160
+ let regexStr = format.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, '\\$1'); // escape ký tự đặc biệt
161
+ regexStr = regexStr.replace(/\{[a-zA-Z0-9_]+\}/g, '([^\\/\\-.]+)');
162
+ // Cho phép match toàn bộ chuỗi
163
+ regexStr = '^' + regexStr + '$';
164
+ const regex = new RegExp(regexStr);
165
+ return regex.test(str);
166
+ }
167
+
168
+ hasParameter(path) {
169
+ return /\{[a-zA-Z0-9_]+\??\}/.test(path);
170
+ }
171
+
172
+ /**
173
+ * Check if parameter is optional (has ? suffix)
174
+ * @param {string} path - The path segment to check (e.g., "{name?}")
175
+ * @returns {boolean}
176
+ */
177
+ isOptionalParameter(path) {
178
+ return /\{[a-zA-Z0-9_]+\?\}/.test(path);
179
+ }
180
+
181
+ isAnyParameter(path) {
182
+ return path.includes('*') || path.toLowerCase() === '{any}';
183
+ }
184
+
185
+ /**
186
+ * Lấy tên tham số đầu tiên trong path có format {param}
187
+ * Đảm bảo hoạt động trên tất cả các trình duyệt (không dùng lookbehind/lookahead)
188
+ * @param {string} format - Chuỗi path, ví dụ: "abc-{name}.html"
189
+ * @returns {string|null} - Trả về tên param đầu tiên nếu có, ngược lại trả về null
190
+ */
191
+ getParameterName(format) {
192
+ // Dùng RegExp đơn giản, không lookbehind/lookahead để tương thích trình duyệt cũ
193
+ // Hỗ trợ cả {param} và {param?} (optional)
194
+ var regex = /\{([a-zA-Z0-9_]+)\??\}/;
195
+ var match = regex.exec(format);
196
+ if (match && match[1]) {
197
+ return match[1];
198
+ }
199
+ return null;
200
+ }
201
+
202
+ /**
203
+ * Lấy giá trị tham số đầu tiên trong path theo format {param} hoặc {param?}
204
+ * @param {string} format - Chuỗi format, ví dụ: "{id}" hoặc "{id?}"
205
+ * @param {string} path - Chuỗi path thực tế, ví dụ: "1"
206
+ * @returns {string|null} - Giá trị param nếu tìm thấy, ngược lại trả về null
207
+ */
208
+ getParameterValue(format, path) {
209
+ if (typeof path !== 'string' || typeof format !== 'string') return null;
210
+
211
+ // Nếu format chỉ là {param} hoặc {param?} thì trả về path
212
+ if (format.match(/^\{[a-zA-Z0-9_]+\??\}$/)) {
213
+ return path;
214
+ }
215
+
216
+ // Escape các ký tự đặc biệt trong format (trừ {param})
217
+ let regexStr = format.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, '\\$1');
218
+ // Thay thế {param} hoặc {param?} thành nhóm bắt
219
+ regexStr = regexStr.replace(/\{[a-zA-Z0-9_]+\??\}/g, '([^\\/\\-.]+)');
220
+ // Tạo regex
221
+ const regex = new RegExp('^' + regexStr + '$');
222
+ const match = regex.exec(path);
223
+ if (match && match[1]) {
224
+ return match[1];
225
+ }
226
+ return null;
227
+ }
228
+
229
+ getAnyParameterValue(format, path) {
230
+ // Nếu format chứa dấu * thì xem như là {any}
231
+ if (typeof format === 'string' && format.includes('*')) {
232
+ format = format.replace(/\*/g, '{any}');
233
+ }
234
+ if (typeof path !== 'string' || typeof format !== 'string') return null;
235
+ // Escape các ký tự đặc biệt trong format (trừ {param})
236
+ let regexStr = format.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, '\\$1');
237
+ // Thay thế từng {param} thành nhóm bắt
238
+ regexStr = regexStr.replace(/\{[a-zA-Z0-9_]+\}/g, '([^\\/\\-.]+)');
239
+ const regex = new RegExp('^' + regexStr + '$');
240
+ const valueMatch = regex.exec(path);
241
+ if (!valueMatch) return null;
242
+ // Trả về giá trị đầu tiên (hoặc null nếu không có)
243
+ return valueMatch[1] || null;
244
+ }
245
+
246
+ /**
247
+ * Check if a route with parameters matches the given path
248
+ * @param {object<{path: string, view: string, params: object}>} route - The route pattern (e.g., "/web/users/{id}")
249
+ * @param {string} urlPath - The actual path to match (e.g., "/web/users/1")
250
+ * @returns {Object|null} - Returns {params} if match, null if no match
251
+ */
252
+ checkParameterRoute(route, urlPath) {
253
+ const routePathParts = this.App.Helper.trim(route.path, '/').split('/');
254
+ const urlPathParts = this.App.Helper.trim(urlPath, '/').split('/');
255
+
256
+ // Count trailing optional parameters in route
257
+ let trailingOptionalCount = 0;
258
+ for (let i = routePathParts.length - 1; i >= 0; i--) {
259
+ if (this.isOptionalParameter(routePathParts[i])) {
260
+ trailingOptionalCount++;
261
+ } else {
262
+ break; // Stop at first non-optional segment from the end
263
+ }
264
+ }
265
+
266
+ // URL can have fewer segments if route has trailing optional params
267
+ // Required segments = total - optional trailing segments
268
+ const requiredSegments = routePathParts.length - trailingOptionalCount;
269
+
270
+ // URL must have at least required segments and at most total segments
271
+ if (urlPathParts.length < requiredSegments || urlPathParts.length > routePathParts.length) {
272
+ return null;
273
+ }
274
+
275
+ const lastRoutePathIndex = routePathParts.length - 1;
276
+ const params = {};
277
+
278
+ for (let i = 0; i < routePathParts.length; i++) {
279
+ const routePathPart = routePathParts[i];
280
+ const urlPathPart = urlPathParts[i]; // May be undefined for optional params
281
+
282
+ if (this.isAnyParameter(routePathPart)) {
283
+ const paramValue = urlPathPart !== undefined
284
+ ? this.getAnyParameterValue(routePathPart, urlPathPart)
285
+ : null;
286
+ params.any = paramValue;
287
+ if (i === lastRoutePathIndex && i === 0) {
288
+ return params;
289
+ }
290
+ } else if (this.hasParameter(routePathPart)) {
291
+ const paramName = this.getParameterName(routePathPart);
292
+ const isOptional = this.isOptionalParameter(routePathPart);
293
+
294
+ // If URL segment doesn't exist
295
+ if (urlPathPart === undefined || urlPathPart === '') {
296
+ if (isOptional) {
297
+ // Optional param with no value -> null
298
+ params[paramName] = null;
299
+ continue;
300
+ } else {
301
+ // Required param missing
302
+ return null;
303
+ }
304
+ }
305
+
306
+ const paramValue = this.getParameterValue(routePathPart, urlPathPart);
307
+ if (paramValue === null && !isOptional) {
308
+ return null;
309
+ }
310
+ params[paramName] = paramValue;
311
+ }
312
+ else {
313
+ // Regular route part - exact match required
314
+ if (routePathPart !== urlPathPart) {
315
+ return null;
316
+ }
317
+ }
318
+ }
319
+
320
+ return params;
321
+ }
322
+
323
+ matchRoute(path) {
324
+ const normalizedPath = this.normalizePath(path);
325
+
326
+ // Process routes in order - first match wins
327
+ for (const route of this.routes) {
328
+ const routePath = route.path;
329
+
330
+ // Check if route has parameters
331
+ const hasParameters = routePath.split('/').some(part => this.hasParameter(part) || this.isAnyParameter(part));
332
+
333
+ if (hasParameters) {
334
+ // Handle parameter route
335
+ const params = this.checkParameterRoute(route, normalizedPath);
336
+ if (params !== null) {
337
+ return { route, params, path: normalizedPath };
338
+ }
339
+ } else {
340
+ // Handle exact route match
341
+ if (routePath === normalizedPath) {
342
+ return { route, params: {}, path: normalizedPath };
343
+ }
344
+ }
345
+ }
346
+
347
+ return null;
348
+ }
349
+
350
+ async handleRoute(path, ignoreSetActiveRoute = false) {
351
+ // Remove query string for route matching
352
+ const pathForMatching = path.split('?')[0];
353
+ const match = this.matchRoute(pathForMatching);
354
+
355
+ if (!match) {
356
+ return;
357
+ }
358
+
359
+ const { route, params } = match;
360
+
361
+ // Call beforeEach hook
362
+ if (this._beforeEach) {
363
+ const result = await this._beforeEach(route, params);
364
+ if (result === false) {
365
+ return; // Navigation cancelled
366
+ }
367
+ }
368
+ const URLParts = Router.getUrlParts();
369
+ const query = URLParts.query;
370
+ const fragment = URLParts.hash;
371
+
372
+ if (!ignoreSetActiveRoute) {
373
+ Router.addActiveRoute(route, match.path, params, query, fragment);
374
+ }
375
+
376
+ // Update current route
377
+ this.currentRoute = { ...route, $params: params, $query: query, $fragment: fragment, $urlPath: match.path };
378
+
379
+ // Render view
380
+ if (this.App.View && (route.view || route.component)) {
381
+ this.App.View.mountView(route.view || route.component, params, Router.getActiveRoute());
382
+ }
383
+
384
+ // Call afterEach hook with proper arguments: (to, from)
385
+ // to = route object with path property
386
+ if (this._afterEach) {
387
+ const toRoute = { ...route, path: match.path };
388
+ this._afterEach(toRoute, this.currentRoute);
389
+ }
390
+ }
391
+
392
+ /**
393
+ * Hydrate server-rendered views
394
+ * Scans SSR HTML and attaches JavaScript behavior without re-rendering
395
+ *
396
+ * Flow:
397
+ * 1. Get active route (from Router.activeRoute)
398
+ * 2. Call beforeEach hook
399
+ * 3. Call View.scanView() to:
400
+ * - Scan page view + all parent layouts
401
+ * - Setup view hierarchy (virtualRender)
402
+ * - Attach event handlers to existing DOM
403
+ * - Setup state subscriptions
404
+ * - Mount all views bottom-up
405
+ * 4. Call afterEach hook
406
+ * 5. Mark hydration complete
407
+ */
408
+ async hydrateViews() {
409
+
410
+ if (!this.App?.View) {
411
+ console.error('❌ Router.hydrateViews: App.View not available');
412
+ return;
413
+ }
414
+
415
+ // Get active route info
416
+ const activeRoute = Router.activeRoute || this.currentRoute;
417
+
418
+ if (!activeRoute) {
419
+ return;
420
+ }
421
+
422
+ const { $route: route, $params: params, $urlPath: urlPath, $query: query, $fragment: fragment } = activeRoute;
423
+
424
+ // Call beforeEach hook (if exists)
425
+ if (this._beforeEach) {
426
+ const result = await this._beforeEach(route, params, urlPath);
427
+ if (result === false) {
428
+ return;
429
+ }
430
+ }
431
+
432
+ // Handle view hydration
433
+ if (route.view || route.component) {
434
+ try {
435
+ const viewName = route.view || route.component;
436
+
437
+ this.App.View.mountViewScan(viewName, params, activeRoute);
438
+ // Call afterEach hook with proper arguments: (to, from)
439
+ if (this._afterEach) {
440
+ const toRoute = { ...route, path: urlPath };
441
+ this._afterEach(toRoute, this.currentRoute);
442
+ }
443
+
444
+
445
+ } catch (error) {
446
+ console.error('❌ Router.hydrateViews: Error during hydration:', error);
447
+ }
448
+ }
449
+ }
450
+
451
+
452
+ handlePopState(event) {
453
+ const path = window.location.pathname + window.location.search;
454
+ this.currentUri = path; // Update currentUri during back/forward navigation
455
+ this.handleRoute(path);
456
+ }
457
+
458
+ navigate(path) {
459
+
460
+ if (this.mode === 'history') {
461
+ window.history.pushState({}, '', path);
462
+ try {
463
+ this.handleRoute(path);
464
+ this.currentUri = path;
465
+ } catch (error) {
466
+ console.error('❌ Router.navigate handleRoute error:', error);
467
+ }
468
+ } else {
469
+ // Hash mode
470
+ window.location.hash = path;
471
+ try {
472
+ this.handleRoute(path);
473
+ this.currentUri = path;
474
+ } catch (error) {
475
+ console.error('❌ Router.navigate handleRoute (hash mode) error:', error);
476
+ }
477
+ }
478
+ }
479
+
480
+ /**
481
+ * Generate URL with file extension
482
+ * @param {string} route - Route pattern (e.g., '/users/{id}')
483
+ * @param {object} params - Parameters to fill in
484
+ * @param {string} extension - File extension (e.g., '.html', '.php')
485
+ * @returns {string} Generated URL
486
+ */
487
+ generateUrl(route, params = {}, extension = '') {
488
+ let url = route;
489
+
490
+ // Replace parameters
491
+ for (const [key, value] of Object.entries(params)) {
492
+ url = url.replace(`{${key}}`, value);
493
+ }
494
+
495
+ // Add extension if provided
496
+ if (extension && !url.endsWith(extension)) {
497
+ url += extension;
498
+ }
499
+
500
+ return url;
501
+ }
502
+
503
+ /**
504
+ * Navigate to route with file extension
505
+ * @param {string} route - Route pattern
506
+ * @param {object} params - Parameters
507
+ * @param {string} extension - File extension
508
+ */
509
+ navigateTo(route, params = {}, extension = '') {
510
+ const url = this.generateUrl(route, params, extension);
511
+ this.navigate(url);
512
+ }
513
+
514
+ start(skipInitial = false) {
515
+ // Detect if page has server-rendered content
516
+ // Check container for data-server-rendered attribute
517
+ const container = this.App?.View?.container || document.querySelector('#spa-content, #app-root, #app');
518
+ const serverRendered = container.getAttribute('data-server-rendered');
519
+ const isServerRendered = serverRendered === 'true';
520
+ const URLParts = Router.getUrlParts();
521
+ const initialPath = this.mode === 'history' ? (window.location.pathname + window.location.search) : (window.location.hash.substring(1) || this.defaultRoute);
522
+
523
+ this.setActiveRouteForCurrentPath(initialPath);
524
+
525
+ // Add event listeners
526
+ if (this.mode === 'history') {
527
+ window.addEventListener('popstate', this.handlePopState);
528
+ } else {
529
+ window.addEventListener('hashchange', this.handlePopState);
530
+ }
531
+
532
+ // Choose initial rendering strategy
533
+ if (isServerRendered) {
534
+ // SSR: Hydrate existing HTML
535
+ console.log('🚀 Router.start: Starting SSR hydration...');
536
+ this.hydrateViews();
537
+ } else if (!skipInitial) {
538
+ // CSR: Render from scratch
539
+ this.handleRoute(initialPath);
540
+ } else {
541
+ console.log('🔍 Router.start: Skipping initial route handling' + (this.mode === 'history' ? '' : '( hash )') + ' but activeRoute is set');
542
+ }
543
+
544
+ this.setupAutoNavigation();
545
+
546
+ }
547
+
548
+ stop() {
549
+ if (this.mode === 'history') {
550
+ window.removeEventListener('popstate', this.handlePopState);
551
+ } else {
552
+ window.removeEventListener('hashchange', this.handlePopState);
553
+ }
554
+ // Remove auto-navigation listener
555
+ document.removeEventListener('click', this._autoNavHandler);
556
+ }
557
+
558
+ /**
559
+ * Set activeRoute for current path without rendering view
560
+ * Used when starting router with skipInitial = true
561
+ */
562
+ setActiveRouteForCurrentPath(path) {
563
+ // console.log('🔍 setActiveRouteForCurrentPath called with:', path);
564
+ // Remove query string for route matching
565
+ const pathForMatching = path.split('?')[0];
566
+ const match = this.matchRoute(pathForMatching);
567
+
568
+ if (match) {
569
+ const { route, params } = match;
570
+ const URLParts = Router.getUrlParts();
571
+ const query = URLParts.query;
572
+ const fragment = URLParts.hash;
573
+ // console.log('✅ Setting activeRoute for current path:', route.path, 'with params:', params);
574
+
575
+ // Set activeRoute without rendering
576
+ Router.addActiveRoute(route, match.path, params, query, fragment);
577
+ this.currentRoute = { ...route, $params: params, $query: query, $fragment: fragment, $urlPath: match.path };
578
+
579
+ // console.log('✅ activeRoute set successfully:', Router.activeRoute);
580
+ } else {
581
+ console.log('❌ No matching route found for current path:', path);
582
+ }
583
+ }
584
+
585
+ /**
586
+ * Setup auto-detect navigation for internal links
587
+ */
588
+ setupAutoNavigation() {
589
+ // console.log('🔍 setupAutoNavigation called');
590
+ // Store reference to handler for removal
591
+ this._autoNavHandler = this.handleAutoNavigation.bind(this);
592
+ document.addEventListener('click', this._autoNavHandler);
593
+ // console.log('✅ Auto-navigation setup complete - event listener added');
594
+ }
595
+
596
+ /**
597
+ * Handle auto-detect navigation
598
+ * @param {Event} e - Click event
599
+ */
600
+ handleAutoNavigation(e) {
601
+ // console.log('🔍 handleAutoNavigation called for:', e.target);
602
+
603
+ // Check for data-nav-link attribute first (highest priority)
604
+ const oneNavElement = e.target.closest('[data-nav-link]');
605
+ if (oneNavElement) {
606
+ // Check if navigation is disabled
607
+ if (oneNavElement.hasAttribute('data-nav-disabled')) {
608
+ return;
609
+ }
610
+
611
+ const navPath = oneNavElement.getAttribute('data-nav-link');
612
+
613
+ if (navPath && navPath.trim() !== '') {
614
+ e.preventDefault();
615
+ if (navPath === this.currentUri) {
616
+ return;
617
+ }
618
+ this.navigate(navPath);
619
+ return;
620
+ }
621
+ }
622
+
623
+ // Check for data-navigate attribute (alternative to data-nav-link)
624
+ const navigateElement = e.target.closest('[data-navigate]');
625
+ if (navigateElement) {
626
+ // Check if navigation is disabled
627
+ if (navigateElement.hasAttribute('data-nav-disabled')) {
628
+ return;
629
+ }
630
+
631
+ const navPath = navigateElement.getAttribute('data-navigate');
632
+
633
+ if (navPath && navPath.trim() !== '') {
634
+ e.preventDefault();
635
+ if (navPath === this.currentUri) {
636
+ console.log('🚫 Same path - no navigation needed:', navPath);
637
+ return;
638
+ }
639
+ this.navigate(navPath);
640
+ return;
641
+ }
642
+ }
643
+
644
+ // Fallback to traditional <a> tag handling
645
+ const link = e.target.closest('a[href]');
646
+ if (!link) return;
647
+ if(Router.isCurrentPath(link.href, this.mode)){
648
+ return;
649
+ }
650
+ if(this.mode !== "hash" && link.href.startsWith('#')){
651
+ return;
652
+ }
653
+ // Skip if link has target="_blank"
654
+ if (link.target === '_blank') {
655
+ return;
656
+ }
657
+
658
+ // Skip if link has data-nav="disabled" or on-nav="false"
659
+ if (link.dataset.nav === 'disabled' || link.getAttribute('data-nav') === 'false') {
660
+ return;
661
+ }
662
+
663
+ // Skip if link has data-nav="false" (explicitly disabled)
664
+ if (link.dataset.nav === 'false') {
665
+ return;
666
+ }
667
+
668
+ // Skip mailto, tel, and other special protocols
669
+ if (link.href.startsWith('mailto:') || link.href.startsWith('tel:') || link.href.startsWith('javascript:')) {
670
+ return;
671
+ }
672
+
673
+ const href = link.href;
674
+
675
+ // Check if it's an external URL (different domain)
676
+ try {
677
+ const linkUrl = new URL(href);
678
+ const currentUrl = new URL(window.location.href);
679
+
680
+ // If different host, skip (external link)
681
+ if (linkUrl.host !== currentUrl.host) {
682
+ return;
683
+ }
684
+ } catch (error) {
685
+ // If URL parsing fails, treat as internal link
686
+ console.log('⚠️ URL parsing failed, treating as internal:', href);
687
+ }
688
+
689
+ // Check if it's a full URL with same domain (and not same path - already checked above)
690
+ if (href.startsWith('http://') || href.startsWith('https://')) {
691
+ try {
692
+ const linkUrl = new URL(href);
693
+ const currentUrl = new URL(window.location.href);
694
+
695
+ if (linkUrl.host === currentUrl.host) {
696
+ // Same domain, extract path with query string for navigation
697
+ const path = linkUrl.pathname + linkUrl.search;
698
+ e.preventDefault();
699
+ if (path === this.currentUri) {
700
+ return;
701
+ }
702
+ this.navigate(path);
703
+ return;
704
+ }
705
+ } catch (error) {
706
+ console.log('⚠️ URL parsing failed for full URL:', href);
707
+ }
708
+ }
709
+
710
+ // Handle relative URLs (and not same path - already checked above)
711
+ if (href && !href.startsWith('http') && !href.startsWith('//')) {
712
+ e.preventDefault();
713
+ if (href === this.currentUri) {
714
+ return;
715
+ }
716
+ this.navigate(href);
717
+ return;
718
+ }
719
+
720
+ }
721
+ static getUrlParts() {
722
+ const { location } = window;
723
+ const { search, hash, pathname } = location;
724
+ return {
725
+ url: location.href,
726
+ protocol: location.protocol,
727
+ search: search.startsWith('?') ? search.substring(1) : search,
728
+ path: pathname,
729
+ query: Object.fromEntries(new URLSearchParams(search)),
730
+ hash: hash.startsWith('#') ? hash.substring(1) : hash
731
+ };
732
+ }
733
+
734
+ static addActiveRoute(route, urlPath, params, query = {}, fragment = null) {
735
+ if (!route.path) {
736
+ return;
737
+ }
738
+ if (!Router.containers[route.path]) {
739
+ Router.containers[route.path] = new ActiveRoute(route, urlPath, params, query, fragment);
740
+ Router.activeRoute = Router.containers[route.path];
741
+ } else {
742
+ Router.containers[route.path].$urlPath = urlPath;
743
+ Router.containers[route.path].$params = params;
744
+ Router.containers[route.path].$query = query;
745
+ Router.containers[route.path].$fragment = fragment;
746
+ Router.activeRoute = Router.containers[route.path];
747
+ }
748
+ }
749
+
750
+ /**
751
+ * Kiểm tra xem URL có khớp với route hiện tại không
752
+ * @param {string} url - URL cần kiểm tra (có thể là rỗng, hash, query, relative, hoặc full URL)
753
+ * @param {string} mode - Chế độ routing: 'history' hoặc 'hash' (mặc định: 'history')
754
+ * @returns {boolean} - true nếu URL khớp với route hiện tại
755
+ *
756
+ * Xử lý các định dạng URL khác nhau:
757
+ *
758
+ * Mode 'history':
759
+ * - Chuỗi rỗng: trả về false
760
+ * - Hash fragment (#abc): cùng path (chỉ khác hash fragment)
761
+ * - Query string (?name=value): cùng path (chỉ khác query)
762
+ * - Absolute path (/path/to/page): so sánh pathname và search
763
+ * - Relative path (abc, abc/def): so sánh với segment(s) cuối của current path
764
+ * - Full URL (http://example.com/path): parse và so sánh pathname + search
765
+ *
766
+ * Mode 'hash':
767
+ * - Chỉ chấp nhận: "/abc...", "abc...", hoặc "[schema]://[host]/#/abc..."
768
+ * - Từ chối: "#abc" (standalone), "?name=value", "http://..." (không có hash)
769
+ * - So sánh với hash path từ window.location.hash
770
+ */
771
+ static isCurrentPath(url, mode = 'history') {
772
+ // Kiểm tra URL rỗng hoặc null
773
+ if (!url || typeof url !== 'string' || url.trim() === '') {
774
+ return false;
775
+ }
776
+
777
+ // Lấy route hiện tại
778
+ const route = Router.getActiveRoute();
779
+ if (!route) {
780
+ return false;
781
+ }
782
+
783
+ const currentPath = route.getPath();
784
+ if (!currentPath) {
785
+ return false;
786
+ }
787
+
788
+ // Chuẩn hóa current path dựa trên mode
789
+ let currentPathname, currentSearch;
790
+
791
+ // Mode hash: lấy path từ hash thay vì pathname
792
+ if (mode === 'hash') {
793
+ // Trong hash mode, current path được lưu trong hash (bỏ ký tự #)
794
+ const hashPath = window.location.hash.substring(1) || '';
795
+ if (hashPath) {
796
+ const hashParts = hashPath.split('?');
797
+ currentPathname = hashParts[0];
798
+ currentSearch = hashParts[1] ? '?' + hashParts[1] : '';
799
+ } else {
800
+ // Nếu hash rỗng, dùng currentPath làm fallback
801
+ const pathParts = currentPath.split('?');
802
+ currentPathname = pathParts[0];
803
+ currentSearch = pathParts[1] ? '?' + pathParts[1] : '';
804
+ }
805
+ } else {
806
+ // History mode: sử dụng pathname và search từ window.location hoặc currentPath
807
+ try {
808
+ // Thử parse current path như URL (có thể là relative)
809
+ if (currentPath.startsWith('http://') || currentPath.startsWith('https://')) {
810
+ const currentUrl = new URL(currentPath);
811
+ currentPathname = currentUrl.pathname;
812
+ currentSearch = currentUrl.search;
813
+ } else {
814
+ // Relative path - sử dụng trực tiếp
815
+ const pathParts = currentPath.split('?');
816
+ currentPathname = pathParts[0];
817
+ currentSearch = pathParts[1] ? '?' + pathParts[1] : '';
818
+ }
819
+ } catch (e) {
820
+ // Fallback: sử dụng currentPath trực tiếp
821
+ const pathParts = currentPath.split('?');
822
+ currentPathname = pathParts[0];
823
+ currentSearch = pathParts[1] ? '?' + pathParts[1] : '';
824
+ }
825
+ }
826
+
827
+ // Chuẩn hóa input URL
828
+ const normalizedUrl = url.trim();
829
+
830
+ // Hash mode: chỉ chấp nhận "/abc...", "abc...", hoặc "[schema]://[host]/#/abc..."
831
+ if (mode === 'hash') {
832
+ // Từ chối standalone hash fragment (#abc không có ://) hoặc query string (?name=value)
833
+ if ((normalizedUrl.startsWith('#') && !normalizedUrl.includes('://')) ||
834
+ normalizedUrl.startsWith('?')) {
835
+ return false;
836
+ }
837
+
838
+ let inputPathname, inputSearch;
839
+
840
+ // Trường hợp: Full URL với hash [schema]://[host]/#/abc...
841
+ if (normalizedUrl.startsWith('http://') || normalizedUrl.startsWith('https://')) {
842
+ try {
843
+ const inputUrl = new URL(normalizedUrl);
844
+
845
+ // Kiểm tra host có khớp với window.location.host không
846
+ if (inputUrl.host !== window.location.host) {
847
+ return false; // Host khác nhau
848
+ }
849
+
850
+ // Trích xuất hash path (bỏ ký tự #)
851
+ const hashPath = inputUrl.hash.substring(1) || '';
852
+ if (!hashPath) {
853
+ return false; // Không có hash path trong URL
854
+ }
855
+
856
+ // Parse hash path
857
+ const hashParts = hashPath.split('?');
858
+ inputPathname = hashParts[0];
859
+ inputSearch = hashParts[1] ? '?' + hashParts[1] : '';
860
+ } catch (e) {
861
+ return false; // Định dạng URL không hợp lệ
862
+ }
863
+ } else {
864
+ // Trường hợp: Absolute path (/abc...) hoặc relative path (abc...)
865
+ // Bỏ hash và query để so sánh
866
+ const urlWithoutHash = normalizedUrl.split('#')[0];
867
+ const pathParts = urlWithoutHash.split('?');
868
+ inputPathname = pathParts[0];
869
+ inputSearch = pathParts[1] ? '?' + pathParts[1] : '';
870
+ }
871
+
872
+ // So sánh với current hash path
873
+ return currentPathname === inputPathname && currentSearch === inputSearch;
874
+ }
875
+
876
+ // History mode: xử lý tất cả các định dạng URL như trước
877
+ // Trường hợp 1: Hash fragment (#abc) - cùng path, chỉ khác hash fragment
878
+ if (normalizedUrl.startsWith('#')) {
879
+ return true; // Cùng path, khác hash fragment
880
+ }
881
+
882
+ // Trường hợp 2: Query string (?name=value) - cùng path, chỉ khác query
883
+ if (normalizedUrl.startsWith('?')) {
884
+ return true; // Cùng path, khác query
885
+ }
886
+
887
+ // Trường hợp 3: Absolute path (/path/to/page)
888
+ if (normalizedUrl.startsWith('/')) {
889
+ try {
890
+ // Bỏ hash nếu có (trong history mode, hash là fragment)
891
+ const urlWithoutHash = normalizedUrl.split('#')[0];
892
+ const pathParts = urlWithoutHash.split('?');
893
+ const inputPathname = pathParts[0];
894
+ const inputSearch = pathParts[1] ? '?' + pathParts[1] : '';
895
+
896
+ // So sánh pathname và search
897
+ return currentPathname === inputPathname && currentSearch === inputSearch;
898
+ } catch (e) {
899
+ // Nếu parse thất bại, so sánh chuỗi đơn giản
900
+ const urlWithoutHash = normalizedUrl.split('#')[0];
901
+ return currentPath === urlWithoutHash || currentPathname === urlWithoutHash;
902
+ }
903
+ }
904
+
905
+ // Trường hợp 4: Full URL (http://example.com/path) - chỉ trong history mode
906
+ if (normalizedUrl.startsWith('http://') || normalizedUrl.startsWith('https://')) {
907
+ try {
908
+ const inputUrl = new URL(normalizedUrl);
909
+ const inputPathname = inputUrl.pathname;
910
+ const inputSearch = inputUrl.search;
911
+
912
+ // So sánh với window.location nếu currentPath là relative
913
+ if (!currentPath.startsWith('http://') && !currentPath.startsWith('https://')) {
914
+ // Current path là relative, so sánh với window.location
915
+ return window.location.pathname === inputPathname &&
916
+ window.location.search === inputSearch;
917
+ }
918
+
919
+ // Cả hai đều là full URL, so sánh trực tiếp
920
+ return currentPathname === inputPathname && currentSearch === inputSearch;
921
+ } catch (e) {
922
+ return false; // Định dạng URL không hợp lệ
923
+ }
924
+ }
925
+
926
+ // Trường hợp 5: Relative path (abc, abc/def, ...) - so sánh với segment(s) cuối của current path
927
+ // Bỏ hash và query từ relative URL
928
+ const relativeUrl = normalizedUrl.split('#')[0].split('?')[0];
929
+
930
+ // Lấy segment(s) cuối của current pathname
931
+ const currentSegments = currentPathname.split('/').filter(s => s);
932
+ const inputSegments = relativeUrl.split('/').filter(s => s);
933
+
934
+ // Nếu input có nhiều segments hơn current, không thể khớp
935
+ if (inputSegments.length > currentSegments.length) {
936
+ return false;
937
+ }
938
+
939
+ // So sánh N segments cuối, trong đó N = inputSegments.length
940
+ const startIndex = currentSegments.length - inputSegments.length;
941
+ for (let i = 0; i < inputSegments.length; i++) {
942
+ if (currentSegments[startIndex + i] !== inputSegments[i]) {
943
+ return false;
944
+ }
945
+ }
946
+
947
+ return true;
948
+ }
949
+
950
+ static getCachedRoute(routePath) {
951
+ return Router.containers[routePath] || null;
952
+ }
953
+
954
+ /**
955
+ * Get current route
956
+ * @returns {ActiveRoute} current route
957
+ */
958
+ static getCurrentRoute() {
959
+ return Router.activeRoute || null;
960
+ }
961
+ /**
962
+ * Get active route
963
+ * @returns {ActiveRoute} active route
964
+ */
965
+ static getActiveRoute() {
966
+ return Router.activeRoute || null;
967
+ }
968
+ static getCurrentPath() {
969
+ return window.location.pathname;
970
+ }
971
+ static getCurrentUri() {
972
+ return this.mode === 'history' ? Router.getCurrentPath() : Router.getCurrentHash();
973
+ }
974
+ static getCurrentHash() {
975
+ const fragmentString = window.location.hash.substring(1) || '';
976
+ return fragmentString;
977
+ }
978
+ static getCurrentUrl() {
979
+ return this.mode === 'history' ? Router.getCurrentPath() + window.location.search : Router.getCurrentHash();
980
+ }
981
+ static getCurrentQuery() {
982
+ const query = Object.fromEntries(new URLSearchParams(window.location.search));
983
+ return query;
984
+ }
985
+ static getCurrentFragment() {
986
+ const fragmentString = window.location.hash.substring(1) || '';
987
+ return fragmentString;
988
+ }
989
+ }
990
+
991
+ export default Router;
992
+
993
+ export const useRoute = () => Router.getCurrentRoute();
994
+ export const useParams = () => useRoute()?.getParams() || {};
995
+ export const useQuery = () => Router.getCurrentQuery() || {};
996
+ export const useFragment = () => Router.getCurrentFragment() || {};