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