ElectronApp.js 29 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184
  1. window.OPEN_URL = 'https://www.draw.io/open';
  2. window.TEMPLATE_PATH = 'templates';
  3. window.DRAW_MATH_URL = 'mathjax/src/main/webapp/current';
  4. FeedbackDialog.feedbackUrl = 'https://log.draw.io/email';
  5. (function()
  6. {
  7. // Overrides default mode
  8. App.mode = App.MODE_DEVICE;
  9. // Disables new window option in edit diagram dialog
  10. EditDiagramDialog.showNewWindowOption = false;
  11. // Redirects printing to iframe to avoid document.write
  12. var printDialogCreatePrintPreview = PrintDialog.createPrintPreview;
  13. PrintDialog.createPrintPreview = function()
  14. {
  15. var iframe = document.createElement('iframe');
  16. document.body.appendChild(iframe);
  17. var result = printDialogCreatePrintPreview.apply(this, arguments);
  18. result.wnd = iframe.contentWindow;
  19. result.iframe = iframe;
  20. // Workaround for lost gradients in print output
  21. result.previousGetBaseUrl = mxSvgCanvas2D.prototype.getBaseUrl;
  22. mxSvgCanvas2D.prototype.getBaseUrl = function()
  23. {
  24. return '';
  25. };
  26. return result;
  27. };
  28. var oldWindowOpen = window.open;
  29. window.open = function(url)
  30. {
  31. if (url != null && url.startsWith('http'))
  32. {
  33. const {shell} = require('electron');
  34. shell.openExternal(url);
  35. }
  36. else
  37. {
  38. return oldWindowOpen(url);
  39. }
  40. }
  41. mxPrintPreview.prototype.addPageBreak = function(doc)
  42. {
  43. // Do nothing
  44. };
  45. mxPrintPreview.prototype.closeDocument = function()
  46. {
  47. var doc = this.wnd.document;
  48. // Removes all event handlers in the print output
  49. mxEvent.release(doc.body);
  50. };
  51. PrintDialog.printPreview = function(preview)
  52. {
  53. if (preview.iframe != null)
  54. {
  55. preview.iframe.contentWindow.print();
  56. preview.iframe.parentNode.removeChild(preview.iframe);
  57. mxSvgCanvas2D.prototype.getBaseUrl = preview.previousGetBaseUrl;
  58. preview.iframe = null;
  59. }
  60. };
  61. PrintDialog.previewEnabled = false;
  62. var menusInit = Menus.prototype.init;
  63. Menus.prototype.init = function()
  64. {
  65. menusInit.apply(this, arguments);
  66. var editorUi = this.editorUi;
  67. editorUi.actions.put('useOffline', new Action(mxResources.get('useOffline') + '...', function()
  68. {
  69. editorUi.openLink('https://www.draw.io/')
  70. }));
  71. // Replaces file menu to replace openFrom menu with open and rename downloadAs to export
  72. this.put('file', new Menu(mxUtils.bind(this, function(menu, parent)
  73. {
  74. this.addMenuItems(menu, ['new', 'open', '-', 'synchronize', '-', 'save', 'saveAs', '-', 'import'], parent);
  75. this.addSubmenu('exportAs', menu, parent);
  76. menu.addSeparator(parent);
  77. this.addSubmenu('embed', menu, parent);
  78. menu.addSeparator(parent);
  79. this.addMenuItems(menu, ['newLibrary', 'openLibrary', '-', 'pageSetup',
  80. 'print', '-', 'close'], parent);
  81. // LATER: Find API for application.quit
  82. })));
  83. };
  84. var graphCreateLinkForHint = Graph.prototype.createLinkForHint;
  85. Graph.prototype.createLinkForHint = function(href, label)
  86. {
  87. var a = graphCreateLinkForHint.call(this, href, label);
  88. if (href != null && !this.isCustomLink(href))
  89. {
  90. // KNOWN: Event with gesture handler mouseUp the middle click opens a framed window
  91. mxEvent.addListener(a, 'click', mxUtils.bind(this, function(evt)
  92. {
  93. this.openLink(a.getAttribute('href'), a.getAttribute('target'));
  94. mxEvent.consume(evt);
  95. }));
  96. }
  97. return a;
  98. };
  99. Graph.prototype.openLink = function(url, target)
  100. {
  101. require('electron').shell.openExternal(url);
  102. };
  103. // Initializes the user interface
  104. var editorUiInit = EditorUi.prototype.init;
  105. EditorUi.prototype.init = function()
  106. {
  107. editorUiInit.apply(this, arguments);
  108. var editorUi = this;
  109. var graph = this.editor.graph;
  110. global.__emt_isModified =
  111. e => {
  112. if (this.getCurrentFile())
  113. {
  114. return this.getCurrentFile().isModified()
  115. }
  116. return false
  117. }
  118. // global.__emt_getCurrentFile = e => {
  119. // return this.getCurrentFile()
  120. // }
  121. // Adds support for libraries
  122. this.actions.addAction('newLibrary...', mxUtils.bind(this, function()
  123. {
  124. editorUi.showLibraryDialog(null, null, null, null, App.MODE_DEVICE);
  125. }));
  126. this.actions.addAction('openLibrary...', mxUtils.bind(this, function()
  127. {
  128. editorUi.pickLibrary(App.MODE_DEVICE);
  129. }));
  130. // Replaces import action
  131. this.actions.addAction('import...', mxUtils.bind(this, function()
  132. {
  133. if (editorUi.getCurrentFile() != null)
  134. {
  135. const electron = require('electron');
  136. var remote = electron.remote;
  137. var dialog = remote.dialog;
  138. var paths = dialog.showOpenDialog({properties: ['openFile']});
  139. if (paths !== undefined && paths[0] != null)
  140. {
  141. var path = paths[0];
  142. var asImage = /\.png$/i.test(path) || /\.gif$/i.test(path) || /\.jpe?g$/i.test(path);
  143. var encoding = (asImage || /\.vsdx$/i.test(path) || /\.vssx$/i.test(path)) ?
  144. 'base64' : 'utf-8';
  145. if (editorUi.spinner.spin(document.body, mxResources.get('loading')))
  146. {
  147. var fs = require('fs');
  148. fs.readFile(path, encoding, mxUtils.bind(this, function (e, data)
  149. {
  150. if (e)
  151. {
  152. editorUi.spinner.stop();
  153. editorUi.handleError(e);
  154. }
  155. else
  156. {
  157. try
  158. {
  159. if (editorUi.isLucidChartData(data))
  160. {
  161. editorUi.convertLucidChart(data, function(xml)
  162. {
  163. editorUi.spinner.stop();
  164. graph.setSelectionCells(editorUi.importXml(xml));
  165. }, function(e)
  166. {
  167. editorUi.spinner.stop();
  168. editorUi.handleError(e);
  169. });
  170. }
  171. else if (/(\.vsdx)($|\?)/i.test(path))
  172. {
  173. editorUi.importVisio(editorUi.base64ToBlob(data, 'application/octet-stream'), function(xml)
  174. {
  175. editorUi.spinner.stop();
  176. graph.setSelectionCells(editorUi.importXml(xml));
  177. });
  178. }
  179. else if (!editorUi.isOffline() && new XMLHttpRequest().upload && editorUi.isRemoteFileFormat(data, path))
  180. {
  181. // Asynchronous parsing via server
  182. editorUi.parseFile(new Blob([data], {type : 'application/octet-stream'}), mxUtils.bind(this, function(xhr)
  183. {
  184. if (xhr.readyState == 4)
  185. {
  186. editorUi.spinner.stop();
  187. if (xhr.status >= 200 && xhr.status <= 299)
  188. {
  189. graph.setSelectionCells(editorUi.importXml(xhr.responseText));
  190. }
  191. }
  192. }), path);
  193. }
  194. else
  195. {
  196. if (/\.png$/i.test(path))
  197. {
  198. var tmp = editorUi.extractGraphModelFromPng(data);
  199. if (tmp != null)
  200. {
  201. asImage = false;
  202. data = tmp;
  203. }
  204. }
  205. else if (/\.svg$/i.test(path))
  206. {
  207. // LATER: Use importXml without throwing exception if no data
  208. // Checks if SVG contains content attribute
  209. var root = mxUtils.parseXml(data);
  210. var svgs = root.getElementsByTagName('svg');
  211. if (svgs.length > 0)
  212. {
  213. var svgRoot = svgs[0];
  214. var cont = svgRoot.getAttribute('content');
  215. if (cont != null && cont.charAt(0) != '<' && cont.charAt(0) != '%')
  216. {
  217. cont = unescape((window.atob) ? atob(cont) : Base64.decode(cont, true));
  218. }
  219. if (cont != null && cont.charAt(0) == '%')
  220. {
  221. cont = decodeURIComponent(cont);
  222. }
  223. if (cont != null && (cont.substring(0, 8) === '<mxfile ' ||
  224. cont.substring(0, 14) === '<mxGraphModel '))
  225. {
  226. asImage = false;
  227. data = cont;
  228. }
  229. else
  230. {
  231. asImage = true;
  232. data = btoa(data);
  233. }
  234. }
  235. }
  236. if (asImage)
  237. {
  238. var img = new Image();
  239. img.onload = function()
  240. {
  241. editorUi.resizeImage(img, img.src, function(data2, w, h)
  242. {
  243. editorUi.spinner.stop();
  244. var pt = graph.getInsertPoint();
  245. graph.setSelectionCell(graph.insertVertex(null, null, '', pt.x, pt.y, w, h,
  246. 'shape=image;aspect=fixed;image=' + editorUi.convertDataUri(data2) + ';'));
  247. }, true);
  248. };
  249. img.onerror = function(e)
  250. {
  251. editorUi.spinner.stop();
  252. editorUi.handleError();
  253. };
  254. var format = path.substring(path.lastIndexOf('.') + 1);
  255. if (format == 'svg')
  256. {
  257. format = 'svg+xml';
  258. }
  259. img.src = 'data:image/' + format + ';base64,' + data;
  260. }
  261. else
  262. {
  263. editorUi.spinner.stop();
  264. if (data != null)
  265. {
  266. graph.setSelectionCells(editorUi.importXml(data));
  267. }
  268. }
  269. }
  270. }
  271. catch(e)
  272. {
  273. editorUi.spinner.stop();
  274. editorUi.handleError(e);
  275. }
  276. }
  277. }));
  278. }
  279. }
  280. }
  281. }));
  282. // Replaces new action
  283. var oldNew = this.actions.get('new').funct;
  284. this.actions.addAction('new...', mxUtils.bind(this, function()
  285. {
  286. if (this.getCurrentFile() == null)
  287. {
  288. oldNew();
  289. }
  290. else
  291. {
  292. const ipc = require('electron').ipcRenderer
  293. ipc.sendSync('winman', {action: 'newfile', opt: {width: 1600}})
  294. }
  295. }), null, null, Editor.ctrlKey + '+N');
  296. this.actions.get('open').shortcut = Editor.ctrlKey + '+O';
  297. // Adds shortcut keys for file operations
  298. editorUi.keyHandler.bindAction(78, true, 'new'); // Ctrl+N
  299. editorUi.keyHandler.bindAction(79, true, 'open'); // Ctrl+O
  300. }
  301. var appLoad = App.prototype.load;
  302. App.prototype.load = function()
  303. {
  304. appLoad.apply(this, arguments);
  305. const {ipcRenderer} = require('electron');
  306. ipcRenderer.on('args-obj', (event, argsObj) =>
  307. {
  308. this.loadArgs(argsObj)
  309. })
  310. }
  311. App.prototype.loadArgs = function(argsObj)
  312. {
  313. var paths = argsObj.args;
  314. // If a file is passed
  315. if (paths !== undefined && paths[0] != null)
  316. {
  317. var path = paths[0];
  318. var success = mxUtils.bind(this, function(fileEntry, data, stat)
  319. {
  320. var file = new LocalFile(this, data, '');
  321. file.fileObject = fileEntry;
  322. file.stat = stat;
  323. this.fileLoaded(file);
  324. });
  325. var error = mxUtils.bind(this, function(e)
  326. {
  327. if (e.code === 'ENOENT')
  328. {
  329. var title = path.replace(/^.*[\\\/]/, '');
  330. var data = this.emptyDiagramXml;
  331. var file = new LocalFile(this, data, title, null);
  332. file.fileObject = new Object();
  333. file.fileObject.path = path;
  334. file.fileObject.name = title;
  335. file.fileObject.type = 'utf-8';
  336. this.fileCreated(file, null, null, null);
  337. this.saveFile();
  338. }
  339. else
  340. {
  341. this.handleError(e);
  342. }
  343. });
  344. // Tries to open the file
  345. this.readGraphFile(success, error, path);
  346. }
  347. // If no file is passed, but there is the "create-if-not-exists" flag
  348. else if (argsObj.create != null)
  349. {
  350. var title = 'Untitled document';
  351. var data = this.emptyDiagramXml;
  352. var file = new LocalFile(this, data, title, null);
  353. this.fileCreated(file, null, null, null);
  354. }
  355. }
  356. // Uses local picker
  357. App.prototype.pickFile = function()
  358. {
  359. var doPickFile = mxUtils.bind(this, function()
  360. {
  361. this.chooseFileEntry(mxUtils.bind(this, function(fileEntry, data, stat)
  362. {
  363. var file = new LocalFile(this, data, '');
  364. file.fileObject = fileEntry;
  365. file.stat = stat;
  366. this.fileLoaded(file);
  367. }));
  368. });
  369. var file = this.getCurrentFile();
  370. if (file != null && file.isModified())
  371. {
  372. this.confirm(mxResources.get('allChangesLost'), null, doPickFile,
  373. mxResources.get('cancel'), mxResources.get('discardChanges'));
  374. }
  375. else
  376. {
  377. doPickFile();
  378. }
  379. };
  380. /**
  381. * Selects a library to load from a picker
  382. *
  383. * @param mode the device mode, ignored in this case
  384. */
  385. App.prototype.pickLibrary = function(mode)
  386. {
  387. this.chooseFileEntry(mxUtils.bind(this, function(fileEntry, data, stat)
  388. {
  389. try
  390. {
  391. var library = new LocalLibrary(this, data, fileEntry.name);
  392. library.fileObject = fileEntry;
  393. this.loadLibrary(library);
  394. }
  395. catch (e)
  396. {
  397. this.handleError(e, mxResources.get('errorLoadingFile'));
  398. }
  399. }));
  400. };
  401. // Uses local picker
  402. App.prototype.chooseFileEntry = function(fn)
  403. {
  404. const electron = require('electron');
  405. var remote = electron.remote;
  406. var dialog = remote.dialog;
  407. var paths = dialog.showOpenDialog({properties: ['openFile']});
  408. if (paths !== undefined && paths[0] != null)
  409. {
  410. this.readGraphFile(fn, mxUtils.bind(this, function(err)
  411. {
  412. this.handleError(err);
  413. }), paths[0]);
  414. }
  415. else
  416. {
  417. this.spinner.stop();
  418. }
  419. };
  420. App.prototype.readGraphFile = function(fn, fnErr, path)
  421. {
  422. var fs = require('fs');
  423. var index = path.lastIndexOf('.png');
  424. var isPng = index > -1 && index == path.length - 4;
  425. var encoding = isPng ? 'base64' : 'utf-8'
  426. fs.readFile(path, encoding, mxUtils.bind(this, function (e, data)
  427. {
  428. if (e)
  429. {
  430. fnErr(e);
  431. }
  432. else
  433. {
  434. if (isPng)
  435. {
  436. // Detecting png by extension. Would need https://github.com/mscdex/mmmagic
  437. // to do it by inspection
  438. data = this.extractGraphModelFromPng('data:image/png;base64,' + data);
  439. }
  440. var fileEntry = new Object();
  441. fileEntry.path = path;
  442. fileEntry.name = path.replace(/^.*[\\\/]/, '');
  443. fileEntry.type = encoding;
  444. fs.stat(path, function(err, stat)
  445. {
  446. if (err)
  447. {
  448. fnErr(err);
  449. }
  450. else
  451. {
  452. fn(fileEntry, data, stat);
  453. }
  454. });
  455. }
  456. }));
  457. };
  458. // Disables temp files in Electron
  459. var LocalFileCtor = LocalFile;
  460. LocalFile = function(ui, data, title, temp)
  461. {
  462. LocalFileCtor.call(this, ui, data, title, false);
  463. };
  464. mxUtils.extend(LocalFile, LocalFileCtor);
  465. LocalFile.prototype.getLatestVersion = function(success, error)
  466. {
  467. if (this.fileObject == null)
  468. {
  469. if (error != null)
  470. {
  471. error({message: mxResources.get('fileNotFound')});
  472. }
  473. }
  474. else
  475. {
  476. this.ui.readGraphFile(mxUtils.bind(this, function(fileEntry, data, stat)
  477. {
  478. var file = new LocalFile(this, data, '');
  479. file.stat = stat;
  480. success(file);
  481. }), error, this.fileObject.path);
  482. }
  483. };
  484. // Call save as for copy
  485. LocalFile.prototype.copyFile = function(success, error)
  486. {
  487. this.saveAs(this.ui.getCopyFilename(this), success, error);
  488. };
  489. /**
  490. * Adds all listeners.
  491. */
  492. LocalFile.prototype.getDescriptor = function()
  493. {
  494. return this.stat;
  495. };
  496. /**
  497. * Updates the descriptor of this file with the one from the given file.
  498. */
  499. LocalFile.prototype.setDescriptor = function(stat)
  500. {
  501. this.stat = stat;
  502. };
  503. LocalFile.prototype.reloadFile = function(success)
  504. {
  505. if (this.fileObject == null)
  506. {
  507. this.ui.handleError({message: mxResources.get('fileNotFound')});
  508. }
  509. else
  510. {
  511. this.ui.spinner.stop();
  512. var fn = mxUtils.bind(this, function()
  513. {
  514. this.setModified(false);
  515. var page = this.ui.currentPage;
  516. var viewState = this.ui.editor.graph.getViewState();
  517. var selection = this.ui.editor.graph.getSelectionCells();
  518. if (this.ui.spinner.spin(document.body, mxResources.get('loading')))
  519. {
  520. this.ui.readGraphFile(mxUtils.bind(this, function(fileEntry, data, stat)
  521. {
  522. this.ui.spinner.stop();
  523. var file = new LocalFile(this.ui, data, '');
  524. file.fileObject = fileEntry;
  525. file.stat = stat;
  526. this.ui.fileLoaded(file);
  527. this.ui.restoreViewState(page, viewState, selection);
  528. if (this.backupPatch != null)
  529. {
  530. this.patch([this.backupPatch]);
  531. }
  532. if (success != null)
  533. {
  534. success();
  535. }
  536. }), mxUtils.bind(this, function(err)
  537. {
  538. this.handleFileError(err);
  539. }), this.fileObject.path);
  540. }
  541. });
  542. if (this.isModified() && this.backupPatch == null)
  543. {
  544. this.ui.confirm(mxResources.get('allChangesLost'), mxUtils.bind(this, function()
  545. {
  546. this.handleFileSuccess(DrawioFile.SYNC == 'manual');
  547. }), fn, mxResources.get('cancel'), mxResources.get('discardChanges'));
  548. }
  549. else
  550. {
  551. fn();
  552. }
  553. }
  554. };
  555. LocalFile.prototype.isAutosave = function()
  556. {
  557. return this.fileObject != null && DrawioFile.prototype.isAutosave.apply(this, arguments);
  558. };
  559. LocalFile.prototype.isAutosaveOptional = function()
  560. {
  561. return this.fileObject != null;
  562. };
  563. LocalLibrary.prototype.isAutosave = function()
  564. {
  565. return this.fileObject != null;
  566. };
  567. LocalFile.prototype.getTitle = function()
  568. {
  569. return (this.fileObject != null) ? this.fileObject.name : this.title;
  570. };
  571. LocalFile.prototype.isRenamable = function()
  572. {
  573. return false;
  574. };
  575. // Restores default implementation of open with autosave
  576. LocalFile.prototype.open = DrawioFile.prototype.open;
  577. LocalFile.prototype.save = function(revision, success, error, unloading, overwrite)
  578. {
  579. DrawioFile.prototype.save.apply(this, arguments);
  580. this.saveFile(revision, success, error, unloading, overwrite);
  581. };
  582. LocalLibrary.prototype.save = function(revision, success, error)
  583. {
  584. LocalFile.prototype.saveFile.apply(this, arguments);
  585. };
  586. LocalFile.prototype.isConflict = function(stat)
  587. {
  588. return stat != null && this.stat != null && stat.mtimeMs != this.stat.mtimeMs;
  589. };
  590. LocalFile.prototype.saveFile = function(revision, success, error, unloading, overwrite)
  591. {
  592. if (!this.savingFile)
  593. {
  594. var fn = mxUtils.bind(this, function()
  595. {
  596. var doSave = mxUtils.bind(this, function(data, enc)
  597. {
  598. var savedData = this.data;
  599. // Makes sure no changes get lost while the file is saved
  600. var prevModified = this.isModified;
  601. var modified = this.isModified();
  602. this.setModified(false);
  603. this.savingFile = true;
  604. var fs = require('fs');
  605. var errorWrapper = mxUtils.bind(this, function()
  606. {
  607. this.savingFile = false;
  608. this.isModified = prevModified;
  609. this.setModified(modified || this.isModified());
  610. if (error != null)
  611. {
  612. error();
  613. }
  614. });
  615. var writeFile = mxUtils.bind(this, function()
  616. {
  617. if (data == null || data.length == 0)
  618. {
  619. this.ui.handleError({message: mxResources.get('errorSavingFile')});
  620. errorWrapper();
  621. }
  622. else
  623. {
  624. fs.writeFile(this.fileObject.path, data, enc || this.fileObject.encoding,
  625. mxUtils.bind(this, function (e)
  626. {
  627. if (e)
  628. {
  629. errorWrapper();
  630. }
  631. else
  632. {
  633. fs.stat(this.fileObject.path, mxUtils.bind(this, function(e2, stat2)
  634. {
  635. if (e2)
  636. {
  637. errorWrapper();
  638. }
  639. else
  640. {
  641. this.savingFile = false;
  642. this.isModified = prevModified;
  643. var lastDesc = this.stat;
  644. this.stat = stat2;
  645. this.fileSaved(savedData, lastDesc, mxUtils.bind(this, function()
  646. {
  647. this.contentChanged();
  648. if (success != null)
  649. {
  650. success();
  651. }
  652. }), error);
  653. }
  654. }));
  655. }
  656. }));
  657. }
  658. });
  659. if (overwrite)
  660. {
  661. writeFile();
  662. }
  663. else
  664. {
  665. fs.stat(this.fileObject.path, mxUtils.bind(this, function(err, stat)
  666. {
  667. if (this.isConflict(stat))
  668. {
  669. this.inConflictState = true;
  670. errorWrapper();
  671. }
  672. else if (err != null && err.code !== 'ENOENT')
  673. {
  674. errorWrapper();
  675. }
  676. else
  677. {
  678. writeFile();
  679. }
  680. }));
  681. }
  682. });
  683. if (!/(\.png)$/i.test(this.fileObject.name))
  684. {
  685. doSave(this.getData());
  686. }
  687. else
  688. {
  689. this.ui.getEmbeddedPng(function(data)
  690. {
  691. doSave(atob(data), 'binary');
  692. }, error);
  693. }
  694. });
  695. if (this.fileObject == null)
  696. {
  697. const electron = require('electron');
  698. var remote = electron.remote;
  699. var dialog = remote.dialog;
  700. var path = dialog.showSaveDialog({defaultPath: this.title});
  701. if (path != null)
  702. {
  703. this.fileObject = new Object();
  704. this.fileObject.path = path;
  705. this.fileObject.name = path.replace(/^.*[\\\/]/, '');
  706. this.fileObject.type = 'utf-8';
  707. fn();
  708. }
  709. else
  710. {
  711. this.ui.spinner.stop();
  712. }
  713. }
  714. else
  715. {
  716. fn();
  717. }
  718. }
  719. };
  720. LocalFile.prototype.saveAs = function(title, success, error)
  721. {
  722. const electron = require('electron');
  723. var remote = electron.remote;
  724. var dialog = remote.dialog;
  725. var filename = this.title;
  726. // Adds default extension
  727. if (filename.length > 0 && (!/(\.xml)$/i.test(filename) && !/(\.html)$/i.test(filename) &&
  728. !/(\.svg)$/i.test(filename) && !/(\.png)$/i.test(filename) && !/(\.drawio)$/i.test(filename)))
  729. {
  730. filename += '.drawio';
  731. }
  732. var path = dialog.showSaveDialog({defaultPath: filename});
  733. if (path != null)
  734. {
  735. this.fileObject = new Object();
  736. this.fileObject.path = path;
  737. this.fileObject.name = path.replace(/^.*[\\\/]/, '');
  738. this.fileObject.type = 'utf-8';
  739. this.save(false, success, error, null, true);
  740. }
  741. };
  742. App.prototype.saveFile = function(forceDialog)
  743. {
  744. var file = this.getCurrentFile();
  745. if (file != null)
  746. {
  747. if (!forceDialog && file.getTitle() != null)
  748. {
  749. file.save(true, mxUtils.bind(this, function()
  750. {
  751. file.handleFileSuccess(true);
  752. }), mxUtils.bind(this, function(err)
  753. {
  754. file.handleFileError(err, true);
  755. }));
  756. }
  757. else
  758. {
  759. file.saveAs(null, mxUtils.bind(this, function()
  760. {
  761. file.handleFileSuccess(true);
  762. }), mxUtils.bind(this, function(err)
  763. {
  764. file.handleFileError(err, true);
  765. }));
  766. }
  767. }
  768. };
  769. /**
  770. * Translates this point by the given vector.
  771. *
  772. * @param {number} dx X-coordinate of the translation.
  773. * @param {number} dy Y-coordinate of the translation.
  774. */
  775. App.prototype.saveLibrary = function(name, images, file, mode, noSpin, noReload, fn)
  776. {
  777. mode = (mode != null) ? mode : this.mode;
  778. noSpin = (noSpin != null) ? noSpin : false;
  779. noReload = (noReload != null) ? noReload : false;
  780. var xml = this.createLibraryDataFromImages(images);
  781. var error = mxUtils.bind(this, function(resp)
  782. {
  783. this.spinner.stop();
  784. if (fn != null)
  785. {
  786. fn();
  787. }
  788. // Null means cancel by user and is ignored
  789. if (resp != null)
  790. {
  791. this.handleError(resp, mxResources.get('errorSavingFile'));
  792. }
  793. });
  794. // Handles special case for local libraries
  795. if (file == null)
  796. {
  797. file = new LocalLibrary(this, xml, name);
  798. }
  799. if (noSpin || this.spinner.spin(document.body, mxResources.get('saving')))
  800. {
  801. file.setData(xml);
  802. var doSave = mxUtils.bind(this, function()
  803. {
  804. file.save(true, mxUtils.bind(this, function(resp)
  805. {
  806. this.spinner.stop();
  807. this.hideDialog(true);
  808. if (!noReload)
  809. {
  810. this.libraryLoaded(file, images)
  811. }
  812. if (fn != null)
  813. {
  814. fn();
  815. }
  816. }), error);
  817. });
  818. if (name != file.getTitle())
  819. {
  820. var oldHash = file.getHash();
  821. file.rename(name, mxUtils.bind(this, function(resp)
  822. {
  823. // Change hash in stored settings
  824. if (file.constructor != LocalLibrary && oldHash != file.getHash())
  825. {
  826. mxSettings.removeCustomLibrary(oldHash);
  827. mxSettings.addCustomLibrary(file.getHash());
  828. }
  829. // Workaround for library files changing hash so
  830. // the old library cannot be removed from the
  831. // sidebar using the updated file in libraryLoaded
  832. this.removeLibrarySidebar(oldHash);
  833. doSave();
  834. }), error)
  835. }
  836. else
  837. {
  838. doSave();
  839. }
  840. }
  841. };
  842. /**
  843. * Updates action states depending on the selection.
  844. */
  845. var editorUiUpdateActionStates = EditorUi.prototype.updateActionStates;
  846. EditorUi.prototype.updateActionStates = function()
  847. {
  848. editorUiUpdateActionStates.apply(this, arguments);
  849. var file = this.getCurrentFile();
  850. var syncEnabled = file != null && file.fileObject != null;
  851. this.actions.get('synchronize').setEnabled(syncEnabled);
  852. };
  853. EditorUi.prototype.saveLocalFile = function(data, filename, mimeType, base64Encoded, format, allowBrowser)
  854. {
  855. this.saveData(filename, format, data, mimeType, base64Encoded);
  856. };
  857. EditorUi.prototype.saveRequest = function(filename, format, fn, data, base64Encoded, mimeType)
  858. {
  859. var xhr = fn(null, '1');
  860. if (xhr != null && this.spinner.spin(document.body, mxResources.get('saving')))
  861. {
  862. xhr.send(mxUtils.bind(this, function()
  863. {
  864. this.spinner.stop();
  865. if (xhr.getStatus() >= 200 && xhr.getStatus() <= 299)
  866. {
  867. this.saveData(filename, format, xhr.getText(), mimeType, true);
  868. }
  869. else
  870. {
  871. this.handleError({message: mxResources.get('errorSavingFile')});
  872. }
  873. }), function(resp)
  874. {
  875. this.spinner.stop();
  876. this.handleError(resp);
  877. });
  878. }
  879. };
  880. function mxElectronRequest(reqType, reqObj)
  881. {
  882. this.reqType = reqType;
  883. this.reqObj = reqObj;
  884. };
  885. //Extends mxXmlRequest
  886. mxUtils.extend(mxElectronRequest, mxXmlRequest);
  887. mxElectronRequest.prototype.send = function(callback, error)
  888. {
  889. const ipcRenderer = require('electron').ipcRenderer;
  890. ipcRenderer.send(this.reqType, this.reqObj);
  891. ipcRenderer.once(this.reqType + '-success', (event, data) =>
  892. {
  893. this.response = data;
  894. callback();
  895. })
  896. ipcRenderer.once(this.reqType + '-error', (event, err) =>
  897. {
  898. this.hasError = true;
  899. error(err);
  900. })
  901. };
  902. mxElectronRequest.prototype.getStatus = function()
  903. {
  904. return this.hasError? 500 : 200;
  905. }
  906. mxElectronRequest.prototype.getText = function()
  907. {
  908. return this.response;
  909. }
  910. if (mxIsElectron5)
  911. {
  912. //Direct export to pdf
  913. var origCreateDownloadRequest = EditorUi.prototype.createDownloadRequest;
  914. EditorUi.prototype.createDownloadRequest = function(filename, format, ignoreSelection, base64, transparent, currentPage)
  915. {
  916. if (format == 'pdf')
  917. {
  918. var bounds = this.editor.graph.getGraphBounds();
  919. // Exports only current page for images that does not contain file data, but for
  920. // the other formats with XML included or pdf with all pages, we need to send the complete data and use
  921. // the from/to URL parameters to specify the page to be exported.
  922. var data = this.getFileData(true, null, null, null, ignoreSelection, currentPage == false? false : format != 'xmlpng');
  923. var allPages = null;
  924. if (bounds.width * bounds.height > MAX_AREA || data.length > MAX_REQUEST_SIZE)
  925. {
  926. throw {message: mxResources.get('drawingTooLarge')};
  927. }
  928. if (currentPage == false)
  929. {
  930. allPages = '1';
  931. }
  932. var bg = this.editor.graph.background;
  933. return new mxElectronRequest('pdf-export', {
  934. xml: data,
  935. bg: (bg != null) ? bg : mxConstants.NONE,
  936. filename: (filename != null) ? filename : null,
  937. allPages: allPages
  938. });
  939. }
  940. else
  941. {
  942. return origCreateDownloadRequest.apply(this, arguments);
  943. }
  944. };
  945. //Export Dialog Pdf case
  946. var origExportFile = ExportDialog.exportFile;
  947. ExportDialog.exportFile = function(editorUi, name, format, bg, s, b)
  948. {
  949. var graph = editorUi.editor.graph;
  950. if (format == 'pdf')
  951. {
  952. var data = editorUi.getFileData(true, null, null, null, null, true);
  953. var bounds = graph.getGraphBounds();
  954. var w = Math.floor(bounds.width * s / graph.view.scale);
  955. var h = Math.floor(bounds.height * s / graph.view.scale);
  956. if (data.length <= MAX_REQUEST_SIZE && w * h < MAX_AREA)
  957. {
  958. editorUi.hideDialog();
  959. editorUi.saveRequest(name, format,
  960. function(newTitle, base64)
  961. {
  962. return new mxElectronRequest('pdf-export', {
  963. xml: data,
  964. bg: (bg != null) ? bg : mxConstants.NONE,
  965. filename: (newTitle != null) ? newTitle : null,
  966. w: w,
  967. h: h,
  968. border: b
  969. });
  970. });
  971. }
  972. else
  973. {
  974. mxUtils.alert(mxResources.get('drawingTooLarge'));
  975. }
  976. }
  977. else
  978. {
  979. return origExportFile.apply(this, arguments);
  980. }
  981. };
  982. }
  983. EditorUi.prototype.saveData = function(filename, format, data, mimeType, base64Encoded)
  984. {
  985. const electron = require('electron');
  986. var remote = electron.remote;
  987. var dialog = remote.dialog;
  988. var resume = (this.spinner != null && this.spinner.pause != null) ? this.spinner.pause() : function() {};
  989. // Spinner.stop is asynchronous so we must invoke save dialog asynchronously
  990. // to give the spinner some time to stop spinning
  991. window.setTimeout(mxUtils.bind(this, function()
  992. {
  993. var path = dialog.showSaveDialog({defaultPath: filename});
  994. if (path != null)
  995. {
  996. if (data == null || data.length == 0)
  997. {
  998. this.handleError({message: mxResources.get('errorSavingFile')});
  999. }
  1000. else
  1001. {
  1002. var fs = require('fs');
  1003. resume();
  1004. var fileObject = new Object();
  1005. fileObject.path = path;
  1006. fileObject.name = path.replace(/^.*[\\\/]/, '');
  1007. fileObject.type = (base64Encoded) ? 'base64' : 'utf-8';
  1008. fs.writeFile(fileObject.path, data, fileObject.type, mxUtils.bind(this, function (e)
  1009. {
  1010. this.spinner.stop();
  1011. if (e)
  1012. {
  1013. this.handleError({message: mxResources.get('errorSavingFile')});
  1014. }
  1015. }));
  1016. }
  1017. }
  1018. }), 0);
  1019. };
  1020. EditorUi.prototype.addBeforeUnloadListener = function() {};
  1021. })();