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