Remove unused blade settings pages not linked from UI Convert remaining frontend pages to SPA actions Convert settings user_settings and general to SPA actions Convert settings profile pages to SPA actions Convert contacts and projects add/edit pages to SPA actions Convert clients add/edit page to SPA action with loading pattern Refactor component scoped IDs from $id to $sid Fix jqhtml comment syntax and implement universal error component system Update all application code to use new unified error system Remove all backwards compatibility - unified error system complete Phase 5: Remove old response classes Phase 3-4: Ajax response handler sends new format, old helpers deprecated Phase 2: Add client-side unified error foundation Phase 1: Add server-side unified error foundation Add unified Ajax error response system with constants 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
2204 lines
82 KiB
JavaScript
Executable File
2204 lines
82 KiB
JavaScript
Executable File
(() => {
|
|
// 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("></" + tagName + ">");
|
|
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, "<").replace(/>/g, ">");
|
|
html.push(escaped_content);
|
|
html.push(`</${tagName}>`);
|
|
}
|
|
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: <button $id="save_btn">Save</button>
|
|
* Rendered: <button id="save_btn:abc123" data-id="save_btn">Save</button>
|
|
* 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: <User_Card $id="active_user" />
|
|
* 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 <script src="https://code.jquery.com/jquery-3.7.1.min.js"><\/script> before loading JQHTML.');
|
|
}
|
|
function getJqhtml() {
|
|
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 JQHTML components. Ensure @jqhtml/core is imported and initialized before attempting to use debug features.");
|
|
}
|
|
var DebugOverlay = class _DebugOverlay {
|
|
constructor(options = {}) {
|
|
this.$container = null;
|
|
this.$statusIndicator = null;
|
|
this.$ = getJQuery();
|
|
if (!this.$) {
|
|
throw new Error("jQuery is required for DebugOverlay");
|
|
}
|
|
this.options = {
|
|
position: "bottom",
|
|
theme: "dark",
|
|
compact: false,
|
|
showStatus: true,
|
|
autoHide: false,
|
|
...options
|
|
};
|
|
}
|
|
/**
|
|
* Static method to show debug overlay (singleton pattern)
|
|
*/
|
|
static show(options) {
|
|
if (!_DebugOverlay.instance) {
|
|
_DebugOverlay.instance = new _DebugOverlay(options);
|
|
}
|
|
_DebugOverlay.instance.display();
|
|
return _DebugOverlay.instance;
|
|
}
|
|
/**
|
|
* Static method to hide debug overlay
|
|
*/
|
|
static hide() {
|
|
if (_DebugOverlay.instance) {
|
|
_DebugOverlay.instance.hide();
|
|
}
|
|
}
|
|
/**
|
|
* Static method to toggle debug overlay visibility
|
|
*/
|
|
static toggle() {
|
|
if (_DebugOverlay.instance && _DebugOverlay.instance.$container) {
|
|
if (_DebugOverlay.instance.$container.is(":visible")) {
|
|
_DebugOverlay.hide();
|
|
} else {
|
|
_DebugOverlay.instance.display();
|
|
}
|
|
} else {
|
|
_DebugOverlay.show();
|
|
}
|
|
}
|
|
/**
|
|
* Static method to destroy debug overlay
|
|
*/
|
|
static destroy() {
|
|
if (_DebugOverlay.instance) {
|
|
_DebugOverlay.instance.destroy();
|
|
_DebugOverlay.instance = null;
|
|
}
|
|
}
|
|
/**
|
|
* Display the debug overlay
|
|
*/
|
|
display() {
|
|
if (this.$container) {
|
|
this.$container.show();
|
|
return;
|
|
}
|
|
this.createOverlay();
|
|
if (this.options.showStatus) {
|
|
this.createStatusIndicator();
|
|
}
|
|
}
|
|
/**
|
|
* Hide the debug overlay
|
|
*/
|
|
hide() {
|
|
if (this.$container) {
|
|
this.$container.hide();
|
|
}
|
|
if (this.$statusIndicator) {
|
|
this.$statusIndicator.hide();
|
|
}
|
|
}
|
|
/**
|
|
* Remove the debug overlay completely
|
|
*/
|
|
destroy() {
|
|
if (this.$container) {
|
|
this.$container.remove();
|
|
this.$container = null;
|
|
}
|
|
if (this.$statusIndicator) {
|
|
this.$statusIndicator.remove();
|
|
this.$statusIndicator = null;
|
|
}
|
|
}
|
|
/**
|
|
* Update the status indicator
|
|
*/
|
|
updateStatus(mode) {
|
|
if (!this.$statusIndicator)
|
|
return;
|
|
this.$statusIndicator.text("Debug: " + mode);
|
|
this.$statusIndicator.attr("class", "jqhtml-debug-status" + (mode !== "Off" ? " active" : ""));
|
|
}
|
|
createOverlay() {
|
|
this.addStyles();
|
|
this.$container = this.$("<div>").addClass(`jqhtml-debug-overlay ${this.options.theme} ${this.options.position}`);
|
|
const $content = this.$("<div>").addClass("jqhtml-debug-content");
|
|
const $controls = this.$("<div>").addClass("jqhtml-debug-controls");
|
|
const $title = this.$("<span>").addClass("jqhtml-debug-title").html("<strong>\u{1F41B} JQHTML Debug:</strong>");
|
|
$controls.append($title);
|
|
const buttons = [
|
|
{ text: "Slow Motion + Flash", action: "enableSlowMotionDebug", class: "success" },
|
|
{ text: "Basic Debug", action: "enableBasicDebug", class: "" },
|
|
{ text: "Full Debug", action: "enableFullDebug", class: "" },
|
|
{ text: "Sequential", action: "enableSequentialMode", class: "" },
|
|
{ text: "Clear Debug", action: "clearAllDebug", class: "danger" },
|
|
{ text: "Settings", action: "showDebugInfo", class: "" }
|
|
];
|
|
buttons.forEach((btn) => {
|
|
const $button = this.$("<button>").text(btn.text).addClass("jqhtml-debug-btn" + (btn.class ? ` ${btn.class}` : "")).on("click", () => this.executeAction(btn.action));
|
|
$controls.append($button);
|
|
});
|
|
const $toggleBtn = this.$("<button>").text(this.options.compact ? "\u25BC" : "\u25B2").addClass("jqhtml-debug-toggle").on("click", () => this.toggle());
|
|
$controls.append($toggleBtn);
|
|
$content.append($controls);
|
|
this.$container.append($content);
|
|
this.$("body").append(this.$container);
|
|
}
|
|
createStatusIndicator() {
|
|
this.$statusIndicator = this.$("<div>").addClass("jqhtml-debug-status").text("Debug: Off").css({
|
|
position: "fixed",
|
|
top: "10px",
|
|
right: "10px",
|
|
background: "#2c3e50",
|
|
color: "white",
|
|
padding: "5px 10px",
|
|
borderRadius: "4px",
|
|
fontSize: "0.75rem",
|
|
zIndex: "10001",
|
|
opacity: "0.8",
|
|
fontFamily: "monospace"
|
|
});
|
|
this.$("body").append(this.$statusIndicator);
|
|
}
|
|
addStyles() {
|
|
if (this.$("#jqhtml-debug-styles").length > 0)
|
|
return;
|
|
const $style = this.$("<style>").attr("id", "jqhtml-debug-styles").text('.jqhtml-debug-overlay {position: fixed;left: 0;right: 0;z-index: 10000;font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, monospace;font-size: 0.8rem;box-shadow: 0 2px 10px rgba(0,0,0,0.2);}.jqhtml-debug-overlay.top {top: 0;}.jqhtml-debug-overlay.bottom {bottom: 0;}.jqhtml-debug-overlay.dark {background: #34495e;color: #ecf0f1;}.jqhtml-debug-overlay.light {background: #f8f9fa;color: #333;border-bottom: 1px solid #dee2e6;}.jqhtml-debug-content {padding: 0.5rem 1rem;}.jqhtml-debug-controls {display: flex;flex-wrap: wrap;gap: 8px;align-items: center;}.jqhtml-debug-title {margin-right: 10px;font-weight: bold;}.jqhtml-debug-btn {padding: 4px 8px;border: none;border-radius: 3px;background: #3498db;color: white;cursor: pointer;font-size: 0.75rem;transition: background 0.2s;}.jqhtml-debug-btn:hover {background: #2980b9;}.jqhtml-debug-btn.success {background: #27ae60;}.jqhtml-debug-btn.success:hover {background: #229954;}.jqhtml-debug-btn.danger {background: #e74c3c;}.jqhtml-debug-btn.danger:hover {background: #c0392b;}.jqhtml-debug-toggle {padding: 4px 8px;border: none;border-radius: 3px;background: #7f8c8d;color: white;cursor: pointer;font-size: 0.75rem;margin-left: auto;}.jqhtml-debug-toggle:hover {background: #6c7b7d;}.jqhtml-debug-status.active {background: #27ae60 !important;}@media (max-width: 768px) {.jqhtml-debug-controls {flex-direction: column;align-items: flex-start;}.jqhtml-debug-title {margin-bottom: 5px;}}');
|
|
this.$("head").append($style);
|
|
}
|
|
toggle() {
|
|
this.options.compact = !this.options.compact;
|
|
const $toggleBtn = this.$container.find(".jqhtml-debug-toggle");
|
|
$toggleBtn.text(this.options.compact ? "\u25BC" : "\u25B2");
|
|
const $buttons = this.$container.find(".jqhtml-debug-btn");
|
|
if (this.options.compact) {
|
|
$buttons.hide();
|
|
} else {
|
|
$buttons.show();
|
|
}
|
|
}
|
|
executeAction(action) {
|
|
const jqhtml2 = getJqhtml();
|
|
if (!jqhtml2) {
|
|
console.warn("JQHTML not available - make sure it's loaded and exposed globally");
|
|
return;
|
|
}
|
|
switch (action) {
|
|
case "enableSlowMotionDebug":
|
|
jqhtml2.setDebugSettings({
|
|
logFullLifecycle: true,
|
|
sequentialProcessing: true,
|
|
delayAfterComponent: 150,
|
|
delayAfterRender: 200,
|
|
delayAfterRerender: 250,
|
|
flashComponents: true,
|
|
flashDuration: 800,
|
|
flashColors: {
|
|
create: "#3498db",
|
|
render: "#27ae60",
|
|
ready: "#9b59b6"
|
|
},
|
|
profilePerformance: true,
|
|
highlightSlowRenders: 30,
|
|
logDispatch: true
|
|
});
|
|
this.updateStatus("Slow Motion");
|
|
console.log("\u{1F41B} Slow Motion Debug Mode Enabled");
|
|
break;
|
|
case "enableBasicDebug":
|
|
jqhtml2.enableDebugMode("basic");
|
|
this.updateStatus("Basic");
|
|
console.log("\u{1F41B} Basic Debug Mode Enabled");
|
|
break;
|
|
case "enableFullDebug":
|
|
jqhtml2.enableDebugMode("full");
|
|
this.updateStatus("Full");
|
|
console.log("\u{1F41B} Full Debug Mode Enabled");
|
|
break;
|
|
case "enableSequentialMode":
|
|
jqhtml2.setDebugSettings({
|
|
logCreationReady: true,
|
|
sequentialProcessing: true,
|
|
flashComponents: true,
|
|
profilePerformance: true
|
|
});
|
|
this.updateStatus("Sequential");
|
|
console.log("\u{1F41B} Sequential Processing Mode Enabled");
|
|
break;
|
|
case "clearAllDebug":
|
|
jqhtml2.clearDebugSettings();
|
|
this.updateStatus("Off");
|
|
console.log("\u{1F41B} All Debug Modes Disabled");
|
|
break;
|
|
case "showDebugInfo":
|
|
const settings = JSON.stringify(jqhtml2.debug, null, 2);
|
|
console.log("\u{1F41B} Current Debug Settings:", settings);
|
|
alert("Debug settings logged to console:\n\n" + (Object.keys(jqhtml2.debug).length > 0 ? settings : "No debug settings active"));
|
|
break;
|
|
}
|
|
}
|
|
};
|
|
DebugOverlay.instance = null;
|
|
if (typeof window !== "undefined") {
|
|
const urlParams = new URLSearchParams(window.location.search);
|
|
if (urlParams.get("debug") === "true" || urlParams.get("jqhtml-debug") === "true") {
|
|
document.addEventListener("DOMContentLoaded", () => {
|
|
DebugOverlay.show();
|
|
});
|
|
}
|
|
}
|
|
function init_jquery_plugin(jQuery) {
|
|
if (!jQuery || !jQuery.fn) {
|
|
throw new Error("jQuery is required for JQHTML. Please ensure jQuery is loaded before initializing JQHTML.");
|
|
}
|
|
if (typeof window !== "undefined" && window.$ !== jQuery && !jQuery.__jqhtml_checked) {
|
|
devWarn('jQuery instance appears to be bundled with webpack/modules rather than loaded globally.\nFor best compatibility, it is recommended to:\n1. Include jQuery via <script> tag from a CDN (UMD format)\n2. Configure webpack with: externals: { jquery: "$" }\n3. Remove jquery from package.json dependencies\n\nTo suppress this warning, set: window.JQHTML_SUPPRESS_WARNINGS = true');
|
|
jQuery.__jqhtml_checked = true;
|
|
}
|
|
const _jqhtml_original_jquery = jQuery;
|
|
const JQueryWithComponentSupport = function(selector, context) {
|
|
if (selector && typeof selector === "object" && selector.$ && typeof selector.$id === "function" && typeof selector.id === "function") {
|
|
return selector.$;
|
|
}
|
|
return new _jqhtml_original_jquery(selector, context);
|
|
};
|
|
Object.setPrototypeOf(JQueryWithComponentSupport, _jqhtml_original_jquery);
|
|
for (const key in _jqhtml_original_jquery) {
|
|
if (_jqhtml_original_jquery.hasOwnProperty(key)) {
|
|
JQueryWithComponentSupport[key] = _jqhtml_original_jquery[key];
|
|
}
|
|
}
|
|
JQueryWithComponentSupport.prototype = _jqhtml_original_jquery.prototype;
|
|
JQueryWithComponentSupport.fn = _jqhtml_original_jquery.fn;
|
|
if (typeof window !== "undefined") {
|
|
window.jQuery = JQueryWithComponentSupport;
|
|
window.$ = JQueryWithComponentSupport;
|
|
}
|
|
jQuery = JQueryWithComponentSupport;
|
|
const originalVal = jQuery.fn.val;
|
|
jQuery.fn.val = function(value) {
|
|
if (arguments.length === 0) {
|
|
const firstEl = this.first();
|
|
if (firstEl.length === 0)
|
|
return void 0;
|
|
const component = firstEl.data("_component");
|
|
const tagName = firstEl.prop("tagName");
|
|
if (component && typeof component.val === "function" && tagName !== "INPUT" && tagName !== "TEXTAREA") {
|
|
return component.val();
|
|
}
|
|
return originalVal.call(this);
|
|
} else {
|
|
this.each(function() {
|
|
const $el = jQuery(this);
|
|
const component = $el.data("_component");
|
|
const tagName = $el.prop("tagName");
|
|
if (component && typeof component.val === "function" && tagName !== "INPUT" && tagName !== "TEXTAREA") {
|
|
component.val(value);
|
|
} else {
|
|
originalVal.call($el, value);
|
|
}
|
|
});
|
|
return this;
|
|
}
|
|
};
|
|
jQuery.fn.component = function(componentOrName, args = {}) {
|
|
const element = this.first ? this.first() : this;
|
|
if (!componentOrName) {
|
|
if (element.length === 0) {
|
|
return null;
|
|
}
|
|
const comp = element.data("_component");
|
|
return comp || null;
|
|
}
|
|
const existingComponent = element.data("_component");
|
|
if (existingComponent) {
|
|
return existingComponent;
|
|
}
|
|
let ComponentClass;
|
|
let componentName;
|
|
if (typeof componentOrName === "string") {
|
|
componentName = componentOrName;
|
|
const found = get_component_class(componentOrName);
|
|
args = { ...args, _component_name: componentName };
|
|
if (!found) {
|
|
ComponentClass = Component;
|
|
} else {
|
|
ComponentClass = found;
|
|
}
|
|
} else {
|
|
ComponentClass = componentOrName;
|
|
}
|
|
let targetElement = element;
|
|
if (componentName) {
|
|
const template = get_template(componentName);
|
|
const expectedTag = args._tag || template.tag || "div";
|
|
const currentTag = element.prop("tagName").toLowerCase();
|
|
if (currentTag !== expectedTag.toLowerCase()) {
|
|
if (args._inner_html) {
|
|
const newElement = jQuery(`<${expectedTag}></${expectedTag}>`);
|
|
const oldEl = element[0];
|
|
if (oldEl && oldEl.attributes) {
|
|
for (let i = 0; i < oldEl.attributes.length; i++) {
|
|
const attr = oldEl.attributes[i];
|
|
newElement.attr(attr.name, attr.value);
|
|
}
|
|
}
|
|
newElement.html(element.html());
|
|
element.replaceWith(newElement);
|
|
targetElement = newElement;
|
|
} else {
|
|
console.warn(`[JQHTML] Component '${componentName}' expects tag '<${expectedTag}>' but element is '<${currentTag}>'. Element tag will not be changed. Consider using the correct tag.`);
|
|
}
|
|
}
|
|
}
|
|
const component = new ComponentClass(targetElement, args);
|
|
component.boot();
|
|
applyDebugDelay("component");
|
|
return component;
|
|
};
|
|
const _jqhtml_jquery_overrides = {};
|
|
const dom_insertion_methods = ["append", "prepend", "before", "after", "replaceWith"];
|
|
for (const fnname of dom_insertion_methods) {
|
|
_jqhtml_jquery_overrides[fnname] = jQuery.fn[fnname];
|
|
jQuery.fn[fnname] = function(...args) {
|
|
const resolvedArgs = args.map((arg) => {
|
|
if (arg && typeof arg === "object" && arg instanceof Component) {
|
|
return arg.$;
|
|
}
|
|
return arg;
|
|
});
|
|
const $elements = resolvedArgs.filter((arg) => arg instanceof jQuery);
|
|
const ret = _jqhtml_jquery_overrides[fnname].apply(this, resolvedArgs);
|
|
for (const $e of $elements) {
|
|
if ($e.closest("html").length > 0) {
|
|
$e.find(".Component").addBack(".Component").each(function() {
|
|
const $comp = jQuery(this);
|
|
const component = $comp.data("_component");
|
|
if (component && !component._ready_state) {
|
|
component.boot();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
return ret;
|
|
};
|
|
}
|
|
jQuery.fn.shallowFind = function(selector) {
|
|
const results = [];
|
|
this.each(function() {
|
|
const traverse = (parent) => {
|
|
for (let i = 0; i < parent.children.length; i++) {
|
|
const child = parent.children[i];
|
|
if (jQuery(child).is(selector)) {
|
|
results.push(child);
|
|
} else {
|
|
traverse(child);
|
|
}
|
|
}
|
|
};
|
|
traverse(this);
|
|
});
|
|
return jQuery(results);
|
|
};
|
|
const originalEmpty = jQuery.fn.empty;
|
|
const originalHtml = jQuery.fn.html;
|
|
const originalText = jQuery.fn.text;
|
|
jQuery.fn.empty = function() {
|
|
return this.each(function() {
|
|
jQuery(this).find(".Component").each(function() {
|
|
const component = jQuery(this).data("_component");
|
|
if (component && !component._stopped) {
|
|
component._stop();
|
|
}
|
|
});
|
|
originalEmpty.call(jQuery(this));
|
|
});
|
|
};
|
|
jQuery.fn.html = function(value) {
|
|
if (arguments.length === 0) {
|
|
return originalHtml.call(this);
|
|
}
|
|
return this.each(function() {
|
|
jQuery(this).empty();
|
|
originalHtml.call(jQuery(this), value);
|
|
});
|
|
};
|
|
jQuery.fn.text = function(value) {
|
|
if (arguments.length === 0) {
|
|
return originalText.call(this);
|
|
}
|
|
return this.each(function() {
|
|
jQuery(this).empty();
|
|
originalText.call(jQuery(this), value);
|
|
});
|
|
};
|
|
}
|
|
if (typeof window !== "undefined" && window.jQuery) {
|
|
init_jquery_plugin(window.jQuery);
|
|
}
|
|
var version = "2.2.185";
|
|
var jqhtml = {
|
|
// Core
|
|
Component,
|
|
LifecycleManager,
|
|
// Registry
|
|
register_component,
|
|
get_component_class,
|
|
register_template,
|
|
get_template,
|
|
get_template_by_class,
|
|
create_component,
|
|
has_component,
|
|
get_component_names,
|
|
get_registered_templates,
|
|
list_components,
|
|
// Template system
|
|
process_instructions,
|
|
extract_slots,
|
|
render_template,
|
|
escape_html,
|
|
// Version property - internal
|
|
__version: version,
|
|
// Debug settings
|
|
debug: {
|
|
enabled: false,
|
|
verbose: false
|
|
},
|
|
// Debug helper functions (mainly for internal use but exposed for advanced debugging)
|
|
setDebugSettings(settings) {
|
|
Object.assign(this.debug, settings);
|
|
},
|
|
enableDebugMode(level = "basic") {
|
|
if (level === "basic") {
|
|
this.debug.logCreationReady = true;
|
|
this.debug.logDispatch = true;
|
|
this.debug.flashComponents = true;
|
|
} else {
|
|
this.debug.logFullLifecycle = true;
|
|
this.debug.logDispatchVerbose = true;
|
|
this.debug.flashComponents = true;
|
|
this.debug.profilePerformance = true;
|
|
this.debug.traceDataFlow = true;
|
|
}
|
|
},
|
|
clearDebugSettings() {
|
|
this.debug = {};
|
|
},
|
|
// Debug overlay methods
|
|
showDebugOverlay(options) {
|
|
return DebugOverlay.show(options);
|
|
},
|
|
hideDebugOverlay() {
|
|
return DebugOverlay.hide();
|
|
},
|
|
// Export DebugOverlay class for direct access
|
|
DebugOverlay,
|
|
// Install globals function
|
|
installGlobals() {
|
|
if (typeof window !== "undefined") {
|
|
window.jqhtml = this;
|
|
window.Component = Component;
|
|
window.Jqhtml_LifecycleManager = LifecycleManager;
|
|
}
|
|
},
|
|
// Version display function - shows version of core library and all registered templates
|
|
_version() {
|
|
console.log(`JQHTML Core v${this.__version}`);
|
|
console.log("Registered Templates:");
|
|
const templateNames = get_component_names();
|
|
if (templateNames.length === 0) {
|
|
console.log(" (no templates registered)");
|
|
} else {
|
|
for (const name of templateNames) {
|
|
const template = get_template(name);
|
|
const templateVersion = template ? template._jqhtml_version || "unknown" : "unknown";
|
|
console.log(` - ${name}: v${templateVersion}`);
|
|
}
|
|
}
|
|
return this.__version;
|
|
},
|
|
// Public version function - returns the stamped version number
|
|
version() {
|
|
return version;
|
|
}
|
|
};
|
|
if (typeof window !== "undefined" && !window.jqhtml) {
|
|
window.jqhtml = jqhtml;
|
|
window.Component = Component;
|
|
window.Component = Component;
|
|
window.Jqhtml_LifecycleManager = LifecycleManager;
|
|
if (jqhtml.debug?.enabled) {
|
|
console.log("[JQHTML] Auto-registered window.jqhtml global for template compatibility");
|
|
}
|
|
}
|
|
|
|
// storage/rsx-tmp/npm-compile/entry_6459e8ed0f60bda4f121420766012d53.js
|
|
window._rsx_npm = window._rsx_npm || {};
|
|
window._rsx_npm.jqhtml = jqhtml;
|
|
window._rsx_npm._Base_Jqhtml_Component = Component;
|
|
})();
|