Commit 412ac978 authored by jan.koester's avatar jan.koester
Browse files

bugfixes editor

parent 1215b153
Loading
Loading
Loading
Loading
+17 −0
Changes for editor/html/css/editor.css: 17 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -225,6 +225,10 @@ body {
    color: var(--text-muted);
}

.tree-node-toggle.has-children {
    cursor: pointer;
}

.tree-node-name {
    flex: 1;
    overflow: hidden;
@@ -236,6 +240,19 @@ body {
    padding-left: 16px;
}

.tree-node.collapsed > .tree-children {
    display: none;
}

.tree-root-header {
    font-weight: 600;
    border-bottom: 1px solid var(--border);
    margin-bottom: 4px;
    padding-bottom: 6px;
    cursor: pointer;
    user-select: none;
}

/* Preview / visual canvas */
#preview-pane {
    flex: 1;
+2 −0
Changes for editor/html/index.html: 2 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -74,6 +74,8 @@
                    <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>
                    <button id="btn-expand-all" data-i18n-title="I18N_TITLE_EXPAND_ALL">&#x229E;</button>
                    <button id="btn-collapse-all" data-i18n-title="I18N_TITLE_COLLAPSE_ALL">&#x229F;</button>
                </div>
                <div id="document-tree"></div>
            </div>
+39 −1
Changes for editor/html/js/editor.js: 39 added lines, 1 removed line.
Original line number Diff line number Diff line
@@ -11,6 +11,7 @@
    var currentDocSeo = {};
    var currentDocGlobalCss = ''; // page-level CSS collected by HTML import, see doc-seo-dialog
    var currentDocPublishParams = {}; // target name -> last-used publish params for this template
    var currentDocLastPublishTarget = ''; // target name last successfully published to, see applyPreferredPublishTarget
    var importHtmlPendingSource = ''; // raw HTML staged in the import dialog, once past step 1
    var importHtmlPendingCandidates = []; // last parse-html result for the block picker
    var saveDialogMode = 'save';
@@ -61,6 +62,10 @@
                Preview.highlight(uuid);
                PropertiesPanel.showProperties(uuid);
            });
            DocumentTree.setOnRootSelect(function() {
                populateDocSeoDialog();
                document.getElementById('doc-seo-dialog').showModal();
            });
            Canvas.setOnChange(refreshDocument);
        });
    }
@@ -256,6 +261,8 @@
            currentDocSeo = resp.seo || {};
            currentDocGlobalCss = resp.global_css || '';
            currentDocPublishParams = resp.publish_params || {};
            currentDocLastPublishTarget = resp.last_publish_target || '';
            applyPreferredPublishTarget();
            Canvas.setTreeData(resp.tree || []);
            DocumentTree.render(resp.tree || []);
            Preview.refresh();
@@ -426,6 +433,14 @@
        document.getElementById('btn-remove').addEventListener('click', function() {
            removeSelected();
        });

        document.getElementById('btn-expand-all').addEventListener('click', function() {
            DocumentTree.expandAll();
        });

        document.getElementById('btn-collapse-all').addEventListener('click', function() {
            DocumentTree.collapseAll();
        });
    }

    function removeSelected() {
@@ -2311,7 +2326,7 @@
        select.disabled = false;
        publishBtn.disabled = false;
        publishBtn.style.opacity = '1';
        renderPreviewPublishFields(targets[0].name);
        applyPreferredPublishTarget();

        // Bind change event (only once)
        if (!select._bound) {
@@ -2322,6 +2337,28 @@
        }
    }

    // Defaults the toolbar publish-target dropdown to whichever target was
    // last successfully published to for this document (currentDocLastPublishTarget,
    // see DocumentState::lastPublishTarget), falling back to the first target
    // otherwise. Called both once targets are known (showPreviewPublishTargets)
    // and once the document's last-used target is known (refreshDocument) --
    // whichever of the two async loads finishes last ends up applying it.
    function applyPreferredPublishTarget() {
        if (!currentPublishTargets.length) return;
        var select = document.getElementById('preview-publish-target');
        var name = currentPublishTargets[0].name;
        if (currentDocLastPublishTarget) {
            for (var i = 0; i < currentPublishTargets.length; i++) {
                if (currentPublishTargets[i].name === currentDocLastPublishTarget) {
                    name = currentDocLastPublishTarget;
                    break;
                }
            }
        }
        select.value = name;
        renderPreviewPublishFields(name);
    }

    // Builds the actual <select>/<textarea>/<input>/image-picker element for one
    // publish-form field descriptor (shared by the toolbar quick-publish bar,
    // the main publish dialog, and the SEO & Social sub-dialog). "image" opens
