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,1962 @@
1
+ import { SEOTagConfig } from './SEOConfig.js';
2
+ import { ViewTemplates } from './ViewTemplate.js';
3
+ import { hasData, uniqId } from '../helpers/utils.js';
4
+ import { View, ViewEngine } from './View.js';
5
+ import { ViewState } from './ViewState.js';
6
+ import { ATTR } from './ViewConfig.js';
7
+ import logger from './services/LoggerService.js';
8
+ import { OneMarkup } from './OneMarkup.js';
9
+ import { StorageService } from './services/StorageService.js';
10
+ import OneDOM from './OneDOM.js';
11
+ import { viewLoader } from './ViewLoader.js';
12
+ import { ViewController } from './ViewController.js';
13
+
14
+
15
+ class SSRViewData {
16
+ constructor(viewData) {
17
+ const { instances, indexMap } = this.parseViewData(viewData);
18
+ this.instances = instances;
19
+ this.indexMap = indexMap;
20
+ this.index = 0;
21
+ this.maxIndex = indexMap.length - 1;
22
+ }
23
+ parseViewData(viewData) {
24
+ let instances = viewData.instances || {};
25
+ const instancesMap = new Map();
26
+ const keys = Object.keys(instances);
27
+ keys.forEach(key => {
28
+ instancesMap.set(key, instances[key]);
29
+ });
30
+ const indexMap = keys;
31
+ return {
32
+ instances: instancesMap,
33
+ indexMap: indexMap,
34
+ };
35
+ }
36
+ get(index = null) {
37
+ if (index === null) {
38
+ index = this.index;
39
+ }
40
+ this.index = index;
41
+ return this.instances.get(this.indexMap[index]) ?? null;
42
+ }
43
+ next() {
44
+ this.index++;
45
+ return this.get(this.index);
46
+ }
47
+ prev() {
48
+ this.index--;
49
+ return this.get(this.index);
50
+ }
51
+ getById(id) {
52
+ return this.instances.get(id) ?? null;
53
+ }
54
+ scan() {
55
+ let index = this.index;
56
+ this.index++;
57
+ let instance = this.get(index);
58
+ if (!instance) {
59
+ this.index--;
60
+ return null;
61
+ }
62
+ return instance;
63
+ }
64
+ }
65
+
66
+ class SSRViewDataCollection {
67
+ constructor(views) {
68
+ /**
69
+ * @type {Object<string, SSRViewData>}
70
+ */
71
+ this.views = new Map();
72
+ this.setViews(views);
73
+ }
74
+ setViews(views) {
75
+ if (typeof views !== 'object' || !views || Object.keys(views).length === 0) {
76
+ return;
77
+ }
78
+ Object.keys(views).forEach(name => {
79
+ this.views.set(name, new SSRViewData(views[name]));
80
+ });
81
+ }
82
+ get(name) {
83
+ return this.views.get(name) ?? null;
84
+ }
85
+ scan(name) {
86
+ return this.get(name)?.scan() ?? null;
87
+ }
88
+ getInstance(name, id) {
89
+ return this.get(name)?.getById(id) ?? null;
90
+ }
91
+ }
92
+
93
+ export class ViewManager {
94
+ constructor(App = null) {
95
+ /**
96
+ * @type {Application}
97
+ */
98
+ this.App = App;
99
+
100
+ this.markupService = OneMarkup;
101
+ /**
102
+ * @type {StorageService}
103
+ */
104
+ this.storageService = StorageService.getInstance('onejs_view_data');
105
+ /**
106
+ * @type {HTMLElement}
107
+ */
108
+ this.container = null;
109
+ /**
110
+ * @type {Object<string, function>}
111
+ */
112
+ this.stateChangeListeners = {};
113
+ /**
114
+ * @type {ViewEngine}
115
+ */
116
+ this.currentMasterView = null;
117
+ /**
118
+ * @type {ViewEngine}
119
+ */
120
+ this.Engine = View;
121
+ /**
122
+ * @type {ViewState}
123
+ */
124
+ this.State = ViewState;
125
+ this.Controller = ViewController
126
+ /**
127
+ * @type {Object<string, ViewEngine>}
128
+ */
129
+ this.templates = ViewTemplates;
130
+ /**
131
+ * @type {Object<string, string>}
132
+ */
133
+ this._sections = {};
134
+ /**
135
+ * @type {Array<string>}
136
+ */
137
+ this._changedSections = [];
138
+ /**
139
+ * @type {Object<string, Array<string>>}
140
+ */
141
+ this._stacks = {};
142
+
143
+ /**
144
+ * @type {boolean}
145
+ */
146
+ this.scanMode = false;
147
+ /**
148
+ * @type {Object<string, boolean>}
149
+ */
150
+ this._once = {};
151
+ /**
152
+ * @type {HTMLElement}
153
+ */
154
+ this.vitualContainer = document.createElement('div');
155
+ this.vitualContainer.setAttribute('id', 'data-vitual-container');
156
+ this.vitualContainer.style.display = 'none';
157
+ /**
158
+ * @type {Array<ViewEngine>}
159
+ */
160
+ this.VIEW_MOUNTED_QUEUE = [];
161
+ /**
162
+ * @type {ViewEngine}
163
+ */
164
+ this.CURRENT_SUPER_VIEW = null;
165
+
166
+ this.CURRENT_SUPER_VIEW_PATH = null;
167
+ /**
168
+ * @type {boolean}
169
+ */
170
+ this.CURRENT_SUPER_VIEW_MOUNTED = false;
171
+ /**
172
+ * @type {ViewEngine}
173
+ */
174
+ this.CURRENT_VIEW = null;
175
+
176
+ this.CURRENT_VIEW_PATH = null;
177
+ /**
178
+ * @type {boolean}
179
+ */
180
+ this.CURRENT_VIEW_MOUNTED = false;
181
+
182
+ /**
183
+ * @type {Array<ViewEngine>}
184
+ */
185
+ this.SUPER_VIEW_STACK = [];
186
+ /**
187
+ * @type {Array<ViewEngine>}
188
+ */
189
+ this.ALL_VIEW_STACK = [];
190
+ /**
191
+ * @type {ViewEngine}
192
+ */
193
+ this.PAGE_VIEW = null;
194
+ /**
195
+ * @type {number}
196
+ */
197
+ this.renderTimes = -1;
198
+
199
+ /**
200
+ * @type {Object<string, ViewEngine>}
201
+ */
202
+ this.cachedViews = {};
203
+ /**
204
+ * @type {Object<string, any>}
205
+ */
206
+ this.serverRenderData = {};
207
+ /**
208
+ * @type {Map<string, ViewEngine>}
209
+ */
210
+ this.viewMap = new Map();
211
+
212
+ /**
213
+ * @type {Map<string, Object<string, any>>}
214
+ */
215
+ this.cachedPageData = new Map();
216
+
217
+ // document.body.appendChild(this.vitualContainer);
218
+ /**
219
+ * @type {Object<string, any>}
220
+ */
221
+ this.wrapperConfig = {
222
+
223
+ };
224
+
225
+ this.listeners = {};
226
+ this.systemData = {};
227
+ this.ssrData = {};
228
+ this.ssrViewManager = new SSRViewDataCollection();
229
+
230
+ /**
231
+ * @type {ViewLoader}
232
+ */
233
+ this.viewLoader = viewLoader;
234
+
235
+ /**
236
+ * @type {Map<string, ViewEngine>}
237
+ */
238
+ this.cachedPageView = new Map();
239
+
240
+ this.cachedTimes = 600; // mặc định cache 10 phút
241
+
242
+
243
+ /**
244
+ * @type {Object<string, any>}
245
+ */
246
+ this.SEO = {
247
+ tags: SEOTagConfig,
248
+ updateItem: function (key, value) {
249
+ const items = this.tags[key];
250
+ if (!items || items.length == 0) return false;
251
+ items.forEach(item => {
252
+ let element = document.querySelector(item.selector);
253
+ if (!element) {
254
+ element = document.createElement(item.tag);
255
+ if (item.attrs) {
256
+ Object.entries(item.attrs).forEach(([key, value]) => {
257
+ element.setAttribute(key, value);
258
+ });
259
+ }
260
+ document.head.appendChild(element);
261
+ };
262
+
263
+ if (item.attribute) {
264
+ if (item.attribute == "@content") {
265
+ element.textContent = value;
266
+ } else {
267
+ element.setAttribute(item.attribute, value);
268
+ }
269
+ }
270
+ });
271
+ return true;
272
+ }
273
+ };
274
+
275
+
276
+ }
277
+
278
+ setApp(app) {
279
+ this.App = app;
280
+ }
281
+ setContainer(container) {
282
+ this.container = container;
283
+ }
284
+
285
+ /**
286
+ * Initialize SPA
287
+ */
288
+ init(data = {}) {
289
+ // console.log('App.View initialized', data);
290
+
291
+ // Clear sections and stacks on page load
292
+ this._sections = {};
293
+ this._stacks = {};
294
+ this._once = {};
295
+ this._changedSections = [];
296
+
297
+ // Initialize views if not already done
298
+ if (!this.templates) {
299
+ this.templates = ViewTemplates;
300
+ }
301
+
302
+ // Initialize current scope
303
+ this._currentScope = 'web';
304
+
305
+ this.ssrData = data?.ssrData || {};
306
+ this.ssrViewManager.setViews(this.ssrData);
307
+ this.systemData = data?.systemData || {};
308
+ }
309
+ updateSystemData(data = {}) {
310
+ this.systemData = { ...this.systemData, ...data };
311
+ }
312
+
313
+ /**
314
+ * Get view by name
315
+ * @param {string} name - View name
316
+ * @returns {ViewEngine|null} View object or null if not found
317
+ */
318
+ getView(name) {
319
+ return this.templates[name] || null;
320
+ }
321
+
322
+ /**
323
+ * Load view dynamically (lazy loading)
324
+ * @param {string} name - View name
325
+ * @param {Object} data - Data to pass to view
326
+ * @returns {Promise<ViewEngine>} View instance
327
+ */
328
+ async loadViewDynamic(name, data = {}) {
329
+ try {
330
+ // Check if view is already in templates
331
+ if (this.templates[name]) {
332
+ return this.view(name, data);
333
+ }
334
+
335
+ // Load view dynamically using ViewLoader
336
+ logger.debug(`[ViewManager] Loading view dynamically: ${name}`);
337
+ const ViewClass = await this.viewLoader.load(name);
338
+
339
+ // Register view in templates
340
+ this.templates[name] = ViewClass;
341
+
342
+ // Create view instance
343
+ return this.view(name, data);
344
+ } catch (error) {
345
+ logger.error(`[ViewManager] Failed to load view dynamically: ${name}`, error);
346
+ throw error;
347
+ }
348
+ }
349
+
350
+ /**
351
+ * Preload views for better performance
352
+ * @param {Array<string>} viewNames - View names to preload
353
+ * @returns {Promise<void>}
354
+ */
355
+ async preloadViews(viewNames) {
356
+ logger.debug('[ViewManager] Preloading views:', viewNames);
357
+ const promises = viewNames.map(name => this.loadViewDynamic(name).catch(error => {
358
+ logger.warn(`[ViewManager] Failed to preload view: ${name}`, error);
359
+ }));
360
+ await Promise.all(promises);
361
+ }
362
+
363
+
364
+ /**
365
+ * Load and render view with master view handling
366
+ * @param {string} name - View name
367
+ * @param {Object} data - Data to pass to view
368
+ * @param {string} urlPath - URL path for the view
369
+ * @returns {Object<html: string, error: string, superView: ViewEngine, isSuperView: boolean, needInsert: boolean, ultraView: ViewEngine>} Final rendered HTML string
370
+ */
371
+ loadView(name, data = {}, urlPath = '') {
372
+ // console.log('🔍 App.View.loadView called with:', name, data);
373
+ if (this.templates[name]) {
374
+ this.clearOldRendering();
375
+ }
376
+ this.renderTimes++;
377
+ let message = null;
378
+ this.CURRENT_SUPER_VIEW_MOUNTED = false;
379
+
380
+ try {
381
+ let hasCache = false;
382
+ if (this.cachedTimes > 0) {
383
+ let cacheKey = name.replace('.', '_') + '_' + urlPath?.replace(/\//g, '_');
384
+ const cachedData = this.storageService.get(cacheKey);
385
+ if (cachedData) {
386
+ data = { ...data, ...cachedData };
387
+ hasCache = true;
388
+ }
389
+ }
390
+ // Get view from templates
391
+ let view = this.view(name, hasCache ? { ...data } : null);
392
+ if (!view) {
393
+ message = `App.View.loadView: View '${name}' not found`;
394
+ return {
395
+ error: `App.View.loadView: View '${name}' not found`,
396
+ html: null,
397
+ superView: null,
398
+ isSuperView: false,
399
+ needInsert: false,
400
+ };
401
+ }
402
+ if (urlPath) {
403
+ view.__.urlPath = urlPath;
404
+ }
405
+ if (this.cachedTimes > 0) {
406
+ if (this.PAGE_VIEW instanceof ViewEngine) {
407
+ const oldCacheData = this.PAGE_VIEW.data;
408
+ let cacheKey = this.PAGE_VIEW.path.replace('.', '_') + '_' + this.PAGE_VIEW.urlPath?.replace(/\//g, '_');
409
+ this.storageService.set(cacheKey, oldCacheData, 3600); // cache trong 1 giờ
410
+ }
411
+ }
412
+
413
+ // Store view in array for tracking
414
+ this.PAGE_VIEW = view;
415
+ let superView = null;
416
+ let superViewPath = null;
417
+ let result;
418
+ let currentViewPath = view.__.path;
419
+ let currentView = view;
420
+ let ultraView = view;
421
+ let renderIndex = 0;
422
+ // vòng lặp để render view cho đến khi không có super view hoặc không có super view thì render view
423
+ do {
424
+ try {
425
+ // kiểm tra view có super view không
426
+ if (view.__.hasSuperView) {
427
+ this.ALL_VIEW_STACK.unshift(view);
428
+ superViewPath = view.__.superViewPath;
429
+ result = this.renderView(view);
430
+ view = result;
431
+ if (view && typeof view === 'object') {
432
+ view.__.setIsSuperView(true);
433
+ superView = view;
434
+ ultraView = view;
435
+ }
436
+ }
437
+ // kiểm tra view có phải là super view không
438
+ else if (view.__.isSuperView) {
439
+ if (superViewPath !== view.path) {
440
+ this.SUPER_VIEW_STACK.unshift(view);
441
+ superViewPath = view.__.path;
442
+ }
443
+
444
+ superView = view;
445
+ ultraView = view;
446
+ if (view.__.hasSuperView) {
447
+ result = this.renderView(view, renderIndex > 0 ? view.data : null);
448
+ view = result;
449
+ view.__.setIsSuperView(true);
450
+ if (view && typeof view === 'object') {
451
+ superView = view;
452
+ superViewPath = view.__.path;
453
+ ultraView = view;
454
+ }
455
+ } else {
456
+ result = '';// nếu là super view thì không cần thêm vào queue và không render. để bước sau xử lý
457
+ }
458
+ }
459
+ // nếu không có super view và không phải là super view thì render view
460
+ else {
461
+
462
+ this.ALL_VIEW_STACK.unshift(view);
463
+ // this.PAGE_VIEW = view;
464
+ // console.log('🔍 App.View.loadView normal view:', view.path);
465
+ result = this.renderView(view, renderIndex > 0 ? view.data : null);
466
+ ultraView = view;
467
+ }
468
+ } catch (error) {
469
+ message = `App.View.loadView: Error rendering view '${name}':` + error.message;
470
+ console.error('🔍 App.View.loadView error:', error);
471
+ return '';
472
+ }
473
+ renderIndex++;
474
+ } while (result && typeof result === 'object' && result instanceof ViewEngine)
475
+ // Update #spa-root with the final string
476
+ // console.log("view after check", { currentViewPath, superViewPath })
477
+ try {
478
+ let html = result;
479
+ // diểu kiễn có cần insert content vào html không
480
+ const needInsert = !(superViewPath && superViewPath === this.CURRENT_SUPER_VIEW_PATH);
481
+ if (superViewPath) {
482
+ // kiểm tra view có phải là super view không
483
+ if (!needInsert) { // nếu không cần insert content vào html thì set trang thái super view mounted = true
484
+ // console.log('🔍 App.View.loadView need insert super view:', superViewPath);
485
+ this.CURRENT_SUPER_VIEW_MOUNTED = true;
486
+ } else { // nếu cần insert content vào html thì set trang thái super view mounted = false và render super view
487
+ this.CURRENT_SUPER_VIEW_PATH = superViewPath;
488
+ this.CURRENT_SUPER_VIEW = superView;
489
+ // this.CURRENT_SUPER_VIEW_MOUNTED = false;
490
+ html = superView.__.render();
491
+ // console.log('🔍 App.View.loadView render super view:', html);
492
+
493
+
494
+ }
495
+ }
496
+ // console.log('🔍 App.View.loadView need insert:', needInsert);
497
+ // console.log('🔍 App.View.loadView return in try');
498
+ return {
499
+ html: html,
500
+ isSuperView: superViewPath ? true : false,
501
+ needInsert: needInsert,
502
+ superView: superView,
503
+ ultraView: ultraView,
504
+ error: null
505
+ };
506
+
507
+ } catch (error) {
508
+ message = 'App.View.loadView: Error updating DOM:', error;
509
+
510
+ }
511
+
512
+ // console.log(`🎉 App.View.loadView: Successfully loaded view '${name}'`);
513
+
514
+ } catch (error) {
515
+ message = `App.View.loadView: Critical error loading view '${name}':`, error;
516
+
517
+ }
518
+ return {
519
+ html: null,
520
+ needInsert: false,
521
+ superView: null,
522
+ ultraView: null,
523
+ isSuperView: false,
524
+ error: message
525
+ };
526
+ }
527
+
528
+
529
+ scanView(name, route = null) {
530
+ if (this.templates[name]) {
531
+ this.clearOldRendering();
532
+ }
533
+ this.renderTimes++;
534
+ let message = null;
535
+ this.CURRENT_SUPER_VIEW_MOUNTED = false;
536
+
537
+ try {
538
+ const viewData = this.ssrViewManager.scan(name);
539
+ if (!viewData) {
540
+ message = `App.View.scanView: View '${name}' not found`;
541
+ return {
542
+ error: `App.View.scanView: View '${name}' not found`,
543
+ html: null,
544
+ superView: null,
545
+ isSuperView: false,
546
+ needInsert: false,
547
+ };
548
+ }
549
+ const data = viewData.data;
550
+ data.__SSR_VIEW_ID__ = viewData.viewId;
551
+ // console.log('🔍 App.View.scanView called with:', name, data);
552
+
553
+ // Get view from templates
554
+ let view = this.view(name, data);
555
+ if (!view) {
556
+ message = `App.View.loadView: View '${name}' not found`;
557
+ return {
558
+ error: `App.View.loadView: View '${name}' not found`,
559
+ html: null,
560
+ superView: null,
561
+ isSuperView: false,
562
+ needInsert: false,
563
+ };
564
+ }
565
+ if (route && route.$urlPath) {
566
+ view.__.urlPath = route.$urlPath;
567
+ }
568
+
569
+
570
+ // Store view in array for tracking
571
+ this.PAGE_VIEW = view;
572
+ let superView = null;
573
+ let superViewPath = null;
574
+ let result;
575
+ let currentViewPath = view.__.path;
576
+ let currentView = view;
577
+ let ultraView = view;
578
+ view.__.__scan(viewData);
579
+ // view.__scanData = viewData;
580
+ // vòng lặp để render view cho đến khi không có super view hoặc không có super view thì render view
581
+ let renderIndex = 0;
582
+ do {
583
+ try {
584
+ // kiểm tra view có super view không
585
+ if (view.__.hasSuperView) {
586
+ this.ALL_VIEW_STACK.unshift(view);
587
+ superViewPath = view.__.superViewPath;
588
+ result = this.scanRenderedView(view);
589
+ view = result;
590
+ if (view && typeof view === 'object') {
591
+ view.__.setIsSuperView(true);
592
+ superView = view;
593
+ ultraView = view;
594
+ if (!view.__.isScanned) {
595
+
596
+ // ============================================================
597
+ // CRITICAL FIX: Scan super view DOM + attach events
598
+ // ============================================================
599
+ // Get super view SSR data and scan it
600
+ const superViewData = this.ssrViewManager.scan(superViewPath);
601
+ if (superViewData) {
602
+ // logger.log(`🔍 View.scanView: Scanning super view ${superViewPath}`);
603
+ superView.__.__scan(superViewData);
604
+ // superView.__scanData = superViewData;
605
+ // logger.log(`✅ View.scanView: Super view ${superViewPath} scanned`);
606
+ } else {
607
+ // logger.warn(`⚠️ View.scanView: No SSR data for super view ${superViewPath}`);
608
+ }
609
+ }
610
+ }
611
+ }
612
+ // kiểm tra view có phải là super view không
613
+ else if (view.__.isSuperView) {
614
+ if (superViewPath !== view.__.path) {
615
+ this.SUPER_VIEW_STACK.unshift(view);
616
+ superViewPath = view.__.path;
617
+ ultraView = view;
618
+ }
619
+
620
+ superView = view;
621
+ ultraView = view;
622
+ if (view.__.hasSuperView) {
623
+ result = this.scanRenderedView(view);
624
+ view = result;
625
+ view.__.setIsSuperView(true);
626
+ if (view && typeof view === 'object') {
627
+ superView = view;
628
+ superViewPath = view.__.path;
629
+ ultraView = view;
630
+ if (!view.__.isScanned) {
631
+ // ============================================================
632
+ // CRITICAL FIX: Scan nested super view
633
+ // ============================================================
634
+ const nestedSuperViewData = this.ssrViewManager.scan(superViewPath);
635
+ if (nestedSuperViewData) {
636
+ // logger.log(`🔍 View.scanView: Scanning nested super view ${superViewPath}`);
637
+ superView.__.__scan(nestedSuperViewData);
638
+ // superView.__scanData = nestedSuperViewData;
639
+ // logger.log(`✅ View.scanView: Nested super view ${superViewPath} scanned`);
640
+ }
641
+ }
642
+ }
643
+ } else {
644
+ result = '';// nếu là super view thì không cần thêm vào queue và không render. để bước sau xử lý
645
+ }
646
+ }
647
+ // nếu không có super view và không phải là super view thì render view
648
+ else {
649
+ this.ALL_VIEW_STACK.unshift(view);
650
+ this.PAGE_VIEW = view;
651
+ // console.log('🔍 App.View.scanView normal view:', view.path);
652
+ result = this.scanRenderedView(view);
653
+ ultraView = view;
654
+ }
655
+ } catch (error) {
656
+ message = `App.View.scanView: Error rendering view '${name}':`, error;
657
+ console.error('🔍 App.View.scanView error:', error);
658
+ return '';
659
+ }
660
+ renderIndex++;
661
+ } while (result && typeof result === 'object' && result.constructor === this.Engine)
662
+ // Update #spa-root with the final string
663
+ // console.log("view after check", { currentViewPath, superViewPath })
664
+ try {
665
+ let html = result;
666
+ // diểu kiễn có cần insert content vào html không
667
+ const needInsert = !(superViewPath && superViewPath === this.CURRENT_SUPER_VIEW_PATH);
668
+ if (superViewPath) {
669
+ // kiểm tra view có phải là super view không
670
+ if (!needInsert) { // nếu không cần insert content vào html thì set trang thái super view mounted = true
671
+ // console.log('🔍 App.View.scanView need insert super view:', superViewPath);
672
+ this.CURRENT_SUPER_VIEW_MOUNTED = true;
673
+ } else { // nếu cần insert content vào html thì set trang thái super view mounted = false và render super view
674
+ this.CURRENT_SUPER_VIEW_PATH = superViewPath;
675
+ this.CURRENT_SUPER_VIEW = superView;
676
+ this.CURRENT_SUPER_VIEW_MOUNTED = false;
677
+ html = superView.__.virtualRender();
678
+
679
+
680
+ }
681
+ }
682
+
683
+ // ============================================================
684
+ // LIFECYCLE: Mount all views in bottom-up order
685
+ // ============================================================
686
+ // After scanning and virtual render complete, mount all views
687
+ // in reverse order (deepest layout → page view)
688
+ this.mountAllViewsFromStack(this.renderTimes).then(() => {
689
+ logger.log('✅ View.scanView: All views mounted successfully in bottom-up order');
690
+ }).catch(error => {
691
+ logger.error('❌ View.scanView: Error mounting views:', error);
692
+ });
693
+
694
+ return {
695
+ html: html,
696
+ isSuperView: superViewPath ? true : false,
697
+ needInsert: needInsert,
698
+ superView: superView,
699
+ ultraView: ultraView,
700
+ error: null
701
+ };
702
+
703
+ } catch (error) {
704
+ message = 'App.View.scanView: Error updating DOM:' + error.message;
705
+
706
+ }
707
+
708
+ // console.log(`🎉 App.View.scanView: Successfully loaded view '${name}'`);
709
+
710
+ } catch (error) {
711
+ message = `App.View.scanView: Critical error loading view '${name}':` + error.message;
712
+
713
+ }
714
+ return {
715
+ html: null,
716
+ needInsert: false,
717
+ superView: null,
718
+ isSuperView: false,
719
+ ultraView: null,
720
+ error: message
721
+ };
722
+ }
723
+
724
+ mountView(viewName, params = {}, route = null) {
725
+ try {
726
+ // ============================================================
727
+ // CLEANUP: Destroy old views before loading new view
728
+ // This ensures CSS and scripts are properly removed
729
+ // ============================================================
730
+ let currentSuperView = this.CURRENT_SUPER_VIEW;
731
+ if (currentSuperView && currentSuperView instanceof ViewEngine && !currentSuperView.isDestroyed) {
732
+ // Call destroy() to remove CSS and scripts
733
+ currentSuperView.__._lifecycleManager.destroy();
734
+ }
735
+
736
+ // Destroy old PAGE_VIEW if exists
737
+ if (this.PAGE_VIEW && this.PAGE_VIEW instanceof ViewEngine && !this.PAGE_VIEW.isDestroyed) {
738
+ // Only destroy if it's different from currentSuperView to avoid double destroy
739
+ if (this.PAGE_VIEW !== currentSuperView) {
740
+ this.PAGE_VIEW.__._lifecycleManager.destroy();
741
+ }
742
+ }
743
+
744
+ const viewResult = this.loadView(viewName, params, route?.$urlPath || '');
745
+ if (viewResult.error) {
746
+ console.error('View rendering error:', viewResult.error);
747
+ return;
748
+ }
749
+
750
+ if (viewResult.needInsert && viewResult.html) {
751
+ const container = this.container || document.querySelector('#app-root') || document.querySelector('#app') || document.body;
752
+ const html = viewResult.html
753
+ if (container) {
754
+ OneDOM.setHTML(container, html);
755
+ }
756
+ } else {
757
+ // console.log('Router: Not updating DOM - needInsert:', viewResult.needInsert, 'html:', !!viewResult.html);
758
+ }
759
+
760
+ // Emit changed sections
761
+ if (this.emitChangedSections) {
762
+ this.emitChangedSections();
763
+ }
764
+
765
+ if (viewResult.ultraView && viewResult.ultraView instanceof ViewEngine) {
766
+ viewResult.ultraView.__._lifecycleManager.mounted();
767
+ }
768
+ this.CURRENT_SUPER_VIEW_MOUNTED = true; // set trang thái super view mounted = true
769
+
770
+ this.scrollToTop()
771
+
772
+ } catch (error) {
773
+ console.error('Error rendering view:', error);
774
+ }
775
+ }
776
+
777
+ mountViewScan(viewName, params = {}, route = null) {
778
+
779
+
780
+ // ============================================================
781
+ // CORE HYDRATION: Scan SSR HTML and attach JS behavior
782
+ // ============================================================
783
+ // scanView() will:
784
+ // 1. Parse SSR data from HTML comments
785
+ // 2. Create view instances (page + layouts)
786
+ // 3. Call virtualRender() to setup relationships
787
+ // 4. Scan DOM and attach event handlers
788
+ // 5. Setup state subscriptions
789
+ // 6. Mount all views in bottom-up order (deepest layout → page)
790
+ const scanResult = this.scanView(viewName);
791
+ // console.log(scanResult);
792
+ if (scanResult.error) {
793
+ console.error('❌ Router.hydrateViews: Scan error:', scanResult.error);
794
+ return;
795
+ }
796
+
797
+ if (scanResult.needInsert && scanResult.html) {
798
+ const container = this.container || document.querySelector('#app-root') || document.querySelector('#app') || document.body;
799
+ const html = scanResult.html
800
+ if (container) {
801
+ OneDOM.setHTML(container, html);
802
+ }
803
+ } else {
804
+ console.log('Router: Not updating DOM - needInsert:', scanResult.needInsert, 'html:', !!scanResult.html);
805
+ }
806
+
807
+ // Emit changed sections
808
+ if (this.emitChangedSections) {
809
+ this.emitChangedSections();
810
+ }
811
+
812
+ if (scanResult.ultraView && scanResult.ultraView instanceof ViewEngine) {
813
+ scanResult.ultraView.__._lifecycleManager.mounted();
814
+ }
815
+ this.CURRENT_SUPER_VIEW_MOUNTED = true; // set trang thái super view mounted = true
816
+
817
+ this._isHydrated = true;
818
+ }
819
+
820
+
821
+ mountContent(htmlContent) {
822
+ const container = this.container || document.querySelector('#app-root') || document.querySelector('#app') || document.body;
823
+ if (container) {
824
+ OneDOM.setHTML(container, htmlContent);
825
+ }
826
+ }
827
+
828
+ // ============================================================================
829
+ // EVENT FUNCTIONS
830
+ // ============================================================================
831
+
832
+
833
+ on(event, callback) {
834
+ if (typeof event !== 'string' || event === '') {
835
+ return false;
836
+ }
837
+ if (typeof callback !== 'function') {
838
+ return false;
839
+ }
840
+ if (!this.listeners[event]) {
841
+ this.listeners[event] = [];
842
+ }
843
+ this.listeners[event].push(callback);
844
+ }
845
+ off(event, callback) {
846
+ if (typeof event !== 'string' || event === '') {
847
+ return false;
848
+ }
849
+ if (typeof callback !== 'function') {
850
+ return false;
851
+ }
852
+ if (!this.listeners[event]) {
853
+ return;
854
+ }
855
+ this.listeners[event] = this.listeners[event].filter(cb => cb !== callback);
856
+ }
857
+ emit(event, ...args) {
858
+ if (typeof event !== 'string' || event === '') {
859
+ return false;
860
+ }
861
+ if (!this.listeners[event]) {
862
+ return;
863
+ }
864
+ this.listeners[event].forEach(callback => callback(...args));
865
+ }
866
+ // ============================================================================
867
+ // CORE FUNCTIONS
868
+ // ============================================================================
869
+
870
+ /**
871
+ * Generate unique view ID
872
+ * @returns {string} UUID v4
873
+ */
874
+ generateViewId() {
875
+ return uniqId();
876
+ }
877
+
878
+ /**
879
+ * Execute function and return result
880
+ * @param {Function} fn - Function to execute
881
+ * @returns {string} Result as string
882
+ */
883
+ execute(fn, defaultValue = '') {
884
+ try {
885
+ const result = fn();
886
+ return result !== undefined ? result : defaultValue;
887
+ } catch (error) {
888
+ logger.error('App.execute error:', error);
889
+ return defaultValue;
890
+ }
891
+ }
892
+
893
+ evaluate(fn, defaultValue = '') {
894
+ try {
895
+ const result = fn();
896
+ return result !== undefined ? result : defaultValue;
897
+ } catch (error) {
898
+ logger.error('App.evaluate error:', error);
899
+ return defaultValue;
900
+ }
901
+ }
902
+
903
+ /**
904
+ * Escape string for HTML output
905
+ * @param {*} value - Value to escape
906
+ * @returns {string} Escaped string
907
+ */
908
+ escString(value) {
909
+ if (value === null || value === undefined) {
910
+ return '';
911
+ }
912
+
913
+ const str = String(value);
914
+ return str
915
+ .replace(/&/g, '&amp;')
916
+ .replace(/</g, '&lt;')
917
+ .replace(/>/g, '&gt;')
918
+ .replace(/"/g, '&quot;')
919
+ .replace(/'/g, '&#39;');
920
+ }
921
+
922
+ /**
923
+ * Get text content (for preloader messages)
924
+ * @param {string} key - Text key
925
+ * @returns {string} Text content
926
+ */
927
+ text(key) {
928
+ const texts = {
929
+ 'loading': 'Loading...',
930
+ 'error': 'Error occurred',
931
+ 'not_found': 'Not found',
932
+ 'unauthorized': 'Unauthorized'
933
+ };
934
+
935
+ return texts[key] || key;
936
+ }
937
+
938
+
939
+ // ============================================================================
940
+ // INIT FUNCTIONS
941
+ // ============================================================================
942
+ setSuperViewPath(superView) {
943
+ this.CURRENT_SUPER_VIEW_PATH = superView;
944
+ }
945
+ addViewEngine(renderTimes, viewEngine) {
946
+ if (typeof this.VIEW_MOUNTED_QUEUE[renderTimes] === 'undefined') {
947
+ this.VIEW_MOUNTED_QUEUE[renderTimes] = [];
948
+ }
949
+ this.VIEW_MOUNTED_QUEUE[renderTimes].push(viewEngine);
950
+ }
951
+ /**
952
+ * Gọi hàm mounted của ViewEngine
953
+ * @param {number} renderTimes - Số lần render hiện tại (dùng làm key cho queue).
954
+ * @param {string} viewEngineId - ID của ViewEngine cần gọi hàm mounted.
955
+ */
956
+ callViewEngineMounted(renderTimes, viewEngineId) {
957
+ return;
958
+ }
959
+
960
+ /**
961
+ * Mount all views in bottom-up order (last → first)
962
+ * This ensures parent views mount after their children
963
+ *
964
+ * Flow:
965
+ * 1. Layout View 2 (deepest) mounted first
966
+ * 2. Layout View 1 mounted
967
+ * 3. Included views mounted
968
+ * 4. First View (page view) mounted last
969
+ *
970
+ * @param {number} renderTimes - Số lần render hiện tại
971
+ * @returns {Promise<void>}
972
+ */
973
+ async mountAllViewsBottomUp(renderTimes) {
974
+ // logger.log(`🔄 View.mountAllViewsBottomUp: Starting bottom-up mounting for renderTimes=${renderTimes}`);
975
+
976
+ // Check if queue exists and has views
977
+ if (!this.VIEW_MOUNTED_QUEUE[renderTimes] || !this.VIEW_MOUNTED_QUEUE[renderTimes].length) {
978
+ // logger.warn(`⚠️ View.mountAllViewsBottomUp: No views in queue for renderTimes=${renderTimes}`);
979
+ return;
980
+ }
981
+
982
+ // Wait for super view to be ready
983
+ if (!this.CURRENT_SUPER_VIEW_MOUNTED) {
984
+ await new Promise(resolve => {
985
+ const checkInterval = setInterval(() => {
986
+ if (this.CURRENT_SUPER_VIEW_MOUNTED) {
987
+ clearInterval(checkInterval);
988
+ resolve();
989
+ }
990
+ }, 50); // Check every 50ms (faster than 100ms)
991
+ });
992
+ }
993
+
994
+ // Get queue for this render
995
+ const queue = this.VIEW_MOUNTED_QUEUE[renderTimes];
996
+
997
+ // Mount in reverse order (bottom-up: last → first)
998
+ // This ensures deepest layouts mount first, then parent views
999
+ for (let i = queue.length - 1; i >= 0; i--) {
1000
+ const viewEngine = queue[i];
1001
+
1002
+ try {
1003
+ // logger.log(`🎯 View.mountAllViewsBottomUp: Mounting view ${i + 1}/${queue.length} - ${viewEngine.path} (${viewEngine.id})`);
1004
+
1005
+ // Call beforeMount lifecycle
1006
+ if (viewEngine.__ && typeof viewEngine.__._lifecycleManager.beforeMount === 'function') {
1007
+ viewEngine.__._lifecycleManager.beforeMount();
1008
+ }
1009
+
1010
+ // Call mounted lifecycle
1011
+ if (viewEngine.__ && typeof viewEngine.__._lifecycleManager.mounted === 'function') {
1012
+ viewEngine.__._lifecycleManager.mounted();
1013
+ }
1014
+
1015
+ // logger.log(`✅ View.mountAllViewsBottomUp: Successfully mounted ${viewEngine.path}`);
1016
+ } catch (error) {
1017
+ logger.error(`❌ View.mountAllViewsBottomUp: Error mounting ${viewEngine.path}:`, error);
1018
+ // Continue mounting other views even if one fails
1019
+ }
1020
+ }
1021
+
1022
+ // Clear the queue after mounting all views
1023
+ this.VIEW_MOUNTED_QUEUE[renderTimes] = [];
1024
+ // logger.log(`✅ View.mountAllViewsBottomUp: All views mounted successfully, queue cleared`);
1025
+ }
1026
+
1027
+ /**
1028
+ * Mount all views using stack-based order
1029
+ * Uses ALL_VIEW_STACK which is built during scanView
1030
+ * Order: super views first (bottom), then included views, then page view (top)
1031
+ *
1032
+ * @param {number} renderTimes - Số lần render hiện tại
1033
+ * @returns {Promise<void>}
1034
+ */
1035
+ async mountAllViewsFromStack(renderTimes) {
1036
+ // logger.log(`🔄 View.mountAllViewsFromStack: Starting stack-based mounting`);
1037
+
1038
+ // Check if we have views in queue
1039
+ if (!this.VIEW_MOUNTED_QUEUE[renderTimes] || !this.VIEW_MOUNTED_QUEUE[renderTimes].length) {
1040
+ logger.warn(`⚠️ View.mountAllViewsFromStack: No views in queue`);
1041
+ return;
1042
+ }
1043
+
1044
+ // Wait for super view to be ready
1045
+ if (!this.CURRENT_SUPER_VIEW_MOUNTED) {
1046
+ logger.log(`⏳ View.mountAllViewsFromStack: Waiting for super view...`);
1047
+ await new Promise(resolve => {
1048
+ const checkInterval = setInterval(() => {
1049
+ if (this.CURRENT_SUPER_VIEW_MOUNTED) {
1050
+ clearInterval(checkInterval);
1051
+ resolve();
1052
+ }
1053
+ }, 50);
1054
+ });
1055
+ }
1056
+
1057
+ // If we have a stack (from scanView), use it for ordering
1058
+ if (this.ALL_VIEW_STACK && this.ALL_VIEW_STACK.length > 0) {
1059
+ // logger.log(`📚 View.mountAllViewsFromStack: Using ALL_VIEW_STACK (${this.ALL_VIEW_STACK.length} views)`);
1060
+
1061
+ // Mount super views first (they're at the beginning of stack)
1062
+ const superViews = this.SUPER_VIEW_STACK || [];
1063
+ for (let i = superViews.length - 1; i >= 0; i--) {
1064
+ const viewEngine = superViews[i];
1065
+ try {
1066
+ // logger.log(`🏛️ View.mountAllViewsFromStack: Mounting super view ${viewEngine.path}`);
1067
+ if (viewEngine.__ && typeof viewEngine.__._lifecycleManager.beforeMount === 'function') {
1068
+ viewEngine.__._lifecycleManager.beforeMount();
1069
+ }
1070
+ if (viewEngine.__ && typeof viewEngine.__._lifecycleManager.mounted === 'function') {
1071
+ viewEngine.__._lifecycleManager.mounted();
1072
+ }
1073
+ // logger.log(`✅ Mounted super view: ${viewEngine.path}`);
1074
+ } catch (error) {
1075
+ logger.error(`❌ Error mounting super view ${viewEngine.path}:`, error);
1076
+ }
1077
+ }
1078
+
1079
+ // Then mount page view and its includes (from ALL_VIEW_STACK)
1080
+ for (let i = this.ALL_VIEW_STACK.length - 1; i >= 0; i--) {
1081
+ const viewEngine = this.ALL_VIEW_STACK[i];
1082
+
1083
+ // Skip if already mounted as super view
1084
+ if (superViews.includes(viewEngine)) {
1085
+ continue;
1086
+ }
1087
+
1088
+ try {
1089
+ // logger.log(`📄 View.mountAllViewsFromStack: Mounting view ${viewEngine.path}`);
1090
+ if (viewEngine.__ && typeof viewEngine.__._lifecycleManager.beforeMount === 'function') {
1091
+ viewEngine.__._lifecycleManager.beforeMount();
1092
+ }
1093
+ if (viewEngine.__ && typeof viewEngine.__._lifecycleManager.mounted === 'function') {
1094
+ viewEngine.__._lifecycleManager.mounted();
1095
+ }
1096
+ // logger.log(`✅ Mounted view: ${viewEngine.path}`);
1097
+ } catch (error) {
1098
+ logger.error(`❌ Error mounting view ${viewEngine.path}:`, error);
1099
+ }
1100
+ }
1101
+
1102
+ // Clear stacks
1103
+ this.ALL_VIEW_STACK = [];
1104
+ this.SUPER_VIEW_STACK = [];
1105
+ // logger.log(`✅ View.mountAllViewsFromStack: Stack-based mounting complete`);
1106
+ } else {
1107
+ // Fallback to bottom-up queue mounting
1108
+ // logger.log(`⚠️ View.mountAllViewsFromStack: No stack available, falling back to queue mounting`);
1109
+ await this.mountAllViewsBottomUp(renderTimes);
1110
+ }
1111
+
1112
+ // Clear the queue
1113
+ this.VIEW_MOUNTED_QUEUE[renderTimes] = [];
1114
+ }
1115
+
1116
+ // ============================================================================
1117
+ // VIEW FUNCTIONS
1118
+ // ============================================================================
1119
+
1120
+ templateToDom(template) {
1121
+ /**
1122
+ * @type {HTMLTemplateElement}
1123
+ */
1124
+ const templator = document.createElement('template');
1125
+ templator.innerHTML = template;
1126
+ return templator.content.firstChild;
1127
+ }
1128
+
1129
+
1130
+
1131
+ /**
1132
+ * Include a view
1133
+ * @param {string} viewName - Name of the view to include
1134
+ * @param {Object} data - Data to pass to the view
1135
+ * @returns {string} Rendered view content
1136
+ */
1137
+ include(viewName, data = {}) {
1138
+ try {
1139
+ const view = this.view(viewName, { ...data }, false);
1140
+ return view;
1141
+ } catch (error) {
1142
+ console.error(`App.View.include error for '${viewName}':`, error);
1143
+ return '';
1144
+ }
1145
+ }
1146
+
1147
+ /**
1148
+ * Include a view if it exists
1149
+ * @param {string} viewName - Name of the view to include
1150
+ * @param {Object} data - Data to pass to the view
1151
+ * @returns {string} Rendered view content or empty string
1152
+ */
1153
+ includeIf(viewName, data = {}) {
1154
+ try {
1155
+ return this.exists(viewName) ? this.include(viewName, data) : null;
1156
+ } catch (error) {
1157
+ console.error(`App.View.includeIf error for '${viewName}':`, error);
1158
+ return null;
1159
+ }
1160
+ }
1161
+
1162
+ /**
1163
+ * Extend a view (for @extends directive)
1164
+ * @param {string} superViewName - Name of the parent view
1165
+ * @param {Object} data - Data to pass to the parent view
1166
+ * @returns {App.View.Engine} View Engine
1167
+ */
1168
+ extendView(superViewName, data = {}) {
1169
+ try {
1170
+ const view = this.view(superViewName, hasData(data) ? data : null, true);
1171
+ if (!view) {
1172
+ // console.warn(`App.View.extendView: Parent view '${superViewName}' not found`);
1173
+ return null;
1174
+ }
1175
+ this.cachedViews[superViewName] = view;
1176
+ return this.cachedViews[superViewName];
1177
+
1178
+ } catch (error) {
1179
+ // console.error(`App.View.extendView error for '${superViewName}':`, error);
1180
+ return null;
1181
+ }
1182
+ }
1183
+
1184
+
1185
+ registerSubscribe(attr, yieldKey, defaultContent = '') {
1186
+ let output = '';
1187
+ const subscribeAttr = [];
1188
+ if (typeof attr === 'string') {
1189
+ output += ` ${attr}="${this.yieldContent(yieldKey, defaultContent)}"`;
1190
+ subscribeAttr.push(`${attr}:${yieldKey}`);
1191
+ }
1192
+ else if (typeof attr === 'object') {
1193
+ const keys = Object.keys(attr);
1194
+ for (const key of keys) {
1195
+ if (key.toLowerCase() === '#content') {
1196
+ output += ` ${ATTR.KEYS.YIELD_CONTENT}="${attr[key]}"`;
1197
+ }
1198
+ else if (key.toLowerCase() === '#children') {
1199
+ output += ` ${ATTR.KEYS.YIELD_CHILDREN}="${attr[key]}"`;
1200
+ }
1201
+ else {
1202
+ output += ` ${key}="${this.yieldContent(attr[key], defaultContent)}"`;
1203
+ subscribeAttr.push(`${key}:${attr[key]}`);
1204
+ }
1205
+ }
1206
+ }
1207
+ output += ` ${ATTR.KEYS.YIELD_ATTR}="${subscribeAttr.join(',')}"`;
1208
+ return output;
1209
+ }
1210
+
1211
+ exists(viewName) {
1212
+ return this.templates && this.templates[viewName] ? true : false;
1213
+ }
1214
+
1215
+ // ========================================================================
1216
+ // RENDER/SCAN METHODS - OPTIMIZED
1217
+ // ========================================================================
1218
+
1219
+ /**
1220
+ * Unified view rendering/scanning method
1221
+ *
1222
+ * @param {View} view - View engine instance
1223
+ * @param {Object} variableData - Variable data to pass to the view
1224
+ * @param {string} mode - Render mode: 'csr' (render) or 'ssr' (virtualRender)
1225
+ * @returns {string|View} Rendered content or ViewEngine for extends
1226
+ *
1227
+ * @description
1228
+ * Handles both CSR (Client-Side Rendering) and SSR (Server-Side Scanning):
1229
+ * - CSR mode: Calls render(), prerender() for actual HTML generation
1230
+ * - SSR mode: Calls virtualRender(), virtualPrerender() for relationship setup
1231
+ *
1232
+ * Async Data Handling:
1233
+ * - @await('client'): Loads data from current URL via getURIDAta()
1234
+ * - @fetch(url, data, headers): Loads data using custom fetch config
1235
+ *
1236
+ * Note: virtualRender/virtualPrerender do NOT generate HTML!
1237
+ * They only setup view hierarchy, sections, state subscriptions, and prepare for hydration.
1238
+ */
1239
+ renderOrScanView(view, variableData = null, mode = 'csr') {
1240
+ let result = null;
1241
+ const renderTimes = this.renderTimes;
1242
+
1243
+
1244
+ // Determine render methods based on mode
1245
+ // CSR: render() generates HTML
1246
+ // SSR: virtualRender() only sets up relationships (NO HTML)
1247
+ const renderMethod = mode === 'ssr' ? 'virtualRender' : 'render';
1248
+ const prerenderMethod = mode === 'ssr' ? 'virtualPrerender' : 'prerender';
1249
+
1250
+ // ====================================================================
1251
+ // CASE 1: No async data - simple render/scan
1252
+ // ====================================================================
1253
+ if (!(view.__.hasAwaitData || view.__.hasFetchData)) {
1254
+ if (variableData) {
1255
+ view.__.updateVariableData({ ...variableData });
1256
+ }
1257
+ result = view.__[renderMethod]();
1258
+ // console.log('renderOrScanView', view.path);
1259
+ // console.log('renderMethod:', renderMethod);
1260
+ // console.log(result);
1261
+
1262
+ return result;
1263
+ }
1264
+
1265
+ // ====================================================================
1266
+ // CASE 2: Has async data but no prerender
1267
+ // ====================================================================
1268
+ const isPrerender = view.__.hasPrerender;
1269
+ if (!isPrerender) {
1270
+ if (variableData) {
1271
+ view.__.updateVariableData(variableData);
1272
+ }
1273
+ result = view.__[renderMethod]();
1274
+ }
1275
+
1276
+ // ====================================================================
1277
+ // CASE 3A: Has @await - Load data by current URL
1278
+ // ====================================================================
1279
+ if (view.__.hasAwaitData) {
1280
+ // First: Show prerender (loading state)
1281
+ if (isPrerender) {
1282
+ result = view.__[prerenderMethod]();
1283
+ }
1284
+
1285
+ // Handle based on mode
1286
+ if (mode === 'csr') {
1287
+ // CSR: Load data from current URL then re-render
1288
+ this.App.Api.getURIDAta().then(res => {
1289
+ if (!res || typeof res !== 'object' || !hasData(res.data)) {
1290
+ logger.warn('App.View.renderOrScanView: No data returned from getURIDAta for view', view.path);
1291
+ return;
1292
+ }
1293
+ view.__.refresh(res.data);
1294
+
1295
+ });
1296
+ } else {
1297
+ // SSR: Just setup relationships (no data loading needed)
1298
+ result = view.__[renderMethod]();
1299
+ // Note: Views are added to queue automatically in mountAllViewsFromStack
1300
+ }
1301
+ }
1302
+
1303
+ // ====================================================================
1304
+ // CASE 3B: Has @fetch - Load data by fetch config
1305
+ // ====================================================================
1306
+ else if (view.__.hasFetchData) {
1307
+ // First: Show prerender (loading state)
1308
+ if (isPrerender) {
1309
+ result = view.__[prerenderMethod]();
1310
+ }
1311
+
1312
+ // Handle based on mode
1313
+ if (mode === 'csr') {
1314
+ // CSR: Fetch data using config (url, method, data, headers)
1315
+ const fetchConfig = view.__.fetch || {};
1316
+ // TODO: Implement fetch logic with config
1317
+ // this.App.API.fetch(fetchConfig).then(data => {
1318
+ // view[renderMethod]();
1319
+ // if (addToQueue) {
1320
+ // this.callViewEngineMounted(renderTimes, view.id);
1321
+ // }
1322
+ // });
1323
+ const { url = '', method = 'GET', data = null, params = null, headers = {} } = fetchConfig;
1324
+ this.App.Http.request(method, url, String(method).toLowerCase() === 'get' ? params : data, {headers}).then(response => {
1325
+ if (!response || typeof response !== 'object' || !hasData(response.data)) {
1326
+ logger.warn('App.View.renderOrScanView: No data returned from fetch for view', view.path);
1327
+ return;
1328
+ }
1329
+ view.__.refresh(response.data);
1330
+ });
1331
+
1332
+ } else {
1333
+ // SSR: Just setup relationships (no data loading needed)
1334
+ result = view.__[renderMethod]();
1335
+ // Note: Views are added to queue automatically in mountAllViewsFromStack
1336
+ }
1337
+ }
1338
+
1339
+ return result;
1340
+ }
1341
+
1342
+ /**
1343
+ * Render a view (CSR - Client-Side Rendering)
1344
+ *
1345
+ * Generates actual HTML content by calling:
1346
+ * - view.render() - Returns HTML string
1347
+ * - view.prerender() - Returns loading state HTML
1348
+ *
1349
+ * @param {View} view - View engine
1350
+ * @param {Object} variableData - Variable data to pass to the view
1351
+ * @returns {string} Rendered HTML content
1352
+ */
1353
+ renderView(view, variableData = null, isScan = false) {
1354
+ // Handle null/undefined views
1355
+ if (!view) {
1356
+ console.warn('renderView: view is null or undefined');
1357
+ return '';
1358
+ }
1359
+
1360
+ // Handle non-View instances
1361
+ if (!(view instanceof View)) {
1362
+ // If it's already a string, return it
1363
+ if (typeof view === 'string') {
1364
+ return view;
1365
+ }
1366
+ // Otherwise, return empty string
1367
+ console.warn('renderView: view is not a View instance', view);
1368
+ return '';
1369
+ }
1370
+
1371
+ return this.renderOrScanView(view, variableData, isScan ? 'ssr' : 'csr');
1372
+ }
1373
+
1374
+ /**
1375
+ * Scan rendered view (SSR - Server-Side Scanning)
1376
+ *
1377
+ * DOES NOT generate HTML - only sets up relationships by calling:
1378
+ * - view.virtualRender() - Setup hierarchy, sections, state (NO HTML)
1379
+ * - view.virtualPrerender() - Setup loading relationships (NO HTML)
1380
+ *
1381
+ * Purpose of virtual methods:
1382
+ * 1. Setup view hierarchy (extends/includes)
1383
+ * 2. Register sections with parent views
1384
+ * 3. Setup state subscriptions for reactive blocks
1385
+ * 4. Prepare view instances for DOM hydration
1386
+ * 5. Map server data to client view structure
1387
+ *
1388
+ * @param {View} view - View engine
1389
+ * @param {Object} variableData - Variable data to pass to the view
1390
+ * @returns {View} View instance for extends chain
1391
+ */
1392
+ scanRenderedView(view, variableData = null) {
1393
+ return view instanceof View ? this.renderOrScanView(view, variableData, 'ssr') : view;
1394
+ }
1395
+
1396
+
1397
+ /**
1398
+ * Render a view by name
1399
+ * @param {string} name - View name
1400
+ * @param {Object} data - Data to pass to view
1401
+ * @param {string} scope - View scope
1402
+ * @returns {ViewEngine|null} Rendered view content
1403
+ */
1404
+ view(name, data = null, cache = false) {
1405
+ try {
1406
+ // check if view is cached
1407
+ if (cache && this.cachedViews[name]) {
1408
+ const view = this.cachedViews[name];
1409
+ view.reset();
1410
+ if (data) {
1411
+ view.__.updateVariableData({ ...data });
1412
+ }
1413
+ return view;
1414
+ }
1415
+ // check if view is valid
1416
+ if (!this.templates[name]) {
1417
+ console.warn(`App.View.view: View '${name}' not found in context`);
1418
+ return null;
1419
+ }
1420
+ // check if view is valid
1421
+ if (typeof this.templates[name] !== 'function') {
1422
+ console.warn(`App.View.view: View '${name}' render function is not valid`);
1423
+ return null;
1424
+ }
1425
+
1426
+ // get view wrapper
1427
+ const viewWrapper = this.templates[name];
1428
+ // create view
1429
+ const view = viewWrapper(data ? { ...data } : {}, { App: this.App, View: this, ...this.systemData });
1430
+ // view.updateVariableData(data);
1431
+ // check if view is valid
1432
+ if (!view) {
1433
+ console.error(`App.View.view: View config not found for '${name}' in scope '${scope}'`);
1434
+ return null;
1435
+ }
1436
+
1437
+ view.__.setApp(this.App);
1438
+ // cache view
1439
+ this.cachedViews[name] = view;
1440
+ return view;
1441
+
1442
+ } catch (error) {
1443
+ console.error(`App.View.view: Critical error loading view '${name}':`, error);
1444
+ return null;
1445
+ }
1446
+ };
1447
+
1448
+
1449
+ /**
1450
+ * Clear old rendering data and cleanup memory
1451
+ * Called before each new render to prevent memory leaks
1452
+ *
1453
+ * Cleanup tasks:
1454
+ * 1. Remove old views from previous render cycles
1455
+ * 2. Clear event listeners from unmounted views
1456
+ * 3. Cleanup old render queues (keep only last 3 cycles)
1457
+ * 4. Trim view cache if too large (LRU eviction)
1458
+ */
1459
+ clearOldRendering() {
1460
+ const currentRenderTime = this.renderTimes;
1461
+
1462
+ // ================================================================
1463
+ // 1. Cleanup old render queues (keep only last 3 cycles)
1464
+ // ================================================================
1465
+ if (currentRenderTime > 3) {
1466
+ const oldRenderTime = currentRenderTime - 3;
1467
+ if (this.VIEW_MOUNTED_QUEUE[oldRenderTime]) {
1468
+ // logger.log(`🗑️ View.clearOldRendering: Cleaning queue for renderTime=${oldRenderTime}`);
1469
+
1470
+ // Unmount views from old queue
1471
+ const oldViews = this.VIEW_MOUNTED_QUEUE[oldRenderTime];
1472
+ if (Array.isArray(oldViews)) {
1473
+ oldViews.forEach(view => {
1474
+ if (view && typeof view === 'object') {
1475
+ this.unmountView(view);
1476
+ }
1477
+ });
1478
+ }
1479
+
1480
+ // Delete old queue
1481
+ delete this.VIEW_MOUNTED_QUEUE[oldRenderTime];
1482
+ }
1483
+ }
1484
+
1485
+ // ================================================================
1486
+ // 2. Trim view cache if too large (LRU: max 50 views)
1487
+ // ================================================================
1488
+ const MAX_CACHED_VIEWS = 50;
1489
+ const cachedKeys = Object.keys(this.cachedViews);
1490
+ if (cachedKeys.length > MAX_CACHED_VIEWS) {
1491
+ // logger.log(`🗑️ View.clearOldRendering: Cache too large (${cachedKeys.length}), trimming to ${MAX_CACHED_VIEWS}`);
1492
+
1493
+ // Remove oldest views (simple strategy: remove first N)
1494
+ const toRemove = cachedKeys.slice(0, cachedKeys.length - MAX_CACHED_VIEWS);
1495
+ toRemove.forEach(key => {
1496
+ const view = this.cachedViews[key];
1497
+ if (view) {
1498
+ this.unmountView(view);
1499
+ }
1500
+ delete this.cachedViews[key];
1501
+ });
1502
+ }
1503
+ this.ALL_VIEW_STACK = [];
1504
+ this.SUPER_VIEW_STACK = [];
1505
+ this.PAGE_VIEW = null;
1506
+
1507
+ // ================================================================
1508
+ // 3. Clear orphaned event data to prevent memory leaks
1509
+ // ================================================================
1510
+ if (this.CURRENT_VIEW && this.CURRENT_VIEW.__) {
1511
+ this.CURRENT_VIEW.__.clearOrphanedEventData();
1512
+ }
1513
+ }
1514
+
1515
+
1516
+
1517
+ /**
1518
+ * Unmount a view and cleanup its resources
1519
+ *
1520
+ * @param {ViewEngine} view - View to unmount
1521
+ * @returns {boolean} Success status
1522
+ *
1523
+ * @description
1524
+ * Properly cleanup a view by:
1525
+ * 1. Call beforeUnmount() lifecycle hook
1526
+ * 2. Remove event listeners via removeEvents()
1527
+ * 3. Remove from viewMap
1528
+ * 4. Call unmounted() lifecycle hook
1529
+ * 5. Call destroy() if defined
1530
+ */
1531
+ unmountView(view) {
1532
+ if (!view || typeof view !== 'object') {
1533
+ logger.warn('⚠️ View.unmountView: Invalid view object');
1534
+ return false;
1535
+ }
1536
+
1537
+ try {
1538
+ logger.log(`🗑️ View.unmountView: Unmounting view ${view.id} (${view.path})`);
1539
+
1540
+ // Step 1: Call beforeUnmount lifecycle
1541
+ if (view.__ && typeof view.__._lifecycleManager.beforeUnmount === 'function') {
1542
+ view.__._lifecycleManager.beforeUnmount();
1543
+ }
1544
+
1545
+ // Step 2: Remove event listeners
1546
+ if (typeof view.__._lifecycleManager.removeEvents === 'function') {
1547
+ view.__._lifecycleManager.removeEvents();
1548
+ }
1549
+
1550
+ // Step 3: Remove from viewMap
1551
+ if (this.viewMap.has(view.id)) {
1552
+ this.viewMap.delete(view.id);
1553
+ }
1554
+
1555
+ // Step 4: Call unmounted lifecycle
1556
+ if (view.__ && typeof view.__._lifecycleManager.unmounted === 'function') {
1557
+ view.__._lifecycleManager.unmounted();
1558
+ }
1559
+
1560
+ // Step 5: Call destroy if defined
1561
+ if (view.__ && typeof view.__._lifecycleManager.destroy === 'function') {
1562
+ view.__._lifecycleManager.destroy();
1563
+ }
1564
+
1565
+ logger.log(`✅ View.unmountView: View ${view.id} unmounted successfully`);
1566
+ return true;
1567
+
1568
+ } catch (error) {
1569
+ logger.error(`❌ View.unmountView: Error unmounting view ${view.id}:`, error);
1570
+ return false;
1571
+ }
1572
+ }
1573
+
1574
+ /**
1575
+ * Reset view state
1576
+ * Clears view stacks but does NOT unmount views
1577
+ * Use clearOldRendering() for proper cleanup
1578
+ */
1579
+ resetView() {
1580
+ logger.log('🔄 View.resetView: Resetting view stacks');
1581
+ this.SUPER_VIEW_STACK = [];
1582
+ this.ALL_VIEW_STACK = [];
1583
+ this.PAGE_VIEW = null;
1584
+
1585
+ }
1586
+
1587
+ /**
1588
+ * Clear orphaned event data from all views
1589
+ * Call this when DOM changes significantly to prevent memory leaks
1590
+ */
1591
+ clearOrphanedEventData() {
1592
+ // logger.log('🗑️ View.clearOrphanedEventData: Cleaning up orphaned event handlers');
1593
+
1594
+ // Clear from all cached views
1595
+ Object.values(this.cachedViews).forEach(view => {
1596
+ if (view && typeof view.clearOrphanedEventData === 'function') {
1597
+ if (view.__) {
1598
+ view.__.clearOrphanedEventData();
1599
+ }
1600
+ }
1601
+ });
1602
+
1603
+ // Clear from current views in stacks
1604
+ [...this.ALL_VIEW_STACK, ...this.SUPER_VIEW_STACK].forEach(view => {
1605
+ if (view && typeof view.clearOrphanedEventData === 'function') {
1606
+ if (view.__) {
1607
+ view.__.clearOrphanedEventData();
1608
+ }
1609
+ }
1610
+ });
1611
+
1612
+ // Clear from current view
1613
+ if (this.CURRENT_VIEW && typeof this.CURRENT_VIEW.clearOrphanedEventData === 'function') {
1614
+ if (this.CURRENT_VIEW && this.CURRENT_VIEW.__) {
1615
+ this.CURRENT_VIEW.__.clearOrphanedEventData();
1616
+ }
1617
+ }
1618
+
1619
+ if (this.CURRENT_SUPER_VIEW && typeof this.CURRENT_SUPER_VIEW.clearOrphanedEventData === 'function') {
1620
+ if (this.CURRENT_SUPER_VIEW && this.CURRENT_SUPER_VIEW.__) {
1621
+ this.CURRENT_SUPER_VIEW.__.clearOrphanedEventData();
1622
+ }
1623
+ }
1624
+ }
1625
+
1626
+
1627
+ // ============================================================================
1628
+ // SECTION FUNCTIONS
1629
+ // ============================================================================
1630
+
1631
+
1632
+ /**
1633
+ * Define a section
1634
+ * @param {string} name - Section name
1635
+ * @param {string} content - Section content
1636
+ * @param {string} type - Section type
1637
+ */
1638
+ section(name, content, type = 'string') {
1639
+ let oldContent = this._sections[name];
1640
+ this._sections[name] = content;
1641
+ if (oldContent !== content) {
1642
+ this._changedSections.push(name);
1643
+ }
1644
+ };
1645
+
1646
+ /**
1647
+ * Yield a section content
1648
+ * @param {string} name - Section name
1649
+ * @param {string} defaultContent - Default content if section not found
1650
+ * @returns {string} Section content
1651
+ */
1652
+ yield(name, defaultContent = '') {
1653
+ return this._sections[name] || defaultContent;
1654
+ }
1655
+ yieldContent(name, defaultContent = '') {
1656
+ return this._sections[name] || defaultContent;
1657
+ }
1658
+
1659
+
1660
+ /**
1661
+ * Render all sections as HTML
1662
+ * @returns {string} Rendered sections HTML
1663
+ */
1664
+ renderSections() {
1665
+ let html = '';
1666
+ for (const [name, content] of Object.entries(this._sections)) {
1667
+ html += content;
1668
+ }
1669
+ return html;
1670
+ }
1671
+
1672
+ /**
1673
+ * Check if section exists
1674
+ * @param {string} name - Section name
1675
+ * @returns {boolean} True if section exists
1676
+ */
1677
+ hasSection(name) {
1678
+ return name in this._sections;
1679
+ }
1680
+
1681
+ /**
1682
+ * Get changed sections
1683
+ * @returns {string[]} Changed sections
1684
+ */
1685
+ getChangedSections() {
1686
+ return this._changedSections;
1687
+ };
1688
+
1689
+ /**
1690
+ * Reset changed sections list
1691
+ */
1692
+ resetChangedSections() {
1693
+ this._changedSections = [];
1694
+ };
1695
+
1696
+ /**
1697
+ * Check if section has changed
1698
+ * @param {string} name - Section name
1699
+ * @returns {boolean} True if section has changed
1700
+ */
1701
+ isChangedSection(name) {
1702
+ return this._changedSections.includes(name);
1703
+ };
1704
+
1705
+
1706
+ /**
1707
+ * Emit changed sections to subscribed elements
1708
+ */
1709
+ emitChangedSections() {
1710
+ this._changedSections.forEach(name => {
1711
+ if (this.SEO.updateItem(name, this._sections[name])) {
1712
+ return;
1713
+ }
1714
+ // Escape special characters in CSS selector
1715
+ const escapedName = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1716
+
1717
+ // Find elements subscribed to this section
1718
+ const subscribedElements = document.querySelectorAll(`[data-yield-subscribe-key*="${escapedName}"]`);
1719
+
1720
+ subscribedElements.forEach(element => {
1721
+ const subscribeKey = element.getAttribute('data-yield-subscribe-key');
1722
+ const subscribeTarget = element.getAttribute('data-yield-subscribe-target');
1723
+ const subscribeAttr = element.getAttribute('data-yield-subscribe-attr');
1724
+
1725
+ if (subscribeKey && subscribeKey.includes(name)) {
1726
+ const sectionContent = this._sections[name] || '';
1727
+
1728
+ if (subscribeTarget === 'content' || subscribeTarget === 'children') {
1729
+ element.innerHTML = sectionContent;
1730
+ } else if (subscribeTarget === 'attr' || subscribeTarget === 'attribute') {
1731
+ if (subscribeAttr) {
1732
+ element.setAttribute(subscribeAttr, sectionContent);
1733
+ }
1734
+ } else {
1735
+ element.innerHTML = sectionContent;
1736
+ }
1737
+ }
1738
+ });
1739
+
1740
+ // Find elements with spa-yield-attr containing section name
1741
+ const attrElements = document.querySelectorAll('[data-yield-attr]');
1742
+ attrElements.forEach(element => {
1743
+ const yieldAttr = element.getAttribute('data-yield-attr');
1744
+
1745
+ if (yieldAttr && yieldAttr.includes(name)) {
1746
+ const attrPairs = yieldAttr.split(',');
1747
+
1748
+ attrPairs.forEach(pair => {
1749
+ let paths = pair.split(':');
1750
+ let attrName = paths.shift();
1751
+ let sectionName = paths.join(':');
1752
+
1753
+ if (sectionName && sectionName.trim() === name) {
1754
+ const sectionContent = this._sections[name] || '';
1755
+ element.setAttribute(attrName.trim(), sectionContent);
1756
+ }
1757
+ });
1758
+ }
1759
+ });
1760
+
1761
+ let nameParts = name.split('.');
1762
+ let type = nameParts.shift();
1763
+ let key = nameParts.join('.');
1764
+
1765
+ if (type === 'block') {
1766
+ let blocks = OneMarkup.find('subscribe', { type: 'block', key: key });
1767
+ if (blocks && blocks.length > 0) {
1768
+ blocks.forEach(block => {
1769
+ block.replaceContent(this._sections[name]);
1770
+ });
1771
+ }
1772
+ }
1773
+
1774
+ // Find elements with app-yield-{name} attributes
1775
+ if (name.split(':').length <= 1) {
1776
+ const specialElements = document.querySelectorAll(`[data-yield-${escapedName}]`);
1777
+ specialElements.forEach(element => {
1778
+ const sectionContent = this._sections[name] || '';
1779
+ element.innerHTML = sectionContent;
1780
+ });
1781
+
1782
+ }
1783
+
1784
+ const contentElements = document.querySelectorAll(`[data-yield-content="${escapedName}"]`);
1785
+ contentElements.forEach(element => {
1786
+ element.innerHTML = this._sections[name] || '';
1787
+ });
1788
+ const childrenElements = document.querySelectorAll(`[data-yield-children="${escapedName}"]`);
1789
+ childrenElements.forEach(element => {
1790
+ element.innerHTML = this._sections[name] || '';
1791
+ });
1792
+ });
1793
+
1794
+ // Reset changed sections after processing
1795
+ this.resetChangedSections();
1796
+ };
1797
+
1798
+
1799
+ loadServerData(data = {}) {
1800
+
1801
+ }
1802
+
1803
+
1804
+
1805
+ // ============================================================================
1806
+ // STACK FUNCTIONS
1807
+ // ============================================================================
1808
+
1809
+ // Initialize stacks storage
1810
+
1811
+
1812
+ /**
1813
+ * Push content to a stack
1814
+ * @param {string} name - Stack name
1815
+ * @param {string} content - Content to push
1816
+ */
1817
+ push(name, content) {
1818
+ if (!this._stacks[name]) {
1819
+ this._stacks[name] = [];
1820
+ }
1821
+ this._stacks[name].push(content);
1822
+ };
1823
+
1824
+ /**
1825
+ * Get stack content
1826
+ * @param {string} name - Stack name
1827
+ * @returns {string} Stack content
1828
+ */
1829
+ stack(name) {
1830
+ return this._stacks[name] ? this._stacks[name].join('') : '';
1831
+ };
1832
+
1833
+ // ============================================================================
1834
+ // ONCE FUNCTIONS
1835
+ // ============================================================================
1836
+
1837
+ // Initialize once storage
1838
+
1839
+ /**
1840
+ * Execute content only once
1841
+ * @param {string} content - Content to execute once
1842
+ * @returns {string} Content if not executed before, empty string otherwise
1843
+ */
1844
+ once(content) {
1845
+ const hash = this._hashContent(content);
1846
+ if (!this._once[hash]) {
1847
+ this._once[hash] = true;
1848
+ return content;
1849
+ }
1850
+ return '';
1851
+ };
1852
+
1853
+ /**
1854
+ * Hash content for once tracking
1855
+ * @param {string} content - Content to hash
1856
+ * @returns {string} Hash string
1857
+ */
1858
+ _hashContent(content) {
1859
+ let hash = 0;
1860
+ if (content.length === 0) return hash.toString();
1861
+
1862
+ for (let i = 0; i < content.length; i++) {
1863
+ const char = content.charCodeAt(i);
1864
+ hash = ((hash << 5) - hash) + char;
1865
+ hash = hash & hash; // Convert to 32-bit integer
1866
+ }
1867
+
1868
+ return hash.toString();
1869
+ };
1870
+
1871
+ // ============================================================================
1872
+ // AUTH FUNCTIONS
1873
+ // ============================================================================
1874
+
1875
+ /**
1876
+ * Check if user is authenticated
1877
+ * @param {string} guard - Guard name (optional)
1878
+ * @returns {boolean} True if authenticated
1879
+ */
1880
+ isAuth(guard = null) {
1881
+ // This should be implemented based on your auth system
1882
+ // For now, return false as placeholder
1883
+ return false;
1884
+ };
1885
+
1886
+ /**
1887
+ * Check if user has permission
1888
+ * @param {string} permission - Permission name
1889
+ * @param {*} model - Model instance (optional)
1890
+ * @returns {boolean} True if user has permission
1891
+ */
1892
+ can(permission, model = null) {
1893
+ // This should be implemented based on your permission system
1894
+ // For now, return false as placeholder
1895
+ return false;
1896
+ };
1897
+
1898
+ // ============================================================================
1899
+ // ERROR FUNCTIONS
1900
+ // ============================================================================
1901
+
1902
+ /**
1903
+ * Check if field has error
1904
+ * @param {string} field - Field name
1905
+ * @returns {boolean} True if field has error
1906
+ */
1907
+ hasError(field) {
1908
+ // This should be implemented based on your error handling system
1909
+ // For now, return false as placeholder
1910
+ return false;
1911
+ };
1912
+
1913
+ /**
1914
+ * Get first error for field
1915
+ * @param {string} field - Field name
1916
+ * @returns {string} Error message
1917
+ */
1918
+ firstError(field) {
1919
+ // This should be implemented based on your error handling system
1920
+ // For now, return empty string as placeholder
1921
+ return '';
1922
+ };
1923
+
1924
+ // ============================================================================
1925
+ // CSRF FUNCTIONS
1926
+ // ============================================================================
1927
+
1928
+ /**
1929
+ * Get CSRF token
1930
+ * @returns {string} CSRF token
1931
+ */
1932
+ csrfToken() {
1933
+ // This should be implemented based on your CSRF system
1934
+ // For now, return empty string as placeholder
1935
+ return '';
1936
+ };
1937
+
1938
+ // ============================================================================
1939
+ // LOOP FUNCTIONS
1940
+ // ============================================================================
1941
+
1942
+
1943
+
1944
+ /**
1945
+ * Get route URL
1946
+ * @param {string} name - Route name
1947
+ * @param {object} params - Route parameters
1948
+ * @returns {string} Route URL
1949
+ */
1950
+ route(name, params = {}) {
1951
+ return this.App.Router.getURL(name, params);
1952
+ }
1953
+
1954
+
1955
+ // ============================================================================
1956
+ // MISC FUNCTIONS
1957
+ // ============================================================================
1958
+ scrollToTop(behavior = 'auto') {
1959
+ window.scrollTo({ top: 0, behavior: behavior == 'smooth' ? 'smooth' : 'auto' });
1960
+ }
1961
+
1962
+ }