Files
websitedev/scripts/cms-editor-client.js
T
Do Siki 162e5782e9
CI Pipeline with Test Management / 🧪 Run Tests & Generate Reports (push) Waiting to run
CI Pipeline with Test Management / 🐳 Docker Integration Tests (push) Blocked by required conditions
CI Pipeline with Test Management / 🏗️ Build Docker Image (push) Blocked by required conditions
CI Pipeline with Test Management / 📊 Generate Test Summary (push) Blocked by required conditions
Test Reporting & Gherkin Analysis / 🧪 Run Tests & Generate Reports (push) Waiting to run
Test Reporting & Gherkin Analysis / 📊 Analyze Test Coverage (push) Blocked by required conditions
Test Reporting & Gherkin Analysis / 🔄 Sync with Linear (push) Blocked by required conditions
Test Reporting & Gherkin Analysis / ⚡ Performance Monitoring (push) Blocked by required conditions
refactor(cms): split oversized modules below the 400-line limit
- content-editor.js: extract the /save handler to scripts/cms-save.js
- cms-logo-page.js: inline the ~280-line canvas editor script to
  scripts/cms-logo-client.js (page is now the HTML/CSS shell only)
- cms-editor-client.js: move the keyboard-shortcut section to
  scripts/cms-editor-shortcuts.js, inlined after the main client

All modules now under the hard limit; full pre-deploy suite green.

Closes MITHOME-80
2026-08-22 12:31:28 +02:00

