ElectronApp.js 52 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141
  1. window.PLUGINS_BASE_PATH = '.';
  2. window.TEMPLATE_PATH = 'templates';
  3. window.DRAW_MATH_URL = 'math/es5';
  4. window.DRAWIO_BASE_URL = '.'; //Prevent access to online website since it is not allowed
  5. window.DRAWIO_SERVER_URL = '.';
  6. FeedbackDialog.feedbackUrl = 'https://log.draw.io/email';
  7. EditorUi.draftSaveDelay = 5000;
  8. //Disables eval for JS (uses shapes-14-6-5.min.js)
  9. mxStencilRegistry.allowEval = false;
  10. (async function()
  11. {
  12. let requestSync = async function(msg)
  13. {
  14. if (typeof msg === 'string')
  15. {
  16. msg = {
  17. action: msg
  18. };
  19. }
  20. async function doRequest()
  21. {
  22. return new Promise((resolve, reject) => {
  23. electron.request(msg, function(data)
  24. {
  25. resolve(data);
  26. }, function(errMsg, e)
  27. {
  28. reject(e || errMsg);
  29. });
  30. });
  31. };
  32. return await doRequest();
  33. };
  34. // Overrides default mode
  35. App.mode = App.MODE_DEVICE;
  36. // Disables all external transmission functionality
  37. App.prototype.isExternalDataComms = function()
  38. {
  39. return false;
  40. };
  41. // Disables preview option in embed dialog
  42. EmbedDialog.showPreviewOption = false;
  43. // Disables new window option in edit diagram dialog
  44. EditDiagramDialog.showNewWindowOption = false;
  45. PrintDialog.previewEnabled = false;
  46. PrintDialog.electronPrint = function(editorUi, args)
  47. {
  48. var graph = editorUi.editor.graph;
  49. var file = editorUi.getCurrentFile();
  50. if (file)
  51. {
  52. file.updateFileData();
  53. }
  54. var xml = editorUi.getFileData(true, null, null, null,
  55. !args.selection, false, null, null, null, false, true);
  56. var extras = {globalVars: graph.getExportVariables()};
  57. if (Graph.translateDiagram)
  58. {
  59. extras.diagramLanguage = Graph.diagramLanguage;
  60. }
  61. if (args.grid)
  62. {
  63. extras.grid = {
  64. size: graph.gridSize,
  65. steps: graph.view.gridSteps,
  66. color: graph.view.gridColor
  67. };
  68. }
  69. new mxElectronRequest('export', {
  70. fileTitle: editorUi.getBaseFilename(true),
  71. print: true,
  72. format: 'pdf',
  73. xml: xml,
  74. from: args.pagesFrom - 1,
  75. to: args.pagesTo - 1,
  76. allPages: args.allPages ? '1' : '0',
  77. pageScale: 1,
  78. fit: args.fit ? '1' : '0',
  79. sheetsAcross: args.sheetsAcross,
  80. sheetsDown: args.sheetsDown,
  81. scale: args.scale,
  82. extras: JSON.stringify(extras),
  83. border: args.border,
  84. crop: args.crop ? '1' : '0'
  85. }).send(function(){}, function(){});
  86. };
  87. var oldWindowOpen = window.open;
  88. window.open = async function(url)
  89. {
  90. // Only open a native electron window when url is empty. We use this in our code in several places.
  91. if (url == null)
  92. {
  93. return oldWindowOpen(url);
  94. }
  95. else
  96. {
  97. // Open external will filter urls based on their protocol
  98. await requestSync({action: 'openExternal', url: url});
  99. }
  100. }
  101. var origAppMain = App.main;
  102. App.main = async function()
  103. {
  104. // Set AutoSave delay
  105. var draftSaveDelay = mxSettings.getDraftSaveDelay();
  106. if (draftSaveDelay != null)
  107. {
  108. EditorUi.draftSaveDelay = draftSaveDelay * 1000;
  109. EditorUi.enableDrafts = draftSaveDelay > 0;
  110. }
  111. //Load desktop plugins
  112. var plugins = (mxSettings.settings != null) ? mxSettings.getPlugins() : null;
  113. App.initPluginCallback();
  114. if (plugins != null && plugins.length > 0)
  115. {
  116. // Workaround for body not defined if plugins are used in dev mode
  117. if (urlParams['dev'] == '1')
  118. {
  119. EditorUi.debug('App.main', 'Skipped plugins', plugins);
  120. }
  121. else
  122. {
  123. for (var i = 0; i < plugins.length; i++)
  124. {
  125. try
  126. {
  127. if (plugins[i].indexOf('..') >= 0)
  128. {
  129. continue;
  130. }
  131. else if (plugins[i].startsWith('/plugins/'))
  132. {
  133. plugins[i] = '.' + plugins[i];
  134. }
  135. else if (plugins[i].startsWith('plugins/'))
  136. {
  137. plugins[i] = './' + plugins[i];
  138. }
  139. // External plugins in App Data folder (Needs enabling plugins)
  140. if (!plugins[i].startsWith('./plugins/'))
  141. {
  142. let pluginFile = await requestSync({
  143. action: 'getPluginFile',
  144. plugin: plugins[i]
  145. });
  146. if (pluginFile != null)
  147. {
  148. plugins[i] = 'file://' + pluginFile;
  149. }
  150. else
  151. {
  152. continue; //skip not found files
  153. }
  154. }
  155. try
  156. {
  157. mxscript(plugins[i]);
  158. }
  159. catch (e)
  160. {
  161. // ignore
  162. }
  163. }
  164. catch (e)
  165. {
  166. // ignore
  167. }
  168. }
  169. }
  170. }
  171. //Remove old relaxed CSP and add strict one
  172. var allMeta = document.getElementsByTagName('meta');
  173. for (var i = 0; i < allMeta.length; i++)
  174. {
  175. if (allMeta[i].getAttribute('http-equiv') == 'Content-Security-Policy')
  176. {
  177. allMeta[i].parentNode.removeChild(allMeta[i]);
  178. }
  179. break;
  180. }
  181. mxmeta(null, 'default-src \'self\'; connect-src \'self\' https://fonts.googleapis.com https://fonts.gstatic.com; img-src * data:; media-src *; font-src *; style-src \'self\' \'unsafe-inline\' https://fonts.googleapis.com', 'Content-Security-Policy');
  182. //Disable web plugins loading
  183. urlParams['plugins'] = '0';
  184. origAppMain.apply(this, arguments);
  185. };
  186. var menusInit = Menus.prototype.init;
  187. Menus.prototype.init = function()
  188. {
  189. menusInit.apply(this, arguments);
  190. var editorUi = this.editorUi;
  191. editorUi.actions.put('useOffline', new Action(mxResources.get('useOffline') + '...', function()
  192. {
  193. editorUi.openLink('https://www.draw.io/')
  194. }));
  195. this.put('openRecent', new Menu(function(menu, parent)
  196. {
  197. var recent = editorUi.getRecent();
  198. if (recent != null)
  199. {
  200. for (var i = 0; i < recent.length; i++)
  201. {
  202. (function(entry)
  203. {
  204. menu.addItem(entry.title, null, function()
  205. {
  206. function doOpenRecent()
  207. {
  208. //Simulate opening a file via args
  209. editorUi.loadArgs({args: [entry.id]});
  210. };
  211. var file = editorUi.getCurrentFile();
  212. if (file != null && file.isModified())
  213. {
  214. editorUi.confirm(mxResources.get('allChangesLost'), null, doOpenRecent,
  215. mxResources.get('cancel'), mxResources.get('discardChanges'));
  216. }
  217. else
  218. {
  219. doOpenRecent();
  220. }
  221. }, parent);
  222. })(recent[i]);
  223. }
  224. menu.addSeparator(parent);
  225. }
  226. menu.addItem(mxResources.get('reset'), null, function()
  227. {
  228. editorUi.resetRecent();
  229. }, parent);
  230. }));
  231. // Replaces file menu to replace openFrom menu with open and rename downloadAs to export
  232. this.put('file', new Menu(mxUtils.bind(this, function(menu, parent)
  233. {
  234. this.addMenuItems(menu, ['new', 'open'], parent);
  235. this.addSubmenu('openRecent', menu, parent);
  236. this.addMenuItems(menu, ['-', 'synchronize', '-', 'save', 'saveAs', '-', 'import'], parent);
  237. this.addSubmenu('exportAs', menu, parent);
  238. menu.addSeparator(parent);
  239. this.addSubmenu('embed', menu, parent);
  240. menu.addSeparator(parent);
  241. this.addMenuItems(menu, ['newLibrary', 'openLibrary'], parent);
  242. var file = editorUi.getCurrentFile();
  243. if (file != null && editorUi.fileNode != null)
  244. {
  245. var filename = (file.getTitle() != null) ?
  246. file.getTitle() : editorUi.defaultFilename;
  247. if (!/(\.html)$/i.test(filename) &&
  248. !/(\.svg)$/i.test(filename))
  249. {
  250. this.addMenuItems(menu, ['-', 'properties']);
  251. }
  252. }
  253. this.addMenuItems(menu, ['-', 'pageSetup', 'print', '-', 'close', '-', 'exit'], parent);
  254. })));
  255. };
  256. var graphCreateLinkForHint = Graph.prototype.createLinkForHint;
  257. Graph.prototype.createLinkForHint = function(href, label, associatedCell)
  258. {
  259. var a = graphCreateLinkForHint.call(this, href, label, associatedCell);
  260. if (href != null && !this.isCustomLink(href))
  261. {
  262. // KNOWN: Event with gesture handler mouseUp the middle click opens a framed window
  263. mxEvent.addListener(a, 'click', mxUtils.bind(this, function(evt)
  264. {
  265. this.openLink(a.getAttribute('href'), a.getAttribute('target'));
  266. mxEvent.consume(evt);
  267. }));
  268. }
  269. return a;
  270. };
  271. Graph.prototype.openLink = async function(url, target)
  272. {
  273. await requestSync({action: 'openExternal', url: url});
  274. };
  275. // Initializes the user interface
  276. var editorUiInit = EditorUi.prototype.init;
  277. EditorUi.prototype.init = async function()
  278. {
  279. editorUiInit.apply(this, arguments);
  280. var editorUi = this;
  281. var graph = this.editor.graph;
  282. electron.registerMsgListener('isModified', (uniqueId) =>
  283. {
  284. const currentFile = editorUi.getCurrentFile();
  285. let reply = {isModified: false, draftPath: null, uniqueId: uniqueId};
  286. if (currentFile != null)
  287. {
  288. reply.isModified = currentFile.isModified();
  289. reply.draftPath = EditorUi.enableDrafts && currentFile.fileObject? currentFile.fileObject.draftFileName : null;
  290. }
  291. electron.sendMessage('isModified-result', reply);
  292. });
  293. electron.registerMsgListener('removeDraft', () =>
  294. {
  295. editorUi.getCurrentFile().removeDraft();
  296. electron.sendMessage('draftRemoved', {});
  297. });
  298. // Adds support for libraries
  299. this.actions.addAction('newLibrary...', mxUtils.bind(this, function()
  300. {
  301. editorUi.showLibraryDialog(null, null, null, null, App.MODE_DEVICE);
  302. }));
  303. this.actions.addAction('openLibrary...', mxUtils.bind(this, function()
  304. {
  305. editorUi.pickLibrary(App.MODE_DEVICE);
  306. }));
  307. // Replaces import action
  308. this.actions.addAction('import...', mxUtils.bind(this, async function()
  309. {
  310. if (editorUi.getCurrentFile() != null)
  311. {
  312. var lastDir = localStorage.getItem('.lastImpDir');
  313. var paths = await requestSync({
  314. action: 'showOpenDialog',
  315. defaultPath: lastDir || (await requestSync('getDocumentsFolder')),
  316. properties: ['openFile']
  317. });
  318. if (paths !== undefined && paths[0] != null)
  319. {
  320. var path = paths[0];
  321. localStorage.setItem('.lastImpDir', await requestSync({action: 'dirname', path: path}));
  322. var asImage = /\.png$/i.test(path) || /\.gif$/i.test(path) || /\.jpe?g$/i.test(path);
  323. var encoding = (asImage || /\.pdf$/i.test(path) || /\.vsdx$/i.test(path) || /\.vssx$/i.test(path)) ?
  324. 'base64' : 'utf-8';
  325. if (editorUi.spinner.spin(document.body, mxResources.get('loading')))
  326. {
  327. electron.request({action: 'readFile', filename: path, encoding: encoding} , mxUtils.bind(this, function (data)
  328. {
  329. try
  330. {
  331. if (editorUi.isLucidChartData(data))
  332. {
  333. editorUi.convertLucidChart(data, function(xml)
  334. {
  335. editorUi.spinner.stop();
  336. graph.setSelectionCells(editorUi.importXml(xml));
  337. }, function(e)
  338. {
  339. editorUi.spinner.stop();
  340. editorUi.handleError(e);
  341. });
  342. }
  343. else if (/(\.vsdx)($|\?)/i.test(path))
  344. {
  345. editorUi.importVisio(editorUi.base64ToBlob(data, 'application/octet-stream'), function(xml)
  346. {
  347. editorUi.spinner.stop();
  348. graph.setSelectionCells(editorUi.importXml(xml));
  349. });
  350. }
  351. else if (editorUi.isRemoteFileFormat(data, path))
  352. {
  353. editorUi.spinner.stop();
  354. editorUi.showError(mxResources.get('error'), mxResources.get('notInDesktop'));
  355. }
  356. else
  357. {
  358. if (/\.pdf$/i.test(path))
  359. {
  360. var tmp = Editor.extractGraphModelFromPdf(data);
  361. if (tmp != null)
  362. {
  363. data = tmp;
  364. }
  365. }
  366. else if (/\.png$/i.test(path))
  367. {
  368. var tmp = editorUi.extractGraphModelFromPng(data);
  369. if (tmp != null)
  370. {
  371. asImage = false;
  372. data = tmp;
  373. }
  374. }
  375. else if (/\.svg$/i.test(path))
  376. {
  377. // LATER: Use importXml without throwing exception if no data
  378. // Checks if SVG contains content attribute
  379. var root = mxUtils.parseXml(data);
  380. var svgs = root.getElementsByTagName('svg');
  381. if (svgs.length > 0)
  382. {
  383. var svgRoot = svgs[0];
  384. var cont = svgRoot.getAttribute('content');
  385. if (cont != null && cont.charAt(0) != '<' && cont.charAt(0) != '%')
  386. {
  387. cont = unescape((window.atob) ? atob(cont) : Base64.decode(cont, true));
  388. }
  389. if (cont != null && cont.charAt(0) == '%')
  390. {
  391. cont = decodeURIComponent(cont);
  392. }
  393. if (cont != null && (cont.substring(0, 8) === '<mxfile ' ||
  394. cont.substring(0, 14) === '<mxGraphModel '))
  395. {
  396. asImage = false;
  397. data = cont;
  398. }
  399. else
  400. {
  401. asImage = true;
  402. }
  403. }
  404. }
  405. if (asImage)
  406. {
  407. var img = new Image();
  408. img.onload = function()
  409. {
  410. editorUi.resizeImage(img, img.src, function(data2, w, h)
  411. {
  412. editorUi.spinner.stop();
  413. var pt = graph.getInsertPoint();
  414. graph.setSelectionCell(graph.insertVertex(null, null, '', pt.x, pt.y, w, h,
  415. 'shape=image;aspect=fixed;image=' + editorUi.convertDataUri(data2) + ';'));
  416. }, true);
  417. };
  418. img.onerror = function(e)
  419. {
  420. editorUi.spinner.stop();
  421. editorUi.handleError();
  422. };
  423. var format = path.substring(path.lastIndexOf('.') + 1);
  424. img.src = (format == 'svg') ? Editor.createSvgDataUri(data) :
  425. 'data:image/' + format + ';base64,' + data;
  426. }
  427. else
  428. {
  429. editorUi.spinner.stop();
  430. if (data != null)
  431. {
  432. graph.setSelectionCells(editorUi.importXml(data));
  433. }
  434. }
  435. }
  436. }
  437. catch(e)
  438. {
  439. editorUi.spinner.stop();
  440. editorUi.handleError(e);
  441. }
  442. }), function(e)
  443. {
  444. editorUi.spinner.stop();
  445. editorUi.handleError(e);
  446. });
  447. }
  448. }
  449. }
  450. }));
  451. // Replaces new action
  452. var oldNew = this.actions.get('new').funct;
  453. this.actions.addAction('new...', mxUtils.bind(this, function()
  454. {
  455. if (this.getCurrentFile() == null)
  456. {
  457. oldNew();
  458. }
  459. else
  460. {
  461. electron.sendMessage('newfile', {width: 1600});
  462. }
  463. }), null, null, Editor.ctrlKey + '+N');
  464. this.actions.get('open').shortcut = Editor.ctrlKey + '+O';
  465. // Adds shortcut keys for file operations
  466. editorUi.keyHandler.bindAction(78, true, 'new'); // Ctrl+N
  467. editorUi.keyHandler.bindAction(79, true, 'open'); // Ctrl+O
  468. var isFullScreen = await requestSync('isFullscreen');
  469. var fullscreenAction = editorUi.actions.addAction('fullscreen', async function()
  470. {
  471. electron.sendMessage('toggleFullscreen');
  472. isFullScreen = await requestSync('isFullscreen');
  473. });
  474. fullscreenAction.visible = true;
  475. fullscreenAction.setToggleAction(true);
  476. fullscreenAction.setSelectedCallback(function()
  477. {
  478. return isFullScreen;
  479. });
  480. function createGraph()
  481. {
  482. var graph = new Graph();
  483. graph.setExtendParents(false);
  484. graph.setExtendParentsOnAdd(false);
  485. graph.setConstrainChildren(false);
  486. graph.setHtmlLabels(true);
  487. graph.getModel().maintainEdgeParent = false;
  488. return graph;
  489. };
  490. async function cloneMxCLipboardToSys()
  491. {
  492. var cells = mxClipboard.getCells();
  493. if (cells && cells.length > 0)
  494. {
  495. try
  496. {
  497. var tmpGraph = createGraph();
  498. tmpGraph.importCells(cells, 0, 0, tmpGraph.getDefaultParent());
  499. var codec = new mxCodec();
  500. var node = codec.encode(tmpGraph.getModel());
  501. var modelString = mxUtils.getXml(node);
  502. await requestSync({
  503. action: 'clipboardAction',
  504. method: 'writeText',
  505. data: encodeURIComponent(modelString)
  506. });
  507. }
  508. catch(e)
  509. {
  510. //Ignore
  511. }
  512. }
  513. };
  514. async function cloneSysCLipboardToMx()
  515. {
  516. try
  517. {
  518. var modelString = await requestSync({
  519. action: 'clipboardAction',
  520. method: 'readText',
  521. });
  522. if (modelString)
  523. {
  524. modelString = decodeURIComponent(modelString);
  525. var xmlDoc = mxUtils.parseXml(modelString);
  526. var tmpGraph = createGraph();
  527. var codec = new mxCodec(xmlDoc);
  528. var model = tmpGraph.getModel();
  529. codec.decode(xmlDoc.documentElement, model);
  530. mxClipboard.setCells(model.root.children[0].children);
  531. }
  532. }
  533. catch(e)
  534. {
  535. //Ignore, the contents of mxClipboard will be used
  536. }
  537. };
  538. //Set system clipboard on menu copy/cut
  539. var origCut = this.actions.get('cut').funct;
  540. editorUi.actions.addAction('cut', function()
  541. {
  542. origCut();
  543. cloneMxCLipboardToSys();
  544. }, null, 'sprite-cut', Editor.ctrlKey + '+X');
  545. var origCopy = this.actions.get('copy').funct;
  546. editorUi.actions.addAction('copy', function()
  547. {
  548. origCopy();
  549. cloneMxCLipboardToSys();
  550. }, null, 'sprite-copy', Editor.ctrlKey + '+C');
  551. //Get data from system clipboard for pase/pasteHere
  552. var origPaste = this.actions.get('paste').funct;
  553. editorUi.actions.addAction('paste', function()
  554. {
  555. cloneSysCLipboardToMx();
  556. origPaste();
  557. }, false, 'sprite-paste', Editor.ctrlKey + '+V');
  558. var origPasteHere = this.actions.get('pasteHere').funct;
  559. editorUi.actions.addAction('pasteHere', function()
  560. {
  561. cloneSysCLipboardToMx();
  562. origPasteHere();
  563. });
  564. //Enable paste action even if mxClipboard is empty! TODO Is this OK?
  565. editorUi.updatePasteActionStates = function()
  566. {
  567. var graph = this.editor.graph;
  568. var paste = this.actions.get('paste');
  569. var pasteHere = this.actions.get('pasteHere');
  570. paste.setEnabled(this.editor.graph.cellEditor.isContentEditing() ||
  571. (graph.isEnabled() && !graph.isCellLocked(graph.getDefaultParent())));
  572. pasteHere.setEnabled(paste.isEnabled());
  573. };
  574. editorUi.actions.addAction('plugins...', function()
  575. {
  576. var pluginsMap = {};
  577. //Initialize it with plugins in settings
  578. var plugins = (mxSettings.settings != null) ? mxSettings.getPlugins() : null;
  579. if (plugins != null)
  580. {
  581. for (var i = 0; i < plugins.length; i++)
  582. {
  583. pluginsMap[plugins[i]] = true;
  584. }
  585. }
  586. editorUi.showDialog(new PluginsDialog(editorUi, async function(callback)
  587. {
  588. var div = document.createElement('div');
  589. var title = document.createElement('span');
  590. title.style.marginTop = '6px';
  591. mxUtils.write(title, mxResources.get('builtinPlugins') + ': ');
  592. div.appendChild(title);
  593. var pluginsSelect = document.createElement('select');
  594. pluginsSelect.style.width = '150px';
  595. for (var i = 0; i < App.publicPlugin.length; i++)
  596. {
  597. var p = App.publicPlugin[i];
  598. if (pluginsMap[App.pluginRegistry[p]]) continue;
  599. var option = document.createElement('option');
  600. mxUtils.write(option, p);
  601. option.value = p;
  602. pluginsSelect.appendChild(option);
  603. }
  604. div.appendChild(pluginsSelect);
  605. mxUtils.br(div);
  606. mxUtils.br(div);
  607. title = document.createElement('span');
  608. mxUtils.write(title, mxResources.get('extPlugins') + ': ');
  609. div.appendChild(title);
  610. if (await requestSync('isPluginsEnabled'))
  611. {
  612. var extPluginsBtn = mxUtils.button(mxResources.get('selectFile') + '...', async function()
  613. {
  614. var warningMsgs = mxResources.get('pluginWarning').split('\\n');
  615. var warningMsg = warningMsgs.pop(); //Last line in the message
  616. if (!warningMsg)
  617. {
  618. warningMsg = warningMsgs.pop();
  619. }
  620. if (!confirm(warningMsg))
  621. {
  622. return;
  623. }
  624. var lastDir = localStorage.getItem('.lastPluginDir');
  625. var paths = await requestSync({
  626. action: 'showOpenDialog',
  627. defaultPath: lastDir || (await requestSync('getDocumentsFolder')),
  628. filters: [
  629. { name: 'draw.io Plugins', extensions: ['js'] },
  630. { name: 'All Files', extensions: ['*'] }
  631. ],
  632. properties: ['openFile']
  633. });
  634. if (paths !== undefined && paths[0] != null)
  635. {
  636. try
  637. {
  638. let ret = await requestSync({
  639. action: 'installPlugin',
  640. filePath: paths[0]
  641. });
  642. localStorage.setItem('.lastPluginDir', ret.selDir);
  643. callback(ret.pluginName);
  644. editorUi.hideDialog();
  645. }
  646. catch (e)
  647. {
  648. if (e.message == 'fileExists')
  649. {
  650. alert(mxResources.get('fileExists'));
  651. }
  652. else
  653. {
  654. alert('Adding plugin failed.');
  655. }
  656. }
  657. }
  658. });
  659. extPluginsBtn.className = 'geBtn';
  660. div.appendChild(extPluginsBtn);
  661. }
  662. else
  663. {
  664. title = document.createElement('span');
  665. mxUtils.write(title, mxResources.get('pluginsDisabled'));
  666. div.appendChild(title);
  667. }
  668. var dlg = new CustomDialog(editorUi, div, mxUtils.bind(this, function()
  669. {
  670. var newP = App.pluginRegistry[pluginsSelect.value];
  671. pluginsMap[newP] = true;
  672. callback(newP);
  673. }));
  674. editorUi.showDialog(dlg.container, 300, 125, true, true);
  675. },
  676. async function(plugin)
  677. {
  678. delete pluginsMap[plugin];
  679. await requestSync({
  680. action: 'uninstallPlugin',
  681. plugin: plugin
  682. });
  683. }, true).container, 360, 225, true, false);
  684. });
  685. editorUi.actions.addAction('exit', function()
  686. {
  687. electron.request({
  688. action: 'exit'
  689. });
  690. });
  691. }
  692. var appLoad = App.prototype.load;
  693. App.prototype.load = function()
  694. {
  695. appLoad.apply(this, arguments);
  696. electron.registerMsgListener('args-obj', (argsObj) =>
  697. {
  698. this.loadArgs(argsObj)
  699. })
  700. var editorUi = this;
  701. electron.registerMsgListener('export-vsdx', (argsObj) =>
  702. {
  703. var file = new LocalFile(editorUi, argsObj.xml, '');
  704. editorUi.fileLoaded(file);
  705. try
  706. {
  707. editorUi.saveData = function(filename, format, data, mimeType, base64Encoded)
  708. {
  709. electron.sendMessage('export-vsdx-finished', data);
  710. };
  711. var expSuccess = new VsdxExport(editorUi).exportCurrentDiagrams();
  712. if (!expSuccess)
  713. {
  714. electron.sendMessage('export-vsdx-finished', null);
  715. }
  716. }
  717. catch (e)
  718. {
  719. electron.sendMessage('export-vsdx-finished', null);
  720. }
  721. })
  722. //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)
  723. electron.sendMessage('app-load-finished', null);
  724. //Change offline translation
  725. mxResources.parse('notInOffline=' + mxResources.get('notInDesktop'));
  726. }
  727. App.prototype.loadArgs = function(argsObj)
  728. {
  729. var paths = argsObj.args;
  730. // If a file is passed, and it is not an argument (has a leading -)
  731. if (paths !== undefined && paths[0] != null && paths[0].indexOf('-') != 0 && this.spinner.spin(document.body, mxResources.get('loading')))
  732. {
  733. var path = paths[0];
  734. this.hideDialog();
  735. var success = mxUtils.bind(this, function(fileEntry, data, stat, name, isModified)
  736. {
  737. this.spinner.stop();
  738. if (data != null)
  739. {
  740. var file = new LocalFile(this, data, name || '');
  741. file.fileObject = fileEntry;
  742. file.stat = stat;
  743. file.setModified(isModified? true : false);
  744. this.fileLoaded(file);
  745. }
  746. });
  747. var error = mxUtils.bind(this, function(e)
  748. {
  749. this.spinner.stop();
  750. if (e.code === 'ENOENT')
  751. {
  752. var title = path.replace(/^.*[\\\/]/, '');
  753. var data = this.emptyDiagramXml;
  754. var file = new LocalFile(this, data, title, null);
  755. file.fileObject = new Object();
  756. file.fileObject.path = path;
  757. file.fileObject.name = title;
  758. file.fileObject.type = 'utf-8';
  759. this.fileCreated(file, null, null, null);
  760. this.saveFile();
  761. }
  762. else
  763. {
  764. this.handleError(e);
  765. }
  766. });
  767. // Tries to open the file
  768. this.readGraphFile(success, error, path);
  769. }
  770. // If no file is passed, but there is the "create-if-not-exists" flag
  771. else if (argsObj.create != null)
  772. {
  773. var title = 'Untitled document';
  774. var data = this.emptyDiagramXml;
  775. var file = new LocalFile(this, data, title, null);
  776. this.fileCreated(file, null, null, null);
  777. }
  778. else
  779. {
  780. this.checkDrafts();
  781. }
  782. }
  783. var origFileLoaded = EditorUi.prototype.fileLoaded;
  784. EditorUi.prototype.fileLoaded = async function(file)
  785. {
  786. var oldFile = this.getCurrentFile();
  787. if (oldFile != null)
  788. {
  789. //TODO This assumes the user confirmed discarding the file changes to get to this function?
  790. if (EditorUi.enableDrafts)
  791. {
  792. oldFile.removeDraft();
  793. }
  794. if (oldFile.fileObject != null)
  795. {
  796. await requestSync({action: 'unwatchFile', path: oldFile.fileObject.path});
  797. }
  798. }
  799. if (file != null)
  800. {
  801. if (file.fileObject == null)
  802. {
  803. var fname = file.getTitle();
  804. var fileInfo = openFilesMap[fname];
  805. if (fileInfo != null)
  806. {
  807. file.fileObject = {
  808. name: fileInfo.name,
  809. path: fileInfo.path,
  810. type: fileInfo.type || 'utf-8'
  811. };
  812. //delete it such that it is not used again incorrectly
  813. delete openFilesMap[fname];
  814. }
  815. }
  816. if (file.fileObject != null)
  817. {
  818. file.addToRecent();
  819. await requestSync({
  820. action: 'watchFile',
  821. path: file.fileObject.path,
  822. listener: mxUtils.bind(this, function(curr, prev)
  823. {
  824. EditorUi.debug('EditorUi.watchFile', [this],
  825. 'file', [file], 'stat', [file.stat],
  826. 'curr', [curr], 'prev', [prev],
  827. 'inConflictState', file.inConflictState,
  828. 'unwatchedSaves', file.unwatchedSaves);
  829. // File not deleted, changed (not just accessed) and not already in conflict state
  830. if (curr.mtimeMs != 0 && curr.mtimeMs != prev.mtimeMs && !file.inConflictState)
  831. {
  832. //Ignore our own changes
  833. if (file.unwatchedSaves || (file.stat != null && file.stat.mtimeMs == curr.mtimeMs))
  834. {
  835. file.unwatchedSaves = false;
  836. return;
  837. }
  838. file.inConflictState = true;
  839. file.addConflictStatus(null, mxUtils.bind(this, function()
  840. {
  841. file.ui.editor.setStatus(mxUtils.htmlEntities(
  842. mxResources.get('updatingDocument')));
  843. file.synchronizeFile(mxUtils.bind(this, function()
  844. {
  845. file.handleFileSuccess(false);
  846. }), mxUtils.bind(this, function(err)
  847. {
  848. file.handleFileError(err, true);
  849. }));
  850. }));
  851. }
  852. })
  853. });
  854. }
  855. }
  856. origFileLoaded.apply(this, arguments);
  857. };
  858. // Uses local picker
  859. App.prototype.pickFile = function()
  860. {
  861. var doPickFile = mxUtils.bind(this, function()
  862. {
  863. this.chooseFileEntry(mxUtils.bind(this, function(fileEntry, data, stat, name, isModified)
  864. {
  865. var file = new LocalFile(this, data, '');
  866. file.fileObject = fileEntry;
  867. file.stat = stat;
  868. file.setModified(isModified? true : false);
  869. this.fileLoaded(file);
  870. }));
  871. });
  872. var file = this.getCurrentFile();
  873. if (file != null && file.isModified())
  874. {
  875. this.confirm(mxResources.get('allChangesLost'), null, doPickFile,
  876. mxResources.get('cancel'), mxResources.get('discardChanges'));
  877. }
  878. else
  879. {
  880. doPickFile();
  881. }
  882. };
  883. /**
  884. * Selects a library to load from a picker
  885. *
  886. * @param mode the device mode, ignored in this case
  887. */
  888. App.prototype.pickLibrary = function(mode)
  889. {
  890. this.chooseFileEntry(mxUtils.bind(this, function(fileEntry, data, stat)
  891. {
  892. try
  893. {
  894. var library = new DesktopLibrary(this, data, fileEntry);
  895. this.loadLibrary(library);
  896. this.showSidebar();
  897. }
  898. catch (e)
  899. {
  900. this.handleError(e, mxResources.get('errorLoadingFile'));
  901. }
  902. }));
  903. };
  904. // Uses local picker
  905. App.prototype.chooseFileEntry = async function(fn)
  906. {
  907. var lastDir = localStorage.getItem('.lastOpenDir');
  908. var paths = await requestSync({
  909. action: 'showOpenDialog',
  910. defaultPath: lastDir || (await requestSync('getDocumentsFolder')),
  911. filters: [
  912. { name: 'draw.io Diagrams', extensions: ['drawio', 'xml', 'png', 'svg', 'html'] },
  913. { name: 'VSDX Documents', extensions: ['vsdx'] },
  914. { name: 'All Files', extensions: ['*'] }
  915. ],
  916. properties: ['openFile']
  917. });
  918. if (paths !== undefined && paths[0] != null)
  919. {
  920. localStorage.setItem('.lastOpenDir', await requestSync({action: 'dirname', path: paths[0]}));
  921. this.readGraphFile(fn, mxUtils.bind(this, function(err)
  922. {
  923. this.handleError(err);
  924. }), paths[0]);
  925. }
  926. else
  927. {
  928. this.spinner.stop();
  929. }
  930. };
  931. EditorUi.prototype.normalizeFilename = function(title, defaultExtension)
  932. {
  933. var tokens = title.split('.');
  934. var ext = (tokens.length > 1) ? tokens[tokens.length - 1] : '';
  935. defaultExtension = (defaultExtension != null) ? defaultExtension : 'drawio';
  936. if (tokens.length == 1 || mxUtils.indexOf(['xml',
  937. 'html', 'drawio', 'png', 'svg'], ext) < 0)
  938. {
  939. tokens.push(defaultExtension);
  940. }
  941. return tokens.join('.');
  942. };
  943. //In order not to repeat the logic for opening a file, we collect files information here and use them in openLocalFile
  944. var origOpenFiles = EditorUi.prototype.openFiles;
  945. var openFilesMap = {};
  946. EditorUi.prototype.openFiles = function(files, temp)
  947. {
  948. openFilesMap = {};
  949. for (var i = 0; i < files.length; i++)
  950. {
  951. openFilesMap[files[i].name] = files[i];
  952. }
  953. origOpenFiles.apply(this, arguments);
  954. };
  955. App.prototype.readGraphFile = function(fn, fnErr, path, noDraftCheck)
  956. {
  957. var index = path.lastIndexOf('.png');
  958. var isPng = index > -1 && index == path.length - 4;
  959. var isVsdx = /\.vsdx$/i.test(path) || /\.vssx$/i.test(path);
  960. var encoding = isVsdx? null : ((isPng || /\.pdf$/i.test(path)) ? 'base64' : 'utf-8');
  961. var fileEntry = new Object(), stat = null;
  962. fileEntry.path = path;
  963. fileEntry.name = path.replace(/^.*[\\\/]/, '');
  964. fileEntry.type = encoding;
  965. var checkDrafts = mxUtils.bind(this, function()
  966. {
  967. if (noDraftCheck) return;
  968. electron.request({
  969. action: 'getFileDrafts',
  970. fileObject: fileEntry
  971. }, mxUtils.bind(this, function(drafts)
  972. {
  973. if (drafts.length > 0)
  974. {
  975. var dlg = new DraftDialog(this, mxResources.get('unsavedChanges'),
  976. (drafts.length > 1) ? null : drafts[0].data, mxUtils.bind(this, async function(index)
  977. {
  978. index = index || 0;
  979. this.hideDialog();
  980. fn(fileEntry, drafts[index].data, stat, null, true);
  981. await requestSync({action: 'deleteFile', file: drafts[index].path});
  982. }), mxUtils.bind(this, async function(index)
  983. {
  984. index = index || 0;
  985. await requestSync({action: 'deleteFile', file: drafts[index].path});
  986. this.hideDialog();
  987. }), null, null, null, (drafts.length > 1) ? drafts : null);
  988. this.showDialog(dlg.container, 640, 480, true, false);
  989. dlg.init();
  990. }
  991. }),
  992. mxUtils.bind(this, function(errMsg, err)
  993. {
  994. //TODO Currently ignored, maybe we should retry?
  995. }));
  996. });
  997. var readData = mxUtils.bind(this, function (data)
  998. {
  999. // VSDX and PDF files are imported instead of being opened
  1000. if (isVsdx)
  1001. {
  1002. var name = fileEntry.name;
  1003. this.importVisio(data, mxUtils.bind(this, function(xml)
  1004. {
  1005. var dot = name.lastIndexOf('.');
  1006. if (dot >= 0)
  1007. {
  1008. name = name.substring(0, name.lastIndexOf('.')) + '.drawio';
  1009. }
  1010. else
  1011. {
  1012. name = name + '.drawio';
  1013. }
  1014. if (xml.substring(0, 10) == '<mxlibrary')
  1015. {
  1016. // Creates new temporary file if library is dropped in splash screen
  1017. if (this.getCurrentFile() == null && urlParams['embed'] != '1')
  1018. {
  1019. this.openLocalFile(this.emptyDiagramXml, this.defaultFilename);
  1020. }
  1021. try
  1022. {
  1023. this.loadLibrary(new LocalLibrary(this, xml, name));
  1024. this.showSidebar();
  1025. }
  1026. catch (e)
  1027. {
  1028. this.handleError(e, mxResources.get('errorLoadingFile'));
  1029. }
  1030. fn();
  1031. }
  1032. else
  1033. {
  1034. fn(null, xml, null, name, false);
  1035. }
  1036. checkDrafts();
  1037. }), null, name);
  1038. return;
  1039. }
  1040. else if (/\.pdf$/i.test(path))
  1041. {
  1042. var tmp = Editor.extractGraphModelFromPdf('data:application/pdf;base64,' + data);
  1043. if (tmp != null)
  1044. {
  1045. var name = fileEntry.name;
  1046. if (name.substring(name.length - 4) == '.pdf')
  1047. {
  1048. name = name.substring(0, name.length - 4);
  1049. }
  1050. name = name.substring(0, name.lastIndexOf('.')) + '.drawio';
  1051. fn(null, tmp, null, name, false);
  1052. // Fixes ignore filename in above callback
  1053. var file = this.getCurrentFile();
  1054. if (file != null)
  1055. {
  1056. file.rename(name);
  1057. }
  1058. checkDrafts();
  1059. return;
  1060. }
  1061. }
  1062. else if (isPng)
  1063. {
  1064. // Detecting png by extension. Would need https://github.com/mscdex/mmmagic
  1065. // to do it by inspection
  1066. data = this.extractGraphModelFromPng('data:image/png;base64,' + data);
  1067. }
  1068. electron.request({action: 'fileStat', file: path}, mxUtils.bind(this, function(stat_p)
  1069. {
  1070. stat = stat_p;
  1071. fn(fileEntry, data, stat, null, false);
  1072. electron.request({action: 'isFileWritable', file: path}, mxUtils.bind(this, function(isWritable)
  1073. {
  1074. if (!isWritable)
  1075. {
  1076. var file = this.getCurrentFile();
  1077. if (file != null && file.fileObject != null && file.fileObject.path == path)
  1078. {
  1079. file.setEditable(false);
  1080. this.editor.setStatus('<div class="geStatusBox" title="' +
  1081. mxUtils.htmlEntities(mxResources.get('readOnly')) + '">' +
  1082. mxUtils.htmlEntities(mxResources.get('readOnly')) + '</div>');
  1083. }
  1084. }
  1085. }));
  1086. checkDrafts();
  1087. }), function(errMsg, err)
  1088. {
  1089. fnErr(err);
  1090. });
  1091. });
  1092. electron.request({
  1093. action: 'readFile',
  1094. filename: path,
  1095. encoding: encoding
  1096. }, readData, function(errMsg, err)
  1097. {
  1098. fnErr(err);
  1099. checkDrafts();
  1100. });
  1101. };
  1102. // Disables temp files in Electron
  1103. var LocalFileCtor = LocalFile;
  1104. LocalFile = function(ui, data, title, temp)
  1105. {
  1106. LocalFileCtor.call(this, ui, data, title, false);
  1107. };
  1108. mxUtils.extend(LocalFile, LocalFileCtor);
  1109. LocalFile.prototype.getLatestVersion = function(success, error)
  1110. {
  1111. if (this.fileObject == null)
  1112. {
  1113. if (error != null)
  1114. {
  1115. error({message: mxResources.get('fileNotFound')});
  1116. }
  1117. }
  1118. else
  1119. {
  1120. this.ui.readGraphFile(mxUtils.bind(this, function(fileEntry, data, stat, name, isModified)
  1121. {
  1122. var file = new LocalFile(this.ui, data, '');
  1123. file.stat = stat;
  1124. file.setModified(isModified? true : false);
  1125. success(file);
  1126. }), error, this.fileObject.path, true);
  1127. }
  1128. };
  1129. // Call save as for copy
  1130. LocalFile.prototype.copyFile = function(success, error)
  1131. {
  1132. this.saveAs(this.ui.getCopyFilename(this), success, error);
  1133. };
  1134. /**
  1135. * Adds all listeners.
  1136. */
  1137. LocalFile.prototype.getDescriptor = function()
  1138. {
  1139. return this.stat;
  1140. };
  1141. /**
  1142. * Updates the descriptor of this file with the one from the given file.
  1143. */
  1144. LocalFile.prototype.setDescriptor = function(stat)
  1145. {
  1146. this.stat = stat;
  1147. };
  1148. LocalFile.prototype.isAutosave = function()
  1149. {
  1150. return this.fileObject != null && DrawioFile.prototype.isAutosave.apply(this, arguments);
  1151. };
  1152. LocalFile.prototype.isAutosaveOptional = function()
  1153. {
  1154. return this.fileObject != null;
  1155. };
  1156. LocalFile.prototype.getTitle = function()
  1157. {
  1158. return (this.fileObject != null) ? this.fileObject.name : this.title;
  1159. };
  1160. LocalFile.prototype.isRenamable = function()
  1161. {
  1162. return false;
  1163. };
  1164. var autoSaveEnabled = false;
  1165. LocalFile.prototype.save = function(revision, success, error, unloading, overwrite)
  1166. {
  1167. if (!this.isEditable())
  1168. {
  1169. this.saveAs(this.title, success, error);
  1170. return;
  1171. }
  1172. DrawioFile.prototype.save.apply(this, [revision, mxUtils.bind(this, function()
  1173. {
  1174. this.saveFile(revision, mxUtils.bind(this, function()
  1175. {
  1176. //Only for first save after auto save is enabled (excluding the save as [overwrite])
  1177. if (autoSaveEnabled && !overwrite && EditorUi.enableDrafts)
  1178. {
  1179. this.removeDraft();
  1180. }
  1181. autoSaveEnabled = false;
  1182. success.apply(this, arguments);
  1183. }), error, unloading, overwrite);
  1184. }), error, unloading, overwrite]);
  1185. };
  1186. LocalFile.prototype.isConflict = function(stat)
  1187. {
  1188. return stat != null && this.stat != null && stat.mtimeMs != this.stat.mtimeMs;
  1189. };
  1190. LocalFile.prototype.saveFile = async function(revision, success, error, unloading, overwrite)
  1191. {
  1192. //Safeguard in case saveFile is called from online code in the future
  1193. if (typeof success !== 'function')
  1194. {
  1195. if (typeof unloading === 'function')
  1196. {
  1197. //Call error
  1198. unloading({message: 'This is a bug, please report!'}); //Original draw.io function parameters are (title, revision, success, error, useCurrentData)
  1199. }
  1200. return;
  1201. }
  1202. if (!this.savingFile)
  1203. {
  1204. var fn = mxUtils.bind(this, function()
  1205. {
  1206. var doSave = mxUtils.bind(this, function(data, enc)
  1207. {
  1208. var savedData = this.data;
  1209. // Makes sure no changes get lost while the file is saved
  1210. this.setShadowModified(false);
  1211. this.savingFile = true;
  1212. var errorWrapper = mxUtils.bind(this, function(e)
  1213. {
  1214. this.savingFile = false;
  1215. if (error != null)
  1216. {
  1217. error(e);
  1218. }
  1219. });
  1220. this.unwatchedSaves = true; //Multiple saves doesn't call watch the same number, so use a boolean and check for changes
  1221. electron.request({
  1222. action: 'saveFile',
  1223. fileObject: this.fileObject,
  1224. defEnc: enc,
  1225. data: data,
  1226. origStat: this.stat,
  1227. overwrite: overwrite
  1228. }, mxUtils.bind(this, function(stat)
  1229. {
  1230. //No changes during the saving process?
  1231. this.setModified(this.getShadowModified());
  1232. this.savingFile = false;
  1233. var lastDesc = this.stat;
  1234. this.stat = stat;
  1235. this.fileSaved(savedData, lastDesc, mxUtils.bind(this, function()
  1236. {
  1237. this.contentChanged();
  1238. if (success != null)
  1239. {
  1240. success();
  1241. }
  1242. }), error);
  1243. }),
  1244. mxUtils.bind(this, function(errMsg, err)
  1245. {
  1246. if (errMsg == 'empty data')
  1247. {
  1248. this.ui.handleError({message: mxResources.get('errorSavingFile')});
  1249. errorWrapper();
  1250. }
  1251. else if (errMsg == 'conflict')
  1252. {
  1253. this.inConflictState = true;
  1254. errorWrapper();
  1255. }
  1256. else
  1257. {
  1258. errorWrapper(err || errMsg);
  1259. }
  1260. }));
  1261. });
  1262. if (!/(\.png)$/i.test(this.fileObject.name))
  1263. {
  1264. doSave(this.getData());
  1265. }
  1266. else
  1267. {
  1268. var p = this.ui.getPngFileProperties(this.ui.fileNode);
  1269. this.ui.getEmbeddedPng(function(data)
  1270. {
  1271. doSave(atob(data), 'binary');
  1272. }, error, null, p.scale, p.border);
  1273. }
  1274. });
  1275. try
  1276. {
  1277. if (this.fileObject == null)
  1278. {
  1279. var lastDir = localStorage.getItem('.lastSaveDir');
  1280. var name = this.ui.normalizeFilename(this.getTitle(),
  1281. this.constructor == LocalLibrary ? 'xml' : null);
  1282. var ext = null;
  1283. if (name != null)
  1284. {
  1285. var idx = name.lastIndexOf('.');
  1286. if (idx > 0)
  1287. {
  1288. ext = name.substring(idx + 1);
  1289. }
  1290. }
  1291. var path = await requestSync({
  1292. action: 'showSaveDialog',
  1293. defaultPath: (lastDir || (await requestSync('getDocumentsFolder'))) + '/' + name,
  1294. filters: this.ui.createFileSystemFilters(ext)
  1295. });
  1296. if (path != null)
  1297. {
  1298. localStorage.setItem('.lastSaveDir', await requestSync({action: 'dirname', path: path}));
  1299. this.fileObject = new Object();
  1300. this.fileObject.path = path;
  1301. this.fileObject.name = path.replace(/^.*[\\\/]/, '');
  1302. this.fileObject.type = 'utf-8';
  1303. this.title = this.fileObject.name;
  1304. this.addToRecent();
  1305. fn();
  1306. }
  1307. else
  1308. {
  1309. this.ui.spinner.stop();
  1310. }
  1311. }
  1312. else
  1313. {
  1314. fn();
  1315. }
  1316. }
  1317. catch (e)
  1318. {
  1319. error(e);
  1320. }
  1321. }
  1322. };
  1323. LocalFile.prototype.addToRecent = function()
  1324. {
  1325. if (this.fileObject == null) return;
  1326. var title = this.fileObject.path;
  1327. if (title.length > 100)
  1328. {
  1329. title = '...' + title.substr(title.length - 97);
  1330. }
  1331. this.ui.addRecent({id: this.fileObject.path, title: title});
  1332. };
  1333. LocalFile.prototype.saveAs = async function(title, success, error)
  1334. {
  1335. try
  1336. {
  1337. var lastDir = (this.fileObject != null && this.fileObject.path != null) ?
  1338. await requestSync({action: 'dirname', path: this.fileObject.path}) :
  1339. localStorage.getItem('.lastSaveDir');
  1340. var name = this.ui.normalizeFilename(this.getTitle(),
  1341. this.constructor == LocalLibrary ? 'xml' : null);
  1342. var ext = null;
  1343. if (name != null)
  1344. {
  1345. var idx = name.lastIndexOf('.');
  1346. if (idx > 0)
  1347. {
  1348. ext = name.substring(idx + 1);
  1349. }
  1350. }
  1351. var path = await requestSync({
  1352. action: 'showSaveDialog',
  1353. defaultPath: (lastDir || (await requestSync('getDocumentsFolder'))) + '/' + name,
  1354. filters: this.ui.createFileSystemFilters(ext)
  1355. });
  1356. if (path != null)
  1357. {
  1358. localStorage.setItem('.lastSaveDir', await requestSync({action: 'dirname', path: path}));
  1359. this.fileObject = new Object();
  1360. this.fileObject.path = path;
  1361. this.fileObject.name = path.replace(/^.*[\\\/]/, '');
  1362. this.fileObject.type = 'utf-8';
  1363. this.title = this.fileObject.name;
  1364. this.addToRecent();
  1365. this.setEditable(true); //In case original file is read only
  1366. this.save(false, success, error, null, true);
  1367. }
  1368. }
  1369. catch (e)
  1370. {
  1371. error(e);
  1372. }
  1373. };
  1374. LocalFile.prototype.saveDraft = function(data)
  1375. {
  1376. // Save draft only if file is not saved (prevents creating draft file after actual file is saved)
  1377. if (!this.isModified()) return;
  1378. if (this.fileObject == null)
  1379. {
  1380. //Use indexed db for unsaved files
  1381. DrawioFile.prototype.saveDraft.apply(this, arguments);
  1382. }
  1383. else
  1384. {
  1385. electron.request({
  1386. action: 'saveDraft',
  1387. fileObject: this.fileObject,
  1388. data: (data != null) ? data : this.ui.getFileData()
  1389. }, mxUtils.bind(this, function(draftFileName)
  1390. {
  1391. this.fileObject.draftFileName = draftFileName;
  1392. }), mxUtils.bind(this, function(msg, e)
  1393. {
  1394. //TODO Currently ignored, maybe we should retry?
  1395. }));
  1396. }
  1397. };
  1398. LocalFile.prototype.removeDraft = async function()
  1399. {
  1400. try
  1401. {
  1402. if (this.fileObject == null)
  1403. {
  1404. //Use indexed db for unsaved files
  1405. DrawioFile.prototype.removeDraft.apply(this, arguments);
  1406. }
  1407. else if (this.fileObject.draftFileName != null)
  1408. {
  1409. await requestSync({action: 'deleteFile', file: this.fileObject.draftFileName});
  1410. }
  1411. }
  1412. catch (e)
  1413. {
  1414. // ignore
  1415. }
  1416. };
  1417. LocalLibrary.prototype.addToRecent = function()
  1418. {
  1419. // do nothing
  1420. };
  1421. /**
  1422. * Loads the given file handle as a local file.
  1423. */
  1424. App.prototype.createFileSystemFilters = function(defaultExt)
  1425. {
  1426. var ext = [];
  1427. for (var i = 0; i < this.editor.diagramFileTypes.length; i++)
  1428. {
  1429. var obj = {name: mxResources.get(this.editor.diagramFileTypes[i].description) +
  1430. ' (.' + this.editor.diagramFileTypes[i].extension + ')',
  1431. extensions: [this.editor.diagramFileTypes[i].extension]};
  1432. if (this.editor.diagramFileTypes[i].extension == defaultExt)
  1433. {
  1434. ext.splice(0, 0, obj);
  1435. }
  1436. else
  1437. {
  1438. ext.push(obj);
  1439. }
  1440. }
  1441. return ext;
  1442. };
  1443. /**
  1444. * Loads the given file handle as a local file.
  1445. */
  1446. App.prototype.saveFile = function(forceDialog)
  1447. {
  1448. var file = this.getCurrentFile();
  1449. if (file != null)
  1450. {
  1451. if (!forceDialog && file.getTitle() != null)
  1452. {
  1453. file.save(true, mxUtils.bind(this, function()
  1454. {
  1455. if (EditorUi.enableDrafts)
  1456. {
  1457. file.removeDraft();
  1458. }
  1459. file.handleFileSuccess(true);
  1460. }), mxUtils.bind(this, function(err)
  1461. {
  1462. file.handleFileError(err, true);
  1463. }));
  1464. }
  1465. else
  1466. {
  1467. let oldFileObject = file.fileObject;
  1468. file.saveAs(null, mxUtils.bind(this, function()
  1469. {
  1470. if (EditorUi.enableDrafts)
  1471. {
  1472. //Workaround to delete the correct draft as the file object is updated in place
  1473. let curFileObject = file.fileObject;
  1474. file.fileObject = oldFileObject;
  1475. file.removeDraft();
  1476. file.fileObject = curFileObject;
  1477. }
  1478. file.handleFileSuccess(true);
  1479. }), mxUtils.bind(this, function(err)
  1480. {
  1481. file.handleFileError(err, true);
  1482. }));
  1483. }
  1484. }
  1485. };
  1486. /**
  1487. * Translates this point by the given vector.
  1488. */
  1489. App.prototype.saveLibrary = function(name, images, file, mode, noSpin, noReload, fn)
  1490. {
  1491. mode = (mode != null) ? mode : this.mode;
  1492. noSpin = (noSpin != null) ? noSpin : false;
  1493. noReload = (noReload != null) ? noReload : false;
  1494. var xml = this.createLibraryDataFromImages(images);
  1495. var error = mxUtils.bind(this, function(resp)
  1496. {
  1497. this.spinner.stop();
  1498. if (fn != null)
  1499. {
  1500. fn();
  1501. }
  1502. // Null means cancel by user and is ignored
  1503. if (resp != null)
  1504. {
  1505. this.handleError(resp, mxResources.get('errorSavingFile'));
  1506. }
  1507. });
  1508. // Handles special case for local libraries
  1509. if (file == null)
  1510. {
  1511. file = new LocalLibrary(this, xml, name);
  1512. }
  1513. if (noSpin || this.spinner.spin(document.body, mxResources.get('saving')))
  1514. {
  1515. file.setData(xml);
  1516. var doSave = mxUtils.bind(this, function()
  1517. {
  1518. file.save(true, mxUtils.bind(this, function(resp)
  1519. {
  1520. this.spinner.stop();
  1521. this.hideDialog(true);
  1522. if (!noReload)
  1523. {
  1524. this.libraryLoaded(file, images)
  1525. }
  1526. if (fn != null)
  1527. {
  1528. fn();
  1529. }
  1530. }), error);
  1531. });
  1532. if (name != file.getTitle())
  1533. {
  1534. var oldHash = file.getHash();
  1535. file.rename(name, mxUtils.bind(this, function(resp)
  1536. {
  1537. // Change hash in stored settings
  1538. if (file.constructor != LocalLibrary && oldHash != file.getHash())
  1539. {
  1540. mxSettings.removeCustomLibrary(oldHash);
  1541. mxSettings.addCustomLibrary(file.getHash());
  1542. }
  1543. // Workaround for library files changing hash so
  1544. // the old library cannot be removed from the
  1545. // sidebar using the updated file in libraryLoaded
  1546. this.removeLibrarySidebar(oldHash);
  1547. doSave();
  1548. }), error)
  1549. }
  1550. else
  1551. {
  1552. doSave();
  1553. }
  1554. }
  1555. };
  1556. App.prototype.checkForUpdates = function()
  1557. {
  1558. electron.sendMessage('checkForUpdates');
  1559. };
  1560. App.prototype.toggleSpellCheck = function()
  1561. {
  1562. electron.sendMessage('toggleSpellCheck');
  1563. };
  1564. App.prototype.toggleStoreBkp = function()
  1565. {
  1566. electron.sendMessage('toggleStoreBkp');
  1567. };
  1568. App.prototype.toggleGoogleFonts = function()
  1569. {
  1570. electron.sendMessage('toggleGoogleFonts');
  1571. };
  1572. App.prototype.openDevTools = function()
  1573. {
  1574. electron.sendMessage('openDevTools');
  1575. };
  1576. App.prototype.desktopZoomIn = function()
  1577. {
  1578. electron.sendMessage('zoomIn');
  1579. };
  1580. App.prototype.desktopZoomOut = function()
  1581. {
  1582. electron.sendMessage('zoomOut');
  1583. };
  1584. App.prototype.desktopResetZoom = function()
  1585. {
  1586. electron.sendMessage('resetZoom');
  1587. };
  1588. /**
  1589. * Copies the given cells and XML to the clipboard as an embedded image.
  1590. */
  1591. EditorUi.prototype.writeImageToClipboard = async function(dataUrl, w, h, error)
  1592. {
  1593. try
  1594. {
  1595. await requestSync({
  1596. action: 'clipboardAction',
  1597. method: 'writeImage',
  1598. data: {dataUrl: dataUrl, w: w, h: h}
  1599. });
  1600. }
  1601. catch (e)
  1602. {
  1603. error(e);
  1604. }
  1605. };
  1606. /**
  1607. * Updates action states depending on the selection.
  1608. */
  1609. var editorUiUpdateActionStates = EditorUi.prototype.updateActionStates;
  1610. EditorUi.prototype.updateActionStates = function()
  1611. {
  1612. editorUiUpdateActionStates.apply(this, arguments);
  1613. var file = this.getCurrentFile();
  1614. var syncEnabled = file != null && file.fileObject != null;
  1615. this.actions.get('synchronize').setEnabled(syncEnabled);
  1616. };
  1617. EditorUi.prototype.saveLocalFile = function(data, filename, mimeType, base64Encoded, format, allowBrowser)
  1618. {
  1619. this.saveData(filename, format, data, mimeType, base64Encoded);
  1620. };
  1621. EditorUi.prototype.saveRequest = function(filename, format, fn, data, base64Encoded, mimeType)
  1622. {
  1623. var xhr = fn(null, '1');
  1624. if (xhr != null && this.spinner.spin(document.body, mxResources.get('saving')))
  1625. {
  1626. xhr.send(mxUtils.bind(this, function()
  1627. {
  1628. this.spinner.stop();
  1629. if (xhr.getStatus() >= 200 && xhr.getStatus() <= 299)
  1630. {
  1631. this.saveData(filename, format, xhr.getText(), mimeType, true);
  1632. }
  1633. else
  1634. {
  1635. this.handleError({message: mxResources.get('errorSavingFile')});
  1636. }
  1637. }), mxUtils.bind(this, function(resp)
  1638. {
  1639. this.spinner.stop();
  1640. this.handleError(resp);
  1641. }));
  1642. }
  1643. };
  1644. function mxElectronRequest(reqType, reqObj)
  1645. {
  1646. this.reqType = reqType;
  1647. this.reqObj = reqObj;
  1648. };
  1649. //Extends mxXmlRequest
  1650. mxUtils.extend(mxElectronRequest, mxXmlRequest);
  1651. mxElectronRequest.prototype.send = function(callback, error)
  1652. {
  1653. electron.sendMessage(this.reqType, this.reqObj);
  1654. electron.listenOnce(this.reqType + '-success', (data) =>
  1655. {
  1656. this.response = data;
  1657. callback();
  1658. electron.sendMessage(this.reqType + '-finalize');
  1659. })
  1660. electron.listenOnce(this.reqType + '-error', (err) =>
  1661. {
  1662. this.hasError = true;
  1663. error(err);
  1664. electron.sendMessage(this.reqType + '-finalize');
  1665. })
  1666. };
  1667. mxElectronRequest.prototype.getStatus = function()
  1668. {
  1669. return this.hasError? 500 : 200;
  1670. }
  1671. mxElectronRequest.prototype.getText = function()
  1672. {
  1673. return this.response;
  1674. }
  1675. // Direct export to pdf
  1676. EditorUi.prototype.createDownloadRequest = function(filename, format, ignoreSelection, base64,
  1677. transparent, currentPage, scale, border, grid, includeXml, pageRange, w, h, crop, margin,
  1678. fit, sheetsAcross, sheetsDown)
  1679. {
  1680. var params = this.downloadRequestBuilder(filename, format, ignoreSelection, base64,
  1681. transparent, currentPage, scale, border, grid, includeXml, pageRange, w, h, crop,
  1682. margin, fit, sheetsAcross, sheetsDown);
  1683. return new mxElectronRequest('export', params);
  1684. };
  1685. var origSetAutosave = Editor.prototype.setAutosave;
  1686. Editor.prototype.setAutosave = function(value)
  1687. {
  1688. autoSaveEnabled = value;
  1689. return origSetAutosave.apply(this, arguments);
  1690. }
  1691. //Export Dialog Pdf case
  1692. var origExportFile = ExportDialog.exportFile;
  1693. ExportDialog.exportFile = function(editorUi, name, format, bg, s, b, dpi)
  1694. {
  1695. var graph = editorUi.editor.graph;
  1696. if (format == 'xml' || format == 'svg')
  1697. {
  1698. return origExportFile.apply(this, arguments);
  1699. }
  1700. else
  1701. {
  1702. var data = editorUi.getFileData(true, null, null, null, null, true);
  1703. var bounds = graph.getGraphBounds();
  1704. var w = Math.floor(bounds.width * s / graph.view.scale);
  1705. var h = Math.floor(bounds.height * s / graph.view.scale);
  1706. editorUi.hideDialog();
  1707. if ((format == 'png' || format == 'jpg' || format == 'jpeg') && editorUi.isExportToCanvas())
  1708. {
  1709. if (format == 'png')
  1710. {
  1711. editorUi.exportImage(s, bg == null || bg == 'none', true,
  1712. false, false, b, true, false, null, null, dpi);
  1713. }
  1714. else
  1715. {
  1716. editorUi.exportImage(s, false, true, false,
  1717. false, b, true, false, 'jpeg');
  1718. }
  1719. }
  1720. else
  1721. {
  1722. var extras = {globalVars: graph.getExportVariables()};
  1723. if (Graph.translateDiagram)
  1724. {
  1725. extras.diagramLanguage = Graph.diagramLanguage;
  1726. }
  1727. editorUi.saveRequest(name, format,
  1728. function(newTitle, base64)
  1729. {
  1730. return new mxElectronRequest('export', {
  1731. format: format,
  1732. xml: data,
  1733. bg: (bg != null) ? bg : mxConstants.NONE,
  1734. filename: (newTitle != null) ? newTitle : null,
  1735. w: w,
  1736. h: h,
  1737. border: b,
  1738. base64: (base64 || '0'),
  1739. extras: JSON.stringify(extras),
  1740. dpi: dpi > 0? dpi : null
  1741. });
  1742. });
  1743. }
  1744. }
  1745. };
  1746. EditorUi.prototype.saveData = async function(filename, format, data, mimeType, base64Encoded)
  1747. {
  1748. var resume = (this.spinner != null && this.spinner.pause != null) ? this.spinner.pause() : function() {};
  1749. var lastDir = localStorage.getItem('.lastExpDir');
  1750. // Spinner.stop is asynchronous so we must invoke save dialog asynchronously
  1751. // to give the spinner some time to stop spinning
  1752. window.setTimeout(mxUtils.bind(this, async function()
  1753. {
  1754. var dlgConfig = {
  1755. action: 'showSaveDialog',
  1756. defaultPath: (lastDir || (await requestSync('getDocumentsFolder'))) + '/' + filename
  1757. };
  1758. var filters = null;
  1759. switch (format)
  1760. {
  1761. case 'xmlpng':
  1762. case 'png':
  1763. filters = [
  1764. { name: 'PNG Images', extensions: ['png'] }
  1765. ];
  1766. break;
  1767. case 'jpg':
  1768. case 'jpeg':
  1769. filters = [
  1770. { name: 'JPEG Images', extensions: ['jpg', 'jpeg'] }
  1771. ];
  1772. break;
  1773. case 'svg':
  1774. filters = [
  1775. { name: 'SVG Images', extensions: ['svg'] }
  1776. ];
  1777. break;
  1778. case 'pdf':
  1779. filters = [
  1780. { name: 'PDF Documents', extensions: ['pdf'] }
  1781. ];
  1782. break;
  1783. case 'vsdx':
  1784. filters = [
  1785. { name: 'VSDX Documents', extensions: ['vsdx'] }
  1786. ];
  1787. break;
  1788. case 'html':
  1789. filters = [
  1790. { name: 'HTML Documents', extensions: ['html'] }
  1791. ];
  1792. break;
  1793. case 'xml':
  1794. filters = [
  1795. { name: 'XML Documents', extensions: ['xml'] }
  1796. ];
  1797. break;
  1798. case 'txt':
  1799. filters = [
  1800. { name: 'Plain Text', extensions: ['txt'] }
  1801. ];
  1802. break;
  1803. };
  1804. dlgConfig['filters'] = filters;
  1805. //showSaveDialog
  1806. var path = await requestSync(dlgConfig);
  1807. if (path != null)
  1808. {
  1809. localStorage.setItem('.lastExpDir', await requestSync({action: 'dirname', path: path}));
  1810. if (data == null || data.length == 0)
  1811. {
  1812. this.handleError({message: mxResources.get('errorSavingFile')});
  1813. }
  1814. else
  1815. {
  1816. resume();
  1817. electron.request({
  1818. action: 'writeFile',
  1819. path: path,
  1820. data: data,
  1821. enc: (base64Encoded) ? 'base64' : 'utf-8'
  1822. }, mxUtils.bind(this, function ()
  1823. {
  1824. this.spinner.stop();
  1825. }), mxUtils.bind(this, function (e)
  1826. {
  1827. this.spinner.stop();
  1828. this.handleError(e, mxResources.get('errorSavingFile'));
  1829. }));
  1830. }
  1831. }
  1832. }), 50);
  1833. };
  1834. EditorUi.prototype.addBeforeUnloadListener = function() {};
  1835. EditorUi.prototype.loadDesktopLib = function(libPath, success, error)
  1836. {
  1837. this.readGraphFile(mxUtils.bind(this, function(fileEntry, data, stat)
  1838. {
  1839. var library = new DesktopLibrary(this, data, fileEntry);
  1840. this.loadLibrary(library);
  1841. this.showSidebar();
  1842. success(library);
  1843. }), error, libPath);
  1844. };
  1845. })();