diff options
| author | historia <[not public]> | 2026-06-29 22:31:11 -0400 |
|---|---|---|
| committer | historia <[not public]> | 2026-06-29 22:31:11 -0400 |
| commit | 069d3c1c81d71042706372df153254487a765dfc (patch) | |
| tree | d83a4a3954d2b8304f49d83e365f4c2b075b91eb /internal/admin/static/editor.js | |
| parent | 6c5271fb085b4571a10f106391974c249cd84317 (diff) | |
| download | thehouseoficarus-069d3c1c81d71042706372df153254487a765dfc.tar.gz | |
feat: wip web admin for mapping and crud
Diffstat (limited to 'internal/admin/static/editor.js')
| -rw-r--r-- | internal/admin/static/editor.js | 174 |
1 files changed, 174 insertions, 0 deletions
diff --git a/internal/admin/static/editor.js b/internal/admin/static/editor.js new file mode 100644 index 0000000..d9ec36f --- /dev/null +++ b/internal/admin/static/editor.js @@ -0,0 +1,174 @@ +var editorType = ''; +var editorFields = []; +var currentID = null; +var allIDs = []; + +function initEditor(type, fields) { + editorType = type; + editorFields = fields; + loadList(); + var hash = window.location.hash.substring(1); + if (hash) { loadItem(hash); } +} + +function loadList() { + API.get('/api/' + editorType).then(function(data) { + allIDs = data.ids || data || []; + renderList(''); + }).catch(function(e) { + notify('Failed to load list: ' + e.message, 'error'); + }); +} + +function renderList(filter) { + var el = $('#listEntries'); + var f = filter.toLowerCase(); + var filtered = allIDs.filter(function(id) { return !f || String(id).toLowerCase().indexOf(f) >= 0; }); + el.innerHTML = filtered.map(function(id) { + return '<div class="item' + (id === currentID ? ' active' : '') + '" onclick="loadItem(\'' + esc(id) + '\')">' + esc(id) + '</div>'; + }).join(''); +} + +function filterList(val) { + renderList(val); +} + +function loadItem(id) { + currentID = id; + renderList(''); + window.location.hash = id; + API.get('/api/' + editorType + '/' + encodeURIComponent(id)).then(function(data) { + renderEditor(id, data); + }).catch(function(e) { + $('#editorMain').innerHTML = '<p style="color:var(--danger)">Failed to load: ' + esc(e.message) + '</p>'; + }); +} + +function renderEditor(id, data) { + var obj = data; + var html = '<h2>' + esc(editorType.charAt(0).toUpperCase() + editorType.slice(1)) + ': ' + esc(id) + '</h2>'; + + html += '<div class="tabs"><button class="tab active" onclick="switchTab(event,\'fields\')">Fields</button>'; + html += '<button class="tab" onclick="switchTab(event,\'yaml\')">Raw YAML</button></div>'; + + html += '<div class="tab-content" id="tabFields">'; + html += '<div class="form-grid">'; + editorFields.forEach(function(f) { + var val = getNested(obj, f.key); + val = val === undefined || val === null ? '' : val; + var cls = f.type === 'textarea' ? 'form-group full' : 'form-group'; + if (f.type === 'textarea') { + var displayVal; + if (f.encode) { + displayVal = f.encode(val); + } else if (typeof val === 'object') { + displayVal = JSON.stringify(val, null, 2); + } else { + displayVal = String(val); + } + html += '<div class="'+cls+'"><label>' + esc(f.label) + '</label><textarea id="f_' + esc(f.key) + '" rows="4">' + esc(displayVal) + '</textarea></div>'; + } else if (f.type === 'checkbox') { + html += '<div class="'+cls+'"><label><input type="checkbox" id="f_' + esc(f.key) + '"' + (val ? ' checked' : '') + '> ' + esc(f.label) + '</label></div>'; + } else if (f.type === 'number') { + html += '<div class="'+cls+'"><label>' + esc(f.label) + '</label><input type="number" id="f_' + esc(f.key) + '" value="' + escAttr(String(val)) + '"></div>'; + } else if (f.type === 'color') { + html += '<div class="'+cls+' color-field"><label>' + esc(f.label) + '</label><span class="color-swatch" style="background:#' + escAttr(String(val) || '808080') + '"></span><input id="f_' + esc(f.key) + '" value="' + escAttr(String(val) || '') + '"></div>'; + } else { + html += '<div class="'+cls+'"><label>' + esc(f.label) + '</label><input id="f_' + esc(f.key) + '" value="' + escAttr(String(val)) + '"></div>'; + } + }); + html += '</div>'; + html += '</div>'; + + html += '<div class="tab-content" id="tabYaml" style="display:none">'; + html += '<pre>' + esc(obj._raw || JSON.stringify(obj, null, 2)) + '</pre>'; + html += '</div>'; + + html += '<div class="btn-row">'; + html += '<button class="btn btn-primary" onclick="saveItem(\'' + esc(id) + '\')">Save</button>'; + html += '<button class="btn btn-danger" onclick="deleteItem(\'' + esc(id) + '\')">Delete</button>'; + html += '</div>'; + + $('#editorMain').innerHTML = html; + setTimeout(function() { + document.querySelectorAll('.color-field').forEach(function(el) { bindColorField(el); }); + }, 50); +} + +function saveItem(id) { + var data = {}; + editorFields.forEach(function(f) { + var el = $('#f_' + f.key); + if (!el) return; + var val; + if (f.type === 'checkbox') { + val = el.checked; + } else if (f.type === 'number') { + val = parseFloat(el.value) || 0; + } else if (f.type === 'textarea') { + val = el.value; + } else { + val = el.value; + } + if (f.parser) { + val = f.parser(val); + } + setNested(data, f.key, val); + }); + data.id = id; + + API.put('/api/' + editorType + '/' + encodeURIComponent(id), data).then(function() { + notify(editorType + ' ' + id + ' saved', 'success'); + updateUndoBar(); + }).catch(function(e) { notify('Save failed: ' + e.message, 'error'); }); +} + +function deleteItem(id) { + if (!confirm('Delete ' + editorType + ' "' + id + '"?')) return; + API.del('/api/' + editorType + '/' + encodeURIComponent(id)).then(function() { + notify(editorType + ' ' + id + ' deleted', 'success'); + updateUndoBar(); + currentID = null; + $('#editorMain').innerHTML = '<p style="color:#888;text-align:center;margin-top:60px">Deleted</p>'; + loadList(); + }).catch(function(e) { notify('Delete failed: ' + e.message, 'error'); }); +} + +function createNew() { + var name = prompt(editorType + ' ID:'); + if (!name) return; + API.post('/api/' + editorType, {id: name}).then(function() { + notify(editorType + ' ' + name + ' created', 'success'); + updateUndoBar(); + loadList(); + loadItem(name); + }).catch(function(e) { notify('Create failed: ' + e.message, 'error'); }); +} + +function switchTab(e, tab) { + e.target.parentElement.querySelectorAll('.tab').forEach(function(t) { t.classList.remove('active'); }); + e.target.classList.add('active'); + var tabs = $('#editorMain').querySelectorAll('.tab-content'); + tabs.forEach(function(t) { t.style.display = 'none'; }); + var target = document.getElementById('tab' + tab.charAt(0).toUpperCase() + tab.slice(1)); + if (target) target.style.display = 'block'; +} + +function getNested(obj, path) { + return path.split('.').reduce(function(o, k) { return o && o[k] !== undefined ? o[k] : undefined; }, obj); +} + +function setNested(obj, path, val) { + var keys = path.split('.'); + var last = keys.pop(); + var target = keys.reduce(function(o, k) { + if (!o[k]) o[k] = {}; + return o[k]; + }, obj); + target[last] = val; +} + +function esc(s) { return String(s || '').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); } +function escAttr(s) { return String(s || '').replace(/&/g,'&').replace(/"/g,'"'); } + +window.refreshPage = function() { loadList(); if (currentID) loadItem(currentID); }; |
