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.
- package/README.md +87 -0
- package/docs/integration_analysis.md +116 -0
- package/docs/onejs_analysis.md +108 -0
- package/docs/optimization_implementation_group2.md +458 -0
- package/docs/optimization_plan.md +130 -0
- package/index.js +16 -0
- package/package.json +13 -0
- package/src/app.js +61 -0
- package/src/core/API.js +72 -0
- package/src/core/ChildrenRegistry.js +410 -0
- package/src/core/DOMBatcher.js +207 -0
- package/src/core/ErrorBoundary.js +226 -0
- package/src/core/EventDelegator.js +416 -0
- package/src/core/Helper.js +817 -0
- package/src/core/LoopContext.js +97 -0
- package/src/core/OneDOM.js +246 -0
- package/src/core/OneMarkup.js +444 -0
- package/src/core/Router.js +996 -0
- package/src/core/SEOConfig.js +321 -0
- package/src/core/SectionEngine.js +75 -0
- package/src/core/TemplateEngine.js +83 -0
- package/src/core/View.js +273 -0
- package/src/core/ViewConfig.js +229 -0
- package/src/core/ViewController.js +1410 -0
- package/src/core/ViewControllerOptimized.js +164 -0
- package/src/core/ViewIdentifier.js +361 -0
- package/src/core/ViewLoader.js +272 -0
- package/src/core/ViewManager.js +1962 -0
- package/src/core/ViewState.js +761 -0
- package/src/core/ViewSystem.js +301 -0
- package/src/core/ViewTemplate.js +4 -0
- package/src/core/helpers/BindingHelper.js +239 -0
- package/src/core/helpers/ConfigHelper.js +37 -0
- package/src/core/helpers/EventHelper.js +172 -0
- package/src/core/helpers/LifecycleHelper.js +17 -0
- package/src/core/helpers/ReactiveHelper.js +169 -0
- package/src/core/helpers/RenderHelper.js +15 -0
- package/src/core/helpers/ResourceHelper.js +89 -0
- package/src/core/helpers/TemplateHelper.js +11 -0
- package/src/core/managers/BindingManager.js +671 -0
- package/src/core/managers/ConfigurationManager.js +136 -0
- package/src/core/managers/EventManager.js +309 -0
- package/src/core/managers/LifecycleManager.js +356 -0
- package/src/core/managers/ReactiveManager.js +334 -0
- package/src/core/managers/RenderEngine.js +292 -0
- package/src/core/managers/ResourceManager.js +441 -0
- package/src/core/managers/ViewHierarchyManager.js +258 -0
- package/src/core/managers/ViewTemplateManager.js +127 -0
- package/src/core/reactive/ReactiveComponent.js +592 -0
- package/src/core/services/EventService.js +418 -0
- package/src/core/services/HttpService.js +106 -0
- package/src/core/services/LoggerService.js +57 -0
- package/src/core/services/StateService.js +512 -0
- package/src/core/services/StorageService.js +856 -0
- package/src/core/services/StoreService.js +258 -0
- package/src/core/services/TemplateDetectorService.js +361 -0
- package/src/core/services/Test.js +18 -0
- package/src/helpers/devWarnings.js +205 -0
- package/src/helpers/performance.js +226 -0
- package/src/helpers/utils.js +287 -0
- package/src/init.js +343 -0
- package/src/plugins/auto-plugin.js +34 -0
- package/src/services/Test.js +18 -0
- package/src/types/index.js +193 -0
- package/src/utils/date-helper.js +51 -0
- package/src/utils/helpers.js +39 -0
- 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, '&')
|
|
916
|
+
.replace(/</g, '<')
|
|
917
|
+
.replace(/>/g, '>')
|
|
918
|
+
.replace(/"/g, '"')
|
|
919
|
+
.replace(/'/g, ''');
|
|
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
|
+
}
|