const state = { page: null, selected: null, clipboard: null, }; const el = { regions: document.querySelector("#regions"), reload: document.querySelector("#reload"), save: document.querySelector("#save"), render: document.querySelector("#render"), status: document.querySelector("#status"), empty: document.querySelector("#empty-state"), form: document.querySelector("#block-form"), selectedRegion: document.querySelector("#selected-region"), selectedTitle: document.querySelector("#selected-title"), id: document.querySelector("#block-id"), label: document.querySelector("#block-label"), template: document.querySelector("#block-template"), anchor: document.querySelector("#block-anchor"), enabled: document.querySelector("#block-enabled"), json: document.querySelector("#block-json"), moveUp: document.querySelector("#move-up"), moveDown: document.querySelector("#move-down"), duplicate: document.querySelector("#duplicate"), copy: document.querySelector("#copy"), pasteAfter: document.querySelector("#paste-after"), deleteBlock: document.querySelector("#delete-block"), }; function setStatus(message) { el.status.textContent = message; } function clone(value) { return JSON.parse(JSON.stringify(value)); } function activeRegion() { if (!state.selected) return null; return state.page.regions[state.selected.regionIndex]; } function activeBlock() { const region = activeRegion(); if (!region || state.selected.blockIndex == null) return null; return region.blocks[state.selected.blockIndex]; } function uniqueId(base) { const ids = new Set(state.page.regions.flatMap((region) => region.blocks.map((block) => block.id))); let candidate = `${base}-copy`; let index = 2; while (ids.has(candidate)) { candidate = `${base}-copy-${index}`; index += 1; } return candidate; } function renderRegions() { el.regions.innerHTML = ""; state.page.regions.forEach((region, regionIndex) => { const regionNode = document.createElement("section"); const title = document.createElement("h3"); const list = document.createElement("div"); title.className = "region-title"; title.textContent = region.label || region.id; list.className = "block-list"; region.blocks.forEach((block, blockIndex) => { const button = document.createElement("button"); const meta = document.createElement("span"); const isActive = state.selected?.regionIndex === regionIndex && state.selected?.blockIndex === blockIndex; button.type = "button"; button.className = `block-btn${isActive ? " active" : ""}${block.enabled === false ? " disabled" : ""}`; button.textContent = block.adminLabel || block.id; meta.className = "block-meta"; meta.textContent = `${block.id} · ${block.template}`; button.append(meta); button.addEventListener("click", () => { state.selected = { regionIndex, blockIndex }; renderAll(); }); list.append(button); }); regionNode.append(title, list); el.regions.append(regionNode); }); } function renderEditor() { const region = activeRegion(); const block = activeBlock(); if (!block) { el.empty.classList.remove("hidden"); el.form.classList.add("hidden"); el.selectedRegion.textContent = "Выберите блок"; el.selectedTitle.textContent = "Контентный слой"; return; } el.empty.classList.add("hidden"); el.form.classList.remove("hidden"); el.selectedRegion.textContent = region.label || region.id; el.selectedTitle.textContent = block.adminLabel || block.id; el.id.value = block.id || ""; el.label.value = block.adminLabel || ""; el.template.value = block.template || ""; el.anchor.value = block.anchor || ""; el.enabled.checked = block.enabled !== false; el.json.value = JSON.stringify(block, null, 2); } function renderAll() { renderRegions(); renderEditor(); } function syncBlockFromFields() { const block = activeBlock(); if (!block) return; block.id = el.id.value.trim(); block.adminLabel = el.label.value.trim(); block.anchor = el.anchor.value.trim() || null; block.enabled = el.enabled.checked; el.json.value = JSON.stringify(block, null, 2); renderRegions(); } function syncBlockFromJson() { const region = activeRegion(); if (!region) return false; try { const parsed = JSON.parse(el.json.value); region.blocks[state.selected.blockIndex] = parsed; renderAll(); setStatus("JSON блока применён локально. Нажмите «Сохранить JSON», чтобы записать файл."); return true; } catch (error) { setStatus(`Ошибка JSON: ${error.message}`); return false; } } async function loadPage() { const response = await fetch("/api/page/home"); if (!response.ok) throw new Error(await response.text()); state.page = await response.json(); state.selected = null; renderAll(); setStatus("Модель home.json загружена."); } async function savePage() { if (activeBlock()) syncBlockFromFields(); const response = await fetch("/api/page/home", { method: "PUT", headers: { "content-type": "application/json" }, body: JSON.stringify(state.page), }); if (!response.ok) throw new Error(await response.text()); setStatus("content/pages/home.json сохранён."); } async function renderHome() { const response = await fetch("/api/render/home", { method: "POST" }); const payload = await response.json(); if (!response.ok || !payload.ok) { throw new Error(payload.error || "Render failed"); } setStatus(payload.stdout || "index.html собран."); } function moveSelected(delta) { const region = activeRegion(); const index = state.selected?.blockIndex; if (!region || index == null) return; const nextIndex = index + delta; if (nextIndex < 0 || nextIndex >= region.blocks.length) return; const [block] = region.blocks.splice(index, 1); region.blocks.splice(nextIndex, 0, block); state.selected.blockIndex = nextIndex; renderAll(); } function duplicateSelected() { const region = activeRegion(); const block = activeBlock(); if (!region || !block) return; const duplicate = clone(block); duplicate.id = uniqueId(block.id); duplicate.adminLabel = `${block.adminLabel || block.id} — копия`; duplicate.scopeIds = true; region.blocks.splice(state.selected.blockIndex + 1, 0, duplicate); state.selected.blockIndex += 1; renderAll(); } function copySelected() { const block = activeBlock(); if (!block) return; state.clipboard = clone(block); setStatus(`Скопирован блок: ${block.adminLabel || block.id}`); } function pasteAfterSelected() { const region = activeRegion(); if (!region || !state.clipboard) return; const pasted = clone(state.clipboard); pasted.id = uniqueId(pasted.id); pasted.adminLabel = `${pasted.adminLabel || pasted.id} — вставка`; pasted.scopeIds = true; region.blocks.splice(state.selected.blockIndex + 1, 0, pasted); state.selected.blockIndex += 1; renderAll(); } function deleteSelected() { const region = activeRegion(); if (!region || state.selected.blockIndex == null) return; region.blocks.splice(state.selected.blockIndex, 1); state.selected.blockIndex = Math.min(state.selected.blockIndex, region.blocks.length - 1); if (state.selected.blockIndex < 0) state.selected = null; renderAll(); } for (const input of [el.id, el.label, el.anchor, el.enabled]) { input.addEventListener("input", syncBlockFromFields); input.addEventListener("change", syncBlockFromFields); } el.json.addEventListener("blur", syncBlockFromJson); el.reload.addEventListener("click", () => loadPage().catch((error) => setStatus(error.message))); el.save.addEventListener("click", () => savePage().catch((error) => setStatus(error.message))); el.render.addEventListener("click", () => renderHome().catch((error) => setStatus(error.message))); el.moveUp.addEventListener("click", () => moveSelected(-1)); el.moveDown.addEventListener("click", () => moveSelected(1)); el.duplicate.addEventListener("click", duplicateSelected); el.copy.addEventListener("click", copySelected); el.pasteAfter.addEventListener("click", pasteAfterSelected); el.deleteBlock.addEventListener("click", deleteSelected); loadPage().catch((error) => setStatus(error.message));