ElectronApp.js 19 KB

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