Commit 7bad2152 authored by jan.koester's avatar jan.koester
Browse files

copy paste editor

parent ee19277d
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
@@ -47,6 +47,8 @@
                    <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>
+12 −0
Original line number Diff line number Diff line
@@ -125,6 +125,18 @@ var EditorApi = (function() {
            return request('POST', '/api/element/outdent', { uuid: uuid });
        },

        copyElement: function(uuid) {
            return request('POST', '/api/element/copy', { uuid: uuid });
        },

        pasteElement: function(xml, parentUuid, position) {
            return request('POST', '/api/element/paste', {
                xml: xml,
                parent_uuid: parentUuid || '',
                position: position || 'after'
            });
        },

        getProperties: function(uuid) {
            return request('GET', '/api/element/' + uuid + '/properties');
        },
+70 −0
Original line number Diff line number Diff line
@@ -22,6 +22,7 @@
            try { bindDialogs(); } catch(e) { console.error('bindDialogs:', e); }
            try { bindSettingsDialog(); } catch(e) { console.error('bindSettingsDialog:', e); }
            try { bindAiDialog(); } catch(e) { console.error('bindAiDialog:', e); }
            try { bindClipboardShortcuts(); } catch(e) { console.error('bindClipboardShortcuts:', e); }
            Preview.clear();

            DocumentTree.setOnSelect(function(uuid) {
@@ -89,6 +90,67 @@
        });
    }

    // --- Copy / paste elements ---

    var _clipboardXml = '';

    function bindClipboardShortcuts() {
        document.addEventListener('keydown', function(e) {
            // Ignore when typing in an input, textarea or contenteditable field.
            var t = e.target;
            if (t && (t.tagName === 'INPUT' || t.tagName === 'TEXTAREA' ||
                      t.isContentEditable)) {
                return;
            }
            if (!(e.ctrlKey || e.metaKey)) return;
            var key = (e.key || '').toLowerCase();
            if (key === 'c') {
                if (DocumentTree.getSelectedUuid()) {
                    e.preventDefault();
                    copySelected();
                }
            } else if (key === 'v') {
                e.preventDefault();
                pasteClipboard();
            }
        });
    }

    function copySelected() {
        var uuid = DocumentTree.getSelectedUuid();
        if (!uuid) return;
        EditorApi.copyElement(uuid).then(function(resp) {
            _clipboardXml = resp.xml || '';
            try { localStorage.setItem('webedit-clipboard', _clipboardXml); } catch(e) {}
        }).catch(function(err) {
            console.error('Failed to copy element:', err);
            alert(I18n.t('I18N_ACTION_FAILED', 'Aktion fehlgeschlagen') + ': ' +
                  (err.error || I18n.t('I18N_COPY_FAILED', 'Kopieren fehlgeschlagen')));
        });
    }

    function pasteClipboard() {
        var xml = _clipboardXml;
        if (!xml) {
            try { xml = localStorage.getItem('webedit-clipboard') || ''; } catch(e) {}
        }
        if (!xml) return;

        var parentUuid = DocumentTree.getSelectedUuid() || '';
        var position = '';
        if (parentUuid) {
            position = DocumentTree.canSelectedHaveChildren() ? 'child' : 'after';
        }

        EditorApi.pasteElement(xml, parentUuid, position).then(function() {
            refreshDocument();
        }).catch(function(err) {
            console.error('Failed to paste element:', err);
            alert(I18n.t('I18N_ACTION_FAILED', 'Aktion fehlgeschlagen') + ': ' +
                  (err.error || I18n.t('I18N_PASTE_FAILED', 'Einfügen fehlgeschlagen')));
        });
    }

    // --- Refresh document tree ---

    function refreshDocument() {
@@ -232,6 +294,14 @@
            EditorApi.outdent(uuid).then(refreshDocument);
        });

        document.getElementById('btn-copy-el').addEventListener('click', function() {
            copySelected();
        });

        document.getElementById('btn-paste-el').addEventListener('click', function() {
            pasteClipboard();
        });

        document.getElementById('btn-remove').addEventListener('click', function() {
            var uuid = DocumentTree.getSelectedUuid();
            if (!uuid) return;
+182 −0
Original line number Diff line number Diff line
@@ -113,6 +113,18 @@ bool webedit::Api::handleRequest(libhttppp::HttpRequest &curreq, const int tid,
        return true;
    }

    // Route: /api/element/copy — serialize a subtree to XML for the clipboard
    if (path == "/api/element/copy") {
        handleCopyElement(curreq, sessionid);
        return true;
    }

    // Route: /api/element/paste — insert clipboard XML relative to a target
    if (path == "/api/element/paste") {
        handlePasteElement(curreq, sessionid);
        return true;
    }

    // Route: /api/element/{uuid}/properties
    if (path.find("/api/element/") == 0 && path.ends_with("/properties")) {
        std::string uuid = path.substr(13); // after "/api/element/"
@@ -970,6 +982,156 @@ void webedit::Api::handleImportXml(libhttppp::HttpRequest &curreq,
    json_object_put(req);
}

// --- Copy element (serialize subtree to clipboard XML) ---

void webedit::Api::handleCopyElement(libhttppp::HttpRequest &curreq,
                                      const std::string &sessionid) {
    std::string body = getRequestBody(curreq);
    json_object *req = json_tokener_parse(body.c_str());
    if (!req) {
        sendJsonError(curreq, 400, "Invalid JSON");
        return;
    }

    json_object *uuidObj = nullptr;
    json_object_object_get_ex(req, "uuid", &uuidObj);
    const char *uuid = uuidObj ? json_object_get_string(uuidObj) : nullptr;

    if (!uuid) {
        json_object_put(req);
        sendJsonError(curreq, 400, "Missing uuid");
        return;
    }

    auto &doc = getDocState(sessionid);
    std::lock_guard<std::mutex> lk(doc.mtx);

    auto *node = findElement(doc.root, uuid);
    if (!node) {
        json_object_put(req);
        sendJsonError(curreq, 404, "Element not found");
        return;
    }

    std::string xml = serializeElementXml(node);

    json_object *resp = json_object_new_object();
    json_object_object_add(resp, "status", json_object_new_string("ok"));
    json_object_object_add(resp, "xml", json_object_new_string(xml.c_str()));
    sendJson(curreq, resp);
    json_object_put(resp);
    json_object_put(req);
}

// --- Paste element (insert clipboard XML relative to a target) ---

void webedit::Api::handlePasteElement(libhttppp::HttpRequest &curreq,
                                       const std::string &sessionid) {
    std::string body = getRequestBody(curreq);
    json_object *req = json_tokener_parse(body.c_str());
    if (!req) {
        sendJsonError(curreq, 400, "Invalid JSON");
        return;
    }

    json_object *xmlObj = nullptr, *parentObj = nullptr, *posObj = nullptr;
    json_object_object_get_ex(req, "xml", &xmlObj);
    json_object_object_get_ex(req, "parent_uuid", &parentObj);
    json_object_object_get_ex(req, "position", &posObj);

    const char *xml        = xmlObj ? json_object_get_string(xmlObj) : nullptr;
    const char *parentUuid = parentObj ? json_object_get_string(parentObj) : nullptr;
    const char *position   = posObj ? json_object_get_string(posObj) : "after";

    if (!xml || strlen(xml) == 0) {
        json_object_put(req);
        sendJsonError(curreq, 400, "Missing xml");
        return;
    }

    tinyxml2::XMLDocument xmlDoc;
    if (xmlDoc.Parse(xml) != tinyxml2::XML_SUCCESS) {
        json_object_put(req);
        sendJsonError(curreq, 400, "Invalid XML");
        return;
    }

    auto *rootEl = xmlDoc.FirstChildElement("WebEditor");
    if (!rootEl) rootEl = xmlDoc.FirstChildElement();
    if (!rootEl) {
        json_object_put(req);
        sendJsonError(curreq, 400, "Empty XML document");
        return;
    }

    auto &doc = getDocState(sessionid);
    std::lock_guard<std::mutex> lk(doc.mtx);

    // Build the chain of new elements from the clipboard XML.
    // New instances get fresh instance IDs (fromXmlObject does not read them),
    // so pasting never collides with existing elements.
    blogi::webedit::EditPlugin *first = nullptr;
    blogi::webedit::EditPlugin *last = nullptr;
    for (auto *child = rootEl->FirstChildElement(); child;
         child = child->NextSiblingElement()) {
        if (strcmp(child->Name(), "property") == 0) continue;
        const char *typeName = child->Name();
        auto *el = createPluginByName(typeName, doc);
        if (!el) continue;

        el->fromXmlObject(child);
        loadChildrenFromXml(child, el, doc);

        if (!first) first = el;
        if (last) last->setNextEl(el);
        last = el;
    }

    if (!first) {
        json_object_put(req);
        sendJsonError(curreq, 400, "Nothing to paste");
        return;
    }

    // Determine insertion point.
    blogi::webedit::EditPlugin *target =
        (parentUuid && strlen(parentUuid) > 0) ? findElement(doc.root, parentUuid) : nullptr;

    if (!target) {
        // No selection: append to the top-level chain.
        if (!doc.root) {
            doc.root = first;
        } else {
            auto *tail = doc.root;
            while (tail->nextElement())
                tail = const_cast<blogi::webedit::EditPlugin*>(tail->nextElement());
            tail->setNextEl(first);
        }
    } else if (strcmp(position, "child") == 0 && target->canHaveChildren()) {
        // Insert as last child of target.
        if (!target->getChildElement()) {
            target->setChildElement(*first);
        } else {
            auto *tail = const_cast<blogi::webedit::EditPlugin*>(target->getChildElement());
            while (tail->nextElement())
                tail = const_cast<blogi::webedit::EditPlugin*>(tail->nextElement());
            tail->setNextEl(first);
        }
    } else {
        // Insert as sibling directly after target, preserving the chain.
        auto *oldNext = const_cast<blogi::webedit::EditPlugin*>(target->nextElement());
        target->setNextEl(first);
        last->setNextEl(oldNext);
    }

    json_object *resp = json_object_new_object();
    json_object_object_add(resp, "status", json_object_new_string("ok"));
    json_object_object_add(resp, "uuid", json_object_new_string(first->getInstanceId().c_str()));
    sendJson(curreq, resp);
    json_object_put(resp);
    json_object_put(req);
}

// --- Save document to DB ---

void webedit::Api::handleSaveDocument(libhttppp::HttpRequest &curreq,
@@ -2394,6 +2556,26 @@ std::string webedit::Api::exportTreeXml(DocumentState &doc) {
    return printer.CStr();
}

std::string webedit::Api::serializeElementXml(blogi::webedit::EditPlugin *node) {
    tinyxml2::XMLDocument xmlDoc;
    auto *root = xmlDoc.NewElement("WebEditor");
    root->SetAttribute("Version", "1.0");
    root->SetAttribute("editor", "blogi-webedit");
    xmlDoc.InsertEndChild(root);

    if (node) {
        // toXmlObject serializes the node and its children, but NOT its
        // following siblings — exactly the subtree we want for copy.
        auto *xmlEl = node->toXmlObject(&xmlDoc);
        if (xmlEl) root->InsertEndChild(xmlEl);
    }

    tinyxml2::XMLPrinter printer;
    xmlDoc.Print(&printer);
    return printer.CStr();
}


blogi::webedit::EditPlugin *webedit::Api::findElement(
        blogi::webedit::EditPlugin *root, const std::string &uuid) {
    auto *el = root;
+3 −0
Original line number Diff line number Diff line
@@ -75,6 +75,8 @@ namespace webedit {
                               const std::string &direction);
        void handleIndentElement(libhttppp::HttpRequest &curreq, const std::string &sessionid);
        void handleOutdentElement(libhttppp::HttpRequest &curreq, const std::string &sessionid);
        void handleCopyElement(libhttppp::HttpRequest &curreq, const std::string &sessionid);
        void handlePasteElement(libhttppp::HttpRequest &curreq, const std::string &sessionid);
        void handleGetProperties(libhttppp::HttpRequest &curreq, const std::string &sessionid,
                                 const std::string &uuid);
        void handleSetProperties(libhttppp::HttpRequest &curreq, const std::string &sessionid,
@@ -148,6 +150,7 @@ namespace webedit {
        json_object *serializeTree(const blogi::webedit::EditPlugin *node);
        std::string renderTree(const blogi::webedit::EditPlugin *node);
        std::string exportTreeXml(DocumentState &doc);
        std::string serializeElementXml(blogi::webedit::EditPlugin *node);

        blogi::webedit::EditPlugin *findElement(blogi::webedit::EditPlugin *root,
                                                 const std::string &uuid);