@jqhtml/core 2.3.67 → 2.3.68
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/component-cache.d.ts +18 -0
- package/dist/component-cache.d.ts.map +1 -1
- package/dist/component-events.d.ts +11 -6
- package/dist/component-events.d.ts.map +1 -1
- package/dist/component-queue.d.ts +23 -12
- package/dist/component-queue.d.ts.map +1 -1
- package/dist/component.d.ts +40 -0
- package/dist/component.d.ts.map +1 -1
- package/dist/data-proxy.d.ts +5 -2
- package/dist/data-proxy.d.ts.map +1 -1
- package/dist/escape.d.ts +13 -0
- package/dist/escape.d.ts.map +1 -0
- package/dist/index.cjs +1319 -1066
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1320 -1066
- package/dist/index.js.map +1 -1
- package/dist/instruction-processor.d.ts.map +1 -1
- package/dist/jqhtml-core.esm.js +1321 -1067
- package/dist/jqhtml-core.esm.js.map +1 -1
- package/dist/jquery-plugin.d.ts.map +1 -1
- package/dist/lifecycle-manager.d.ts +0 -4
- package/dist/lifecycle-manager.d.ts.map +1 -1
- package/dist/load-coordinator.d.ts +18 -9
- package/dist/load-coordinator.d.ts.map +1 -1
- package/dist/local-storage.d.ts +41 -8
- package/dist/local-storage.d.ts.map +1 -1
- package/dist/preload-data.d.ts +8 -0
- package/dist/preload-data.d.ts.map +1 -1
- package/package.json +1 -1
- package/dist/template-renderer.d.ts +0 -22
- package/dist/template-renderer.d.ts.map +0 -1
package/dist/jqhtml-core.esm.js
CHANGED
|
@@ -1,8 +1,199 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* JQHTML Core v2.3.
|
|
2
|
+
* JQHTML Core v2.3.68
|
|
3
3
|
* (c) 2025 JQHTML Team
|
|
4
4
|
* Released under the MIT License
|
|
5
5
|
*/
|
|
6
|
+
/**
|
|
7
|
+
* JQHTML Debug Module
|
|
8
|
+
*
|
|
9
|
+
* Provides comprehensive debugging capabilities for JQHTML components
|
|
10
|
+
*/
|
|
11
|
+
// Global debug state
|
|
12
|
+
let performanceMetrics = new Map();
|
|
13
|
+
/**
|
|
14
|
+
* Development warning helper
|
|
15
|
+
* Warnings are suppressed in production builds or when JQHTML_SUPPRESS_WARNINGS is set
|
|
16
|
+
*/
|
|
17
|
+
function devWarn(message) {
|
|
18
|
+
// Check if warnings are suppressed
|
|
19
|
+
if (typeof window !== 'undefined' && window.JQHTML_SUPPRESS_WARNINGS) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
// Check if in production mode
|
|
23
|
+
if (typeof process !== 'undefined' && process.env && process.env.NODE_ENV === 'production') {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
console.warn(`[JQHTML Dev Warning] ${message}`);
|
|
27
|
+
}
|
|
28
|
+
// Get global jqhtml object
|
|
29
|
+
function getJqhtml() {
|
|
30
|
+
if (typeof window !== 'undefined' && window.jqhtml) {
|
|
31
|
+
return window.jqhtml;
|
|
32
|
+
}
|
|
33
|
+
// Fallback: try to get from global if available
|
|
34
|
+
if (typeof globalThis !== 'undefined' && globalThis.jqhtml) {
|
|
35
|
+
return globalThis.jqhtml;
|
|
36
|
+
}
|
|
37
|
+
throw new Error('FATAL: window.jqhtml is not defined. The JQHTML runtime must be loaded before using debug features. ' +
|
|
38
|
+
'Import and initialize @jqhtml/core before attempting to use debug functionality.');
|
|
39
|
+
}
|
|
40
|
+
// Visual flash effect
|
|
41
|
+
function flashComponent(component, eventType) {
|
|
42
|
+
const jqhtml = getJqhtml();
|
|
43
|
+
if (!jqhtml?.debug?.flashComponents)
|
|
44
|
+
return;
|
|
45
|
+
const duration = jqhtml.debug.flashDuration || 500;
|
|
46
|
+
const colors = jqhtml.debug.flashColors || {};
|
|
47
|
+
const color = colors[eventType] || (eventType === 'create' ? '#3498db' :
|
|
48
|
+
eventType === 'render' ? '#27ae60' :
|
|
49
|
+
'#9b59b6');
|
|
50
|
+
// Store original border
|
|
51
|
+
const originalBorder = component.$.css('border');
|
|
52
|
+
// Apply flash border
|
|
53
|
+
component.$.css({
|
|
54
|
+
'border': `2px solid ${color}`,
|
|
55
|
+
'transition': `border ${duration}ms ease-out`
|
|
56
|
+
});
|
|
57
|
+
// Remove after duration
|
|
58
|
+
setTimeout(() => {
|
|
59
|
+
component.$.css('border', originalBorder || '');
|
|
60
|
+
}, duration);
|
|
61
|
+
}
|
|
62
|
+
// Log lifecycle event
|
|
63
|
+
function logLifecycle(component, phase, status) {
|
|
64
|
+
const jqhtml = getJqhtml();
|
|
65
|
+
if (!jqhtml?.debug)
|
|
66
|
+
return;
|
|
67
|
+
const shouldLog = jqhtml.debug.logFullLifecycle ||
|
|
68
|
+
(jqhtml.debug.logCreationReady && (phase === 'create' || phase === 'ready'));
|
|
69
|
+
if (!shouldLog)
|
|
70
|
+
return;
|
|
71
|
+
const componentName = component.constructor.name;
|
|
72
|
+
const timestamp = new Date().toISOString();
|
|
73
|
+
const prefix = `[JQHTML ${timestamp}]`;
|
|
74
|
+
if (status === 'start') {
|
|
75
|
+
console.log(`${prefix} ${componentName}#${component._cid} → ${phase} starting...`);
|
|
76
|
+
// Start performance tracking
|
|
77
|
+
if (jqhtml.debug.profilePerformance) {
|
|
78
|
+
performanceMetrics.set(`${component._cid}_${phase}`, Date.now());
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
let message = `${prefix} ${componentName}#${component._cid} ✓ ${phase} complete`;
|
|
83
|
+
// Add performance data
|
|
84
|
+
if (jqhtml.debug.profilePerformance) {
|
|
85
|
+
const startTime = performanceMetrics.get(`${component._cid}_${phase}`);
|
|
86
|
+
if (startTime) {
|
|
87
|
+
const duration = Date.now() - startTime;
|
|
88
|
+
message += ` (${duration}ms)`;
|
|
89
|
+
// Highlight slow renders
|
|
90
|
+
if (phase === 'render' && jqhtml.debug.highlightSlowRenders &&
|
|
91
|
+
duration > jqhtml.debug.highlightSlowRenders) {
|
|
92
|
+
console.warn(`${prefix} SLOW RENDER: ${componentName}#${component._cid} took ${duration}ms`);
|
|
93
|
+
component.$.css('outline', '2px dashed red');
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
console.log(message);
|
|
98
|
+
// Visual feedback
|
|
99
|
+
if (jqhtml.debug.flashComponents && (phase === 'create' || phase === 'render' || phase === 'ready')) {
|
|
100
|
+
flashComponent(component, phase);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
// Update component tree if enabled
|
|
104
|
+
if (jqhtml.debug.showComponentTree) {
|
|
105
|
+
updateComponentTree();
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// Apply delays based on lifecycle phase
|
|
109
|
+
function applyDebugDelay(phase) {
|
|
110
|
+
const jqhtml = getJqhtml();
|
|
111
|
+
if (!jqhtml?.debug)
|
|
112
|
+
return;
|
|
113
|
+
let delayMs = 0;
|
|
114
|
+
switch (phase) {
|
|
115
|
+
case 'component':
|
|
116
|
+
delayMs = jqhtml.debug.delayAfterComponent || 0;
|
|
117
|
+
break;
|
|
118
|
+
case 'render':
|
|
119
|
+
delayMs = jqhtml.debug.delayAfterRender || 0;
|
|
120
|
+
break;
|
|
121
|
+
case 'rerender':
|
|
122
|
+
delayMs = jqhtml.debug.delayAfterRerender || 0;
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
if (delayMs > 0) {
|
|
126
|
+
console.log(`[JQHTML Debug] Applying ${delayMs}ms delay after ${phase}`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
// Log instruction processing
|
|
130
|
+
function logInstruction(type, data) {
|
|
131
|
+
const jqhtml = getJqhtml();
|
|
132
|
+
if (!jqhtml?.debug?.logInstructionProcessing)
|
|
133
|
+
return;
|
|
134
|
+
console.log(`[JQHTML Instruction] ${type}:`, data);
|
|
135
|
+
}
|
|
136
|
+
// Log data changes
|
|
137
|
+
function logDataChange(component, property, oldValue, newValue) {
|
|
138
|
+
const jqhtml = getJqhtml();
|
|
139
|
+
if (!jqhtml?.debug?.traceDataFlow)
|
|
140
|
+
return;
|
|
141
|
+
console.log(`[JQHTML Data] ${component.constructor.name}#${component._cid}.data.${property}:`, { old: oldValue, new: newValue });
|
|
142
|
+
}
|
|
143
|
+
// Update component tree visualization
|
|
144
|
+
function updateComponentTree() {
|
|
145
|
+
// This would update a debug panel if implemented
|
|
146
|
+
// For now, just log the tree structure periodically
|
|
147
|
+
console.log('[JQHTML Tree] Component hierarchy updated');
|
|
148
|
+
}
|
|
149
|
+
// Router dispatch logging
|
|
150
|
+
function logDispatch(url, route, params, verbose = false) {
|
|
151
|
+
const jqhtml = getJqhtml();
|
|
152
|
+
if (!jqhtml?.debug)
|
|
153
|
+
return;
|
|
154
|
+
const shouldLog = jqhtml.debug.logDispatch || jqhtml.debug.logDispatchVerbose;
|
|
155
|
+
if (!shouldLog)
|
|
156
|
+
return;
|
|
157
|
+
const isVerbose = jqhtml.debug.logDispatchVerbose || verbose;
|
|
158
|
+
if (isVerbose) {
|
|
159
|
+
console.group(`[JQHTML Router] Dispatching: ${url}`);
|
|
160
|
+
console.log('Matched route:', route);
|
|
161
|
+
console.log('Extracted params:', params);
|
|
162
|
+
console.log('Route component:', route.component);
|
|
163
|
+
console.log('Route layout:', route.layout);
|
|
164
|
+
console.log('Route meta:', route.meta);
|
|
165
|
+
console.groupEnd();
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
console.log(`[JQHTML Router] ${url} → ${route.component} (params: ${JSON.stringify(params)})`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
// Check if sequential processing is enabled
|
|
172
|
+
function isSequentialProcessing() {
|
|
173
|
+
const jqhtml = getJqhtml();
|
|
174
|
+
return jqhtml?.debug?.sequentialProcessing || false;
|
|
175
|
+
}
|
|
176
|
+
// Error handling with break on error
|
|
177
|
+
function handleComponentError(component, phase, error) {
|
|
178
|
+
const jqhtml = getJqhtml();
|
|
179
|
+
console.error(`[JQHTML Error] ${component.constructor.name}#${component._cid} failed in ${phase}:`, error);
|
|
180
|
+
if (jqhtml?.debug?.breakOnError) {
|
|
181
|
+
debugger; // This will pause execution in dev tools
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
// Additional debug suggestions that could be implemented:
|
|
185
|
+
//
|
|
186
|
+
// 1. Component Inspector - Click on any component to see its data/args/state
|
|
187
|
+
// 2. Time Travel Debugging - Record state changes and replay them
|
|
188
|
+
// 3. Network Request Tracking - Log all AJAX calls made during load()
|
|
189
|
+
// 4. Memory Leak Detection - Track component creation/destruction
|
|
190
|
+
// 5. Template Compilation Debugging - Show compiled template functions
|
|
191
|
+
// 6. Event Flow Visualization - Show event bubbling through components
|
|
192
|
+
// 7. Dependency Graph - Show which components depend on which data
|
|
193
|
+
// 8. Hot Reload Support - Reload components without losing state
|
|
194
|
+
// 9. Performance Budgets - Warn when components exceed size/time limits
|
|
195
|
+
// 10. Accessibility Auditing - Check for missing ARIA attributes
|
|
196
|
+
|
|
6
197
|
/**
|
|
7
198
|
* JQHTML v2 Lifecycle Manager
|
|
8
199
|
*
|
|
@@ -51,8 +242,9 @@ class LifecycleManager {
|
|
|
51
242
|
// Check if stopped during create
|
|
52
243
|
if (component._stopped)
|
|
53
244
|
return;
|
|
54
|
-
//
|
|
55
|
-
|
|
245
|
+
// NOTE: no trigger('create') here. component.create() already fires it exactly
|
|
246
|
+
// once - its `_ready_state >= 1` guard makes a second create() a no-op, so this
|
|
247
|
+
// duplicate trigger only ever double-fired the event for subscribers.
|
|
56
248
|
// Check for lifecycle truncation flags
|
|
57
249
|
const load_only = component._load_only;
|
|
58
250
|
const load_render_only = component._load_render_only;
|
|
@@ -178,8 +370,30 @@ class LifecycleManager {
|
|
|
178
370
|
return;
|
|
179
371
|
}
|
|
180
372
|
catch (error) {
|
|
181
|
-
|
|
182
|
-
|
|
373
|
+
// A boot that throws must leave a STOPPED component behind, not a half-booted
|
|
374
|
+
// one. The component never reaches 'ready', so an ancestor parked in
|
|
375
|
+
// _wait_for_children_ready() can only be released by the child's 'stop' event;
|
|
376
|
+
// nothing else stops it, so the whole subtree above it would hang forever.
|
|
377
|
+
//
|
|
378
|
+
// And the throw must stop here: _boot() is called unawaited by
|
|
379
|
+
// instruction-processor.initialize_component() and by the jQuery plugin, so a
|
|
380
|
+
// rethrow surfaces as an unhandled promise rejection instead of an error report.
|
|
381
|
+
// The error object stays a console.error argument so callers (and tests) that
|
|
382
|
+
// grep the console for the thrown message still see it.
|
|
383
|
+
try {
|
|
384
|
+
handleComponentError(component, 'boot', error);
|
|
385
|
+
}
|
|
386
|
+
catch {
|
|
387
|
+
// handleComponentError needs the global jqhtml object and throws without it.
|
|
388
|
+
// Never lose the original error to a failure in the reporting path.
|
|
389
|
+
console.error(`Error booting component ${component.component_name()}:`, error);
|
|
390
|
+
}
|
|
391
|
+
try {
|
|
392
|
+
component._stop();
|
|
393
|
+
}
|
|
394
|
+
catch (stop_error) {
|
|
395
|
+
console.error(`[JQHTML] on_stop() threw while unwinding a failed boot:`, stop_error);
|
|
396
|
+
}
|
|
183
397
|
}
|
|
184
398
|
}
|
|
185
399
|
/**
|
|
@@ -188,20 +402,6 @@ class LifecycleManager {
|
|
|
188
402
|
unregister_component(component) {
|
|
189
403
|
this.active_components.delete(component);
|
|
190
404
|
}
|
|
191
|
-
/**
|
|
192
|
-
* Wait for all active components to reach ready state
|
|
193
|
-
*/
|
|
194
|
-
async wait_for_ready() {
|
|
195
|
-
const ready_promises = [];
|
|
196
|
-
for (const component of this.active_components) {
|
|
197
|
-
if (component._ready_state < 4) {
|
|
198
|
-
ready_promises.push(new Promise((resolve) => {
|
|
199
|
-
component.on('ready', () => resolve());
|
|
200
|
-
}));
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
await Promise.all(ready_promises);
|
|
204
|
-
}
|
|
205
405
|
}
|
|
206
406
|
|
|
207
407
|
/**
|
|
@@ -664,30 +864,32 @@ function process_instructions(instructions, target, context, slots) {
|
|
|
664
864
|
// Set innerHTML once - this is the performance win
|
|
665
865
|
// Use native innerHTML for better performance
|
|
666
866
|
target[0].innerHTML = html.join('');
|
|
667
|
-
// Second pass: initialize special attributes and events
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
867
|
+
// Second pass: initialize special attributes and events.
|
|
868
|
+
// One querySelectorAll over the freshly injected subtree, in document
|
|
869
|
+
// order, rather than one querySelector per tracked element: a scan per
|
|
870
|
+
// element made this pass quadratic in the size of the template. Child
|
|
871
|
+
// components have not rendered yet (their elements are empty), so every
|
|
872
|
+
// data-tid here belongs to this level.
|
|
873
|
+
const root = target[0];
|
|
874
|
+
for (const el of Array.from(root.querySelectorAll('[data-tid]'))) {
|
|
875
|
+
const tagData = tagElements[el.getAttribute('data-tid')];
|
|
876
|
+
el.removeAttribute('data-tid');
|
|
877
|
+
if (tagData) {
|
|
674
878
|
// Bind to the component whose template wrote this element, which for
|
|
675
879
|
// spliced content is not the component being rendered.
|
|
676
|
-
apply_attributes(
|
|
880
|
+
apply_attributes($(el), tagData.attrs, tagData.context);
|
|
677
881
|
}
|
|
678
882
|
}
|
|
679
883
|
// Third pass: initialize and boot components in parallel
|
|
680
884
|
// Like v1, all sibling components at this level boot simultaneously
|
|
681
885
|
// DO NOT await - let children boot in background while parent continues
|
|
682
|
-
for (const
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
if (
|
|
686
|
-
const element = $(el);
|
|
687
|
-
el.removeAttribute('data-cid');
|
|
886
|
+
for (const el of Array.from(root.querySelectorAll('[data-cid]'))) {
|
|
887
|
+
const compData = components[el.getAttribute('data-cid')];
|
|
888
|
+
el.removeAttribute('data-cid');
|
|
889
|
+
if (compData) {
|
|
688
890
|
// Boot this component (which will render and boot its children recursively)
|
|
689
891
|
// Fire and forget - don't wait for boot to complete
|
|
690
|
-
initialize_component(
|
|
892
|
+
initialize_component($(el), compData);
|
|
691
893
|
}
|
|
692
894
|
}
|
|
693
895
|
}
|
|
@@ -752,21 +954,13 @@ function process_tag_to_html(instruction, html, tagElements, components, context
|
|
|
752
954
|
if (!key.startsWith('$') && !key.startsWith('on') && !key.startsWith('@') &&
|
|
753
955
|
!key.startsWith('data-bind-') && !key.startsWith('data-__-on-') &&
|
|
754
956
|
(typeof value === 'string' || typeof value === 'number')) {
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
}
|
|
763
|
-
else {
|
|
764
|
-
html.push(` id="${value}:${context._cid}"`);
|
|
765
|
-
}
|
|
766
|
-
}
|
|
767
|
-
else {
|
|
768
|
-
html.push(` ${key}="${value}"`);
|
|
769
|
-
}
|
|
957
|
+
// A hand-written `id` is emitted exactly as written - including on a tracked
|
|
958
|
+
// element. It used to be rewritten to `<value>:<cid>` whenever the element
|
|
959
|
+
// happened to carry some OTHER tracked attribute, so `<label id="x" @click=…>`
|
|
960
|
+
// and `<label id="x">` disagreed. Per-instance ids are what $sid is for, and
|
|
961
|
+
// the compiler emits those already scoped (codegen.ts), so they arrive here as
|
|
962
|
+
// ordinary values and pass straight through.
|
|
963
|
+
html.push(` ${key}="${escape_attribute(value)}"`);
|
|
770
964
|
}
|
|
771
965
|
}
|
|
772
966
|
// Close opening tag
|
|
@@ -777,6 +971,16 @@ function process_tag_to_html(instruction, html, tagElements, components, context
|
|
|
777
971
|
html.push('>');
|
|
778
972
|
}
|
|
779
973
|
}
|
|
974
|
+
/**
|
|
975
|
+
* A double quote inside an attribute value would end the attribute in the
|
|
976
|
+
* HTML string this pass builds; an interpolated value can never legitimately
|
|
977
|
+
* do that, so it is encoded. Nothing else is touched - the value is otherwise
|
|
978
|
+
* emitted exactly as the template produced it.
|
|
979
|
+
*/
|
|
980
|
+
function escape_attribute(value) {
|
|
981
|
+
const s = typeof value === 'string' ? value : String(value);
|
|
982
|
+
return s.indexOf('"') === -1 ? s : s.replace(/"/g, '"');
|
|
983
|
+
}
|
|
780
984
|
/**
|
|
781
985
|
* Process a component instruction to HTML
|
|
782
986
|
*/
|
|
@@ -1142,220 +1346,29 @@ async function initialize_component(element, compData) {
|
|
|
1142
1346
|
if (props._load_only === true || props['$_load_only'] === true) {
|
|
1143
1347
|
options._load_only = true;
|
|
1144
1348
|
}
|
|
1145
|
-
if (props._load_render_only === true || props['$_load_render_only'] === true) {
|
|
1146
|
-
options._load_render_only = true;
|
|
1147
|
-
}
|
|
1148
|
-
// Create component instance - element first, then options
|
|
1149
|
-
const instance = new ComponentClass(element, options);
|
|
1150
|
-
// Set the instantiator (the component that rendered this one in their template)
|
|
1151
|
-
instance._instantiator = context;
|
|
1152
|
-
// Boot the component immediately - this runs its full lifecycle
|
|
1153
|
-
await instance._boot();
|
|
1154
|
-
}
|
|
1155
|
-
/**
|
|
1156
|
-
* Utility to extract slots from instructions
|
|
1157
|
-
*/
|
|
1158
|
-
function extract_slots(instructions) {
|
|
1159
|
-
const slots = {};
|
|
1160
|
-
for (const instruction of instructions) {
|
|
1161
|
-
if (typeof instruction === 'object' && 'slot' in instruction) {
|
|
1162
|
-
const [name] = instruction.slot;
|
|
1163
|
-
slots[name] = instruction;
|
|
1164
|
-
}
|
|
1165
|
-
}
|
|
1166
|
-
return slots;
|
|
1167
|
-
}
|
|
1168
|
-
|
|
1169
|
-
/**
|
|
1170
|
-
* JQHTML Debug Module
|
|
1171
|
-
*
|
|
1172
|
-
* Provides comprehensive debugging capabilities for JQHTML components
|
|
1173
|
-
*/
|
|
1174
|
-
// Global debug state
|
|
1175
|
-
let performanceMetrics = new Map();
|
|
1176
|
-
/**
|
|
1177
|
-
* Development warning helper
|
|
1178
|
-
* Warnings are suppressed in production builds or when JQHTML_SUPPRESS_WARNINGS is set
|
|
1179
|
-
*/
|
|
1180
|
-
function devWarn(message) {
|
|
1181
|
-
// Check if warnings are suppressed
|
|
1182
|
-
if (typeof window !== 'undefined' && window.JQHTML_SUPPRESS_WARNINGS) {
|
|
1183
|
-
return;
|
|
1184
|
-
}
|
|
1185
|
-
// Check if in production mode
|
|
1186
|
-
if (typeof process !== 'undefined' && process.env && process.env.NODE_ENV === 'production') {
|
|
1187
|
-
return;
|
|
1188
|
-
}
|
|
1189
|
-
console.warn(`[JQHTML Dev Warning] ${message}`);
|
|
1190
|
-
}
|
|
1191
|
-
// Get global jqhtml object
|
|
1192
|
-
function getJqhtml() {
|
|
1193
|
-
if (typeof window !== 'undefined' && window.jqhtml) {
|
|
1194
|
-
return window.jqhtml;
|
|
1195
|
-
}
|
|
1196
|
-
// Fallback: try to get from global if available
|
|
1197
|
-
if (typeof globalThis !== 'undefined' && globalThis.jqhtml) {
|
|
1198
|
-
return globalThis.jqhtml;
|
|
1199
|
-
}
|
|
1200
|
-
throw new Error('FATAL: window.jqhtml is not defined. The JQHTML runtime must be loaded before using debug features. ' +
|
|
1201
|
-
'Import and initialize @jqhtml/core before attempting to use debug functionality.');
|
|
1202
|
-
}
|
|
1203
|
-
// Visual flash effect
|
|
1204
|
-
function flashComponent(component, eventType) {
|
|
1205
|
-
const jqhtml = getJqhtml();
|
|
1206
|
-
if (!jqhtml?.debug?.flashComponents)
|
|
1207
|
-
return;
|
|
1208
|
-
const duration = jqhtml.debug.flashDuration || 500;
|
|
1209
|
-
const colors = jqhtml.debug.flashColors || {};
|
|
1210
|
-
const color = colors[eventType] || (eventType === 'create' ? '#3498db' :
|
|
1211
|
-
eventType === 'render' ? '#27ae60' :
|
|
1212
|
-
'#9b59b6');
|
|
1213
|
-
// Store original border
|
|
1214
|
-
const originalBorder = component.$.css('border');
|
|
1215
|
-
// Apply flash border
|
|
1216
|
-
component.$.css({
|
|
1217
|
-
'border': `2px solid ${color}`,
|
|
1218
|
-
'transition': `border ${duration}ms ease-out`
|
|
1219
|
-
});
|
|
1220
|
-
// Remove after duration
|
|
1221
|
-
setTimeout(() => {
|
|
1222
|
-
component.$.css('border', originalBorder || '');
|
|
1223
|
-
}, duration);
|
|
1224
|
-
}
|
|
1225
|
-
// Log lifecycle event
|
|
1226
|
-
function logLifecycle(component, phase, status) {
|
|
1227
|
-
const jqhtml = getJqhtml();
|
|
1228
|
-
if (!jqhtml?.debug)
|
|
1229
|
-
return;
|
|
1230
|
-
const shouldLog = jqhtml.debug.logFullLifecycle ||
|
|
1231
|
-
(jqhtml.debug.logCreationReady && (phase === 'create' || phase === 'ready'));
|
|
1232
|
-
if (!shouldLog)
|
|
1233
|
-
return;
|
|
1234
|
-
const componentName = component.constructor.name;
|
|
1235
|
-
const timestamp = new Date().toISOString();
|
|
1236
|
-
const prefix = `[JQHTML ${timestamp}]`;
|
|
1237
|
-
if (status === 'start') {
|
|
1238
|
-
console.log(`${prefix} ${componentName}#${component._cid} → ${phase} starting...`);
|
|
1239
|
-
// Start performance tracking
|
|
1240
|
-
if (jqhtml.debug.profilePerformance) {
|
|
1241
|
-
performanceMetrics.set(`${component._cid}_${phase}`, Date.now());
|
|
1242
|
-
}
|
|
1243
|
-
}
|
|
1244
|
-
else {
|
|
1245
|
-
let message = `${prefix} ${componentName}#${component._cid} ✓ ${phase} complete`;
|
|
1246
|
-
// Add performance data
|
|
1247
|
-
if (jqhtml.debug.profilePerformance) {
|
|
1248
|
-
const startTime = performanceMetrics.get(`${component._cid}_${phase}`);
|
|
1249
|
-
if (startTime) {
|
|
1250
|
-
const duration = Date.now() - startTime;
|
|
1251
|
-
message += ` (${duration}ms)`;
|
|
1252
|
-
// Highlight slow renders
|
|
1253
|
-
if (phase === 'render' && jqhtml.debug.highlightSlowRenders &&
|
|
1254
|
-
duration > jqhtml.debug.highlightSlowRenders) {
|
|
1255
|
-
console.warn(`${prefix} SLOW RENDER: ${componentName}#${component._cid} took ${duration}ms`);
|
|
1256
|
-
component.$.css('outline', '2px dashed red');
|
|
1257
|
-
}
|
|
1258
|
-
}
|
|
1259
|
-
}
|
|
1260
|
-
console.log(message);
|
|
1261
|
-
// Visual feedback
|
|
1262
|
-
if (jqhtml.debug.flashComponents && (phase === 'create' || phase === 'render' || phase === 'ready')) {
|
|
1263
|
-
flashComponent(component, phase);
|
|
1264
|
-
}
|
|
1265
|
-
}
|
|
1266
|
-
// Update component tree if enabled
|
|
1267
|
-
if (jqhtml.debug.showComponentTree) {
|
|
1268
|
-
updateComponentTree();
|
|
1269
|
-
}
|
|
1270
|
-
}
|
|
1271
|
-
// Apply delays based on lifecycle phase
|
|
1272
|
-
function applyDebugDelay(phase) {
|
|
1273
|
-
const jqhtml = getJqhtml();
|
|
1274
|
-
if (!jqhtml?.debug)
|
|
1275
|
-
return;
|
|
1276
|
-
let delayMs = 0;
|
|
1277
|
-
switch (phase) {
|
|
1278
|
-
case 'component':
|
|
1279
|
-
delayMs = jqhtml.debug.delayAfterComponent || 0;
|
|
1280
|
-
break;
|
|
1281
|
-
case 'render':
|
|
1282
|
-
delayMs = jqhtml.debug.delayAfterRender || 0;
|
|
1283
|
-
break;
|
|
1284
|
-
case 'rerender':
|
|
1285
|
-
delayMs = jqhtml.debug.delayAfterRerender || 0;
|
|
1286
|
-
break;
|
|
1287
|
-
}
|
|
1288
|
-
if (delayMs > 0) {
|
|
1289
|
-
console.log(`[JQHTML Debug] Applying ${delayMs}ms delay after ${phase}`);
|
|
1290
|
-
}
|
|
1291
|
-
}
|
|
1292
|
-
// Log instruction processing
|
|
1293
|
-
function logInstruction(type, data) {
|
|
1294
|
-
const jqhtml = getJqhtml();
|
|
1295
|
-
if (!jqhtml?.debug?.logInstructionProcessing)
|
|
1296
|
-
return;
|
|
1297
|
-
console.log(`[JQHTML Instruction] ${type}:`, data);
|
|
1298
|
-
}
|
|
1299
|
-
// Log data changes
|
|
1300
|
-
function logDataChange(component, property, oldValue, newValue) {
|
|
1301
|
-
const jqhtml = getJqhtml();
|
|
1302
|
-
if (!jqhtml?.debug?.traceDataFlow)
|
|
1303
|
-
return;
|
|
1304
|
-
console.log(`[JQHTML Data] ${component.constructor.name}#${component._cid}.data.${property}:`, { old: oldValue, new: newValue });
|
|
1305
|
-
}
|
|
1306
|
-
// Update component tree visualization
|
|
1307
|
-
function updateComponentTree() {
|
|
1308
|
-
// This would update a debug panel if implemented
|
|
1309
|
-
// For now, just log the tree structure periodically
|
|
1310
|
-
console.log('[JQHTML Tree] Component hierarchy updated');
|
|
1311
|
-
}
|
|
1312
|
-
// Router dispatch logging
|
|
1313
|
-
function logDispatch(url, route, params, verbose = false) {
|
|
1314
|
-
const jqhtml = getJqhtml();
|
|
1315
|
-
if (!jqhtml?.debug)
|
|
1316
|
-
return;
|
|
1317
|
-
const shouldLog = jqhtml.debug.logDispatch || jqhtml.debug.logDispatchVerbose;
|
|
1318
|
-
if (!shouldLog)
|
|
1319
|
-
return;
|
|
1320
|
-
const isVerbose = jqhtml.debug.logDispatchVerbose || verbose;
|
|
1321
|
-
if (isVerbose) {
|
|
1322
|
-
console.group(`[JQHTML Router] Dispatching: ${url}`);
|
|
1323
|
-
console.log('Matched route:', route);
|
|
1324
|
-
console.log('Extracted params:', params);
|
|
1325
|
-
console.log('Route component:', route.component);
|
|
1326
|
-
console.log('Route layout:', route.layout);
|
|
1327
|
-
console.log('Route meta:', route.meta);
|
|
1328
|
-
console.groupEnd();
|
|
1329
|
-
}
|
|
1330
|
-
else {
|
|
1331
|
-
console.log(`[JQHTML Router] ${url} → ${route.component} (params: ${JSON.stringify(params)})`);
|
|
1332
|
-
}
|
|
1333
|
-
}
|
|
1334
|
-
// Check if sequential processing is enabled
|
|
1335
|
-
function isSequentialProcessing() {
|
|
1336
|
-
const jqhtml = getJqhtml();
|
|
1337
|
-
return jqhtml?.debug?.sequentialProcessing || false;
|
|
1349
|
+
if (props._load_render_only === true || props['$_load_render_only'] === true) {
|
|
1350
|
+
options._load_render_only = true;
|
|
1351
|
+
}
|
|
1352
|
+
// Create component instance - element first, then options
|
|
1353
|
+
const instance = new ComponentClass(element, options);
|
|
1354
|
+
// Set the instantiator (the component that rendered this one in their template)
|
|
1355
|
+
instance._instantiator = context;
|
|
1356
|
+
// Boot the component immediately - this runs its full lifecycle
|
|
1357
|
+
await instance._boot();
|
|
1338
1358
|
}
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1359
|
+
/**
|
|
1360
|
+
* Utility to extract slots from instructions
|
|
1361
|
+
*/
|
|
1362
|
+
function extract_slots(instructions) {
|
|
1363
|
+
const slots = {};
|
|
1364
|
+
for (const instruction of instructions) {
|
|
1365
|
+
if (typeof instruction === 'object' && 'slot' in instruction) {
|
|
1366
|
+
const [name] = instruction.slot;
|
|
1367
|
+
slots[name] = instruction;
|
|
1368
|
+
}
|
|
1345
1369
|
}
|
|
1370
|
+
return slots;
|
|
1346
1371
|
}
|
|
1347
|
-
// Additional debug suggestions that could be implemented:
|
|
1348
|
-
//
|
|
1349
|
-
// 1. Component Inspector - Click on any component to see its data/args/state
|
|
1350
|
-
// 2. Time Travel Debugging - Record state changes and replay them
|
|
1351
|
-
// 3. Network Request Tracking - Log all AJAX calls made during load()
|
|
1352
|
-
// 4. Memory Leak Detection - Track component creation/destruction
|
|
1353
|
-
// 5. Template Compilation Debugging - Show compiled template functions
|
|
1354
|
-
// 6. Event Flow Visualization - Show event bubbling through components
|
|
1355
|
-
// 7. Dependency Graph - Show which components depend on which data
|
|
1356
|
-
// 8. Hot Reload Support - Reload components without losing state
|
|
1357
|
-
// 9. Performance Budgets - Warn when components exceed size/time limits
|
|
1358
|
-
// 10. Accessibility Auditing - Check for missing ARIA attributes
|
|
1359
1372
|
|
|
1360
1373
|
/**
|
|
1361
1374
|
* JQHTML Cache Key Serializer
|
|
@@ -1624,15 +1637,12 @@ class Load_Coordinator {
|
|
|
1624
1637
|
/**
|
|
1625
1638
|
* Check if a component should execute on_load() or wait for existing request
|
|
1626
1639
|
* Returns true if component should execute (is leader), false if should wait (is follower)
|
|
1640
|
+
*
|
|
1641
|
+
* @param key - The dedup key the caller captured for this load. Every coordinator
|
|
1642
|
+
* method takes it, because args may legally change during on_load()
|
|
1643
|
+
* and a recomputed key would address a different (or no) entry.
|
|
1627
1644
|
*/
|
|
1628
|
-
static should_execute_on_load(component) {
|
|
1629
|
-
const { key } = this.generate_invocation_key(component.component_name(), component.args);
|
|
1630
|
-
// No identity means no coordination. Without this, every un-keyable component
|
|
1631
|
-
// would share the single `null` registry entry and adopt an unrelated
|
|
1632
|
-
// component's data - the worst failure this system can produce.
|
|
1633
|
-
if (key === null) {
|
|
1634
|
-
return true;
|
|
1635
|
-
}
|
|
1645
|
+
static should_execute_on_load(component, key) {
|
|
1636
1646
|
const entry = this._registry.get(key);
|
|
1637
1647
|
if (!entry) {
|
|
1638
1648
|
// No existing request - this component becomes the leader
|
|
@@ -1651,26 +1661,27 @@ class Load_Coordinator {
|
|
|
1651
1661
|
* Register a leader component that will execute on_load()
|
|
1652
1662
|
* Creates coordination entry and returns a function to call when data is ready
|
|
1653
1663
|
*
|
|
1664
|
+
* @param key - The dedup key captured by the caller (see should_execute_on_load)
|
|
1654
1665
|
* @returns A function that accepts the final data and completes coordination
|
|
1655
1666
|
*/
|
|
1656
|
-
static register_leader(component,
|
|
1657
|
-
|
|
1658
|
-
// Un-keyable component: run alone, register nothing. See should_execute_on_load().
|
|
1659
|
-
if (key === null) {
|
|
1660
|
-
return () => { };
|
|
1661
|
-
}
|
|
1662
|
-
// Create a promise that we control - it resolves when complete_coordination is called
|
|
1667
|
+
static register_leader(component, key) {
|
|
1668
|
+
// Create a promise that we control - it settles when coordination completes or fails
|
|
1663
1669
|
let resolve_promise;
|
|
1664
|
-
|
|
1670
|
+
let reject_promise;
|
|
1671
|
+
const coordination_promise = new Promise((resolve, reject) => {
|
|
1665
1672
|
resolve_promise = resolve;
|
|
1673
|
+
reject_promise = reject;
|
|
1666
1674
|
});
|
|
1675
|
+
// A leader with no followers still has to be able to reject this promise on
|
|
1676
|
+
// error; without a handler that rejection surfaces as an unhandled rejection.
|
|
1677
|
+
coordination_promise.catch(() => { });
|
|
1667
1678
|
const entry = {
|
|
1668
1679
|
status: 'loading',
|
|
1669
|
-
promise: coordination_promise, // Followers await THIS promise, not
|
|
1680
|
+
promise: coordination_promise, // Followers await THIS promise, not on_load's
|
|
1670
1681
|
resolve_promise,
|
|
1682
|
+
reject_promise,
|
|
1671
1683
|
leader_component: component,
|
|
1672
1684
|
leader_data: null,
|
|
1673
|
-
leader_error: null,
|
|
1674
1685
|
waiting: []
|
|
1675
1686
|
};
|
|
1676
1687
|
this._registry.set(key, entry);
|
|
@@ -1679,10 +1690,9 @@ class Load_Coordinator {
|
|
|
1679
1690
|
}
|
|
1680
1691
|
/**
|
|
1681
1692
|
* Get the coordination promise for a follower component
|
|
1682
|
-
* Returns a promise that resolves when the leader completes
|
|
1693
|
+
* Returns a promise that resolves when the leader completes, or rejects if it failed
|
|
1683
1694
|
*/
|
|
1684
|
-
static get_coordination_promise(
|
|
1685
|
-
const { key } = this.generate_invocation_key(component.component_name(), component.args);
|
|
1695
|
+
static get_coordination_promise(key) {
|
|
1686
1696
|
const entry = this._registry.get(key);
|
|
1687
1697
|
if (!entry || entry.status !== 'loading') {
|
|
1688
1698
|
return null;
|
|
@@ -1694,7 +1704,7 @@ class Load_Coordinator {
|
|
|
1694
1704
|
* Stores leader's data, marks entry as completed, and resolves the promise.
|
|
1695
1705
|
* Followers retrieve the data themselves via get_leader_data().
|
|
1696
1706
|
*
|
|
1697
|
-
* @param key - The coordination key
|
|
1707
|
+
* @param key - The coordination key captured at registration
|
|
1698
1708
|
* @param leader - The leader component
|
|
1699
1709
|
* @param final_data - The final data after on_load and normalization
|
|
1700
1710
|
* @private
|
|
@@ -1704,14 +1714,14 @@ class Load_Coordinator {
|
|
|
1704
1714
|
if (!entry) {
|
|
1705
1715
|
return;
|
|
1706
1716
|
}
|
|
1707
|
-
//
|
|
1708
|
-
//
|
|
1717
|
+
// Stored as TEXT so each follower parses its own object graph - sharing one
|
|
1718
|
+
// decoded object would let one component's mutation reach every other.
|
|
1709
1719
|
try {
|
|
1710
|
-
entry.leader_data = JSON.
|
|
1720
|
+
entry.leader_data = JSON.stringify(final_data);
|
|
1711
1721
|
}
|
|
1712
1722
|
catch (error) {
|
|
1713
|
-
//
|
|
1714
|
-
entry.leader_data =
|
|
1723
|
+
// Unserializable data cannot be shared; followers load independently.
|
|
1724
|
+
entry.leader_data = null;
|
|
1715
1725
|
}
|
|
1716
1726
|
entry.status = 'completed';
|
|
1717
1727
|
if (window.jqhtml?.debug?.verbose) {
|
|
@@ -1719,22 +1729,22 @@ class Load_Coordinator {
|
|
|
1719
1729
|
}
|
|
1720
1730
|
// Resolve the promise - followers waiting on it will now wake up
|
|
1721
1731
|
entry.resolve_promise();
|
|
1722
|
-
//
|
|
1723
|
-
//
|
|
1724
|
-
|
|
1732
|
+
// Nobody is waiting, so nothing will ever come back to collect the data.
|
|
1733
|
+
// Without this the registry grows once per distinct name+args, forever.
|
|
1734
|
+
if (entry.waiting.length === 0) {
|
|
1735
|
+
this._registry.delete(key);
|
|
1736
|
+
}
|
|
1725
1737
|
}
|
|
1726
1738
|
/**
|
|
1727
1739
|
* Get leader's data for a follower component
|
|
1728
1740
|
* Called by follower after coordination promise resolves
|
|
1729
|
-
* Returns
|
|
1741
|
+
* Returns a freshly parsed copy and cleans up if this is the last follower
|
|
1742
|
+
*
|
|
1743
|
+
* @param key - The dedup key captured by the follower before it began waiting
|
|
1730
1744
|
*/
|
|
1731
|
-
static get_leader_data(component) {
|
|
1732
|
-
const { key } = this.generate_invocation_key(component.component_name(), component.args);
|
|
1733
|
-
if (!key) {
|
|
1734
|
-
return null;
|
|
1735
|
-
}
|
|
1745
|
+
static get_leader_data(component, key) {
|
|
1736
1746
|
const entry = this._registry.get(key);
|
|
1737
|
-
if (!entry || entry.status !== 'completed') {
|
|
1747
|
+
if (!entry || entry.status !== 'completed' || entry.leader_data === null) {
|
|
1738
1748
|
return null;
|
|
1739
1749
|
}
|
|
1740
1750
|
// Remove this follower from the waiting list
|
|
@@ -1742,6 +1752,8 @@ class Load_Coordinator {
|
|
|
1742
1752
|
if (follower_index !== -1) {
|
|
1743
1753
|
entry.waiting.splice(follower_index, 1);
|
|
1744
1754
|
}
|
|
1755
|
+
// One parse per follower: each gets its OWN object, never a shared reference.
|
|
1756
|
+
const data = JSON.parse(entry.leader_data);
|
|
1745
1757
|
if (window.jqhtml?.debug?.verbose) {
|
|
1746
1758
|
console.log(`[Load Coordinator] Follower ${component._cid} retrieving data from leader ${entry.leader_component._cid}`, { key, remaining_followers: entry.waiting.length });
|
|
1747
1759
|
}
|
|
@@ -1752,29 +1764,25 @@ class Load_Coordinator {
|
|
|
1752
1764
|
console.log(`[Load Coordinator] Coordination complete for key: ${key}`, { registry_size: this._registry.size });
|
|
1753
1765
|
}
|
|
1754
1766
|
}
|
|
1755
|
-
return
|
|
1767
|
+
return data;
|
|
1756
1768
|
}
|
|
1757
1769
|
/**
|
|
1758
1770
|
* Handle leader on_load() error
|
|
1759
|
-
*
|
|
1771
|
+
* Rejects every waiting follower and clears the entry so the next component retries
|
|
1772
|
+
*
|
|
1773
|
+
* @param key - The dedup key captured at registration
|
|
1760
1774
|
*/
|
|
1761
|
-
static handle_leader_error(
|
|
1762
|
-
const { key } = this.generate_invocation_key(component.component_name(), component.args);
|
|
1775
|
+
static handle_leader_error(key, error) {
|
|
1763
1776
|
const entry = this._registry.get(key);
|
|
1764
1777
|
if (!entry) {
|
|
1765
1778
|
return;
|
|
1766
1779
|
}
|
|
1767
|
-
entry.leader_error = error;
|
|
1768
1780
|
entry.status = 'failed';
|
|
1769
|
-
console.error(`[Load Coordinator] Leader ${
|
|
1770
|
-
//
|
|
1771
|
-
//
|
|
1772
|
-
//
|
|
1773
|
-
|
|
1774
|
-
console.error(`[Load Coordinator] Follower ${follower._cid} failed due to leader error`, error);
|
|
1775
|
-
// The follower's lifecycle will handle the error naturally
|
|
1776
|
-
// as the promise rejection will propagate
|
|
1777
|
-
}
|
|
1781
|
+
console.error(`[Load Coordinator] Leader ${entry.leader_component._cid} on_load() failed for key: ${key}`, error);
|
|
1782
|
+
// Reject FIRST: followers are parked on this promise and their catch in _load()
|
|
1783
|
+
// propagates the failure exactly as if their own on_load() had thrown. Deleting
|
|
1784
|
+
// without rejecting left them awaiting a promise nobody could ever settle.
|
|
1785
|
+
entry.reject_promise(error);
|
|
1778
1786
|
// Clear coordination entry so future requests can retry
|
|
1779
1787
|
this._registry.delete(key);
|
|
1780
1788
|
if (window.jqhtml?.debug?.verbose) {
|
|
@@ -1873,6 +1881,31 @@ Load_Coordinator._registry = new Map();
|
|
|
1873
1881
|
* - Transient UI state (convenience, not required)
|
|
1874
1882
|
*
|
|
1875
1883
|
* @internal This class is not exposed in the public API
|
|
1884
|
+
*
|
|
1885
|
+
* ---------------------------------------------------------------------------
|
|
1886
|
+
* REPORTING WHAT WAS DROPPED
|
|
1887
|
+
*
|
|
1888
|
+
* `this.data` is always the serialized copy this file produces - in EVERY cache
|
|
1889
|
+
* mode, including 'none' (see component.ts `_apply_load_result`). Whatever the
|
|
1890
|
+
* serializer cannot express is therefore not merely missing from the cache, it
|
|
1891
|
+
* is missing from `this.data`, and an author needs to be told once rather than
|
|
1892
|
+
* discover it on the next cache hit.
|
|
1893
|
+
*
|
|
1894
|
+
* Two small optional parameters carry that:
|
|
1895
|
+
*
|
|
1896
|
+
* process_for_serialization(value, verbose, seen, path, collector?)
|
|
1897
|
+
* `collector(path, kind)` is invoked for every value that is dropped or
|
|
1898
|
+
* degraded. `path` is a dotted/bracketed path from the root the caller
|
|
1899
|
+
* named; `kind` is one of Strip_Kind. Paths are only BUILT when a
|
|
1900
|
+
* collector is present, so the no-collector path costs nothing.
|
|
1901
|
+
*
|
|
1902
|
+
* Jqhtml_Local_Storage.normalize_for_cache(value, component_name?)
|
|
1903
|
+
* With a component name, and only in development mode, each dropped path
|
|
1904
|
+
* prints exactly ONE console.warn for the life of the page (keyed
|
|
1905
|
+
* `${component_name}:${path}`, the same once-only shape as
|
|
1906
|
+
* warn_uncacheable_args in component-cache.ts). Production converts
|
|
1907
|
+
* silently. Neither ever throws.
|
|
1908
|
+
* ---------------------------------------------------------------------------
|
|
1876
1909
|
*/
|
|
1877
1910
|
// ============================================================================
|
|
1878
1911
|
// Class Registry for Serialization
|
|
@@ -1881,7 +1914,7 @@ Load_Coordinator._registry = new Map();
|
|
|
1881
1914
|
const class_registry = Object.create(null);
|
|
1882
1915
|
// Markers for serialized class instances (unique to avoid collisions with user data)
|
|
1883
1916
|
// Replaced at build time by rollup (see rollup.config.js).
|
|
1884
|
-
const CORE_VERSION = '2.3.
|
|
1917
|
+
const CORE_VERSION = '2.3.68';
|
|
1885
1918
|
const CLASS_MARKER = '__jqhtml_class__';
|
|
1886
1919
|
const PROPS_MARKER = '__jqhtml_props__';
|
|
1887
1920
|
/**
|
|
@@ -1897,21 +1930,20 @@ function register_cache_class(klass) {
|
|
|
1897
1930
|
}
|
|
1898
1931
|
class_registry[klass.name] = klass;
|
|
1899
1932
|
}
|
|
1900
|
-
// ============================================================================
|
|
1901
|
-
// Serialization (Object → String)
|
|
1902
|
-
// ============================================================================
|
|
1903
1933
|
/**
|
|
1904
1934
|
* Serialize a value to JSON string, handling ES6 class instances recursively.
|
|
1905
1935
|
* Returns null if serialization fails for any reason.
|
|
1906
1936
|
*
|
|
1907
1937
|
* @param value - The value to serialize
|
|
1908
1938
|
* @param verbose - Whether to log warnings
|
|
1939
|
+
* @param path - Root path used when reporting drops (only used with a collector)
|
|
1940
|
+
* @param collector - Optional reporter, see the file header
|
|
1909
1941
|
* @returns Serialized JSON string, or null if serialization failed
|
|
1910
1942
|
*/
|
|
1911
|
-
function serialize_value(value, verbose) {
|
|
1943
|
+
function serialize_value(value, verbose, path = '', collector) {
|
|
1912
1944
|
try {
|
|
1913
1945
|
const seen = new WeakSet();
|
|
1914
|
-
const processed = process_for_serialization(value, verbose, seen);
|
|
1946
|
+
const processed = process_for_serialization(value, verbose, seen, path, collector);
|
|
1915
1947
|
if (processed === undefined) {
|
|
1916
1948
|
// Serialization failed - undefined signals failure
|
|
1917
1949
|
return null;
|
|
@@ -1927,14 +1959,23 @@ function serialize_value(value, verbose) {
|
|
|
1927
1959
|
}
|
|
1928
1960
|
/**
|
|
1929
1961
|
* Recursively process a value for serialization.
|
|
1930
|
-
*
|
|
1962
|
+
*
|
|
1963
|
+
* Values the serializer cannot express are DROPPED, never passed through: the
|
|
1964
|
+
* result of this function is what becomes `this.data`, so letting a function or
|
|
1965
|
+
* a DOM node survive here would make `this.data` differ from what the cache can
|
|
1966
|
+
* ever return. A dropped property is omitted from an object and becomes `null`
|
|
1967
|
+
* in an array - JSON's own semantics for the same situation.
|
|
1968
|
+
*
|
|
1969
|
+
* Returns undefined for a value that has to be dropped.
|
|
1931
1970
|
*
|
|
1932
1971
|
* @param value - The value to process
|
|
1933
1972
|
* @param verbose - Whether to log warnings
|
|
1934
1973
|
* @param seen - WeakSet to detect circular references
|
|
1935
|
-
* @
|
|
1974
|
+
* @param path - Dotted path of this value (only built when a collector is present)
|
|
1975
|
+
* @param collector - Optional reporter for dropped/degraded values
|
|
1976
|
+
* @returns Processed value ready for JSON.stringify, or undefined if dropped
|
|
1936
1977
|
*/
|
|
1937
|
-
function process_for_serialization(value, verbose, seen) {
|
|
1978
|
+
function process_for_serialization(value, verbose, seen, path = '', collector) {
|
|
1938
1979
|
// Handle null and undefined
|
|
1939
1980
|
if (value === null) {
|
|
1940
1981
|
return null;
|
|
@@ -1949,12 +1990,44 @@ function process_for_serialization(value, verbose, seen) {
|
|
|
1949
1990
|
return value;
|
|
1950
1991
|
}
|
|
1951
1992
|
// bigint, symbol, function cannot be serialized
|
|
1993
|
+
if (typeof value === 'function') {
|
|
1994
|
+
collector?.(path, 'function');
|
|
1995
|
+
}
|
|
1996
|
+
else if (typeof value === 'symbol') {
|
|
1997
|
+
collector?.(path, 'symbol');
|
|
1998
|
+
}
|
|
1999
|
+
else if (typeof value === 'bigint') {
|
|
2000
|
+
collector?.(path, 'bigint');
|
|
2001
|
+
}
|
|
1952
2002
|
if (verbose) {
|
|
1953
2003
|
console.warn(`[JQHTML Cache] Cannot serialize ${typeof value} - value will be omitted`);
|
|
1954
2004
|
}
|
|
1955
2005
|
// Return undefined to omit this value (not fail entire serialization)
|
|
1956
2006
|
return undefined;
|
|
1957
2007
|
}
|
|
2008
|
+
// Live objects that only LOOK like data. Each is checked before anything
|
|
2009
|
+
// treats it as a bag of properties, because walking one produces a
|
|
2010
|
+
// meaningless husk (a jQuery object becomes {0: ..., length: 1}) instead of
|
|
2011
|
+
// the drop the author needs to hear about.
|
|
2012
|
+
if (typeof value.nodeType === 'number') {
|
|
2013
|
+
collector?.(path, 'dom-node');
|
|
2014
|
+
return undefined;
|
|
2015
|
+
}
|
|
2016
|
+
if (value.jquery) {
|
|
2017
|
+
collector?.(path, 'jquery');
|
|
2018
|
+
return undefined;
|
|
2019
|
+
}
|
|
2020
|
+
// Duck-typed on purpose: importing Jqhtml_Component here would be a cycle
|
|
2021
|
+
// (component.ts imports this file). _cid + component_name() is the pair no
|
|
2022
|
+
// plain data object carries.
|
|
2023
|
+
if (value._cid !== undefined && typeof value.component_name === 'function') {
|
|
2024
|
+
collector?.(path, 'component');
|
|
2025
|
+
return undefined;
|
|
2026
|
+
}
|
|
2027
|
+
if (typeof value.then === 'function') {
|
|
2028
|
+
collector?.(path, 'promise');
|
|
2029
|
+
return undefined;
|
|
2030
|
+
}
|
|
1958
2031
|
// Handle circular references.
|
|
1959
2032
|
// `seen` tracks only the current ancestor path (the DFS stack): an object is
|
|
1960
2033
|
// added on the way down and removed on the way back up (see finally below).
|
|
@@ -1963,10 +2036,11 @@ function process_for_serialization(value, verbose, seen) {
|
|
|
1963
2036
|
// JSON round-trip semantics do not preserve identity anyway, so emitting a
|
|
1964
2037
|
// copy per reference is correct; silently omitting the property is not.
|
|
1965
2038
|
if (seen.has(value)) {
|
|
2039
|
+
collector?.(path, 'circular');
|
|
1966
2040
|
if (verbose) {
|
|
1967
|
-
console.warn('[JQHTML Cache] Circular reference detected -
|
|
2041
|
+
console.warn('[JQHTML Cache] Circular reference detected - cutting the cycle');
|
|
1968
2042
|
}
|
|
1969
|
-
return undefined; //
|
|
2043
|
+
return undefined; // Cut the cycle here; the rest of the tree still serializes
|
|
1970
2044
|
}
|
|
1971
2045
|
seen.add(value);
|
|
1972
2046
|
try {
|
|
@@ -1975,7 +2049,7 @@ function process_for_serialization(value, verbose, seen) {
|
|
|
1975
2049
|
const result = [];
|
|
1976
2050
|
for (let i = 0; i < value.length; i++) {
|
|
1977
2051
|
const item = value[i];
|
|
1978
|
-
const processed = process_for_serialization(item, verbose, seen);
|
|
2052
|
+
const processed = process_for_serialization(item, verbose, seen, collector ? `${path}[${i}]` : '', collector);
|
|
1979
2053
|
// For arrays, we keep undefined as null to preserve indices
|
|
1980
2054
|
result.push(processed === undefined ? null : processed);
|
|
1981
2055
|
}
|
|
@@ -1992,8 +2066,9 @@ function process_for_serialization(value, verbose, seen) {
|
|
|
1992
2066
|
if (value instanceof Map) {
|
|
1993
2067
|
const entries = [];
|
|
1994
2068
|
for (const [k, v] of value) {
|
|
1995
|
-
const
|
|
1996
|
-
const
|
|
2069
|
+
const entry_path = collector ? `${path}.get(${String(k)})` : '';
|
|
2070
|
+
const processedKey = process_for_serialization(k, verbose, seen, entry_path, collector);
|
|
2071
|
+
const processedValue = process_for_serialization(v, verbose, seen, entry_path, collector);
|
|
1997
2072
|
entries.push([processedKey, processedValue]);
|
|
1998
2073
|
}
|
|
1999
2074
|
return {
|
|
@@ -2004,8 +2079,9 @@ function process_for_serialization(value, verbose, seen) {
|
|
|
2004
2079
|
// Handle Set objects
|
|
2005
2080
|
if (value instanceof Set) {
|
|
2006
2081
|
const items = [];
|
|
2082
|
+
let index = 0;
|
|
2007
2083
|
for (const item of value) {
|
|
2008
|
-
items.push(process_for_serialization(item, verbose, seen));
|
|
2084
|
+
items.push(process_for_serialization(item, verbose, seen, collector ? `${path}[Set ${index++}]` : '', collector));
|
|
2009
2085
|
}
|
|
2010
2086
|
return {
|
|
2011
2087
|
[CLASS_MARKER]: 'Set',
|
|
@@ -2016,58 +2092,115 @@ function process_for_serialization(value, verbose, seen) {
|
|
|
2016
2092
|
const ctor = value.constructor;
|
|
2017
2093
|
// Handle registered class instances
|
|
2018
2094
|
if (ctor && ctor.name && class_registry[ctor.name]) {
|
|
2019
|
-
const props = {};
|
|
2020
|
-
// Extract own enumerable properties (bypass any toJSON method)
|
|
2021
|
-
for (const key of Object.keys(value)) {
|
|
2022
|
-
const propValue = value[key];
|
|
2023
|
-
const processed = process_for_serialization(propValue, verbose, seen);
|
|
2024
|
-
// Only include if not undefined (failed serialization)
|
|
2025
|
-
if (processed !== undefined || propValue === undefined) {
|
|
2026
|
-
props[key] = processed;
|
|
2027
|
-
}
|
|
2028
|
-
}
|
|
2029
2095
|
return {
|
|
2030
2096
|
[CLASS_MARKER]: ctor.name,
|
|
2031
|
-
[PROPS_MARKER]:
|
|
2097
|
+
[PROPS_MARKER]: process_own_properties(value, verbose, seen, path, collector)
|
|
2032
2098
|
};
|
|
2033
2099
|
}
|
|
2034
2100
|
// Handle plain objects (constructor is Object or no constructor)
|
|
2035
2101
|
if (ctor === Object || ctor === undefined || ctor === null) {
|
|
2036
|
-
|
|
2037
|
-
for (const key of Object.keys(value)) {
|
|
2038
|
-
const propValue = value[key];
|
|
2039
|
-
const processed = process_for_serialization(propValue, verbose, seen);
|
|
2040
|
-
// Only include if not undefined (unless original was undefined)
|
|
2041
|
-
if (processed !== undefined || propValue === undefined) {
|
|
2042
|
-
result[key] = processed;
|
|
2043
|
-
}
|
|
2044
|
-
}
|
|
2045
|
-
return result;
|
|
2102
|
+
return process_own_properties(value, verbose, seen, path, collector);
|
|
2046
2103
|
}
|
|
2047
|
-
// Unregistered class instance - convert to plain object for hot/cold parity
|
|
2048
|
-
//
|
|
2049
|
-
//
|
|
2104
|
+
// Unregistered class instance - convert to plain object for hot/cold parity.
|
|
2105
|
+
// Properties are preserved, prototype methods are lost, exactly as a cache
|
|
2106
|
+
// hit would produce, so the loss shows up on the first load rather than on
|
|
2107
|
+
// the next page view.
|
|
2108
|
+
collector?.(path, 'unregistered-class', ctor.name);
|
|
2050
2109
|
if (verbose) {
|
|
2051
2110
|
console.warn(`[JQHTML Cache] Converting unregistered class "${ctor.name}" to plain object. ` +
|
|
2052
2111
|
`Methods will be lost. Call jqhtml.register_cache_class(${ctor.name}) to preserve the class.`);
|
|
2053
2112
|
}
|
|
2054
|
-
|
|
2055
|
-
const result = {};
|
|
2056
|
-
for (const key of Object.keys(value)) {
|
|
2057
|
-
const propValue = value[key];
|
|
2058
|
-
const processed = process_for_serialization(propValue, verbose, seen);
|
|
2059
|
-
// Only include if not undefined (unless original was undefined)
|
|
2060
|
-
if (processed !== undefined || propValue === undefined) {
|
|
2061
|
-
result[key] = processed;
|
|
2062
|
-
}
|
|
2063
|
-
}
|
|
2064
|
-
return result;
|
|
2113
|
+
return process_own_properties(value, verbose, seen, path, collector);
|
|
2065
2114
|
}
|
|
2066
2115
|
finally {
|
|
2067
2116
|
// Pop this object off the ancestor path now that its subtree is done.
|
|
2068
2117
|
seen.delete(value);
|
|
2069
2118
|
}
|
|
2070
2119
|
}
|
|
2120
|
+
/**
|
|
2121
|
+
* Walk an object's own enumerable properties (bypassing any toJSON) into a plain
|
|
2122
|
+
* object. Shared by plain objects, registered class instances and degraded
|
|
2123
|
+
* unregistered instances - all three keep exactly the same property semantics.
|
|
2124
|
+
*/
|
|
2125
|
+
function process_own_properties(value, verbose, seen, path, collector) {
|
|
2126
|
+
const result = {};
|
|
2127
|
+
for (const key of Object.keys(value)) {
|
|
2128
|
+
const propValue = value[key];
|
|
2129
|
+
const processed = process_for_serialization(propValue, verbose, seen, collector ? `${path}.${key}` : '', collector);
|
|
2130
|
+
// Only include if not undefined (unless original was undefined)
|
|
2131
|
+
if (processed !== undefined || propValue === undefined) {
|
|
2132
|
+
result[key] = processed;
|
|
2133
|
+
}
|
|
2134
|
+
}
|
|
2135
|
+
return result;
|
|
2136
|
+
}
|
|
2137
|
+
// ============================================================================
|
|
2138
|
+
// Development warnings for dropped values
|
|
2139
|
+
// ============================================================================
|
|
2140
|
+
// One warning per component + path, for the life of the page. A list of 50 rows
|
|
2141
|
+
// each carrying an $on_select callback must print ONE warning, not 50, and must
|
|
2142
|
+
// not print it again on the next reload() or on the next instance of the same
|
|
2143
|
+
// component. Same shape as _warned_uncacheable in component-cache.ts.
|
|
2144
|
+
const _warned_stripped = new Set();
|
|
2145
|
+
/**
|
|
2146
|
+
* What was lost, and where the value actually belongs. Every message names a
|
|
2147
|
+
* destination, because "it was dropped" without "put it here instead" is not
|
|
2148
|
+
* actionable.
|
|
2149
|
+
*/
|
|
2150
|
+
function strip_advice(kind, detail) {
|
|
2151
|
+
switch (kind) {
|
|
2152
|
+
case 'function':
|
|
2153
|
+
return 'a function cannot be serialized. Callbacks are configuration, not loaded data - ' +
|
|
2154
|
+
'pass it in this.args (from the parent template) and keep only ids in this.data.';
|
|
2155
|
+
case 'promise':
|
|
2156
|
+
return 'a promise cannot be serialized. Await it inside on_load() and store the RESULT, ' +
|
|
2157
|
+
'or hold the pending promise in this.state.';
|
|
2158
|
+
case 'dom-node':
|
|
2159
|
+
return 'a DOM node cannot be serialized. Elements, timers, files and sockets belong in this.state.';
|
|
2160
|
+
case 'jquery':
|
|
2161
|
+
return 'a jQuery object cannot be serialized. Elements, timers, files and sockets belong in this.state.';
|
|
2162
|
+
case 'component':
|
|
2163
|
+
return 'a Jqhtml_Component cannot be serialized. Keep component references in this.state ' +
|
|
2164
|
+
'and address children with this.sid().';
|
|
2165
|
+
case 'symbol':
|
|
2166
|
+
return 'a symbol cannot be serialized. Use a string, or keep it in this.state.';
|
|
2167
|
+
case 'bigint':
|
|
2168
|
+
return 'a bigint cannot be serialized. Convert it to a string in on_load(), or keep it in this.state.';
|
|
2169
|
+
case 'circular':
|
|
2170
|
+
return 'it points back to one of its own ancestors, and the cycle was cut. Store the graph ' +
|
|
2171
|
+
'flat (ids instead of back-references) in this.data, or keep the linked form in this.state.';
|
|
2172
|
+
case 'unregistered-class':
|
|
2173
|
+
return `it is an instance of the unregistered class "${detail}", so it was degraded to a plain ` +
|
|
2174
|
+
`object and its methods are gone. Call jqhtml.register_cache_class(${detail}) at startup ` +
|
|
2175
|
+
`to get real instances back.`;
|
|
2176
|
+
}
|
|
2177
|
+
}
|
|
2178
|
+
/**
|
|
2179
|
+
* Build the collector normalize_for_cache() hands to the serializer, or
|
|
2180
|
+
* `undefined` when nobody is listening - production, or a caller that named no
|
|
2181
|
+
* component. Returning undefined (rather than a no-op function) is what keeps
|
|
2182
|
+
* the serializer from building path strings at all on the hot path.
|
|
2183
|
+
*/
|
|
2184
|
+
function make_strip_collector(component_name) {
|
|
2185
|
+
if (!component_name)
|
|
2186
|
+
return undefined;
|
|
2187
|
+
if (get_config().mode !== 'development')
|
|
2188
|
+
return undefined;
|
|
2189
|
+
return (path, kind, detail) => {
|
|
2190
|
+
try {
|
|
2191
|
+
const dedupe_key = `${component_name}:${path}`;
|
|
2192
|
+
if (_warned_stripped.has(dedupe_key))
|
|
2193
|
+
return;
|
|
2194
|
+
_warned_stripped.add(dedupe_key);
|
|
2195
|
+
console.warn(`[JQHTML] <${component_name}> dropped ${path} from this.data: ${strip_advice(kind, detail)}\n` +
|
|
2196
|
+
` this.data is always the serialized copy jqhtml stores in and restores from cache, ` +
|
|
2197
|
+
`so only serializable values survive on_load() - in every cache mode.`, { component: component_name, path, kind });
|
|
2198
|
+
}
|
|
2199
|
+
catch {
|
|
2200
|
+
// A warning must never be able to break a load.
|
|
2201
|
+
}
|
|
2202
|
+
};
|
|
2203
|
+
}
|
|
2071
2204
|
// ============================================================================
|
|
2072
2205
|
// Deserialization (String → Object)
|
|
2073
2206
|
// ============================================================================
|
|
@@ -2411,36 +2544,36 @@ class Jqhtml_Local_Storage {
|
|
|
2411
2544
|
/**
|
|
2412
2545
|
* Perform a serialize/deserialize round-trip on a value.
|
|
2413
2546
|
*
|
|
2414
|
-
*
|
|
2415
|
-
*
|
|
2416
|
-
*
|
|
2417
|
-
*
|
|
2418
|
-
*
|
|
2419
|
-
* registrations immediately rather than only after a page reload.
|
|
2547
|
+
* The result is ALWAYS jqhtml's own copy: nothing of the caller's object graph
|
|
2548
|
+
* is aliased through, and a value the serializer cannot express is dropped
|
|
2549
|
+
* rather than smuggled out. That is what makes `this.data` identical whether it
|
|
2550
|
+
* came from on_load() or from a cache hit - the old "on failure return the
|
|
2551
|
+
* original" branch broke exactly that promise, and only on the hard cases.
|
|
2420
2552
|
*
|
|
2421
2553
|
* @param {any} value - The value to normalize
|
|
2422
|
-
* @
|
|
2554
|
+
* @param {string} [component_name] - Names the component in development warnings.
|
|
2555
|
+
* Omit to convert silently (the unit tests and non-component callers).
|
|
2556
|
+
* @returns {any} The round-tripped value. Never the original object.
|
|
2423
2557
|
*/
|
|
2424
|
-
static normalize_for_cache(value) {
|
|
2558
|
+
static normalize_for_cache(value, component_name) {
|
|
2425
2559
|
const verbose = this._is_verbose();
|
|
2560
|
+
const collector = make_strip_collector(component_name);
|
|
2426
2561
|
// Serialize to JSON string
|
|
2427
|
-
const serialized = serialize_value(value, verbose);
|
|
2562
|
+
const serialized = serialize_value(value, verbose, 'this.data', collector);
|
|
2428
2563
|
if (serialized === null) {
|
|
2429
|
-
//
|
|
2430
|
-
//
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
}
|
|
2434
|
-
return value;
|
|
2564
|
+
// The value as a WHOLE could not be expressed (it was itself a function,
|
|
2565
|
+
// a promise, a DOM node...). The round trip of such a value is nothing;
|
|
2566
|
+
// returning the original would put an uncacheable object into this.data.
|
|
2567
|
+
return {};
|
|
2435
2568
|
}
|
|
2436
2569
|
// Deserialize back to object
|
|
2437
2570
|
const deserialized = deserialize_value(serialized, verbose);
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
return
|
|
2571
|
+
// `null` from deserialize_value() is ambiguous: it means "failed", but a value
|
|
2572
|
+
// that legitimately serialized to the text `null` also comes back as null. The
|
|
2573
|
+
// serialized text disambiguates.
|
|
2574
|
+
if (deserialized === null && serialized !== 'null') {
|
|
2575
|
+
// Only reachable when a registered class constructor blew up on restore.
|
|
2576
|
+
return {};
|
|
2444
2577
|
}
|
|
2445
2578
|
return deserialized;
|
|
2446
2579
|
}
|
|
@@ -2463,7 +2596,10 @@ class Jqhtml_Local_Storage {
|
|
|
2463
2596
|
console.warn('[JQHTML Local Storage] Quota exceeded, clearing JQHTML keys and retrying');
|
|
2464
2597
|
// Clear only JQHTML keys and retry once
|
|
2465
2598
|
this._clear_jqhtml_keys();
|
|
2466
|
-
|
|
2599
|
+
// Re-stamp the SCOPE MARKER, not the raw developer key: _validate_scope()
|
|
2600
|
+
// compares against `${CORE_VERSION}::${key}`, so writing the bare key here
|
|
2601
|
+
// made the very next write look like a scope change and wipe the cache again.
|
|
2602
|
+
localStorage.setItem('_jqhtml_cache_key', this._scope_marker);
|
|
2467
2603
|
try {
|
|
2468
2604
|
localStorage.setItem(scoped_key, serialized);
|
|
2469
2605
|
}
|
|
@@ -2504,17 +2640,22 @@ Jqhtml_Local_Storage._initialized = false;
|
|
|
2504
2640
|
* Extracted from component.ts - handles lifecycle event registration,
|
|
2505
2641
|
* triggering, and state tracking.
|
|
2506
2642
|
*
|
|
2507
|
-
*
|
|
2508
|
-
*
|
|
2643
|
+
* EVERY event is "sticky", not just the lifecycle ones: trigger() records the event
|
|
2644
|
+
* name and its payload, and a handler registered afterwards fires immediately with that
|
|
2645
|
+
* last payload. This is deliberate and applies to custom events too - a subscriber that
|
|
2646
|
+
* arrives after `trigger('row_selected', row)` still learns which row is selected,
|
|
2647
|
+
* which is what makes late-attaching UI work without a separate "current state" read.
|
|
2509
2648
|
*/
|
|
2510
2649
|
/**
|
|
2511
2650
|
* Register a callback for an event.
|
|
2512
2651
|
*
|
|
2513
|
-
*
|
|
2514
|
-
*
|
|
2515
|
-
* AND registers for future occurrences.
|
|
2652
|
+
* All events are "sticky", lifecycle (create, render, load, loaded, ready, stop) and
|
|
2653
|
+
* custom alike: if the event has already been triggered, the callback fires immediately
|
|
2654
|
+
* with the payload of that last trigger AND registers for future occurrences.
|
|
2516
2655
|
*
|
|
2517
|
-
*
|
|
2656
|
+
* invalidate(event_name) drops the stored marker when a component wants later
|
|
2657
|
+
* subscribers to wait for the next real occurrence instead (reload() does this for
|
|
2658
|
+
* 'ready').
|
|
2518
2659
|
*
|
|
2519
2660
|
* @param component - The component instance
|
|
2520
2661
|
* @param event_name - Name of the event
|
|
@@ -2646,6 +2787,20 @@ function event_invalidate(component, event_name) {
|
|
|
2646
2787
|
* - Data freeze/unfreeze enforcement via Proxy
|
|
2647
2788
|
* - Detached on_load() execution with restricted access
|
|
2648
2789
|
*/
|
|
2790
|
+
/**
|
|
2791
|
+
* JSON for an error message. The value being rejected is arbitrary author data and may
|
|
2792
|
+
* be cyclic or otherwise unserializable - the diagnostic must never throw over the
|
|
2793
|
+
* error it is trying to report.
|
|
2794
|
+
*/
|
|
2795
|
+
function safe_json(value) {
|
|
2796
|
+
try {
|
|
2797
|
+
const text = JSON.stringify(value);
|
|
2798
|
+
return text === undefined ? String(value) : text;
|
|
2799
|
+
}
|
|
2800
|
+
catch {
|
|
2801
|
+
return String(value);
|
|
2802
|
+
}
|
|
2803
|
+
}
|
|
2649
2804
|
/**
|
|
2650
2805
|
* Set up the `this.data` property on a component using Object.defineProperty
|
|
2651
2806
|
* with a Proxy that enforces freeze/unfreeze semantics.
|
|
@@ -2659,9 +2814,78 @@ function event_invalidate(component, event_name) {
|
|
|
2659
2814
|
*/
|
|
2660
2815
|
function setup_data_property(component) {
|
|
2661
2816
|
let _data = {};
|
|
2817
|
+
// Wrapper memo for the deep-freeze proxies. Keyed by the RAW target object, so a
|
|
2818
|
+
// value read twice returns the same wrapper and `this.data.items === this.data.items`
|
|
2819
|
+
// holds - identity comparisons and `Set`/`Map` membership on nested data keep working.
|
|
2820
|
+
// WeakMap: a wrapper dies with the object it wraps.
|
|
2821
|
+
const frozen_wrappers = new WeakMap();
|
|
2822
|
+
// Only plain objects and arrays are wrapped. Anything with a real prototype
|
|
2823
|
+
// (Date, Map, Set, and classes restored by register_cache_class()) carries internal
|
|
2824
|
+
// slots that a Proxy receiver cannot satisfy: `this.data.created.getTime()` would
|
|
2825
|
+
// call the method with the proxy as `this` and throw. Those values pass through raw -
|
|
2826
|
+
// a shallow hole we accept, because breaking Date/Map/Set reads would be worse than
|
|
2827
|
+
// missing a rare mutation of one.
|
|
2828
|
+
const is_wrappable = (value) => {
|
|
2829
|
+
if (value === null || typeof value !== 'object')
|
|
2830
|
+
return false;
|
|
2831
|
+
if (Array.isArray(value))
|
|
2832
|
+
return true;
|
|
2833
|
+
const proto = Object.getPrototypeOf(value);
|
|
2834
|
+
return proto === Object.prototype || proto === null;
|
|
2835
|
+
};
|
|
2836
|
+
// Dotted path for error messages: array indices read as [0], keys as .name
|
|
2837
|
+
const join_path = (path, target, prop) => Array.isArray(target) ? `${path}[${String(prop)}]` : `${path}.${String(prop)}`;
|
|
2838
|
+
const frozen_violation = (path, verb, value) => {
|
|
2839
|
+
const value_text = verb === 'modify' ? ` = ${safe_json(value)}` : '';
|
|
2840
|
+
console.error(`[JQHTML] ERROR: Component "${component.component_name()}" attempted to ${verb} ${path} outside of on_create() or on_load().\n\n` +
|
|
2841
|
+
`RESTRICTION: this.data can ONLY be modified in:\n` +
|
|
2842
|
+
` - on_create() (set initial defaults, synchronous only)\n` +
|
|
2843
|
+
` - on_load() (fetch data from APIs, can be async)\n\n` +
|
|
2844
|
+
`NESTED VALUES COUNT: the freeze is DEEP. Mutating an object or array inside\n` +
|
|
2845
|
+
`this.data (push, splice, assigning a property, delete) is modifying this.data.\n\n` +
|
|
2846
|
+
`WHY: this.data represents loaded state. Modifying it outside these methods bypasses the framework's render cycle.\n\n` +
|
|
2847
|
+
`FIX:\n` +
|
|
2848
|
+
` ❌ In on_ready(): ${path}${value_text};\n` +
|
|
2849
|
+
` ✅ In on_load(): fetch the value, so the framework can re-render\n` +
|
|
2850
|
+
` ✅ For component-local bookkeeping: this.state (unrestricted, never cached)`);
|
|
2851
|
+
throw new Error(`[JQHTML] Cannot ${verb} ${path} outside of on_create() or on_load(). ` +
|
|
2852
|
+
`this.data is frozen after on_create() and unfrozen only during on_load(). ` +
|
|
2853
|
+
`The freeze is deep - nested objects and arrays are frozen too.`);
|
|
2854
|
+
};
|
|
2855
|
+
// Read-only view of a nested value. Handed out ONLY while __data_frozen is true
|
|
2856
|
+
// (the top-level get trap checks), so the traps here never need to re-check: a
|
|
2857
|
+
// wrapper the author kept a reference to stays read-only for its whole life.
|
|
2858
|
+
const create_frozen_wrapper = (obj, path) => {
|
|
2859
|
+
const existing = frozen_wrappers.get(obj);
|
|
2860
|
+
if (existing)
|
|
2861
|
+
return existing;
|
|
2862
|
+
const wrapper = new Proxy(obj, {
|
|
2863
|
+
get: (target, prop) => {
|
|
2864
|
+
const value = target[prop];
|
|
2865
|
+
// Functions are returned raw: array methods are invoked with the wrapper as
|
|
2866
|
+
// `this`, so push/splice still land in this trap's set/deleteProperty.
|
|
2867
|
+
return is_wrappable(value) ? create_frozen_wrapper(value, join_path(path, target, prop)) : value;
|
|
2868
|
+
},
|
|
2869
|
+
set: (target, prop, value) => frozen_violation(join_path(path, target, prop), 'modify', value),
|
|
2870
|
+
deleteProperty: (target, prop) => frozen_violation(join_path(path, target, prop), 'delete')
|
|
2871
|
+
});
|
|
2872
|
+
frozen_wrappers.set(obj, wrapper);
|
|
2873
|
+
return wrapper;
|
|
2874
|
+
};
|
|
2662
2875
|
// Helper to create frozen proxy for data object
|
|
2663
2876
|
const create_proxy = (obj) => {
|
|
2664
2877
|
return new Proxy(obj, {
|
|
2878
|
+
get: (target, prop) => {
|
|
2879
|
+
const value = target[prop];
|
|
2880
|
+
// Deep freeze: while frozen, every nested object/array is read through a
|
|
2881
|
+
// read-only wrapper, so this.data.items.push(x) throws instead of silently
|
|
2882
|
+
// mutating. Unfrozen (on_create, on_load's detached run, _apply_load_result)
|
|
2883
|
+
// reads return the raw value, because those phases may legally mutate it.
|
|
2884
|
+
if (component.__data_frozen && is_wrappable(value)) {
|
|
2885
|
+
return create_frozen_wrapper(value, `this.data.${String(prop)}`);
|
|
2886
|
+
}
|
|
2887
|
+
return value;
|
|
2888
|
+
},
|
|
2665
2889
|
set: (target, prop, value) => {
|
|
2666
2890
|
if (component.__data_frozen) {
|
|
2667
2891
|
console.error(`[JQHTML] ERROR: Component "${component.component_name()}" attempted to modify this.data.${String(prop)} outside of on_create() or on_load().\n\n` +
|
|
@@ -2731,10 +2955,13 @@ function setup_data_property(component) {
|
|
|
2731
2955
|
* All other property access (this.$, this.$sid, etc.) throws errors.
|
|
2732
2956
|
*
|
|
2733
2957
|
* @param component - The component instance
|
|
2734
|
-
* @param
|
|
2958
|
+
* @param dedup_key - The dedup key captured by the caller, or null to run uncoordinated.
|
|
2959
|
+
* The key is passed in rather than recomputed because args may legally
|
|
2960
|
+
* change while on_load() runs, and a recomputed key would address a
|
|
2961
|
+
* different entry than the one this leader registered.
|
|
2735
2962
|
* @returns The resulting data and optional coordination completion function
|
|
2736
2963
|
*/
|
|
2737
|
-
async function execute_on_load_detached(component,
|
|
2964
|
+
async function execute_on_load_detached(component, dedup_key = null) {
|
|
2738
2965
|
// Clone this.data from the snapshot captured after on_create()
|
|
2739
2966
|
const data_clone = component.__initial_data_snapshot
|
|
2740
2967
|
? JSON.parse(JSON.stringify(component.__initial_data_snapshot))
|
|
@@ -2742,14 +2969,23 @@ async function execute_on_load_detached(component, use_load_coordinator = false)
|
|
|
2742
2969
|
// Create a read-only proxy for args that blocks all modifications
|
|
2743
2970
|
// Can't use JSON clone because args may contain functions (_slots, callbacks)
|
|
2744
2971
|
const component_name = component.component_name();
|
|
2972
|
+
// Wrapper memo for this one on_load() run. Without it every nested read built a
|
|
2973
|
+
// fresh Proxy, so `this.args.filter !== this.args.filter` for an object arg and
|
|
2974
|
+
// identity comparisons inside on_load() silently failed. Lives only for the duration
|
|
2975
|
+
// of this call - args may legally be replaced between loads.
|
|
2976
|
+
const readonly_wrappers = new WeakMap();
|
|
2745
2977
|
const create_readonly_proxy = (obj, path = 'this.args') => {
|
|
2746
2978
|
if (obj === null || typeof obj !== 'object')
|
|
2747
2979
|
return obj;
|
|
2748
|
-
|
|
2980
|
+
const existing = readonly_wrappers.get(obj);
|
|
2981
|
+
if (existing)
|
|
2982
|
+
return existing;
|
|
2983
|
+
const proxy = new Proxy(obj, {
|
|
2749
2984
|
get(target, prop) {
|
|
2750
2985
|
const value = target[prop];
|
|
2751
|
-
// Recursively wrap nested objects
|
|
2752
|
-
|
|
2986
|
+
// Recursively wrap nested objects. Functions are returned raw - `typeof value`
|
|
2987
|
+
// is already 'object' here, so a function never reaches this branch.
|
|
2988
|
+
if (value !== null && typeof value === 'object') {
|
|
2753
2989
|
return create_readonly_proxy(value, `${path}.${String(prop)}`);
|
|
2754
2990
|
}
|
|
2755
2991
|
return value;
|
|
@@ -2771,6 +3007,8 @@ async function execute_on_load_detached(component, use_load_coordinator = false)
|
|
|
2771
3007
|
`this.args is read-only in on_load().`);
|
|
2772
3008
|
}
|
|
2773
3009
|
});
|
|
3010
|
+
readonly_wrappers.set(obj, proxy);
|
|
3011
|
+
return proxy;
|
|
2774
3012
|
};
|
|
2775
3013
|
// Create a detached context object that on_load will operate on
|
|
2776
3014
|
const detached_context = {
|
|
@@ -2830,27 +3068,22 @@ async function execute_on_load_detached(component, use_load_coordinator = false)
|
|
|
2830
3068
|
`Only this.data can be modified in on_load().`);
|
|
2831
3069
|
}
|
|
2832
3070
|
});
|
|
2833
|
-
//
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
await component._call_lifecycle('on_load', restricted_this);
|
|
2837
|
-
}
|
|
2838
|
-
catch (error) {
|
|
2839
|
-
if (use_load_coordinator) {
|
|
2840
|
-
// Handle error and notify coordinator
|
|
2841
|
-
Load_Coordinator.handle_leader_error(component, error);
|
|
2842
|
-
}
|
|
2843
|
-
throw error;
|
|
2844
|
-
}
|
|
2845
|
-
})();
|
|
2846
|
-
// If using Load_Coordinator, register as leader
|
|
2847
|
-
// Note: We store the completion function but DON'T call it here
|
|
2848
|
-
// It will be called by _load() after _apply_load_result() updates this.data
|
|
3071
|
+
// Register as leader BEFORE on_load() runs, so the error path always finds the entry
|
|
3072
|
+
// it has to reject. The completion function is stored, not called here: _load() calls
|
|
3073
|
+
// it after _apply_load_result() has updated this.data.
|
|
2849
3074
|
let complete_coordination = null;
|
|
2850
|
-
if (
|
|
2851
|
-
complete_coordination = Load_Coordinator.register_leader(component,
|
|
3075
|
+
if (dedup_key !== null) {
|
|
3076
|
+
complete_coordination = Load_Coordinator.register_leader(component, dedup_key);
|
|
3077
|
+
}
|
|
3078
|
+
try {
|
|
3079
|
+
await component._call_lifecycle('on_load', restricted_this);
|
|
3080
|
+
}
|
|
3081
|
+
catch (error) {
|
|
3082
|
+
if (dedup_key !== null) {
|
|
3083
|
+
Load_Coordinator.handle_leader_error(dedup_key, error);
|
|
3084
|
+
}
|
|
3085
|
+
throw error;
|
|
2852
3086
|
}
|
|
2853
|
-
await on_load_promise;
|
|
2854
3087
|
// Note: We don't validate args changes here because external code (like parent
|
|
2855
3088
|
// components calling reload()) is allowed to modify component.args during on_load().
|
|
2856
3089
|
// The read-only proxy only prevents code INSIDE on_load() from modifying args.
|
|
@@ -2952,6 +3185,21 @@ function warn_uncacheable_component(component, uncacheable_property, uncacheable
|
|
|
2952
3185
|
all_args: component.args ? Object.keys(component.args) : [],
|
|
2953
3186
|
});
|
|
2954
3187
|
}
|
|
3188
|
+
/**
|
|
3189
|
+
* Read an HTML-cache entry, rejecting anything that is not a well-formed snapshot.
|
|
3190
|
+
* Entries written by an older core are bare strings; they are treated as a miss (and
|
|
3191
|
+
* are cleared wholesale by the CORE_VERSION scope marker on the next version bump).
|
|
3192
|
+
*/
|
|
3193
|
+
function read_html_snapshot(html_cache_key) {
|
|
3194
|
+
const entry = Jqhtml_Local_Storage.get(html_cache_key);
|
|
3195
|
+
if (entry !== null &&
|
|
3196
|
+
typeof entry === 'object' &&
|
|
3197
|
+
typeof entry.cid === 'string' &&
|
|
3198
|
+
typeof entry.html === 'string') {
|
|
3199
|
+
return entry;
|
|
3200
|
+
}
|
|
3201
|
+
return null;
|
|
3202
|
+
}
|
|
2955
3203
|
/**
|
|
2956
3204
|
* Read cache during create() phase.
|
|
2957
3205
|
* Sets component._cache_key, _cached_html, _use_cached_data_hit as side effects.
|
|
@@ -2983,11 +3231,11 @@ function read_cache_in_create(component) {
|
|
|
2983
3231
|
if (cache_mode === 'html') {
|
|
2984
3232
|
// HTML cache mode - check for cached HTML to inject on first render
|
|
2985
3233
|
const html_cache_key = `${cache_key}::html`;
|
|
2986
|
-
const cached_html =
|
|
2987
|
-
if (cached_html !== null
|
|
3234
|
+
const cached_html = read_html_snapshot(html_cache_key);
|
|
3235
|
+
if (cached_html !== null) {
|
|
2988
3236
|
component._cached_html = cached_html;
|
|
2989
3237
|
if (window.jqhtml?.debug?.verbose) {
|
|
2990
|
-
console.log(`[Cache html] Component ${component._cid} (${component.component_name()}) found cached HTML`, { cache_key: html_cache_key, html_length: cached_html.length });
|
|
3238
|
+
console.log(`[Cache html] Component ${component._cid} (${component.component_name()}) found cached HTML`, { cache_key: html_cache_key, html_length: cached_html.html.length, snapshot_cid: cached_html.cid });
|
|
2991
3239
|
}
|
|
2992
3240
|
}
|
|
2993
3241
|
else {
|
|
@@ -3044,10 +3292,10 @@ function check_cache_on_reload(component) {
|
|
|
3044
3292
|
if (cache_mode === 'html') {
|
|
3045
3293
|
// HTML cache mode - check for cached HTML
|
|
3046
3294
|
const html_cache_key = `${cache_key}::html`;
|
|
3047
|
-
const cached_html =
|
|
3048
|
-
if (cached_html !== null
|
|
3295
|
+
const cached_html = read_html_snapshot(html_cache_key);
|
|
3296
|
+
if (cached_html !== null) {
|
|
3049
3297
|
if (window.jqhtml?.debug?.verbose) {
|
|
3050
|
-
console.log(`[Cache html] reload() - Component ${component._cid} (${component.component_name()}) found cached HTML (args changed)`, { cache_key: html_cache_key, html_length: cached_html.length });
|
|
3298
|
+
console.log(`[Cache html] reload() - Component ${component._cid} (${component.component_name()}) found cached HTML (args changed)`, { cache_key: html_cache_key, html_length: cached_html.html.length, snapshot_cid: cached_html.cid });
|
|
3051
3299
|
}
|
|
3052
3300
|
component._cached_html = cached_html;
|
|
3053
3301
|
// Call _render() directly (not render()) since _reload() manages the full lifecycle
|
|
@@ -3087,319 +3335,73 @@ function write_html_cache_snapshot(component) {
|
|
|
3087
3335
|
if (component._is_dynamic) {
|
|
3088
3336
|
component._should_cache_html_after_ready = false;
|
|
3089
3337
|
const html = component.$.html();
|
|
3090
|
-
const html_cache_key = `${component._cache_key}::html`;
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
console.log(`[Cache html] Component ${component._cid} (${component.component_name()}) is static (no data change) - skipping HTML cache`);
|
|
3100
|
-
}
|
|
3101
|
-
}
|
|
3102
|
-
}
|
|
3103
|
-
/**
|
|
3104
|
-
* Write data/HTML to cache after on_load() completes.
|
|
3105
|
-
* Called from _apply_load_result().
|
|
3106
|
-
*
|
|
3107
|
-
* @param component - The component instance
|
|
3108
|
-
* @param data_changed - Whether this.data changed during on_load
|
|
3109
|
-
*/
|
|
3110
|
-
function write_cache_on_loaded(component, data_changed) {
|
|
3111
|
-
if (!data_changed || !component._cache_key) {
|
|
3112
|
-
return;
|
|
3113
|
-
}
|
|
3114
|
-
const cache_mode = Jqhtml_Local_Storage.get_cache_mode();
|
|
3115
|
-
if (cache_mode === 'html') {
|
|
3116
|
-
// HTML cache mode - flag to cache HTML after children ready in _ready()
|
|
3117
|
-
component._should_cache_html_after_ready = true;
|
|
3118
|
-
if (window.jqhtml?.debug?.verbose) {
|
|
3119
|
-
console.log(`[Cache html] Component ${component._cid} (${component.component_name()}) will cache HTML after ready`, { cache_key: component._cache_key });
|
|
3120
|
-
}
|
|
3121
|
-
}
|
|
3122
|
-
else {
|
|
3123
|
-
// Data cache mode (default) - write this.data to cache
|
|
3124
|
-
Jqhtml_Local_Storage.set(component._cache_key, component.data);
|
|
3125
|
-
if (window.jqhtml?.debug?.verbose) {
|
|
3126
|
-
console.log(`[Cache data] Component ${component._cid} (${component.component_name()}) cached data after on_load()`, { cache_key: component._cache_key, data: component.data });
|
|
3127
|
-
}
|
|
3128
|
-
}
|
|
3129
|
-
}
|
|
3130
|
-
|
|
3131
|
-
/**
|
|
3132
|
-
* JQHTML v2 Template Renderer
|
|
3133
|
-
*
|
|
3134
|
-
* Connects compiled templates to components
|
|
3135
|
-
* Processes instruction arrays and handles bindings
|
|
3136
|
-
*/
|
|
3137
|
-
/**
|
|
3138
|
-
* Process slot-only template inheritance
|
|
3139
|
-
*
|
|
3140
|
-
* When a component's template returns {_slots: {...}}, walk the prototype
|
|
3141
|
-
* chain to find a parent template and invoke it with the child's slots as content.
|
|
3142
|
-
*
|
|
3143
|
-
* @param component The component instance
|
|
3144
|
-
* @param childSlots The slots object from the child template
|
|
3145
|
-
* @returns [instructions, context] tuple or null if no parent found
|
|
3146
|
-
*/
|
|
3147
|
-
async function process_slot_inheritance(component, childSlots) {
|
|
3148
|
-
// Walk the prototype chain to find parent class with template
|
|
3149
|
-
let currentClass = Object.getPrototypeOf(component.constructor);
|
|
3150
|
-
console.log(`[JQHTML] Walking prototype chain for ${component.constructor.name}`);
|
|
3151
|
-
while (currentClass && currentClass !== Jqhtml_Component && currentClass.name !== 'Object') {
|
|
3152
|
-
const className = currentClass.name;
|
|
3153
|
-
console.log(`[JQHTML] Checking parent class: ${className}`);
|
|
3154
|
-
// Skip internal framework classes
|
|
3155
|
-
if (className === '_Jqhtml_Component' || className === '_Base_Jqhtml_Component') {
|
|
3156
|
-
currentClass = Object.getPrototypeOf(currentClass);
|
|
3157
|
-
continue;
|
|
3158
|
-
}
|
|
3159
|
-
// Try to find template for this parent class
|
|
3160
|
-
try {
|
|
3161
|
-
const parentTemplate = get_template(className);
|
|
3162
|
-
console.log(`[JQHTML] Template found for ${className}:`, parentTemplate ? parentTemplate.name : 'null');
|
|
3163
|
-
// Check if we got the default template (means no real template found)
|
|
3164
|
-
if (parentTemplate && parentTemplate.name !== 'Jqhtml_Component') {
|
|
3165
|
-
console.log(`[JQHTML] Invoking parent template ${className}`);
|
|
3166
|
-
// Found a real parent template - invoke it with child's slots as content
|
|
3167
|
-
const [parentInstructions, parentContext] = parentTemplate.render.call(component, component.data, component.args, childSlots // Pass child slots as content parameter
|
|
3168
|
-
);
|
|
3169
|
-
// Check if parent also returns slots (recursive inheritance)
|
|
3170
|
-
if (parentInstructions && typeof parentInstructions === 'object' && parentInstructions._slots) {
|
|
3171
|
-
// Parent also has slot-only template - recurse
|
|
3172
|
-
console.log(`[JQHTML] Parent also slot-only, recursing`);
|
|
3173
|
-
return await process_slot_inheritance(component, parentInstructions._slots);
|
|
3174
|
-
}
|
|
3175
|
-
// Parent returned HTML - we're done
|
|
3176
|
-
console.log(`[JQHTML] Parent returned instructions, inheritance complete`);
|
|
3177
|
-
return [parentInstructions, parentContext];
|
|
3178
|
-
}
|
|
3179
|
-
}
|
|
3180
|
-
catch (error) {
|
|
3181
|
-
console.warn(`[JQHTML] Error looking up parent template for ${className}:`, error);
|
|
3182
|
-
}
|
|
3183
|
-
// Move to next parent
|
|
3184
|
-
currentClass = Object.getPrototypeOf(currentClass);
|
|
3185
|
-
}
|
|
3186
|
-
// No parent template found
|
|
3187
|
-
console.warn(`[JQHTML] No parent template found after walking chain`);
|
|
3188
|
-
return null;
|
|
3189
|
-
}
|
|
3190
|
-
/**
|
|
3191
|
-
* Render a template for a component
|
|
3192
|
-
* Templates are functions that return [instructions, context] tuples
|
|
3193
|
-
*/
|
|
3194
|
-
async function render_template(component, template_fn) {
|
|
3195
|
-
// Get template from registry if not provided
|
|
3196
|
-
let render_fn = template_fn;
|
|
3197
|
-
if (!render_fn) {
|
|
3198
|
-
const template_def = get_template_by_class(component.constructor);
|
|
3199
|
-
render_fn = template_def.render;
|
|
3200
|
-
}
|
|
3201
|
-
if (!render_fn) {
|
|
3202
|
-
// No template, component should handle its own rendering
|
|
3203
|
-
return;
|
|
3204
|
-
}
|
|
3205
|
-
// Clear existing content
|
|
3206
|
-
component.$.empty();
|
|
3207
|
-
// Execute template function
|
|
3208
|
-
// Templates receive: (data, args, content)
|
|
3209
|
-
// Provide a default empty content function if none provided
|
|
3210
|
-
const defaultContent = () => '';
|
|
3211
|
-
let [instructions, context] = render_fn.call(component, component.data, component.args, defaultContent // Default content function that returns empty string
|
|
3212
|
-
);
|
|
3213
|
-
// Check for slot-only template inheritance
|
|
3214
|
-
// If instructions is {_slots: {...}}, this is a slot-only template
|
|
3215
|
-
// We need to find the parent template and invoke it with these slots
|
|
3216
|
-
if (instructions && typeof instructions === 'object' && instructions._slots) {
|
|
3217
|
-
console.log(`[JQHTML] Slot-only template detected for ${component.constructor.name}, invoking inheritance`);
|
|
3218
|
-
const result = await process_slot_inheritance(component, instructions._slots);
|
|
3219
|
-
if (result) {
|
|
3220
|
-
console.log(`[JQHTML] Parent template found, using parent instructions`);
|
|
3221
|
-
instructions = result[0];
|
|
3222
|
-
context = result[1];
|
|
3223
|
-
}
|
|
3224
|
-
else {
|
|
3225
|
-
console.warn(`[JQHTML] No parent template found for ${component.constructor.name}, rendering empty`);
|
|
3226
|
-
// No parent template found - return empty
|
|
3227
|
-
instructions = [];
|
|
3228
|
-
}
|
|
3229
|
-
}
|
|
3230
|
-
// Process instructions to generate DOM
|
|
3231
|
-
await process_instructions(instructions, component.$, component);
|
|
3232
|
-
// Process bindings after DOM is created
|
|
3233
|
-
await process_bindings(component);
|
|
3234
|
-
// Attach event handlers
|
|
3235
|
-
await attach_event_handlers(component);
|
|
3236
|
-
}
|
|
3237
|
-
/**
|
|
3238
|
-
* Process data bindings (attributes created by :prop syntax)
|
|
3239
|
-
*/
|
|
3240
|
-
async function process_bindings(component) {
|
|
3241
|
-
// Find all elements with data-bind-* attributes
|
|
3242
|
-
component.$.find('[data-bind-prop], [data-bind-value], [data-bind-text], [data-bind-html], [data-bind-class], [data-bind-style]').each((_, element) => {
|
|
3243
|
-
const el = $(element);
|
|
3244
|
-
const attrs = element.attributes;
|
|
3245
|
-
for (let i = 0; i < attrs.length; i++) {
|
|
3246
|
-
const attr = attrs[i];
|
|
3247
|
-
if (attr.name.startsWith('data-bind-')) {
|
|
3248
|
-
const binding_type = attr.name.substring(10); // Remove 'data-bind-'
|
|
3249
|
-
const expression = attr.value;
|
|
3250
|
-
try {
|
|
3251
|
-
// Evaluate expression in component context
|
|
3252
|
-
const value = evaluate_expression(expression, component);
|
|
3253
|
-
// Apply binding based on type
|
|
3254
|
-
switch (binding_type) {
|
|
3255
|
-
case 'prop':
|
|
3256
|
-
// Generic property binding
|
|
3257
|
-
const prop_name = el.attr('data-bind-prop-name') || 'value';
|
|
3258
|
-
el.prop(prop_name, value);
|
|
3259
|
-
break;
|
|
3260
|
-
case 'value':
|
|
3261
|
-
el.val(value);
|
|
3262
|
-
break;
|
|
3263
|
-
case 'text':
|
|
3264
|
-
el.text(value);
|
|
3265
|
-
break;
|
|
3266
|
-
case 'html':
|
|
3267
|
-
el.html(value);
|
|
3268
|
-
break;
|
|
3269
|
-
case 'class':
|
|
3270
|
-
if (typeof value === 'object') {
|
|
3271
|
-
// Object syntax: {active: true, disabled: false}
|
|
3272
|
-
Object.entries(value).forEach(([className, enabled]) => {
|
|
3273
|
-
el.toggleClass(className, !!enabled);
|
|
3274
|
-
});
|
|
3275
|
-
}
|
|
3276
|
-
else {
|
|
3277
|
-
// String syntax
|
|
3278
|
-
el.addClass(String(value));
|
|
3279
|
-
}
|
|
3280
|
-
break;
|
|
3281
|
-
case 'style':
|
|
3282
|
-
if (typeof value === 'object') {
|
|
3283
|
-
el.css(value);
|
|
3284
|
-
}
|
|
3285
|
-
else {
|
|
3286
|
-
el.attr('style', String(value));
|
|
3287
|
-
}
|
|
3288
|
-
break;
|
|
3289
|
-
default:
|
|
3290
|
-
// Custom binding - set as attribute
|
|
3291
|
-
el.attr(binding_type, value);
|
|
3292
|
-
}
|
|
3293
|
-
}
|
|
3294
|
-
catch (error) {
|
|
3295
|
-
console.error(`Error evaluating binding "${expression}":`, error);
|
|
3296
|
-
}
|
|
3297
|
-
}
|
|
3298
|
-
}
|
|
3299
|
-
});
|
|
3300
|
-
}
|
|
3301
|
-
/**
|
|
3302
|
-
* Attach event handlers (attributes created by @event syntax)
|
|
3303
|
-
*/
|
|
3304
|
-
async function attach_event_handlers(component) {
|
|
3305
|
-
// Find all elements with data-on-* attributes
|
|
3306
|
-
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) => {
|
|
3307
|
-
const el = $(element);
|
|
3308
|
-
const attrs = element.attributes;
|
|
3309
|
-
for (let i = 0; i < attrs.length; i++) {
|
|
3310
|
-
const attr = attrs[i];
|
|
3311
|
-
if (attr.name.startsWith('data-on-')) {
|
|
3312
|
-
const event_name = attr.name.substring(8); // Remove 'data-on-'
|
|
3313
|
-
const handler_expr = attr.value;
|
|
3314
|
-
// Remove the attribute to avoid re-processing
|
|
3315
|
-
el.removeAttr(attr.name);
|
|
3316
|
-
// Attach event handler
|
|
3317
|
-
el.on(event_name, function (event) {
|
|
3318
|
-
try {
|
|
3319
|
-
// Create handler function in component context
|
|
3320
|
-
const handler = evaluate_handler(handler_expr, component);
|
|
3321
|
-
if (typeof handler === 'function') {
|
|
3322
|
-
// Call with component as 'this' and pass event
|
|
3323
|
-
handler.call(component, event);
|
|
3324
|
-
}
|
|
3325
|
-
else {
|
|
3326
|
-
// Expression to execute
|
|
3327
|
-
evaluate_expression(handler_expr, component, { $event: event });
|
|
3328
|
-
}
|
|
3329
|
-
}
|
|
3330
|
-
catch (error) {
|
|
3331
|
-
console.error(`Error in ${event_name} handler "${handler_expr}":`, error);
|
|
3332
|
-
}
|
|
3333
|
-
});
|
|
3334
|
-
}
|
|
3335
|
-
}
|
|
3336
|
-
});
|
|
3337
|
-
}
|
|
3338
|
-
/**
|
|
3339
|
-
* Evaluate an expression in component context
|
|
3340
|
-
*/
|
|
3341
|
-
function evaluate_expression(expression, component, locals = {}) {
|
|
3342
|
-
// Create evaluation context
|
|
3343
|
-
const context = {
|
|
3344
|
-
// Component properties
|
|
3345
|
-
data: component.data,
|
|
3346
|
-
args: component.args,
|
|
3347
|
-
$: component.$,
|
|
3348
|
-
// Component methods
|
|
3349
|
-
$sid: component.$sid.bind(component),
|
|
3350
|
-
// Locals (like $event)
|
|
3351
|
-
...locals
|
|
3352
|
-
};
|
|
3353
|
-
// Build function that evaluates in context
|
|
3354
|
-
const keys = Object.keys(context);
|
|
3355
|
-
const values = Object.values(context);
|
|
3356
|
-
try {
|
|
3357
|
-
// Use Function constructor for better performance than eval
|
|
3358
|
-
const fn = new Function(...keys, `return (${expression})`);
|
|
3359
|
-
return fn(...values);
|
|
3338
|
+
const html_cache_key = `${component._cache_key}::html`;
|
|
3339
|
+
// Store the cid this markup was rendered under alongside it: every scoped id inside
|
|
3340
|
+
// reads "<name>:<cid>", and a reader cannot tell the snapshotting component's own
|
|
3341
|
+
// elements from its children's without knowing which cid was the owner's.
|
|
3342
|
+
// See Jqhtml_Component._rescope_cached_html().
|
|
3343
|
+
Jqhtml_Local_Storage.set(html_cache_key, { cid: component._cid, html });
|
|
3344
|
+
if (window.jqhtml?.debug?.verbose) {
|
|
3345
|
+
console.log(`[Cache html] Component ${component._cid} (${component.component_name()}) cached HTML after children on_render complete`, { cache_key: html_cache_key, html_length: html.length });
|
|
3346
|
+
}
|
|
3360
3347
|
}
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3348
|
+
else {
|
|
3349
|
+
component._should_cache_html_after_ready = false;
|
|
3350
|
+
if (window.jqhtml?.debug?.verbose) {
|
|
3351
|
+
console.log(`[Cache html] Component ${component._cid} (${component.component_name()}) is static (no data change) - skipping HTML cache`);
|
|
3352
|
+
}
|
|
3364
3353
|
}
|
|
3365
3354
|
}
|
|
3366
3355
|
/**
|
|
3367
|
-
*
|
|
3356
|
+
* Write data/HTML to cache after on_load() completes.
|
|
3357
|
+
* Called from _apply_load_result().
|
|
3358
|
+
*
|
|
3359
|
+
* @param component - The component instance
|
|
3360
|
+
* @param data_changed - Whether this.data changed during on_load
|
|
3368
3361
|
*/
|
|
3369
|
-
function
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
return component[expression];
|
|
3362
|
+
function write_cache_on_loaded(component, data_changed) {
|
|
3363
|
+
if (!data_changed || !component._cache_key) {
|
|
3364
|
+
return;
|
|
3373
3365
|
}
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3366
|
+
const cache_mode = Jqhtml_Local_Storage.get_cache_mode();
|
|
3367
|
+
if (cache_mode === 'html') {
|
|
3368
|
+
// HTML cache mode - flag to cache HTML after children ready in _ready()
|
|
3369
|
+
component._should_cache_html_after_ready = true;
|
|
3370
|
+
if (window.jqhtml?.debug?.verbose) {
|
|
3371
|
+
console.log(`[Cache html] Component ${component._cid} (${component.component_name()}) will cache HTML after ready`, { cache_key: component._cache_key });
|
|
3372
|
+
}
|
|
3380
3373
|
}
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3374
|
+
else {
|
|
3375
|
+
// Data cache mode (default) - write this.data to cache
|
|
3376
|
+
Jqhtml_Local_Storage.set(component._cache_key, component.data);
|
|
3377
|
+
if (window.jqhtml?.debug?.verbose) {
|
|
3378
|
+
console.log(`[Cache data] Component ${component._cid} (${component.component_name()}) cached data after on_load()`, { cache_key: component._cache_key, data: component.data });
|
|
3379
|
+
}
|
|
3384
3380
|
}
|
|
3385
3381
|
}
|
|
3382
|
+
|
|
3386
3383
|
/**
|
|
3387
|
-
*
|
|
3384
|
+
* HTML escaping for interpolated text.
|
|
3385
|
+
*
|
|
3386
|
+
* Output matches what `div.textContent = s; div.innerHTML` produced before -
|
|
3387
|
+
* a text node serialises only &, < and > as entities, and null/undefined as
|
|
3388
|
+
* "" - but without touching the DOM. Interpolation runs for every value of
|
|
3389
|
+
* every render, and a createElement per value was the most expensive thing a
|
|
3390
|
+
* text-heavy template did.
|
|
3388
3391
|
*/
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3392
|
+
const NEEDS_ESCAPE = /[&<>]/;
|
|
3393
|
+
const ESCAPE_ALL = /[&<>]/g;
|
|
3394
|
+
const ENTITY = { '&': '&', '<': '<', '>': '>' };
|
|
3395
|
+
const to_entity = (c) => ENTITY[c];
|
|
3396
|
+
function escape_html(value) {
|
|
3397
|
+
if (value === null || value === undefined)
|
|
3398
|
+
return '';
|
|
3399
|
+
const s = typeof value === 'string' ? value : String(value);
|
|
3400
|
+
return NEEDS_ESCAPE.test(s) ? s.replace(ESCAPE_ALL, to_entity) : s;
|
|
3393
3401
|
}
|
|
3394
|
-
/**
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
*/
|
|
3398
|
-
function escape_html_nl2br(str) {
|
|
3399
|
-
const div = document.createElement('div');
|
|
3400
|
-
div.textContent = str;
|
|
3401
|
-
// Escape HTML first, then replace newlines with <br />
|
|
3402
|
-
return div.innerHTML.replace(/\n/g, '<br />');
|
|
3402
|
+
/** Escaped, then newlines become <br /> - the <%br= %> construct. */
|
|
3403
|
+
function escape_html_nl2br(value) {
|
|
3404
|
+
return escape_html(value).replace(/\n/g, '<br />');
|
|
3403
3405
|
}
|
|
3404
3406
|
|
|
3405
3407
|
/**
|
|
@@ -3522,13 +3524,14 @@ function describe(value) {
|
|
|
3522
3524
|
/**
|
|
3523
3525
|
* JQHTML Component Lifecycle Queue
|
|
3524
3526
|
*
|
|
3525
|
-
* Serializes lifecycle operations (render, reload, refresh) per component.
|
|
3527
|
+
* Serializes lifecycle operations (render, reload, refresh, load) per component.
|
|
3526
3528
|
* Replaces _create_debounced_function with a proper queue that:
|
|
3527
3529
|
*
|
|
3528
3530
|
* - At most one operation runs at a time per component
|
|
3529
|
-
* - At most one
|
|
3530
|
-
* - Same-type pending operations collapse
|
|
3531
|
-
*
|
|
3531
|
+
* - At most one PENDING entry per type; different types queue behind each other FIFO
|
|
3532
|
+
* - Same-type pending operations collapse: the newest executor wins, and every
|
|
3533
|
+
* collapsed caller is settled with the value of the executor that actually ran
|
|
3534
|
+
* - A caller is settled BEFORE the next pending entry starts
|
|
3532
3535
|
*
|
|
3533
3536
|
* Boot bypasses this queue entirely - it runs the lifecycle directly.
|
|
3534
3537
|
*/
|
|
@@ -3536,92 +3539,106 @@ class Component_Queue {
|
|
|
3536
3539
|
constructor() {
|
|
3537
3540
|
/** Currently executing operation */
|
|
3538
3541
|
this._current = null;
|
|
3539
|
-
/**
|
|
3540
|
-
|
|
3542
|
+
/**
|
|
3543
|
+
* Operations waiting to run after the current one, in FIFO order.
|
|
3544
|
+
* At most one entry per type: a second reload() while a reload is pending
|
|
3545
|
+
* collapses into it, but a render() queued over a pending reload() is its
|
|
3546
|
+
* own entry and both run. Replacing the older entry (the previous behaviour)
|
|
3547
|
+
* dropped that operation entirely while still resolving its callers as if it
|
|
3548
|
+
* had run.
|
|
3549
|
+
*/
|
|
3550
|
+
this._pending = [];
|
|
3541
3551
|
}
|
|
3542
3552
|
/**
|
|
3543
3553
|
* Enqueue a lifecycle operation.
|
|
3544
3554
|
*
|
|
3545
3555
|
* Behavior:
|
|
3546
3556
|
* - Nothing running → execute immediately
|
|
3547
|
-
* - Something running, no pending →
|
|
3548
|
-
* - Something running, same type pending → collapse
|
|
3549
|
-
*
|
|
3557
|
+
* - Something running, no pending entry of this type → queue one (FIFO)
|
|
3558
|
+
* - Something running, same type pending → collapse: keep every caller, but
|
|
3559
|
+
* swap in the newest executor ("only the most recent call executes")
|
|
3560
|
+
*
|
|
3561
|
+
* Every caller collapsed onto one entry receives the value that entry's
|
|
3562
|
+
* executor returned - or its error, if it threw.
|
|
3550
3563
|
*
|
|
3551
3564
|
* @param type - Operation type ('render', 'reload', 'refresh', 'load')
|
|
3552
3565
|
* @param executor - The async function to execute
|
|
3553
|
-
* @returns Promise that
|
|
3566
|
+
* @returns Promise that settles with the result of the execution that ran
|
|
3554
3567
|
*/
|
|
3555
3568
|
enqueue(type, executor) {
|
|
3556
|
-
// If nothing running, execute immediately
|
|
3569
|
+
// If nothing running, execute immediately.
|
|
3570
|
+
// The Promise executor runs synchronously, so the operation's synchronous
|
|
3571
|
+
// prologue (_render(), the ready invalidation) still happens in the calling
|
|
3572
|
+
// tick, as it did before the queue existed.
|
|
3557
3573
|
if (!this._current) {
|
|
3558
|
-
return
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
|
|
3568
|
-
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
const old_resolvers = this._pending.resolvers;
|
|
3575
|
-
const old_rejecters = this._pending.rejecters;
|
|
3576
|
-
return new Promise((resolve, reject) => {
|
|
3577
|
-
this._pending = {
|
|
3578
|
-
type,
|
|
3579
|
-
executor,
|
|
3580
|
-
resolvers: [...old_resolvers, resolve],
|
|
3581
|
-
rejecters: [...old_rejecters, reject]
|
|
3582
|
-
};
|
|
3583
|
-
});
|
|
3584
|
-
}
|
|
3574
|
+
return new Promise((resolve, reject) => {
|
|
3575
|
+
this._run({ type, executor, resolvers: [resolve], rejecters: [reject] });
|
|
3576
|
+
});
|
|
3577
|
+
}
|
|
3578
|
+
const existing = this._pending.find(entry => entry.type === type);
|
|
3579
|
+
if (existing) {
|
|
3580
|
+
// Same type already pending → collapse (fan-in).
|
|
3581
|
+
// The newest executor replaces the pending one: the documented rule is
|
|
3582
|
+
// "only the most recent call executes", and the older closure may carry
|
|
3583
|
+
// stale arguments. Keeping the OLD executor made two rapid `await load()`
|
|
3584
|
+
// calls resolve the second one `false` without ever running on_load().
|
|
3585
|
+
existing.executor = executor;
|
|
3586
|
+
return new Promise((resolve, reject) => {
|
|
3587
|
+
existing.resolvers.push(resolve);
|
|
3588
|
+
existing.rejecters.push(reject);
|
|
3589
|
+
});
|
|
3585
3590
|
}
|
|
3586
|
-
// No pending entry yet —
|
|
3591
|
+
// No pending entry of this type yet — queue one behind whatever is already waiting.
|
|
3587
3592
|
return new Promise((resolve, reject) => {
|
|
3588
|
-
this._pending
|
|
3589
|
-
type,
|
|
3590
|
-
executor,
|
|
3591
|
-
resolvers: [resolve],
|
|
3592
|
-
rejecters: [reject]
|
|
3593
|
-
};
|
|
3593
|
+
this._pending.push({ type, executor, resolvers: [resolve], rejecters: [reject] });
|
|
3594
3594
|
});
|
|
3595
3595
|
}
|
|
3596
3596
|
/**
|
|
3597
|
-
*
|
|
3597
|
+
* Run one entry, settle its callers, then start the next pending entry.
|
|
3598
3598
|
* @private
|
|
3599
3599
|
*/
|
|
3600
|
-
|
|
3600
|
+
_run(entry) {
|
|
3601
|
+
// Mark busy BEFORE invoking the executor: the executor's synchronous prologue
|
|
3602
|
+
// can call back into enqueue(), and that call must queue rather than start a
|
|
3603
|
+
// second concurrent operation.
|
|
3604
|
+
this._current = { type: entry.type };
|
|
3601
3605
|
const promise = (async () => {
|
|
3602
3606
|
try {
|
|
3603
|
-
await executor();
|
|
3607
|
+
const value = await entry.executor();
|
|
3608
|
+
// Settle this entry's callers FIRST, while the queue still reads as busy.
|
|
3609
|
+
for (const resolve of entry.resolvers)
|
|
3610
|
+
resolve(value);
|
|
3604
3611
|
}
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
const pending = this._pending;
|
|
3610
|
-
this._pending = null;
|
|
3611
|
-
try {
|
|
3612
|
-
await this._execute(pending.type, pending.executor);
|
|
3613
|
-
for (const resolve of pending.resolvers)
|
|
3614
|
-
resolve();
|
|
3615
|
-
}
|
|
3616
|
-
catch (err) {
|
|
3617
|
-
for (const reject of pending.rejecters)
|
|
3618
|
-
reject(err);
|
|
3619
|
-
}
|
|
3620
|
-
}
|
|
3612
|
+
catch (error) {
|
|
3613
|
+
// A throwing executor rejects only its own callers; the queue continues.
|
|
3614
|
+
for (const reject of entry.rejecters)
|
|
3615
|
+
reject(error);
|
|
3621
3616
|
}
|
|
3622
3617
|
})();
|
|
3623
|
-
|
|
3624
|
-
|
|
3618
|
+
// The drain is deliberately OUTSIDE the awaited chain above: chaining it
|
|
3619
|
+
// inside made `await c.render()` also wait for a reload that was queued
|
|
3620
|
+
// after it.
|
|
3621
|
+
promise.then(() => {
|
|
3622
|
+
if (this._pending.length === 0) {
|
|
3623
|
+
// Nothing queued: release immediately, so the very common
|
|
3624
|
+
// `await c.reload(); c.render();` still starts the render in that tick.
|
|
3625
|
+
this._current = null;
|
|
3626
|
+
return;
|
|
3627
|
+
}
|
|
3628
|
+
// A queued entry must not start until every caller of the operation that
|
|
3629
|
+
// just finished has actually resumed. Those callers sit behind an unknown
|
|
3630
|
+
// number of microtask hops - an `async` public method adds two just by
|
|
3631
|
+
// returning the queue's promise - so draining on a microtask would start
|
|
3632
|
+
// the next on_load() before the previous caller's `await` came back. A
|
|
3633
|
+
// macrotask is the only boundary that is after all of them. _current stays
|
|
3634
|
+
// set until then, so nothing can start concurrently in the meantime.
|
|
3635
|
+
setTimeout(() => {
|
|
3636
|
+
this._current = null;
|
|
3637
|
+
const next = this._pending.shift();
|
|
3638
|
+
if (next)
|
|
3639
|
+
this._run(next);
|
|
3640
|
+
}, 0);
|
|
3641
|
+
});
|
|
3625
3642
|
}
|
|
3626
3643
|
/**
|
|
3627
3644
|
* Check if any operation is currently running.
|
|
@@ -3633,7 +3650,7 @@ class Component_Queue {
|
|
|
3633
3650
|
* Check if there's a pending operation waiting.
|
|
3634
3651
|
*/
|
|
3635
3652
|
get has_pending() {
|
|
3636
|
-
return this._pending
|
|
3653
|
+
return this._pending.length > 0;
|
|
3637
3654
|
}
|
|
3638
3655
|
}
|
|
3639
3656
|
|
|
@@ -3684,11 +3701,16 @@ function capture_component_data(component_name, args, data, cache_key) {
|
|
|
3684
3701
|
try {
|
|
3685
3702
|
const cloned_args = JSON.parse(JSON.stringify(args));
|
|
3686
3703
|
const cloned_data = JSON.parse(JSON.stringify(data));
|
|
3687
|
-
|
|
3704
|
+
const entry = {
|
|
3688
3705
|
component: component_name,
|
|
3689
3706
|
args: cloned_args,
|
|
3690
3707
|
data: cloned_data,
|
|
3691
|
-
}
|
|
3708
|
+
};
|
|
3709
|
+
// Carry the key forward so the client matches on the SAME identity the server
|
|
3710
|
+
// used, rather than re-deriving one from args and hoping the shapes agree.
|
|
3711
|
+
if (cache_key !== null)
|
|
3712
|
+
entry.key = cache_key;
|
|
3713
|
+
_capture_buffer.push(entry);
|
|
3692
3714
|
}
|
|
3693
3715
|
catch (error) {
|
|
3694
3716
|
// Non-serializable data — skip capture for this component
|
|
@@ -3734,10 +3756,15 @@ function set_preload_data(entries) {
|
|
|
3734
3756
|
return;
|
|
3735
3757
|
_preload_cache.clear();
|
|
3736
3758
|
for (const entry of entries) {
|
|
3737
|
-
//
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3759
|
+
// The captured key wins. Falling back to args means matching what
|
|
3760
|
+
// generate_cache_key() computes for a component with no cache_id(), which
|
|
3761
|
+
// INCLUDES content serialization - without it an object arg keyed to null
|
|
3762
|
+
// here and the entry was dropped, so preload never applied to it.
|
|
3763
|
+
const key = entry.key !== undefined
|
|
3764
|
+
? entry.key
|
|
3765
|
+
: Load_Coordinator.generate_invocation_key(entry.component, entry.args, { allow_content_serialization: true }).key;
|
|
3766
|
+
if (key !== null) {
|
|
3767
|
+
_preload_cache.set(key, entry.data);
|
|
3741
3768
|
}
|
|
3742
3769
|
}
|
|
3743
3770
|
}
|
|
@@ -3850,9 +3877,69 @@ function init_viewport_resize(jQuery) {
|
|
|
3850
3877
|
* - Scoped IDs using _cid pattern
|
|
3851
3878
|
* - Event emission and CSS class hierarchy
|
|
3852
3879
|
*/
|
|
3880
|
+
/**
|
|
3881
|
+
* The `jqhtml` argument every compiled render function receives: the same
|
|
3882
|
+
* escaping the runtime exports, the object-printer entry point, and the
|
|
3883
|
+
* dynamic tag name check. One shared object, not one per render.
|
|
3884
|
+
*/
|
|
3885
|
+
const render_utilities = {
|
|
3886
|
+
escape_html,
|
|
3887
|
+
escape_html_nl2br,
|
|
3888
|
+
print_object,
|
|
3889
|
+
dynamic_component_name,
|
|
3890
|
+
};
|
|
3853
3891
|
// WeakMap storage for protected lifecycle method implementations (Option 2)
|
|
3854
3892
|
// See docs/internal/lifecycle-method-protection.md for design details
|
|
3855
3893
|
const lifecycle_impls = new WeakMap();
|
|
3894
|
+
/**
|
|
3895
|
+
* Resolve the template a template inherits FROM, in the one order CLAUDE.md
|
|
3896
|
+
* documents ("Template Inheritance Resolution"): the explicit `extends=""`
|
|
3897
|
+
* attribute first, then the JS class prototype chain.
|
|
3898
|
+
*
|
|
3899
|
+
* This is the single implementation. Both slot-only rendering (which invokes the
|
|
3900
|
+
* parent template with the child's slots) and default-attribute collection walk
|
|
3901
|
+
* it, so a `<Define>`'s class/attrs can never resolve through a different chain
|
|
3902
|
+
* than its markup does.
|
|
3903
|
+
*
|
|
3904
|
+
* @param template_def The template whose parent is wanted
|
|
3905
|
+
* @param ctor The component class to walk the prototype chain of (null skips it)
|
|
3906
|
+
* @returns The parent template and the name it was found under, or null
|
|
3907
|
+
*/
|
|
3908
|
+
function resolve_parent_template(template_def, ctor) {
|
|
3909
|
+
const verbose = window.jqhtml?.debug?.verbose;
|
|
3910
|
+
// 1. Explicit extends="" wins - the author said exactly which template to use.
|
|
3911
|
+
if (template_def?.extends) {
|
|
3912
|
+
const parent = get_template(template_def.extends);
|
|
3913
|
+
// get_template() never returns null: a miss yields DEFAULT_TEMPLATE, whose
|
|
3914
|
+
// name is 'Jqhtml_Component'. That is "not found", not a parent.
|
|
3915
|
+
if (parent && parent.name !== 'Jqhtml_Component') {
|
|
3916
|
+
if (verbose)
|
|
3917
|
+
console.log(`[JQHTML] resolve_parent_template: explicit extends -> ${template_def.extends}`);
|
|
3918
|
+
return { template: parent, name: template_def.extends };
|
|
3919
|
+
}
|
|
3920
|
+
if (verbose)
|
|
3921
|
+
console.log(`[JQHTML] resolve_parent_template: extends="${template_def.extends}" has no template`);
|
|
3922
|
+
}
|
|
3923
|
+
// 2. Otherwise walk the JS prototype chain for the nearest ancestor with a template.
|
|
3924
|
+
// Stop at the framework base, at Object, and at Function.prototype (name === ''),
|
|
3925
|
+
// which is what getPrototypeOf() hands back once the class chain is exhausted.
|
|
3926
|
+
let current_class = ctor ? Object.getPrototypeOf(ctor) : null;
|
|
3927
|
+
while (typeof current_class === 'function' && current_class.name &&
|
|
3928
|
+
current_class.name !== 'Object' && current_class.name !== 'Jqhtml_Component') {
|
|
3929
|
+
const class_name = current_class.name;
|
|
3930
|
+
// Internal base classes are aliases of Jqhtml_Component, never a parent template.
|
|
3931
|
+
if (class_name !== '_Jqhtml_Component' && class_name !== '_Base_Jqhtml_Component') {
|
|
3932
|
+
const class_template = get_template(class_name);
|
|
3933
|
+
if (class_template && class_template.name !== 'Jqhtml_Component' && class_template !== template_def) {
|
|
3934
|
+
if (verbose)
|
|
3935
|
+
console.log(`[JQHTML] resolve_parent_template: prototype chain -> ${class_name}`);
|
|
3936
|
+
return { template: class_template, name: class_name };
|
|
3937
|
+
}
|
|
3938
|
+
}
|
|
3939
|
+
current_class = Object.getPrototypeOf(current_class);
|
|
3940
|
+
}
|
|
3941
|
+
return null;
|
|
3942
|
+
}
|
|
3856
3943
|
class Jqhtml_Component {
|
|
3857
3944
|
constructor(element, args = {}) {
|
|
3858
3945
|
this._ready_state = 0; // 0=created, 1=init, 2=loaded, 3=rendered, 4=ready
|
|
@@ -3877,7 +3964,10 @@ class Jqhtml_Component {
|
|
|
3877
3964
|
// Cache mode properties
|
|
3878
3965
|
this._cache_key = null; // Cache key for caching
|
|
3879
3966
|
// 'html' mode caching
|
|
3880
|
-
|
|
3967
|
+
// The snapshot carries the cid it was taken under, not just markup: every scoped id
|
|
3968
|
+
// inside it reads "<name>:<cid>", so replaying it into a different instance requires
|
|
3969
|
+
// knowing which cid was the snapshotting component's own. See _rescope_cached_html().
|
|
3970
|
+
this._cached_html = null; // Cached HTML snapshot to inject on first render
|
|
3881
3971
|
this._used_cached_html = false; // Flag if cached HTML was used (forces re-render after on_load)
|
|
3882
3972
|
this._should_cache_html_after_ready = false; // Flag to cache HTML after on_ready lifecycle
|
|
3883
3973
|
this._is_dynamic = false; // True if this.data changed during on_load() (used for HTML cache sync)
|
|
@@ -4121,19 +4211,58 @@ class Jqhtml_Component {
|
|
|
4121
4211
|
return child._render(null, options);
|
|
4122
4212
|
}
|
|
4123
4213
|
this._log_lifecycle('render', 'start');
|
|
4214
|
+
// --- Shared re-render prologue -------------------------------------------------
|
|
4215
|
+
// Everything below runs for BOTH the cached-HTML path and the template path. The
|
|
4216
|
+
// cached path used to return before it, so a cache-mode reload injected over a live
|
|
4217
|
+
// subtree without ever stopping the old children: their on_stop() never ran (timers
|
|
4218
|
+
// and listeners leaked) and they stayed in _dom_children as dead entries that every
|
|
4219
|
+
// later _wait_for_children_ready() walked.
|
|
4220
|
+
// Determine child-finding strategy: If component is off-DOM, children can't register
|
|
4221
|
+
// via _find_dom_parent() (no parent in DOM to find), so we'll need find() fallback later.
|
|
4222
|
+
// If attached to DOM, children register normally and we can use the fast _dom_children path.
|
|
4223
|
+
// Check is cheap ($.contains uses native Node.contains, ~500K ops/sec).
|
|
4224
|
+
if (!$.contains(document.documentElement, this.$[0])) {
|
|
4225
|
+
this._use_dom_fallback = true;
|
|
4226
|
+
}
|
|
4227
|
+
else {
|
|
4228
|
+
this._use_dom_fallback = false;
|
|
4229
|
+
}
|
|
4230
|
+
// If this is not the first render, stop child components and clear DOM
|
|
4231
|
+
if (this._did_first_render) {
|
|
4232
|
+
// Stop all child components before clearing DOM
|
|
4233
|
+
this.$.find('.Component').each(function () {
|
|
4234
|
+
const child = $(this).data('_component');
|
|
4235
|
+
if (child && !child._stopped) {
|
|
4236
|
+
child._stop(); // Stop just the component, DOM will be cleared next
|
|
4237
|
+
}
|
|
4238
|
+
});
|
|
4239
|
+
// Clear the DOM
|
|
4240
|
+
this.$[0].innerHTML = '';
|
|
4241
|
+
}
|
|
4242
|
+
else {
|
|
4243
|
+
this._did_first_render = true;
|
|
4244
|
+
}
|
|
4245
|
+
// (No removeClass('_Component_Stopped') here: _render() returns above when
|
|
4246
|
+
// _stopped, and nothing ever clears _stopped, so a stopped component can never
|
|
4247
|
+
// reach this line. Clearing the class would only ever hide a real stopped state.)
|
|
4248
|
+
// Clear DOM children tracking - they're destroyed by innerHTML = '' anyway
|
|
4249
|
+
this._dom_children.clear();
|
|
4250
|
+
// --- End shared re-render prologue ---------------------------------------------
|
|
4124
4251
|
// HTML CACHE MODE - If we have cached HTML, inject it directly and skip template rendering
|
|
4125
4252
|
if (this._cached_html !== null) {
|
|
4126
4253
|
if (window.jqhtml?.debug?.verbose) {
|
|
4127
|
-
console.log(`[Cache html] Component ${this._cid} (${this.component_name()}) injecting cached HTML`, { html_length: this._cached_html.length });
|
|
4128
|
-
}
|
|
4129
|
-
// Inject cached HTML directly
|
|
4130
|
-
|
|
4254
|
+
console.log(`[Cache html] Component ${this._cid} (${this.component_name()}) injecting cached HTML`, { html_length: this._cached_html.html.length, snapshot_cid: this._cached_html.cid });
|
|
4255
|
+
}
|
|
4256
|
+
// Inject cached HTML directly, with every scoped id re-scoped to THIS instance.
|
|
4257
|
+
// The snapshot was taken from another component instance (or an earlier life of
|
|
4258
|
+
// this one), so its ids carry that instance's cids verbatim. Injecting them as-is
|
|
4259
|
+
// put duplicate ids in the document, and $sid() - which is getElementById first -
|
|
4260
|
+
// could hand this component the other instance's element.
|
|
4261
|
+
this.$[0].innerHTML = this._rescope_cached_html(this._cached_html);
|
|
4131
4262
|
// Mark that we used cached HTML (forces re-render after on_load)
|
|
4132
4263
|
this._used_cached_html = true;
|
|
4133
4264
|
// Clear cached HTML so next render uses template
|
|
4134
4265
|
this._cached_html = null;
|
|
4135
|
-
// Mark first render complete
|
|
4136
|
-
this._did_first_render = true;
|
|
4137
4266
|
this._log_lifecycle('render', 'complete (cached HTML)');
|
|
4138
4267
|
// Call on_render() after cache inject (unless suppressed by _load_render_only)
|
|
4139
4268
|
if (!options.skip_on_render) {
|
|
@@ -4156,39 +4285,10 @@ class Jqhtml_Component {
|
|
|
4156
4285
|
this._data_on_last_render = JSON.stringify(this.data);
|
|
4157
4286
|
return current_render_id;
|
|
4158
4287
|
}
|
|
4159
|
-
// Determine child-finding strategy: If component is off-DOM, children can't register
|
|
4160
|
-
// via _find_dom_parent() (no parent in DOM to find), so we'll need find() fallback later.
|
|
4161
|
-
// If attached to DOM, children register normally and we can use the fast _dom_children path.
|
|
4162
|
-
// Check is cheap ($.contains uses native Node.contains, ~500K ops/sec).
|
|
4163
|
-
if (!$.contains(document.documentElement, this.$[0])) {
|
|
4164
|
-
this._use_dom_fallback = true;
|
|
4165
|
-
}
|
|
4166
|
-
else {
|
|
4167
|
-
this._use_dom_fallback = false;
|
|
4168
|
-
}
|
|
4169
|
-
// If this is not the first render, stop child components and clear DOM
|
|
4170
|
-
if (this._did_first_render) {
|
|
4171
|
-
// Stop all child components before clearing DOM
|
|
4172
|
-
this.$.find('.Component').each(function () {
|
|
4173
|
-
const child = $(this).data('_component');
|
|
4174
|
-
if (child && !child._stopped) {
|
|
4175
|
-
child._stop(); // Stop just the component, DOM will be cleared next
|
|
4176
|
-
}
|
|
4177
|
-
});
|
|
4178
|
-
// Clear the DOM
|
|
4179
|
-
this.$[0].innerHTML = '';
|
|
4180
|
-
}
|
|
4181
|
-
else {
|
|
4182
|
-
this._did_first_render = true;
|
|
4183
|
-
}
|
|
4184
|
-
// Remove _Component_Stopped class if present (allows re-render after stop)
|
|
4185
|
-
this.$.removeClass('_Component_Stopped');
|
|
4186
4288
|
// Capture data state before first render for comparison later
|
|
4187
4289
|
if (this._data_before_render === null) {
|
|
4188
4290
|
this._data_before_render = JSON.stringify(this.data);
|
|
4189
4291
|
}
|
|
4190
|
-
// Clear DOM children tracking - they're destroyed by innerHTML = '' anyway
|
|
4191
|
-
this._dom_children.clear();
|
|
4192
4292
|
// Get template and render it
|
|
4193
4293
|
let template_def;
|
|
4194
4294
|
// If we have a component name, try looking up by name first (for unregistered components)
|
|
@@ -4200,24 +4300,8 @@ class Jqhtml_Component {
|
|
|
4200
4300
|
template_def = get_template_by_class(this.constructor);
|
|
4201
4301
|
}
|
|
4202
4302
|
if (template_def && template_def.render) {
|
|
4203
|
-
//
|
|
4204
|
-
const jqhtml =
|
|
4205
|
-
escape_html: (str) => {
|
|
4206
|
-
const div = document.createElement('div');
|
|
4207
|
-
div.textContent = String(str);
|
|
4208
|
-
return div.innerHTML;
|
|
4209
|
-
},
|
|
4210
|
-
escape_html_nl2br: (str) => {
|
|
4211
|
-
const div = document.createElement('div');
|
|
4212
|
-
div.textContent = String(str);
|
|
4213
|
-
// Escape HTML first, then replace newlines with <br />
|
|
4214
|
-
return div.innerHTML.replace(/\n/g, '<br />');
|
|
4215
|
-
},
|
|
4216
|
-
// Objects reaching <%= %> / <%!= %> / <%br= %> - see value-printers.ts
|
|
4217
|
-
print_object,
|
|
4218
|
-
// <{expression}> name validation - see component-name.ts
|
|
4219
|
-
dynamic_component_name
|
|
4220
|
-
};
|
|
4303
|
+
// Utilities the compiled render function calls (see render_utilities)
|
|
4304
|
+
const jqhtml = render_utilities;
|
|
4221
4305
|
// Execute template function
|
|
4222
4306
|
// Commented out debug logging
|
|
4223
4307
|
// console.log('[Component] About to call render, this:', this);
|
|
@@ -4256,69 +4340,48 @@ class Jqhtml_Component {
|
|
|
4256
4340
|
// Check for template inheritance (slot-only OR explicit extends)
|
|
4257
4341
|
// If instructions is {_slots: {...}}, find parent template and invoke it
|
|
4258
4342
|
if (instructions && typeof instructions === 'object' && instructions._slots && !Array.isArray(instructions)) {
|
|
4259
|
-
const
|
|
4260
|
-
|
|
4261
|
-
|
|
4262
|
-
|
|
4263
|
-
//
|
|
4264
|
-
if (
|
|
4265
|
-
|
|
4266
|
-
|
|
4267
|
-
|
|
4268
|
-
}
|
|
4269
|
-
// If no explicit extends, walk the prototype chain to find parent class with template
|
|
4270
|
-
if (!parentTemplate) {
|
|
4271
|
-
let currentClass = Object.getPrototypeOf(this.constructor);
|
|
4272
|
-
while (currentClass && currentClass.name !== 'Object' && currentClass.name !== 'Jqhtml_Component') {
|
|
4273
|
-
const className = currentClass.name;
|
|
4274
|
-
console.log(`[JQHTML] Checking parent: ${className}`);
|
|
4275
|
-
try {
|
|
4276
|
-
const classTemplate = get_template(className);
|
|
4277
|
-
if (classTemplate && classTemplate.name !== 'Jqhtml_Component') {
|
|
4278
|
-
console.log(`[JQHTML] Found parent template: ${className}`);
|
|
4279
|
-
parentTemplate = classTemplate;
|
|
4280
|
-
parentTemplateName = className;
|
|
4281
|
-
break;
|
|
4282
|
-
}
|
|
4283
|
-
}
|
|
4284
|
-
catch (error) {
|
|
4285
|
-
console.warn(`[JQHTML] Error finding parent template ${className}:`, error);
|
|
4286
|
-
}
|
|
4287
|
-
currentClass = Object.getPrototypeOf(currentClass);
|
|
4288
|
-
}
|
|
4343
|
+
const component_name = template_def.name || this.args._component_name || this.constructor.name;
|
|
4344
|
+
const parent = resolve_parent_template(template_def, this.constructor);
|
|
4345
|
+
// A slot-only template is nothing but an override of a parent. With no parent
|
|
4346
|
+
// there is nothing to render, and rendering empty hides the mistake (usually a
|
|
4347
|
+
// missing register_template) behind a blank element. Fail loudly instead.
|
|
4348
|
+
if (!parent) {
|
|
4349
|
+
throw new Error(`[JQHTML] ${component_name}: slot-only template has no parent template. ` +
|
|
4350
|
+
`Add extends="Parent_Name" to <Define:${component_name}>, extend a class whose ` +
|
|
4351
|
+
`template is registered, or give the template markup of its own.`);
|
|
4289
4352
|
}
|
|
4290
|
-
//
|
|
4291
|
-
|
|
4292
|
-
|
|
4293
|
-
|
|
4294
|
-
|
|
4295
|
-
|
|
4296
|
-
const
|
|
4297
|
-
|
|
4298
|
-
|
|
4299
|
-
|
|
4300
|
-
// Return in render function format: [instructions, context]
|
|
4301
|
-
// The template expression handler expects this format
|
|
4302
|
-
return [slotInstructions, slotContext];
|
|
4303
|
-
}
|
|
4304
|
-
// Slot not found, return empty
|
|
4305
|
-
return '';
|
|
4306
|
-
};
|
|
4307
|
-
// Invoke parent template with child's slots as content function
|
|
4308
|
-
const [parentInstructions, parentContext] = parentTemplate.render.bind(this)(this.data, this.args, contentFunction, // Pass content function that invokes child slots
|
|
4309
|
-
jqhtml);
|
|
4310
|
-
console.log(`[JQHTML] Parent template invoked successfully`);
|
|
4311
|
-
instructions = parentInstructions;
|
|
4312
|
-
context = parentContext;
|
|
4313
|
-
}
|
|
4314
|
-
catch (error) {
|
|
4315
|
-
console.warn(`[JQHTML] Error invoking parent template ${parentTemplateName}:`, error);
|
|
4316
|
-
instructions = [];
|
|
4353
|
+
// Create a content function that invokes child slots
|
|
4354
|
+
// When parent calls content('slotName'), it invokes the child's slot function
|
|
4355
|
+
const childSlots = instructions._slots;
|
|
4356
|
+
const contentFunction = (slotName, ...slotArgs) => {
|
|
4357
|
+
if (childSlots[slotName] && typeof childSlots[slotName] === 'function') {
|
|
4358
|
+
// Invoke the slot function with everything content() was given
|
|
4359
|
+
const [slotInstructions, slotContext] = childSlots[slotName](...slotArgs);
|
|
4360
|
+
// Return in render function format: [instructions, context]
|
|
4361
|
+
// The template expression handler expects this format
|
|
4362
|
+
return [slotInstructions, slotContext];
|
|
4317
4363
|
}
|
|
4364
|
+
// Slot not found, return empty
|
|
4365
|
+
return '';
|
|
4366
|
+
};
|
|
4367
|
+
// Invoke parent template with child's slots as content function.
|
|
4368
|
+
// A parent template that throws is a real defect in the parent's markup. It used
|
|
4369
|
+
// to be swallowed into an empty render plus a warning nobody reads; it is now
|
|
4370
|
+
// re-thrown, which routes it through the boot error path: logged, this component
|
|
4371
|
+
// stopped, its waiting ancestors released.
|
|
4372
|
+
try {
|
|
4373
|
+
const [parentInstructions, parentContext] = parent.template.render.bind(this)(this.data, this.args, contentFunction, // Pass content function that invokes child slots
|
|
4374
|
+
jqhtml);
|
|
4375
|
+
instructions = parentInstructions;
|
|
4376
|
+
context = parentContext;
|
|
4318
4377
|
}
|
|
4319
|
-
|
|
4320
|
-
|
|
4321
|
-
|
|
4378
|
+
catch (error) {
|
|
4379
|
+
// Rethrown, not swallowed - only re-labelled so the report names both ends
|
|
4380
|
+
// of the inheritance, which the raw template error does not.
|
|
4381
|
+
const wrapped = new Error(`[JQHTML] ${component_name}: parent template ${parent.name} threw while ` +
|
|
4382
|
+
`rendering slot-only child: ${error?.message ?? error}`);
|
|
4383
|
+
wrapped.cause = error; // ES2020 target: Error(message, {cause}) is not typed
|
|
4384
|
+
throw wrapped;
|
|
4322
4385
|
}
|
|
4323
4386
|
}
|
|
4324
4387
|
// Process instructions to generate DOM. ['_content', ...] markers from
|
|
@@ -4339,10 +4402,33 @@ class Jqhtml_Component {
|
|
|
4339
4402
|
}
|
|
4340
4403
|
this._fire_viewport_resize();
|
|
4341
4404
|
}
|
|
4405
|
+
// "Already rendered once" - read before the state below can move to 3.
|
|
4406
|
+
const isRerender = this._ready_state >= 3;
|
|
4407
|
+
// HTML CACHE MODE: Mark on_render complete after the post-on_load render.
|
|
4408
|
+
// This signals to parent components that this component's DOM is fully rendered
|
|
4409
|
+
// with fresh data and ready for HTML snapshot. It is set BEFORE trigger('render')
|
|
4410
|
+
// because _wait_for_children_on_render() waits on that very event and reads the
|
|
4411
|
+
// flag from its handler.
|
|
4412
|
+
if (this._ready_state >= 2) {
|
|
4413
|
+
this._on_render_complete = true;
|
|
4414
|
+
// State 3 = "rendered with loaded data, ready phase not finished yet".
|
|
4415
|
+
// Only meaningful once the load phase has happened (>= 2); before that the
|
|
4416
|
+
// component is still walking 0 -> 1 -> 2 and must not be pushed forward.
|
|
4417
|
+
// The ready phase (_ready(), render()'s executor, _reload()) restores 4.
|
|
4418
|
+
this._ready_state = 3;
|
|
4419
|
+
this._update_debug_attrs();
|
|
4420
|
+
}
|
|
4421
|
+
else if (!this.__has_custom_on_load) {
|
|
4422
|
+
// A component with no custom on_load() skips the load phase entirely, so it never
|
|
4423
|
+
// reaches _ready_state 2 and would never set the flag above - and its data cannot
|
|
4424
|
+
// change, so this first render IS its final, fresh render. Without this, an
|
|
4425
|
+
// html-cache parent waited forever in _wait_for_children_on_render() for a
|
|
4426
|
+
// template-only child and never snapshotted, rendered or fired 'ready'.
|
|
4427
|
+
this._on_render_complete = true;
|
|
4428
|
+
}
|
|
4342
4429
|
// Emit lifecycle event
|
|
4343
4430
|
this.trigger('render');
|
|
4344
4431
|
// Apply debug delay after render
|
|
4345
|
-
const isRerender = this._ready_state >= 3; // Already rendered once
|
|
4346
4432
|
applyDebugDelay(isRerender ? 'rerender' : 'render');
|
|
4347
4433
|
// Store args snapshot for reload() comparison (skip if non-serializable)
|
|
4348
4434
|
try {
|
|
@@ -4354,12 +4440,6 @@ class Jqhtml_Component {
|
|
|
4354
4440
|
}
|
|
4355
4441
|
// Store data snapshot for refresh() comparison
|
|
4356
4442
|
this._data_on_last_render = JSON.stringify(this.data);
|
|
4357
|
-
// HTML CACHE MODE: Mark on_render complete after second render (post-on_load)
|
|
4358
|
-
// This signals to parent components that this component's DOM is fully rendered
|
|
4359
|
-
// with fresh data and ready for HTML snapshot
|
|
4360
|
-
if (this._ready_state >= 2) {
|
|
4361
|
-
this._on_render_complete = true;
|
|
4362
|
-
}
|
|
4363
4443
|
// Return the render ID so callers can check if this render is still current
|
|
4364
4444
|
return current_render_id;
|
|
4365
4445
|
}
|
|
@@ -4379,10 +4459,12 @@ class Jqhtml_Component {
|
|
|
4379
4459
|
render(id = null) {
|
|
4380
4460
|
if (this._stopped)
|
|
4381
4461
|
return Promise.resolve();
|
|
4382
|
-
//
|
|
4383
|
-
//
|
|
4384
|
-
|
|
4385
|
-
//
|
|
4462
|
+
// If id provided, delegate to child component.
|
|
4463
|
+
// NOTE: this runs BEFORE invalidate('ready'). render(sid) does not start a
|
|
4464
|
+
// lifecycle on THIS component - it delegates to the redrawable child, which
|
|
4465
|
+
// invalidates and re-triggers its own 'ready'. Invalidating here would delete
|
|
4466
|
+
// this component's sticky ready marker with nothing left to re-trigger it, so
|
|
4467
|
+
// every later `await comp.ready()` would hang.
|
|
4386
4468
|
if (id) {
|
|
4387
4469
|
const $element = this.$sid(id);
|
|
4388
4470
|
if ($element.length === 0) {
|
|
@@ -4397,14 +4479,23 @@ class Jqhtml_Component {
|
|
|
4397
4479
|
}
|
|
4398
4480
|
return child.render();
|
|
4399
4481
|
}
|
|
4482
|
+
// Invalidate ready event so new handlers wait for this render cycle to complete
|
|
4483
|
+
// This prevents .on('ready') handlers from firing immediately based on previous lifecycle
|
|
4484
|
+
this.invalidate('ready');
|
|
4400
4485
|
// Enqueue the full render lifecycle through the component queue
|
|
4401
4486
|
// Return the queue promise so `await this.render()` resolves when the
|
|
4402
4487
|
// full render lifecycle (DOM update, children ready, on_ready) completes
|
|
4403
4488
|
return this._queue.enqueue('render', async () => {
|
|
4489
|
+
// The queued executor can start long after render() was called - re-check.
|
|
4490
|
+
if (this._stopped)
|
|
4491
|
+
return;
|
|
4404
4492
|
// Execute render phase synchronously and capture render ID
|
|
4405
4493
|
const render_id = this._render();
|
|
4406
4494
|
// Wait for all child components to be ready
|
|
4407
4495
|
await this._wait_for_children_ready();
|
|
4496
|
+
// Stopped while waiting for children: no hook, no event on a dead component.
|
|
4497
|
+
if (this._stopped)
|
|
4498
|
+
return;
|
|
4408
4499
|
// Check if this render is still current before calling on_ready
|
|
4409
4500
|
// If _render_count changed, another render happened and we should skip on_ready
|
|
4410
4501
|
if (this._render_count !== render_id) {
|
|
@@ -4412,7 +4503,15 @@ class Jqhtml_Component {
|
|
|
4412
4503
|
}
|
|
4413
4504
|
// Call on_ready hook with authorization
|
|
4414
4505
|
await this._call_lifecycle('on_ready');
|
|
4506
|
+
// on_ready() may itself have awaited long enough for a stop().
|
|
4507
|
+
if (this._stopped)
|
|
4508
|
+
return;
|
|
4415
4509
|
this._fire_viewport_resize();
|
|
4510
|
+
// Back to the ready state: _render() dropped it to 3. Set BEFORE the trigger
|
|
4511
|
+
// so a handler that calls ready() during dispatch takes the already-ready
|
|
4512
|
+
// fast path instead of waiting for another trigger that will never come.
|
|
4513
|
+
this._ready_state = 4;
|
|
4514
|
+
this._update_debug_attrs();
|
|
4416
4515
|
// Trigger ready event
|
|
4417
4516
|
this.trigger('ready');
|
|
4418
4517
|
});
|
|
@@ -4442,40 +4541,54 @@ class Jqhtml_Component {
|
|
|
4442
4541
|
async load() {
|
|
4443
4542
|
if (this._stopped)
|
|
4444
4543
|
return false;
|
|
4445
|
-
//
|
|
4446
|
-
|
|
4447
|
-
|
|
4544
|
+
// No custom on_load() means there is nothing to run and nothing can have changed.
|
|
4545
|
+
// Running the detached execution anyway seeded this.data with the empty detached
|
|
4546
|
+
// clone (create() takes no snapshot for these components), silently wiping whatever
|
|
4547
|
+
// on_create() had set.
|
|
4548
|
+
if (!this.__has_custom_on_load)
|
|
4549
|
+
return false;
|
|
4550
|
+
// The queue settles this caller with the value of the executor that actually
|
|
4551
|
+
// ran - including one that collapsed two rapid load() calls into one run - so
|
|
4552
|
+
// the result is returned from the executor, not read back out of a closure
|
|
4553
|
+
// variable that a discarded executor would have left at its initial value.
|
|
4554
|
+
return this._queue.enqueue('load', async () => {
|
|
4555
|
+
// The queued executor can start long after load() was called - re-check.
|
|
4556
|
+
if (this._stopped)
|
|
4557
|
+
return false;
|
|
4448
4558
|
// Snapshot current data for change detection
|
|
4449
4559
|
const data_before = JSON.stringify(this.data);
|
|
4450
4560
|
// Execute on_load() on detached proxy (restores data to on_create snapshot)
|
|
4451
|
-
const { data: result_data } = await this._execute_on_load_detached(
|
|
4452
|
-
//
|
|
4561
|
+
const { data: result_data } = await this._execute_on_load_detached(null);
|
|
4562
|
+
// Stopped during on_load(): don't write data or cache for a dead component.
|
|
4563
|
+
if (this._stopped)
|
|
4564
|
+
return false;
|
|
4565
|
+
// Atomically update this.data: unfreeze → normalize → assign → refreeze.
|
|
4566
|
+
// The round trip runs in EVERY cache mode - see _apply_load_result().
|
|
4453
4567
|
this.__data_frozen = false;
|
|
4454
|
-
this.data = result_data;
|
|
4455
|
-
const cache_mode = Jqhtml_Local_Storage.get_cache_mode();
|
|
4456
|
-
if (cache_mode === 'data') {
|
|
4457
|
-
const normalized = Jqhtml_Local_Storage.normalize_for_cache(this.data);
|
|
4458
|
-
this.data = normalized;
|
|
4459
|
-
}
|
|
4568
|
+
this.data = Jqhtml_Local_Storage.normalize_for_cache(result_data, this.component_name());
|
|
4460
4569
|
this.__data_frozen = true;
|
|
4570
|
+
const cache_mode = Jqhtml_Local_Storage.get_cache_mode();
|
|
4461
4571
|
// Detect if data changed
|
|
4462
4572
|
const data_after = JSON.stringify(this.data);
|
|
4463
|
-
data_changed = data_before !== data_after;
|
|
4573
|
+
const data_changed = data_before !== data_after;
|
|
4464
4574
|
// Update cache with fresh data if changed
|
|
4465
4575
|
if (data_changed) {
|
|
4466
|
-
//
|
|
4467
|
-
|
|
4468
|
-
|
|
4469
|
-
|
|
4470
|
-
|
|
4576
|
+
// ONE derivation, shared with the cache read in create() and with _load().
|
|
4577
|
+
// The copy that used to live here omitted content serialization, so an
|
|
4578
|
+
// object-arg component computed a null key and silently never wrote the
|
|
4579
|
+
// cache that create() would later read with the content-serialized key.
|
|
4580
|
+
// Recomputed rather than reused: args may have changed since boot.
|
|
4581
|
+
const { cache_key, uncacheable_property, uncacheable_reason } = generate_cache_key(this);
|
|
4582
|
+
if (cache_key === null) {
|
|
4583
|
+
// Leave the same record _load() leaves: the offending arg on the element,
|
|
4584
|
+
// and one dev warning. A silent catch here made a throwing cache_id()
|
|
4585
|
+
// undiagnosable - the component simply stopped being cached.
|
|
4586
|
+
if (uncacheable_property) {
|
|
4587
|
+
this.$.attr('data-nocache', uncacheable_reason ? `${uncacheable_property}:${uncacheable_reason}` : uncacheable_property);
|
|
4471
4588
|
}
|
|
4472
|
-
|
|
4589
|
+
warn_uncacheable_component(this, uncacheable_property, uncacheable_reason);
|
|
4473
4590
|
}
|
|
4474
|
-
else {
|
|
4475
|
-
const result = Load_Coordinator.generate_invocation_key(this.component_name(), this.args);
|
|
4476
|
-
cache_key = result.key;
|
|
4477
|
-
}
|
|
4478
|
-
if (cache_key && cache_mode !== 'html') {
|
|
4591
|
+
else if (cache_mode !== 'html') {
|
|
4479
4592
|
Jqhtml_Local_Storage.set(cache_key, this.data);
|
|
4480
4593
|
}
|
|
4481
4594
|
}
|
|
@@ -4483,10 +4596,13 @@ class Jqhtml_Component {
|
|
|
4483
4596
|
// Suppressed by _load_only and _load_render_only flags (preloading mode)
|
|
4484
4597
|
if (!this._load_only && !this._load_render_only) {
|
|
4485
4598
|
await this._call_lifecycle('on_loaded');
|
|
4599
|
+
// on_loaded() may have awaited across a stop(); don't fire 'loaded' after it.
|
|
4600
|
+
if (this._stopped)
|
|
4601
|
+
return data_changed;
|
|
4486
4602
|
this.trigger('loaded');
|
|
4487
4603
|
}
|
|
4604
|
+
return data_changed;
|
|
4488
4605
|
});
|
|
4489
|
-
return data_changed;
|
|
4490
4606
|
}
|
|
4491
4607
|
/**
|
|
4492
4608
|
* Create phase - Quick setup, prepare UI
|
|
@@ -4509,15 +4625,18 @@ class Jqhtml_Component {
|
|
|
4509
4625
|
// OPTIMIZATION: Skip cache operations and snapshot if no custom on_load()
|
|
4510
4626
|
// Components without on_load() don't fetch data, so nothing to cache or restore
|
|
4511
4627
|
if (this.__has_custom_on_load) {
|
|
4628
|
+
// Snapshot this.data the moment on_create() returns, BEFORE any cache read.
|
|
4629
|
+
// on_load() restarts from this snapshot every time, and CLAUDE.md promises that
|
|
4630
|
+
// is the on_create() state. read_cache_in_create() REPLACES this.data on a warm
|
|
4631
|
+
// 'data' cache, so snapshotting after it made a warm boot restart on_load() from
|
|
4632
|
+
// cached data - `this.data.items.push(x)` doubled its rows on every reload.
|
|
4633
|
+
this.__initial_data_snapshot = JSON.parse(JSON.stringify(this.data));
|
|
4512
4634
|
// CACHE CHECK - Read from cache based on cache mode ('data' or 'html')
|
|
4513
4635
|
// Skip cache for detached elements — no point hydrating from cache when not in DOM
|
|
4514
4636
|
// @see component-cache.ts for full implementation
|
|
4515
4637
|
if (!this._is_detached) {
|
|
4516
4638
|
read_cache_in_create(this);
|
|
4517
4639
|
}
|
|
4518
|
-
// Snapshot this.data after on_create() completes
|
|
4519
|
-
// This will be restored before each on_load() execution to reset state
|
|
4520
|
-
this.__initial_data_snapshot = JSON.parse(JSON.stringify(this.data));
|
|
4521
4640
|
}
|
|
4522
4641
|
// Freeze this.data after on_create() - only on_load() can modify it now
|
|
4523
4642
|
this.__data_frozen = true;
|
|
@@ -4648,26 +4767,7 @@ class Jqhtml_Component {
|
|
|
4648
4767
|
// follower never runs on_load() and adopts the leader's data with NO
|
|
4649
4768
|
// revalidation, so a wrong key here is permanently wrong data. Redundant
|
|
4650
4769
|
// concurrent requests are the cheaper failure.
|
|
4651
|
-
|
|
4652
|
-
let uncacheable_property;
|
|
4653
|
-
let uncacheable_reason;
|
|
4654
|
-
if (typeof this.cache_id === 'function') {
|
|
4655
|
-
try {
|
|
4656
|
-
const custom_cache_id = this.cache_id();
|
|
4657
|
-
cache_key = `${this.component_name()}::${String(custom_cache_id)}`;
|
|
4658
|
-
}
|
|
4659
|
-
catch (error) {
|
|
4660
|
-
// cache_id() threw error - disable caching
|
|
4661
|
-
uncacheable_property = 'cache_id()';
|
|
4662
|
-
uncacheable_reason = 'cache-id-threw';
|
|
4663
|
-
}
|
|
4664
|
-
}
|
|
4665
|
-
else {
|
|
4666
|
-
const result = Load_Coordinator.generate_invocation_key(this.component_name(), this.args, { allow_content_serialization: true });
|
|
4667
|
-
cache_key = result.key;
|
|
4668
|
-
uncacheable_property = result.uncacheable_property;
|
|
4669
|
-
uncacheable_reason = result.uncacheable_reason;
|
|
4670
|
-
}
|
|
4770
|
+
const { cache_key, uncacheable_property, uncacheable_reason } = generate_cache_key(this);
|
|
4671
4771
|
const dedup_key = Load_Coordinator.generate_invocation_key(this.component_name(), this.args).key;
|
|
4672
4772
|
// SSR preload check: if preloaded data exists for this component+args, use it
|
|
4673
4773
|
if (cache_key !== null && has_preload_data()) {
|
|
@@ -4698,24 +4798,26 @@ class Jqhtml_Component {
|
|
|
4698
4798
|
if (window.jqhtml?.debug?.verbose) {
|
|
4699
4799
|
console.log(`[Load Deduplication] Component ${this._cid} (${this.component_name()}) has no dedup key - loading independently`);
|
|
4700
4800
|
}
|
|
4701
|
-
const { data: result_data } = await this._execute_on_load_detached();
|
|
4801
|
+
const { data: result_data } = await this._execute_on_load_detached(null);
|
|
4702
4802
|
await this._apply_load_result(result_data, data_before_load);
|
|
4703
4803
|
return;
|
|
4704
4804
|
}
|
|
4705
4805
|
// Check if this component should execute on_load() or wait for existing request
|
|
4706
|
-
const should_execute = Load_Coordinator.should_execute_on_load(this);
|
|
4806
|
+
const should_execute = Load_Coordinator.should_execute_on_load(this, dedup_key);
|
|
4707
4807
|
if (!should_execute) {
|
|
4708
4808
|
// This component is a follower - wait for leader to complete
|
|
4709
4809
|
if (window.jqhtml?.debug?.verbose) {
|
|
4710
4810
|
console.log(`[Load Deduplication] Component ${this._cid} (${this.component_name()}) is a follower, waiting for leader`, { args: this.args });
|
|
4711
4811
|
}
|
|
4712
|
-
|
|
4812
|
+
// Every coordinator call uses the key captured ABOVE, before on_load() began.
|
|
4813
|
+
// Recomputing it here would miss the entry whenever args changed meanwhile.
|
|
4814
|
+
const coordination_promise = Load_Coordinator.get_coordination_promise(dedup_key);
|
|
4713
4815
|
if (coordination_promise) {
|
|
4714
4816
|
try {
|
|
4715
|
-
// Wait for leader to complete
|
|
4817
|
+
// Wait for leader to complete (rejects if the leader's on_load() threw)
|
|
4716
4818
|
await coordination_promise;
|
|
4717
4819
|
// Retrieve leader's data from coordinator
|
|
4718
|
-
const leader_data = Load_Coordinator.get_leader_data(this);
|
|
4820
|
+
const leader_data = Load_Coordinator.get_leader_data(this, dedup_key);
|
|
4719
4821
|
if (leader_data !== null) {
|
|
4720
4822
|
// Apply leader's data using the same method as leader
|
|
4721
4823
|
// This properly handles freeze/unfreeze and normalization
|
|
@@ -4733,15 +4835,11 @@ class Jqhtml_Component {
|
|
|
4733
4835
|
throw error;
|
|
4734
4836
|
}
|
|
4735
4837
|
}
|
|
4736
|
-
//
|
|
4737
|
-
|
|
4738
|
-
|
|
4739
|
-
this.
|
|
4740
|
-
this.
|
|
4741
|
-
if (!this._load_only && !this._load_render_only) {
|
|
4742
|
-
await this._call_lifecycle('on_loaded');
|
|
4743
|
-
this.trigger('loaded');
|
|
4744
|
-
}
|
|
4838
|
+
// No leader data to adopt (the entry was already collected, or its data could
|
|
4839
|
+
// not be serialized). Load independently rather than firing load/loaded with the
|
|
4840
|
+
// untouched on_create() data.
|
|
4841
|
+
const { data: result_data } = await this._execute_on_load_detached(null);
|
|
4842
|
+
await this._apply_load_result(result_data, data_before_load);
|
|
4745
4843
|
return;
|
|
4746
4844
|
}
|
|
4747
4845
|
// This component is a leader - execute on_load() on detached proxy
|
|
@@ -4749,7 +4847,7 @@ class Jqhtml_Component {
|
|
|
4749
4847
|
console.log(`[Load Deduplication] Component ${this._cid} (${this.component_name()}) is the leader`, { args: this.args });
|
|
4750
4848
|
}
|
|
4751
4849
|
// Execute on_load on detached proxy with Load_Coordinator registration
|
|
4752
|
-
const { data: result_data, complete_coordination } = await this._execute_on_load_detached(
|
|
4850
|
+
const { data: result_data, complete_coordination } = await this._execute_on_load_detached(dedup_key);
|
|
4753
4851
|
// Apply result via sequential queue
|
|
4754
4852
|
await this._apply_load_result(result_data, data_before_load);
|
|
4755
4853
|
// Complete coordination AFTER this.data is updated
|
|
@@ -4763,8 +4861,8 @@ class Jqhtml_Component {
|
|
|
4763
4861
|
* @see data-proxy.ts for full implementation
|
|
4764
4862
|
* @private
|
|
4765
4863
|
*/
|
|
4766
|
-
async _execute_on_load_detached(
|
|
4767
|
-
return execute_on_load_detached(this,
|
|
4864
|
+
async _execute_on_load_detached(dedup_key = null) {
|
|
4865
|
+
return execute_on_load_detached(this, dedup_key);
|
|
4768
4866
|
}
|
|
4769
4867
|
/**
|
|
4770
4868
|
* Apply the result of on_load() to this.data via the sequential queue.
|
|
@@ -4787,21 +4885,22 @@ class Jqhtml_Component {
|
|
|
4787
4885
|
await my_turn;
|
|
4788
4886
|
try {
|
|
4789
4887
|
// Now it's our turn to set this.data
|
|
4790
|
-
//
|
|
4888
|
+
// Every on_load() result is round-tripped through the cache serializer before it
|
|
4889
|
+
// becomes this.data - in EVERY cache mode, not just 'data'. Two consequences the
|
|
4890
|
+
// rest of the framework depends on:
|
|
4891
|
+
// - this.data is jqhtml's own copy. The author's object graph is never aliased,
|
|
4892
|
+
// so mutating what on_load() returned cannot reach a frozen this.data.
|
|
4893
|
+
// - this.data is BY CONSTRUCTION what the cache stores and what a cache hit
|
|
4894
|
+
// returns. Hot and cold are the same object shape in every mode, so turning
|
|
4895
|
+
// caching on can never change what a component sees.
|
|
4896
|
+
// Dates, Maps, Sets and register_cache_class() classes are reconstructed; anything
|
|
4897
|
+
// the serializer cannot express is dropped and warned about once (local-storage.ts).
|
|
4791
4898
|
this.__data_frozen = false;
|
|
4792
|
-
this.data = result_data;
|
|
4793
|
-
// DATA MODE: Normalize this.data through serialize/deserialize round-trip
|
|
4794
|
-
// This ensures "hot" data (fresh from on_load) behaves identically to "cold" data
|
|
4795
|
-
const cache_mode = Jqhtml_Local_Storage.get_cache_mode();
|
|
4796
|
-
if (cache_mode === 'data') {
|
|
4797
|
-
const normalized = Jqhtml_Local_Storage.normalize_for_cache(this.data);
|
|
4798
|
-
this.data = normalized;
|
|
4799
|
-
if (window.jqhtml?.debug?.verbose) {
|
|
4800
|
-
console.log(`[Cache data] Component ${this._cid} (${this.component_name()}) normalized this.data after on_load()`, { data: this.data });
|
|
4801
|
-
}
|
|
4802
|
-
}
|
|
4803
|
-
// Freeze this.data
|
|
4899
|
+
this.data = Jqhtml_Local_Storage.normalize_for_cache(result_data, this.component_name());
|
|
4804
4900
|
this.__data_frozen = true;
|
|
4901
|
+
if (window.jqhtml?.debug?.verbose) {
|
|
4902
|
+
console.log(`[Cache] Component ${this._cid} (${this.component_name()}) normalized this.data after on_load()`, { data: this.data });
|
|
4903
|
+
}
|
|
4805
4904
|
// SSR data capture: record this component's data for preloading
|
|
4806
4905
|
if (is_capture_enabled() && this._has_on_load()) {
|
|
4807
4906
|
capture_component_data(this.component_name(), this.args, this.data, this._cache_key);
|
|
@@ -4887,9 +4986,11 @@ class Jqhtml_Component {
|
|
|
4887
4986
|
callback();
|
|
4888
4987
|
return Promise.resolve();
|
|
4889
4988
|
}
|
|
4890
|
-
// Return promise that resolves when ready event fires
|
|
4989
|
+
// Return promise that resolves when ready event fires.
|
|
4990
|
+
// once(), not on(): a promise can only settle once, so a permanent listener
|
|
4991
|
+
// just accumulates on the component - one per ready() call, forever.
|
|
4891
4992
|
return new Promise((resolve) => {
|
|
4892
|
-
this.
|
|
4993
|
+
this.once('ready', () => {
|
|
4893
4994
|
if (callback)
|
|
4894
4995
|
callback();
|
|
4895
4996
|
resolve();
|
|
@@ -4947,9 +5048,18 @@ class Jqhtml_Component {
|
|
|
4947
5048
|
if (child._ready_state >= 4) {
|
|
4948
5049
|
continue;
|
|
4949
5050
|
}
|
|
4950
|
-
//
|
|
5051
|
+
// A stopped child will never fire 'ready' again - waiting on it would wedge
|
|
5052
|
+
// this parent (and everything awaiting its ready()) forever.
|
|
5053
|
+
if (child._stopped) {
|
|
5054
|
+
continue;
|
|
5055
|
+
}
|
|
5056
|
+
// Create promise that resolves when child reaches ready.
|
|
5057
|
+
// once(), not on(): a permanent listener would be appended to the child on
|
|
5058
|
+
// every render/reload cycle of a polling parent and never removed.
|
|
5059
|
+
// Raced against 'stop' so a child stopped mid-wait releases the parent.
|
|
4951
5060
|
const ready_promise = new Promise((resolve) => {
|
|
4952
|
-
child.
|
|
5061
|
+
child.once('ready', () => resolve());
|
|
5062
|
+
child.once('stop', () => resolve());
|
|
4953
5063
|
});
|
|
4954
5064
|
ready_promises.push(ready_promise);
|
|
4955
5065
|
}
|
|
@@ -4975,22 +5085,42 @@ class Jqhtml_Component {
|
|
|
4975
5085
|
// Create promises for each child that hasn't completed on_render yet
|
|
4976
5086
|
const render_promises = [];
|
|
4977
5087
|
for (const child of children) {
|
|
4978
|
-
// If child already completed on_render post-on_load, skip
|
|
4979
|
-
if (child._on_render_complete) {
|
|
5088
|
+
// If child already completed on_render post-on_load (or is dead), skip
|
|
5089
|
+
if (child._on_render_complete || child._stopped) {
|
|
4980
5090
|
continue;
|
|
4981
5091
|
}
|
|
4982
|
-
//
|
|
5092
|
+
// Event-driven, not polled: the child sets _on_render_complete just before it
|
|
5093
|
+
// fires 'render', and 'stop' ends the wait for a child that dies first.
|
|
5094
|
+
//
|
|
5095
|
+
// A plain once('render') would be wrong: events are sticky, and this child has
|
|
5096
|
+
// already rendered at least once (the pre-on_load render), so once() would fire
|
|
5097
|
+
// immediately off that stale marker and let the parent snapshot half-rendered DOM.
|
|
5098
|
+
// The handler therefore re-checks the flag and stays registered until it is set.
|
|
4983
5099
|
const render_promise = new Promise((resolve) => {
|
|
4984
|
-
|
|
4985
|
-
const
|
|
4986
|
-
if (
|
|
4987
|
-
|
|
4988
|
-
|
|
4989
|
-
|
|
4990
|
-
|
|
5100
|
+
let settled = false;
|
|
5101
|
+
const on_child_event = () => {
|
|
5102
|
+
if (settled)
|
|
5103
|
+
return;
|
|
5104
|
+
if (!child._on_render_complete && !child._stopped)
|
|
5105
|
+
return; // pre-load render
|
|
5106
|
+
settled = true;
|
|
5107
|
+
// There is no off() in the event API; splice this handler out of both queues
|
|
5108
|
+
// the way once() does, so a long-lived child does not accumulate handlers
|
|
5109
|
+
// across repeated waits.
|
|
5110
|
+
for (const event_name of ['render', 'stop']) {
|
|
5111
|
+
const callbacks = child._lifecycle_callbacks.get(event_name);
|
|
5112
|
+
if (callbacks) {
|
|
5113
|
+
const idx = callbacks.indexOf(on_child_event);
|
|
5114
|
+
if (idx !== -1)
|
|
5115
|
+
callbacks.splice(idx, 1);
|
|
5116
|
+
}
|
|
4991
5117
|
}
|
|
5118
|
+
resolve();
|
|
4992
5119
|
};
|
|
4993
|
-
|
|
5120
|
+
// 'stop' first: registering 'render' can fire the handler synchronously (sticky),
|
|
5121
|
+
// and the handler must be able to deregister both queues when it does.
|
|
5122
|
+
child.on('stop', on_child_event);
|
|
5123
|
+
child.on('render', on_child_event);
|
|
4994
5124
|
});
|
|
4995
5125
|
render_promises.push(render_promise);
|
|
4996
5126
|
}
|
|
@@ -5094,8 +5224,17 @@ class Jqhtml_Component {
|
|
|
5094
5224
|
this.next_reload_force_refresh = null;
|
|
5095
5225
|
this._render();
|
|
5096
5226
|
await this._wait_for_children_ready();
|
|
5227
|
+
// Stopped while waiting for children: no hook, no event on a dead component.
|
|
5228
|
+
if (this._stopped)
|
|
5229
|
+
return;
|
|
5097
5230
|
await this._call_lifecycle('on_ready');
|
|
5231
|
+
// on_ready() may itself have awaited long enough for a stop().
|
|
5232
|
+
if (this._stopped)
|
|
5233
|
+
return;
|
|
5098
5234
|
this._fire_viewport_resize();
|
|
5235
|
+
// Restore the ready state (_render() dropped it to 3) before the trigger
|
|
5236
|
+
this._ready_state = 4;
|
|
5237
|
+
this._update_debug_attrs();
|
|
5099
5238
|
this.trigger('ready');
|
|
5100
5239
|
this._log_lifecycle('reload', 'complete (no on_load)');
|
|
5101
5240
|
return;
|
|
@@ -5123,10 +5262,16 @@ class Jqhtml_Component {
|
|
|
5123
5262
|
// STEP 2: Call on_load() on detached proxy
|
|
5124
5263
|
// This uses the same detached execution as _load() - on_load runs isolated
|
|
5125
5264
|
// and result is applied via sequential queue
|
|
5126
|
-
const { data: result_data } = await this._execute_on_load_detached(
|
|
5265
|
+
const { data: result_data } = await this._execute_on_load_detached(null); // null = no coordination
|
|
5266
|
+
// Stopped during on_load(): don't write data or re-render a dead component.
|
|
5267
|
+
if (this._stopped)
|
|
5268
|
+
return;
|
|
5127
5269
|
// Apply result via sequential queue
|
|
5128
5270
|
// This handles normalization, caching, and setting _is_dynamic
|
|
5129
5271
|
await this._apply_load_result(result_data, data_before_load);
|
|
5272
|
+
// The apply queue can yield; a stop() there must abort the render/ready tail.
|
|
5273
|
+
if (this._stopped)
|
|
5274
|
+
return;
|
|
5130
5275
|
// Re-read data state after apply (for render decision below)
|
|
5131
5276
|
const data_after_load = JSON.stringify(this.data);
|
|
5132
5277
|
const data_changed = data_after_load !== data_before_load;
|
|
@@ -5135,6 +5280,15 @@ class Jqhtml_Component {
|
|
|
5135
5280
|
const force_refresh = this.next_reload_force_refresh !== null ? this.next_reload_force_refresh : true;
|
|
5136
5281
|
// Track if we need to render
|
|
5137
5282
|
let should_render = false;
|
|
5283
|
+
// HTML CACHE MODE: a cache injection above replaced the subtree with inert markup -
|
|
5284
|
+
// a snapshot string, with no live child components in it. It must be re-rendered
|
|
5285
|
+
// from the template regardless of what the load decided, exactly as _should_rerender()
|
|
5286
|
+
// forces it at boot. Without this, a reload whose data came back unchanged left dead
|
|
5287
|
+
// HTML (stopped children, no instances) on screen.
|
|
5288
|
+
const used_cached_html = this._used_cached_html;
|
|
5289
|
+
if (used_cached_html) {
|
|
5290
|
+
this._used_cached_html = false;
|
|
5291
|
+
}
|
|
5138
5292
|
if (force_refresh) {
|
|
5139
5293
|
// reload(true) or reload() - always render if we haven't yet, OR data changed
|
|
5140
5294
|
should_render = !rendered_from_cache || data_changed;
|
|
@@ -5155,6 +5309,11 @@ class Jqhtml_Component {
|
|
|
5155
5309
|
}
|
|
5156
5310
|
}
|
|
5157
5311
|
// STEP 3: Perform second render if needed
|
|
5312
|
+
// The cache injection's inert markup is not optional to replace - this overrides both
|
|
5313
|
+
// the reload() and the refresh()/reload(false) decision above.
|
|
5314
|
+
if (used_cached_html) {
|
|
5315
|
+
should_render = true;
|
|
5316
|
+
}
|
|
5158
5317
|
if (should_render) {
|
|
5159
5318
|
this._render();
|
|
5160
5319
|
}
|
|
@@ -5169,11 +5328,33 @@ class Jqhtml_Component {
|
|
|
5169
5328
|
// STEP 3.5 & 4: Wait for children and call on_ready (only if we rendered)
|
|
5170
5329
|
if (rendered_from_cache || should_render) {
|
|
5171
5330
|
await this._wait_for_children_ready();
|
|
5331
|
+
// Stopped while waiting for children: no hook, no event on a dead component.
|
|
5332
|
+
if (this._stopped)
|
|
5333
|
+
return;
|
|
5172
5334
|
await this._call_lifecycle('on_ready');
|
|
5335
|
+
// on_ready() may itself have awaited long enough for a stop().
|
|
5336
|
+
if (this._stopped)
|
|
5337
|
+
return;
|
|
5173
5338
|
this._fire_viewport_resize();
|
|
5339
|
+
// Restore the ready state (the load phase dropped it to 2, _render() to 3)
|
|
5340
|
+
// before the trigger, so handlers see a ready component.
|
|
5341
|
+
this._ready_state = 4;
|
|
5342
|
+
this._update_debug_attrs();
|
|
5174
5343
|
// Trigger ready event so parent callbacks fire on subsequent reloads
|
|
5175
5344
|
this.trigger('ready');
|
|
5176
5345
|
}
|
|
5346
|
+
else {
|
|
5347
|
+
// refresh()/reload(false) found unchanged data: no re-render, and on_ready()
|
|
5348
|
+
// is deliberately NOT called (documented behaviour). But the component IS
|
|
5349
|
+
// ready - and this cycle deleted the sticky 'ready' marker at the top of
|
|
5350
|
+
// _reload() and dropped _ready_state to 2 in the load phase. Without
|
|
5351
|
+
// restoring both, ready() never resolves again and a parent re-entering
|
|
5352
|
+
// _wait_for_children_ready() hangs on this child forever (the documented
|
|
5353
|
+
// polling pattern breaks after its first quiet refresh).
|
|
5354
|
+
this._ready_state = 4;
|
|
5355
|
+
this._update_debug_attrs();
|
|
5356
|
+
this.trigger('ready');
|
|
5357
|
+
}
|
|
5177
5358
|
this._log_lifecycle('reload', 'complete');
|
|
5178
5359
|
}
|
|
5179
5360
|
/**
|
|
@@ -5198,26 +5379,28 @@ class Jqhtml_Component {
|
|
|
5198
5379
|
this.__gate_resume = null;
|
|
5199
5380
|
resume();
|
|
5200
5381
|
}
|
|
5201
|
-
//
|
|
5202
|
-
//
|
|
5203
|
-
|
|
5204
|
-
|
|
5205
|
-
|
|
5206
|
-
|
|
5207
|
-
|
|
5208
|
-
|
|
5209
|
-
|
|
5210
|
-
}
|
|
5211
|
-
// Full cleanup path: component has custom stop logic
|
|
5382
|
+
// There is deliberately NO fast path for components without stop logic. The
|
|
5383
|
+
// marker class, the parent de-registration and the 'stop' event are not cleanup
|
|
5384
|
+
// the component asked for, they are how the rest of the framework learns this
|
|
5385
|
+
// component is dead: the class is the documented, observable stopped state; a
|
|
5386
|
+
// child left in _dom_parent._dom_children is a dead component every later
|
|
5387
|
+
// _wait_for_children_ready() and _get_dom_children() walks; and 'stop' is what
|
|
5388
|
+
// releases a parent (and boot's failure path) waiting on a child that will never
|
|
5389
|
+
// fire 'ready'. Skipping them for the common no-on_stop() child was what leaked
|
|
5390
|
+
// stopped children into the parent's registry.
|
|
5212
5391
|
this._log_lifecycle('destroy', 'start');
|
|
5213
5392
|
this.$.addClass('_Component_Stopped');
|
|
5214
5393
|
// Unregister from lifecycle manager
|
|
5215
5394
|
this._lifecycle_manager.unregister_component(this);
|
|
5216
|
-
// Call user's on_stop() hook with authorization (sync)
|
|
5217
|
-
|
|
5218
|
-
|
|
5219
|
-
|
|
5220
|
-
|
|
5395
|
+
// Call user's on_stop() hook with authorization (sync) - only when actually
|
|
5396
|
+
// overridden, since _call_lifecycle_sync() on the base no-op costs per child
|
|
5397
|
+
// on every re-render.
|
|
5398
|
+
if (this.on_stop !== Jqhtml_Component.prototype.on_stop) {
|
|
5399
|
+
const stopResult = this._call_lifecycle_sync('on_stop');
|
|
5400
|
+
if (stopResult && typeof stopResult.then === 'function') {
|
|
5401
|
+
console.warn(`[JQHTML] Component "${this.component_name()}" returned a Promise from on_stop(). ` +
|
|
5402
|
+
`on_stop() must be synchronous code. Remove 'async' from the function declaration.`);
|
|
5403
|
+
}
|
|
5221
5404
|
}
|
|
5222
5405
|
// Fire registered stop callbacks
|
|
5223
5406
|
this.trigger('stop');
|
|
@@ -5493,6 +5676,48 @@ class Jqhtml_Component {
|
|
|
5493
5676
|
_generate_cid() {
|
|
5494
5677
|
return uid();
|
|
5495
5678
|
}
|
|
5679
|
+
/**
|
|
5680
|
+
* Re-scope every scoped id in an HTML-cache snapshot so it belongs to THIS instance.
|
|
5681
|
+
*
|
|
5682
|
+
* A snapshot is the inner HTML of some component instance, so it contains ids of the
|
|
5683
|
+
* form `<name>:<cid>`: the snapshotting component's own elements carry its cid, and
|
|
5684
|
+
* elements rendered by nested children carry each child's cid. Replayed verbatim into
|
|
5685
|
+
* a second instance those ids are duplicates of the originals, and $sid() resolves via
|
|
5686
|
+
* getElementById(), which returns whichever matching element is first in the document -
|
|
5687
|
+
* so one instance could read and write another instance's DOM.
|
|
5688
|
+
*
|
|
5689
|
+
* Mapping: the snapshot owner's cid becomes this._cid (so $sid() finds the elements
|
|
5690
|
+
* this component is about to treat as its own), and every other cid becomes a fresh
|
|
5691
|
+
* uid(), consistently within one injection. Two instances hydrated from one snapshot
|
|
5692
|
+
* therefore share no id at all.
|
|
5693
|
+
*
|
|
5694
|
+
* The ':' separator is written only by the compiler ($sid) and the instruction
|
|
5695
|
+
* processor (a scoped plain id), and cids are uid() output - [a-z][a-z0-9]* - so the
|
|
5696
|
+
* pattern is exact. A plain unscoped id has no ':' and is left alone.
|
|
5697
|
+
*
|
|
5698
|
+
* data-cid is the debug mirror of a component's own _cid (config.ts debug_attributes).
|
|
5699
|
+
* The snapshot's values are stale cids belonging to components that no longer exist,
|
|
5700
|
+
* so they go through the same mapping rather than being left to lie in DevTools.
|
|
5701
|
+
*
|
|
5702
|
+
* @private
|
|
5703
|
+
*/
|
|
5704
|
+
_rescope_cached_html(snapshot) {
|
|
5705
|
+
const cid_map = new Map();
|
|
5706
|
+
cid_map.set(snapshot.cid, this._cid);
|
|
5707
|
+
const remap = (old_cid) => {
|
|
5708
|
+
let new_cid = cid_map.get(old_cid);
|
|
5709
|
+
if (new_cid === undefined) {
|
|
5710
|
+
new_cid = uid();
|
|
5711
|
+
cid_map.set(old_cid, new_cid);
|
|
5712
|
+
}
|
|
5713
|
+
return new_cid;
|
|
5714
|
+
};
|
|
5715
|
+
return snapshot.html
|
|
5716
|
+
// id="<name>:<cid>" - the name half may itself contain ':' (an author-written
|
|
5717
|
+
// id="a:b" is scoped as "a:b:cid"), so the cid is anchored at the end.
|
|
5718
|
+
.replace(/ id="([^"]*):([a-z][a-z0-9]*)"/g, (_match, name, old_cid) => ` id="${name}:${remap(old_cid)}"`)
|
|
5719
|
+
.replace(/ data-cid="([a-z][a-z0-9]*)"/g, (_match, old_cid) => ` data-cid="${remap(old_cid)}"`);
|
|
5720
|
+
}
|
|
5496
5721
|
_apply_css_classes() {
|
|
5497
5722
|
const hierarchy = this.constructor.get_class_hierarchy();
|
|
5498
5723
|
// If component name differs from class name, add component name to hierarchy
|
|
@@ -5532,26 +5757,29 @@ class Jqhtml_Component {
|
|
|
5532
5757
|
}
|
|
5533
5758
|
if (!template)
|
|
5534
5759
|
return;
|
|
5535
|
-
// Walk the
|
|
5536
|
-
//
|
|
5760
|
+
// Walk the inheritance chain to collect defaultAttributes from parent templates,
|
|
5761
|
+
// applying parent first so the child's own attributes take precedence.
|
|
5762
|
+
//
|
|
5763
|
+
// This uses resolve_parent_template(), the same helper the slot-only render path
|
|
5764
|
+
// uses, so a <Define>'s class/attrs resolve through EXACTLY the chain its markup
|
|
5765
|
+
// does. It previously followed only `extends=`, so a slot-only JS subclass
|
|
5766
|
+
// (class Users_Grid extends Grid_Abstract) inherited the parent's template but
|
|
5767
|
+
// none of the parent <Define>'s class="" or attributes.
|
|
5537
5768
|
const templateChain = [];
|
|
5538
5769
|
let currentTemplate = template;
|
|
5539
|
-
|
|
5540
|
-
|
|
5770
|
+
let currentClass = this.constructor;
|
|
5771
|
+
const seen = new Set();
|
|
5772
|
+
while (currentTemplate && !seen.has(currentTemplate)) {
|
|
5773
|
+
seen.add(currentTemplate);
|
|
5541
5774
|
templateChain.unshift(currentTemplate); // Add to front so parent comes first
|
|
5542
|
-
|
|
5543
|
-
if (
|
|
5544
|
-
try {
|
|
5545
|
-
currentTemplate = get_template(currentTemplate.extends);
|
|
5546
|
-
}
|
|
5547
|
-
catch (error) {
|
|
5548
|
-
// Parent template not found, stop chain
|
|
5549
|
-
break;
|
|
5550
|
-
}
|
|
5551
|
-
}
|
|
5552
|
-
else {
|
|
5775
|
+
const parent = resolve_parent_template(currentTemplate, currentClass);
|
|
5776
|
+
if (!parent)
|
|
5553
5777
|
break;
|
|
5554
|
-
|
|
5778
|
+
currentTemplate = parent.template;
|
|
5779
|
+
// Advance the prototype walk too: the next parent must be resolved relative to
|
|
5780
|
+
// the class we just landed on, not the original one. A template-only parent has
|
|
5781
|
+
// no class, which is correct - its own chain can only be an explicit extends=.
|
|
5782
|
+
currentClass = get_component_class(parent.name) || null;
|
|
5555
5783
|
}
|
|
5556
5784
|
// Apply defaultAttributes from each template in the chain (parent first, child last)
|
|
5557
5785
|
for (const tmpl of templateChain) {
|
|
@@ -5628,8 +5856,10 @@ class Jqhtml_Component {
|
|
|
5628
5856
|
}
|
|
5629
5857
|
}
|
|
5630
5858
|
else {
|
|
5631
|
-
// Regular attributes - apply directly if not already set
|
|
5632
|
-
|
|
5859
|
+
// Regular attributes - apply directly if not already set.
|
|
5860
|
+
// === undefined, not falsy: tabindex="0" and an empty-string attribute
|
|
5861
|
+
// written at the invocation are SET, and a Define default must not win.
|
|
5862
|
+
if (this.$.attr(key) === undefined) {
|
|
5633
5863
|
this.$.attr(key, value);
|
|
5634
5864
|
}
|
|
5635
5865
|
}
|
|
@@ -6290,13 +6520,12 @@ function init_jquery_plugin(jQuery) {
|
|
|
6290
6520
|
const _jqhtml_original_jquery = jQuery;
|
|
6291
6521
|
// Override jQuery constructor to handle component instances
|
|
6292
6522
|
const JQueryWithComponentSupport = function (selector, context) {
|
|
6293
|
-
//
|
|
6294
|
-
|
|
6295
|
-
|
|
6296
|
-
|
|
6297
|
-
|
|
6298
|
-
|
|
6299
|
-
// Return the component's jQuery element
|
|
6523
|
+
// Unwrap a component instance to its root element, so $(component) behaves
|
|
6524
|
+
// exactly like component.$ (addClass, find, [0] and every other jQuery method).
|
|
6525
|
+
// The check is an instanceof on the class itself: the previous duck-type tested
|
|
6526
|
+
// for an id() method, which Jqhtml_Component has never had, so this branch was
|
|
6527
|
+
// dead and $(component) wrapped the plain object instead.
|
|
6528
|
+
if (selector instanceof Jqhtml_Component) {
|
|
6300
6529
|
return selector.$;
|
|
6301
6530
|
}
|
|
6302
6531
|
// Otherwise, call original jQuery constructor
|
|
@@ -6372,13 +6601,12 @@ function init_jquery_plugin(jQuery) {
|
|
|
6372
6601
|
// Check if component already exists on this element
|
|
6373
6602
|
const existingComponent = element.data('_component');
|
|
6374
6603
|
if (existingComponent) {
|
|
6375
|
-
// Stop existing component (
|
|
6376
|
-
|
|
6377
|
-
|
|
6378
|
-
|
|
6379
|
-
|
|
6380
|
-
|
|
6381
|
-
}
|
|
6604
|
+
// Stop the existing component. A throwing stop() is NOT swallowed: the caller
|
|
6605
|
+
// asked to replace a live component, and overwriting one whose on_stop() failed
|
|
6606
|
+
// would leave its timers, intervals and listeners running against DOM that is
|
|
6607
|
+
// about to be destroyed. Let the throw reach the caller with the old component
|
|
6608
|
+
// still in place, so the failure is visible and recoverable.
|
|
6609
|
+
existingComponent.stop();
|
|
6382
6610
|
// Remove component classes (any class starting with a capital letter, or a
|
|
6383
6611
|
// component name with the reserved underscore prefix - except BEM classes)
|
|
6384
6612
|
const classes = element.attr('class');
|
|
@@ -6394,7 +6622,15 @@ function init_jquery_plugin(jQuery) {
|
|
|
6394
6622
|
const capital_first = cls[0] === cls[0].toUpperCase() && cls[0] !== cls[0].toLowerCase();
|
|
6395
6623
|
return !capital_first && !is_component_name(cls);
|
|
6396
6624
|
});
|
|
6397
|
-
|
|
6625
|
+
// Drop the attribute entirely when nothing survived the strip: attr('class', '')
|
|
6626
|
+
// leaves a `class=""` in the markup, which is noise in the DOM and in snapshot
|
|
6627
|
+
// comparisons for an element that now has no classes at all.
|
|
6628
|
+
if (nonComponentClasses.length > 0) {
|
|
6629
|
+
element.attr('class', nonComponentClasses.join(' '));
|
|
6630
|
+
}
|
|
6631
|
+
else {
|
|
6632
|
+
element.removeAttr('class');
|
|
6633
|
+
}
|
|
6398
6634
|
}
|
|
6399
6635
|
// Remove component data
|
|
6400
6636
|
element.removeData('_component');
|
|
@@ -6435,18 +6671,35 @@ function init_jquery_plugin(jQuery) {
|
|
|
6435
6671
|
const expectedTag = args._tag || template.tag || 'div';
|
|
6436
6672
|
const currentTag = element.prop('tagName').toLowerCase();
|
|
6437
6673
|
if (currentTag !== expectedTag.toLowerCase()) {
|
|
6438
|
-
// Tag mismatch
|
|
6439
|
-
|
|
6440
|
-
|
|
6674
|
+
// Tag mismatch. Replace whenever we can actually do it: the element must be in
|
|
6675
|
+
// a document (replaceWith() on a parentless element is a silent no-op that would
|
|
6676
|
+
// strand the component on a detached node) and must not be <body>.
|
|
6677
|
+
//
|
|
6678
|
+
// This used to require a truthy args._inner_html, i.e. server-rendered content.
|
|
6679
|
+
// boot.ts always sets _inner_html - but it empties the placeholder first, so a
|
|
6680
|
+
// placeholder with no server content carries '' and fell through to a warning,
|
|
6681
|
+
// hydrating into the wrong tag. Hydration of an empty placeholder still works:
|
|
6682
|
+
// the copied innerHTML is simply ''.
|
|
6683
|
+
const oldEl = element[0];
|
|
6684
|
+
const can_replace = currentTag !== 'body' && !!(oldEl && oldEl.parentNode);
|
|
6685
|
+
if (can_replace) {
|
|
6441
6686
|
const newElement = jQuery(`<${expectedTag}></${expectedTag}>`);
|
|
6442
6687
|
// Copy all attributes from old element to new element
|
|
6443
|
-
const oldEl = element[0];
|
|
6444
6688
|
if (oldEl && oldEl.attributes) {
|
|
6445
6689
|
for (let i = 0; i < oldEl.attributes.length; i++) {
|
|
6446
6690
|
const attr = oldEl.attributes[i];
|
|
6447
6691
|
newElement.attr(attr.name, attr.value);
|
|
6448
6692
|
}
|
|
6449
6693
|
}
|
|
6694
|
+
// Carry the jQuery data store across. $-args and anything the caller stashed
|
|
6695
|
+
// with .data() live there, and they are read back off the element during
|
|
6696
|
+
// component construction - dropping them silently changed the component's args.
|
|
6697
|
+
const old_data = element.data();
|
|
6698
|
+
if (old_data) {
|
|
6699
|
+
for (const key of Object.keys(old_data)) {
|
|
6700
|
+
newElement.data(key, old_data[key]);
|
|
6701
|
+
}
|
|
6702
|
+
}
|
|
6450
6703
|
// Copy innerHTML
|
|
6451
6704
|
newElement.html(element.html());
|
|
6452
6705
|
// Replace in DOM
|
|
@@ -6761,12 +7014,14 @@ function init(jQuery, config) {
|
|
|
6761
7014
|
}
|
|
6762
7015
|
}
|
|
6763
7016
|
// Version - will be replaced during build with actual version from package.json
|
|
6764
|
-
const version = '2.3.
|
|
7017
|
+
const version = '2.3.68';
|
|
6765
7018
|
// Default export with all functionality
|
|
6766
7019
|
const jqhtml = {
|
|
6767
7020
|
// Core
|
|
6768
7021
|
Jqhtml_Component,
|
|
6769
7022
|
LifecycleManager,
|
|
7023
|
+
// Internal - exposed for debugging and for tests that inspect coordination state
|
|
7024
|
+
Load_Coordinator,
|
|
6770
7025
|
// Registry
|
|
6771
7026
|
register,
|
|
6772
7027
|
register_component,
|
|
@@ -6782,7 +7037,6 @@ const jqhtml = {
|
|
|
6782
7037
|
// Template system
|
|
6783
7038
|
process_instructions,
|
|
6784
7039
|
extract_slots,
|
|
6785
|
-
render_template,
|
|
6786
7040
|
escape_html,
|
|
6787
7041
|
escape_html_nl2br,
|
|
6788
7042
|
// Version property - internal
|
|
@@ -6905,5 +7159,5 @@ if (typeof window !== 'undefined' && !window.jqhtml) {
|
|
|
6905
7159
|
}
|
|
6906
7160
|
}
|
|
6907
7161
|
|
|
6908
|
-
export { COMPONENT_NAME_PATTERN, COMPONENT_NAME_RULE, Jqhtml_Component, LifecycleManager as Jqhtml_LifecycleManager, Jqhtml_Local_Storage, LifecycleManager, Load_Coordinator, add_object_printer, applyDebugDelay, boot, clear_preload_data, configure, create_component, debug_overlay, jqhtml as default, devWarn, dynamic_component_name, escape_html, escape_html_nl2br, extract_slots, get_captured_data, get_component_class, get_component_names, get_config, get_object_printers, get_registered_templates, get_template, get_template_by_class, handleComponentError, has_component, init, init_jquery_plugin, isSequentialProcessing, is_component_name, list_components, logDataChange, logDispatch, logInstruction, logLifecycle, print_object, process_instructions, register, register_cache_class, register_component, register_template,
|
|
7162
|
+
export { COMPONENT_NAME_PATTERN, COMPONENT_NAME_RULE, Jqhtml_Component, LifecycleManager as Jqhtml_LifecycleManager, Jqhtml_Local_Storage, LifecycleManager, Load_Coordinator, add_object_printer, applyDebugDelay, boot, clear_preload_data, configure, create_component, debug_overlay, jqhtml as default, devWarn, dynamic_component_name, escape_html, escape_html_nl2br, extract_slots, get_captured_data, get_component_class, get_component_names, get_config, get_object_printers, get_registered_templates, get_template, get_template_by_class, handleComponentError, has_component, init, init_jquery_plugin, isSequentialProcessing, is_component_name, list_components, logDataChange, logDispatch, logInstruction, logLifecycle, print_object, process_instructions, register, register_cache_class, register_component, register_template, set_preload_data, start_data_capture, stop_data_capture, version };
|
|
6909
7163
|
//# sourceMappingURL=jqhtml-core.esm.js.map
|