@jqhtml/core 2.3.61 → 2.3.67

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/index.cjs CHANGED
@@ -203,6 +203,38 @@ class LifecycleManager {
203
203
  }
204
204
  }
205
205
 
206
+ /**
207
+ * The one rule for "is this name a component?"
208
+ *
209
+ * An optional SINGLE leading underscore, then a capital letter, then letters,
210
+ * digits and underscores. The underscore prefix is a namespace reserved for
211
+ * framework-provided components (`_Root_Layout`) so they cannot collide with
212
+ * application components. Two or more leading underscores are not a component
213
+ * name; a lower-case first letter after the optional underscore is an HTML tag.
214
+ */
215
+ const COMPONENT_NAME_PATTERN = /^_?[A-Z][A-Za-z0-9_]*$/;
216
+ const COMPONENT_NAME_RULE = 'must start with a capital letter, optionally preceded by a single underscore';
217
+ function is_component_name(name) {
218
+ return COMPONENT_NAME_PATTERN.test(name);
219
+ }
220
+ /**
221
+ * The render-time check behind <{expression}>: the expression must evaluate
222
+ * to a component name string that a literal tag would accept - no more, no
223
+ * less. A valid name that names nothing renders the ordinary placeholder
224
+ * component, exactly as an undefined literal tag does.
225
+ */
226
+ function dynamic_component_name(value) {
227
+ if (typeof value !== 'string' || value === '') {
228
+ const shown = value === '' ? 'an empty string' : typeof value === 'string' ? `'${value}'` : `${value === null ? 'null' : typeof value}`;
229
+ throw new Error(`[JQHTML] Dynamic component tag <{...}> evaluated to ${shown}; expected a non-empty component name string`);
230
+ }
231
+ if (!is_component_name(value)) {
232
+ throw new Error(`[JQHTML] Dynamic component tag <{...}> evaluated to '${value}', which is not a valid component name: ` +
233
+ `a component name ${COMPONENT_NAME_RULE}, then letters, digits and underscores`);
234
+ }
235
+ return value;
236
+ }
237
+
206
238
  /**
207
239
  * JQHTML v2 Component Registry
208
240
  *
@@ -249,9 +281,9 @@ function register_component(nameOrClass, component_class, template) {
249
281
  if (!component_class) {
250
282
  throw new Error('Component class is required when registering by name');
251
283
  }
252
- // Validate component name starts with capital letter
253
- if (!/^[A-Z]/.test(name)) {
254
- throw new Error(`Component name '${name}' must start with a capital letter. Convention is First_Letter_With_Underscores.`);
284
+ // Validate component name against the shared rule
285
+ if (!is_component_name(name)) {
286
+ throw new Error(`Component name '${name}' ${COMPONENT_NAME_RULE}. Convention is First_Letter_With_Underscores.`);
255
287
  }
256
288
  component_classes.set(name, component_class);
257
289
  // If template provided, register it
@@ -320,9 +352,9 @@ function register_template(template_def) {
320
352
  if (!name) {
321
353
  throw new Error('Template must have a name property');
322
354
  }
323
- // Validate template name starts with capital letter
324
- if (!/^[A-Z]/.test(name)) {
325
- throw new Error(`Template name '${name}' must start with a capital letter. Convention is First_Letter_With_Underscores.`);
355
+ // Validate template name against the shared rule
356
+ if (!is_component_name(name)) {
357
+ throw new Error(`Template name '${name}' ${COMPONENT_NAME_RULE}. Convention is First_Letter_With_Underscores.`);
326
358
  }
327
359
  // Check for duplicate registration
328
360
  if (component_templates.has(name)) {
@@ -1100,13 +1132,10 @@ async function initialize_component(element, compData) {
1100
1132
  if (slots) {
1101
1133
  options._slots = slots;
1102
1134
  }
1103
- // Always pass _component_name when component name differs from class name
1104
- // This happens when:
1105
- // 1. No class registered for component (uses Jqhtml_Component)
1106
- // 2. Class inherited from parent via extends chain (uses parent class but child template)
1107
- if (ComponentClass.name !== name) {
1108
- options._component_name = name;
1109
- }
1135
+ // Always pass the invocation name: it is what component_name() reports, it
1136
+ // selects the template when the class is inherited via an extends chain or
1137
+ // is the bare Jqhtml_Component, and it is the class the element carries.
1138
+ options._component_name = name;
1110
1139
  // Pass lifecycle truncation flags via options (not DOM attributes, since _ prefix is filtered).
1111
1140
  // Accept both the plain key (cascaded/programmatic) and the '$'-prefixed key (template-authored).
1112
1141
  if (props._load_only === true || props['$_load_only'] === true) {
@@ -1851,7 +1880,7 @@ Load_Coordinator._registry = new Map();
1851
1880
  const class_registry = Object.create(null);
1852
1881
  // Markers for serialized class instances (unique to avoid collisions with user data)
1853
1882
  // Replaced at build time by rollup (see rollup.config.js).
1854
- const CORE_VERSION = '2.3.61';
1883
+ const CORE_VERSION = '2.3.67';
1855
1884
  const CLASS_MARKER = '__jqhtml_class__';
1856
1885
  const PROPS_MARKER = '__jqhtml_props__';
1857
1886
  /**
@@ -3099,511 +3128,910 @@ function write_cache_on_loaded(component, data_changed) {
3099
3128
  }
3100
3129
 
3101
3130
  /**
3102
- * JQHTML Component Lifecycle Queue
3131
+ * JQHTML v2 Template Renderer
3103
3132
  *
3104
- * Serializes lifecycle operations (render, reload, refresh) per component.
3105
- * Replaces _create_debounced_function with a proper queue that:
3133
+ * Connects compiled templates to components
3134
+ * Processes instruction arrays and handles bindings
3135
+ */
3136
+ /**
3137
+ * Process slot-only template inheritance
3106
3138
  *
3107
- * - At most one operation runs at a time per component
3108
- * - At most one operation is pending (waiting for the current to finish)
3109
- * - Same-type pending operations collapse (fan-in: all callers share one execution)
3110
- * - Different-type pending operations: new replaces old (old callers get the new result)
3139
+ * When a component's template returns {_slots: {...}}, walk the prototype
3140
+ * chain to find a parent template and invoke it with the child's slots as content.
3111
3141
  *
3112
- * Boot bypasses this queue entirely - it runs the lifecycle directly.
3142
+ * @param component The component instance
3143
+ * @param childSlots The slots object from the child template
3144
+ * @returns [instructions, context] tuple or null if no parent found
3113
3145
  */
