Commit 83687815 authored by jan.koester's avatar jan.koester
Browse files

drag and drop editor

parent 49c5bc06
Loading
Loading
Loading
Loading
+52 −133
Changes for editor/html/css/editor.css: 52 added lines, 133 removed lines.
Original line number Diff line number Diff line
@@ -94,83 +94,86 @@ body {
    font-size: 14px;
}

/* Main App Layout */
/* Main App Layout — the preview/canvas is the sole child now that the sidebar
   tree and docked properties panel have been replaced by the visual canvas
   (click/drag directly in the preview) and the floating properties panel. */
#app {
    display: flex;
    flex: 1;
    overflow: hidden;
}

/* Sidebars */
#sidebar-left, #sidebar-right {
    width: 280px;
    min-width: 200px;
    background: var(--bg-secondary);
    border-right: 1px solid var(--border);
/* Preview / visual canvas */
#preview-pane {
    flex: 1;
    display: flex;
    flex-direction: column;
    overflow-y: auto;
}

#sidebar-right {
    border-right: none;
    border-left: 1px solid var(--border);
}

#sidebar-left h3, #sidebar-right h3 {
    font-size: 12px;
    text-transform: uppercase;
    letter-spacing: 1px;
    color: var(--text-muted);
    padding: 12px 12px 8px;
    border-bottom: 1px solid var(--border);
}

/* Widget Palette */
#widget-palette {
    flex-shrink: 0;
    background: var(--bg-primary);
    min-width: 0;
}

#widget-list {
#preview-toolbar {
    padding: 8px;
    display: flex;
    flex-wrap: wrap;
    gap: 4px;
    border-bottom: 1px solid var(--border);
    background: var(--bg-secondary);
}

.widget-btn {
    padding: 6px 10px;
#preview-toolbar button {
    padding: 6px 12px;
    background: var(--bg-tertiary);
    color: var(--text-primary);
    border: 1px solid var(--border);
    border-radius: 4px;
    cursor: pointer;
    font-size: 12px;
    transition: all 0.15s;
    font-size: 13px;
}

.widget-btn:hover {
    background: var(--accent);
    color: var(--bg-primary);
#preview-toolbar button:hover {
    background: var(--bg-hover);
    border-color: var(--accent);
}

/* Document Tree */
#document-tree-panel {
#preview-frame {
    flex: 1;
    border: none;
    overflow: auto;
}

/* Floating panels — the contextual properties panel and the widget-type
   insertion popover both anchor near whatever the user just clicked on the
   canvas, instead of living in a permanently-docked sidebar. */
.floating-panel {
    position: fixed;
    z-index: 1000;
    width: 320px;
    max-height: min(70vh, 560px);
    overflow-y: auto;
    background: var(--bg-secondary);
    border: 1px solid var(--border);
    border-radius: 6px;
    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35);
    display: flex;
    flex-direction: column;
    overflow: hidden;
}

#tree-actions {
.floating-panel h3 {
    font-size: 12px;
    text-transform: uppercase;
    letter-spacing: 1px;
    color: var(--text-muted);
    padding: 12px 12px 8px;
    border-bottom: 1px solid var(--border);
    margin: 0;
}

.floating-panel-actions {
    display: flex;
    gap: 4px;
    padding: 8px;
    border-bottom: 1px solid var(--border);
}

#tree-actions button {
.floating-panel-actions button {
    flex: 1;
    padding: 4px;
    background: var(--bg-tertiary);
@@ -181,107 +184,23 @@ body {
    font-size: 14px;
}

#tree-actions button:hover {
.floating-panel-actions button:hover {
    background: var(--bg-hover);
    border-color: var(--accent);
}

#document-tree {
#properties-content {
    flex: 1;
    overflow-y: auto;
    padding: 4px;
}

.tree-node {
    cursor: pointer;
    user-select: none;
}

.tree-node-header {
    display: flex;
    align-items: center;
    gap: 4px;
    padding: 4px 8px;
    border-radius: 3px;
    font-size: 13px;
    transition: background 0.1s;
}

.tree-node-header:hover {
    background: var(--bg-hover);
}

.tree-node-header.selected {
    background: var(--accent);
    color: var(--bg-primary);
}

.tree-node-toggle {
    width: 16px;
    font-size: 10px;
    text-align: center;
    color: var(--text-muted);
}

.tree-node-name {
    flex: 1;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.tree-children {
    padding-left: 16px;
}

/* Preview */
#preview-pane {
    flex: 1;
    display: flex;
    flex-direction: column;
    background: var(--bg-primary);
    min-width: 0;
}

