/* createJsonTree({ container: domElement, data: jsonData, onChange: (data) => { }, // optional: callback on change onSave: (data) => { } // optional: if set, a save button will be added }); */function createJsonTree({ container, data, onChange = () => {}, onSave = null }) { container.innerHTML = ""; container.classList.add("json-tree-root"); const history = []; const redoStack = []; let lastSnapshot = clone(data); function clone(obj) { return JSON.parse(JSON.stringify(obj)); } function diff(oldObj, newObj) { const changes = {}; function walk(o, n, path = "") { const keys = new Set([ ...Object.keys(o || {}), ...Object.keys(n || {}) ]); for (const key of keys) { const oldVal = o?.[key]; const newVal = n?.[key]; const currentPath = path ? `${path}.${key}` : key; if ( typeof oldVal === "object" && oldVal !== null && typeof newVal === "object" && newVal !== null && !Array.isArray(oldVal) ) { walk(oldVal, newVal, currentPath); continue; } if (JSON.stringify(oldVal) !== JSON.stringify(newVal)) { changes[currentPath] = newVal; } } } walk(oldObj, newObj); return changes; } function pushHistory() { history.push(clone(data)); redoStack.length = 0; } function undo() { if (!history.length) return; redoStack.push(clone(data)); data = history.pop(); render(); } function redo() { if (!redoStack.length) return; history.push(clone(data)); data = redoStack.pop(); render(); } function save() { if (onSave) onSave(data); } function autoResize(input) { input.style.width = "10px"; input.style.width = input.scrollWidth + "px"; } function remove(key, parent) { if (key === null || parent == null) return; if (!confirm("Wirklich löschen?")) return; pushHistory(); if (Array.isArray(parent)) { parent.splice(key, 1); } else { delete parent[key]; } onChange(data); render(); } function render() { container.innerHTML = ""; const controls = document.createElement("div"); controls.className = "json-controls"; const undoBtn = document.createElement("button"); undoBtn.className = "monolyth"; undoBtn.textContent = "Undo"; undoBtn.onclick = undo; const redoBtn = document.createElement("button"); redoBtn.className = "monolyth"; redoBtn.textContent = "Redo"; redoBtn.onclick = redo; controls.appendChild(undoBtn); controls.appendChild(redoBtn); if (onSave) { const saveBtn = document.createElement("button"); saveBtn.className = "monolyth"; saveBtn.textContent = "Save"; saveBtn.onclick = save; controls.appendChild(saveBtn); } container.appendChild(controls); container.appendChild(renderNode(data, null, null, "root", 0)); } function renderNode(value, key, parent, path, level) { const wrapper = document.createElement("div"); wrapper.className = "json-line"; wrapper.style.marginLeft = `${level * 18}px`; const keySpan = document.createElement("span"); keySpan.className = "json-key"; if (key !== null) { keySpan.textContent = `"${key}": `; } const removeBtn = document.createElement("span"); removeBtn.className = "json-remove"; removeBtn.textContent = " [x]"; removeBtn.onclick = (evt) => { evt.stopPropagation(); remove(key, parent); }; // OBJECT / ARRAY if (typeof value === "object" && value !== null) { const isArray = Array.isArray(value); const header = document.createElement("div"); header.className = "json-header"; const toggle = document.createElement("span"); toggle.className = "json-toggle"; toggle.textContent = isArray ? "[ ]" : "{ }"; const keyLabel = document.createElement("span"); keyLabel.className = "json-key"; if (key !== null) keyLabel.textContent = `"${key}": `; const addBtn = document.createElement("span"); addBtn.className = "json-add"; addBtn.textContent = " [+]"; const children = document.createElement("div"); children.className = "json-children"; keyLabel.onclick = toggle.onclick = () => { children.classList.toggle("collapsed"); }; addBtn.onclick = () => { let newKey = ""; let newValue; feedbox({ title: `Key hinzufügen`, message: `
${!isArray ? ` ` : ``}
`, buttons: { yes: { text: 'Anlegen', onClick: () => { pushHistory(); if (!isArray) { const input = document.querySelector('#newJsonKeyName'); newKey = input?.value?.trim(); if (!newKey) return; } const selectedType = document.querySelector('#newJsonValueType').value; switch (selectedType) { case "number": newValue = 0; break; case "boolean": newValue = false; break; case "object": newValue = {}; break; case "array": newValue = []; break; default: newValue = ""; } if (isArray) { value.push(newValue); } else { value[newKey] = newValue; } onChange(data); render(); } }, no: { text: 'Abbrechen' } } }); }; const inner = document.createElement("div"); const entries = isArray ? value.map((v, i) => [i, v]) : Object.entries(value); entries.forEach(([k, v]) => { inner.appendChild( renderNode(v, k, value, `${path}.${k}`, level + 1) ); }); children.appendChild(inner); header.appendChild(keyLabel); header.appendChild(toggle); header.appendChild(addBtn); if (key !== null) header.appendChild(removeBtn); wrapper.appendChild(header); wrapper.appendChild(children); return wrapper; } // STRING if (typeof value === "string") { const input = document.createElement("input"); input.className = "json-input string"; input.value = value; input.oninput = () => { pushHistory(); parent[key] = input.value; autoResize(input); onChange(data); }; setTimeout(() => autoResize(input), 0); wrapper.appendChild(keySpan); wrapper.appendChild(input); if (key !== null) wrapper.appendChild(removeBtn); return wrapper; } // NUMBER if (typeof value === "number") { const input = document.createElement("input"); input.type = "number"; input.className = "json-input number"; input.value = value; input.oninput = () => { pushHistory(); parent[key] = Number(input.value); autoResize(input); onChange(data); }; setTimeout(() => autoResize(input), 0); wrapper.appendChild(keySpan); wrapper.appendChild(input); if (key !== null) wrapper.appendChild(removeBtn); return wrapper; } // BOOLEAN if (typeof value === "boolean") { const input = document.createElement("input"); input.type = "checkbox"; input.checked = value; input.onchange = () => { pushHistory(); parent[key] = input.checked; onChange(data); }; wrapper.appendChild(keySpan); wrapper.appendChild(input); if (key !== null) wrapper.appendChild(removeBtn); return wrapper; } const span = document.createElement("span"); span.textContent = String(value); wrapper.appendChild(keySpan); wrapper.appendChild(span); if (key !== null) wrapper.appendChild(removeBtn); return wrapper; } pushHistory(); render(); return { undo, redo, save, refresh: render, getData: () => data, getChanges() { return diff(lastSnapshot, data); }, commit() { lastSnapshot = clone(data); }, update(newData) { data = newData; lastSnapshot = clone(newData); pushHistory(); render(); } }; }