summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJakob L. Kreuze <zerodaysfordays@sdf.org>2025-11-25 14:16:21 -0500
committerJakob L. Kreuze <zerodaysfordays@sdf.org>2025-11-25 14:16:21 -0500
commitc6aac96f1c5eb0ed1ee7a21a1e6f43f221737d58 (patch)
treed506fcbca66c35a804770b591f0aad1be890bfe5
parentb15a4163d8354feaafe6529c6a344e2b5f614f82 (diff)
card editor: initial commit
-rw-r--r--card-editor.html215
1 files changed, 215 insertions, 0 deletions
diff --git a/card-editor.html b/card-editor.html
new file mode 100644
index 0000000..b838b59
--- /dev/null
+++ b/card-editor.html
@@ -0,0 +1,215 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<title>JSON List Editor</title>
+<style>
+ body{font-family:Arial,Helvetica,sans-serif;margin:20px;}
+ h1{margin-bottom:5px;}
+ textarea{width:100%;height:120px;font-family:monospace;}
+ button{margin:5px 0;}
+ table{border-collapse:collapse;width:100%;margin-top:10px;}
+ th,td{border:1px solid #aaa;padding:4px 6px;text-align:left;}
+ th{background:#f0f0f0;}
+ input[type=text]{width:100%;box-sizing:border-box;}
+ .hidden{display:none;}
+ .del-btn{background:#e74c3c;color:white;border:none;padding:4px 6px;cursor:pointer;}
+</style>
+</head>
+<body>
+
+<h1>JSON List Editor</h1>
+
+<p>Paste or load a JSON array of objects below and click <strong>Load</strong>.</p>
+
+<textarea id="jsonInput" placeholder='[{"id":1,"name":"Alice","age":30},{"id":2,"name":"Bob","_secret":"xyz"}]'></textarea><br>
+<button id="loadBtn">Load</button>
+<button id="addBtn">New card</button>
+<button id="saveBtn">Download JSON</button>
+<input type="file" id="fileInput" accept=".json" style="display:none;">
+
+<div id="tableContainer"></div>
+
+<script>
+/* --------------- Global State --------------- */
+let data = []; // array of objects
+let fieldKeys = []; // columns that will be shown (no leading "_")
+
+/* --------------- Load & Parse JSON --------------- */
+function loadJSON(text) {
+ try {
+ const parsed = JSON.parse(text);
+ if (!Array.isArray(parsed)) throw new Error('JSON must be an array of objects');
+ data = parsed.map(obj => ({...obj})); // clone to avoid accidental mutations
+ computeFieldKeys();
+ renderTable();
+ } catch (e) {
+ alert('Error parsing JSON: ' + e.message);
+ }
+}
+
+function computeFieldKeys() {
+ const keys = new Set();
+ data.forEach(obj => {
+ Object.keys(obj).forEach(k => {
+ if (!k.startsWith('_')) keys.add(k);
+ });
+ });
+ fieldKeys = Array.from(keys);
+}
+
+/* --------------- Render Table --------------- */
+function renderTable() {
+ const container = document.getElementById('tableContainer');
+ container.innerHTML = ''; // clear previous
+
+ if (!data.length) return;
+
+ const table = document.createElement('table');
+ const thead = document.createElement('thead');
+ const headerRow = document.createElement('tr');
+
+ // Build header
+ fieldKeys.forEach(k => {
+ const th = document.createElement('th');
+ th.textContent = k;
+ headerRow.appendChild(th);
+ });
+ thead.appendChild(headerRow);
+ table.appendChild(thead);
+
+ // Extra header for Delete button
+ const delTh = document.createElement('th');
+ delTh.textContent = 'Delete';
+ headerRow.appendChild(delTh);
+
+ // Build body
+ const tbody = document.createElement('tbody');
+ data.forEach((obj, rowIndex) => {
+ const tr = document.createElement('tr');
+ fieldKeys.forEach(k => {
+ const td = document.createElement('td');
+ const input = document.createElement('textarea');
+ input.rows = 2;
+ // const input = document.createElement('input');
+ // input.type = 'text';
+ input.value = obj[k] !== undefined ? obj[k] : '';
+ input.dataset.row = rowIndex;
+ input.dataset.key = k;
+ input.addEventListener('input', onFieldChange);
+ td.appendChild(input);
+ tr.appendChild(td);
+ });
+ tbody.appendChild(tr);
+
+ // Delete button column
+ const delTd = document.createElement('td');
+ const delBtn = document.createElement('button');
+ delBtn.textContent = 'x';
+ delBtn.className = 'del-btn';
+ delBtn.dataset.row = rowIndex;
+ delBtn.addEventListener('click', () => onDeleteRow(rowIndex));
+ delTd.appendChild(delBtn);
+ tr.appendChild(delTd);
+ });
+
+ table.appendChild(tbody);
+ container.appendChild(table);
+}
+
+function onFieldChange(e) {
+ const row = e.target.dataset.row;
+ const key = e.target.dataset.key;
+ const value = e.target.value;
+ data[row][key] = value;
+}
+
+/**
+ * Remove all keys from an object whose value is the empty string.
+ *
+ * @param {Object} obj - The source object to clean.
+ * @param {boolean} [inPlace=false] - If true the original object is mutated.
+ * @returns {Object} A new object (or the original if inPlace is true) without empty‑string values.
+ *
+ * @example
+ * const data = { a: 'foo', b: '', c: 'bar', d: '' };
+ * const cleaned = removeEmptyStrings(data);
+ * // cleaned === { a: 'foo', c: 'bar' }
+ */
+function removeEmptyStrings(obj, inPlace = false) {
+ if (typeof obj !== 'object' || obj === null) {
+ throw new TypeError('Expected an object');
+ }
+
+ // If we’re mutating, work directly on the original
+ const target = inPlace ? obj : {};
+
+ for (const key in obj) {
+ if (!Object.prototype.hasOwnProperty.call(obj, key)) continue;
+
+ const value = obj[key];
+
+ if (value === '') {
+ if (!inPlace) continue; // skip adding it to the new object
+ delete target[key]; // remove from the original
+ } else {
+ target[key] = value; // copy non‑empty value
+ }
+ }
+
+ return target;
+}
+
+function onDeleteRow(idx) {
+ data.splice(idx, 1);
+ renderTable();
+}
+
+/* --------------- File I/O --------------- */
+document.getElementById('loadBtn').addEventListener('click', () => {
+ const text = document.getElementById('jsonInput').value;
+ loadJSON(text);
+});
+
+document.getElementById('saveBtn').addEventListener('click', () => {
+ if (!data.length) return;
+ data.forEach(removeEmptyStrings);
+ const blob = new Blob([JSON.stringify(data, null, 2)], {type: 'application/json'});
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = 'edited.json';
+ a.click();
+ URL.revokeObjectURL(url);
+});
+
+/* --------------- Optional: Drag‑and‑Drop or File Input --------------- */
+document.getElementById('fileInput').addEventListener('change', evt => {
+ const file = evt.target.files[0];
+ if (!file) return;
+ const reader = new FileReader();
+ reader.onload = e => loadJSON(e.target.result);
+ reader.readAsText(file);
+});
+
+/* --------------- Optional: Load file via button --------------- */
+const fileBtn = document.createElement('button');
+fileBtn.textContent = 'Load from File';
+fileBtn.addEventListener('click', () => document.getElementById('fileInput').click());
+document.body.insertBefore(fileBtn, document.getElementById('tableContainer'));
+
+/* --------------- Add New Object --------------- */
+document.getElementById('addBtn').addEventListener('click', () => {
+ if (!fieldKeys.length) {
+ alert('Load a JSON file first, then you can add a new object.');
+ return;
+ }
+ const newObj = {};
+ fieldKeys.forEach(k => newObj[k] = '');
+ newObj.id = crypto.randomUUID();
+ data.push(newObj);
+ renderTable();
+});
+</script>
+</body>
+</html>