var editorType = ''; var editorFields = []; var currentID = null; var allIDs = []; var treeData = { root: [], dirs: {} }; var collapsedDirs = {}; 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) { if (Array.isArray(data)) { treeData = { root: data, dirs: {} }; allIDs = data.slice(); } else { treeData = { root: data.root || [], dirs: data.dirs || {} }; allIDs = treeData.root.slice(); Object.keys(treeData.dirs).sort().forEach(function(dir) { Array.prototype.push.apply(allIDs, treeData.dirs[dir]); }); } renderList(''); }).catch(function(e) { notify('Failed to load list: ' + e.message, 'error'); }); } function renderList(filter) { var el = $('#listEntries'); var f = (filter || '').toLowerCase(); var html = ''; var hasDirs = Object.keys(treeData.dirs).length > 0; if (treeData.root.length > 0 || !hasDirs) { html += renderDirGroup(hasDirs ? 'Root' : '', treeData.root, f); } if (hasDirs) { Object.keys(treeData.dirs).sort().forEach(function(dir) { html += renderDirGroup(dir, treeData.dirs[dir], f); }); } el.innerHTML = html; } function renderDirGroup(name, ids, filter) { if (ids.length === 0 && !filter && !name) return ''; var isFiltering = !!filter; var displayIds = ids; if (isFiltering) { displayIds = ids.filter(function(id) { return String(id).toLowerCase().indexOf(filter) >= 0; }); if (displayIds.length === 0) return ''; } var expanded = isFiltering ? true : (collapsedDirs[name] === true); var displayName = name || editorType.charAt(0).toUpperCase() + editorType.slice(1); var h = '
Failed to load: ' + esc(e.message) + '
'; }); } function renderEditor(id, data) { var obj = data; var html = 'Deleted
'; 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'; if (tab === 'yaml' && currentID) { API.get('/api/' + editorType + '/' + encodeURIComponent(currentID)).then(function(data) { var pre = document.querySelector('#tabYaml pre'); if (pre) pre.textContent = data._raw || JSON.stringify(data, null, 2); }); } } 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,'"'); } function escAttr(s) { return String(s || '').replace(/&/g,'&').replace(/"/g,'"'); } window.refreshPage = function() { loadList(); if (currentID) loadItem(currentID); }; function renderMoveBar() { var dirs = Object.keys(treeData.dirs).sort(); var html = ''; html += ''; return html; } function moveToDirectory() { var sel = $('#moveDirSelect'); var idx = sel.selectedIndex; if (idx === 0) return; var dir = sel.value; API.post('/api/' + editorType + '/move', {id: currentID, dir: dir}).then(function() { notify('Moved ' + currentID + ' to ' + (dir || 'Root'), 'success'); updateUndoBar(); loadList(); loadItem(currentID); }).catch(function(e) { notify('Move failed: ' + e.message, 'error'); }); } function createNewDir() { var name = prompt('New directory name:'); if (!name) return; API.post('/api/' + editorType + '/dirs', {action: 'create', name: name}).then(function() { notify('Directory ' + name + ' created', 'success'); updateUndoBar(); loadList(); }).catch(function(e) { notify('Failed to create directory: ' + e.message, 'error'); }); } function showDirContextMenu(e, dir) { e.preventDefault(); closeContextMenu(); if (dir === 'Root') return; var overlay = document.createElement('div'); overlay.className = 'cm-overlay'; overlay.id = '_ctxMenuOverlay'; overlay.onclick = function() { closeContextMenu(); }; overlay.addEventListener('contextmenu', function(ev) { ev.preventDefault(); ev.stopPropagation(); var cx = ev.clientX, cy = ev.clientY; closeContextMenu(); var el = document.elementFromPoint(cx, cy); if (el) el.dispatchEvent(new MouseEvent('contextmenu', {bubbles:true, cancelable:true, clientX:cx, clientY:cy, button:2})); }); var menu = document.createElement('div'); menu.className = 'cm-menu'; menu.id = '_ctxMenu'; menu.style.left = e.clientX + 'px'; menu.style.top = e.clientY + 'px'; menu.oncontextmenu = function(ev) { ev.preventDefault(); }; function addBtn(label, cls, handler) { var btn = document.createElement('button'); btn.textContent = label; if (cls) btn.className = cls; btn.onclick = function() { closeContextMenu(); handler(); }; menu.appendChild(btn); } addBtn('Rename Directory', '', function() { renameDirInline(dir); }); addBtn('Delete Directory', 'danger', function() { API.post('/api/' + editorType + '/dirs', {action: 'delete', dir: dir}).then(function(r) { notify('Deleted directory ' + dir + (r.moved ? ' (' + r.moved + ' files moved to root)' : ''), 'success'); updateUndoBar(); loadList(); }).catch(function(e) { notify('Delete failed: ' + e.message, 'error'); }); }); document.body.appendChild(overlay); document.body.appendChild(menu); } function renameDirInline(dir) { var name = prompt('New name for directory "' + dir + '":', dir); if (!name || name === dir) return; API.post('/api/' + editorType + '/dirs', {action: 'rename_dir', dir: dir, new_name: name}).then(function() { notify('Renamed directory ' + dir + ' to ' + name, 'success'); loadList(); }).catch(function(e) { notify('Rename failed: ' + e.message, 'error'); }); } function showItemContextMenu(e, id) { e.preventDefault(); e.stopPropagation(); closeContextMenu(); var overlay = document.createElement('div'); overlay.className = 'cm-overlay'; overlay.id = '_ctxMenuOverlay'; overlay.onclick = function() { closeContextMenu(); }; overlay.addEventListener('contextmenu', function(ev) { ev.preventDefault(); ev.stopPropagation(); var cx = ev.clientX, cy = ev.clientY; closeContextMenu(); var el = document.elementFromPoint(cx, cy); if (el) el.dispatchEvent(new MouseEvent('contextmenu', {bubbles:true, cancelable:true, clientX:cx, clientY:cy, button:2})); }); var menu = document.createElement('div'); menu.className = 'cm-menu'; menu.id = '_ctxMenu'; menu.style.left = e.clientX + 'px'; menu.style.top = e.clientY + 'px'; menu.oncontextmenu = function(ev) { ev.preventDefault(); }; function addBtn(label, cls, handler) { var btn = document.createElement('button'); btn.textContent = label; if (cls) btn.className = cls; btn.onclick = function() { closeContextMenu(); handler(); }; menu.appendChild(btn); } addBtn('Rename', '', function() { startInlineItemRename(id); }); addBtn('Duplicate', '', function() { duplicateFromMenu(id); }); addBtn('Delete', 'danger', function() { deleteItem(id); }); document.body.appendChild(overlay); document.body.appendChild(menu); } function duplicateFromMenu(id) { API.get('/api/' + editorType + '/' + encodeURIComponent(id)).then(function(data) { var dir = ''; if (data._path) { var parts = data._path.split('/'); var parentDir = parts[parts.length - 2]; if (parentDir !== editorType) dir = parentDir; } var clean = JSON.parse(JSON.stringify(data)); delete clean._raw; delete clean._path; clean.id = ''; var baseID = id + '_copy'; var newID = baseID; var counter = 2; while (true) { var exists = false; for (var i = 0; i < allIDs.length; i++) { if (allIDs[i] === newID) { exists = true; break; } } if (!exists) break; newID = baseID + '_' + counter; counter++; } clean.id = newID; API.post('/api/' + editorType, {id: newID}).then(function() { var putOp = API.put('/api/' + editorType + '/' + encodeURIComponent(newID), clean); if (dir) { return putOp.then(function() { return API.post('/api/' + editorType + '/move', {id: newID, dir: dir}); }); } return putOp; }).then(function() { notify('Duplicated as ' + newID, 'success'); updateUndoBar(); loadList(); loadItem(newID); }).catch(function(e) { notify('Duplicate failed: ' + e.message, 'error'); }); }).catch(function(e) { notify('Duplicate failed: ' + e.message, 'error'); }); } function closeContextMenu() { var o = document.getElementById('_ctxMenuOverlay'); var m = document.getElementById('_ctxMenu'); if (o) o.remove(); if (m) m.remove(); } function startInlineItemRename(id) { var el = document.querySelector('.item.active'); if (!el) return; var origHTML = el.innerHTML; el.innerHTML = ''; var input = el.querySelector('.item-rename-input'); input.focus(); input.select(); function finish(newID) { newID = (newID || '').trim(); if (!newID || newID === id) { el.innerHTML = origHTML; return; } if (allIDs.indexOf(newID) >= 0) { notify('An entity named "' + newID + '" already exists.', 'error'); el.innerHTML = origHTML; return; } entityRename(editorType, id, newID).then(function() { if (currentID === id) currentID = newID; var typeIDVar = editorType.slice(0, -1) + 'ID'; if (window[typeIDVar] === id) window[typeIDVar] = newID; loadList(); if (currentID === newID) loadItem(newID); }).catch(function(e) { notify('Rename failed: ' + e.message, 'error'); el.innerHTML = origHTML; }); } input.onkeydown = function(e) { if (e.key === 'Enter') { input.blur(); } else if (e.key === 'Escape') { el.innerHTML = origHTML; } }; input.onblur = function() { finish(input.value); }; } function dirDragOver(e, dir) { e.preventDefault(); e.stopPropagation(); e.currentTarget.classList.add('dir-drop-target'); } function dirDragLeave(e) { e.currentTarget.classList.remove('dir-drop-target'); } function dirDrop(e, dir) { e.preventDefault(); e.stopPropagation(); e.currentTarget.classList.remove('dir-drop-target'); if (dir === 'Root') dir = ''; var id = e.dataTransfer.getData('text/plain'); if (!id) return; API.post('/api/' + editorType + '/move', {id: id, dir: dir}).then(function() { notify('Moved ' + id + ' to ' + (dir || 'Root'), 'success'); updateUndoBar(); loadList(); if (currentID === id) loadItem(id); }).catch(function(e) { notify('Move failed: ' + e.message, 'error'); }); } function itemDragStart(e, id) { e.dataTransfer.setData('text/plain', id); e.dataTransfer.effectAllowed = 'move'; }