1/**
2 * Copyright (c) 2006-2020, JGraph Ltd
3 * Copyright (c) 2006-2020, draw.io AG
4 */
5(function()
6{
7	// Adds scrollbars for menus that exceed the page height
8	var mxPopupMenuShowMenu = mxPopupMenu.prototype.showMenu;
9	mxPopupMenu.prototype.showMenu = function()
10	{
11		mxPopupMenuShowMenu.apply(this, arguments);
12
13		this.div.style.overflowY = 'auto';
14		this.div.style.overflowX = 'hidden';
15		var h0 = Math.max(document.body.clientHeight, document.documentElement.clientHeight);
16		this.div.style.maxHeight = (h0 - 10) + 'px';
17	};
18
19	Menus.prototype.createHelpLink = function(href)
20	{
21		var link = document.createElement('span');
22		link.setAttribute('title', mxResources.get('help'));
23		link.style.cssText = 'color:blue;text-decoration:underline;margin-left:8px;cursor:help;';
24
25		var icon = document.createElement('img');
26		mxUtils.setOpacity(icon, 50);
27		icon.style.height = '16px';
28		icon.style.width = '16px';
29		icon.setAttribute('border', '0');
30		icon.setAttribute('valign', 'bottom');
31		icon.setAttribute('src', Editor.helpImage);
32		link.appendChild(icon);
33
34		mxEvent.addGestureListeners(link, mxUtils.bind(this, function(evt)
35		{
36			this.editorUi.hideCurrentMenu();
37			this.editorUi.openLink(href);
38			mxEvent.consume(evt);
39		}));
40
41		return link;
42	};
43
44	Menus.prototype.addLinkToItem = function(item, href)
45	{
46		if (item != null)
47		{
48			item.firstChild.nextSibling.appendChild(this.createHelpLink(href));
49		}
50	};
51
52	var menusInit = Menus.prototype.init;
53	Menus.prototype.init = function()
54	{
55		menusInit.apply(this, arguments);
56		var editorUi = this.editorUi;
57		var graph = editorUi.editor.graph;
58		var isGraphEnabled = mxUtils.bind(graph, graph.isEnabled);
59		var googleEnabled = ((urlParams['embed'] != '1' && urlParams['gapi'] != '0') ||
60			(urlParams['embed'] == '1' && urlParams['gapi'] == '1')) && mxClient.IS_SVG &&
61			isLocalStorage && (document.documentMode == null || document.documentMode >= 10);
62		var dropboxEnabled = ((urlParams['embed'] != '1' && urlParams['db'] != '0') || (urlParams['embed'] == '1' && urlParams['db'] == '1')) &&
63			mxClient.IS_SVG && (document.documentMode == null || document.documentMode > 9);
64		var oneDriveEnabled = (window.location.hostname == 'www.draw.io' || window.location.hostname == 'test.draw.io' ||
65			window.location.hostname == 'drive.draw.io' || window.location.hostname == 'app.diagrams.net') &&
66			(((urlParams['embed'] != '1' && urlParams['od'] != '0') || (urlParams['embed'] == '1' && urlParams['od'] == '1')) &&
67			!mxClient.IS_IOS && (navigator.userAgent.indexOf('MSIE') < 0 || document.documentMode >= 10));
68		var trelloEnabled = urlParams['tr'] == '1' && mxClient.IS_SVG && (document.documentMode == null ||
69			document.documentMode > 9);
70
71		if (!mxClient.IS_SVG && !editorUi.isOffline())
72		{
73			var img = new Image();
74			img.src = IMAGE_PATH + '/help.png';
75		}
76
77		if (urlParams['noFileMenu'] == '1')
78		{
79			this.defaultMenuItems = this.defaultMenuItems.filter(function(m)
80			{
81				return m != 'file';
82			});
83		}
84
85		editorUi.actions.addAction('new...', function()
86		{
87			var compact = editorUi.isOffline();
88
89			if (!compact && urlParams['newTempDlg'] == '1' && editorUi.mode == App.MODE_GOOGLE)
90			{
91				function driveObjToTempDlg(item)
92				{
93					return {id: item.id, isExt: true, url: item.downloadUrl, title: item.title, imgUrl: item.thumbnailLink,
94							changedBy: item.lastModifyingUserName, lastModifiedOn: item.modifiedDate}
95				};
96
97				var tempDlg = new TemplatesDialog(editorUi, function(templateXml, title, infoObj)
98				{
99					var templateLibs = infoObj.libs, templateClibs = infoObj.clibs;
100
101					editorUi.pickFolder(editorUi.mode, function(folderId)
102					{
103						editorUi.createFile(title, templateXml, (templateLibs != null &&
104							templateLibs.length > 0) ? templateLibs : null, null, function()
105						{
106							editorUi.hideDialog();
107						}, null, folderId, null, (templateClibs != null &&
108							templateClibs.length > 0) ? templateClibs : null);
109					}, editorUi.stateArg == null ||
110						editorUi.stateArg.folderId == null);
111
112				}, null, null, null, 'user', function(callback, error, username)
113				{
114					var oneWeek = new Date();
115					oneWeek.setDate(oneWeek.getDate() - 7);
116
117					editorUi.drive.listFiles(null, oneWeek, username? true : false, function(resp)
118					{
119						var results = [];
120
121						for (var i = 0; i < resp.items.length; i++)
122						{
123							results.push(driveObjToTempDlg(resp.items[i]));
124						}
125
126						callback(results);
127					}, error)
128				}, function(str, callback, error, username)
129				{
130					editorUi.drive.listFiles(str, null, username? true : false, function(resp)
131					{
132						var results = [];
133
134						for (var i = 0; i < resp.items.length; i++)
135						{
136							results.push(driveObjToTempDlg(resp.items[i]));
137						}
138
139						callback(results);
140					}, error)
141				}, function(obj, callback, error)
142				{
143					editorUi.drive.getFile(obj.id, function(file)
144					{
145						callback(file.data);
146					}, error);
147				}, null, null, false, false);
148
149				editorUi.showDialog(tempDlg.container, window.innerWidth, window.innerHeight, true, false, null, false, true);
150
151				return;
152			};
153
154			var dlg = new NewDialog(editorUi, compact, !(editorUi.mode == App.MODE_DEVICE && 'chooseFileSystemEntries' in window));
155
156			editorUi.showDialog(dlg.container, (compact) ? 350 : 620, (compact) ? 70 : 460, true, true, function(cancel)
157			{
158				editorUi.sidebar.hideTooltip();
159
160				if (cancel && editorUi.getCurrentFile() == null)
161				{
162					editorUi.showSplash();
163				}
164			});
165
166			dlg.init();
167		});
168
169		editorUi.actions.put('insertTemplate', new Action(mxResources.get('template') + '...', function()
170		{
171			if (graph.isEnabled() && !graph.isCellLocked(graph.getDefaultParent()))
172			{
173				var dlg = new NewDialog(editorUi, null, false, function(xml)
174				{
175					editorUi.hideDialog();
176
177					if (xml != null)
178					{
179						var insertPoint = editorUi.editor.graph.getFreeInsertPoint();
180						graph.setSelectionCells(editorUi.importXml(xml,
181							Math.max(insertPoint.x, 20),
182							Math.max(insertPoint.y, 20),
183							true, null, null, true));
184						graph.scrollCellToVisible(graph.getSelectionCell());
185					}
186				}, null, null, null, null, null, null, null, null, null, null,
187					false, mxResources.get('insert'));
188
189				editorUi.showDialog(dlg.container, 620, 460, true, true, function()
190				{
191					editorUi.sidebar.hideTooltip();
192				});
193
194				dlg.init();
195			}
196		})).isEnabled = isGraphEnabled;
197
198		var pointAction = editorUi.actions.addAction('points', function()
199		{
200			editorUi.editor.graph.view.setUnit(mxConstants.POINTS);
201		});
202
203		pointAction.setToggleAction(true);
204		pointAction.setSelectedCallback(function() { return editorUi.editor.graph.view.unit == mxConstants.POINTS; });
205
206		var inchAction = editorUi.actions.addAction('inches', function()
207		{
208			editorUi.editor.graph.view.setUnit(mxConstants.INCHES);
209		});
210
211		inchAction.setToggleAction(true);
212		inchAction.setSelectedCallback(function() { return editorUi.editor.graph.view.unit == mxConstants.INCHES; });
213
214		var mmAction = editorUi.actions.addAction('millimeters', function()
215		{
216			editorUi.editor.graph.view.setUnit(mxConstants.MILLIMETERS);
217		});
218
219		mmAction.setToggleAction(true);
220		mmAction.setSelectedCallback(function() { return editorUi.editor.graph.view.unit == mxConstants.MILLIMETERS; });
221
222		var meterAction = editorUi.actions.addAction('meters', function()
223		{
224			editorUi.editor.graph.view.setUnit(mxConstants.METERS);
225		});
226
227		meterAction.setToggleAction(true);
228		meterAction.setSelectedCallback(function() { return editorUi.editor.graph.view.unit == mxConstants.METERS; });
229
230		this.put('units', new Menu(mxUtils.bind(this, function(menu, parent)
231		{
232			this.addMenuItems(menu, ['points', 'inches', 'millimeters', 'meters'], parent);
233		})));
234
235		var rulerAction = editorUi.actions.addAction('ruler', function()
236		{
237			mxSettings.setRulerOn(!mxSettings.isRulerOn());
238			mxSettings.save();
239
240			if (editorUi.ruler != null)
241			{
242				editorUi.ruler.destroy();
243				editorUi.ruler = null;
244				editorUi.refresh();
245			}
246			else
247			{
248				editorUi.ruler = new mxDualRuler(editorUi, editorUi.editor.graph.view.unit);
249				editorUi.refresh();
250			}
251		});
252		rulerAction.setEnabled(editorUi.canvasSupported && document.documentMode != 9);
253		rulerAction.setToggleAction(true);
254		rulerAction.setSelectedCallback(function() { return editorUi.ruler != null; });
255
256        var fullscreenAction = editorUi.actions.addAction('fullscreen', function()
257		{
258			if (urlParams['embedInline'] == '1')
259			{
260				editorUi.setInlineFullscreen(!Editor.inlineFullscreen);
261			}
262			else
263			{
264				if (document.fullscreenElement == null)
265				{
266					document.body.requestFullscreen();
267				}
268				else
269				{
270					document.exitFullscreen();
271				}
272			}
273		});
274		fullscreenAction.visible = urlParams['embedInline'] == '1' ||
275			(document.fullscreenEnabled && document.body.requestFullscreen != null);
276		fullscreenAction.setToggleAction(true);
277		fullscreenAction.setSelectedCallback(function()
278		{
279			return urlParams['embedInline'] == '1' ?
280				Editor.inlineFullscreen :
281				document.fullscreenElement != null;
282		});
283
284		editorUi.actions.addAction('properties...', function()
285		{
286			var dlg = new FilePropertiesDialog(editorUi);
287			editorUi.showDialog(dlg.container, 320, 120, true, true);
288			dlg.init();
289		}).isEnabled = isGraphEnabled;
290
291		if (window.mxFreehand)
292		{
293			editorUi.actions.put('insertFreehand', new Action(mxResources.get('freehand') + '...', function(evt)
294			{
295				if (graph.isEnabled())
296				{
297					if (this.freehandWindow == null)
298					{
299						this.freehandWindow = new FreehandWindow(editorUi, document.body.offsetWidth - 420, 102, 176, 84);
300					}
301
302					if (graph.freehand.isDrawing())
303					{
304						graph.freehand.stopDrawing();
305					}
306					else
307					{
308						graph.freehand.startDrawing();
309					}
310
311					this.freehandWindow.window.setVisible(graph.freehand.isDrawing());
312				}
313			})).isEnabled = function()
314			{
315				return isGraphEnabled() && mxClient.IS_SVG;
316			};
317		}
318
319		editorUi.actions.put('exportXml', new Action(mxResources.get('formatXml') + '...', function()
320		{
321			var div = document.createElement('div');
322			div.style.whiteSpace = 'nowrap';
323			var noPages = editorUi.pages == null || editorUi.pages.length <= 1;
324
325			var hd = document.createElement('h3');
326			mxUtils.write(hd, mxResources.get('formatXml'));
327			hd.style.cssText = 'width:100%;text-align:center;margin-top:0px;margin-bottom:4px';
328			div.appendChild(hd);
329
330			var selection = editorUi.addCheckbox(div, mxResources.get('selectionOnly'),
331				false, graph.isSelectionEmpty());
332			var compressed = editorUi.addCheckbox(div, mxResources.get('compressed'), true);
333			var pages = editorUi.addCheckbox(div, mxResources.get('allPages'), !noPages, noPages);
334			pages.style.marginBottom = '16px';
335
336			mxEvent.addListener(selection, 'change', function()
337			{
338				if (selection.checked)
339				{
340					pages.setAttribute('disabled', 'disabled');
341				}
342				else
343				{
344					pages.removeAttribute('disabled');
345				}
346			});
347
348			var dlg = new CustomDialog(editorUi, div, mxUtils.bind(this, function()
349			{
350				editorUi.downloadFile('xml', !compressed.checked, null,
351					!selection.checked, noPages || !pages.checked);
352			}), null, mxResources.get('export'));
353
354			editorUi.showDialog(dlg.container, 300, 200, true, true);
355		}));
356
357		if (Editor.enableExportUrl)
358		{
359			editorUi.actions.put('exportUrl', new Action(mxResources.get('url') + '...', function()
360			{
361				editorUi.showPublishLinkDialog(mxResources.get('url'), true, null, null,
362					function(linkTarget, linkColor, allPages, lightbox, editLink, layers, width, height, tags)
363				{
364					var params = [];
365
366					if (tags)
367					{
368						params.push('tags=%7B%7D');
369					}
370
371					var dlg = new EmbedDialog(editorUi, editorUi.createLink(linkTarget, linkColor,
372						allPages, lightbox, editLink, layers, null, true, params));
373					editorUi.showDialog(dlg.container, 450, 240, true, true);
374					dlg.init();
375				});
376			}));
377		}
378
379		editorUi.actions.put('exportHtml', new Action(mxResources.get('formatHtmlEmbedded') + '...', function()
380		{
381			if (editorUi.spinner.spin(document.body, mxResources.get('loading')))
382			{
383				editorUi.getPublicUrl(editorUi.getCurrentFile(), function(url)
384				{
385					editorUi.spinner.stop();
386
387					editorUi.showHtmlDialog(mxResources.get('export'), null, url, function(publicUrl, zoomEnabled,
388						initialZoom, linkTarget, linkColor, fit, allPages, layers, tags, lightbox, editLink)
389					{
390						editorUi.createHtml(publicUrl, zoomEnabled, initialZoom, linkTarget, linkColor, fit, allPages,
391							layers, tags, lightbox, editLink, mxUtils.bind(this, function(html, scriptTag)
392							{
393								var basename = editorUi.getBaseFilename(allPages);
394								var result = '<!--[if IE]><meta http-equiv="X-UA-Compatible" content="IE=5,IE=9" ><![endif]-->\n' +
395									'<!DOCTYPE html>\n<html>\n<head>\n<title>' + mxUtils.htmlEntities(basename) + '</title>\n' +
396									'<meta charset="utf-8"/>\n</head>\n<body>' + html + '\n' + scriptTag + '\n</body>\n</html>';
397								editorUi.saveData(basename + ((basename.substring(basename.lenth - 7) ==
398									'.drawio') ? '' : '.drawio') + '.html', 'html', result, 'text/html');
399							}));
400					});
401				});
402			}
403		}));
404
405		editorUi.actions.put('exportPdf', new Action(mxResources.get('formatPdf') + '...', function()
406		{
407			if (!EditorUi.isElectronApp && (editorUi.isOffline() || editorUi.printPdfExport))
408			{
409				// Export PDF action for chrome OS (same as print with different dialog title)
410				editorUi.showDialog(new PrintDialog(editorUi, mxResources.get('formatPdf')).container, 360,
411						(editorUi.pages != null && editorUi.pages.length > 1 && (editorUi.editor.editable ||
412						urlParams['hide-pages'] != '1')) ?
413						450 : 370, true, true);
414			}
415			else
416			{
417				var noPages = editorUi.pages == null || editorUi.pages.length <= 1;
418				var div = document.createElement('div');
419				div.style.whiteSpace = 'nowrap';
420
421				var hd = document.createElement('h3');
422				mxUtils.write(hd, mxResources.get('formatPdf'));
423				hd.style.cssText = 'width:100%;text-align:center;margin-top:0px;margin-bottom:4px';
424				div.appendChild(hd);
425
426				var cropEnableFn = function()
427				{
428					if (allPages != this && this.checked)
429					{
430						crop.removeAttribute('disabled');
431						crop.checked = !graph.pageVisible;
432					}
433					else
434					{
435						crop.setAttribute('disabled', 'disabled');
436						crop.checked = false;
437					}
438				};
439
440				var dlgH = 200;
441				var pageCount = 1;
442				var currentPage = null;
443
444				if (editorUi.pdfPageExport && !noPages)
445				{
446					var allPages = editorUi.addRadiobox(div, 'pages', mxResources.get('allPages'), true);
447					var pagesRadio = editorUi.addRadiobox(div, 'pages', mxResources.get('pages') + ':', false, null, true);
448
449					var pagesFromInput = document.createElement('input');
450					pagesFromInput.style.cssText = 'margin:0 8px 0 8px;'
451					pagesFromInput.setAttribute('value', '1');
452					pagesFromInput.setAttribute('type', 'number');
453					pagesFromInput.setAttribute('min', '1');
454					pagesFromInput.style.width = '50px';
455					div.appendChild(pagesFromInput);
456
457					var span = document.createElement('span');
458					mxUtils.write(span, mxResources.get('to'));
459					div.appendChild(span);
460
461					var pagesToInput = pagesFromInput.cloneNode(true);
462					div.appendChild(pagesToInput);
463
464					mxEvent.addListener(pagesFromInput, 'focus', function()
465					{
466						pagesRadio.checked = true;
467					});
468
469					mxEvent.addListener(pagesToInput, 'focus', function()
470					{
471						pagesRadio.checked = true;
472					});
473
474					function validatePageRange()
475					{
476						pagesToInput.value = Math.max(1, Math.min(pageCount, Math.max(parseInt(pagesToInput.value), parseInt(pagesFromInput.value))));
477						pagesFromInput.value = Math.max(1, Math.min(pageCount, Math.min(parseInt(pagesToInput.value), parseInt(pagesFromInput.value))));
478					};
479
480					mxEvent.addListener(pagesFromInput, 'change', validatePageRange);
481					mxEvent.addListener(pagesToInput, 'change', validatePageRange);
482
483					if (editorUi.pages != null)
484					{
485						pageCount = editorUi.pages.length;
486
487						if (editorUi.currentPage != null)
488						{
489							for (var i = 0; i < editorUi.pages.length; i++)
490							{
491								if (editorUi.currentPage == editorUi.pages[i])
492								{
493									currentPage = i + 1;
494									pagesFromInput.value = currentPage;
495									pagesToInput.value = currentPage;
496									break;
497								}
498							}
499						}
500					}
501
502					pagesFromInput.setAttribute('max', pageCount);
503					pagesToInput.setAttribute('max', pageCount);
504					mxUtils.br(div);
505
506					var selection = editorUi.addRadiobox(div, 'pages', mxResources.get('selectionOnly'), false, graph.isSelectionEmpty());
507					var crop = editorUi.addCheckbox(div, mxResources.get('crop'), false, true);
508					var grid = editorUi.addCheckbox(div, mxResources.get('grid'), false, false);
509
510					mxEvent.addListener(allPages, 'change', cropEnableFn);
511					mxEvent.addListener(pagesRadio, 'change', cropEnableFn);
512					mxEvent.addListener(selection, 'change', cropEnableFn);
513					dlgH += 64;
514				}
515				else
516				{
517					var selection = editorUi.addCheckbox(div, mxResources.get('selectionOnly'),
518							false, graph.isSelectionEmpty());
519					var crop = editorUi.addCheckbox(div, mxResources.get('crop'),
520							!graph.pageVisible || !editorUi.pdfPageExport,
521							!editorUi.pdfPageExport);
522					var grid = editorUi.addCheckbox(div, mxResources.get('grid'), false, false);
523
524					// Crop is only enabled if selection only is selected
525					if (!editorUi.pdfPageExport)
526					{
527						mxEvent.addListener(selection, 'change', cropEnableFn);
528					}
529				}
530
531				var isDrawioWeb = !mxClient.IS_CHROMEAPP && !EditorUi.isElectronApp &&
532					editorUi.getServiceName() == 'draw.io';
533
534				var transparentBkg = null, include = null;
535
536				if (EditorUi.isElectronApp || isDrawioWeb)
537				{
538					include = editorUi.addCheckbox(div, mxResources.get('includeCopyOfMyDiagram'),
539						Editor.defaultIncludeDiagram);
540					dlgH += 30;
541				}
542
543				if (isDrawioWeb)
544				{
545					transparentBkg = editorUi.addCheckbox(div,
546							mxResources.get('transparentBackground'), false);
547
548					dlgH += 30;
549				}
550
551				var dlg = new CustomDialog(editorUi, div, mxUtils.bind(this, function()
552				{
553					var pageRange = null;
554
555					if (!noPages)
556					{
557						var from = parseInt(pagesFromInput.value);
558						var to = parseInt(pagesToInput.value);
559						pageRange = (!allPages.checked &&
560							(from != currentPage || to != currentPage)) ?
561							{from: Math.max(0, Math.min(pageCount - 1, from - 1)),
562							to: Math.max(0, Math.min(pageCount - 1, to - 1))} : null;
563					}
564
565					editorUi.downloadFile('pdf', null, null, !selection.checked,
566						noPages? true : !allPages.checked && pageRange == null,
567						!crop.checked, transparentBkg != null && transparentBkg.checked, null,
568						null, grid.checked, include != null && include.checked, pageRange);
569				}), null, mxResources.get('export'));
570				editorUi.showDialog(dlg.container, 300, dlgH, true, true);
571			}
572		}));
573
574		editorUi.actions.addAction('open...', function()
575		{
576			editorUi.pickFile();
577		});
578
579		editorUi.actions.addAction('close', function()
580		{
581			var currentFile = editorUi.getCurrentFile();
582
583			function fn()
584			{
585				if (currentFile != null)
586				{
587					currentFile.removeDraft();
588				}
589
590				editorUi.fileLoaded(null);
591			};
592
593			if (currentFile != null && currentFile.isModified())
594			{
595				editorUi.confirm(mxResources.get('allChangesLost'), null, fn,
596					mxResources.get('cancel'), mxResources.get('discardChanges'));
597			}
598			else
599			{
600				fn();
601			}
602		});
603
604		editorUi.actions.addAction('editShape...', mxUtils.bind(this, function()
605		{
606			var cells = graph.getSelectionCells();
607
608			if (graph.getSelectionCount() == 1)
609			{
610				var cell = graph.getSelectionCell();
611				var state = graph.view.getState(cell);
612
613				if (state != null && state.shape != null && state.shape.stencil != null)
614				{
615			    	var dlg = new EditShapeDialog(editorUi, cell, mxResources.get('editShape') + ':', 630, 400);
616					editorUi.showDialog(dlg.container, 640, 480, true, false);
617					dlg.init();
618				}
619			}
620		}));
621
622		editorUi.actions.addAction('revisionHistory...', function()
623		{
624			if (!editorUi.isRevisionHistorySupported())
625			{
626				editorUi.showError(mxResources.get('error'), mxResources.get('notAvailable'), mxResources.get('ok'));
627			}
628			else if (editorUi.spinner.spin(document.body, mxResources.get('loading')))
629			{
630				editorUi.getRevisions(mxUtils.bind(this, function(revs, restoreFn)
631				{
632					editorUi.spinner.stop();
633					var dlg = new RevisionDialog(editorUi, revs, restoreFn);
634					editorUi.showDialog(dlg.container, 640, 480, true, true);
635					dlg.init();
636				}), mxUtils.bind(this, function(err)
637				{
638					editorUi.handleError(err);
639				}));
640			}
641		});
642
643		editorUi.actions.addAction('createRevision', function()
644		{
645			editorUi.actions.get('save').funct();
646		}, null, null, Editor.ctrlKey + '+S');
647
648		var action = editorUi.actions.addAction('synchronize', function()
649		{
650			editorUi.synchronizeCurrentFile(DrawioFile.SYNC == 'none');
651		}, null, null, 'Alt+Shift+S');
652
653		// Changes the label if synchronization is disabled
654		if (DrawioFile.SYNC == 'none')
655		{
656			action.label = mxResources.get('refresh');
657		}
658
659		editorUi.actions.addAction('upload...', function()
660		{
661			var file = editorUi.getCurrentFile();
662
663			if (file != null)
664			{
665				// Data is pulled from global variable after tab loads
666				// LATER: Change to use message passing to deal with potential cross-domain
667				window.drawdata = editorUi.getFileData();
668				var filename = (file.getTitle() != null) ? file.getTitle() : editorUi.defaultFilename;
669				editorUi.openLink(window.location.protocol + '//' + window.location.host + '/?create=drawdata&' +
670						((editorUi.mode == App.MODE_DROPBOX) ? 'mode=dropbox&' : '') +
671						'title=' + encodeURIComponent(filename), null, true);
672			}
673		});
674
675		if (typeof(MathJax) !== 'undefined')
676		{
677			var action = editorUi.actions.addAction('mathematicalTypesetting', function()
678			{
679				var change = new ChangePageSetup(editorUi);
680				change.ignoreColor = true;
681				change.ignoreImage = true;
682				change.mathEnabled = !editorUi.isMathEnabled();
683
684				graph.model.execute(change);
685			});
686
687			action.setToggleAction(true);
688			action.setSelectedCallback(function() { return editorUi.isMathEnabled(); });
689			action.isEnabled = isGraphEnabled;
690		}
691
692		if (isLocalStorage)
693		{
694			var action = editorUi.actions.addAction('showStartScreen', function()
695			{
696				mxSettings.setShowStartScreen(!mxSettings.getShowStartScreen());
697				mxSettings.save();
698			});
699
700			action.setToggleAction(true);
701			action.setSelectedCallback(function() { return mxSettings.getShowStartScreen(); });
702		}
703
704		var autosaveAction = editorUi.actions.addAction('autosave', function()
705		{
706			editorUi.editor.setAutosave(!editorUi.editor.autosave);
707		});
708
709		autosaveAction.setToggleAction(true);
710		autosaveAction.setSelectedCallback(function()
711		{
712			return autosaveAction.isEnabled() && editorUi.editor.autosave;
713		});
714
715		editorUi.actions.addAction('editGeometry...', function()
716		{
717			var cells = graph.getSelectionCells();
718			var vertices = [];
719
720			for (var i = 0; i < cells.length; i++)
721			{
722				if (graph.getModel().isVertex(cells[i]))
723				{
724					vertices.push(cells[i]);
725				}
726			}
727
728			if (vertices.length > 0)
729			{
730				var dlg = new EditGeometryDialog(editorUi, vertices);
731				editorUi.showDialog(dlg.container, 200, 270, true, true);
732				dlg.init();
733			}
734		}, null, null, Editor.ctrlKey + '+Shift+M');
735
736		var currentStyle = null;
737
738		editorUi.actions.addAction('copyStyle', function()
739		{
740			if (graph.isEnabled() && !graph.isSelectionEmpty())
741			{
742				currentStyle = graph.copyStyle(graph.getSelectionCell())
743			}
744		}, null, null, Editor.ctrlKey + '+Shift+C');
745
746		editorUi.actions.addAction('pasteStyle', function()
747		{
748			if (graph.isEnabled() && !graph.isSelectionEmpty() && currentStyle != null)
749			{
750				graph.pasteStyle(currentStyle, graph.getSelectionCells())
751			}
752		}, null, null, Editor.ctrlKey + '+Shift+V');
753
754		editorUi.actions.put('pageBackgroundImage', new Action(mxResources.get('backgroundImage') + '...', function()
755		{
756			if (!editorUi.isOffline())
757			{
758				var apply = function(image)
759				{
760					editorUi.setBackgroundImage(image);
761				};
762
763				var dlg = new BackgroundImageDialog(editorUi, apply);
764				editorUi.showDialog(dlg.container, 320, 170, true, true);
765				dlg.init();
766			}
767		}));
768
769		editorUi.actions.put('exportSvg', new Action(mxResources.get('formatSvg') + '...', function()
770		{
771			editorUi.showExportDialog(mxResources.get('formatSvg'), true, mxResources.get('export'),
772				'https://www.diagrams.net/doc/faq/export-diagram',
773				mxUtils.bind(this, function(scale, transparentBackground, ignoreSelection,
774					addShadow, editable, embedImages, border, cropImage, currentPage,
775					linkTarget, grid, keepTheme, exportType, embedFonts)
776				{
777					var val = parseInt(scale);
778
779					if (!isNaN(val) && val > 0)
780					{
781						editorUi.exportSvg(val / 100, transparentBackground, ignoreSelection,
782							addShadow, editable, embedImages, border, !cropImage, false,
783							linkTarget, keepTheme, exportType, embedFonts);
784					}
785				}), true, null, 'svg', true);
786		}));
787
788		editorUi.actions.put('exportPng', new Action(mxResources.get('formatPng') + '...', function()
789		{
790			if (editorUi.isExportToCanvas())
791			{
792				editorUi.showExportDialog(mxResources.get('image'), false, mxResources.get('export'),
793					'https://www.diagrams.net/doc/faq/export-diagram',
794					mxUtils.bind(this, function(scale, transparentBackground, ignoreSelection, addShadow, editable,
795						embedImages, border, cropImage, currentPage, dummy, grid, keepTheme, exportType)
796					{
797						var val = parseInt(scale);
798
799						if (!isNaN(val) && val > 0)
800						{
801							editorUi.exportImage(val / 100, transparentBackground, ignoreSelection,
802								addShadow, editable, border, !cropImage, false, null, grid, null,
803								keepTheme, exportType);
804						}
805					}), true, Editor.defaultIncludeDiagram, 'png', true);
806			}
807			else if (!editorUi.isOffline() && (!mxClient.IS_IOS || !navigator.standalone))
808			{
809				editorUi.showRemoteExportDialog(mxResources.get('export'), null, mxUtils.bind(this, function(ignoreSelection, editable, transparent, scale, border)
810				{
811					editorUi.downloadFile((editable) ? 'xmlpng' : 'png', null, null, ignoreSelection, null, null, transparent, scale, border);
812				}), false, true);
813			}
814		}));
815
816		editorUi.actions.put('exportJpg', new Action(mxResources.get('formatJpg') + '...', function()
817		{
818			if (editorUi.isExportToCanvas())
819			{
820				editorUi.showExportDialog(mxResources.get('image'), false, mxResources.get('export'),
821					'https://www.diagrams.net/doc/faq/export-diagram',
822					mxUtils.bind(this, function(scale, transparentBackground, ignoreSelection, addShadow, editable,
823						embedImages, border, cropImage, currentPage, dummy, grid, keepTheme, exportType)
824					{
825						var val = parseInt(scale);
826
827						if (!isNaN(val) && val > 0)
828						{
829							editorUi.exportImage(val / 100, false, ignoreSelection,
830								addShadow, false, border, !cropImage, false, 'jpeg',
831								grid, null, keepTheme, exportType);
832						}
833					}), true, false, 'jpeg', true);
834			}
835			else if (!editorUi.isOffline() && (!mxClient.IS_IOS || !navigator.standalone))
836			{
837				editorUi.showRemoteExportDialog(mxResources.get('export'), null, mxUtils.bind(this, function(ignoreSelection, editable, tranaparent, scale, border)
838				{
839					editorUi.downloadFile('jpeg', null, null, ignoreSelection, null, null, null, scale, border);
840				}), true, true);
841			}
842		}));
843
844		action = editorUi.actions.addAction('copyAsImage', mxUtils.bind(this, function()
845		{
846			var cells = mxUtils.sortCells(graph.model.getTopmostCells(graph.getSelectionCells()));
847			var xml = mxUtils.getXml((cells.length == 0) ? editorUi.editor.getGraphXml() : graph.encodeCells(cells));
848			editorUi.copyImage(cells, xml);
849		}));
850
851		// Disabled in Safari as operation is not allowed
852		action.visible = Editor.enableNativeCipboard && editorUi.isExportToCanvas() && !mxClient.IS_SF;
853
854		action = editorUi.actions.put('shadowVisible', new Action(mxResources.get('shadow'), function()
855		{
856			graph.setShadowVisible(!graph.shadowVisible);
857		}));
858		action.setToggleAction(true);
859		action.setSelectedCallback(function() { return graph.shadowVisible; });
860
861		editorUi.actions.put('about', new Action(mxResources.get('about') + ' ' + EditorUi.VERSION + '...', function()
862		{
863			if (editorUi.isOffline() || mxClient.IS_CHROMEAPP || EditorUi.isElectronApp)
864			{
865				editorUi.alert(editorUi.editor.appName + ' ' + EditorUi.VERSION);
866			}
867			else
868			{
869				editorUi.openLink('https://www.diagrams.net/');
870			}
871		}));
872
873		editorUi.actions.addAction('support...', function()
874		{
875			if (EditorUi.isElectronApp)
876			{
877				editorUi.openLink('https://github.com/jgraph/drawio-desktop/wiki/Getting-Support');
878			}
879			else
880			{
881				editorUi.openLink('https://github.com/jgraph/drawio/wiki/Getting-Support');
882			}
883		});
884
885		editorUi.actions.addAction('exportOptionsDisabled...', function()
886		{
887			editorUi.handleError({message: mxResources.get('exportOptionsDisabledDetails')},
888				mxResources.get('exportOptionsDisabled'));
889		});
890
891		editorUi.actions.addAction('keyboardShortcuts...', function()
892		{
893			if (mxClient.IS_SVG && !mxClient.IS_CHROMEAPP && !EditorUi.isElectronApp)
894			{
895				editorUi.openLink('shortcuts.svg');
896			}
897			else
898			{
899				editorUi.openLink('https://viewer.diagrams.net/#Uhttps%3A%2F%2Fviewer.diagrams.net%2Fshortcuts.svg');
900			}
901		});
902
903		editorUi.actions.addAction('feedback...', function()
904		{
905			var dlg = new FeedbackDialog(editorUi);
906			editorUi.showDialog(dlg.container, 610, 360, true, false);
907			dlg.init();
908		});
909
910		editorUi.actions.addAction('quickStart...', function()
911		{
912			editorUi.openLink('https://www.youtube.com/watch?v=Z0D96ZikMkc');
913		});
914
915		editorUi.actions.addAction('forkme', function()
916		{
917			if (EditorUi.isElectronApp)
918			{
919				editorUi.openLink('https://github.com/jgraph/drawio-desktop');
920			}
921			else
922			{
923				editorUi.openLink('https://github.com/jgraph/drawio');
924			}
925		}).label = 'Fork me on GitHub...';
926
927		editorUi.actions.addAction('downloadDesktop...', function()
928		{
929			editorUi.openLink('https://get.diagrams.net/');
930		});
931
932		action = editorUi.actions.addAction('tags', mxUtils.bind(this, function()
933		{
934			if (this.tagsWindow == null)
935			{
936				this.tagsWindow = new TagsWindow(editorUi, document.body.offsetWidth - 400, 60, 212, 200);
937				this.tagsWindow.window.addListener('show', mxUtils.bind(this, function()
938				{
939					editorUi.fireEvent(new mxEventObject('tags'));
940					this.tagsWindow.window.fit();
941				}));
942				this.tagsWindow.window.addListener('hide', function()
943				{
944					editorUi.fireEvent(new mxEventObject('tags'));
945				});
946				this.tagsWindow.window.setVisible(true);
947				editorUi.fireEvent(new mxEventObject('tags'));
948			}
949			else
950			{
951				this.tagsWindow.window.setVisible(!this.tagsWindow.window.isVisible());
952			}
953		}));
954		action.setToggleAction(true);
955		action.setSelectedCallback(mxUtils.bind(this, function() { return this.tagsWindow != null && this.tagsWindow.window.isVisible(); }));
956
957		action = editorUi.actions.addAction('findReplace...', mxUtils.bind(this, function(arg1, evt)
958		{
959			var findReplace = graph.isEnabled() && (evt == null || !mxEvent.isShiftDown(evt));
960			var evtName = (findReplace) ? 'findReplace' : 'find';
961			var name = evtName + 'Window';
962
963			if (this[name] == null)
964			{
965				var w = (findReplace) ? ((uiTheme == 'min') ? 330 : 300) : 240;
966				var h = (findReplace) ? ((uiTheme == 'min') ? 304 : 288) : 170;
967				this[name] = new FindWindow(editorUi,
968					document.body.offsetWidth - (w + 20),
969					100, w, h, findReplace);
970				this[name].window.addListener('show', function()
971				{
972					editorUi.fireEvent(new mxEventObject(evtName));
973				});
974				this[name].window.addListener('hide', function()
975				{
976					editorUi.fireEvent(new mxEventObject(evtName));
977				});
978				this[name].window.setVisible(true);
979			}
980			else
981			{
982				this[name].window.setVisible(!this[name].window.isVisible());
983			}
984		}), null, null, Editor.ctrlKey + '+F');
985		action.setToggleAction(true);
986		action.setSelectedCallback(mxUtils.bind(this, function()
987		{
988			var name = (graph.isEnabled()) ? 'findReplaceWindow' : 'findWindow';
989
990			return this[name] != null && this[name].window.isVisible();
991		}));
992
993		editorUi.actions.put('exportVsdx', new Action(mxResources.get('formatVsdx') + ' (beta)...', function()
994		{
995			var noPages = editorUi.pages == null || editorUi.pages.length <= 1;
996
997			if (noPages)
998			{
999				editorUi.exportVisio();
1000			}
1001			else
1002			{
1003				var div = document.createElement('div');
1004				div.style.whiteSpace = 'nowrap';
1005
1006				var hd = document.createElement('h3');
1007				mxUtils.write(hd, mxResources.get('formatVsdx'));
1008				hd.style.cssText = 'width:100%;text-align:center;margin-top:0px;margin-bottom:4px';
1009				div.appendChild(hd);
1010
1011				var pages = editorUi.addCheckbox(div, mxResources.get('allPages'), !noPages, noPages);
1012				pages.style.marginBottom = '16px';
1013
1014				var dlg = new CustomDialog(editorUi, div, mxUtils.bind(this, function()
1015				{
1016					editorUi.exportVisio(!pages.checked);
1017				}), null, mxResources.get('export'));
1018
1019				editorUi.showDialog(dlg.container, 300, 130, true, true);
1020			}
1021		}));
1022
1023		if (isLocalStorage && localStorage != null && urlParams['embed'] != '1')
1024		{
1025			editorUi.actions.addAction('configuration...', function()
1026			{
1027				// Add help, link button
1028				var value = localStorage.getItem(Editor.configurationKey);
1029
1030				var buttons = [[mxResources.get('reset'), function(evt, input)
1031				{
1032					editorUi.confirm(mxResources.get('areYouSure'), function()
1033					{
1034						try
1035						{
1036							localStorage.removeItem(Editor.configurationKey);
1037
1038							if (mxEvent.isShiftDown(evt))
1039							{
1040								localStorage.removeItem('.drawio-config');
1041								localStorage.removeItem('.mode');
1042							}
1043
1044							editorUi.hideDialog();
1045							editorUi.alert(mxResources.get('restartForChangeRequired'));
1046						}
1047						catch (e)
1048						{
1049							editorUi.handleError(e);
1050						}
1051					});
1052				}]];
1053
1054				if (!EditorUi.isElectronApp)
1055				{
1056					buttons.push([mxResources.get('share'), function(evt, input)
1057					{
1058						if (input.value.length > 0)
1059						{
1060							try
1061							{
1062								var obj = JSON.parse(input.value);
1063								var url = window.location.protocol + '//' + window.location.host +
1064									'/' + editorUi.getSearch() + '#_CONFIG_' +
1065									Graph.compress(JSON.stringify(obj));
1066								var dlg = new EmbedDialog(editorUi, url);
1067								editorUi.showDialog(dlg.container, 450, 240, true);
1068								dlg.init();
1069							}
1070							catch (e)
1071							{
1072								editorUi.handleError(e);
1073							}
1074						}
1075						else
1076						{
1077							editorUi.handleError({message: mxResources.get('invalidInput')});
1078						}
1079					}])
1080				}
1081
1082		    	var dlg = new TextareaDialog(editorUi, mxResources.get('configuration') + ':',
1083		    		(value != null) ? JSON.stringify(JSON.parse(value), null, 2) : '', function(newValue)
1084				{
1085					if (newValue != null)
1086					{
1087						try
1088						{
1089							if (newValue.length > 0)
1090							{
1091								var obj = JSON.parse(newValue);
1092
1093								localStorage.setItem(Editor.configurationKey, JSON.stringify(obj));
1094							}
1095							else
1096							{
1097								localStorage.removeItem(Editor.configurationKey);
1098							}
1099
1100							editorUi.hideDialog();
1101							editorUi.alert(mxResources.get('restartForChangeRequired'));
1102						}
1103						catch (e)
1104						{
1105							editorUi.handleError(e);
1106						}
1107					}
1108				}, null, null, null, null, null, true, null, null,
1109					'https://www.diagrams.net/doc/faq/configure-diagram-editor',
1110					buttons);
1111
1112		    	dlg.textarea.style.width = '600px';
1113		    	dlg.textarea.style.height = '380px';
1114				editorUi.showDialog(dlg.container, 620, 460, true, false);
1115				dlg.init();
1116			});
1117		}
1118
1119		// Adds language menu to options only if localStorage is available for
1120		// storing the choice. We do not want to use cookies for older browsers.
1121		// Note that the URL param lang=XX is available for setting the language
1122		// in older browsers. URL param has precedence over the saved setting.
1123		if (mxClient.IS_CHROMEAPP || isLocalStorage)
1124		{
1125			this.put('language', new Menu(mxUtils.bind(this, function(menu, parent)
1126			{
1127				var addLangItem = mxUtils.bind(this, function (id)
1128				{
1129					var lang = (id == '') ? mxResources.get('automatic') : mxLanguageMap[id];
1130					var item = null;
1131
1132					if (lang != '')
1133					{
1134						item = menu.addItem(lang, null, mxUtils.bind(this, function()
1135						{
1136							mxSettings.setLanguage(id);
1137							mxSettings.save();
1138
1139							// Shows dialog in new language
1140							mxClient.language = id;
1141							mxResources.loadDefaultBundle = false;
1142							mxResources.add(RESOURCE_BASE);
1143
1144							editorUi.alert(mxResources.get('restartForChangeRequired'));
1145						}), parent);
1146
1147						if (id == mxLanguage || (id == '' && mxLanguage == null))
1148						{
1149							menu.addCheckmark(item, Editor.checkmarkImage);
1150						}
1151					}
1152
1153					return item;
1154				});
1155
1156				var item = addLangItem('');
1157				menu.addSeparator(parent);
1158
1159				// LATER: Sort menu by language name
1160				for(var langId in mxLanguageMap)
1161				{
1162					addLangItem(langId);
1163				}
1164			})));
1165
1166			// Extends the menubar with the language menu
1167			var menusCreateMenuBar = Menus.prototype.createMenubar;
1168			Menus.prototype.createMenubar = function(container)
1169			{
1170				var menubar = menusCreateMenuBar.apply(this, arguments);
1171
1172				if (menubar != null && urlParams['noLangIcon'] != '1')
1173				{
1174					var langMenu = this.get('language');
1175
1176					if (langMenu != null)
1177					{
1178						var elt = menubar.addMenu('', langMenu.funct);
1179						elt.setAttribute('title', mxResources.get('language'));
1180						elt.style.width = '16px';
1181						elt.style.paddingTop = '2px';
1182						elt.style.paddingLeft = '4px';
1183						elt.style.zIndex = '1';
1184						elt.style.position = 'absolute';
1185						elt.style.display = 'block';
1186						elt.style.cursor = 'pointer';
1187						elt.style.right = '17px';
1188
1189						if (uiTheme == 'atlas')
1190						{
1191							elt.style.top = '6px';
1192							elt.style.right = '15px';
1193						}
1194						else if (uiTheme == 'min')
1195						{
1196							elt.style.top = '2px';
1197						}
1198						else
1199						{
1200							elt.style.top = '0px';
1201						}
1202
1203						if (EditorUi.isElectronApp)
1204						{
1205							elt.style.right = '95px';
1206						}
1207
1208						var icon = document.createElement('div');
1209						icon.style.backgroundImage = 'url(' + Editor.globeImage + ')';
1210						icon.style.backgroundPosition = 'center center';
1211						icon.style.backgroundRepeat = 'no-repeat';
1212						icon.style.backgroundSize = '19px 19px';
1213						icon.style.position = 'absolute';
1214						icon.style.height = '19px';
1215						icon.style.width = '19px';
1216						icon.style.marginTop = '2px';
1217						icon.style.zIndex = '1';
1218						elt.appendChild(icon);
1219						mxUtils.setOpacity(elt, 40);
1220
1221						if (uiTheme == 'atlas' || uiTheme == 'dark')
1222						{
1223							elt.style.opacity = '0.85';
1224							elt.style.filter = 'invert(100%)';
1225						}
1226
1227						document.body.appendChild(elt);
1228					}
1229				}
1230
1231				return menubar;
1232			};
1233		}
1234
1235		editorUi.customLayoutConfig = [{'layout': 'mxHierarchicalLayout',
1236			'config':
1237			{'orientation': 'west',
1238			'intraCellSpacing': 30,
1239			'interRankCellSpacing': 100,
1240			'interHierarchySpacing': 60,
1241			'parallelEdgeSpacing': 10}}];
1242
1243		// Adds action
1244		editorUi.actions.addAction('runLayout', function()
1245		{
1246	    	var dlg = new TextareaDialog(editorUi, 'Run Layouts:',
1247	    		JSON.stringify(editorUi.customLayoutConfig, null, 2),
1248	    		function(newValue)
1249			{
1250				if (newValue.length > 0)
1251				{
1252					try
1253					{
1254						var layoutList = JSON.parse(newValue);
1255						editorUi.executeLayoutList(layoutList)
1256						editorUi.customLayoutConfig = layoutList;
1257					}
1258					catch (e)
1259					{
1260						editorUi.handleError(e);
1261
1262						if (window.console != null)
1263						{
1264							console.error(e);
1265						}
1266					}
1267				}
1268			}, null, null, null, null, null, true, null, null,
1269				'https://www.diagrams.net/doc/faq/apply-layouts');
1270
1271	    	dlg.textarea.style.width = '600px';
1272	    	dlg.textarea.style.height = '380px';
1273			editorUi.showDialog(dlg.container, 620, 460, true, true);
1274			dlg.init();
1275		});
1276
1277		var layoutMenu = this.get('layout');
1278		var layoutMenuFunct = layoutMenu.funct;
1279
1280		layoutMenu.funct = function(menu, parent)
1281		{
1282			layoutMenuFunct.apply(this, arguments);
1283
1284			menu.addItem(mxResources.get('orgChart'), null, function()
1285			{
1286				var branchOptimizer = null, parentChildSpacingVal = 20, siblingSpacingVal = 20, notExecuted = true;
1287
1288				// Invoked when orgchart code was loaded
1289				var delayed = function()
1290				{
1291					editorUi.loadingOrgChart = false;
1292					editorUi.spinner.stop();
1293
1294					if (typeof mxOrgChartLayout !== 'undefined' && branchOptimizer != null && notExecuted)
1295					{
1296						var graph = editorUi.editor.graph;
1297						var orgChartLayout = new mxOrgChartLayout(graph, branchOptimizer, parentChildSpacingVal, siblingSpacingVal);
1298
1299						var cell = graph.getDefaultParent();
1300
1301						if (graph.model.getChildCount(graph.getSelectionCell()) > 1)
1302						{
1303							cell = graph.getSelectionCell();
1304						}
1305
1306						orgChartLayout.execute(cell);
1307						notExecuted = false;
1308					}
1309				};
1310
1311				// Invoked from dialog
1312				function doLayout()
1313				{
1314					if (typeof mxOrgChartLayout === 'undefined' && !editorUi.loadingOrgChart && !editorUi.isOffline(true))
1315					{
1316						if (editorUi.spinner.spin(document.body, mxResources.get('loading')))
1317						{
1318							editorUi.loadingOrgChart = true;
1319
1320							if (urlParams['dev'] == '1')
1321							{
1322								mxscript('js/orgchart/bridge.min.js', function()
1323								{
1324									mxscript('js/orgchart/bridge.collections.min.js', function()
1325									{
1326										mxscript('js/orgchart/OrgChart.Layout.min.js', function()
1327										{
1328											mxscript('js/orgchart/mxOrgChartLayout.js', delayed);
1329										});
1330									});
1331								});
1332							}
1333							else
1334							{
1335								mxscript('js/extensions.min.js', delayed);
1336							}
1337						}
1338					}
1339					else
1340					{
1341						delayed();
1342					}
1343				};
1344
1345				var div = document.createElement('div');
1346
1347				var title = document.createElement('div');
1348				title.style.marginTop = '6px';
1349				title.style.display = 'inline-block';
1350				title.style.width = '140px';
1351				mxUtils.write(title, mxResources.get('orgChartType') + ': ');
1352
1353				div.appendChild(title);
1354
1355				var typeSelect = document.createElement('select');
1356				typeSelect.style.width = '200px';
1357				typeSelect.style.boxSizing = 'border-box';
1358
1359				//Types are hardcoded here since the code is not loaded yet
1360				var typesArr = [mxResources.get('linear'),
1361					mxResources.get('hanger2'),
1362					mxResources.get('hanger4'),
1363					mxResources.get('fishbone1'),
1364					mxResources.get('fishbone2'),
1365					mxResources.get('1ColumnLeft'),
1366					mxResources.get('1ColumnRight'),
1367					mxResources.get('smart')
1368				];
1369
1370				for (var i = 0; i < typesArr.length; i++)
1371				{
1372					var option = document.createElement('option');
1373					mxUtils.write(option, typesArr[i]);
1374					option.value = i;
1375
1376					if (i == 2)
1377					{
1378						option.setAttribute('selected', 'selected');
1379					}
1380
1381					typeSelect.appendChild(option);
1382				}
1383
1384				mxEvent.addListener(typeSelect, 'change', function()
1385				{
1386					branchOptimizer = typeSelect.value;
1387				});
1388
1389				div.appendChild(typeSelect);
1390
1391				title = document.createElement('div');
1392				title.style.marginTop = '6px';
1393				title.style.display = 'inline-block';
1394				title.style.width = '140px';
1395				mxUtils.write(title, mxResources.get('parentChildSpacing') + ': ');
1396				div.appendChild(title);
1397
1398				var parentChildSpacing = document.createElement('input');
1399				parentChildSpacing.type = 'number';
1400				parentChildSpacing.value = parentChildSpacingVal;
1401				parentChildSpacing.style.width = '200px';
1402				parentChildSpacing.style.boxSizing = 'border-box';
1403				div.appendChild(parentChildSpacing);
1404
1405				mxEvent.addListener(parentChildSpacing, 'change', function()
1406				{
1407					parentChildSpacingVal = parentChildSpacing.value;
1408				});
1409
1410				title = document.createElement('div');
1411				title.style.marginTop = '6px';
1412				title.style.display = 'inline-block';
1413				title.style.width = '140px';
1414				mxUtils.write(title, mxResources.get('siblingSpacing') + ': ');
1415				div.appendChild(title);
1416
1417				var siblingSpacing = document.createElement('input');
1418				siblingSpacing.type = 'number';
1419				siblingSpacing.value = siblingSpacingVal;
1420				siblingSpacing.style.width = '200px';
1421				siblingSpacing.style.boxSizing = 'border-box';
1422				div.appendChild(siblingSpacing);
1423
1424				mxEvent.addListener(siblingSpacing, 'change', function()
1425				{
1426					siblingSpacingVal = siblingSpacing.value;
1427				});
1428
1429				var dlg = new CustomDialog(editorUi, div, function()
1430				{
1431					if (branchOptimizer == null)
1432					{
1433						branchOptimizer = 2;
1434					}
1435
1436					doLayout();
1437				});
1438
1439				editorUi.showDialog(dlg.container, 355, 140, true, true);
1440			}, parent, null, isGraphEnabled());
1441
1442			menu.addSeparator(parent);
1443
1444			menu.addItem(mxResources.get('parallels'), null, mxUtils.bind(this, function()
1445			{
1446				// Keeps parallel edges apart
1447				var layout = new mxParallelEdgeLayout(graph);
1448				layout.checkOverlap = true;
1449				layout.spacing = 20;
1450
1451	    		editorUi.executeLayout(function()
1452	    		{
1453	    			layout.execute(graph.getDefaultParent(), (!graph.isSelectionEmpty()) ?
1454	    				graph.getSelectionCells() : null);
1455	    		}, false);
1456			}), parent);
1457
1458			menu.addSeparator(parent);
1459			editorUi.menus.addMenuItem(menu, 'runLayout', parent, null, null, mxResources.get('apply') + '...');
1460		};
1461
1462		this.put('help', new Menu(mxUtils.bind(this, function(menu, parent)
1463		{
1464			if (!mxClient.IS_CHROMEAPP && editorUi.isOffline())
1465			{
1466				this.addMenuItems(menu, ['about'], parent);
1467			}
1468			else
1469			{
1470				// No translation for menu item since help is english only
1471				var item = menu.addItem('Search:', null, null, parent, null, null, false);
1472				item.style.backgroundColor = Editor.isDarkMode() ? '#505759' : 'whiteSmoke';
1473				item.style.cursor = 'default';
1474
1475				var input = document.createElement('input');
1476				input.setAttribute('type', 'text');
1477				input.setAttribute('size', '25');
1478				input.style.marginLeft = '8px';
1479
1480				mxEvent.addListener(input, 'keydown', mxUtils.bind(this, function(e)
1481				{
1482					var term = mxUtils.trim(input.value);
1483
1484					if (e.keyCode == 13 && term.length > 0)
1485					{
1486						this.editorUi.openLink('https://www.diagrams.net/search?src=' +
1487							EditorUi.isElectronApp? 'DESKTOP' : encodeURIComponent(location.host) +
1488							'&search=' + encodeURIComponent(term));
1489						input.value = '';
1490						EditorUi.logEvent({category: 'SEARCH-HELP', action: 'search', label: term});
1491
1492						window.setTimeout(mxUtils.bind(this, function()
1493						{
1494							this.editorUi.hideCurrentMenu();
1495						}), 0);
1496					}
1497	                else if (e.keyCode == 27)
1498	                {
1499	                    input.value = '';
1500	                }
1501				}));
1502
1503				item.firstChild.nextSibling.appendChild(input);
1504
1505				mxEvent.addGestureListeners(input, function(evt)
1506				{
1507					if (document.activeElement != input)
1508					{
1509						input.focus();
1510					}
1511
1512					mxEvent.consume(evt);
1513				}, function(evt)
1514				{
1515					mxEvent.consume(evt);
1516				}, function(evt)
1517				{
1518					mxEvent.consume(evt);
1519				});
1520
1521				window.setTimeout(function()
1522				{
1523					input.focus();
1524				}, 0);
1525
1526				if (EditorUi.isElectronApp)
1527				{
1528					editorUi.actions.addAction('website...', function()
1529					{
1530						editorUi.openLink('https://www.diagrams.net');
1531					});
1532
1533					editorUi.actions.addAction('check4Updates', function()
1534					{
1535						editorUi.checkForUpdates();
1536					});
1537
1538					console.log('electron help menu');
1539					this.addMenuItems(menu, ['-', 'keyboardShortcuts', 'quickStart',
1540						'website', 'support', '-'], parent);
1541
1542					if (urlParams['disableUpdate'] != '1')
1543					{
1544						this.addMenuItems(menu, ['check4Updates', '-'], parent);
1545					}
1546
1547					this.addMenuItems(menu, ['forkme', '-', 'about'], parent);
1548
1549				}
1550				else
1551				{
1552					this.addMenuItems(menu, ['-', 'keyboardShortcuts', 'quickStart',
1553						'support', '-', 'forkme', 'downloadDesktop', '-', 'about'], parent);
1554				}
1555			}
1556
1557			if (urlParams['test'] == '1')
1558			{
1559				menu.addSeparator(parent);
1560				this.addSubmenu('testDevelop', menu, parent);
1561			}
1562		})));
1563
1564		// Experimental
1565		mxResources.parse('diagramLanguage=Diagram Language');
1566		editorUi.actions.addAction('diagramLanguage...', function()
1567		{
1568			var lang = prompt('Language Code', Graph.diagramLanguage || '');
1569
1570			if (lang != null)
1571			{
1572				Graph.diagramLanguage = (lang.length > 0) ? lang : null;
1573				graph.refresh();
1574			}
1575		});
1576
1577		// Only visible in test mode
1578		if (urlParams['test'] == '1')
1579		{
1580			mxResources.parse('testDevelop=Develop');
1581			mxResources.parse('showBoundingBox=Show bounding box');
1582			mxResources.parse('createSidebarEntry=Create Sidebar Entry');
1583			mxResources.parse('testCheckFile=Check File');
1584			mxResources.parse('testDiff=Diff/Sync');
1585			mxResources.parse('testInspect=Inspect');
1586			mxResources.parse('testShowConsole=Show Console');
1587			mxResources.parse('testXmlImageExport=XML Image Export');
1588			mxResources.parse('testDownloadRtModel=Export RT model');
1589			mxResources.parse('testImportRtModel=Import RT model');
1590
1591			editorUi.actions.addAction('createSidebarEntry', mxUtils.bind(this, function()
1592			{
1593				if (!graph.isSelectionEmpty())
1594				{
1595					var cells = graph.cloneCells(graph.getSelectionCells());
1596					var bbox = graph.getBoundingBoxFromGeometry(cells);
1597					cells = graph.moveCells(cells, -bbox.x, -bbox.y);
1598
1599					editorUi.showTextDialog('Create Sidebar Entry', 'this.addDataEntry(\'tag1 tag2\', ' +
1600						bbox.width + ', ' + bbox.height + ', \'The Title\', \'' +
1601						Graph.compress(mxUtils.getXml(graph.encodeCells(cells))) + '\'),');
1602				}
1603			}));
1604
1605			editorUi.actions.addAction('showBoundingBox', mxUtils.bind(this, function()
1606			{
1607				var b = graph.getGraphBounds();
1608				var tr = graph.view.translate;
1609				var s = graph.view.scale;
1610				graph.insertVertex(graph.getDefaultParent(), null, '',
1611					b.x / s - tr.x, b.y / s - tr.y, b.width / s, b.height / s,
1612					'fillColor=none;strokeColor=red;');
1613			}));
1614
1615			editorUi.actions.addAction('testCheckFile', mxUtils.bind(this, function()
1616			{
1617				var xml = (editorUi.pages != null && editorUi.getCurrentFile() != null) ?
1618					editorUi.getCurrentFile().getAnonymizedXmlForPages(editorUi.pages) : '';
1619
1620		    	var dlg = new TextareaDialog(editorUi, 'Paste Data:', xml,
1621		    		function(newValue)
1622				{
1623					if (newValue.length > 0)
1624					{
1625						try
1626						{
1627							if (newValue.charAt(0) != '<')
1628							{
1629								newValue = Graph.decompress(newValue);
1630								mxLog.debug('See console for uncompressed XML');
1631								console.log('xml', newValue);
1632							}
1633
1634							var doc = mxUtils.parseXml(newValue);
1635							var pages = editorUi.getPagesForNode(doc.documentElement, 'mxGraphModel');
1636
1637							if (pages != null && pages.length > 0)
1638							{
1639								try
1640								{
1641									var checksum = editorUi.getHashValueForPages(pages);
1642									mxLog.debug('Checksum: ', checksum);
1643								}
1644								catch (e)
1645								{
1646									mxLog.debug('Error: ', e.message);
1647								}
1648							}
1649							else
1650							{
1651								mxLog.debug('No pages found for checksum');
1652							}
1653
1654							// Checks for duplicates
1655							function checkModel(node)
1656							{
1657								var pageId = node.parentNode.id;
1658								var all = node.childNodes;
1659								var allIds = {};
1660								var childs = {};
1661								var root = null;
1662								var dups = {};
1663
1664								for (var i = 0; i < all.length; i++)
1665								{
1666									var el = all[i];
1667
1668									if (el.id != null && el.id.length > 0)
1669									{
1670										if (allIds[el.id] == null)
1671										{
1672											allIds[el.id] = el.id;
1673											var pid = el.getAttribute('parent');
1674
1675											if (pid == null)
1676											{
1677												if (root != null)
1678												{
1679													mxLog.debug(pageId + ': Multiple roots: ' + el.id);
1680												}
1681												else
1682												{
1683													root = el.id;
1684												}
1685											}
1686											else
1687											{
1688												if (childs[pid] == null)
1689												{
1690													childs[pid] = [];
1691												}
1692
1693												childs[pid].push(el.id);
1694											}
1695										}
1696										else
1697										{
1698											dups[el.id] = el.id;
1699										}
1700									}
1701								}
1702
1703								if (Object.keys(dups).length > 0)
1704								{
1705									var log = pageId + ': ' + Object.keys(dups).length + ' Duplicates: ' + Object.keys(dups).join(', ');
1706									mxLog.debug(log + ' (see console)');
1707								}
1708								else
1709								{
1710									mxLog.debug(pageId + ': Checked');
1711								}
1712
1713								// Checks tree for cycles
1714								var visited = {};
1715
1716								function visit(id)
1717								{
1718									if (visited[id] == null)
1719									{
1720										visited[id] = true;
1721
1722										if (childs[id] != null)
1723										{
1724											while (childs[id].length > 0)
1725											{
1726												var temp = childs[id].pop();
1727												visit(temp);
1728											}
1729
1730											delete childs[id];
1731										}
1732									}
1733									else
1734									{
1735										mxLog.debug(pageId + ': Visited: ' + id);
1736									}
1737								};
1738
1739								if (root == null)
1740								{
1741									mxLog.debug(pageId + ': No root');
1742								}
1743								else
1744								{
1745									visit(root);
1746
1747									if (Object.keys(visited).length != Object.keys(allIds).length)
1748									{
1749										mxLog.debug(pageId + ': Invalid tree: (see console)');
1750										console.log(pageId + ': Invalid tree', childs);
1751									}
1752								}
1753							};
1754
1755							var roots = doc.getElementsByTagName('root');
1756
1757							for (var i = 0; i < roots.length; i++)
1758							{
1759								checkModel(roots[i]);
1760							}
1761
1762							mxLog.show();
1763						}
1764						catch (e)
1765						{
1766							editorUi.handleError(e);
1767
1768							if (window.console != null)
1769							{
1770								console.error(e);
1771							}
1772						}
1773					}
1774				});
1775
1776		    	dlg.textarea.style.width = '600px';
1777		    	dlg.textarea.style.height = '380px';
1778				editorUi.showDialog(dlg.container, 620, 460, true, true);
1779				dlg.init();
1780			}));
1781
1782			var snapshot = null;
1783
1784			editorUi.actions.addAction('testDiff', mxUtils.bind(this, function()
1785			{
1786				if (editorUi.pages != null)
1787				{
1788					var buttons = [['Snapshot', function(evt, input)
1789					{
1790						snapshot = editorUi.getPagesForNode(mxUtils.parseXml(
1791							editorUi.getFileData(true)).documentElement);
1792						dlg.textarea.value = 'Snapshot updated ' + new Date().toLocaleString();
1793					}], ['Diff', function(evt, input)
1794					{
1795						try
1796						{
1797							dlg.textarea.value = JSON.stringify(editorUi.diffPages(
1798								snapshot, editorUi.pages), null, 2);
1799						}
1800						catch (e)
1801						{
1802							editorUi.handleError(e);
1803						}
1804					}]];
1805
1806			    	var dlg = new TextareaDialog(editorUi, 'Diff/Sync:', '',
1807			    		function(newValue)
1808					{
1809						var file = editorUi.getCurrentFile();
1810
1811						if (newValue.length > 0 && file != null)
1812						{
1813							try
1814							{
1815								var patch = JSON.parse(newValue);
1816								file.patch([patch], null, true);
1817								editorUi.hideDialog();
1818							}
1819							catch (e)
1820							{
1821								editorUi.handleError(e);
1822							}
1823						}
1824					}, null, 'Close', null, null, null, true, null, 'Patch', null, buttons);
1825
1826			    	dlg.textarea.style.width = '600px';
1827			    	dlg.textarea.style.height = '380px';
1828
1829
1830					if (snapshot == null)
1831					{
1832						snapshot = editorUi.getPagesForNode(mxUtils.parseXml(
1833							editorUi.getFileData(true)).documentElement);
1834						dlg.textarea.value = 'Snapshot created ' + new Date().toLocaleString();
1835					}
1836					else
1837					{
1838						dlg.textarea.value = JSON.stringify(editorUi.diffPages(
1839							snapshot, editorUi.pages), null, 2);
1840					}
1841
1842					editorUi.showDialog(dlg.container, 620, 460, true, true);
1843					dlg.init();
1844				}
1845				else
1846				{
1847					editorUi.alert('No pages');
1848				}
1849			}));
1850
1851			editorUi.actions.addAction('testInspect', mxUtils.bind(this, function()
1852			{
1853				console.log(editorUi, graph.getModel());
1854			}));
1855
1856			editorUi.actions.addAction('testXmlImageExport', mxUtils.bind(this, function()
1857			{
1858				var bg = '#ffffff';
1859				var scale = 1;
1860				var b = 1;
1861
1862				var imgExport = new mxImageExport();
1863				var bounds = graph.getGraphBounds();
1864				var vs = graph.view.scale;
1865
1866	        	// New image export
1867				var xmlDoc = mxUtils.createXmlDocument();
1868				var root = xmlDoc.createElement('output');
1869				xmlDoc.appendChild(root);
1870
1871			    // Renders graph. Offset will be multiplied with state's scale when painting state.
1872				var xmlCanvas = new mxXmlCanvas2D(root);
1873				xmlCanvas.translate(Math.floor((b / scale - bounds.x) / vs), Math.floor((b / scale - bounds.y) / vs));
1874				xmlCanvas.scale(scale / vs);
1875
1876				var stateCounter = 0;
1877
1878				var canvasSave = xmlCanvas.save;
1879				xmlCanvas.save = function()
1880				{
1881					stateCounter++;
1882					canvasSave.apply(this, arguments);
1883				};
1884
1885				var canvasRestore = xmlCanvas.restore;
1886				xmlCanvas.restore = function()
1887				{
1888					stateCounter--;
1889					canvasRestore.apply(this, arguments);
1890				};
1891
1892				var exportDrawShape = imgExport.drawShape;
1893				imgExport.drawShape = function(state)
1894				{
1895					mxLog.debug('entering shape', state, stateCounter);
1896					exportDrawShape.apply(this, arguments);
1897					mxLog.debug('leaving shape', state, stateCounter);
1898				};
1899
1900			    imgExport.drawState(graph.getView().getState(graph.model.root), xmlCanvas);
1901
1902				// Puts request data together
1903				var w = Math.ceil(bounds.width * scale / vs + 2 * b);
1904				var h = Math.ceil(bounds.height * scale / vs + 2 * b);
1905
1906				mxLog.show();
1907				mxLog.debug(mxUtils.getXml(root));
1908				mxLog.debug('stateCounter', stateCounter);
1909			}));
1910
1911			editorUi.actions.addAction('testShowConsole', function()
1912			{
1913				if (!mxLog.isVisible())
1914				{
1915					mxLog.show();
1916				}
1917				else
1918				{
1919					mxLog.window.fit();
1920				}
1921
1922				mxLog.window.div.style.zIndex = mxPopupMenu.prototype.zIndex - 2;
1923			});
1924
1925			this.put('testDevelop', new Menu(mxUtils.bind(this, function(menu, parent)
1926			{
1927				this.addMenuItems(menu, ['createSidebarEntry', 'showBoundingBox', '-',
1928					'testCheckFile', 'testDiff', '-', 'testInspect', '-',
1929					'testXmlImageExport', '-', 'testShowConsole'], parent);
1930			})));
1931		}
1932
1933		editorUi.actions.addAction('shapes...', function()
1934		{
1935			if (mxClient.IS_CHROMEAPP || !editorUi.isOffline())
1936			{
1937				editorUi.showDialog(new MoreShapesDialog(editorUi, true).container, 640, (isLocalStorage) ?
1938						((mxClient.IS_IOS) ? 480 : 460) : 440, true, true);
1939			}
1940			else
1941			{
1942				editorUi.showDialog(new MoreShapesDialog(editorUi, false).container, 360, (isLocalStorage) ?
1943						((mxClient.IS_IOS) ? 300 : 280) : 260, true, true);
1944			}
1945		});
1946
1947		editorUi.actions.put('createShape', new Action(mxResources.get('shape') + '...', function(evt)
1948		{
1949			if (graph.isEnabled())
1950			{
1951				var cell = new mxCell('', new mxGeometry(0, 0, 120, 120), editorUi.defaultCustomShapeStyle);
1952				cell.vertex = true;
1953
1954		    	var dlg = new EditShapeDialog(editorUi, cell, mxResources.get('editShape') + ':', 630, 400);
1955				editorUi.showDialog(dlg.container, 640, 480, true, false);
1956				dlg.init();
1957			}
1958		})).isEnabled = isGraphEnabled;
1959
1960		editorUi.actions.put('embedHtml', new Action(mxResources.get('html') + '...', function()
1961		{
1962			if (editorUi.spinner.spin(document.body, mxResources.get('loading')))
1963			{
1964				editorUi.getPublicUrl(editorUi.getCurrentFile(), function(url)
1965				{
1966					editorUi.spinner.stop();
1967
1968					editorUi.showHtmlDialog(mxResources.get('create'), 'https://www.diagrams.net/doc/faq/embed-html-options',
1969						url, function(publicUrl, zoomEnabled, initialZoom, linkTarget, linkColor, fit, allPages, layers, tags, lightbox, editLink)
1970					{
1971						editorUi.createHtml(publicUrl, zoomEnabled, initialZoom, linkTarget, linkColor, fit, allPages,
1972							layers, tags, lightbox, editLink, mxUtils.bind(this, function(html, scriptTag)
1973							{
1974								var dlg = new EmbedDialog(editorUi, html + '\n' + scriptTag, null, null, function()
1975								{
1976									var wnd = window.open();
1977									var doc = wnd.document;
1978
1979									if (doc != null)
1980									{
1981										if (document.compatMode === 'CSS1Compat')
1982										{
1983											doc.writeln('<!DOCTYPE html>');
1984										}
1985
1986										doc.writeln('<html>');
1987										doc.writeln('<head><title>' + encodeURIComponent(mxResources.get('preview')) +
1988											'</title><meta charset="utf-8"></head>');
1989										doc.writeln('<body>');
1990										doc.writeln(html);
1991
1992										var direct = mxClient.IS_IE || mxClient.IS_EDGE || document.documentMode != null;
1993
1994										if (direct)
1995										{
1996											doc.writeln(scriptTag);
1997										}
1998
1999										doc.writeln('</body>');
2000										doc.writeln('</html>');
2001										doc.close();
2002
2003										// Adds script tag after closing page and delay to fix timing issues
2004										if (!direct)
2005										{
2006											var info = wnd.document.createElement('div');
2007											info.marginLeft = '26px';
2008											info.marginTop = '26px';
2009											mxUtils.write(info, mxResources.get('updatingDocument'));
2010
2011											var img = wnd.document.createElement('img');
2012											img.setAttribute('src', window.location.protocol + '//' + window.location.hostname +
2013												'/' + IMAGE_PATH + '/spin.gif');
2014											img.style.marginLeft = '6px';
2015											info.appendChild(img);
2016
2017											wnd.document.body.insertBefore(info, wnd.document.body.firstChild);
2018
2019											window.setTimeout(function()
2020											{
2021												var script = document.createElement('script');
2022												script.type = 'text/javascript';
2023												script.src = /<script.*?src="(.*?)"/.exec(scriptTag)[1];
2024												doc.body.appendChild(script);
2025
2026												info.parentNode.removeChild(info);
2027											}, 20);
2028										}
2029									}
2030									else
2031									{
2032										editorUi.handleError({message: mxResources.get('errorUpdatingPreview')});
2033									}
2034								});
2035								editorUi.showDialog(dlg.container, 450, 240, true, true);
2036								dlg.init();
2037							}));
2038					});
2039				});
2040			}
2041		}));
2042
2043		editorUi.actions.put('liveImage', new Action('Live image...', function()
2044		{
2045			var current = editorUi.getCurrentFile();
2046
2047			if (current != null && editorUi.spinner.spin(document.body, mxResources.get('loading')))
2048			{
2049				editorUi.getPublicUrl(editorUi.getCurrentFile(), function(url)
2050				{
2051					editorUi.spinner.stop();
2052
2053					if (url != null)
2054					{
2055						var dlg = new EmbedDialog(editorUi, '<img src="' + ((current.constructor != DriveFile) ?
2056							url : 'https://drive.google.com/uc?id=' + current.getId()) + '"/>');
2057						editorUi.showDialog(dlg.container, 450, 240, true, true);
2058						dlg.init();
2059					}
2060					else
2061					{
2062						editorUi.handleError({message: mxResources.get('invalidPublicUrl')});
2063					}
2064				});
2065			}
2066		}));
2067
2068		editorUi.actions.put('embedImage', new Action(mxResources.get('image') + '...', function()
2069		{
2070			editorUi.showEmbedImageDialog(function(fit, shadow, retina, lightbox, editLink, layers)
2071			{
2072				if (editorUi.spinner.spin(document.body, mxResources.get('loading')))
2073				{
2074					editorUi.createEmbedImage(fit, shadow, retina, lightbox, editLink, layers, function(result)
2075					{
2076						editorUi.spinner.stop();
2077						var dlg = new EmbedDialog(editorUi, result);
2078						editorUi.showDialog(dlg.container, 450, 240, true, true);
2079						dlg.init();
2080					}, function(err)
2081					{
2082						editorUi.spinner.stop();
2083						editorUi.handleError(err);
2084					});
2085				}
2086			}, mxResources.get('image'), mxResources.get('retina'), editorUi.isExportToCanvas());
2087		}));
2088
2089		editorUi.actions.put('embedSvg', new Action(mxResources.get('formatSvg') + '...', function()
2090		{
2091			editorUi.showEmbedImageDialog(function(fit, shadow, image, lightbox, editLink, layers)
2092			{
2093				if (editorUi.spinner.spin(document.body, mxResources.get('loading')))
2094				{
2095					editorUi.createEmbedSvg(fit, shadow, image, lightbox, editLink, layers, function(result)
2096					{
2097						editorUi.spinner.stop();
2098
2099						var dlg = new EmbedDialog(editorUi, result);
2100						editorUi.showDialog(dlg.container, 450, 240, true, true);
2101						dlg.init();
2102					}, function(err)
2103					{
2104						editorUi.spinner.stop();
2105						editorUi.handleError(err);
2106					});
2107				}
2108			}, mxResources.get('formatSvg'), mxResources.get('image'),
2109				true, 'https://www.diagrams.net/doc/faq/embed-svg.html');
2110		}));
2111
2112		editorUi.actions.put('embedIframe', new Action(mxResources.get('iframe') + '...', function()
2113		{
2114			var bounds = graph.getGraphBounds();
2115
2116			editorUi.showPublishLinkDialog(mxResources.get('iframe'), null, '100%',
2117				Math.ceil(bounds.height / graph.view.scale) + 2,
2118				function(linkTarget, linkColor, allPages, lightbox, editLink, layers, width, height, tags)
2119			{
2120				if (editorUi.spinner.spin(document.body, mxResources.get('loading')))
2121				{
2122					editorUi.getPublicUrl(editorUi.getCurrentFile(), function(url)
2123					{
2124						editorUi.spinner.stop();
2125						var params = [];
2126
2127						if (tags)
2128						{
2129							params.push('tags=%7B%7D');
2130						}
2131
2132						var dlg = new EmbedDialog(editorUi, '<iframe frameborder="0" style="width:' + width +
2133							';height:' + height + ';" src="' + editorUi.createLink(linkTarget, linkColor,
2134							allPages, lightbox, editLink, layers, url, null, params) + '"></iframe>');
2135						editorUi.showDialog(dlg.container, 450, 240, true, true);
2136						dlg.init();
2137					});
2138				}
2139			}, true);
2140		}));
2141
2142		editorUi.actions.put('embedNotion', new Action(mxResources.get('notion') + '...', function()
2143		{
2144			editorUi.showPublishLinkDialog(mxResources.get('notion'), null, null, null,
2145				function(linkTarget, linkColor, allPages, lightbox, editLink, layers, width, height, tags)
2146			{
2147				if (editorUi.spinner.spin(document.body, mxResources.get('loading')))
2148				{
2149					editorUi.getPublicUrl(editorUi.getCurrentFile(), function(url)
2150					{
2151						editorUi.spinner.stop();
2152						var params = ['border=0'];
2153
2154						if (tags)
2155						{
2156							params.push('tags=%7B%7D');
2157						}
2158
2159						var dlg = new EmbedDialog(editorUi, editorUi.createLink(linkTarget, linkColor,
2160							allPages, lightbox, editLink, layers, url, null, params, true));
2161						editorUi.showDialog(dlg.container, 450, 240, true, true);
2162						dlg.init();
2163					});
2164				}
2165			}, true);
2166		}));
2167
2168		editorUi.actions.put('publishLink', new Action(mxResources.get('link') + '...', function()
2169		{
2170			editorUi.showPublishLinkDialog(null, null, null, null,
2171				function(linkTarget, linkColor, allPages, lightbox, editLink, layers, width, height, tags)
2172			{
2173				if (editorUi.spinner.spin(document.body, mxResources.get('loading')))
2174				{
2175					editorUi.getPublicUrl(editorUi.getCurrentFile(), function(url)
2176					{
2177						editorUi.spinner.stop();
2178
2179						var params = [];
2180
2181						if (tags)
2182						{
2183							params.push('tags=%7B%7D');
2184						}
2185
2186						var dlg = new EmbedDialog(editorUi, editorUi.createLink(linkTarget, linkColor,
2187							allPages, lightbox, editLink, layers, url, null, params));
2188						editorUi.showDialog(dlg.container, 450, 240, true, true);
2189						dlg.init();
2190					});
2191				}
2192			});
2193		}));
2194
2195		editorUi.actions.addAction('microsoftOffice...', function()
2196		{
2197			editorUi.openLink('https://office.draw.io');
2198		});
2199
2200		editorUi.actions.addAction('googleDocs...', function()
2201		{
2202			editorUi.openLink('http://docsaddon.draw.io');
2203		});
2204
2205		editorUi.actions.addAction('googleSlides...', function()
2206		{
2207			editorUi.openLink('https://slidesaddon.draw.io');
2208		});
2209
2210		editorUi.actions.addAction('googleSheets...', function()
2211		{
2212			editorUi.openLink('https://sheetsaddon.draw.io');
2213		});
2214
2215		editorUi.actions.addAction('googleSites...', function()
2216		{
2217			if (editorUi.spinner.spin(document.body, mxResources.get('loading')))
2218			{
2219				editorUi.getPublicUrl(editorUi.getCurrentFile(), function(url)
2220				{
2221					editorUi.spinner.stop();
2222					var dlg = new GoogleSitesDialog(editorUi, url);
2223					editorUi.showDialog(dlg.container, 420, 256, true, true);
2224					dlg.init();
2225				});
2226			}
2227		});
2228
2229		// Adds plugins menu item only if localStorage is available for storing the plugins
2230		if (isLocalStorage || mxClient.IS_CHROMEAPP)
2231		{
2232			var action = editorUi.actions.addAction('scratchpad', function()
2233			{
2234				editorUi.toggleScratchpad();
2235			});
2236
2237			action.setToggleAction(true);
2238			action.setSelectedCallback(function()
2239			{
2240				return editorUi.scratchpad != null;
2241			});
2242
2243			if (urlParams['plugins'] != '0')
2244			{
2245				editorUi.actions.addAction('plugins...', function()
2246				{
2247					editorUi.showDialog(new PluginsDialog(editorUi).container, 360, 170, true, false);
2248				});
2249			}
2250		}
2251
2252		var action = editorUi.actions.addAction('search', function()
2253		{
2254			var visible = editorUi.sidebar.isEntryVisible('search');
2255			editorUi.sidebar.showPalette('search', !visible);
2256
2257			if (isLocalStorage)
2258			{
2259				mxSettings.settings.search = !visible;
2260				mxSettings.save();
2261			}
2262		});
2263
2264		action.label = mxResources.get('searchShapes');
2265		action.setToggleAction(true);
2266		action.setSelectedCallback(function() { return editorUi.sidebar.isEntryVisible('search'); });
2267
2268		if (urlParams['embed'] == '1')
2269		{
2270			editorUi.actions.get('save').funct = function(exit)
2271			{
2272				if (graph.isEditing())
2273				{
2274					graph.stopEditing();
2275				}
2276
2277				var data = (urlParams['pages'] != '0' || (editorUi.pages != null && editorUi.pages.length > 1)) ?
2278					editorUi.getFileData(true) : mxUtils.getXml(editorUi.editor.getGraphXml());
2279
2280				if (urlParams['proto'] == 'json')
2281				{
2282					var msg = editorUi.createLoadMessage('save');
2283					msg.xml = data;
2284
2285					if (exit)
2286					{
2287						msg.exit = true;
2288					}
2289
2290					data = JSON.stringify(msg);
2291				}
2292
2293				var parent = window.opener || window.parent;
2294				parent.postMessage(data, '*');
2295
2296				if (urlParams['modified'] != '0' && urlParams['keepmodified'] != '1')
2297				{
2298					editorUi.editor.modified = false;
2299					editorUi.editor.setStatus('');
2300				}
2301
2302				//Add support to saving files if embedded mode is running with files
2303				var file = editorUi.getCurrentFile();
2304
2305				if (file != null && file.constructor != EmbedFile && (file.constructor != LocalFile || file.mode != null))
2306				{
2307					editorUi.saveFile();
2308				}
2309			};
2310
2311			var saveAndExitAction = editorUi.actions.addAction('saveAndExit', function()
2312			{
2313				editorUi.actions.get('save').funct(true);
2314			});
2315
2316			saveAndExitAction.label = urlParams['publishClose'] == '1' ? mxResources.get('publish') : mxResources.get('saveAndExit');
2317
2318			editorUi.actions.addAction('exit', function()
2319			{
2320				if (urlParams['embedInline'] == '1')
2321				{
2322					editorUi.sendEmbeddedSvgExport();
2323				}
2324				else
2325				{
2326					var fn = function()
2327					{
2328						editorUi.editor.modified = false;
2329						var msg = (urlParams['proto'] == 'json') ? JSON.stringify({event: 'exit',
2330							modified: editorUi.editor.modified}) : '';
2331						var parent = window.opener || window.parent;
2332						parent.postMessage(msg, '*');
2333					}
2334
2335					if (!editorUi.editor.modified)
2336					{
2337						fn();
2338					}
2339					else
2340					{
2341						editorUi.confirm(mxResources.get('allChangesLost'), null, fn,
2342							mxResources.get('cancel'), mxResources.get('discardChanges'));
2343					}
2344				}
2345			});
2346		}
2347
2348		this.put('exportAs', new Menu(mxUtils.bind(this, function(menu, parent)
2349		{
2350			if (editorUi.isExportToCanvas())
2351			{
2352				this.addMenuItems(menu, ['exportPng'], parent);
2353
2354				if (editorUi.jpgSupported)
2355				{
2356					this.addMenuItems(menu, ['exportJpg'], parent);
2357				}
2358			}
2359
2360			// Disabled for standalone mode in iOS because new tab cannot be closed
2361			else if (!editorUi.isOffline() && (!mxClient.IS_IOS || !navigator.standalone))
2362			{
2363				this.addMenuItems(menu, ['exportPng', 'exportJpg'], parent);
2364			}
2365
2366			this.addMenuItems(menu, ['exportSvg', '-'], parent);
2367
2368			// Redirects export to PDF to print in Chrome App
2369			if (editorUi.isOffline() || editorUi.printPdfExport)
2370			{
2371				this.addMenuItems(menu, ['exportPdf'], parent);
2372			}
2373			// Disabled for standalone mode in iOS because new tab cannot be closed
2374			else if (!editorUi.isOffline() && (!mxClient.IS_IOS || !navigator.standalone))
2375			{
2376				this.addMenuItems(menu, ['exportPdf'], parent);
2377			}
2378
2379			if (!mxClient.IS_IE && (typeof(VsdxExport) !== 'undefined' || !editorUi.isOffline()))
2380			{
2381				this.addMenuItems(menu, ['exportVsdx'], parent);
2382			}
2383
2384			this.addMenuItems(menu, ['-', 'exportHtml', 'exportXml', 'exportUrl'], parent);
2385
2386			if (!editorUi.isOffline())
2387			{
2388				menu.addSeparator(parent);
2389				this.addMenuItem(menu, 'export', parent).firstChild.nextSibling.innerHTML = mxResources.get('advanced') + '...';
2390			}
2391		})));
2392
2393		this.put('importFrom', new Menu(mxUtils.bind(this, function(menu, parent)
2394		{
2395			var doImportFile = mxUtils.bind(this, function(data, mime, filename)
2396			{
2397				// Gets insert location
2398				var view = graph.view;
2399				var bds = graph.getGraphBounds();
2400				var x = graph.snap(Math.ceil(Math.max(0, bds.x / view.scale - view.translate.x) + 4 * graph.gridSize));
2401				var y = graph.snap(Math.ceil(Math.max(0, (bds.y + bds.height) / view.scale - view.translate.y) + 4 * graph.gridSize));
2402
2403				if (data.substring(0, 11) == 'data:image/')
2404				{
2405					editorUi.loadImage(data, mxUtils.bind(this, function(img)
2406	    			{
2407			    		var resizeImages = true;
2408
2409			    		var doInsert = mxUtils.bind(this, function()
2410			    		{
2411		    				editorUi.resizeImage(img, data, mxUtils.bind(this, function(data2, w2, h2)
2412	    	    			{
2413	    		    			var s = (resizeImages) ? Math.min(1, Math.min(editorUi.maxImageSize / w2, editorUi.maxImageSize / h2)) : 1;
2414
2415    							editorUi.importFile(data, mime, x, y, Math.round(w2 * s), Math.round(h2 * s), filename, function(cells)
2416    							{
2417    								editorUi.spinner.stop();
2418    								graph.setSelectionCells(cells);
2419    								graph.scrollCellToVisible(graph.getSelectionCell());
2420    							});
2421	    	    			}), resizeImages);
2422			    		});
2423
2424			    		if (data.length > editorUi.resampleThreshold)
2425			    		{
2426			    			editorUi.confirmImageResize(function(doResize)
2427	    					{
2428	    						resizeImages = doResize;
2429	    						doInsert();
2430	    					});
2431			    		}
2432			    		else
2433		    			{
2434			    			doInsert();
2435		    			}
2436	    			}), mxUtils.bind(this, function()
2437	    			{
2438	    				editorUi.handleError({message: mxResources.get('cannotOpenFile')});
2439	    			}));
2440				}
2441				else
2442				{
2443					editorUi.importFile(data, mime, x, y, 0, 0, filename, function(cells)
2444					{
2445						editorUi.spinner.stop();
2446						graph.setSelectionCells(cells);
2447						graph.scrollCellToVisible(graph.getSelectionCell());
2448					});
2449				}
2450			});
2451
2452			var getMimeType = mxUtils.bind(this, function(filename)
2453			{
2454				var mime = 'text/xml';
2455
2456				if (/\.png$/i.test(filename))
2457				{
2458					mime = 'image/png';
2459				}
2460				else if (/\.jpe?g$/i.test(filename))
2461				{
2462					mime = 'image/jpg';
2463				}
2464				else if (/\.gif$/i.test(filename))
2465				{
2466					mime = 'image/gif';
2467				}
2468				else if (/\.pdf$/i.test(filename))
2469				{
2470					mime = 'application/pdf';
2471				}
2472
2473				return mime;
2474			});
2475
2476			function pickFileFromService(service)
2477			{
2478				// Drive requires special arguments for libraries and bypassing realtime
2479				service.pickFile(function(id)
2480				{
2481					if (editorUi.spinner.spin(document.body, mxResources.get('loading')))
2482					{
2483						// NOTE The third argument in getFile says denyConvert to match
2484						// the existing signature in the original DriveClient which has
2485						// as slightly different semantic, but works the same way.
2486						service.getFile(id, function(file)
2487						{
2488							var mime = (file.getData().substring(0, 11) == 'data:image/') ? getMimeType(file.getTitle()) : 'text/xml';
2489
2490							// Imports SVG as images
2491							if (/\.svg$/i.test(file.getTitle()) && !editorUi.editor.isDataSvg(file.getData()))
2492							{
2493								file.setData(Editor.createSvgDataUri(file.getData()));
2494								mime = 'image/svg+xml';
2495							}
2496
2497							doImportFile(file.getData(), mime, file.getTitle());
2498						},
2499						function(resp)
2500						{
2501							editorUi.handleError(resp, (resp != null) ? mxResources.get('errorLoadingFile') : null);
2502						}, service == editorUi.drive);
2503					}
2504				}, true);
2505			};
2506
2507			if (typeof(google) != 'undefined' && typeof(google.picker) != 'undefined')
2508			{
2509				if (editorUi.drive != null)
2510				{
2511					// Requires special arguments for libraries and realtime
2512					menu.addItem(mxResources.get('googleDrive') + '...', null, function()
2513					{
2514						pickFileFromService(editorUi.drive);
2515					}, parent);
2516				}
2517				else if (googleEnabled && typeof window.DriveClient === 'function')
2518				{
2519					menu.addItem(mxResources.get('googleDrive') + ' (' + mxResources.get('loading') + '...)', null, function()
2520					{
2521						// do nothing
2522					}, parent, null, false);
2523				}
2524			}
2525
2526			if (editorUi.oneDrive != null)
2527			{
2528				menu.addItem(mxResources.get('oneDrive') + '...', null, function()
2529				{
2530					pickFileFromService(editorUi.oneDrive);
2531				}, parent);
2532			}
2533			else if (oneDriveEnabled && typeof window.OneDriveClient === 'function')
2534			{
2535				menu.addItem(mxResources.get('oneDrive') + ' (' + mxResources.get('loading') + '...)', null, function()
2536				{
2537					// do nothing
2538				}, parent, null, false);
2539			}
2540
2541			if (editorUi.dropbox != null)
2542			{
2543				menu.addItem(mxResources.get('dropbox') + '...', null, function()
2544				{
2545					pickFileFromService(editorUi.dropbox);
2546				}, parent);
2547			}
2548			else if (dropboxEnabled && typeof window.DropboxClient === 'function')
2549			{
2550				menu.addItem(mxResources.get('dropbox') + ' (' + mxResources.get('loading') + '...)', null, function()
2551				{
2552					// do nothing
2553				}, parent, null, false);
2554			}
2555
2556			menu.addSeparator(parent);
2557
2558			if (editorUi.gitHub != null)
2559			{
2560				menu.addItem(mxResources.get('github') + '...', null, function()
2561				{
2562					pickFileFromService(editorUi.gitHub);
2563				}, parent);
2564			}
2565
2566			if (editorUi.gitLab != null)
2567			{
2568				menu.addItem(mxResources.get('gitlab') + '...', null, function()
2569				{
2570					pickFileFromService(editorUi.gitLab);
2571				}, parent);
2572			}
2573
2574			if (editorUi.notion != null)
2575			{
2576				menu.addSeparator(parent);
2577				menu.addItem(mxResources.get('notion') + '...', null, function()
2578				{
2579					pickFileFromService(editorUi.notion);
2580				}, parent);
2581			}
2582
2583			if (editorUi.trello != null)
2584			{
2585				menu.addItem(mxResources.get('trello') + '...', null, function()
2586				{
2587					pickFileFromService(editorUi.trello);
2588				}, parent);
2589			}
2590			else if (trelloEnabled && typeof window.TrelloClient === 'function')
2591			{
2592				menu.addItem(mxResources.get('trello') + ' (' + mxResources.get('loading') + '...)', null, function()
2593				{
2594					// do nothing
2595				}, parent, null, false);
2596			}
2597
2598			menu.addSeparator(parent);
2599
2600			if (isLocalStorage && urlParams['browser'] != '0')
2601			{
2602				menu.addItem(mxResources.get('browser') + '...', null, function()
2603				{
2604					editorUi.importLocalFile(false);
2605				}, parent);
2606			}
2607
2608			if (urlParams['noDevice'] != '1')
2609			{
2610				menu.addItem(mxResources.get('device') + '...', null, function()
2611				{
2612					editorUi.importLocalFile(true);
2613				}, parent);
2614			}
2615
2616			if (!editorUi.isOffline())
2617			{
2618				menu.addSeparator(parent);
2619
2620				menu.addItem(mxResources.get('url') + '...', null, function()
2621				{
2622					var dlg = new FilenameDialog(editorUi, '', mxResources.get('import'), function(fileUrl)
2623					{
2624						if (fileUrl != null && fileUrl.length > 0 && editorUi.spinner.spin(document.body, mxResources.get('loading')))
2625						{
2626							var mime = (/(\.png)($|\?)/i.test(fileUrl)) ? 'image/png' : 'text/xml';
2627
2628							// Uses proxy to avoid CORS issues
2629							editorUi.editor.loadUrl(PROXY_URL + '?url=' + encodeURIComponent(fileUrl), function(data)
2630							{
2631								doImportFile(data, mime, fileUrl);
2632							},
2633							function ()
2634							{
2635								editorUi.spinner.stop();
2636								editorUi.handleError(null, mxResources.get('errorLoadingFile'));
2637							}, mime == 'image/png');
2638						}
2639					}, mxResources.get('url'));
2640					editorUi.showDialog(dlg.container, 300, 80, true, true);
2641					dlg.init();
2642				}, parent);
2643			}
2644		}))).isEnabled = isGraphEnabled;
2645
2646		this.put('theme', new Menu(mxUtils.bind(this, function(menu, parent)
2647		{
2648			var theme = (urlParams['sketch'] == '1') ? 'sketch' : mxSettings.getUi();
2649
2650			var item = menu.addItem(mxResources.get('automatic'), null, function()
2651			{
2652				mxSettings.setUi('');
2653				editorUi.alert(mxResources.get('restartForChangeRequired'));
2654			}, parent);
2655
2656			if (theme != 'kennedy' && theme != 'atlas' &&
2657				theme != 'dark' && theme != 'min' &&
2658				theme != 'sketch')
2659			{
2660				menu.addCheckmark(item, Editor.checkmarkImage);
2661			}
2662
2663			menu.addSeparator(parent);
2664
2665			item = menu.addItem(mxResources.get('default'), null, function()
2666			{
2667				mxSettings.setUi('kennedy');
2668				editorUi.alert(mxResources.get('restartForChangeRequired'));
2669			}, parent);
2670
2671			if (theme == 'kennedy')
2672			{
2673				menu.addCheckmark(item, Editor.checkmarkImage);
2674			}
2675
2676			item = menu.addItem(mxResources.get('minimal'), null, function()
2677			{
2678				mxSettings.setUi('min');
2679				editorUi.alert(mxResources.get('restartForChangeRequired'));
2680			}, parent);
2681
2682			if (theme == 'min')
2683			{
2684				menu.addCheckmark(item, Editor.checkmarkImage);
2685			}
2686
2687			item = menu.addItem(mxResources.get('atlas'), null, function()
2688			{
2689				mxSettings.setUi('atlas');
2690				editorUi.alert(mxResources.get('restartForChangeRequired'));
2691			}, parent);
2692
2693			if (theme == 'atlas')
2694			{
2695				menu.addCheckmark(item, Editor.checkmarkImage);
2696			}
2697
2698			if (theme == 'dark' || (!mxClient.IS_IE && !mxClient.IS_IE11))
2699			{
2700				item = menu.addItem(mxResources.get('dark'), null, function()
2701				{
2702					mxSettings.setUi('dark');
2703					editorUi.alert(mxResources.get('restartForChangeRequired'));
2704				}, parent);
2705
2706				if (theme == 'dark')
2707				{
2708					menu.addCheckmark(item, Editor.checkmarkImage);
2709				}
2710			}
2711
2712			menu.addSeparator(parent);
2713
2714			item = menu.addItem(mxResources.get('sketch'), null, function()
2715			{
2716				mxSettings.setUi('sketch');
2717				editorUi.alert(mxResources.get('restartForChangeRequired'));
2718			}, parent);
2719
2720			if (theme == 'sketch')
2721			{
2722				menu.addCheckmark(item, Editor.checkmarkImage);
2723			}
2724		})));
2725
2726		var renameAction = this.editorUi.actions.addAction('rename...', mxUtils.bind(this, function()
2727		{
2728			var file = this.editorUi.getCurrentFile();
2729
2730			if (file != null)
2731			{
2732				if (file.constructor == LocalFile && file.fileHandle != null)
2733				{
2734					editorUi.showSaveFilePicker(mxUtils.bind(editorUi, function(fileHandle, desc)
2735					{
2736						file.invalidFileHandle = null;
2737						file.fileHandle = fileHandle;
2738						file.title = desc.name;
2739						file.desc = desc;
2740						editorUi.save(desc.name);
2741					}), null, editorUi.createFileSystemOptions(file.getTitle()));
2742				}
2743				else
2744				{
2745					var filename = (file.getTitle() != null) ? file.getTitle() : this.editorUi.defaultFilename;
2746
2747					var dlg = new FilenameDialog(this.editorUi, filename, mxResources.get('rename'), mxUtils.bind(this, function(title)
2748					{
2749						if (title != null && title.length > 0 && file != null && title != file.getTitle() &&
2750							this.editorUi.spinner.spin(document.body, mxResources.get('renaming')))
2751						{
2752							// Delete old file, save new file in dropbox if autosize is enabled
2753							file.rename(title, mxUtils.bind(this, function(resp)
2754							{
2755								this.editorUi.spinner.stop();
2756							}),
2757							mxUtils.bind(this, function(resp)
2758							{
2759								this.editorUi.handleError(resp, (resp != null) ? mxResources.get('errorRenamingFile') : null);
2760							}));
2761						}
2762					}), (file.constructor == DriveFile || file.constructor == StorageFile) ?
2763						mxResources.get('diagramName') : null, function(name)
2764					{
2765						if (name != null && name.length > 0)
2766						{
2767							return true;
2768						}
2769
2770						editorUi.showError(mxResources.get('error'), mxResources.get('invalidName'), mxResources.get('ok'));
2771
2772						return false;
2773					}, null, null, null, null, editorUi.editor.fileExtensions);
2774					this.editorUi.showDialog(dlg.container, 340, 96, true, true);
2775					dlg.init();
2776				}
2777			}
2778		}));
2779
2780		renameAction.isEnabled = function()
2781		{
2782			return this.enabled && isGraphEnabled.apply(this, arguments);
2783		}
2784
2785		renameAction.visible = urlParams['embed'] != '1';
2786
2787		editorUi.actions.addAction('makeCopy...', mxUtils.bind(this, function()
2788		{
2789			var file = editorUi.getCurrentFile();
2790
2791			if (file != null)
2792			{
2793				var title = editorUi.getCopyFilename(file);
2794
2795				if (file.constructor == DriveFile)
2796				{
2797					var dlg = new CreateDialog(editorUi, title, mxUtils.bind(this, function(newTitle, mode)
2798					{
2799						if (mode == '_blank')
2800						{
2801							editorUi.editor.editAsNew(editorUi.getFileData(), newTitle);
2802						}
2803						else
2804						{
2805							// Mode is "download" if Create button is pressed, means use Google Drive
2806							if (mode == 'download')
2807							{
2808								mode = App.MODE_GOOGLE;
2809							}
2810
2811							if (newTitle != null && newTitle.length > 0)
2812							{
2813								if (mode == App.MODE_GOOGLE)
2814								{
2815									if (editorUi.spinner.spin(document.body, mxResources.get('saving')))
2816									{
2817										// Saveas does not update the file descriptor in Google Drive
2818										file.saveAs(newTitle, mxUtils.bind(this, function(resp)
2819										{
2820											// Replaces file descriptor in-place and saves
2821											file.desc = resp;
2822
2823											// Makes sure the latest XML is in the file
2824											file.save(false, mxUtils.bind(this, function()
2825											{
2826												editorUi.spinner.stop();
2827												file.setModified(false);
2828												file.addAllSavedStatus();
2829											}), mxUtils.bind(this, function(resp)
2830											{
2831												editorUi.handleError(resp);
2832											}));
2833										}), mxUtils.bind(this, function(resp)
2834										{
2835											editorUi.handleError(resp);
2836										}));
2837									}
2838								}
2839								else
2840								{
2841									editorUi.createFile(newTitle, editorUi.getFileData(true), null, mode);
2842								}
2843							}
2844						}
2845					}), mxUtils.bind(this, function()
2846					{
2847						editorUi.hideDialog();
2848					}), mxResources.get('makeCopy'), mxResources.get('create'), null,
2849						null, true, null, true, null, null, null, null,
2850						editorUi.editor.fileExtensions);
2851					editorUi.showDialog(dlg.container, 420, 380, true, true);
2852					dlg.init();
2853				}
2854				else
2855				{
2856					// Creates a copy with no predefined storage
2857					editorUi.editor.editAsNew(this.editorUi.getFileData(true), title);
2858				}
2859			}
2860		}));
2861
2862		editorUi.actions.addAction('moveToFolder...', mxUtils.bind(this, function()
2863		{
2864			var file = editorUi.getCurrentFile();
2865
2866			if (file.getMode() == App.MODE_GOOGLE || file.getMode() == App.MODE_ONEDRIVE)
2867			{
2868				var isInRoot = false;
2869
2870				if (file.getMode() == App.MODE_GOOGLE && file.desc.parents != null)
2871				{
2872					for (var i = 0; i < file.desc.parents.length; i++)
2873					{
2874						if (file.desc.parents[i].isRoot)
2875						{
2876							isInRoot = true;
2877							break;
2878						}
2879					}
2880				}
2881
2882				editorUi.pickFolder(file.getMode(), mxUtils.bind(this, function(folderId)
2883				{
2884	            	if (editorUi.spinner.spin(document.body, mxResources.get('moving')))
2885	            	{
2886	            	    file.move(folderId, mxUtils.bind(this, function(resp)
2887	            		{
2888	            	    	editorUi.spinner.stop();
2889	        			}), mxUtils.bind(this, function(resp)
2890	        			{
2891	        				editorUi.handleError(resp);
2892	        			}));
2893	            	}
2894				}), null, true, isInRoot);
2895			}
2896		}));
2897
2898		this.put('publish', new Menu(mxUtils.bind(this, function(menu, parent)
2899		{
2900			this.addMenuItems(menu, ['publishLink'], parent);
2901		})));
2902
2903		editorUi.actions.put('useOffline', new Action(mxResources.get('useOffline') + '...', function()
2904		{
2905			editorUi.openLink('https://app.draw.io/')
2906		}));
2907
2908		editorUi.actions.put('downloadDesktop', new Action(mxResources.get('downloadDesktop') + '...', function()
2909		{
2910			editorUi.openLink('https://get.draw.io/')
2911		}));
2912
2913		this.editorUi.actions.addAction('share...', mxUtils.bind(this, function()
2914		{
2915			try
2916			{
2917				var file = editorUi.getCurrentFile();
2918
2919				if (file != null)
2920				{
2921					file.share();
2922				}
2923			}
2924			catch (e)
2925			{
2926				editorUi.handleError(e);
2927			}
2928		}));
2929
2930		this.put('embed', new Menu(mxUtils.bind(this, function(menu, parent)
2931		{
2932			var file = editorUi.getCurrentFile();
2933
2934			if (file != null && (file.getMode() == App.MODE_GOOGLE ||
2935				file.getMode() == App.MODE_GITHUB) && /(\.png)$/i.test(file.getTitle()))
2936			{
2937				this.addMenuItems(menu, ['liveImage', '-'], parent);
2938			}
2939
2940			this.addMenuItems(menu, ['embedImage', 'embedSvg', '-', 'embedHtml'], parent);
2941
2942			if (!navigator.standalone && !editorUi.isOffline())
2943			{
2944				this.addMenuItems(menu, ['embedIframe'], parent);
2945			}
2946
2947			if (urlParams['embed'] != '1' && !editorUi.isOffline())
2948			{
2949				this.addMenuItems(menu, ['-', 'googleDocs', 'googleSlides', 'googleSheets', '-', 'microsoftOffice', '-', 'embedNotion'], parent);
2950			}
2951		})));
2952
2953		editorUi.addInsertItem = function(menu, parent, title, method)
2954		{
2955			if (method != 'plantUml' || (EditorUi.enablePlantUml && !editorUi.isOffline()))
2956			{
2957				menu.addItem(title, null, mxUtils.bind(this, function()
2958				{
2959					if (method == 'fromText' || method == 'formatSql' ||
2960						method == 'plantUml' || method == 'mermaid')
2961					{
2962						var dlg = new ParseDialog(editorUi, title, method);
2963						editorUi.showDialog(dlg.container, 620, 420, true, false);
2964						editorUi.dialog.container.style.overflow = 'auto';
2965						dlg.init();
2966					}
2967					else
2968					{
2969						var dlg = new CreateGraphDialog(editorUi, title, method);
2970						editorUi.showDialog(dlg.container, 620, 420, true, false);
2971						// Executed after dialog is added to dom
2972						dlg.init();
2973					}
2974				}), parent, null, isGraphEnabled());
2975			}
2976		};
2977
2978		var insertVertex = function(value, w, h, style)
2979		{
2980			var cell = new mxCell(value, new mxGeometry(0, 0, w, h), style);
2981			cell.vertex = true;
2982
2983			var pt = graph.getCenterInsertPoint(graph.getBoundingBoxFromGeometry([cell], true));
2984			cell.geometry.x = pt.x;
2985    	    cell.geometry.y = pt.y;
2986
2987    		graph.getModel().beginUpdate();
2988    		try
2989    	    {
2990    			cell = graph.addCell(cell);
2991    	    	graph.fireEvent(new mxEventObject('cellsInserted', 'cells', [cell]));
2992    	    }
2993    		finally
2994    		{
2995    			graph.getModel().endUpdate();
2996    		}
2997
2998    		graph.scrollCellToVisible(cell);
2999    		graph.setSelectionCell(cell);
3000    		graph.container.focus();
3001
3002    		if (graph.editAfterInsert)
3003    		{
3004    	        graph.startEditing(cell);
3005    		}
3006
3007    		// Async call is workaroun for touch events resetting hover icons
3008    		window.setTimeout(function()
3009    		{
3010	    		if (editorUi.hoverIcons != null)
3011				{
3012					editorUi.hoverIcons.update(graph.view.getState(cell));
3013				}
3014    		}, 0);
3015
3016	    	return cell;
3017		};
3018
3019		editorUi.actions.put('insertText', new Action(mxResources.get('text'), function()
3020		{
3021			if (graph.isEnabled() && !graph.isCellLocked(graph.getDefaultParent()))
3022			{
3023    			graph.startEditingAtCell(insertVertex('Text', 40, 20, 'text;html=1;resizable=0;autosize=1;' +
3024    				'align=center;verticalAlign=middle;points=[];fillColor=none;strokeColor=none;rounded=0;'));
3025			}
3026		}), null, null, Editor.ctrlKey + '+Shift+X').isEnabled = isGraphEnabled;
3027
3028		editorUi.actions.put('insertRectangle', new Action(mxResources.get('rectangle'), function()
3029		{
3030			if (graph.isEnabled() && !graph.isCellLocked(graph.getDefaultParent()))
3031			{
3032    	    	insertVertex('', 120, 60, 'whiteSpace=wrap;html=1;');
3033			}
3034		}), null, null, Editor.ctrlKey + '+K').isEnabled = isGraphEnabled;
3035
3036		editorUi.actions.put('insertEllipse', new Action(mxResources.get('ellipse'), function()
3037		{
3038			if (graph.isEnabled() && !graph.isCellLocked(graph.getDefaultParent()))
3039			{
3040    	    	insertVertex('', 80, 80, 'ellipse;whiteSpace=wrap;html=1;');
3041			}
3042		}), null, null, Editor.ctrlKey + '+Shift+K').isEnabled = isGraphEnabled;
3043
3044		editorUi.actions.put('insertRhombus', new Action(mxResources.get('rhombus'), function()
3045		{
3046			if (graph.isEnabled() && !graph.isCellLocked(graph.getDefaultParent()))
3047			{
3048    	    	insertVertex('', 80, 80, 'rhombus;whiteSpace=wrap;html=1;');
3049			}
3050		})).isEnabled = isGraphEnabled;
3051
3052		editorUi.addInsertMenuItems = mxUtils.bind(this, function(menu, parent, methods)
3053		{
3054			for (var i = 0; i < methods.length; i++)
3055			{
3056				if (methods[i] == '-')
3057				{
3058					menu.addSeparator(parent);
3059				}
3060				else
3061				{
3062					editorUi.addInsertItem(menu, parent, mxResources.get(methods[i]) + '...', methods[i]);
3063				}
3064			}
3065		});
3066
3067		this.put('insert', new Menu(mxUtils.bind(this, function(menu, parent)
3068		{
3069			this.addMenuItems(menu, ['insertRectangle', 'insertEllipse',
3070				'insertRhombus', '-', 'insertText', 'insertLink', '-',
3071				'createShape', 'insertFreehand', '-', 'insertImage'], parent);
3072
3073			if (editorUi.insertTemplateEnabled && !editorUi.isOffline())
3074			{
3075				this.addMenuItems(menu, ['insertTemplate'], parent);
3076			}
3077
3078			menu.addSeparator(parent);
3079			this.addSubmenu('insertLayout', menu, parent, mxResources.get('layout'));
3080			this.addSubmenu('insertAdvanced', menu, parent, mxResources.get('advanced'));
3081		})));
3082
3083		this.put('insertLayout', new Menu(mxUtils.bind(this, function(menu, parent)
3084		{
3085			editorUi.addInsertMenuItems(menu, parent, ['horizontalFlow', 'verticalFlow', '-', 'horizontalTree',
3086				'verticalTree', 'radialTree', '-', 'organic', 'circle']);
3087		})));
3088
3089        this.put('insertAdvanced', new Menu(mxUtils.bind(this, function(menu, parent)
3090        {
3091			editorUi.addInsertMenuItems(menu, parent, ['fromText', 'plantUml', 'mermaid', '-', 'formatSql']);
3092
3093			menu.addItem(mxResources.get('csv') + '...', null, function()
3094			{
3095				editorUi.showImportCsvDialog();
3096			}, parent, null, isGraphEnabled());
3097        })));
3098
3099		this.put('openRecent', new Menu(function(menu, parent)
3100		{
3101			var recent = editorUi.getRecent();
3102
3103			if (recent != null)
3104			{
3105				for (var i = 0; i < recent.length; i++)
3106				{
3107					(function(entry)
3108					{
3109						var modeKey = entry.mode;
3110
3111						// Google and oneDrive use different keys
3112						if (modeKey == App.MODE_GOOGLE)
3113						{
3114							modeKey = 'googleDrive';
3115						}
3116						else if (modeKey == App.MODE_ONEDRIVE)
3117						{
3118							modeKey = 'oneDrive';
3119						}
3120
3121						menu.addItem(entry.title + ' (' + mxResources.get(modeKey) + ')', null, function()
3122						{
3123							editorUi.loadFile(entry.id);
3124						}, parent);
3125					})(recent[i]);
3126				}
3127
3128				menu.addSeparator(parent);
3129			}
3130
3131			menu.addItem(mxResources.get('reset'), null, function()
3132			{
3133				editorUi.resetRecent();
3134			}, parent);
3135		}));
3136
3137		this.put('openFrom', new Menu(function(menu, parent)
3138		{
3139			if (editorUi.drive != null)
3140			{
3141				menu.addItem(mxResources.get('googleDrive') + '...', null, function()
3142				{
3143					editorUi.pickFile(App.MODE_GOOGLE);
3144				}, parent);
3145			}
3146			else if (googleEnabled && typeof window.DriveClient === 'function')
3147			{
3148				menu.addItem(mxResources.get('googleDrive') + ' (' + mxResources.get('loading') + '...)', null, function()
3149				{
3150					// do nothing
3151				}, parent, null, false);
3152			}
3153
3154			if (editorUi.oneDrive != null)
3155			{
3156				menu.addItem(mxResources.get('oneDrive') + '...', null, function()
3157				{
3158					editorUi.pickFile(App.MODE_ONEDRIVE);
3159				}, parent);
3160			}
3161			else if (oneDriveEnabled && typeof window.OneDriveClient === 'function')
3162			{
3163				menu.addItem(mxResources.get('oneDrive') + ' (' + mxResources.get('loading') + '...)', null, function()
3164				{
3165					// do nothing
3166				}, parent, null, false);
3167			}
3168
3169			if (editorUi.dropbox != null)
3170			{
3171				menu.addItem(mxResources.get('dropbox') + '...', null, function()
3172				{
3173					editorUi.pickFile(App.MODE_DROPBOX);
3174				}, parent);
3175			}
3176			else if (dropboxEnabled && typeof window.DropboxClient === 'function')
3177			{
3178				menu.addItem(mxResources.get('dropbox') + ' (' + mxResources.get('loading') + '...)', null, function()
3179				{
3180					// do nothing
3181				}, parent, null, false);
3182			}
3183
3184			menu.addSeparator(parent);
3185
3186			if (editorUi.gitHub != null)
3187			{
3188				menu.addItem(mxResources.get('github') + '...', null, function()
3189				{
3190					editorUi.pickFile(App.MODE_GITHUB);
3191				}, parent);
3192			}
3193
3194			if (editorUi.gitLab != null)
3195			{
3196				menu.addItem(mxResources.get('gitlab') + '...', null, function()
3197				{
3198					editorUi.pickFile(App.MODE_GITLAB);
3199				}, parent);
3200			}
3201
3202			if (editorUi.notion != null)
3203			{
3204				menu.addSeparator(parent);
3205				menu.addItem(mxResources.get('notion') + '...', null, function()
3206				{
3207					editorUi.pickFile(App.MODE_NOTION);
3208				}, parent);
3209			}
3210
3211			if (editorUi.trello != null)
3212			{
3213				menu.addItem(mxResources.get('trello') + '...', null, function()
3214				{
3215					editorUi.pickFile(App.MODE_TRELLO);
3216				}, parent);
3217			}
3218			else if (trelloEnabled && typeof window.TrelloClient === 'function')
3219			{
3220				menu.addItem(mxResources.get('trello') + ' (' + mxResources.get('loading') + '...)', null, function()
3221				{
3222					// do nothing
3223				}, parent, null, false);
3224			}
3225
3226			menu.addSeparator(parent);
3227
3228			if (isLocalStorage && urlParams['browser'] != '0')
3229			{
3230				menu.addItem(mxResources.get('browser') + '...', null, function()
3231				{
3232					editorUi.pickFile(App.MODE_BROWSER);
3233				}, parent);
3234			}
3235
3236			//if (!mxClient.IS_IOS)
3237			if (urlParams['noDevice'] != '1')
3238			{
3239				menu.addItem(mxResources.get('device') + '...', null, function()
3240				{
3241					editorUi.pickFile(App.MODE_DEVICE);
3242				}, parent);
3243			}
3244
3245			if (!editorUi.isOffline())
3246			{
3247				menu.addSeparator(parent);
3248
3249				menu.addItem(mxResources.get('url') + '...', null, function()
3250				{
3251					var dlg = new FilenameDialog(editorUi, '', mxResources.get('open'), function(fileUrl)
3252					{
3253						if (fileUrl != null && fileUrl.length > 0)
3254						{
3255							if (editorUi.getCurrentFile() == null)
3256							{
3257								window.location.hash = '#U' + encodeURIComponent(fileUrl);
3258							}
3259							else
3260							{
3261								window.openWindow(((mxClient.IS_CHROMEAPP) ?
3262									'https://www.draw.io/' : 'https://' + location.host + '/') +
3263									window.location.search + '#U' + encodeURIComponent(fileUrl));
3264							}
3265						}
3266					}, mxResources.get('url'));
3267					editorUi.showDialog(dlg.container, 300, 80, true, true);
3268					dlg.init();
3269				}, parent);
3270			}
3271		}));
3272
3273		if (Editor.enableCustomLibraries)
3274		{
3275			this.put('newLibrary', new Menu(function(menu, parent)
3276			{
3277				if (typeof(google) != 'undefined' && typeof(google.picker) != 'undefined')
3278				{
3279					if (editorUi.drive != null)
3280					{
3281						menu.addItem(mxResources.get('googleDrive') + '...', null, function()
3282						{
3283							editorUi.showLibraryDialog(null, null, null, null, App.MODE_GOOGLE);
3284						}, parent);
3285					}
3286					else if (googleEnabled && typeof window.DriveClient === 'function')
3287					{
3288						menu.addItem(mxResources.get('googleDrive') + ' (' + mxResources.get('loading') + '...)', null, function()
3289						{
3290							// do nothing
3291						}, parent, null, false);
3292					}
3293				}
3294
3295				if (editorUi.oneDrive != null)
3296				{
3297					menu.addItem(mxResources.get('oneDrive') + '...', null, function()
3298					{
3299						editorUi.showLibraryDialog(null, null, null, null, App.MODE_ONEDRIVE);
3300					}, parent);
3301				}
3302				else if (oneDriveEnabled && typeof window.OneDriveClient === 'function')
3303				{
3304					menu.addItem(mxResources.get('oneDrive') + ' (' + mxResources.get('loading') + '...)', null, function()
3305					{
3306						// do nothing
3307					}, parent, null, false);
3308				}
3309
3310				if (editorUi.dropbox != null)
3311				{
3312					menu.addItem(mxResources.get('dropbox') + '...', null, function()
3313					{
3314						editorUi.showLibraryDialog(null, null, null, null, App.MODE_DROPBOX);
3315					}, parent);
3316				}
3317				else if (dropboxEnabled && typeof window.DropboxClient === 'function')
3318				{
3319					menu.addItem(mxResources.get('dropbox') + ' (' + mxResources.get('loading') + '...)', null, function()
3320					{
3321						// do nothing
3322					}, parent, null, false);
3323				}
3324
3325				menu.addSeparator(parent);
3326
3327				if (editorUi.gitHub != null)
3328				{
3329					menu.addItem(mxResources.get('github') + '...', null, function()
3330					{
3331						editorUi.showLibraryDialog(null, null, null, null, App.MODE_GITHUB);
3332					}, parent);
3333				}
3334
3335				if (editorUi.gitLab != null)
3336				{
3337					menu.addItem(mxResources.get('gitlab') + '...', null, function()
3338					{
3339						editorUi.showLibraryDialog(null, null, null, null, App.MODE_GITLAB);
3340					}, parent);
3341				}
3342
3343				if (editorUi.notion != null)
3344				{
3345					menu.addSeparator(parent);
3346					menu.addItem(mxResources.get('notion') + '...', null, function()
3347					{
3348						editorUi.showLibraryDialog(null, null, null, null, App.MODE_NOTION);
3349					}, parent);
3350				}
3351
3352				if (editorUi.trello != null)
3353				{
3354					menu.addItem(mxResources.get('trello') + '...', null, function()
3355					{
3356						editorUi.showLibraryDialog(null, null, null, null, App.MODE_TRELLO);
3357					}, parent);
3358				}
3359				else if (trelloEnabled && typeof window.TrelloClient === 'function')
3360				{
3361					menu.addItem(mxResources.get('trello') + ' (' + mxResources.get('loading') + '...)', null, function()
3362					{
3363						// do nothing
3364					}, parent, null, false);
3365				}
3366
3367				menu.addSeparator(parent);
3368
3369				if (isLocalStorage && urlParams['browser'] != '0')
3370				{
3371					menu.addItem(mxResources.get('browser') + '...', null, function()
3372					{
3373						editorUi.showLibraryDialog(null, null, null, null, App.MODE_BROWSER);
3374					}, parent);
3375				}
3376
3377				//if (!mxClient.IS_IOS)
3378				if (urlParams['noDevice'] != '1')
3379				{
3380					menu.addItem(mxResources.get('device') + '...', null, function()
3381					{
3382						editorUi.showLibraryDialog(null, null, null, null, App.MODE_DEVICE);
3383					}, parent);
3384				}
3385			}));
3386
3387			this.put('openLibraryFrom', new Menu(function(menu, parent)
3388			{
3389				if (typeof(google) != 'undefined' && typeof(google.picker) != 'undefined')
3390				{
3391					if (editorUi.drive != null)
3392					{
3393						menu.addItem(mxResources.get('googleDrive') + '...', null, function()
3394						{
3395							editorUi.pickLibrary(App.MODE_GOOGLE);
3396						}, parent);
3397					}
3398					else if (googleEnabled && typeof window.DriveClient === 'function')
3399					{
3400						menu.addItem(mxResources.get('googleDrive') + ' (' + mxResources.get('loading') + '...)', null, function()
3401						{
3402							// do nothing
3403						}, parent, null, false);
3404					}
3405				}
3406
3407				if (editorUi.oneDrive != null)
3408				{
3409					menu.addItem(mxResources.get('oneDrive') + '...', null, function()
3410					{
3411						editorUi.pickLibrary(App.MODE_ONEDRIVE);
3412					}, parent);
3413				}
3414				else if (oneDriveEnabled && typeof window.OneDriveClient === 'function')
3415				{
3416					menu.addItem(mxResources.get('oneDrive') + ' (' + mxResources.get('loading') + '...)', null, function()
3417					{
3418						// do nothing
3419					}, parent, null, false);
3420				}
3421
3422				if (editorUi.dropbox != null)
3423				{
3424					menu.addItem(mxResources.get('dropbox') + '...', null, function()
3425					{
3426						editorUi.pickLibrary(App.MODE_DROPBOX);
3427					}, parent);
3428				}
3429				else if (dropboxEnabled && typeof window.DropboxClient === 'function')
3430				{
3431					menu.addItem(mxResources.get('dropbox') + ' (' + mxResources.get('loading') + '...)', null, function()
3432					{
3433						// do nothing
3434					}, parent, null, false);
3435				}
3436
3437				menu.addSeparator(parent);
3438
3439				if (editorUi.gitHub != null)
3440				{
3441					menu.addItem(mxResources.get('github') + '...', null, function()
3442					{
3443						editorUi.pickLibrary(App.MODE_GITHUB);
3444					}, parent);
3445				}
3446
3447				if (editorUi.gitLab != null)
3448				{
3449					menu.addItem(mxResources.get('gitlab') + '...', null, function()
3450					{
3451						editorUi.pickLibrary(App.MODE_GITLAB);
3452					}, parent);
3453				}
3454
3455				if (editorUi.notion != null)
3456				{
3457					menu.addSeparator(parent);
3458					menu.addItem(mxResources.get('notion') + '...', null, function()
3459					{
3460						editorUi.pickLibrary(App.MODE_NOTION);
3461					}, parent);
3462				}
3463
3464				if (editorUi.trello != null)
3465				{
3466					menu.addItem(mxResources.get('trello') + '...', null, function()
3467					{
3468						editorUi.pickLibrary(App.MODE_TRELLO);
3469					}, parent);
3470				}
3471				else if (trelloEnabled && typeof window.TrelloClient === 'function')
3472				{
3473					menu.addItem(mxResources.get('trello') + ' (' + mxResources.get('loading') + '...)', null, function()
3474					{
3475						// do nothing
3476					}, parent, null, false);
3477				}
3478
3479				menu.addSeparator(parent);
3480
3481				if (isLocalStorage && urlParams['browser'] != '0')
3482				{
3483					menu.addItem(mxResources.get('browser') + '...', null, function()
3484					{
3485						editorUi.pickLibrary(App.MODE_BROWSER);
3486					}, parent);
3487				}
3488
3489				//if (!mxClient.IS_IOS)
3490				if (urlParams['noDevice'] != '1')
3491				{
3492					menu.addItem(mxResources.get('device') + '...', null, function()
3493					{
3494						editorUi.pickLibrary(App.MODE_DEVICE);
3495					}, parent);
3496				}
3497
3498				if (!editorUi.isOffline())
3499				{
3500					menu.addSeparator(parent);
3501
3502					menu.addItem(mxResources.get('url') + '...', null, function()
3503					{
3504						var dlg = new FilenameDialog(editorUi, '', mxResources.get('open'), function(fileUrl)
3505						{
3506							if (fileUrl != null && fileUrl.length > 0 && editorUi.spinner.spin(document.body, mxResources.get('loading')))
3507							{
3508								var realUrl = fileUrl;
3509
3510								if (!editorUi.editor.isCorsEnabledForUrl(fileUrl))
3511								{
3512									realUrl = PROXY_URL + '?url=' + encodeURIComponent(fileUrl);
3513								}
3514
3515								// Uses proxy to avoid CORS issues
3516								mxUtils.get(realUrl, function(req)
3517								{
3518									if (req.getStatus() >= 200 && req.getStatus() <= 299)
3519									{
3520										editorUi.spinner.stop();
3521
3522										try
3523										{
3524											editorUi.loadLibrary(new UrlLibrary(this, req.getText(), fileUrl));
3525										}
3526										catch (e)
3527										{
3528											editorUi.handleError(e, mxResources.get('errorLoadingFile'));
3529										}
3530									}
3531									else
3532									{
3533										editorUi.spinner.stop();
3534										editorUi.handleError(null, mxResources.get('errorLoadingFile'));
3535									}
3536								}, function()
3537								{
3538									editorUi.spinner.stop();
3539									editorUi.handleError(null, mxResources.get('errorLoadingFile'));
3540								});
3541							}
3542						}, mxResources.get('url'));
3543						editorUi.showDialog(dlg.container, 300, 80, true, true);
3544						dlg.init();
3545					}, parent);
3546				}
3547
3548				if (urlParams['confLib'] == '1')
3549				{
3550					menu.addSeparator(parent);
3551
3552					menu.addItem(mxResources.get('confluenceCloud') + '...', null, function()
3553					{
3554						editorUi.showRemotelyStoredLibrary(mxResources.get('libraries'));
3555					}, parent);
3556				}
3557			}));
3558		}
3559
3560		// Overrides edit menu to add find, copyAsImage editGeometry
3561		this.put('edit', new Menu(mxUtils.bind(this, function(menu, parent)
3562		{
3563			this.addMenuItems(menu, ['undo', 'redo', '-', 'cut', 'copy', 'copyAsImage', 'paste',
3564				'delete', '-', 'duplicate', '-', 'findReplace', '-', 'editData', 'editTooltip', '-',
3565				'editStyle',  'editGeometry', '-', 'edit', '-', 'editLink', 'openLink', '-',
3566                'selectVertices', 'selectEdges', 'selectAll', 'selectNone', '-', 'lockUnlock']);
3567		})));
3568
3569		var action = editorUi.actions.addAction('comments', mxUtils.bind(this, function()
3570		{
3571			if (this.commentsWindow == null)
3572			{
3573				// LATER: Check outline window for initial placement
3574				this.commentsWindow = new CommentsWindow(editorUi, document.body.offsetWidth - 380, 120, 300, 350);
3575				//TODO Are these events needed?
3576				this.commentsWindow.window.addListener('show', function()
3577				{
3578					editorUi.fireEvent(new mxEventObject('comments'));
3579				});
3580				this.commentsWindow.window.addListener('hide', function()
3581				{
3582					editorUi.fireEvent(new mxEventObject('comments'));
3583				});
3584				this.commentsWindow.window.setVisible(true);
3585				editorUi.fireEvent(new mxEventObject('comments'));
3586			}
3587			else
3588			{
3589				var isVisible = !this.commentsWindow.window.isVisible();
3590				this.commentsWindow.window.setVisible(isVisible);
3591
3592				this.commentsWindow.refreshCommentsTime();
3593
3594				if (isVisible && this.commentsWindow.hasError)
3595				{
3596					this.commentsWindow.refreshComments();
3597				}
3598			}
3599		}));
3600		action.setToggleAction(true);
3601		action.setSelectedCallback(mxUtils.bind(this, function() { return this.commentsWindow != null && this.commentsWindow.window.isVisible(); }));
3602
3603		// Destroys comments window to force update or disable if not supported
3604		editorUi.editor.addListener('fileLoaded', mxUtils.bind(this, function()
3605		{
3606			if (this.commentsWindow != null)
3607			{
3608				this.commentsWindow.destroy();
3609				this.commentsWindow = null;
3610			}
3611		}));
3612
3613		// Extends toolbar dropdown to add comments
3614		var viewPanelsMenu = this.get('viewPanels');
3615		var viewPanelsFunct = viewPanelsMenu.funct;
3616
3617		viewPanelsMenu.funct = function(menu, parent)
3618		{
3619			viewPanelsFunct.apply(this, arguments);
3620
3621			editorUi.menus.addMenuItems(menu, ['tags'], parent);
3622
3623			if (editorUi.commentsSupported())
3624			{
3625				editorUi.menus.addMenuItems(menu, ['comments'], parent);
3626			}
3627		};
3628
3629		// Overrides view menu to add search and scratchpad
3630		this.put('view', new Menu(mxUtils.bind(this, function(menu, parent)
3631		{
3632			this.addMenuItems(menu, ((this.editorUi.format != null) ? ['formatPanel'] : []).
3633				concat(['outline', 'layers', 'tags']).concat((editorUi.commentsSupported()) ?
3634				['comments', '-'] : ['-']));
3635
3636			this.addMenuItems(menu, ['-', 'search'], parent);
3637
3638			if (isLocalStorage || mxClient.IS_CHROMEAPP)
3639			{
3640				var item = this.addMenuItem(menu, 'scratchpad', parent);
3641
3642				if (!editorUi.isOffline() || mxClient.IS_CHROMEAPP || EditorUi.isElectronApp)
3643				{
3644					this.addLinkToItem(item, 'https://www.diagrams.net/doc/faq/scratchpad');
3645				}
3646			}
3647
3648			this.addMenuItems(menu, ['shapes', '-', 'pageView', 'pageScale']);
3649			this.addSubmenu('units', menu, parent);
3650			this.addMenuItems(menu, ['-', 'scrollbars', 'tooltips', 'ruler', '-',
3651                'grid', 'guides'], parent);
3652
3653			if (mxClient.IS_SVG && (document.documentMode == null || document.documentMode > 9))
3654			{
3655				this.addMenuItem(menu, 'shadowVisible', parent);
3656			}
3657
3658			this.addMenuItems(menu, ['-', 'connectionArrows', 'connectionPoints', '-',
3659			                         'resetView', 'zoomIn', 'zoomOut'], parent);
3660
3661			if (urlParams['sketch'] != '1')
3662			{
3663				 this.addMenuItems(menu, ['-', 'fullscreen'], parent);
3664			}
3665		})));
3666
3667		this.put('extras', new Menu(mxUtils.bind(this, function(menu, parent)
3668		{
3669			if (urlParams['noLangIcon'] == '1')
3670			{
3671				this.addSubmenu('language', menu, parent);
3672				menu.addSeparator(parent);
3673			}
3674
3675			if (urlParams['embed'] != '1')
3676			{
3677				this.addSubmenu('theme', menu, parent);
3678				menu.addSeparator(parent);
3679			}
3680
3681			if (typeof(MathJax) !== 'undefined')
3682			{
3683				var item = this.addMenuItem(menu, 'mathematicalTypesetting', parent);
3684
3685				if (!editorUi.isOffline() || mxClient.IS_CHROMEAPP || EditorUi.isElectronApp)
3686				{
3687					this.addLinkToItem(item, 'https://www.diagrams.net/doc/faq/math-typesetting');
3688				}
3689			}
3690
3691			this.addMenuItems(menu, ['copyConnect', 'collapseExpand', '-'], parent);
3692
3693			if (urlParams['embed'] != '1' && (isLocalStorage || mxClient.IS_CHROMEAPP))
3694			{
3695				this.addMenuItems(menu, ['showStartScreen'], parent);
3696			}
3697
3698			if (urlParams['embed'] != '1')
3699			{
3700				this.addMenuItems(menu, ['autosave'], parent);
3701			}
3702
3703			menu.addSeparator(parent);
3704
3705			if (!editorUi.isOfflineApp() && isLocalStorage)
3706			{
3707				this.addMenuItem(menu, 'plugins', parent);
3708			}
3709
3710			this.addMenuItems(menu, ['-', 'editDiagram'], parent);
3711
3712			if (Graph.translateDiagram)
3713			{
3714				this.addMenuItems(menu, ['diagramLanguage']);
3715			}
3716
3717			this.addMenuItems(menu, ['-', 'configuration'], parent);
3718
3719			// Adds trailing separator in case new plugin entries are added
3720			menu.addSeparator(parent);
3721
3722			if (urlParams['newTempDlg'] == '1')
3723			{
3724				editorUi.actions.addAction('templates', function()
3725				{
3726					function driveObjToTempDlg(item)
3727					{
3728						return {id: item.id, isExt: true, url: item.downloadUrl, title: item.title, imgUrl: item.thumbnailLink,
3729								changedBy: item.lastModifyingUserName, lastModifiedOn: item.modifiedDate}
3730					};
3731
3732					var tempDlg = new TemplatesDialog(editorUi, function(xml){console.log(arguments)}, null,
3733							null, null, 'user', function(callback, error, username)
3734					{
3735						var oneWeek = new Date();
3736						oneWeek.setDate(oneWeek.getDate() - 7);
3737
3738						editorUi.drive.listFiles(null, oneWeek, username? true : false, function(resp)
3739						{
3740							var results = [];
3741
3742							for (var i = 0; i < resp.items.length; i++)
3743							{
3744								results.push(driveObjToTempDlg(resp.items[i]));
3745							}
3746
3747							callback(results);
3748						}, error)
3749					}, function(str, callback, error, username)
3750					{
3751						editorUi.drive.listFiles(str, null, username? true : false, function(resp)
3752						{
3753							var results = [];
3754
3755							for (var i = 0; i < resp.items.length; i++)
3756							{
3757								results.push(driveObjToTempDlg(resp.items[i]));
3758							}
3759
3760							callback(results);
3761						}, error)
3762					}, function(obj, callback, error)
3763					{
3764						editorUi.drive.getFile(obj.id, function(file)
3765						{
3766							callback(file.data);
3767						}, error);
3768					}, null, function(callback)
3769					{
3770						callback({'Test': []}, 1);
3771					}, true, false);
3772
3773					editorUi.showDialog(tempDlg.container, window.innerWidth, window.innerHeight, true, false, null, false, true);
3774				});
3775				this.addMenuItem(menu, 'templates', parent);
3776			}
3777		})));
3778
3779		this.put('file', new Menu(mxUtils.bind(this, function(menu, parent)
3780		{
3781			if (urlParams['embed'] == '1')
3782			{
3783				this.addSubmenu('importFrom', menu, parent);
3784				this.addSubmenu('exportAs', menu, parent);
3785				this.addSubmenu('embed', menu, parent);
3786
3787				if (urlParams['libraries'] == '1')
3788				{
3789					this.addMenuItems(menu, ['-'], parent);
3790					this.addSubmenu('newLibrary', menu, parent);
3791					this.addSubmenu('openLibraryFrom', menu, parent);
3792				}
3793
3794				if (editorUi.isRevisionHistorySupported())
3795				{
3796					this.addMenuItems(menu, ['-', 'revisionHistory'], parent);
3797				}
3798
3799				this.addMenuItems(menu, ['-', 'pageSetup', 'print', '-', 'rename'], parent);
3800
3801				if (urlParams['embedInline'] != '1')
3802				{
3803					if (urlParams['noSaveBtn'] == '1')
3804					{
3805						if (urlParams['saveAndExit'] != '0')
3806						{
3807							this.addMenuItems(menu, ['saveAndExit'], parent);
3808						}
3809					}
3810					else
3811					{
3812						this.addMenuItems(menu, ['save'], parent);
3813
3814						if (urlParams['saveAndExit'] == '1')
3815						{
3816							this.addMenuItems(menu, ['saveAndExit'], parent);
3817						}
3818					}
3819				}
3820
3821				if (urlParams['noExitBtn'] != '1')
3822				{
3823					this.addMenuItems(menu, ['exit'], parent);
3824				}
3825			}
3826			else
3827			{
3828				var file = this.editorUi.getCurrentFile();
3829
3830				if (file != null && file.constructor == DriveFile)
3831				{
3832					if (file.isRestricted())
3833					{
3834						this.addMenuItems(menu, ['exportOptionsDisabled'], parent);
3835					}
3836
3837					this.addMenuItems(menu, ['save', '-', 'share'], parent);
3838
3839					var item = this.addMenuItem(menu, 'synchronize', parent);
3840
3841					if (!editorUi.isOffline() || mxClient.IS_CHROMEAPP || EditorUi.isElectronApp)
3842					{
3843						this.addLinkToItem(item, 'https://www.diagrams.net/doc/faq/synchronize');
3844					}
3845
3846					menu.addSeparator(parent);
3847				}
3848				else
3849				{
3850					this.addMenuItems(menu, ['new'], parent);
3851				}
3852
3853				this.addSubmenu('openFrom', menu, parent);
3854
3855				if (isLocalStorage)
3856				{
3857					this.addSubmenu('openRecent', menu, parent);
3858				}
3859
3860				if (file != null && file.constructor == DriveFile)
3861				{
3862					this.addMenuItems(menu, ['new', '-', 'rename', 'makeCopy', 'moveToFolder'], parent);
3863				}
3864				else
3865				{
3866					if (!mxClient.IS_CHROMEAPP && !EditorUi.isElectronApp &&
3867						file != null && (file.constructor != LocalFile ||
3868						file.fileHandle != null))
3869					{
3870						menu.addSeparator(parent);
3871						var item = this.addMenuItem(menu, 'synchronize', parent);
3872
3873						if (!editorUi.isOffline() || mxClient.IS_CHROMEAPP || EditorUi.isElectronApp)
3874						{
3875							this.addLinkToItem(item, 'https://www.diagrams.net/doc/faq/synchronize');
3876						}
3877					}
3878
3879					this.addMenuItems(menu, ['-', 'save', 'saveAs', '-'], parent);
3880
3881					if (!mxClient.IS_CHROMEAPP && !EditorUi.isElectronApp &&
3882						editorUi.getServiceName() == 'draw.io' &&
3883						!editorUi.isOfflineApp() && file != null)
3884					{
3885						this.addMenuItems(menu, ['share', '-'], parent);
3886					}
3887
3888					this.addMenuItems(menu, ['rename'], parent);
3889
3890					if (editorUi.isOfflineApp())
3891					{
3892						if (navigator.onLine && urlParams['stealth'] != '1' && urlParams['lockdown'] != '1')
3893						{
3894							this.addMenuItems(menu, ['upload'], parent);
3895						}
3896					}
3897					else
3898					{
3899						this.addMenuItems(menu, ['makeCopy'], parent);
3900
3901						if (file != null && file.constructor == OneDriveFile)
3902						{
3903							this.addMenuItems(menu, ['moveToFolder'], parent);
3904						}
3905					}
3906				}
3907
3908				menu.addSeparator(parent);
3909				this.addSubmenu('importFrom', menu, parent);
3910				this.addSubmenu('exportAs', menu, parent);
3911				menu.addSeparator(parent);
3912				this.addSubmenu('embed', menu, parent);
3913				this.addSubmenu('publish', menu, parent);
3914				menu.addSeparator(parent);
3915				this.addSubmenu('newLibrary', menu, parent);
3916				this.addSubmenu('openLibraryFrom', menu, parent);
3917
3918				if (editorUi.isRevisionHistorySupported())
3919				{
3920					this.addMenuItems(menu, ['-', 'revisionHistory'], parent);
3921				}
3922
3923				if (file != null && editorUi.fileNode != null && urlParams['embedInline'] != '1')
3924				{
3925					var filename = (file.getTitle() != null) ?
3926						file.getTitle() : editorUi.defaultFilename;
3927
3928					if (!/(\.html)$/i.test(filename) &&
3929						!/(\.svg)$/i.test(filename))
3930					{
3931						this.addMenuItems(menu, ['-', 'properties']);
3932					}
3933				}
3934
3935				this.addMenuItems(menu, ['-', 'pageSetup'], parent);
3936
3937				// Cannot use print in standalone mode on iOS as we cannot open new windows
3938				if (!mxClient.IS_IOS || !navigator.standalone)
3939				{
3940					this.addMenuItems(menu, ['print'], parent);
3941				}
3942
3943				this.addMenuItems(menu, ['-', 'close']);
3944			}
3945		})));
3946
3947		/**
3948		 * External Fonts undoable change
3949		 */
3950		function ChangeExtFonts(ui, extFonts, customFonts)
3951		{
3952			this.ui = ui;
3953			this.extFonts = extFonts;
3954			this.previousExtFonts = extFonts;
3955			this.customFonts = customFonts;
3956			this.prevCustomFonts = customFonts;
3957		};
3958
3959		/**
3960		 * Implementation of the undoable External Fonts Change.
3961		 */
3962		ChangeExtFonts.prototype.execute = function()
3963		{
3964			var graph = this.ui.editor.graph;
3965			this.customFonts = this.prevCustomFonts;
3966			this.prevCustomFonts = this.ui.menus.customFonts;
3967			this.ui.fireEvent(new mxEventObject('customFontsChanged', 'customFonts', this.customFonts));
3968
3969			this.extFonts = this.previousExtFonts;
3970			var tmp = graph.extFonts;
3971
3972			for (var i = 0; tmp != null && i < tmp.length; i++)
3973			{
3974				var fontElem = document.getElementById('extFont_' + tmp[i].name);
3975
3976				if (fontElem != null)
3977				{
3978					fontElem.parentNode.removeChild(fontElem);
3979				}
3980			}
3981
3982			graph.extFonts = [];
3983
3984			for (var i = 0; this.previousExtFonts != null && i < this.previousExtFonts.length; i++)
3985			{
3986				this.ui.editor.graph.addExtFont(this.previousExtFonts[i].name, this.previousExtFonts[i].url);
3987			}
3988
3989			this.previousExtFonts = tmp;
3990		};
3991
3992		//Replace the default font family menu
3993		this.put('fontFamily', new Menu(mxUtils.bind(this, function(menu, parent)
3994		{
3995			var addItem = mxUtils.bind(this, function(fontName, fontUrl, deletable, fontLabel, tooltip)
3996			{
3997				var graph = editorUi.editor.graph;
3998
3999				var tr = this.styleChange(menu, fontLabel || fontName,
4000					(urlParams['ext-fonts'] != '1') ?
4001						[mxConstants.STYLE_FONTFAMILY, 'fontSource', 'FType'] : [mxConstants.STYLE_FONTFAMILY],
4002					(urlParams['ext-fonts'] != '1') ?
4003						[fontName, (fontUrl != null) ? encodeURIComponent(fontUrl) : null, null] : [fontName],
4004					null, parent, function()
4005				{
4006					if (urlParams['ext-fonts'] != '1')
4007					{
4008						graph.setFont(fontName, fontUrl);
4009					}
4010					else
4011					{
4012						document.execCommand('fontname', false, fontName);
4013						//Add the font to the file in case it was a previous font from the settings
4014						graph.addExtFont(fontName, fontUrl);
4015					}
4016
4017					editorUi.fireEvent(new mxEventObject('styleChanged',
4018						'keys', (urlParams['ext-fonts'] != '1') ?
4019							[mxConstants.STYLE_FONTFAMILY, 'fontSource', 'FType'] : [mxConstants.STYLE_FONTFAMILY],
4020						'values', (urlParams['ext-fonts'] != '1') ?
4021							[fontName, (fontUrl != null) ? encodeURIComponent(fontUrl) : null, null] : [fontName],
4022						'cells', [graph.cellEditor.getEditingCell()]));
4023				}, function()
4024				{
4025					graph.updateLabelElements(graph.getSelectionCells(), function(elt)
4026					{
4027						elt.removeAttribute('face');
4028						elt.style.fontFamily = null;
4029
4030						if (elt.nodeName == 'PRE')
4031						{
4032							graph.replaceElement(elt, 'div');
4033						}
4034					});
4035
4036					//Add the font to the file in case it was a previous font from the settings
4037					if (urlParams['ext-fonts'] == '1')
4038					{
4039						graph.addExtFont(fontName, fontUrl);
4040					}
4041				});
4042
4043				if (deletable)
4044				{
4045					var img = document.createElement('span');
4046					img.className = 'geSprite geSprite-delete';
4047					img.style.cursor = 'pointer';
4048					img.style.display = 'inline-block';
4049					tr.firstChild.nextSibling.nextSibling.appendChild(img);
4050
4051					mxEvent.addListener(img, (mxClient.IS_POINTER) ? 'pointerup' : 'mouseup', mxUtils.bind(this, function(evt)
4052					{
4053						if (urlParams['ext-fonts'] != '1')
4054						{
4055							delete Graph.recentCustomFonts[fontName.toLowerCase()];
4056
4057							for (var i = 0; i < this.customFonts.length; i++)
4058							{
4059								if (this.customFonts[i].name == fontName &&
4060									this.customFonts[i].url == fontUrl)
4061								{
4062									this.customFonts.splice(i, 1);
4063									editorUi.fireEvent(new mxEventObject('customFontsChanged'));
4064
4065									break;
4066								}
4067							}
4068						}
4069						else
4070						{
4071							var extFonts = mxUtils.clone(this.editorUi.editor.graph.extFonts);
4072
4073							if (extFonts != null && extFonts.length > 0)
4074							{
4075								for (var i = 0; i < extFonts.length; i++)
4076								{
4077									if (extFonts[i].name == fontName)
4078									{
4079										extFonts.splice(i, 1);
4080										break;
4081									}
4082								}
4083							}
4084
4085							var customFonts = mxUtils.clone(this.customFonts);
4086
4087							for (var i = 0; i < customFonts.length; i++)
4088							{
4089								if (customFonts[i].name == fontName)
4090								{
4091									customFonts.splice(i, 1);
4092									break;
4093								}
4094							}
4095
4096							var change = new ChangeExtFonts(this.editorUi, extFonts, customFonts);
4097							this.editorUi.editor.graph.model.execute(change);
4098						}
4099
4100						this.editorUi.hideCurrentMenu();
4101						mxEvent.consume(evt);
4102					}));
4103				}
4104
4105				Graph.addFont(fontName, fontUrl);
4106				tr.firstChild.nextSibling.style.fontFamily = fontName;
4107
4108				if (tooltip != null)
4109				{
4110					tr.setAttribute('title', tooltip);
4111				}
4112			});
4113
4114			var reserved = {};
4115
4116			for (var i = 0; i < this.defaultFonts.length; i++)
4117			{
4118				var value = this.defaultFonts[i];
4119
4120				if (typeof value === 'string')
4121				{
4122					addItem(value);
4123				}
4124				else if (value.fontFamily != null && value.fontUrl != null)
4125				{
4126					reserved[encodeURIComponent(value.fontFamily) + '@' +
4127						encodeURIComponent(value.fontUrl)] = true;
4128					addItem(value.fontFamily, value.fontUrl);
4129				}
4130			}
4131
4132			menu.addSeparator(parent);
4133
4134			if (urlParams['ext-fonts'] != '1')
4135			{
4136				// Special entries in the font menu are composed of custom fonts
4137				// from the local storage and actual used fonts in the file
4138				var duplicates = {};
4139				var fontNames = {};
4140				var entries = [];
4141
4142				function addEntry(entry)
4143				{
4144					var key = encodeURIComponent(entry.name) +
4145						((entry.url == null) ? '' :
4146						'@' + encodeURIComponent(entry.url));
4147
4148					if (!reserved[key])
4149					{
4150						var label = entry.name;
4151						var counter = 0;
4152
4153						while (fontNames[label.toLowerCase()] != null)
4154						{
4155							label = entry.name + ' (' + (++counter) + ')';
4156						}
4157
4158						if (duplicates[key] == null)
4159						{
4160							entries.push({name: entry.name, url: entry.url,
4161								label: label, title: entry.url});
4162							fontNames[label.toLowerCase()] = entry;
4163							duplicates[key] = entry;
4164						}
4165					}
4166				};
4167
4168				// Adds custom user defined fonts from local storage
4169				for (var i = 0; i < this.customFonts.length; i++)
4170				{
4171					addEntry(this.customFonts[i]);
4172				}
4173
4174				// Adds fonts that were recently used in the editor
4175				for (var key in Graph.recentCustomFonts)
4176				{
4177					addEntry(Graph.recentCustomFonts[key]);
4178				}
4179
4180				// Sorts by label
4181				entries.sort(function(a, b)
4182				{
4183					if (a.label < b.label)
4184					{
4185						return -1;
4186					}
4187					else if (a.label > b.label)
4188					{
4189						return 1;
4190					}
4191					else
4192					{
4193						return 0;
4194					}
4195				});
4196
4197				if (entries.length > 0)
4198				{
4199					for (var i = 0; i < entries.length; i++)
4200					{
4201						addItem(entries[i].name, entries[i].url, true,
4202							entries[i].label, entries[i].url);
4203					}
4204
4205					menu.addSeparator(parent);
4206				}
4207
4208				menu.addItem(mxResources.get('reset'), null, mxUtils.bind(this, function()
4209				{
4210					Graph.recentCustomFonts = {};
4211					this.customFonts = [];
4212					editorUi.fireEvent(new mxEventObject('customFontsChanged'));
4213				}), parent);
4214
4215				menu.addSeparator(parent);
4216			}
4217			else
4218			{
4219				//Load custom fonts already in the Graph
4220				var extFonts = this.editorUi.editor.graph.extFonts;
4221
4222				//Merge external fonts with custom fonts
4223				if (extFonts != null && extFonts.length > 0)
4224				{
4225					var custMap = {}, changed = false;
4226
4227					for (var i = 0; i < this.customFonts.length; i++)
4228					{
4229						custMap[this.customFonts[i].name] = true;
4230					}
4231
4232					for (var i = 0; i < extFonts.length; i++)
4233					{
4234						if (!custMap[extFonts[i].name])
4235						{
4236							this.customFonts.push(extFonts[i]);
4237							changed = true;
4238						}
4239					}
4240
4241					if (changed)
4242					{
4243						this.editorUi.fireEvent(new mxEventObject('customFontsChanged', 'customFonts', this.customFonts));
4244					}
4245				}
4246
4247				if (this.customFonts.length > 0)
4248				{
4249					for (var i = 0; i < this.customFonts.length; i++)
4250					{
4251						var name = this.customFonts[i].name, url = this.customFonts[i].url;
4252						addItem(name, url, true);
4253
4254						//Load external fonts without saving them to the file
4255						this.editorUi.editor.graph.addExtFont(name, url, true);
4256					}
4257
4258					menu.addSeparator(parent);
4259
4260					menu.addItem(mxResources.get('reset'), null, mxUtils.bind(this, function()
4261					{
4262						var change = new ChangeExtFonts(this.editorUi, [], []);
4263						editorUi.editor.graph.model.execute(change);
4264					}), parent);
4265
4266					menu.addSeparator(parent);
4267				}
4268			}
4269
4270			menu.addItem(mxResources.get('custom') + '...', null, mxUtils.bind(this, function()
4271			{
4272				var graph = this.editorUi.editor.graph;
4273				var curFontName = graph.getStylesheet().getDefaultVertexStyle()
4274					[mxConstants.STYLE_FONTFAMILY];
4275				var curType = 's';
4276				var curUrl = null;
4277
4278				// Handles in-place editing custom fonts via font family lookup
4279				if (urlParams['ext-fonts'] != '1' && graph.isEditing())
4280				{
4281					var node = graph.getSelectedEditingElement();
4282
4283					if (node != null)
4284					{
4285						var css = mxUtils.getCurrentStyle(node);
4286
4287						if (css != null)
4288						{
4289							curFontName = Graph.stripQuotes(css.fontFamily);
4290							curUrl = Graph.getFontUrl(curFontName, null);
4291
4292							if (curUrl != null)
4293							{
4294			    				if (Graph.isGoogleFontUrl(curUrl))
4295			    				{
4296			    					curUrl = null;
4297			    					curType = 'g';
4298			    				}
4299			    				else
4300			    				{
4301			    					curType = 'w';
4302			    				}
4303							}
4304						}
4305					}
4306				}
4307				else
4308				{
4309			    	var state = graph.getView().getState(graph.getSelectionCell());
4310
4311			    	if (state != null)
4312			    	{
4313			    		curFontName = state.style[mxConstants.STYLE_FONTFAMILY] || curFontName;
4314
4315			    		if (urlParams['ext-fonts'] != '1')
4316			    		{
4317			    			var temp = state.style['fontSource'];
4318
4319			    			if (temp != null)
4320			    			{
4321				    			temp = decodeURIComponent(temp);
4322
4323			    				if (Graph.isGoogleFontUrl(temp))
4324			    				{
4325			    					curType = 'g';
4326			    				}
4327			    				else
4328			    				{
4329			    					curType = 'w';
4330				    				curUrl = temp;
4331			    				}
4332			    			}
4333			    		}
4334			    		else
4335			    		{
4336			    			curType = state.style['FType'] || curType;
4337
4338			    			if (curType == 'w')
4339			    			{
4340				    			var extFonts = this.editorUi.editor.graph.extFonts;
4341				    			var webFont = null;
4342
4343				    			if (extFonts != null)
4344			    				{
4345				    				webFont = extFonts.find(function(ef)
4346		    						{
4347				    					return ef.name == curFontName;
4348		    						});
4349				    			}
4350
4351				    			// TODO: Resource is not defined
4352				    			curUrl = webFont != null? webFont.url : mxResources.get('urlNotFound', null, 'URL not found');
4353			    			}
4354			    		}
4355			    	}
4356				}
4357
4358    			if (curUrl != null && curUrl.substring(0, PROXY_URL.length) == PROXY_URL)
4359				{
4360    				curUrl = decodeURIComponent(curUrl.substr((PROXY_URL + '?url=').length));
4361				}
4362
4363		    	// Saves the current selection state
4364		    	var selState = null;
4365
4366		    	if (document.activeElement == graph.cellEditor.textarea)
4367				{
4368					selState = graph.cellEditor.saveSelection();
4369				}
4370
4371				var dlg = new FontDialog(this.editorUi, curFontName, curUrl, curType, mxUtils.bind(this, function(fontName, fontUrl, type)
4372				{
4373					// Restores the selection state
4374					if (selState != null)
4375					{
4376						graph.cellEditor.restoreSelection(selState);
4377						selState = null;
4378					}
4379
4380					if (fontName != null && fontName.length > 0)
4381					{
4382						if (urlParams['ext-fonts'] != '1' && graph.isEditing())
4383						{
4384							graph.setFont(fontName, fontUrl);
4385						}
4386						else
4387						{
4388							graph.getModel().beginUpdate();
4389
4390							try
4391							{
4392								graph.stopEditing(false);
4393
4394								if (urlParams['ext-fonts'] != '1')
4395								{
4396									graph.setCellStyles(mxConstants.STYLE_FONTFAMILY, fontName);
4397									graph.setCellStyles('fontSource', (fontUrl != null) ?
4398										encodeURIComponent(fontUrl) : null);
4399									graph.setCellStyles('FType', null);
4400								}
4401								else
4402								{
4403									graph.setCellStyles(mxConstants.STYLE_FONTFAMILY, fontName);
4404
4405									if (type != 's')
4406									{
4407										graph.setCellStyles('FType', type);
4408
4409										if (fontUrl.indexOf('http://') == 0)
4410										{
4411											fontUrl = PROXY_URL + '?url=' + encodeURIComponent(fontUrl);
4412										}
4413
4414										this.editorUi.editor.graph.addExtFont(fontName, fontUrl);
4415									}
4416								}
4417
4418								var addToCustom = true;
4419
4420								for (var i = 0; i < this.customFonts.length; i++)
4421								{
4422									if (this.customFonts[i].name == fontName)
4423									{
4424										addToCustom = false;
4425										break;
4426									}
4427								}
4428
4429								if (addToCustom)
4430								{
4431									this.customFonts.push({name: fontName, url: fontUrl});
4432									this.editorUi.fireEvent(new mxEventObject('customFontsChanged', 'customFonts', this.customFonts));
4433								}
4434							}
4435							finally
4436							{
4437								graph.getModel().endUpdate();
4438							}
4439						}
4440					}
4441				}));
4442				this.editorUi.showDialog(dlg.container, 380, Editor.enableWebFonts ? 250 : 180, true, true);
4443				dlg.init();
4444			}), parent, null, true);
4445		})));
4446	};
4447})();