aboutsummaryrefslogtreecommitdiff
path: root/internal/admin/static/cardeditor.js
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-07-01 06:32:15 -0400
committerhistoria <[not public]>2026-07-01 06:32:15 -0400
commit649ef4b7416ce7053145878841fc9b0bff2f5fec (patch)
tree1dae6678dac94d5f5948973059b7ddf8a70c3659 /internal/admin/static/cardeditor.js
parentbf7c27c2cbf3eb56a0a4c5141e39b152c51614b5 (diff)
downloadthehouseoficarus-649ef4b7416ce7053145878841fc9b0bff2f5fec.tar.gz
feat: admin gui card code unified, unhinged conversation tree GUI sort of implemented
Diffstat (limited to 'internal/admin/static/cardeditor.js')
-rw-r--r--internal/admin/static/cardeditor.js452
1 files changed, 452 insertions, 0 deletions
diff --git a/internal/admin/static/cardeditor.js b/internal/admin/static/cardeditor.js
new file mode 100644
index 0000000..0235894
--- /dev/null
+++ b/internal/admin/static/cardeditor.js
@@ -0,0 +1,452 @@
+// cardeditor.js — Shared utilities for card-based entity editors.
+// Used by mobeditor, objecteditor, itemeditor, and future editors.
+// Prefix all exports with ced_ to avoid collisions with editor.js globals.
+
+// ---- Path helpers ----
+
+function ced_cardPath(sec) {
+ return sec.path || '';
+}
+
+function ced_getVal(data, sec, key) {
+ var p = ced_cardPath(sec);
+ if (sec.isArray || p === 'on_look') {
+ return data[p] && data[p][key] !== undefined ? data[p][key] : '';
+ }
+ var fp = p ? p + '.' + key : key;
+ var v = getNested(data, fp);
+ return v === undefined || v === null ? '' : v;
+}
+
+// ---- Field ID helpers ----
+
+function ced_fieldID(sec, key, idx) {
+ if (idx !== undefined && idx >= 0) return 'f_' + sec.id + '_' + idx + '_' + key.replace(/\./g, '_');
+ return 'f_' + sec.id + '_' + key.replace(/\./g, '_');
+}
+
+function ced_fieldIDSub(sec, subKey, idx, fieldKey) {
+ return 'f_' + sec.id + '_' + subKey + '_' + idx + '_' + fieldKey;
+}
+
+// ---- Value collector ----
+
+function ced_collectFieldValue(el, type) {
+ if (type === 'checkbox') return el.checked;
+ if (type === 'select') return el.value;
+ if (type === 'search') return el.value;
+ if (type === 'number') { var n = parseFloat(el.value); return isNaN(n) ? 0 : n; }
+ if (type === 'json') {
+ var raw = el.value.trim();
+ if (!raw) return null;
+ try { return JSON.parse(raw); } catch(e) { return raw; }
+ }
+ if (type === 'textarea') return el.value;
+ return el.value;
+}
+
+// ---- Tags rendering (comma-separated string arrays) ----
+
+function ced_renderTags(sec, il, data, getter) {
+ var sectionRoot = data[ced_cardPath(sec)] || data;
+ var arr = getter ? getter(sectionRoot, il) : (sectionRoot[il.key] || []);
+ var str = Array.isArray(arr) ? arr.join(', ') : String(arr || '');
+ var fid = ced_fieldID(sec, il.key);
+ return '<label class="obj-field obj-wide"><span>' + esc(il.label) + '</span><input id="' + fid + '" value="' + escAttr(str) + '" placeholder="comma separated"></label>';
+}
+
+// ---- KV rendering (key=value map) ----
+
+function ced_renderKV(sec, il, data, getter) {
+ var sectionRoot = data[ced_cardPath(sec)] || data;
+ var map = getter ? getter(sectionRoot, il) : (sectionRoot[il.key] || {});
+ var h = '<div class="obj-inline-group">';
+ h += '<span class="obj-inline-label">' + esc(il.label) + '</span>';
+ h += '<div class="obj-kv-list" data-sec="' + sec.id + '" data-key="' + il.key + '">';
+ Object.keys(map).forEach(function(k, idx) {
+ h += '<div class="obj-kv-row"><input value="' + escAttr(k) + '" placeholder="key" class="obj-kv-key">';
+ h += '<input value="' + escAttr(String(map[k])) + '" placeholder="val" class="obj-kv-val">';
+ h += '<button class="btn btn-sm btn-danger" onclick="this.parentElement.remove()">X</button></div>';
+ });
+ h += '</div>';
+ h += '<button class="btn btn-sm obj-sub-add" onclick="ced_addKVRow(\'' + sec.id + '\',\'' + il.key + '\')">+ Add</button>';
+ h += '</div>';
+ return h;
+}
+
+function ced_addKVRow(cardID, subKey) {
+ var el = document.querySelector('.obj-kv-list[data-sec="' + cardID + '"][data-key="' + subKey + '"]');
+ if (!el) return;
+ var row = document.createElement('div');
+ row.className = 'obj-kv-row';
+ row.innerHTML = '<input value="" placeholder="key" class="obj-kv-key"><input value="0" placeholder="val" class="obj-kv-val"><button class="btn btn-sm btn-danger" onclick="this.parentElement.remove()">X</button>';
+ el.appendChild(row);
+}
+
+// ---- Search autocomplete ----
+
+function ced_setupSearchFields() {
+ document.querySelectorAll('.search-input').forEach(function(input) {
+ if (input._searchSetup) return;
+ input._searchSetup = true;
+ var entityType = input.getAttribute('data-search-type') || 'items';
+ var results = input.parentElement.querySelector('.search-results');
+ if (!results) return;
+
+ results._targetInput = input;
+
+ input.addEventListener('input', function() {
+ var val = input.value.trim();
+ if (!val) { results.style.display = 'none'; results.innerHTML = ''; results._selectedIndex = -1; return; }
+ API.get('/api/search?q=' + encodeURIComponent(val)).then(function(data) {
+ var items = [];
+ if (entityType === 'items' && data.items) items = data.items;
+ if (entityType === 'objects' && data.objects) items = data.objects;
+ if (entityType === 'mobs' && data.mobs) items = data.mobs;
+ if (entityType === 'drops' && data.drops) items = data.drops;
+ if (items.length === 0) {
+ results.style.display = 'block';
+ results.innerHTML = '<div class="sr-item" style="color:#888">No results</div>';
+ return;
+ }
+ results.style.display = 'block';
+ results.innerHTML = items.map(function(r) {
+ return '<div class="sr-item" onclick="event.stopPropagation();ced_selectSearchResult(this)">' + esc(r.id) + ' <span style="color:#aaa">' + esc(r.name || '') + '</span></div>';
+ }).join('');
+ results._items = items;
+ results._selectedIndex = -1;
+ }).catch(function() {
+ results.style.display = 'none';
+ });
+ });
+
+ input.addEventListener('focus', function() {
+ if (input.value.trim()) {
+ input.dispatchEvent(new Event('input'));
+ }
+ });
+
+ input.addEventListener('blur', function() {
+ setTimeout(function() { results.style.display = 'none'; }, 200);
+ });
+
+ input.addEventListener('keydown', function(e) {
+ if (results.style.display === 'none' || !results.children.length) return;
+ if (results._selectedIndex === undefined) results._selectedIndex = -1;
+
+ if (e.key === 'ArrowDown') {
+ e.preventDefault();
+ results._selectedIndex = Math.min(results._selectedIndex + 1, results.children.length - 1);
+ ced_updateSearchHighlight(results);
+ } else if (e.key === 'ArrowUp') {
+ e.preventDefault();
+ results._selectedIndex = Math.max(results._selectedIndex - 1, 0);
+ ced_updateSearchHighlight(results);
+ } else if (e.key === 'Enter') {
+ if (results._selectedIndex >= 0) {
+ e.preventDefault();
+ ced_selectSearchResult(results.children[results._selectedIndex]);
+ }
+ } else if (e.key === 'Escape') {
+ results.style.display = 'none';
+ results._selectedIndex = -1;
+ }
+ });
+ });
+
+ document.addEventListener('click', function(e) {
+ if (!e.target.closest('.search-results') && !e.target.closest('.search-input')) {
+ document.querySelectorAll('.search-results').forEach(function(r) { r.style.display = 'none'; });
+ }
+ });
+}
+
+function ced_selectSearchResult(el) {
+ var results = el.parentElement;
+ var idx = Array.prototype.indexOf.call(results.children, el);
+ if (results._items && results._items[idx] && results._targetInput) {
+ results._targetInput.value = results._items[idx].id;
+ results._targetInput.dispatchEvent(new Event('input'));
+ results.style.display = 'none';
+ results.innerHTML = '';
+ }
+}
+
+function ced_updateSearchHighlight(results) {
+ for (var i = 0; i < results.children.length; i++) {
+ results.children[i].classList.toggle('active', i === results._selectedIndex);
+ if (i === results._selectedIndex) results.children[i].scrollIntoView({block: 'nearest'});
+ }
+}
+
+// ---- Generic field renderer (used by all card-based editors) ----
+
+function ced_renderField(sec, f, data, idx, subPath, optStyle, cardPathFn, fieldIDFn, getValFn) {
+ var key = f[0], label = f[1], type = f[2], hint = f[3], wide = f[4], showIf = f[5], searchEntity = f[6];
+ var fp = subPath ? subPath + '.' + key : key;
+ var fid = fieldIDFn(sec, fp, idx);
+ var val;
+
+ if (sec.isArray && idx >= 0) {
+ var arr = data[cardPathFn(sec)] || [];
+ var item = arr[idx] || {};
+ val = item[key];
+ } else if (subPath) {
+ var sectionRoot = data[cardPathFn(sec)] || {};
+ var subObj = sectionRoot[subPath] || {};
+ val = subObj[key];
+ } else {
+ val = getValFn(data, sec, key);
+ }
+ val = val === undefined || val === null ? '' : val;
+
+ var hiddenStyle = '';
+ if (showIf) {
+ var sectionData = data[cardPathFn(sec)] || {};
+ if (!showIf(sectionData)) hiddenStyle = ' style="display:none"';
+ }
+
+ function wrap(cls, inner) {
+ var w = (wide === 'wide') ? ' obj-wide' : '';
+ if (wide === 'narrow') w = ' obj-narrow';
+ var s = optStyle ? ' style="' + optStyle + '"' : '';
+ return '<label class="' + cls + w + '"' + hiddenStyle + s + '>' + inner + '</label>';
+ }
+
+ if (type === 'checkbox') {
+ return wrap('obj-field obj-check', '<input type="checkbox" id="' + fid + '"' + (val ? ' checked' : '') + '> <span>' + esc(label) + '</span>');
+ }
+ if (type === 'json') {
+ var jstr = typeof val === 'object' ? JSON.stringify(val) : String(val);
+ return wrap('obj-field', '<span>' + esc(label) + '</span><textarea id="' + fid + '" rows="2" placeholder="{}">' + esc(jstr) + '</textarea>');
+ }
+ if (type === 'textarea') {
+ var dstr = typeof val === 'object' ? (Array.isArray(val) ? val.map(function(v){return v.text||'';}).join('\n') : JSON.stringify(val)) : String(val);
+ return wrap('obj-field', '<span>' + esc(label) + '</span><textarea id="' + fid + '" rows="3">' + esc(dstr) + '</textarea>');
+ }
+ if (type === 'select') {
+ var opts = hint ? hint.split('|') : [];
+ var inner = '<span>' + esc(label) + '</span><select id="' + fid + '">';
+ opts.forEach(function(o) {
+ inner += '<option value="' + escAttr(o) + '"' + (String(val) === o ? ' selected' : '') + '>' + esc(o) + '</option>';
+ });
+ inner += '</select>';
+ return wrap('obj-field', inner);
+ }
+ if (type === 'color') {
+ var swatch = '<span class="color-swatch" style="background:#' + escAttr(String(val) || '808080') + '"></span>';
+ return wrap('obj-field color-field', '<span>' + esc(label) + '</span><div class="obj-color-row">' + swatch + '<input id="' + fid + '" value="' + escAttr(String(val)) + '" class="color-input"></div>');
+ }
+ if (type === 'search') {
+ var entity = searchEntity || 'items';
+ var ph = hint ? ' placeholder="' + esc(hint) + '"' : ' placeholder="Search ' + entity + '..."';
+ return wrap('obj-field obj-search', '<span>' + esc(label) + '</span><div style="position:relative;width:100%"><input id="' + fid + '" value="' + escAttr(String(val)) + '" class="search-input" data-search-type="' + entity + '"' + ph + '><div class="search-results" style="display:none"></div></div>');
+ }
+ var ph = hint ? ' placeholder="' + esc(hint) + '"' : '';
+ return wrap('obj-field', '<span>' + esc(label) + '</span><input id="' + fid + '" value="' + escAttr(String(val)) + '"' + ph + '>');
+}
+
+// ---- Subtable row add/remove ----
+
+function ced_addSubRow(sec, subKey, dataRef, cardPathFn) {
+ var p = cardPathFn(sec);
+ var sectionRoot = p ? (dataRef[p] || {}) : dataRef;
+ var raw = sectionRoot[subKey];
+ var arr;
+ if (Array.isArray(raw)) {
+ arr = raw;
+ } else if (raw && typeof raw === 'object') {
+ arr = [raw];
+ } else {
+ arr = [];
+ }
+ var empty = {};
+ var st = (sec.subtables || []).find(function(s) { return s.key === subKey; });
+ if (st) {
+ st.fields.forEach(function(f) {
+ empty[f[0]] = f[2] === 'checkbox' ? false : (f[2] === 'number' ? 0 : '');
+ });
+ }
+ arr.push(empty);
+ sectionRoot[subKey] = arr;
+}
+
+function ced_removeSubRow(sec, subKey, idx, dataRef, cardPathFn) {
+ var p = cardPathFn(sec);
+ var sectionRoot = p ? (dataRef[p] || {}) : dataRef;
+ var raw = sectionRoot[subKey];
+ var arr;
+ if (Array.isArray(raw)) {
+ arr = raw;
+ } else if (raw && typeof raw === 'object') {
+ arr = [raw];
+ } else {
+ arr = [];
+ }
+ arr.splice(idx, 1);
+}
+
+// ---- Field save helpers ----
+
+function ced_saveSectionFields(sec, sectionObj, cardPathFn, fieldIDFn) {
+ var hasValues = sec.always;
+ sec.fields.forEach(function(f) {
+ var fid = fieldIDFn(sec, f[0]);
+ var el = document.getElementById(fid);
+ if (!el) return;
+ var val = ced_collectFieldValue(el, f[2]);
+ if (val !== '' && val !== false && val !== 0 && val !== null) hasValues = true;
+ sectionObj[f[0]] = val;
+ });
+ return hasValues;
+}
+
+function ced_saveSubtable(sec, st, sectionObj, cardPathFn, fieldIDSubFn) {
+ var items = [];
+ for (var i = 0; ; i++) {
+ var item = {};
+ var found = false;
+ st.fields.forEach(function(f) {
+ var fid = fieldIDSubFn(sec, st.key, i, f[0]);
+ var el = document.getElementById(fid);
+ if (el) found = true;
+ if (el) item[f[0]] = ced_collectFieldValue(el, f[2]);
+ });
+ if (!found) break;
+ if (Object.keys(item).length > 0) items.push(item);
+ }
+ if (st.single) {
+ if (items.length > 0) { sectionObj[st.key] = items[0]; return true; }
+ } else {
+ if (items.length > 0) { sectionObj[st.key] = items; return true; }
+ }
+ return false;
+}
+
+function ced_saveInlineFields(sec, sectionObj, cardPathFn, fieldIDFn) {
+ var hasValues = false;
+ sec.inline.forEach(function(il) {
+ if (il.type === 'tags') {
+ var fid = fieldIDFn(sec, il.key);
+ var el = document.getElementById(fid);
+ if (el && el.value.trim()) {
+ sectionObj[il.key] = el.value.split(',').map(function(s) { return s.trim(); }).filter(function(s) { return s; });
+ hasValues = true;
+ }
+ } else if (il.type === 'kv') {
+ var kvEl = document.querySelector('.obj-kv-list[data-sec="' + sec.id + '"][data-key="' + il.key + '"]');
+ if (kvEl) {
+ var map = {};
+ kvEl.querySelectorAll('.obj-kv-row').forEach(function(row) {
+ var keyEl = row.querySelector('.obj-kv-key');
+ var valEl = row.querySelector('.obj-kv-val');
+ if (keyEl && keyEl.value.trim() && valEl) {
+ map[keyEl.value.trim()] = parseFloat(valEl.value) || 0;
+ }
+ });
+ if (Object.keys(map).length > 0) { sectionObj[il.key] = map; hasValues = true; }
+ }
+ } else {
+ var ssec = {};
+ var sHas = false;
+ il.fields.forEach(function(f) {
+ var fp = il.path + '.' + f[0];
+ var fid = fieldIDFn(sec, fp);
+ var el = document.getElementById(fid);
+ if (el) sHas = true;
+ if (el) ssec[f[0]] = ced_collectFieldValue(el, f[2]);
+ });
+ if (sHas) { sectionObj[il.path] = ssec; hasValues = true; }
+ }
+ });
+ return hasValues;
+}
+
+// ---- Validation ----
+
+function ced_validateFields(SECTIONS, fieldIDFn, fieldIDSubFn) {
+ var errors = [];
+ document.querySelectorAll('.obj-field-error').forEach(function(field) {
+ field.classList.remove('obj-field-error');
+ });
+
+ document.querySelectorAll('input[id][type="text"], input[id]:not([type])').forEach(function(input) {
+ var id = input.id;
+ if (!id) return;
+ var labelEl = input.closest('.obj-field');
+ if (!labelEl) return;
+
+ if (input.classList.contains('search-input') || input.classList.contains('color-input')) return;
+
+ var isNumber = false;
+ var labelText = '';
+
+ var fieldDiv = labelEl.closest('[data-card]');
+ var cardID = fieldDiv ? fieldDiv.getAttribute('data-card') : '';
+ var sec = SECTIONS.find(function(s) { return s.id === cardID; });
+ if (!sec) return;
+
+ var baseMatch = id.match(/^f_(\w+)_(\w+)$/);
+ var subMatch = id.match(/^f_(\w+)_(\w+)_(\d+)_(\w.+)$/);
+
+ if (subMatch) {
+ var subKey = subMatch[2], fieldKey = subMatch[4];
+ var st = (sec.subtables || []).find(function(s) { return s.key === subKey; });
+ if (st) {
+ var nf = st.fields.find(function(f) { return f[0] === fieldKey && f[2] === 'number'; });
+ if (nf) { isNumber = true; labelText = nf[1]; }
+ }
+ } else if (baseMatch) {
+ var fieldKey = baseMatch[2];
+ var nf = sec.fields ? sec.fields.find(function(f) {
+ var fid = fieldIDFn(sec, f[0], -1);
+ return fid === id && f[2] === 'number';
+ }) : null;
+ if (!nf && sec.inline) {
+ sec.inline.forEach(function(il) {
+ if (il.fields) {
+ var inf = il.fields.find(function(f) {
+ var fid = fieldIDFn(sec, il.path + '.' + f[0], -1);
+ return fid === id && f[2] === 'number';
+ });
+ if (inf) nf = inf;
+ }
+ });
+ }
+ if (nf) { isNumber = true; labelText = nf[1]; }
+ }
+
+ if (isNumber) {
+ var rawVal = input.value.trim();
+ if (rawVal !== '' && !/^-?\d*\.?\d*$/.test(rawVal)) {
+ labelEl.classList.add('obj-field-error');
+ input.focus();
+ input.select();
+ labelText = labelText || (labelEl.querySelector('span') || {}).textContent || '';
+ errors.push('Invalid value in "' + esc(labelText) + '": "' + esc(rawVal) + '"');
+ }
+ }
+ });
+
+ if (errors.length > 0) {
+ notify(errors.join('; '), 'error');
+ return false;
+ }
+ return true;
+}
+
+// ---- Output cleanup ----
+
+function ced_cleanOutput(obj) {
+ for (var key in obj) {
+ var val = obj[key];
+ if (val === '' || val === null || val === undefined) {
+ delete obj[key];
+ } else if (typeof val === 'object' && val !== null && !Array.isArray(val)) {
+ ced_cleanOutput(val);
+ if (Object.keys(val).length === 0) delete obj[key];
+ } else if (Array.isArray(val) && val.length === 0) {
+ delete obj[key];
+ }
+ }
+}