window.PLUGINS_BASE_PATH = '.'; window.TEMPLATE_PATH = 'templates'; window.DRAW_MATH_URL = 'math'; window.DRAWIO_BASE_URL = '.'; //Prevent access to online website since it is not allowed FeedbackDialog.feedbackUrl = 'https://log.draw.io/email'; //Disables eval for JS (uses shapes-14-6-5.min.js) mxStencilRegistry.allowEval = false; (function() { // Overrides default mode App.mode = App.MODE_DEVICE; // Disables preview option in embed dialog EmbedDialog.showPreviewOption = false; // Disables new window option in edit diagram dialog EditDiagramDialog.showNewWindowOption = false; PrintDialog.previewEnabled = false; PrintDialog.electronPrint = function(editorUi, allPages, pagesFrom, pagesTo, fit, sheetsAcross, sheetsDown, zoom, pageScale, pageFormat) { var xml = '', title = ''; var file = editorUi.getCurrentFile(); if (file) { file.updateFileData(); xml = file.getData(); title = file.title; } new mxElectronRequest('export', { print: true, format: 'pdf', xml: xml, from: pagesFrom - 1, to: pagesTo - 1, allPages: allPages, pageWidth: pageFormat.width, pageHeight: pageFormat.height, pageScale: pageScale, fit: fit, sheetsAcross: sheetsAcross, sheetsDown: sheetsDown, scale: zoom, fileTitle: title }).send(function(){}, function(){}); }; var oldWindowOpen = window.open; window.open = function(url) { if (url != null && url.startsWith('http')) { const {shell} = require('electron'); shell.openExternal(url); } else { return oldWindowOpen(url); } } var origAppMain = App.main; App.main = function() { //TODO Move all file system operations to this worker to offload the renderer thread //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 App.filesWorker = new Worker('electronFilesWorker.js'); App.filesWorkerReqId = 1; App.filesWorkerReqInfo = {}; App.filesWorkerReq = function(msg, callback, error) { msg.reqId = App.filesWorkerReqId++; App.filesWorkerReqInfo[msg.reqId] = {callback: callback, error: error}; App.filesWorker.postMessage(msg); }; App.filesWorker.onmessage = function(e) { var resp = e.data; var callbacks = App.filesWorkerReqInfo[resp.reqId]; if (resp.error) { callbacks.error(resp.msg, resp.e); } else { callbacks.callback(resp.data); } delete App.filesWorkerReqInfo[resp.reqId]; }; //Load desktop plugins var plugins = (mxSettings.settings != null) ? mxSettings.getPlugins() : null; App.initPluginCallback(); if (plugins != null && plugins.length > 0) { for (var i = 0; i < plugins.length; i++) { try { if (plugins[i].startsWith('/plugins/')) { plugins[i] = '.' + plugins[i]; } else if (plugins[i].startsWith('plugins/')) { plugins[i] = './' + plugins[i]; } //Support old plugins added using file:// workaround else if (!plugins[i].startsWith('file://')) { var fs = require('fs'); var sysPath = require('path'); var pluginsFile = sysPath.join(getAppDataFolder(), '/plugins', plugins[i]); if (fs.existsSync(pluginsFile)) { plugins[i] = 'file://' + pluginsFile; } else { continue; //skip not found files } } mxscript(plugins[i]); } catch (e) { // ignore } } } //Disable web plugins loading urlParams['plugins'] = '0'; origAppMain.apply(this, arguments); }; var menusInit = Menus.prototype.init; Menus.prototype.init = function() { menusInit.apply(this, arguments); var editorUi = this.editorUi; editorUi.actions.put('useOffline', new Action(mxResources.get('useOffline') + '...', function() { editorUi.openLink('https://www.draw.io/') })); this.put('openRecent', new Menu(function(menu, parent) { var recent = editorUi.getRecent(); if (recent != null) { for (var i = 0; i < recent.length; i++) { (function(entry) { menu.addItem(entry.title, null, function() { function doOpenRecent() { //Simulate opening a file via args editorUi.loadArgs({args: [entry.id]}); }; var file = editorUi.getCurrentFile(); if (file != null && file.isModified()) { editorUi.confirm(mxResources.get('allChangesLost'), null, doOpenRecent, mxResources.get('cancel'), mxResources.get('discardChanges')); } else { doOpenRecent(); } }, parent); })(recent[i]); } menu.addSeparator(parent); } menu.addItem(mxResources.get('reset'), null, function() { editorUi.resetRecent(); }, parent); })); // Replaces file menu to replace openFrom menu with open and rename downloadAs to export this.put('file', new Menu(mxUtils.bind(this, function(menu, parent) { this.addMenuItems(menu, ['new', 'open'], parent); this.addSubmenu('openRecent', menu, parent); this.addMenuItems(menu, ['-', 'synchronize', '-', 'save', 'saveAs', '-', 'import'], parent); this.addSubmenu('exportAs', menu, parent); menu.addSeparator(parent); this.addSubmenu('embed', menu, parent); menu.addSeparator(parent); this.addMenuItems(menu, ['newLibrary', 'openLibrary'], parent); var file = editorUi.getCurrentFile(); if (file != null && editorUi.fileNode != null) { var filename = (file.getTitle() != null) ? file.getTitle() : editorUi.defaultFilename; if (!/(\.html)$/i.test(filename) && !/(\.svg)$/i.test(filename)) { this.addMenuItems(menu, ['-', 'properties']); } } this.addMenuItems(menu, ['-', 'pageSetup', 'print', '-', 'close'], parent); // LATER: Find API for application.quit }))); }; function getDocumentsFolder() { //On windows, misconfigured Documents folder cause an exception try { return require('@electron/remote').app.getPath('documents'); } catch(e) {} return '.'; }; function getAppDataFolder() { try { var fs = require('fs'); var appDataDir = require('@electron/remote').app.getPath('appData'); var drawioDir = appDataDir + '/draw.io'; if (!fs.existsSync(drawioDir)) //Usually this dir already exists { fs.mkdirSync(drawioDir); } return drawioDir; } catch(e) {} return '.'; }; var graphCreateLinkForHint = Graph.prototype.createLinkForHint; Graph.prototype.createLinkForHint = function(href, label) { var a = graphCreateLinkForHint.call(this, href, label); if (href != null && !this.isCustomLink(href)) { // KNOWN: Event with gesture handler mouseUp the middle click opens a framed window mxEvent.addListener(a, 'click', mxUtils.bind(this, function(evt) { this.openLink(a.getAttribute('href'), a.getAttribute('target')); mxEvent.consume(evt); })); } return a; }; Graph.prototype.openLink = function(url, target) { require('electron').shell.openExternal(url); }; // Initializes the user interface var editorUiInit = EditorUi.prototype.init; EditorUi.prototype.init = function() { editorUiInit.apply(this, arguments); var editorUi = this; var graph = this.editor.graph; global.__emt_isModified = e => { if (editorUi.getCurrentFile()) { return editorUi.getCurrentFile().isModified() } return false } // global.__emt_getCurrentFile = e => { // return this.getCurrentFile() // } // Adds support for libraries this.actions.addAction('newLibrary...', mxUtils.bind(this, function() { editorUi.showLibraryDialog(null, null, null, null, App.MODE_DEVICE); })); this.actions.addAction('openLibrary...', mxUtils.bind(this, function() { editorUi.pickLibrary(App.MODE_DEVICE); })); // Replaces import action this.actions.addAction('import...', mxUtils.bind(this, function() { if (editorUi.getCurrentFile() != null) { var remote = require('@electron/remote'); var dialog = remote.dialog; const sysPath = require('path') var lastDir = localStorage.getItem('.lastImpDir'); var paths = dialog.showOpenDialogSync({ defaultPath: lastDir || getDocumentsFolder(), properties: ['openFile'] }); if (paths !== undefined && paths[0] != null) { var path = paths[0]; localStorage.setItem('.lastImpDir', sysPath.dirname(path)); var asImage = /\.png$/i.test(path) || /\.gif$/i.test(path) || /\.jpe?g$/i.test(path); var encoding = (asImage || /\.pdf$/i.test(path) || /\.vsdx$/i.test(path) || /\.vssx$/i.test(path)) ? 'base64' : 'utf-8'; if (editorUi.spinner.spin(document.body, mxResources.get('loading'))) { var fs = require('fs'); fs.readFile(path, encoding, mxUtils.bind(this, function (e, data) { if (e) { editorUi.spinner.stop(); editorUi.handleError(e); } else { try { if (editorUi.isLucidChartData(data)) { editorUi.convertLucidChart(data, function(xml) { editorUi.spinner.stop(); graph.setSelectionCells(editorUi.importXml(xml)); }, function(e) { editorUi.spinner.stop(); editorUi.handleError(e); }); } else if (/(\.vsdx)($|\?)/i.test(path)) { editorUi.importVisio(editorUi.base64ToBlob(data, 'application/octet-stream'), function(xml) { editorUi.spinner.stop(); graph.setSelectionCells(editorUi.importXml(xml)); }); } else if (!editorUi.isOffline() && new XMLHttpRequest().upload && editorUi.isRemoteFileFormat(data, path)) { // Asynchronous parsing via server editorUi.parseFile(new Blob([data], {type : 'application/octet-stream'}), mxUtils.bind(this, function(xhr) { if (xhr.readyState == 4) { editorUi.spinner.stop(); if (xhr.status >= 200 && xhr.status <= 299) { graph.setSelectionCells(editorUi.importXml(xhr.responseText)); } } }), path); } else { if (/\.pdf$/i.test(path)) { var tmp = Editor.extractGraphModelFromPdf(data); if (tmp != null) { data = tmp; } } else if (/\.png$/i.test(path)) { var tmp = editorUi.extractGraphModelFromPng(data); if (tmp != null) { asImage = false; data = tmp; } } else if (/\.svg$/i.test(path)) { // LATER: Use importXml without throwing exception if no data // Checks if SVG contains content attribute var root = mxUtils.parseXml(data); var svgs = root.getElementsByTagName('svg'); if (svgs.length > 0) { var svgRoot = svgs[0]; var cont = svgRoot.getAttribute('content'); if (cont != null && cont.charAt(0) != '<' && cont.charAt(0) != '%') { cont = unescape((window.atob) ? atob(cont) : Base64.decode(cont, true)); } if (cont != null && cont.charAt(0) == '%') { cont = decodeURIComponent(cont); } if (cont != null && (cont.substring(0, 8) === ' 0) { try { var tmpGraph = createGraph(); tmpGraph.importCells(cells, 0, 0, tmpGraph.getDefaultParent()); var remote = require('@electron/remote'); var clipboard = remote.clipboard; var codec = new mxCodec(); var node = codec.encode(tmpGraph.getModel()); var modelString = mxUtils.getXml(node); clipboard.writeText(encodeURIComponent(modelString)); } catch(e) { //Ignore } } }; function cloneSysCLipboardToMx() { try { var remote = require('@electron/remote'); var clipboard = remote.clipboard; var modelString = clipboard.readText(); if (modelString) { modelString = decodeURIComponent(modelString); var xmlDoc = mxUtils.parseXml(modelString); var tmpGraph = createGraph(); var codec = new mxCodec(xmlDoc); var model = tmpGraph.getModel(); codec.decode(xmlDoc.documentElement, model); mxClipboard.setCells(model.root.children[0].children); } } catch(e) { //Ignore, the contents of mxClipboard will be used } }; //Set system clipboard on menu copy/cut var origCut = this.actions.get('cut').funct; editorUi.actions.addAction('cut', function() { origCut(); cloneMxCLipboardToSys(); }, null, 'sprite-cut', Editor.ctrlKey + '+X'); var origCopy = this.actions.get('copy').funct; editorUi.actions.addAction('copy', function() { origCopy(); cloneMxCLipboardToSys(); }, null, 'sprite-copy', Editor.ctrlKey + '+C'); //Get data from system clipboard for pase/pasteHere var origPaste = this.actions.get('paste').funct; editorUi.actions.addAction('paste', function() { cloneSysCLipboardToMx(); origPaste(); }, false, 'sprite-paste', Editor.ctrlKey + '+V'); var origPasteHere = this.actions.get('pasteHere').funct; editorUi.actions.addAction('pasteHere', function() { cloneSysCLipboardToMx(); origPasteHere(); }); //Enable paste action even if mxClipboard is empty! TODO Is this OK? editorUi.updatePasteActionStates = function() { var graph = this.editor.graph; var paste = this.actions.get('paste'); var pasteHere = this.actions.get('pasteHere'); paste.setEnabled(this.editor.graph.cellEditor.isContentEditing() || (graph.isEnabled() && !graph.isCellLocked(graph.getDefaultParent()))); pasteHere.setEnabled(paste.isEnabled()); }; editorUi.actions.addAction('plugins...', function() { editorUi.showDialog(new PluginsDialog(editorUi, function(callback) { var div = document.createElement('div'); var title = document.createElement('span'); title.style.marginTop = '6px'; mxUtils.write(title, mxResources.get('builtinPlugins') + ': '); div.appendChild(title); var pluginsSelect = document.createElement('select'); pluginsSelect.style.width = '150px'; for (var i = 0; i < App.publicPlugin.length; i++) { var option = document.createElement('option'); mxUtils.write(option, App.publicPlugin[i]); option.value = App.publicPlugin[i]; pluginsSelect.appendChild(option); } div.appendChild(pluginsSelect); mxUtils.br(div); mxUtils.br(div); title = document.createElement('span'); mxUtils.write(title, mxResources.get('extPlugins') + ': '); div.appendChild(title); var extPluginsBtn = mxUtils.button(mxResources.get('selectFile') + '...', function() { var remote = require('@electron/remote'); var dialog = remote.dialog; const sysPath = require('path'); var lastDir = localStorage.getItem('.lastPluginDir'); var paths = dialog.showOpenDialogSync({ defaultPath: lastDir || getDocumentsFolder(), filters: [ { name: 'draw.io Plugins', extensions: ['js'] }, { name: 'All Files', extensions: ['*'] } ], properties: ['openFile'] }); if (paths !== undefined && paths[0] != null) { localStorage.setItem('.lastPluginDir', sysPath.dirname(paths[0])); var fs = require('fs'); var pluginsDir = sysPath.join(getAppDataFolder(), '/plugins'); if (!fs.existsSync(pluginsDir)) { fs.mkdirSync(pluginsDir); } var pluginName = sysPath.basename(paths[0]); var dstFile = sysPath.join(pluginsDir, pluginName); if (fs.existsSync(dstFile)) { alert(mxResources.get('fileExists')); } else { fs.copyFile(paths[0], dstFile, (err) => { if (err) { alert('Adding plugin failed.'); } else { callback(pluginName); editorUi.hideDialog(); } }); } } }); extPluginsBtn.className = 'geBtn'; div.appendChild(extPluginsBtn); var dlg = new CustomDialog(editorUi, div, mxUtils.bind(this, function() { callback(App.pluginRegistry[pluginsSelect.value]); })); editorUi.showDialog(dlg.container, 300, 120, true, true); }, function(plugin) { var fs = require('fs'); const sysPath = require('path') var pluginsFile = sysPath.join(getAppDataFolder(), '/plugins', plugin); if (fs.existsSync(pluginsFile)) { fs.unlinkSync(pluginsFile); } }).container, 360, 170, true, false); }); } var appLoad = App.prototype.load; App.prototype.load = function() { appLoad.apply(this, arguments); const {ipcRenderer} = require('electron'); ipcRenderer.on('args-obj', (event, argsObj) => { this.loadArgs(argsObj) }) var editorUi = this; ipcRenderer.on('export-vsdx', (event, argsObj) => { var file = new LocalFile(editorUi, argsObj.xml, ''); editorUi.fileLoaded(file); try { editorUi.saveData = function(filename, format, data, mimeType, base64Encoded) { ipcRenderer.send('export-vsdx-finished', data); }; var expSuccess = new VsdxExport(editorUi).exportCurrentDiagrams(); if (!expSuccess) { ipcRenderer.send('export-vsdx-finished', null); } } catch (e) { ipcRenderer.send('export-vsdx-finished', null); } }) //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) ipcRenderer.send('app-load-finished', null); } App.prototype.loadArgs = function(argsObj) { var paths = argsObj.args; // If a file is passed, and it is not an argument (has a leading -) if (paths !== undefined && paths[0] != null && paths[0].indexOf('-') != 0 && this.spinner.spin(document.body, mxResources.get('loading'))) { var path = paths[0]; this.hideDialog(); var success = mxUtils.bind(this, function(fileEntry, data, stat, name, isModified) { this.spinner.stop(); if (data != null) { var file = new LocalFile(this, data, name || ''); file.fileObject = fileEntry; file.stat = stat; file.setModified(isModified? true : false); this.fileLoaded(file); } }); var error = mxUtils.bind(this, function(e) { this.spinner.stop(); if (e.code === 'ENOENT') { var title = path.replace(/^.*[\\\/]/, ''); var data = this.emptyDiagramXml; var file = new LocalFile(this, data, title, null); file.fileObject = new Object(); file.fileObject.path = path; file.fileObject.name = title; file.fileObject.type = 'utf-8'; this.fileCreated(file, null, null, null); this.saveFile(); } else { this.handleError(e); } }); // Tries to open the file this.readGraphFile(success, error, path); } // If no file is passed, but there is the "create-if-not-exists" flag else if (argsObj.create != null) { var title = 'Untitled document'; var data = this.emptyDiagramXml; var file = new LocalFile(this, data, title, null); this.fileCreated(file, null, null, null); } } var origFileLoaded = EditorUi.prototype.fileLoaded; EditorUi.prototype.fileLoaded = function(file) { var fs = require('fs'); var oldFile = this.getCurrentFile(); if (oldFile != null && oldFile.fileObject != null) { fs.unwatchFile(oldFile.fileObject.path); } if (file != null) { if (file.fileObject == null) { var fname = file.getTitle(); var fileInfo = openFilesMap[fname]; if (fileInfo != null) { file.fileObject = { name: fileInfo.name, path: fileInfo.path, type: fileInfo.type || 'utf-8' }; //delete it such that it is not used again incorrectly delete openFilesMap[fname]; } } if (file.fileObject != null) { var title = file.fileObject.path; if (title.length > 100) { title = '...' + title.substr(title.length - 97); } this.addRecent({id: file.fileObject.path, title: title}); fs.watchFile(file.fileObject.path, mxUtils.bind(this, function(curr, prev) { //File is changed (not just accessed) if (curr.mtimeMs != prev.mtimeMs) { //Ignore our own changes if (file.unwatchedSaves || (file.state != null && file.stat.mtimeMs == curr.mtimeMs)) { file.unwatchedSaves = false; return; } file.inConflictState = true; this.showError(mxResources.get('externalChanges'), mxResources.get('fileChangedSyncDialog'), mxResources.get('synchronize'), mxUtils.bind(this, function() { if (this.spinner.spin(document.body, mxResources.get('updatingDocument'))) { file.synchronizeFile(mxUtils.bind(this, function() { this.spinner.stop(); }), mxUtils.bind(this, function(err) { file.handleFileError(err, true); })); } }), null, null, null, mxResources.get('cancel'), mxUtils.bind(this, function() { this.hideDialog(); file.handleFileError(null, false); }), 340, 150); } })); } } origFileLoaded.apply(this, arguments); }; // Uses local picker App.prototype.pickFile = function() { var doPickFile = mxUtils.bind(this, function() { this.chooseFileEntry(mxUtils.bind(this, function(fileEntry, data, stat, name, isModified) { var file = new LocalFile(this, data, ''); file.fileObject = fileEntry; file.stat = stat; file.setModified(isModified? true : false); this.fileLoaded(file); })); }); var file = this.getCurrentFile(); if (file != null && file.isModified()) { this.confirm(mxResources.get('allChangesLost'), null, doPickFile, mxResources.get('cancel'), mxResources.get('discardChanges')); } else { doPickFile(); } }; /** * Selects a library to load from a picker * * @param mode the device mode, ignored in this case */ App.prototype.pickLibrary = function(mode) { this.chooseFileEntry(mxUtils.bind(this, function(fileEntry, data, stat) { try { var library = new DesktopLibrary(this, data, fileEntry); this.loadLibrary(library); } catch (e) { this.handleError(e, mxResources.get('errorLoadingFile')); } })); }; // Uses local picker App.prototype.chooseFileEntry = function(fn) { var remote = require('@electron/remote'); var dialog = remote.dialog; const sysPath = require('path') var lastDir = localStorage.getItem('.lastOpenDir'); var paths = dialog.showOpenDialogSync({ defaultPath: lastDir || getDocumentsFolder(), filters: [ { name: 'draw.io Diagrams', extensions: ['drawio', 'xml', 'png', 'svg', 'html'] }, { name: 'VSDX Documents', extensions: ['vsdx'] }, { name: 'All Files', extensions: ['*'] } ], properties: ['openFile'] }); if (paths !== undefined && paths[0] != null) { localStorage.setItem('.lastOpenDir', sysPath.dirname(paths[0])); this.readGraphFile(fn, mxUtils.bind(this, function(err) { this.handleError(err); }), paths[0]); } else { this.spinner.stop(); } }; //In order not to repeat the logic for opening a file, we collect files information here and use them in openLocalFile var origOpenFiles = EditorUi.prototype.openFiles; var openFilesMap = {}; EditorUi.prototype.openFiles = function(files, temp) { openFilesMap = {}; for (var i = 0; i < files.length; i++) { openFilesMap[files[i].name] = files[i]; } origOpenFiles.apply(this, arguments); }; App.prototype.readGraphFile = function(fn, fnErr, path) { var fs = require('fs'); var index = path.lastIndexOf('.png'); var isPng = index > -1 && index == path.length - 4; var isVsdx = /\.vsdx$/i.test(path) || /\.vssx$/i.test(path); var encoding = isVsdx? null : ((isPng || /\.pdf$/i.test(path)) ? 'base64' : 'utf-8'); var isModified = false, fileLoaded = false; var readData = mxUtils.bind(this, function (e, data) { if (e) { fnErr(e); fileLoaded = true; } else { var fileEntry = new Object(); fileEntry.path = path; fileEntry.name = path.replace(/^.*[\\\/]/, ''); fileEntry.type = encoding; // VSDX and PDF files are imported instead of being opened if (isVsdx) { var name = fileEntry.name; this.importVisio(data, mxUtils.bind(this, function(xml) { var dot = name.lastIndexOf('.'); if (dot >= 0) { name = name.substring(0, name.lastIndexOf('.')) + '.drawio'; } else { name = name + '.drawio'; } if (xml.substring(0, 10) == ' {}); //Ignore errors! }), mxUtils.bind(this, function() { this.hideDialog(); fs.unlink(bkpFile, (err) => {}); //Ignore errors! })); this.showDialog(dlg.container, 640, 480, true, false, mxUtils.bind(this, function(cancel) { if (cancel) { //TODO Rename backup file? } })); dlg.init(); } }); var bkpFile = getBkpFilePath(path); fs.readFile(bkpFile, encoding, checkBkpFile); }; // Disables temp files in Electron var LocalFileCtor = LocalFile; LocalFile = function(ui, data, title, temp) { LocalFileCtor.call(this, ui, data, title, false); }; mxUtils.extend(LocalFile, LocalFileCtor); LocalFile.prototype.getLatestVersion = function(success, error) { if (this.fileObject == null) { if (error != null) { error({message: mxResources.get('fileNotFound')}); } } else { this.ui.readGraphFile(mxUtils.bind(this, function(fileEntry, data, stat, name, isModified) { var file = new LocalFile(this, data, ''); file.stat = stat; file.setModified(isModified? true : false); success(file); }), error, this.fileObject.path); } }; // Call save as for copy LocalFile.prototype.copyFile = function(success, error) { this.saveAs(this.ui.getCopyFilename(this), success, error); }; /** * Adds all listeners. */ LocalFile.prototype.getDescriptor = function() { return this.stat; }; /** * Updates the descriptor of this file with the one from the given file. */ LocalFile.prototype.setDescriptor = function(stat) { this.stat = stat; }; LocalFile.prototype.reloadFile = function(success) { if (this.fileObject == null) { this.ui.handleError({message: mxResources.get('fileNotFound')}); } else { this.ui.spinner.stop(); var fn = mxUtils.bind(this, function() { this.setModified(false); var page = this.ui.currentPage; var viewState = this.ui.editor.graph.getViewState(); var selection = this.ui.editor.graph.getSelectionCells(); if (this.ui.spinner.spin(document.body, mxResources.get('loading'))) { this.ui.readGraphFile(mxUtils.bind(this, function(fileEntry, data, stat, name, isModified) { this.ui.spinner.stop(); var file = new LocalFile(this.ui, data, ''); file.fileObject = fileEntry; file.stat = stat; file.setModified(isModified? true : false); this.ui.fileLoaded(file); this.ui.restoreViewState(page, viewState, selection); if (this.backupPatch != null) { this.patch([this.backupPatch]); } if (success != null) { success(); } }), mxUtils.bind(this, function(err) { this.handleFileError(err); }), this.fileObject.path); } }); if (this.isModified() && this.backupPatch == null) { this.ui.confirm(mxResources.get('allChangesLost'), mxUtils.bind(this, function() { this.handleFileSuccess(DrawioFile.SYNC == 'manual'); }), fn, mxResources.get('cancel'), mxResources.get('discardChanges')); } else { fn(); } } }; LocalFile.prototype.isAutosave = function() { return this.fileObject != null && DrawioFile.prototype.isAutosave.apply(this, arguments); }; LocalFile.prototype.isAutosaveOptional = function() { return this.fileObject != null; }; LocalFile.prototype.getTitle = function() { return (this.fileObject != null) ? this.fileObject.name : this.title; }; LocalFile.prototype.isRenamable = function() { return false; }; // Restores default implementation of open with autosave LocalFile.prototype.open = DrawioFile.prototype.open; LocalFile.prototype.save = function(revision, success, error, unloading, overwrite) { DrawioFile.prototype.save.apply(this, [revision, mxUtils.bind(this, function() { this.saveFile(revision, success, error, unloading, overwrite); }), error, unloading, overwrite]); }; LocalFile.prototype.isConflict = function(stat) { return stat != null && this.stat != null && stat.mtimeMs != this.stat.mtimeMs; }; LocalFile.prototype.getFilename = function() { var filename = this.title; // Adds default extension if (filename.length > 0 && (!/(\.xml)$/i.test(filename) && !/(\.html)$/i.test(filename) && !/(\.svg)$/i.test(filename) && !/(\.png)$/i.test(filename) && !/(\.drawio)$/i.test(filename))) { filename += '.drawio'; } return filename; }; function getBkpFilePath(filePath) { const path = require('path'); return path.join(path.dirname(filePath), '~$' + path.basename(filePath) + '.bkp'); }; // Prototype inheritance needs new functions to be added to subclasses LocalLibrary.prototype.getFilename = LocalFile.prototype.getFilename; LocalFile.prototype.saveFile = function(revision, success, error, unloading, overwrite) { //Safeguard in case saveFile is called from online code in the future if (typeof success !== 'function') { if (typeof unloading === 'function') { //Call error unloading({message: 'This is a bug, please report!'}); //Original draw.io function parameters are (title, revision, success, error, useCurrentData) } return; } if (!this.savingFile) { var fn = mxUtils.bind(this, function() { var doSave = mxUtils.bind(this, function(data, enc) { var savedData = this.data; // Makes sure no changes get lost while the file is saved this.setShadowModified(false); this.savingFile = true; var errorWrapper = mxUtils.bind(this, function(e) { this.savingFile = false; if (error != null) { error(e); } }); if (this.fileObject.bkpPath == null) { this.fileObject.bkpPath = getBkpFilePath(this.fileObject.path); } this.unwatchedSaves = true; //Multiple saves doesn't call watch the same number, so use a boolean and check for changes App.filesWorkerReq({ action: 'saveFile', fileObject: this.fileObject, defEnc: enc, data: data, origStat: this.stat, overwrite: overwrite }, mxUtils.bind(this, function(resp) { //No changes during the saving process? this.setModified(this.getShadowModified()); this.savingFile = false; var lastDesc = this.stat; this.stat = resp.stat; this.fileSaved(savedData, lastDesc, mxUtils.bind(this, function() { this.contentChanged(); if (success != null) { success(); } }), error); }), mxUtils.bind(this, function(errMsg, err) { if (errMsg == 'empty data') { this.ui.handleError({message: mxResources.get('errorSavingFile')}); } else if (errMsg == 'conflict') { this.inConflictState = true; } errorWrapper(); })); }); if (!/(\.png)$/i.test(this.fileObject.name)) { doSave(this.getData()); } else { var p = this.ui.getPngFileProperties(this.ui.fileNode); this.ui.getEmbeddedPng(function(data) { doSave(atob(data), 'binary'); }, error, null, p.scale, p.border); } }); if (this.fileObject == null) { var remote = require('@electron/remote'); var dialog = remote.dialog; const sysPath = require('path') var lastDir = localStorage.getItem('.lastSaveDir'); var name = this.getFilename(); var ext = null; if (name != null) { var idx = name.lastIndexOf('.'); if (idx > 0) { ext = name.substring(idx + 1); name = name.substring(0, idx); } } var path = dialog.showSaveDialogSync({ defaultPath: (lastDir || getDocumentsFolder()) + '/' + name, filters: this.ui.createFileSystemFilters(ext) }); if (path != null) { localStorage.setItem('.lastSaveDir', sysPath.dirname(path)); this.fileObject = new Object(); this.fileObject.path = path; this.fileObject.name = path.replace(/^.*[\\\/]/, ''); this.fileObject.type = 'utf-8'; fn(); } else { this.ui.spinner.stop(); } } else { fn(); } } }; LocalFile.prototype.saveAs = function(title, success, error) { var remote = require('@electron/remote'); var dialog = remote.dialog; const sysPath = require('path') var lastDir = localStorage.getItem('.lastSaveDir'); var name = this.getFilename(); var ext = null; if (name == '' && this.fileObject != null && this.fileObject.name != null) { name = this.fileObject.name; var idx = name.lastIndexOf('.'); if (idx > 0) { ext = name.substring(idx + 1); name = name.substring(0, idx); } } var path = dialog.showSaveDialogSync({ defaultPath: (lastDir || getDocumentsFolder()) + '/' + name, filters: this.ui.createFileSystemFilters(ext) }); if (path != null) { localStorage.setItem('.lastSaveDir', sysPath.dirname(path)); this.fileObject = new Object(); this.fileObject.path = path; this.fileObject.name = path.replace(/^.*[\\\/]/, ''); this.fileObject.type = 'utf-8'; this.save(false, success, error, null, true); } }; /** * Loads the given file handle as a local file. */ App.prototype.createFileSystemFilters = function(defaultExt) { var ext = []; for (var i = 0; i < this.editor.diagramFileTypes.length; i++) { var obj = {name: mxResources.get(this.editor.diagramFileTypes[i].description) + ' (.' + this.editor.diagramFileTypes[i].extension + ')', extensions: [this.editor.diagramFileTypes[i].extension]}; if (this.editor.diagramFileTypes[i].extension == defaultExt) { ext.splice(0, 0, obj); } else { ext.push(obj); } } return ext; }; /** * Loads the given file handle as a local file. */ App.prototype.saveFile = function(forceDialog) { var file = this.getCurrentFile(); if (file != null) { if (!forceDialog && file.getTitle() != null) { file.save(true, mxUtils.bind(this, function() { if (EditorUi.enableDrafts) { file.removeDraft(); } file.handleFileSuccess(true); }), mxUtils.bind(this, function(err) { file.handleFileError(err, true); })); } else { file.saveAs(null, mxUtils.bind(this, function() { if (EditorUi.enableDrafts) { file.removeDraft(); } file.handleFileSuccess(true); }), mxUtils.bind(this, function(err) { file.handleFileError(err, true); })); } } }; /** * Translates this point by the given vector. */ App.prototype.saveLibrary = function(name, images, file, mode, noSpin, noReload, fn) { mode = (mode != null) ? mode : this.mode; noSpin = (noSpin != null) ? noSpin : false; noReload = (noReload != null) ? noReload : false; var xml = this.createLibraryDataFromImages(images); var error = mxUtils.bind(this, function(resp) { this.spinner.stop(); if (fn != null) { fn(); } // Null means cancel by user and is ignored if (resp != null) { this.handleError(resp, mxResources.get('errorSavingFile')); } }); // Handles special case for local libraries if (file == null) { file = new LocalLibrary(this, xml, name); } if (noSpin || this.spinner.spin(document.body, mxResources.get('saving'))) { file.setData(xml); var doSave = mxUtils.bind(this, function() { file.save(true, mxUtils.bind(this, function(resp) { this.spinner.stop(); this.hideDialog(true); if (!noReload) { this.libraryLoaded(file, images) } if (fn != null) { fn(); } }), error); }); if (name != file.getTitle()) { var oldHash = file.getHash(); file.rename(name, mxUtils.bind(this, function(resp) { // Change hash in stored settings if (file.constructor != LocalLibrary && oldHash != file.getHash()) { mxSettings.removeCustomLibrary(oldHash); mxSettings.addCustomLibrary(file.getHash()); } // Workaround for library files changing hash so // the old library cannot be removed from the // sidebar using the updated file in libraryLoaded this.removeLibrarySidebar(oldHash); doSave(); }), error) } else { doSave(); } } }; App.prototype.checkForUpdates = function() { const ipcRenderer = require('electron').ipcRenderer; ipcRenderer.send('checkForUpdates'); } var origUpdateHeader = App.prototype.updateHeader; App.prototype.updateHeader = function() { origUpdateHeader.apply(this, arguments); document.querySelectorAll('.geMenuItem').forEach(i => i.style.webkitAppRegion = 'no-drag'); var menubarContainer = document.querySelector('.geMenubarContainer'); if (urlParams['sketch'] == '1') { menubarContainer = this.menubarContainer; //TODO find a better place for dragging the window menubarContainer.parentNode.style.webkitAppRegion = 'drag'; } menubarContainer.style.webkitAppRegion = 'drag'; //Add window control buttons this.windowControls = document.createElement('div'); this.windowControls.id = 'geWindow-controls'; this.windowControls.innerHTML = '
' + ' ' + ' ' + ' ' + '
' + '
' + ' ' + ' ' + ' ' + '
' + '
' + ' ' + ' ' + ' ' + '
' + '
' + ' ' + ' ' + ' ' + '
'; if (uiTheme == 'atlas') { this.windowControls.style.top = '9px'; } menubarContainer.appendChild(this.windowControls); var handleDarkModeChange = mxUtils.bind(this, function () { if (uiTheme == 'atlas' || Editor.isDarkMode()) { this.windowControls.style.fill = 'white'; document.querySelectorAll('#geWindow-controls .button').forEach(b => b.className = 'button dark'); } else { this.windowControls.style.fill = '#999'; document.querySelectorAll('#geWindow-controls .button').forEach(b => b.className = 'button white'); } }); handleDarkModeChange(); this.addListener('darkModeChanged', handleDarkModeChange); if (urlParams['sketch'] == '1') { this.windowControls.style.position = 'inherit'; } if (this.appIcon != null) { this.appIcon.style.webkitAppRegion = 'no-drag'; } if (this.menubar != null) { this.menubar.container.style.webkitAppRegion = 'no-drag'; } const remote = require('@electron/remote'); const win = remote.getCurrentWindow(); window.onbeforeunload = (event) => { /* If window is reloaded, remove win event listeners (DOM element listeners get auto garbage collected but not Electron win listeners as the win is not dereferenced unless closed) */ win.removeAllListeners(); } // Make minimise/maximise/restore/close buttons work when they are clicked document.getElementById('min-button').addEventListener("click", event => { win.minimize(); }); document.getElementById('max-button').addEventListener("click", event => { win.maximize(); }); document.getElementById('restore-button').addEventListener("click", event => { win.unmaximize(); }); document.getElementById('close-button').addEventListener("click", event => { win.close(); }); // Toggle maximise/restore buttons when maximisation/unmaximisation occurs toggleMaxRestoreButtons(); win.on('maximize', toggleMaxRestoreButtons); win.on('unmaximize', toggleMaxRestoreButtons); win.on('resize', toggleMaxRestoreButtons); function toggleMaxRestoreButtons() { if (win.isMaximized()) { document.body.classList.add('geMaximized'); } else { document.body.classList.remove('geMaximized'); } } } /** * Copies the given cells and XML to the clipboard as an embedded image. */ EditorUi.prototype.writeImageToClipboard = function(dataUrl, w, h, error) { try { const remote = require('@electron/remote'); remote.clipboard.write({image: remote. nativeImage.createFromDataURL(dataUrl), html: ''}); } catch (e) { error(e); } }; /** * Updates action states depending on the selection. */ var editorUiUpdateActionStates = EditorUi.prototype.updateActionStates; EditorUi.prototype.updateActionStates = function() { editorUiUpdateActionStates.apply(this, arguments); var file = this.getCurrentFile(); var syncEnabled = file != null && file.fileObject != null; this.actions.get('synchronize').setEnabled(syncEnabled); }; EditorUi.prototype.saveLocalFile = function(data, filename, mimeType, base64Encoded, format, allowBrowser) { this.saveData(filename, format, data, mimeType, base64Encoded); }; EditorUi.prototype.saveRequest = function(filename, format, fn, data, base64Encoded, mimeType) { var xhr = fn(null, '1'); if (xhr != null && this.spinner.spin(document.body, mxResources.get('saving'))) { xhr.send(mxUtils.bind(this, function() { this.spinner.stop(); if (xhr.getStatus() >= 200 && xhr.getStatus() <= 299) { this.saveData(filename, format, xhr.getText(), mimeType, true); } else { this.handleError({message: mxResources.get('errorSavingFile')}); } }), mxUtils.bind(this, function(resp) { this.spinner.stop(); this.handleError(resp); })); } }; function mxElectronRequest(reqType, reqObj) { this.reqType = reqType; this.reqObj = reqObj; }; //Extends mxXmlRequest mxUtils.extend(mxElectronRequest, mxXmlRequest); mxElectronRequest.prototype.send = function(callback, error) { const ipcRenderer = require('electron').ipcRenderer; ipcRenderer.send(this.reqType, this.reqObj); ipcRenderer.once(this.reqType + '-success', (event, data) => { this.response = data; callback(); ipcRenderer.send(this.reqType + '-finalize'); }) ipcRenderer.once(this.reqType + '-error', (event, err) => { this.hasError = true; error(err); ipcRenderer.send(this.reqType + '-finalize'); }) }; mxElectronRequest.prototype.getStatus = function() { return this.hasError? 500 : 200; } mxElectronRequest.prototype.getText = function() { return this.response; } //Direct export to pdf EditorUi.prototype.createDownloadRequest = function(filename, format, ignoreSelection, base64, transparent, currentPage, scale, border, grid, includeXml) { var graph = this.editor.graph; var bounds = graph.getGraphBounds(); // Exports only current page for images that does not contain file data, but for // the other formats with XML included or pdf with all pages, we need to send the complete data and use // the from/to URL parameters to specify the page to be exported. var data = this.getFileData(true, null, null, null, ignoreSelection, currentPage == false? false : format != 'xmlpng'); var range = null; var allPages = null; var embed = (includeXml) ? '1' : '0'; if (format == 'pdf' && currentPage == false) { allPages = '1'; } if (format == 'xmlpng') { embed = '1'; format = 'png'; // Finds the current page number if (this.pages != null && this.currentPage != null) { for (var i = 0; i < this.pages.length; i++) { if (this.pages[i] == this.currentPage) { range = i; break; } } } } var bg = graph.background; if (format == 'png' && transparent) { bg = mxConstants.NONE; } else if (!transparent && (bg == null || bg == mxConstants.NONE)) { bg = '#ffffff'; } var extras = {globalVars: graph.getExportVariables()}; if (grid) { extras.grid = { size: graph.gridSize, steps: graph.view.gridSteps, color: graph.view.gridColor }; } return new mxElectronRequest('export', { format: format, xml: data, from: range, bg: (bg != null) ? bg : mxConstants.NONE, filename: (filename != null) ? filename : null, allPages: allPages, base64: base64, embedXml: embed, extras: encodeURIComponent(JSON.stringify(extras)), scale: scale, border: border }); }; //Export Dialog Pdf case var origExportFile = ExportDialog.exportFile; ExportDialog.exportFile = function(editorUi, name, format, bg, s, b, dpi) { var graph = editorUi.editor.graph; if (format == 'xml' || format == 'svg') { return origExportFile.apply(this, arguments); } else { var data = editorUi.getFileData(true, null, null, null, null, true); var bounds = graph.getGraphBounds(); var w = Math.floor(bounds.width * s / graph.view.scale); var h = Math.floor(bounds.height * s / graph.view.scale); editorUi.hideDialog(); if ((format == 'png' || format == 'jpg' || format == 'jpeg') && editorUi.isExportToCanvas()) { if (format == 'png') { editorUi.exportImage(s, bg == null || bg == 'none', true, false, false, b, true, false, null, null, dpi); } else { editorUi.exportImage(s, false, true, false, false, b, true, false, 'jpeg'); } } else { var extras = {globalVars: graph.getExportVariables()}; editorUi.saveRequest(name, format, function(newTitle, base64) { return new mxElectronRequest('export', { format: format, xml: data, bg: (bg != null) ? bg : mxConstants.NONE, filename: (newTitle != null) ? newTitle : null, w: w, h: h, border: b, base64: (base64 || '0'), extras: JSON.stringify(extras), dpi: dpi > 0? dpi : null }); }); } } }; EditorUi.prototype.saveData = function(filename, format, data, mimeType, base64Encoded) { var remote = require('@electron/remote'); var dialog = remote.dialog; var resume = (this.spinner != null && this.spinner.pause != null) ? this.spinner.pause() : function() {}; const sysPath = require('path') var lastDir = localStorage.getItem('.lastExpDir'); // Spinner.stop is asynchronous so we must invoke save dialog asynchronously // to give the spinner some time to stop spinning window.setTimeout(mxUtils.bind(this, function() { var dlgConfig = {defaultPath: (lastDir || getDocumentsFolder()) + '/' + filename}; var filters = null; switch (format) { case 'xmlpng': case 'png': filters = [ { name: 'PNG Images', extensions: ['png'] } ]; break; case 'jpg': case 'jpeg': filters = [ { name: 'JPEG Images', extensions: ['jpg', 'jpeg'] } ]; break; case 'svg': filters = [ { name: 'SVG Images', extensions: ['svg'] } ]; break; case 'pdf': filters = [ { name: 'PDF Documents', extensions: ['pdf'] } ]; break; case 'vsdx': filters = [ { name: 'VSDX Documents', extensions: ['vsdx'] } ]; break; case 'html': filters = [ { name: 'HTML Documents', extensions: ['html'] } ]; break; case 'xml': filters = [ { name: 'XML Documents', extensions: ['xml'] } ]; break; }; dlgConfig['filters'] = filters; var path = dialog.showSaveDialogSync(dlgConfig); if (path != null) { localStorage.setItem('.lastExpDir', sysPath.dirname(path)); if (data == null || data.length == 0) { this.handleError({message: mxResources.get('errorSavingFile')}); } else { var fs = require('fs'); resume(); var fileObject = new Object(); fileObject.path = path; fileObject.name = path.replace(/^.*[\\\/]/, ''); fileObject.type = (base64Encoded) ? 'base64' : 'utf-8'; fs.writeFile(fileObject.path, data, fileObject.type, mxUtils.bind(this, function (e) { this.spinner.stop(); if (e) { this.handleError({message: mxResources.get('errorSavingFile')}); } })); } } }), 50); }; EditorUi.prototype.addBeforeUnloadListener = function() {}; EditorUi.prototype.loadDesktopLib = function(libPath, success, error) { this.readGraphFile(mxUtils.bind(this, function(fileEntry, data, stat) { var library = new DesktopLibrary(this, data, fileEntry); this.loadLibrary(library); success(library); }), error, libPath); }; })();