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