@@ -2465,6 +2502,7 @@
                    return;
                }
                currentDocPublishParams[target] = params;
                currentDocLastPublishTarget = target;
                publishBtn.innerHTML = '&#x2705; OK';
                setTimeout(function() {
                    publishBtn.innerHTML = origText;
+83 −6
Changes for editor/html/js/tree.js: 83 added lines, 6 removed lines.
Original line number Diff line number Diff line
@@ -12,7 +12,12 @@ var DocumentTree = (function() {
    var selectedUuid = null;
    var onSelectCallback = null;
    var onContextMenuCallback = null;
    var onRootSelectCallback = null;
    var lastTreeData = [];
    // Collapsed state survives across render() calls (e.g. after every
    // document refresh), keyed by widget uuid, so toggling a node doesn't
    // get reset the next time something else in the document changes.
    var collapsedUuids = {};

    function setOnSelect(cb) {
        onSelectCallback = cb;
@@ -22,13 +27,22 @@ var DocumentTree = (function() {
        onContextMenuCallback = cb;
    }

    function setOnRootSelect(cb) {
        onRootSelectCallback = cb;
    }

    function render(treeData) {
        lastTreeData = treeData || [];
        var container = document.getElementById('document-tree');
        container.innerHTML = '';

        container.appendChild(renderRootNode());

        if (!lastTreeData.length) {
            container.innerHTML = '<p class="hint">Dokument ist leer. Widget hinzufügen.</p>';
            var hint = document.createElement('p');
            hint.className = 'hint';
            hint.textContent = I18n.t('I18N_DOCUMENT_EMPTY', 'Dokument ist leer. Widget hinzufügen.');
            container.appendChild(hint);
            return;
        }

@@ -37,9 +51,37 @@ var DocumentTree = (function() {
        }
    }

    // Synthetic entry above the widget tree, representing the document
    // itself rather than any widget -- there's no uuid to select on the
    // canvas, it just opens the document-level SEO/Social + Page CSS dialog.
    function renderRootNode() {
        var header = document.createElement('div');
        header.className = 'tree-node-header tree-root-header';

        var icon = document.createElement('span');
        icon.className = 'tree-node-toggle';
        icon.textContent = '⚙';
        header.appendChild(icon);

        var name = document.createElement('span');
        name.className = 'tree-node-name';
        name.textContent = I18n.t('I18N_DOCUMENT_ROOT', 'Dokument');
        header.appendChild(name);

        header.addEventListener('click', function(e) {
            e.stopPropagation();
            if (onRootSelectCallback) onRootSelectCallback();
        });

        return header;
    }

    function renderNode(node, depth) {
        var hasChildren = !!(node.children && node.children.length > 0);
        var isCollapsed = hasChildren && !!collapsedUuids[node.uuid];

        var div = document.createElement('div');
        div.className = 'tree-node';
        div.className = 'tree-node' + (isCollapsed ? ' collapsed' : '');
        div.setAttribute('data-uuid', node.uuid);
        div.setAttribute('data-can-children', node.can_have_children ? '1' : '0');

@@ -51,8 +93,18 @@ var DocumentTree = (function() {

        var toggle = document.createElement('span');
        toggle.className = 'tree-node-toggle';
        if (node.children && node.children.length > 0) {
            toggle.textContent = '▼';
        if (hasChildren) {
            toggle.textContent = isCollapsed ? '▶' : '▼';
            toggle.classList.add('has-children');
            toggle.addEventListener('click', function(e) {
                e.stopPropagation();
                if (collapsedUuids[node.uuid]) {
                    delete collapsedUuids[node.uuid];
                } else {
                    collapsedUuids[node.uuid] = true;
                }
                render(lastTreeData);
            });
        } else if (node.can_have_children) {
            toggle.textContent = '▷';
        } else {
@@ -79,7 +131,7 @@ var DocumentTree = (function() {

        div.appendChild(header);

        if (node.children && node.children.length > 0) {
        if (hasChildren) {
            var childContainer = document.createElement('div');
            childContainer.className = 'tree-children';
            for (var i = 0; i < node.children.length; i++) {
@@ -120,12 +172,37 @@ var DocumentTree = (function() {
        highlightSelected(null);
    }

    function forEachNodeWithChildren(nodes, fn) {
        for (var i = 0; i < nodes.length; i++) {
            if (nodes[i].children && nodes[i].children.length > 0) {
                fn(nodes[i]);
                forEachNodeWithChildren(nodes[i].children, fn);
            }
        }
    }

    function expandAll() {
        collapsedUuids = {};
        render(lastTreeData);
    }

    function collapseAll() {
        collapsedUuids = {};
        forEachNodeWithChildren(lastTreeData, function(node) {
            collapsedUuids[node.uuid] = true;
        });
        render(lastTreeData);
    }

    return {
        render: render,
        selectNode: selectNode,
        highlightSelected: highlightSelected,
        clearSelection: clearSelection,
        setOnSelect: setOnSelect,
        setOnContextMenu: setOnContextMenu
        setOnContextMenu: setOnContextMenu,
        setOnRootSelect: setOnRootSelect,
        expandAll: expandAll,
        collapseAll: collapseAll
    };
})();
+19 −5
Changes for editor/src/webedit_api.cpp: 19 added lines, 5 removed lines.
Original line number Diff line number Diff line
@@ -508,6 +508,7 @@ void webedit::Api::loadDocumentTreeFromXml(DocumentState &doc, const std::string
    doc.ownedElements.clear();
    doc.seo = blogi::SeoMeta();
    doc.globalCss.clear();
    doc.lastPublishTarget.clear();
    doc.publishParams.clear();

    tinyxml2::XMLDocument xmlDoc;
@@ -523,6 +524,9 @@ void webedit::Api::loadDocumentTreeFromXml(DocumentState &doc, const std::string
    if (const char *globalCss = rootEl->Attribute("GlobalCss")) {
        doc.globalCss = globalCss;
    }
    if (const char *lastTarget = rootEl->Attribute("LastPublishTarget")) {
        doc.lastPublishTarget = lastTarget;
    }

    if (auto *seoEl = rootEl->FirstChildElement("SeoMeta")) {
        auto attr = [&](const char *name) { const char *v = seoEl->Attribute(name); return v ? v : ""; };
@@ -1050,6 +1054,7 @@ void webedit::Api::handleNewDocument(libhttppp::HttpRequest &curreq,
    doc.currentDocGroup.clear();
    doc.seo = blogi::SeoMeta();
    doc.globalCss.clear();
    doc.lastPublishTarget.clear();
    doc.publishParams.clear();
    persistDocumentStateToSession(sessionid, doc);

@@ -1072,6 +1077,7 @@ void webedit::Api::handleGetDocument(libhttppp::HttpRequest &curreq,
    json_object_object_add(resp, "id", json_object_new_string(doc.currentDocId.c_str()));
    json_object_object_add(resp, "seo", seoMetaToJson(doc.seo));
    json_object_object_add(resp, "global_css", json_object_new_string(doc.globalCss.c_str()));
    json_object_object_add(resp, "last_publish_target", json_object_new_string(doc.lastPublishTarget.c_str()));
    json_object_object_add(resp, "publish_params", publishParamsMapToJson(doc.publishParams));

    json_object *tree = json_object_new_array();
@@ -4892,14 +4898,19 @@ webedit::Api::PublishCoreResult webedit::Api::publishToConnectionCore(

        if (pubStatus == "success" || pubStatus == "ok" || pubStatus == "duplicate") {
            result.ok = true;
            // Remember these params on the template itself (not just this publish) so
            // the next publish to the same target starts pre-filled. Only once the blog
            // actually accepted the content — "duplicate" is still an unresolved
            // overwrite prompt at this point, not a completed publish.
            if (paramsObj && (pubStatus == "success" || pubStatus == "ok")) {
            // Remember these params (and which target was used) on the template
            // itself (not just this publish) so the next publish to the same
            // target starts pre-filled and the target dropdown defaults to it.
            // Only once the blog actually accepted the content — "duplicate" is
            // still an unresolved overwrite prompt at this point, not a
            // completed publish.
            if (pubStatus == "success" || pubStatus == "ok") {
                doc.lastPublishTarget = target;
                if (paramsObj) {
                    doc.publishParams[target] =
                        json_object_to_json_string_ext(paramsObj, JSON_C_TO_STRING_NOSLASHESCAPE);
                }
            }
        } else {
            // Auth might have expired
            if (pubStatus == "error") {
@@ -5701,6 +5712,9 @@ std::string webedit::Api::exportTreeXml(DocumentState &doc) {
    if (!doc.globalCss.empty()) {
        root->SetAttribute("GlobalCss", doc.globalCss.c_str());
    }
    if (!doc.lastPublishTarget.empty()) {
        root->SetAttribute("LastPublishTarget", doc.lastPublishTarget.c_str());
    }

    const blogi::SeoMeta &seo = doc.seo;
    if (!seo.ogTitle.empty() || !seo.ogDescription.empty() || !seo.ogImage.empty() ||
Loading