initial files
This commit is contained in:
361
public/javascript/JSON.js
Normal file
361
public/javascript/JSON.js
Normal file
@@ -0,0 +1,361 @@
|
||||
/*
|
||||
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: `<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);
|
||||
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();
|
||||
}
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user