346 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Browser-side script of the Content Editor editor page.
// Inlined into the HTML template at render time by content-editor.js.
// Test coverage: scripts/test-content-editor-serializer.js runs this exact code.
// ── Render ──────────────────────────────────────────────────────────────────
function render(obj, container) {
container.innerHTML = '';
renderObject(obj, container, '');
}
function renderObject(obj, container, prefix) {
for (const [key, val] of Object.entries(obj)) {
const path = prefix ? prefix + '.' + key : key;
if (Array.isArray(val)) {
renderArray(key, val, container, path);
} else if (typeof val === 'object' && val !== null) {
renderObject(val, container, path);
} else {
renderPrimitive(path, val, container);
}
}
}
function renderPrimitive(path, val, container) {
const isLong = String(val).length > 80 || String(val).includes('<');
const div = document.createElement('div');
div.className = 'field';
const type = val === null ? 'null' : typeof val;
let control;
if (type === 'boolean') {
control = `<input type="checkbox" data-path="${esc(path)}" data-type="boolean" ${val ? 'checked' : ''}>`;
} else if (type === 'number') {
control = `<input type="number" data-path="${esc(path)}" data-type="number" value="${esc(val)}">`;
} else {
control = isLong
? `<textarea data-path="${esc(path)}" data-type="${type}" rows="${Math.min(8,Math.max(2,Math.ceil(String(val).length/80)))}">${esc(val ?? '')}<\/textarea>`
: `<input type="text" data-path="${esc(path)}" data-type="${type}" value="${esc(val ?? '')}">`;
}
div.innerHTML = `
<label>${path}</label>
${control}
`;
container.appendChild(div);
}
function renderArray(key, arr, container, path) {
const section = document.createElement('div');
section.className = 'array-section';
section.dataset.arrayPath = path;
const label = document.createElement('div');
label.className = 'array-label';
label.textContent = path;
section.appendChild(label);
const items = document.createElement('div');
items.className = 'array-items';
items.dataset.arrayItems = path;
section.appendChild(items);
arr.forEach((item, i) => {
if (typeof item === 'object' && item !== null) {
items.appendChild(makeObjCard(item, i, path));
} else {
items.appendChild(makeStrItem(item, i, path));
}
});
// Template for adding new items
const sample = arr.length > 0 ? arr[arr.length - 1] : '';
const isObj = typeof sample === 'object' && sample !== null;
const addBtn = document.createElement('button');
addBtn.className = 'btn-add';
addBtn.textContent = '➕ Új elem hozzáadása';
addBtn.onclick = () => {
const idx = items.children.length;
if (isObj) {
const blank = blankLike(sample);
items.appendChild(makeObjCard(blank, idx, path));
} else {
items.appendChild(makeStrItem('', idx, path));
}
reindexItems(items);
};
section.appendChild(addBtn);
container.appendChild(section);
}
function blankLike(value) {
if (Array.isArray(value)) return [];
if (value && typeof value === 'object') {
return Object.fromEntries(Object.entries(value).map(([key, child]) => [key, blankLike(child)]));
}
if (typeof value === 'boolean') return false;
if (typeof value === 'number') return 0;
return '';
}
function makeStrItem(val, idx, path) {
const wrap = document.createElement('div');
wrap.className = 'str-item';
const type = val === null ? 'null' : typeof val;
const ta = type === 'boolean' ? document.createElement('input') : document.createElement('textarea');
ta.dataset.path = path + '[' + idx + ']';
ta.dataset.type = type;
if (type === 'boolean') {
ta.type = 'checkbox';
ta.checked = val;
} else {
ta.value = val ?? '';
ta.rows = Math.min(6, Math.max(2, Math.ceil(String(val ?? '').length / 80)));
}
const del = document.createElement('button');
del.className = 'btn-del';
del.textContent = '❌';
del.title = 'Törlés';
del.onclick = () => {
// WHY: capture the container BEFORE removing — a detached node has no
// ancestors, so closest() would return null and reindexing would silently
// not run (sparse arrays → schema errors on save).
const container = wrap.closest('.array-items');
wrap.remove();
reindexItems(container);
};
wrap.appendChild(ta);
wrap.appendChild(del);
return wrap;
}
function makeObjCard(obj, idx, path) {
const card = document.createElement('div');
card.className = 'obj-card';
const hdr = document.createElement('div');
hdr.className = 'card-header';
hdr.textContent = path + '[' + idx + ']';
card.appendChild(hdr);
renderObject(obj, card, path + '[' + idx + ']');
const del = document.createElement('button');
del.className = 'btn-del-card';
del.textContent = '❌ Törlés';
del.onclick = () => {
// Same as above: capture before detaching, or reindexing is skipped.
const container = card.closest('.array-items');
card.remove();
reindexItems(container);
};
card.appendChild(del);
return card;
}
function reindexItems(itemsEl) {
if (!itemsEl) return;
const path = itemsEl.dataset.arrayItems;
// WHY: rewrite only the index that directly follows THIS array's own path prefix.
// A generic "replace first [n]" rule corrupts nested arrays (e.g. deleting from
// services[1].specs.items rewrites the OUTER services index and scatters paths
// across services[0..n], producing sparse arrays and schema errors).
const prefix = path + '[';
Array.from(itemsEl.children).forEach((child, i) => {
child.querySelectorAll('[data-path]').forEach(el => {
const old = el.dataset.path;
if (typeof old !== 'string' || !old.startsWith(prefix)) return;
const rest = old.slice(prefix.length);
const bracketEnd = rest.indexOf(']');
const suffix = bracketEnd === -1 ? '' : rest.slice(bracketEnd);
el.dataset.path = prefix + i + suffix;
});
// Update card header
const hdr = child.querySelector('.card-header');
if (hdr) hdr.textContent = path + '[' + i + ']';
});
}
// ── Collect & Save ───────────────────────────────────────────────────────────
function collect() {
const result = JSON.parse(JSON.stringify(DATA)); // deep clone as base
// Wipe all arrays so we rebuild them from DOM
clearArrays(result);
document.querySelectorAll('[data-path]').forEach(el => {
setPath(result, el.dataset.path, readValue(el));
});
return result;
}
function readValue(el) {
switch (el.dataset.type) {
case 'boolean': return el.checked;
case 'number': return Number(el.value);
case 'null': return el.value === '' ? null : el.value;
default: return el.value;
}
}
function clearArrays(obj) {
for (const k of Object.keys(obj)) {
if (Array.isArray(obj[k])) obj[k] = [];
else if (typeof obj[k] === 'object' && obj[k] !== null) clearArrays(obj[k]);
}
}
function setPath(obj, path, value) {
const parts = parsePath(path);
let cur = obj;
for (let i = 0; i < parts.length - 1; i++) {
const part = parts[i];
if (cur[part] === undefined || cur[part] === null) {
cur[part] = typeof parts[i + 1] === 'number' ? [] : {};
}
cur = cur[part];
}
cur[parts[parts.length - 1]] = value;
}
function parsePath(path) {
const parts = [];
let token = '';
let inIndex = false;
for (const char of path) {
if (char === '.') {
if (!inIndex && token) parts.push(token);
token = '';
} else if (char === '[') {
if (token) parts.push(token);
token = '';
inIndex = true;
} else if (char === ']') {
parts.push(Number(token));
token = '';
inIndex = false;
} else {
token += char;
}
}
if (token) parts.push(token);
return parts;
}
async function save() {
const status = document.getElementById('saveStatus');
try {
const data = collect();
const res = await fetch('/save?file=' + FILE, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': CSRF_TOKEN, 'X-Content-Hash': CONTENT_HASH },
body: JSON.stringify(data, null, 2)
});
if (res.status === 401) { location.href = '/login'; return false; }
if (res.status === 409) {
if (confirm('A tartalom megváltozott, mióta ez a lap megnyílt (pl. deploy vagy másik fül mentett).\n\nOK = lap frissítése az új tartalommal (a szerkesztésed elvész)\nMégse = maradsz ezen a lapon, a mentés nem történt meg.')) {
location.reload();
}
return false;
}
const json = await res.json();
if (json.ok) {
// Refresh the optimistic-lock fingerprint with the server-computed hash of
// the saved content, so the user's own subsequent saves don't trip 409.
if (json.contentHash) CONTENT_HASH = json.contentHash;
status.textContent = '✅ Mentve!';
status.style.color = '#10b981';
status.style.visibility = 'visible';
setTimeout(() => status.style.visibility = 'hidden', 3000);
return true;
} else {
status.textContent = '❌ Hiba: ' + json.error;
status.style.color = '#f87171';
status.style.visibility = 'visible';
setTimeout(() => status.style.visibility = 'hidden', 5000);
return false;
}
} catch (e) {
status.textContent = '❌ Hálózati hiba mentéskor';
status.style.color = '#f87171';
status.style.visibility = 'visible';
setTimeout(() => status.style.visibility = 'hidden', 5000);
return false;
}
}
async function publish() {
const btn = document.getElementById('publishBtn');
const status = document.getElementById('saveStatus');
// Save first — abort publish if save failed (e.g. 409 conflict, validation error)
const saved = await save();
if (!saved) return;
// WHY: lock the button width and remember the label so the running state
// neither resizes the bottom bar nor permanently swaps the env-specific label.
const originalLabel = btn.textContent;
btn.style.minWidth = btn.offsetWidth + 'px';
btn.textContent = '⏳ Élesítés folyamatban...';
btn.disabled = true;
try {
const res = await fetch('/publish', { method: 'POST', headers: { 'X-CSRF-Token': CSRF_TOKEN } });
if (res.status === 401) { location.href = '/login'; return; }
const json = await res.json();
if (json.ok) {
status.textContent = '🚀 Sikeresen elküldve a szerverre!';
status.style.color = '#10b981';
} else {
status.textContent = '❌ Hiba az élesítésnél: ' + json.error;
status.style.color = '#f87171';
}
} catch (e) {
status.textContent = '❌ Hálózati hiba';
status.style.color = '#f87171';
}
btn.textContent = originalLabel;
btn.style.minWidth = '';
btn.disabled = false;
status.style.visibility = 'visible';
setTimeout(() => status.style.visibility = 'hidden', 5000);
}
async function logout() {
if (!confirm('Biztosan ki szeretnél lépni?')) return;
try {
// Invalidates the server-side session cookie (Basic Auth cache is not
// affected — the login page is public, no 401-overwrite is needed).
await fetch('/logout', { method: 'POST', headers: { 'X-CSRF-Token': CSRF_TOKEN } });
} catch (e) { /* network error — continue to the login page */ }
location.href = '/login';
}
function esc(v) {
return String(v).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
// Boot
render(DATA, document.getElementById('editor'));
// Auto-dismiss toast
const toast = document.querySelector('.toast');
if (toast) setTimeout(() => toast.remove(), 3500);