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