#preview-toolbar {
    padding: 8px;
    border-bottom: 1px solid var(--border);
    background: var(--bg-secondary);
}

#preview-toolbar button {
    padding: 6px 12px;
    background: var(--bg-tertiary);
    color: var(--text-primary);
    border: 1px solid var(--border);
    border-radius: 4px;
    cursor: pointer;
    font-size: 13px;
}

#preview-toolbar button:hover {
    background: var(--bg-hover);
    border-color: var(--accent);
}

#preview-frame {
    flex: 1;
    border: none;
    overflow: auto;
}

/* Properties Panel */
#properties-panel {
    flex: 1;
    display: flex;
    flex-direction: column;
    overflow: hidden;
#widget-type-list {
    padding: 4px;
}

#properties-content {
    flex: 1;
    overflow-y: auto;
    padding: 8px;
#widget-type-list .conn-item {
    cursor: pointer;
}

.hint {
+21 −27
Changes for editor/html/index.html: 21 added lines, 27 removed lines.
Original line number Diff line number Diff line
@@ -36,6 +36,10 @@
            <button id="btn-open" data-i18n-title="I18N_TEMPLATES">&#x1F4C2; <span data-i18n="I18N_TEMPLATES">Öffnen</span></button>
            <button id="btn-template-wizard" title="CSV Vorlagen-Wizard">&#x1F9EA; <span>CSV-Wizard</span></button>
        </div>
        <div class="toolbar-group">
            <button id="btn-copy-el" data-i18n-title="I18N_COPY_ELEMENT">&#x2398; <span data-i18n="I18N_COPY_ELEMENT">Kopieren</span></button>
            <button id="btn-paste-el" data-i18n-title="I18N_PASTE_ELEMENT">&#x1F4CB; <span data-i18n="I18N_PASTE_ELEMENT">Einf&uuml;gen</span></button>
        </div>
        <div class="toolbar-group">
            <button id="btn-import-xml" data-i18n-title="I18N_TITLE_IMPORT_XML">&#x2B07; <span data-i18n="I18N_IMPORT_XML">Import XML</span></button>
            <button id="btn-export-xml" data-i18n-title="I18N_TITLE_EXPORT_XML">&#x2B06; <span data-i18n="I18N_EXPORT_XML">Export XML</span></button>
@@ -57,28 +61,7 @@
    </header>

    <div id="app">
        <!-- Left Sidebar: Widget Palette + Document Tree -->
        <aside id="sidebar-left">
            <div id="widget-palette">
                <h3 data-i18n="I18N_TOOLBOX">Widgets</h3>
                <div id="widget-list"></div>
            </div>
            <div id="document-tree-panel">
                <h3 data-i18n="I18N_CONTENT_INDEX">Dokumentbaum</h3>
                <div id="tree-actions">
                    <button id="btn-move-up" data-i18n-title="I18N_MOVE_UP">&#x2191;</button>
                    <button id="btn-move-down" data-i18n-title="I18N_MOVE_DOWN">&#x2193;</button>
                    <button id="btn-indent" data-i18n-title="I18N_INDENT">&#x2192;</button>
                    <button id="btn-outdent" data-i18n-title="I18N_OUTDENT">&#x2190;</button>
                    <button id="btn-copy-el" data-i18n-title="I18N_COPY_ELEMENT">&#x2398;</button>
                    <button id="btn-paste-el" data-i18n-title="I18N_PASTE_ELEMENT">&#x1F4CB;</button>
                    <button id="btn-remove" data-i18n-title="I18N_REMOVE_ELEMENT">&#x2716;</button>
                </div>
                <div id="document-tree"></div>
            </div>
        </aside>

        <!-- Center: Preview -->
        <!-- Center: Preview / visual canvas (the primary editing surface) -->
        <main id="preview-pane">
            <div id="preview-toolbar">
                <button id="btn-refresh-preview" data-i18n-title="I18N_TITLE_REFRESH_TREE">&#x1F504; <span data-i18n="I18N_VIEW">Vorschau</span></button>
@@ -95,16 +78,27 @@
            <div id="preview-publish-fields" style="display:none;padding:0 0.5em 0.4em;background:var(--bg-secondary);border-bottom:1px solid var(--border);font-size:0.8rem;"></div>
            <div id="preview-frame"></div>
        </main>
    </div>

        <!-- Right Sidebar: Properties -->
        <aside id="sidebar-right">
            <div id="properties-panel">
    <!-- Floating contextual properties panel: shown near whatever element is
         selected on the canvas, replacing the old docked sidebar. -->
    <div id="floating-properties-panel" class="floating-panel" style="display:none">
        <div id="floating-panel-actions" class="floating-panel-actions">
            <button id="btn-move-up" data-i18n-title="I18N_MOVE_UP">&#x2191;</button>
            <button id="btn-move-down" data-i18n-title="I18N_MOVE_DOWN">&#x2193;</button>
            <button id="btn-indent" data-i18n-title="I18N_INDENT">&#x2192;</button>
            <button id="btn-outdent" data-i18n-title="I18N_OUTDENT">&#x2190;</button>
            <button id="btn-remove" data-i18n-title="I18N_REMOVE_ELEMENT">&#x2716;</button>
        </div>
        <h3 data-i18n="I18N_WIDGET_PROPERTIES">Eigenschaften</h3>
        <div id="properties-content">
            <p class="hint" data-i18n="I18N_SELECT_WIDGET">Element auswählen um Eigenschaften zu bearbeiten.</p>
        </div>
    </div>
        </aside>

    <!-- Widget-type popover: opened by the canvas's hover "+" insertion points. -->
    <div id="widget-type-popover" class="floating-panel" style="display:none">
        <div id="widget-type-list"></div>
    </div>

    <!-- Dialogs -->
@@ -484,7 +478,7 @@

    <script src="js/i18n.js"></script>
    <script src="js/api.js"></script>
    <script src="js/tree.js"></script>
    <script src="js/canvas.js"></script>
    <script src="js/properties.js"></script>
    <script src="js/preview.js"></script>
    <script src="js/media-browser.js"></script>
+11 −0
Changes for editor/html/js/api.js: 11 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -151,6 +151,17 @@ var EditorApi = (function() {
            return request('POST', '/api/element/outdent', { uuid: uuid });
        },

        // Arbitrary-position move for the visual canvas's drag-and-drop reordering.
        // position is 'before' | 'after' | 'child' relative to targetUuid (empty
        // targetUuid moves the element to the top level).
        moveElementTo: function(uuid, targetUuid, position) {
            return request('POST', '/api/element/move', {
                uuid: uuid,
                target_uuid: targetUuid || '',
                position: position || 'after'
            });
        },

        copyElement: function(uuid) {
            return request('POST', '/api/element/copy', { uuid: uuid });
        },
+178 −0
Changes for editor/html/js/canvas.js: 178 added lines, 0 removed lines.
Original line number Diff line number Diff line
/*******************************************************************************
 * blogi-webedit Visual Canvas
 *
 * Replaces the old sidebar document tree (tree.js): selection, reordering and
 * new-element insertion now happen by interacting with the live preview iframe
 * directly. The iframe is cross-origin (see preview.js), so all of this is
 * driven by postMessage events sent from the injected canvas script built in
 * webedit_api.cpp's handlePreviewPage/buildCanvasScript.
 *******************************************************************************/

var Canvas = (function() {
    'use strict';

    var selectedUuid = null;
    var onSelectCallback = null;
    var onChangeCallback = null;
    var treeData = [];

    function getSelectedUuid() {
        return selectedUuid;
    }

    function clearSelection() {
        selectedUuid = null;
    }

    function setOnSelect(cb) {
        onSelectCallback = cb;
    }

    // Called after a mutation driven by the canvas (reorder, insert) so the
    // caller (editor.js) can refresh the document/preview — mirrors the old
    // DocumentTree.render() being the hub every mutation routed through.
    function setOnChange(cb) {
        onChangeCallback = cb;
    }

    // Replaces DocumentTree.render(treeData): there's no sidebar tree to draw
    // anymore, the tree data is only cached for canSelectedHaveChildren lookups
    // used when picking add/paste insertion targets.
    function setTreeData(data) {
        treeData = data || [];
    }

    function findNode(nodes, uuid) {
        for (var i = 0; i < nodes.length; i++) {
            if (nodes[i].uuid === uuid) return nodes[i];
            if (nodes[i].children) {
                var found = findNode(nodes[i].children, uuid);
                if (found) return found;
            }
        }
        return null;
    }

    function canSelectedHaveChildren() {
        if (!selectedUuid) return false;
        var node = findNode(treeData, selectedUuid);
        return !!(node && node.can_have_children);
    }

    // The injected canvas script reports element rects relative to the iframe's
    // own viewport. Translate to parent-page coordinates by adding the iframe
    // element's own offset — that's always readable (it's the iframe element,
    // not its cross-origin content).
    function translateRect(rect) {
        if (!rect) return null;
        var iframe = document.getElementById('preview-iframe');
        var off = iframe ? iframe.getBoundingClientRect() : { top: 0, left: 0 };
        return {
            top: rect.top + off.top,
            left: rect.left + off.left,
            width: rect.width,
            height: rect.height
        };
    }

    function handleMessage(ev) {
        var d = ev.data;
        if (!d || typeof d.type !== 'string' || d.type.indexOf('blogi-') !== 0) return;

        if (d.type === 'blogi-select') {
            selectedUuid = d.uuid;
            if (onSelectCallback) onSelectCallback(d.uuid, translateRect(d.rect));
        } else if (d.type === 'blogi-text-edit') {
            var props = {};
            props[d.key] = d.value;
            // The canvas already shows the edited text — no refresh needed here,
            // that would just re-fetch and redraw what's already correct.
            EditorApi.setProperties(d.uuid, props).catch(function(err) {
                console.error('Inline text edit failed to save:', err);
            });
        } else if (d.type === 'blogi-reorder') {
            EditorApi.moveElementTo(d.uuid, d.targetUuid, d.position).then(function() {
                if (onChangeCallback) onChangeCallback();
            }).catch(function(err) {
                console.error('Reorder failed:', err);
                alert((typeof I18n !== 'undefined' ? I18n.t('I18N_ACTION_FAILED', 'Aktion fehlgeschlagen') : 'Aktion fehlgeschlagen') +
                      ': ' + ((err && err.error) || ''));
            });
        } else if (d.type === 'blogi-insert-request') {
            openInsertPopover(d.targetUuid, d.position, translateRect(d.rect));
        }
    }

    function bindMessageListener() {
        window.addEventListener('message', handleMessage);
    }

    // --- Widget-type insertion popover (replaces the old sidebar palette) ---

    function closePopover() {
        var popover = document.getElementById('widget-type-popover');
        if (popover) popover.style.display = 'none';
        document.removeEventListener('mousedown', onOutsideClick, true);
    }

    function onOutsideClick(ev) {
        var popover = document.getElementById('widget-type-popover');
        if (popover && !popover.contains(ev.target)) closePopover();
    }

    function openInsertPopover(targetUuid, position, rect) {
        var popover = document.getElementById('widget-type-popover');
        var list = document.getElementById('widget-type-list');
        if (!popover || !list) return;

        list.innerHTML = '<p class="hint">Lade...</p>';
        popover.style.display = 'block';
        var left = rect ? rect.left : 40;
        var top = rect ? rect.top : 40;
        popover.style.left = Math.max(8, Math.min(left, window.innerWidth - 220)) + 'px';
        popover.style.top = Math.max(8, Math.min(top, window.innerHeight - 100)) + 'px';

        EditorApi.getPlugins().then(function(resp) {
            list.innerHTML = '';
            var pluginsList = resp.plugins || [];
            if (!pluginsList.length) {
                list.innerHTML = '<p class="hint">Keine Widgets verfügbar.</p>';
                return;
            }
            pluginsList.forEach(function(p) {
                var item = document.createElement('div');
                item.className = 'conn-item';
                item.textContent = p.name;
                item.addEventListener('click', function() {
                    closePopover();
                    EditorApi.addElement(p.name, targetUuid, position).then(function() {
                        if (onChangeCallback) onChangeCallback();
                    }).catch(function(err) {
                        console.error('Failed to add widget:', err);
                        alert((typeof I18n !== 'undefined' ? I18n.t('I18N_ACTION_FAILED', 'Aktion fehlgeschlagen') : 'Aktion fehlgeschlagen') +
                              ': ' + ((err && err.error) || ''));
                    });
                });
                list.appendChild(item);
            });
        }).catch(function() {
            list.innerHTML = '<p class="hint">Fehler beim Laden der Widgets.</p>';
        });

        // Defer binding so the click that opened the popover doesn't immediately
        // close it via the capture-phase outside-click listener.
        setTimeout(function() {
            document.addEventListener('mousedown', onOutsideClick, true);
        }, 0);
    }

    return {
        getSelectedUuid: getSelectedUuid,
        clearSelection: clearSelection,
        canSelectedHaveChildren: canSelectedHaveChildren,
        setOnSelect: setOnSelect,
        setOnChange: setOnChange,
        setTreeData: setTreeData,
        bindMessageListener: bindMessageListener
    };
})();
+27 −71

File changed.

Preview size limit exceeded, changes collapsed.

Loading