(function () { const config = window.pageEditorConfig; const initialData = window.pageEditorData; const backendUrl = (window.pageEditorBackendUrl || "").replace(/\/$/, ""); const form = document.getElementById("pageContentForm"); const pageJsonInput = document.getElementById("pageJson"); const activeTabInput = document.getElementById("activeTabInput"); if (!config || !initialData || !form || !pageJsonInput || !activeTabInput) { return; } const state = JSON.parse(JSON.stringify(initialData)); const iconOptions = Array.from( new Set( (config.tabs || []) .flatMap((tab) => collectIcons(tab.schema)) .filter(Boolean), ), ); ensureIconDatalist(iconOptions); renderAllSections(); document.querySelectorAll("[data-tab-key]").forEach((tabTrigger) => { tabTrigger.addEventListener("shown.bs.tab", function () { activeTabInput.value = this.dataset.tabKey; }); }); form.addEventListener("submit", function () { pageJsonInput.value = JSON.stringify(state); }); form.addEventListener("reset", function () { window.setTimeout(function () { Object.keys(state).forEach((key) => delete state[key]); Object.assign(state, JSON.parse(JSON.stringify(initialData))); renderAllSections(); }, 0); }); function renderAllSections() { config.tabs.forEach((tab) => renderSection(tab.key)); } function renderSection(tabKey) { const tab = config.tabs.find((item) => item.key === tabKey); const container = document.querySelector( `.page-editor-section[data-section-key="${tabKey}"]`, ); if (!tab || !container) return; container.innerHTML = ""; renderField(tab.schema, container, state, tab.schema.key, tabKey); } function renderField(schema, container, parent, key, tabKey) { if (schema.type === "object") { if (!isObject(parent[key])) { parent[key] = {}; } const groupWrapper = document.createElement("div"); groupWrapper.className = "row g-3"; container.appendChild(groupWrapper); (schema.fields || []).forEach((field) => { renderField(field, groupWrapper, parent[key], field.key, tabKey); }); return; } if (schema.type === "array") { if (!Array.isArray(parent[key])) { parent[key] = []; } const col = createCol(schema.colClass || "col-12"); const card = document.createElement("div"); card.className = "border rounded-3 bg-light-subtle p-3"; const header = document.createElement("div"); header.className = "d-flex justify-content-between align-items-center mb-3"; header.innerHTML = `
${schema.helpText ? `
${escapeHtml(schema.helpText)}
` : ""}
`; header.querySelector("button").addEventListener("click", function () { parent[key].push(createDefaultValue(schema.itemSchema)); renderSection(tabKey); }); card.appendChild(header); if (parent[key].length === 0) { const empty = document.createElement("div"); empty.className = "text-muted small"; empty.textContent = `No ${schema.itemLabel || "items"} yet.`; card.appendChild(empty); } else { parent[key].forEach((item, index) => { const itemCard = document.createElement("div"); itemCard.className = "card shadow-sm border-0 mb-3"; const itemHeader = document.createElement("div"); itemHeader.className = "card-header bg-white d-flex justify-content-between align-items-center"; itemHeader.innerHTML = ` ${escapeHtml(schema.itemLabel || "Item")} ${index + 1} `; itemHeader.querySelector("button").addEventListener("click", function () { parent[key].splice(index, 1); renderSection(tabKey); }); const itemBody = document.createElement("div"); itemBody.className = "card-body"; if (schema.itemSchema.type === "primitive") { renderPrimitiveArrayItem(schema, itemBody, parent[key], index); } else if (schema.itemSchema.type === "variant") { renderVariantArrayItem(schema.itemSchema, itemBody, parent[key], index, tabKey); } else { const bodyRow = document.createElement("div"); bodyRow.className = "row g-3"; itemBody.appendChild(bodyRow); (schema.itemSchema.fields || []).forEach((field) => { renderField(field, bodyRow, parent[key][index], field.key, tabKey); }); } itemCard.appendChild(itemHeader); itemCard.appendChild(itemBody); card.appendChild(itemCard); }); } col.appendChild(card); container.appendChild(col); return; } if (schema.type === "checkbox") { renderCheckbox(schema, container, parent, key); return; } renderLeafField(schema, container, parent, key); } function renderPrimitiveArrayItem(arraySchema, container, targetArray, index) { const fieldSchema = arraySchema.itemSchema; const row = document.createElement("div"); row.className = "row g-3"; container.appendChild(row); const holder = { value: targetArray[index] || "" }; renderLeafField( { key: "value", label: fieldSchema.label || arraySchema.itemLabel || "Value", type: fieldSchema.fieldType || "text", maxLength: fieldSchema.maxLength, placeholder: fieldSchema.placeholder, helpText: fieldSchema.helpText, rows: fieldSchema.fieldType === "textarea" ? 3 : undefined, }, row, holder, "value", ); const input = row.querySelector("input, textarea"); if (input) { input.addEventListener("input", function () { targetArray[index] = holder.value; }); input.addEventListener("change", function () { targetArray[index] = holder.value; }); } } function renderVariantArrayItem(variantSchema, container, targetArray, index, tabKey) { const item = targetArray[index]; if (!isObject(item)) { targetArray[index] = {}; } const currentType = targetArray[index][variantSchema.discriminator] || variantSchema.options[0].value; targetArray[index][variantSchema.discriminator] = currentType; const currentVariant = variantSchema.variants[currentType]; const typeRow = document.createElement("div"); typeRow.className = "row g-3 mb-2"; container.appendChild(typeRow); renderLeafField( { key: variantSchema.discriminator, label: "Section Type", type: "select", options: variantSchema.options, }, typeRow, targetArray[index], variantSchema.discriminator, ); const selectInput = typeRow.querySelector("select"); if (selectInput) { selectInput.addEventListener("change", function () { const newType = this.value; targetArray[index] = { type: newType }; renderSection(tabKey); }); } if (currentVariant && currentVariant.schema) { const sectionRow = document.createElement("div"); sectionRow.className = "row g-3"; container.appendChild(sectionRow); (currentVariant.schema.fields || []).forEach((field) => { renderField(field, sectionRow, targetArray[index], field.key, tabKey); }); } } function renderLeafField(schema, container, parent, key) { if (schema.type === "hidden") { parent[key] = parent[key] || ""; return; } if (typeof parent[key] === "undefined" || parent[key] === null) { parent[key] = schema.type === "number" ? 0 : ""; } const col = createCol(schema.colClass || inferColClass(schema.type)); const label = document.createElement("label"); label.className = "form-label fw-semibold"; label.textContent = schema.label || key; col.appendChild(label); if (schema.type === "textarea") { const textarea = document.createElement("textarea"); textarea.className = "form-control"; textarea.rows = schema.rows || 4; textarea.value = parent[key] || ""; if (schema.placeholder) textarea.placeholder = schema.placeholder; if (schema.maxLength) textarea.maxLength = schema.maxLength; textarea.addEventListener("input", function () { parent[key] = textarea.value; updateCounter(counter, textarea.value.length, schema.maxLength); }); col.appendChild(textarea); const counter = appendHelp(col, schema, parent[key]); container.appendChild(col); return; } if (schema.type === "image") { const group = document.createElement("div"); group.className = "input-group"; const input = document.createElement("input"); input.type = "text"; input.className = "form-control"; input.value = parent[key] || ""; input.addEventListener("input", function () { parent[key] = input.value; preview.src = resolveImageUrl(input.value); preview.classList.toggle("d-none", !input.value); }); const button = document.createElement("button"); button.type = "button"; button.className = "btn btn-outline-primary"; button.innerHTML = 'Upload'; button.addEventListener("click", function () { openImagePicker(schema.imageType || config.imageType, function (path) { parent[key] = path; input.value = path; preview.src = resolveImageUrl(path); preview.classList.toggle("d-none", !path); }); }); group.appendChild(input); group.appendChild(button); col.appendChild(group); const preview = document.createElement("img"); preview.className = "img-thumbnail uploaded-preview mt-2"; preview.style.maxHeight = "200px"; preview.src = resolveImageUrl(parent[key]); preview.classList.toggle("d-none", !parent[key]); col.appendChild(preview); appendHelp(col, schema, parent[key], schema.imageHint); container.appendChild(col); return; } const input = schema.type === "select" ? document.createElement("select") : document.createElement("input"); input.className = "form-control"; if (schema.type === "select") { (schema.options || []).forEach((option) => { const optionEl = document.createElement("option"); if (typeof option === "string") { optionEl.value = option; optionEl.textContent = option; } else { optionEl.value = option.value; optionEl.textContent = option.label; } input.appendChild(optionEl); }); input.value = parent[key] || input.options[0]?.value || ""; parent[key] = input.value; input.addEventListener("change", function () { parent[key] = input.value; }); } else { input.type = schema.type === "url" || schema.type === "number" || schema.type === "color" ? schema.type : "text"; input.value = parent[key] || ""; if (schema.placeholder) input.placeholder = schema.placeholder; if (schema.maxLength) input.maxLength = schema.maxLength; if (schema.step) input.step = schema.step; if (schema.type === "icon") { input.setAttribute("list", "cms-icon-options"); } input.addEventListener("input", function () { parent[key] = schema.type === "number" ? Number(input.value || 0) : input.value; updateCounter(counter, String(input.value || "").length, schema.maxLength); }); } col.appendChild(input); const counter = appendHelp(col, schema, parent[key]); container.appendChild(col); } function renderCheckbox(schema, container, parent, key) { if (typeof parent[key] !== "boolean") { parent[key] = Boolean(parent[key]); } const col = createCol(schema.colClass || "col-12"); const wrapper = document.createElement("div"); wrapper.className = "form-check mt-4"; const input = document.createElement("input"); input.type = "checkbox"; input.className = "form-check-input"; input.checked = parent[key]; input.addEventListener("change", function () { parent[key] = input.checked; }); const label = document.createElement("label"); label.className = "form-check-label fw-semibold"; label.textContent = schema.label || key; wrapper.appendChild(input); wrapper.appendChild(label); col.appendChild(wrapper); if (schema.helpText) { const help = document.createElement("div"); help.className = "form-text"; help.textContent = schema.helpText; col.appendChild(help); } container.appendChild(col); } function appendHelp(col, schema, value, extraHint) { const wrapper = document.createElement("div"); wrapper.className = "d-flex justify-content-between gap-3"; const help = document.createElement("div"); help.className = "form-text"; help.textContent = [schema.helpText, extraHint].filter(Boolean).join(" "); wrapper.appendChild(help); let counter = null; if (schema.maxLength) { counter = document.createElement("div"); counter.className = "form-text text-end ms-auto"; updateCounter(counter, String(value || "").length, schema.maxLength); wrapper.appendChild(counter); } if (help.textContent || counter) { col.appendChild(wrapper); } return counter; } function updateCounter(counter, currentLength, maxLength) { if (!counter || !maxLength) return; counter.textContent = `${currentLength}/${maxLength}`; } function openImagePicker(imageType, onSuccess) { const fileInput = document.createElement("input"); fileInput.type = "file"; fileInput.accept = "image/*"; fileInput.style.display = "none"; document.body.appendChild(fileInput); fileInput.addEventListener("change", async function () { if (!fileInput.files || !fileInput.files[0]) { fileInput.remove(); return; } try { const formData = new FormData(); formData.append("image", fileInput.files[0]); const response = await fetch( `/admin/upload/image?imageType=${encodeURIComponent(imageType)}`, { method: "POST", body: formData, }, ); const result = await response.json(); if (!result.success || !result.path) { throw new Error(result.error || "Upload failed"); } onSuccess(result.path); showToast("Success", "Image uploaded successfully", "success"); } catch (error) { showToast("Error", error.message || "Upload failed", "danger"); } finally { fileInput.remove(); } }); fileInput.click(); } function showToast(title, message, type) { const container = document.querySelector(".toast-container") || createToastContainer(); const toast = document.createElement("div"); toast.className = `toast align-items-center text-white bg-${type || "info"} border-0`; toast.setAttribute("role", "alert"); toast.innerHTML = `
${escapeHtml( title, )}: ${escapeHtml(message)}
`; container.appendChild(toast); new bootstrap.Toast(toast, { autohide: true, delay: 3000 }).show(); toast.addEventListener("hidden.bs.toast", function () { toast.remove(); }); } function createToastContainer() { const container = document.createElement("div"); container.className = "toast-container position-fixed top-0 end-0 p-3"; document.body.appendChild(container); return container; } function createDefaultValue(schema) { if (!schema) return ""; if (schema.type === "primitive") return ""; if (schema.type === "variant") { return { [schema.discriminator]: schema.options[0].value }; } if (schema.type === "object") { const value = {}; (schema.fields || []).forEach((field) => { if (field.type === "array") value[field.key] = []; else if (field.type === "object") value[field.key] = createDefaultValue(field); else if (field.type === "checkbox") value[field.key] = false; else if (field.type === "number") value[field.key] = 0; else value[field.key] = ""; }); return value; } return ""; } function createCol(colClass) { const div = document.createElement("div"); div.className = colClass; return div; } function inferColClass(type) { if (type === "textarea" || type === "image") return "col-12"; if (type === "checkbox") return "col-12"; return "col-md-6"; } function resolveImageUrl(path) { if (!path) return ""; if (/^https?:\/\//i.test(path)) return path; if (path.startsWith("/")) return `${backendUrl}${path}`; return `${backendUrl}/${path}`; } function collectIcons(schema) { if (!schema) return []; if (schema.type === "icon") return schema.options || []; if (schema.type === "object") return (schema.fields || []).flatMap(collectIcons); if (schema.type === "array") return collectIcons(schema.itemSchema); if (schema.type === "variant") { return Object.values(schema.variants || {}).flatMap((variant) => collectIcons(variant.schema), ); } return []; } function ensureIconDatalist(options) { const existing = document.getElementById("cms-icon-options"); if (existing) existing.remove(); const dataList = document.createElement("datalist"); dataList.id = "cms-icon-options"; options.forEach((option) => { const item = document.createElement("option"); item.value = option; dataList.appendChild(item); }); document.body.appendChild(dataList); } function isObject(value) { return Boolean(value) && typeof value === "object" && !Array.isArray(value); } function escapeHtml(value) { return String(value || "") .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); } })();