Commit bce5afd3 authored by Jan Köster's avatar Jan Köster
Browse files

el templates

parent 1cf9fe79
Loading
Loading
Loading
Loading
+63 −0
Changes for editor/html/css/editor.css: 63 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -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;
+35 −0
Changes for editor/html/index.html: 35 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -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">&#x1F6E0; <span data-i18n="I18N_TOOLS">Werkzeuge</span></button>
            <button id="btn-settings" data-i18n-title="Settings">&#x2699; <span data-i18n="Settings">Einstellungen</span></button>
            <button id="btn-logout" data-i18n-title="Logout">&#x1F6AA; <span data-i18n="Logout">Abmelden</span></button>
        </div>
@@ -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&uuml;gen</li>
            <li data-action="save-template" data-i18n="I18N_SAVE_AS_TEMPLATE">Als globale Vorlage speichern&hellip;</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
@@ -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>
@@ -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>
+21 −0
Changes for editor/html/js/api.js: 21 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -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 });
+73 −1
Changes for editor/html/js/editor.js: 73 added lines, 1 removed line.
Original line number Diff line number Diff line
@@ -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); }
@@ -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() {
@@ -281,6 +345,10 @@
            SurveyManager.open();
        });

        document.getElementById('btn-tools').addEventListener('click', function() {
            TemplateManager.open();
        });

        document.getElementById('btn-publish-now').addEventListener('click', function() {
            publishFromToolbar();
        });
@@ -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;
@@ -359,7 +432,6 @@
            PropertiesPanel.clear();
            refreshDocument();
        });
        });
    }

    // --- Save dialog ---
+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 ? ' &middot; ' + 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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
    }

    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