1window.PLUGINS_BASE_PATH = '.'; 2window.TEMPLATE_PATH = 'templates'; 3window.DRAW_MATH_URL = 'math'; 4window.DRAWIO_BASE_URL = '.'; //Prevent access to online website since it is not allowed 5FeedbackDialog.feedbackUrl = 'https://log.draw.io/email'; 6 7//Disables eval for JS (uses shapes-14-6-5.min.js) 8mxStencilRegistry.allowEval = false; 9 10(function() 11{ 12 // Overrides default mode 13 App.mode = App.MODE_DEVICE; 14 15 // Disables preview option in embed dialog 16 EmbedDialog.showPreviewOption = false; 17 18 // Disables new window option in edit diagram dialog 19 EditDiagramDialog.showNewWindowOption = false; 20 21 PrintDialog.previewEnabled = false; 22 23 PrintDialog.electronPrint = function(editorUi, allPages, pagesFrom, pagesTo, 24 fit, sheetsAcross, sheetsDown, zoom, pageScale, pageFormat) 25 { 26 var xml = '', title = ''; 27 var file = editorUi.getCurrentFile(); 28 29 if (file) 30 { 31 file.updateFileData(); 32 xml = file.getData(); 33 title = file.title; 34 } 35 36 new mxElectronRequest('export', { 37 print: true, 38 format: 'pdf', 39 xml: xml, 40 from: pagesFrom - 1, 41 to: pagesTo - 1, 42 allPages: allPages, 43 pageWidth: pageFormat.width, 44 pageHeight: pageFormat.height, 45 pageScale: pageScale, 46 fit: fit, 47 sheetsAcross: sheetsAcross, 48 sheetsDown: sheetsDown, 49 scale: zoom, 50 fileTitle: title 51 }).send(function(){}, function(){}); 52 }; 53 54 var oldWindowOpen = window.open; 55 window.open = function(url) 56 { 57 if (url != null && url.startsWith('http')) 58 { 59 const {shell} = require('electron'); 60 shell.openExternal(url); 61 } 62 else 63 { 64 return oldWindowOpen(url); 65 } 66 } 67 68 var origAppMain = App.main; 69 70 App.main = function() 71 { 72 //TODO Move all file system operations to this worker to offload the renderer thread 73 //TODO Use async version of any sync function used here especially if it is in critical path. For example, open dialog sync block the UI until dialog is shown 74 App.filesWorker = new Worker('electronFilesWorker.js'); 75 76 App.filesWorkerReqId = 1; 77 App.filesWorkerReqInfo = {}; 78 79 App.filesWorkerReq = function(msg, callback, error) 80 { 81 msg.reqId = App.filesWorkerReqId++; 82 App.filesWorkerReqInfo[msg.reqId] = {callback: callback, error: error}; 83 App.filesWorker.postMessage(msg); 84 }; 85 86 App.filesWorker.onmessage = function(e) 87 { 88 var resp = e.data; 89 var callbacks = App.filesWorkerReqInfo[resp.reqId]; 90 91 if (resp.error) 92 { 93 callbacks.error(resp.msg, resp.e); 94 } 95 else 96 { 97 callbacks.callback(resp.data); 98 } 99 100 delete App.filesWorkerReqInfo[resp.reqId]; 101 }; 102 103 //Load desktop plugins 104 var plugins = (mxSettings.settings != null) ? mxSettings.getPlugins() : null; 105 App.initPluginCallback(); 106 107 if (plugins != null && plugins.length > 0) 108 { 109 for (var i = 0; i < plugins.length; i++) 110 { 111 try 112 { 113 if (plugins[i].startsWith('/plugins/')) 114 { 115 plugins[i] = '.' + plugins[i]; 116 } 117 else if (plugins[i].startsWith('plugins/')) 118 { 119 plugins[i] = './' + plugins[i]; 120 } 121 //Support old plugins added using file:// workaround 122 else if (!plugins[i].startsWith('file://')) 123 { 124 var fs = require('fs'); 125 var sysPath = require('path'); 126 var pluginsFile = sysPath.join(getAppDataFolder(), '/plugins', plugins[i]); 127 128 if (fs.existsSync(pluginsFile)) 129 { 130 plugins[i] = 'file://' + pluginsFile; 131 } 132 else 133 { 134 continue; //skip not found files 135 } 136 } 137 138 mxscript(plugins[i]); 139 } 140 catch (e) 141 { 142 // ignore 143 } 144 } 145 } 146 147 //Disable web plugins loading 148 urlParams['plugins'] = '0'; 149 origAppMain.apply(this, arguments); 150 }; 151 152 var menusInit = Menus.prototype.init; 153 Menus.prototype.init = function() 154 { 155 menusInit.apply(this, arguments); 156 157 var editorUi = this.editorUi; 158 159 editorUi.actions.put('useOffline', new Action(mxResources.get('useOffline') + '...', function() 160 { 161 editorUi.openLink('https://www.draw.io/') 162 })); 163 164 this.put('openRecent', new Menu(function(menu, parent) 165 { 166 var recent = editorUi.getRecent(); 167 168 if (recent != null) 169 { 170 for (var i = 0; i < recent.length; i++) 171 { 172 (function(entry) 173 { 174 menu.addItem(entry.title, null, function() 175 { 176 function doOpenRecent() 177 { 178 //Simulate opening a file via args 179 editorUi.loadArgs({args: [entry.id]}); 180 }; 181 182 var file = editorUi.getCurrentFile(); 183 184 if (file != null && file.isModified()) 185 { 186 editorUi.confirm(mxResources.get('allChangesLost'), null, doOpenRecent, 187 mxResources.get('cancel'), mxResources.get('discardChanges')); 188 } 189 else 190 { 191 doOpenRecent(); 192 } 193 }, parent); 194 })(recent[i]); 195 } 196 197 menu.addSeparator(parent); 198 } 199 200 menu.addItem(mxResources.get('reset'), null, function() 201 { 202 editorUi.resetRecent(); 203 }, parent); 204 })); 205 206 // Replaces file menu to replace openFrom menu with open and rename downloadAs to export 207 this.put('file', new Menu(mxUtils.bind(this, function(menu, parent) 208 { 209 this.addMenuItems(menu, ['new', 'open'], parent); 210 this.addSubmenu('openRecent', menu, parent); 211 this.addMenuItems(menu, ['-', 'synchronize', '-', 'save', 'saveAs', '-', 'import'], parent); 212 this.addSubmenu('exportAs', menu, parent); 213 menu.addSeparator(parent); 214 this.addSubmenu('embed', menu, parent); 215 menu.addSeparator(parent); 216 this.addMenuItems(menu, ['newLibrary', 'openLibrary'], parent); 217 218 var file = editorUi.getCurrentFile(); 219 220 if (file != null && editorUi.fileNode != null) 221 { 222 var filename = (file.getTitle() != null) ? 223 file.getTitle() : editorUi.defaultFilename; 224 225 if (!/(\.html)$/i.test(filename) && 226 !/(\.svg)$/i.test(filename)) 227 { 228 this.addMenuItems(menu, ['-', 'properties']); 229 } 230 } 231 232 this.addMenuItems(menu, ['-', 'pageSetup', 'print', '-', 'close'], parent); 233 // LATER: Find API for application.quit 234 }))); 235 }; 236 237 function getDocumentsFolder() 238 { 239 //On windows, misconfigured Documents folder cause an exception 240 try 241 { 242 return require('@electron/remote').app.getPath('documents'); 243 } 244 catch(e) {} 245 246 return '.'; 247 }; 248 249 function getAppDataFolder() 250 { 251 try 252 { 253 var fs = require('fs'); 254 var appDataDir = require('@electron/remote').app.getPath('appData'); 255 var drawioDir = appDataDir + '/draw.io'; 256 257 if (!fs.existsSync(drawioDir)) //Usually this dir already exists 258 { 259 fs.mkdirSync(drawioDir); 260 } 261 262 return drawioDir; 263 } 264 catch(e) {} 265 266 return '.'; 267 }; 268 269 var graphCreateLinkForHint = Graph.prototype.createLinkForHint; 270 271 Graph.prototype.createLinkForHint = function(href, label) 272 { 273 var a = graphCreateLinkForHint.call(this, href, label); 274 275 if (href != null && !this.isCustomLink(href)) 276 { 277 // KNOWN: Event with gesture handler mouseUp the middle click opens a framed window 278 mxEvent.addListener(a, 'click', mxUtils.bind(this, function(evt) 279 { 280 this.openLink(a.getAttribute('href'), a.getAttribute('target')); 281 mxEvent.consume(evt); 282 })); 283 } 284 285 return a; 286 }; 287 288 Graph.prototype.openLink = function(url, target) 289 { 290 require('electron').shell.openExternal(url); 291 }; 292 293 // Initializes the user interface 294 var editorUiInit = EditorUi.prototype.init; 295 EditorUi.prototype.init = function() 296 { 297 editorUiInit.apply(this, arguments); 298 299 var editorUi = this; 300 var graph = this.editor.graph; 301 302 global.__emt_isModified = 303 e => { 304 if (editorUi.getCurrentFile()) 305 { 306 return editorUi.getCurrentFile().isModified() 307 } 308 309 return false 310 } 311 312 // global.__emt_getCurrentFile = e => { 313 // return this.getCurrentFile() 314 // } 315 316 // Adds support for libraries 317 this.actions.addAction('newLibrary...', mxUtils.bind(this, function() 318 { 319 editorUi.showLibraryDialog(null, null, null, null, App.MODE_DEVICE); 320 })); 321 322 this.actions.addAction('openLibrary...', mxUtils.bind(this, function() 323 { 324 editorUi.pickLibrary(App.MODE_DEVICE); 325 })); 326 327 // Replaces import action 328 this.actions.addAction('import...', mxUtils.bind(this, function() 329 { 330 if (editorUi.getCurrentFile() != null) 331 { 332 var remote = require('@electron/remote'); 333 var dialog = remote.dialog; 334 const sysPath = require('path') 335 var lastDir = localStorage.getItem('.lastImpDir'); 336 337 var paths = dialog.showOpenDialogSync({ 338 defaultPath: lastDir || getDocumentsFolder(), 339 properties: ['openFile'] 340 }); 341 342 if (paths !== undefined && paths[0] != null) 343 { 344 var path = paths[0]; 345 localStorage.setItem('.lastImpDir', sysPath.dirname(path)); 346 var asImage = /\.png$/i.test(path) || /\.gif$/i.test(path) || /\.jpe?g$/i.test(path); 347 var encoding = (asImage || /\.pdf$/i.test(path) || /\.vsdx$/i.test(path) || /\.vssx$/i.test(path)) ? 348 'base64' : 'utf-8'; 349 350 if (editorUi.spinner.spin(document.body, mxResources.get('loading'))) 351 { 352 var fs = require('fs'); 353 354 fs.readFile(path, encoding, mxUtils.bind(this, function (e, data) 355 { 356 if (e) 357 { 358 editorUi.spinner.stop(); 359 editorUi.handleError(e); 360 } 361 else 362 { 363 try 364 { 365 if (editorUi.isLucidChartData(data)) 366 { 367 editorUi.convertLucidChart(data, function(xml) 368 { 369 editorUi.spinner.stop(); 370 graph.setSelectionCells(editorUi.importXml(xml)); 371 }, function(e) 372 { 373 editorUi.spinner.stop(); 374 editorUi.handleError(e); 375 }); 376 } 377 else if (/(\.vsdx)($|\?)/i.test(path)) 378 { 379 editorUi.importVisio(editorUi.base64ToBlob(data, 'application/octet-stream'), function(xml) 380 { 381 editorUi.spinner.stop(); 382 graph.setSelectionCells(editorUi.importXml(xml)); 383 }); 384 } 385 else if (!editorUi.isOffline() && new XMLHttpRequest().upload && editorUi.isRemoteFileFormat(data, path)) 386 { 387 // Asynchronous parsing via server 388 editorUi.parseFile(new Blob([data], {type : 'application/octet-stream'}), mxUtils.bind(this, function(xhr) 389 { 390 if (xhr.readyState == 4) 391 { 392 editorUi.spinner.stop(); 393 394 if (xhr.status >= 200 && xhr.status <= 299) 395 { 396 graph.setSelectionCells(editorUi.importXml(xhr.responseText)); 397 } 398 } 399 }), path); 400 } 401 else 402 { 403 if (/\.pdf$/i.test(path)) 404 { 405 var tmp = Editor.extractGraphModelFromPdf(data); 406 407 if (tmp != null) 408 { 409 data = tmp; 410 } 411 } 412 else if (/\.png$/i.test(path)) 413 { 414 var tmp = editorUi.extractGraphModelFromPng(data); 415 416 if (tmp != null) 417 { 418 asImage = false; 419 data = tmp; 420 } 421 } 422 else if (/\.svg$/i.test(path)) 423 { 424 // LATER: Use importXml without throwing exception if no data 425 // Checks if SVG contains content attribute 426 var root = mxUtils.parseXml(data); 427 var svgs = root.getElementsByTagName('svg'); 428 429 if (svgs.length > 0) 430 { 431 var svgRoot = svgs[0]; 432 var cont = svgRoot.getAttribute('content'); 433 434 if (cont != null && cont.charAt(0) != '<' && cont.charAt(0) != '%') 435 { 436 cont = unescape((window.atob) ? atob(cont) : Base64.decode(cont, true)); 437 } 438 439 if (cont != null && cont.charAt(0) == '%') 440 { 441 cont = decodeURIComponent(cont); 442 } 443 444 if (cont != null && (cont.substring(0, 8) === '<mxfile ' || 445 cont.substring(0, 14) === '<mxGraphModel ')) 446 { 447 asImage = false; 448 data = cont; 449 } 450 else 451 { 452 asImage = true; 453 } 454 } 455 } 456 457 if (asImage) 458 { 459 var img = new Image(); 460 img.onload = function() 461 { 462 editorUi.resizeImage(img, img.src, function(data2, w, h) 463 { 464 editorUi.spinner.stop(); 465 var pt = graph.getInsertPoint(); 466 graph.setSelectionCell(graph.insertVertex(null, null, '', pt.x, pt.y, w, h, 467 'shape=image;aspect=fixed;image=' + editorUi.convertDataUri(data2) + ';')); 468 }, true); 469 }; 470 471 img.onerror = function(e) 472 { 473 editorUi.spinner.stop(); 474 editorUi.handleError(); 475 }; 476 477 var format = path.substring(path.lastIndexOf('.') + 1); 478 img.src = (format == 'svg') ? Editor.createSvgDataUri(data) : 479 'data:image/' + format + ';base64,' + data; 480 } 481 else 482 { 483 editorUi.spinner.stop(); 484 485 if (data != null) 486 { 487 graph.setSelectionCells(editorUi.importXml(data)); 488 } 489 } 490 } 491 } 492 catch(e) 493 { 494 editorUi.spinner.stop(); 495 editorUi.handleError(e); 496 } 497 } 498 })); 499 } 500 } 501 } 502 })); 503 504 // Replaces new action 505 var oldNew = this.actions.get('new').funct; 506 507 this.actions.addAction('new...', mxUtils.bind(this, function() 508 { 509 if (this.getCurrentFile() == null) 510 { 511 oldNew(); 512 } 513 else 514 { 515 const ipc = require('electron').ipcRenderer 516 ipc.sendSync('winman', {action: 'newfile', opt: {width: 1600}}) 517 } 518 }), null, null, Editor.ctrlKey + '+N'); 519 520 this.actions.get('open').shortcut = Editor.ctrlKey + '+O'; 521 522 // Adds shortcut keys for file operations 523 editorUi.keyHandler.bindAction(78, true, 'new'); // Ctrl+N 524 editorUi.keyHandler.bindAction(79, true, 'open'); // Ctrl+O 525 526 function createGraph() 527 { 528 var graph = new Graph(); 529 graph.setExtendParents(false); 530 graph.setExtendParentsOnAdd(false); 531 graph.setConstrainChildren(false); 532 graph.setHtmlLabels(true); 533 graph.getModel().maintainEdgeParent = false; 534 return graph; 535 }; 536 537 function cloneMxCLipboardToSys() 538 { 539 var cells = mxClipboard.getCells(); 540 541 if (cells && cells.length > 0) 542 { 543 try 544 { 545 var tmpGraph = createGraph(); 546 tmpGraph.importCells(cells, 0, 0, tmpGraph.getDefaultParent()); 547 var remote = require('@electron/remote'); 548 var clipboard = remote.clipboard; 549 var codec = new mxCodec(); 550 var node = codec.encode(tmpGraph.getModel()); 551 var modelString = mxUtils.getXml(node); 552 clipboard.writeText(encodeURIComponent(modelString)); 553 } 554 catch(e) 555 { 556 //Ignore 557 } 558 } 559 }; 560 561 function cloneSysCLipboardToMx() 562 { 563 try 564 { 565 var remote = require('@electron/remote'); 566 var clipboard = remote.clipboard; 567 var modelString = clipboard.readText(); 568 569 if (modelString) 570 { 571 modelString = decodeURIComponent(modelString); 572 var xmlDoc = mxUtils.parseXml(modelString); 573 var tmpGraph = createGraph(); 574 var codec = new mxCodec(xmlDoc); 575 var model = tmpGraph.getModel(); 576 codec.decode(xmlDoc.documentElement, model); 577 mxClipboard.setCells(model.root.children[0].children); 578 } 579 } 580 catch(e) 581 { 582 //Ignore, the contents of mxClipboard will be used 583 } 584 }; 585 586 //Set system clipboard on menu copy/cut 587 var origCut = this.actions.get('cut').funct; 588 589 editorUi.actions.addAction('cut', function() 590 { 591 origCut(); 592 cloneMxCLipboardToSys(); 593 }, null, 'sprite-cut', Editor.ctrlKey + '+X'); 594 595 var origCopy = this.actions.get('copy').funct; 596 597 editorUi.actions.addAction('copy', function() 598 { 599 origCopy(); 600 cloneMxCLipboardToSys(); 601 }, null, 'sprite-copy', Editor.ctrlKey + '+C'); 602 603 //Get data from system clipboard for pase/pasteHere 604 var origPaste = this.actions.get('paste').funct; 605 606 editorUi.actions.addAction('paste', function() 607 { 608 cloneSysCLipboardToMx(); 609 origPaste(); 610 }, false, 'sprite-paste', Editor.ctrlKey + '+V'); 611 612 var origPasteHere = this.actions.get('pasteHere').funct; 613 614 editorUi.actions.addAction('pasteHere', function() 615 { 616 cloneSysCLipboardToMx(); 617 origPasteHere(); 618 }); 619 620 //Enable paste action even if mxClipboard is empty! TODO Is this OK? 621 editorUi.updatePasteActionStates = function() 622 { 623 var graph = this.editor.graph; 624 var paste = this.actions.get('paste'); 625 var pasteHere = this.actions.get('pasteHere'); 626 627 paste.setEnabled(this.editor.graph.cellEditor.isContentEditing() || 628 (graph.isEnabled() && !graph.isCellLocked(graph.getDefaultParent()))); 629 pasteHere.setEnabled(paste.isEnabled()); 630 }; 631 632 editorUi.actions.addAction('plugins...', function() 633 { 634 editorUi.showDialog(new PluginsDialog(editorUi, function(callback) 635 { 636 var div = document.createElement('div'); 637 638 var title = document.createElement('span'); 639 title.style.marginTop = '6px'; 640 mxUtils.write(title, mxResources.get('builtinPlugins') + ': '); 641 div.appendChild(title); 642 643 var pluginsSelect = document.createElement('select'); 644 pluginsSelect.style.width = '150px'; 645 646 for (var i = 0; i < App.publicPlugin.length; i++) 647 { 648 var option = document.createElement('option'); 649 mxUtils.write(option, App.publicPlugin[i]); 650 option.value = App.publicPlugin[i]; 651 pluginsSelect.appendChild(option); 652 } 653 654 div.appendChild(pluginsSelect); 655 mxUtils.br(div); 656 mxUtils.br(div); 657 658 title = document.createElement('span'); 659 mxUtils.write(title, mxResources.get('extPlugins') + ': '); 660 div.appendChild(title); 661 662 var extPluginsBtn = mxUtils.button(mxResources.get('selectFile') + '...', function() 663 { 664 var remote = require('@electron/remote'); 665 var dialog = remote.dialog; 666 const sysPath = require('path'); 667 var lastDir = localStorage.getItem('.lastPluginDir'); 668 669 var paths = dialog.showOpenDialogSync({ 670 defaultPath: lastDir || getDocumentsFolder(), 671 filters: [ 672 { name: 'draw.io Plugins', extensions: ['js'] }, 673 { name: 'All Files', extensions: ['*'] } 674 ], 675 properties: ['openFile'] 676 }); 677 678 if (paths !== undefined && paths[0] != null) 679 { 680 localStorage.setItem('.lastPluginDir', sysPath.dirname(paths[0])); 681 var fs = require('fs'); 682 var pluginsDir = sysPath.join(getAppDataFolder(), '/plugins'); 683 684 if (!fs.existsSync(pluginsDir)) 685 { 686 fs.mkdirSync(pluginsDir); 687 } 688 689 var pluginName = sysPath.basename(paths[0]); 690 var dstFile = sysPath.join(pluginsDir, pluginName); 691 692 if (fs.existsSync(dstFile)) 693 { 694 alert(mxResources.get('fileExists')); 695 } 696 else 697 { 698 fs.copyFile(paths[0], dstFile, (err) => 699 { 700 if (err) 701 { 702 alert('Adding plugin failed.'); 703 } 704 else 705 { 706 callback(pluginName); 707 editorUi.hideDialog(); 708 } 709 }); 710 } 711 } 712 }); 713 714 extPluginsBtn.className = 'geBtn'; 715 div.appendChild(extPluginsBtn); 716 717 var dlg = new CustomDialog(editorUi, div, mxUtils.bind(this, function() 718 { 719 callback(App.pluginRegistry[pluginsSelect.value]); 720 })); 721 editorUi.showDialog(dlg.container, 300, 120, true, true); 722 }, 723 function(plugin) 724 { 725 var fs = require('fs'); 726 const sysPath = require('path') 727 var pluginsFile = sysPath.join(getAppDataFolder(), '/plugins', plugin); 728 729 if (fs.existsSync(pluginsFile)) 730 { 731 fs.unlinkSync(pluginsFile); 732 } 733 }).container, 360, 170, true, false); 734 }); 735 } 736 737 var appLoad = App.prototype.load; 738 739 App.prototype.load = function() 740 { 741 appLoad.apply(this, arguments); 742 const {ipcRenderer} = require('electron'); 743 744 ipcRenderer.on('args-obj', (event, argsObj) => 745 { 746 this.loadArgs(argsObj) 747 }) 748 749 var editorUi = this; 750 751 ipcRenderer.on('export-vsdx', (event, argsObj) => 752 { 753 var file = new LocalFile(editorUi, argsObj.xml, ''); 754 755 editorUi.fileLoaded(file); 756 757 try 758 { 759 editorUi.saveData = function(filename, format, data, mimeType, base64Encoded) 760 { 761 ipcRenderer.send('export-vsdx-finished', data); 762 }; 763 764 var expSuccess = new VsdxExport(editorUi).exportCurrentDiagrams(); 765 766 if (!expSuccess) 767 { 768 ipcRenderer.send('export-vsdx-finished', null); 769 } 770 } 771 catch (e) 772 { 773 ipcRenderer.send('export-vsdx-finished', null); 774 } 775 }) 776 777 //We do some async stuff during app loading so we need to know exactly when loading is finished (it is not when onload is finished) 778 ipcRenderer.send('app-load-finished', null); 779 } 780 781 App.prototype.loadArgs = function(argsObj) 782 { 783 var paths = argsObj.args; 784 785 // If a file is passed, and it is not an argument (has a leading -) 786 if (paths !== undefined && paths[0] != null && paths[0].indexOf('-') != 0 && this.spinner.spin(document.body, mxResources.get('loading'))) 787 { 788 var path = paths[0]; 789 this.hideDialog(); 790 791 var success = mxUtils.bind(this, function(fileEntry, data, stat, name, isModified) 792 { 793 this.spinner.stop(); 794 795 if (data != null) 796 { 797 var file = new LocalFile(this, data, name || ''); 798 file.fileObject = fileEntry; 799 file.stat = stat; 800 file.setModified(isModified? true : false); 801 this.fileLoaded(file); 802 } 803 }); 804 805 var error = mxUtils.bind(this, function(e) 806 { 807 this.spinner.stop(); 808 809 if (e.code === 'ENOENT') 810 { 811 var title = path.replace(/^.*[\\\/]/, ''); 812 var data = this.emptyDiagramXml; 813 var file = new LocalFile(this, data, title, null); 814 815 file.fileObject = new Object(); 816 file.fileObject.path = path; 817 file.fileObject.name = title; 818 file.fileObject.type = 'utf-8'; 819 this.fileCreated(file, null, null, null); 820 this.saveFile(); 821 } 822 else 823 { 824 this.handleError(e); 825 } 826 827 }); 828 829 // Tries to open the file 830 this.readGraphFile(success, error, path); 831 } 832 // If no file is passed, but there is the "create-if-not-exists" flag 833 else if (argsObj.create != null) 834 { 835 var title = 'Untitled document'; 836 var data = this.emptyDiagramXml; 837 var file = new LocalFile(this, data, title, null); 838 this.fileCreated(file, null, null, null); 839 } 840 } 841 842 var origFileLoaded = EditorUi.prototype.fileLoaded; 843 844 EditorUi.prototype.fileLoaded = function(file) 845 { 846 var fs = require('fs'); 847 var oldFile = this.getCurrentFile(); 848 849 if (oldFile != null && oldFile.fileObject != null) 850 { 851 fs.unwatchFile(oldFile.fileObject.path); 852 } 853 854 if (file != null) 855 { 856 if (file.fileObject == null) 857 { 858 var fname = file.getTitle(); 859 860 var fileInfo = openFilesMap[fname]; 861 862 if (fileInfo != null) 863 { 864 file.fileObject = { 865 name: fileInfo.name, 866 path: fileInfo.path, 867 type: fileInfo.type || 'utf-8' 868 }; 869 //delete it such that it is not used again incorrectly 870 delete openFilesMap[fname]; 871 } 872 } 873 874 if (file.fileObject != null) 875 { 876 var title = file.fileObject.path; 877 878 if (title.length > 100) 879 { 880 title = '...' + title.substr(title.length - 97); 881 } 882 883 this.addRecent({id: file.fileObject.path, title: title}); 884 885 fs.watchFile(file.fileObject.path, mxUtils.bind(this, function(curr, prev) 886 { 887 //File is changed (not just accessed) 888 if (curr.mtimeMs != prev.mtimeMs) 889 { 890 //Ignore our own changes 891 if (file.unwatchedSaves || (file.state != null && file.stat.mtimeMs == curr.mtimeMs)) 892 { 893 file.unwatchedSaves = false; 894 return; 895 } 896 897 file.inConflictState = true; 898 899 this.showError(mxResources.get('externalChanges'), 900 mxResources.get('fileChangedSyncDialog'), 901 mxResources.get('synchronize'), mxUtils.bind(this, function() 902 { 903 if (this.spinner.spin(document.body, mxResources.get('updatingDocument'))) 904 { 905 file.synchronizeFile(mxUtils.bind(this, function() 906 { 907 this.spinner.stop(); 908 }), mxUtils.bind(this, function(err) 909 { 910 file.handleFileError(err, true); 911 })); 912 } 913 }), null, null, null, 914 mxResources.get('cancel'), mxUtils.bind(this, function() 915 { 916 this.hideDialog(); 917 file.handleFileError(null, false); 918 }), 340, 150); 919 } 920 })); 921 } 922 } 923 924 origFileLoaded.apply(this, arguments); 925 }; 926 927 // Uses local picker 928 App.prototype.pickFile = function() 929 { 930 var doPickFile = mxUtils.bind(this, function() 931 { 932 this.chooseFileEntry(mxUtils.bind(this, function(fileEntry, data, stat, name, isModified) 933 { 934 var file = new LocalFile(this, data, ''); 935 file.fileObject = fileEntry; 936 file.stat = stat; 937 file.setModified(isModified? true : false); 938 this.fileLoaded(file); 939 })); 940 }); 941 942 var file = this.getCurrentFile(); 943 944 if (file != null && file.isModified()) 945 { 946 this.confirm(mxResources.get('allChangesLost'), null, doPickFile, 947 mxResources.get('cancel'), mxResources.get('discardChanges')); 948 } 949 else 950 { 951 doPickFile(); 952 } 953 }; 954 955 /** 956 * Selects a library to load from a picker 957 * 958 * @param mode the device mode, ignored in this case 959 */ 960 App.prototype.pickLibrary = function(mode) 961 { 962 this.chooseFileEntry(mxUtils.bind(this, function(fileEntry, data, stat) 963 { 964 try 965 { 966 var library = new DesktopLibrary(this, data, fileEntry); 967 this.loadLibrary(library); 968 } 969 catch (e) 970 { 971 this.handleError(e, mxResources.get('errorLoadingFile')); 972 } 973 })); 974 }; 975 976 // Uses local picker 977 App.prototype.chooseFileEntry = function(fn) 978 { 979 var remote = require('@electron/remote'); 980 var dialog = remote.dialog; 981 const sysPath = require('path') 982 var lastDir = localStorage.getItem('.lastOpenDir'); 983 984 var paths = dialog.showOpenDialogSync({ 985 defaultPath: lastDir || getDocumentsFolder(), 986 filters: [ 987 { name: 'draw.io Diagrams', extensions: ['drawio', 'xml', 'png', 'svg', 'html'] }, 988 { name: 'VSDX Documents', extensions: ['vsdx'] }, 989 { name: 'All Files', extensions: ['*'] } 990 ], 991 properties: ['openFile'] 992 }); 993 994 if (paths !== undefined && paths[0] != null) 995 { 996 localStorage.setItem('.lastOpenDir', sysPath.dirname(paths[0])); 997 998 this.readGraphFile(fn, mxUtils.bind(this, function(err) 999 { 1000 this.handleError(err); 1001 }), paths[0]); 1002 } 1003 else 1004 { 1005 this.spinner.stop(); 1006 } 1007 }; 1008 1009 //In order not to repeat the logic for opening a file, we collect files information here and use them in openLocalFile 1010 var origOpenFiles = EditorUi.prototype.openFiles; 1011 var openFilesMap = {}; 1012 1013 EditorUi.prototype.openFiles = function(files, temp) 1014 { 1015 openFilesMap = {}; 1016 1017 for (var i = 0; i < files.length; i++) 1018 { 1019 openFilesMap[files[i].name] = files[i]; 1020 } 1021 1022 origOpenFiles.apply(this, arguments); 1023 }; 1024 1025 App.prototype.readGraphFile = function(fn, fnErr, path) 1026 { 1027 var fs = require('fs'); 1028 var index = path.lastIndexOf('.png'); 1029 var isPng = index > -1 && index == path.length - 4; 1030 var isVsdx = /\.vsdx$/i.test(path) || /\.vssx$/i.test(path); 1031 var encoding = isVsdx? null : ((isPng || /\.pdf$/i.test(path)) ? 'base64' : 'utf-8'); 1032 var isModified = false, fileLoaded = false; 1033 1034 var readData = mxUtils.bind(this, function (e, data) 1035 { 1036 if (e) 1037 { 1038 fnErr(e); 1039 fileLoaded = true; 1040 } 1041 else 1042 { 1043 var fileEntry = new Object(); 1044 fileEntry.path = path; 1045 fileEntry.name = path.replace(/^.*[\\\/]/, ''); 1046 fileEntry.type = encoding; 1047 1048 // VSDX and PDF files are imported instead of being opened 1049 if (isVsdx) 1050 { 1051 var name = fileEntry.name; 1052 1053 this.importVisio(data, mxUtils.bind(this, function(xml) 1054 { 1055 var dot = name.lastIndexOf('.'); 1056 1057 if (dot >= 0) 1058 { 1059 name = name.substring(0, name.lastIndexOf('.')) + '.drawio'; 1060 } 1061 else 1062 { 1063 name = name + '.drawio'; 1064 } 1065 1066 if (xml.substring(0, 10) == '<mxlibrary') 1067 { 1068 // Creates new temporary file if library is dropped in splash screen 1069 if (this.getCurrentFile() == null && urlParams['embed'] != '1') 1070 { 1071 this.openLocalFile(this.emptyDiagramXml, this.defaultFilename); 1072 } 1073 1074 try 1075 { 1076 this.loadLibrary(new LocalLibrary(this, xml, name)); 1077 } 1078 catch (e) 1079 { 1080 this.handleError(e, mxResources.get('errorLoadingFile')); 1081 } 1082 1083 fn(); 1084 } 1085 else 1086 { 1087 fn(null, xml, null, name, isModified); 1088 } 1089 1090 fileLoaded = true; 1091 }), null, name); 1092 1093 return; 1094 } 1095 else if (/\.pdf$/i.test(path)) 1096 { 1097 var tmp = Editor.extractGraphModelFromPdf('data:application/pdf;base64,' + data); 1098 1099 if (tmp != null) 1100 { 1101 var name = fileEntry.name; 1102 fn(null, tmp, null, name.substring(0, name.lastIndexOf('.')) + '.drawio', isModified); 1103 fileLoaded = true; 1104 1105 return; 1106 } 1107 } 1108 else if (isPng) 1109 { 1110 // Detecting png by extension. Would need https://github.com/mscdex/mmmagic 1111 // to do it by inspection 1112 data = this.extractGraphModelFromPng('data:image/png;base64,' + data); 1113 } 1114 1115 fs.stat(path, function(err, stat) 1116 { 1117 if (err) 1118 { 1119 fnErr(err); 1120 } 1121 else 1122 { 1123 fn(fileEntry, data, stat, null, isModified); 1124 } 1125 1126 fileLoaded = true; 1127 }); 1128 } 1129 }); 1130 1131 fs.readFile(path, encoding, readData); 1132 1133 //Check if a bkp file exists, if one exists, ask user to restore/ignore 1134 var checkBkpFile = mxUtils.bind(this, function (e, data) 1135 { 1136 //Backup file must be loaded after actual file 1137 if (!fileLoaded) 1138 { 1139 setTimeout(function() 1140 { 1141 checkBkpFile(e, data); 1142 }, 10); 1143 return; 1144 } 1145 1146 if (!e) 1147 { 1148 var dlg = new DraftDialog(this, mxResources.get('backupFound'), 1149 data, mxUtils.bind(this, function() 1150 { 1151 this.hideDialog(); 1152 isModified = true; 1153 readData(null, data); 1154 fs.unlink(bkpFile, (err) => {}); //Ignore errors! 1155 }), mxUtils.bind(this, function() 1156 { 1157 this.hideDialog(); 1158 fs.unlink(bkpFile, (err) => {}); //Ignore errors! 1159 })); 1160 1161 this.showDialog(dlg.container, 640, 480, true, false, mxUtils.bind(this, function(cancel) 1162 { 1163 if (cancel) 1164 { 1165 //TODO Rename backup file? 1166 } 1167 })); 1168 1169 dlg.init(); 1170 } 1171 }); 1172 1173 var bkpFile = getBkpFilePath(path); 1174 fs.readFile(bkpFile, encoding, checkBkpFile); 1175 }; 1176 1177 // Disables temp files in Electron 1178 var LocalFileCtor = LocalFile; 1179 1180 LocalFile = function(ui, data, title, temp) 1181 { 1182 LocalFileCtor.call(this, ui, data, title, false); 1183 }; 1184 1185 mxUtils.extend(LocalFile, LocalFileCtor); 1186 1187 LocalFile.prototype.getLatestVersion = function(success, error) 1188 { 1189 if (this.fileObject == null) 1190 { 1191 if (error != null) 1192 { 1193 error({message: mxResources.get('fileNotFound')}); 1194 } 1195 } 1196 else 1197 { 1198 this.ui.readGraphFile(mxUtils.bind(this, function(fileEntry, data, stat, name, isModified) 1199 { 1200 var file = new LocalFile(this, data, ''); 1201 file.stat = stat; 1202 file.setModified(isModified? true : false); 1203 success(file); 1204 }), error, this.fileObject.path); 1205 } 1206 }; 1207 1208 // Call save as for copy 1209 LocalFile.prototype.copyFile = function(success, error) 1210 { 1211 this.saveAs(this.ui.getCopyFilename(this), success, error); 1212 }; 1213 1214 /** 1215 * Adds all listeners. 1216 */ 1217 LocalFile.prototype.getDescriptor = function() 1218 { 1219 return this.stat; 1220 }; 1221 1222 /** 1223 * Updates the descriptor of this file with the one from the given file. 1224 */ 1225 LocalFile.prototype.setDescriptor = function(stat) 1226 { 1227 this.stat = stat; 1228 }; 1229 1230 LocalFile.prototype.reloadFile = function(success) 1231 { 1232 if (this.fileObject == null) 1233 { 1234 this.ui.handleError({message: mxResources.get('fileNotFound')}); 1235 } 1236 else 1237 { 1238 this.ui.spinner.stop(); 1239 1240 var fn = mxUtils.bind(this, function() 1241 { 1242 this.setModified(false); 1243 var page = this.ui.currentPage; 1244 var viewState = this.ui.editor.graph.getViewState(); 1245 var selection = this.ui.editor.graph.getSelectionCells(); 1246 1247 if (this.ui.spinner.spin(document.body, mxResources.get('loading'))) 1248 { 1249 this.ui.readGraphFile(mxUtils.bind(this, function(fileEntry, data, stat, name, isModified) 1250 { 1251 this.ui.spinner.stop(); 1252 1253 var file = new LocalFile(this.ui, data, ''); 1254 file.fileObject = fileEntry; 1255 file.stat = stat; 1256 file.setModified(isModified? true : false); 1257 this.ui.fileLoaded(file); 1258 this.ui.restoreViewState(page, viewState, selection); 1259 1260 if (this.backupPatch != null) 1261 { 1262 this.patch([this.backupPatch]); 1263 } 1264 1265 if (success != null) 1266 { 1267 success(); 1268 } 1269 }), mxUtils.bind(this, function(err) 1270 { 1271 this.handleFileError(err); 1272 }), this.fileObject.path); 1273 } 1274 }); 1275 1276 if (this.isModified() && this.backupPatch == null) 1277 { 1278 this.ui.confirm(mxResources.get('allChangesLost'), mxUtils.bind(this, function() 1279 { 1280 this.handleFileSuccess(DrawioFile.SYNC == 'manual'); 1281 }), fn, mxResources.get('cancel'), mxResources.get('discardChanges')); 1282 } 1283 else 1284 { 1285 fn(); 1286 } 1287 } 1288 }; 1289 1290 LocalFile.prototype.isAutosave = function() 1291 { 1292 return this.fileObject != null && DrawioFile.prototype.isAutosave.apply(this, arguments); 1293 }; 1294 1295 LocalFile.prototype.isAutosaveOptional = function() 1296 { 1297 return this.fileObject != null; 1298 }; 1299 1300 LocalFile.prototype.getTitle = function() 1301 { 1302 return (this.fileObject != null) ? this.fileObject.name : this.title; 1303 }; 1304 1305 LocalFile.prototype.isRenamable = function() 1306 { 1307 return false; 1308 }; 1309 1310 // Restores default implementation of open with autosave 1311 LocalFile.prototype.open = DrawioFile.prototype.open; 1312 1313 LocalFile.prototype.save = function(revision, success, error, unloading, overwrite) 1314 { 1315 DrawioFile.prototype.save.apply(this, [revision, mxUtils.bind(this, function() 1316 { 1317 this.saveFile(revision, success, error, unloading, overwrite); 1318 }), error, unloading, overwrite]); 1319 }; 1320 1321 LocalFile.prototype.isConflict = function(stat) 1322 { 1323 return stat != null && this.stat != null && stat.mtimeMs != this.stat.mtimeMs; 1324 }; 1325 1326 LocalFile.prototype.getFilename = function() 1327 { 1328 var filename = this.title; 1329 1330 // Adds default extension 1331 if (filename.length > 0 && (!/(\.xml)$/i.test(filename) && !/(\.html)$/i.test(filename) && 1332 !/(\.svg)$/i.test(filename) && !/(\.png)$/i.test(filename) && !/(\.drawio)$/i.test(filename))) 1333 { 1334 filename += '.drawio'; 1335 } 1336 1337 return filename; 1338 }; 1339 1340 function getBkpFilePath(filePath) 1341 { 1342 const path = require('path'); 1343 return path.join(path.dirname(filePath), '~$' + path.basename(filePath) + '.bkp'); 1344 }; 1345 1346 // Prototype inheritance needs new functions to be added to subclasses 1347 LocalLibrary.prototype.getFilename = LocalFile.prototype.getFilename; 1348 1349 LocalFile.prototype.saveFile = function(revision, success, error, unloading, overwrite) 1350 { 1351 //Safeguard in case saveFile is called from online code in the future 1352 if (typeof success !== 'function') 1353 { 1354 if (typeof unloading === 'function') 1355 { 1356 //Call error 1357 unloading({message: 'This is a bug, please report!'}); //Original draw.io function parameters are (title, revision, success, error, useCurrentData) 1358 } 1359 return; 1360 } 1361 1362 if (!this.savingFile) 1363 { 1364 var fn = mxUtils.bind(this, function() 1365 { 1366 var doSave = mxUtils.bind(this, function(data, enc) 1367 { 1368 var savedData = this.data; 1369 1370 // Makes sure no changes get lost while the file is saved 1371 this.setShadowModified(false); 1372 this.savingFile = true; 1373 1374 var errorWrapper = mxUtils.bind(this, function(e) 1375 { 1376 this.savingFile = false; 1377 1378 if (error != null) 1379 { 1380 error(e); 1381 } 1382 }); 1383 1384 if (this.fileObject.bkpPath == null) 1385 { 1386 this.fileObject.bkpPath = getBkpFilePath(this.fileObject.path); 1387 } 1388 1389 this.unwatchedSaves = true; //Multiple saves doesn't call watch the same number, so use a boolean and check for changes 1390 1391 App.filesWorkerReq({ 1392 action: 'saveFile', 1393 fileObject: this.fileObject, 1394 defEnc: enc, 1395 data: data, 1396 origStat: this.stat, 1397 overwrite: overwrite 1398 }, mxUtils.bind(this, function(resp) 1399 { 1400 //No changes during the saving process? 1401 this.setModified(this.getShadowModified()); 1402 this.savingFile = false; 1403 var lastDesc = this.stat; 1404 this.stat = resp.stat; 1405 1406 this.fileSaved(savedData, lastDesc, mxUtils.bind(this, function() 1407 { 1408 this.contentChanged(); 1409 1410 if (success != null) 1411 { 1412 success(); 1413 } 1414 }), error); 1415 }), 1416 mxUtils.bind(this, function(errMsg, err) 1417 { 1418 if (errMsg == 'empty data') 1419 { 1420 this.ui.handleError({message: mxResources.get('errorSavingFile')}); 1421 } 1422 else if (errMsg == 'conflict') 1423 { 1424 this.inConflictState = true; 1425 } 1426 1427 errorWrapper(); 1428 })); 1429 }); 1430 1431 if (!/(\.png)$/i.test(this.fileObject.name)) 1432 { 1433 doSave(this.getData()); 1434 } 1435 else 1436 { 1437 var p = this.ui.getPngFileProperties(this.ui.fileNode); 1438 1439 this.ui.getEmbeddedPng(function(data) 1440 { 1441 doSave(atob(data), 'binary'); 1442 }, error, null, p.scale, p.border); 1443 } 1444 }); 1445 1446 if (this.fileObject == null) 1447 { 1448 var remote = require('@electron/remote'); 1449 var dialog = remote.dialog; 1450 const sysPath = require('path') 1451 var lastDir = localStorage.getItem('.lastSaveDir'); 1452 var name = this.getFilename(); 1453 var ext = null; 1454 1455 if (name != null) 1456 { 1457 var idx = name.lastIndexOf('.'); 1458 1459 if (idx > 0) 1460 { 1461 ext = name.substring(idx + 1); 1462 name = name.substring(0, idx); 1463 } 1464 } 1465 1466 var path = dialog.showSaveDialogSync({ 1467 defaultPath: (lastDir || getDocumentsFolder()) + '/' + name, 1468 filters: this.ui.createFileSystemFilters(ext) 1469 }); 1470 1471 if (path != null) 1472 { 1473 localStorage.setItem('.lastSaveDir', sysPath.dirname(path)); 1474 this.fileObject = new Object(); 1475 this.fileObject.path = path; 1476 this.fileObject.name = path.replace(/^.*[\\\/]/, ''); 1477 this.fileObject.type = 'utf-8'; 1478 fn(); 1479 } 1480 else 1481 { 1482 this.ui.spinner.stop(); 1483 } 1484 } 1485 else 1486 { 1487 fn(); 1488 } 1489 } 1490 }; 1491 1492 LocalFile.prototype.saveAs = function(title, success, error) 1493 { 1494 var remote = require('@electron/remote'); 1495 var dialog = remote.dialog; 1496 const sysPath = require('path') 1497 var lastDir = localStorage.getItem('.lastSaveDir'); 1498 var name = this.getFilename(); 1499 var ext = null; 1500 1501 if (name == '' && this.fileObject != null && this.fileObject.name != null) 1502 { 1503 name = this.fileObject.name; 1504 var idx = name.lastIndexOf('.'); 1505 1506 if (idx > 0) 1507 { 1508 ext = name.substring(idx + 1); 1509 name = name.substring(0, idx); 1510 } 1511 } 1512 1513 var path = dialog.showSaveDialogSync({ 1514 defaultPath: (lastDir || getDocumentsFolder()) + '/' + name, 1515 filters: this.ui.createFileSystemFilters(ext) 1516 }); 1517 1518 if (path != null) 1519 { 1520 localStorage.setItem('.lastSaveDir', sysPath.dirname(path)); 1521 this.fileObject = new Object(); 1522 this.fileObject.path = path; 1523 this.fileObject.name = path.replace(/^.*[\\\/]/, ''); 1524 this.fileObject.type = 'utf-8'; 1525 1526 this.save(false, success, error, null, true); 1527 } 1528 }; 1529 1530 /** 1531 * Loads the given file handle as a local file. 1532 */ 1533 App.prototype.createFileSystemFilters = function(defaultExt) 1534 { 1535 var ext = []; 1536 1537 for (var i = 0; i < this.editor.diagramFileTypes.length; i++) 1538 { 1539 var obj = {name: mxResources.get(this.editor.diagramFileTypes[i].description) + 1540 ' (.' + this.editor.diagramFileTypes[i].extension + ')', 1541 extensions: [this.editor.diagramFileTypes[i].extension]}; 1542 1543 if (this.editor.diagramFileTypes[i].extension == defaultExt) 1544 { 1545 ext.splice(0, 0, obj); 1546 } 1547 else 1548 { 1549 ext.push(obj); 1550 } 1551 } 1552 1553 return ext; 1554 }; 1555 1556 /** 1557 * Loads the given file handle as a local file. 1558 */ 1559 App.prototype.saveFile = function(forceDialog) 1560 { 1561 var file = this.getCurrentFile(); 1562 1563 if (file != null) 1564 { 1565 if (!forceDialog && file.getTitle() != null) 1566 { 1567 file.save(true, mxUtils.bind(this, function() 1568 { 1569 if (EditorUi.enableDrafts) 1570 { 1571 file.removeDraft(); 1572 } 1573 1574 file.handleFileSuccess(true); 1575 }), mxUtils.bind(this, function(err) 1576 { 1577 file.handleFileError(err, true); 1578 })); 1579 } 1580 else 1581 { 1582 file.saveAs(null, mxUtils.bind(this, function() 1583 { 1584 if (EditorUi.enableDrafts) 1585 { 1586 file.removeDraft(); 1587 } 1588 1589 file.handleFileSuccess(true); 1590 }), mxUtils.bind(this, function(err) 1591 { 1592 file.handleFileError(err, true); 1593 })); 1594 } 1595 } 1596 }; 1597 1598 /** 1599 * Translates this point by the given vector. 1600 */ 1601 App.prototype.saveLibrary = function(name, images, file, mode, noSpin, noReload, fn) 1602 { 1603 mode = (mode != null) ? mode : this.mode; 1604 noSpin = (noSpin != null) ? noSpin : false; 1605 noReload = (noReload != null) ? noReload : false; 1606 var xml = this.createLibraryDataFromImages(images); 1607 1608 var error = mxUtils.bind(this, function(resp) 1609 { 1610 this.spinner.stop(); 1611 1612 if (fn != null) 1613 { 1614 fn(); 1615 } 1616 1617 // Null means cancel by user and is ignored 1618 if (resp != null) 1619 { 1620 this.handleError(resp, mxResources.get('errorSavingFile')); 1621 } 1622 }); 1623 1624 // Handles special case for local libraries 1625 if (file == null) 1626 { 1627 file = new LocalLibrary(this, xml, name); 1628 } 1629 1630 if (noSpin || this.spinner.spin(document.body, mxResources.get('saving'))) 1631 { 1632 file.setData(xml); 1633 1634 var doSave = mxUtils.bind(this, function() 1635 { 1636 file.save(true, mxUtils.bind(this, function(resp) 1637 { 1638 this.spinner.stop(); 1639 this.hideDialog(true); 1640 1641 if (!noReload) 1642 { 1643 this.libraryLoaded(file, images) 1644 } 1645 1646 if (fn != null) 1647 { 1648 fn(); 1649 } 1650 }), error); 1651 }); 1652 1653 if (name != file.getTitle()) 1654 { 1655 var oldHash = file.getHash(); 1656 1657 file.rename(name, mxUtils.bind(this, function(resp) 1658 { 1659 // Change hash in stored settings 1660 if (file.constructor != LocalLibrary && oldHash != file.getHash()) 1661 { 1662 mxSettings.removeCustomLibrary(oldHash); 1663 mxSettings.addCustomLibrary(file.getHash()); 1664 } 1665 1666 // Workaround for library files changing hash so 1667 // the old library cannot be removed from the 1668 // sidebar using the updated file in libraryLoaded 1669 this.removeLibrarySidebar(oldHash); 1670 1671 doSave(); 1672 }), error) 1673 } 1674 else 1675 { 1676 doSave(); 1677 } 1678 } 1679 }; 1680 1681 App.prototype.checkForUpdates = function() 1682 { 1683 const ipcRenderer = require('electron').ipcRenderer; 1684 ipcRenderer.send('checkForUpdates'); 1685 } 1686 1687 var origUpdateHeader = App.prototype.updateHeader; 1688 1689 App.prototype.updateHeader = function() 1690 { 1691 origUpdateHeader.apply(this, arguments); 1692 1693 document.querySelectorAll('.geMenuItem').forEach(i => i.style.webkitAppRegion = 'no-drag'); 1694 var menubarContainer = document.querySelector('.geMenubarContainer'); 1695 1696 if (urlParams['sketch'] == '1') 1697 { 1698 menubarContainer = this.menubarContainer; 1699 //TODO find a better place for dragging the window 1700 menubarContainer.parentNode.style.webkitAppRegion = 'drag'; 1701 } 1702 1703 menubarContainer.style.webkitAppRegion = 'drag'; 1704 1705 //Add window control buttons 1706 this.windowControls = document.createElement('div'); 1707 this.windowControls.id = 'geWindow-controls'; 1708 this.windowControls.innerHTML = 1709 '<div class="button" id="min-button">' + 1710 ' <svg width="10" height="1" viewBox="0 0 11 1">' + 1711 ' <path d="m11 0v1h-11v-1z" stroke-width=".26208"/>' + 1712 ' </svg>' + 1713 '</div>' + 1714 '<div class="button" id="max-button">' + 1715 ' <svg width="10" height="10" viewBox="0 0 10 10">' + 1716 ' <path d="m10-1.6667e-6v10h-10v-10zm-1.001 1.001h-7.998v7.998h7.998z" stroke-width=".25" />' + 1717 ' </svg>' + 1718 '</div>' + 1719 '<div class="button" id="restore-button">' + 1720 ' <svg width="10" height="10" viewBox="0 0 11 11">' + 1721 ' <path' + 1722 ' d="m11 8.7978h-2.2021v2.2022h-8.7979v-8.7978h2.2021v-2.2022h8.7979zm-3.2979-5.5h-6.6012v6.6011h6.6012zm2.1968-2.1968h-6.6012v1.1011h5.5v5.5h1.1011z"' + 1723 ' stroke-width=".275" />' + 1724 ' </svg>' + 1725 '</div>' + 1726 '<div class="button" id="close-button">' + 1727 ' <svg width="10" height="10" viewBox="0 0 12 12">' + 1728 ' <path' + 1729 ' d="m6.8496 6 5.1504 5.1504-0.84961 0.84961-5.1504-5.1504-5.1504 5.1504-0.84961-0.84961 5.1504-5.1504-5.1504-5.1504 0.84961-0.84961 5.1504 5.1504 5.1504-5.1504 0.84961 0.84961z"' + 1730 ' stroke-width=".3" />' + 1731 ' </svg>' + 1732 '</div>'; 1733 1734 if (uiTheme == 'atlas') 1735 { 1736 this.windowControls.style.top = '9px'; 1737 } 1738 1739 menubarContainer.appendChild(this.windowControls); 1740 1741 var handleDarkModeChange = mxUtils.bind(this, function () 1742 { 1743 if (uiTheme == 'atlas' || Editor.isDarkMode()) 1744 { 1745 this.windowControls.style.fill = 'white'; 1746 document.querySelectorAll('#geWindow-controls .button').forEach(b => b.className = 'button dark'); 1747 } 1748 else 1749 { 1750 this.windowControls.style.fill = '#999'; 1751 document.querySelectorAll('#geWindow-controls .button').forEach(b => b.className = 'button white'); 1752 } 1753 }); 1754 1755 handleDarkModeChange(); 1756 this.addListener('darkModeChanged', handleDarkModeChange); 1757 1758 if (urlParams['sketch'] == '1') 1759 { 1760 this.windowControls.style.position = 'inherit'; 1761 } 1762 1763 if (this.appIcon != null) 1764 { 1765 this.appIcon.style.webkitAppRegion = 'no-drag'; 1766 } 1767 1768 if (this.menubar != null) 1769 { 1770 this.menubar.container.style.webkitAppRegion = 'no-drag'; 1771 } 1772 1773 const remote = require('@electron/remote'); 1774 const win = remote.getCurrentWindow(); 1775 1776 window.onbeforeunload = (event) => { 1777 /* If window is reloaded, remove win event listeners 1778 (DOM element listeners get auto garbage collected but not 1779 Electron win listeners as the win is not dereferenced unless closed) */ 1780 win.removeAllListeners(); 1781 } 1782 1783 // Make minimise/maximise/restore/close buttons work when they are clicked 1784 document.getElementById('min-button').addEventListener("click", event => { 1785 win.minimize(); 1786 }); 1787 1788 document.getElementById('max-button').addEventListener("click", event => { 1789 win.maximize(); 1790 }); 1791 1792 document.getElementById('restore-button').addEventListener("click", event => { 1793 win.unmaximize(); 1794 }); 1795 1796 document.getElementById('close-button').addEventListener("click", event => { 1797 win.close(); 1798 }); 1799 1800 // Toggle maximise/restore buttons when maximisation/unmaximisation occurs 1801 toggleMaxRestoreButtons(); 1802 win.on('maximize', toggleMaxRestoreButtons); 1803 win.on('unmaximize', toggleMaxRestoreButtons); 1804 win.on('resize', toggleMaxRestoreButtons); 1805 1806 function toggleMaxRestoreButtons() { 1807 if (win.isMaximized()) { 1808 document.body.classList.add('geMaximized'); 1809 } else { 1810 document.body.classList.remove('geMaximized'); 1811 } 1812 } 1813 } 1814 1815 /** 1816 * Copies the given cells and XML to the clipboard as an embedded image. 1817 */ 1818 EditorUi.prototype.writeImageToClipboard = function(dataUrl, w, h, error) 1819 { 1820 try 1821 { 1822 const remote = require('@electron/remote'); 1823 1824 remote.clipboard.write({image: remote. 1825 nativeImage.createFromDataURL(dataUrl), html: '<img src="' + 1826 dataUrl + '" width="' + w + '" height="' + h + '">'}); 1827 } 1828 catch (e) 1829 { 1830 error(e); 1831 } 1832 }; 1833 1834 /** 1835 * Updates action states depending on the selection. 1836 */ 1837 var editorUiUpdateActionStates = EditorUi.prototype.updateActionStates; 1838 EditorUi.prototype.updateActionStates = function() 1839 { 1840 editorUiUpdateActionStates.apply(this, arguments); 1841 1842 var file = this.getCurrentFile(); 1843 var syncEnabled = file != null && file.fileObject != null; 1844 this.actions.get('synchronize').setEnabled(syncEnabled); 1845 }; 1846 1847 EditorUi.prototype.saveLocalFile = function(data, filename, mimeType, base64Encoded, format, allowBrowser) 1848 { 1849 this.saveData(filename, format, data, mimeType, base64Encoded); 1850 }; 1851 1852 EditorUi.prototype.saveRequest = function(filename, format, fn, data, base64Encoded, mimeType) 1853 { 1854 var xhr = fn(null, '1'); 1855 1856 if (xhr != null && this.spinner.spin(document.body, mxResources.get('saving'))) 1857 { 1858 xhr.send(mxUtils.bind(this, function() 1859 { 1860 this.spinner.stop(); 1861 1862 if (xhr.getStatus() >= 200 && xhr.getStatus() <= 299) 1863 { 1864 this.saveData(filename, format, xhr.getText(), mimeType, true); 1865 } 1866 else 1867 { 1868 this.handleError({message: mxResources.get('errorSavingFile')}); 1869 } 1870 }), mxUtils.bind(this, function(resp) 1871 { 1872 this.spinner.stop(); 1873 this.handleError(resp); 1874 })); 1875 } 1876 }; 1877 1878 function mxElectronRequest(reqType, reqObj) 1879 { 1880 this.reqType = reqType; 1881 this.reqObj = reqObj; 1882 }; 1883 1884 //Extends mxXmlRequest 1885 mxUtils.extend(mxElectronRequest, mxXmlRequest); 1886 1887 mxElectronRequest.prototype.send = function(callback, error) 1888 { 1889 const ipcRenderer = require('electron').ipcRenderer; 1890 ipcRenderer.send(this.reqType, this.reqObj); 1891 1892 ipcRenderer.once(this.reqType + '-success', (event, data) => 1893 { 1894 this.response = data; 1895 callback(); 1896 ipcRenderer.send(this.reqType + '-finalize'); 1897 }) 1898 1899 ipcRenderer.once(this.reqType + '-error', (event, err) => 1900 { 1901 this.hasError = true; 1902 error(err); 1903 ipcRenderer.send(this.reqType + '-finalize'); 1904 }) 1905 }; 1906 1907 mxElectronRequest.prototype.getStatus = function() 1908 { 1909 return this.hasError? 500 : 200; 1910 } 1911 1912 mxElectronRequest.prototype.getText = function() 1913 { 1914 return this.response; 1915 } 1916 1917 //Direct export to pdf 1918 EditorUi.prototype.createDownloadRequest = function(filename, format, ignoreSelection, base64, transparent, 1919 currentPage, scale, border, grid, includeXml) 1920 { 1921 var graph = this.editor.graph; 1922 var bounds = graph.getGraphBounds(); 1923 1924 // Exports only current page for images that does not contain file data, but for 1925 // the other formats with XML included or pdf with all pages, we need to send the complete data and use 1926 // the from/to URL parameters to specify the page to be exported. 1927 var data = this.getFileData(true, null, null, null, ignoreSelection, currentPage == false? false : format != 'xmlpng'); 1928 var range = null; 1929 var allPages = null; 1930 1931 var embed = (includeXml) ? '1' : '0'; 1932 1933 if (format == 'pdf' && currentPage == false) 1934 { 1935 allPages = '1'; 1936 } 1937 1938 if (format == 'xmlpng') 1939 { 1940 embed = '1'; 1941 format = 'png'; 1942 1943 // Finds the current page number 1944 if (this.pages != null && this.currentPage != null) 1945 { 1946 for (var i = 0; i < this.pages.length; i++) 1947 { 1948 if (this.pages[i] == this.currentPage) 1949 { 1950 range = i; 1951 break; 1952 } 1953 } 1954 } 1955 } 1956 1957 var bg = graph.background; 1958 1959 if (format == 'png' && transparent) 1960 { 1961 bg = mxConstants.NONE; 1962 } 1963 else if (!transparent && (bg == null || bg == mxConstants.NONE)) 1964 { 1965 bg = '#ffffff'; 1966 } 1967 1968 var extras = {globalVars: graph.getExportVariables()}; 1969 1970 if (grid) 1971 { 1972 extras.grid = { 1973 size: graph.gridSize, 1974 steps: graph.view.gridSteps, 1975 color: graph.view.gridColor 1976 }; 1977 } 1978 1979 return new mxElectronRequest('export', { 1980 format: format, 1981 xml: data, 1982 from: range, 1983 bg: (bg != null) ? bg : mxConstants.NONE, 1984 filename: (filename != null) ? filename : null, 1985 allPages: allPages, 1986 base64: base64, 1987 embedXml: embed, 1988 extras: encodeURIComponent(JSON.stringify(extras)), 1989 scale: scale, 1990 border: border 1991 }); 1992 }; 1993 1994 //Export Dialog Pdf case 1995 var origExportFile = ExportDialog.exportFile; 1996 1997 ExportDialog.exportFile = function(editorUi, name, format, bg, s, b, dpi) 1998 { 1999 var graph = editorUi.editor.graph; 2000 2001 if (format == 'xml' || format == 'svg') 2002 { 2003 return origExportFile.apply(this, arguments); 2004 } 2005 else 2006 { 2007 var data = editorUi.getFileData(true, null, null, null, null, true); 2008 var bounds = graph.getGraphBounds(); 2009 var w = Math.floor(bounds.width * s / graph.view.scale); 2010 var h = Math.floor(bounds.height * s / graph.view.scale); 2011 2012 editorUi.hideDialog(); 2013 2014 if ((format == 'png' || format == 'jpg' || format == 'jpeg') && editorUi.isExportToCanvas()) 2015 { 2016 if (format == 'png') 2017 { 2018 editorUi.exportImage(s, bg == null || bg == 'none', true, 2019 false, false, b, true, false, null, null, dpi); 2020 } 2021 else 2022 { 2023 editorUi.exportImage(s, false, true, 2024 false, false, b, true, false, 'jpeg'); 2025 } 2026 } 2027 else 2028 { 2029 var extras = {globalVars: graph.getExportVariables()}; 2030 2031 editorUi.saveRequest(name, format, 2032 function(newTitle, base64) 2033 { 2034 return new mxElectronRequest('export', { 2035 format: format, 2036 xml: data, 2037 bg: (bg != null) ? bg : mxConstants.NONE, 2038 filename: (newTitle != null) ? newTitle : null, 2039 w: w, 2040 h: h, 2041 border: b, 2042 base64: (base64 || '0'), 2043 extras: JSON.stringify(extras), 2044 dpi: dpi > 0? dpi : null 2045 }); 2046 }); 2047 } 2048 } 2049 }; 2050 2051 EditorUi.prototype.saveData = function(filename, format, data, mimeType, base64Encoded) 2052 { 2053 var remote = require('@electron/remote'); 2054 var dialog = remote.dialog; 2055 var resume = (this.spinner != null && this.spinner.pause != null) ? this.spinner.pause() : function() {}; 2056 const sysPath = require('path') 2057 var lastDir = localStorage.getItem('.lastExpDir'); 2058 2059 // Spinner.stop is asynchronous so we must invoke save dialog asynchronously 2060 // to give the spinner some time to stop spinning 2061 window.setTimeout(mxUtils.bind(this, function() 2062 { 2063 var dlgConfig = {defaultPath: (lastDir || getDocumentsFolder()) + '/' + filename}; 2064 var filters = null; 2065 2066 switch (format) 2067 { 2068 case 'xmlpng': 2069 case 'png': 2070 filters = [ 2071 { name: 'PNG Images', extensions: ['png'] } 2072 ]; 2073 break; 2074 case 'jpg': 2075 case 'jpeg': 2076 filters = [ 2077 { name: 'JPEG Images', extensions: ['jpg', 'jpeg'] } 2078 ]; 2079 break; 2080 case 'svg': 2081 filters = [ 2082 { name: 'SVG Images', extensions: ['svg'] } 2083 ]; 2084 break; 2085 case 'pdf': 2086 filters = [ 2087 { name: 'PDF Documents', extensions: ['pdf'] } 2088 ]; 2089 break; 2090 case 'vsdx': 2091 filters = [ 2092 { name: 'VSDX Documents', extensions: ['vsdx'] } 2093 ]; 2094 break; 2095 case 'html': 2096 filters = [ 2097 { name: 'HTML Documents', extensions: ['html'] } 2098 ]; 2099 break; 2100 case 'xml': 2101 filters = [ 2102 { name: 'XML Documents', extensions: ['xml'] } 2103 ]; 2104 break; 2105 }; 2106 2107 dlgConfig['filters'] = filters; 2108 var path = dialog.showSaveDialogSync(dlgConfig); 2109 2110 if (path != null) 2111 { 2112 localStorage.setItem('.lastExpDir', sysPath.dirname(path)); 2113 2114 if (data == null || data.length == 0) 2115 { 2116 this.handleError({message: mxResources.get('errorSavingFile')}); 2117 } 2118 else 2119 { 2120 var fs = require('fs'); 2121 resume(); 2122 2123 var fileObject = new Object(); 2124 fileObject.path = path; 2125 fileObject.name = path.replace(/^.*[\\\/]/, ''); 2126 fileObject.type = (base64Encoded) ? 'base64' : 'utf-8'; 2127 2128 fs.writeFile(fileObject.path, data, fileObject.type, mxUtils.bind(this, function (e) 2129 { 2130 this.spinner.stop(); 2131 2132 if (e) 2133 { 2134 this.handleError({message: mxResources.get('errorSavingFile')}); 2135 } 2136 })); 2137 } 2138 } 2139 }), 50); 2140 }; 2141 2142 EditorUi.prototype.addBeforeUnloadListener = function() {}; 2143 2144 EditorUi.prototype.loadDesktopLib = function(libPath, success, error) 2145 { 2146 this.readGraphFile(mxUtils.bind(this, function(fileEntry, data, stat) 2147 { 2148 var library = new DesktopLibrary(this, data, fileEntry); 2149 this.loadLibrary(library); 2150 success(library); 2151 }), error, libPath); 2152 }; 2153})(); 2154