Skip to content

Commit 8398e49

Browse files
rpeyronnopeslide
andauthored
Convert from&to native mermaid objects, options and copy/download (#16)
* Allow global and local configuration, and from & to native mermaid conversion * Copy and download functions * Removed blob: dependancy to allow use with default drawio-desktop * Update drawio_desktop/src/mermaid-plugin.js Co-authored-by: nopeslide <[email protected]> * Added deepmerge and deep-object-diff packages * Replaced merge & diff * Clean local dev stuff * Ensure beginUpdate Co-authored-by: nopeslide <[email protected]>
1 parent 2a32a8f commit 8398e49

File tree

3 files changed

+554
-272
lines changed

3 files changed

+554
-272
lines changed

drawio_desktop/package.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,14 @@
1414
"dev": "npm run webpack:development"
1515
},
1616
"devDependencies": {
17-
"mermaid": "^8.8.1",
17+
"mermaid": "^8.9.1",
1818
"raw-loader": "^4.0.2",
1919
"webpack": "^5.1.3",
2020
"webpack-cli": "^4.0.0"
21+
},
22+
"dependencies": {
23+
"deep-object-diff": "^1.1.0",
24+
"deepmerge": "^4.2.2",
25+
"is-object": "^1.0.2"
2126
}
2227
}

drawio_desktop/src/mermaid-plugin.js

Lines changed: 255 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
1-
import "./shapes/shapeMermaid";
1+
import { mermaid_plugin_defaults, mxShapeMermaid } from "./shapes/shapeMermaid";
22
import "./palettes/mermaid/paletteMermaid";
33
import mermaid from 'mermaid'
44