3114
- class Component_Queue {
3115
- constructor() {
3116
- /** Currently executing operation */
3117
- this._current = null;
3118
- /** Next operation waiting to execute after current completes */
3119
- this._pending = null;
3120
- }
3121
- /**
3122
- * Enqueue a lifecycle operation.
3123
- *
3124
- * Behavior:
3125
- * - Nothing running → execute immediately
3126
- * - Something running, no pending → create pending entry
3127
- * - Something running, same type pending → collapse (share pending promise)
3128
- * - Something running, different type pending → replace pending (all callers get new result)
3129
- *
3130
- * @param type - Operation type ('render', 'reload', 'refresh', 'load')
3131
- * @param executor - The async function to execute
3132
- * @returns Promise that resolves when the operation completes
3133
- */
3134
- enqueue(type, executor) {
3135
- // If nothing running, execute immediately
3136
- if (!this._current) {
3137
- return this._execute(type, executor);
3138
- }
3139
- // Something is running — add to pending
3140
- if (this._pending) {
3141
- if (this._pending.type === type) {
3142
- // Same type pending → collapse (fan-in)
3143
- // All callers share the same future execution
3144
- return new Promise((resolve, reject) => {
3145
- this._pending.resolvers.push(resolve);
3146
- this._pending.rejecters.push(reject);
3147
- });
3148
- }
3149
- else {
3150
- // Different type pending → replace
3151
- // Old pending callers join the new operation
3152
- // (their operation was superseded by a different type)
3153
- const old_resolvers = this._pending.resolvers;
3154
- const old_rejecters = this._pending.rejecters;
3155
- return new Promise((resolve, reject) => {
3156
- this._pending = {
3157
- type,
3158
- executor,
3159
- resolvers: [...old_resolvers, resolve],
3160
- rejecters: [...old_rejecters, reject]
3161
- };
3162
- });
3163
- }
3146
+ async function process_slot_inheritance(component, childSlots) {
3147
+ // Walk the prototype chain to find parent class with template
3148
+ let currentClass = Object.getPrototypeOf(component.constructor);
3149
+ console.log(`[JQHTML] Walking prototype chain for ${component.constructor.name}`);
3150
+ while (currentClass && currentClass !== Jqhtml_Component && currentClass.name !== 'Object') {
3151
+ const className = currentClass.name;
3152
+ console.log(`[JQHTML] Checking parent class: ${className}`);
3153
+ // Skip internal framework classes
3154
+ if (className === '_Jqhtml_Component' || className === '_Base_Jqhtml_Component') {
3155
+ currentClass = Object.getPrototypeOf(currentClass);
3156
+ continue;
3164
3157
  }
3165
- // No pending entry yet — create one
3166
- return new Promise((resolve, reject) => {
3167
- this._pending = {
3168
- type,
3169
- executor,
3170
- resolvers: [resolve],
3171
- rejecters: [reject]
3172
- };
3173
- });
3174
- }
3175
- /**
3176
- * Execute an operation and handle pending queue after completion.
3177
- * @private
3178
- */
3179
- _execute(type, executor) {
3180
- const promise = (async () => {
3181
- try {
3182
- await executor();
3183
- }
3184
- finally {
3185
- this._current = null;
3186
- // Run pending if exists
3187
- if (this._pending) {
3188
- const pending = this._pending;
3189
- this._pending = null;
3190
- try {
3191
- await this._execute(pending.type, pending.executor);
3192
- for (const resolve of pending.resolvers)
3193
- resolve();
3194
- }
3195
- catch (err) {
3196
- for (const reject of pending.rejecters)
3197
- reject(err);
3198
- }
3158
+ // Try to find template for this parent class
3159
+ try {
3160
+ const parentTemplate = get_template(className);
3161
+ console.log(`[JQHTML] Template found for ${className}:`, parentTemplate ? parentTemplate.name : 'null');
3162
+ // Check if we got the default template (means no real template found)
3163
+ if (parentTemplate && parentTemplate.name !== 'Jqhtml_Component') {
3164
+ console.log(`[JQHTML] Invoking parent template ${className}`);
3165
+ // Found a real parent template - invoke it with child's slots as content
3166
+ const [parentInstructions, parentContext] = parentTemplate.render.call(component, component.data, component.args, childSlots // Pass child slots as content parameter
3167
+ );
3168
+ // Check if parent also returns slots (recursive inheritance)
3169
+ if (parentInstructions && typeof parentInstructions === 'object' && parentInstructions._slots) {
3170
+ // Parent also has slot-only template - recurse
3171
+ console.log(`[JQHTML] Parent also slot-only, recursing`);
3172
+ return await process_slot_inheritance(component, parentInstructions._slots);
3199
3173
  }
3174
+ // Parent returned HTML - we're done
3175
+ console.log(`[JQHTML] Parent returned instructions, inheritance complete`);
3176
+ return [parentInstructions, parentContext];
3200
3177
  }
3201
- })();
3202
- this._current = { type, promise };
3203
- return promise;
3204
- }
3205
- /**
3206
- * Check if any operation is currently running.
3207
- */
3208
- get is_busy() {
3209
- return this._current !== null;
3210
- }
3211
- /**
3212
- * Check if there's a pending operation waiting.
3213
- */
3214
- get has_pending() {
3215
- return this._pending !== null;
3178
+ }
3179
+ catch (error) {
3180
+ console.warn(`[JQHTML] Error looking up parent template for ${className}:`, error);
3181
+ }
3182
+ // Move to next parent
3183
+ currentClass = Object.getPrototypeOf(currentClass);
3216
3184
  }
3217
- }
3218
-
3219
- /**
3220
- * JQHTML SSR Preload Data System
3221
- *
3222
- * Captures component data during SSR rendering and replays it on the client
3223
- * to skip redundant on_load() calls during hydration.
3224
- *
3225
- * Server-side: start_data_capture() → render → get_captured_data()
3226
- * Client-side: set_preload_data(entries) → components boot with preloaded data → clear_preload_data()
3227
- */
3228
- // --- Capture (Server Side) ---
3229
- let _capture_enabled = false;
3230
- let _capture_buffer = [];
3231
- // Track captured keys to avoid duplicates from Load Coordinator followers
3232
- let _captured_keys = new Set();
3233
- /**
3234
- * Enable data capture mode. After this call, every component that completes
3235
- * on_load() has its final this.data recorded.
3236
- *
3237
- * Idempotent — calling twice is fine.
3238
- */
3239
- function start_data_capture() {
3240
- _capture_enabled = true;
3241
- _capture_buffer = [];
3242
- _captured_keys.clear();
3185
+ // No parent template found
3186
+ console.warn(`[JQHTML] No parent template found after walking chain`);
3187
+ return null;
3243
3188
  }
3244
3189
  /**
3245
- * Record a component's data after on_load() completes.
3246
- * Called internally from _apply_load_result().
3247
- *
3248
- * @param component_name - Component name
3249
- * @param args - Component args at time of on_load()
3250
- * @param data - this.data after on_load() completed
3251
- * @param cache_key - The cache key for deduplication (null = always capture)
3190
+ * Render a template for a component
3191
+ * Templates are functions that return [instructions, context] tuples
3252
3192
  */
3253
- function capture_component_data(component_name, args, data, cache_key) {
3254
- if (!_capture_enabled)
3255
- return;
3256
- // Deduplicate: if Load Coordinator already captured this key, skip
3257
- if (cache_key !== null) {
3258
- if (_captured_keys.has(cache_key))
3259
- return;
3260
- _captured_keys.add(cache_key);
3193
+ async function render_template(component, template_fn) {
3194
+ // Get template from registry if not provided
3195
+ let render_fn = template_fn;
3196
+ if (!render_fn) {
3197
+ const template_def = get_template_by_class(component.constructor);
3198
+ render_fn = template_def.render;
3261
3199
  }
3262
- // Deep clone args and data to plain objects (no proxies, no class instances)
3263
- try {
3264
- const cloned_args = JSON.parse(JSON.stringify(args));
3265
- const cloned_data = JSON.parse(JSON.stringify(data));
3266
- _capture_buffer.push({
3267
- component: component_name,
3268
- args: cloned_args,
3269
- data: cloned_data,
3270
- });
3200
+ if (!render_fn) {
3201
+ // No template, component should handle its own rendering
3202
+ return;
3271
3203
  }
3272
- catch (error) {
3273
- // Non-serializable data — skip capture for this component
3274
- if (typeof console !== 'undefined') {
3275
- console.warn(`[JQHTML SSR Capture] Failed to serialize data for ${component_name}:`, error);
3204
+ // Clear existing content
3205
+ component.$.empty();
3206
+ // Execute template function
3207
+ // Templates receive: (data, args, content)
3208
+ // Provide a default empty content function if none provided
3209
+ const defaultContent = () => '';
3210
+ let [instructions, context] = render_fn.call(component, component.data, component.args, defaultContent // Default content function that returns empty string
3211
+ );
3212
+ // Check for slot-only template inheritance
3213
+ // If instructions is {_slots: {...}}, this is a slot-only template
3214
+ // We need to find the parent template and invoke it with these slots
3215
+ if (instructions && typeof instructions === 'object' && instructions._slots) {
3216
+ console.log(`[JQHTML] Slot-only template detected for ${component.constructor.name}, invoking inheritance`);
3217
+ const result = await process_slot_inheritance(component, instructions._slots);
3218
+ if (result) {
3219
+ console.log(`[JQHTML] Parent template found, using parent instructions`);
3220
+ instructions = result[0];
3221
+ context = result[1];
3222
+ }
3223
+ else {
3224
+ console.warn(`[JQHTML] No parent template found for ${component.constructor.name}, rendering empty`);
3225
+ // No parent template found - return empty
3226
+ instructions = [];
3276
3227
  }
3277
3228
  }
3229
+ // Process instructions to generate DOM
3230
+ await process_instructions(instructions, component.$, component);
3231
+ // Process bindings after DOM is created
3232
+ await process_bindings(component);
3233
+ // Attach event handlers
3234
+ await attach_event_handlers(component);
3278
3235
  }
3279
3236
  /**
3280
- * Returns all captured component data and resets the capture buffer.
3281
- * One-shot: calling clears the buffer.
3282
- */
3283
- function get_captured_data() {
3284
- const result = _capture_buffer;
3285
- _capture_buffer = [];
3286
- _captured_keys.clear();
3287
- return result;
3288
- }
3289
- /**
3290
- * Check if data capture is currently enabled.
3237
+ * Process data bindings (attributes created by :prop syntax)
3291
3238
  */
3292
- function is_capture_enabled() {
3293
- return _capture_enabled;
3239
+ async function process_bindings(component) {
3240
+ // Find all elements with data-bind-* attributes
3241
+ component.$.find('[data-bind-prop], [data-bind-value], [data-bind-text], [data-bind-html], [data-bind-class], [data-bind-style]').each((_, element) => {
3242
+ const el = $(element);
3243
+ const attrs = element.attributes;
3244
+ for (let i = 0; i < attrs.length; i++) {
3245
+ const attr = attrs[i];
3246
+ if (attr.name.startsWith('data-bind-')) {
3247
+ const binding_type = attr.name.substring(10); // Remove 'data-bind-'
3248
+ const expression = attr.value;
3249
+ try {
3250
+ // Evaluate expression in component context
3251
+ const value = evaluate_expression(expression, component);
3252
+ // Apply binding based on type
3253
+ switch (binding_type) {
3254
+ case 'prop':
3255
+ // Generic property binding
3256
+ const prop_name = el.attr('data-bind-prop-name') || 'value';
3257
+ el.prop(prop_name, value);
3258
+ break;
3259
+ case 'value':
3260
+ el.val(value);
3261
+ break;
3262
+ case 'text':
3263
+ el.text(value);
3264
+ break;
3265
+ case 'html':
3266
+ el.html(value);
3267
+ break;
3268
+ case 'class':
3269
+ if (typeof value === 'object') {
3270
+ // Object syntax: {active: true, disabled: false}
3271
+ Object.entries(value).forEach(([className, enabled]) => {
3272
+ el.toggleClass(className, !!enabled);
3273
+ });
3274
+ }
3275
+ else {
3276
+ // String syntax
3277
+ el.addClass(String(value));
3278
+ }
3279
+ break;
3280
+ case 'style':
3281
+ if (typeof value === 'object') {
3282
+ el.css(value);
3283
+ }
3284
+ else {
3285
+ el.attr('style', String(value));
3286
+ }
3287
+ break;
3288
+ default:
3289
+ // Custom binding - set as attribute
3290
+ el.attr(binding_type, value);
3291
+ }
3292
+ }
3293
+ catch (error) {
3294
+ console.error(`Error evaluating binding "${expression}":`, error);
3295
+ }
3296
+ }
3297
+ }
3298
+ });
3294
3299
  }
3295
3300
  /**
3296
- * Stop data capture and clear all state.
3301
+ * Attach event handlers (attributes created by @event syntax)
3297
3302
  */
3298
- function stop_data_capture() {
3299
- _capture_enabled = false;
3300
- _capture_buffer = [];
3301
- _captured_keys.clear();
3303
+ async function attach_event_handlers(component) {
3304
+ // Find all elements with data-on-* attributes
3305
+ 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) => {
3306
+ const el = $(element);
3307
+ const attrs = element.attributes;
3308
+ for (let i = 0; i < attrs.length; i++) {
3309
+ const attr = attrs[i];
3310
+ if (attr.name.startsWith('data-on-')) {
3311
+ const event_name = attr.name.substring(8); // Remove 'data-on-'
3312
+ const handler_expr = attr.value;
3313
+ // Remove the attribute to avoid re-processing
3314
+ el.removeAttr(attr.name);
3315
+ // Attach event handler
3316
+ el.on(event_name, function (event) {
3317
+ try {
3318
+ // Create handler function in component context
3319
+ const handler = evaluate_handler(handler_expr, component);
3320
+ if (typeof handler === 'function') {
3321
+ // Call with component as 'this' and pass event
3322
+ handler.call(component, event);
3323
+ }
3324
+ else {
3325
+ // Expression to execute
3326
+ evaluate_expression(handler_expr, component, { $event: event });
3327
+ }
3328
+ }
3329
+ catch (error) {
3330
+ console.error(`Error in ${event_name} handler "${handler_expr}":`, error);
3331
+ }
3332
+ });
3333
+ }
3334
+ }
3335
+ });
3302
3336
  }
3303
- // --- Preload (Client Side) ---
3304
- let _preload_cache = new Map();
3305
3337
  /**
3306
- * Seed the preload cache with SSR-captured data.
3307
- * Must be called before any component _load() runs.
3308
- *
3309
- * @param entries - Array of { component, args, data } entries from get_captured_data()
3338
+ * Evaluate an expression in component context
3310
3339
  */
3311
- function set_preload_data(entries) {
3312
- if (!entries || entries.length === 0)
3313
- return;
3314
- _preload_cache.clear();
3315
- for (const entry of entries) {
3316
- // Generate key using the same algorithm as Load Coordinator
3317
- const result = Load_Coordinator.generate_invocation_key(entry.component, entry.args);
3318
- if (result.key !== null) {
3319
- _preload_cache.set(result.key, entry.data);
3320
- }
3340
+ function evaluate_expression(expression, component, locals = {}) {
3341
+ // Create evaluation context
3342
+ const context = {
3343
+ // Component properties
3344
+ data: component.data,
3345
+ args: component.args,
3346
+ $: component.$,
3347
+ // Component methods
3348
+ $sid: component.$sid.bind(component),
3349
+ // Locals (like $event)
3350
+ ...locals
3351
+ };
3352
+ // Build function that evaluates in context
3353
+ const keys = Object.keys(context);
3354
+ const values = Object.values(context);
3355
+ try {
3356
+ // Use Function constructor for better performance than eval
3357
+ const fn = new Function(...keys, `return (${expression})`);
3358
+ return fn(...values);
3359
+ }
3360
+ catch (error) {
3361
+ console.error(`Invalid expression: ${expression}`, error);
3362
+ return undefined;
3321
3363
  }
3322
3364
  }
3323
3365
  /**
3324
- * Check if preloaded data exists for a given cache key.
3325
- * If found, returns the data and removes the entry (one-shot).
3326
- *
3327
- * @param cache_key - The cache key (same format as Load Coordinator)
3328
- * @returns The preloaded data, or null if no match
3366
+ * Evaluate a handler expression (might be method name or inline code)
3329
3367
  */
3330
- function consume_preload_data(cache_key) {
3331
- if (_preload_cache.size === 0)
3368
+ function evaluate_handler(expression, component) {
3369
+ // Check if it's a method name on the component
3370
+ if (expression in component && typeof component[expression] === 'function') {
3371
+ return component[expression];
3372
+ }
3373
+ // Otherwise treat as inline code
3374
+ try {
3375
+ return new Function('$event', `
3376
+ const { data, args, $, emit, $sid } = this;
3377
+ ${expression}
3378
+ `).bind(component);
3379
+ }
3380
+ catch (error) {
3381
+ console.error(`Invalid handler: ${expression}`, error);
3332
3382
  return null;
3333
- const data = _preload_cache.get(cache_key);
3334
- if (data !== undefined) {
3335
- _preload_cache.delete(cache_key);
3336
- return data;
3337
3383
  }
3338
- return null;
3339
3384
  }
3340
3385
  /**
3341
- * Clear any remaining unconsumed preload entries.
3342
- * Call after initial page hydration is complete.
3386
+ * Helper to escape HTML for safe output
3343
3387
  */
3344
- function clear_preload_data() {
3345
- _preload_cache.clear();
3388
+ function escape_html(str) {
3389
+ const div = document.createElement('div');
3390
+ div.textContent = str;
3391
+ return div.innerHTML;
3346
3392
  }
3347
3393
  /**
3348
- * Check if any preload data is available (for fast early exit in _load).
3394
+ * Helper to escape HTML and convert newlines to <br /> tags
3395
+ * Used by <%br= %> syntax for displaying user-entered text with line breaks
3349
3396
  */
3350
- function has_preload_data() {
3351
- return _preload_cache.size > 0;
3397
+ function escape_html_nl2br(str) {
3398
+ const div = document.createElement('div');
3399
+ div.textContent = str;
3400
+ // Escape HTML first, then replace newlines with <br />
3401
+ return div.innerHTML.replace(/\n/g, '<br />');
3352
3402
  }
3353
3403
 
3354
3404
  /**
3355
- * JQHTML Viewport Resize Dispatch
3405
+ * Value printers: how <%= %> renders an OBJECT.
3356
3406
  *
3357
- * One framework-owned, debounced window 'resize' listener fans out to every live
3358
- * component in the document by calling on_viewport_resize(viewport_width).
3407
+ * Primitives never come here - the generated template code routes only
3408
+ * non-null, non-array objects to print_object(), so a hundred-row list of
3409
+ * strings costs exactly what it did before. An object reaching an
3410
+ * interpolation is walked through the printer chain in registration order;
3411
+ * the first printer to return anything but undefined handles it. A printer
3412
+ * returns a string (escaped or not according to the construct, exactly as a
3413
+ * string literal at that site) or a component descriptor:
3359
3414
  *
3360
- * Components never bind their own window listener, so there is nothing to unbind
3361
- * in on_stop() - a component that is stopped or removed from the document simply
3362
- * stops being found by the dispatcher.
3363
- */
3364
- /** Trailing-edge debounce applied to the window resize listener. */
3365
- const VIEWPORT_RESIZE_DEBOUNCE_MS = 30;
3366
- let debounce_timer = null;
3367
- let listener_installed = false;
3368
- /**
3369
- * Current viewport width in CSS pixels.
3415
+ * { component: { name: 'Rich_Text_Display', args: { value }, attrs: { class: 'x' } } }
3370
3416
  *
3371
- * window.innerWidth is used (not $(window).width()) because it matches the width
3372
- * CSS media queries resolve against. It includes the scrollbar gutter.
3417
+ * which jqhtml mounts as if the template had written the tag - live in the
3418
+ * browser, server-rendered under SSR - with no environment awareness on the
3419
+ * printer's side. A descriptor carries no content.
3373
3420
  *
3374
- * Returns 0 outside a browser (SSR).
3421
+ * Attribute position (<div title="<%= v %>">) compiles through a different
3422
+ * path and never consults printers.
3375
3423
  */
3376
- function get_viewport_width() {
3377
- return typeof window !== 'undefined' ? window.innerWidth : 0;
3378
- }
3424
+ const printers = [];
3379
3425
  /**
3380
- * Call on_viewport_resize(width) on every component currently in the document.
3381
- *
3382
- * Walks $('.Component') in document order (parents before children). Elements
3383
- * with no component instance and stopped components are skipped. A component
3384
- * that throws is logged and the walk continues - one bad handler must not stop
3385
- * the rest of the page from resizing.
3426
+ * Register a printer at the end of the chain. Printers run in registration
3427
+ * order; return undefined to decline, a string, or a component descriptor.
3386
3428
  */
3387
- function dispatch_viewport_resize(jQuery) {
3388
- const viewport_width = get_viewport_width();
3389
- jQuery('.Component').each(function () {
3390
- const component = jQuery(this).data('_component');
3391
- if (!component || component._stopped)
3392
- return;
3393
- if (typeof component.on_viewport_resize !== 'function')
3394
- return;
3395
- try {
3396
- component.on_viewport_resize(viewport_width);
3397
- }
3398
- catch (error) {
3399
- console.error(`[JQHTML] Error in on_viewport_resize() for ${component.component_name?.() || 'component'}:`, error);
3400
- }
3401
- });
3429
+ function add_object_printer(printer) {
3430
+ if (typeof printer !== 'function') {
3431
+ throw new Error('[JQHTML] add_object_printer() expects a function (value) => undefined | string | descriptor');
3432
+ }
3433
+ printers.push(printer);
3434
+ }
3435
+ /** The registered printers, in order. Read-only view for diagnostics. */
3436
+ function get_object_printers() {
3437
+ return printers;
3402
3438
  }
3403
3439
  /**
3404
- * Install the single window resize listener. Idempotent - repeated init() calls
3405
- * and non-browser environments are no-ops.
3440
+ * Render an object at an interpolation site. Called by compiled templates for
3441
+ * every non-null, non-array object reaching <%= %>, <%!= %> or <%br= %>.
3442
+ * Throws when nothing handles the value or a printer returns garbage.
3406
3443
  */
3407
- function init_viewport_resize(jQuery) {
3408
- if (typeof window === 'undefined')
3409
- return;
3410
- if (listener_installed)
3411
- return;
3412
- listener_installed = true;
3413
- jQuery(window).on('resize.jqhtml_viewport', function () {
3414
- if (debounce_timer !== null)
3415
- clearTimeout(debounce_timer);
3416
- debounce_timer = setTimeout(function () {
3417
- debounce_timer = null;
3418
- dispatch_viewport_resize(jQuery);
3419
- }, VIEWPORT_RESIZE_DEBOUNCE_MS);
3420
- });
3444
+ function print_object(value, mode) {
3445
+ if (value === null || typeof value !== 'object') {
3446
+ throw new TypeError(`[JQHTML] print_object() is the object branch of interpolation; got ${typeof value}`);
3447
+ }
3448
+ for (let i = 0; i < printers.length; i++) {
3449
+ const result = printers[i](value);
3450
+ if (result === undefined)
3451
+ continue;
3452
+ if (typeof result === 'string')
3453
+ return print_string(result, mode);
3454
+ if (is_descriptor(result))
3455
+ return descriptor_to_instruction(result, i);
3456
+ throw new Error(`[JQHTML] Object printer #${i + 1} of ${printers.length} returned ${describe(result)}; ` +
3457
+ `a printer must return undefined (decline), a string, or a { component: { name, args, attrs } } descriptor`);
3458
+ }
3459
+ throw new Error(`[JQHTML] Cannot interpolate a ${constructor_name(value)} object: ` +
3460
+ (printers.length
3461
+ ? `none of the ${printers.length} registered object printer(s) handled it. `
3462
+ : `no object printers are registered. `) +
3463
+ `Register one with jqhtml.add_object_printer(fn), or interpolate a primitive.`);
3464
+ }
3465
+ function print_string(text, mode) {
3466
+ if (mode === 'escape')
3467
+ return escape_html(text);
3468
+ if (mode === 'nl2br')
3469
+ return escape_html_nl2br(text);
3470
+ return text;
3471
+ }
3472
+ function is_descriptor(value) {
3473
+ return !!value && typeof value === 'object' && !Array.isArray(value) &&
3474
+ !!value.component && typeof value.component === 'object' && !Array.isArray(value.component);
3475
+ }
3476
+ function descriptor_to_instruction(descriptor, index) {
3477
+ const { name, args, attrs } = descriptor.component;
3478
+ const where = `Object printer #${index + 1}`;
3479
+ if (typeof name !== 'string' || !is_component_name(name)) {
3480
+ throw new Error(`[JQHTML] ${where} returned a descriptor whose component name ${describe(name)} is invalid: ` +
3481
+ `a component name ${COMPONENT_NAME_RULE}, then letters, digits and underscores`);
3482
+ }
3483
+ const props = {};
3484
+ if (attrs !== undefined) {
3485
+ if (!is_plain_object(attrs))
3486
+ throw new Error(`[JQHTML] ${where}: descriptor attrs must be a plain object`);
3487
+ for (const [key, value] of Object.entries(attrs)) {
3488
+ if (key.startsWith('$'))
3489
+ throw new Error(`[JQHTML] ${where}: descriptor attrs key '${key}' - component arguments go in args, without the $ sigil`);
3490
+ props[key] = value;
3491
+ }
3492
+ }
3493
+ if (args !== undefined) {
3494
+ if (!is_plain_object(args))
3495
+ throw new Error(`[JQHTML] ${where}: descriptor args must be a plain object`);
3496
+ for (const [key, value] of Object.entries(args)) {
3497
+ if (key.startsWith('$'))
3498
+ throw new Error(`[JQHTML] ${where}: descriptor args key '${key}' - the $ sigil is template syntax, not part of the argument name`);
3499
+ props[`$${key}`] = value;
3500
+ }
3501
+ }
3502
+ return { comp: [name, props] };
3503
+ }
3504
+ function is_plain_object(value) {
3505
+ return !!value && typeof value === 'object' && !Array.isArray(value);
3506
+ }
3507
+ function constructor_name(value) {
3508
+ const ctor = value.constructor;
3509
+ return ctor && ctor.name ? ctor.name : 'Object';
3510
+ }
3511
+ function describe(value) {
3512
+ if (value === null)
3513
+ return 'null';
3514
+ if (typeof value === 'string')
3515
+ return `'${value}'`;
3516
+ if (typeof value === 'object')
3517
+ return Array.isArray(value) ? 'an array' : `a ${constructor_name(value)} object without a valid component property`;
3518
+ return `${typeof value} ${String(value)}`;
3421
3519
  }
3422
3520
 
3423
3521
  /**
3424
- * JQHTML v2 Component Base Class
3522
+ * JQHTML Component Lifecycle Queue
3425
3523
  *
3426
- * Core component implementation following v2 specification:
3427
- * - 5-stage lifecycle coordinated by LifecycleManager
3428
- * - Direct jQuery manipulation (no virtual DOM)
3429
- * - Scoped IDs using _cid pattern
3430
- * - Event emission and CSS class hierarchy
3524
+ * Serializes lifecycle operations (render, reload, refresh) per component.
3525
+ * Replaces _create_debounced_function with a proper queue that:
3526
+ *
3527
+ * - At most one operation runs at a time per component
3528
+ * - At most one operation is pending (waiting for the current to finish)
3529
+ * - Same-type pending operations collapse (fan-in: all callers share one execution)
3530
+ * - Different-type pending operations: new replaces old (old callers get the new result)
3531
+ *
3532
+ * Boot bypasses this queue entirely - it runs the lifecycle directly.
3431
3533
  */
3432
- // WeakMap storage for protected lifecycle method implementations (Option 2)
3433
- // See docs/internal/lifecycle-method-protection.md for design details
3434
- const lifecycle_impls = new WeakMap();
3435
- class Jqhtml_Component {
3436
- constructor(element, args = {}) {
3437
- this._ready_state = 0; // 0=created, 1=init, 2=loaded, 3=rendered, 4=ready
3438
- this._instantiator = null; // Component that instantiated this one
3439
- this._dom_parent = null; // Closest component in DOM tree (for lifecycle)
3440
- this._dom_children = new Set(); // Components in DOM subtree (for lifecycle)
3441
- this._use_dom_fallback = false; // If true, use find() fallback instead of _dom_children optimization
3442
- this._stopped = false;
3443
- this._booted = false; // Guard to prevent double boot() calls
3444
- this._data_before_render = null; // Store data state before initial render
3445
- this._lifecycle_callbacks = new Map();
3446
- this._lifecycle_states = new Map(); // Track which lifecycle events have occurred and their data
3447
- this._did_first_render = false; // Track if component has rendered at least once
3448
- this._render_count = 0; // Incremented each time _render() is called, used to detect stale renders
3449
- this._args_on_last_render = null; // Args snapshot from last render (for reload() comparison)
3450
- this._data_on_last_render = null; // Data snapshot from last render (for refresh() comparison)
3451
- this.__initial_data_snapshot = null; // Snapshot of this.data after on_create() for restoration before on_load()
3452
- this.__data_frozen = false; // Track if this.data is currently frozen
3453
- this._queue = new Component_Queue(); // Lifecycle operation queue (render/reload/refresh)
3454
- this.next_reload_force_refresh = null; // State machine for reload()/refresh() debounce precedence
3455
- this.__lifecycle_authorized = false; // Flag for lifecycle method protection
3456
- // Cache mode properties
3457
- this._cache_key = null; // Cache key for caching
3458
- // 'html' mode caching
3459
- this._cached_html = null; // Cached HTML to inject on first render
3460
- this._used_cached_html = false; // Flag if cached HTML was used (forces re-render after on_load)
3461
- this._should_cache_html_after_ready = false; // Flag to cache HTML after on_ready lifecycle
3462
- this._is_dynamic = false; // True if this.data changed during on_load() (used for HTML cache sync)
3463
- // on_render synchronization (HTML cache mode)
3464
- this._on_render_complete = false; // True after on_render() has been called post-on_load
3465
- // use_cached_data feature - skip on_load() when cache hit occurs
3466
- this._use_cached_data_hit = false; // True if use_cached_data=true AND cache was used
3467
- // _load_only: create + load only. No render, no children, no on_render, no on_loaded, no on_ready.
3468
- this._load_only = false;
3469
- // _load_render_only: create + render + load + re-render. No on_render, no on_loaded, no on_ready.
3470
- this._load_render_only = false;
3471
- // Detached optimization: true when element is not in the DOM at boot time.
3472
- // Skips initial render and cache read — just on_load then render.
3473
- // Override with _force_initial_render to keep normal double-render behavior.
3474
- this._is_detached = false;
3475
- // _force_initial_render: override detached optimization — render even when not in DOM.
3476
- // Use when you need the loading spinner visible immediately after appending.
3477
- this._force_initial_render = false;
3478
- // rendered event - fires once after the synchronous render chain completes
3479
- // (after on_load's re-render if applicable, or after first render if no on_load)
3480
- this._has_rendered = false;
3481
- // Sequential queue for setting this.data after on_load completes
3482
- // Multiple _load calls can run on_load in parallel, but setting this.data is serialized
3483
- this._load_queue = Promise.resolve();
3484
- // Cached flag: true if this component has a custom on_load() override
3485
- // Set in constructor (prototype chain is established before constructor runs)
3486
- // Used to skip expensive cache/load operations for static components
3487
- this.__has_custom_on_load = false;
3488
- // gate_load() — "load gates" registered during on_create() that must settle
3489
- // before the component's FIRST on_load() runs. See gate_load() / _await_load_gates().
3490
- this.__load_gates = []; // Pending gates (cleared once the first load begins)
3491
- this.__first_load_started = false; // Latch: true once the first load phase begins (enforces one-shot + throw-after)
3492
- this.__gate_resume = null; // Resolver to release an in-progress gate wait early (reload/refresh/stop)
3493
- // Detect custom on_load() override immediately
3494
- // Prototype chain is set up before constructor runs, so this.on_load
3495
- // correctly resolves to child class override if one exists
3496
- this.__has_custom_on_load = this.on_load !== Jqhtml_Component.prototype.on_load;
3497
- this._cid = this._generate_cid();
3498
- this._lifecycle_manager = LifecycleManager.get_instance();
3499
- // Create or wrap element
3500
- if (element) {
3501
- this.$ = $(element);
3502
- }
3503
- else {
3504
- // Use native createElement for better performance
3505
- const div = document.createElement('div');
3506
- this.$ = $(div);
3507
- }
3508
- // Extract $ attributes from element and merge with args
3509
- // $ attributes are stored via jQuery .data() (not as data-* attributes in DOM)
3510
- const dataAttrs = {};
3511
- // Get all jQuery .data() values from the element
3512
- if (this.$.length > 0) {
3513
- // Get all data via jQuery .data() without arguments (returns all data as object)
3514
- const allData = this.$.data() || {};
3515
- for (const key in allData) {
3516
- // Skip internal attributes
3517
- if (key !== 'cid' && key !== 'tid' && key !== 'componentName' && key !== 'readyState' &&
3518
- key !== '_lifecycleState' && !key.startsWith('_')) {
3519
- dataAttrs[key] = allData[key];
3520
- }
3521
- }
3522
- }
3523
- // Get template to check for defineArgs
3524
- let template_for_args;
3525
- if (args._component_name) {
3526
- template_for_args = get_template(args._component_name);
3527
- }
3528
- else {
3529
- template_for_args = get_template_by_class(this.constructor);
3530
- }
3531
- // Merge in order: defineArgs (defaults from Define tag) < dataAttrs < args (invocation overrides)
3532
- const defineArgs = template_for_args?.defineArgs || {};
3533
- this.args = { ...defineArgs, ...dataAttrs, ...args };
3534
- // Set lifecycle truncation flags from args
3535
- if (this.args._load_only === true) {
3536
- this._load_only = true;
3537
- }
3538
- if (this.args._load_render_only === true) {
3539
- this._load_render_only = true;
3540
- }
3541
- if (this.args._force_initial_render === true) {
3542
- this._force_initial_render = true;
3543
- }
3544
- // Attach component to element
3545
- this.$.data('_component', this);
3546
- // Apply CSS classes and attributes
3547
- this._apply_css_classes();
3548
- this._apply_default_attributes(); // Apply defaultAttributes from template
3549
- this._set_attributes();
3550
- // Find DOM parent component (for lifecycle coordination)
3551
- this._find_dom_parent();
3552
- // Setup data property with freeze enforcement using Proxy
3553
- // @see data-proxy.ts for full implementation
3554
- setup_data_property(this);
3555
- // Initialize this.state as an empty object (convention for arbitrary component state)
3556
- // No special meaning to framework - mutable anywhere, not cached, not frozen
3557
- this.state = {};
3558
- this._log_lifecycle('construct', 'complete');
3534
+ class Component_Queue {
3535
+ constructor() {
3536
+ /** Currently executing operation */
3537
+ this._current = null;
3538
+ /** Next operation waiting to execute after current completes */
3539
+ this._pending = null;
3559
3540
  }
3560
3541
  /**
3561
- * Protect lifecycle methods from manual invocation
3562
- * Stores original implementations in WeakMap, replaces with guarded wrappers
3563
- * @private
3542
+ * Enqueue a lifecycle operation.
3543
+ *
3544
+ * Behavior:
3545
+ * - Nothing running → execute immediately
3546
+ * - Something running, no pending → create pending entry
3547
+ * - Something running, same type pending → collapse (share pending promise)
3548
+ * - Something running, different type pending → replace pending (all callers get new result)
3549
+ *
3550
+ * @param type - Operation type ('render', 'reload', 'refresh', 'load')
3551
+ * @param executor - The async function to execute
3552
+ * @returns Promise that resolves when the operation completes
3564
3553
  */
3565
- _protect_lifecycle_methods() {
3566
- const methods = {
3567
- on_create: 'Called automatically during creation.',
3568
- on_render: 'Use render() to trigger a re-render.',
3569
- on_load: 'Use reload() to refresh data.',
3570
- on_ready: 'Called automatically when ready.',
3571
- on_stop: 'Use stop() to stop the component.'
3572
- };
3573
- const impls = {};
3574
- const self = this;
3575
- for (const [name, help] of Object.entries(methods)) {
3576
- const original = this[name];
3577
- // Skip if using base class default (empty method)
3578
- if (original === Jqhtml_Component.prototype[name])
3579
- continue;
3580
- impls[name] = original;
3581
- // Create wrapper with same function name (for stack traces)
3582
- this[name] = {
3583
- [name](...args) {
3584
- if (!self.__lifecycle_authorized) {
3585
- throw new Error(`[JQHTML] ${name}() cannot be called manually. ${help}\n` +
3586
- `Component: ${self.component_name()} (_cid: ${self._cid})`);
3587
- }
3588
- return lifecycle_impls.get(self)[name].apply(self, args);
3589
- }
3590
- }[name];
3554
+ enqueue(type, executor) {
3555
+ // If nothing running, execute immediately
3556
+ if (!this._current) {
3557
+ return this._execute(type, executor);
3591
3558
  }
3592
- lifecycle_impls.set(this, impls);
3559
+ // Something is running — add to pending
3560
+ if (this._pending) {
3561
+ if (this._pending.type === type) {
3562
+ // Same type pending → collapse (fan-in)
3563
+ // All callers share the same future execution
3564
+ return new Promise((resolve, reject) => {
3565
+ this._pending.resolvers.push(resolve);
3566
+ this._pending.rejecters.push(reject);
3567
+ });
3568
+ }
3569
+ else {
3570
+ // Different type pending → replace
3571
+ // Old pending callers join the new operation
3572
+ // (their operation was superseded by a different type)
3573
+ const old_resolvers = this._pending.resolvers;
3574
+ const old_rejecters = this._pending.rejecters;
3575
+ return new Promise((resolve, reject) => {
3576
+ this._pending = {
3577
+ type,
3578
+ executor,
3579
+ resolvers: [...old_resolvers, resolve],
3580
+ rejecters: [...old_rejecters, reject]
3581
+ };
3582
+ });
3583
+ }
3584
+ }
3585
+ // No pending entry yet — create one
3586
+ return new Promise((resolve, reject) => {
3587
+ this._pending = {
3588
+ type,
3589
+ executor,
3590
+ resolvers: [resolve],
3591
+ rejecters: [reject]
3592
+ };
3593
+ });
3593
3594
  }
3594
3595
  /**
3595
- * Call a lifecycle method with authorization (async)
3596
- * Framework calls this to invoke lifecycle methods, bypassing the protection wrapper.
3597
- * The flag is set momentarily for the wrapper check, then reset BEFORE user code runs.
3598
- * This ensures any nested lifecycle calls from user code will fail the flag check.
3596
+ * Execute an operation and handle pending queue after completion.
3599
3597
  * @private
3600
3598
  */
3601
- async _call_lifecycle(name, context) {
3602
- // Get the original implementation (bypasses wrapper entirely)
3603
- const impl = lifecycle_impls.get(this)?.[name] || this[name];
3604
- // Call original directly - no flag needed since we bypass the wrapper
3605
- return await impl.call(context || this);
3606
- }
3599
+ _execute(type, executor) {
3600
+ const promise = (async () => {
3601
+ try {
3602
+ await executor();
3603
+ }
3604
+ finally {
3605
+ this._current = null;
3606
+ // Run pending if exists
3607
+ if (this._pending) {
3608
+ const pending = this._pending;
3609
+ this._pending = null;
3610
+ try {
3611
+ await this._execute(pending.type, pending.executor);
3612
+ for (const resolve of pending.resolvers)
3613
+ resolve();
3614
+ }
3615
+ catch (err) {
3616
+ for (const reject of pending.rejecters)
3617
+ reject(err);
3618
+ }
3619
+ }
3620
+ }
3621
+ })();
3622
+ this._current = { type, promise };
3623
+ return promise;
3624
+ }
3625
+ /**
3626
+ * Check if any operation is currently running.
3627
+ */
3628
+ get is_busy() {
3629
+ return this._current !== null;
3630
+ }
3631
+ /**
3632
+ * Check if there's a pending operation waiting.
3633
+ */
3634
+ get has_pending() {
3635
+ return this._pending !== null;
3636
+ }
3637
+ }
3638
+
3639
+ /**
3640
+ * JQHTML SSR Preload Data System
3641
+ *
3642
+ * Captures component data during SSR rendering and replays it on the client
3643
+ * to skip redundant on_load() calls during hydration.
3644
+ *
3645
+ * Server-side: start_data_capture() → render → get_captured_data()
3646
+ * Client-side: set_preload_data(entries) → components boot with preloaded data → clear_preload_data()
3647
+ */
3648
+ // --- Capture (Server Side) ---
3649
+ let _capture_enabled = false;
3650
+ let _capture_buffer = [];
3651
+ // Track captured keys to avoid duplicates from Load Coordinator followers
3652
+ let _captured_keys = new Set();
3653
+ /**
3654
+ * Enable data capture mode. After this call, every component that completes
3655
+ * on_load() has its final this.data recorded.
3656
+ *
3657
+ * Idempotent — calling twice is fine.
3658
+ */
3659
+ function start_data_capture() {
3660
+ _capture_enabled = true;
3661
+ _capture_buffer = [];
3662
+ _captured_keys.clear();
3663
+ }
3664
+ /**
3665
+ * Record a component's data after on_load() completes.
3666
+ * Called internally from _apply_load_result().
3667
+ *
3668
+ * @param component_name - Component name
3669
+ * @param args - Component args at time of on_load()
3670
+ * @param data - this.data after on_load() completed
3671
+ * @param cache_key - The cache key for deduplication (null = always capture)
3672
+ */
3673
+ function capture_component_data(component_name, args, data, cache_key) {
3674
+ if (!_capture_enabled)
3675
+ return;
3676
+ // Deduplicate: if Load Coordinator already captured this key, skip
3677
+ if (cache_key !== null) {
3678
+ if (_captured_keys.has(cache_key))
3679
+ return;
3680
+ _captured_keys.add(cache_key);
3681
+ }
3682
+ // Deep clone args and data to plain objects (no proxies, no class instances)
3683
+ try {
3684
+ const cloned_args = JSON.parse(JSON.stringify(args));
3685
+ const cloned_data = JSON.parse(JSON.stringify(data));
3686
+ _capture_buffer.push({
3687
+ component: component_name,
3688
+ args: cloned_args,
3689
+ data: cloned_data,
3690
+ });
3691
+ }
3692
+ catch (error) {
3693
+ // Non-serializable data — skip capture for this component
3694
+ if (typeof console !== 'undefined') {
3695
+ console.warn(`[JQHTML SSR Capture] Failed to serialize data for ${component_name}:`, error);
3696
+ }
3697
+ }
3698
+ }
3699
+ /**
3700
+ * Returns all captured component data and resets the capture buffer.
3701
+ * One-shot: calling clears the buffer.
3702
+ */
3703
+ function get_captured_data() {
3704
+ const result = _capture_buffer;
3705
+ _capture_buffer = [];
3706
+ _captured_keys.clear();
3707
+ return result;
3708
+ }
3709
+ /**
3710
+ * Check if data capture is currently enabled.
3711
+ */
3712
+ function is_capture_enabled() {
3713
+ return _capture_enabled;
3714
+ }
3715
+ /**
3716
+ * Stop data capture and clear all state.
3717
+ */
3718
+ function stop_data_capture() {
3719
+ _capture_enabled = false;
3720
+ _capture_buffer = [];
3721
+ _captured_keys.clear();
3722
+ }
3723
+ // --- Preload (Client Side) ---
3724
+ let _preload_cache = new Map();
3725
+ /**
3726
+ * Seed the preload cache with SSR-captured data.
3727
+ * Must be called before any component _load() runs.
3728
+ *
3729
+ * @param entries - Array of { component, args, data } entries from get_captured_data()
3730
+ */
3731
+ function set_preload_data(entries) {
3732
+ if (!entries || entries.length === 0)
3733
+ return;
3734
+ _preload_cache.clear();
3735
+ for (const entry of entries) {
3736
+ // Generate key using the same algorithm as Load Coordinator
3737
+ const result = Load_Coordinator.generate_invocation_key(entry.component, entry.args);
3738
+ if (result.key !== null) {
3739
+ _preload_cache.set(result.key, entry.data);
3740
+ }
3741
+ }
3742
+ }
3743
+ /**
3744
+ * Check if preloaded data exists for a given cache key.
3745
+ * If found, returns the data and removes the entry (one-shot).
3746
+ *
3747
+ * @param cache_key - The cache key (same format as Load Coordinator)
3748
+ * @returns The preloaded data, or null if no match
3749
+ */
3750
+ function consume_preload_data(cache_key) {
3751
+ if (_preload_cache.size === 0)
3752
+ return null;
3753
+ const data = _preload_cache.get(cache_key);
3754
+ if (data !== undefined) {
3755
+ _preload_cache.delete(cache_key);
3756
+ return data;
3757
+ }
3758
+ return null;
3759
+ }
3760
+ /**
3761
+ * Clear any remaining unconsumed preload entries.
3762
+ * Call after initial page hydration is complete.
3763
+ */
3764
+ function clear_preload_data() {
3765
+ _preload_cache.clear();
3766
+ }
3767
+ /**
3768
+ * Check if any preload data is available (for fast early exit in _load).
3769
+ */
3770
+ function has_preload_data() {
3771
+ return _preload_cache.size > 0;
3772
+ }
3773
+
3774
+ /**
3775
+ * JQHTML Viewport Resize Dispatch
3776
+ *
3777
+ * One framework-owned, debounced window 'resize' listener fans out to every live
3778
+ * component in the document by calling on_viewport_resize(viewport_width).
3779
+ *
3780
+ * Components never bind their own window listener, so there is nothing to unbind
3781
+ * in on_stop() - a component that is stopped or removed from the document simply
3782
+ * stops being found by the dispatcher.
3783
+ */
3784
+ /** Trailing-edge debounce applied to the window resize listener. */
3785
+ const VIEWPORT_RESIZE_DEBOUNCE_MS = 30;
3786
+ let debounce_timer = null;
3787
+ let listener_installed = false;
3788
+ /**
3789
+ * Current viewport width in CSS pixels.
3790
+ *
3791
+ * window.innerWidth is used (not $(window).width()) because it matches the width
3792
+ * CSS media queries resolve against. It includes the scrollbar gutter.
3793
+ *
3794
+ * Returns 0 outside a browser (SSR).
3795
+ */
3796
+ function get_viewport_width() {
3797
+ return typeof window !== 'undefined' ? window.innerWidth : 0;
3798
+ }
3799
+ /**
3800
+ * Call on_viewport_resize(width) on every component currently in the document.
3801
+ *
3802
+ * Walks $('.Component') in document order (parents before children). Elements
3803
+ * with no component instance and stopped components are skipped. A component
3804
+ * that throws is logged and the walk continues - one bad handler must not stop
3805
+ * the rest of the page from resizing.
3806
+ */
3807
+ function dispatch_viewport_resize(jQuery) {
3808
+ const viewport_width = get_viewport_width();
3809
+ jQuery('.Component').each(function () {
3810
+ const component = jQuery(this).data('_component');
3811
+ if (!component || component._stopped)
3812
+ return;
3813
+ if (typeof component.on_viewport_resize !== 'function')
3814
+ return;
3815
+ try {
3816
+ component.on_viewport_resize(viewport_width);
3817
+ }
3818
+ catch (error) {
3819
+ console.error(`[JQHTML] Error in on_viewport_resize() for ${component.component_name?.() || 'component'}:`, error);
3820
+ }
3821
+ });
3822
+ }
3823
+ /**
3824
+ * Install the single window resize listener. Idempotent - repeated init() calls
3825
+ * and non-browser environments are no-ops.
3826
+ */
3827
+ function init_viewport_resize(jQuery) {
3828
+ if (typeof window === 'undefined')
3829
+ return;
3830
+ if (listener_installed)
3831
+ return;
3832
+ listener_installed = true;
3833
+ jQuery(window).on('resize.jqhtml_viewport', function () {
3834
+ if (debounce_timer !== null)
3835
+ clearTimeout(debounce_timer);
3836
+ debounce_timer = setTimeout(function () {
3837
+ debounce_timer = null;
3838
+ dispatch_viewport_resize(jQuery);
3839
+ }, VIEWPORT_RESIZE_DEBOUNCE_MS);
3840
+ });
3841
+ }
3842
+
3843
+ /**
3844
+ * JQHTML v2 Component Base Class
3845
+ *
3846
+ * Core component implementation following v2 specification:
3847
+ * - 5-stage lifecycle coordinated by LifecycleManager
3848
+ * - Direct jQuery manipulation (no virtual DOM)
3849
+ * - Scoped IDs using _cid pattern
3850
+ * - Event emission and CSS class hierarchy
3851
+ */
3852
+ // WeakMap storage for protected lifecycle method implementations (Option 2)
3853
+ // See docs/internal/lifecycle-method-protection.md for design details
3854
+ const lifecycle_impls = new WeakMap();
3855
+ class Jqhtml_Component {
3856
+ constructor(element, args = {}) {
3857
+ this._ready_state = 0; // 0=created, 1=init, 2=loaded, 3=rendered, 4=ready
3858
+ this._instantiator = null; // Component that instantiated this one
3859
+ this._dom_parent = null; // Closest component in DOM tree (for lifecycle)
3860
+ this._dom_children = new Set(); // Components in DOM subtree (for lifecycle)
3861
+ this._use_dom_fallback = false; // If true, use find() fallback instead of _dom_children optimization
3862
+ this._stopped = false;
3863
+ this._booted = false; // Guard to prevent double boot() calls
3864
+ this._data_before_render = null; // Store data state before initial render
3865
+ this._lifecycle_callbacks = new Map();
3866
+ this._lifecycle_states = new Map(); // Track which lifecycle events have occurred and their data
3867
+ this._did_first_render = false; // Track if component has rendered at least once
3868
+ this._render_count = 0; // Incremented each time _render() is called, used to detect stale renders
3869
+ this._args_on_last_render = null; // Args snapshot from last render (for reload() comparison)
3870
+ this._data_on_last_render = null; // Data snapshot from last render (for refresh() comparison)
3871
+ this.__initial_data_snapshot = null; // Snapshot of this.data after on_create() for restoration before on_load()
3872
+ this.__data_frozen = false; // Track if this.data is currently frozen
3873
+ this._queue = new Component_Queue(); // Lifecycle operation queue (render/reload/refresh)
3874
+ this.next_reload_force_refresh = null; // State machine for reload()/refresh() debounce precedence
3875
+ this.__lifecycle_authorized = false; // Flag for lifecycle method protection
3876
+ // Cache mode properties
3877
+ this._cache_key = null; // Cache key for caching
3878
+ // 'html' mode caching
3879
+ this._cached_html = null; // Cached HTML to inject on first render
3880
+ this._used_cached_html = false; // Flag if cached HTML was used (forces re-render after on_load)
3881
+ this._should_cache_html_after_ready = false; // Flag to cache HTML after on_ready lifecycle
3882
+ this._is_dynamic = false; // True if this.data changed during on_load() (used for HTML cache sync)
3883
+ // on_render synchronization (HTML cache mode)
3884
+ this._on_render_complete = false; // True after on_render() has been called post-on_load
3885
+ // use_cached_data feature - skip on_load() when cache hit occurs
3886
+ this._use_cached_data_hit = false; // True if use_cached_data=true AND cache was used
3887
+ // _load_only: create + load only. No render, no children, no on_render, no on_loaded, no on_ready.
3888
+ this._load_only = false;
3889
+ // _load_render_only: create + render + load + re-render. No on_render, no on_loaded, no on_ready.
3890
+ this._load_render_only = false;
3891
+ // Detached optimization: true when element is not in the DOM at boot time.
3892
+ // Skips initial render and cache read — just on_load then render.
3893
+ // Override with _force_initial_render to keep normal double-render behavior.
3894
+ this._is_detached = false;
3895
+ // _force_initial_render: override detached optimization — render even when not in DOM.
3896
+ // Use when you need the loading spinner visible immediately after appending.
3897
+ this._force_initial_render = false;
3898
+ // rendered event - fires once after the synchronous render chain completes
3899
+ // (after on_load's re-render if applicable, or after first render if no on_load)
3900
+ this._has_rendered = false;
3901
+ // Sequential queue for setting this.data after on_load completes
3902
+ // Multiple _load calls can run on_load in parallel, but setting this.data is serialized
3903
+ this._load_queue = Promise.resolve();
3904
+ // Cached flag: true if this component has a custom on_load() override
3905
+ // Set in constructor (prototype chain is established before constructor runs)
3906
+ // Used to skip expensive cache/load operations for static components
3907
+ this.__has_custom_on_load = false;
3908
+ // gate_load() — "load gates" registered during on_create() that must settle
3909
+ // before the component's FIRST on_load() runs. See gate_load() / _await_load_gates().
3910
+ this.__load_gates = []; // Pending gates (cleared once the first load begins)
3911
+ this.__first_load_started = false; // Latch: true once the first load phase begins (enforces one-shot + throw-after)
3912
+ this.__gate_resume = null; // Resolver to release an in-progress gate wait early (reload/refresh/stop)
3913
+ // Detect custom on_load() override immediately
3914
+ // Prototype chain is set up before constructor runs, so this.on_load
3915
+ // correctly resolves to child class override if one exists
3916
+ this.__has_custom_on_load = this.on_load !== Jqhtml_Component.prototype.on_load;
3917
+ this._cid = this._generate_cid();
3918
+ this._lifecycle_manager = LifecycleManager.get_instance();
3919
+ // Create or wrap element
3920
+ if (element) {
3921
+ this.$ = $(element);
3922
+ }
3923
+ else {
3924
+ // Use native createElement for better performance
3925
+ const div = document.createElement('div');
3926
+ this.$ = $(div);
3927
+ }
3928
+ // Extract $ attributes from element and merge with args
3929
+ // $ attributes are stored via jQuery .data() (not as data-* attributes in DOM)
3930
+ const dataAttrs = {};
3931
+ // Get all jQuery .data() values from the element
3932
+ if (this.$.length > 0) {
3933
+ // Get all data via jQuery .data() without arguments (returns all data as object)
3934
+ const allData = this.$.data() || {};
3935
+ for (const key in allData) {
3936
+ // Skip internal attributes
3937
+ if (key !== 'cid' && key !== 'tid' && key !== 'componentName' && key !== 'readyState' &&
3938
+ key !== '_lifecycleState' && !key.startsWith('_')) {
3939
+ dataAttrs[key] = allData[key];
3940
+ }
3941
+ }
3942
+ }
3943
+ // Get template to check for defineArgs
3944
+ let template_for_args;
3945
+ if (args._component_name) {
3946
+ template_for_args = get_template(args._component_name);
3947
+ }
3948
+ else {
3949
+ template_for_args = get_template_by_class(this.constructor);
3950
+ }
3951
+ // Merge in order: defineArgs (defaults from Define tag) < dataAttrs < args (invocation overrides)
3952
+ const defineArgs = template_for_args?.defineArgs || {};
3953
+ this.args = { ...defineArgs, ...dataAttrs, ...args };
3954
+ // The name this instance was invoked as. Every invocation path passes it
3955
+ // in args._component_name (template tag, $(el).component('Name'), boot
3956
+ // placeholder); a class handed directly to $(el).component(Class) has
3957
+ // only its own name to offer.
3958
+ const ctor = this.constructor;
3959
+ this._component_name = (typeof args._component_name === 'string' && args._component_name)
3960
+ || ctor.component_name
3961
+ || ctor.name;
3962
+ // Set lifecycle truncation flags from args
3963
+ if (this.args._load_only === true) {
3964
+ this._load_only = true;
3965
+ }
3966
+ if (this.args._load_render_only === true) {
3967
+ this._load_render_only = true;
3968
+ }
3969
+ if (this.args._force_initial_render === true) {
3970
+ this._force_initial_render = true;
3971
+ }
3972
+ // Attach component to element
3973
+ this.$.data('_component', this);
3974
+ // Apply CSS classes and attributes
3975
+ this._apply_css_classes();
3976
+ this._apply_default_attributes(); // Apply defaultAttributes from template
3977
+ this._set_attributes();
3978
+ // Find DOM parent component (for lifecycle coordination)
3979
+ this._find_dom_parent();
3980
+ // Setup data property with freeze enforcement using Proxy
3981
+ // @see data-proxy.ts for full implementation
3982
+ setup_data_property(this);
3983
+ // Initialize this.state as an empty object (convention for arbitrary component state)
3984
+ // No special meaning to framework - mutable anywhere, not cached, not frozen
3985
+ this.state = {};
3986
+ this._log_lifecycle('construct', 'complete');
3987
+ }
3988
+ /**
3989
+ * Protect lifecycle methods from manual invocation
3990
+ * Stores original implementations in WeakMap, replaces with guarded wrappers
3991
+ * @private
3992
+ */
3993
+ _protect_lifecycle_methods() {
3994
+ const methods = {
3995
+ on_create: 'Called automatically during creation.',
3996
+ on_render: 'Use render() to trigger a re-render.',
3997
+ on_load: 'Use reload() to refresh data.',
3998
+ on_ready: 'Called automatically when ready.',
3999
+ on_stop: 'Use stop() to stop the component.'
4000
+ };
4001
+ const impls = {};
4002
+ const self = this;
4003
+ for (const [name, help] of Object.entries(methods)) {
4004
+ const original = this[name];
4005
+ // Skip if using base class default (empty method)
4006
+ if (original === Jqhtml_Component.prototype[name])
4007
+ continue;
4008
+ impls[name] = original;
4009
+ // Create wrapper with same function name (for stack traces)
4010
+ this[name] = {
4011
+ [name](...args) {
4012
+ if (!self.__lifecycle_authorized) {
4013
+ throw new Error(`[JQHTML] ${name}() cannot be called manually. ${help}\n` +
4014
+ `Component: ${self.component_name()} (_cid: ${self._cid})`);
4015
+ }
4016
+ return lifecycle_impls.get(self)[name].apply(self, args);
4017
+ }
4018
+ }[name];
4019
+ }
4020
+ lifecycle_impls.set(this, impls);
4021
+ }
4022
+ /**
4023
+ * Call a lifecycle method with authorization (async)
4024
+ * Framework calls this to invoke lifecycle methods, bypassing the protection wrapper.
4025
+ * The flag is set momentarily for the wrapper check, then reset BEFORE user code runs.
4026
+ * This ensures any nested lifecycle calls from user code will fail the flag check.
4027
+ * @private
4028
+ */
4029
+ async _call_lifecycle(name, context) {
4030
+ // Get the original implementation (bypasses wrapper entirely)
4031
+ const impl = lifecycle_impls.get(this)?.[name] || this[name];
4032
+ // Call original directly - no flag needed since we bypass the wrapper
4033
+ return await impl.call(context || this);
4034
+ }
3607
4035
  /**
3608
4036
  * Call a lifecycle method with authorization (sync version for sync methods like on_stop)
3609
4037
  * @private
@@ -3783,7 +4211,11 @@ class Jqhtml_Component {
3783
4211
  div.textContent = String(str);
3784
4212
  // Escape HTML first, then replace newlines with <br />
3785
4213
  return div.innerHTML.replace(/\n/g, '<br />');
3786
- }
4214
+ },
4215
+ // Objects reaching <%= %> / <%!= %> / <%br= %> - see value-printers.ts
4216
+ print_object,
4217
+ // <{expression}> name validation - see component-name.ts
4218
+ dynamic_component_name
3787
4219
  };
3788
4220
  // Execute template function
3789
4221
  // Commented out debug logging
@@ -4785,771 +5217,919 @@ class Jqhtml_Component {
4785
5217
  if (stopResult && typeof stopResult.then === 'function') {
4786
5218
  console.warn(`[JQHTML] Component "${this.component_name()}" returned a Promise from on_stop(). ` +
4787
5219
  `on_stop() must be synchronous code. Remove 'async' from the function declaration.`);
4788
- }
4789
- // Fire registered stop callbacks
4790
- this.trigger('stop');
4791
- // Remove from DOM parent's children
4792
- if (this._dom_parent) {
4793
- this._dom_parent._dom_children.delete(this);
4794
- }
4795
- this._ready_state = 99;
4796
- this._update_debug_attrs();
4797
- this._log_lifecycle('destroy', 'complete');
4798
- }
4799
- /**
4800
- * Stop component lifecycle - stops all descendant components then self
4801
- * Leaves DOM intact, just stops lifecycle engine and fires cleanup hooks
4802
- */
4803
- stop() {
4804
- // Stop all descendant components (flat iteration, no recursion needed)
4805
- this.$.find('.Component').each(function () {
4806
- const child = $(this).data('_component');
4807
- if (child && !child._stopped) {
4808
- child._stop(); // Not recursive - we already have all descendants
4809
- }
4810
- });
4811
- // Then stop self
4812
- this._stop();
4813
- }
4814
- // -------------------------------------------------------------------------
4815
- // Overridable Lifecycle Hooks
4816
- // -------------------------------------------------------------------------
4817
- on_render() { }
4818
- on_create() { }
4819
- on_load() { } // Override to fetch data asynchronously
4820
- on_loaded() { } // Override to process loaded data (e.g., clone this.data to this.state)
4821
- async on_ready() { }
4822
- on_stop() { }
4823
- /**
4824
- * Called with the viewport width in CSS pixels:
4825
- * - after every on_render()
4826
- * - after every on_ready()
4827
- * - on window resize, debounced 30ms, for every component in the document
4828
- *
4829
- * MUST be synchronous. Prefer CSS media/container queries for pure styling -
4830
- * override this only when layout requires imperative measurement.
4831
- */
4832
- on_viewport_resize(viewport_width) { }
4833
- /**
4834
- * Should component re-render after load?
4835
- * By default, only re-renders if data has changed
4836
- * Override to control re-rendering behavior
4837
- */
4838
- /**
4839
- * Internal method to determine if component should re-render after on_load()
4840
- * @private
4841
- */
4842
- _should_rerender() {
4843
- // HTML CACHE MODE - If we used cached HTML, always re-render to get live components
4844
- if (this._used_cached_html) {
4845
- if (window.jqhtml?.debug?.verbose) {
4846
- console.log(`[Cache html] Component ${this._cid} (${this.component_name()}) forcing re-render after cached HTML`);
4847
- }
4848
- // Clear the flag
4849
- this._used_cached_html = false;
4850
- return true;
4851
- }
4852
- // OPTIMIZATION: If no custom on_load(), data cannot change, skip serialization
4853
- // Data is frozen after on_create() and only unfrozen during on_load()
4854
- if (!this.__has_custom_on_load) {
4855
- return false;
4856
- }
4857
- // Compare current data state with data state before initial render
4858
- const currentDataState = JSON.stringify(this.data);
4859
- const dataChanged = this._data_before_render !== currentDataState;
4860
- // Update stored state for next comparison
4861
- if (dataChanged) {
4862
- this._data_before_render = currentDataState;
4863
- }
4864
- return dataChanged;
4865
- }
4866
- // -------------------------------------------------------------------------
4867
- // Public API
4868
- // -------------------------------------------------------------------------
4869
- /**
4870
- * Get component name for debugging
4871
- */
4872
- component_name() {
4873
- return this.constructor.name;
4874
- }
4875
- /**
4876
- * Register event callback - delegates to component-events.ts
4877
- * @see component-events.ts for full documentation
4878
- */
4879
- on(event_name, callback) {
4880
- return event_on(this, event_name, callback);
4881
- }
4882
- /**
4883
- * Register a callback that fires exactly once - delegates to component-events.ts
4884
- * If the event already occurred, fires immediately and does not register.
4885
- * @see component-events.ts for full documentation
4886
- */
4887
- once(event_name, callback) {
4888
- return event_once(this, event_name, callback);
4889
- }
4890
- /**
4891
- * Trigger a lifecycle event - delegates to component-events.ts
4892
- * @see component-events.ts for full documentation
4893
- */
4894
- trigger(event_name, data) {
4895
- event_trigger(this, event_name, data);
4896
- }
4897
- /**
4898
- * Check if any callbacks are registered for a given event
4899
- */
4900
- _on_registered(event_name) {
4901
- return event_on_registered(this, event_name);
4902
- }
4903
- /**
4904
- * Invalidate a lifecycle event - delegates to component-events.ts
4905
- * @see component-events.ts for full documentation
4906
- */
4907
- invalidate(event_name) {
4908
- event_invalidate(this, event_name);
4909
- }
4910
- /**
4911
- * Find element by scoped ID
4912
- *
4913
- * Searches for elements with id="local_id:THIS_COMPONENT_CID"
4914
- *
4915
- * Example:
4916
- * Template: <button $sid="save_btn">Save</button>
4917
- * Rendered: <button id="save_btn:abc123" data-sid="save_btn">Save</button>
4918
- * Access: this.$sid('save_btn') // Returns jQuery element
4919
- *
4920
- * Performance: Uses native document.getElementById() when component is in DOM,
4921
- * falls back to jQuery.find() for components not yet attached to DOM.
4922
- *
4923
- * @param local_id The local ID (without _cid suffix)
4924
- * @returns jQuery element with id="local_id:_cid", or empty jQuery object if not found
4925
- */
4926
- $sid(local_id) {
4927
- const scopedId = `${local_id}:${this._cid}`;
4928
- // Try getElementById first (fast path - works when component is in DOM)
4929
- const el = document.getElementById(scopedId);
4930
- if (el) {
4931
- return $(el);
4932
- }
4933
- // Fallback: component not in DOM yet, search within component subtree
4934
- // This allows $sid() to work on components before they're appended to body
4935
- // Must escape the ID because it contains ':' which jQuery treats as a pseudo-selector
4936
- return this.$.find(`#${$.escapeSelector(scopedId)}`);
4937
- }
4938
- /**
4939
- * Get component instance by scoped ID
4940
- *
4941
- * Convenience method that finds element by scoped ID and returns the component instance.
4942
- *
4943
- * Example:
4944
- * Template: <User_Card $sid="active_user" />
4945
- * Access: const user = this.sid('active_user'); // Returns User_Card instance
4946
- * user.data.name // Access component's data
4947
- *
4948
- * To get the scoped ID string itself:
4949
- * this.$sid('active_user').attr('id') // Returns "active_user:abc123xyz"
4950
- *
4951
- * @param local_id The local ID (without _cid suffix)
4952
- * @returns Component instance or null if not found or not a component
4953
- */
4954
- sid(local_id) {
4955
- const element = this.$sid(local_id);
4956
- const component = element.data('_component');
4957
- // If no component found but element exists, warn developer
4958
- if (!component && element.length > 0) {
4959
- console.warn(`Component ${this.constructor.name} tried to call .sid('${local_id}') - ` +
4960
- `${local_id} exists, however, it is not a component or $redrawable. ` +
4961
- `Did you forget to add $redrawable to the tag?`);
4962
- }
4963
- return component || null;
4964
- }
4965
- /**
4966
- * Get the component that instantiated this component (rendered it in their template)
4967
- * Returns null if component was created programmatically via $().component()
4968
- */
4969
- instantiator() {
4970
- return this._instantiator;
5220
+ }
5221
+ // Fire registered stop callbacks
5222
+ this.trigger('stop');
5223
+ // Remove from DOM parent's children
5224
+ if (this._dom_parent) {
5225
+ this._dom_parent._dom_children.delete(this);
5226
+ }
5227
+ this._ready_state = 99;
5228
+ this._update_debug_attrs();
5229
+ this._log_lifecycle('destroy', 'complete');
4971
5230
  }
4972
5231
  /**
4973
- * Find descendant components by CSS selector
5232
+ * Stop component lifecycle - stops all descendant components then self
5233
+ * Leaves DOM intact, just stops lifecycle engine and fires cleanup hooks
4974
5234
  */
4975
- find(selector) {
4976
- const components = [];
4977
- this.$.find(selector).each((_, el) => {
4978
- const comp = $(el).data('_component');
4979
- if (comp instanceof Jqhtml_Component) {
4980
- components.push(comp);
5235
+ stop() {
5236
+ // Stop all descendant components (flat iteration, no recursion needed)
5237
+ this.$.find('.Component').each(function () {
5238
+ const child = $(this).data('_component');
5239
+ if (child && !child._stopped) {
5240
+ child._stop(); // Not recursive - we already have all descendants
4981
5241
  }
4982
5242
  });
4983
- return components;
5243
+ // Then stop self
5244
+ this._stop();
4984
5245
  }
5246
+ // -------------------------------------------------------------------------
5247
+ // Overridable Lifecycle Hooks
5248
+ // -------------------------------------------------------------------------
5249
+ on_render() { }
5250
+ on_create() { }
5251
+ on_load() { } // Override to fetch data asynchronously
5252
+ on_loaded() { } // Override to process loaded data (e.g., clone this.data to this.state)
5253
+ async on_ready() { }
5254
+ on_stop() { }
4985
5255
  /**
4986
- * Find closest ancestor component matching selector
5256
+ * Called with the viewport width in CSS pixels:
5257
+ * - after every on_render()
5258
+ * - after every on_ready()
5259
+ * - on window resize, debounced 30ms, for every component in the document
4987
5260
  *
4988
- * A bare component name - a plain identifier starting with a capital letter,
4989
- * such as 'Parent_Dashboard' - is treated as the component-name class
4990
- * selector '.Parent_Dashboard'. Components carry a CSS class for every name
4991
- * in their prototype chain, so a base class name matches its subclasses too.
4992
- * Every other selector ('.Foo', '#id', '[attr]', 'div > .Component') is
4993
- * passed to jQuery unchanged.
5261
+ * MUST be synchronous. Prefer CSS media/container queries for pure styling -
5262
+ * override this only when layout requires imperative measurement.
4994
5263
  */
4995
- closest(selector) {
4996
- const search = Jqhtml_Component.COMPONENT_NAME_PATTERN.test(selector)
4997
- ? `.${selector}`
4998
- : selector;
4999
- let current = this.$.parent();
5000
- while (current.length > 0) {
5001
- if (current.is(search)) {
5002
- const comp = current.data('_component');
5003
- if (comp instanceof Jqhtml_Component) {
5004
- return comp;
5005
- }
5006
- }
5007
- current = current.parent();
5008
- }
5009
- return null;
5010
- }
5011
- // -------------------------------------------------------------------------
5012
- // Static Methods
5013
- // -------------------------------------------------------------------------
5264
+ on_viewport_resize(viewport_width) { }
5014
5265
  /**
5015
- * Get CSS class hierarchy for this component type
5266
+ * Should component re-render after load?
5267
+ * By default, only re-renders if data has changed
5268
+ * Override to control re-rendering behavior
5016
5269
  */
5017
- static get_class_hierarchy() {
5018
- // v2.2.13 - Fixed to handle undefined values in prototype chain
5019
- const classes = [];
5020
- let ctor = this;
5021
- while (ctor) {
5022
- // Check if constructor has a valid name property
5023
- if (!ctor.name || typeof ctor.name !== 'string') {
5024
- // console.warn('[JQHTML v2.2.13] Invalid constructor name in hierarchy:', ctor);
5025
- break;
5026
- }
5027
- // Only add valid class names (non-empty strings, not 'Object')
5028
- if (ctor.name !== 'Object' && ctor.name !== '') {
5029
- // Normalize base class names - handle different import patterns
5030
- let normalizedName = ctor.name;
5031
- if (normalizedName === '_Jqhtml_Component' || normalizedName === '_Base_Jqhtml_Component') {
5032
- normalizedName = 'Component'; // Use 'Component' instead of 'Jqhtml_Component' for CSS class
5033
- }
5034
- else if (normalizedName === 'Jqhtml_Component') {
5035
- normalizedName = 'Component'; // Use 'Component' instead of 'Jqhtml_Component' for CSS class
5036
- }
5037
- classes.push(normalizedName);
5038
- }
5039
- // Get the next prototype
5040
- const nextProto = Object.getPrototypeOf(ctor);
5041
- // Stop if we've reached the end of the chain
5042
- if (!nextProto || nextProto === Object.prototype || nextProto.constructor === Object) {
5043
- break;
5044
- }
5045
- ctor = nextProto;
5046
- }
5047
- return classes;
5048
- }
5049
- // -------------------------------------------------------------------------
5050
- // Private Implementation
5051
- // -------------------------------------------------------------------------
5052
- _generate_cid() {
5053
- return uid();
5054
- }
5055
- _apply_css_classes() {
5056
- const hierarchy = this.constructor.get_class_hierarchy();
5057
- // If component name differs from class name, add component name to hierarchy
5058
- // This happens when:
5059
- // 1. Using base Jqhtml_Component with _component_name
5060
- // 2. Using parent class via extends chain (e.g., Contacts_DataGrid using DataGrid_Abstract class)
5061
- const classesToAdd = [...hierarchy];
5062
- if (this.args._component_name && this.args._component_name !== this.constructor.name) {
5063
- // Add component name at the beginning (most specific)
5064
- classesToAdd.unshift(this.args._component_name);
5065
- }
5066
- // Filter out private classes (starting with _) and invalid class names
5067
- const publicClasses = classesToAdd.filter(className => {
5068
- // Guard against undefined, null, or non-string values
5069
- if (!className || typeof className !== 'string') {
5070
- console.warn('[JQHTML] Filtered out invalid class name:', className);
5071
- return false;
5072
- }
5073
- return !className.startsWith('_');
5074
- });
5075
- if (publicClasses.length > 0) {
5076
- this.$.addClass(publicClasses.join(' '));
5077
- }
5078
- }
5079
- _apply_default_attributes() {
5080
- // Get template using same logic as render() method
5081
- let template;
5082
- // If we have a component name, try looking up by name first (for unregistered components)
5083
- if (this.args._component_name) {
5084
- template = get_template(this.args._component_name);
5085
- }
5086
- else {
5087
- // Otherwise look up by class
5088
- template = get_template_by_class(this.constructor);
5089
- }
5090
- if (!template)
5091
- return;
5092
- // Walk the extends chain to collect all defaultAttributes from parent templates
5093
- // Apply from parent to child so child attributes take precedence
5094
- const templateChain = [];
5095
- let currentTemplate = template;
5096
- // Build chain from child to parent
5097
- while (currentTemplate) {
5098
- templateChain.unshift(currentTemplate); // Add to front so parent comes first
5099
- // Check if this template extends another
5100
- if (currentTemplate.extends) {
5101
- try {
5102
- currentTemplate = get_template(currentTemplate.extends);
5103
- }
5104
- catch (error) {
5105
- // Parent template not found, stop chain
5106
- break;
5107
- }
5108
- }
5109
- else {
5110
- break;
5111
- }
5112
- }
5113
- // Apply defaultAttributes from each template in the chain (parent first, child last)
5114
- for (const tmpl of templateChain) {
5115
- if (!tmpl.defaultAttributes)
5116
- continue;
5117
- // Filter out 'tag' since it determines tag name, not an attribute
5118
- const defineAttrs = { ...tmpl.defaultAttributes };
5119
- delete defineAttrs.tag;
5120
- // Debug logging
5121
- if (window.jqhtml?.debug?.enabled) {
5122
- const componentName = tmpl.name || this.args._component_name || this.constructor.name;
5123
- console.log(`[Component] Applying defaultAttributes for ${componentName}:`, defineAttrs);
5124
- }
5125
- // Apply each default attribute
5126
- for (const [key, value] of Object.entries(defineAttrs)) {
5127
- if (key === 'class') {
5128
- // Special handling for class - merge with existing
5129
- const existingClasses = this.$.attr('class');
5130
- if (existingClasses) {
5131
- const existing = existingClasses.split(/\s+/).filter(c => c);
5132
- const newClasses = String(value).split(/\s+/).filter(c => c);
5133
- for (const newClass of newClasses) {
5134
- if (!existing.includes(newClass)) {
5135
- existing.push(newClass);
5136
- }
5137
- }
5138
- this.$.attr('class', existing.join(' '));
5139
- }
5140
- else {
5141
- this.$.attr('class', value);
5142
- }
5143
- }
5144
- else if (key === 'style') {
5145
- // Special handling for style - merge with existing
5146
- // Define attributes are applied AFTER invocation attributes
5147
- // So we only add Define style properties that don't already exist
5148
- // This ensures invocation wins conflicts (invocation overrides Define)
5149
- const existingStyle = this.$.attr('style');
5150
- if (existingStyle) {
5151
- // Parse existing styles (from invocation)
5152
- const existingRules = new Map();
5153
- existingStyle.split(';').forEach(rule => {
5154
- const [prop, val] = rule.split(':').map(s => s.trim());
5155
- if (prop && val)
5156
- existingRules.set(prop, val);
5157
- });
5158
- // Parse Define styles and only add if not already present
5159
- String(value).split(';').forEach(rule => {
5160
- const [prop, val] = rule.split(':').map(s => s.trim());
5161
- if (prop && val && !existingRules.has(prop)) {
5162
- // Only add if property doesn't exist (invocation wins)
5163
- existingRules.set(prop, val);
5164
- }
5165
- });
5166
- // Build merged style string
5167
- const merged = Array.from(existingRules.entries())
5168
- .map(([prop, val]) => `${prop}: ${val}`)
5169
- .join('; ');
5170
- this.$.attr('style', merged);
5171
- }
5172
- else {
5173
- this.$.attr('style', value);
5174
- }
5175
- }
5176
- else if (key.startsWith('$') || key.startsWith('data-')) {
5177
- // Data attributes - also update args
5178
- const dataKey = key.startsWith('$') ? key.substring(1) :
5179
- key.startsWith('data-') ? key.substring(5) : key;
5180
- // Only apply if not already set in args
5181
- if (!(dataKey in this.args)) {
5182
- this.args[dataKey] = value;
5183
- this.$.data(dataKey, value);
5184
- this.$.attr(key.startsWith('$') ? `data-${dataKey}` : key, String(value));
5185
- }
5186
- }
5187
- else {
5188
- // Regular attributes - apply directly if not already set
5189
- if (!this.$.attr(key)) {
5190
- this.$.attr(key, value);
5191
- }
5192
- }
5270
+ /**
5271
+ * Internal method to determine if component should re-render after on_load()
5272
+ * @private
5273
+ */
5274
+ _should_rerender() {
5275
+ // HTML CACHE MODE - If we used cached HTML, always re-render to get live components
5276
+ if (this._used_cached_html) {
5277
+ if (window.jqhtml?.debug?.verbose) {
5278
+ console.log(`[Cache html] Component ${this._cid} (${this.component_name()}) forcing re-render after cached HTML`);
5193
5279
  }
5280
+ // Clear the flag
5281
+ this._used_cached_html = false;
5282
+ return true;
5194
5283
  }
5195
- }
5196
- _set_attributes() {
5197
- // data-cid on the rendered element is debug-only: it mirrors this._cid so component
5198
- // boundaries are legible in DevTools. The runtime never reads it - scoping uses the
5199
- // _cid property, and the transient data-cid placeholder the instruction processor
5200
- // matches on is written and removed before this ever runs.
5201
- if (debug_attributes_enabled()) {
5202
- this.$.attr('data-cid', this._cid);
5284
+ // OPTIMIZATION: If no custom on_load(), data cannot change, skip serialization
5285
+ // Data is frozen after on_create() and only unfrozen during on_load()
5286
+ if (!this.__has_custom_on_load) {
5287
+ return false;
5203
5288
  }
5204
- // Only set lifecycle state attribute if verbose mode enabled
5205
- if (window.jqhtml?.debug?.verbose) {
5206
- this.$.attr('data-_lifecycle-state', this._ready_state.toString());
5289
+ // Compare current data state with data state before initial render
5290
+ const currentDataState = JSON.stringify(this.data);
5291
+ const dataChanged = this._data_before_render !== currentDataState;
5292
+ // Update stored state for next comparison
5293
+ if (dataChanged) {
5294
+ this._data_before_render = currentDataState;
5207
5295
  }
5296
+ return dataChanged;
5208
5297
  }
5209
- _update_debug_attrs() {
5210
- // Only update lifecycle state attribute if verbose mode enabled
5211
- if (window.jqhtml?.debug?.verbose) {
5212
- this.$.attr('data-_lifecycle-state', this._ready_state.toString());
5213
- }
5298
+ // -------------------------------------------------------------------------
5299
+ // Public API
5300
+ // -------------------------------------------------------------------------
5301
+ /**
5302
+ * Get component name for debugging
5303
+ */
5304
+ /**
5305
+ * The name this component was invoked as - the tag written in the template
5306
+ * (<User_Card> -> 'User_Card'), the name given to $(el).component('User_Card'),
5307
+ * or the boot placeholder's data-component-init-name. For a template-only
5308
+ * component this is the template name, not 'Jqhtml_Component'. Falls back to
5309
+ * the class's static component_name, then its JS class name.
5310
+ */
5311
+ component_name() {
5312
+ return this._component_name;
5214
5313
  }
5215
- _find_dom_parent() {
5216
- let current = this.$.parent();
5217
- while (current.length > 0) {
5218
- const parent = current.data('_component');
5219
- if (parent instanceof Jqhtml_Component) {
5220
- this._dom_parent = parent;
5221
- parent._dom_children.add(this);
5222
- break;
5223
- }
5224
- current = current.parent();
5225
- }
5314
+ /**
5315
+ * Register event callback - delegates to component-events.ts
5316
+ * @see component-events.ts for full documentation
5317
+ */
5318
+ on(event_name, callback) {
5319
+ return event_on(this, event_name, callback);
5226
5320
  }
5227
5321
  /**
5228
- * Get DOM children (components in DOM subtree)
5229
- * Uses fast _dom_children registry when possible, falls back to DOM traversal for off-DOM components
5230
- * @private - Used internally for lifecycle coordination
5322
+ * Register a callback that fires exactly once - delegates to component-events.ts
5323
+ * If the event already occurred, fires immediately and does not register.
5324
+ * @see component-events.ts for full documentation
5231
5325
  */
5232
- _get_dom_children() {
5233
- // If component was off-DOM during render, children couldn't register via _find_dom_parent()
5234
- // Use DOM traversal to find direct children (works off-DOM, slightly slower)
5235
- if (this._use_dom_fallback) {
5236
- const directChildren = [];
5237
- this.$.find('.Component').each((_, el) => {
5238
- const $el = $(el);
5239
- const comp = $el.data('_component');
5240
- if (comp instanceof Jqhtml_Component) {
5241
- // Only include if this component is the direct parent (not a grandparent)
5242
- // Check: closest parent component is us (or no parent = we're the root)
5243
- const closestParent = $el.parent().closest('.Component');
5244
- if (closestParent.length === 0 || closestParent.data('_component') === this) {
5245
- directChildren.push(comp);
5246
- }
5247
- }
5248
- });
5249
- return directChildren;
5250
- }
5251
- // Fast path: component was in-DOM during render, children registered normally
5252
- // Filter out any that have since been removed from DOM (destroyed/replaced)
5253
- const children = Array.from(this._dom_children);
5254
- return children.filter(child => {
5255
- return $.contains(document.documentElement, child.$[0]);
5256
- });
5326
+ once(event_name, callback) {
5327
+ return event_once(this, event_name, callback);
5257
5328
  }
5258
- _log_lifecycle(phase, status) {
5259
- // Use new debug module
5260
- logLifecycle(this, phase, status);
5261
- // Keep legacy support for window.JQHTML_DEBUG
5262
- if (typeof window !== 'undefined' && window.JQHTML_DEBUG) {
5263
- window.JQHTML_DEBUG.log(this.component_name(), phase, status, {
5264
- cid: this._cid,
5265
- ready_state: this._ready_state,
5266
- args: this.args
5267
- });
5268
- }
5329
+ /**
5330
+ * Trigger a lifecycle event - delegates to component-events.ts
5331
+ * @see component-events.ts for full documentation
5332
+ */
5333
+ trigger(event_name, data) {
5334
+ event_trigger(this, event_name, data);
5269
5335
  }
5270
- _log_debug(action, ...args) {
5271
- if (typeof window !== 'undefined' && window.JQHTML_DEBUG) {
5272
- window.JQHTML_DEBUG.log(this.component_name(), 'debug', `${action}: ${args.map(a => JSON.stringify(a)).join(', ')}`);
5336
+ /**
5337
+ * Check if any callbacks are registered for a given event
5338
+ */
5339
+ _on_registered(event_name) {
5340
+ return event_on_registered(this, event_name);
5341
+ }
5342
+ /**
5343
+ * Invalidate a lifecycle event - delegates to component-events.ts
5344
+ * @see component-events.ts for full documentation
5345
+ */
5346
+ invalidate(event_name) {
5347
+ event_invalidate(this, event_name);
5348
+ }
5349
+ /**
5350
+ * Find element by scoped ID
5351
+ *
5352
+ * Searches for elements with id="local_id:THIS_COMPONENT_CID"
5353
+ *
5354
+ * Example:
5355
+ * Template: <button $sid="save_btn">Save</button>
5356
+ * Rendered: <button id="save_btn:abc123" data-sid="save_btn">Save</button>
5357
+ * Access: this.$sid('save_btn') // Returns jQuery element
5358
+ *
5359
+ * Performance: Uses native document.getElementById() when component is in DOM,
5360
+ * falls back to jQuery.find() for components not yet attached to DOM.
5361
+ *
5362
+ * @param local_id The local ID (without _cid suffix)
5363
+ * @returns jQuery element with id="local_id:_cid", or empty jQuery object if not found
5364
+ */
5365
+ $sid(local_id) {
5366
+ const scopedId = `${local_id}:${this._cid}`;
5367
+ // Try getElementById first (fast path - works when component is in DOM)
5368
+ const el = document.getElementById(scopedId);
5369
+ if (el) {
5370
+ return $(el);
5273
5371
  }
5372
+ // Fallback: component not in DOM yet, search within component subtree
5373
+ // This allows $sid() to work on components before they're appended to body
5374
+ // Must escape the ID because it contains ':' which jQuery treats as a pseudo-selector
5375
+ return this.$.find(`#${$.escapeSelector(scopedId)}`);
5274
5376
  }
5275
- }
5276
- // Static properties
5277
- Jqhtml_Component.__jqhtml_component = true; // Marker for unified register() detection
5278
- // A bare component name, as accepted by closest() in place of a class selector
5279
- Jqhtml_Component.COMPONENT_NAME_PATTERN = /^[A-Z][A-Za-z0-9_]*$/;
5280
-
5281
- /**
5282
- * JQHTML v2 Template Renderer
5283
- *
5284
- * Connects compiled templates to components
5285
- * Processes instruction arrays and handles bindings
5286
- */
5287
- /**
5288
- * Process slot-only template inheritance
5289
- *
5290
- * When a component's template returns {_slots: {...}}, walk the prototype
5291
- * chain to find a parent template and invoke it with the child's slots as content.
5292
- *
5293
- * @param component The component instance
5294
- * @param childSlots The slots object from the child template
5295
- * @returns [instructions, context] tuple or null if no parent found
5296
- */
5297
- async function process_slot_inheritance(component, childSlots) {
5298
- // Walk the prototype chain to find parent class with template
5299
- let currentClass = Object.getPrototypeOf(component.constructor);
5300
- console.log(`[JQHTML] Walking prototype chain for ${component.constructor.name}`);
5301
- while (currentClass && currentClass !== Jqhtml_Component && currentClass.name !== 'Object') {
5302
- const className = currentClass.name;
5303
- console.log(`[JQHTML] Checking parent class: ${className}`);
5304
- // Skip internal framework classes
5305
- if (className === '_Jqhtml_Component' || className === '_Base_Jqhtml_Component') {
5306
- currentClass = Object.getPrototypeOf(currentClass);
5307
- continue;
5377
+ /**
5378
+ * Get component instance by scoped ID
5379
+ *
5380
+ * Convenience method that finds element by scoped ID and returns the component instance.
5381
+ *
5382
+ * Example:
5383
+ * Template: <User_Card $sid="active_user" />
5384
+ * Access: const user = this.sid('active_user'); // Returns User_Card instance
5385
+ * user.data.name // Access component's data
5386
+ *
5387
+ * To get the scoped ID string itself:
5388
+ * this.$sid('active_user').attr('id') // Returns "active_user:abc123xyz"
5389
+ *
5390
+ * @param local_id The local ID (without _cid suffix)
5391
+ * @returns Component instance or null if not found or not a component
5392
+ */
5393
+ sid(local_id) {
5394
+ const element = this.$sid(local_id);
5395
+ const component = element.data('_component');
5396
+ // If no component found but element exists, warn developer
5397
+ if (!component && element.length > 0) {
5398
+ console.warn(`Component ${this.constructor.name} tried to call .sid('${local_id}') - ` +
5399
+ `${local_id} exists, however, it is not a component or $redrawable. ` +
5400
+ `Did you forget to add $redrawable to the tag?`);
5308
5401
  }
5309
- // Try to find template for this parent class
5310
- try {
5311
- const parentTemplate = get_template(className);
5312
- console.log(`[JQHTML] Template found for ${className}:`, parentTemplate ? parentTemplate.name : 'null');
5313
- // Check if we got the default template (means no real template found)
5314
- if (parentTemplate && parentTemplate.name !== 'Jqhtml_Component') {
5315
- console.log(`[JQHTML] Invoking parent template ${className}`);
5316
- // Found a real parent template - invoke it with child's slots as content
5317
- const [parentInstructions, parentContext] = parentTemplate.render.call(component, component.data, component.args, childSlots // Pass child slots as content parameter
5318
- );
5319
- // Check if parent also returns slots (recursive inheritance)
5320
- if (parentInstructions && typeof parentInstructions === 'object' && parentInstructions._slots) {
5321
- // Parent also has slot-only template - recurse
5322
- console.log(`[JQHTML] Parent also slot-only, recursing`);
5323
- return await process_slot_inheritance(component, parentInstructions._slots);
5402
+ return component || null;
5403
+ }
5404
+ /**
5405
+ * Get the component that instantiated this component (rendered it in their template)
5406
+ * Returns null if component was created programmatically via $().component()
5407
+ */
5408
+ instantiator() {
5409
+ return this._instantiator;
5410
+ }
5411
+ /**
5412
+ * Find descendant components by CSS selector
5413
+ */
5414
+ find(selector) {
5415
+ const components = [];
5416
+ this.$.find(selector).each((_, el) => {
5417
+ const comp = $(el).data('_component');
5418
+ if (comp instanceof Jqhtml_Component) {
5419
+ components.push(comp);
5420
+ }
5421
+ });
5422
+ return components;
5423
+ }
5424
+ /**
5425
+ * Find closest ancestor component matching selector
5426
+ *
5427
+ * A bare component name - a plain identifier starting with a capital letter,
5428
+ * optionally preceded by a single underscore, such as 'Parent_Dashboard' or
5429
+ * '_Root_Layout' - is treated as the component-name class
5430
+ * selector '.Parent_Dashboard'. Components carry a CSS class for every name
5431
+ * in their prototype chain, so a base class name matches its subclasses too.
5432
+ * Every other selector ('.Foo', '#id', '[attr]', 'div > .Component') is
5433
+ * passed to jQuery unchanged.
5434
+ */
5435
+ closest(selector) {
5436
+ const search = Jqhtml_Component.COMPONENT_NAME_PATTERN.test(selector)
5437
+ ? `.${selector}`
5438
+ : selector;
5439
+ let current = this.$.parent();
5440
+ while (current.length > 0) {
5441
+ if (current.is(search)) {
5442
+ const comp = current.data('_component');
5443
+ if (comp instanceof Jqhtml_Component) {
5444
+ return comp;
5324
5445
  }
5325
- // Parent returned HTML - we're done
5326
- console.log(`[JQHTML] Parent returned instructions, inheritance complete`);
5327
- return [parentInstructions, parentContext];
5328
5446
  }
5447
+ current = current.parent();
5329
5448
  }
5330
- catch (error) {
5331
- console.warn(`[JQHTML] Error looking up parent template for ${className}:`, error);
5449
+ return null;
5450
+ }
5451
+ // -------------------------------------------------------------------------
5452
+ // Static Methods
5453
+ // -------------------------------------------------------------------------
5454
+ /**
5455
+ * Get CSS class hierarchy for this component type
5456
+ */
5457
+ static get_class_hierarchy() {
5458
+ // v2.2.13 - Fixed to handle undefined values in prototype chain
5459
+ const classes = [];
5460
+ let ctor = this;
5461
+ while (ctor) {
5462
+ // Check if constructor has a valid name property
5463
+ if (!ctor.name || typeof ctor.name !== 'string') {
5464
+ // console.warn('[JQHTML v2.2.13] Invalid constructor name in hierarchy:', ctor);
5465
+ break;
5466
+ }
5467
+ // Only add valid class names (non-empty strings, not 'Object')
5468
+ if (ctor.name !== 'Object' && ctor.name !== '') {
5469
+ // Normalize base class names - handle different import patterns
5470
+ let normalizedName = ctor.name;
5471
+ if (normalizedName === '_Jqhtml_Component' || normalizedName === '_Base_Jqhtml_Component') {
5472
+ normalizedName = 'Component'; // Use 'Component' instead of 'Jqhtml_Component' for CSS class
5473
+ }
5474
+ else if (normalizedName === 'Jqhtml_Component') {
5475
+ normalizedName = 'Component'; // Use 'Component' instead of 'Jqhtml_Component' for CSS class
5476
+ }
5477
+ classes.push(normalizedName);
5478
+ }
5479
+ // Get the next prototype
5480
+ const nextProto = Object.getPrototypeOf(ctor);
5481
+ // Stop if we've reached the end of the chain
5482
+ if (!nextProto || nextProto === Object.prototype || nextProto.constructor === Object) {
5483
+ break;
5484
+ }
5485
+ ctor = nextProto;
5332
5486
  }
5333
- // Move to next parent
5334
- currentClass = Object.getPrototypeOf(currentClass);
5487
+ return classes;
5335
5488
  }
5336
- // No parent template found
5337
- console.warn(`[JQHTML] No parent template found after walking chain`);
5338
- return null;
5339
- }
5340
- /**
5341
- * Render a template for a component
5342
- * Templates are functions that return [instructions, context] tuples
5343
- */
5344
- async function render_template(component, template_fn) {
5345
- // Get template from registry if not provided
5346
- let render_fn = template_fn;
5347
- if (!render_fn) {
5348
- const template_def = get_template_by_class(component.constructor);
5349
- render_fn = template_def.render;
5489
+ // -------------------------------------------------------------------------
5490
+ // Private Implementation
5491
+ // -------------------------------------------------------------------------
5492
+ _generate_cid() {
5493
+ return uid();
5350
5494
  }
5351
- if (!render_fn) {
5352
- // No template, component should handle its own rendering
5353
- return;
5495
+ _apply_css_classes() {
5496
+ const hierarchy = this.constructor.get_class_hierarchy();
5497
+ // If component name differs from class name, add component name to hierarchy
5498
+ // This happens when:
5499
+ // 1. Using base Jqhtml_Component with _component_name
5500
+ // 2. Using parent class via extends chain (e.g., Contacts_DataGrid using DataGrid_Abstract class)
5501
+ const classesToAdd = [...hierarchy];
5502
+ if (this.args._component_name && this.args._component_name !== this.constructor.name) {
5503
+ // Add component name at the beginning (most specific)
5504
+ classesToAdd.unshift(this.args._component_name);
5505
+ }
5506
+ // Filter out private/mangled classes (starting with _) and invalid class names.
5507
+ // A component name with the reserved single-underscore prefix (_Root_Layout)
5508
+ // is public and is kept - see component-name.ts.
5509
+ const publicClasses = classesToAdd.filter(className => {
5510
+ // Guard against undefined, null, or non-string values
5511
+ if (!className || typeof className !== 'string') {
5512
+ console.warn('[JQHTML] Filtered out invalid class name:', className);
5513
+ return false;
5514
+ }
5515
+ return !className.startsWith('_') || is_component_name(className);
5516
+ });
5517
+ if (publicClasses.length > 0) {
5518
+ this.$.addClass(publicClasses.join(' '));
5519
+ }
5354
5520
  }
5355
- // Clear existing content
5356
- component.$.empty();
5357
- // Execute template function
5358
- // Templates receive: (data, args, content)
5359
- // Provide a default empty content function if none provided
5360
- const defaultContent = () => '';
5361
- let [instructions, context] = render_fn.call(component, component.data, component.args, defaultContent // Default content function that returns empty string
5362
- );
5363
- // Check for slot-only template inheritance
5364
- // If instructions is {_slots: {...}}, this is a slot-only template
5365
- // We need to find the parent template and invoke it with these slots
5366
- if (instructions && typeof instructions === 'object' && instructions._slots) {
5367
- console.log(`[JQHTML] Slot-only template detected for ${component.constructor.name}, invoking inheritance`);
5368
- const result = await process_slot_inheritance(component, instructions._slots);
5369
- if (result) {
5370
- console.log(`[JQHTML] Parent template found, using parent instructions`);
5371
- instructions = result[0];
5372
- context = result[1];
5521
+ _apply_default_attributes() {
5522
+ // Get template using same logic as render() method
5523
+ let template;
5524
+ // If we have a component name, try looking up by name first (for unregistered components)
5525
+ if (this.args._component_name) {
5526
+ template = get_template(this.args._component_name);
5373
5527
  }
5374
5528
  else {
5375
- console.warn(`[JQHTML] No parent template found for ${component.constructor.name}, rendering empty`);
5376
- // No parent template found - return empty
5377
- instructions = [];
5529
+ // Otherwise look up by class
5530
+ template = get_template_by_class(this.constructor);
5378
5531
  }
5379
- }
5380
- // Process instructions to generate DOM
5381
- await process_instructions(instructions, component.$, component);
5382
- // Process bindings after DOM is created
5383
- await process_bindings(component);
5384
- // Attach event handlers
5385
- await attach_event_handlers(component);
5386
- }
5387
- /**
5388
- * Process data bindings (attributes created by :prop syntax)
5389
- */
5390
- async function process_bindings(component) {
5391
- // Find all elements with data-bind-* attributes
5392
- component.$.find('[data-bind-prop], [data-bind-value], [data-bind-text], [data-bind-html], [data-bind-class], [data-bind-style]').each((_, element) => {
5393
- const el = $(element);
5394
- const attrs = element.attributes;
5395
- for (let i = 0; i < attrs.length; i++) {
5396
- const attr = attrs[i];
5397
- if (attr.name.startsWith('data-bind-')) {
5398
- const binding_type = attr.name.substring(10); // Remove 'data-bind-'
5399
- const expression = attr.value;
5532
+ if (!template)
5533
+ return;
5534
+ // Walk the extends chain to collect all defaultAttributes from parent templates
5535
+ // Apply from parent to child so child attributes take precedence
5536
+ const templateChain = [];
5537
+ let currentTemplate = template;
5538
+ // Build chain from child to parent
5539
+ while (currentTemplate) {
5540
+ templateChain.unshift(currentTemplate); // Add to front so parent comes first
5541
+ // Check if this template extends another
5542
+ if (currentTemplate.extends) {
5400
5543
  try {
5401
- // Evaluate expression in component context
5402
- const value = evaluate_expression(expression, component);
5403
- // Apply binding based on type
5404
- switch (binding_type) {
5405
- case 'prop':
5406
- // Generic property binding
5407
- const prop_name = el.attr('data-bind-prop-name') || 'value';
5408
- el.prop(prop_name, value);
5409
- break;
5410
- case 'value':
5411
- el.val(value);
5412
- break;
5413
- case 'text':
5414
- el.text(value);
5415
- break;
5416
- case 'html':
5417
- el.html(value);
5418
- break;
5419
- case 'class':
5420
- if (typeof value === 'object') {
5421
- // Object syntax: {active: true, disabled: false}
5422
- Object.entries(value).forEach(([className, enabled]) => {
5423
- el.toggleClass(className, !!enabled);
5424
- });
5425
- }
5426
- else {
5427
- // String syntax
5428
- el.addClass(String(value));
5429
- }
5430
- break;
5431
- case 'style':
5432
- if (typeof value === 'object') {
5433
- el.css(value);
5544
+ currentTemplate = get_template(currentTemplate.extends);
5545
+ }
5546
+ catch (error) {
5547
+ // Parent template not found, stop chain
5548
+ break;
5549
+ }
5550
+ }
5551
+ else {
5552
+ break;
5553
+ }
5554
+ }
5555
+ // Apply defaultAttributes from each template in the chain (parent first, child last)
5556
+ for (const tmpl of templateChain) {
5557
+ if (!tmpl.defaultAttributes)
5558
+ continue;
5559
+ // Filter out 'tag' since it determines tag name, not an attribute
5560
+ const defineAttrs = { ...tmpl.defaultAttributes };
5561
+ delete defineAttrs.tag;
5562
+ // Debug logging
5563
+ if (window.jqhtml?.debug?.enabled) {
5564
+ const componentName = tmpl.name || this.args._component_name || this.constructor.name;
5565
+ console.log(`[Component] Applying defaultAttributes for ${componentName}:`, defineAttrs);
5566
+ }
5567
+ // Apply each default attribute
5568
+ for (const [key, value] of Object.entries(defineAttrs)) {
5569
+ if (key === 'class') {
5570
+ // Special handling for class - merge with existing
5571
+ const existingClasses = this.$.attr('class');
5572
+ if (existingClasses) {
5573
+ const existing = existingClasses.split(/\s+/).filter(c => c);
5574
+ const newClasses = String(value).split(/\s+/).filter(c => c);
5575
+ for (const newClass of newClasses) {
5576
+ if (!existing.includes(newClass)) {
5577
+ existing.push(newClass);
5434
5578
  }
5435
- else {
5436
- el.attr('style', String(value));
5579
+ }
5580
+ this.$.attr('class', existing.join(' '));
5581
+ }
5582
+ else {
5583
+ this.$.attr('class', value);
5584
+ }
5585
+ }
5586
+ else if (key === 'style') {
5587
+ // Special handling for style - merge with existing
5588
+ // Define attributes are applied AFTER invocation attributes
5589
+ // So we only add Define style properties that don't already exist
5590
+ // This ensures invocation wins conflicts (invocation overrides Define)
5591
+ const existingStyle = this.$.attr('style');
5592
+ if (existingStyle) {
5593
+ // Parse existing styles (from invocation)
5594
+ const existingRules = new Map();
5595
+ existingStyle.split(';').forEach(rule => {
5596
+ const [prop, val] = rule.split(':').map(s => s.trim());
5597
+ if (prop && val)
5598
+ existingRules.set(prop, val);
5599
+ });
5600
+ // Parse Define styles and only add if not already present
5601
+ String(value).split(';').forEach(rule => {
5602
+ const [prop, val] = rule.split(':').map(s => s.trim());
5603
+ if (prop && val && !existingRules.has(prop)) {
5604
+ // Only add if property doesn't exist (invocation wins)
5605
+ existingRules.set(prop, val);
5437
5606
  }
5438
- break;
5439
- default:
5440
- // Custom binding - set as attribute
5441
- el.attr(binding_type, value);
5607
+ });
5608
+ // Build merged style string
5609
+ const merged = Array.from(existingRules.entries())
5610
+ .map(([prop, val]) => `${prop}: ${val}`)
5611
+ .join('; ');
5612
+ this.$.attr('style', merged);
5613
+ }
5614
+ else {
5615
+ this.$.attr('style', value);
5442
5616
  }
5443
5617
  }
5444
- catch (error) {
5445
- console.error(`Error evaluating binding "${expression}":`, error);
5618
+ else if (key.startsWith('$') || key.startsWith('data-')) {
5619
+ // Data attributes - also update args
5620
+ const dataKey = key.startsWith('$') ? key.substring(1) :
5621
+ key.startsWith('data-') ? key.substring(5) : key;
5622
+ // Only apply if not already set in args
5623
+ if (!(dataKey in this.args)) {
5624
+ this.args[dataKey] = value;
5625
+ this.$.data(dataKey, value);
5626
+ this.$.attr(key.startsWith('$') ? `data-${dataKey}` : key, String(value));
5627
+ }
5628
+ }
5629
+ else {
5630
+ // Regular attributes - apply directly if not already set
5631
+ if (!this.$.attr(key)) {
5632
+ this.$.attr(key, value);
5633
+ }
5446
5634
  }
5447
5635
  }
5448
5636
  }
5449
- });
5637
+ }
5638
+ _set_attributes() {
5639
+ // data-cid on the rendered element is debug-only: it mirrors this._cid so component
5640
+ // boundaries are legible in DevTools. The runtime never reads it - scoping uses the
5641
+ // _cid property, and the transient data-cid placeholder the instruction processor
5642
+ // matches on is written and removed before this ever runs.
5643
+ if (debug_attributes_enabled()) {
5644
+ this.$.attr('data-cid', this._cid);
5645
+ }
5646
+ // Only set lifecycle state attribute if verbose mode enabled
5647
+ if (window.jqhtml?.debug?.verbose) {
5648
+ this.$.attr('data-_lifecycle-state', this._ready_state.toString());
5649
+ }
5650
+ }
5651
+ _update_debug_attrs() {
5652
+ // Only update lifecycle state attribute if verbose mode enabled
5653
+ if (window.jqhtml?.debug?.verbose) {
5654
+ this.$.attr('data-_lifecycle-state', this._ready_state.toString());
5655
+ }
5656
+ }
5657
+ _find_dom_parent() {
5658
+ let current = this.$.parent();
5659
+ while (current.length > 0) {
5660
+ const parent = current.data('_component');
5661
+ if (parent instanceof Jqhtml_Component) {
5662
+ this._dom_parent = parent;
5663
+ parent._dom_children.add(this);
5664
+ break;
5665
+ }
5666
+ current = current.parent();
5667
+ }
5668
+ }
5669
+ /**
5670
+ * Get DOM children (components in DOM subtree)
5671
+ * Uses fast _dom_children registry when possible, falls back to DOM traversal for off-DOM components
5672
+ * @private - Used internally for lifecycle coordination
5673
+ */
5674
+ _get_dom_children() {
5675
+ // If component was off-DOM during render, children couldn't register via _find_dom_parent()
5676
+ // Use DOM traversal to find direct children (works off-DOM, slightly slower)
5677
+ if (this._use_dom_fallback) {
5678
+ const directChildren = [];
5679
+ this.$.find('.Component').each((_, el) => {
5680
+ const $el = $(el);
5681
+ const comp = $el.data('_component');
5682
+ if (comp instanceof Jqhtml_Component) {
5683
+ // Only include if this component is the direct parent (not a grandparent)
5684
+ // Check: closest parent component is us (or no parent = we're the root)
5685
+ const closestParent = $el.parent().closest('.Component');
5686
+ if (closestParent.length === 0 || closestParent.data('_component') === this) {
5687
+ directChildren.push(comp);
5688
+ }
5689
+ }
5690
+ });
5691
+ return directChildren;
5692
+ }
5693
+ // Fast path: component was in-DOM during render, children registered normally
5694
+ // Filter out any that have since been removed from DOM (destroyed/replaced)
5695
+ const children = Array.from(this._dom_children);
5696
+ return children.filter(child => {
5697
+ return $.contains(document.documentElement, child.$[0]);
5698
+ });
5699
+ }
5700
+ _log_lifecycle(phase, status) {
5701
+ // Use new debug module
5702
+ logLifecycle(this, phase, status);
5703
+ // Keep legacy support for window.JQHTML_DEBUG
5704
+ if (typeof window !== 'undefined' && window.JQHTML_DEBUG) {
5705
+ window.JQHTML_DEBUG.log(this.component_name(), phase, status, {
5706
+ cid: this._cid,
5707
+ ready_state: this._ready_state,
5708
+ args: this.args
5709
+ });
5710
+ }
5711
+ }
5712
+ _log_debug(action, ...args) {
5713
+ if (typeof window !== 'undefined' && window.JQHTML_DEBUG) {
5714
+ window.JQHTML_DEBUG.log(this.component_name(), 'debug', `${action}: ${args.map(a => JSON.stringify(a)).join(', ')}`);
5715
+ }
5716
+ }
5450
5717
  }
5718
+ // Static properties
5719
+ Jqhtml_Component.__jqhtml_component = true; // Marker for unified register() detection
5720
+ // A bare component name, as accepted by closest() in place of a class selector
5721
+ Jqhtml_Component.COMPONENT_NAME_PATTERN = COMPONENT_NAME_PATTERN;
5722
+
5723
+ var light_css = "html[data-jqhtml-debug] .jqhtml-debug-hit {\n outline: 2px solid #e5322d;\n outline-offset: -2px;\n}\n\nhtml[data-jqhtml-debug] .jqhtml-debug-hit.jqhtml-debug-depth-0 {\n outline-color: #e5322d;\n}\n\nhtml[data-jqhtml-debug] .jqhtml-debug-hit.jqhtml-debug-depth-1 {\n outline-color: #2f6fe4;\n}\n\nhtml[data-jqhtml-debug] .jqhtml-debug-hit.jqhtml-debug-depth-2 {\n outline-color: #1f9d55;\n}\n\nhtml[data-jqhtml-debug] .jqhtml-debug-hit.jqhtml-debug-depth-3 {\n outline-color: #d97706;\n}\n\nhtml[data-jqhtml-debug] .jqhtml-debug-hit.jqhtml-debug-depth-4 {\n outline-color: #7c3aed;\n}\n\nhtml[data-jqhtml-debug] .jqhtml-debug-hit.jqhtml-debug-depth-5 {\n outline-color: #0e7490;\n}";
5724
+
5725
+ var shadow_css = ":host {\n all: initial;\n display: block;\n position: fixed;\n box-sizing: border-box;\n top: 0;\n left: 0;\n width: 100vw;\n height: 100vh;\n z-index: 2147483647;\n pointer-events: none;\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, \"Liberation Mono\", monospace;\n font-size: 12px;\n font-weight: normal;\n font-style: normal;\n font-variant: normal;\n line-height: 1.4;\n color: #1c1c1c;\n text-align: left;\n text-transform: none;\n text-indent: 0;\n text-shadow: none;\n letter-spacing: normal;\n word-spacing: normal;\n white-space: normal;\n word-break: normal;\n overflow-wrap: anywhere;\n hyphens: none;\n tab-size: 4;\n direction: ltr;\n visibility: visible;\n cursor: default;\n list-style: none;\n quotes: none;\n}\n\n.jqhtml-debug-layer {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n overflow: hidden;\n pointer-events: none;\n}\n\n.jqhtml-debug-label {\n position: absolute;\n box-sizing: border-box;\n display: block;\n padding: 1px 5px;\n min-height: 18px;\n color: #ffffff;\n background: #e5322d;\n border-radius: 0 0 3px 0;\n font-size: 11px;\n line-height: 16px;\n white-space: normal;\n overflow-wrap: anywhere;\n overflow: hidden;\n pointer-events: none;\n}\n\n.jqhtml-debug-label.jqhtml-debug-depth-0 {\n background: #e5322d;\n}\n\n.jqhtml-debug-label.jqhtml-debug-depth-1 {\n background: #2f6fe4;\n}\n\n.jqhtml-debug-label.jqhtml-debug-depth-2 {\n background: #1f9d55;\n}\n\n.jqhtml-debug-label.jqhtml-debug-depth-3 {\n background: #d97706;\n}\n\n.jqhtml-debug-label.jqhtml-debug-depth-4 {\n background: #7c3aed;\n}\n\n.jqhtml-debug-label.jqhtml-debug-depth-5 {\n background: #0e7490;\n}\n\n.jqhtml-debug-label-name {\n font-weight: bold;\n}\n\n.jqhtml-debug-label-args {\n opacity: 0.9;\n margin-left: 6px;\n}\n\n.jqhtml-debug-modal {\n display: none;\n position: absolute;\n box-sizing: border-box;\n top: 24px;\n right: 24px;\n width: 520px;\n max-width: calc(100% - 48px);\n max-height: calc(100% - 48px);\n overflow: auto;\n padding: 0 0 12px 0;\n background: #ffffff;\n color: #1c1c1c;\n border: 1px solid #d0d0d0;\n border-radius: 6px;\n box-shadow: 0 8px 32px rgba(0, 0, 0, 0.25);\n pointer-events: auto;\n}\n\n.jqhtml-debug-modal.jqhtml-debug-modal-open {\n display: block;\n}\n\n.jqhtml-debug-title {\n display: flex;\n align-items: center;\n gap: 8px;\n position: sticky;\n top: 0;\n padding: 8px 12px;\n background: #1c1c1c;\n color: #ffffff;\n font-weight: bold;\n}\n\n.jqhtml-debug-title-name {\n flex: 1 1 auto;\n font-size: 13px;\n}\n\n.jqhtml-debug-button {\n all: unset;\n display: inline-block;\n box-sizing: border-box;\n padding: 2px 8px;\n border: 1px solid rgba(255, 255, 255, 0.6);\n border-radius: 3px;\n color: #ffffff;\n font-size: 11px;\n line-height: 16px;\n cursor: pointer;\n user-select: none;\n}\n\n.jqhtml-debug-button:hover {\n background: rgba(255, 255, 255, 0.15);\n}\n\n.jqhtml-debug-section {\n padding: 8px 12px 0 12px;\n}\n\n.jqhtml-debug-h {\n margin: 4px 0;\n color: #666666;\n font-size: 11px;\n text-transform: uppercase;\n letter-spacing: 0.06em;\n}\n\n.jqhtml-debug-table {\n display: grid;\n grid-template-columns: max-content max-content 1fr;\n column-gap: 12px;\n row-gap: 2px;\n align-items: baseline;\n}\n\n.jqhtml-debug-key {\n color: #005cc5;\n white-space: nowrap;\n}\n\n.jqhtml-debug-type {\n color: #666666;\n font-size: 11px;\n white-space: nowrap;\n}\n\n.jqhtml-debug-value {\n white-space: pre-wrap;\n overflow-wrap: anywhere;\n}\n\n.jqhtml-debug-empty {\n color: #666666;\n font-style: italic;\n}\n\n.jqhtml-debug-list {\n display: block;\n margin: 0;\n padding: 0;\n}\n\n.jqhtml-debug-link {\n display: block;\n padding: 2px 4px;\n margin: 0 -4px;\n border-radius: 3px;\n cursor: pointer;\n}\n\n.jqhtml-debug-link:hover {\n background: #eef3ff;\n}\n\n.jqhtml-debug-link-cid {\n color: #666666;\n margin-left: 6px;\n}\n\n.jqhtml-debug-note {\n margin-top: 4px;\n color: #92400e;\n font-size: 11px;\n}";
5726
+
5451
5727
  /**
5452
- * Attach event handlers (attributes created by @event syntax)
5728
+ * Component debug overlay.
5729
+ *
5730
+ * `jqhtml.debug_overlay.enable()` turns the page into an inspector: hovering
5731
+ * any element outlines the component under the pointer and every component
5732
+ * above it, each with a small tab naming the component and its simple args;
5733
+ * clicking opens a modal describing the innermost component (identity, args,
5734
+ * data, state, ancestry, instantiator) instead of firing the click. Alt+click
5735
+ * passes through. `disable()` puts everything back.
5736
+ *
5737
+ * Non-invasiveness, the design goal (details in ./CLAUDE.md):
5738
+ * - The overlay's own UI lives in a shadow root on a host element appended to
5739
+ * <body>, styled by shadow.scss. Page CSS cannot select into it and :host
5740
+ * cuts inheritance, so its look is fixed whatever the page does.
5741
+ * - The only thing added to application elements is a pair of classes,
5742
+ * .jqhtml-debug-hit / .jqhtml-debug-depth-N, which light.scss paints as an
5743
+ * inset outline (no layout effect) and only while <html data-jqhtml-debug>.
5744
+ * - Listeners are attached on enable and removed on disable. The stylesheets
5745
+ * and the (detached) host stay once installed: inert, and cheap to re-enable.
5746
+ * - Nothing is registered per component, so components created after enable
5747
+ * are covered automatically.
5453
5748
  */
5454
- async function attach_event_handlers(component) {
5455
- // Find all elements with data-on-* attributes
5456
- 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) => {
5457
- const el = $(element);
5458
- const attrs = element.attributes;
5459
- for (let i = 0; i < attrs.length; i++) {
5460
- const attr = attrs[i];
5461
- if (attr.name.startsWith('data-on-')) {
5462
- const event_name = attr.name.substring(8); // Remove 'data-on-'
5463
- const handler_expr = attr.value;
5464
- // Remove the attribute to avoid re-processing
5465
- el.removeAttr(attr.name);
5466
- // Attach event handler
5467
- el.on(event_name, function (event) {
5468
- try {
5469
- // Create handler function in component context
5470
- const handler = evaluate_handler(handler_expr, component);
5471
- if (typeof handler === 'function') {
5472
- // Call with component as 'this' and pass event
5473
- handler.call(component, event);
5474
- }
5475
- else {
5476
- // Expression to execute
5477
- evaluate_expression(handler_expr, component, { $event: event });
5478
- }
5479
- }
5480
- catch (error) {
5481
- console.error(`Error in ${event_name} handler "${handler_expr}":`, error);
5482
- }
5483
- });
5484
- }
5485
- }
5749
+ const PREFIX = 'jqhtml-debug-';
5750
+ const ROOT_ATTR = 'data-jqhtml-debug'; // on <html> while enabled - gates light.scss
5751
+ const HOST_ATTR = 'data-jqhtml-debug-root'; // the shadow host
5752
+ const LIGHT_STYLE_ID = 'jqhtml-debug-light-styles';
5753
+ const HIT_CLASS = `${PREFIX}hit`;
5754
+ const DEPTH_CLASS = `${PREFIX}depth-`;
5755
+ const DEPTH_COLORS = 6; // keep in step with $depth-colors in _tokens.scss
5756
+ const LABEL_HEIGHT = 18;
5757
+ const READY_STATES = ['created', 'init', 'loaded', 'rendered', 'ready'];
5758
+ const state = {
5759
+ enabled: false,
5760
+ host: null,
5761
+ shadow: null,
5762
+ layer: null,
5763
+ modal: null,
5764
+ chain: [],
5765
+ hit_elements: [],
5766
+ modal_component: null,
5767
+ };
5768
+ // ---------------------------------------------------------------------------
5769
+ // Public API
5770
+ // ---------------------------------------------------------------------------
5771
+ const debug_overlay = {
5772
+ /** Turn the overlay on. Returns false where there is no DOM (SSR). */
5773
+ enable() {
5774
+ if (typeof document === 'undefined' || !document.documentElement)
5775
+ return false;
5776
+ if (state.enabled)
5777
+ return true;
5778
+ install();
5779
+ if (!state.host.isConnected)
5780
+ (document.body || document.documentElement).appendChild(state.host);
5781
+ document.documentElement.setAttribute(ROOT_ATTR, '');
5782
+ document.addEventListener('mouseover', on_mouseover, true);
5783
+ document.addEventListener('mouseout', on_mouseout, true);
5784
+ document.addEventListener('click', on_click, true);
5785
+ document.addEventListener('keydown', on_keydown, true);
5786
+ document.addEventListener('scroll', on_reposition, true);
5787
+ window.addEventListener('resize', on_reposition);
5788
+ state.enabled = true;
5789
+ return true;
5790
+ },
5791
+ /** Turn the overlay off. Styles stay installed but match nothing. */
5792
+ disable() {
5793
+ if (!state.enabled)
5794
+ return;
5795
+ document.removeEventListener('mouseover', on_mouseover, true);
5796
+ document.removeEventListener('mouseout', on_mouseout, true);
5797
+ document.removeEventListener('click', on_click, true);
5798
+ document.removeEventListener('keydown', on_keydown, true);
5799
+ document.removeEventListener('scroll', on_reposition, true);
5800
+ window.removeEventListener('resize', on_reposition);
5801
+ clear_hover();
5802
+ close_modal();
5803
+ document.documentElement.removeAttribute(ROOT_ATTR);
5804
+ if (state.host && state.host.isConnected)
5805
+ state.host.remove();
5806
+ state.enabled = false;
5807
+ },
5808
+ is_enabled() {
5809
+ return state.enabled;
5810
+ },
5811
+ /**
5812
+ * Open the modal for a component, an element inside one, or a jQuery
5813
+ * object, without needing a click. Handy from the console and from tests.
5814
+ */
5815
+ inspect(target) {
5816
+ if (!state.enabled)
5817
+ return false;
5818
+ const component = target instanceof Jqhtml_Component
5819
+ ? target
5820
+ : chain_for(target && target.jquery ? target[0] : target)[0];
5821
+ if (!component)
5822
+ return false;
5823
+ open_modal(component);
5824
+ return true;
5825
+ },
5826
+ };
5827
+ // ---------------------------------------------------------------------------
5828
+ // Installation (once)
5829
+ // ---------------------------------------------------------------------------
5830
+ function install() {
5831
+ if (state.host)
5832
+ return;
5833
+ if (!document.getElementById(LIGHT_STYLE_ID)) {
5834
+ const style = document.createElement('style');
5835
+ style.id = LIGHT_STYLE_ID;
5836
+ style.textContent = light_css;
5837
+ document.head.appendChild(style);
5838
+ }
5839
+ const host = document.createElement('div');
5840
+ host.setAttribute(HOST_ATTR, '');
5841
+ const shadow = host.attachShadow({ mode: 'open' });
5842
+ const style = document.createElement('style');
5843
+ style.textContent = shadow_css;
5844
+ shadow.appendChild(style);
5845
+ const layer = el('div', 'layer');
5846
+ shadow.appendChild(layer);
5847
+ const modal = el('div', 'modal');
5848
+ shadow.appendChild(modal);
5849
+ state.host = host;
5850
+ state.shadow = shadow;
5851
+ state.layer = layer;
5852
+ state.modal = modal;
5853
+ }
5854
+ // ---------------------------------------------------------------------------
5855
+ // Component chain
5856
+ // ---------------------------------------------------------------------------
5857
+ /** Components from the one containing `node` outward to the root. */
5858
+ function chain_for(node) {
5859
+ const chain = [];
5860
+ let element = node instanceof Element ? node : (node && node.parentElement) || null;
5861
+ while (element) {
5862
+ const owner = element.closest('.Component');
5863
+ if (!owner)
5864
+ break;
5865
+ const component = $(owner).data('_component');
5866
+ if (component instanceof Jqhtml_Component && !component.$.hasClass('_Component_Stopped'))
5867
+ chain.push(component);
5868
+ element = owner.parentElement;
5869
+ }
5870
+ return chain;
5871
+ }
5872
+ function is_ours(event) {
5873
+ return !!state.host && event.composedPath().includes(state.host);
5874
+ }
5875
+ // ---------------------------------------------------------------------------
5876
+ // Hover
5877
+ // ---------------------------------------------------------------------------
5878
+ function on_mouseover(event) {
5879
+ if (is_ours(event))
5880
+ return;
5881
+ const chain = chain_for(event.target);
5882
+ if (chain[0] === state.chain[0])
5883
+ return;
5884
+ set_hover(chain);
5885
+ }
5886
+ function on_mouseout(event) {
5887
+ if (!event.relatedTarget)
5888
+ clear_hover(); // pointer left the document
5889
+ }
5890
+ function on_reposition() {
5891
+ if (state.chain.length)
5892
+ render_labels();
5893
+ }
5894
+ function set_hover(chain) {
5895
+ clear_hover();
5896
+ state.chain = chain;
5897
+ chain.forEach((component, depth) => {
5898
+ const element = component.$[0];
5899
+ element.classList.add(HIT_CLASS, DEPTH_CLASS + (depth % DEPTH_COLORS));
5900
+ state.hit_elements.push(element);
5486
5901
  });
5902
+ render_labels();
5903
+ }
5904
+ function clear_hover() {
5905
+ for (const element of state.hit_elements) {
5906
+ element.classList.remove(HIT_CLASS);
5907
+ for (let i = 0; i < DEPTH_COLORS; i++)
5908
+ element.classList.remove(DEPTH_CLASS + i);
5909
+ }
5910
+ state.hit_elements = [];
5911
+ state.chain = [];
5912
+ if (state.layer)
5913
+ state.layer.textContent = '';
5487
5914
  }
5488
5915
  /**
5489
- * Evaluate an expression in component context
5916
+ * One tab per component in the chain, pinned to the component's top-left
5917
+ * corner and no wider than the component. Outermost first so the innermost
5918
+ * paints on top; tabs that share a corner (a child filling its parent, the
5919
+ * usual case) stack downward instead of hiding each other.
5490
5920
  */
5491
- function evaluate_expression(expression, component, locals = {}) {
5492
- // Create evaluation context
5493
- const context = {
5494
- // Component properties
5495
- data: component.data,
5496
- args: component.args,
5497
- $: component.$,
5498
- // Component methods
5499
- $sid: component.$sid.bind(component),
5500
- // Locals (like $event)
5501
- ...locals
5502
- };
5503
- // Build function that evaluates in context
5504
- const keys = Object.keys(context);
5505
- const values = Object.values(context);
5506
- try {
5507
- // Use Function constructor for better performance than eval
5508
- const fn = new Function(...keys, `return (${expression})`);
5509
- return fn(...values);
5921
+ function render_labels() {
5922
+ const layer = state.layer;
5923
+ layer.textContent = '';
5924
+ const taken = new Map();
5925
+ for (let depth = state.chain.length - 1; depth >= 0; depth--) {
5926
+ const component = state.chain[depth];
5927
+ const rect = component.$[0].getBoundingClientRect();
5928
+ if (rect.width <= 0 || rect.height <= 0)
5929
+ continue;
5930
+ const left = Math.max(0, Math.round(rect.left));
5931
+ const top = Math.max(0, Math.round(rect.top));
5932
+ const key = `${left},${top}`;
5933
+ const stacked = taken.get(key) || 0;
5934
+ taken.set(key, stacked + 1);
5935
+ const label = el('div', `label ${PREFIX}depth-${depth % DEPTH_COLORS}`);
5936
+ label.style.left = `${left}px`;
5937
+ label.style.top = `${top + stacked * LABEL_HEIGHT}px`;
5938
+ label.style.maxWidth = `${Math.max(24, Math.round(rect.width))}px`;
5939
+ label.appendChild(el('span', 'label-name', component.component_name()));
5940
+ const args = simple_args(component);
5941
+ if (args)
5942
+ label.appendChild(el('span', 'label-args', args));
5943
+ layer.appendChild(label);
5510
5944
  }
5511
- catch (error) {
5512
- console.error(`Invalid expression: ${expression}`, error);
5513
- return undefined;
5945
+ }
5946
+ /** `$id=7 $title="Users"` - only strings, numbers and booleans, never internals. */
5947
+ function simple_args(component) {
5948
+ const parts = [];
5949
+ for (const [key, value] of Object.entries(component.args || {})) {
5950
+ if (key.startsWith('_'))
5951
+ continue;
5952
+ if (typeof value === 'string')
5953
+ parts.push(`$${key}=${JSON.stringify(truncate(value, 24))}`);
5954
+ else if (typeof value === 'number' || typeof value === 'boolean')
5955
+ parts.push(`$${key}=${value}`);
5514
5956
  }
5957
+ return parts.join(' ');
5515
5958
  }
5516
- /**
5517
- * Evaluate a handler expression (might be method name or inline code)
5518
- */
5519
- function evaluate_handler(expression, component) {
5520
- // Check if it's a method name on the component
5521
- if (expression in component && typeof component[expression] === 'function') {
5522
- return component[expression];
5959
+ // ---------------------------------------------------------------------------
5960
+ // Click -> modal
5961
+ // ---------------------------------------------------------------------------
5962
+ function on_click(event) {
5963
+ if (is_ours(event) || event.altKey)
5964
+ return;
5965
+ event.preventDefault();
5966
+ event.stopPropagation();
5967
+ event.stopImmediatePropagation();
5968
+ const chain = chain_for(event.target);
5969
+ if (!chain.length) {
5970
+ close_modal();
5971
+ return;
5523
5972
  }
5524
- // Otherwise treat as inline code
5973
+ open_modal(chain[0]);
5974
+ }
5975
+ function on_keydown(event) {
5976
+ if (event.key === 'Escape' && state.modal_component) {
5977
+ close_modal();
5978
+ event.stopPropagation();
5979
+ }
5980
+ }
5981
+ function close_modal() {
5982
+ if (!state.modal)
5983
+ return;
5984
+ state.modal.classList.remove(`${PREFIX}modal-open`);
5985
+ state.modal.textContent = '';
5986
+ state.modal_component = null;
5987
+ }
5988
+ function open_modal(component) {
5989
+ const modal = state.modal;
5990
+ modal.textContent = '';
5991
+ state.modal_component = component;
5992
+ const chain = chain_for(component.$[0]);
5993
+ const ancestors = chain.slice(1);
5994
+ const element = component.$[0];
5995
+ // Title bar
5996
+ const title = el('div', 'title');
5997
+ title.appendChild(el('span', 'title-name', `<${component.component_name()}>`));
5998
+ const log = el('button', 'button', 'Log to console');
5999
+ log.addEventListener('click', () => {
6000
+ console.log(`[JQHTML debug] <${component.component_name()}>`, component, element);
6001
+ });
6002
+ title.appendChild(log);
6003
+ const close = el('button', 'button', 'Close');
6004
+ close.addEventListener('click', close_modal);
6005
+ title.appendChild(close);
6006
+ modal.appendChild(title);
6007
+ // Identity
6008
+ const ctor = component.constructor;
6009
+ const hierarchy = ctor.get_class_hierarchy ? ctor.get_class_hierarchy() : [ctor.name];
6010
+ const identity = table();
6011
+ row(identity, 'name', '', component.component_name());
6012
+ row(identity, 'class', '', hierarchy.join(' → '));
6013
+ row(identity, 'element', '', describe_element(element));
6014
+ row(identity, '_cid', '', String(component._cid));
6015
+ row(identity, 'lifecycle', '', READY_STATES[component._ready_state] || String(component._ready_state));
6016
+ const nocache = element.getAttribute('data-nocache');
6017
+ if (nocache)
6018
+ row(identity, 'cache', '', `declined: ${nocache}`);
6019
+ modal.appendChild(section('Identity', identity));
6020
+ // Args / data / state
6021
+ modal.appendChild(section('Args', object_table(component.args)));
6022
+ modal.appendChild(section('Data', object_table(component.data)));
6023
+ modal.appendChild(section('State', object_table(component.state)));
6024
+ // Ancestry
6025
+ const list = el('div', 'list');
6026
+ if (!ancestors.length)
6027
+ list.appendChild(el('div', 'empty', 'none - this is a root component'));
6028
+ for (const ancestor of ancestors) {
6029
+ const link = el('div', 'link');
6030
+ link.appendChild(el('span', 'link-name', `<${ancestor.component_name()}>`));
6031
+ link.appendChild(el('span', 'link-cid', String(ancestor._cid)));
6032
+ link.addEventListener('click', () => open_modal(ancestor));
6033
+ list.appendChild(link);
6034
+ }
6035
+ modal.appendChild(section('Ancestry (DOM, nearest first)', list));
6036
+ // Instantiator - the component whose template wrote this tag. Differs from
6037
+ // the DOM parent for markup written in a slot body or default content.
6038
+ const instantiator = component.instantiator ? component.instantiator() : null;
6039
+ const inst = el('div', 'list');
6040
+ if (instantiator) {
6041
+ const link = el('div', 'link');
6042
+ link.appendChild(el('span', 'link-name', `<${instantiator.component_name()}>`));
6043
+ link.appendChild(el('span', 'link-cid', String(instantiator._cid)));
6044
+ link.addEventListener('click', () => open_modal(instantiator));
6045
+ inst.appendChild(link);
6046
+ if (ancestors[0] && instantiator !== ancestors[0]) {
6047
+ inst.appendChild(el('div', 'note', 'Written in this component\'s template but rendered inside a different parent (slot or default content): handlers and $sid bind here.'));
6048
+ }
6049
+ }
6050
+ else {
6051
+ inst.appendChild(el('div', 'empty', 'none - created directly'));
6052
+ }
6053
+ modal.appendChild(section('Instantiator', inst));
6054
+ modal.classList.add(`${PREFIX}modal-open`);
6055
+ }
6056
+ // ---------------------------------------------------------------------------
6057
+ // DOM helpers (shadow side)
6058
+ // ---------------------------------------------------------------------------
6059
+ function el(tag, classes, text) {
6060
+ const node = document.createElement(tag);
6061
+ node.className = classes.split(' ').map((c) => (c.startsWith(PREFIX) ? c : PREFIX + c)).join(' ');
6062
+ if (text !== undefined)
6063
+ node.textContent = text;
6064
+ return node;
6065
+ }
6066
+ function section(heading, body) {
6067
+ const wrap = el('div', 'section');
6068
+ wrap.appendChild(el('div', 'h', heading));
6069
+ wrap.appendChild(body);
6070
+ return wrap;
6071
+ }
6072
+ function table() {
6073
+ return el('div', 'table');
6074
+ }
6075
+ function row(t, key, type, value) {
6076
+ t.appendChild(el('span', 'key', key));
6077
+ t.appendChild(el('span', 'type', type));
6078
+ t.appendChild(el('span', 'value', value));
6079
+ }
6080
+ function object_table(source) {
6081
+ const entries = source && typeof source === 'object' ? Object.entries(source) : [];
6082
+ if (!entries.length)
6083
+ return el('div', 'empty', 'empty');
6084
+ const t = table();
6085
+ for (const [key, value] of entries)
6086
+ row(t, key, type_of(value), preview(value));
6087
+ return t;
6088
+ }
6089
+ function type_of(value) {
6090
+ if (value === null)
6091
+ return 'null';
6092
+ if (Array.isArray(value))
6093
+ return `array(${value.length})`;
6094
+ if (value instanceof Jqhtml_Component)
6095
+ return 'component';
6096
+ if (value && value.jquery)
6097
+ return `jQuery(${value.length})`;
6098
+ if (typeof value === 'object' && value instanceof Element)
6099
+ return 'element';
6100
+ return typeof value;
6101
+ }
6102
+ function preview(value) {
6103
+ if (value === undefined)
6104
+ return 'undefined';
6105
+ if (value === null)
6106
+ return 'null';
6107
+ if (typeof value === 'string')
6108
+ return JSON.stringify(truncate(value, 200));
6109
+ if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint')
6110
+ return String(value);
6111
+ if (typeof value === 'function')
6112
+ return `ƒ ${value.name || 'anonymous'}()`;
6113
+ if (value instanceof Jqhtml_Component)
6114
+ return `<${value.component_name()}> ${value._cid}`;
6115
+ if (value.jquery)
6116
+ return value.length ? describe_element(value[0]) : 'empty';
6117
+ if (value instanceof Element)
6118
+ return describe_element(value);
5525
6119
  try {
5526
- return new Function('$event', `
5527
- const { data, args, $, emit, $sid } = this;
5528
- ${expression}
5529
- `).bind(component);
6120
+ return truncate(JSON.stringify(value), 200);
5530
6121
  }
5531
- catch (error) {
5532
- console.error(`Invalid handler: ${expression}`, error);
5533
- return null;
6122
+ catch {
6123
+ return `{${Object.keys(value).join(', ')}}`;
5534
6124
  }
5535
6125
  }
5536
- /**
5537
- * Helper to escape HTML for safe output
5538
- */
5539
- function escape_html(str) {
5540
- const div = document.createElement('div');
5541
- div.textContent = str;
5542
- return div.innerHTML;
6126
+ function describe_element(element) {
6127
+ const id = element.id ? `#${element.id}` : '';
6128
+ const classes = element.classList.length ? `.${Array.from(element.classList).join('.')}` : '';
6129
+ return `${element.tagName.toLowerCase()}${id}${classes}`;
5543
6130
  }
5544
- /**
5545
- * Helper to escape HTML and convert newlines to <br /> tags
5546
- * Used by <%br= %> syntax for displaying user-entered text with line breaks
5547
- */
5548
- function escape_html_nl2br(str) {
5549
- const div = document.createElement('div');
5550
- div.textContent = str;
5551
- // Escape HTML first, then replace newlines with <br />
5552
- return div.innerHTML.replace(/\n/g, '<br />');
6131
+ function truncate(text, max) {
6132
+ return text.length > max ? `${text.slice(0, max - 1)}…` : text;
5553
6133
  }
5554
6134
 
5555
6135
  /**
@@ -5798,16 +6378,20 @@ function init_jquery_plugin(jQuery) {
5798
6378
  catch (error) {
5799
6379
  console.warn('[JQHTML] Error stopping existing component during replacement:', error);
5800
6380
  }
5801
- // Remove component classes (any class starting with capital letter, except BEM classes)
6381
+ // Remove component classes (any class starting with a capital letter, or a
6382
+ // component name with the reserved underscore prefix - except BEM classes)
5802
6383
  const classes = element.attr('class');
5803
6384
  if (classes) {
5804
6385
  const classList = classes.split(/\s+/);
5805
6386
  const nonComponentClasses = classList.filter((cls) => {
5806
6387
  // Keep class if:
5807
6388
  // 1. It's empty
5808
- // 2. It doesn't start with a capital letter
6389
+ // 2. It is neither capital-first nor a component name (see component-name.ts)
5809
6390
  // 3. It's a BEM-style class (contains __) - these persist across reinitialization
5810
- return !cls || cls[0] !== cls[0].toUpperCase() || cls[0] === cls[0].toLowerCase() || cls.includes('__');
6391
+ if (!cls || cls.includes('__'))
6392
+ return true;
6393
+ const capital_first = cls[0] === cls[0].toUpperCase() && cls[0] !== cls[0].toLowerCase();
6394
+ return !capital_first && !is_component_name(cls);
5811
6395
  });
5812
6396
  element.attr('class', nonComponentClasses.join(' '));
5813
6397
  }
@@ -5836,8 +6420,11 @@ function init_jquery_plugin(jQuery) {
5836
6420
  }
5837
6421
  }
5838
6422
  else {
5839
- // Direct class reference
6423
+ // Direct class reference. The invocation name is the class's own; it is
6424
+ // recorded for component_name() but does not drive the template tag
6425
+ // check below, which is for named (template-backed) invocations.
5840
6426
  ComponentClass = componentOrName;
6427
+ args = { ...args, _component_name: componentOrName.component_name || componentOrName.name };
5841
6428
  }
5842
6429
  // Check if element tag matches expected tag from template
5843
6430
  let targetElement = element;
@@ -6173,7 +6760,7 @@ function init(jQuery, config) {
6173
6760
  }
6174
6761
  }
6175
6762
  // Version - will be replaced during build with actual version from package.json
6176
- const version = '2.3.61';
6763
+ const version = '2.3.67';
6177
6764
  // Default export with all functionality
6178
6765
  const jqhtml = {
6179
6766
  // Core
@@ -6204,6 +6791,14 @@ const jqhtml = {
6204
6791
  // Integration configuration - see config.ts
6205
6792
  configure,
6206
6793
  get_config,
6794
+ // Value printers: how <%= %> renders an object (see value-printers.ts).
6795
+ // print_object and dynamic_component_name are called by compiled templates.
6796
+ add_object_printer,
6797
+ print_object,
6798
+ dynamic_component_name,
6799
+ // Component debug overlay: hover outlines + click-to-inspect modal.
6800
+ // jqhtml.debug_overlay.enable() / .disable() / .is_enabled() / .inspect(x)
6801
+ debug_overlay,
6207
6802
  // Debug settings
6208
6803
  debug: {
6209
6804
  enabled: false,
@@ -6309,18 +6904,23 @@ if (typeof window !== 'undefined' && !window.jqhtml) {
6309
6904
  }
6310
6905
  }
6311
6906
 
6907
+ exports.COMPONENT_NAME_PATTERN = COMPONENT_NAME_PATTERN;
6908
+ exports.COMPONENT_NAME_RULE = COMPONENT_NAME_RULE;
6312
6909
  exports.Jqhtml_Component = Jqhtml_Component;
6313
6910
  exports.Jqhtml_LifecycleManager = LifecycleManager;
6314
6911
  exports.Jqhtml_Local_Storage = Jqhtml_Local_Storage;
6315
6912
  exports.LifecycleManager = LifecycleManager;
6316
6913
  exports.Load_Coordinator = Load_Coordinator;
6914
+ exports.add_object_printer = add_object_printer;
6317
6915
  exports.applyDebugDelay = applyDebugDelay;
6318
6916
  exports.boot = boot;
6319
6917
  exports.clear_preload_data = clear_preload_data;
6320
6918
  exports.configure = configure;
6321
6919
  exports.create_component = create_component;
6920
+ exports.debug_overlay = debug_overlay;
6322
6921
  exports.default = jqhtml;
6323
6922
  exports.devWarn = devWarn;
6923
+ exports.dynamic_component_name = dynamic_component_name;
6324
6924
  exports.escape_html = escape_html;
6325
6925
  exports.escape_html_nl2br = escape_html_nl2br;
6326
6926
  exports.extract_slots = extract_slots;
@@ -6328,6 +6928,7 @@ exports.get_captured_data = get_captured_data;
6328
6928
  exports.get_component_class = get_component_class;
6329
6929
  exports.get_component_names = get_component_names;
6330
6930
  exports.get_config = get_config;
6931
+ exports.get_object_printers = get_object_printers;
6331
6932
  exports.get_registered_templates = get_registered_templates;
6332
6933
  exports.get_template = get_template;
6333
6934
  exports.get_template_by_class = get_template_by_class;
@@ -6336,11 +6937,13 @@ exports.has_component = has_component;
6336
6937
  exports.init = init;
6337
6938
  exports.init_jquery_plugin = init_jquery_plugin;
6338
6939
  exports.isSequentialProcessing = isSequentialProcessing;
6940
+ exports.is_component_name = is_component_name;
6339
6941
  exports.list_components = list_components;
6340
6942
  exports.logDataChange = logDataChange;
6341
6943
  exports.logDispatch = logDispatch;
6342
6944
  exports.logInstruction = logInstruction;
6343
6945
  exports.logLifecycle = logLifecycle;
6946
+ exports.print_object = print_object;
6344
6947
  exports.process_instructions = process_instructions;
6345
6948
  exports.register = register;
6346
6949
  exports.register_cache_class = register_cache_class;