Loading editor/html/css/editor.css +63 −0 Changes for editor/html/css/editor.css: 63 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -298,6 +298,69 @@ body { margin: 0; } /* Element context menu (right-click on a document-tree node) */ .context-menu { width: auto; min-width: 220px; padding: 4px 0; } .context-menu ul { list-style: none; margin: 0; padding: 0; } .context-menu li { padding: 0.5em 1em; cursor: pointer; font-size: 0.85rem; white-space: nowrap; } .context-menu li:hover { background: var(--bg-secondary); } /* Tools dialog: global element template list */ .tools-tpl-row { display: flex; align-items: center; justify-content: space-between; gap: 0.5em; padding: 0.5em; border: 1px solid var(--border); border-radius: 6px; margin-bottom: 0.4em; } .tools-tpl-info { display: flex; flex-direction: column; gap: 0.1em; min-width: 0; } .tools-tpl-meta { font-size: 0.75rem; color: var(--text-muted); } .tools-tpl-actions { display: flex; gap: 0.4em; flex-shrink: 0; } .tools-tpl-actions button { padding: 0.2em 0.6em; font-size: 0.8rem; } .tools-tpl-delete { background: #c00 !important; } /* Properties Panel (docked in #sidebar-right) */ #properties-panel { flex: 1; Loading editor/html/index.html +35 −0 Changes for editor/html/index.html: 35 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -51,6 +51,7 @@ <span id="doc-name" class="doc-name">Untitled</span> </div> <div class="toolbar-group" style="margin-left:auto"> <button id="btn-tools" data-i18n-title="I18N_TOOLS" title="Werkzeuge">🛠 <span data-i18n="I18N_TOOLS">Werkzeuge</span></button> <button id="btn-settings" data-i18n-title="Settings">⚙ <span data-i18n="Settings">Einstellungen</span></button> <button id="btn-logout" data-i18n-title="Logout">🚪 <span data-i18n="Logout">Abmelden</span></button> </div> Loading Loading @@ -115,6 +116,16 @@ <div id="widget-type-list"></div> </div> <!-- Element context menu: opened by right-click on a document-tree node. --> <div id="element-context-menu" class="floating-panel context-menu" style="display:none"> <ul> <li data-action="copy" data-i18n="I18N_COPY_ELEMENT">Kopieren</li> <li data-action="paste" data-i18n="I18N_PASTE_ELEMENT">Einfügen</li> <li data-action="save-template" data-i18n="I18N_SAVE_AS_TEMPLATE">Als globale Vorlage speichern…</li> <li data-action="remove" data-i18n="I18N_REMOVE_ELEMENT">Entfernen</li> </ul> </div> <!-- Dialogs --> <!-- Merged Projects + Templates modal: the sidebar picks a project (which auto- connects and filters the template list); the content area shows that project's Loading Loading @@ -542,6 +553,29 @@ </div> </dialog> <!-- Tools Dialog: "Werkzeuge" — currently just the global element template library ("Vorlagen"); the set-nav leaves room for future tool sections. --> <dialog id="tools-dialog" class="settings-dialog"> <div class="set-modal"> <div class="set-sidebar"> <h3 data-i18n="I18N_TOOLS">Werkzeuge</h3> <ul class="set-nav"> <li class="set-nav-item active" data-section="templates" data-i18n="I18N_TEMPLATES">Vorlagen</li> </ul> </div> <div class="set-content" id="tools-content"> <div id="tools-templates" class="set-section"> <h4 data-i18n="I18N_GLOBAL_TEMPLATES">Globale Vorlagen</h4> <p class="hint" data-i18n="I18N_GLOBAL_TEMPLATES_HINT">Per Rechtsklick auf ein Element im Dokumentbaum kannst du es (inkl. Kinder) als globale Vorlage speichern.</p> <div id="tools-template-list"></div> </div> </div> </div> <div class="dialog-actions"> <button id="btn-tools-close" data-i18n="Close">Schließen</button> </div> </dialog> <script src="js/i18n.js"></script> <script src="js/api.js"></script> <script src="js/tree.js"></script> Loading @@ -550,6 +584,7 @@ <script src="js/preview.js"></script> <script src="js/media-browser.js"></script> <script src="js/survey-manager.js"></script> <script src="js/template-manager.js"></script> <script src="js/editor.js"></script> </body> </html> editor/html/js/api.js +21 −0 Changes for editor/html/js/api.js: 21 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -485,6 +485,27 @@ var EditorApi = (function() { return request('DELETE', '/api/survey/delete/' + encodeURIComponent(id)); }, // Global element templates (reusable XML subtrees) listElementTemplates: function() { return request('GET', '/api/element-templates'); }, getElementTemplate: function(id) { return request('GET', '/api/element-template/' + encodeURIComponent(id)); }, saveElementTemplate: function(uuid, name) { return request('POST', '/api/element-template/save', { uuid: uuid, name: name }); }, renameElementTemplate: function(id, name) { return request('POST', '/api/element-template/rename', { id: id, name: name }); }, deleteElementTemplate: function(id) { return request('DELETE', '/api/element-template/delete/' + encodeURIComponent(id)); }, publishSurvey: function(surveyId, connId) { return requestWithReconnect(connId, 'POST', '/api/survey/publish', { survey_id: surveyId, conn_id: connId }); Loading editor/html/js/editor.js +73 −1 Changes for editor/html/js/editor.js: 73 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -35,7 +35,9 @@ try { bindSettingsDialog(); } catch(e) { console.error('bindSettingsDialog:', e); } try { bindAiDialog(); } catch(e) { console.error('bindAiDialog:', e); } try { bindClipboardShortcuts(); } catch(e) { console.error('bindClipboardShortcuts:', e); } try { bindElementContextMenu(); } catch(e) { console.error('bindElementContextMenu:', e); } try { SurveyManager.init(); } catch(e) { console.error('SurveyManager.init:', e); } try { TemplateManager.init(refreshDocument); } catch(e) { console.error('TemplateManager.init:', e); } try { Preview.bindToggle(); } catch(e) { console.error('Preview.bindToggle:', e); } try { Canvas.bindMessageListener(); } catch(e) { console.error('Canvas.bindMessageListener:', e); } try { initProjectSelector(); } catch(e) { console.error('initProjectSelector:', e); } Loading Loading @@ -179,6 +181,68 @@ }); } // --- Element context menu (right-click on a document-tree node) --- function closeElementContextMenu() { var menu = document.getElementById('element-context-menu'); if (menu) menu.style.display = 'none'; document.removeEventListener('mousedown', onElementContextMenuOutsideClick, true); } function onElementContextMenuOutsideClick(ev) { var menu = document.getElementById('element-context-menu'); if (menu && !menu.contains(ev.target)) closeElementContextMenu(); } function openElementContextMenu(uuid, x, y) { var menu = document.getElementById('element-context-menu'); if (!menu) return; menu.style.left = x + 'px'; menu.style.top = y + 'px'; menu.style.display = 'block'; setTimeout(function() { document.addEventListener('mousedown', onElementContextMenuOutsideClick, true); }, 0); } function bindElementContextMenu() { var menu = document.getElementById('element-context-menu'); if (!menu) return; menu.addEventListener('click', function(e) { var item = e.target.closest('li[data-action]'); if (!item) return; var action = item.getAttribute('data-action'); closeElementContextMenu(); if (action === 'copy') { copySelected(); } else if (action === 'paste') { pasteClipboard(); } else if (action === 'remove') { removeSelected(); } else if (action === 'save-template') { saveSelectedAsTemplate(); } }); DocumentTree.setOnContextMenu(function(uuid, x, y) { Canvas.setSelectedUuid(uuid); Preview.highlight(uuid); PropertiesPanel.showProperties(uuid); openElementContextMenu(uuid, x, y); }); } function saveSelectedAsTemplate() { var uuid = Canvas.getSelectedUuid(); if (!uuid) return; var name = prompt(I18n.t('I18N_TEMPLATE_NAME_PROMPT', 'Name der Vorlage:'), ''); if (!name) return; EditorApi.saveElementTemplate(uuid, name).catch(function(err) { console.error('Failed to save element template:', err); alert(I18n.t('I18N_ACTION_FAILED', 'Aktion fehlgeschlagen') + ': ' + ((err && err.error) || '')); }); } // --- Refresh document tree --- function refreshDocument() { Loading Loading @@ -281,6 +345,10 @@ SurveyManager.open(); }); document.getElementById('btn-tools').addEventListener('click', function() { TemplateManager.open(); }); document.getElementById('btn-publish-now').addEventListener('click', function() { publishFromToolbar(); }); Loading Loading @@ -351,6 +419,11 @@ }); document.getElementById('btn-remove').addEventListener('click', function() { removeSelected(); }); } function removeSelected() { var uuid = Canvas.getSelectedUuid(); if (!uuid) return; if (!confirm(I18n.t('I18N_REMOVE_CONFIRM') + '?')) return; Loading @@ -359,7 +432,6 @@ PropertiesPanel.clear(); refreshDocument(); }); }); } // --- Save dialog --- Loading editor/html/js/template-manager.js 0 → 100644 +141 −0 Changes for editor/html/js/template-manager.js: 141 added lines, 0 removed lines. Original line number Diff line number Diff line /******************************************************************************* * blogi-webedit Template Manager ("Werkzeuge" > "Vorlagen") * * Lists global element templates (reusable XML subtrees saved from the * document tree's right-click "Als globale Vorlage speichern..." action) and * lets the user insert / rename / delete them. *******************************************************************************/ var TemplateManager = (function() { 'use strict'; var _onChange = null; function el(id) { return document.getElementById(id); } function formatDate(iso) { if (!iso) return ''; return iso.replace('T', ' ').substring(0, 16); } function loadTemplates() { var list = el('tools-template-list'); list.innerHTML = '<p class="hint">' + I18n.t('I18N_LOADING', 'Lade...') + '</p>'; EditorApi.listElementTemplates().then(function(resp) { renderList(resp.templates || []); }).catch(function(err) { console.error('Failed to load element templates:', err); list.innerHTML = '<p class="hint">' + I18n.t('I18N_FAILED_LOAD_TEMPLATES', 'Vorlagen konnten nicht geladen werden') + '</p>'; }); } function renderList(templates) { var list = el('tools-template-list'); list.innerHTML = ''; if (!templates.length) { list.innerHTML = '<p class="hint">' + I18n.t('I18N_NO_TEMPLATES', 'Keine globalen Vorlagen vorhanden.') + '</p>'; return; } templates.forEach(function(tpl) { var row = document.createElement('div'); row.className = 'tools-tpl-row'; var info = document.createElement('div'); info.className = 'tools-tpl-info'; info.innerHTML = '<strong>' + escapeHtml(tpl.name) + '</strong>' + '<span class="tools-tpl-meta">' + escapeHtml(tpl.root_type) + (tpl.modified ? ' · ' + escapeHtml(formatDate(tpl.modified)) : '') + '</span>'; row.appendChild(info); var actions = document.createElement('div'); actions.className = 'tools-tpl-actions'; var btnInsert = document.createElement('button'); btnInsert.className = 'set-save-btn'; btnInsert.textContent = I18n.t('I18N_INSERT_TEMPLATE', 'Einfügen'); btnInsert.addEventListener('click', function() { insertTemplate(tpl.id); }); actions.appendChild(btnInsert); var btnRename = document.createElement('button'); btnRename.className = 'set-save-btn'; btnRename.textContent = I18n.t('I18N_RENAME_TEMPLATE', 'Umbenennen'); btnRename.addEventListener('click', function() { renameTemplate(tpl.id, tpl.name); }); actions.appendChild(btnRename); var btnDelete = document.createElement('button'); btnDelete.className = 'set-save-btn tools-tpl-delete'; btnDelete.textContent = I18n.t('I18N_DELETE_TEMPLATE', 'Löschen'); btnDelete.addEventListener('click', function() { deleteTemplate(tpl.id); }); actions.appendChild(btnDelete); row.appendChild(actions); list.appendChild(row); }); } function escapeHtml(s) { return (s || '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>'); } function insertTemplate(id) { EditorApi.getElementTemplate(id).then(function(tpl) { var parentUuid = Canvas.getSelectedUuid() || ''; var position = ''; if (parentUuid) { position = Canvas.canSelectedHaveChildren() ? 'child' : 'after'; } return EditorApi.pasteElement(tpl.xml, parentUuid, position); }).then(function() { if (_onChange) _onChange(); document.getElementById('tools-dialog').close(); }).catch(function(err) { console.error('Failed to insert element template:', err); alert(I18n.t('I18N_ACTION_FAILED', 'Aktion fehlgeschlagen') + ': ' + ((err && err.error) || '')); }); } function renameTemplate(id, currentName) { var name = prompt(I18n.t('I18N_TEMPLATE_NAME_PROMPT', 'Name der Vorlage:'), currentName || ''); if (!name || name === currentName) return; EditorApi.renameElementTemplate(id, name).then(loadTemplates).catch(function(err) { console.error('Failed to rename element template:', err); alert(I18n.t('I18N_ACTION_FAILED', 'Aktion fehlgeschlagen') + ': ' + ((err && err.error) || '')); }); } function deleteTemplate(id) { if (!confirm(I18n.t('I18N_DELETE_TEMPLATE_CONFIRM', 'Vorlage löschen?'))) return; EditorApi.deleteElementTemplate(id).then(loadTemplates).catch(function(err) { console.error('Failed to delete element template:', err); alert(I18n.t('I18N_ACTION_FAILED', 'Aktion fehlgeschlagen') + ': ' + ((err && err.error) || '')); }); } function open() { loadTemplates(); document.getElementById('tools-dialog').showModal(); } function init(onChange) { _onChange = typeof onChange === 'function' ? onChange : null; var closeBtn = el('btn-tools-close'); if (closeBtn) { closeBtn.addEventListener('click', function() { document.getElementById('tools-dialog').close(); }); } } return { init: init, open: open }; })(); Loading
editor/html/css/editor.css +63 −0 Changes for editor/html/css/editor.css: 63 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -298,6 +298,69 @@ body { margin: 0; } /* Element context menu (right-click on a document-tree node) */ .context-menu { width: auto; min-width: 220px; padding: 4px 0; } .context-menu ul { list-style: none; margin: 0; padding: 0; } .context-menu li { padding: 0.5em 1em; cursor: pointer; font-size: 0.85rem; white-space: nowrap; } .context-menu li:hover { background: var(--bg-secondary); } /* Tools dialog: global element template list */ .tools-tpl-row { display: flex; align-items: center; justify-content: space-between; gap: 0.5em; padding: 0.5em; border: 1px solid var(--border); border-radius: 6px; margin-bottom: 0.4em; } .tools-tpl-info { display: flex; flex-direction: column; gap: 0.1em; min-width: 0; } .tools-tpl-meta { font-size: 0.75rem; color: var(--text-muted); } .tools-tpl-actions { display: flex; gap: 0.4em; flex-shrink: 0; } .tools-tpl-actions button { padding: 0.2em 0.6em; font-size: 0.8rem; } .tools-tpl-delete { background: #c00 !important; } /* Properties Panel (docked in #sidebar-right) */ #properties-panel { flex: 1; Loading
editor/html/index.html +35 −0 Changes for editor/html/index.html: 35 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -51,6 +51,7 @@ <span id="doc-name" class="doc-name">Untitled</span> </div> <div class="toolbar-group" style="margin-left:auto"> <button id="btn-tools" data-i18n-title="I18N_TOOLS" title="Werkzeuge">🛠 <span data-i18n="I18N_TOOLS">Werkzeuge</span></button> <button id="btn-settings" data-i18n-title="Settings">⚙ <span data-i18n="Settings">Einstellungen</span></button> <button id="btn-logout" data-i18n-title="Logout">🚪 <span data-i18n="Logout">Abmelden</span></button> </div> Loading Loading @@ -115,6 +116,16 @@ <div id="widget-type-list"></div> </div> <!-- Element context menu: opened by right-click on a document-tree node. --> <div id="element-context-menu" class="floating-panel context-menu" style="display:none"> <ul> <li data-action="copy" data-i18n="I18N_COPY_ELEMENT">Kopieren</li> <li data-action="paste" data-i18n="I18N_PASTE_ELEMENT">Einfügen</li> <li data-action="save-template" data-i18n="I18N_SAVE_AS_TEMPLATE">Als globale Vorlage speichern…</li> <li data-action="remove" data-i18n="I18N_REMOVE_ELEMENT">Entfernen</li> </ul> </div> <!-- Dialogs --> <!-- Merged Projects + Templates modal: the sidebar picks a project (which auto- connects and filters the template list); the content area shows that project's Loading Loading @@ -542,6 +553,29 @@ </div> </dialog> <!-- Tools Dialog: "Werkzeuge" — currently just the global element template library ("Vorlagen"); the set-nav leaves room for future tool sections. --> <dialog id="tools-dialog" class="settings-dialog"> <div class="set-modal"> <div class="set-sidebar"> <h3 data-i18n="I18N_TOOLS">Werkzeuge</h3> <ul class="set-nav"> <li class="set-nav-item active" data-section="templates" data-i18n="I18N_TEMPLATES">Vorlagen</li> </ul> </div> <div class="set-content" id="tools-content"> <div id="tools-templates" class="set-section"> <h4 data-i18n="I18N_GLOBAL_TEMPLATES">Globale Vorlagen</h4> <p class="hint" data-i18n="I18N_GLOBAL_TEMPLATES_HINT">Per Rechtsklick auf ein Element im Dokumentbaum kannst du es (inkl. Kinder) als globale Vorlage speichern.</p> <div id="tools-template-list"></div> </div> </div> </div> <div class="dialog-actions"> <button id="btn-tools-close" data-i18n="Close">Schließen</button> </div> </dialog> <script src="js/i18n.js"></script> <script src="js/api.js"></script> <script src="js/tree.js"></script> Loading @@ -550,6 +584,7 @@ <script src="js/preview.js"></script> <script src="js/media-browser.js"></script> <script src="js/survey-manager.js"></script> <script src="js/template-manager.js"></script> <script src="js/editor.js"></script> </body> </html>
editor/html/js/api.js +21 −0 Changes for editor/html/js/api.js: 21 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -485,6 +485,27 @@ var EditorApi = (function() { return request('DELETE', '/api/survey/delete/' + encodeURIComponent(id)); }, // Global element templates (reusable XML subtrees) listElementTemplates: function() { return request('GET', '/api/element-templates'); }, getElementTemplate: function(id) { return request('GET', '/api/element-template/' + encodeURIComponent(id)); }, saveElementTemplate: function(uuid, name) { return request('POST', '/api/element-template/save', { uuid: uuid, name: name }); }, renameElementTemplate: function(id, name) { return request('POST', '/api/element-template/rename', { id: id, name: name }); }, deleteElementTemplate: function(id) { return request('DELETE', '/api/element-template/delete/' + encodeURIComponent(id)); }, publishSurvey: function(surveyId, connId) { return requestWithReconnect(connId, 'POST', '/api/survey/publish', { survey_id: surveyId, conn_id: connId }); Loading
editor/html/js/editor.js +73 −1 Changes for editor/html/js/editor.js: 73 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -35,7 +35,9 @@ try { bindSettingsDialog(); } catch(e) { console.error('bindSettingsDialog:', e); } try { bindAiDialog(); } catch(e) { console.error('bindAiDialog:', e); } try { bindClipboardShortcuts(); } catch(e) { console.error('bindClipboardShortcuts:', e); } try { bindElementContextMenu(); } catch(e) { console.error('bindElementContextMenu:', e); } try { SurveyManager.init(); } catch(e) { console.error('SurveyManager.init:', e); } try { TemplateManager.init(refreshDocument); } catch(e) { console.error('TemplateManager.init:', e); } try { Preview.bindToggle(); } catch(e) { console.error('Preview.bindToggle:', e); } try { Canvas.bindMessageListener(); } catch(e) { console.error('Canvas.bindMessageListener:', e); } try { initProjectSelector(); } catch(e) { console.error('initProjectSelector:', e); } Loading Loading @@ -179,6 +181,68 @@ }); } // --- Element context menu (right-click on a document-tree node) --- function closeElementContextMenu() { var menu = document.getElementById('element-context-menu'); if (menu) menu.style.display = 'none'; document.removeEventListener('mousedown', onElementContextMenuOutsideClick, true); } function onElementContextMenuOutsideClick(ev) { var menu = document.getElementById('element-context-menu'); if (menu && !menu.contains(ev.target)) closeElementContextMenu(); } function openElementContextMenu(uuid, x, y) { var menu = document.getElementById('element-context-menu'); if (!menu) return; menu.style.left = x + 'px'; menu.style.top = y + 'px'; menu.style.display = 'block'; setTimeout(function() { document.addEventListener('mousedown', onElementContextMenuOutsideClick, true); }, 0); } function bindElementContextMenu() { var menu = document.getElementById('element-context-menu'); if (!menu) return; menu.addEventListener('click', function(e) { var item = e.target.closest('li[data-action]'); if (!item) return; var action = item.getAttribute('data-action'); closeElementContextMenu(); if (action === 'copy') { copySelected(); } else if (action === 'paste') { pasteClipboard(); } else if (action === 'remove') { removeSelected(); } else if (action === 'save-template') { saveSelectedAsTemplate(); } }); DocumentTree.setOnContextMenu(function(uuid, x, y) { Canvas.setSelectedUuid(uuid); Preview.highlight(uuid); PropertiesPanel.showProperties(uuid); openElementContextMenu(uuid, x, y); }); } function saveSelectedAsTemplate() { var uuid = Canvas.getSelectedUuid(); if (!uuid) return; var name = prompt(I18n.t('I18N_TEMPLATE_NAME_PROMPT', 'Name der Vorlage:'), ''); if (!name) return; EditorApi.saveElementTemplate(uuid, name).catch(function(err) { console.error('Failed to save element template:', err); alert(I18n.t('I18N_ACTION_FAILED', 'Aktion fehlgeschlagen') + ': ' + ((err && err.error) || '')); }); } // --- Refresh document tree --- function refreshDocument() { Loading Loading @@ -281,6 +345,10 @@ SurveyManager.open(); }); document.getElementById('btn-tools').addEventListener('click', function() { TemplateManager.open(); }); document.getElementById('btn-publish-now').addEventListener('click', function() { publishFromToolbar(); }); Loading Loading @@ -351,6 +419,11 @@ }); document.getElementById('btn-remove').addEventListener('click', function() { removeSelected(); }); } function removeSelected() { var uuid = Canvas.getSelectedUuid(); if (!uuid) return; if (!confirm(I18n.t('I18N_REMOVE_CONFIRM') + '?')) return; Loading @@ -359,7 +432,6 @@ PropertiesPanel.clear(); refreshDocument(); }); }); } // --- Save dialog --- Loading
editor/html/js/template-manager.js 0 → 100644 +141 −0 Changes for editor/html/js/template-manager.js: 141 added lines, 0 removed lines. Original line number Diff line number Diff line /******************************************************************************* * blogi-webedit Template Manager ("Werkzeuge" > "Vorlagen") * * Lists global element templates (reusable XML subtrees saved from the * document tree's right-click "Als globale Vorlage speichern..." action) and * lets the user insert / rename / delete them. *******************************************************************************/ var TemplateManager = (function() { 'use strict'; var _onChange = null; function el(id) { return document.getElementById(id); } function formatDate(iso) { if (!iso) return ''; return iso.replace('T', ' ').substring(0, 16); } function loadTemplates() { var list = el('tools-template-list'); list.innerHTML = '<p class="hint">' + I18n.t('I18N_LOADING', 'Lade...') + '</p>'; EditorApi.listElementTemplates().then(function(resp) { renderList(resp.templates || []); }).catch(function(err) { console.error('Failed to load element templates:', err); list.innerHTML = '<p class="hint">' + I18n.t('I18N_FAILED_LOAD_TEMPLATES', 'Vorlagen konnten nicht geladen werden') + '</p>'; }); } function renderList(templates) { var list = el('tools-template-list'); list.innerHTML = ''; if (!templates.length) { list.innerHTML = '<p class="hint">' + I18n.t('I18N_NO_TEMPLATES', 'Keine globalen Vorlagen vorhanden.') + '</p>'; return; } templates.forEach(function(tpl) { var row = document.createElement('div'); row.className = 'tools-tpl-row'; var info = document.createElement('div'); info.className = 'tools-tpl-info'; info.innerHTML = '<strong>' + escapeHtml(tpl.name) + '</strong>' + '<span class="tools-tpl-meta">' + escapeHtml(tpl.root_type) + (tpl.modified ? ' · ' + escapeHtml(formatDate(tpl.modified)) : '') + '</span>'; row.appendChild(info); var actions = document.createElement('div'); actions.className = 'tools-tpl-actions'; var btnInsert = document.createElement('button'); btnInsert.className = 'set-save-btn'; btnInsert.textContent = I18n.t('I18N_INSERT_TEMPLATE', 'Einfügen'); btnInsert.addEventListener('click', function() { insertTemplate(tpl.id); }); actions.appendChild(btnInsert); var btnRename = document.createElement('button'); btnRename.className = 'set-save-btn'; btnRename.textContent = I18n.t('I18N_RENAME_TEMPLATE', 'Umbenennen'); btnRename.addEventListener('click', function() { renameTemplate(tpl.id, tpl.name); }); actions.appendChild(btnRename); var btnDelete = document.createElement('button'); btnDelete.className = 'set-save-btn tools-tpl-delete'; btnDelete.textContent = I18n.t('I18N_DELETE_TEMPLATE', 'Löschen'); btnDelete.addEventListener('click', function() { deleteTemplate(tpl.id); }); actions.appendChild(btnDelete); row.appendChild(actions); list.appendChild(row); }); } function escapeHtml(s) { return (s || '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>'); } function insertTemplate(id) { EditorApi.getElementTemplate(id).then(function(tpl) { var parentUuid = Canvas.getSelectedUuid() || ''; var position = ''; if (parentUuid) { position = Canvas.canSelectedHaveChildren() ? 'child' : 'after'; } return EditorApi.pasteElement(tpl.xml, parentUuid, position); }).then(function() { if (_onChange) _onChange(); document.getElementById('tools-dialog').close(); }).catch(function(err) { console.error('Failed to insert element template:', err); alert(I18n.t('I18N_ACTION_FAILED', 'Aktion fehlgeschlagen') + ': ' + ((err && err.error) || '')); }); } function renameTemplate(id, currentName) { var name = prompt(I18n.t('I18N_TEMPLATE_NAME_PROMPT', 'Name der Vorlage:'), currentName || ''); if (!name || name === currentName) return; EditorApi.renameElementTemplate(id, name).then(loadTemplates).catch(function(err) { console.error('Failed to rename element template:', err); alert(I18n.t('I18N_ACTION_FAILED', 'Aktion fehlgeschlagen') + ': ' + ((err && err.error) || '')); }); } function deleteTemplate(id) { if (!confirm(I18n.t('I18N_DELETE_TEMPLATE_CONFIRM', 'Vorlage löschen?'))) return; EditorApi.deleteElementTemplate(id).then(loadTemplates).catch(function(err) { console.error('Failed to delete element template:', err); alert(I18n.t('I18N_ACTION_FAILED', 'Aktion fehlgeschlagen') + ': ' + ((err && err.error) || '')); }); } function open() { loadTemplates(); document.getElementById('tools-dialog').showModal(); } function init(onChange) { _onChange = typeof onChange === 'function' ? onChange : null; var closeBtn = el('btn-tools-close'); if (closeBtn) { closeBtn.addEventListener('click', function() { document.getElementById('tools-dialog').close(); }); } } return { init: init, open: open }; })();