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