5+
import merge from 'deepmerge'
6+
import { diff, addedDiff, deletedDiff, updatedDiff, detailedDiff } from 'deep-object-diff'
7+
import isObject from 'is-object'
8+
59
/**
610
* Constructs a new parse dialog.
711
*/
@@ -14,6 +18,9 @@ var DialogMermaid = function (editorUi, shape) {
1418
var graph = editorUi.editor.graph;
1519
graph.getModel().beginUpdate();
1620
graph.labelChanged(shape.state.cell,text);
21+
// To replace valueChanged in mxShapeMermaid.prototype.paintVertexShape
22+
shape.updateImage();
23+
shape.redraw();
1724
graph.getModel().endUpdate();
1825
editorUi.spinner.stop();
1926

@@ -35,8 +42,16 @@ var DialogMermaid = function (editorUi, shape) {
3542
<div style="flex: 0 0 4em; display: flex; flex-direction: row; align-items: end">
3643
<pre id="plugin_mermaid_parserstatus" style="flex: 1; text-align: left; overflow-x: auto"></pre>
3744
<div id="plugin_mermaid_buttons" style="flex: initial; text-align: right; align-self: flex-end;">
38-
<p style="margin-block: unset;">
39-
<a target="_blank" href="https://mermaid-js.github.io/mermaid/#/./n00b-syntaxReference">[ Syntax ]</a>
45+
<p style="margin-block: unset; font-size: 90%">
46+
<br />Download as |
47+
<a id="plugin_mermaid_button_dl_svg" href="#">SVG</a> |
48+
<a id="plugin_mermaid_button_dl_png" href="#">PNG</a> |
49+
<br />Copy as |
50+
<span style="display: none;"><a id="plugin_mermaid_button_html" href="#">HTML</a> | </span>
51+
<span style="display: none;"><a id="plugin_mermaid_button_svg" href="#">SVG</a> | </span>
52+
<a id="plugin_mermaid_button_png" href="#">PNG</a> |
53+
<br />Help |
54+
<a target="_blank" href="https://mermaid-js.github.io/mermaid/#/./n00b-syntaxReference">Syntax</a> |
4055
</p><br /></div>
4156
</div>
4257
<div style="flex: 0 0 32px;"></div>
@@ -119,6 +134,81 @@ var DialogMermaid = function (editorUi, shape) {
119134
textarea.addEventListener('input', handleInput, false);
120135
}
121136

137+
// Handle copy
138+
function generateCanvas(callback, background=null) {
139+
var svg = div.querySelector('#graph-div');
140+
141+
// https://stackoverflow.com/questions/60551658/saving-offscreencanvas-content-to-disk-as-png-in-electron
142+
// https://stackoverflow.com/questions/32230894/convert-very-large-svg-to-png-using-canvas
143+
//var svg_xml = (new XMLSerializer()).serializeToString(svg);
144+
//var blob = new Blob([svg_xml], {type:'image/svg+xml;charset=utf-8'});
145+
//var url = window.URL.createObjectURL(blob);
146+
var url = "data:image/svg+xml;base64," + btoa(unescape(encodeURIComponent(div.querySelector('#graph-div').outerHTML)));
147+
148+
var scale = 3;
149+
var img = new Image();
150+
img.width = svg.getBBox().width * scale ;
151+
img.height = svg.getBBox().height * scale ;
152+
img.onload = () => {
153+
var canvas = document.createElement('canvas');
154+
var context = canvas.getContext('2d');
155+
canvas.width = svg.getBBox().width * scale;
156+
canvas.height = svg.getBBox().height * scale;
157+
158+
// Add a white background to cope with the transparent image problem getting black on windows...
159+
if (background) {
160+
context.fillStyle = background;
161+
context.fillRect(0, 0, canvas.width, canvas.height);
162+
}
163+
164+
context.drawImage(img, svg.getBBox().x * scale, svg.getBBox().y * scale, svg.getBBox().width * scale, svg.getBBox().height * scale);
165+
window.URL.revokeObjectURL(url);
166+
167+
callback(canvas);
168+
}
169+
img.src = url;
170+
}
171+
172+
div.querySelector('#plugin_mermaid_button_dl_svg').onclick = async function() {
173+
var aDownloadLink = document.createElement('a');
174+
aDownloadLink.download = 'image.svg';
175+
aDownloadLink.href = "data:image/svg+xml;base64," + btoa(unescape(encodeURIComponent(div.querySelector('#graph-div').outerHTML)));
176+
aDownloadLink.click();
177+
}
178+
179+
div.querySelector('#plugin_mermaid_button_dl_png').onclick = async function() {
180+
generateCanvas(function(canvas) {
181+
var aDownloadLink = document.createElement('a');
182+
aDownloadLink.download = 'image.png';
183+
aDownloadLink.href = canvas.toDataURL();
184+
aDownloadLink.click();
185+
});
186+
}
187+
188+
div.querySelector('#plugin_mermaid_button_png').onclick = async function() {
189+
generateCanvas(function(canvas) {
190+
canvas.toBlob(function(imgBlob) {
191+
navigator.clipboard.write( [ new ClipboardItem({[imgBlob.type]: imgBlob }) ] );
192+
}, 'image/png');
193+
}, 'white');
194+
}
195+
196+
// (hidden) Buggy - Oddly makes the whole electron stop working...
197+
div.querySelector('#plugin_mermaid_button_svg').onclick = async function() {
198+
var svg_xml = (new XMLSerializer()).serializeToString(div.querySelector('#graph-div'));
199+
var svg_blob = new Blob([svg_xml], {type : 'image/svg+xml;charset=utf-8'});
200+
var clip_item = new ClipboardItem( {'image/svg+xml': svg_blob } );
201+
navigator.clipboard.write( [ clip_item ] );
202+
}
203+
204+
// (hidden) Tested, but not very usefull as not much destination applications support it... (Libreoffice Writer, with poor SVG render)
205+
div.querySelector('#plugin_mermaid_button_html').onclick = async function() {
206+
navigator.clipboard.write( [ new ClipboardItem(
207+
{ 'text/html' : new Blob(["<img src='" + "data:image/svg+xml;base64," +
208+
btoa(unescape(encodeURIComponent(div.querySelector('#graph-div').outerHTML))) + "'>"], {type : 'text/html'}) }) ]
209+
);
210+
}
211+
122212
var cancelBtn = mxUtils.button(mxResources.get('close'), function () {
123213
win.destroy();
124214
});
@@ -147,23 +237,177 @@ var DialogMermaid = function (editorUi, shape) {
147237
};
148238

149239
Draw.loadPlugin(function (ui) {
240+
241+
// Build mermaid settings : by least order
242+
// - mermaid_plugin_defaults : this plugin defaults
243+
// - EditorUi.defaultMermaidConfig : drawio defaults mermaid
244+
// - Editor.config.defaultMermaidConfig : drawio config (from PreConfig and local configuration)
245+
246+
let mermaid_settings = {};
247+
mermaid_settings = merge(mermaid_settings, mermaid_plugin_defaults);
248+
try {
249+
mermaid_settings = merge(mermaid_settings, window.EditorUi.defaultMermaidConfig);
250+
} catch (e) {
251+
if (!e instanceof TypeError) {
252+
throw e;
253+
}
254+
}
255+
try {
256+
mermaid_settings = merge(mermaid_settings, window.Editor.config.defaultMermaidConfig);
257+
} catch (e) {
258+
if (!e instanceof TypeError) {
259+
throw e;
260+
}
261+
}
262+
263+
// Result is updated back in EditorUi.defaultMermaidConfig to have consistent settings with native mermaid
264+
// Note that the result will not be consistent if the diagram is updated in native mermaid without the plugin,
265+
// but no solution would be perfect until native mermaid allow some configuration...
266+
// As mermaid version are not the same between native mermaid and the plugin one, render may be different.
267+
window.EditorUi.defaultMermaidConfig = mermaid_settings;
268+
269+
// Handle defaults
270+
Object.assign(mermaid_plugin_defaults, mermaid_settings);
271+
mxShapeMermaid.prototype.customProperties = mxShapeMermaid.prototype.buildCustomProperties(mermaid_settings);
272+
150273
// Adds custom sidebar entry
151274
ui.sidebar.addMermaidPalette();
152275

153-
ui.editor.graph.addListener(mxEvent.DOUBLE_CLICK, function (sender, evt) {
154-
var cell = evt.getProperty("cell");
276+
function isCellPluginMermaid(cell) {
155277
if (!cell) {
156-
return;
278+
return false;
157279
}
158280
if (cell.style.indexOf("shape=mxgraph.mermaid.abstract.mermaid") < 0) {
159-
return;
281+
return false;
282+
}
283+
return true;
284+
}
285+
286+
function isCellNativeMermaid(cell) {
287+
if (!cell) { return false; }
288+
if (mxUtils.isNode(cell.value)) {
289+
if (cell.getAttribute('mermaidData', '') != '') {
290+
return true;
291+
}
160292
}
293+
return false;
294+
}
161295

162-
var shape = ui.editor.graph.view.states["map"][cell.mxObjectId].shape;
296+
ui.editor.graph.addListener(mxEvent.DOUBLE_CLICK, function (sender, evt) {
297+
var cell = evt.getProperty("cell");
298+
if (isCellPluginMermaid(cell)) {
299+
var shape = ui.editor.graph.view.states["map"][cell.mxObjectId].shape;
163300

164-
if (shape) {
165-
var dlg = new DialogMermaid(ui,shape);
301+
if (shape) {
302+
var dlg = new DialogMermaid(ui,shape);
303+
}
304+
evt.consume();
166305
}
167-
evt.consume();
168306
});
307+
308+
// Add convert menus
309+
mxResources.parse('mermaidconvertfrom=Convert to Mermaid plugin shape...');
310+
mxResources.parse('mermaidconvertto=Convert to native Mermaid shape...');
311+
312+
var uiCreatePopupMenu = ui.menus.createPopupMenu;
313+
ui.menus.createPopupMenu = function(menu, cell, evt)
314+
{
315+
uiCreatePopupMenu.apply(this, arguments);
316+
317+
var graph = ui.editor.graph;
318+
var cell = graph.getSelectionCell();
319+
320+
if (isCellPluginMermaid(cell)) {
321+
this.addMenuItems(menu, ['-', 'mermaidconvertto'], null, evt);
322+
}
323+
324+
if (isCellNativeMermaid(cell)) {
325+
this.addMenuItems(menu, ['-', 'mermaidconvertfrom'], null, evt);
326+
}
327+
328+
};
329+
330+
ui.actions.addAction('mermaidconvertto', function()
331+
{
332+
let graph =ui.editor.graph ;
333+
let cell = graph.getSelectionCell();
334+
if (!isCellPluginMermaid(cell)) return;
335+
336+
graph.getModel().beginUpdate();
337+
try
338+
{
339+
let state = graph.view.getState(cell, true);
340+
let mermaidData = JSON.stringify({data: graph.convertValueToString(cell), config: state.shape.getRenderOptions() /*getStyleOptions()*/}, null, 2)
341+
state.shape.redraw();
342+
let image = state.shape.image.replace(";base64",""); // ;base64 breaks the style
343+
graph.setCellStyle('shape=image;noLabel=1;verticalAlign=top;imageAspect=1;' + 'image=' + image + ';', [cell]);
344+
graph.setAttributeForCell(cell, 'mermaidData', mermaidData );
345+
346+
graph.view.getState(cell, true).destroy();
347+
graph.view.getState(cell, true);
348+
}
349+
finally
350+
{
351+
graph.getModel().endUpdate();
352+
}
353+
354+
});
355+
356+
357+
ui.actions.addAction('mermaidconvertfrom', function()
358+
{
359+
let graph = ui.editor.graph;
360+
let cell = graph.getSelectionCell();
361+
if (!isCellNativeMermaid(cell)) return;
362+
363+
try {
364+
365+
graph.getModel().beginUpdate();
366+
367+
var data = JSON.parse(cell.getAttribute('mermaidData', ''));
368+
369+
// Default style from paletteMermaid
370+
let style = 'shadow=0;dashed=0;align=left;strokeWidth=1;shape=mxgraph.mermaid.abstract.mermaid;labelBackgroundColor=#ffffff;noLabel=1;';
371+
372+
function addToStyle(basestyle, value) {
373+
if (isObject(value)) {
374+
for(let key in value) {
375+
addToStyle( (basestyle == '') ? key : basestyle + "_" + key , value[key] );
376+
}
377+
} else {
378+
style += encodeURI(basestyle) + "=" + encodeURI(value) + ";";
379+
}
380+
}
381+
382+
let configDiff = diff(mermaid_plugin_defaults, data.config);
383+
addToStyle('', configDiff);
384+
385+
// cell.value = data.data;
386+
graph.setAttributeForCell(cell, 'mermaidData', "" );
387+
graph.labelChanged(cell,data.data);
388+
389+
graph.setCellStyle(style, [cell]);
390+
391+
graph.view.getState(cell, true).destroy();
392+
graph.view.getState(cell, true);
393+
394+
}
395+
catch (error)
396+
{
397+
console.error(error);
398+
}
399+
finally
400+
{
401+
graph.getModel().endUpdate();
402+
}
403+
404+
});
405+
406+
407+
408+
409+
169410
});
411+
412+
413+

0 commit comments

Comments
 (0)