@jqhtml/core 2.2.222
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +24 -0
- package/dist/component-registry.d.ts +64 -0
- package/dist/component-registry.d.ts.map +1 -0
- package/dist/component.d.ts +336 -0
- package/dist/component.d.ts.map +1 -0
- package/dist/debug-entry.d.ts +36 -0
- package/dist/debug-entry.d.ts.map +1 -0
- package/dist/debug-overlay.d.ts +61 -0
- package/dist/debug-overlay.d.ts.map +1 -0
- package/dist/debug.d.ts +19 -0
- package/dist/debug.d.ts.map +1 -0
- package/dist/index.cjs +4384 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +91 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4347 -0
- package/dist/index.js.map +1 -0
- package/dist/instruction-processor.d.ts +31 -0
- package/dist/instruction-processor.d.ts.map +1 -0
- package/dist/jqhtml-core.esm.js +4352 -0
- package/dist/jqhtml-core.esm.js.map +1 -0
- package/dist/jqhtml-debug.esm.js +575 -0
- package/dist/jqhtml-debug.esm.js.map +1 -0
- package/dist/jquery-plugin.d.ts +30 -0
- package/dist/jquery-plugin.d.ts.map +1 -0
- package/dist/lifecycle-manager.d.ts +34 -0
- package/dist/lifecycle-manager.d.ts.map +1 -0
- package/dist/load-coordinator.d.ts +79 -0
- package/dist/load-coordinator.d.ts.map +1 -0
- package/dist/local-storage.d.ts +147 -0
- package/dist/local-storage.d.ts.map +1 -0
- package/dist/template-renderer.d.ts +17 -0
- package/dist/template-renderer.d.ts.map +1 -0
- package/laravel-bridge/README.md +242 -0
- package/laravel-bridge/autoload.php +51 -0
- package/laravel-bridge/composer.json +34 -0
- package/laravel-bridge/config/jqhtml.php +82 -0
- package/laravel-bridge/examples/node-integration.js +201 -0
- package/laravel-bridge/src/JqhtmlErrorFormatter.php +187 -0
- package/laravel-bridge/src/JqhtmlException.php +173 -0
- package/laravel-bridge/src/JqhtmlExceptionRenderer.php +93 -0
- package/laravel-bridge/src/JqhtmlServiceProvider.php +72 -0
- package/laravel-bridge/src/Middleware/JqhtmlErrorMiddleware.php +90 -0
- package/laravel-bridge/tests/ExceptionFormattingTest.php +219 -0
- package/package.json +74 -0
|
@@ -0,0 +1,4352 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JQHTML Core v2.2.222
|
|
3
|
+
* (c) 2025 JQHTML Team
|
|
4
|
+
* Released under the MIT License
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* JQHTML v2 Lifecycle Manager
|
|
8
|
+
*
|
|
9
|
+
* Simple lifecycle orchestration - no queues, no batching.
|
|
10
|
+
* Components boot when created. That's it.
|
|
11
|
+
*
|
|
12
|
+
* Lifecycle order:
|
|
13
|
+
* 1. create() - Calls on_create() BEFORE first render
|
|
14
|
+
* 2. render() - Creates DOM, instantiates child components, calls on_render()
|
|
15
|
+
* 3. _load() - Calls on_load(), may trigger re-render if this.data changes
|
|
16
|
+
* 4. ready() - Waits for children to be ready, calls on_ready()
|
|
17
|
+
*/
|
|
18
|
+
class LifecycleManager {
|
|
19
|
+
static get_instance() {
|
|
20
|
+
if (!LifecycleManager.instance) {
|
|
21
|
+
LifecycleManager.instance = new LifecycleManager();
|
|
22
|
+
}
|
|
23
|
+
return LifecycleManager.instance;
|
|
24
|
+
}
|
|
25
|
+
constructor() {
|
|
26
|
+
this.active_components = new Set();
|
|
27
|
+
// No automatic cleanup on DOM removal - components stop explicitly via:
|
|
28
|
+
// 1. Parent component re-render (calls .empty() which stops children)
|
|
29
|
+
// 2. Manual .empty(), .html(), .text() calls (overridden to stop children)
|
|
30
|
+
// 3. Explicit .stop() call
|
|
31
|
+
// This allows components to be moved in DOM without lifecycle interruption
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Boot a component - run its full lifecycle
|
|
35
|
+
* Called when component is created
|
|
36
|
+
*/
|
|
37
|
+
async boot_component(component) {
|
|
38
|
+
this.active_components.add(component);
|
|
39
|
+
try {
|
|
40
|
+
// Create phase - runs BEFORE first render
|
|
41
|
+
await component.create();
|
|
42
|
+
// Check if stopped during create
|
|
43
|
+
if (component._stopped)
|
|
44
|
+
return;
|
|
45
|
+
// Trigger create event
|
|
46
|
+
component.trigger('create');
|
|
47
|
+
// Render phase - creates DOM and child components
|
|
48
|
+
// Note: _render() now calls on_render() internally after DOM update
|
|
49
|
+
// Capture render ID to detect if another render happens before ready
|
|
50
|
+
let render_id = component._render();
|
|
51
|
+
// Check if stopped during render
|
|
52
|
+
if (component._stopped)
|
|
53
|
+
return;
|
|
54
|
+
// Load phase - may modify this.data
|
|
55
|
+
await component._load();
|
|
56
|
+
// Check if stopped during load
|
|
57
|
+
if (component._stopped)
|
|
58
|
+
return;
|
|
59
|
+
// If data changed during load, re-render
|
|
60
|
+
// Note: _render() now calls on_render() internally after DOM update
|
|
61
|
+
if (component._should_rerender()) {
|
|
62
|
+
render_id = component._render();
|
|
63
|
+
// Check if stopped during re-render
|
|
64
|
+
if (component._stopped)
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
// Check if this render is still current before proceeding to ready
|
|
68
|
+
// If _render_count changed, another render happened and we should skip ready
|
|
69
|
+
if (component._render_count !== render_id) {
|
|
70
|
+
return; // Stale render, don't call ready
|
|
71
|
+
}
|
|
72
|
+
// Ready phase - waits for children, then calls on_ready()
|
|
73
|
+
await component._ready();
|
|
74
|
+
// Check if stopped during ready
|
|
75
|
+
if (component._stopped)
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
console.error(`Error booting component ${component.component_name()}:`, error);
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Unregister a component (called on destroy)
|
|
85
|
+
*/
|
|
86
|
+
unregister_component(component) {
|
|
87
|
+
this.active_components.delete(component);
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Wait for all active components to reach ready state
|
|
91
|
+
*/
|
|
92
|
+
async wait_for_ready() {
|
|
93
|
+
const ready_promises = [];
|
|
94
|
+
for (const component of this.active_components) {
|
|
95
|
+
if (component._ready_state < 4) {
|
|
96
|
+
ready_promises.push(new Promise((resolve) => {
|
|
97
|
+
component.on('ready', () => resolve());
|
|
98
|
+
}));
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
await Promise.all(ready_promises);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* JQHTML v2 Component Registry
|
|
107
|
+
*
|
|
108
|
+
* Global registry for component classes and templates
|
|
109
|
+
* Enables dynamic component instantiation and template association
|
|
110
|
+
*/
|
|
111
|
+
// Registry storage
|
|
112
|
+
const component_classes = new Map();
|
|
113
|
+
const component_templates = new Map();
|
|
114
|
+
// Track warnings to only show once per component name
|
|
115
|
+
const warned_components = new Set();
|
|
116
|
+
// Default template for components without registered templates
|
|
117
|
+
const DEFAULT_TEMPLATE = {
|
|
118
|
+
name: 'Jqhtml_Component', // Default name
|
|
119
|
+
tag: 'div',
|
|
120
|
+
render: function (data, args, content) {
|
|
121
|
+
const _output = [];
|
|
122
|
+
// Check for _inner_html first (server-rendered content)
|
|
123
|
+
if (args._inner_html) {
|
|
124
|
+
_output.push(args._inner_html);
|
|
125
|
+
return [_output, this];
|
|
126
|
+
}
|
|
127
|
+
// Just render the content/slots
|
|
128
|
+
if (content && typeof content === 'function') {
|
|
129
|
+
const result = content(); // Call with no args to get default content
|
|
130
|
+
// Handle both tuple and string returns
|
|
131
|
+
if (Array.isArray(result) && result.length === 2) {
|
|
132
|
+
// It's a [instructions, context] tuple
|
|
133
|
+
_output.push(...result[0]);
|
|
134
|
+
}
|
|
135
|
+
else if (typeof result === 'string') {
|
|
136
|
+
// It's a plain string
|
|
137
|
+
_output.push(result);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return [_output, this];
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
function register_component(nameOrClass, component_class, template) {
|
|
144
|
+
// Handle overloaded signatures
|
|
145
|
+
if (typeof nameOrClass === 'string') {
|
|
146
|
+
// Called with (name, class, template?)
|
|
147
|
+
const name = nameOrClass;
|
|
148
|
+
if (!component_class) {
|
|
149
|
+
throw new Error('Component class is required when registering by name');
|
|
150
|
+
}
|
|
151
|
+
// Validate component name starts with capital letter
|
|
152
|
+
if (!/^[A-Z]/.test(name)) {
|
|
153
|
+
throw new Error(`Component name '${name}' must start with a capital letter. Convention is First_Letter_With_Underscores.`);
|
|
154
|
+
}
|
|
155
|
+
component_classes.set(name, component_class);
|
|
156
|
+
// If template provided, register it
|
|
157
|
+
if (template) {
|
|
158
|
+
// Validate template name matches component name
|
|
159
|
+
if (template.name !== name) {
|
|
160
|
+
throw new Error(`Template name '${template.name}' must match component name '${name}'`);
|
|
161
|
+
}
|
|
162
|
+
register_template(template);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
// Called with just (class) - extract name from class
|
|
167
|
+
const component_class = nameOrClass;
|
|
168
|
+
const name = component_class.name;
|
|
169
|
+
if (!name || name === 'Jqhtml_Component') {
|
|
170
|
+
throw new Error('Component class must have a name when registering without explicit name');
|
|
171
|
+
}
|
|
172
|
+
component_classes.set(name, component_class);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Get a component class by name
|
|
177
|
+
* If no class found, walks the template extends chain to find parent class
|
|
178
|
+
*/
|
|
179
|
+
function get_component_class(name) {
|
|
180
|
+
// First check if class directly registered
|
|
181
|
+
const directClass = component_classes.get(name);
|
|
182
|
+
if (directClass) {
|
|
183
|
+
return directClass;
|
|
184
|
+
}
|
|
185
|
+
// No direct class found - walk template extends chain to find parent with class
|
|
186
|
+
const template = component_templates.get(name);
|
|
187
|
+
if (template && template.extends) {
|
|
188
|
+
// Recursively check parent templates
|
|
189
|
+
const visited = new Set([name]); // Prevent infinite loops
|
|
190
|
+
let currentTemplateName = template.extends;
|
|
191
|
+
while (currentTemplateName && !visited.has(currentTemplateName)) {
|
|
192
|
+
visited.add(currentTemplateName);
|
|
193
|
+
// Check if this parent has a registered class
|
|
194
|
+
const parentClass = component_classes.get(currentTemplateName);
|
|
195
|
+
if (parentClass) {
|
|
196
|
+
if (window.jqhtml?.debug?.enabled) {
|
|
197
|
+
console.log(`[JQHTML] Component '${name}' using class from parent '${currentTemplateName}' via extends chain`);
|
|
198
|
+
}
|
|
199
|
+
return parentClass;
|
|
200
|
+
}
|
|
201
|
+
// Continue walking up the chain
|
|
202
|
+
const parentTemplate = component_templates.get(currentTemplateName);
|
|
203
|
+
if (parentTemplate && parentTemplate.extends) {
|
|
204
|
+
currentTemplateName = parentTemplate.extends;
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return undefined;
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Register a template - name is extracted from template.name property
|
|
215
|
+
* Returns true if registered, false if duplicate
|
|
216
|
+
*/
|
|
217
|
+
function register_template(template_def) {
|
|
218
|
+
const name = template_def.name;
|
|
219
|
+
if (!name) {
|
|
220
|
+
throw new Error('Template must have a name property');
|
|
221
|
+
}
|
|
222
|
+
// Validate template name starts with capital letter
|
|
223
|
+
if (!/^[A-Z]/.test(name)) {
|
|
224
|
+
throw new Error(`Template name '${name}' must start with a capital letter. Convention is First_Letter_With_Underscores.`);
|
|
225
|
+
}
|
|
226
|
+
// Check for duplicate registration
|
|
227
|
+
if (component_templates.has(name)) {
|
|
228
|
+
console.warn(`[JQHTML] Template '${name}' already registered, skipping duplicate registration`);
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
component_templates.set(name, template_def);
|
|
232
|
+
if (window.jqhtml?.debug?.enabled) {
|
|
233
|
+
console.log(`[JQHTML] Successfully registered template: ${name}`);
|
|
234
|
+
}
|
|
235
|
+
// Also attach metadata to the component class if it exists
|
|
236
|
+
const component_class = component_classes.get(name);
|
|
237
|
+
if (component_class) {
|
|
238
|
+
component_class._jqhtml_metadata = {
|
|
239
|
+
tag: template_def.tag,
|
|
240
|
+
defaultAttributes: template_def.defaultAttributes || {}
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
return true;
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Get template for a component by name
|
|
247
|
+
*/
|
|
248
|
+
function get_template(name) {
|
|
249
|
+
const template = component_templates.get(name);
|
|
250
|
+
if (!template) {
|
|
251
|
+
// Check if we have a class but no template - walk prototype chain
|
|
252
|
+
const component_class = component_classes.get(name);
|
|
253
|
+
if (component_class) {
|
|
254
|
+
// Class exists but no template - walk up prototype chain to find ancestor template
|
|
255
|
+
const inherited_template = get_template_by_class(component_class);
|
|
256
|
+
if (inherited_template !== DEFAULT_TEMPLATE) {
|
|
257
|
+
if (window.jqhtml?.debug?.enabled) {
|
|
258
|
+
console.log(`[JQHTML] Component '${name}' has no template, using template from prototype chain`);
|
|
259
|
+
}
|
|
260
|
+
return inherited_template;
|
|
261
|
+
}
|
|
262
|
+
// Class exists but no template in chain
|
|
263
|
+
if (window.jqhtml?.debug?.enabled && !warned_components.has(name)) {
|
|
264
|
+
warned_components.add(name);
|
|
265
|
+
console.log(`[JQHTML] No template found for class: ${name}, using default div template`);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
// No class and no template
|
|
270
|
+
// Suppress warning for _Jqhtml_Component and Redrawable (internal components)
|
|
271
|
+
// Only warn once per component name
|
|
272
|
+
if (name !== '_Jqhtml_Component' && name !== 'Redrawable' && !warned_components.has(name)) {
|
|
273
|
+
warned_components.add(name);
|
|
274
|
+
console.warn(`[JQHTML] Creating ${name} with defaults - no template or class defined`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
if (window.jqhtml?.debug?.verbose) {
|
|
278
|
+
const registered = Array.from(component_templates.keys());
|
|
279
|
+
console.log(`[JQHTML] Looking for template '${name}' in: [${registered.join(', ')}]`);
|
|
280
|
+
}
|
|
281
|
+
return DEFAULT_TEMPLATE;
|
|
282
|
+
}
|
|
283
|
+
return template;
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Get template for a component class - walks up inheritance chain
|
|
287
|
+
*/
|
|
288
|
+
function get_template_by_class(component_class) {
|
|
289
|
+
// First check if class has static template property
|
|
290
|
+
if (component_class.template) {
|
|
291
|
+
return component_class.template;
|
|
292
|
+
}
|
|
293
|
+
// Then check registered templates by class name
|
|
294
|
+
let currentClass = component_class;
|
|
295
|
+
while (currentClass && currentClass.name !== 'Object') {
|
|
296
|
+
// Normalize class name - handle different import patterns
|
|
297
|
+
let normalizedName = currentClass.name;
|
|
298
|
+
if (normalizedName === '_Jqhtml_Component' || normalizedName === '_Base_Jqhtml_Component') {
|
|
299
|
+
normalizedName = 'Jqhtml_Component';
|
|
300
|
+
}
|
|
301
|
+
const template = component_templates.get(normalizedName);
|
|
302
|
+
if (template) {
|
|
303
|
+
return template;
|
|
304
|
+
}
|
|
305
|
+
// Walk up the prototype chain
|
|
306
|
+
currentClass = Object.getPrototypeOf(currentClass);
|
|
307
|
+
}
|
|
308
|
+
return DEFAULT_TEMPLATE;
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Create a component instance by name
|
|
312
|
+
* If no component class is registered, uses the default Component class
|
|
313
|
+
*/
|
|
314
|
+
function create_component(name, element, args = {}) {
|
|
315
|
+
const ComponentClass = get_component_class(name) || Jqhtml_Component;
|
|
316
|
+
return new ComponentClass(element, args);
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Check if a component is registered
|
|
320
|
+
*/
|
|
321
|
+
function has_component(name) {
|
|
322
|
+
return component_classes.has(name);
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Get all registered component names
|
|
326
|
+
*/
|
|
327
|
+
function get_component_names() {
|
|
328
|
+
return Array.from(component_classes.keys());
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Get all registered template names
|
|
332
|
+
*/
|
|
333
|
+
function get_registered_templates() {
|
|
334
|
+
return Array.from(component_templates.keys());
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* List all registered components with their template status
|
|
338
|
+
*/
|
|
339
|
+
function list_components() {
|
|
340
|
+
const result = {};
|
|
341
|
+
// Add all classes
|
|
342
|
+
for (const name of component_classes.keys()) {
|
|
343
|
+
result[name] = {
|
|
344
|
+
has_class: true,
|
|
345
|
+
has_template: component_templates.has(name)
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
// Add any templates without classes
|
|
349
|
+
for (const name of component_templates.keys()) {
|
|
350
|
+
if (!result[name]) {
|
|
351
|
+
result[name] = {
|
|
352
|
+
has_class: false,
|
|
353
|
+
has_template: true
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
return result;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* JQHTML v2 Instruction Processor
|
|
362
|
+
*
|
|
363
|
+
* Processes instruction arrays generated by the parser
|
|
364
|
+
* Uses v1's HTML string building approach for performance
|
|
365
|
+
*/
|
|
366
|
+
// Base36 incrementing CID generator
|
|
367
|
+
// Format: aa, ab, ..., az, a0, ..., a9, ba, ..., zz, z9, aaa, ...
|
|
368
|
+
// First digit must always be a letter (a-z), subsequent digits can be a-z0-9
|
|
369
|
+
let _cid_increment = 'aa';
|
|
370
|
+
function uid() {
|
|
371
|
+
const current = _cid_increment;
|
|
372
|
+
// Increment for next use
|
|
373
|
+
const chars = _cid_increment.split('');
|
|
374
|
+
let carry = true;
|
|
375
|
+
// Process from right to left
|
|
376
|
+
for (let i = chars.length - 1; i >= 0 && carry; i--) {
|
|
377
|
+
const char = chars[i];
|
|
378
|
+
if (char >= 'a' && char < 'z') {
|
|
379
|
+
// a-y increments to next letter
|
|
380
|
+
chars[i] = String.fromCharCode(char.charCodeAt(0) + 1);
|
|
381
|
+
carry = false;
|
|
382
|
+
}
|
|
383
|
+
else if (char === 'z') {
|
|
384
|
+
// z transitions to '0' (start of numeric range)
|
|
385
|
+
chars[i] = '0';
|
|
386
|
+
carry = false;
|
|
387
|
+
}
|
|
388
|
+
else if (char >= '0' && char < '9') {
|
|
389
|
+
// 0-8 increments to next number
|
|
390
|
+
chars[i] = String.fromCharCode(char.charCodeAt(0) + 1);
|
|
391
|
+
carry = false;
|
|
392
|
+
}
|
|
393
|
+
else if (char === '9') {
|
|
394
|
+
// 9 wraps back to 'a' with carry
|
|
395
|
+
chars[i] = 'a';
|
|
396
|
+
carry = true;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
// If we still have carry after processing all digits, we need more digits
|
|
400
|
+
if (carry) {
|
|
401
|
+
chars.unshift('a'); // Prepend new 'a' (first digit must be letter)
|
|
402
|
+
}
|
|
403
|
+
// Ensure first digit is never numeric
|
|
404
|
+
if (chars[0] >= '0' && chars[0] <= '9') {
|
|
405
|
+
chars[0] = 'a';
|
|
406
|
+
chars.unshift('a');
|
|
407
|
+
}
|
|
408
|
+
_cid_increment = chars.join('');
|
|
409
|
+
return current;
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* Process an array of instructions and append to target
|
|
413
|
+
* Uses v1 approach: build HTML string, set innerHTML, then initialize
|
|
414
|
+
*/
|
|
415
|
+
function process_instructions(instructions, target, context, slots) {
|
|
416
|
+
// Build HTML string and track elements that need initialization
|
|
417
|
+
const html = [];
|
|
418
|
+
const tagElements = {};
|
|
419
|
+
const components = {};
|
|
420
|
+
// Process all instructions to build HTML
|
|
421
|
+
for (const instruction of instructions) {
|
|
422
|
+
process_instruction_to_html(instruction, html, tagElements, components, context, slots);
|
|
423
|
+
}
|
|
424
|
+
// Set innerHTML once - this is the performance win
|
|
425
|
+
// Use native innerHTML for better performance
|
|
426
|
+
target[0].innerHTML = html.join('');
|
|
427
|
+
// Second pass: initialize special attributes and events
|
|
428
|
+
for (const [tid, tagData] of Object.entries(tagElements)) {
|
|
429
|
+
// Use native querySelector for better performance
|
|
430
|
+
const el = target[0].querySelector(`[data-tid="${tid}"]`);
|
|
431
|
+
if (el) {
|
|
432
|
+
const element = $(el);
|
|
433
|
+
el.removeAttribute('data-tid');
|
|
434
|
+
apply_attributes(element, tagData.attrs, context);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
// Third pass: initialize and boot components in parallel
|
|
438
|
+
// Like v1, all sibling components at this level boot simultaneously
|
|
439
|
+
// DO NOT await - let children boot in background while parent continues
|
|
440
|
+
for (const [cid, compData] of Object.entries(components)) {
|
|
441
|
+
// Use native querySelector for better performance
|
|
442
|
+
const el = target[0].querySelector(`[data-cid="${cid}"]`);
|
|
443
|
+
if (el) {
|
|
444
|
+
const element = $(el);
|
|
445
|
+
el.removeAttribute('data-cid');
|
|
446
|
+
// Boot this component (which will render and boot its children recursively)
|
|
447
|
+
// Fire and forget - don't wait for boot to complete
|
|
448
|
+
initialize_component(element, compData);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* Process a single instruction into HTML
|
|
454
|
+
*/
|
|
455
|
+
function process_instruction_to_html(instruction, html, tagElements, components, context, slots) {
|
|
456
|
+
if (typeof instruction === 'string') {
|
|
457
|
+
// Raw HTML/text - no escaping, as requested
|
|
458
|
+
html.push(instruction);
|
|
459
|
+
}
|
|
460
|
+
else if ('tag' in instruction) {
|
|
461
|
+
// HTML tag
|
|
462
|
+
process_tag_to_html(instruction, html, tagElements, components, context);
|
|
463
|
+
}
|
|
464
|
+
else if ('comp' in instruction) {
|
|
465
|
+
// Component
|
|
466
|
+
process_component_to_html(instruction, html, components, context);
|
|
467
|
+
}
|
|
468
|
+
else if ('slot' in instruction) {
|
|
469
|
+
// Slot content
|
|
470
|
+
process_slot_to_html(instruction, html, tagElements, components, context, slots);
|
|
471
|
+
}
|
|
472
|
+
else if ('rawtag' in instruction) {
|
|
473
|
+
// Raw content tag (textarea, pre)
|
|
474
|
+
process_rawtag_to_html(instruction, html);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
/**
|
|
478
|
+
* Process a tag instruction to HTML
|
|
479
|
+
*/
|
|
480
|
+
function process_tag_to_html(instruction, html, tagElements, components, context) {
|
|
481
|
+
const [tagName, attrs, selfClosing] = instruction.tag;
|
|
482
|
+
// Check if we need to track this element for second pass
|
|
483
|
+
const needsTracking = Object.keys(attrs).some(key => key === '$sid' || key.startsWith('$') || key.startsWith('@') ||
|
|
484
|
+
key.startsWith('on') ||
|
|
485
|
+
key.startsWith('data-bind-') || key.startsWith('data-__-on-'));
|
|
486
|
+
// Start building tag
|
|
487
|
+
html.push(`<${tagName}`);
|
|
488
|
+
// Add tracking ID if needed
|
|
489
|
+
let tid = null;
|
|
490
|
+
if (needsTracking) {
|
|
491
|
+
tid = uid();
|
|
492
|
+
html.push(` data-tid="${tid}"`);
|
|
493
|
+
tagElements[tid] = { attrs, context };
|
|
494
|
+
}
|
|
495
|
+
// Add simple string/numeric attributes directly
|
|
496
|
+
for (const [key, value] of Object.entries(attrs)) {
|
|
497
|
+
if (!key.startsWith('$') && !key.startsWith('on') && !key.startsWith('@') &&
|
|
498
|
+
!key.startsWith('data-bind-') && !key.startsWith('data-__-on-') &&
|
|
499
|
+
(typeof value === 'string' || typeof value === 'number')) {
|
|
500
|
+
if (key === 'id' && tid) {
|
|
501
|
+
// Special handling for id attribute - scope to parent component's _cid
|
|
502
|
+
// This is for regular id="foo" attributes that need scoping (rare case)
|
|
503
|
+
// Most scoping happens via $sid attribute which becomes data-sid
|
|
504
|
+
// Don't double-scope if already scoped (contains :)
|
|
505
|
+
if (typeof value === 'string' && value.includes(':')) {
|
|
506
|
+
html.push(` id="${value}"`);
|
|
507
|
+
}
|
|
508
|
+
else {
|
|
509
|
+
html.push(` id="${value}:${context._cid}"`);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
else {
|
|
513
|
+
html.push(` ${key}="${value}"`);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
// Close opening tag
|
|
518
|
+
if (selfClosing) {
|
|
519
|
+
html.push(' />');
|
|
520
|
+
}
|
|
521
|
+
else {
|
|
522
|
+
html.push('>');
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* Process a component instruction to HTML
|
|
527
|
+
*/
|
|
528
|
+
function process_component_to_html(instruction, html, components, context) {
|
|
529
|
+
const [componentName, props, contentOrSlots] = instruction.comp;
|
|
530
|
+
// Determine if third parameter is a function (default content) or object (named slots)
|
|
531
|
+
let contentFn;
|
|
532
|
+
let slots;
|
|
533
|
+
if (contentOrSlots) {
|
|
534
|
+
if (typeof contentOrSlots === 'function') {
|
|
535
|
+
// Single function = default content
|
|
536
|
+
contentFn = contentOrSlots;
|
|
537
|
+
}
|
|
538
|
+
else if (typeof contentOrSlots === 'object') {
|
|
539
|
+
// Object = named slots
|
|
540
|
+
slots = contentOrSlots;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
// Generate unique ID for this component
|
|
544
|
+
const cid = uid();
|
|
545
|
+
// Get component class (or default) and template to determine tag name
|
|
546
|
+
get_component_class(componentName) || Jqhtml_Component;
|
|
547
|
+
const template = get_template(componentName);
|
|
548
|
+
// Get tag name with precedence: _tag (invocation) > tag (template) > 'div'
|
|
549
|
+
const tagName = props._tag || template.tag || 'div';
|
|
550
|
+
// Create element with tracking ID
|
|
551
|
+
html.push(`<${tagName} data-cid="${cid}"`);
|
|
552
|
+
// Handle id attributes for components
|
|
553
|
+
// The compiled code always generates both 'id' (scoped) and 'data-sid' (base) for $sid attributes
|
|
554
|
+
// We just pass through what the compiler gave us - NEVER regenerate
|
|
555
|
+
if (props['data-sid']) {
|
|
556
|
+
const baseId = props['data-sid'];
|
|
557
|
+
// The compiled code ALWAYS sets props['id'] with the correct scoped value
|
|
558
|
+
// Just use it directly - it already has the correct parent _cid baked in
|
|
559
|
+
html.push(` id="${props['id']}" data-sid="${baseId}"`);
|
|
560
|
+
}
|
|
561
|
+
// Regular id passes through unchanged
|
|
562
|
+
else if (props['id']) {
|
|
563
|
+
html.push(` id="${props['id']}"`);
|
|
564
|
+
}
|
|
565
|
+
// Close tag - other attributes will be added during initialization
|
|
566
|
+
html.push('></' + tagName + '>');
|
|
567
|
+
// Track for initialization
|
|
568
|
+
components[cid] = {
|
|
569
|
+
name: componentName,
|
|
570
|
+
props,
|
|
571
|
+
contentFn,
|
|
572
|
+
slots,
|
|
573
|
+
context
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
/**
|
|
577
|
+
* Process a slot instruction to HTML
|
|
578
|
+
*/
|
|
579
|
+
function process_slot_to_html(instruction, html, tagElements, components, context, parentSlots) {
|
|
580
|
+
const [slotName] = instruction.slot;
|
|
581
|
+
// Check if parent provided content for this slot
|
|
582
|
+
if (parentSlots && slotName in parentSlots) {
|
|
583
|
+
const parentSlot = parentSlots[slotName];
|
|
584
|
+
const [, slotProps, contentFn] = parentSlot.slot;
|
|
585
|
+
// Execute slot content function with props
|
|
586
|
+
const [content] = contentFn.call(context, slotProps);
|
|
587
|
+
// Process the content to HTML
|
|
588
|
+
for (const item of content) {
|
|
589
|
+
process_instruction_to_html(item, html, tagElements, components, context);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
else if (slotName === 'default' && instruction.slot[2]) {
|
|
593
|
+
// Use default slot content
|
|
594
|
+
const [, , defaultFn] = instruction.slot;
|
|
595
|
+
const [content] = defaultFn.call(context, {});
|
|
596
|
+
for (const item of content) {
|
|
597
|
+
process_instruction_to_html(item, html, tagElements, components, context);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
/**
|
|
602
|
+
* Process a rawtag instruction to HTML
|
|
603
|
+
*
|
|
604
|
+
* Raw content tags (textarea, pre) preserve exact whitespace without collapsing.
|
|
605
|
+
* Content is inserted as textContent (not innerHTML) to prevent any interpretation.
|
|
606
|
+
*/
|
|
607
|
+
function process_rawtag_to_html(instruction, html) {
|
|
608
|
+
const [tagName, attrs, rawContent] = instruction.rawtag;
|
|
609
|
+
// Build opening tag
|
|
610
|
+
html.push(`<${tagName}`);
|
|
611
|
+
// Add attributes
|
|
612
|
+
for (const [key, value] of Object.entries(attrs)) {
|
|
613
|
+
if (typeof value === 'string' || typeof value === 'number') {
|
|
614
|
+
const escaped_value = String(value).replace(/"/g, '"');
|
|
615
|
+
html.push(` ${key}="${escaped_value}"`);
|
|
616
|
+
}
|
|
617
|
+
else if (typeof value === 'boolean' && value) {
|
|
618
|
+
// Boolean attribute (e.g., disabled, readonly)
|
|
619
|
+
html.push(` ${key}`);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
html.push('>');
|
|
623
|
+
// Add raw content - escape HTML entities but preserve whitespace
|
|
624
|
+
const escaped_content = rawContent
|
|
625
|
+
.replace(/&/g, '&')
|
|
626
|
+
.replace(/</g, '<')
|
|
627
|
+
.replace(/>/g, '>');
|
|
628
|
+
html.push(escaped_content);
|
|
629
|
+
// Close tag
|
|
630
|
+
html.push(`</${tagName}>`);
|
|
631
|
+
}
|
|
632
|
+
/**
|
|
633
|
+
* Apply attributes with special handling to a DOM element
|
|
634
|
+
*
|
|
635
|
+
* This function processes various attribute types and applies them to the element:
|
|
636
|
+
* - $sid: Creates component-scoped IDs (handled elsewhere in HTML generation)
|
|
637
|
+
* - $*: Stored via jQuery .data() only (added to context.args for components)
|
|
638
|
+
* - @*: Event handlers with automatic preventDefault and context binding
|
|
639
|
+
* - on*: Standard event handlers with context binding
|
|
640
|
+
* - data-*: Data attributes (stored both as attributes and via .data(), added to context.args)
|
|
641
|
+
* - class: Merges with existing classes (no duplicates)
|
|
642
|
+
* - style: Merges with existing styles (rule by rule, new values override)
|
|
643
|
+
* - All other attributes: Applied directly to the element as standard HTML attributes
|
|
644
|
+
*
|
|
645
|
+
* For class and style attributes, merge logic is applied:
|
|
646
|
+
* - class: Splits both existing and new, combines without duplicates
|
|
647
|
+
* - style: Parses both into property maps, merges (new overrides), rebuilds
|
|
648
|
+
*
|
|
649
|
+
* Regular HTML attributes (href, title, role, etc.) are applied directly to the DOM
|
|
650
|
+
* and are NOT converted to data attributes or added to component args.
|
|
651
|
+
*/
|
|
652
|
+
function apply_attributes(element, attrs, context) {
|
|
653
|
+
for (const [key, value] of Object.entries(attrs)) {
|
|
654
|
+
if (key === '$sid' || key === 'id') {
|
|
655
|
+
// Already handled in HTML generation
|
|
656
|
+
continue;
|
|
657
|
+
}
|
|
658
|
+
else if (key.startsWith('$')) {
|
|
659
|
+
// $ attributes: store via jQuery .data() only
|
|
660
|
+
// Does NOT create data-* attributes on the DOM element
|
|
661
|
+
const dataKey = key.substring(1);
|
|
662
|
+
element.data(dataKey, value);
|
|
663
|
+
// } else if (key.startsWith('@')) {
|
|
664
|
+
// // Event handler with @ prefix (e.g., @click)
|
|
665
|
+
// const eventName = key.substring(1); // Remove @ to get 'click'
|
|
666
|
+
// if (typeof value === 'function') {
|
|
667
|
+
// element.on(eventName, function(e: any) {
|
|
668
|
+
// e.preventDefault(); // Auto-preventDefault as per JQHTML design
|
|
669
|
+
// // Bind 'this' to the component context and call the function
|
|
670
|
+
// value.bind(context)(e, element);
|
|
671
|
+
// });
|
|
672
|
+
// } else {
|
|
673
|
+
// console.warn("(JQHTML) Tried to assign a non function to @ event handler "+key)
|
|
674
|
+
// }
|
|
675
|
+
}
|
|
676
|
+
else if (key.startsWith('data-__-on-')) {
|
|
677
|
+
// Event handler
|
|
678
|
+
const eventName = key.substring(11); // 'data-__-on-'.length = 11
|
|
679
|
+
if (typeof value === 'function') {
|
|
680
|
+
element.on(eventName, function (e) {
|
|
681
|
+
value.bind(context)(e, element);
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
else {
|
|
685
|
+
console.warn("(JQHTML) Tried to assign a non function to on event handler " + key);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
else if (key.startsWith('on')) {
|
|
689
|
+
// Event handler
|
|
690
|
+
const eventName = key.substring(2);
|
|
691
|
+
if (typeof value === 'function') {
|
|
692
|
+
element.on(eventName, function (e) {
|
|
693
|
+
value.bind(context)(e, element);
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
else {
|
|
697
|
+
console.warn("(JQHTML) Tried to assign a non function to on event handler " + key);
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
else if (key.startsWith('data-')) {
|
|
701
|
+
// Data attributes - go into DOM
|
|
702
|
+
const attrValue = typeof value === "string" ? value.trim() : value;
|
|
703
|
+
element.attr(key, attrValue);
|
|
704
|
+
// Get version without 'data-' prefix
|
|
705
|
+
const dataKey = key.substring(5);
|
|
706
|
+
// Always set element.data
|
|
707
|
+
element.data(dataKey, value);
|
|
708
|
+
}
|
|
709
|
+
else if (key === 'class') {
|
|
710
|
+
// Handle class attribute with merge logic
|
|
711
|
+
const existingClasses = element.attr('class');
|
|
712
|
+
// Debug logging
|
|
713
|
+
if (window.jqhtml?.debug?.enabled) {
|
|
714
|
+
console.log(`[InstructionProcessor] Merging class attribute:`, {
|
|
715
|
+
existing: existingClasses,
|
|
716
|
+
new: value
|
|
717
|
+
});
|
|
718
|
+
}
|
|
719
|
+
if (!existingClasses) {
|
|
720
|
+
// No existing classes - set directly
|
|
721
|
+
const attrValue = typeof value === "string" ? value.trim() : value;
|
|
722
|
+
element.attr('class', attrValue);
|
|
723
|
+
}
|
|
724
|
+
else {
|
|
725
|
+
// Merge classes - split and add if not present
|
|
726
|
+
const existing = existingClasses.split(/\s+/).filter(c => c);
|
|
727
|
+
const newClasses = String(value).split(/\s+/).filter(c => c);
|
|
728
|
+
for (const newClass of newClasses) {
|
|
729
|
+
if (!existing.includes(newClass)) {
|
|
730
|
+
existing.push(newClass);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
element.attr('class', existing.join(' '));
|
|
734
|
+
}
|
|
735
|
+
// Debug logging result
|
|
736
|
+
if (window.jqhtml?.debug?.enabled) {
|
|
737
|
+
console.log(`[InstructionProcessor] Class after merge:`, element.attr('class'));
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
else if (key === 'style') {
|
|
741
|
+
// Handle style attribute with merge logic
|
|
742
|
+
const existingStyle = element.attr('style');
|
|
743
|
+
if (!existingStyle) {
|
|
744
|
+
// No existing style - set directly
|
|
745
|
+
const attrValue = typeof value === "string" ? value.trim() : value;
|
|
746
|
+
element.attr('style', attrValue);
|
|
747
|
+
}
|
|
748
|
+
else {
|
|
749
|
+
// Merge styles rule by rule
|
|
750
|
+
// Parse existing styles into map
|
|
751
|
+
const styleMap = {};
|
|
752
|
+
existingStyle.split(';').forEach(rule => {
|
|
753
|
+
const [prop, val] = rule.split(':').map(s => s.trim());
|
|
754
|
+
if (prop && val) {
|
|
755
|
+
styleMap[prop] = val;
|
|
756
|
+
}
|
|
757
|
+
});
|
|
758
|
+
// Parse and merge new styles
|
|
759
|
+
String(value).split(';').forEach(rule => {
|
|
760
|
+
const [prop, val] = rule.split(':').map(s => s.trim());
|
|
761
|
+
if (prop && val) {
|
|
762
|
+
styleMap[prop] = val; // Override existing or add new
|
|
763
|
+
}
|
|
764
|
+
});
|
|
765
|
+
// Rebuild style string
|
|
766
|
+
const mergedStyle = Object.entries(styleMap)
|
|
767
|
+
.map(([prop, val]) => `${prop}: ${val}`)
|
|
768
|
+
.join('; ');
|
|
769
|
+
element.attr('style', mergedStyle);
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
else {
|
|
773
|
+
// Regular HTML attributes - apply directly to the element
|
|
774
|
+
// These do NOT become data attributes or go into context.args
|
|
775
|
+
// Examples: href, title, role, aria-*, tabindex, etc.
|
|
776
|
+
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
|
|
777
|
+
const attrValue = typeof value === "string" ? value.trim() : String(value);
|
|
778
|
+
element.attr(key, attrValue);
|
|
779
|
+
}
|
|
780
|
+
else if (typeof value === 'object') {
|
|
781
|
+
// Complex values can't be DOM attributes, store as jQuery data only
|
|
782
|
+
console.warn(`(JQHTML) Unexpected value for '${key}' on`, element);
|
|
783
|
+
// element.data(key, value);
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
/**
|
|
789
|
+
* Initialize a component on a DOM element
|
|
790
|
+
*
|
|
791
|
+
* This function sets up a component instance on the given element by:
|
|
792
|
+
* 1. Building the component's CSS classes (component name + Jqhtml_Component)
|
|
793
|
+
* 2. Applying attributes from Define tag (template.defaultAttributes) first
|
|
794
|
+
* 3. Applying attributes from invocation (props) second, which override Define attributes
|
|
795
|
+
* 4. Creating the component instance
|
|
796
|
+
*
|
|
797
|
+
* Attribute handling rules:
|
|
798
|
+
* - 'as': Determines tag name (handled in process_component_to_html)
|
|
799
|
+
* - 'class': Merged from both Define and invocation
|
|
800
|
+
* - 'style': Merged with rule-by-rule precedence (invocation wins conflicts)
|
|
801
|
+
* - '$*' and 'data-*': Become data attributes and component args
|
|
802
|
+
* - '@*': Event handlers (only allowed in invocation, not Define)
|
|
803
|
+
* - All other attributes: Applied directly to DOM element
|
|
804
|
+
*/
|
|
805
|
+
async function initialize_component(element, compData) {
|
|
806
|
+
const { name, props, contentFn, slots, context } = compData;
|
|
807
|
+
// Get component class (use default Jqhtml_Component if not registered)
|
|
808
|
+
const ComponentClass = get_component_class(name) || Jqhtml_Component;
|
|
809
|
+
// Apply invocation attributes to the element
|
|
810
|
+
// The component constructor will handle defaultAttributes
|
|
811
|
+
// Filter out internal props that start with underscore
|
|
812
|
+
const invocationAttrs = {};
|
|
813
|
+
for (const [key, value] of Object.entries(props)) {
|
|
814
|
+
if (!key.startsWith('_')) {
|
|
815
|
+
invocationAttrs[key] = value;
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
// Debug logging
|
|
819
|
+
if (window.jqhtml?.debug?.enabled) {
|
|
820
|
+
console.log(`[InstructionProcessor] Applying invocation attributes for ${name}:`, invocationAttrs);
|
|
821
|
+
}
|
|
822
|
+
// Apply invocation attributes (component constructor will apply defaultAttributes first)
|
|
823
|
+
apply_attributes(element, invocationAttrs, context);
|
|
824
|
+
// Component will set its own data-cid during construction
|
|
825
|
+
// No need to add any tracking attributes here
|
|
826
|
+
// Prepare options for component constructor
|
|
827
|
+
// Only pass the content function - all other attributes are already on the DOM
|
|
828
|
+
const options = {};
|
|
829
|
+
if (contentFn) {
|
|
830
|
+
options._innerhtml_function = contentFn;
|
|
831
|
+
}
|
|
832
|
+
// Pass named slots if provided
|
|
833
|
+
if (slots) {
|
|
834
|
+
options._slots = slots;
|
|
835
|
+
}
|
|
836
|
+
// Always pass _component_name when component name differs from class name
|
|
837
|
+
// This happens when:
|
|
838
|
+
// 1. No class registered for component (uses Jqhtml_Component)
|
|
839
|
+
// 2. Class inherited from parent via extends chain (uses parent class but child template)
|
|
840
|
+
if (ComponentClass.name !== name) {
|
|
841
|
+
options._component_name = name;
|
|
842
|
+
}
|
|
843
|
+
// Create component instance - element first, then options
|
|
844
|
+
const instance = new ComponentClass(element, options);
|
|
845
|
+
// Set the instantiator (the component that rendered this one in their template)
|
|
846
|
+
instance._instantiator = context;
|
|
847
|
+
// Boot the component immediately - this runs its full lifecycle
|
|
848
|
+
await instance._boot();
|
|
849
|
+
}
|
|
850
|
+
/**
|
|
851
|
+
* Utility to extract slots from instructions
|
|
852
|
+
*/
|
|
853
|
+
function extract_slots(instructions) {
|
|
854
|
+
const slots = {};
|
|
855
|
+
for (const instruction of instructions) {
|
|
856
|
+
if (typeof instruction === 'object' && 'slot' in instruction) {
|
|
857
|
+
const [name] = instruction.slot;
|
|
858
|
+
slots[name] = instruction;
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
return slots;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
/**
|
|
865
|
+
* JQHTML Debug Module
|
|
866
|
+
*
|
|
867
|
+
* Provides comprehensive debugging capabilities for JQHTML components
|
|
868
|
+
*/
|
|
869
|
+
// Global debug state
|
|
870
|
+
let performanceMetrics = new Map();
|
|
871
|
+
/**
|
|
872
|
+
* Development warning helper
|
|
873
|
+
* Warnings are suppressed in production builds or when JQHTML_SUPPRESS_WARNINGS is set
|
|
874
|
+
*/
|
|
875
|
+
function devWarn(message) {
|
|
876
|
+
// Check if warnings are suppressed
|
|
877
|
+
if (typeof window !== 'undefined' && window.JQHTML_SUPPRESS_WARNINGS) {
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
880
|
+
// Check if in production mode
|
|
881
|
+
if (typeof process !== 'undefined' && process.env && process.env.NODE_ENV === 'production') {
|
|
882
|
+
return;
|
|
883
|
+
}
|
|
884
|
+
console.warn(`[JQHTML Dev Warning] ${message}`);
|
|
885
|
+
}
|
|
886
|
+
// Get global jqhtml object
|
|
887
|
+
function getJqhtml$1() {
|
|
888
|
+
if (typeof window !== 'undefined' && window.jqhtml) {
|
|
889
|
+
return window.jqhtml;
|
|
890
|
+
}
|
|
891
|
+
// Fallback: try to get from global if available
|
|
892
|
+
if (typeof globalThis !== 'undefined' && globalThis.jqhtml) {
|
|
893
|
+
return globalThis.jqhtml;
|
|
894
|
+
}
|
|
895
|
+
throw new Error('FATAL: window.jqhtml is not defined. The JQHTML runtime must be loaded before using debug features. ' +
|
|
896
|
+
'Import and initialize @jqhtml/core before attempting to use debug functionality.');
|
|
897
|
+
}
|
|
898
|
+
// Visual flash effect
|
|
899
|
+
function flashComponent(component, eventType) {
|
|
900
|
+
const jqhtml = getJqhtml$1();
|
|
901
|
+
if (!jqhtml?.debug?.flashComponents)
|
|
902
|
+
return;
|
|
903
|
+
const duration = jqhtml.debug.flashDuration || 500;
|
|
904
|
+
const colors = jqhtml.debug.flashColors || {};
|
|
905
|
+
const color = colors[eventType] || (eventType === 'create' ? '#3498db' :
|
|
906
|
+
eventType === 'render' ? '#27ae60' :
|
|
907
|
+
'#9b59b6');
|
|
908
|
+
// Store original border
|
|
909
|
+
const originalBorder = component.$.css('border');
|
|
910
|
+
// Apply flash border
|
|
911
|
+
component.$.css({
|
|
912
|
+
'border': `2px solid ${color}`,
|
|
913
|
+
'transition': `border ${duration}ms ease-out`
|
|
914
|
+
});
|
|
915
|
+
// Remove after duration
|
|
916
|
+
setTimeout(() => {
|
|
917
|
+
component.$.css('border', originalBorder || '');
|
|
918
|
+
}, duration);
|
|
919
|
+
}
|
|
920
|
+
// Log lifecycle event
|
|
921
|
+
function logLifecycle(component, phase, status) {
|
|
922
|
+
const jqhtml = getJqhtml$1();
|
|
923
|
+
if (!jqhtml?.debug)
|
|
924
|
+
return;
|
|
925
|
+
const shouldLog = jqhtml.debug.logFullLifecycle ||
|
|
926
|
+
(jqhtml.debug.logCreationReady && (phase === 'create' || phase === 'ready'));
|
|
927
|
+
if (!shouldLog)
|
|
928
|
+
return;
|
|
929
|
+
const componentName = component.constructor.name;
|
|
930
|
+
const timestamp = new Date().toISOString();
|
|
931
|
+
const prefix = `[JQHTML ${timestamp}]`;
|
|
932
|
+
if (status === 'start') {
|
|
933
|
+
console.log(`${prefix} ${componentName}#${component._cid} → ${phase} starting...`);
|
|
934
|
+
// Start performance tracking
|
|
935
|
+
if (jqhtml.debug.profilePerformance) {
|
|
936
|
+
performanceMetrics.set(`${component._cid}_${phase}`, Date.now());
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
else {
|
|
940
|
+
let message = `${prefix} ${componentName}#${component._cid} ✓ ${phase} complete`;
|
|
941
|
+
// Add performance data
|
|
942
|
+
if (jqhtml.debug.profilePerformance) {
|
|
943
|
+
const startTime = performanceMetrics.get(`${component._cid}_${phase}`);
|
|
944
|
+
if (startTime) {
|
|
945
|
+
const duration = Date.now() - startTime;
|
|
946
|
+
message += ` (${duration}ms)`;
|
|
947
|
+
// Highlight slow renders
|
|
948
|
+
if (phase === 'render' && jqhtml.debug.highlightSlowRenders &&
|
|
949
|
+
duration > jqhtml.debug.highlightSlowRenders) {
|
|
950
|
+
console.warn(`${prefix} SLOW RENDER: ${componentName}#${component._cid} took ${duration}ms`);
|
|
951
|
+
component.$.css('outline', '2px dashed red');
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
console.log(message);
|
|
956
|
+
// Visual feedback
|
|
957
|
+
if (jqhtml.debug.flashComponents && (phase === 'create' || phase === 'render' || phase === 'ready')) {
|
|
958
|
+
flashComponent(component, phase);
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
// Update component tree if enabled
|
|
962
|
+
if (jqhtml.debug.showComponentTree) {
|
|
963
|
+
updateComponentTree();
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
// Apply delays based on lifecycle phase
|
|
967
|
+
function applyDebugDelay(phase) {
|
|
968
|
+
const jqhtml = getJqhtml$1();
|
|
969
|
+
if (!jqhtml?.debug)
|
|
970
|
+
return;
|
|
971
|
+
let delayMs = 0;
|
|
972
|
+
switch (phase) {
|
|
973
|
+
case 'component':
|
|
974
|
+
delayMs = jqhtml.debug.delayAfterComponent || 0;
|
|
975
|
+
break;
|
|
976
|
+
case 'render':
|
|
977
|
+
delayMs = jqhtml.debug.delayAfterRender || 0;
|
|
978
|
+
break;
|
|
979
|
+
case 'rerender':
|
|
980
|
+
delayMs = jqhtml.debug.delayAfterRerender || 0;
|
|
981
|
+
break;
|
|
982
|
+
}
|
|
983
|
+
if (delayMs > 0) {
|
|
984
|
+
console.log(`[JQHTML Debug] Applying ${delayMs}ms delay after ${phase}`);
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
// Log instruction processing
|
|
988
|
+
function logInstruction(type, data) {
|
|
989
|
+
const jqhtml = getJqhtml$1();
|
|
990
|
+
if (!jqhtml?.debug?.logInstructionProcessing)
|
|
991
|
+
return;
|
|
992
|
+
console.log(`[JQHTML Instruction] ${type}:`, data);
|
|
993
|
+
}
|
|
994
|
+
// Log data changes
|
|
995
|
+
function logDataChange(component, property, oldValue, newValue) {
|
|
996
|
+
const jqhtml = getJqhtml$1();
|
|
997
|
+
if (!jqhtml?.debug?.traceDataFlow)
|
|
998
|
+
return;
|
|
999
|
+
console.log(`[JQHTML Data] ${component.constructor.name}#${component._cid}.data.${property}:`, { old: oldValue, new: newValue });
|
|
1000
|
+
}
|
|
1001
|
+
// Update component tree visualization
|
|
1002
|
+
function updateComponentTree() {
|
|
1003
|
+
// This would update a debug panel if implemented
|
|
1004
|
+
// For now, just log the tree structure periodically
|
|
1005
|
+
console.log('[JQHTML Tree] Component hierarchy updated');
|
|
1006
|
+
}
|
|
1007
|
+
// Router dispatch logging
|
|
1008
|
+
function logDispatch(url, route, params, verbose = false) {
|
|
1009
|
+
const jqhtml = getJqhtml$1();
|
|
1010
|
+
if (!jqhtml?.debug)
|
|
1011
|
+
return;
|
|
1012
|
+
const shouldLog = jqhtml.debug.logDispatch || jqhtml.debug.logDispatchVerbose;
|
|
1013
|
+
if (!shouldLog)
|
|
1014
|
+
return;
|
|
1015
|
+
const isVerbose = jqhtml.debug.logDispatchVerbose || verbose;
|
|
1016
|
+
if (isVerbose) {
|
|
1017
|
+
console.group(`[JQHTML Router] Dispatching: ${url}`);
|
|
1018
|
+
console.log('Matched route:', route);
|
|
1019
|
+
console.log('Extracted params:', params);
|
|
1020
|
+
console.log('Route component:', route.component);
|
|
1021
|
+
console.log('Route layout:', route.layout);
|
|
1022
|
+
console.log('Route meta:', route.meta);
|
|
1023
|
+
console.groupEnd();
|
|
1024
|
+
}
|
|
1025
|
+
else {
|
|
1026
|
+
console.log(`[JQHTML Router] ${url} → ${route.component} (params: ${JSON.stringify(params)})`);
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
// Check if sequential processing is enabled
|
|
1030
|
+
function isSequentialProcessing() {
|
|
1031
|
+
const jqhtml = getJqhtml$1();
|
|
1032
|
+
return jqhtml?.debug?.sequentialProcessing || false;
|
|
1033
|
+
}
|
|
1034
|
+
// Error handling with break on error
|
|
1035
|
+
function handleComponentError(component, phase, error) {
|
|
1036
|
+
const jqhtml = getJqhtml$1();
|
|
1037
|
+
console.error(`[JQHTML Error] ${component.constructor.name}#${component._cid} failed in ${phase}:`, error);
|
|
1038
|
+
if (jqhtml?.debug?.breakOnError) {
|
|
1039
|
+
debugger; // This will pause execution in dev tools
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
// Additional debug suggestions that could be implemented:
|
|
1043
|
+
//
|
|
1044
|
+
// 1. Component Inspector - Click on any component to see its data/args/state
|
|
1045
|
+
// 2. Time Travel Debugging - Record state changes and replay them
|
|
1046
|
+
// 3. Network Request Tracking - Log all AJAX calls made during load()
|
|
1047
|
+
// 4. Memory Leak Detection - Track component creation/destruction
|
|
1048
|
+
// 5. Template Compilation Debugging - Show compiled template functions
|
|
1049
|
+
// 6. Event Flow Visualization - Show event bubbling through components
|
|
1050
|
+
// 7. Dependency Graph - Show which components depend on which data
|
|
1051
|
+
// 8. Hot Reload Support - Reload components without losing state
|
|
1052
|
+
// 9. Performance Budgets - Warn when components exceed size/time limits
|
|
1053
|
+
// 10. Accessibility Auditing - Check for missing ARIA attributes
|
|
1054
|
+
|
|
1055
|
+
/**
|
|
1056
|
+
* JQHTML v2 Component Base Class
|
|
1057
|
+
*
|
|
1058
|
+
* Core component implementation following v2 specification:
|
|
1059
|
+
* - 5-stage lifecycle coordinated by LifecycleManager
|
|
1060
|
+
* - Direct jQuery manipulation (no virtual DOM)
|
|
1061
|
+
* - Scoped IDs using _cid pattern
|
|
1062
|
+
* - Event emission and CSS class hierarchy
|
|
1063
|
+
*/
|
|
1064
|
+
class Jqhtml_Component {
|
|
1065
|
+
constructor(element, args = {}) {
|
|
1066
|
+
this._ready_state = 0; // 0=created, 1=init, 2=loaded, 3=rendered, 4=ready
|
|
1067
|
+
this._instantiator = null; // Component that instantiated this one
|
|
1068
|
+
this._dom_parent = null; // Closest component in DOM tree (for lifecycle)
|
|
1069
|
+
this._dom_children = new Set(); // Components in DOM subtree (for lifecycle)
|
|
1070
|
+
this._use_dom_fallback = false; // If true, use find() fallback instead of _dom_children optimization
|
|
1071
|
+
this._stopped = false;
|
|
1072
|
+
this._booted = false; // Guard to prevent double boot() calls
|
|
1073
|
+
this._data_before_render = null; // Store data state before initial render
|
|
1074
|
+
this._lifecycle_callbacks = new Map();
|
|
1075
|
+
this._lifecycle_states = new Set(); // Track which lifecycle events have occurred
|
|
1076
|
+
this.__loading = false; // Flag to prevent render() calls during on_load()
|
|
1077
|
+
this._did_first_render = false; // Track if component has rendered at least once
|
|
1078
|
+
this._render_count = 0; // Incremented each time _render() is called, used to detect stale renders
|
|
1079
|
+
this._args_on_last_render = null; // Args snapshot from last render (for reload() comparison)
|
|
1080
|
+
this._data_on_last_render = null; // Data snapshot from last render (for refresh() comparison)
|
|
1081
|
+
this.__initial_data_snapshot = null; // Snapshot of this.data after on_create() for restoration before on_load()
|
|
1082
|
+
this.__data_frozen = false; // Track if this.data is currently frozen
|
|
1083
|
+
this.next_reload_force_refresh = null; // State machine for reload()/refresh() debounce precedence
|
|
1084
|
+
this._cid = this._generate_cid();
|
|
1085
|
+
this._lifecycle_manager = LifecycleManager.get_instance();
|
|
1086
|
+
// Create or wrap element
|
|
1087
|
+
if (element) {
|
|
1088
|
+
this.$ = $(element);
|
|
1089
|
+
}
|
|
1090
|
+
else {
|
|
1091
|
+
// Use native createElement for better performance
|
|
1092
|
+
const div = document.createElement('div');
|
|
1093
|
+
this.$ = $(div);
|
|
1094
|
+
}
|
|
1095
|
+
// Extract $ attributes from element and merge with args
|
|
1096
|
+
// $ attributes are stored via jQuery .data() (not as data-* attributes in DOM)
|
|
1097
|
+
const dataAttrs = {};
|
|
1098
|
+
// Get all jQuery .data() values from the element
|
|
1099
|
+
if (this.$.length > 0) {
|
|
1100
|
+
// Get all data via jQuery .data() without arguments (returns all data as object)
|
|
1101
|
+
const allData = this.$.data() || {};
|
|
1102
|
+
for (const key in allData) {
|
|
1103
|
+
// Skip internal attributes
|
|
1104
|
+
if (key !== 'cid' && key !== 'tid' && key !== 'componentName' && key !== 'readyState' &&
|
|
1105
|
+
key !== '_lifecycleState' && !key.startsWith('_')) {
|
|
1106
|
+
dataAttrs[key] = allData[key];
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
// Get template to check for defineArgs
|
|
1111
|
+
let template_for_args;
|
|
1112
|
+
if (args._component_name) {
|
|
1113
|
+
template_for_args = get_template(args._component_name);
|
|
1114
|
+
}
|
|
1115
|
+
else {
|
|
1116
|
+
template_for_args = get_template_by_class(this.constructor);
|
|
1117
|
+
}
|
|
1118
|
+
// Merge in order: defineArgs (defaults from Define tag) < dataAttrs < args (invocation overrides)
|
|
1119
|
+
const defineArgs = template_for_args?.defineArgs || {};
|
|
1120
|
+
this.args = { ...defineArgs, ...dataAttrs, ...args };
|
|
1121
|
+
// Attach component to element
|
|
1122
|
+
this.$.data('_component', this);
|
|
1123
|
+
// Apply CSS classes and attributes
|
|
1124
|
+
this._apply_css_classes();
|
|
1125
|
+
this._apply_default_attributes(); // Apply defaultAttributes from template
|
|
1126
|
+
this._set_attributes();
|
|
1127
|
+
// Find DOM parent component (for lifecycle coordination)
|
|
1128
|
+
this._find_dom_parent();
|
|
1129
|
+
// Setup data property with freeze enforcement using Proxy
|
|
1130
|
+
let _data = {};
|
|
1131
|
+
// Helper to create frozen proxy for data object
|
|
1132
|
+
const createDataProxy = (obj) => {
|
|
1133
|
+
return new Proxy(obj, {
|
|
1134
|
+
set: (target, prop, value) => {
|
|
1135
|
+
if (this.__data_frozen) {
|
|
1136
|
+
console.error(`[JQHTML] ERROR: Component "${this.component_name()}" attempted to modify this.data.${String(prop)} outside of on_create() or on_load().\n\n` +
|
|
1137
|
+
`RESTRICTION: this.data can ONLY be modified in:\n` +
|
|
1138
|
+
` - on_create() (set initial defaults, synchronous only)\n` +
|
|
1139
|
+
` - on_load() (fetch data from APIs, can be async)\n\n` +
|
|
1140
|
+
`WHY: this.data represents loaded state. Modifying it outside these methods bypasses the framework's render cycle.\n\n` +
|
|
1141
|
+
`FIX: Modify this.data in on_create() (for defaults) or on_load() (for fetched data):\n` +
|
|
1142
|
+
` ❌ In on_ready(): this.data.${String(prop)} = ${JSON.stringify(value)};\n` +
|
|
1143
|
+
` ✅ In on_create(): this.data.${String(prop)} = ${JSON.stringify(value)}; // Set default\n` +
|
|
1144
|
+
` ✅ In on_load(): this.data.${String(prop)} = ${JSON.stringify(value)}; // Fetch from API\n` +
|
|
1145
|
+
` ✅ For component state: this.args.${String(prop)} = ${JSON.stringify(value)}; (accessible in on_load)`);
|
|
1146
|
+
throw new Error(`[JQHTML] Cannot modify this.data.${String(prop)} outside of on_create() or on_load(). ` +
|
|
1147
|
+
`this.data is frozen after on_create() and unfrozen only during on_load().`);
|
|
1148
|
+
}
|
|
1149
|
+
target[prop] = value;
|
|
1150
|
+
return true;
|
|
1151
|
+
},
|
|
1152
|
+
deleteProperty: (target, prop) => {
|
|
1153
|
+
if (this.__data_frozen) {
|
|
1154
|
+
console.error(`[JQHTML] ERROR: Component "${this.component_name()}" attempted to delete this.data.${String(prop)} outside of on_create() or on_load().\n\n` +
|
|
1155
|
+
`RESTRICTION: this.data can ONLY be modified in:\n` +
|
|
1156
|
+
` - on_create() (set initial defaults, synchronous only)\n` +
|
|
1157
|
+
` - on_load() (fetch data from APIs, can be async)\n\n` +
|
|
1158
|
+
`WHY: this.data represents loaded state. Modifying it outside these methods bypasses the framework's render cycle.`);
|
|
1159
|
+
throw new Error(`[JQHTML] Cannot delete this.data.${String(prop)} outside of on_create() or on_load(). ` +
|
|
1160
|
+
`this.data is frozen after on_create() and unfrozen only during on_load().`);
|
|
1161
|
+
}
|
|
1162
|
+
delete target[prop];
|
|
1163
|
+
return true;
|
|
1164
|
+
}
|
|
1165
|
+
});
|
|
1166
|
+
};
|
|
1167
|
+
// Create initial proxied data object
|
|
1168
|
+
_data = createDataProxy({});
|
|
1169
|
+
Object.defineProperty(this, 'data', {
|
|
1170
|
+
get: () => _data,
|
|
1171
|
+
set: (value) => {
|
|
1172
|
+
if (this.__data_frozen) {
|
|
1173
|
+
console.error(`[JQHTML] ERROR: Component "${this.component_name()}" attempted to reassign this.data outside of on_create() or on_load().\n\n` +
|
|
1174
|
+
`RESTRICTION: this.data can ONLY be modified in:\n` +
|
|
1175
|
+
` - on_create() (set initial defaults, synchronous only)\n` +
|
|
1176
|
+
` - on_load() (fetch data from APIs, can be async)\n\n` +
|
|
1177
|
+
`WHY: this.data represents loaded state. Modifying it outside these methods bypasses the framework's render cycle.\n\n` +
|
|
1178
|
+
`FIX: Modify this.data in on_create() (for defaults) or on_load() (for fetched data):\n` +
|
|
1179
|
+
` ❌ In on_ready(): this.data = {...};\n` +
|
|
1180
|
+
` ✅ In on_create(): this.data.count = 0; // Set default\n` +
|
|
1181
|
+
` ✅ In on_load(): this.data = await fetch(...); // Fetch from API\n` +
|
|
1182
|
+
` ✅ For component state: this.args.count = 5; (accessible in on_load)`);
|
|
1183
|
+
throw new Error(`[JQHTML] Cannot reassign this.data outside of on_create() or on_load(). ` +
|
|
1184
|
+
`this.data is frozen after on_create() and unfrozen only during on_load().`);
|
|
1185
|
+
}
|
|
1186
|
+
// When setting, wrap the new value in a proxy too
|
|
1187
|
+
_data = createDataProxy(value);
|
|
1188
|
+
},
|
|
1189
|
+
enumerable: true,
|
|
1190
|
+
configurable: false
|
|
1191
|
+
});
|
|
1192
|
+
// Initialize this.state as an empty object (convention for arbitrary component state)
|
|
1193
|
+
// No special meaning to framework - mutable anywhere, not cached, not frozen
|
|
1194
|
+
this.state = {};
|
|
1195
|
+
this._log_lifecycle('construct', 'complete');
|
|
1196
|
+
}
|
|
1197
|
+
/**
|
|
1198
|
+
* Boot - Start the full component lifecycle
|
|
1199
|
+
* Called immediately after construction by instruction processor
|
|
1200
|
+
*/
|
|
1201
|
+
/**
|
|
1202
|
+
* Internal boot method - starts component lifecycle
|
|
1203
|
+
* @private
|
|
1204
|
+
*/
|
|
1205
|
+
async _boot() {
|
|
1206
|
+
// Guard: prevent double boot() calls
|
|
1207
|
+
if (this._booted)
|
|
1208
|
+
return;
|
|
1209
|
+
this._booted = true;
|
|
1210
|
+
await this._lifecycle_manager.boot_component(this);
|
|
1211
|
+
}
|
|
1212
|
+
// -------------------------------------------------------------------------
|
|
1213
|
+
// Lifecycle Methods (called by LifecycleManager)
|
|
1214
|
+
// -------------------------------------------------------------------------
|
|
1215
|
+
/**
|
|
1216
|
+
* Internal render phase - Create DOM structure
|
|
1217
|
+
* Called top-down (parent before children) when part of lifecycle
|
|
1218
|
+
* This is an internal method - users should call render() instead
|
|
1219
|
+
*
|
|
1220
|
+
* @param id Optional scoped ID - if provided, delegates to child component's _render()
|
|
1221
|
+
* @returns The current _render_count after incrementing (used to detect stale renders)
|
|
1222
|
+
* @private
|
|
1223
|
+
*/
|
|
1224
|
+
_render(id = null) {
|
|
1225
|
+
// Increment render count to track this specific render
|
|
1226
|
+
this._render_count++;
|
|
1227
|
+
const current_render_id = this._render_count;
|
|
1228
|
+
if (this._stopped)
|
|
1229
|
+
return current_render_id;
|
|
1230
|
+
// If id provided, delegate to child component
|
|
1231
|
+
if (id) {
|
|
1232
|
+
// First check if element with scoped ID exists
|
|
1233
|
+
const $element = this.$sid(id);
|
|
1234
|
+
if ($element.length === 0) {
|
|
1235
|
+
throw new Error(`[JQHTML] render("${id}") - no such id.\n` +
|
|
1236
|
+
`Component "${this.component_name()}" has no child element with $sid="${id}".`);
|
|
1237
|
+
}
|
|
1238
|
+
// Element exists, check if it's a component
|
|
1239
|
+
const child = $element.data('_component');
|
|
1240
|
+
if (!child) {
|
|
1241
|
+
throw new Error(`[JQHTML] render("${id}") - element is not a component or does not have $redrawable attribute set.\n` +
|
|
1242
|
+
`Element with $sid="${id}" exists but is not initialized as a component.\n` +
|
|
1243
|
+
`Add $redrawable attribute or make it a proper component.`);
|
|
1244
|
+
}
|
|
1245
|
+
return child._render();
|
|
1246
|
+
}
|
|
1247
|
+
// Prevent render() calls during on_load()
|
|
1248
|
+
if (this.__loading) {
|
|
1249
|
+
throw new Error(`[JQHTML] Component "${this.component_name()}" attempted to call render() during on_load().\n` +
|
|
1250
|
+
`on_load() should ONLY modify this.data. DOM updates happen automatically after on_load() completes.\n\n` +
|
|
1251
|
+
`Fix: Remove the this.render() call from on_load().\n` +
|
|
1252
|
+
`The framework will automatically re-render if this.data changes during on_load().`);
|
|
1253
|
+
}
|
|
1254
|
+
this._log_lifecycle('render', 'start');
|
|
1255
|
+
// Determine child-finding strategy: If component is off-DOM, children can't register
|
|
1256
|
+
// via _find_dom_parent() (no parent in DOM to find), so we'll need find() fallback later.
|
|
1257
|
+
// If attached to DOM, children register normally and we can use the fast _dom_children path.
|
|
1258
|
+
// Check is cheap ($.contains uses native Node.contains, ~500K ops/sec).
|
|
1259
|
+
if (!$.contains(document.documentElement, this.$[0])) {
|
|
1260
|
+
this._use_dom_fallback = true;
|
|
1261
|
+
}
|
|
1262
|
+
else {
|
|
1263
|
+
this._use_dom_fallback = false;
|
|
1264
|
+
}
|
|
1265
|
+
// If this is not the first render, stop child components and clear DOM
|
|
1266
|
+
if (this._did_first_render) {
|
|
1267
|
+
// Stop all child components before clearing DOM
|
|
1268
|
+
this.$.find('.Component').each(function () {
|
|
1269
|
+
const child = $(this).data('_component');
|
|
1270
|
+
if (child && !child._stopped) {
|
|
1271
|
+
child._stop(); // Stop just the component, DOM will be cleared next
|
|
1272
|
+
}
|
|
1273
|
+
});
|
|
1274
|
+
// Clear the DOM
|
|
1275
|
+
this.$[0].innerHTML = '';
|
|
1276
|
+
}
|
|
1277
|
+
else {
|
|
1278
|
+
this._did_first_render = true;
|
|
1279
|
+
}
|
|
1280
|
+
// Remove _Component_Stopped class if present (allows re-render after stop)
|
|
1281
|
+
this.$.removeClass('_Component_Stopped');
|
|
1282
|
+
// Capture data state before first render for comparison later
|
|
1283
|
+
if (this._data_before_render === null) {
|
|
1284
|
+
this._data_before_render = JSON.stringify(this.data);
|
|
1285
|
+
}
|
|
1286
|
+
// Clear DOM children tracking - they're destroyed by innerHTML = '' anyway
|
|
1287
|
+
this._dom_children.clear();
|
|
1288
|
+
// Get template and render it
|
|
1289
|
+
let template_def;
|
|
1290
|
+
// If we have a component name, try looking up by name first (for unregistered components)
|
|
1291
|
+
if (this.args._component_name) {
|
|
1292
|
+
template_def = get_template(this.args._component_name);
|
|
1293
|
+
}
|
|
1294
|
+
else {
|
|
1295
|
+
// Otherwise look up by class
|
|
1296
|
+
template_def = get_template_by_class(this.constructor);
|
|
1297
|
+
}
|
|
1298
|
+
if (template_def && template_def.render) {
|
|
1299
|
+
// Create jqhtml utilities object
|
|
1300
|
+
const jqhtml = {
|
|
1301
|
+
escape_html: (str) => {
|
|
1302
|
+
const div = document.createElement('div');
|
|
1303
|
+
div.textContent = String(str);
|
|
1304
|
+
return div.innerHTML;
|
|
1305
|
+
}
|
|
1306
|
+
};
|
|
1307
|
+
// Execute template function
|
|
1308
|
+
// Commented out debug logging
|
|
1309
|
+
// console.log('[Component] About to call render, this:', this);
|
|
1310
|
+
// console.log('[Component] this.constructor.name:', this.constructor.name);
|
|
1311
|
+
// console.log('[Component] this.data:', this.data);
|
|
1312
|
+
// console.log('[Component] this.args:', this.args);
|
|
1313
|
+
// Create content function that handles both default content and named slots
|
|
1314
|
+
const createContentFunction = () => {
|
|
1315
|
+
const defaultContentFn = this.args._innerhtml_function;
|
|
1316
|
+
const slotsObj = this.args._slots;
|
|
1317
|
+
// Return a function that handles both slot names and default content
|
|
1318
|
+
return (slotName, ...slotArgs) => {
|
|
1319
|
+
// If slot name provided and slots object exists, try to use named slot
|
|
1320
|
+
if (slotName && slotsObj && slotsObj[slotName]) {
|
|
1321
|
+
// Call the slot function - it returns [instructions, context]
|
|
1322
|
+
return slotsObj[slotName](...slotArgs);
|
|
1323
|
+
}
|
|
1324
|
+
// If slot name provided but not found, return empty
|
|
1325
|
+
else if (slotName) {
|
|
1326
|
+
return '';
|
|
1327
|
+
}
|
|
1328
|
+
// No slot name = default content
|
|
1329
|
+
else if (defaultContentFn) {
|
|
1330
|
+
return defaultContentFn(this);
|
|
1331
|
+
}
|
|
1332
|
+
// No content at all
|
|
1333
|
+
else {
|
|
1334
|
+
return '';
|
|
1335
|
+
}
|
|
1336
|
+
};
|
|
1337
|
+
};
|
|
1338
|
+
const contentFunction = createContentFunction();
|
|
1339
|
+
let [instructions, context] = template_def.render.bind(this)(this.data, this.args, contentFunction, // Content function with slot support
|
|
1340
|
+
jqhtml // Utilities object
|
|
1341
|
+
);
|
|
1342
|
+
// Check for template inheritance (slot-only OR explicit extends)
|
|
1343
|
+
// If instructions is {_slots: {...}}, find parent template and invoke it
|
|
1344
|
+
if (instructions && typeof instructions === 'object' && instructions._slots && !Array.isArray(instructions)) {
|
|
1345
|
+
const componentName = template_def.name || this.args._component_name || this.constructor.name;
|
|
1346
|
+
console.log(`[JQHTML] Slot-only template detected for ${componentName}`);
|
|
1347
|
+
let parentTemplate = null;
|
|
1348
|
+
let parentTemplateName = null;
|
|
1349
|
+
// First check for explicit extends attribute in template metadata
|
|
1350
|
+
if (template_def.extends) {
|
|
1351
|
+
console.log(`[JQHTML] Using explicit extends: ${template_def.extends}`);
|
|
1352
|
+
parentTemplate = get_template(template_def.extends);
|
|
1353
|
+
parentTemplateName = template_def.extends;
|
|
1354
|
+
}
|
|
1355
|
+
// If no explicit extends, walk the prototype chain to find parent class with template
|
|
1356
|
+
if (!parentTemplate) {
|
|
1357
|
+
let currentClass = Object.getPrototypeOf(this.constructor);
|
|
1358
|
+
while (currentClass && currentClass.name !== 'Object' && currentClass.name !== 'Jqhtml_Component') {
|
|
1359
|
+
const className = currentClass.name;
|
|
1360
|
+
console.log(`[JQHTML] Checking parent: ${className}`);
|
|
1361
|
+
try {
|
|
1362
|
+
const classTemplate = get_template(className);
|
|
1363
|
+
if (classTemplate && classTemplate.name !== 'Jqhtml_Component') {
|
|
1364
|
+
console.log(`[JQHTML] Found parent template: ${className}`);
|
|
1365
|
+
parentTemplate = classTemplate;
|
|
1366
|
+
parentTemplateName = className;
|
|
1367
|
+
break;
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
catch (error) {
|
|
1371
|
+
console.warn(`[JQHTML] Error finding parent template ${className}:`, error);
|
|
1372
|
+
}
|
|
1373
|
+
currentClass = Object.getPrototypeOf(currentClass);
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
// If we found a parent template, invoke it with child's slots
|
|
1377
|
+
if (parentTemplate) {
|
|
1378
|
+
try {
|
|
1379
|
+
// Create a content function that invokes child slots
|
|
1380
|
+
// When parent calls content('slotName'), it invokes the child's slot function
|
|
1381
|
+
const childSlots = instructions._slots;
|
|
1382
|
+
const contentFunction = (slotName, data) => {
|
|
1383
|
+
if (childSlots[slotName] && typeof childSlots[slotName] === 'function') {
|
|
1384
|
+
// Invoke the slot function with data parameter
|
|
1385
|
+
const [slotInstructions, slotContext] = childSlots[slotName](data);
|
|
1386
|
+
// Return in render function format: [instructions, context]
|
|
1387
|
+
// The template expression handler expects this format
|
|
1388
|
+
return [slotInstructions, slotContext];
|
|
1389
|
+
}
|
|
1390
|
+
// Slot not found, return empty
|
|
1391
|
+
return '';
|
|
1392
|
+
};
|
|
1393
|
+
// Invoke parent template with child's slots as content function
|
|
1394
|
+
const [parentInstructions, parentContext] = parentTemplate.render.bind(this)(this.data, this.args, contentFunction, // Pass content function that invokes child slots
|
|
1395
|
+
jqhtml);
|
|
1396
|
+
console.log(`[JQHTML] Parent template invoked successfully`);
|
|
1397
|
+
instructions = parentInstructions;
|
|
1398
|
+
context = parentContext;
|
|
1399
|
+
}
|
|
1400
|
+
catch (error) {
|
|
1401
|
+
console.warn(`[JQHTML] Error invoking parent template ${parentTemplateName}:`, error);
|
|
1402
|
+
instructions = [];
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
else {
|
|
1406
|
+
console.warn(`[JQHTML] No parent template found for ${this.constructor.name}, rendering empty`);
|
|
1407
|
+
instructions = [];
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
// Flatten any nested content instructions before processing
|
|
1411
|
+
// Instructions may contain ['_content', [...]] markers from content() calls
|
|
1412
|
+
const flattenedInstructions = this._flatten_instructions(instructions);
|
|
1413
|
+
// Process instructions to generate DOM
|
|
1414
|
+
// This kicks off child component boots but doesn't wait for them
|
|
1415
|
+
process_instructions(flattenedInstructions, this.$, this);
|
|
1416
|
+
}
|
|
1417
|
+
// Don't update ready state here - let phases complete in order
|
|
1418
|
+
this._update_debug_attrs();
|
|
1419
|
+
this._log_lifecycle('render', 'complete');
|
|
1420
|
+
// Call on_render() immediately after render completes
|
|
1421
|
+
// This ensures event handlers are always re-attached after DOM updates
|
|
1422
|
+
const renderResult = this.on_render();
|
|
1423
|
+
if (renderResult && typeof renderResult.then === 'function') {
|
|
1424
|
+
console.warn(`[JQHTML] Component "${this.component_name()}" returned a Promise from on_render(). ` +
|
|
1425
|
+
`on_render() must be synchronous code. Remove 'async' from the function declaration.`);
|
|
1426
|
+
}
|
|
1427
|
+
// Emit lifecycle event
|
|
1428
|
+
this.trigger('render');
|
|
1429
|
+
// Apply debug delay after render
|
|
1430
|
+
const isRerender = this._ready_state >= 3; // Already rendered once
|
|
1431
|
+
applyDebugDelay(isRerender ? 'rerender' : 'render');
|
|
1432
|
+
// Store args snapshot for reload() comparison (skip if non-serializable)
|
|
1433
|
+
try {
|
|
1434
|
+
this._args_on_last_render = JSON.parse(JSON.stringify(this.args));
|
|
1435
|
+
}
|
|
1436
|
+
catch (error) {
|
|
1437
|
+
// Args contain circular references - skip snapshot (reload comparison will be disabled)
|
|
1438
|
+
this._args_on_last_render = null;
|
|
1439
|
+
}
|
|
1440
|
+
// Store data snapshot for refresh() comparison
|
|
1441
|
+
this._data_on_last_render = JSON.stringify(this.data);
|
|
1442
|
+
// Return the render ID so callers can check if this render is still current
|
|
1443
|
+
return current_render_id;
|
|
1444
|
+
}
|
|
1445
|
+
/**
|
|
1446
|
+
* Public render method - re-renders component and completes lifecycle
|
|
1447
|
+
* This is what users should call when they want to update a component.
|
|
1448
|
+
*
|
|
1449
|
+
* Lifecycle sequence:
|
|
1450
|
+
* 1. _render() - Updates DOM synchronously, calls on_render(), fires 'render' event
|
|
1451
|
+
* 2. Async continuation (fire and forget):
|
|
1452
|
+
* - _wait_for_children_ready() - Waits for all children to reach ready state
|
|
1453
|
+
* - on_ready() - Calls user's ready hook
|
|
1454
|
+
* - trigger('ready') - Fires ready event
|
|
1455
|
+
*
|
|
1456
|
+
* Returns immediately after _render() completes - does NOT wait for children
|
|
1457
|
+
*/
|
|
1458
|
+
render(id = null) {
|
|
1459
|
+
if (this._stopped)
|
|
1460
|
+
return;
|
|
1461
|
+
// If id provided, delegate to child component
|
|
1462
|
+
if (id) {
|
|
1463
|
+
const $element = this.$sid(id);
|
|
1464
|
+
if ($element.length === 0) {
|
|
1465
|
+
throw new Error(`[JQHTML] render("${id}") - no such id.\n` +
|
|
1466
|
+
`Component "${this.component_name()}" has no child element with $sid="${id}".`);
|
|
1467
|
+
}
|
|
1468
|
+
const child = $element.data('_component');
|
|
1469
|
+
if (!child) {
|
|
1470
|
+
throw new Error(`[JQHTML] render("${id}") - element is not a component or does not have $redrawable attribute set.\n` +
|
|
1471
|
+
`Element with $sid="${id}" exists but is not initialized as a component.\n` +
|
|
1472
|
+
`Add $redrawable attribute or make it a proper component.`);
|
|
1473
|
+
}
|
|
1474
|
+
return child.render();
|
|
1475
|
+
}
|
|
1476
|
+
// Execute render phase synchronously and capture render ID
|
|
1477
|
+
const render_id = this._render();
|
|
1478
|
+
// Fire off async lifecycle completion in background (don't await)
|
|
1479
|
+
(async () => {
|
|
1480
|
+
// Wait for all child components to be ready
|
|
1481
|
+
await this._wait_for_children_ready();
|
|
1482
|
+
// Check if this render is still current before calling on_ready
|
|
1483
|
+
// If _render_count changed, another render happened and we should skip on_ready
|
|
1484
|
+
if (this._render_count !== render_id) {
|
|
1485
|
+
return; // Stale render, don't call on_ready
|
|
1486
|
+
}
|
|
1487
|
+
// Call on_ready hook
|
|
1488
|
+
await this.on_ready();
|
|
1489
|
+
// Trigger ready event
|
|
1490
|
+
await this.trigger('ready');
|
|
1491
|
+
})();
|
|
1492
|
+
}
|
|
1493
|
+
/**
|
|
1494
|
+
* Alias for render() - re-renders component with current data
|
|
1495
|
+
* Provided for API consistency and clarity
|
|
1496
|
+
*/
|
|
1497
|
+
redraw(id = null) {
|
|
1498
|
+
return this.render(id);
|
|
1499
|
+
}
|
|
1500
|
+
/**
|
|
1501
|
+
* Create phase - Quick setup, prepare UI
|
|
1502
|
+
* Called bottom-up (children before parent)
|
|
1503
|
+
*/
|
|
1504
|
+
async create() {
|
|
1505
|
+
if (this._stopped || this._ready_state >= 1)
|
|
1506
|
+
return;
|
|
1507
|
+
this._log_lifecycle('create', 'start');
|
|
1508
|
+
// Call on_create() and validate it's synchronous
|
|
1509
|
+
const result = this.on_create();
|
|
1510
|
+
if (result && typeof result.then === 'function') {
|
|
1511
|
+
console.warn(`[JQHTML] Component "${this.component_name()}" returned a Promise from on_create(). ` +
|
|
1512
|
+
`on_create() must be synchronous code. Remove 'async' from the function declaration.`);
|
|
1513
|
+
await result; // Still wait for it to avoid breaking existing code
|
|
1514
|
+
}
|
|
1515
|
+
// CACHE READ - Hydrate this.data from cache BEFORE first render
|
|
1516
|
+
// This happens after on_create() but before render, allowing instant first render with cached data
|
|
1517
|
+
const { Load_Coordinator } = await Promise.resolve().then(function () { return loadCoordinator; });
|
|
1518
|
+
const { Jqhtml_Local_Storage } = await Promise.resolve().then(function () { return localStorage$1; });
|
|
1519
|
+
// Check if component implements cache_id() for custom cache key
|
|
1520
|
+
let cache_key = null;
|
|
1521
|
+
let uncacheable_property;
|
|
1522
|
+
if (typeof this.cache_id === 'function') {
|
|
1523
|
+
try {
|
|
1524
|
+
const custom_cache_id = this.cache_id();
|
|
1525
|
+
cache_key = `${this.component_name()}::${String(custom_cache_id)}`;
|
|
1526
|
+
}
|
|
1527
|
+
catch (error) {
|
|
1528
|
+
// cache_id() threw error - disable caching
|
|
1529
|
+
uncacheable_property = 'cache_id()';
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
else {
|
|
1533
|
+
// Use standard args-based cache key generation
|
|
1534
|
+
const result = Load_Coordinator.generate_invocation_key(this.component_name(), this.args);
|
|
1535
|
+
cache_key = result.key;
|
|
1536
|
+
uncacheable_property = result.uncacheable_property;
|
|
1537
|
+
}
|
|
1538
|
+
// If cache_key is null, caching disabled
|
|
1539
|
+
if (cache_key === null) {
|
|
1540
|
+
// Set data-nocache attribute for debugging (shows which property prevented caching)
|
|
1541
|
+
if (uncacheable_property) {
|
|
1542
|
+
this.$.attr('data-nocache', uncacheable_property);
|
|
1543
|
+
}
|
|
1544
|
+
if (window.jqhtml?.debug?.verbose) {
|
|
1545
|
+
console.log(`[Cache] Component ${this._cid} (${this.component_name()}) has non-serializable args - caching disabled`, { uncacheable_property });
|
|
1546
|
+
}
|
|
1547
|
+
return; // Skip cache check
|
|
1548
|
+
}
|
|
1549
|
+
if (window.jqhtml?.debug?.verbose) {
|
|
1550
|
+
console.log(`[Cache] Component ${this._cid} (${this.component_name()}) checking cache in create()`, { cache_key, has_cache_key_set: Jqhtml_Local_Storage.has_cache_key() });
|
|
1551
|
+
}
|
|
1552
|
+
const cached_data = Jqhtml_Local_Storage.get(cache_key);
|
|
1553
|
+
if (cached_data !== null) {
|
|
1554
|
+
this.data = cached_data;
|
|
1555
|
+
if (window.jqhtml?.debug?.verbose) {
|
|
1556
|
+
console.log(`[Cache] Component ${this._cid} (${this.component_name()}) hydrated from cache in create()`, { cache_key, data: this.data });
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
else {
|
|
1560
|
+
if (window.jqhtml?.debug?.verbose) {
|
|
1561
|
+
console.log(`[Cache] Component ${this._cid} (${this.component_name()}) cache miss in create()`, { cache_key });
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
// Snapshot this.data after on_create() completes
|
|
1565
|
+
// This will be restored before each on_load() execution to reset state
|
|
1566
|
+
this.__initial_data_snapshot = JSON.parse(JSON.stringify(this.data));
|
|
1567
|
+
// Freeze this.data after on_create() - only on_load() can modify it now
|
|
1568
|
+
this.__data_frozen = true;
|
|
1569
|
+
this._ready_state = 1;
|
|
1570
|
+
this._update_debug_attrs();
|
|
1571
|
+
this._log_lifecycle('create', 'complete');
|
|
1572
|
+
// Emit lifecycle event
|
|
1573
|
+
this.trigger('create');
|
|
1574
|
+
}
|
|
1575
|
+
/**
|
|
1576
|
+
* Load phase - Fetch data from APIs
|
|
1577
|
+
* Called bottom-up, fully parallel
|
|
1578
|
+
* NO DOM MODIFICATIONS ALLOWED IN THIS PHASE
|
|
1579
|
+
* @private - Internal lifecycle method, not for external use
|
|
1580
|
+
*/
|
|
1581
|
+
async _load() {
|
|
1582
|
+
if (this._stopped || this._ready_state >= 2)
|
|
1583
|
+
return;
|
|
1584
|
+
this._log_lifecycle('load', 'start');
|
|
1585
|
+
// Restore this.data to initial state from snapshot (skip on first load)
|
|
1586
|
+
// This ensures on_load() always starts with clean state
|
|
1587
|
+
const is_first_load = this._ready_state < 2;
|
|
1588
|
+
if (!is_first_load && this.__initial_data_snapshot) {
|
|
1589
|
+
this.data = JSON.parse(JSON.stringify(this.__initial_data_snapshot));
|
|
1590
|
+
}
|
|
1591
|
+
// Unfreeze this.data so on_load() can modify it
|
|
1592
|
+
this.__data_frozen = false;
|
|
1593
|
+
// Import coordinator and storage lazily to avoid circular dependency
|
|
1594
|
+
const { Load_Coordinator } = await Promise.resolve().then(function () { return loadCoordinator; });
|
|
1595
|
+
const { Jqhtml_Local_Storage } = await Promise.resolve().then(function () { return localStorage$1; });
|
|
1596
|
+
// Check if component implements cache_id() for custom cache key
|
|
1597
|
+
let cache_key = null;
|
|
1598
|
+
let uncacheable_property;
|
|
1599
|
+
if (typeof this.cache_id === 'function') {
|
|
1600
|
+
try {
|
|
1601
|
+
const custom_cache_id = this.cache_id();
|
|
1602
|
+
cache_key = `${this.component_name()}::${String(custom_cache_id)}`;
|
|
1603
|
+
}
|
|
1604
|
+
catch (error) {
|
|
1605
|
+
// cache_id() threw error - disable caching
|
|
1606
|
+
uncacheable_property = 'cache_id()';
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
else {
|
|
1610
|
+
// Use standard args-based cache key generation
|
|
1611
|
+
const result = Load_Coordinator.generate_invocation_key(this.component_name(), this.args);
|
|
1612
|
+
cache_key = result.key;
|
|
1613
|
+
uncacheable_property = result.uncacheable_property;
|
|
1614
|
+
}
|
|
1615
|
+
// If cache_key is null, args are not serializable - skip load deduplication and caching
|
|
1616
|
+
if (cache_key === null) {
|
|
1617
|
+
// Set data-nocache attribute for debugging (shows which property prevented caching)
|
|
1618
|
+
if (uncacheable_property) {
|
|
1619
|
+
this.$.attr('data-nocache', uncacheable_property);
|
|
1620
|
+
}
|
|
1621
|
+
if (window.jqhtml?.debug?.verbose) {
|
|
1622
|
+
console.log(`[Cache] Component ${this._cid} (${this.component_name()}) has non-serializable args - load deduplication and caching disabled`, { uncacheable_property });
|
|
1623
|
+
}
|
|
1624
|
+
// Execute on_load() without deduplication or caching
|
|
1625
|
+
await this.on_load();
|
|
1626
|
+
this.__data_frozen = true;
|
|
1627
|
+
return;
|
|
1628
|
+
}
|
|
1629
|
+
// Store "before" snapshot for comparison after on_load()
|
|
1630
|
+
const data_before_load = JSON.stringify(this.data);
|
|
1631
|
+
// Check if this component should execute on_load() or wait for existing request
|
|
1632
|
+
const should_execute = Load_Coordinator.should_execute_on_load(this);
|
|
1633
|
+
if (!should_execute) {
|
|
1634
|
+
// This component is a follower - wait for leader to complete
|
|
1635
|
+
if (window.jqhtml?.debug?.verbose) {
|
|
1636
|
+
console.log(`[Load Deduplication] Component ${this._cid} (${this.component_name()}) is a follower, waiting for leader`, { args: this.args });
|
|
1637
|
+
}
|
|
1638
|
+
const coordination_promise = Load_Coordinator.get_coordination_promise(this);
|
|
1639
|
+
if (coordination_promise) {
|
|
1640
|
+
try {
|
|
1641
|
+
// Wait for leader to complete
|
|
1642
|
+
await coordination_promise;
|
|
1643
|
+
// Data has already been copied by coordinator
|
|
1644
|
+
if (window.jqhtml?.debug?.verbose) {
|
|
1645
|
+
console.log(`[Load Deduplication] Component ${this._cid} received data from leader`, { data: this.data });
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
catch (error) {
|
|
1649
|
+
// Leader failed - error will propagate naturally
|
|
1650
|
+
console.error(`[Load Deduplication] Component ${this._cid} failed due to leader error:`, error);
|
|
1651
|
+
throw error;
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
this._ready_state = 2;
|
|
1655
|
+
this._update_debug_attrs();
|
|
1656
|
+
this._log_lifecycle('load', 'complete (follower)');
|
|
1657
|
+
this.trigger('load');
|
|
1658
|
+
return;
|
|
1659
|
+
}
|
|
1660
|
+
// This component is a leader - execute on_load() normally
|
|
1661
|
+
if (window.jqhtml?.debug?.verbose) {
|
|
1662
|
+
console.log(`[Load Deduplication] Component ${this._cid} (${this.component_name()}) is the leader`, { args: this.args });
|
|
1663
|
+
}
|
|
1664
|
+
// Capture state before on_load() for validation
|
|
1665
|
+
let argsBeforeLoad = null;
|
|
1666
|
+
try {
|
|
1667
|
+
argsBeforeLoad = JSON.stringify(this.args);
|
|
1668
|
+
}
|
|
1669
|
+
catch (error) {
|
|
1670
|
+
// Args contain circular references - skip validation
|
|
1671
|
+
}
|
|
1672
|
+
const propertiesBeforeLoad = new Set(Object.keys(this));
|
|
1673
|
+
// Set loading flag to prevent render() calls during on_load()
|
|
1674
|
+
this.__loading = true;
|
|
1675
|
+
// Create restricted proxy to prevent DOM access during on_load()
|
|
1676
|
+
const restricted_this = new Proxy(this, {
|
|
1677
|
+
get(target, prop) {
|
|
1678
|
+
// Only allow access to this.args and this.data
|
|
1679
|
+
if (prop === 'args' || prop === 'data') {
|
|
1680
|
+
return target[prop];
|
|
1681
|
+
}
|
|
1682
|
+
// Block everything else
|
|
1683
|
+
console.error(`[JQHTML] ERROR: Component "${target.component_name()}" attempted to access this.${String(prop)} during on_load().\n\n` +
|
|
1684
|
+
`RESTRICTION: on_load() may ONLY access:\n` +
|
|
1685
|
+
` - this.args (read during on_load, modify before/after)\n` +
|
|
1686
|
+
` - this.data (read/write)\n\n` +
|
|
1687
|
+
`WHY: on_load() is for data fetching only. All other component functionality should happen in other lifecycle methods.\n\n` +
|
|
1688
|
+
`FIX:\n` +
|
|
1689
|
+
` - DOM manipulation → use on_render() or on_ready()\n` +
|
|
1690
|
+
` - Component methods → call them before/after on_load(), not inside it\n` +
|
|
1691
|
+
` - Other properties → restructure code to only use this.args and this.data in on_load()`);
|
|
1692
|
+
throw new Error(`[JQHTML] Cannot access this.${String(prop)} during on_load(). ` +
|
|
1693
|
+
`on_load() may only access this.args and this.data.`);
|
|
1694
|
+
},
|
|
1695
|
+
set(target, prop, value) {
|
|
1696
|
+
// Only allow setting this.data
|
|
1697
|
+
if (prop === 'data') {
|
|
1698
|
+
target[prop] = value;
|
|
1699
|
+
return true;
|
|
1700
|
+
}
|
|
1701
|
+
// Block setting this.args
|
|
1702
|
+
if (prop === 'args') {
|
|
1703
|
+
console.error(`[JQHTML] ERROR: Component "${target.component_name()}" attempted to modify this.args during on_load().\n\n` +
|
|
1704
|
+
`RESTRICTION: on_load() may ONLY modify:\n` +
|
|
1705
|
+
` - this.data (read/write)\n\n` +
|
|
1706
|
+
`WHY: this.args is component state that on_load() depends on. Modifying it inside on_load() creates circular dependencies.\n\n` +
|
|
1707
|
+
`FIX: Modify this.args BEFORE calling on_load() (in on_create() or other lifecycle methods), not inside on_load():\n` +
|
|
1708
|
+
` ❌ Inside on_load(): this.args.filter = 'new_value';\n` +
|
|
1709
|
+
` ✅ In on_create(): this.args.filter = this.args.initial_filter || 'default';`);
|
|
1710
|
+
throw new Error(`[JQHTML] Cannot modify this.args during on_load(). ` +
|
|
1711
|
+
`Modify this.args in other lifecycle methods, not inside on_load().`);
|
|
1712
|
+
}
|
|
1713
|
+
// Block setting any other properties
|
|
1714
|
+
console.error(`[JQHTML] ERROR: Component "${target.component_name()}" attempted to modify this.${String(prop)} during on_load().\n\n` +
|
|
1715
|
+
`RESTRICTION: on_load() may ONLY modify:\n` +
|
|
1716
|
+
` - this.data (read/write)\n\n` +
|
|
1717
|
+
`WHY: on_load() is for data fetching only. Setting properties on the component instance should happen in other lifecycle methods.\n\n` +
|
|
1718
|
+
`FIX: Store your data in this.data instead:\n` +
|
|
1719
|
+
` ❌ this.${String(prop)} = value;\n` +
|
|
1720
|
+
` ✅ this.data.${String(prop)} = value;`);
|
|
1721
|
+
throw new Error(`[JQHTML] Cannot modify this.${String(prop)} during on_load(). ` +
|
|
1722
|
+
`Only this.data can be modified in on_load().`);
|
|
1723
|
+
}
|
|
1724
|
+
});
|
|
1725
|
+
// Create promise for this on_load() call with restricted this context
|
|
1726
|
+
const on_load_promise = (async () => {
|
|
1727
|
+
try {
|
|
1728
|
+
// TODO: Implement proper error handling for on_load() failures
|
|
1729
|
+
// - Should errors trigger re-render with error state?
|
|
1730
|
+
// - Should errors clear cached data?
|
|
1731
|
+
// - Should errors reset state machine flags (next_reload_force_refresh)?
|
|
1732
|
+
// - Should partial data be preserved or rolled back?
|
|
1733
|
+
// - Should followers be notified differently based on error type?
|
|
1734
|
+
await this.on_load.call(restricted_this);
|
|
1735
|
+
}
|
|
1736
|
+
catch (error) {
|
|
1737
|
+
// Handle error and notify coordinator
|
|
1738
|
+
Load_Coordinator.handle_leader_error(this, error);
|
|
1739
|
+
throw error;
|
|
1740
|
+
}
|
|
1741
|
+
})();
|
|
1742
|
+
// Register as leader and get cleanup function
|
|
1743
|
+
const complete_coordination = Load_Coordinator.register_leader(this, on_load_promise);
|
|
1744
|
+
try {
|
|
1745
|
+
await on_load_promise;
|
|
1746
|
+
}
|
|
1747
|
+
finally {
|
|
1748
|
+
// Always clear loading flag and complete coordination
|
|
1749
|
+
this.__loading = false;
|
|
1750
|
+
complete_coordination();
|
|
1751
|
+
// Freeze this.data after on_load() completes
|
|
1752
|
+
this.__data_frozen = true;
|
|
1753
|
+
}
|
|
1754
|
+
// Validate that on_load() only modified this.data
|
|
1755
|
+
let argsAfterLoad = null;
|
|
1756
|
+
try {
|
|
1757
|
+
argsAfterLoad = JSON.stringify(this.args);
|
|
1758
|
+
}
|
|
1759
|
+
catch (error) {
|
|
1760
|
+
// Args contain circular references - skip validation
|
|
1761
|
+
}
|
|
1762
|
+
const propertiesAfterLoad = Object.keys(this);
|
|
1763
|
+
// Check if args were modified (skip if args are non-serializable)
|
|
1764
|
+
if (argsBeforeLoad !== null && argsAfterLoad !== null && argsBeforeLoad !== argsAfterLoad) {
|
|
1765
|
+
console.error(`[JQHTML] WARNING: Component "${this.component_name()}" modified this.args in on_load().\n` +
|
|
1766
|
+
`on_load() should ONLY modify this.data.\n\n` +
|
|
1767
|
+
`Before: ${argsBeforeLoad}\n` +
|
|
1768
|
+
`After: ${argsAfterLoad}\n\n` +
|
|
1769
|
+
`Fix: Modify this.args in on_create() or other lifecycle methods, not in on_load().\n` +
|
|
1770
|
+
`this.args stores state that on_load() depends on. Modifying it inside on_load() creates circular dependencies.`);
|
|
1771
|
+
}
|
|
1772
|
+
// Check if new properties were added to the component instance
|
|
1773
|
+
const newProperties = propertiesAfterLoad.filter(prop => !propertiesBeforeLoad.has(prop) && prop !== 'data');
|
|
1774
|
+
if (newProperties.length > 0) {
|
|
1775
|
+
console.error(`[JQHTML] WARNING: Component "${this.component_name()}" added new properties in on_load().\n` +
|
|
1776
|
+
`on_load() should ONLY modify this.data. New properties detected: ${newProperties.join(', ')}\n\n` +
|
|
1777
|
+
`Fix: Store your data in this.data instead:\n` +
|
|
1778
|
+
` ❌ this.${newProperties[0]} = value;\n` +
|
|
1779
|
+
` ✅ this.data.${newProperties[0]} = value;`);
|
|
1780
|
+
}
|
|
1781
|
+
// Check if data changed during on_load() - if so, update cache (but not if empty)
|
|
1782
|
+
const data_after_load = JSON.stringify(this.data);
|
|
1783
|
+
if (data_after_load !== data_before_load && data_after_load !== '{}') {
|
|
1784
|
+
Jqhtml_Local_Storage.set(cache_key, this.data);
|
|
1785
|
+
if (window.jqhtml?.debug?.verbose) {
|
|
1786
|
+
console.log(`[Cache] Component ${this._cid} (${this.component_name()}) updated cache after on_load()`, { cache_key, data: this.data });
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
this._ready_state = 2;
|
|
1790
|
+
this._update_debug_attrs();
|
|
1791
|
+
this._log_lifecycle('load', 'complete');
|
|
1792
|
+
// Emit lifecycle event
|
|
1793
|
+
this.trigger('load');
|
|
1794
|
+
}
|
|
1795
|
+
/**
|
|
1796
|
+
* Ready phase - Component fully initialized
|
|
1797
|
+
* Called bottom-up (children before parent)
|
|
1798
|
+
* @private
|
|
1799
|
+
*/
|
|
1800
|
+
async _ready() {
|
|
1801
|
+
if (this._stopped || this._ready_state >= 4)
|
|
1802
|
+
return;
|
|
1803
|
+
this._log_lifecycle('ready', 'start');
|
|
1804
|
+
// Wait for all children to reach ready state (bottom-up execution)
|
|
1805
|
+
await this._wait_for_children_ready();
|
|
1806
|
+
await this.on_ready();
|
|
1807
|
+
this._ready_state = 4;
|
|
1808
|
+
this._update_debug_attrs();
|
|
1809
|
+
this._log_lifecycle('ready', 'complete');
|
|
1810
|
+
// Emit lifecycle event
|
|
1811
|
+
this.trigger('ready');
|
|
1812
|
+
}
|
|
1813
|
+
/**
|
|
1814
|
+
* Public API: Wait for component to be fully ready
|
|
1815
|
+
* Returns a promise that resolves when the component reaches ready state.
|
|
1816
|
+
* Optionally accepts a callback that executes when ready.
|
|
1817
|
+
*
|
|
1818
|
+
* @example
|
|
1819
|
+
* // Promise pattern
|
|
1820
|
+
* await component.ready();
|
|
1821
|
+
*
|
|
1822
|
+
* @example
|
|
1823
|
+
* // Callback pattern
|
|
1824
|
+
* component.ready(() => {
|
|
1825
|
+
* console.log('Component is ready!');
|
|
1826
|
+
* });
|
|
1827
|
+
*
|
|
1828
|
+
* @example
|
|
1829
|
+
* // Both patterns work together
|
|
1830
|
+
* await component.ready(() => console.log('Callback fired'));
|
|
1831
|
+
*
|
|
1832
|
+
* @param callback Optional callback to execute when ready
|
|
1833
|
+
*/
|
|
1834
|
+
ready(callback) {
|
|
1835
|
+
// If already ready, execute callback immediately and resolve
|
|
1836
|
+
if (this._ready_state >= 4) {
|
|
1837
|
+
if (callback)
|
|
1838
|
+
callback();
|
|
1839
|
+
return Promise.resolve();
|
|
1840
|
+
}
|
|
1841
|
+
// Return promise that resolves when ready event fires
|
|
1842
|
+
return new Promise((resolve) => {
|
|
1843
|
+
this.on('ready', () => {
|
|
1844
|
+
if (callback)
|
|
1845
|
+
callback();
|
|
1846
|
+
resolve();
|
|
1847
|
+
});
|
|
1848
|
+
});
|
|
1849
|
+
}
|
|
1850
|
+
/**
|
|
1851
|
+
* Wait for all child components to reach ready state
|
|
1852
|
+
* Ensures bottom-up ordering (children ready before parent)
|
|
1853
|
+
* @private
|
|
1854
|
+
*/
|
|
1855
|
+
async _wait_for_children_ready() {
|
|
1856
|
+
const children = this._get_dom_children();
|
|
1857
|
+
if (children.length === 0) {
|
|
1858
|
+
return; // No children, nothing to wait for
|
|
1859
|
+
}
|
|
1860
|
+
// Create promises for each child that hasn't reached ready yet
|
|
1861
|
+
const ready_promises = [];
|
|
1862
|
+
for (const child of children) {
|
|
1863
|
+
// If child already ready, skip
|
|
1864
|
+
if (child._ready_state >= 4) {
|
|
1865
|
+
continue;
|
|
1866
|
+
}
|
|
1867
|
+
// Create promise that resolves when child reaches ready
|
|
1868
|
+
const ready_promise = new Promise((resolve) => {
|
|
1869
|
+
child.on('ready', () => resolve());
|
|
1870
|
+
});
|
|
1871
|
+
ready_promises.push(ready_promise);
|
|
1872
|
+
}
|
|
1873
|
+
// Wait for all children to be ready
|
|
1874
|
+
await Promise.all(ready_promises);
|
|
1875
|
+
}
|
|
1876
|
+
/**
|
|
1877
|
+
* Reload component - re-fetch data and re-render (debounced)
|
|
1878
|
+
*
|
|
1879
|
+
* This is the public API that automatically debounces calls to _reload()
|
|
1880
|
+
* Multiple rapid calls to reload() will be coalesced into a single execution
|
|
1881
|
+
*
|
|
1882
|
+
* @param always_render - If true (default), always re-render after on_load().
|
|
1883
|
+
* If false, only re-render if data actually changed.
|
|
1884
|
+
*/
|
|
1885
|
+
async reload(always_render) {
|
|
1886
|
+
// Default to true if undefined
|
|
1887
|
+
const force_refresh = always_render !== undefined ? always_render : true;
|
|
1888
|
+
// State machine: reload(true) takes precedence over reload(false)
|
|
1889
|
+
if (force_refresh) {
|
|
1890
|
+
this.next_reload_force_refresh = true;
|
|
1891
|
+
}
|
|
1892
|
+
else {
|
|
1893
|
+
// Only set to false if not already true
|
|
1894
|
+
if (this.next_reload_force_refresh !== true) {
|
|
1895
|
+
this.next_reload_force_refresh = false;
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
// Lazy-create debounced _reload function on first use
|
|
1899
|
+
if (!this._reload_debounced) {
|
|
1900
|
+
this._reload_debounced = this._create_debounced_function(this._reload.bind(this), 0);
|
|
1901
|
+
}
|
|
1902
|
+
return this._reload_debounced();
|
|
1903
|
+
}
|
|
1904
|
+
/**
|
|
1905
|
+
* Refresh component - re-fetch data and re-render only if data changed (debounced)
|
|
1906
|
+
*
|
|
1907
|
+
* Similar to reload() but only re-renders if the data actually changed after on_load().
|
|
1908
|
+
* Useful for checking server for updates without forcing unnecessary re-renders.
|
|
1909
|
+
*
|
|
1910
|
+
* Uses the same debouncing as reload() and plays nice with it - if reload() is called
|
|
1911
|
+
* while refresh() is queued, reload() takes precedence and will always render.
|
|
1912
|
+
*/
|
|
1913
|
+
async refresh() {
|
|
1914
|
+
return this.reload(false);
|
|
1915
|
+
}
|
|
1916
|
+
/**
|
|
1917
|
+
* Internal reload implementation - re-fetch data and re-render
|
|
1918
|
+
*
|
|
1919
|
+
* COMPLETE RELOAD PROCESS (Source of Truth):
|
|
1920
|
+
*
|
|
1921
|
+
* STEP 1: Cache check (if args changed since last render)
|
|
1922
|
+
* - Generate cache key from component name + current args
|
|
1923
|
+
* - If cached data exists and is non-empty:
|
|
1924
|
+
* - Unfreeze this.data (temporarily)
|
|
1925
|
+
* - Hydrate this.data with cached data
|
|
1926
|
+
* - Re-freeze this.data
|
|
1927
|
+
* - Render immediately (stale-while-revalidate)
|
|
1928
|
+
* - Set rendered_from_cache flag
|
|
1929
|
+
*
|
|
1930
|
+
* STEP 2: Call on_load() to fetch fresh data
|
|
1931
|
+
* - Unfreeze this.data
|
|
1932
|
+
* - Restore this.data to on_create() snapshot
|
|
1933
|
+
* - Call on_load() directly (no _load() wrapper)
|
|
1934
|
+
* - Freeze this.data after completion
|
|
1935
|
+
* - If data changed and non-empty: write to cache
|
|
1936
|
+
*
|
|
1937
|
+
* STEP 3: Conditionally re-render
|
|
1938
|
+
* - If didn't render from cache yet, OR data changed after on_load():
|
|
1939
|
+
* - Call render() to update DOM
|
|
1940
|
+
*
|
|
1941
|
+
* STEP 3.5: Wait for all children to be ready
|
|
1942
|
+
* - Bottom-up ordering (children ready before parent)
|
|
1943
|
+
* - Same as main lifecycle
|
|
1944
|
+
*
|
|
1945
|
+
* STEP 4: Call on_ready()
|
|
1946
|
+
* - Always call on_ready() after reload completes
|
|
1947
|
+
*
|
|
1948
|
+
* @private - Use reload() instead (debounced wrapper)
|
|
1949
|
+
*/
|
|
1950
|
+
async _reload() {
|
|
1951
|
+
if (this._stopped)
|
|
1952
|
+
return;
|
|
1953
|
+
this._log_lifecycle('reload', 'start');
|
|
1954
|
+
// STEP 1: Cache check (if args changed)
|
|
1955
|
+
let rendered_from_cache = false;
|
|
1956
|
+
let data_before_load = null;
|
|
1957
|
+
// Check if args changed (skip if args are non-serializable)
|
|
1958
|
+
let args_changed = false;
|
|
1959
|
+
if (this._args_on_last_render) {
|
|
1960
|
+
try {
|
|
1961
|
+
args_changed = JSON.stringify(this.args) !== JSON.stringify(this._args_on_last_render);
|
|
1962
|
+
}
|
|
1963
|
+
catch (error) {
|
|
1964
|
+
// Args contain circular references - cannot detect change, assume changed
|
|
1965
|
+
args_changed = true;
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
if (args_changed) {
|
|
1969
|
+
const { Load_Coordinator } = await Promise.resolve().then(function () { return loadCoordinator; });
|
|
1970
|
+
const { Jqhtml_Local_Storage } = await Promise.resolve().then(function () { return localStorage$1; });
|
|
1971
|
+
// Check if component implements cache_id() for custom cache key
|
|
1972
|
+
let cache_key = null;
|
|
1973
|
+
if (typeof this.cache_id === 'function') {
|
|
1974
|
+
try {
|
|
1975
|
+
const custom_cache_id = this.cache_id();
|
|
1976
|
+
cache_key = `${this.component_name()}::${String(custom_cache_id)}`;
|
|
1977
|
+
}
|
|
1978
|
+
catch (error) {
|
|
1979
|
+
// cache_id() threw error - disable caching
|
|
1980
|
+
cache_key = null;
|
|
1981
|
+
}
|
|
1982
|
+
}
|
|
1983
|
+
else {
|
|
1984
|
+
// Use standard args-based cache key generation
|
|
1985
|
+
const result = Load_Coordinator.generate_invocation_key(this.component_name(), this.args);
|
|
1986
|
+
cache_key = result.key;
|
|
1987
|
+
}
|
|
1988
|
+
// Only use cache if args are serializable
|
|
1989
|
+
if (cache_key !== null) {
|
|
1990
|
+
const cached_data = Jqhtml_Local_Storage.get(cache_key);
|
|
1991
|
+
if (cached_data !== null && JSON.stringify(cached_data) !== '{}') {
|
|
1992
|
+
if (window.jqhtml?.debug?.verbose) {
|
|
1993
|
+
console.log(`[Cache] reload() - Component ${this._cid} (${this.component_name()}) hydrated from cache (args changed)`, { cache_key, data: cached_data });
|
|
1994
|
+
}
|
|
1995
|
+
// Unfreeze to set cached data, then re-freeze
|
|
1996
|
+
this.__data_frozen = false;
|
|
1997
|
+
this.data = cached_data;
|
|
1998
|
+
this.__data_frozen = true;
|
|
1999
|
+
await this.render();
|
|
2000
|
+
rendered_from_cache = true;
|
|
2001
|
+
}
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
2004
|
+
// Capture data state before on_load for comparison
|
|
2005
|
+
data_before_load = JSON.stringify(this.data);
|
|
2006
|
+
// STEP 2: Call on_load() to fetch fresh data
|
|
2007
|
+
// Unfreeze this.data so we can restore snapshot and on_load() can modify it
|
|
2008
|
+
this.__data_frozen = false;
|
|
2009
|
+
// Restore this.data to initial state from snapshot (if not first load)
|
|
2010
|
+
if (this.__initial_data_snapshot) {
|
|
2011
|
+
this.data = JSON.parse(JSON.stringify(this.__initial_data_snapshot));
|
|
2012
|
+
}
|
|
2013
|
+
try {
|
|
2014
|
+
await this.on_load();
|
|
2015
|
+
}
|
|
2016
|
+
finally {
|
|
2017
|
+
// Freeze this.data after on_load() completes
|
|
2018
|
+
this.__data_frozen = true;
|
|
2019
|
+
}
|
|
2020
|
+
// Check if data changed during on_load() - if so, update cache (but not if empty)
|
|
2021
|
+
const data_after_load = JSON.stringify(this.data);
|
|
2022
|
+
const data_changed = data_after_load !== data_before_load;
|
|
2023
|
+
if (data_changed && data_after_load !== '{}') {
|
|
2024
|
+
const { Load_Coordinator } = await Promise.resolve().then(function () { return loadCoordinator; });
|
|
2025
|
+
const { Jqhtml_Local_Storage } = await Promise.resolve().then(function () { return localStorage$1; });
|
|
2026
|
+
// Check if component implements cache_id() for custom cache key
|
|
2027
|
+
let cache_key = null;
|
|
2028
|
+
if (typeof this.cache_id === 'function') {
|
|
2029
|
+
try {
|
|
2030
|
+
const custom_cache_id = this.cache_id();
|
|
2031
|
+
cache_key = `${this.component_name()}::${String(custom_cache_id)}`;
|
|
2032
|
+
}
|
|
2033
|
+
catch (error) {
|
|
2034
|
+
// cache_id() threw error - disable caching
|
|
2035
|
+
cache_key = null;
|
|
2036
|
+
}
|
|
2037
|
+
}
|
|
2038
|
+
else {
|
|
2039
|
+
// Use standard args-based cache key generation
|
|
2040
|
+
const result = Load_Coordinator.generate_invocation_key(this.component_name(), this.args);
|
|
2041
|
+
cache_key = result.key;
|
|
2042
|
+
}
|
|
2043
|
+
// Only update cache if args are serializable
|
|
2044
|
+
if (cache_key !== null) {
|
|
2045
|
+
Jqhtml_Local_Storage.set(cache_key, this.data);
|
|
2046
|
+
if (window.jqhtml?.debug?.verbose) {
|
|
2047
|
+
console.log(`[Cache] Component ${this._cid} (${this.component_name()}) updated cache after on_load() in reload()`, { cache_key, data: this.data });
|
|
2048
|
+
}
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
2051
|
+
// STEP 3: Conditionally re-render (with refresh() support)
|
|
2052
|
+
// Read force_refresh from state machine (default true)
|
|
2053
|
+
const force_refresh = this.next_reload_force_refresh !== null ? this.next_reload_force_refresh : true;
|
|
2054
|
+
// Track if we need to render
|
|
2055
|
+
let should_render = false;
|
|
2056
|
+
if (force_refresh) {
|
|
2057
|
+
// reload(true) or reload() - always render if we haven't yet, OR data changed
|
|
2058
|
+
should_render = !rendered_from_cache || data_changed;
|
|
2059
|
+
}
|
|
2060
|
+
else {
|
|
2061
|
+
// refresh() / reload(false) - only render if data changed from last render
|
|
2062
|
+
if (rendered_from_cache) {
|
|
2063
|
+
// First render was called (args changed, cache available)
|
|
2064
|
+
// Compare data after on_load() vs data used in that first render
|
|
2065
|
+
const data_from_first_render = this._data_on_last_render;
|
|
2066
|
+
should_render = data_after_load !== data_from_first_render;
|
|
2067
|
+
}
|
|
2068
|
+
else {
|
|
2069
|
+
// First render was NOT called (args same or no cache)
|
|
2070
|
+
// Compare data after on_load() vs last recorded render
|
|
2071
|
+
const last_rendered_data = this._data_on_last_render;
|
|
2072
|
+
should_render = data_after_load !== last_rendered_data;
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
2075
|
+
// STEP 3: Perform second render if needed
|
|
2076
|
+
if (should_render) {
|
|
2077
|
+
this._render();
|
|
2078
|
+
}
|
|
2079
|
+
// Reset state machine based on what we just executed
|
|
2080
|
+
if (force_refresh === false && this.next_reload_force_refresh === false) {
|
|
2081
|
+
this.next_reload_force_refresh = null;
|
|
2082
|
+
}
|
|
2083
|
+
else if (force_refresh === true && this.next_reload_force_refresh === true) {
|
|
2084
|
+
this.next_reload_force_refresh = null;
|
|
2085
|
+
}
|
|
2086
|
+
// If they don't match, another call was queued - leave it
|
|
2087
|
+
// STEP 3.5 & 4: Wait for children and call on_ready (only if we rendered)
|
|
2088
|
+
if (rendered_from_cache || should_render) {
|
|
2089
|
+
await this._wait_for_children_ready();
|
|
2090
|
+
await this.on_ready();
|
|
2091
|
+
}
|
|
2092
|
+
this._log_lifecycle('reload', 'complete');
|
|
2093
|
+
}
|
|
2094
|
+
/**
|
|
2095
|
+
* Destroy the component and cleanup
|
|
2096
|
+
* Called automatically by MutationObserver when component is removed from DOM
|
|
2097
|
+
* Can also be called manually for explicit cleanup
|
|
2098
|
+
*/
|
|
2099
|
+
/**
|
|
2100
|
+
* Internal stop method - stops just this component (no children)
|
|
2101
|
+
* Sets stopped flag, calls lifecycle hooks, but leaves DOM intact
|
|
2102
|
+
* @private
|
|
2103
|
+
*/
|
|
2104
|
+
_stop() {
|
|
2105
|
+
// Guard: prevent double _stop() calls
|
|
2106
|
+
if (this._stopped)
|
|
2107
|
+
return;
|
|
2108
|
+
this._stopped = true;
|
|
2109
|
+
// Early bailout: skip expensive cleanup if no handlers registered
|
|
2110
|
+
// Only matters for aborting boot() lifecycle - minimal cleanup sufficient
|
|
2111
|
+
const has_custom_stop = this.on_stop !== Jqhtml_Component.prototype.on_stop;
|
|
2112
|
+
const has_destroy_callbacks = this._on_registered('destroy');
|
|
2113
|
+
if (!has_custom_stop && !has_destroy_callbacks) {
|
|
2114
|
+
// Fast path: no cleanup logic defined, just mark as stopped
|
|
2115
|
+
this._lifecycle_manager.unregister_component(this);
|
|
2116
|
+
this._ready_state = 99;
|
|
2117
|
+
return;
|
|
2118
|
+
}
|
|
2119
|
+
// Full cleanup path: component has custom stop logic
|
|
2120
|
+
this._log_lifecycle('destroy', 'start');
|
|
2121
|
+
this.$.addClass('_Component_Stopped');
|
|
2122
|
+
// Unregister from lifecycle manager
|
|
2123
|
+
this._lifecycle_manager.unregister_component(this);
|
|
2124
|
+
// Call user's on_stop() hook
|
|
2125
|
+
const stopResult = this.on_stop();
|
|
2126
|
+
if (stopResult && typeof stopResult.then === 'function') {
|
|
2127
|
+
console.warn(`[JQHTML] Component "${this.component_name()}" returned a Promise from on_stop(). ` +
|
|
2128
|
+
`on_stop() must be synchronous code. Remove 'async' from the function declaration.`);
|
|
2129
|
+
}
|
|
2130
|
+
// Fire registered destroy callbacks
|
|
2131
|
+
this.trigger('destroy');
|
|
2132
|
+
// Trigger jQuery destroy event
|
|
2133
|
+
this.$.trigger('destroy');
|
|
2134
|
+
// Remove from DOM parent's children
|
|
2135
|
+
if (this._dom_parent) {
|
|
2136
|
+
this._dom_parent._dom_children.delete(this);
|
|
2137
|
+
}
|
|
2138
|
+
this._ready_state = 99;
|
|
2139
|
+
this._update_debug_attrs();
|
|
2140
|
+
this._log_lifecycle('destroy', 'complete');
|
|
2141
|
+
}
|
|
2142
|
+
/**
|
|
2143
|
+
* Stop component lifecycle - stops all descendant components then self
|
|
2144
|
+
* Leaves DOM intact, just stops lifecycle engine and fires cleanup hooks
|
|
2145
|
+
*/
|
|
2146
|
+
stop() {
|
|
2147
|
+
// Stop all descendant components (flat iteration, no recursion needed)
|
|
2148
|
+
this.$.find('.Component').each(function () {
|
|
2149
|
+
const child = $(this).data('_component');
|
|
2150
|
+
if (child && !child._stopped) {
|
|
2151
|
+
child._stop(); // Not recursive - we already have all descendants
|
|
2152
|
+
}
|
|
2153
|
+
});
|
|
2154
|
+
// Then stop self
|
|
2155
|
+
this._stop();
|
|
2156
|
+
}
|
|
2157
|
+
// -------------------------------------------------------------------------
|
|
2158
|
+
// Overridable Lifecycle Hooks
|
|
2159
|
+
// -------------------------------------------------------------------------
|
|
2160
|
+
on_render() { }
|
|
2161
|
+
on_create() { }
|
|
2162
|
+
async on_load() { }
|
|
2163
|
+
async on_ready() { }
|
|
2164
|
+
on_stop() { }
|
|
2165
|
+
/**
|
|
2166
|
+
* Should component re-render after load?
|
|
2167
|
+
* By default, only re-renders if data has changed
|
|
2168
|
+
* Override to control re-rendering behavior
|
|
2169
|
+
*/
|
|
2170
|
+
/**
|
|
2171
|
+
* Internal method to determine if component should re-render after on_load()
|
|
2172
|
+
* @private
|
|
2173
|
+
*/
|
|
2174
|
+
_should_rerender() {
|
|
2175
|
+
// Compare current data state with data state before initial render
|
|
2176
|
+
const currentDataState = JSON.stringify(this.data);
|
|
2177
|
+
const dataChanged = this._data_before_render !== currentDataState;
|
|
2178
|
+
// Update stored state for next comparison
|
|
2179
|
+
if (dataChanged) {
|
|
2180
|
+
this._data_before_render = currentDataState;
|
|
2181
|
+
}
|
|
2182
|
+
return dataChanged;
|
|
2183
|
+
}
|
|
2184
|
+
// -------------------------------------------------------------------------
|
|
2185
|
+
// Public API
|
|
2186
|
+
// -------------------------------------------------------------------------
|
|
2187
|
+
/**
|
|
2188
|
+
* Get component name for debugging
|
|
2189
|
+
*/
|
|
2190
|
+
component_name() {
|
|
2191
|
+
return this.constructor.name;
|
|
2192
|
+
}
|
|
2193
|
+
/**
|
|
2194
|
+
* Register event callback
|
|
2195
|
+
* Supports lifecycle events ('render', 'create', 'load', 'ready', 'stop') and custom events
|
|
2196
|
+
* Lifecycle event callbacks fire after the lifecycle method completes
|
|
2197
|
+
* If a lifecycle event has already occurred, the callback fires immediately AND registers for future occurrences
|
|
2198
|
+
* Custom events only fire when explicitly triggered via .trigger()
|
|
2199
|
+
*/
|
|
2200
|
+
on(event_name, callback) {
|
|
2201
|
+
// Initialize callback array for this event if needed
|
|
2202
|
+
if (!this._lifecycle_callbacks.has(event_name)) {
|
|
2203
|
+
this._lifecycle_callbacks.set(event_name, []);
|
|
2204
|
+
}
|
|
2205
|
+
// Add callback to queue
|
|
2206
|
+
this._lifecycle_callbacks.get(event_name).push(callback);
|
|
2207
|
+
// If this lifecycle event has already occurred, fire the callback immediately
|
|
2208
|
+
// (only for lifecycle events - custom events don't have this behavior)
|
|
2209
|
+
if (this._lifecycle_states.has(event_name)) {
|
|
2210
|
+
try {
|
|
2211
|
+
callback(this);
|
|
2212
|
+
}
|
|
2213
|
+
catch (error) {
|
|
2214
|
+
console.error(`[JQHTML] Error in ${event_name} callback:`, error);
|
|
2215
|
+
}
|
|
2216
|
+
}
|
|
2217
|
+
return this;
|
|
2218
|
+
}
|
|
2219
|
+
/**
|
|
2220
|
+
* Trigger a lifecycle event - fires all registered callbacks
|
|
2221
|
+
* Marks event as occurred so future .on() calls fire immediately
|
|
2222
|
+
*/
|
|
2223
|
+
trigger(event_name) {
|
|
2224
|
+
// Mark this event as occurred
|
|
2225
|
+
this._lifecycle_states.add(event_name);
|
|
2226
|
+
// Fire all registered callbacks for this event
|
|
2227
|
+
const callbacks = this._lifecycle_callbacks.get(event_name);
|
|
2228
|
+
if (callbacks) {
|
|
2229
|
+
for (const callback of callbacks) {
|
|
2230
|
+
try {
|
|
2231
|
+
callback.bind(this)(this);
|
|
2232
|
+
}
|
|
2233
|
+
catch (error) {
|
|
2234
|
+
console.error(`[JQHTML] Error in ${event_name} callback:`, error);
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
}
|
|
2238
|
+
}
|
|
2239
|
+
/**
|
|
2240
|
+
* Check if any callbacks are registered for a given event
|
|
2241
|
+
* Used to determine if cleanup logic needs to run
|
|
2242
|
+
*/
|
|
2243
|
+
_on_registered(event_name) {
|
|
2244
|
+
const callbacks = this._lifecycle_callbacks.get(event_name);
|
|
2245
|
+
return !!(callbacks && callbacks.length > 0);
|
|
2246
|
+
}
|
|
2247
|
+
/**
|
|
2248
|
+
* Find element by scoped ID
|
|
2249
|
+
*
|
|
2250
|
+
* Searches for elements with id="local_id:THIS_COMPONENT_CID"
|
|
2251
|
+
*
|
|
2252
|
+
* Example:
|
|
2253
|
+
* Template: <button $sid="save_btn">Save</button>
|
|
2254
|
+
* Rendered: <button id="save_btn:abc123" data-sid="save_btn">Save</button>
|
|
2255
|
+
* Access: this.$sid('save_btn') // Returns jQuery element
|
|
2256
|
+
*
|
|
2257
|
+
* Performance: Uses native document.getElementById() when component is in DOM,
|
|
2258
|
+
* falls back to jQuery.find() for components not yet attached to DOM.
|
|
2259
|
+
*
|
|
2260
|
+
* @param local_id The local ID (without _cid suffix)
|
|
2261
|
+
* @returns jQuery element with id="local_id:_cid", or empty jQuery object if not found
|
|
2262
|
+
*/
|
|
2263
|
+
$sid(local_id) {
|
|
2264
|
+
const scopedId = `${local_id}:${this._cid}`;
|
|
2265
|
+
// Try getElementById first (fast path - works when component is in DOM)
|
|
2266
|
+
const el = document.getElementById(scopedId);
|
|
2267
|
+
if (el) {
|
|
2268
|
+
return $(el);
|
|
2269
|
+
}
|
|
2270
|
+
// Fallback: component not in DOM yet, search within component subtree
|
|
2271
|
+
// This allows $sid() to work on components before they're appended to body
|
|
2272
|
+
// Must escape the ID because it contains ':' which jQuery treats as a pseudo-selector
|
|
2273
|
+
return this.$.find(`#${$.escapeSelector(scopedId)}`);
|
|
2274
|
+
}
|
|
2275
|
+
/**
|
|
2276
|
+
* Get component instance by scoped ID
|
|
2277
|
+
*
|
|
2278
|
+
* Convenience method that finds element by scoped ID and returns the component instance.
|
|
2279
|
+
*
|
|
2280
|
+
* Example:
|
|
2281
|
+
* Template: <User_Card $sid="active_user" />
|
|
2282
|
+
* Access: const user = this.sid('active_user'); // Returns User_Card instance
|
|
2283
|
+
* user.data.name // Access component's data
|
|
2284
|
+
*
|
|
2285
|
+
* To get the scoped ID string itself:
|
|
2286
|
+
* this.$sid('active_user').attr('id') // Returns "active_user:abc123xyz"
|
|
2287
|
+
*
|
|
2288
|
+
* @param local_id The local ID (without _cid suffix)
|
|
2289
|
+
* @returns Component instance or null if not found or not a component
|
|
2290
|
+
*/
|
|
2291
|
+
sid(local_id) {
|
|
2292
|
+
const element = this.$sid(local_id);
|
|
2293
|
+
const component = element.data('_component');
|
|
2294
|
+
// If no component found but element exists, warn developer
|
|
2295
|
+
if (!component && element.length > 0) {
|
|
2296
|
+
console.warn(`Component ${this.constructor.name} tried to call .sid('${local_id}') - ` +
|
|
2297
|
+
`${local_id} exists, however, it is not a component or $redrawable. ` +
|
|
2298
|
+
`Did you forget to add $redrawable to the tag?`);
|
|
2299
|
+
}
|
|
2300
|
+
return component || null;
|
|
2301
|
+
}
|
|
2302
|
+
/**
|
|
2303
|
+
* Get the component that instantiated this component (rendered it in their template)
|
|
2304
|
+
* Returns null if component was created programmatically via $().component()
|
|
2305
|
+
*/
|
|
2306
|
+
instantiator() {
|
|
2307
|
+
return this._instantiator;
|
|
2308
|
+
}
|
|
2309
|
+
/**
|
|
2310
|
+
* Find descendant components by CSS selector
|
|
2311
|
+
*/
|
|
2312
|
+
find(selector) {
|
|
2313
|
+
const components = [];
|
|
2314
|
+
this.$.find(selector).each((_, el) => {
|
|
2315
|
+
const comp = $(el).data('_component');
|
|
2316
|
+
if (comp instanceof Jqhtml_Component) {
|
|
2317
|
+
components.push(comp);
|
|
2318
|
+
}
|
|
2319
|
+
});
|
|
2320
|
+
return components;
|
|
2321
|
+
}
|
|
2322
|
+
/**
|
|
2323
|
+
* Find closest ancestor component matching selector
|
|
2324
|
+
*/
|
|
2325
|
+
closest(selector) {
|
|
2326
|
+
let current = this.$.parent();
|
|
2327
|
+
while (current.length > 0) {
|
|
2328
|
+
if (current.is(selector)) {
|
|
2329
|
+
const comp = current.data('_component');
|
|
2330
|
+
if (comp instanceof Jqhtml_Component) {
|
|
2331
|
+
return comp;
|
|
2332
|
+
}
|
|
2333
|
+
}
|
|
2334
|
+
current = current.parent();
|
|
2335
|
+
}
|
|
2336
|
+
return null;
|
|
2337
|
+
}
|
|
2338
|
+
// -------------------------------------------------------------------------
|
|
2339
|
+
// Static Methods
|
|
2340
|
+
// -------------------------------------------------------------------------
|
|
2341
|
+
/**
|
|
2342
|
+
* Get CSS class hierarchy for this component type
|
|
2343
|
+
*/
|
|
2344
|
+
static get_class_hierarchy() {
|
|
2345
|
+
// v2.2.13 - Fixed to handle undefined values in prototype chain
|
|
2346
|
+
const classes = [];
|
|
2347
|
+
let ctor = this;
|
|
2348
|
+
while (ctor) {
|
|
2349
|
+
// Check if constructor has a valid name property
|
|
2350
|
+
if (!ctor.name || typeof ctor.name !== 'string') {
|
|
2351
|
+
// console.warn('[JQHTML v2.2.13] Invalid constructor name in hierarchy:', ctor);
|
|
2352
|
+
break;
|
|
2353
|
+
}
|
|
2354
|
+
// Only add valid class names (non-empty strings, not 'Object')
|
|
2355
|
+
if (ctor.name !== 'Object' && ctor.name !== '') {
|
|
2356
|
+
// Normalize base class names - handle different import patterns
|
|
2357
|
+
let normalizedName = ctor.name;
|
|
2358
|
+
if (normalizedName === '_Jqhtml_Component' || normalizedName === '_Base_Jqhtml_Component') {
|
|
2359
|
+
normalizedName = 'Component'; // Use 'Component' instead of 'Jqhtml_Component' for CSS class
|
|
2360
|
+
}
|
|
2361
|
+
else if (normalizedName === 'Jqhtml_Component') {
|
|
2362
|
+
normalizedName = 'Component'; // Use 'Component' instead of 'Jqhtml_Component' for CSS class
|
|
2363
|
+
}
|
|
2364
|
+
classes.push(normalizedName);
|
|
2365
|
+
}
|
|
2366
|
+
// Get the next prototype
|
|
2367
|
+
const nextProto = Object.getPrototypeOf(ctor);
|
|
2368
|
+
// Stop if we've reached the end of the chain
|
|
2369
|
+
if (!nextProto || nextProto === Object.prototype || nextProto.constructor === Object) {
|
|
2370
|
+
break;
|
|
2371
|
+
}
|
|
2372
|
+
ctor = nextProto;
|
|
2373
|
+
}
|
|
2374
|
+
return classes;
|
|
2375
|
+
}
|
|
2376
|
+
// -------------------------------------------------------------------------
|
|
2377
|
+
// Private Implementation
|
|
2378
|
+
// -------------------------------------------------------------------------
|
|
2379
|
+
_generate_cid() {
|
|
2380
|
+
return uid();
|
|
2381
|
+
}
|
|
2382
|
+
/**
|
|
2383
|
+
* Flatten instruction array - converts ['_content', [...]] markers to flat array
|
|
2384
|
+
* Recursively flattens nested content from content() calls
|
|
2385
|
+
*/
|
|
2386
|
+
_flatten_instructions(instructions) {
|
|
2387
|
+
const result = [];
|
|
2388
|
+
for (const instruction of instructions) {
|
|
2389
|
+
// Check if this is a _content marker: ['_content', [...]]
|
|
2390
|
+
if (Array.isArray(instruction) && instruction[0] === '_content' && Array.isArray(instruction[1])) {
|
|
2391
|
+
// Recursively flatten the content instructions
|
|
2392
|
+
const contentInstructions = this._flatten_instructions(instruction[1]);
|
|
2393
|
+
result.push(...contentInstructions);
|
|
2394
|
+
}
|
|
2395
|
+
else {
|
|
2396
|
+
// Regular instruction - keep as is
|
|
2397
|
+
result.push(instruction);
|
|
2398
|
+
}
|
|
2399
|
+
}
|
|
2400
|
+
return result;
|
|
2401
|
+
}
|
|
2402
|
+
_apply_css_classes() {
|
|
2403
|
+
const hierarchy = this.constructor.get_class_hierarchy();
|
|
2404
|
+
// If component name differs from class name, add component name to hierarchy
|
|
2405
|
+
// This happens when:
|
|
2406
|
+
// 1. Using base Jqhtml_Component with _component_name
|
|
2407
|
+
// 2. Using parent class via extends chain (e.g., Contacts_DataGrid using DataGrid_Abstract class)
|
|
2408
|
+
const classesToAdd = [...hierarchy];
|
|
2409
|
+
if (this.args._component_name && this.args._component_name !== this.constructor.name) {
|
|
2410
|
+
// Add component name at the beginning (most specific)
|
|
2411
|
+
classesToAdd.unshift(this.args._component_name);
|
|
2412
|
+
}
|
|
2413
|
+
// Filter out private classes (starting with _) and invalid class names
|
|
2414
|
+
const publicClasses = classesToAdd.filter(className => {
|
|
2415
|
+
// Guard against undefined, null, or non-string values
|
|
2416
|
+
if (!className || typeof className !== 'string') {
|
|
2417
|
+
console.warn('[JQHTML] Filtered out invalid class name:', className);
|
|
2418
|
+
return false;
|
|
2419
|
+
}
|
|
2420
|
+
return !className.startsWith('_');
|
|
2421
|
+
});
|
|
2422
|
+
if (publicClasses.length > 0) {
|
|
2423
|
+
this.$.addClass(publicClasses.join(' '));
|
|
2424
|
+
}
|
|
2425
|
+
}
|
|
2426
|
+
_apply_default_attributes() {
|
|
2427
|
+
// Get template using same logic as render() method
|
|
2428
|
+
let template;
|
|
2429
|
+
// If we have a component name, try looking up by name first (for unregistered components)
|
|
2430
|
+
if (this.args._component_name) {
|
|
2431
|
+
template = get_template(this.args._component_name);
|
|
2432
|
+
}
|
|
2433
|
+
else {
|
|
2434
|
+
// Otherwise look up by class
|
|
2435
|
+
template = get_template_by_class(this.constructor);
|
|
2436
|
+
}
|
|
2437
|
+
if (!template)
|
|
2438
|
+
return;
|
|
2439
|
+
// Walk the extends chain to collect all defaultAttributes from parent templates
|
|
2440
|
+
// Apply from parent to child so child attributes take precedence
|
|
2441
|
+
const templateChain = [];
|
|
2442
|
+
let currentTemplate = template;
|
|
2443
|
+
// Build chain from child to parent
|
|
2444
|
+
while (currentTemplate) {
|
|
2445
|
+
templateChain.unshift(currentTemplate); // Add to front so parent comes first
|
|
2446
|
+
// Check if this template extends another
|
|
2447
|
+
if (currentTemplate.extends) {
|
|
2448
|
+
try {
|
|
2449
|
+
currentTemplate = get_template(currentTemplate.extends);
|
|
2450
|
+
}
|
|
2451
|
+
catch (error) {
|
|
2452
|
+
// Parent template not found, stop chain
|
|
2453
|
+
break;
|
|
2454
|
+
}
|
|
2455
|
+
}
|
|
2456
|
+
else {
|
|
2457
|
+
break;
|
|
2458
|
+
}
|
|
2459
|
+
}
|
|
2460
|
+
// Apply defaultAttributes from each template in the chain (parent first, child last)
|
|
2461
|
+
for (const tmpl of templateChain) {
|
|
2462
|
+
if (!tmpl.defaultAttributes)
|
|
2463
|
+
continue;
|
|
2464
|
+
// Filter out 'tag' since it determines tag name, not an attribute
|
|
2465
|
+
const defineAttrs = { ...tmpl.defaultAttributes };
|
|
2466
|
+
delete defineAttrs.tag;
|
|
2467
|
+
// Debug logging
|
|
2468
|
+
if (window.jqhtml?.debug?.enabled) {
|
|
2469
|
+
const componentName = tmpl.name || this.args._component_name || this.constructor.name;
|
|
2470
|
+
console.log(`[Component] Applying defaultAttributes for ${componentName}:`, defineAttrs);
|
|
2471
|
+
}
|
|
2472
|
+
// Apply each default attribute
|
|
2473
|
+
for (const [key, value] of Object.entries(defineAttrs)) {
|
|
2474
|
+
if (key === 'class') {
|
|
2475
|
+
// Special handling for class - merge with existing
|
|
2476
|
+
const existingClasses = this.$.attr('class');
|
|
2477
|
+
if (existingClasses) {
|
|
2478
|
+
const existing = existingClasses.split(/\s+/).filter(c => c);
|
|
2479
|
+
const newClasses = String(value).split(/\s+/).filter(c => c);
|
|
2480
|
+
for (const newClass of newClasses) {
|
|
2481
|
+
if (!existing.includes(newClass)) {
|
|
2482
|
+
existing.push(newClass);
|
|
2483
|
+
}
|
|
2484
|
+
}
|
|
2485
|
+
this.$.attr('class', existing.join(' '));
|
|
2486
|
+
}
|
|
2487
|
+
else {
|
|
2488
|
+
this.$.attr('class', value);
|
|
2489
|
+
}
|
|
2490
|
+
}
|
|
2491
|
+
else if (key === 'style') {
|
|
2492
|
+
// Special handling for style - merge with existing
|
|
2493
|
+
// Define attributes are applied AFTER invocation attributes
|
|
2494
|
+
// So we only add Define style properties that don't already exist
|
|
2495
|
+
// This ensures invocation wins conflicts (invocation overrides Define)
|
|
2496
|
+
const existingStyle = this.$.attr('style');
|
|
2497
|
+
if (existingStyle) {
|
|
2498
|
+
// Parse existing styles (from invocation)
|
|
2499
|
+
const existingRules = new Map();
|
|
2500
|
+
existingStyle.split(';').forEach(rule => {
|
|
2501
|
+
const [prop, val] = rule.split(':').map(s => s.trim());
|
|
2502
|
+
if (prop && val)
|
|
2503
|
+
existingRules.set(prop, val);
|
|
2504
|
+
});
|
|
2505
|
+
// Parse Define styles and only add if not already present
|
|
2506
|
+
String(value).split(';').forEach(rule => {
|
|
2507
|
+
const [prop, val] = rule.split(':').map(s => s.trim());
|
|
2508
|
+
if (prop && val && !existingRules.has(prop)) {
|
|
2509
|
+
// Only add if property doesn't exist (invocation wins)
|
|
2510
|
+
existingRules.set(prop, val);
|
|
2511
|
+
}
|
|
2512
|
+
});
|
|
2513
|
+
// Build merged style string
|
|
2514
|
+
const merged = Array.from(existingRules.entries())
|
|
2515
|
+
.map(([prop, val]) => `${prop}: ${val}`)
|
|
2516
|
+
.join('; ');
|
|
2517
|
+
this.$.attr('style', merged);
|
|
2518
|
+
}
|
|
2519
|
+
else {
|
|
2520
|
+
this.$.attr('style', value);
|
|
2521
|
+
}
|
|
2522
|
+
}
|
|
2523
|
+
else if (key.startsWith('$') || key.startsWith('data-')) {
|
|
2524
|
+
// Data attributes - also update args
|
|
2525
|
+
const dataKey = key.startsWith('$') ? key.substring(1) :
|
|
2526
|
+
key.startsWith('data-') ? key.substring(5) : key;
|
|
2527
|
+
// Only apply if not already set in args
|
|
2528
|
+
if (!(dataKey in this.args)) {
|
|
2529
|
+
this.args[dataKey] = value;
|
|
2530
|
+
this.$.data(dataKey, value);
|
|
2531
|
+
this.$.attr(key.startsWith('$') ? `data-${dataKey}` : key, String(value));
|
|
2532
|
+
}
|
|
2533
|
+
}
|
|
2534
|
+
else {
|
|
2535
|
+
// Regular attributes - apply directly if not already set
|
|
2536
|
+
if (!this.$.attr(key)) {
|
|
2537
|
+
this.$.attr(key, value);
|
|
2538
|
+
}
|
|
2539
|
+
}
|
|
2540
|
+
}
|
|
2541
|
+
}
|
|
2542
|
+
}
|
|
2543
|
+
_set_attributes() {
|
|
2544
|
+
// Always set data-cid
|
|
2545
|
+
this.$.attr('data-cid', this._cid);
|
|
2546
|
+
// Only set lifecycle state attribute if verbose mode enabled
|
|
2547
|
+
if (window.jqhtml?.debug?.verbose) {
|
|
2548
|
+
this.$.attr('data-_lifecycle-state', this._ready_state.toString());
|
|
2549
|
+
}
|
|
2550
|
+
}
|
|
2551
|
+
_update_debug_attrs() {
|
|
2552
|
+
// Only update lifecycle state attribute if verbose mode enabled
|
|
2553
|
+
if (window.jqhtml?.debug?.verbose) {
|
|
2554
|
+
this.$.attr('data-_lifecycle-state', this._ready_state.toString());
|
|
2555
|
+
}
|
|
2556
|
+
}
|
|
2557
|
+
_find_dom_parent() {
|
|
2558
|
+
let current = this.$.parent();
|
|
2559
|
+
while (current.length > 0) {
|
|
2560
|
+
const parent = current.data('_component');
|
|
2561
|
+
if (parent instanceof Jqhtml_Component) {
|
|
2562
|
+
this._dom_parent = parent;
|
|
2563
|
+
parent._dom_children.add(this);
|
|
2564
|
+
break;
|
|
2565
|
+
}
|
|
2566
|
+
current = current.parent();
|
|
2567
|
+
}
|
|
2568
|
+
}
|
|
2569
|
+
/**
|
|
2570
|
+
* Get DOM children (components in DOM subtree)
|
|
2571
|
+
* Uses fast _dom_children registry when possible, falls back to DOM traversal for off-DOM components
|
|
2572
|
+
* @private - Used internally for lifecycle coordination
|
|
2573
|
+
*/
|
|
2574
|
+
_get_dom_children() {
|
|
2575
|
+
// If component was off-DOM during render, children couldn't register via _find_dom_parent()
|
|
2576
|
+
// Use DOM traversal to find direct children (works off-DOM, slightly slower)
|
|
2577
|
+
if (this._use_dom_fallback) {
|
|
2578
|
+
const directChildren = [];
|
|
2579
|
+
this.$.find('.Component').each((_, el) => {
|
|
2580
|
+
const $el = $(el);
|
|
2581
|
+
const comp = $el.data('_component');
|
|
2582
|
+
if (comp instanceof Jqhtml_Component) {
|
|
2583
|
+
// Only include if this component is the direct parent (not a grandparent)
|
|
2584
|
+
// Check: closest parent component is us (or no parent = we're the root)
|
|
2585
|
+
const closestParent = $el.parent().closest('.Component');
|
|
2586
|
+
if (closestParent.length === 0 || closestParent.data('_component') === this) {
|
|
2587
|
+
directChildren.push(comp);
|
|
2588
|
+
}
|
|
2589
|
+
}
|
|
2590
|
+
});
|
|
2591
|
+
return directChildren;
|
|
2592
|
+
}
|
|
2593
|
+
// Fast path: component was in-DOM during render, children registered normally
|
|
2594
|
+
// Filter out any that have since been removed from DOM (destroyed/replaced)
|
|
2595
|
+
const children = Array.from(this._dom_children);
|
|
2596
|
+
return children.filter(child => {
|
|
2597
|
+
return $.contains(document.documentElement, child.$[0]);
|
|
2598
|
+
});
|
|
2599
|
+
}
|
|
2600
|
+
_log_lifecycle(phase, status) {
|
|
2601
|
+
// Use new debug module
|
|
2602
|
+
logLifecycle(this, phase, status);
|
|
2603
|
+
// Keep legacy support for window.JQHTML_DEBUG
|
|
2604
|
+
if (typeof window !== 'undefined' && window.JQHTML_DEBUG) {
|
|
2605
|
+
window.JQHTML_DEBUG.log(this.component_name(), phase, status, {
|
|
2606
|
+
cid: this._cid,
|
|
2607
|
+
ready_state: this._ready_state,
|
|
2608
|
+
args: this.args
|
|
2609
|
+
});
|
|
2610
|
+
}
|
|
2611
|
+
}
|
|
2612
|
+
_log_debug(action, ...args) {
|
|
2613
|
+
if (typeof window !== 'undefined' && window.JQHTML_DEBUG) {
|
|
2614
|
+
window.JQHTML_DEBUG.log(this.component_name(), 'debug', `${action}: ${args.map(a => JSON.stringify(a)).join(', ')}`);
|
|
2615
|
+
}
|
|
2616
|
+
}
|
|
2617
|
+
/**
|
|
2618
|
+
* Creates a debounced function with exclusivity and promise fan-in
|
|
2619
|
+
*
|
|
2620
|
+
* When invoked, immediately runs the callback exclusively.
|
|
2621
|
+
* For subsequent invocations, applies a delay before running the callback exclusively again.
|
|
2622
|
+
* The delay starts after the current asynchronous operation resolves.
|
|
2623
|
+
*
|
|
2624
|
+
* If delay is 0, the function only prevents enqueueing multiple executions,
|
|
2625
|
+
* but will still run them immediately in an exclusive sequential manner.
|
|
2626
|
+
*
|
|
2627
|
+
* The most recent invocation's parameters are used when the function executes.
|
|
2628
|
+
* Returns a promise that resolves when the next exclusive execution completes.
|
|
2629
|
+
*
|
|
2630
|
+
* @private
|
|
2631
|
+
*/
|
|
2632
|
+
_create_debounced_function(callback, delay) {
|
|
2633
|
+
let running = false;
|
|
2634
|
+
let queued = false;
|
|
2635
|
+
let last_end_time = 0; // timestamp of last completed run
|
|
2636
|
+
let timer = null;
|
|
2637
|
+
let next_args = [];
|
|
2638
|
+
let resolve_queue = [];
|
|
2639
|
+
let reject_queue = [];
|
|
2640
|
+
const run_function = async () => {
|
|
2641
|
+
const these_resolves = resolve_queue;
|
|
2642
|
+
const these_rejects = reject_queue;
|
|
2643
|
+
const args = next_args;
|
|
2644
|
+
resolve_queue = [];
|
|
2645
|
+
reject_queue = [];
|
|
2646
|
+
next_args = [];
|
|
2647
|
+
queued = false;
|
|
2648
|
+
running = true;
|
|
2649
|
+
try {
|
|
2650
|
+
const result = await callback(...args);
|
|
2651
|
+
for (const resolve of these_resolves)
|
|
2652
|
+
resolve(result);
|
|
2653
|
+
}
|
|
2654
|
+
catch (err) {
|
|
2655
|
+
for (const reject of these_rejects)
|
|
2656
|
+
reject(err);
|
|
2657
|
+
}
|
|
2658
|
+
finally {
|
|
2659
|
+
running = false;
|
|
2660
|
+
last_end_time = Date.now();
|
|
2661
|
+
if (queued) {
|
|
2662
|
+
clearTimeout(timer);
|
|
2663
|
+
timer = setTimeout(run_function, Math.max(delay, 0));
|
|
2664
|
+
}
|
|
2665
|
+
else {
|
|
2666
|
+
timer = null;
|
|
2667
|
+
}
|
|
2668
|
+
}
|
|
2669
|
+
};
|
|
2670
|
+
return function (...args) {
|
|
2671
|
+
next_args = args;
|
|
2672
|
+
return new Promise((resolve, reject) => {
|
|
2673
|
+
resolve_queue.push(resolve);
|
|
2674
|
+
reject_queue.push(reject);
|
|
2675
|
+
// Nothing running and nothing scheduled
|
|
2676
|
+
if (!running && !timer) {
|
|
2677
|
+
const first_call = last_end_time === 0;
|
|
2678
|
+
const since = first_call ? Infinity : Date.now() - last_end_time;
|
|
2679
|
+
if (since >= delay) {
|
|
2680
|
+
run_function();
|
|
2681
|
+
}
|
|
2682
|
+
else {
|
|
2683
|
+
const wait = Math.max(delay - since, 0);
|
|
2684
|
+
clearTimeout(timer);
|
|
2685
|
+
timer = setTimeout(run_function, wait);
|
|
2686
|
+
}
|
|
2687
|
+
return;
|
|
2688
|
+
}
|
|
2689
|
+
// If already running or timer exists, just mark queued
|
|
2690
|
+
// The finally{} of run_function handles scheduling after full delay
|
|
2691
|
+
queued = true;
|
|
2692
|
+
});
|
|
2693
|
+
};
|
|
2694
|
+
}
|
|
2695
|
+
}
|
|
2696
|
+
|
|
2697
|
+
/**
|
|
2698
|
+
* JQHTML v2 Template Renderer
|
|
2699
|
+
*
|
|
2700
|
+
* Connects compiled templates to components
|
|
2701
|
+
* Processes instruction arrays and handles bindings
|
|
2702
|
+
*/
|
|
2703
|
+
/**
|
|
2704
|
+
* Process slot-only template inheritance
|
|
2705
|
+
*
|
|
2706
|
+
* When a component's template returns {_slots: {...}}, walk the prototype
|
|
2707
|
+
* chain to find a parent template and invoke it with the child's slots as content.
|
|
2708
|
+
*
|
|
2709
|
+
* @param component The component instance
|
|
2710
|
+
* @param childSlots The slots object from the child template
|
|
2711
|
+
* @returns [instructions, context] tuple or null if no parent found
|
|
2712
|
+
*/
|
|
2713
|
+
async function process_slot_inheritance(component, childSlots) {
|
|
2714
|
+
// Walk the prototype chain to find parent class with template
|
|
2715
|
+
let currentClass = Object.getPrototypeOf(component.constructor);
|
|
2716
|
+
console.log(`[JQHTML] Walking prototype chain for ${component.constructor.name}`);
|
|
2717
|
+
while (currentClass && currentClass !== Jqhtml_Component && currentClass.name !== 'Object') {
|
|
2718
|
+
const className = currentClass.name;
|
|
2719
|
+
console.log(`[JQHTML] Checking parent class: ${className}`);
|
|
2720
|
+
// Skip internal framework classes
|
|
2721
|
+
if (className === '_Jqhtml_Component' || className === '_Base_Jqhtml_Component') {
|
|
2722
|
+
currentClass = Object.getPrototypeOf(currentClass);
|
|
2723
|
+
continue;
|
|
2724
|
+
}
|
|
2725
|
+
// Try to find template for this parent class
|
|
2726
|
+
try {
|
|
2727
|
+
const parentTemplate = get_template(className);
|
|
2728
|
+
console.log(`[JQHTML] Template found for ${className}:`, parentTemplate ? parentTemplate.name : 'null');
|
|
2729
|
+
// Check if we got the default template (means no real template found)
|
|
2730
|
+
if (parentTemplate && parentTemplate.name !== 'Jqhtml_Component') {
|
|
2731
|
+
console.log(`[JQHTML] Invoking parent template ${className}`);
|
|
2732
|
+
// Found a real parent template - invoke it with child's slots as content
|
|
2733
|
+
const [parentInstructions, parentContext] = parentTemplate.render.call(component, component.data, component.args, childSlots // Pass child slots as content parameter
|
|
2734
|
+
);
|
|
2735
|
+
// Check if parent also returns slots (recursive inheritance)
|
|
2736
|
+
if (parentInstructions && typeof parentInstructions === 'object' && parentInstructions._slots) {
|
|
2737
|
+
// Parent also has slot-only template - recurse
|
|
2738
|
+
console.log(`[JQHTML] Parent also slot-only, recursing`);
|
|
2739
|
+
return await process_slot_inheritance(component, parentInstructions._slots);
|
|
2740
|
+
}
|
|
2741
|
+
// Parent returned HTML - we're done
|
|
2742
|
+
console.log(`[JQHTML] Parent returned instructions, inheritance complete`);
|
|
2743
|
+
return [parentInstructions, parentContext];
|
|
2744
|
+
}
|
|
2745
|
+
}
|
|
2746
|
+
catch (error) {
|
|
2747
|
+
console.warn(`[JQHTML] Error looking up parent template for ${className}:`, error);
|
|
2748
|
+
}
|
|
2749
|
+
// Move to next parent
|
|
2750
|
+
currentClass = Object.getPrototypeOf(currentClass);
|
|
2751
|
+
}
|
|
2752
|
+
// No parent template found
|
|
2753
|
+
console.warn(`[JQHTML] No parent template found after walking chain`);
|
|
2754
|
+
return null;
|
|
2755
|
+
}
|
|
2756
|
+
/**
|
|
2757
|
+
* Render a template for a component
|
|
2758
|
+
* Templates are functions that return [instructions, context] tuples
|
|
2759
|
+
*/
|
|
2760
|
+
async function render_template(component, template_fn) {
|
|
2761
|
+
// Get template from registry if not provided
|
|
2762
|
+
let render_fn = template_fn;
|
|
2763
|
+
if (!render_fn) {
|
|
2764
|
+
const template_def = get_template_by_class(component.constructor);
|
|
2765
|
+
render_fn = template_def.render;
|
|
2766
|
+
}
|
|
2767
|
+
if (!render_fn) {
|
|
2768
|
+
// No template, component should handle its own rendering
|
|
2769
|
+
return;
|
|
2770
|
+
}
|
|
2771
|
+
// Clear existing content
|
|
2772
|
+
component.$.empty();
|
|
2773
|
+
// Execute template function
|
|
2774
|
+
// Templates receive: (data, args, content)
|
|
2775
|
+
// Provide a default empty content function if none provided
|
|
2776
|
+
const defaultContent = () => '';
|
|
2777
|
+
let [instructions, context] = render_fn.call(component, component.data, component.args, defaultContent // Default content function that returns empty string
|
|
2778
|
+
);
|
|
2779
|
+
// Check for slot-only template inheritance
|
|
2780
|
+
// If instructions is {_slots: {...}}, this is a slot-only template
|
|
2781
|
+
// We need to find the parent template and invoke it with these slots
|
|
2782
|
+
if (instructions && typeof instructions === 'object' && instructions._slots) {
|
|
2783
|
+
console.log(`[JQHTML] Slot-only template detected for ${component.constructor.name}, invoking inheritance`);
|
|
2784
|
+
const result = await process_slot_inheritance(component, instructions._slots);
|
|
2785
|
+
if (result) {
|
|
2786
|
+
console.log(`[JQHTML] Parent template found, using parent instructions`);
|
|
2787
|
+
instructions = result[0];
|
|
2788
|
+
context = result[1];
|
|
2789
|
+
}
|
|
2790
|
+
else {
|
|
2791
|
+
console.warn(`[JQHTML] No parent template found for ${component.constructor.name}, rendering empty`);
|
|
2792
|
+
// No parent template found - return empty
|
|
2793
|
+
instructions = [];
|
|
2794
|
+
}
|
|
2795
|
+
}
|
|
2796
|
+
// Process instructions to generate DOM
|
|
2797
|
+
await process_instructions(instructions, component.$, component);
|
|
2798
|
+
// Process bindings after DOM is created
|
|
2799
|
+
await process_bindings(component);
|
|
2800
|
+
// Attach event handlers
|
|
2801
|
+
await attach_event_handlers(component);
|
|
2802
|
+
}
|
|
2803
|
+
/**
|
|
2804
|
+
* Process data bindings (attributes created by :prop syntax)
|
|
2805
|
+
*/
|
|
2806
|
+
async function process_bindings(component) {
|
|
2807
|
+
// Find all elements with data-bind-* attributes
|
|
2808
|
+
component.$.find('[data-bind-prop], [data-bind-value], [data-bind-text], [data-bind-html], [data-bind-class], [data-bind-style]').each((_, element) => {
|
|
2809
|
+
const el = $(element);
|
|
2810
|
+
const attrs = element.attributes;
|
|
2811
|
+
for (let i = 0; i < attrs.length; i++) {
|
|
2812
|
+
const attr = attrs[i];
|
|
2813
|
+
if (attr.name.startsWith('data-bind-')) {
|
|
2814
|
+
const binding_type = attr.name.substring(10); // Remove 'data-bind-'
|
|
2815
|
+
const expression = attr.value;
|
|
2816
|
+
try {
|
|
2817
|
+
// Evaluate expression in component context
|
|
2818
|
+
const value = evaluate_expression(expression, component);
|
|
2819
|
+
// Apply binding based on type
|
|
2820
|
+
switch (binding_type) {
|
|
2821
|
+
case 'prop':
|
|
2822
|
+
// Generic property binding
|
|
2823
|
+
const prop_name = el.attr('data-bind-prop-name') || 'value';
|
|
2824
|
+
el.prop(prop_name, value);
|
|
2825
|
+
break;
|
|
2826
|
+
case 'value':
|
|
2827
|
+
el.val(value);
|
|
2828
|
+
break;
|
|
2829
|
+
case 'text':
|
|
2830
|
+
el.text(value);
|
|
2831
|
+
break;
|
|
2832
|
+
case 'html':
|
|
2833
|
+
el.html(value);
|
|
2834
|
+
break;
|
|
2835
|
+
case 'class':
|
|
2836
|
+
if (typeof value === 'object') {
|
|
2837
|
+
// Object syntax: {active: true, disabled: false}
|
|
2838
|
+
Object.entries(value).forEach(([className, enabled]) => {
|
|
2839
|
+
el.toggleClass(className, !!enabled);
|
|
2840
|
+
});
|
|
2841
|
+
}
|
|
2842
|
+
else {
|
|
2843
|
+
// String syntax
|
|
2844
|
+
el.addClass(String(value));
|
|
2845
|
+
}
|
|
2846
|
+
break;
|
|
2847
|
+
case 'style':
|
|
2848
|
+
if (typeof value === 'object') {
|
|
2849
|
+
el.css(value);
|
|
2850
|
+
}
|
|
2851
|
+
else {
|
|
2852
|
+
el.attr('style', String(value));
|
|
2853
|
+
}
|
|
2854
|
+
break;
|
|
2855
|
+
default:
|
|
2856
|
+
// Custom binding - set as attribute
|
|
2857
|
+
el.attr(binding_type, value);
|
|
2858
|
+
}
|
|
2859
|
+
}
|
|
2860
|
+
catch (error) {
|
|
2861
|
+
console.error(`Error evaluating binding "${expression}":`, error);
|
|
2862
|
+
}
|
|
2863
|
+
}
|
|
2864
|
+
}
|
|
2865
|
+
});
|
|
2866
|
+
}
|
|
2867
|
+
/**
|
|
2868
|
+
* Attach event handlers (attributes created by @event syntax)
|
|
2869
|
+
*/
|
|
2870
|
+
async function attach_event_handlers(component) {
|
|
2871
|
+
// Find all elements with data-on-* attributes
|
|
2872
|
+
component.$.find('[data-on-click], [data-on-change], [data-on-submit], [data-on-keyup], [data-on-keydown], [data-on-focus], [data-on-blur]').each((_, element) => {
|
|
2873
|
+
const el = $(element);
|
|
2874
|
+
const attrs = element.attributes;
|
|
2875
|
+
for (let i = 0; i < attrs.length; i++) {
|
|
2876
|
+
const attr = attrs[i];
|
|
2877
|
+
if (attr.name.startsWith('data-on-')) {
|
|
2878
|
+
const event_name = attr.name.substring(8); // Remove 'data-on-'
|
|
2879
|
+
const handler_expr = attr.value;
|
|
2880
|
+
// Remove the attribute to avoid re-processing
|
|
2881
|
+
el.removeAttr(attr.name);
|
|
2882
|
+
// Attach event handler
|
|
2883
|
+
el.on(event_name, function (event) {
|
|
2884
|
+
try {
|
|
2885
|
+
// Create handler function in component context
|
|
2886
|
+
const handler = evaluate_handler(handler_expr, component);
|
|
2887
|
+
if (typeof handler === 'function') {
|
|
2888
|
+
// Call with component as 'this' and pass event
|
|
2889
|
+
handler.call(component, event);
|
|
2890
|
+
}
|
|
2891
|
+
else {
|
|
2892
|
+
// Expression to execute
|
|
2893
|
+
evaluate_expression(handler_expr, component, { $event: event });
|
|
2894
|
+
}
|
|
2895
|
+
}
|
|
2896
|
+
catch (error) {
|
|
2897
|
+
console.error(`Error in ${event_name} handler "${handler_expr}":`, error);
|
|
2898
|
+
}
|
|
2899
|
+
});
|
|
2900
|
+
}
|
|
2901
|
+
}
|
|
2902
|
+
});
|
|
2903
|
+
}
|
|
2904
|
+
/**
|
|
2905
|
+
* Evaluate an expression in component context
|
|
2906
|
+
*/
|
|
2907
|
+
function evaluate_expression(expression, component, locals = {}) {
|
|
2908
|
+
// Create evaluation context
|
|
2909
|
+
const context = {
|
|
2910
|
+
// Component properties
|
|
2911
|
+
data: component.data,
|
|
2912
|
+
args: component.args,
|
|
2913
|
+
$: component.$,
|
|
2914
|
+
// Component methods
|
|
2915
|
+
$sid: component.$sid.bind(component),
|
|
2916
|
+
// Locals (like $event)
|
|
2917
|
+
...locals
|
|
2918
|
+
};
|
|
2919
|
+
// Build function that evaluates in context
|
|
2920
|
+
const keys = Object.keys(context);
|
|
2921
|
+
const values = Object.values(context);
|
|
2922
|
+
try {
|
|
2923
|
+
// Use Function constructor for better performance than eval
|
|
2924
|
+
const fn = new Function(...keys, `return (${expression})`);
|
|
2925
|
+
return fn(...values);
|
|
2926
|
+
}
|
|
2927
|
+
catch (error) {
|
|
2928
|
+
console.error(`Invalid expression: ${expression}`, error);
|
|
2929
|
+
return undefined;
|
|
2930
|
+
}
|
|
2931
|
+
}
|
|
2932
|
+
/**
|
|
2933
|
+
* Evaluate a handler expression (might be method name or inline code)
|
|
2934
|
+
*/
|
|
2935
|
+
function evaluate_handler(expression, component) {
|
|
2936
|
+
// Check if it's a method name on the component
|
|
2937
|
+
if (expression in component && typeof component[expression] === 'function') {
|
|
2938
|
+
return component[expression];
|
|
2939
|
+
}
|
|
2940
|
+
// Otherwise treat as inline code
|
|
2941
|
+
try {
|
|
2942
|
+
return new Function('$event', `
|
|
2943
|
+
const { data, args, $, emit, $sid } = this;
|
|
2944
|
+
${expression}
|
|
2945
|
+
`).bind(component);
|
|
2946
|
+
}
|
|
2947
|
+
catch (error) {
|
|
2948
|
+
console.error(`Invalid handler: ${expression}`, error);
|
|
2949
|
+
return null;
|
|
2950
|
+
}
|
|
2951
|
+
}
|
|
2952
|
+
/**
|
|
2953
|
+
* Helper to escape HTML for safe output
|
|
2954
|
+
*/
|
|
2955
|
+
function escape_html(str) {
|
|
2956
|
+
const div = document.createElement('div');
|
|
2957
|
+
div.textContent = str;
|
|
2958
|
+
return div.innerHTML;
|
|
2959
|
+
}
|
|
2960
|
+
|
|
2961
|
+
/**
|
|
2962
|
+
* JQHTML Debug Overlay
|
|
2963
|
+
*
|
|
2964
|
+
* Independent debug controls using pure jQuery DOM manipulation.
|
|
2965
|
+
* Does NOT use JQHTML components so it works even when components are broken.
|
|
2966
|
+
*/
|
|
2967
|
+
// Get global jQuery
|
|
2968
|
+
function getJQuery() {
|
|
2969
|
+
if (typeof window !== 'undefined' && window.$) {
|
|
2970
|
+
return window.$;
|
|
2971
|
+
}
|
|
2972
|
+
if (typeof window !== 'undefined' && window.jQuery) {
|
|
2973
|
+
return window.jQuery;
|
|
2974
|
+
}
|
|
2975
|
+
throw new Error('FATAL: jQuery is not defined. jQuery must be loaded before using JQHTML. ' +
|
|
2976
|
+
'Add <script src="https://code.jquery.com/jquery-3.7.1.min.js"></script> before loading JQHTML.');
|
|
2977
|
+
}
|
|
2978
|
+
// Get global jqhtml object
|
|
2979
|
+
function getJqhtml() {
|
|
2980
|
+
if (typeof window !== 'undefined' && window.jqhtml) {
|
|
2981
|
+
return window.jqhtml;
|
|
2982
|
+
}
|
|
2983
|
+
if (typeof globalThis !== 'undefined' && globalThis.jqhtml) {
|
|
2984
|
+
return globalThis.jqhtml;
|
|
2985
|
+
}
|
|
2986
|
+
throw new Error('FATAL: window.jqhtml is not defined. The JQHTML runtime must be loaded before using JQHTML components. ' +
|
|
2987
|
+
'Ensure @jqhtml/core is imported and initialized before attempting to use debug features.');
|
|
2988
|
+
}
|
|
2989
|
+
class DebugOverlay {
|
|
2990
|
+
constructor(options = {}) {
|
|
2991
|
+
this.$container = null;
|
|
2992
|
+
this.$statusIndicator = null;
|
|
2993
|
+
this.$ = getJQuery();
|
|
2994
|
+
if (!this.$) {
|
|
2995
|
+
throw new Error('jQuery is required for DebugOverlay');
|
|
2996
|
+
}
|
|
2997
|
+
this.options = {
|
|
2998
|
+
position: 'bottom',
|
|
2999
|
+
theme: 'dark',
|
|
3000
|
+
compact: false,
|
|
3001
|
+
showStatus: true,
|
|
3002
|
+
autoHide: false,
|
|
3003
|
+
...options
|
|
3004
|
+
};
|
|
3005
|
+
}
|
|
3006
|
+
/**
|
|
3007
|
+
* Static method to show debug overlay (singleton pattern)
|
|
3008
|
+
*/
|
|
3009
|
+
static show(options) {
|
|
3010
|
+
if (!DebugOverlay.instance) {
|
|
3011
|
+
DebugOverlay.instance = new DebugOverlay(options);
|
|
3012
|
+
}
|
|
3013
|
+
DebugOverlay.instance.display();
|
|
3014
|
+
return DebugOverlay.instance;
|
|
3015
|
+
}
|
|
3016
|
+
/**
|
|
3017
|
+
* Static method to hide debug overlay
|
|
3018
|
+
*/
|
|
3019
|
+
static hide() {
|
|
3020
|
+
if (DebugOverlay.instance) {
|
|
3021
|
+
DebugOverlay.instance.hide();
|
|
3022
|
+
}
|
|
3023
|
+
}
|
|
3024
|
+
/**
|
|
3025
|
+
* Static method to toggle debug overlay visibility
|
|
3026
|
+
*/
|
|
3027
|
+
static toggle() {
|
|
3028
|
+
if (DebugOverlay.instance && DebugOverlay.instance.$container) {
|
|
3029
|
+
if (DebugOverlay.instance.$container.is(':visible')) {
|
|
3030
|
+
DebugOverlay.hide();
|
|
3031
|
+
}
|
|
3032
|
+
else {
|
|
3033
|
+
DebugOverlay.instance.display();
|
|
3034
|
+
}
|
|
3035
|
+
}
|
|
3036
|
+
else {
|
|
3037
|
+
DebugOverlay.show();
|
|
3038
|
+
}
|
|
3039
|
+
}
|
|
3040
|
+
/**
|
|
3041
|
+
* Static method to destroy debug overlay
|
|
3042
|
+
*/
|
|
3043
|
+
static destroy() {
|
|
3044
|
+
if (DebugOverlay.instance) {
|
|
3045
|
+
DebugOverlay.instance.destroy();
|
|
3046
|
+
DebugOverlay.instance = null;
|
|
3047
|
+
}
|
|
3048
|
+
}
|
|
3049
|
+
/**
|
|
3050
|
+
* Display the debug overlay
|
|
3051
|
+
*/
|
|
3052
|
+
display() {
|
|
3053
|
+
if (this.$container) {
|
|
3054
|
+
this.$container.show();
|
|
3055
|
+
return;
|
|
3056
|
+
}
|
|
3057
|
+
this.createOverlay();
|
|
3058
|
+
if (this.options.showStatus) {
|
|
3059
|
+
this.createStatusIndicator();
|
|
3060
|
+
}
|
|
3061
|
+
}
|
|
3062
|
+
/**
|
|
3063
|
+
* Hide the debug overlay
|
|
3064
|
+
*/
|
|
3065
|
+
hide() {
|
|
3066
|
+
if (this.$container) {
|
|
3067
|
+
this.$container.hide();
|
|
3068
|
+
}
|
|
3069
|
+
if (this.$statusIndicator) {
|
|
3070
|
+
this.$statusIndicator.hide();
|
|
3071
|
+
}
|
|
3072
|
+
}
|
|
3073
|
+
/**
|
|
3074
|
+
* Remove the debug overlay completely
|
|
3075
|
+
*/
|
|
3076
|
+
destroy() {
|
|
3077
|
+
if (this.$container) {
|
|
3078
|
+
this.$container.remove();
|
|
3079
|
+
this.$container = null;
|
|
3080
|
+
}
|
|
3081
|
+
if (this.$statusIndicator) {
|
|
3082
|
+
this.$statusIndicator.remove();
|
|
3083
|
+
this.$statusIndicator = null;
|
|
3084
|
+
}
|
|
3085
|
+
}
|
|
3086
|
+
/**
|
|
3087
|
+
* Update the status indicator
|
|
3088
|
+
*/
|
|
3089
|
+
updateStatus(mode) {
|
|
3090
|
+
if (!this.$statusIndicator)
|
|
3091
|
+
return;
|
|
3092
|
+
this.$statusIndicator.text('Debug: ' + mode);
|
|
3093
|
+
this.$statusIndicator.attr('class', 'jqhtml-debug-status' + (mode !== 'Off' ? ' active' : ''));
|
|
3094
|
+
}
|
|
3095
|
+
createOverlay() {
|
|
3096
|
+
// Add styles first
|
|
3097
|
+
this.addStyles();
|
|
3098
|
+
// Create container using jQuery
|
|
3099
|
+
this.$container = this.$('<div>')
|
|
3100
|
+
.addClass(`jqhtml-debug-overlay ${this.options.theme} ${this.options.position}`);
|
|
3101
|
+
// Create content structure
|
|
3102
|
+
const $content = this.$('<div>').addClass('jqhtml-debug-content');
|
|
3103
|
+
const $controls = this.$('<div>').addClass('jqhtml-debug-controls');
|
|
3104
|
+
// Add title
|
|
3105
|
+
const $title = this.$('<span>')
|
|
3106
|
+
.addClass('jqhtml-debug-title')
|
|
3107
|
+
.html('<strong>🐛 JQHTML Debug:</strong>');
|
|
3108
|
+
$controls.append($title);
|
|
3109
|
+
// Create buttons
|
|
3110
|
+
const buttons = [
|
|
3111
|
+
{ text: 'Slow Motion + Flash', action: 'enableSlowMotionDebug', class: 'success' },
|
|
3112
|
+
{ text: 'Basic Debug', action: 'enableBasicDebug', class: '' },
|
|
3113
|
+
{ text: 'Full Debug', action: 'enableFullDebug', class: '' },
|
|
3114
|
+
{ text: 'Sequential', action: 'enableSequentialMode', class: '' },
|
|
3115
|
+
{ text: 'Clear Debug', action: 'clearAllDebug', class: 'danger' },
|
|
3116
|
+
{ text: 'Settings', action: 'showDebugInfo', class: '' }
|
|
3117
|
+
];
|
|
3118
|
+
buttons.forEach(btn => {
|
|
3119
|
+
const $button = this.$('<button>')
|
|
3120
|
+
.text(btn.text)
|
|
3121
|
+
.addClass('jqhtml-debug-btn' + (btn.class ? ` ${btn.class}` : ''))
|
|
3122
|
+
.on('click', () => this.executeAction(btn.action));
|
|
3123
|
+
$controls.append($button);
|
|
3124
|
+
});
|
|
3125
|
+
// Add minimize/close button
|
|
3126
|
+
const $toggleBtn = this.$('<button>')
|
|
3127
|
+
.text(this.options.compact ? '▼' : '▲')
|
|
3128
|
+
.addClass('jqhtml-debug-toggle')
|
|
3129
|
+
.on('click', () => this.toggle());
|
|
3130
|
+
$controls.append($toggleBtn);
|
|
3131
|
+
// Assemble and add to page
|
|
3132
|
+
$content.append($controls);
|
|
3133
|
+
this.$container.append($content);
|
|
3134
|
+
this.$('body').append(this.$container);
|
|
3135
|
+
}
|
|
3136
|
+
createStatusIndicator() {
|
|
3137
|
+
this.$statusIndicator = this.$('<div>')
|
|
3138
|
+
.addClass('jqhtml-debug-status')
|
|
3139
|
+
.text('Debug: Off')
|
|
3140
|
+
.css({
|
|
3141
|
+
position: 'fixed',
|
|
3142
|
+
top: '10px',
|
|
3143
|
+
right: '10px',
|
|
3144
|
+
background: '#2c3e50',
|
|
3145
|
+
color: 'white',
|
|
3146
|
+
padding: '5px 10px',
|
|
3147
|
+
borderRadius: '4px',
|
|
3148
|
+
fontSize: '0.75rem',
|
|
3149
|
+
zIndex: '10001',
|
|
3150
|
+
opacity: '0.8',
|
|
3151
|
+
fontFamily: 'monospace'
|
|
3152
|
+
});
|
|
3153
|
+
this.$('body').append(this.$statusIndicator);
|
|
3154
|
+
}
|
|
3155
|
+
addStyles() {
|
|
3156
|
+
// Check if styles already exist
|
|
3157
|
+
if (this.$('#jqhtml-debug-styles').length > 0)
|
|
3158
|
+
return;
|
|
3159
|
+
// Create and inject CSS using jQuery - concatenated strings for better minification
|
|
3160
|
+
const $style = this.$('<style>')
|
|
3161
|
+
.attr('id', 'jqhtml-debug-styles')
|
|
3162
|
+
.text('.jqhtml-debug-overlay {' +
|
|
3163
|
+
'position: fixed;' +
|
|
3164
|
+
'left: 0;' +
|
|
3165
|
+
'right: 0;' +
|
|
3166
|
+
'z-index: 10000;' +
|
|
3167
|
+
'font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, monospace;' +
|
|
3168
|
+
'font-size: 0.8rem;' +
|
|
3169
|
+
'box-shadow: 0 2px 10px rgba(0,0,0,0.2);' +
|
|
3170
|
+
'}' +
|
|
3171
|
+
'.jqhtml-debug-overlay.top {' +
|
|
3172
|
+
'top: 0;' +
|
|
3173
|
+
'}' +
|
|
3174
|
+
'.jqhtml-debug-overlay.bottom {' +
|
|
3175
|
+
'bottom: 0;' +
|
|
3176
|
+
'}' +
|
|
3177
|
+
'.jqhtml-debug-overlay.dark {' +
|
|
3178
|
+
'background: #34495e;' +
|
|
3179
|
+
'color: #ecf0f1;' +
|
|
3180
|
+
'}' +
|
|
3181
|
+
'.jqhtml-debug-overlay.light {' +
|
|
3182
|
+
'background: #f8f9fa;' +
|
|
3183
|
+
'color: #333;' +
|
|
3184
|
+
'border-bottom: 1px solid #dee2e6;' +
|
|
3185
|
+
'}' +
|
|
3186
|
+
'.jqhtml-debug-content {' +
|
|
3187
|
+
'padding: 0.5rem 1rem;' +
|
|
3188
|
+
'}' +
|
|
3189
|
+
'.jqhtml-debug-controls {' +
|
|
3190
|
+
'display: flex;' +
|
|
3191
|
+
'flex-wrap: wrap;' +
|
|
3192
|
+
'gap: 8px;' +
|
|
3193
|
+
'align-items: center;' +
|
|
3194
|
+
'}' +
|
|
3195
|
+
'.jqhtml-debug-title {' +
|
|
3196
|
+
'margin-right: 10px;' +
|
|
3197
|
+
'font-weight: bold;' +
|
|
3198
|
+
'}' +
|
|
3199
|
+
'.jqhtml-debug-btn {' +
|
|
3200
|
+
'padding: 4px 8px;' +
|
|
3201
|
+
'border: none;' +
|
|
3202
|
+
'border-radius: 3px;' +
|
|
3203
|
+
'background: #3498db;' +
|
|
3204
|
+
'color: white;' +
|
|
3205
|
+
'cursor: pointer;' +
|
|
3206
|
+
'font-size: 0.75rem;' +
|
|
3207
|
+
'transition: background 0.2s;' +
|
|
3208
|
+
'}' +
|
|
3209
|
+
'.jqhtml-debug-btn:hover {' +
|
|
3210
|
+
'background: #2980b9;' +
|
|
3211
|
+
'}' +
|
|
3212
|
+
'.jqhtml-debug-btn.success {' +
|
|
3213
|
+
'background: #27ae60;' +
|
|
3214
|
+
'}' +
|
|
3215
|
+
'.jqhtml-debug-btn.success:hover {' +
|
|
3216
|
+
'background: #229954;' +
|
|
3217
|
+
'}' +
|
|
3218
|
+
'.jqhtml-debug-btn.danger {' +
|
|
3219
|
+
'background: #e74c3c;' +
|
|
3220
|
+
'}' +
|
|
3221
|
+
'.jqhtml-debug-btn.danger:hover {' +
|
|
3222
|
+
'background: #c0392b;' +
|
|
3223
|
+
'}' +
|
|
3224
|
+
'.jqhtml-debug-toggle {' +
|
|
3225
|
+
'padding: 4px 8px;' +
|
|
3226
|
+
'border: none;' +
|
|
3227
|
+
'border-radius: 3px;' +
|
|
3228
|
+
'background: #7f8c8d;' +
|
|
3229
|
+
'color: white;' +
|
|
3230
|
+
'cursor: pointer;' +
|
|
3231
|
+
'font-size: 0.75rem;' +
|
|
3232
|
+
'margin-left: auto;' +
|
|
3233
|
+
'}' +
|
|
3234
|
+
'.jqhtml-debug-toggle:hover {' +
|
|
3235
|
+
'background: #6c7b7d;' +
|
|
3236
|
+
'}' +
|
|
3237
|
+
'.jqhtml-debug-status.active {' +
|
|
3238
|
+
'background: #27ae60 !important;' +
|
|
3239
|
+
'}' +
|
|
3240
|
+
'@media (max-width: 768px) {' +
|
|
3241
|
+
'.jqhtml-debug-controls {' +
|
|
3242
|
+
'flex-direction: column;' +
|
|
3243
|
+
'align-items: flex-start;' +
|
|
3244
|
+
'}' +
|
|
3245
|
+
'.jqhtml-debug-title {' +
|
|
3246
|
+
'margin-bottom: 5px;' +
|
|
3247
|
+
'}' +
|
|
3248
|
+
'}');
|
|
3249
|
+
this.$('head').append($style);
|
|
3250
|
+
}
|
|
3251
|
+
toggle() {
|
|
3252
|
+
// Toggle between compact and full view
|
|
3253
|
+
this.options.compact = !this.options.compact;
|
|
3254
|
+
const $toggleBtn = this.$container.find('.jqhtml-debug-toggle');
|
|
3255
|
+
$toggleBtn.text(this.options.compact ? '▼' : '▲');
|
|
3256
|
+
const $buttons = this.$container.find('.jqhtml-debug-btn');
|
|
3257
|
+
if (this.options.compact) {
|
|
3258
|
+
$buttons.hide();
|
|
3259
|
+
}
|
|
3260
|
+
else {
|
|
3261
|
+
$buttons.show();
|
|
3262
|
+
}
|
|
3263
|
+
}
|
|
3264
|
+
executeAction(action) {
|
|
3265
|
+
const jqhtml = getJqhtml();
|
|
3266
|
+
if (!jqhtml) {
|
|
3267
|
+
console.warn('JQHTML not available - make sure it\'s loaded and exposed globally');
|
|
3268
|
+
return;
|
|
3269
|
+
}
|
|
3270
|
+
switch (action) {
|
|
3271
|
+
case 'enableSlowMotionDebug':
|
|
3272
|
+
jqhtml.setDebugSettings({
|
|
3273
|
+
logFullLifecycle: true,
|
|
3274
|
+
sequentialProcessing: true,
|
|
3275
|
+
delayAfterComponent: 150,
|
|
3276
|
+
delayAfterRender: 200,
|
|
3277
|
+
delayAfterRerender: 250,
|
|
3278
|
+
flashComponents: true,
|
|
3279
|
+
flashDuration: 800,
|
|
3280
|
+
flashColors: {
|
|
3281
|
+
create: '#3498db',
|
|
3282
|
+
render: '#27ae60',
|
|
3283
|
+
ready: '#9b59b6'
|
|
3284
|
+
},
|
|
3285
|
+
profilePerformance: true,
|
|
3286
|
+
highlightSlowRenders: 30,
|
|
3287
|
+
logDispatch: true
|
|
3288
|
+
});
|
|
3289
|
+
this.updateStatus('Slow Motion');
|
|
3290
|
+
console.log('🐛 Slow Motion Debug Mode Enabled');
|
|
3291
|
+
break;
|
|
3292
|
+
case 'enableBasicDebug':
|
|
3293
|
+
jqhtml.enableDebugMode('basic');
|
|
3294
|
+
this.updateStatus('Basic');
|
|
3295
|
+
console.log('🐛 Basic Debug Mode Enabled');
|
|
3296
|
+
break;
|
|
3297
|
+
case 'enableFullDebug':
|
|
3298
|
+
jqhtml.enableDebugMode('full');
|
|
3299
|
+
this.updateStatus('Full');
|
|
3300
|
+
console.log('🐛 Full Debug Mode Enabled');
|
|
3301
|
+
break;
|
|
3302
|
+
case 'enableSequentialMode':
|
|
3303
|
+
jqhtml.setDebugSettings({
|
|
3304
|
+
logCreationReady: true,
|
|
3305
|
+
sequentialProcessing: true,
|
|
3306
|
+
flashComponents: true,
|
|
3307
|
+
profilePerformance: true
|
|
3308
|
+
});
|
|
3309
|
+
this.updateStatus('Sequential');
|
|
3310
|
+
console.log('🐛 Sequential Processing Mode Enabled');
|
|
3311
|
+
break;
|
|
3312
|
+
case 'clearAllDebug':
|
|
3313
|
+
jqhtml.clearDebugSettings();
|
|
3314
|
+
this.updateStatus('Off');
|
|
3315
|
+
console.log('🐛 All Debug Modes Disabled');
|
|
3316
|
+
break;
|
|
3317
|
+
case 'showDebugInfo':
|
|
3318
|
+
const settings = JSON.stringify(jqhtml.debug, null, 2);
|
|
3319
|
+
console.log('🐛 Current Debug Settings:', settings);
|
|
3320
|
+
alert('Debug settings logged to console:\n\n' + (Object.keys(jqhtml.debug).length > 0 ? settings : 'No debug settings active'));
|
|
3321
|
+
break;
|
|
3322
|
+
}
|
|
3323
|
+
}
|
|
3324
|
+
}
|
|
3325
|
+
DebugOverlay.instance = null;
|
|
3326
|
+
// Simplified global convenience functions that use static methods
|
|
3327
|
+
function showDebugOverlay(options) {
|
|
3328
|
+
return DebugOverlay.show(options);
|
|
3329
|
+
}
|
|
3330
|
+
function hideDebugOverlay() {
|
|
3331
|
+
DebugOverlay.hide();
|
|
3332
|
+
}
|
|
3333
|
+
// Auto-initialize if debug query parameter is present
|
|
3334
|
+
if (typeof window !== 'undefined') {
|
|
3335
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
3336
|
+
if (urlParams.get('debug') === 'true' || urlParams.get('jqhtml-debug') === 'true') {
|
|
3337
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
3338
|
+
DebugOverlay.show();
|
|
3339
|
+
});
|
|
3340
|
+
}
|
|
3341
|
+
}
|
|
3342
|
+
|
|
3343
|
+
/**
|
|
3344
|
+
* JQHTML v2 jQuery Plugin
|
|
3345
|
+
*
|
|
3346
|
+
* Extends jQuery with component method:
|
|
3347
|
+
* - $(el).component() - Get component instance
|
|
3348
|
+
* - $(el).component(ComponentClass, args) - Create component
|
|
3349
|
+
*/
|
|
3350
|
+
// Function to initialize jQuery plugin with the correct jQuery instance
|
|
3351
|
+
function init_jquery_plugin(jQuery) {
|
|
3352
|
+
if (!jQuery || !jQuery.fn) {
|
|
3353
|
+
throw new Error('jQuery is required for JQHTML. Please ensure jQuery is loaded before initializing JQHTML.');
|
|
3354
|
+
}
|
|
3355
|
+
// Check if jQuery is from global scope (recommended)
|
|
3356
|
+
if (typeof window !== 'undefined' && window.$ !== jQuery && !jQuery.__jqhtml_checked) {
|
|
3357
|
+
devWarn('jQuery instance appears to be bundled with webpack/modules rather than loaded globally.\n' +
|
|
3358
|
+
'For best compatibility, it is recommended to:\n' +
|
|
3359
|
+
'1. Include jQuery via <script> tag from a CDN (UMD format)\n' +
|
|
3360
|
+
'2. Configure webpack with: externals: { jquery: "$" }\n' +
|
|
3361
|
+
'3. Remove jquery from package.json dependencies\n\n' +
|
|
3362
|
+
'To suppress this warning, set: window.JQHTML_SUPPRESS_WARNINGS = true');
|
|
3363
|
+
// Mark this jQuery instance as checked to avoid duplicate warnings
|
|
3364
|
+
jQuery.__jqhtml_checked = true;
|
|
3365
|
+
}
|
|
3366
|
+
// Store original jQuery constructor
|
|
3367
|
+
const _jqhtml_original_jquery = jQuery;
|
|
3368
|
+
// Override jQuery constructor to handle component instances
|
|
3369
|
+
const JQueryWithComponentSupport = function (selector, context) {
|
|
3370
|
+
// Check if selector is a JQHTML component instance
|
|
3371
|
+
if (selector &&
|
|
3372
|
+
typeof selector === 'object' &&
|
|
3373
|
+
selector.$ &&
|
|
3374
|
+
typeof selector.$sid === 'function' &&
|
|
3375
|
+
typeof selector.id === 'function') {
|
|
3376
|
+
// Return the component's jQuery element
|
|
3377
|
+
return selector.$;
|
|
3378
|
+
}
|
|
3379
|
+
// Otherwise, call original jQuery constructor
|
|
3380
|
+
return new _jqhtml_original_jquery(selector, context);
|
|
3381
|
+
};
|
|
3382
|
+
// Copy all jQuery static properties/methods to the new constructor
|
|
3383
|
+
Object.setPrototypeOf(JQueryWithComponentSupport, _jqhtml_original_jquery);
|
|
3384
|
+
for (const key in _jqhtml_original_jquery) {
|
|
3385
|
+
if (_jqhtml_original_jquery.hasOwnProperty(key)) {
|
|
3386
|
+
JQueryWithComponentSupport[key] = _jqhtml_original_jquery[key];
|
|
3387
|
+
}
|
|
3388
|
+
}
|
|
3389
|
+
// Copy prototype and fn
|
|
3390
|
+
JQueryWithComponentSupport.prototype = _jqhtml_original_jquery.prototype;
|
|
3391
|
+
JQueryWithComponentSupport.fn = _jqhtml_original_jquery.fn;
|
|
3392
|
+
// Replace global jQuery and $ (if in browser environment)
|
|
3393
|
+
if (typeof window !== 'undefined') {
|
|
3394
|
+
window.jQuery = JQueryWithComponentSupport;
|
|
3395
|
+
window.$ = JQueryWithComponentSupport;
|
|
3396
|
+
}
|
|
3397
|
+
// Update local reference for rest of this function
|
|
3398
|
+
jQuery = JQueryWithComponentSupport;
|
|
3399
|
+
// Store original jQuery.fn.val
|
|
3400
|
+
const originalVal = jQuery.fn.val;
|
|
3401
|
+
// Override jQuery.fn.val to support component delegation
|
|
3402
|
+
jQuery.fn.val = function (value) {
|
|
3403
|
+
if (arguments.length === 0) {
|
|
3404
|
+
// Getter mode - return value from first element
|
|
3405
|
+
const firstEl = this.first();
|
|
3406
|
+
if (firstEl.length === 0)
|
|
3407
|
+
return undefined;
|
|
3408
|
+
const component = firstEl.data('_component');
|
|
3409
|
+
const tagName = firstEl.prop('tagName');
|
|
3410
|
+
if (component && typeof component.val === 'function' && tagName !== 'INPUT' && tagName !== 'TEXTAREA') {
|
|
3411
|
+
// Delegate to component's val() method (but not if the component IS an input/textarea)
|
|
3412
|
+
return component.val();
|
|
3413
|
+
}
|
|
3414
|
+
// Fall back to original jQuery val()
|
|
3415
|
+
return originalVal.call(this);
|
|
3416
|
+
}
|
|
3417
|
+
else {
|
|
3418
|
+
// Setter mode - set value on all elements
|
|
3419
|
+
this.each(function () {
|
|
3420
|
+
const $el = jQuery(this);
|
|
3421
|
+
const component = $el.data('_component');
|
|
3422
|
+
const tagName = $el.prop('tagName');
|
|
3423
|
+
if (component && typeof component.val === 'function' && tagName !== 'INPUT' && tagName !== 'TEXTAREA') {
|
|
3424
|
+
// Delegate to component's val() method (but not if the component IS an input/textarea)
|
|
3425
|
+
component.val(value);
|
|
3426
|
+
}
|
|
3427
|
+
else {
|
|
3428
|
+
// Fall back to original jQuery val()
|
|
3429
|
+
originalVal.call($el, value);
|
|
3430
|
+
}
|
|
3431
|
+
});
|
|
3432
|
+
// Return this for jQuery chaining
|
|
3433
|
+
return this;
|
|
3434
|
+
}
|
|
3435
|
+
};
|
|
3436
|
+
// Component instance method
|
|
3437
|
+
jQuery.fn.component = function (componentOrName, args = {}) {
|
|
3438
|
+
const element = this.first ? this.first() : this;
|
|
3439
|
+
if (!componentOrName) {
|
|
3440
|
+
// Getter mode - return existing component or null
|
|
3441
|
+
// Handle empty selector (no elements matched)
|
|
3442
|
+
if (element.length === 0) {
|
|
3443
|
+
return null;
|
|
3444
|
+
}
|
|
3445
|
+
const comp = element.data('_component');
|
|
3446
|
+
// Return null if no component found (instead of throwing)
|
|
3447
|
+
return comp || null;
|
|
3448
|
+
}
|
|
3449
|
+
// Check if component already exists on this element
|
|
3450
|
+
const existingComponent = element.data('_component');
|
|
3451
|
+
if (existingComponent) {
|
|
3452
|
+
// Stop existing component (with error handling to continue on failure)
|
|
3453
|
+
try {
|
|
3454
|
+
existingComponent.stop();
|
|
3455
|
+
}
|
|
3456
|
+
catch (error) {
|
|
3457
|
+
console.warn('[JQHTML] Error stopping existing component during replacement:', error);
|
|
3458
|
+
}
|
|
3459
|
+
// Remove component classes (any class starting with capital letter)
|
|
3460
|
+
const classes = element.attr('class');
|
|
3461
|
+
if (classes) {
|
|
3462
|
+
const classList = classes.split(/\s+/);
|
|
3463
|
+
const nonComponentClasses = classList.filter((cls) => {
|
|
3464
|
+
// Keep class if it doesn't start with capital letter
|
|
3465
|
+
return !cls || cls[0] !== cls[0].toUpperCase() || cls[0] === cls[0].toLowerCase();
|
|
3466
|
+
});
|
|
3467
|
+
element.attr('class', nonComponentClasses.join(' '));
|
|
3468
|
+
}
|
|
3469
|
+
// Remove component data
|
|
3470
|
+
element.removeData('_component');
|
|
3471
|
+
}
|
|
3472
|
+
// Setter mode - create new component
|
|
3473
|
+
let ComponentClass;
|
|
3474
|
+
let componentName;
|
|
3475
|
+
if (typeof componentOrName === 'string') {
|
|
3476
|
+
// Look up by name (use default Component if not found)
|
|
3477
|
+
componentName = componentOrName;
|
|
3478
|
+
const found = get_component_class(componentOrName);
|
|
3479
|
+
// Always pass _component_name when instantiating by string name
|
|
3480
|
+
// This is critical for template resolution, especially when class is inherited via extends chain
|
|
3481
|
+
// Example: 'Contacts_DataGrid' uses DataGrid_Abstract class but needs Contacts_DataGrid template
|
|
3482
|
+
args = { ...args, _component_name: componentName };
|
|
3483
|
+
if (!found) {
|
|
3484
|
+
// Only warn if no template is defined either (truly undefined component)
|
|
3485
|
+
// The get_template() call will handle the warning appropriately
|
|
3486
|
+
// Use the base Jqhtml_Component class
|
|
3487
|
+
ComponentClass = Jqhtml_Component;
|
|
3488
|
+
}
|
|
3489
|
+
else {
|
|
3490
|
+
ComponentClass = found;
|
|
3491
|
+
}
|
|
3492
|
+
}
|
|
3493
|
+
else {
|
|
3494
|
+
// Direct class reference
|
|
3495
|
+
ComponentClass = componentOrName;
|
|
3496
|
+
}
|
|
3497
|
+
// Check if element tag matches expected tag from template
|
|
3498
|
+
let targetElement = element;
|
|
3499
|
+
if (componentName) {
|
|
3500
|
+
const template = get_template(componentName);
|
|
3501
|
+
// Tag precedence: _tag (invocation) > tag (template) > 'div'
|
|
3502
|
+
const expectedTag = args._tag || template.tag || 'div';
|
|
3503
|
+
const currentTag = element.prop('tagName').toLowerCase();
|
|
3504
|
+
if (currentTag !== expectedTag.toLowerCase()) {
|
|
3505
|
+
// Tag mismatch
|
|
3506
|
+
if (args._inner_html) {
|
|
3507
|
+
// Replace element with correct tag (only when using server-rendered content)
|
|
3508
|
+
const newElement = jQuery(`<${expectedTag}></${expectedTag}>`);
|
|
3509
|
+
// Copy all attributes from old element to new element
|
|
3510
|
+
const oldEl = element[0];
|
|
3511
|
+
if (oldEl && oldEl.attributes) {
|
|
3512
|
+
for (let i = 0; i < oldEl.attributes.length; i++) {
|
|
3513
|
+
const attr = oldEl.attributes[i];
|
|
3514
|
+
newElement.attr(attr.name, attr.value);
|
|
3515
|
+
}
|
|
3516
|
+
}
|
|
3517
|
+
// Copy innerHTML
|
|
3518
|
+
newElement.html(element.html());
|
|
3519
|
+
// Replace in DOM
|
|
3520
|
+
element.replaceWith(newElement);
|
|
3521
|
+
targetElement = newElement;
|
|
3522
|
+
}
|
|
3523
|
+
else if (currentTag !== 'body') {
|
|
3524
|
+
// Just warn - don't replace (suppress warning for <body> tags)
|
|
3525
|
+
console.warn(`[JQHTML] Component '${componentName}' expects tag '<${expectedTag}>' but element is '<${currentTag}>'. ` +
|
|
3526
|
+
`Element tag will not be changed. Consider using the correct tag.`);
|
|
3527
|
+
}
|
|
3528
|
+
}
|
|
3529
|
+
}
|
|
3530
|
+
// Create component instance - element first, then args
|
|
3531
|
+
const component = new ComponentClass(targetElement, args);
|
|
3532
|
+
// Boot the component (async, runs in background)
|
|
3533
|
+
component._boot();
|
|
3534
|
+
// Apply debug delay after component creation
|
|
3535
|
+
applyDebugDelay('component');
|
|
3536
|
+
// Return the jQuery element to enable chaining (fluent interface pattern)
|
|
3537
|
+
return targetElement;
|
|
3538
|
+
};
|
|
3539
|
+
// Store original jQuery methods for overriding
|
|
3540
|
+
const _jqhtml_jquery_overrides = {};
|
|
3541
|
+
// EXPERIMENTAL: Override DOM manipulation methods to support component instances as arguments
|
|
3542
|
+
// and to trigger ready() when components are added to the DOM
|
|
3543
|
+
// NOTE: This feature needs thorough testing in production scenarios
|
|
3544
|
+
const dom_insertion_methods = ['append', 'prepend', 'before', 'after', 'replaceWith'];
|
|
3545
|
+
for (const fnname of dom_insertion_methods) {
|
|
3546
|
+
_jqhtml_jquery_overrides[fnname] = jQuery.fn[fnname];
|
|
3547
|
+
jQuery.fn[fnname] = function (...args) {
|
|
3548
|
+
// Resolve all component instances into jQuery elements
|
|
3549
|
+
const resolvedArgs = args.map(arg => {
|
|
3550
|
+
if (arg && typeof arg === 'object' && arg instanceof Jqhtml_Component) {
|
|
3551
|
+
return arg.$;
|
|
3552
|
+
}
|
|
3553
|
+
return arg;
|
|
3554
|
+
});
|
|
3555
|
+
// Make a list of all jQuery elements in the arguments
|
|
3556
|
+
const $elements = resolvedArgs.filter((arg) => arg instanceof jQuery);
|
|
3557
|
+
// Call the original jQuery method with resolved arguments
|
|
3558
|
+
const ret = _jqhtml_jquery_overrides[fnname].apply(this, resolvedArgs);
|
|
3559
|
+
// For each jQuery element that is now in the DOM and hasn't triggered ready yet,
|
|
3560
|
+
// find any uninitialized components and boot them
|
|
3561
|
+
for (const $e of $elements) {
|
|
3562
|
+
// Check if element is in the DOM
|
|
3563
|
+
if ($e.closest('html').length > 0) {
|
|
3564
|
+
// Find any components that haven't been initialized yet
|
|
3565
|
+
$e.find('.Component').addBack('.Component').each(function () {
|
|
3566
|
+
const $comp = jQuery(this);
|
|
3567
|
+
const component = $comp.data('_component');
|
|
3568
|
+
// If component exists and hasn't been booted yet, boot it
|
|
3569
|
+
if (component && !component._ready_state) {
|
|
3570
|
+
component._boot();
|
|
3571
|
+
}
|
|
3572
|
+
});
|
|
3573
|
+
}
|
|
3574
|
+
}
|
|
3575
|
+
return ret;
|
|
3576
|
+
};
|
|
3577
|
+
}
|
|
3578
|
+
// Note: Component destruction is handled automatically by MutationObserver
|
|
3579
|
+
// in lifecycle-manager.ts. No jQuery method overrides needed for cleanup.
|
|
3580
|
+
/**
|
|
3581
|
+
* shallowFind - Find nearest descendants matching selector
|
|
3582
|
+
*
|
|
3583
|
+
* Opposite of closest(): searches downward instead of upward
|
|
3584
|
+
* Stops traversal when a match is found (does not recurse into matched elements)
|
|
3585
|
+
*
|
|
3586
|
+
* Example:
|
|
3587
|
+
* <div id="root">
|
|
3588
|
+
* <div class="Widget"> <!-- MATCHED -->
|
|
3589
|
+
* <div class="Widget"> <!-- EXCLUDED (nested inside another .Widget) -->
|
|
3590
|
+
* </div>
|
|
3591
|
+
* </div>
|
|
3592
|
+
* <div class="Widget"> <!-- MATCHED -->
|
|
3593
|
+
* </div>
|
|
3594
|
+
* </div>
|
|
3595
|
+
*
|
|
3596
|
+
* $('#root').shallowFind('.Widget') returns 2 elements (excludes nested one)
|
|
3597
|
+
*/
|
|
3598
|
+
jQuery.fn.shallowFind = function (selector) {
|
|
3599
|
+
const results = [];
|
|
3600
|
+
// Process each element in the jQuery collection
|
|
3601
|
+
this.each(function () {
|
|
3602
|
+
// Recursive traversal function
|
|
3603
|
+
const traverse = (parent) => {
|
|
3604
|
+
// Check direct children
|
|
3605
|
+
for (let i = 0; i < parent.children.length; i++) {
|
|
3606
|
+
const child = parent.children[i];
|
|
3607
|
+
// Check if this child matches the selector
|
|
3608
|
+
if (jQuery(child).is(selector)) {
|
|
3609
|
+
// Match found - add to results and DON'T recurse into it
|
|
3610
|
+
results.push(child);
|
|
3611
|
+
}
|
|
3612
|
+
else {
|
|
3613
|
+
// No match - recurse into this child
|
|
3614
|
+
traverse(child);
|
|
3615
|
+
}
|
|
3616
|
+
}
|
|
3617
|
+
};
|
|
3618
|
+
// Start traversal from this element
|
|
3619
|
+
traverse(this);
|
|
3620
|
+
});
|
|
3621
|
+
// Return jQuery collection of results
|
|
3622
|
+
return jQuery(results);
|
|
3623
|
+
};
|
|
3624
|
+
// Store original jQuery methods
|
|
3625
|
+
const originalEmpty = jQuery.fn.empty;
|
|
3626
|
+
const originalHtml = jQuery.fn.html;
|
|
3627
|
+
const originalText = jQuery.fn.text;
|
|
3628
|
+
/**
|
|
3629
|
+
* Override jQuery.fn.empty() to stop child components before clearing DOM
|
|
3630
|
+
* This ensures component cleanup hooks fire when DOM is cleared
|
|
3631
|
+
*/
|
|
3632
|
+
jQuery.fn.empty = function () {
|
|
3633
|
+
return this.each(function () {
|
|
3634
|
+
// Stop all child components before clearing DOM
|
|
3635
|
+
jQuery(this).find('.Component').each(function () {
|
|
3636
|
+
const component = jQuery(this).data('_component');
|
|
3637
|
+
if (component && !component._stopped) {
|
|
3638
|
+
component._stop(); // Stop just the component, DOM will be cleared anyway
|
|
3639
|
+
}
|
|
3640
|
+
});
|
|
3641
|
+
// Call original empty
|
|
3642
|
+
originalEmpty.call(jQuery(this));
|
|
3643
|
+
});
|
|
3644
|
+
};
|
|
3645
|
+
/**
|
|
3646
|
+
* Override jQuery.fn.html() to stop child components when setting new content
|
|
3647
|
+
* When used as setter, calls .empty() first to properly cleanup components
|
|
3648
|
+
*/
|
|
3649
|
+
jQuery.fn.html = function (value) {
|
|
3650
|
+
// Getter - just pass through
|
|
3651
|
+
if (arguments.length === 0) {
|
|
3652
|
+
return originalHtml.call(this);
|
|
3653
|
+
}
|
|
3654
|
+
// Setter - empty first (which stops components), then set content
|
|
3655
|
+
return this.each(function () {
|
|
3656
|
+
jQuery(this).empty();
|
|
3657
|
+
originalHtml.call(jQuery(this), value);
|
|
3658
|
+
});
|
|
3659
|
+
};
|
|
3660
|
+
/**
|
|
3661
|
+
* Override jQuery.fn.text() to stop child components when setting new content
|
|
3662
|
+
* When used as setter, calls .empty() first to properly cleanup components
|
|
3663
|
+
*/
|
|
3664
|
+
jQuery.fn.text = function (value) {
|
|
3665
|
+
// Getter - just pass through
|
|
3666
|
+
if (arguments.length === 0) {
|
|
3667
|
+
return originalText.call(this);
|
|
3668
|
+
}
|
|
3669
|
+
// Setter - empty first (which stops components), then set content
|
|
3670
|
+
return this.each(function () {
|
|
3671
|
+
jQuery(this).empty();
|
|
3672
|
+
originalText.call(jQuery(this), value);
|
|
3673
|
+
});
|
|
3674
|
+
};
|
|
3675
|
+
}
|
|
3676
|
+
// Try to auto-initialize if global jQuery exists
|
|
3677
|
+
if (typeof window !== 'undefined' && window.jQuery) {
|
|
3678
|
+
init_jquery_plugin(window.jQuery);
|
|
3679
|
+
}
|
|
3680
|
+
|
|
3681
|
+
/**
|
|
3682
|
+
* Jqhtml_Local_Storage - Scoped local storage for JQHTML component caching
|
|
3683
|
+
*
|
|
3684
|
+
* Provides safe, scoped access to localStorage with automatic handling of
|
|
3685
|
+
* unavailable storage, quota exceeded errors, and scope invalidation.
|
|
3686
|
+
*
|
|
3687
|
+
* Key Features:
|
|
3688
|
+
* - **Manual scoping**: Cache key must be set via set_cache_key() before use
|
|
3689
|
+
* - **Graceful degradation**: Returns null when storage unavailable or cache key not set
|
|
3690
|
+
* - **Quota management**: Auto-clears storage when full and retries operation
|
|
3691
|
+
* - **Scope validation**: Clears storage when cache key changes
|
|
3692
|
+
* - **Developer-friendly keys**: Scoped suffix allows easy inspection in dev tools
|
|
3693
|
+
*
|
|
3694
|
+
* Scoping Strategy:
|
|
3695
|
+
* Storage is scoped by a user-provided cache key (typically a session identifier,
|
|
3696
|
+
* user ID, or combination of relevant scope factors). This scope is stored in
|
|
3697
|
+
* `_jqhtml_cache_key`. If the cache key changes between page loads, all JQHTML
|
|
3698
|
+
* storage is cleared to prevent stale data from different sessions/users.
|
|
3699
|
+
*
|
|
3700
|
+
* Key Format:
|
|
3701
|
+
* Keys are stored as: `jqhtml::developer_key::cache_key`
|
|
3702
|
+
* Example: `jqhtml::User_Profile_data::user_123`
|
|
3703
|
+
*
|
|
3704
|
+
* The `jqhtml::` prefix identifies JQHTML keys, allowing safe clearing of only our keys
|
|
3705
|
+
* without affecting other JavaScript libraries. This enables transparent coexistence
|
|
3706
|
+
* with third-party libraries that also use browser storage.
|
|
3707
|
+
*
|
|
3708
|
+
* Quota Exceeded Handling:
|
|
3709
|
+
* When storage quota is exceeded during a set operation, only JQHTML keys (prefixed with
|
|
3710
|
+
* `jqhtml::`) are cleared, preserving other libraries' data. The operation is then retried
|
|
3711
|
+
* once. This ensures the application continues functioning even when storage is full.
|
|
3712
|
+
*
|
|
3713
|
+
* Usage:
|
|
3714
|
+
* // Must set cache key first (typically done once on page load)
|
|
3715
|
+
* Jqhtml_Local_Storage.set_cache_key('user_123');
|
|
3716
|
+
*
|
|
3717
|
+
* // Then use storage normally
|
|
3718
|
+
* Jqhtml_Local_Storage.set('cached_component', {html: '...', timestamp: Date.now()});
|
|
3719
|
+
* const cached = Jqhtml_Local_Storage.get('cached_component');
|
|
3720
|
+
* Jqhtml_Local_Storage.remove('cached_component');
|
|
3721
|
+
*
|
|
3722
|
+
* IMPORTANT - Volatile Storage:
|
|
3723
|
+
* Storage can be cleared at any time due to:
|
|
3724
|
+
* - User clearing browser data
|
|
3725
|
+
* - Private browsing mode restrictions
|
|
3726
|
+
* - Quota exceeded errors
|
|
3727
|
+
* - Cache key changes
|
|
3728
|
+
* - Browser storage unavailable
|
|
3729
|
+
*
|
|
3730
|
+
* Therefore, NEVER store critical data. Only use for:
|
|
3731
|
+
* - Cached component HTML (performance optimization)
|
|
3732
|
+
* - Transient UI state (convenience, not required)
|
|
3733
|
+
*
|
|
3734
|
+
* @internal This class is not exposed in the public API
|
|
3735
|
+
*/
|
|
3736
|
+
class Jqhtml_Local_Storage {
|
|
3737
|
+
/**
|
|
3738
|
+
* Set the cache key and initialize storage
|
|
3739
|
+
* Must be called before any get/set operations
|
|
3740
|
+
* @param {string} cache_key - Unique identifier for this cache scope
|
|
3741
|
+
*/
|
|
3742
|
+
static set_cache_key(cache_key) {
|
|
3743
|
+
this._cache_key = cache_key;
|
|
3744
|
+
this._init();
|
|
3745
|
+
}
|
|
3746
|
+
/**
|
|
3747
|
+
* Check if cache key has been set
|
|
3748
|
+
* @returns {boolean} True if cache key is configured
|
|
3749
|
+
*/
|
|
3750
|
+
static has_cache_key() {
|
|
3751
|
+
return this._cache_key !== null;
|
|
3752
|
+
}
|
|
3753
|
+
/**
|
|
3754
|
+
* Initialize storage system and validate scope
|
|
3755
|
+
* Called automatically after cache key is set
|
|
3756
|
+
* @private
|
|
3757
|
+
*/
|
|
3758
|
+
static _init() {
|
|
3759
|
+
// Check storage availability
|
|
3760
|
+
if (this._storage_available === null) {
|
|
3761
|
+
this._storage_available = this._is_storage_available();
|
|
3762
|
+
}
|
|
3763
|
+
if (!this._storage_available || !this._cache_key) {
|
|
3764
|
+
return;
|
|
3765
|
+
}
|
|
3766
|
+
// Validate scope
|
|
3767
|
+
this._validate_scope();
|
|
3768
|
+
this._initialized = true;
|
|
3769
|
+
}
|
|
3770
|
+
/**
|
|
3771
|
+
* Check if localStorage is available
|
|
3772
|
+
* @returns {boolean}
|
|
3773
|
+
* @private
|
|
3774
|
+
*/
|
|
3775
|
+
static _is_storage_available() {
|
|
3776
|
+
try {
|
|
3777
|
+
const storage = window.localStorage;
|
|
3778
|
+
const test = '__jqhtml_storage_test__';
|
|
3779
|
+
storage.setItem(test, test);
|
|
3780
|
+
storage.removeItem(test);
|
|
3781
|
+
return true;
|
|
3782
|
+
}
|
|
3783
|
+
catch (e) {
|
|
3784
|
+
return false;
|
|
3785
|
+
}
|
|
3786
|
+
}
|
|
3787
|
+
/**
|
|
3788
|
+
* Validate storage scope and clear JQHTML keys if cache key changed
|
|
3789
|
+
* Only clears keys prefixed with 'jqhtml::' to preserve other libraries' data
|
|
3790
|
+
* @private
|
|
3791
|
+
*/
|
|
3792
|
+
static _validate_scope() {
|
|
3793
|
+
if (!this._storage_available) {
|
|
3794
|
+
return;
|
|
3795
|
+
}
|
|
3796
|
+
try {
|
|
3797
|
+
const stored_key = localStorage.getItem('_jqhtml_cache_key');
|
|
3798
|
+
// If cache key exists and has changed, clear only JQHTML keys
|
|
3799
|
+
if (stored_key !== null && stored_key !== this._cache_key) {
|
|
3800
|
+
console.log('[JQHTML Local Storage] Cache key changed, clearing JQHTML keys:', {
|
|
3801
|
+
old_key: stored_key,
|
|
3802
|
+
new_key: this._cache_key,
|
|
3803
|
+
});
|
|
3804
|
+
this._clear_jqhtml_keys();
|
|
3805
|
+
localStorage.setItem('_jqhtml_cache_key', this._cache_key);
|
|
3806
|
+
}
|
|
3807
|
+
else if (stored_key === null) {
|
|
3808
|
+
// First time JQHTML is using this storage - just set the key, don't clear
|
|
3809
|
+
console.log('[JQHTML Local Storage] Initializing cache key (first use):', {
|
|
3810
|
+
new_key: this._cache_key,
|
|
3811
|
+
});
|
|
3812
|
+
localStorage.setItem('_jqhtml_cache_key', this._cache_key);
|
|
3813
|
+
}
|
|
3814
|
+
}
|
|
3815
|
+
catch (e) {
|
|
3816
|
+
console.error('[JQHTML Local Storage] Failed to validate scope:', e);
|
|
3817
|
+
}
|
|
3818
|
+
}
|
|
3819
|
+
/**
|
|
3820
|
+
* Clear only JQHTML keys from storage (keys starting with 'jqhtml::')
|
|
3821
|
+
* Preserves keys from other libraries
|
|
3822
|
+
* @private
|
|
3823
|
+
*/
|
|
3824
|
+
static _clear_jqhtml_keys() {
|
|
3825
|
+
if (!this._storage_available) {
|
|
3826
|
+
return;
|
|
3827
|
+
}
|
|
3828
|
+
const keys_to_remove = [];
|
|
3829
|
+
// Collect all JQHTML keys
|
|
3830
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
3831
|
+
const key = localStorage.key(i);
|
|
3832
|
+
if (key && key.startsWith('jqhtml::')) {
|
|
3833
|
+
keys_to_remove.push(key);
|
|
3834
|
+
}
|
|
3835
|
+
}
|
|
3836
|
+
// Remove collected keys
|
|
3837
|
+
keys_to_remove.forEach(key => {
|
|
3838
|
+
try {
|
|
3839
|
+
localStorage.removeItem(key);
|
|
3840
|
+
}
|
|
3841
|
+
catch (e) {
|
|
3842
|
+
console.error('[JQHTML Local Storage] Failed to remove key:', key, e);
|
|
3843
|
+
}
|
|
3844
|
+
});
|
|
3845
|
+
console.log(`[JQHTML Local Storage] Cleared ${keys_to_remove.length} JQHTML keys`);
|
|
3846
|
+
}
|
|
3847
|
+
/**
|
|
3848
|
+
* Build scoped key with JQHTML namespace prefix
|
|
3849
|
+
* @param {string} key - Developer-provided key
|
|
3850
|
+
* @returns {string}
|
|
3851
|
+
* @private
|
|
3852
|
+
*/
|
|
3853
|
+
static _build_key(key) {
|
|
3854
|
+
return `jqhtml::${key}::${this._cache_key}`;
|
|
3855
|
+
}
|
|
3856
|
+
/**
|
|
3857
|
+
* Check if storage is ready for use
|
|
3858
|
+
* @returns {boolean}
|
|
3859
|
+
* @private
|
|
3860
|
+
*/
|
|
3861
|
+
static _is_ready() {
|
|
3862
|
+
return this._storage_available === true && this._cache_key !== null && this._initialized;
|
|
3863
|
+
}
|
|
3864
|
+
/**
|
|
3865
|
+
* Set item in localStorage
|
|
3866
|
+
* @param {string} key - Storage key
|
|
3867
|
+
* @param {*} value - Value to store (will be JSON serialized)
|
|
3868
|
+
*/
|
|
3869
|
+
static set(key, value) {
|
|
3870
|
+
if (!this._is_ready()) {
|
|
3871
|
+
return;
|
|
3872
|
+
}
|
|
3873
|
+
// Check size before attempting to store (1MB limit)
|
|
3874
|
+
const serialized = JSON.stringify(value);
|
|
3875
|
+
const size_bytes = new Blob([serialized]).size;
|
|
3876
|
+
const size_mb = size_bytes / (1024 * 1024);
|
|
3877
|
+
if (size_mb > 1) {
|
|
3878
|
+
console.warn(`[JQHTML Local Storage] Skipping set - value too large (${size_mb.toFixed(2)}MB > 1MB limit)`, { key, size_bytes, size_mb });
|
|
3879
|
+
return;
|
|
3880
|
+
}
|
|
3881
|
+
this._set_item(key, value, serialized);
|
|
3882
|
+
}
|
|
3883
|
+
/**
|
|
3884
|
+
* Get item from localStorage
|
|
3885
|
+
* @param {string} key - Storage key
|
|
3886
|
+
* @returns {*|null} Parsed value or null if not found/unavailable
|
|
3887
|
+
*/
|
|
3888
|
+
static get(key) {
|
|
3889
|
+
if (!this._is_ready()) {
|
|
3890
|
+
return null;
|
|
3891
|
+
}
|
|
3892
|
+
return this._get_item(key);
|
|
3893
|
+
}
|
|
3894
|
+
/**
|
|
3895
|
+
* Remove item from localStorage
|
|
3896
|
+
* @param {string} key - Storage key
|
|
3897
|
+
*/
|
|
3898
|
+
static remove(key) {
|
|
3899
|
+
if (!this._is_ready()) {
|
|
3900
|
+
return;
|
|
3901
|
+
}
|
|
3902
|
+
this._remove_item(key);
|
|
3903
|
+
}
|
|
3904
|
+
/**
|
|
3905
|
+
* Internal set implementation with scope validation and quota handling
|
|
3906
|
+
* @param {string} key
|
|
3907
|
+
* @param {*} value - Original value (not used, kept for clarity)
|
|
3908
|
+
* @param {string} serialized - Pre-serialized JSON string
|
|
3909
|
+
* @private
|
|
3910
|
+
*/
|
|
3911
|
+
static _set_item(key, value, serialized) {
|
|
3912
|
+
// Validate scope before every write
|
|
3913
|
+
this._validate_scope();
|
|
3914
|
+
const scoped_key = this._build_key(key);
|
|
3915
|
+
try {
|
|
3916
|
+
localStorage.setItem(scoped_key, serialized);
|
|
3917
|
+
}
|
|
3918
|
+
catch (e) {
|
|
3919
|
+
// Check if quota exceeded
|
|
3920
|
+
if (e.name === 'QuotaExceededError' || e.code === 22) {
|
|
3921
|
+
console.warn('[JQHTML Local Storage] Quota exceeded, clearing JQHTML keys and retrying');
|
|
3922
|
+
// Clear only JQHTML keys and retry once
|
|
3923
|
+
this._clear_jqhtml_keys();
|
|
3924
|
+
localStorage.setItem('_jqhtml_cache_key', this._cache_key);
|
|
3925
|
+
try {
|
|
3926
|
+
localStorage.setItem(scoped_key, serialized);
|
|
3927
|
+
}
|
|
3928
|
+
catch (retry_error) {
|
|
3929
|
+
console.error('[JQHTML Local Storage] Failed to set item after clearing JQHTML keys:', retry_error);
|
|
3930
|
+
}
|
|
3931
|
+
}
|
|
3932
|
+
else {
|
|
3933
|
+
console.error('[JQHTML Local Storage] Failed to set item:', e);
|
|
3934
|
+
}
|
|
3935
|
+
}
|
|
3936
|
+
}
|
|
3937
|
+
/**
|
|
3938
|
+
* Internal get implementation
|
|
3939
|
+
* @param {string} key
|
|
3940
|
+
* @returns {*|null}
|
|
3941
|
+
* @private
|
|
3942
|
+
*/
|
|
3943
|
+
static _get_item(key) {
|
|
3944
|
+
const scoped_key = this._build_key(key);
|
|
3945
|
+
try {
|
|
3946
|
+
const serialized = localStorage.getItem(scoped_key);
|
|
3947
|
+
if (serialized === null) {
|
|
3948
|
+
return null;
|
|
3949
|
+
}
|
|
3950
|
+
return JSON.parse(serialized);
|
|
3951
|
+
}
|
|
3952
|
+
catch (e) {
|
|
3953
|
+
console.error('[JQHTML Local Storage] Failed to get item:', e);
|
|
3954
|
+
return null;
|
|
3955
|
+
}
|
|
3956
|
+
}
|
|
3957
|
+
/**
|
|
3958
|
+
* Internal remove implementation
|
|
3959
|
+
* @param {string} key
|
|
3960
|
+
* @private
|
|
3961
|
+
*/
|
|
3962
|
+
static _remove_item(key) {
|
|
3963
|
+
const scoped_key = this._build_key(key);
|
|
3964
|
+
try {
|
|
3965
|
+
localStorage.removeItem(scoped_key);
|
|
3966
|
+
}
|
|
3967
|
+
catch (e) {
|
|
3968
|
+
console.error('[JQHTML Local Storage] Failed to remove item:', e);
|
|
3969
|
+
}
|
|
3970
|
+
}
|
|
3971
|
+
}
|
|
3972
|
+
Jqhtml_Local_Storage._cache_key = null;
|
|
3973
|
+
Jqhtml_Local_Storage._storage_available = null;
|
|
3974
|
+
Jqhtml_Local_Storage._initialized = false;
|
|
3975
|
+
|
|
3976
|
+
var localStorage$1 = /*#__PURE__*/Object.freeze({
|
|
3977
|
+
__proto__: null,
|
|
3978
|
+
Jqhtml_Local_Storage: Jqhtml_Local_Storage
|
|
3979
|
+
});
|
|
3980
|
+
|
|
3981
|
+
/**
|
|
3982
|
+
* Load Coordinator - Request deduplication for component on_load() calls
|
|
3983
|
+
*
|
|
3984
|
+
* Coordinates parallel component loading to prevent duplicate requests.
|
|
3985
|
+
* When multiple components with identical names and args load simultaneously,
|
|
3986
|
+
* only the first (leader) executes on_load(). Others (followers) wait for
|
|
3987
|
+
* the leader's result.
|
|
3988
|
+
*
|
|
3989
|
+
* Key Concepts:
|
|
3990
|
+
* - **INVOCATION_KEY**: Unique identifier for component name + args combination
|
|
3991
|
+
* - **Leader**: First component to reach on_load() for a given INVOCATION_KEY
|
|
3992
|
+
* - **Follower**: Subsequent components that wait for leader's result
|
|
3993
|
+
*
|
|
3994
|
+
* Lifecycle:
|
|
3995
|
+
* 1. Leader reaches on_load() → create coordination entry
|
|
3996
|
+
* 2. Followers reach on_load() → join waiting queue
|
|
3997
|
+
* 3. Leader completes → distribute data to all followers
|
|
3998
|
+
* 4. Clear coordination entry (no caching)
|
|
3999
|
+
*
|
|
4000
|
+
* @internal This class is not exposed in the public API
|
|
4001
|
+
*/
|
|
4002
|
+
class Load_Coordinator {
|
|
4003
|
+
/**
|
|
4004
|
+
* Generate INVOCATION_KEY from component name and args
|
|
4005
|
+
* Uses deterministic JSON serialization (sorted keys)
|
|
4006
|
+
* Excludes internal properties (those starting with _)
|
|
4007
|
+
*
|
|
4008
|
+
* For functions/objects:
|
|
4009
|
+
* - Checks for ._jqhtml_cache_id property (assigned by RSpade)
|
|
4010
|
+
* - Falls back to .jqhtml_cache_id() method if property doesn't exist
|
|
4011
|
+
* - If neither exists, marks property as uncacheable
|
|
4012
|
+
*
|
|
4013
|
+
* Returns object with:
|
|
4014
|
+
* - key: Cache key string, or null if uncacheable
|
|
4015
|
+
* - uncacheable_property: Name of first property that prevented caching (for debugging)
|
|
4016
|
+
*/
|
|
4017
|
+
static generate_invocation_key(component_name, args) {
|
|
4018
|
+
let uncacheable_property;
|
|
4019
|
+
// Filter out internal properties (starting with _) and serialize args
|
|
4020
|
+
const serializable_args = {};
|
|
4021
|
+
for (const key of Object.keys(args).sort()) {
|
|
4022
|
+
if (key.startsWith('_')) {
|
|
4023
|
+
continue; // Skip internal properties
|
|
4024
|
+
}
|
|
4025
|
+
const value = args[key];
|
|
4026
|
+
const value_type = typeof value;
|
|
4027
|
+
// Handle primitives (string, number, boolean, null, undefined)
|
|
4028
|
+
if (value === null || value === undefined ||
|
|
4029
|
+
value_type === 'string' || value_type === 'number' ||
|
|
4030
|
+
value_type === 'boolean') {
|
|
4031
|
+
serializable_args[key] = value;
|
|
4032
|
+
continue;
|
|
4033
|
+
}
|
|
4034
|
+
// Handle functions and objects
|
|
4035
|
+
if (value_type === 'function' || value_type === 'object') {
|
|
4036
|
+
// Check for ._jqhtml_cache_id property (fastest, what RSpade assigns)
|
|
4037
|
+
if (value._jqhtml_cache_id !== undefined) {
|
|
4038
|
+
serializable_args[key] = `__JQHTML_CACHE_ID__:${String(value._jqhtml_cache_id)}`;
|
|
4039
|
+
continue;
|
|
4040
|
+
}
|
|
4041
|
+
// Check for .jqhtml_cache_id() method (more flexible, for custom objects)
|
|
4042
|
+
if (typeof value.jqhtml_cache_id === 'function') {
|
|
4043
|
+
try {
|
|
4044
|
+
const cache_id = value.jqhtml_cache_id();
|
|
4045
|
+
serializable_args[key] = `__JQHTML_CACHE_ID__:${String(cache_id)}`;
|
|
4046
|
+
continue;
|
|
4047
|
+
}
|
|
4048
|
+
catch (error) {
|
|
4049
|
+
// Method threw error - treat as uncacheable
|
|
4050
|
+
if (!uncacheable_property) {
|
|
4051
|
+
uncacheable_property = key;
|
|
4052
|
+
}
|
|
4053
|
+
return { key: null, uncacheable_property };
|
|
4054
|
+
}
|
|
4055
|
+
}
|
|
4056
|
+
// No cache ID available - this property is uncacheable
|
|
4057
|
+
if (!uncacheable_property) {
|
|
4058
|
+
uncacheable_property = key;
|
|
4059
|
+
}
|
|
4060
|
+
return { key: null, uncacheable_property };
|
|
4061
|
+
}
|
|
4062
|
+
// Unknown type (symbol, bigint, etc.) - uncacheable
|
|
4063
|
+
if (!uncacheable_property) {
|
|
4064
|
+
uncacheable_property = key;
|
|
4065
|
+
}
|
|
4066
|
+
return { key: null, uncacheable_property };
|
|
4067
|
+
}
|
|
4068
|
+
// Try to serialize - if it fails (shouldn't happen now, but safety net), return null
|
|
4069
|
+
try {
|
|
4070
|
+
const sorted_args = JSON.stringify(serializable_args);
|
|
4071
|
+
return { key: `${component_name}::${sorted_args}` };
|
|
4072
|
+
}
|
|
4073
|
+
catch (error) {
|
|
4074
|
+
// Serialization failed unexpectedly
|
|
4075
|
+
return { key: null, uncacheable_property };
|
|
4076
|
+
}
|
|
4077
|
+
}
|
|
4078
|
+
/**
|
|
4079
|
+
* Check if a component should execute on_load() or wait for existing request
|
|
4080
|
+
* Returns true if component should execute (is leader), false if should wait (is follower)
|
|
4081
|
+
*/
|
|
4082
|
+
static should_execute_on_load(component) {
|
|
4083
|
+
const { key } = this.generate_invocation_key(component.component_name(), component.args);
|
|
4084
|
+
const entry = this._registry.get(key);
|
|
4085
|
+
if (!entry) {
|
|
4086
|
+
// No existing request - this component becomes the leader
|
|
4087
|
+
return true;
|
|
4088
|
+
}
|
|
4089
|
+
if (entry.status === 'loading') {
|
|
4090
|
+
// Request in progress - this component becomes a follower
|
|
4091
|
+
entry.waiting.push(component);
|
|
4092
|
+
return false;
|
|
4093
|
+
}
|
|
4094
|
+
// Entry exists but completed/failed - should have been cleaned up
|
|
4095
|
+
// Treat as new leader
|
|
4096
|
+
return true;
|
|
4097
|
+
}
|
|
4098
|
+
/**
|
|
4099
|
+
* Register a leader component that will execute on_load()
|
|
4100
|
+
* Creates coordination entry and returns a function to call when on_load() completes
|
|
4101
|
+
*/
|
|
4102
|
+
static register_leader(component, on_load_promise) {
|
|
4103
|
+
const { key } = this.generate_invocation_key(component.component_name(), component.args);
|
|
4104
|
+
const entry = {
|
|
4105
|
+
status: 'loading',
|
|
4106
|
+
promise: on_load_promise,
|
|
4107
|
+
leader_component: component,
|
|
4108
|
+
leader_data: null,
|
|
4109
|
+
leader_error: null,
|
|
4110
|
+
waiting: []
|
|
4111
|
+
};
|
|
4112
|
+
this._registry.set(key, entry);
|
|
4113
|
+
// Return cleanup function to be called after on_load() completes
|
|
4114
|
+
return () => this._complete_coordination(key, component);
|
|
4115
|
+
}
|
|
4116
|
+
/**
|
|
4117
|
+
* Get the coordination promise for a follower component
|
|
4118
|
+
* Returns a promise that resolves when the leader completes
|
|
4119
|
+
*/
|
|
4120
|
+
static get_coordination_promise(component) {
|
|
4121
|
+
const { key } = this.generate_invocation_key(component.component_name(), component.args);
|
|
4122
|
+
const entry = this._registry.get(key);
|
|
4123
|
+
if (!entry || entry.status !== 'loading') {
|
|
4124
|
+
return null;
|
|
4125
|
+
}
|
|
4126
|
+
return entry.promise;
|
|
4127
|
+
}
|
|
4128
|
+
/**
|
|
4129
|
+
* Complete coordination after leader's on_load() finishes
|
|
4130
|
+
* Distributes data to all waiting followers and cleans up entry
|
|
4131
|
+
* @private
|
|
4132
|
+
*/
|
|
4133
|
+
static _complete_coordination(key, leader) {
|
|
4134
|
+
const entry = this._registry.get(key);
|
|
4135
|
+
if (!entry) {
|
|
4136
|
+
return;
|
|
4137
|
+
}
|
|
4138
|
+
// Capture leader's final state
|
|
4139
|
+
entry.leader_data = leader.data;
|
|
4140
|
+
entry.status = 'completed';
|
|
4141
|
+
// Distribute data to all waiting followers
|
|
4142
|
+
for (const follower of entry.waiting) {
|
|
4143
|
+
try {
|
|
4144
|
+
// Copy leader's data to follower
|
|
4145
|
+
follower.data = entry.leader_data;
|
|
4146
|
+
if (window.jqhtml?.debug?.verbose) {
|
|
4147
|
+
console.log(`[Load Coordinator] Follower ${follower._cid} received data from leader ${leader._cid}`, { key, data: entry.leader_data });
|
|
4148
|
+
}
|
|
4149
|
+
}
|
|
4150
|
+
catch (error) {
|
|
4151
|
+
console.error(`[Load Coordinator] Failed to distribute data to follower ${follower._cid}:`, error);
|
|
4152
|
+
}
|
|
4153
|
+
}
|
|
4154
|
+
// Clear coordination entry immediately (no caching)
|
|
4155
|
+
this._registry.delete(key);
|
|
4156
|
+
if (window.jqhtml?.debug?.verbose) {
|
|
4157
|
+
console.log(`[Load Coordinator] Coordination complete for key: ${key}`, {
|
|
4158
|
+
leader_cid: leader._cid,
|
|
4159
|
+
followers_count: entry.waiting.length,
|
|
4160
|
+
registry_size: this._registry.size
|
|
4161
|
+
});
|
|
4162
|
+
}
|
|
4163
|
+
}
|
|
4164
|
+
/**
|
|
4165
|
+
* Handle leader on_load() error
|
|
4166
|
+
* Propagates error to all followers and cleans up entry
|
|
4167
|
+
*/
|
|
4168
|
+
static handle_leader_error(component, error) {
|
|
4169
|
+
const { key } = this.generate_invocation_key(component.component_name(), component.args);
|
|
4170
|
+
const entry = this._registry.get(key);
|
|
4171
|
+
if (!entry) {
|
|
4172
|
+
return;
|
|
4173
|
+
}
|
|
4174
|
+
entry.leader_error = error;
|
|
4175
|
+
entry.status = 'failed';
|
|
4176
|
+
console.error(`[Load Coordinator] Leader ${component._cid} on_load() failed for key: ${key}`, error);
|
|
4177
|
+
// Propagate error to all followers
|
|
4178
|
+
// Note: Followers will handle errors the same way as if their own on_load() failed
|
|
4179
|
+
// This is transparent to the developer
|
|
4180
|
+
for (const follower of entry.waiting) {
|
|
4181
|
+
console.error(`[Load Coordinator] Follower ${follower._cid} failed due to leader error`, error);
|
|
4182
|
+
// The follower's lifecycle will handle the error naturally
|
|
4183
|
+
// as the promise rejection will propagate
|
|
4184
|
+
}
|
|
4185
|
+
// Clear coordination entry so future requests can retry
|
|
4186
|
+
this._registry.delete(key);
|
|
4187
|
+
if (window.jqhtml?.debug?.verbose) {
|
|
4188
|
+
console.log(`[Load Coordinator] Cleared failed coordination for key: ${key}`, { followers_count: entry.waiting.length });
|
|
4189
|
+
}
|
|
4190
|
+
}
|
|
4191
|
+
/**
|
|
4192
|
+
* Get current registry state (for debugging)
|
|
4193
|
+
*/
|
|
4194
|
+
static get_registry_state() {
|
|
4195
|
+
const state = {};
|
|
4196
|
+
for (const [key, entry] of this._registry.entries()) {
|
|
4197
|
+
state[key] = {
|
|
4198
|
+
status: entry.status,
|
|
4199
|
+
leader_cid: entry.leader_component._cid,
|
|
4200
|
+
waiting_count: entry.waiting.length,
|
|
4201
|
+
waiting_cids: entry.waiting.map(c => c._cid)
|
|
4202
|
+
};
|
|
4203
|
+
}
|
|
4204
|
+
return state;
|
|
4205
|
+
}
|
|
4206
|
+
/**
|
|
4207
|
+
* Clear all coordination entries (for testing/debugging)
|
|
4208
|
+
*/
|
|
4209
|
+
static clear_all() {
|
|
4210
|
+
this._registry.clear();
|
|
4211
|
+
}
|
|
4212
|
+
}
|
|
4213
|
+
Load_Coordinator._registry = new Map();
|
|
4214
|
+
|
|
4215
|
+
var loadCoordinator = /*#__PURE__*/Object.freeze({
|
|
4216
|
+
__proto__: null,
|
|
4217
|
+
Load_Coordinator: Load_Coordinator
|
|
4218
|
+
});
|
|
4219
|
+
|
|
4220
|
+
/**
|
|
4221
|
+
* JQHTML v2 Core Runtime
|
|
4222
|
+
*
|
|
4223
|
+
* Main entry point for the JQHTML runtime library
|
|
4224
|
+
*/
|
|
4225
|
+
// Core exports
|
|
4226
|
+
// Main initialization function
|
|
4227
|
+
function init(jQuery) {
|
|
4228
|
+
// If jQuery is provided, initialize the plugin
|
|
4229
|
+
if (jQuery) {
|
|
4230
|
+
init_jquery_plugin(jQuery);
|
|
4231
|
+
}
|
|
4232
|
+
else if (typeof window !== 'undefined' && window.jQuery) {
|
|
4233
|
+
// Try to use global jQuery
|
|
4234
|
+
init_jquery_plugin(window.jQuery);
|
|
4235
|
+
}
|
|
4236
|
+
else {
|
|
4237
|
+
throw new Error('jQuery is required for JQHTML. Please pass jQuery to init() or ensure it is available globally.');
|
|
4238
|
+
}
|
|
4239
|
+
}
|
|
4240
|
+
// Version - will be replaced during build with actual version from package.json
|
|
4241
|
+
const version = '2.2.222';
|
|
4242
|
+
// Default export with all functionality
|
|
4243
|
+
const jqhtml = {
|
|
4244
|
+
// Core
|
|
4245
|
+
Jqhtml_Component,
|
|
4246
|
+
LifecycleManager,
|
|
4247
|
+
// Registry
|
|
4248
|
+
register_component,
|
|
4249
|
+
get_component_class,
|
|
4250
|
+
register_template,
|
|
4251
|
+
get_template,
|
|
4252
|
+
get_template_by_class,
|
|
4253
|
+
create_component,
|
|
4254
|
+
has_component,
|
|
4255
|
+
get_component_names,
|
|
4256
|
+
get_registered_templates,
|
|
4257
|
+
list_components,
|
|
4258
|
+
// Template system
|
|
4259
|
+
process_instructions,
|
|
4260
|
+
extract_slots,
|
|
4261
|
+
render_template,
|
|
4262
|
+
escape_html,
|
|
4263
|
+
// Version property - internal
|
|
4264
|
+
__version: version,
|
|
4265
|
+
// Debug settings
|
|
4266
|
+
debug: {
|
|
4267
|
+
enabled: false,
|
|
4268
|
+
verbose: false
|
|
4269
|
+
},
|
|
4270
|
+
// Debug helper functions (mainly for internal use but exposed for advanced debugging)
|
|
4271
|
+
setDebugSettings(settings) {
|
|
4272
|
+
Object.assign(this.debug, settings);
|
|
4273
|
+
},
|
|
4274
|
+
enableDebugMode(level = 'basic') {
|
|
4275
|
+
if (level === 'basic') {
|
|
4276
|
+
this.debug.logCreationReady = true;
|
|
4277
|
+
this.debug.logDispatch = true;
|
|
4278
|
+
this.debug.flashComponents = true;
|
|
4279
|
+
}
|
|
4280
|
+
else {
|
|
4281
|
+
this.debug.logFullLifecycle = true;
|
|
4282
|
+
this.debug.logDispatchVerbose = true;
|
|
4283
|
+
this.debug.flashComponents = true;
|
|
4284
|
+
this.debug.profilePerformance = true;
|
|
4285
|
+
this.debug.traceDataFlow = true;
|
|
4286
|
+
}
|
|
4287
|
+
},
|
|
4288
|
+
clearDebugSettings() {
|
|
4289
|
+
this.debug = {};
|
|
4290
|
+
},
|
|
4291
|
+
// Debug overlay methods
|
|
4292
|
+
showDebugOverlay(options) {
|
|
4293
|
+
return DebugOverlay.show(options);
|
|
4294
|
+
},
|
|
4295
|
+
hideDebugOverlay() {
|
|
4296
|
+
return DebugOverlay.hide();
|
|
4297
|
+
},
|
|
4298
|
+
// Export DebugOverlay class for direct access
|
|
4299
|
+
DebugOverlay,
|
|
4300
|
+
// Install globals function
|
|
4301
|
+
installGlobals() {
|
|
4302
|
+
if (typeof window !== 'undefined') {
|
|
4303
|
+
window.jqhtml = this;
|
|
4304
|
+
// Also install class references
|
|
4305
|
+
window.Jqhtml_Component = Jqhtml_Component;
|
|
4306
|
+
window.Jqhtml_LifecycleManager = LifecycleManager;
|
|
4307
|
+
}
|
|
4308
|
+
},
|
|
4309
|
+
// Version display function - shows version of core library and all registered templates
|
|
4310
|
+
_version() {
|
|
4311
|
+
console.log(`JQHTML Core v${this.__version}`);
|
|
4312
|
+
console.log('Registered Templates:');
|
|
4313
|
+
const templateNames = get_component_names();
|
|
4314
|
+
if (templateNames.length === 0) {
|
|
4315
|
+
console.log(' (no templates registered)');
|
|
4316
|
+
}
|
|
4317
|
+
else {
|
|
4318
|
+
for (const name of templateNames) {
|
|
4319
|
+
const template = get_template(name);
|
|
4320
|
+
const templateVersion = template ? (template._jqhtml_version || 'unknown') : 'unknown';
|
|
4321
|
+
console.log(` - ${name}: v${templateVersion}`);
|
|
4322
|
+
}
|
|
4323
|
+
}
|
|
4324
|
+
return this.__version;
|
|
4325
|
+
},
|
|
4326
|
+
// Public version function - returns the stamped version number
|
|
4327
|
+
version() {
|
|
4328
|
+
return version;
|
|
4329
|
+
},
|
|
4330
|
+
// Cache key setter - enables component caching via local storage
|
|
4331
|
+
set_cache_key(cache_key) {
|
|
4332
|
+
Jqhtml_Local_Storage.set_cache_key(cache_key);
|
|
4333
|
+
}
|
|
4334
|
+
};
|
|
4335
|
+
// Auto-register on window for browser environments
|
|
4336
|
+
// This is REQUIRED for compiled templates which use window.jqhtml.register_template()
|
|
4337
|
+
// Templates generated by @jqhtml/webpack-loader and other build tools expect this global
|
|
4338
|
+
// This ensures compatibility whether jqhtml is imported as ES module or loaded via script tag
|
|
4339
|
+
if (typeof window !== 'undefined' && !window.jqhtml) {
|
|
4340
|
+
window.jqhtml = jqhtml;
|
|
4341
|
+
// Also register the component classes for convenience
|
|
4342
|
+
window.Jqhtml_Component = Jqhtml_Component;
|
|
4343
|
+
window.Component = Jqhtml_Component; // For backwards compatibility
|
|
4344
|
+
window.Jqhtml_LifecycleManager = LifecycleManager;
|
|
4345
|
+
// Log in debug mode
|
|
4346
|
+
if (jqhtml.debug?.enabled) {
|
|
4347
|
+
console.log('[JQHTML] Auto-registered window.jqhtml global for template compatibility');
|
|
4348
|
+
}
|
|
4349
|
+
}
|
|
4350
|
+
|
|
4351
|
+
export { DebugOverlay, Jqhtml_Component, LifecycleManager as Jqhtml_LifecycleManager, Jqhtml_Local_Storage, LifecycleManager, Load_Coordinator, applyDebugDelay, create_component, jqhtml as default, devWarn, escape_html, extract_slots, get_component_class, get_component_names, get_registered_templates, get_template, get_template_by_class, handleComponentError, has_component, hideDebugOverlay, init, init_jquery_plugin, isSequentialProcessing, list_components, logDataChange, logDispatch, logInstruction, logLifecycle, process_instructions, register_component, register_template, render_template, showDebugOverlay, version };
|
|
4352
|
+
//# sourceMappingURL=jqhtml-core.esm.js.map
|