diff options
| author | Jakob L. Kreuze <zerodaysfordays@sdf.org> | 2025-11-25 14:16:21 -0500 |
|---|---|---|
| committer | Jakob L. Kreuze <zerodaysfordays@sdf.org> | 2025-11-25 14:16:21 -0500 |
| commit | c6aac96f1c5eb0ed1ee7a21a1e6f43f221737d58 (patch) | |
| tree | d506fcbca66c35a804770b591f0aad1be890bfe5 /card-editor.html | |
| parent | b15a4163d8354feaafe6529c6a344e2b5f614f82 (diff) | |
card editor: initial commit
Diffstat (limited to 'card-editor.html')
| -rw-r--r-- | card-editor.html | 215 |
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> |