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,441 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ResourceManager - Manages view resources (styles and scripts)
|
|
3
|
+
* Handles insertion, removal, and reference counting
|
|
4
|
+
*
|
|
5
|
+
* Extracted from ViewController.js to improve maintainability
|
|
6
|
+
* @author GitHub Copilot
|
|
7
|
+
* @date 2025-12-29
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import logger from '../services/LoggerService.js';
|
|
11
|
+
|
|
12
|
+
export class ResourceManager {
|
|
13
|
+
/**
|
|
14
|
+
* @param {ViewController} controller - Parent controller instance
|
|
15
|
+
*/
|
|
16
|
+
constructor(controller) {
|
|
17
|
+
this.controller = controller;
|
|
18
|
+
this.path = controller.path;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
updateController(newController) {
|
|
22
|
+
this.controller = newController;
|
|
23
|
+
this.path = newController.path;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Insert all resources (styles + scripts)
|
|
28
|
+
*/
|
|
29
|
+
insertResources() {
|
|
30
|
+
this.insertStyles();
|
|
31
|
+
this.insertScripts();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Remove all resources
|
|
36
|
+
*/
|
|
37
|
+
removeResources() {
|
|
38
|
+
this.removeStyles();
|
|
39
|
+
this.removeScripts();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Insert styles into DOM with reference counting
|
|
44
|
+
* Extracted from ViewController.js line 428
|
|
45
|
+
*/
|
|
46
|
+
insertStyles() {
|
|
47
|
+
if (!this.controller.styles || this.controller.styles.length === 0) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
this.controller.styles.forEach(style => {
|
|
52
|
+
const resourceKey = this.controller.App.View.Engine.getResourceKey({
|
|
53
|
+
...style,
|
|
54
|
+
viewPath: this.path,
|
|
55
|
+
resourceType: 'style'
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// Check if already in registry
|
|
59
|
+
let registryEntry = this.controller.App.View.Engine.resourceRegistry.get(resourceKey);
|
|
60
|
+
|
|
61
|
+
if (registryEntry) {
|
|
62
|
+
// Resource already exists - just add this view path to registry
|
|
63
|
+
registryEntry.viewPaths.add(this.path);
|
|
64
|
+
registryEntry.referenceCount++;
|
|
65
|
+
this.controller.insertedResourceKeys.add(resourceKey);
|
|
66
|
+
return; // Don't insert again
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Create and insert new style element
|
|
70
|
+
let element;
|
|
71
|
+
|
|
72
|
+
if (style.type === 'href') {
|
|
73
|
+
// External stylesheet
|
|
74
|
+
element = document.createElement('link');
|
|
75
|
+
element.rel = 'stylesheet';
|
|
76
|
+
element.href = style.href;
|
|
77
|
+
|
|
78
|
+
// Add attributes
|
|
79
|
+
if (style.attributes) {
|
|
80
|
+
Object.entries(style.attributes).forEach(([key, value]) => {
|
|
81
|
+
if (value === true) {
|
|
82
|
+
element.setAttribute(key, '');
|
|
83
|
+
} else {
|
|
84
|
+
element.setAttribute(key, value);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (style.id) {
|
|
90
|
+
element.id = style.id;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
element.setAttribute('data-view-path', this.path);
|
|
94
|
+
element.setAttribute('data-resource-key', resourceKey);
|
|
95
|
+
|
|
96
|
+
document.head.appendChild(element);
|
|
97
|
+
|
|
98
|
+
} else if (style.type === 'code') {
|
|
99
|
+
// Inline CSS
|
|
100
|
+
element = document.createElement('style');
|
|
101
|
+
element.textContent = style.content;
|
|
102
|
+
|
|
103
|
+
if (style.id) {
|
|
104
|
+
element.id = style.id;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (style.attributes) {
|
|
108
|
+
Object.entries(style.attributes).forEach(([key, value]) => {
|
|
109
|
+
if (key !== 'id' && key !== 'type') {
|
|
110
|
+
if (value === true) {
|
|
111
|
+
element.setAttribute(key, '');
|
|
112
|
+
} else {
|
|
113
|
+
element.setAttribute(key, value);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
element.setAttribute('data-view-path', this.path);
|
|
120
|
+
element.setAttribute('data-resource-key', resourceKey);
|
|
121
|
+
|
|
122
|
+
document.head.appendChild(element);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Register in global registry
|
|
126
|
+
this.controller.App.View.Engine.resourceRegistry.set(resourceKey, {
|
|
127
|
+
element: element,
|
|
128
|
+
viewPaths: new Set([this.path]),
|
|
129
|
+
referenceCount: 1,
|
|
130
|
+
resourceType: 'style'
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
this.controller.insertedResourceKeys.add(resourceKey);
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Insert scripts into DOM with reference counting
|
|
139
|
+
* Extracted from ViewController.js line 523
|
|
140
|
+
*/
|
|
141
|
+
insertScripts() {
|
|
142
|
+
if (!this.controller.scripts || this.controller.scripts.length === 0) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
this.controller.scripts.forEach(script => {
|
|
147
|
+
const resourceKey = this.controller.App.View.Engine.getResourceKey({
|
|
148
|
+
...script,
|
|
149
|
+
viewPath: this.path,
|
|
150
|
+
resourceType: 'script'
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// Check if already in registry
|
|
154
|
+
let registryEntry = this.controller.App.View.Engine.resourceRegistry.get(resourceKey);
|
|
155
|
+
|
|
156
|
+
if (registryEntry) {
|
|
157
|
+
// Resource already exists - just add this view path to registry
|
|
158
|
+
registryEntry.viewPaths.add(this.path);
|
|
159
|
+
registryEntry.referenceCount++;
|
|
160
|
+
this.controller.insertedResourceKeys.add(resourceKey);
|
|
161
|
+
|
|
162
|
+
// If script already loaded, call onload callback (only for external scripts with element)
|
|
163
|
+
if (script.onload && registryEntry.element && registryEntry.element.readyState === 'complete') {
|
|
164
|
+
try {
|
|
165
|
+
script.onload();
|
|
166
|
+
} catch (error) {
|
|
167
|
+
logger.warn(`ResourceManager.insertScripts: Error calling onload for ${resourceKey}:`, error);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// For function wrappers, don't execute again (already executed)
|
|
172
|
+
if (script.function && registryEntry.executed) {
|
|
173
|
+
return; // Function already executed, just tracking reference
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return; // Don't insert again
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Create and insert new script element
|
|
180
|
+
let element;
|
|
181
|
+
|
|
182
|
+
if (script.type === 'src') {
|
|
183
|
+
// External script
|
|
184
|
+
element = document.createElement('script');
|
|
185
|
+
element.src = script.src;
|
|
186
|
+
element.type = script.attributes?.type || 'text/javascript';
|
|
187
|
+
|
|
188
|
+
// Add attributes
|
|
189
|
+
if (script.attributes) {
|
|
190
|
+
Object.entries(script.attributes).forEach(([key, value]) => {
|
|
191
|
+
if (key === 'async' || key === 'defer') {
|
|
192
|
+
element[key] = value === true || value === 'true';
|
|
193
|
+
} else if (value === true) {
|
|
194
|
+
element.setAttribute(key, '');
|
|
195
|
+
} else {
|
|
196
|
+
element.setAttribute(key, value);
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (script.id) {
|
|
202
|
+
element.id = script.id;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
element.setAttribute('data-view-path', this.path);
|
|
206
|
+
element.setAttribute('data-resource-key', resourceKey);
|
|
207
|
+
|
|
208
|
+
// Handle onload/onerror
|
|
209
|
+
if (script.onload) {
|
|
210
|
+
element.onload = script.onload;
|
|
211
|
+
}
|
|
212
|
+
if (script.onerror) {
|
|
213
|
+
element.onerror = script.onerror;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
document.body.appendChild(element);
|
|
217
|
+
|
|
218
|
+
} else if (script.type === 'code') {
|
|
219
|
+
// Inline script - Use registered script function if available
|
|
220
|
+
if (script.function) {
|
|
221
|
+
// Get script from ViewEngine.scripts registry
|
|
222
|
+
const scriptCallback = this.controller.App.View.Engine.getScript(this.path, script.function);
|
|
223
|
+
|
|
224
|
+
if (scriptCallback && typeof scriptCallback === 'function') {
|
|
225
|
+
// Function wrapper: Execute only once, track in registry
|
|
226
|
+
try {
|
|
227
|
+
scriptCallback.call(this.controller);
|
|
228
|
+
|
|
229
|
+
// Register in global registry with executed flag
|
|
230
|
+
this.controller.App.View.Engine.resourceRegistry.set(resourceKey, {
|
|
231
|
+
element: null, // No DOM element for function wrapper
|
|
232
|
+
viewPaths: new Set([this.path]),
|
|
233
|
+
referenceCount: 1,
|
|
234
|
+
resourceType: 'script',
|
|
235
|
+
executed: true, // Mark as executed - ensures single execution
|
|
236
|
+
functionName: script.function // Store function name for reference
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
this.controller.insertedResourceKeys.add(resourceKey);
|
|
240
|
+
} catch (error) {
|
|
241
|
+
logger.error(`ResourceManager.insertScripts: Error executing script function ${script.function}:`, error);
|
|
242
|
+
}
|
|
243
|
+
return; // Don't continue to element creation
|
|
244
|
+
} else {
|
|
245
|
+
logger.warn(`ResourceManager.insertScripts: Script function ${script.function} not found in registry for view ${this.path}`);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
} else if (script.content) {
|
|
249
|
+
// Fallback: Insert raw content (legacy support)
|
|
250
|
+
element = document.createElement('script');
|
|
251
|
+
element.textContent = script.content;
|
|
252
|
+
element.type = script.attributes?.type || 'text/javascript';
|
|
253
|
+
|
|
254
|
+
if (script.id) {
|
|
255
|
+
element.id = script.id;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (script.attributes) {
|
|
259
|
+
Object.entries(script.attributes).forEach(([key, value]) => {
|
|
260
|
+
if (key !== 'id' && key !== 'type') {
|
|
261
|
+
if (value === true) {
|
|
262
|
+
element.setAttribute(key, '');
|
|
263
|
+
} else {
|
|
264
|
+
element.setAttribute(key, value);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
element.setAttribute('data-view-path', this.path);
|
|
271
|
+
element.setAttribute('data-resource-key', resourceKey);
|
|
272
|
+
try {
|
|
273
|
+
document.body.appendChild(element);
|
|
274
|
+
} catch (error) {
|
|
275
|
+
logger.warn(`ResourceManager.insertScripts: Error appending script ${resourceKey}:`, error);
|
|
276
|
+
logger.log(element);
|
|
277
|
+
}
|
|
278
|
+
} else {
|
|
279
|
+
logger.warn(`ResourceManager.insertScripts: Script ${resourceKey} has no function or content`);
|
|
280
|
+
return; // Don't register if no content/function
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Register in global registry (for non-function scripts)
|
|
285
|
+
if (element) {
|
|
286
|
+
this.controller.App.View.Engine.resourceRegistry.set(resourceKey, {
|
|
287
|
+
element: element,
|
|
288
|
+
viewPaths: new Set([this.path]),
|
|
289
|
+
referenceCount: 1,
|
|
290
|
+
resourceType: 'script'
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
this.controller.insertedResourceKeys.add(resourceKey);
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Remove styles from registry with reference counting
|
|
300
|
+
* Extracted from ViewController.js line 683
|
|
301
|
+
*/
|
|
302
|
+
removeStyles() {
|
|
303
|
+
if (!this.controller.styles || this.controller.styles.length === 0) {
|
|
304
|
+
// Fallback: Remove all styles with this view path from DOM
|
|
305
|
+
this.removeStylesByViewPath();
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
this.controller.styles.forEach(style => {
|
|
310
|
+
const resourceKey = this.controller.App.View.Engine.getResourceKey({
|
|
311
|
+
...style,
|
|
312
|
+
viewPath: this.path,
|
|
313
|
+
resourceType: 'style'
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
const registryEntry = this.controller.App.View.Engine.resourceRegistry.get(resourceKey);
|
|
317
|
+
|
|
318
|
+
if (registryEntry) {
|
|
319
|
+
// Remove this view path from registry
|
|
320
|
+
registryEntry.viewPaths.delete(this.path);
|
|
321
|
+
registryEntry.referenceCount--;
|
|
322
|
+
|
|
323
|
+
// Only remove from DOM if no other views are using it
|
|
324
|
+
if (registryEntry.referenceCount <= 0) {
|
|
325
|
+
if (registryEntry.element && registryEntry.element.parentNode) {
|
|
326
|
+
registryEntry.element.remove();
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Remove from registry
|
|
330
|
+
this.controller.App.View.Engine.resourceRegistry.delete(resourceKey);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
this.controller.insertedResourceKeys.delete(resourceKey);
|
|
334
|
+
} else {
|
|
335
|
+
// Fallback: If not found in registry, try to remove from DOM directly
|
|
336
|
+
const elements = document.querySelectorAll(`[data-view-path="${this.path}"][data-resource-key="${resourceKey}"]`);
|
|
337
|
+
elements.forEach(element => {
|
|
338
|
+
if (element.parentNode) {
|
|
339
|
+
element.remove();
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
// Final fallback: Remove any remaining styles with this view path
|
|
346
|
+
this.removeStylesByViewPath();
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Remove all styles from DOM that belong to a view path
|
|
351
|
+
* Extracted from ViewController.js line 738
|
|
352
|
+
*/
|
|
353
|
+
removeStylesByViewPath(viewPath = null) {
|
|
354
|
+
const targetPath = viewPath || this.path;
|
|
355
|
+
if (!targetPath) {
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Find all style/link elements with this view path
|
|
360
|
+
const styleElements = document.querySelectorAll(`style[data-view-path="${targetPath}"], link[data-view-path="${targetPath}"]`);
|
|
361
|
+
|
|
362
|
+
styleElements.forEach(element => {
|
|
363
|
+
const resourceKey = element.getAttribute('data-resource-key');
|
|
364
|
+
|
|
365
|
+
if (resourceKey) {
|
|
366
|
+
// Check registry to see if other views are using this resource
|
|
367
|
+
const registryEntry = this.controller.App.View.Engine.resourceRegistry.get(resourceKey);
|
|
368
|
+
|
|
369
|
+
if (registryEntry) {
|
|
370
|
+
// Remove this view path from registry
|
|
371
|
+
registryEntry.viewPaths.delete(targetPath);
|
|
372
|
+
registryEntry.referenceCount--;
|
|
373
|
+
|
|
374
|
+
// Only remove from DOM if no other views are using it
|
|
375
|
+
if (registryEntry.referenceCount <= 0) {
|
|
376
|
+
if (element.parentNode) {
|
|
377
|
+
element.remove();
|
|
378
|
+
}
|
|
379
|
+
// Remove from registry
|
|
380
|
+
this.controller.App.View.Engine.resourceRegistry.delete(resourceKey);
|
|
381
|
+
}
|
|
382
|
+
} else {
|
|
383
|
+
// Not in registry, safe to remove directly
|
|
384
|
+
if (element.parentNode) {
|
|
385
|
+
element.remove();
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Remove from insertedResourceKeys (if this is the same instance)
|
|
390
|
+
if (this.path === targetPath) {
|
|
391
|
+
this.controller.insertedResourceKeys.delete(resourceKey);
|
|
392
|
+
}
|
|
393
|
+
} else {
|
|
394
|
+
// No resource key, but has view path - safe to remove
|
|
395
|
+
if (element.parentNode) {
|
|
396
|
+
element.remove();
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Remove scripts from registry with reference counting
|
|
404
|
+
* Extracted from ViewController.js line 790
|
|
405
|
+
*/
|
|
406
|
+
removeScripts() {
|
|
407
|
+
if (!this.controller.scripts || this.controller.scripts.length === 0) {
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
this.controller.scripts.forEach(script => {
|
|
412
|
+
const resourceKey = this.controller.App.View.Engine.getResourceKey({
|
|
413
|
+
...script,
|
|
414
|
+
viewPath: this.path,
|
|
415
|
+
resourceType: 'script'
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
const registryEntry = this.controller.App.View.Engine.resourceRegistry.get(resourceKey);
|
|
419
|
+
|
|
420
|
+
if (registryEntry) {
|
|
421
|
+
// Remove this view path from registry
|
|
422
|
+
registryEntry.viewPaths.delete(this.path);
|
|
423
|
+
registryEntry.referenceCount--;
|
|
424
|
+
|
|
425
|
+
// Only remove from DOM if no other views are using it
|
|
426
|
+
if (registryEntry.referenceCount <= 0) {
|
|
427
|
+
// For function wrappers, don't remove from DOM (no element)
|
|
428
|
+
// Just remove from registry
|
|
429
|
+
if (registryEntry.element && registryEntry.element.parentNode) {
|
|
430
|
+
registryEntry.element.remove();
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// Remove from registry
|
|
434
|
+
this.controller.App.View.Engine.resourceRegistry.delete(resourceKey);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
this.controller.insertedResourceKeys.delete(resourceKey);
|
|
438
|
+
}
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
}
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ViewHierarchyManager - Manages view relationships and hierarchy
|
|
3
|
+
*
|
|
4
|
+
* Responsibilities:
|
|
5
|
+
* - Manage parent-child relationships
|
|
6
|
+
* - Handle super view (layout inheritance)
|
|
7
|
+
* - Manage original view references
|
|
8
|
+
* - Create and configure child/super views
|
|
9
|
+
*
|
|
10
|
+
* Extracted from ViewController.js (Phase 9) to reduce complexity
|
|
11
|
+
* @author GitHub Copilot
|
|
12
|
+
* @date 2025-12-29
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export class ViewHierarchyManager {
|
|
16
|
+
/**
|
|
17
|
+
* @param {ViewController} controller - Parent controller instance
|
|
18
|
+
*/
|
|
19
|
+
constructor(controller) {
|
|
20
|
+
this.controller = controller;
|
|
21
|
+
this.scanChildrenIDs = [];
|
|
22
|
+
|
|
23
|
+
this.renewChildrenIDs = [];
|
|
24
|
+
|
|
25
|
+
this.rcChildrenIDs = [];
|
|
26
|
+
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Set super view (layout)
|
|
31
|
+
* @param {View} superView - Super view instance
|
|
32
|
+
* @returns {ViewController} Controller instance for chaining
|
|
33
|
+
*/
|
|
34
|
+
setSuperView(superView) {
|
|
35
|
+
this.controller.superView = superView.__;
|
|
36
|
+
return this.controller;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Set parent view
|
|
41
|
+
* @param {View} parent - Parent view instance
|
|
42
|
+
* @returns {ViewController} Controller instance for chaining
|
|
43
|
+
*/
|
|
44
|
+
setParent(parent) {
|
|
45
|
+
this.controller.parent = parent.__;
|
|
46
|
+
return this.controller;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Set original view (for extended views)
|
|
51
|
+
* @param {View} originalView - Original view instance
|
|
52
|
+
* @returns {ViewController} Controller instance for chaining
|
|
53
|
+
*/
|
|
54
|
+
setOriginalView(originalView) {
|
|
55
|
+
this.controller.originalView = originalView.__;
|
|
56
|
+
this.controller.originalViewPath = originalView.path;
|
|
57
|
+
this.controller.originalViewId = originalView.id;
|
|
58
|
+
return this.controller;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Add child view
|
|
63
|
+
* Creates scope object and registers child in ChildrenRegistry
|
|
64
|
+
*
|
|
65
|
+
* @param {View} child - Child view to add
|
|
66
|
+
* @param {Object} data - Data to add to the child view
|
|
67
|
+
* @returns {ViewController} Controller instance for chaining
|
|
68
|
+
*/
|
|
69
|
+
addChild(child, data = {}) {
|
|
70
|
+
// Get parent reactive component if in render context
|
|
71
|
+
const parentRC = this.controller._reactiveManager?.currentRenderingComponent;
|
|
72
|
+
|
|
73
|
+
// Register in ChildrenRegistry (handles controller storage and scope)
|
|
74
|
+
const registry = this.controller._childrenRegistry;
|
|
75
|
+
|
|
76
|
+
// Get correct index BEFORE register() pushes to array
|
|
77
|
+
// Add defensive check for children array
|
|
78
|
+
if (!this.controller.children) {
|
|
79
|
+
console.error('ViewHierarchyManager.addChild: this.controller.children is undefined/null', {
|
|
80
|
+
controller: this.controller,
|
|
81
|
+
child: child,
|
|
82
|
+
viewPath: this.controller?.path
|
|
83
|
+
});
|
|
84
|
+
this.controller.children = [];
|
|
85
|
+
}
|
|
86
|
+
const currentIndex = this.controller.children.length;
|
|
87
|
+
|
|
88
|
+
registry.register(child, {
|
|
89
|
+
data,
|
|
90
|
+
parentReactiveComponent: parentRC,
|
|
91
|
+
index: currentIndex // ✅ Fixed: calculate index before push
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
return this.controller;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Remove child view
|
|
99
|
+
* Removes from registry and children array
|
|
100
|
+
*
|
|
101
|
+
* @param {View} child - Child view to remove
|
|
102
|
+
* @returns {ViewController} Controller instance for chaining
|
|
103
|
+
*/
|
|
104
|
+
removeChild(child) {
|
|
105
|
+
const childController = child.__;
|
|
106
|
+
const childId = child.id;
|
|
107
|
+
|
|
108
|
+
// Remove from registry (handles cleanup of registry, mounted state, RC tracking)
|
|
109
|
+
const registry = this.controller._childrenRegistry;
|
|
110
|
+
if (registry) {
|
|
111
|
+
registry.destroy(childId);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Remove from children array (ViewHierarchyManager's responsibility)
|
|
115
|
+
if (!this.controller.children) {
|
|
116
|
+
this.controller.children = [];
|
|
117
|
+
}
|
|
118
|
+
this.controller.children = this.controller.children.filter(c => c !== childController);
|
|
119
|
+
|
|
120
|
+
return this.controller;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Reset original view (destroy it)
|
|
125
|
+
*/
|
|
126
|
+
resetOriginalView() {
|
|
127
|
+
if (this.controller.originalView && this.controller.originalView instanceof ViewController) {
|
|
128
|
+
this.controller.originalView.destroy();
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Eject original view (clear references)
|
|
134
|
+
*/
|
|
135
|
+
ejectOriginalView() {
|
|
136
|
+
if (this.controller.originalView && this.controller.originalView instanceof ViewController) {
|
|
137
|
+
this.controller.originalView = null;
|
|
138
|
+
this.controller.originalViewId = null;
|
|
139
|
+
this.controller.originalViewPath = null;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Remove this view and all children
|
|
145
|
+
* @returns {ViewController} Controller instance for chaining
|
|
146
|
+
*/
|
|
147
|
+
remove() {
|
|
148
|
+
this.controller.master?.__?.removeChild(this.controller.view);
|
|
149
|
+
|
|
150
|
+
// Add defensive check before forEach
|
|
151
|
+
if (this.controller.children && Array.isArray(this.controller.children)) {
|
|
152
|
+
this.controller.children.forEach(childCtrl => childCtrl.remove());
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
this.controller.master = null;
|
|
156
|
+
this.controller.children = [];
|
|
157
|
+
return this.controller;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Create super view (layout inheritance)
|
|
162
|
+
* Called by __extends template helper
|
|
163
|
+
*
|
|
164
|
+
* @param {string} path - Super view path
|
|
165
|
+
* @param {Object} data - Data to pass to super view
|
|
166
|
+
* @returns {View|null} Super view instance
|
|
167
|
+
*/
|
|
168
|
+
createSuperView(path, data = {}) {
|
|
169
|
+
const originData = this.controller.data ? this.controller.data : {};
|
|
170
|
+
if (originData.__SSR_VIEW_ID__) {
|
|
171
|
+
originData.__SSR_VIEW_ID__ = null;
|
|
172
|
+
delete originData.__SSR_VIEW_ID__;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const viewInstance = this.controller.App.View.extendView(path, { ...originData, ...data });
|
|
176
|
+
if (!viewInstance) {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
this.controller.superViewPath = path;
|
|
181
|
+
this.setSuperView(viewInstance);
|
|
182
|
+
viewInstance.__.setOriginalView(this.controller.view);
|
|
183
|
+
viewInstance.__.viewType = 'layout';
|
|
184
|
+
return viewInstance;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Create child view (include)
|
|
189
|
+
* Called by $include helper
|
|
190
|
+
* Uses ChildrenRegistry for intelligent reuse and cleanup
|
|
191
|
+
*
|
|
192
|
+
* @param {string} path - Child view path
|
|
193
|
+
* @param {Object} data - Data to pass to child view
|
|
194
|
+
* @returns {View|null} Child view instance
|
|
195
|
+
*/
|
|
196
|
+
createChildView(path, data = {}) {
|
|
197
|
+
const registry = this.controller._childrenRegistry;
|
|
198
|
+
|
|
199
|
+
// 1. Try to find reusable child from renewChildrenIDs
|
|
200
|
+
if (this.renewChildrenIDs.length) {
|
|
201
|
+
let childCtrl = this.controller.children.find(c =>
|
|
202
|
+
c.view.name == path && this.renewChildrenIDs.includes(c.view.id)
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
if (childCtrl) {
|
|
206
|
+
const view = childCtrl.view;
|
|
207
|
+
|
|
208
|
+
// Update child in registry (refresh data and parentReactiveComponent)
|
|
209
|
+
const childNode = registry.get(view.id);
|
|
210
|
+
if (childNode) {
|
|
211
|
+
Object.assign(childNode.scope.data, data);
|
|
212
|
+
|
|
213
|
+
// Update parentReactiveComponent for reused child
|
|
214
|
+
const currentRC = this.controller._reactiveManager?.currentRenderingComponent;
|
|
215
|
+
if (currentRC && childNode.parentReactiveComponent !== currentRC) {
|
|
216
|
+
registry.updateParentReactiveComponent(view.id, currentRC);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Mark as needing refresh
|
|
221
|
+
view.__._templateManager.wrapperConfig.enable = true;
|
|
222
|
+
view.__.isScanned = false;
|
|
223
|
+
view.__.isFirstClientRendering = true;
|
|
224
|
+
view.__.updateVariableData(data);
|
|
225
|
+
|
|
226
|
+
this.rcChildrenIDs.push(childCtrl.view.id);
|
|
227
|
+
return view;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// 2. Create new child
|
|
232
|
+
const parentData = this.controller.data ? this.controller.data : {};
|
|
233
|
+
if (parentData.__SSR_VIEW_ID__) {
|
|
234
|
+
parentData.__SSR_VIEW_ID__ = null;
|
|
235
|
+
delete parentData.__SSR_VIEW_ID__;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const viewInstance = this.controller.App.View.include(path, { ...parentData, ...data });
|
|
239
|
+
if (!viewInstance) {
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// 3. Setup for rendering
|
|
244
|
+
viewInstance.__._templateManager.wrapperConfig.enable = true;
|
|
245
|
+
viewInstance.__.isScanned = false;
|
|
246
|
+
viewInstance.__.isFirstClientRendering = true;
|
|
247
|
+
viewInstance.__.setParent(this.controller.view);
|
|
248
|
+
|
|
249
|
+
// 4. Register child (uses ChildrenRegistry)
|
|
250
|
+
this.addChild(viewInstance, data);
|
|
251
|
+
|
|
252
|
+
viewInstance.__.viewType = 'template';
|
|
253
|
+
this.scanChildrenIDs.push(viewInstance.id);
|
|
254
|
+
this.rcChildrenIDs.push(viewInstance.id);
|
|
255
|
+
|
|
256
|
+
return viewInstance;
|
|
257
|
+
}
|
|
258
|
+
}
|