/* === storage/rsx-build/bundles/npm_Bootstrap5_Bundle_6459e8ed0f60bda4f121420766012d53.js === */ (() => { // node_modules/@jqhtml/core/dist/index.js var LifecycleManager = class _LifecycleManager { static get_instance() { if (!_LifecycleManager.instance) { _LifecycleManager.instance = new _LifecycleManager(); } return _LifecycleManager.instance; } constructor() { this.active_components = /* @__PURE__ */ new Set(); } /** * Boot a component - run its full lifecycle * Called when component is created */ async boot_component(component) { this.active_components.add(component); try { await component.create(); if (component._stopped) return; component.trigger("create"); let render_id = component._render(); if (component._stopped) return; await component.load(); if (component._stopped) return; if (component.should_rerender()) { render_id = component._render(); if (component._stopped) return; } if (component._render_count !== render_id) { return; } await component.ready(); if (component._stopped) return; await component.trigger("ready"); } catch (error) { console.error(`Error booting component ${component.component_name()}:`, error); throw error; } } /** * Unregister a component (called on destroy) */ unregister_component(component) { this.active_components.delete(component); } /** * Wait for all active components to reach ready state */ async wait_for_ready() { const ready_promises = []; for (const component of this.active_components) { if (component._ready_state < 4) { ready_promises.push(new Promise((resolve) => { component.on("ready", () => resolve()); })); } } await Promise.all(ready_promises); } }; var component_classes = /* @__PURE__ */ new Map(); var component_templates = /* @__PURE__ */ new Map(); var warned_components = /* @__PURE__ */ new Set(); var DEFAULT_TEMPLATE = { name: "Component", // Default name tag: "div", render: function(data, args, content) { const _output = []; if (args._inner_html) { _output.push(args._inner_html); return [_output, this]; } if (content && typeof content === "function") { const result = content(this); if (Array.isArray(result) && result.length === 2) { _output.push(...result[0]); } else if (typeof result === "string") { _output.push(result); } } return [_output, this]; } }; function register_component(nameOrClass, component_class, template) { if (typeof nameOrClass === "string") { const name = nameOrClass; if (!component_class) { throw new Error("Component class is required when registering by name"); } if (!/^[A-Z]/.test(name)) { throw new Error(`Component name '${name}' must start with a capital letter. Convention is First_Letter_With_Underscores.`); } component_classes.set(name, component_class); if (template) { if (template.name !== name) { throw new Error(`Template name '${template.name}' must match component name '${name}'`); } register_template(template); } } else { const component_class2 = nameOrClass; const name = component_class2.name; if (!name || name === "Component") { throw new Error("Component class must have a name when registering without explicit name"); } component_classes.set(name, component_class2); } } function get_component_class(name) { const directClass = component_classes.get(name); if (directClass) { return directClass; } const template = component_templates.get(name); if (template && template.extends) { const visited = /* @__PURE__ */ new Set([name]); let currentTemplateName = template.extends; while (currentTemplateName && !visited.has(currentTemplateName)) { visited.add(currentTemplateName); const parentClass = component_classes.get(currentTemplateName); if (parentClass) { if (window.jqhtml?.debug?.enabled) { console.log(`[JQHTML] Component '${name}' using class from parent '${currentTemplateName}' via extends chain`); } return parentClass; } const parentTemplate = component_templates.get(currentTemplateName); if (parentTemplate && parentTemplate.extends) { currentTemplateName = parentTemplate.extends; } else { break; } } } return void 0; } function register_template(template_def) { const name = template_def.name; if (!name) { throw new Error("Template must have a name property"); } if (!/^[A-Z]/.test(name)) { throw new Error(`Template name '${name}' must start with a capital letter. Convention is First_Letter_With_Underscores.`); } if (component_templates.has(name)) { console.warn(`[JQHTML] Template '${name}' already registered, skipping duplicate registration`); return false; } component_templates.set(name, template_def); if (window.jqhtml?.debug?.enabled) { console.log(`[JQHTML] Successfully registered template: ${name}`); } const component_class = component_classes.get(name); if (component_class) { component_class._jqhtml_metadata = { tag: template_def.tag, defaultAttributes: template_def.defaultAttributes || {} }; } return true; } function get_template(name) { const template = component_templates.get(name); if (!template) { const component_class = component_classes.get(name); if (component_class) { const inherited_template = get_template_by_class(component_class); if (inherited_template !== DEFAULT_TEMPLATE) { if (window.jqhtml?.debug?.enabled) { console.log(`[JQHTML] Component '${name}' has no template, using template from prototype chain`); } return inherited_template; } if (window.jqhtml?.debug?.enabled && !warned_components.has(name)) { warned_components.add(name); console.log(`[JQHTML] No template found for class: ${name}, using default div template`); } } else { if (name !== "_Jqhtml_Component" && name !== "Redrawable" && !warned_components.has(name)) { warned_components.add(name); console.warn(`[JQHTML] Creating ${name} with defaults - no template or class defined`); } } if (window.jqhtml?.debug?.verbose) { const registered = Array.from(component_templates.keys()); console.log(`[JQHTML] Looking for template '${name}' in: [${registered.join(", ")}]`); } return DEFAULT_TEMPLATE; } return template; } function get_template_by_class(component_class) { if (component_class.template) { return component_class.template; } let currentClass = component_class; while (currentClass && currentClass.name !== "Object") { let normalizedName = currentClass.name; if (normalizedName === "_Jqhtml_Component" || normalizedName === "_Base_Jqhtml_Component") { normalizedName = "Component"; } const template = component_templates.get(normalizedName); if (template) { return template; } currentClass = Object.getPrototypeOf(currentClass); } return DEFAULT_TEMPLATE; } function create_component(name, element, args = {}) { const ComponentClass = get_component_class(name) || Component; return new ComponentClass(element, args); } function has_component(name) { return component_classes.has(name); } function get_component_names() { return Array.from(component_classes.keys()); } function get_registered_templates() { return Array.from(component_templates.keys()); } function list_components() { const result = {}; for (const name of component_classes.keys()) { result[name] = { has_class: true, has_template: component_templates.has(name) }; } for (const name of component_templates.keys()) { if (!result[name]) { result[name] = { has_class: false, has_template: true }; } } return result; } var _cid_increment = "aa"; function uid() { const current = _cid_increment; const chars = _cid_increment.split(""); let carry = true; for (let i = chars.length - 1; i >= 0 && carry; i--) { const char = chars[i]; if (char >= "a" && char < "z") { chars[i] = String.fromCharCode(char.charCodeAt(0) + 1); carry = false; } else if (char === "z") { chars[i] = "0"; carry = false; } else if (char >= "0" && char < "9") { chars[i] = String.fromCharCode(char.charCodeAt(0) + 1); carry = false; } else if (char === "9") { chars[i] = "a"; carry = true; } } if (carry) { chars.unshift("a"); } if (chars[0] >= "0" && chars[0] <= "9") { chars[0] = "a"; chars.unshift("a"); } _cid_increment = chars.join(""); return current; } function process_instructions(instructions, target, context, slots) { const html = []; const tagElements = {}; const components = {}; for (const instruction of instructions) { process_instruction_to_html(instruction, html, tagElements, components, context, slots); } target[0].innerHTML = html.join(""); for (const [tid, tagData] of Object.entries(tagElements)) { const el = target[0].querySelector(`[data-tid="${tid}"]`); if (el) { const element = $(el); el.removeAttribute("data-tid"); apply_attributes(element, tagData.attrs, context); } } for (const [cid, compData] of Object.entries(components)) { const el = target[0].querySelector(`[data-cid="${cid}"]`); if (el) { const element = $(el); el.removeAttribute("data-cid"); initialize_component(element, compData); } } } function process_instruction_to_html(instruction, html, tagElements, components, context, slots) { if (typeof instruction === "string") { html.push(instruction); } else if ("tag" in instruction) { process_tag_to_html(instruction, html, tagElements, components, context); } else if ("comp" in instruction) { process_component_to_html(instruction, html, components, context); } else if ("slot" in instruction) { process_slot_to_html(instruction, html, tagElements, components, context, slots); } else if ("rawtag" in instruction) { process_rawtag_to_html(instruction, html); } } function process_tag_to_html(instruction, html, tagElements, components, context) { const [tagName, attrs, selfClosing] = instruction.tag; const needsTracking = Object.keys(attrs).some((key) => key === "$id" || key.startsWith("$") || key.startsWith("@") || key.startsWith("on") || key.startsWith("data-bind-") || key.startsWith("data-on-")); html.push(`<${tagName}`); let tid = null; if (needsTracking) { tid = uid(); html.push(` data-tid="${tid}"`); tagElements[tid] = { attrs, context }; } for (const [key, value] of Object.entries(attrs)) { if (!key.startsWith("$") && !key.startsWith("on") && !key.startsWith("@") && !key.startsWith("data-bind-") && !key.startsWith("data-on-") && (typeof value === "string" || typeof value === "number")) { if (key === "id" && tid) { html.push(` id="${value}:${context._cid}"`); } else { html.push(` ${key}="${value}"`); } } } if (selfClosing) { html.push(" />"); } else { html.push(">"); } } function process_component_to_html(instruction, html, components, context) { const [componentName, props, contentFn] = instruction.comp; const cid = uid(); get_component_class(componentName) || Component; const template = get_template(componentName); const tagName = props._tag || template.tag || "div"; html.push(`<${tagName} data-cid="${cid}"`); if (props["data-id"]) { const baseId = props["data-id"]; html.push(` id="${props["id"]}" data-id="${baseId}"`); } else if (props["id"]) { html.push(` id="${props["id"]}"`); } html.push(">"); components[cid] = { name: componentName, props, contentFn, context }; } function process_slot_to_html(instruction, html, tagElements, components, context, parentSlots) { const [slotName] = instruction.slot; if (parentSlots && slotName in parentSlots) { const parentSlot = parentSlots[slotName]; const [, slotProps, contentFn] = parentSlot.slot; const [content] = contentFn.call(context, slotProps); for (const item of content) { process_instruction_to_html(item, html, tagElements, components, context); } } else if (slotName === "default" && instruction.slot[2]) { const [, , defaultFn] = instruction.slot; const [content] = defaultFn.call(context, {}); for (const item of content) { process_instruction_to_html(item, html, tagElements, components, context); } } } function process_rawtag_to_html(instruction, html) { const [tagName, attrs, rawContent] = instruction.rawtag; html.push(`<${tagName}`); for (const [key, value] of Object.entries(attrs)) { if (typeof value === "string" || typeof value === "number") { const escaped_value = String(value).replace(/"/g, """); html.push(` ${key}="${escaped_value}"`); } else if (typeof value === "boolean" && value) { html.push(` ${key}`); } } html.push(">"); const escaped_content = rawContent.replace(/&/g, "&").replace(//g, ">"); html.push(escaped_content); html.push(``); } function apply_attributes(element, attrs, context) { for (const [key, value] of Object.entries(attrs)) { if (key === "$id" || key === "id") { continue; } else if (key.startsWith("$")) { const dataKey = key.substring(1); element.data(dataKey, value); context.args[dataKey] = value; if (typeof value == "string" || typeof value == "number") { const attrValue = typeof value === "string" ? value.trim() : value; element.attr(`data-${dataKey}`, attrValue); } } else if (key.startsWith("data-on-")) { const eventName = key.substring(8); if (typeof value === "function") { element.on(eventName, function(e) { value.bind(context)(e, element); }); } else { console.warn("(JQHTML) Tried to assign a non function to on event handler " + key); } } else if (key.startsWith("on")) { const eventName = key.substring(2); if (typeof value === "function") { element.on(eventName, function(e) { value.bind(context)(e, element); }); } else { console.warn("(JQHTML) Tried to assign a non function to on event handler " + key); } } else if (key.startsWith("data-")) { const attrValue = typeof value === "string" ? value.trim() : value; element.attr(key, attrValue); const dataKey = key.substring(5); element.data(dataKey, value); context.args[dataKey] = value; } else if (key === "class") { const existingClasses = element.attr("class"); if (window.jqhtml?.debug?.enabled) { console.log(`[InstructionProcessor] Merging class attribute:`, { existing: existingClasses, new: value }); } if (!existingClasses) { const attrValue = typeof value === "string" ? value.trim() : value; element.attr("class", attrValue); } else { const existing = existingClasses.split(/\s+/).filter((c) => c); const newClasses = String(value).split(/\s+/).filter((c) => c); for (const newClass of newClasses) { if (!existing.includes(newClass)) { existing.push(newClass); } } element.attr("class", existing.join(" ")); } if (window.jqhtml?.debug?.enabled) { console.log(`[InstructionProcessor] Class after merge:`, element.attr("class")); } } else if (key === "style") { const existingStyle = element.attr("style"); if (!existingStyle) { const attrValue = typeof value === "string" ? value.trim() : value; element.attr("style", attrValue); } else { const styleMap = {}; existingStyle.split(";").forEach((rule) => { const [prop, val] = rule.split(":").map((s) => s.trim()); if (prop && val) { styleMap[prop] = val; } }); String(value).split(";").forEach((rule) => { const [prop, val] = rule.split(":").map((s) => s.trim()); if (prop && val) { styleMap[prop] = val; } }); const mergedStyle = Object.entries(styleMap).map(([prop, val]) => `${prop}: ${val}`).join("; "); element.attr("style", mergedStyle); } } else { if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") { const attrValue = typeof value === "string" ? value.trim() : String(value); element.attr(key, attrValue); } else if (typeof value === "object") { console.warn(`(JQHTML) Unexpected value for '${key}' on`, element); } } } } async function initialize_component(element, compData) { const { name, props, contentFn, context } = compData; const ComponentClass = get_component_class(name) || Component; const invocationAttrs = {}; for (const [key, value] of Object.entries(props)) { if (!key.startsWith("_")) { invocationAttrs[key] = value; } } if (window.jqhtml?.debug?.enabled) { console.log(`[InstructionProcessor] Applying invocation attributes for ${name}:`, invocationAttrs); } apply_attributes(element, invocationAttrs, context); const options = {}; if (contentFn) { options._innerhtml_function = contentFn; } if (ComponentClass.name !== name) { options._component_name = name; } const instance = new ComponentClass(element, options); instance._instantiator = context; await instance.boot(); } function extract_slots(instructions) { const slots = {}; for (const instruction of instructions) { if (typeof instruction === "object" && "slot" in instruction) { const [name] = instruction.slot; slots[name] = instruction; } } return slots; } var performanceMetrics = /* @__PURE__ */ new Map(); function devWarn(message) { if (typeof window !== "undefined" && window.JQHTML_SUPPRESS_WARNINGS) { return; } if (typeof process !== "undefined" && process.env && false) { return; } console.warn(`[JQHTML Dev Warning] ${message}`); } function getJqhtml$1() { if (typeof window !== "undefined" && window.jqhtml) { return window.jqhtml; } if (typeof globalThis !== "undefined" && globalThis.jqhtml) { return globalThis.jqhtml; } throw new Error("FATAL: window.jqhtml is not defined. The JQHTML runtime must be loaded before using debug features. Import and initialize @jqhtml/core before attempting to use debug functionality."); } function flashComponent(component, eventType) { const jqhtml2 = getJqhtml$1(); if (!jqhtml2?.debug?.flashComponents) return; const duration = jqhtml2.debug.flashDuration || 500; const colors = jqhtml2.debug.flashColors || {}; const color = colors[eventType] || (eventType === "create" ? "#3498db" : eventType === "render" ? "#27ae60" : "#9b59b6"); const originalBorder = component.$.css("border"); component.$.css({ "border": `2px solid ${color}`, "transition": `border ${duration}ms ease-out` }); setTimeout(() => { component.$.css("border", originalBorder || ""); }, duration); } function logLifecycle(component, phase, status) { const jqhtml2 = getJqhtml$1(); if (!jqhtml2?.debug) return; const shouldLog = jqhtml2.debug.logFullLifecycle || jqhtml2.debug.logCreationReady && (phase === "create" || phase === "ready"); if (!shouldLog) return; const componentName = component.constructor.name; const timestamp = (/* @__PURE__ */ new Date()).toISOString(); const prefix = `[JQHTML ${timestamp}]`; if (status === "start") { console.log(`${prefix} ${componentName}#${component._cid} \u2192 ${phase} starting...`); if (jqhtml2.debug.profilePerformance) { performanceMetrics.set(`${component._cid}_${phase}`, Date.now()); } } else { let message = `${prefix} ${componentName}#${component._cid} \u2713 ${phase} complete`; if (jqhtml2.debug.profilePerformance) { const startTime = performanceMetrics.get(`${component._cid}_${phase}`); if (startTime) { const duration = Date.now() - startTime; message += ` (${duration}ms)`; if (phase === "render" && jqhtml2.debug.highlightSlowRenders && duration > jqhtml2.debug.highlightSlowRenders) { console.warn(`${prefix} SLOW RENDER: ${componentName}#${component._cid} took ${duration}ms`); component.$.css("outline", "2px dashed red"); } } } console.log(message); if (jqhtml2.debug.flashComponents && (phase === "create" || phase === "render" || phase === "ready")) { flashComponent(component, phase); } } if (jqhtml2.debug.showComponentTree) { updateComponentTree(); } } function applyDebugDelay(phase) { const jqhtml2 = getJqhtml$1(); if (!jqhtml2?.debug) return; let delayMs = 0; switch (phase) { case "component": delayMs = jqhtml2.debug.delayAfterComponent || 0; break; case "render": delayMs = jqhtml2.debug.delayAfterRender || 0; break; case "rerender": delayMs = jqhtml2.debug.delayAfterRerender || 0; break; } if (delayMs > 0) { console.log(`[JQHTML Debug] Applying ${delayMs}ms delay after ${phase}`); } } function updateComponentTree() { console.log("[JQHTML Tree] Component hierarchy updated"); } var Component = class _Jqhtml_Component { constructor(element, args = {}) { this.data = {}; this._ready_state = 0; this._instantiator = null; this._dom_parent = null; this._dom_children = /* @__PURE__ */ new Set(); this._use_dom_fallback = false; this._stopped = false; this._booted = false; this._data_before_render = null; this._lifecycle_callbacks = /* @__PURE__ */ new Map(); this._lifecycle_states = /* @__PURE__ */ new Set(); this.__loading = false; this._did_first_render = false; this._render_count = 0; this._cid = this._generate_cid(); this._lifecycle_manager = LifecycleManager.get_instance(); if (element) { this.$ = $(element); } else { const div = document.createElement("div"); this.$ = $(div); } const dataAttrs = {}; if (this.$.length > 0) { const dataset = this.$[0].dataset || {}; for (const key in dataset) { if (key !== "cid" && key !== "tid" && key !== "componentName" && key !== "readyState") { const dataValue = this.$.data(key); if (dataValue !== void 0 && dataValue !== dataset[key]) { dataAttrs[key] = dataValue; } else { dataAttrs[key] = dataset[key]; } } } } let template_for_args; if (args._component_name) { template_for_args = get_template(args._component_name); } else { template_for_args = get_template_by_class(this.constructor); } const defineArgs = template_for_args?.defineArgs || {}; this.args = { ...defineArgs, ...dataAttrs, ...args }; for (const [key, value] of Object.entries(this.args)) { if (key === "cid" || key === "tid" || key === "componentName" || key === "readyState" || key.startsWith("_")) { continue; } if (typeof value === "string" || typeof value === "number") { try { const currentAttr = this.$.attr(`data-${key}`); if (currentAttr != value) { this.$.attr(`data-${key}`, String(value)); } } catch (e) { } } } this.$.data("_component", this); this._apply_css_classes(); this._apply_default_attributes(); this._set_attributes(); this._find_dom_parent(); this._log_lifecycle("construct", "complete"); } /** * Boot - Start the full component lifecycle * Called immediately after construction by instruction processor */ async boot() { if (this._booted) return; this._booted = true; await this._lifecycle_manager.boot_component(this); } // ------------------------------------------------------------------------- // Lifecycle Methods (called by LifecycleManager) // ------------------------------------------------------------------------- /** * Internal render phase - Create DOM structure * Called top-down (parent before children) when part of lifecycle * This is an internal method - users should call render() instead * * @param id Optional scoped ID - if provided, delegates to child component's _render() * @returns The current _render_count after incrementing (used to detect stale renders) * @private */ _render(id = null) { this._render_count++; const current_render_id = this._render_count; if (this._stopped) return current_render_id; if (id) { const $element = this.$sid(id); if ($element.length === 0) { throw new Error(`[JQHTML] render("${id}") - no such id. Component "${this.component_name()}" has no child element with $id="${id}".`); } const child = $element.data("_component"); if (!child) { throw new Error(`[JQHTML] render("${id}") - element is not a component or does not have $redrawable attribute set. Element with $id="${id}" exists but is not initialized as a component. Add $redrawable attribute or make it a proper component.`); } return child._render(); } if (this.__loading) { throw new Error(`[JQHTML] Component "${this.component_name()}" attempted to call render() during on_load(). on_load() should ONLY modify this.data. DOM updates happen automatically after on_load() completes. Fix: Remove the this.render() call from on_load(). The framework will automatically re-render if this.data changes during on_load().`); } this._log_lifecycle("render", "start"); if (!$.contains(document.documentElement, this.$[0])) { this._use_dom_fallback = true; } else { this._use_dom_fallback = false; } if (this._did_first_render) { this.$.find(".Component").each(function() { const child = $(this).data("_component"); if (child && !child._stopped) { child._stop(); } }); this.$[0].innerHTML = ""; } else { this._did_first_render = true; } this.$.removeClass("_Component_Stopped"); if (this._data_before_render === null) { this._data_before_render = JSON.stringify(this.data); } this._dom_children.clear(); let template_def; if (this.args._component_name) { template_def = get_template(this.args._component_name); } else { template_def = get_template_by_class(this.constructor); } if (template_def && template_def.render) { const jqhtml2 = { escape_html: (str) => { const div = document.createElement("div"); div.textContent = String(str); return div.innerHTML; } }; const defaultContent = () => ""; let [instructions, context] = template_def.render.bind(this)( this.data, this.args, this.args._innerhtml_function || defaultContent, // Content function with fallback jqhtml2 // Utilities object ); if (instructions && typeof instructions === "object" && instructions._slots && !Array.isArray(instructions)) { const componentName = template_def.name || this.args._component_name || this.constructor.name; console.log(`[JQHTML] Slot-only template detected for ${componentName}`); let parentTemplate = null; let parentTemplateName = null; if (template_def.extends) { console.log(`[JQHTML] Using explicit extends: ${template_def.extends}`); parentTemplate = get_template(template_def.extends); parentTemplateName = template_def.extends; } if (!parentTemplate) { let currentClass = Object.getPrototypeOf(this.constructor); while (currentClass && currentClass.name !== "Object" && currentClass.name !== "Component") { const className = currentClass.name; console.log(`[JQHTML] Checking parent: ${className}`); try { const classTemplate = get_template(className); if (classTemplate && classTemplate.name !== "Component") { console.log(`[JQHTML] Found parent template: ${className}`); parentTemplate = classTemplate; parentTemplateName = className; break; } } catch (error) { console.warn(`[JQHTML] Error finding parent template ${className}:`, error); } currentClass = Object.getPrototypeOf(currentClass); } } if (parentTemplate) { try { const childSlots = instructions._slots; const contentFunction = (slotName, data) => { if (childSlots[slotName] && typeof childSlots[slotName] === "function") { const [slotInstructions, slotContext] = childSlots[slotName](data); return [slotInstructions, slotContext]; } return ""; }; const [parentInstructions, parentContext] = parentTemplate.render.bind(this)( this.data, this.args, contentFunction, // Pass content function that invokes child slots jqhtml2 ); console.log(`[JQHTML] Parent template invoked successfully`); instructions = parentInstructions; context = parentContext; } catch (error) { console.warn(`[JQHTML] Error invoking parent template ${parentTemplateName}:`, error); instructions = []; } } else { console.warn(`[JQHTML] No parent template found for ${this.constructor.name}, rendering empty`); instructions = []; } } const flattenedInstructions = this._flatten_instructions(instructions); process_instructions(flattenedInstructions, this.$, this); } this._update_debug_attrs(); this._log_lifecycle("render", "complete"); const renderResult = this.on_render(); if (renderResult && typeof renderResult.then === "function") { console.warn(`[JQHTML] Component "${this.component_name()}" returned a Promise from on_render(). on_render() must be synchronous code. Remove 'async' from the function declaration.`); } this.trigger("render"); const isRerender = this._ready_state >= 3; applyDebugDelay(isRerender ? "rerender" : "render"); return current_render_id; } /** * Public render method - re-renders component and completes lifecycle * This is what users should call when they want to update a component. * * Lifecycle sequence: * 1. _render() - Updates DOM synchronously, calls on_render(), fires 'render' event * 2. Async continuation (fire and forget): * - _wait_for_children_ready() - Waits for all children to reach ready state * - on_ready() - Calls user's ready hook * - trigger('ready') - Fires ready event * * Returns immediately after _render() completes - does NOT wait for children */ render(id = null) { if (this._stopped) return; if (id) { const $element = this.$sid(id); if ($element.length === 0) { throw new Error(`[JQHTML] render("${id}") - no such id. Component "${this.component_name()}" has no child element with $id="${id}".`); } const child = $element.data("_component"); if (!child) { throw new Error(`[JQHTML] render("${id}") - element is not a component or does not have $redrawable attribute set. Element with $id="${id}" exists but is not initialized as a component. Add $redrawable attribute or make it a proper component.`); } return child.render(); } const render_id = this._render(); (async () => { await this._wait_for_children_ready(); if (this._render_count !== render_id) { return; } await this.on_ready(); await this.trigger("ready"); })(); } /** * Alias for render() - re-renders component with current data * Provided for API consistency and clarity */ redraw(id = null) { return this.render(id); } /** * Create phase - Quick setup, prepare UI * Called bottom-up (children before parent) */ async create() { if (this._stopped || this._ready_state >= 1) return; this._log_lifecycle("create", "start"); const result = this.on_create(); if (result && typeof result.then === "function") { console.warn(`[JQHTML] Component "${this.component_name()}" returned a Promise from on_create(). on_create() must be synchronous code. Remove 'async' from the function declaration.`); await result; } this._ready_state = 1; this._update_debug_attrs(); this._log_lifecycle("create", "complete"); this.trigger("create"); } /** * Load phase - Fetch data from APIs * Called bottom-up, fully parallel * NO DOM MODIFICATIONS ALLOWED IN THIS PHASE */ async load() { if (this._stopped || this._ready_state >= 2) return; this._log_lifecycle("load", "start"); const argsBeforeLoad = JSON.stringify(this.args); const propertiesBeforeLoad = new Set(Object.keys(this)); this.__loading = true; try { await this.on_load(); } finally { this.__loading = false; } const argsAfterLoad = JSON.stringify(this.args); const propertiesAfterLoad = Object.keys(this); if (argsBeforeLoad !== argsAfterLoad) { console.error(`[JQHTML] WARNING: Component "${this.component_name()}" modified this.args in on_load(). on_load() should ONLY modify this.data. The this.args property is read-only. Before: ${argsBeforeLoad} After: ${argsAfterLoad} Fix: Move your modifications to this.data instead.`); } const newProperties = propertiesAfterLoad.filter((prop) => !propertiesBeforeLoad.has(prop) && prop !== "data"); if (newProperties.length > 0) { console.error(`[JQHTML] WARNING: Component "${this.component_name()}" added new properties in on_load(). on_load() should ONLY modify this.data. New properties detected: ${newProperties.join(", ")} Fix: Store your data in this.data instead: \u274C this.${newProperties[0]} = value; \u2705 this.data.${newProperties[0]} = value;`); } this._ready_state = 2; this._update_debug_attrs(); this._log_lifecycle("load", "complete"); this.trigger("load"); } /** * Ready phase - Component fully initialized * Called bottom-up (children before parent) */ async ready() { if (this._stopped || this._ready_state >= 4) return; this._log_lifecycle("ready", "start"); await this._wait_for_children_ready(); await this.on_ready(); this._ready_state = 4; this._update_debug_attrs(); this._log_lifecycle("ready", "complete"); this.trigger("ready"); } /** * Wait for all child components to reach ready state * Ensures bottom-up ordering (children ready before parent) * @private */ async _wait_for_children_ready() { const children = this._get_dom_children(); if (children.length === 0) { return; } const ready_promises = []; for (const child of children) { if (child._ready_state >= 4) { continue; } const ready_promise = new Promise((resolve) => { child.on("ready", () => resolve()); }); ready_promises.push(ready_promise); } await Promise.all(ready_promises); } /** * Reinitialize the component - full reset and re-initialization * Wipes the innerHTML, resets data to empty, and runs full lifecycle */ async reinitialize() { if (this._stopped) return; this._log_lifecycle("reinitialize", "start"); this.$[0].innerHTML = ""; this.data = {}; this._ready_state = 0; this._data_before_render = null; this._dom_children.clear(); await this._render(); await this.create(); await this.load(); if (this.should_rerender()) { await this._render(); } await this.ready(); this._log_lifecycle("reinitialize", "complete"); } /** * Reload component - re-fetch data and re-render * Re-runs on_load(), always renders, and calls on_ready() */ async reload() { if (this._stopped) return; this._log_lifecycle("reload", "start"); const has_custom_on_load = this.on_load !== _Jqhtml_Component.prototype.on_load; if (has_custom_on_load) { const argsBeforeLoad = JSON.stringify(this.args); const propertiesBeforeLoad = new Set(Object.keys(this)); this.__loading = true; try { await this.on_load(); } finally { this.__loading = false; } const argsAfterLoad = JSON.stringify(this.args); const propertiesAfterLoad = Object.keys(this); if (argsBeforeLoad !== argsAfterLoad) { console.error(`[JQHTML] WARNING: Component "${this.component_name()}" modified this.args in on_load(). on_load() should ONLY modify this.data. The this.args property is read-only. Before: ${argsBeforeLoad} After: ${argsAfterLoad} Fix: Move your modifications to this.data instead.`); } const newProperties = propertiesAfterLoad.filter((prop) => !propertiesBeforeLoad.has(prop) && prop !== "data"); if (newProperties.length > 0) { console.error(`[JQHTML] WARNING: Component "${this.component_name()}" added new properties in on_load(). on_load() should ONLY modify this.data. New properties detected: ${newProperties.join(", ")} Fix: Store your data in this.data instead: \u274C this.${newProperties[0]} = value; \u2705 this.data.${newProperties[0]} = value;`); } } await this.render(); this._log_lifecycle("reload", "complete"); } /** * Destroy the component and cleanup * Called automatically by MutationObserver when component is removed from DOM * Can also be called manually for explicit cleanup */ /** * Internal stop method - stops just this component (no children) * Sets stopped flag, calls lifecycle hooks, but leaves DOM intact * @private */ _stop() { if (this._stopped) return; this._stopped = true; const has_custom_destroy = this.on_destroy !== _Jqhtml_Component.prototype.on_destroy; const has_destroy_callbacks = this._on_registered("destroy"); if (!has_custom_destroy && !has_destroy_callbacks) { this._lifecycle_manager.unregister_component(this); this._ready_state = 99; return; } this._log_lifecycle("destroy", "start"); this.$.addClass("_Component_Stopped"); this._lifecycle_manager.unregister_component(this); const destroyResult = this.on_destroy(); if (destroyResult && typeof destroyResult.then === "function") { console.warn(`[JQHTML] Component "${this.component_name()}" returned a Promise from on_destroy(). on_destroy() must be synchronous code. Remove 'async' from the function declaration.`); } this.trigger("destroy"); this.$.trigger("destroy"); if (this._dom_parent) { this._dom_parent._dom_children.delete(this); } this._ready_state = 99; this._update_debug_attrs(); this._log_lifecycle("destroy", "complete"); } /** * Stop component lifecycle - stops all descendant components then self * Leaves DOM intact, just stops lifecycle engine and fires cleanup hooks */ stop() { this.$.find(".Component").each(function() { const child = $(this).data("_component"); if (child && !child._stopped) { child._stop(); } }); this._stop(); } // ------------------------------------------------------------------------- // Overridable Lifecycle Hooks // ------------------------------------------------------------------------- on_render() { } on_create() { } async on_load() { } async on_ready() { } on_destroy() { } /** * Should component re-render after load? * By default, only re-renders if data has changed * Override to control re-rendering behavior */ should_rerender() { const currentDataState = JSON.stringify(this.data); const dataChanged = this._data_before_render !== currentDataState; if (dataChanged) { this._data_before_render = currentDataState; } return dataChanged; } // ------------------------------------------------------------------------- // Public API // ------------------------------------------------------------------------- /** * Get component name for debugging */ component_name() { return this.constructor.name; } /** * Emit a jQuery event from component root */ emit(event_name, data) { this._log_debug("emit", event_name, data); this.$.trigger(event_name, data); } /** * Register lifecycle event callback * Allowed events: 'render', 'create', 'load', 'ready', 'destroy' * Callbacks fire after the lifecycle method completes * If the event has already occurred, the callback fires immediately AND registers for future occurrences */ on(event_name, callback) { const allowed_events = ["render", "create", "load", "ready", "destroy"]; if (!allowed_events.includes(event_name)) { console.error(`[JQHTML] Component.on() only supports lifecycle events: ${allowed_events.join(", ")}. Received: ${event_name}`); return this; } if (!this._lifecycle_callbacks.has(event_name)) { this._lifecycle_callbacks.set(event_name, []); } this._lifecycle_callbacks.get(event_name).push(callback); if (this._lifecycle_states.has(event_name)) { try { callback(this); } catch (error) { console.error(`[JQHTML] Error in ${event_name} callback:`, error); } } return this; } /** * Trigger a lifecycle event - fires all registered callbacks * Marks event as occurred so future .on() calls fire immediately */ trigger(event_name) { this._lifecycle_states.add(event_name); const callbacks = this._lifecycle_callbacks.get(event_name); if (callbacks) { for (const callback of callbacks) { try { callback.bind(this)(this); } catch (error) { console.error(`[JQHTML] Error in ${event_name} callback:`, error); } } } } /** * Check if any callbacks are registered for a given event * Used to determine if cleanup logic needs to run */ _on_registered(event_name) { const callbacks = this._lifecycle_callbacks.get(event_name); return !!(callbacks && callbacks.length > 0); } /** * Find element by scoped ID * * Searches for elements with id="local_id:THIS_COMPONENT_CID" * * Example: * Template: * Rendered: * Access: this.$sid('save_btn') // Returns jQuery element * * Performance: Uses native document.getElementById() when component is in DOM, * falls back to jQuery.find() for components not yet attached to DOM. * * @param local_id The local ID (without _cid suffix) * @returns jQuery element with id="local_id:_cid", or empty jQuery object if not found */ $id(local_id) { const scopedId = `${local_id}:${this._cid}`; const el = document.getElementById(scopedId); if (el) { return $(el); } return this.$.find(`#${$.escapeSelector(scopedId)}`); } /** * Get component instance by scoped ID * * Convenience method that finds element by scoped ID and returns the component instance. * * Example: * Template: * Access: const user = this.id('active_user'); // Returns User_Card instance * user.data.name // Access component's data * * @param local_id The local ID (without _cid suffix) * @returns Component instance or null if not found or not a component */ id(local_id) { const element = this.$sid(local_id); const component = element.data("_component"); if (!component && element.length > 0) { console.warn(`Component ${this.constructor.name} tried to call .id('${local_id}') - ${local_id} exists, however, it is not a component or $redrawable. Did you forget to add $redrawable to the tag?`); } return component || null; } /** * Get the component that instantiated this component (rendered it in their template) * Returns null if component was created programmatically via $().component() */ instantiator() { return this._instantiator; } /** * Find descendant components by CSS selector */ find(selector) { const components = []; this.$.find(selector).each((_, el) => { const comp = $(el).data("_component"); if (comp instanceof _Jqhtml_Component) { components.push(comp); } }); return components; } /** * Find closest ancestor component matching selector */ closest(selector) { let current = this.$.parent(); while (current.length > 0) { if (current.is(selector)) { const comp = current.data("_component"); if (comp instanceof _Jqhtml_Component) { return comp; } } current = current.parent(); } return null; } // ------------------------------------------------------------------------- // Static Methods // ------------------------------------------------------------------------- /** * Get CSS class hierarchy for this component type */ static get_class_hierarchy() { const classes = []; let ctor = this; while (ctor) { if (!ctor.name || typeof ctor.name !== "string") { break; } if (ctor.name !== "Object" && ctor.name !== "") { let normalizedName = ctor.name; if (normalizedName === "_Jqhtml_Component" || normalizedName === "_Base_Jqhtml_Component") { normalizedName = "Component"; } classes.push(normalizedName); } const nextProto = Object.getPrototypeOf(ctor); if (!nextProto || nextProto === Object.prototype || nextProto.constructor === Object) { break; } ctor = nextProto; } return classes; } // ------------------------------------------------------------------------- // Private Implementation // ------------------------------------------------------------------------- _generate_cid() { return uid(); } /** * Flatten instruction array - converts ['_content', [...]] markers to flat array * Recursively flattens nested content from content() calls */ _flatten_instructions(instructions) { const result = []; for (const instruction of instructions) { if (Array.isArray(instruction) && instruction[0] === "_content" && Array.isArray(instruction[1])) { const contentInstructions = this._flatten_instructions(instruction[1]); result.push(...contentInstructions); } else { result.push(instruction); } } return result; } _apply_css_classes() { const hierarchy = this.constructor.get_class_hierarchy(); const classesToAdd = [...hierarchy]; if (this.args._component_name && this.args._component_name !== this.constructor.name) { classesToAdd.unshift(this.args._component_name); } const publicClasses = classesToAdd.filter((className) => { if (!className || typeof className !== "string") { console.warn("[JQHTML] Filtered out invalid class name:", className); return false; } return !className.startsWith("_"); }); if (publicClasses.length > 0) { this.$.addClass(publicClasses.join(" ")); } } _apply_default_attributes() { let template; if (this.args._component_name) { template = get_template(this.args._component_name); } else { template = get_template_by_class(this.constructor); } if (template && template.defaultAttributes) { const defineAttrs = { ...template.defaultAttributes }; delete defineAttrs.tag; if (window.jqhtml?.debug?.enabled) { const componentName = template.name || this.args._component_name || this.constructor.name; console.log(`[Component] Applying defaultAttributes for ${componentName}:`, defineAttrs); } for (const [key, value] of Object.entries(defineAttrs)) { if (key === "class") { const existingClasses = this.$.attr("class"); if (existingClasses) { const existing = existingClasses.split(/\s+/).filter((c) => c); const newClasses = String(value).split(/\s+/).filter((c) => c); for (const newClass of newClasses) { if (!existing.includes(newClass)) { existing.push(newClass); } } this.$.attr("class", existing.join(" ")); } else { this.$.attr("class", value); } } else if (key === "style") { const existingStyle = this.$.attr("style"); if (existingStyle) { const existingRules = /* @__PURE__ */ new Map(); existingStyle.split(";").forEach((rule) => { const [prop, val] = rule.split(":").map((s) => s.trim()); if (prop && val) existingRules.set(prop, val); }); String(value).split(";").forEach((rule) => { const [prop, val] = rule.split(":").map((s) => s.trim()); if (prop && val) existingRules.set(prop, val); }); const merged = Array.from(existingRules.entries()).map(([prop, val]) => `${prop}: ${val}`).join("; "); this.$.attr("style", merged); } else { this.$.attr("style", value); } } else if (key.startsWith("$") || key.startsWith("data-")) { const dataKey = key.startsWith("$") ? key.substring(1) : key.startsWith("data-") ? key.substring(5) : key; if (!(dataKey in this.args)) { this.args[dataKey] = value; this.$.data(dataKey, value); this.$.attr(key.startsWith("$") ? `data-${dataKey}` : key, String(value)); } } else { if (!this.$.attr(key)) { this.$.attr(key, value); } } } } } _set_attributes() { this.$.attr("data-cid", this._cid); if (window.jqhtml?.debug?.verbose) { this.$.attr("data-_lifecycle-state", this._ready_state.toString()); } } _update_debug_attrs() { if (window.jqhtml?.debug?.verbose) { this.$.attr("data-_lifecycle-state", this._ready_state.toString()); } } _find_dom_parent() { let current = this.$.parent(); while (current.length > 0) { const parent = current.data("_component"); if (parent instanceof _Jqhtml_Component) { this._dom_parent = parent; parent._dom_children.add(this); break; } current = current.parent(); } } /** * Get DOM children (components in DOM subtree) * Uses fast _dom_children registry when possible, falls back to DOM traversal for off-DOM components * @private - Used internally for lifecycle coordination */ _get_dom_children() { if (this._use_dom_fallback) { const directChildren = []; this.$.find(".Component").each((_, el) => { const $el = $(el); const comp = $el.data("_component"); if (comp instanceof _Jqhtml_Component) { const closestParent = $el.parent().closest(".Component"); if (closestParent.length === 0 || closestParent.data("_component") === this) { directChildren.push(comp); } } }); return directChildren; } const children = Array.from(this._dom_children); return children.filter((child) => { return $.contains(document.documentElement, child.$[0]); }); } _log_lifecycle(phase, status) { logLifecycle(this, phase, status); if (typeof window !== "undefined" && window.JQHTML_DEBUG) { window.JQHTML_DEBUG.log(this.component_name(), phase, status, { cid: this._cid, ready_state: this._ready_state, args: this.args }); } } _log_debug(action, ...args) { if (typeof window !== "undefined" && window.JQHTML_DEBUG) { window.JQHTML_DEBUG.log(this.component_name(), "debug", `${action}: ${args.map((a) => JSON.stringify(a)).join(", ")}`); } } }; async function process_slot_inheritance(component, childSlots) { let currentClass = Object.getPrototypeOf(component.constructor); console.log(`[JQHTML] Walking prototype chain for ${component.constructor.name}`); while (currentClass && currentClass !== Component && currentClass.name !== "Object") { const className = currentClass.name; console.log(`[JQHTML] Checking parent class: ${className}`); if (className === "_Jqhtml_Component" || className === "_Base_Jqhtml_Component") { currentClass = Object.getPrototypeOf(currentClass); continue; } try { const parentTemplate = get_template(className); console.log(`[JQHTML] Template found for ${className}:`, parentTemplate ? parentTemplate.name : "null"); if (parentTemplate && parentTemplate.name !== "Component") { console.log(`[JQHTML] Invoking parent template ${className}`); const [parentInstructions, parentContext] = parentTemplate.render.call( component, component.data, component.args, childSlots // Pass child slots as content parameter ); if (parentInstructions && typeof parentInstructions === "object" && parentInstructions._slots) { console.log(`[JQHTML] Parent also slot-only, recursing`); return await process_slot_inheritance(component, parentInstructions._slots); } console.log(`[JQHTML] Parent returned instructions, inheritance complete`); return [parentInstructions, parentContext]; } } catch (error) { console.warn(`[JQHTML] Error looking up parent template for ${className}:`, error); } currentClass = Object.getPrototypeOf(currentClass); } console.warn(`[JQHTML] No parent template found after walking chain`); return null; } async function render_template(component, template_fn) { let render_fn = template_fn; if (!render_fn) { const template_def = get_template_by_class(component.constructor); render_fn = template_def.render; } if (!render_fn) { return; } component.$.empty(); const defaultContent = () => ""; let [instructions, context] = render_fn.call( component, component.data, component.args, defaultContent // Default content function that returns empty string ); if (instructions && typeof instructions === "object" && instructions._slots) { console.log(`[JQHTML] Slot-only template detected for ${component.constructor.name}, invoking inheritance`); const result = await process_slot_inheritance(component, instructions._slots); if (result) { console.log(`[JQHTML] Parent template found, using parent instructions`); instructions = result[0]; context = result[1]; } else { console.warn(`[JQHTML] No parent template found for ${component.constructor.name}, rendering empty`); instructions = []; } } await process_instructions(instructions, component.$, component); await process_bindings(component); await attach_event_handlers(component); } async function process_bindings(component) { component.$.find("[data-bind-prop], [data-bind-value], [data-bind-text], [data-bind-html], [data-bind-class], [data-bind-style]").each((_, element) => { const el = $(element); const attrs = element.attributes; for (let i = 0; i < attrs.length; i++) { const attr = attrs[i]; if (attr.name.startsWith("data-bind-")) { const binding_type = attr.name.substring(10); const expression = attr.value; try { const value = evaluate_expression(expression, component); switch (binding_type) { case "prop": const prop_name = el.attr("data-bind-prop-name") || "value"; el.prop(prop_name, value); break; case "value": el.val(value); break; case "text": el.text(value); break; case "html": el.html(value); break; case "class": if (typeof value === "object") { Object.entries(value).forEach(([className, enabled]) => { el.toggleClass(className, !!enabled); }); } else { el.addClass(String(value)); } break; case "style": if (typeof value === "object") { el.css(value); } else { el.attr("style", String(value)); } break; default: el.attr(binding_type, value); } } catch (error) { console.error(`Error evaluating binding "${expression}":`, error); } } } }); } async function attach_event_handlers(component) { 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) => { const el = $(element); const attrs = element.attributes; for (let i = 0; i < attrs.length; i++) { const attr = attrs[i]; if (attr.name.startsWith("data-on-")) { const event_name = attr.name.substring(8); const handler_expr = attr.value; el.removeAttr(attr.name); el.on(event_name, function(event) { try { const handler = evaluate_handler(handler_expr, component); if (typeof handler === "function") { handler.call(component, event); } else { evaluate_expression(handler_expr, component, { $event: event }); } } catch (error) { console.error(`Error in ${event_name} handler "${handler_expr}":`, error); } }); } } }); } function evaluate_expression(expression, component, locals = {}) { const context = { // Component properties data: component.data, args: component.args, $: component.$, // Component methods emit: component.emit.bind(component), $id: component.$id.bind(component), // Locals (like $event) ...locals }; const keys = Object.keys(context); const values = Object.values(context); try { const fn = new Function(...keys, `return (${expression})`); return fn(...values); } catch (error) { console.error(`Invalid expression: ${expression}`, error); return void 0; } } function evaluate_handler(expression, component) { if (expression in component && typeof component[expression] === "function") { return component[expression]; } try { return new Function("$event", ` const { data, args, $, emit, $id } = this; ${expression} `).bind(component); } catch (error) { console.error(`Invalid handler: ${expression}`, error); return null; } } function escape_html(str) { const div = document.createElement("div"); div.textContent = str; return div.innerHTML; } function getJQuery() { if (typeof window !== "undefined" && window.$) { return window.$; } if (typeof window !== "undefined" && window.jQuery) { return window.jQuery; } throw new Error('FATAL: jQuery is not defined. jQuery must be loaded before using JQHTML. Add