diff options
Diffstat (limited to 'card-editor.js')
| -rw-r--r-- | card-editor.js | 185 |
1 files changed, 185 insertions, 0 deletions
diff --git a/card-editor.js b/card-editor.js new file mode 100644 index 0000000..b60185e --- /dev/null +++ b/card-editor.js @@ -0,0 +1,185 @@ +/* Copyright © 2025 Jakob L. Kreuze + + This file is part of SRS Anywhere. + + SRS Anywhere is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as + published by the Free Software Foundation, either version 3 of the + License, or (at your option) any later version. + + SRS Anywhere is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public + License along with SRS Anywhere. If not, see <https://www.gnu.org/licenses/>. */ + +let data = []; +let fieldKeys = {}; + +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})); + computeFieldKeys(); + renderTable(); + } catch (e) { + alert('Error parsing JSON: ' + e.message); + } +} + +function partition(arr, key = 'type') { + return arr.reduce((acc, item) => { + const k = item[key] || 'default'; + const prop = String(k); + if (!acc[prop]) acc[prop] = []; + acc[prop].push(item); + return acc; + }, {}); +} + +function computeFieldKeys() { + const partitioned = partition(data); + Object.entries(partitioned).forEach(([type, cards]) => { + const keys = new Set(); + cards.forEach(card => { + Object.keys(card).forEach(k => { + if (!k.startsWith('_')) keys.add(k); + }); + }) + fieldKeys[type] = Array.from(keys); + }); +} + +function renderTable() { + const container = document.getElementById('table-container'); + container.innerHTML = ''; // clear previous + + if (!data.length) return; + + Object.entries(fieldKeys).forEach(([type, keys]) => { + const table = document.createElement('table'); + const thead = document.createElement('thead'); + const headerRow = document.createElement('tr'); + const addButton = document.createElement('button'); + + table.style = "margin-bottom: 32px;" + + // Build header + keys.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) => { + if ((obj.type || 'default') != type) { + return; + } + const tr = document.createElement('tr'); + keys.forEach(k => { + const td = document.createElement('td'); + const input = document.createElement('textarea'); + input.rows = 2; + 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 = 'delete'; + delBtn.dataset.row = rowIndex; + delBtn.addEventListener('click', () => { + data.splice(idx, 1); + renderTable(); + }); + delTd.appendChild(delBtn); + tr.appendChild(delTd); + }); + + table.appendChild(tbody); + + addButton.innerHTML = '+'; + addButton.addEventListener('click', () => { + const newObj = {}; + keys.forEach(k => newObj[k] = ''); + newObj.id = crypto.randomUUID(); + data.push(newObj); + renderTable(); + }); + + container.appendChild(addButton); + 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; +} + +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; +} + +window.addEventListener('load', function() { + document.getElementById('cards-input').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); + }); + + document.getElementById('save-button').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); + }); +}) |