ElectronApp.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736
  1. window.OPEN_URL = 'https://www.draw.io/open';
  2. window.TEMPLATE_PATH = 'templates';
  3. FeedbackDialog.feedbackUrl = 'https://log.draw.io/email';
  4. (function()
  5. {
  6. // Overrides default mode
  7. App.mode = App.MODE_DEVICE;
  8. // Disables new window option in edit diagram dialog
  9. EditDiagramDialog.showNewWindowOption = false;
  10. // Redirects printing to iframe to avoid document.write
  11. var printDialogCreatePrintPreview = PrintDialog.createPrintPreview;
  12. PrintDialog.createPrintPreview = function()
  13. {
  14. var iframe = document.createElement('iframe');
  15. document.body.appendChild(iframe);
  16. var result = printDialogCreatePrintPreview.apply(this, arguments);
  17. result.wnd = iframe.contentWindow;
  18. result.iframe = iframe;
  19. // Workaround for lost gradients in print output
  20. result.previousGetBaseUrl = mxSvgCanvas2D.prototype.getBaseUrl;
  21. mxSvgCanvas2D.prototype.getBaseUrl = function()
  22. {
  23. return '';
  24. };
  25. return result;
  26. };
  27. var oldWindowOpen = window.open;
  28. window.open = function(url)
  29. {
  30. if (url != null && url.startsWith('http'))
  31. {
  32. const {shell} = require('electron');
  33. shell.openExternal(url);
  34. }
  35. else
  36. {
  37. return oldWindowOpen(url);
  38. }
  39. }
  40. mxPrintPreview.prototype.addPageBreak = function(doc)
  41. {
  42. // Do nothing
  43. };
  44. mxPrintPreview.prototype.closeDocument = function()
  45. {
  46. var doc = this.wnd.document;
  47. // Removes all event handlers in the print output
  48. mxEvent.release(doc.body);
  49. };
  50. PrintDialog.printPreview = function(preview)
  51. {
  52. if (preview.iframe != null)
  53. {
  54. preview.iframe.contentWindow.print();
  55. preview.iframe.parentNode.removeChild(preview.iframe);
  56. mxSvgCanvas2D.prototype.getBaseUrl = preview.previousGetBaseUrl;
  57. preview.iframe = null;
  58. }
  59. };
  60. PrintDialog.previewEnabled = false;
  61. // Enables PDF export via print
  62. EditorUi.prototype.printPdfExport = true;
  63. var menusInit = Menus.prototype.init;
  64. Menus.prototype.init = function()
  65. {
  66. menusInit.apply(this, arguments);
  67. var editorUi = this.editorUi;
  68. // Replaces file menu to replace openFrom menu with open and rename downloadAs to export
  69. this.put('file', new Menu(mxUtils.bind(this, function(menu, parent)
  70. {
  71. this.addMenuItems(menu, ['new', 'open', '-', 'save', 'saveAs', '-', 'import'], parent);
  72. this.addSubmenu('exportAs', menu, parent);
  73. this.addSubmenu('embed', menu, parent);
  74. this.addMenuItems(menu, ['-', 'newLibrary', 'openLibrary', '-', 'documentProperties', 'print'], parent);
  75. })));
  76. this.put('extras', new Menu(mxUtils.bind(this, function(menu, parent)
  77. {
  78. this.addMenuItems(menu, ['copyConnect', 'collapseExpand', '-', 'mathematicalTypesetting', 'autosave', '-',
  79. 'createShape', 'editDiagram', '-', 'tags', '-', 'online'], parent);
  80. })));
  81. };
  82. // Initializes the user interface
  83. var editorUiInit = EditorUi.prototype.init;
  84. EditorUi.prototype.init = function()
  85. {
  86. editorUiInit.apply(this, arguments);
  87. var editorUi = this;
  88. var graph = this.editor.graph;
  89. this.editor.autosave = false;
  90. global.__emt_isModified = e => {
  91. if (this.getCurrentFile())
  92. return this.getCurrentFile().isModified()
  93. return false
  94. }
  95. // global.__emt_getCurrentFile = e => {
  96. // return this.getCurrentFile()
  97. // }
  98. // Adds support for libraries
  99. this.actions.addAction('newLibrary...', mxUtils.bind(this, function()
  100. {
  101. editorUi.showLibraryDialog(null, null, null, null, App.MODE_DEVICE);
  102. }));
  103. this.actions.addAction('openLibrary...', mxUtils.bind(this, function()
  104. {
  105. editorUi.pickLibrary(App.MODE_DEVICE);
  106. }));
  107. // Replaces import action
  108. this.actions.addAction('import...', mxUtils.bind(this, function()
  109. {
  110. if (editorUi.getCurrentFile() != null)
  111. {
  112. const electron = require('electron');
  113. var remote = electron.remote;
  114. var dialog = remote.dialog;
  115. var paths = dialog.showOpenDialog({properties: ['openFile']});
  116. if (paths !== undefined && paths[0] != null)
  117. {
  118. var fs = require('fs');
  119. var path = paths[0];
  120. var index = path.lastIndexOf('.png');
  121. var isPng = index > -1 && index == path.length - 4;
  122. var encoding = (isPng || /\.gif$/i.test(path) || /\.jpe?g$/i.test(path) ||
  123. /\.vsdx$/i.test(path)) ? 'base64' : 'utf-8'
  124. if (editorUi.spinner.spin(document.body, mxResources.get('loading')))
  125. {
  126. fs.readFile(path, encoding, mxUtils.bind(this, function (e, data)
  127. {
  128. if (e)
  129. {
  130. editorUi.spinner.stop();
  131. editorUi.handleError(e);
  132. }
  133. else
  134. {
  135. try
  136. {
  137. if (isPng)
  138. {
  139. var tmp = editorUi.extractGraphModelFromPng(data);
  140. if (tmp != null)
  141. {
  142. data = tmp;
  143. }
  144. }
  145. if (!editorUi.isOffline() && new XMLHttpRequest().upload && editorUi.isRemoteFileFormat(data, path))
  146. {
  147. // Asynchronous parsing via server
  148. editorUi.parseFile(editorUi.base64ToBlob(data, 'application/octet-stream'), mxUtils.bind(this, function(xhr)
  149. {
  150. if (xhr.readyState == 4)
  151. {
  152. editorUi.spinner.stop();
  153. if (xhr.status >= 200 && xhr.status <= 299)
  154. {
  155. editorUi.editor.graph.setSelectionCells(editorUi.insertTextAt(xhr.responseText, 0, 0, true));
  156. }
  157. }
  158. }), path);
  159. }
  160. else if (isPng || /\.gif$/i.test(path) || /\.jpe?g$/i.test(path))
  161. {
  162. var img = new Image();
  163. img.onload = function()
  164. {
  165. editorUi.resizeImage(img, img.src, function(data2, w, h)
  166. {
  167. editorUi.spinner.stop();
  168. var pt = graph.getInsertPoint();
  169. graph.setSelectionCell(graph.insertVertex(null, null, '', pt.x, pt.y, w, h,
  170. 'shape=image;aspect=fixed;image=' + editorUi.convertDataUri(data2) + ';'));
  171. }, true);
  172. };
  173. img.src = 'data:image/png;base64,' + data;
  174. }
  175. else if (data != null)
  176. {
  177. editorUi.spinner.stop();
  178. graph.setSelectionCells(editorUi.importXml(data));
  179. }
  180. }
  181. catch(e)
  182. {
  183. editorUi.spinner.stop();
  184. editorUi.handleError(e);
  185. }
  186. }
  187. }));
  188. }
  189. }
  190. }
  191. }));
  192. // Replaces new action
  193. var oldNew = this.actions.get('new').funct;
  194. this.actions.addAction('new...', mxUtils.bind(this, function()
  195. {
  196. mxLog.debug(this.getCurrentFile());
  197. if (this.getCurrentFile() == null)
  198. {
  199. oldNew();
  200. }
  201. else {
  202. const ipc = require('electron').ipcRenderer
  203. ipc.sendSync('winman', {action: 'newfile', opt: {width: 1600}})
  204. }
  205. }), null, null, 'Ctrl+N');
  206. this.actions.get('open').shortcut = 'Ctrl+O';
  207. // Adds shortcut keys for file operations
  208. editorUi.keyHandler.bindAction(78, true, 'new'); // Ctrl+N
  209. editorUi.keyHandler.bindAction(79, true, 'open'); // Ctrl+O
  210. editorUi.actions.addAction('keyboardShortcuts...', function()
  211. {
  212. const electron = require('electron');
  213. const remote = electron.remote;
  214. const BrowserWindow = remote.BrowserWindow;
  215. keyboardWindow = new BrowserWindow({width: 1200, height: 1000});
  216. // and load the index.html of the app.
  217. keyboardWindow.loadURL(`file://${__dirname}/shortcuts.svg`);
  218. // Emitted when the window is closed.
  219. keyboardWindow.on('closed', function()
  220. {
  221. // Dereference the window object, usually you would store windows
  222. // in an array if your app supports multi windows, this is the time
  223. // when you should delete the corresponding element.
  224. keyboardWindow = null;
  225. });
  226. });
  227. }
  228. // Uses local picker
  229. App.prototype.pickFile = function()
  230. {
  231. var doPickFile = mxUtils.bind(this, function()
  232. {
  233. this.chooseFileEntry(mxUtils.bind(this, function(fileEntry, data)
  234. {
  235. var file = new LocalFile(this, data, '');
  236. file.fileObject = fileEntry;
  237. this.fileLoaded(file);
  238. }));
  239. });
  240. var file = this.getCurrentFile();
  241. if (file != null && file.isModified())
  242. {
  243. this.confirm(mxResources.get('allChangesLost'), null, doPickFile,
  244. mxResources.get('cancel'), mxResources.get('discardChanges'));
  245. }
  246. else
  247. {
  248. doPickFile();
  249. }
  250. };
  251. /**
  252. * Selects a library to load from a picker
  253. *
  254. * @param mode the device mode, ignored in this case
  255. */
  256. App.prototype.pickLibrary = function(mode)
  257. {
  258. this.chooseFileEntry(mxUtils.bind(this, function(fileEntry, data)
  259. {
  260. try
  261. {
  262. var library = new LocalLibrary(this, data, fileEntry.name);
  263. library.fileObject = fileEntry;
  264. this.loadLibrary(library);
  265. }
  266. catch (e)
  267. {
  268. this.handleError(e, mxResources.get('errorLoadingFile'));
  269. }
  270. }));
  271. };
  272. // Uses local picker
  273. App.prototype.chooseFileEntry = function(fn)
  274. {
  275. const electron = require('electron');
  276. var remote = electron.remote;
  277. var dialog = remote.dialog;
  278. var paths = dialog.showOpenDialog({properties: ['openFile']});
  279. if (paths !== undefined && paths[0] != null)
  280. {
  281. var fs = require('fs');
  282. var path = paths[0];
  283. var index = path.lastIndexOf('.png');
  284. var isPng = index > -1 && index == path.length - 4;
  285. var encoding = isPng ? 'base64' : 'utf-8'
  286. fs.readFile(path, encoding, mxUtils.bind(this, function (e, data)
  287. {
  288. if (e)
  289. {
  290. this.handleError(e);
  291. }
  292. else
  293. {
  294. if (isPng)
  295. {
  296. // Detecting png by extension. Would need https://github.com/mscdex/mmmagic
  297. // to do it by inspection
  298. data = this.extractGraphModelFromPng(data, true);
  299. }
  300. var fileEntry = new Object();
  301. fileEntry.path = path;
  302. fileEntry.name = path.replace(/^.*[\\\/]/, '');
  303. fileEntry.type = encoding;
  304. fn(fileEntry, data);
  305. }
  306. }));
  307. }
  308. };
  309. // Disables temp files in Electron
  310. var LocalFileCtor = LocalFile;
  311. LocalFile = function(ui, data, title, temp)
  312. {
  313. LocalFileCtor.call(this, ui, data, title, false);
  314. };
  315. mxUtils.extend(LocalFile, LocalFileCtor);
  316. LocalFile.prototype.isAutosave = function()
  317. {
  318. return this.ui.editor.autosave && this.fileObject != null;
  319. };
  320. LocalFile.prototype.isAutosaveOptional = function()
  321. {
  322. return true;
  323. };
  324. LocalLibrary.prototype.isAutosave = function()
  325. {
  326. return this.fileObject != null;
  327. };
  328. LocalFile.prototype.getTitle = function()
  329. {
  330. return (this.fileObject != null) ? this.fileObject.name : this.title;
  331. };
  332. LocalFile.prototype.isRenamable = function()
  333. {
  334. return false;
  335. };
  336. // Restores default implementation of open with autosave
  337. LocalFile.prototype.open = DrawioFile.prototype.open;
  338. LocalFile.prototype.save = function(revision, success, error)
  339. {
  340. DrawioFile.prototype.save.apply(this, arguments);
  341. this.saveFile(revision, success, error);
  342. };
  343. LocalFile.prototype.saveFile = function(revision, success, error)
  344. {
  345. var fn = mxUtils.bind(this, function()
  346. {
  347. var doSave = mxUtils.bind(this, function(data, enc)
  348. {
  349. if (!this.savingFile)
  350. {
  351. this.savingFile = true;
  352. // Makes sure no changes get lost while the file is saved
  353. var prevModified = this.isModified;
  354. var modified = this.isModified();
  355. this.setModified(false);
  356. var fs = require('fs');
  357. fs.writeFile(this.fileObject.path, data, enc || this.fileObject.encoding, mxUtils.bind(this, function (e)
  358. {
  359. if (e)
  360. {
  361. this.savingFile = false;
  362. this.isModified = prevModified;
  363. this.setModified(modified || this.isModified());
  364. if (error != null)
  365. {
  366. error();
  367. }
  368. }
  369. else
  370. {
  371. this.savingFile = false;
  372. this.isModified = prevModified;
  373. this.contentChanged();
  374. this.lastData = data;
  375. if (success != null)
  376. {
  377. success();
  378. }
  379. }
  380. }));
  381. }
  382. else
  383. {
  384. // TODO, already saving. Need a better error
  385. if (error != null)
  386. {
  387. error();
  388. }
  389. }
  390. });
  391. if (!/(\.png)$/i.test(this.fileObject.name))
  392. {
  393. doSave(this.getData());
  394. }
  395. else
  396. {
  397. var graph = this.ui.editor.graph;
  398. // Exports PNG for first page while other page is visible by creating a graph
  399. // LATER: Add caching for the graph or SVG while not on first page
  400. if (this.ui.pages != null && this.ui.currentPage != this.ui.pages[0])
  401. {
  402. graph = this.ui.createTemporaryGraph(graph.getStylesheet());
  403. var graphGetGlobalVariable = graph.getGlobalVariable;
  404. var page = this.ui.pages[0];
  405. graph.getGlobalVariable = function(name)
  406. {
  407. if (name == 'page')
  408. {
  409. return page.getName();
  410. }
  411. else if (name == 'pagenumber')
  412. {
  413. return 1;
  414. }
  415. return graphGetGlobalVariable.apply(this, arguments);
  416. };
  417. document.body.appendChild(graph.container);
  418. graph.model.setRoot(page.root);
  419. }
  420. this.ui.exportToCanvas(mxUtils.bind(this, function(canvas)
  421. {
  422. try
  423. {
  424. var data = canvas.toDataURL('image/png');
  425. data = this.ui.writeGraphModelToPng(data, 'zTXt', 'mxGraphModel',
  426. atob(this.ui.editor.graph.compress(this.ui.getFileData(true))));
  427. doSave(atob(data.substring(data.lastIndexOf(',') + 1)), 'binary');
  428. // Removes temporary graph from DOM
  429. if (graph != this.ui.editor.graph)
  430. {
  431. graph.container.parentNode.removeChild(graph.container);
  432. }
  433. }
  434. catch (e)
  435. {
  436. if (error != null)
  437. {
  438. error(e);
  439. }
  440. }
  441. }), null, null, null, mxUtils.bind(this, function(e)
  442. {
  443. if (error != null)
  444. {
  445. error(e);
  446. }
  447. }), null, null, null, null, null, null, graph);
  448. }
  449. });
  450. if (this.fileObject == null)
  451. {
  452. const electron = require('electron');
  453. var remote = electron.remote;
  454. var dialog = remote.dialog;
  455. var path = dialog.showSaveDialog({defaultPath: this.title});
  456. if (path != null)
  457. {
  458. this.fileObject = new Object();
  459. this.fileObject.path = path;
  460. this.fileObject.name = path.replace(/^.*[\\\/]/, '');
  461. this.fileObject.type = 'utf-8';
  462. fn();
  463. }
  464. else if (error != null)
  465. {
  466. error();
  467. }
  468. }
  469. else
  470. {
  471. fn();
  472. }
  473. };
  474. LocalLibrary.prototype.save = function(revision, success, error)
  475. {
  476. LocalFile.prototype.saveFile.apply(this, arguments);
  477. };
  478. LocalFile.prototype.saveAs = function(title, success, error)
  479. {
  480. const electron = require('electron');
  481. var remote = electron.remote;
  482. var dialog = remote.dialog;
  483. var filename = this.title;
  484. // Adds default extension
  485. if (filename.length > 0 && (!/(\.xml)$/i.test(filename) && !/(\.html)$/i.test(filename) &&
  486. !/(\.svg)$/i.test(filename) && !/(\.png)$/i.test(filename)))
  487. {
  488. filename += '.xml';
  489. }
  490. var path = dialog.showSaveDialog({defaultPath: filename});
  491. if (path != null)
  492. {
  493. this.fileObject = new Object();
  494. this.fileObject.path = path;
  495. this.fileObject.name = path.replace(/^.*[\\\/]/, '');
  496. this.fileObject.type = 'utf-8';
  497. this.save(false, success, error);
  498. }
  499. else if (error != null)
  500. {
  501. error();
  502. }
  503. };
  504. App.prototype.saveFile = function(forceDialog)
  505. {
  506. var file = this.getCurrentFile();
  507. if (file != null)
  508. {
  509. if (!forceDialog && file.getTitle() != null)
  510. {
  511. file.save(true, mxUtils.bind(this, function(resp)
  512. {
  513. this.spinner.stop();
  514. this.editor.setStatus(mxUtils.htmlEntities(mxResources.get('allChangesSaved')));
  515. }), mxUtils.bind(this, function(resp)
  516. {
  517. this.editor.setStatus('');
  518. this.handleError(resp, (resp != null) ? mxResources.get('errorSavingFile') : null);
  519. }));
  520. }
  521. else
  522. {
  523. file.saveAs(null, mxUtils.bind(this, function(resp)
  524. {
  525. this.spinner.stop();
  526. this.editor.setStatus(mxUtils.htmlEntities(mxResources.get('allChangesSaved')));
  527. }), mxUtils.bind(this, function(resp)
  528. {
  529. this.editor.setStatus('');
  530. this.handleError(resp, (resp != null) ? mxResources.get('errorSavingFile') : null);
  531. }));
  532. }
  533. }
  534. };
  535. /**
  536. * Translates this point by the given vector.
  537. *
  538. * @param {number} dx X-coordinate of the translation.
  539. * @param {number} dy Y-coordinate of the translation.
  540. */
  541. App.prototype.saveLibrary = function(name, images, file, mode, noSpin, noReload, fn)
  542. {
  543. mode = (mode != null) ? mode : this.mode;
  544. noSpin = (noSpin != null) ? noSpin : false;
  545. noReload = (noReload != null) ? noReload : false;
  546. var xml = this.createLibraryDataFromImages(images);
  547. var error = mxUtils.bind(this, function(resp)
  548. {
  549. this.spinner.stop();
  550. if (fn != null)
  551. {
  552. fn();
  553. }
  554. // Null means cancel by user and is ignored
  555. if (resp != null)
  556. {
  557. this.handleError(resp, mxResources.get('errorSavingFile'));
  558. }
  559. });
  560. // Handles special case for local libraries
  561. if (file == null)
  562. {
  563. file = new LocalLibrary(this, xml, name);
  564. }
  565. if (noSpin || this.spinner.spin(document.body, mxResources.get('saving')))
  566. {
  567. file.setData(xml);
  568. var doSave = mxUtils.bind(this, function()
  569. {
  570. file.save(true, mxUtils.bind(this, function(resp)
  571. {
  572. this.spinner.stop();
  573. this.hideDialog(true);
  574. if (!noReload)
  575. {
  576. this.libraryLoaded(file, images)
  577. }
  578. if (fn != null)
  579. {
  580. fn();
  581. }
  582. }), error);
  583. });
  584. if (name != file.getTitle())
  585. {
  586. var oldHash = file.getHash();
  587. file.rename(name, mxUtils.bind(this, function(resp)
  588. {
  589. // Change hash in stored settings
  590. if (file.constructor != LocalLibrary && oldHash != file.getHash())
  591. {
  592. mxSettings.removeCustomLibrary(oldHash);
  593. mxSettings.addCustomLibrary(file.getHash());
  594. }
  595. // Workaround for library files changing hash so
  596. // the old library cannot be removed from the
  597. // sidebar using the updated file in libraryLoaded
  598. this.removeLibrarySidebar(oldHash);
  599. doSave();
  600. }), error)
  601. }
  602. else
  603. {
  604. doSave();
  605. }
  606. }
  607. };
  608. EditorUi.prototype.saveData = function(filename, format, data, mimeType, base64Encoded)
  609. {
  610. const electron = require('electron');
  611. var remote = electron.remote;
  612. var dialog = remote.dialog;
  613. var path = dialog.showSaveDialog({defaultPath: filename});
  614. if (path != null)
  615. {
  616. this.fileObject = new Object();
  617. this.fileObject.path = path;
  618. this.fileObject.name = path.replace(/^.*[\\\/]/, '');
  619. var isImage = mimeType != null && mimeType.startsWith('image');
  620. this.fileObject.type = base64Encoded ? 'base64' : 'utf-8';
  621. var fs = require('fs');
  622. fs.writeFile(this.fileObject.path, data, this.fileObject.type, mxUtils.bind(this, function (e)
  623. {
  624. if (e)
  625. {
  626. this.handleError({message: mxResources.get('errorSavingFile')});
  627. }
  628. }));
  629. }
  630. };
  631. EditorUi.prototype.addBeforeUnloadListener = function() {};
  632. })();