]> BookStack Code Mirror - bookstack/commitdiff
Updated drawio tinymce plugin to use embeds
authorDan Brown <redacted>
Mon, 23 May 2022 11:24:40 +0000 (12:24 +0100)
committerDan Brown <redacted>
Mon, 23 May 2022 11:24:40 +0000 (12:24 +0100)
- Adds support for handling drawings as embeds, based on image
  extension.
- Adds additional attribute to drawio elements within editor to prevent
  tinymce replacing embeds with a placeholder.
- Updates how contenteditable is applied to drawio blocks within editor,
  to use proper filters instead of using the SetContent event.

resources/js/services/drawio.js
resources/js/wysiwyg/plugin-drawio.js
resources/sass/_tinymce.scss

index 141b61c72d4b3e50f9d38e09dc3fce355ba1716a..78fb968cf4be3d8cca41ebfabc209ffd059f4497 100644 (file)
@@ -99,4 +99,18 @@ async function load(drawingId) {
     return resp.data.content;
 }
 
-export default {show, close, upload, load};
\ No newline at end of file
+
+function buildDrawingContentHtml(drawing) {
+    const isSvg = drawing.url.split('.').pop().toLowerCase() === 'svg';
+    const image = `<img src="${drawing.url}">`;
+    const embed = `<embed src="${drawing.url}" type="image/svg+xml">`;
+    return `<div drawio-diagram="${drawing.id}">${isSvg ? embed : image}</div>`
+}
+
+function buildDrawingContentNode(drawing) {
+    const div = document.createElement('div');
+    div.innerHTML = buildDrawingContentHtml(drawing);
+    return div.children[0];
+}
+
+export default {show, close, upload, load, buildDrawingContentHtml, buildDrawingContentNode};
\ No newline at end of file
index c270c5d20f9e889875472c8a9ec6f323bfea4b92..58dff661f93f73ea127d4164fe7cc9259620c686 100644 (file)
@@ -1,4 +1,5 @@
 import DrawIO from "../services/drawio";
+import {build} from "./config";
 
 let pageEditor = null;
 let currentNode = null;
@@ -15,15 +16,14 @@ function isDrawing(node) {
 function showDrawingManager(mceEditor, selectedNode = null) {
     pageEditor = mceEditor;
     currentNode = selectedNode;
+
     // Show image manager
     window.ImageManager.show(function (image) {
         if (selectedNode) {
-            let imgElem = selectedNode.querySelector('img');
-            pageEditor.dom.setAttrib(imgElem, 'src', image.url);
-            pageEditor.dom.setAttrib(selectedNode, 'drawio-diagram', image.id);
+            pageEditor.dom.replace(buildDrawingNode(image), selectedNode);
         } else {
-            let imgHTML = `<div drawio-diagram="${image.id}" contenteditable="false"><img src="${image.url}"></div>`;
-            pageEditor.insertContent(imgHTML);
+            const drawingHtml = DrawIO.buildDrawingContentHtml(image);
+            pageEditor.insertContent(drawingHtml);
         }
     }, 'drawio');
 }
@@ -34,6 +34,13 @@ function showDrawingEditor(mceEditor, selectedNode = null) {
     DrawIO.show(options.drawioUrl, drawingInit, updateContent);
 }
 
+function buildDrawingNode(drawing) {
+    const drawingEl = DrawIO.buildDrawingContentNode(drawing);
+    drawingEl.setAttribute('contenteditable', 'false');
+    drawingEl.setAttribute('data-ephox-embed-iri', 'true');
+    return drawingEl;
+}
+
 async function updateContent(drawingData) {
     const id = "image-" + Math.random().toString(16).slice(2);
     const loadingImage = window.baseUrl('/loading.gif');
@@ -50,11 +57,9 @@ async function updateContent(drawingData) {
     // Handle updating an existing image
     if (currentNode) {
         DrawIO.close();
-        let imgElem = currentNode.querySelector('img');
         try {
             const img = await DrawIO.upload(drawingData, options.pageId);
-            pageEditor.dom.setAttrib(imgElem, 'src', img.url);
-            pageEditor.dom.setAttrib(currentNode, 'drawio-diagram', img.id);
+            pageEditor.dom.replace(buildDrawingNode(img), currentNode);
         } catch (err) {
             handleUploadError(err);
         }
@@ -62,12 +67,11 @@ async function updateContent(drawingData) {
     }
 
     setTimeout(async () => {
-        pageEditor.insertContent(`<div drawio-diagram contenteditable="false"><img src="${loadingImage}" id="${id}"></div>`);
+        pageEditor.insertContent(`<div drawio-diagram contenteditable="false"><img src="${loadingImage}" alt="Loading" id="${id}"></div>`);
         DrawIO.close();
         try {
             const img = await DrawIO.upload(drawingData, options.pageId);
-            pageEditor.dom.setAttrib(id, 'src', img.url);
-            pageEditor.dom.get(id).parentNode.setAttribute('drawio-diagram', img.id);
+            pageEditor.dom.replace(buildDrawingNode(img), pageEditor.dom.get(id).parentNode);
         } catch (err) {
             pageEditor.dom.remove(id);
             handleUploadError(err);
@@ -86,7 +90,6 @@ function drawingInit() {
 }
 
 /**
- *
  * @param {WysiwygConfigOptions} providedOptions
  * @return {function(Editor, string)}
  */
@@ -130,14 +133,28 @@ export function getPlugin(providedOptions) {
             showDrawingEditor(editor, selectedNode);
         });
 
-        editor.on('SetContent', function () {
-            const drawings = editor.$('body > div[drawio-diagram]');
-            if (!drawings.length) return;
+        editor.on('PreInit', () => {
+            editor.parser.addNodeFilter('div', function(nodes) {
+                for (const node of nodes) {
+                    if (node.attr('drawio-diagram')) {
+                        // Set content editable to be false to prevent direct editing of child content.
+                        node.attr('contenteditable', 'false');
+                        // Set this attribute to prevent drawing contents being parsed as media embeds
+                        // to avoid contents being replaced with placeholder images.
+                        // TinyMCE embed plugin sources looks for this attribute in its logic.
+                        node.attr('data-ephox-embed-iri', 'true');
+                    }
+                }
+            });
 
-            editor.undoManager.transact(function () {
-                drawings.each((index, elem) => {
-                    elem.setAttribute('contenteditable', 'false');
-                });
+            editor.serializer.addNodeFilter('div', function(nodes) {
+                for (const node of nodes) {
+                    // Clean up content attributes
+                    if (node.attr('drawio-diagram')) {
+                        node.attr('contenteditable', null);
+                        node.attr('data-ephox-embed-iri', null);
+                    }
+                }
             });
         });
 
index 0ee3fa40b235298e3ceaf0bf64ab4bb6456b050d..aa6b05fa605ea793616b50d1b39b940f6166996f 100644 (file)
@@ -48,6 +48,11 @@ body.page-content.mce-content-body  {
   display: none;
 }
 
+// Prevent interaction with embed contents
+.page-content.mce-content-body embed {
+  pointer-events: none;
+}
+
 // Details/summary editor usability
 .page-content.mce-content-body details summary {
   pointer-events: none;