Files
radixOS/public/javascript/JSON.js
2026-04-23 15:40:07 +02:00

381 lines
12 KiB
JavaScript

/*
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,
readonly = false
}) {
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 (readonly) return;
if (!history.length) return;
redoStack.push(clone(data));
data = history.pop();
render();
}
function redo() {
if (readonly) return;
if (!redoStack.length) return;
history.push(clone(data));
data = redoStack.pop();
render();
}
function save() {
if (readonly) return;
if (onSave) onSave(data);
}
function autoResize(input) {
input.style.width = "10px";
input.style.width = input.scrollWidth + "px";
}
function remove(key, parent) {
if (readonly) return;
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 = "";
if (onSave && !readonly) {
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;
const saveBtn = document.createElement("button");
saveBtn.className = "monolyth";
saveBtn.textContent = "Save";
saveBtn.onclick = save;
controls.appendChild(undoBtn);
controls.appendChild(redoBtn);
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]";
if (!readonly) {
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 = () => {
if (readonly) return;
children.classList.toggle("collapsed");
};
if (!readonly) {
addBtn.onclick = () => {
let newKey = "";
let newValue;
feedbox({
title: `<span style="color:#f44336">Key</span> hinzufügen`,
message: `
<div style="display:flex;flex-direction:column;gap:8px;">
${!isArray ? `
<input id="newJsonKeyName" placeholder="Key Name" style="padding:6px;" />
` : ``}
<select id="newJsonValueType">
<option value="text">Text</option>
<option value="number">Zahl</option>
<option value="boolean">Boolean</option>
<option value="object">Objekt</option>
<option value="array">Array</option>
</select>
</div>
`,
buttons: {
yes: {
text: '<b>Anlegen</b>',
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);
if (!readonly) header.appendChild(addBtn);
if (key !== null && !readonly) 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;
if (readonly) {
input.disabled = true;
} else {
input.oninput = () => {
pushHistory();
parent[key] = input.value;
autoResize(input);
onChange(data);
};
}
setTimeout(() => autoResize(input), 0);
wrapper.appendChild(keySpan);
wrapper.appendChild(input);
if (key !== null && !readonly) 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;
if (readonly) {
input.disabled = true;
} else {
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 && !readonly) wrapper.appendChild(removeBtn);
return wrapper;
}
// BOOLEAN
if (typeof value === "boolean") {
const input = document.createElement("input");
input.type = "checkbox";
input.checked = value;
if (readonly) {
input.disabled = true;
} else {
input.onchange = () => {
pushHistory();
parent[key] = input.checked;
onChange(data);
};
}
wrapper.appendChild(keySpan);
wrapper.appendChild(input);
if (key !== null && !readonly) wrapper.appendChild(removeBtn);
return wrapper;
}
const span = document.createElement("span");
span.textContent = String(value);
wrapper.appendChild(keySpan);
wrapper.appendChild(span);
if (key !== null && !readonly) 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();
}
};
}