1/**
2 * Copyright (c) 2006-2017, JGraph Ltd
3 * Copyright (c) 2006-2017, Gaudenz Alder
4 */
5(function()
6{
7	/**
8	 * Version
9	 */
10	EditorUi.VERSION = '@DRAWIO-VERSION@';
11
12	/**
13	 * Overrides compact UI setting.
14	 */
15	EditorUi.compactUi = uiTheme != 'atlas';
16
17	/**
18	 * Overrides default grid color for dark mode
19	 */
20	if (Editor.isDarkMode())
21	{
22		mxGraphView.prototype.gridColor = mxGraphView.prototype.defaultDarkGridColor;
23	}
24
25	/**
26	 * Switch to disable logging for mode and search terms.
27	 */
28	EditorUi.enableLogging = urlParams['stealth'] != '1' && urlParams['lockdown'] != '1' &&
29		(/.*\.draw\.io$/.test(window.location.hostname) ||
30		/.*\.diagrams\.net$/.test(window.location.hostname)) &&
31		window.location.hostname != 'support.draw.io';
32
33	/**
34	 * Protocol and hostname to use for embedded files. Default is https://www.draw.io
35	 */
36	EditorUi.drawHost = window.DRAWIO_BASE_URL;
37
38	/**
39	 * Protocol and hostname to use for embedded files. Default is https://www.draw.io
40	 */
41	EditorUi.lightboxHost = window.DRAWIO_LIGHTBOX_URL;
42
43	/**
44	 * Switch to disable logging for mode and search terms.
45	 */
46	EditorUi.lastErrorMessage = null;
47
48	/**
49	 * Switch to disable logging for mode and search terms.
50	 */
51	EditorUi.ignoredAnonymizedChars = '\n\t`~!@#$%^&*()_+{}|:"<>?-=[]\;\'.\/,\n\t';
52
53	/**
54	 * Specifies the URL for the templates index file.
55	 */
56	EditorUi.templateFile = TEMPLATE_PATH + '/index.xml';
57
58	/**
59	 * Specifies the URL for the diffsync cache.
60	 */
61	EditorUi.cacheUrl = (urlParams['dev'] == '1') ? '/cache' : window.REALTIME_URL;
62
63	if (EditorUi.cacheUrl == null && typeof DrawioFile !== 'undefined')
64	{
65		DrawioFile.SYNC = 'none'; //Disable real-time sync
66	}
67
68	/**
69	 * Cache timeout is 10 seconds.
70	 */
71	Editor.cacheTimeout = 10000;
72
73	/**
74	 * Switch to enable PlantUML in the insert from text dialog.
75	 * NOTE: This must also be enabled on the server-side.
76	 */
77	EditorUi.enablePlantUml = EditorUi.enableLogging;
78
79	/**
80	 * https://github.com/electron/electron/issues/2288
81	 */
82	EditorUi.isElectronApp = window != null && window.process != null &&
83		window.process.versions != null && window.process.versions['electron'] != null;
84
85	/**
86	 * Shortcut for capability check.
87	 */
88	EditorUi.nativeFileSupport = !mxClient.IS_OP && !EditorUi.isElectronApp &&
89		urlParams['extAuth'] != '1' && 'showSaveFilePicker' in window &&
90		'showOpenFilePicker' in window;
91
92	/**
93	 * Specifies if drafts should be saved in IndexedDB.
94	 */
95	EditorUi.enableDrafts = !mxClient.IS_CHROMEAPP && !EditorUi.isElectronApp &&
96		isLocalStorage && urlParams['drafts'] != '0';
97
98	/**
99	 * Link for scratchpad help.
100	 */
101	EditorUi.scratchpadHelpLink = 'https://www.diagrams.net/doc/faq/scratchpad';
102
103	/**
104	 * Default Mermaid config without using foreign objects in flowcharts.
105	 */
106	EditorUi.defaultMermaidConfig = {
107		theme:'neutral',
108		arrowMarkerAbsolute:false,
109	    flowchart:
110	    {
111	    	htmlLabels:false
112	    },
113	    sequence:
114	    {
115	    	diagramMarginX:50,
116	    	diagramMarginY:10,
117	    	actorMargin:50,
118	    	width:150,
119	    	height:65,
120	    	boxMargin:10,
121	    	boxTextMargin:5,
122	    	noteMargin:10,
123	    	messageMargin:35,
124	    	mirrorActors:true,
125	    	bottomMarginAdj:1,
126	    	useMaxWidth:true,
127	    	rightAngles:false,
128	    	showSequenceNumbers:false
129	    },
130	    gantt:{
131	    	titleTopMargin:25,
132	    	barHeight:20,
133	    	barGap:4,
134	    	topPadding:50,
135	    	leftPadding:75,
136	    	gridLineStartPadding:35,
137	    	fontSize:11,
138	    	fontFamily:'"Open-Sans", "sans-serif"',
139	    	numberSectionStyles:4,
140	    	axisFormat:'%Y-%m-%d'
141	    }
142	};
143
144	/**
145	 * Updates action states depending on the selection.
146	 */
147	EditorUi.logError = function(message, url, linenumber, colno, err, severity, quiet)
148	{
149		severity = ((severity != null) ? severity : (message.indexOf('NetworkError') >= 0 ||
150			message.indexOf('SecurityError') >= 0 || message.indexOf('NS_ERROR_FAILURE') >= 0 ||
151			message.indexOf('out of memory') >= 0) ? 'CONFIG' : 'SEVERE');
152
153		if (EditorUi.enableLogging && urlParams['dev'] != '1')
154		{
155			try
156			{
157				if (message == EditorUi.lastErrorMessage || (message != null && url != null &&
158					((message.indexOf('Script error') != -1) || (message.indexOf('extension') != -1))))
159				{
160					// TODO log external domain script failure "Script error." is
161					// reported when the error occurs in a script that is hosted
162					// on a domain other than the domain of the current page
163				}
164				// DocumentClosedError seems to be an FF bug an can be ignored for now
165				else if (message != null && message.indexOf('DocumentClosedError') < 0)
166				{
167					EditorUi.lastErrorMessage = message;
168					var logDomain = window.DRAWIO_LOG_URL != null ? window.DRAWIO_LOG_URL : '';
169					err = (err != null) ? err : new Error(message);
170
171					var img = new Image();
172					img.src = logDomain + '/log?severity=' + severity + '&v=' + encodeURIComponent(EditorUi.VERSION) +
173		    			'&msg=clientError:' + encodeURIComponent(message) + ':url:' + encodeURIComponent(window.location.href) +
174		    			':lnum:' + encodeURIComponent(linenumber) + ((colno != null) ? ':colno:' + encodeURIComponent(colno) : '') +
175		    			((err != null && err.stack != null) ? '&stack=' + encodeURIComponent(err.stack) : '');
176				}
177			}
178			catch (e)
179			{
180				// do nothing
181			}
182		}
183
184		try
185		{
186			if (!quiet && window.console != null)
187			{
188				console.error(severity, message, url, linenumber, colno, err);
189			}
190		}
191		catch (e)
192		{
193			// ignore
194		}
195	};
196
197	/**
198	 * Updates action states depending on the selection.
199	 */
200	EditorUi.logEvent = function(data)
201	{
202		if (urlParams['dev'] == '1')
203		{
204			EditorUi.debug('logEvent', data);
205		}
206		else if (EditorUi.enableLogging)
207		{
208			try
209			{
210				var logDomain = window.DRAWIO_LOG_URL != null ? window.DRAWIO_LOG_URL : '';
211				var img = new Image();
212				img.src = logDomain + '/images/1x1.png?' +
213						'v=' + encodeURIComponent(EditorUi.VERSION) +
214						((data != null) ? '&data=' + encodeURIComponent(JSON.stringify(data)) : '');
215	    	}
216			catch (e)
217			{
218	    			// ignore
219			}
220		}
221	};
222
223	/**
224	 * Sending error reports.
225	 */
226	EditorUi.sendReport = function(data, maxLength)
227	{
228		if (urlParams['dev'] == '1')
229		{
230			EditorUi.debug('sendReport', data);
231		}
232		else if (EditorUi.enableLogging)
233		{
234			try
235			{
236				maxLength = (maxLength != null) ? maxLength : 50000;
237
238				if (data.length > maxLength)
239				{
240					data = data.substring(0, maxLength) + '\n...[SHORTENED]'
241				}
242
243				mxUtils.post('/email', 'version=' + encodeURIComponent(EditorUi.VERSION) +
244					'&url=' + encodeURIComponent(window.location.href) +
245					'&data=' + encodeURIComponent(data));
246			}
247			catch (e)
248			{
249				// ignore
250			}
251		}
252	};
253
254	/**
255	 * Adds the listener for automatically saving the diagram for local changes.
256	 */
257	EditorUi.debug = function()
258	{
259		try
260		{
261			if (window.console != null && urlParams['test'] == '1')
262			{
263				var args = [new Date().toISOString()];
264
265				for (var i = 0; i < arguments.length; i++)
266			    {
267					if (arguments[i] != null)
268					{
269						args.push(arguments[i]);
270					}
271			    }
272
273				console.log.apply(console, args);
274			}
275		}
276		catch (e)
277		{
278			// ignore
279		}
280	};
281
282	/**
283	 * Static method for pasing PNG files.
284	 */
285	EditorUi.parsePng = function(f, fn, error)
286	{
287		var pos = 0;
288
289		function fread(d, count)
290		{
291			var start = pos;
292			pos += count;
293
294			return d.substring(start, pos);
295		};
296
297		// Reads unsigned long 32 bit big endian
298		function _freadint(d)
299		{
300			var bytes = fread(d, 4);
301
302			return bytes.charCodeAt(3) + (bytes.charCodeAt(2) << 8) +
303				(bytes.charCodeAt(1) << 16) + (bytes.charCodeAt(0) << 24);
304		};
305
306		// Checks signature
307		if (fread(f,8) != String.fromCharCode(137) + 'PNG' + String.fromCharCode(13, 10, 26, 10))
308		{
309			if (error != null)
310			{
311				error();
312			}
313
314			return;
315		}
316
317		// Reads header chunk
318		fread(f,4);
319
320		if (fread(f,4) != 'IHDR')
321		{
322			if (error != null)
323			{
324				error();
325			}
326
327			return;
328		}
329
330		fread(f, 17);
331
332		do
333		{
334			var n = _freadint(f);
335			var type = fread(f,4);
336
337			if (fn != null)
338			{
339				if (fn(pos - 8, type, n))
340				{
341					break;
342				}
343			}
344
345			value = fread(f,n);
346			fread(f,4);
347
348			if (type == 'IEND')
349			{
350				break;
351			}
352		}
353		while (n);
354	};
355
356	/**
357	 * Removes any values, styles and geometries from the given XML node.
358	 */
359	EditorUi.removeChildNodes = function(node)
360	{
361		while (node.firstChild != null)
362		{
363			node.removeChild(node.firstChild);
364		}
365	};
366
367	/**
368	 * Contains the default XML for an empty diagram.
369	 */
370	EditorUi.prototype.emptyDiagramXml = '<mxGraphModel><root><mxCell id="0"/><mxCell id="1" parent="0"/></root></mxGraphModel>';
371
372	/**
373	 *
374	 */
375	EditorUi.prototype.emptyLibraryXml = '<mxlibrary>[]</mxlibrary>';
376
377	/**
378	 * Sets the delay for autosave in milliseconds. Default is 2000.
379	 */
380	EditorUi.prototype.mode = null;
381
382	/**
383	 * General timeout is 25 seconds.
384	 * LATER: Move to Editor
385	 */
386	EditorUi.prototype.timeout = Editor.prototype.timeout;
387
388	/**
389	 * Allows for two buttons in the sidebar footer.
390	 */
391	EditorUi.prototype.sidebarFooterHeight = 38;
392
393	/**
394	 * Specifies the default custom shape style.
395	 */
396	EditorUi.prototype.defaultCustomShapeStyle = 'shape=stencil(tZRtTsQgEEBPw1+DJR7AoN6DbWftpAgE0Ortd/jYRGq72R+YNE2YgTePloEJGWblgA18ZuKFDcMj5/Sm8boZq+BgjCX4pTyqk6ZlKROitwusOMXKQDODx5iy4pXxZ5qTHiFHawxB0JrQZH7lCabQ0Fr+XWC1/E8zcsT/gAi+Subo2/3Mh6d/oJb5nU1b5tW7r2knautaa3T+U32o7f7vZwpJkaNDLORJjcu7t59m2jXxqX9un+tt022acsfmoKaQZ+vhhswZtS6Ne/ThQGt0IV0N3Yyv6P3CeT9/tHO0XFI5cAE=);whiteSpace=wrap;html=1;';
397
398	/**
399	 * Defines the maximum size for images.
400	 */
401	EditorUi.prototype.maxBackgroundSize = 1600;
402
403	/**
404	 * Defines the maximum size for images.
405	 */
406	EditorUi.prototype.maxImageSize = 520;
407
408	/**
409	 * Defines the maximum width for pasted text.
410	 * Use 0 to disable check.
411	 */
412	EditorUi.prototype.maxTextWidth = 520;
413
414	/**
415	 * Images above 100K should be resampled.
416	 */
417	EditorUi.prototype.resampleThreshold = 100000;
418
419	/**
420	 * Maximum allowed size for images is 1 MB.
421	 */
422	EditorUi.prototype.maxImageBytes = 1000000;
423
424	/**
425	 * Maximum size for background images is 2.5 MB.
426	 */
427	EditorUi.prototype.maxBackgroundBytes = 2500000;
428
429	/**
430	 * Maximum size for text files in labels is 0.5 MB.
431	 */
432	EditorUi.prototype.maxTextBytes = 500000;
433
434	/**
435	 * Holds the current file.
436	 */
437	EditorUi.prototype.currentFile = null;
438
439	/**
440	 * Specifies if PDF export should be done via print dialog. Default is
441	 * false which uses the PhantomJS backend to create the PDF.
442	 */
443	EditorUi.prototype.printPdfExport = false;
444
445	/**
446	 * Specifies if PDF export with pages is enabled.
447	 */
448	EditorUi.prototype.pdfPageExport = true;
449
450	/**
451	 * Restores app defaults for UI
452	 */
453	EditorUi.prototype.formatEnabled = urlParams['format'] != '0';
454
455	/**
456	 * Whether template action should be shown in insert menu.
457	 */
458	EditorUi.prototype.insertTemplateEnabled = true;
459
460	/**
461	 * Restores app defaults for UI
462	 */
463	EditorUi.prototype.closableScratchpad = true;
464
465	/**
466	 * Restores app defaults for UI
467	 */
468	EditorUi.prototype.embedExportBorder = 8;
469
470	/**
471	 * Restores app defaults for UI
472	 */
473	EditorUi.prototype.embedExportBackground = null;
474
475	/**
476	 * Capability check for canvas export
477	 */
478	(function()
479	{
480		EditorUi.prototype.useCanvasForExport = false;
481		EditorUi.prototype.jpgSupported = false;
482
483		// Checks if canvas is supported
484		try
485		{
486			var cnv = document.createElement('canvas');
487			EditorUi.prototype.canvasSupported = !!(cnv.getContext && cnv.getContext('2d'));
488		}
489		catch (e)
490		{
491			// ignore
492		}
493
494		try
495		{
496			var canvas = document.createElement('canvas');
497			var img = new Image();
498
499			// LATER: Capability check should not be async
500			img.onload = function()
501			{
502				try
503				{
504			   		var ctx = canvas.getContext('2d');
505			   		ctx.drawImage(img, 0, 0);
506
507			   		// Works in Chrome, Firefox, Edge, Safari and Opera
508					var result = canvas.toDataURL('image/png');
509					EditorUi.prototype.useCanvasForExport = result != null && result.length > 6;
510				}
511				catch (e)
512				{
513					// ignore
514				}
515			};
516
517			// Checks if SVG with foreignObject can be exported
518			var svg = '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="1px" height="1px" version="1.1"><foreignObject pointer-events="all" width="1" height="1"><div xmlns="http://www.w3.org/1999/xhtml"></div></foreignObject></svg>';
519			img.src = 'data:image/svg+xml;base64,' + btoa(unescape(encodeURIComponent(svg)));
520		}
521		catch (e)
522		{
523			// ignore
524		}
525
526		// Checks for client-side JPG support
527		try
528		{
529		    var canvas = document.createElement('canvas');
530		    canvas.width = canvas.height = 1;
531		    var uri = canvas.toDataURL('image/jpeg');
532
533		    EditorUi.prototype.jpgSupported = (uri.match('image/jpeg') !== null);
534		}
535		catch (e)
536		{
537			// ignore
538		}
539	})();
540
541	/**
542	 * Hook for subclassers.
543	 */
544	EditorUi.prototype.openLink = function(url, target, allowOpener)
545	{
546		// LATER: Replace this with direct calls to graph
547		return this.editor.graph.openLink(url, target, allowOpener);
548	};
549
550	/**
551	 * Hook for subclassers.
552	 */
553	EditorUi.prototype.showSplash = function(force) { };
554
555	/**
556	 * Abstraction for local storage access.
557	 */
558	EditorUi.prototype.getLocalData = function(key, fn)
559	{
560		fn(localStorage.getItem(key));
561	};
562
563	/**
564	 * Abstraction for local storage access.
565	 */
566	EditorUi.prototype.setLocalData = function(key, data, fn)
567	{
568		localStorage.setItem(key, data);
569
570		if (fn != null)
571		{
572			fn();
573		}
574	};
575
576	/**
577	 * Abstraction for local storage access.
578	 */
579	EditorUi.prototype.removeLocalData = function(key, fn)
580	{
581		localStorage.removeItem(key)
582		fn();
583	};
584
585	EditorUi.prototype.setMathEnabled = function(value)
586	{
587		this.editor.graph.mathEnabled = value;
588		this.editor.updateGraphComponents();
589		this.editor.graph.refresh();
590		this.editor.graph.defaultMathEnabled = value;
591
592		this.fireEvent(new mxEventObject('mathEnabledChanged'));
593	};
594
595	EditorUi.prototype.isMathEnabled = function(value)
596	{
597		return this.editor.graph.mathEnabled;
598	};
599
600	/**
601	 * Returns true if offline app, which isn't a defined thing
602	 */
603	EditorUi.prototype.isOfflineApp = function()
604	{
605		return urlParams['offline'] == '1';
606	};
607
608	/**
609	 * Returns true if no external comms allowed or possible
610	 */
611	EditorUi.prototype.isOffline = function(ignoreStealth)
612	{
613		return this.isOfflineApp() || !navigator.onLine || (!ignoreStealth && (urlParams['stealth'] == '1' || urlParams['lockdown'] == '1'));
614	};
615
616	/**
617	 * Translates this point by the given vector.
618	 *
619	 * @param {number} dx X-coordinate of the translation.
620	 * @param {number} dy Y-coordinate of the translation.
621	 */
622	EditorUi.prototype.createSpinner = function(x, y, size)
623	{
624		var autoPosition = (x == null || y == null);
625		size = (size != null) ? size : 24;
626
627		var spinner = new Spinner({
628			lines: 12, // The number of lines to draw
629			length: size, // The length of each line
630			width: Math.round(size / 3), // The line thickness
631			radius: Math.round(size / 2), // The radius of the inner circle
632			rotate: 0, // The rotation offset
633			color: (Editor.isDarkMode()) ? '#c0c0c0' : '#000', // #rgb or #rrggbb
634			speed: 1.5, // Rounds per second
635			trail: 60, // Afterglow percentage
636			shadow: false, // Whether to render a shadow
637			hwaccel: false, // Whether to use hardware acceleration
638			zIndex: 2e9 // The z-index (defaults to 2000000000)
639		});
640
641		// Extends spin method to include an optional label
642		var oldSpin = spinner.spin;
643
644		spinner.spin = function(container, label)
645		{
646			var result = false;
647
648			if (!this.active)
649			{
650				oldSpin.call(this, container);
651				this.active = true;
652
653				if (label != null)
654				{
655					if (autoPosition)
656					{
657						y = Math.max(document.body.clientHeight || 0, document.documentElement.clientHeight || 0) / 2;
658						x = document.body.clientWidth / 2 - 2;
659					}
660
661					var status = document.createElement('div');
662					status.style.position = 'absolute';
663					status.style.whiteSpace = 'nowrap';
664					status.style.background = '#4B4243';
665					status.style.color = 'white';
666					status.style.fontFamily = Editor.defaultHtmlFont;
667					status.style.fontSize = '9pt';
668					status.style.padding = '6px';
669					status.style.paddingLeft = '10px';
670					status.style.paddingRight = '10px';
671					status.style.zIndex = 2e9;
672					status.style.left = Math.max(0, x) + 'px';
673					status.style.top = Math.max(0, y + 70) + 'px';
674
675					mxUtils.setPrefixedStyle(status.style, 'borderRadius', '6px');
676					mxUtils.setPrefixedStyle(status.style, 'transform', 'translate(-50%,-50%)');
677
678					if (!Editor.isDarkMode())
679					{
680						mxUtils.setPrefixedStyle(status.style, 'boxShadow', '2px 2px 3px 0px #ddd');
681					}
682
683					if (label.substring(label.length - 3, label.length) != '...' &&
684						label.charAt(label.length - 1) != '!')
685					{
686						label = label + '...';
687					}
688
689					status.innerHTML = label;
690					container.appendChild(status);
691					spinner.status = status;
692				}
693
694				// Pause returns a function to resume the spinner
695				this.pause = mxUtils.bind(this, function()
696				{
697					var fn = function() { };
698
699					if (this.active)
700					{
701						fn = mxUtils.bind(this, function()
702						{
703							this.spin(container, label);
704						});
705					}
706
707					this.stop();
708
709					return fn;
710				});
711
712				result = true;
713			}
714
715			return result;
716		};
717
718		// Extends stop method to remove the optional label
719		var oldStop = spinner.stop;
720
721		spinner.stop = function()
722		{
723			oldStop.call(this);
724			this.active = false;
725
726			if (spinner.status != null && spinner.status.parentNode != null)
727			{
728				spinner.status.parentNode.removeChild(spinner.status);
729			}
730
731			spinner.status = null;
732		};
733
734		spinner.pause = function()
735		{
736			return function() {};
737		};
738
739		return spinner;
740	};
741
742	/**
743	 * Returns true if the given string contains a compatible graph model.
744	 */
745	EditorUi.prototype.isCompatibleString = function(data)
746	{
747		try
748		{
749			var doc = mxUtils.parseXml(data);
750			var node = this.editor.extractGraphModel(doc.documentElement, true);
751
752			return node != null && node.getElementsByTagName('parsererror').length == 0;
753		}
754		catch (e)
755		{
756			// ignore
757		}
758
759		return false;
760	};
761
762	/**
763	 * Returns true if the given binary data is a Visio file.
764	 */
765	EditorUi.prototype.isVisioData = function(data)
766	{
767		return data.length > 8 && ((data.charCodeAt(0) == 0xD0 && data.charCodeAt(1) == 0xCF &&
768			data.charCodeAt(2) == 0x11 && data.charCodeAt(3) == 0xE0 && data.charCodeAt(4) == 0xA1 && data.charCodeAt(5) == 0xB1 &&
769			data.charCodeAt(6) == 0x1A && data.charCodeAt(7) == 0xE1) || (data.charCodeAt(0) == 0x50 && data.charCodeAt(1) == 0x4B &&
770			data.charCodeAt(2) == 0x03 && data.charCodeAt(3) == 0x04) || (data.charCodeAt(0) == 0x50 && data.charCodeAt(1) == 0x4B &&
771			data.charCodeAt(2) == 0x03 && data.charCodeAt(3) == 0x06));
772	};
773
774	/**
775	 * Returns true if the given binary data is a Visio file that requires remote conversion.
776	 * This code returns true for vss, vsd and vdx files.
777	 */
778	EditorUi.prototype.isRemoteVisioData = function(data)
779	{
780		return data.length > 8 && ((data.charCodeAt(0) == 0xD0 && data.charCodeAt(1) == 0xCF &&
781			data.charCodeAt(2) == 0x11 && data.charCodeAt(3) == 0xE0 && data.charCodeAt(4) == 0xA1 && data.charCodeAt(5) == 0xB1 &&
782			data.charCodeAt(6) == 0x1A && data.charCodeAt(7) == 0xE1) || (data.charCodeAt(0) == 0x3C && data.charCodeAt(1) == 0x3F &&
783			data.charCodeAt(2) == 0x78 && data.charCodeAt(3) == 0x6D && data.charCodeAt(3) == 0x6C));
784	};
785
786	/**
787	 * Returns true if the given binary data is a PNG file.
788	 */
789	EditorUi.prototype.isPngData = function(data)
790	{
791		return data.length > 8 && data.charCodeAt(0) == 137 && data.charCodeAt(1) == 80 &&
792			data.charCodeAt(2) == 78 && data.charCodeAt(3) == 71 && data.charCodeAt(4) == 13 &&
793			data.charCodeAt(5) == 10 && data.charCodeAt(6) == 26 && data.charCodeAt(7) == 10;
794	};
795
796	/**
797	 * Adds keyboard shortcuts for page handling.
798	 */
799    var editorUiCreateKeyHandler = EditorUi.prototype.createKeyHandler;
800    EditorUi.prototype.createKeyHandler = function(editor)
801    {
802    	var keyHandler = editorUiCreateKeyHandler.apply(this, arguments);
803
804    	if (!this.editor.chromeless || this.editor.editable)
805		{
806	    	var keyHandlerGetFunction = keyHandler.getFunction;
807	    	var graph = this.editor.graph;
808	    	var ui = this;
809
810	    	keyHandler.getFunction = function(evt)
811	    	{
812	    		if (graph.isSelectionEmpty() && ui.pages != null && ui.pages.length > 0)
813	    		{
814	    			var idx = ui.getSelectedPageIndex();
815
816	    			if (mxEvent.isShiftDown(evt))
817	    			{
818		    			if (evt.keyCode == 37)
819		    			{
820	    					return function()
821	    					{
822			    				if (idx > 0)
823			    				{
824		    						ui.movePage(idx, idx - 1);
825		    					}
826		    				};
827		    			}
828		    			else if (evt.keyCode == 38)
829		    			{
830	    					return function()
831	    					{
832			    				if (idx > 0)
833			    				{
834		    						ui.movePage(idx, 0);
835		    					}
836		    				};
837		    			}
838		    			else if (evt.keyCode == 39)
839		    			{
840	    					return function()
841	    					{
842			    				if (idx < ui.pages.length - 1)
843			    				{
844		    						ui.movePage(idx, idx + 1);
845		    					}
846		    				};
847		    			}
848		    			else if (evt.keyCode == 40)
849		    			{
850	    					return function()
851	    					{
852			    				if (idx < ui.pages.length - 1)
853			    				{
854		    						ui.movePage(idx, ui.pages.length - 1);
855		    					}
856		    				};
857		    			}
858	    			}
859	    			else if (mxEvent.isControlDown(evt) || (mxClient.IS_MAC && mxEvent.isMetaDown(evt)))
860					{
861	    				if (evt.keyCode == 37)
862		    			{
863	    					return function()
864	    					{
865			    				if (idx > 0)
866			    				{
867		    						ui.selectNextPage(false);
868		    					}
869		    				};
870		    			}
871		    			else if (evt.keyCode == 38)
872		    			{
873	    					return function()
874	    					{
875			    				if (idx > 0)
876			    				{
877			    					ui.selectPage(ui.pages[0]);
878		    					}
879		    				};
880		    			}
881		    			else if (evt.keyCode == 39)
882		    			{
883	    					return function()
884	    					{
885			    				if (idx < ui.pages.length - 1)
886			    				{
887			    					ui.selectNextPage(true);
888		    					}
889		    				};
890		    			}
891		    			else if (evt.keyCode == 40)
892		    			{
893	    					return function()
894	    					{
895			    				if (idx < ui.pages.length - 1)
896			    				{
897			    					ui.selectPage(ui.pages[ui.pages.length - 1]);
898		    					}
899		    				};
900		    			}
901
902					}
903	    		}
904
905	    		return keyHandlerGetFunction.apply(this, arguments);
906	    	};
907		}
908
909    	return keyHandler;
910    };
911
912	/**
913	 * Extracts the mxfile from the given HTML data from a data transfer event.
914	 */
915	var editorUiExtractGraphModelFromHtml = EditorUi.prototype.extractGraphModelFromHtml;
916	EditorUi.prototype.extractGraphModelFromHtml = function(data)
917	{
918		var result = editorUiExtractGraphModelFromHtml.apply(this, arguments);
919
920		if (result == null)
921		{
922			try
923			{
924		    	var idx = data.indexOf('&lt;mxfile ');
925
926		    	if (idx >= 0)
927		    	{
928		    		var idx2 = data.lastIndexOf('&lt;/mxfile&gt;');
929
930		    		if (idx2 > idx)
931		    		{
932		    			result = data.substring(idx, idx2 + 15).replace(/&gt;/g, '>').
933		    				replace(/&lt;/g, '<').replace(/\\&quot;/g, '"').replace(/\n/g, '');
934		    		}
935		    	}
936		    	else
937		    	{
938		    		// Gets compressed data from mxgraph element in HTML document
939					var doc = mxUtils.parseXml(data);
940					var node = this.editor.extractGraphModel(doc.documentElement, this.pages != null ||
941						this.diagramContainer.style.visibility == 'hidden');
942					result = (node != null) ? mxUtils.getXml(node) : '';
943		    	}
944			}
945			catch (e)
946			{
947				// ignore
948			}
949		}
950
951		return result;
952	};
953
954	/**
955	 * Workaround for malformed xhtml meta element bug 07.08.16. The trailing slash was missing causing
956	 * reopen to fail trying to parse. Used in replaceFileData, setFileData and importFile.
957	 */
958	EditorUi.prototype.validateFileData = function(data)
959	{
960		if (data != null && data.length > 0)
961		{
962			var index = data.indexOf('<meta charset="utf-8">');
963
964			if (index >= 0)
965			{
966				var replaceString = '<meta charset="utf-8"/>';
967				var replaceStrLen = replaceString.length;
968				data = data.slice(0, index) + replaceString + data.slice(index + replaceStrLen - 1, data.length);
969			}
970
971			data = Graph.zapGremlins(data);
972		}
973
974		return data;
975	};
976
977	/**
978	 *
979	 */
980	EditorUi.prototype.replaceFileData = function(data)
981	{
982		data = this.validateFileData(data);
983		var node = (data != null && data.length > 0) ? mxUtils.parseXml(data).documentElement : null;
984
985		// Some nodes must be extracted here to find the mxfile node
986		// LATER: Remove duplicate call to extractGraphModel in overridden setGraphXml
987		var tmp = (node != null) ? this.editor.extractGraphModel(node, true) : null;
988
989		if (tmp != null)
990		{
991			node = tmp;
992		}
993
994		if (node != null)
995		{
996			var graph = this.editor.graph;
997
998			graph.model.beginUpdate();
999			try
1000			{
1001				var oldPages = (this.pages != null) ? this.pages.slice() : null;
1002				var nodes = node.getElementsByTagName('diagram');
1003
1004				if (urlParams['pages'] != '0' || nodes.length > 1 ||
1005					(nodes.length == 1 && nodes[0].hasAttribute('name')))
1006				{
1007					this.fileNode = node;
1008					this.pages = (this.pages != null) ? this.pages : [];
1009
1010					// Wraps page nodes
1011					for (var i = nodes.length - 1; i >= 0; i--)
1012					{
1013						var page = this.updatePageRoot(new DiagramPage(nodes[i]));
1014
1015						// Checks for invalid page names
1016						if (page.getName() == null)
1017						{
1018							page.setName(mxResources.get('pageWithNumber', [i + 1]));
1019						}
1020
1021						graph.model.execute(new ChangePage(this, page, (i == 0) ? page : null, 0));
1022					}
1023				}
1024				else
1025				{
1026					// Creates tabbed file structure if enforced by URL
1027					if (urlParams['pages'] != '0' && this.fileNode == null)
1028					{
1029						this.fileNode = node.ownerDocument.createElement('mxfile');
1030						this.currentPage = new DiagramPage(node.ownerDocument.createElement('diagram'));
1031						this.currentPage.setName(mxResources.get('pageWithNumber', [1]));
1032						graph.model.execute(new ChangePage(this, this.currentPage, this.currentPage, 0));
1033					}
1034
1035					// Avoids scroll offset when switching page
1036					this.editor.setGraphXml(node);
1037
1038					// Avoids duplicate parsing of the XML stored in the node
1039					if (this.currentPage != null)
1040					{
1041						this.currentPage.root = this.editor.graph.model.root;
1042					}
1043				}
1044
1045				if (oldPages != null)
1046				{
1047					for (var i = 0; i < oldPages.length; i++)
1048					{
1049						graph.model.execute(new ChangePage(this, oldPages[i], null));
1050					}
1051				}
1052			}
1053			finally
1054			{
1055				graph.model.endUpdate();
1056			}
1057		}
1058	};
1059
1060	/**
1061	 * Translates this point by the given vector.
1062	 *
1063	 * @param {number} dx X-coordinate of the translation.
1064	 * @param {number} dy Y-coordinate of the translation.
1065	 */
1066	EditorUi.prototype.createFileData = function(node, graph, file, url, forceXml, forceSvg, forceHtml,
1067		embeddedCallback, ignoreSelection, compact, uncompressed)
1068	{
1069		graph = (graph != null) ? graph : this.editor.graph;
1070		forceXml = (forceXml != null) ? forceXml : false;
1071		ignoreSelection = (ignoreSelection != null) ? ignoreSelection : true;
1072
1073		var editLink = null;
1074		var redirect = null;
1075
1076		if (file == null || file.getMode() == App.MODE_DEVICE || file.getMode() == App.MODE_BROWSER)
1077		{
1078			editLink = '_blank';
1079		}
1080		else
1081		{
1082			editLink = url;
1083			redirect = editLink;
1084		}
1085
1086		if (node == null)
1087		{
1088			return '';
1089		}
1090		else
1091		{
1092			var fileNode = node;
1093
1094			// Ignores case for possible HTML or XML nodes
1095			if (fileNode.nodeName.toLowerCase() != 'mxfile')
1096			{
1097				if (uncompressed)
1098				{
1099					var diagramNode = node.ownerDocument.createElement('diagram');
1100					diagramNode.setAttribute('id', Editor.guid());
1101					diagramNode.appendChild(node);
1102
1103					fileNode = node.ownerDocument.createElement('mxfile');
1104					fileNode.appendChild(diagramNode);
1105				}
1106				else
1107				{
1108					// Removes control chars in input for correct roundtrip check
1109					var text = Graph.zapGremlins(mxUtils.getXml(node));
1110					var data = Graph.compress(text);
1111
1112					// Fallback to plain XML for invalid compression
1113					// TODO: Remove this fallback with active pages
1114					if (Graph.decompress(data) != text)
1115					{
1116						return text;
1117					}
1118					else
1119					{
1120						var diagramNode = node.ownerDocument.createElement('diagram');
1121						diagramNode.setAttribute('id', Editor.guid());
1122						mxUtils.setTextContent(diagramNode, data);
1123
1124						fileNode = node.ownerDocument.createElement('mxfile');
1125						fileNode.appendChild(diagramNode);
1126					}
1127				}
1128			}
1129
1130			if (!compact)
1131			{
1132				// Removes old metadata
1133				fileNode.removeAttribute('userAgent');
1134				fileNode.removeAttribute('version');
1135				fileNode.removeAttribute('editor');
1136				fileNode.removeAttribute('pages');
1137				fileNode.removeAttribute('type');
1138
1139				if (mxClient.IS_CHROMEAPP)
1140				{
1141					fileNode.setAttribute('host', 'Chrome');
1142				}
1143				else if (EditorUi.isElectronApp)
1144				{
1145					fileNode.setAttribute('host', 'Electron');
1146				}
1147				else
1148				{
1149					fileNode.setAttribute('host', window.location.hostname);
1150				}
1151
1152				// Adds new metadata
1153				fileNode.setAttribute('modified', new Date().toISOString());
1154				fileNode.setAttribute('agent', navigator.appVersion);
1155				fileNode.setAttribute('version', EditorUi.VERSION);
1156				fileNode.setAttribute('etag', Editor.guid());
1157
1158				var md = (file != null) ? file.getMode() : this.mode;
1159
1160				if (md != null)
1161				{
1162					fileNode.setAttribute('type', md);
1163				}
1164
1165				if (fileNode.getElementsByTagName('diagram').length > 1 && this.pages != null)
1166				{
1167					fileNode.setAttribute('pages', this.pages.length);
1168				}
1169			}
1170			else
1171			{
1172				fileNode = fileNode.cloneNode(true);
1173				fileNode.removeAttribute('modified');
1174				fileNode.removeAttribute('host');
1175				fileNode.removeAttribute('agent');
1176				fileNode.removeAttribute('etag');
1177				fileNode.removeAttribute('userAgent');
1178				fileNode.removeAttribute('version');
1179				fileNode.removeAttribute('editor');
1180				fileNode.removeAttribute('type');
1181			}
1182
1183			var xml = (uncompressed) ? mxUtils.getPrettyXml(fileNode) : mxUtils.getXml(fileNode);
1184
1185			// Writes the file as an embedded HTML file
1186			if (!forceSvg && !forceXml && (forceHtml || (file != null && /(\.html)$/i.test(file.getTitle()))))
1187			{
1188				xml = this.getHtml2(mxUtils.getXml(fileNode), graph, (file != null) ? file.getTitle() : null, editLink, redirect);
1189			}
1190			// Maps the XML data to the content attribute in the SVG node
1191			else if (forceSvg || (!forceXml && file != null && /(\.svg)$/i.test(file.getTitle())))
1192			{
1193				if (file != null && (file.getMode() == App.MODE_DEVICE || file.getMode() == App.MODE_BROWSER))
1194				{
1195					url = null;
1196				}
1197
1198				xml = this.getEmbeddedSvg(xml, graph, url, null, embeddedCallback, ignoreSelection, redirect);
1199			}
1200
1201			return xml;
1202		}
1203	};
1204
1205	/**
1206	 * Translates this point by the given vector.
1207	 *
1208	 * @param {number} dx X-coordinate of the translation.
1209	 * @param {number} dy Y-coordinate of the translation.
1210	 */
1211	EditorUi.prototype.getXmlFileData = function(ignoreSelection, currentPage, uncompressed, resolveReferences)
1212	{
1213		ignoreSelection = (ignoreSelection != null) ? ignoreSelection : true;
1214		currentPage = (currentPage != null) ? currentPage : false;
1215		uncompressed = (uncompressed != null) ? uncompressed : !Editor.compressXml;
1216
1217		// Generats graph model XML node for single page export
1218		var node = this.editor.getGraphXml(ignoreSelection, resolveReferences);
1219
1220		if (ignoreSelection && this.fileNode != null && this.currentPage != null)
1221		{
1222			// Updates current page XML if selection is ignored
1223			EditorUi.removeChildNodes(this.currentPage.node);
1224			mxUtils.setTextContent(this.currentPage.node, Graph.compressNode(node));
1225
1226			// Creates a clone of the file node for processing
1227			node = this.fileNode.cloneNode(false);
1228
1229			// Appends the node of the page and applies compression
1230			function appendPage(pageNode)
1231			{
1232				var models = pageNode.getElementsByTagName('mxGraphModel');
1233				var modelNode = (models.length > 0) ? models[0] : null;
1234				var clone = pageNode;
1235
1236				if (modelNode == null && uncompressed)
1237				{
1238					var text = mxUtils.trim(mxUtils.getTextContent(pageNode));
1239					clone = pageNode.cloneNode(false);
1240
1241					if (text.length > 0)
1242					{
1243						var tmp = Graph.decompress(text);
1244
1245						if (tmp != null && tmp.length > 0)
1246						{
1247							clone.appendChild(mxUtils.parseXml(tmp).documentElement);
1248						}
1249					}
1250				}
1251				else if (modelNode != null && !uncompressed)
1252				{
1253					clone = pageNode.cloneNode(false);
1254					mxUtils.setTextContent(clone, Graph.compressNode(modelNode));
1255				}
1256				else
1257				{
1258					clone = pageNode.cloneNode(true);
1259				}
1260
1261				node.appendChild(clone);
1262			};
1263
1264			if (currentPage)
1265			{
1266				appendPage(this.currentPage.node);
1267			}
1268			else
1269			{
1270				// Restores order of pages
1271				for (var i = 0; i < this.pages.length; i++)
1272				{
1273					var page = this.pages[i];
1274					var currNode = page.node;
1275
1276					if (page != this.currentPage)
1277					{
1278						if (page.needsUpdate)
1279						{
1280							var enc = new mxCodec(mxUtils.createXmlDocument());
1281							var temp = enc.encode(new mxGraphModel(page.root));
1282							this.editor.graph.saveViewState(page.viewState,
1283								temp, null, resolveReferences);
1284							EditorUi.removeChildNodes(currNode);
1285							mxUtils.setTextContent(currNode, Graph.compressNode(temp));
1286
1287							// Marks the page as up-to-date
1288							delete page.needsUpdate;
1289						}
1290						else if (resolveReferences)
1291						{
1292							this.updatePageRoot(page);
1293
1294							// Forces update of background page image in offscreen page
1295							if (page.viewState.backgroundImage != null)
1296							{
1297								if (page.viewState.backgroundImage.originalSrc != null)
1298								{
1299									page.viewState.backgroundImage = this.createImageForPageLink(
1300										page.viewState.backgroundImage.originalSrc, page);
1301								}
1302								else if (Graph.isPageLink(page.viewState.backgroundImage.src))
1303								{
1304									page.viewState.backgroundImage = this.createImageForPageLink(
1305										page.viewState.backgroundImage.src, page);
1306								}
1307							}
1308
1309							// Updates the page node
1310							if (page.viewState.backgroundImage != null &&
1311								page.viewState.backgroundImage.originalSrc != null)
1312							{
1313								var enc = new mxCodec(mxUtils.createXmlDocument());
1314								var temp = enc.encode(new mxGraphModel(page.root));
1315								this.editor.graph.saveViewState(page.viewState,
1316									temp, null, resolveReferences);
1317								currNode = currNode.cloneNode(false);
1318								mxUtils.setTextContent(currNode, Graph.compressNode(temp));
1319							}
1320						}
1321					}
1322
1323					appendPage(currNode);
1324				}
1325			}
1326		}
1327
1328		return node;
1329	};
1330
1331	/**
1332	 * Removes any values, styles and geometries from the given XML node.
1333	 */
1334	EditorUi.prototype.anonymizeString = function(text, zeros)
1335	{
1336		var result = [];
1337
1338		for (var i = 0; i < text.length; i++)
1339		{
1340			var c = text.charAt(i);
1341
1342			if (EditorUi.ignoredAnonymizedChars.indexOf(c) >= 0)
1343			{
1344				result.push(c);
1345			}
1346			else if (!isNaN(parseInt(c)))
1347			{
1348				result.push((zeros) ? '0' : Math.round(Math.random() * 9));
1349			}
1350			else if (c.toLowerCase() != c)
1351			{
1352				result.push(String.fromCharCode(65 + Math.round(Math.random() * 25)));
1353			}
1354			else if (c.toUpperCase() != c)
1355			{
1356				result.push(String.fromCharCode(97 + Math.round(Math.random() * 25)));
1357			}
1358			else if (/\s/.test(c))
1359			{
1360				/* any whitespace */
1361				result.push(' ');
1362			}
1363			else
1364			{
1365				result.push('?');
1366			}
1367		}
1368
1369		return result.join('');
1370	};
1371
1372	/**
1373	 * Removes any values, styles and geometries from the given XML node.
1374	 */
1375	EditorUi.prototype.anonymizePatch = function(patch)
1376	{
1377		if (patch[EditorUi.DIFF_INSERT] != null)
1378		{
1379			for (var i = 0; i < patch[EditorUi.DIFF_INSERT].length; i++)
1380			{
1381				try
1382				{
1383					var data = patch[EditorUi.DIFF_INSERT][i].data;
1384					var doc = mxUtils.parseXml(data);
1385					var clone = doc.documentElement.cloneNode(false);
1386
1387					if (clone.getAttribute('name') != null)
1388					{
1389						clone.setAttribute('name', this.anonymizeString(clone.getAttribute('name')));
1390					}
1391
1392					patch[EditorUi.DIFF_INSERT][i].data = mxUtils.getXml(clone);
1393				}
1394				catch (e)
1395				{
1396					patch[EditorUi.DIFF_INSERT][i].data = e.message;
1397				}
1398			}
1399		}
1400
1401		if (patch[EditorUi.DIFF_UPDATE] != null)
1402		{
1403			for (var pageId in patch[EditorUi.DIFF_UPDATE])
1404			{
1405				var diff = patch[EditorUi.DIFF_UPDATE][pageId];
1406
1407				if (diff.name != null)
1408				{
1409					diff.name = this.anonymizeString(diff.name);
1410				}
1411
1412				if (diff.cells != null)
1413				{
1414					var anonymizeCellDiffs = mxUtils.bind(this, function(key)
1415					{
1416						var cellDiffs = diff.cells[key];
1417
1418						if (cellDiffs != null)
1419						{
1420							for (var cellId in cellDiffs)
1421							{
1422								if (cellDiffs[cellId].value != null)
1423								{
1424									cellDiffs[cellId].value = '[' +
1425										cellDiffs[cellId].value.length + ']';
1426								}
1427
1428								if (cellDiffs[cellId].xmlValue != null)
1429								{
1430									cellDiffs[cellId].xmlValue = '[' +
1431										cellDiffs[cellId].xmlValue.length + ']';
1432								}
1433
1434								if (cellDiffs[cellId].style != null)
1435								{
1436									cellDiffs[cellId].style = '[' +
1437										cellDiffs[cellId].style.length + ']';
1438								}
1439
1440								if (Object.keys(cellDiffs[cellId]).length == 0)
1441								{
1442									delete cellDiffs[cellId];
1443								}
1444							}
1445
1446							if (Object.keys(cellDiffs).length == 0)
1447							{
1448								delete diff.cells[key];
1449							}
1450						}
1451					});
1452
1453					anonymizeCellDiffs(EditorUi.DIFF_INSERT);
1454					anonymizeCellDiffs(EditorUi.DIFF_UPDATE);
1455
1456					if (Object.keys(diff.cells).length == 0)
1457					{
1458						delete diff.cells;
1459					}
1460				}
1461
1462				if (Object.keys(diff).length == 0)
1463				{
1464					delete patch[EditorUi.DIFF_UPDATE][pageId];
1465				}
1466			}
1467
1468			if (Object.keys(patch[EditorUi.DIFF_UPDATE]).length == 0)
1469			{
1470				delete patch[EditorUi.DIFF_UPDATE];
1471			}
1472		}
1473
1474		return patch;
1475	};
1476
1477	/**
1478	 * Removes any values, styles and geometries from the given XML node.
1479	 */
1480	EditorUi.prototype.anonymizeAttributes = function(node, zeros)
1481	{
1482		if (node.attributes != null)
1483		{
1484			for (var i = 0; i < node.attributes.length; i++)
1485			{
1486				if (node.attributes[i].name != 'as')
1487				{
1488					node.setAttribute(node.attributes[i].name,
1489						this.anonymizeString(node.attributes[i].value, zeros));
1490				}
1491			}
1492		}
1493
1494		if (node.childNodes != null)
1495		{
1496			for (var i = 0; i < node.childNodes.length; i++)
1497			{
1498				this.anonymizeAttributes(node.childNodes[i], zeros);
1499			}
1500		}
1501	};
1502
1503	/**
1504	 * Removes any values, styles and geometries from the given XML node.
1505	 */
1506	EditorUi.prototype.anonymizeNode = function(node, zeros)
1507	{
1508		var nodes = node.getElementsByTagName('mxCell');
1509
1510		for (var i = 0; i < nodes.length; i++)
1511		{
1512			if (nodes[i].getAttribute('value') != null)
1513			{
1514				nodes[i].setAttribute('value', '[' + nodes[i].getAttribute('value').length + ']');
1515			}
1516
1517			if (nodes[i].getAttribute('xmlValue') != null)
1518			{
1519				nodes[i].setAttribute('xmlValue', '[' + nodes[i].getAttribute('xmlValue').length + ']');
1520			}
1521
1522			if (nodes[i].getAttribute('style') != null)
1523			{
1524				nodes[i].setAttribute('style', '[' + nodes[i].getAttribute('style').length + ']');
1525			}
1526
1527			if (nodes[i].parentNode != null && nodes[i].parentNode.nodeName != 'root' &&
1528				nodes[i].parentNode.parentNode != null)
1529			{
1530				nodes[i].setAttribute('id', nodes[i].parentNode.getAttribute('id'));
1531				nodes[i].parentNode.parentNode.replaceChild(nodes[i], nodes[i].parentNode);
1532			}
1533		}
1534
1535		return node;
1536	};
1537
1538	/**
1539	 * Translates this point by the given vector.
1540	 *
1541	 * @param {number} dx X-coordinate of the translation.
1542	 * @param {number} dy Y-coordinate of the translation.
1543	 */
1544	EditorUi.prototype.synchronizeCurrentFile = function(forceReload)
1545	{
1546		var currentFile = this.getCurrentFile();
1547
1548		if (currentFile != null)
1549		{
1550			if (currentFile.savingFile)
1551			{
1552				this.handleError({message: mxResources.get('busy')});
1553			}
1554			else if (!forceReload && currentFile.invalidChecksum)
1555			{
1556				currentFile.handleFileError(null, true);
1557			}
1558			else if (this.spinner.spin(document.body, mxResources.get('updatingDocument')))
1559			{
1560				currentFile.clearAutosave();
1561				this.editor.setStatus('');
1562
1563				if (forceReload)
1564				{
1565					currentFile.reloadFile(mxUtils.bind(this, function()
1566					{
1567						currentFile.handleFileSuccess(DrawioFile.SYNC == 'manual');
1568					}), mxUtils.bind(this, function(err)
1569					{
1570						currentFile.handleFileError(err, true);
1571					}));
1572				}
1573				else
1574				{
1575					currentFile.synchronizeFile(mxUtils.bind(this, function()
1576					{
1577						currentFile.handleFileSuccess(DrawioFile.SYNC == 'manual');
1578					}), mxUtils.bind(this, function(err)
1579					{
1580						currentFile.handleFileError(err, true);
1581					}));
1582				}
1583			}
1584		}
1585	};
1586
1587	/**
1588	 * Translates this point by the given vector.
1589	 *
1590	 * @param {number} dx X-coordinate of the translation.
1591	 * @param {number} dy Y-coordinate of the translation.
1592	 */
1593	EditorUi.prototype.getFileData = function(forceXml, forceSvg, forceHtml, embeddedCallback,
1594		ignoreSelection, currentPage, node, compact, file, uncompressed, resolveReferences)
1595	{
1596		ignoreSelection = (ignoreSelection != null) ? ignoreSelection : true;
1597		currentPage = (currentPage != null) ? currentPage : false;
1598		var graph = this.editor.graph;
1599
1600		// Forces compression of embedded XML
1601		if (forceSvg || (!forceXml && file != null && /(\.svg)$/i.test(file.getTitle())))
1602		{
1603			var darkTheme = graph.themes != null && graph.defaultThemeName == 'darkTheme';
1604			uncompressed = false;
1605
1606			// Exports SVG for first page while other page is visible by creating a graph
1607			// LATER: Add caching for the graph or SVG while not on first page
1608			// Dark mode requires a refresh that would destroy all handlers
1609			// LATER: Use dark theme here to bypass refresh
1610			if (darkTheme || (this.pages != null && this.currentPage != this.pages[0]))
1611			{
1612				var graphGetGlobalVariable = graph.getGlobalVariable;
1613				graph = this.createTemporaryGraph(darkTheme ?
1614						graph.getDefaultStylesheet() : graph.getStylesheet());
1615				graph.setBackgroundImage = this.editor.graph.setBackgroundImage;
1616				var page = this.pages[0];
1617
1618				if (this.currentPage == page)
1619				{
1620					graph.setBackgroundImage(this.editor.graph.backgroundImage);
1621				}
1622				else if (page.viewState != null && page.viewState != null)
1623				{
1624					graph.setBackgroundImage(page.viewState.backgroundImage);
1625				}
1626
1627				graph.getGlobalVariable = function(name)
1628				{
1629					if (name == 'page')
1630					{
1631						return page.getName();
1632					}
1633					else if (name == 'pagenumber')
1634					{
1635						return 1;
1636					}
1637
1638					return graphGetGlobalVariable.apply(this, arguments);
1639				};
1640
1641				document.body.appendChild(graph.container);
1642				graph.model.setRoot(page.root);
1643			}
1644		}
1645
1646		node = (node != null) ? node : this.getXmlFileData(ignoreSelection,
1647			currentPage, uncompressed, resolveReferences);
1648		file = (file != null) ? file : this.getCurrentFile();
1649
1650		var result = this.createFileData(node, graph, file, window.location.href,
1651			forceXml, forceSvg, forceHtml, embeddedCallback, ignoreSelection, compact,
1652			uncompressed);
1653
1654		// Removes temporary graph from DOM
1655		if (graph != this.editor.graph)
1656		{
1657			graph.container.parentNode.removeChild(graph.container);
1658		}
1659
1660		return result;
1661	};
1662
1663	/**
1664	 *
1665	 */
1666	EditorUi.prototype.getHtml = function(node, graph, title, editLink, redirect, ignoreSelection)
1667	{
1668		ignoreSelection = (ignoreSelection != null) ? ignoreSelection : true;
1669		var bg = null;
1670		var js = EditorUi.drawHost + '/js/embed-static.min.js';
1671
1672		// LATER: Merge common code with EmbedDialog
1673		if (graph != null)
1674		{
1675			var bounds = (ignoreSelection) ? graph.getGraphBounds() : graph.getBoundingBox(graph.getSelectionCells());
1676			var scale = graph.view.scale;
1677			var x0 = Math.floor(bounds.x / scale - graph.view.translate.x);
1678			var y0 = Math.floor(bounds.y / scale - graph.view.translate.y);
1679			bg = graph.background;
1680
1681			// Embed script only used if no redirect
1682			if (redirect == null)
1683			{
1684				var s = this.getBasenames().join(';');
1685
1686				if (s.length > 0)
1687				{
1688					js = EditorUi.drawHost + '/embed.js?s=' + s;
1689				}
1690			}
1691
1692			// Adds embed attributes
1693			node.setAttribute('x0', x0);
1694			node.setAttribute('y0', y0);
1695		}
1696
1697		if (node != null)
1698		{
1699			node.setAttribute('pan', '1');
1700			node.setAttribute('zoom', '1');
1701			node.setAttribute('resize', '0');
1702			node.setAttribute('fit', '0');
1703			node.setAttribute('border', '20');
1704
1705			// Hidden attributes
1706			node.setAttribute('links', '1');
1707
1708			if (editLink != null)
1709			{
1710				node.setAttribute('edit', editLink);
1711			}
1712		}
1713
1714		// Makes XHTML compatible
1715		if (redirect != null)
1716		{
1717			redirect = redirect.replace(/&/g, '&amp;');
1718		}
1719
1720		// Removes control chars in input for correct roundtrip check
1721		var text = (node != null) ? Graph.zapGremlins(mxUtils.getXml(node)) : '';
1722
1723		// Double compression for mxfile not fixed since it may cause imcompatibilites with
1724		// embed clients that rely on this format. HTML files and export use getHtml2.
1725		var data = Graph.compress(text);
1726
1727		// Fallback to URI encoded XML for invalid compression
1728		if (Graph.decompress(data) != text)
1729		{
1730			data = encodeURIComponent(text);
1731		}
1732
1733		var style = 'position:relative;overflow:auto;width:100%;';
1734
1735		return ((redirect == null) ? '<!--[if IE]><meta http-equiv="X-UA-Compatible" content="IE=5,IE=9" ><![endif]-->\n' : '') +
1736			'<!DOCTYPE html>\n<html' + ((redirect != null) ? ' xmlns="http://www.w3.org/1999/xhtml">' : '>') +
1737			'\n<head>\n' + ((redirect == null) ? ((title != null) ? '<title>' + mxUtils.htmlEntities(title) +
1738				'</title>\n' : '') : '<title>diagrams.net</title>\n') +
1739			((redirect != null) ? '<meta http-equiv="refresh" content="0;URL=\'' + redirect + '\'"/>\n' : '') +
1740			'</head>\n<body' +
1741			(((redirect == null && bg != null && bg != mxConstants.NONE) ? ' style="background-color:' + bg + ';">' : '>')) +
1742			'\n<div class="mxgraph" style="' + style + '">\n' +
1743			'<div style="width:1px;height:1px;overflow:hidden;">' + data + '</div>\n</div>\n' +
1744			((redirect == null) ? '<script type="text/javascript" src="' + js + '"></script>' :
1745			'<a style="position:absolute;top:50%;left:50%;margin-top:-128px;margin-left:-64px;" ' +
1746			'href="' + redirect + '" target="_blank"><img border="0" ' +
1747			'src="' + EditorUi.drawHost + '/images/drawlogo128.png"/></a>') +
1748			'\n</body>\n</html>\n';
1749	};
1750
1751	/**
1752	 * Same as above but using the new embed code.
1753	 */
1754	EditorUi.prototype.getHtml2 = function(xml, graph, title, editLink, redirect)
1755	{
1756		var js = window.DRAWIO_VIEWER_URL || EditorUi.drawHost + '/js/viewer-static.min.js';
1757
1758		// Makes XHTML compatible
1759		if (redirect != null)
1760		{
1761			redirect = redirect.replace(/&/g, '&amp;');
1762		}
1763
1764		var data = {highlight: '#0000ff', nav: this.editor.graph.foldingEnabled, resize: true,
1765			xml: Graph.zapGremlins(xml), toolbar: 'pages zoom layers lightbox'};
1766
1767		if (this.pages != null && this.currentPage != null)
1768		{
1769			data.page = mxUtils.indexOf(this.pages, this.currentPage);
1770		}
1771
1772		var style = 'max-width:100%;border:1px solid transparent;';
1773
1774		return ((redirect == null) ? '<!--[if IE]><meta http-equiv="X-UA-Compatible" content="IE=5,IE=9" ><![endif]-->\n' : '') +
1775			'<!DOCTYPE html>\n<html' + ((redirect != null) ? ' xmlns="http://www.w3.org/1999/xhtml">' : '>') +
1776			'\n<head>\n' + ((redirect == null) ? ((title != null) ? '<title>' + mxUtils.htmlEntities(title) +
1777				'</title>\n' : '') : '<title>diagrams.net</title>\n') +
1778			((redirect != null) ? '<meta http-equiv="refresh" content="0;URL=\'' + redirect + '\'"/>\n' : '') +
1779			'<meta charset="utf-8"/>\n</head>\n<body>' +
1780			'\n<div class="mxgraph" style="' + style + '" data-mxgraph="' + mxUtils.htmlEntities(JSON.stringify(data)) + '"></div>\n' +
1781			((redirect == null) ? '<script type="text/javascript" src="' + js + '"></script>' :
1782			'<a style="position:absolute;top:50%;left:50%;margin-top:-128px;margin-left:-64px;" ' +
1783			'href="' + redirect + '" target="_blank"><img border="0" ' +
1784			'src="' + EditorUi.drawHost + '/images/drawlogo128.png"/></a>') +
1785			'\n</body>\n</html>\n';
1786	};
1787
1788	/**
1789	 *
1790	 */
1791	EditorUi.prototype.setFileData = function(data)
1792	{
1793		data = this.validateFileData(data);
1794		this.currentPage = null;
1795		this.fileNode = null;
1796		this.pages = null;
1797
1798		var node = (data != null && data.length > 0) ? mxUtils.parseXml(data).documentElement : null;
1799
1800		// Checks for parser errors
1801		var cause = Editor.extractParserError(node, mxResources.get('invalidOrMissingFile'));
1802
1803		if (cause)
1804		{
1805			throw new Error(mxResources.get('notADiagramFile') + ' (' + cause + ')');
1806		}
1807		else
1808		{
1809			// Some nodes must be extracted here to find the mxfile node
1810			// LATER: Remove duplicate call to extractGraphModel in overridden setGraphXml
1811			var tmp = (node != null) ? this.editor.extractGraphModel(node, true) : null;
1812
1813			if (tmp != null)
1814			{
1815				node = tmp;
1816			}
1817
1818			if (node != null && node.nodeName == 'mxfile')
1819			{
1820				var nodes = node.getElementsByTagName('diagram');
1821
1822				if (urlParams['pages'] != '0' || nodes.length > 1 ||
1823					(nodes.length == 1 && nodes[0].hasAttribute('name')))
1824				{
1825					var selectedPage = null;
1826					this.fileNode = node;
1827					this.pages = [];
1828
1829					// Wraps page nodes
1830					for (var i = 0; i < nodes.length; i++)
1831					{
1832						// Adds page ID based on page order to match
1833						// remote IDs given if IDs are missing here
1834						if (nodes[i].getAttribute('id') == null)
1835						{
1836							nodes[i].setAttribute('id', i);
1837						}
1838
1839						var page = new DiagramPage(nodes[i]);
1840
1841						// Checks for invalid page names
1842						if (page.getName() == null)
1843						{
1844							page.setName(mxResources.get('pageWithNumber', [i + 1]));
1845						}
1846
1847						this.pages.push(page);
1848
1849						if (urlParams['page-id'] != null && page.getId() == urlParams['page-id'])
1850						{
1851							selectedPage = page;
1852						}
1853					}
1854
1855					this.currentPage = (selectedPage != null) ? selectedPage :
1856						this.pages[Math.max(0, Math.min(this.pages.length - 1, urlParams['page'] || 0))];
1857					node = this.currentPage.node;
1858				}
1859			}
1860
1861			// Creates tabbed file structure if enforced by URL
1862			if (urlParams['pages'] != '0' && this.fileNode == null && node != null)
1863			{
1864				this.fileNode = node.ownerDocument.createElement('mxfile');
1865				this.currentPage = new DiagramPage(node.ownerDocument.createElement('diagram'));
1866				this.currentPage.setName(mxResources.get('pageWithNumber', [1]));
1867		 	 	this.pages = [this.currentPage];
1868			}
1869
1870			// Avoids scroll offset when switching page
1871			this.editor.setGraphXml(node);
1872
1873			// Avoids duplicate parsing of the XML stored in the node
1874			if (this.currentPage != null)
1875			{
1876				this.currentPage.root = this.editor.graph.model.root;
1877			}
1878
1879			if (urlParams['layer-ids'] != null)
1880			{
1881				try
1882				{
1883					var layerIds = urlParams['layer-ids'].split(' ');
1884					var layerIdsMap = {};
1885
1886					for (var i = 0; i < layerIds.length; i++)
1887					{
1888						layerIdsMap[layerIds[i]] = true;
1889					}
1890
1891					var model = this.editor.graph.getModel();
1892					var children = model.getChildren(model.root);
1893
1894					// handle layers visibility
1895					for (var i = 0; i < children.length; i++)
1896					{
1897						var child = children[i];
1898						model.setVisible(child, layerIdsMap[child.id] || false);
1899					}
1900				}
1901				catch(e){} //ignore
1902			}
1903		}
1904	};
1905
1906	/**
1907	 * Translates this point by the given vector.
1908	 *
1909	 * @param {number} dx X-coordinate of the translation.
1910	 * @param {number} dy Y-coordinate of the translation.
1911	 */
1912	EditorUi.prototype.getBaseFilename = function(ignorePageName)
1913	{
1914		var file = this.getCurrentFile();
1915		var basename = (file != null && file.getTitle() != null) ? file.getTitle() : this.defaultFilename;
1916
1917		if (/(\.xml)$/i.test(basename) || /(\.html)$/i.test(basename) ||
1918			/(\.svg)$/i.test(basename) || /(\.png)$/i.test(basename))
1919		{
1920			basename = basename.substring(0, basename.lastIndexOf('.'));
1921		}
1922
1923		if (/(\.drawio)$/i.test(basename))
1924		{
1925			basename = basename.substring(0, basename.lastIndexOf('.'));
1926		}
1927
1928		if (!ignorePageName && this.pages != null && this.pages.length > 1 &&
1929			this.currentPage != null && this.currentPage.node.getAttribute('name') != null &&
1930			this.currentPage.getName().length > 0)
1931		{
1932			basename = basename + '-' + this.currentPage.getName();
1933		}
1934
1935		return basename;
1936	};
1937
1938	/**
1939	 * Translates this point by the given vector.
1940	 *
1941	 * @param {number} dx X-coordinate of the translation.
1942	 * @param {number} dy Y-coordinate of the translation.
1943	 */
1944	EditorUi.prototype.downloadFile = function(format, uncompressed, addShadow, ignoreSelection, currentPage,
1945		pageVisible, transparent, scale, border, grid, includeXml, pageRange)
1946	{
1947		try
1948		{
1949			ignoreSelection = (ignoreSelection != null) ? ignoreSelection : this.editor.graph.isSelectionEmpty();
1950			var basename = this.getBaseFilename(!currentPage);
1951			var filename = basename + ((format == 'xml' || (format == 'pdf' &&
1952				includeXml)) ? '.drawio' : '') + '.' + format;
1953
1954			if (format == 'xml')
1955			{
1956		    	var data = Graph.xmlDeclaration +'\n' +
1957		    		this.getFileData(true, null, null, null, ignoreSelection, currentPage,
1958		    			null, null, null, uncompressed);
1959
1960		    	this.saveData(filename, format, data, 'text/xml');
1961			}
1962		    else if (format == 'html')
1963		    {
1964		    	var data = this.getHtml2(this.getFileData(true), this.editor.graph, basename);
1965		    	this.saveData(filename, format, data, 'text/html');
1966		    }
1967		    else if ((format == 'svg' || format == 'xmlsvg') && this.spinner.spin(document.body, mxResources.get('export')))
1968		    {
1969		    	var svg = null;
1970
1971		    	var saveSvg = mxUtils.bind(this, function(data)
1972		    	{
1973		    		if (data.length <= MAX_REQUEST_SIZE)
1974		    		{
1975		    	    	this.saveData(filename, 'svg', data, 'image/svg+xml');
1976		    		}
1977		    		else
1978		    		{
1979		    			this.handleError({message: mxResources.get('drawingTooLarge')}, mxResources.get('error'), mxUtils.bind(this, function()
1980		    			{
1981		    				mxUtils.popup(svg);
1982		    			}));
1983		    		}
1984		    	});
1985
1986		    	if (format == 'svg')
1987		    	{
1988		        	var bg = this.editor.graph.background;
1989
1990		        	if (transparent || bg == mxConstants.NONE)
1991		        	{
1992		        		bg = null;
1993		        	}
1994
1995		        	// Sets or disables alternate text for foreignObjects. Disabling is needed
1996		        	// because PhantomJS seems to ignore switch statements and paint all text.
1997		        	var svgRoot = this.editor.graph.getSvg(bg, null, null, null, null, ignoreSelection);
1998
1999					if (addShadow)
2000					{
2001						this.editor.graph.addSvgShadow(svgRoot);
2002					}
2003
2004					// Embeds the images in the SVG output (async)
2005					this.editor.convertImages(svgRoot, mxUtils.bind(this, mxUtils.bind(this, function(svgRoot2)
2006					{
2007						this.spinner.stop();
2008
2009						saveSvg(Graph.xmlDeclaration + '\n' + Graph.svgDoctype + '\n' + mxUtils.getXml(svgRoot2));
2010					})));
2011		    	}
2012		    	else
2013		    	{
2014		    		filename = basename + '.svg';
2015
2016		    		svg = this.getFileData(false, true, null, mxUtils.bind(this, function(svg)
2017		    		{
2018		    			this.spinner.stop();
2019		        		saveSvg(svg);
2020		    		}), ignoreSelection);
2021		    	}
2022		    }
2023			else
2024			{
2025				if (format == 'xmlpng')
2026				{
2027					filename = basename + '.png';
2028				}
2029				else if (format == 'jpeg')
2030				{
2031					filename = basename + '.jpg';
2032				}
2033
2034				this.saveRequest(filename, format, mxUtils.bind(this, function(newTitle, base64)
2035				{
2036					try
2037					{
2038						var prev = this.editor.graph.pageVisible;
2039
2040						if (pageVisible != null)
2041						{
2042							this.editor.graph.pageVisible = pageVisible;
2043						}
2044
2045						var req = this.createDownloadRequest(newTitle, format, ignoreSelection, base64,
2046							transparent, currentPage, scale, border, grid, includeXml, pageRange);
2047						this.editor.graph.pageVisible = prev;
2048
2049						return req;
2050					}
2051					catch (e)
2052					{
2053						this.handleError(e);
2054					}
2055				}));
2056			}
2057		}
2058		catch (e)
2059		{
2060			this.handleError(e);
2061		}
2062	};
2063
2064	/**
2065	 * Translates this point by the given vector.
2066	 *
2067	 * @param {number} dx X-coordinate of the translation.
2068	 * @param {number} dy Y-coordinate of the translation.
2069	 */
2070	EditorUi.prototype.createDownloadRequest = function(filename, format, ignoreSelection,
2071		base64, transparent, currentPage, scale, border, grid, includeXml, pageRange)
2072	{
2073		var graph = this.editor.graph;
2074		var bounds = graph.getGraphBounds();
2075
2076		// Exports only current page for images that does not contain file data, but for
2077		// the other formats with XML included or pdf with all pages, we need to send the complete data and use
2078		// the from/to URL parameters to specify the page to be exported.
2079		var data = this.getFileData(true, null, null, null, ignoreSelection,
2080			currentPage == false ? false : format != 'xmlpng', null, null,
2081			null, false, format == 'pdf');
2082		var range = '';
2083		var allPages = '';
2084
2085		if (bounds.width * bounds.height > MAX_AREA || data.length > MAX_REQUEST_SIZE)
2086		{
2087			throw {message: mxResources.get('drawingTooLarge')};
2088		}
2089
2090		var embed = (includeXml) ? '1' : '0';
2091
2092		if (format == 'pdf')
2093		{
2094			if (pageRange != null)
2095			{
2096				allPages = '&from=' + pageRange.from + '&to=' + pageRange.to;
2097			}
2098			else if (currentPage == false)
2099			{
2100				allPages = '&allPages=1';
2101			}
2102		}
2103
2104       	if (format == 'xmlpng')
2105       	{
2106       		embed = '1';
2107       		format = 'png';
2108
2109       		// Finds the current page number
2110       		if (this.pages != null && this.currentPage != null)
2111       		{
2112       			for (var i = 0; i < this.pages.length; i++)
2113       			{
2114       				if (this.pages[i] == this.currentPage)
2115       				{
2116       					range = '&from=' + i;
2117       					break;
2118       				}
2119       			}
2120       		}
2121       	}
2122
2123		var bg = graph.background;
2124
2125		if ((format == 'png' || format == 'pdf') && transparent)
2126		{
2127			bg = mxConstants.NONE;
2128		}
2129		else if (!transparent && (bg == null || bg == mxConstants.NONE))
2130		{
2131			bg = '#ffffff';
2132		}
2133
2134		var extras = {globalVars: graph.getExportVariables()};
2135
2136		if (grid)
2137		{
2138			extras.grid = {
2139				size: graph.gridSize,
2140				steps: graph.view.gridSteps,
2141				color: graph.view.gridColor
2142			};
2143		}
2144
2145		if (Graph.translateDiagram)
2146		{
2147			extras.diagramLanguage = Graph.diagramLanguage;
2148		}
2149
2150		return new mxXmlRequest(EXPORT_URL, 'format=' + format + range + allPages +
2151			'&bg=' + ((bg != null) ? bg : mxConstants.NONE) +
2152			'&base64=' + base64 + '&embedXml=' + embed + '&xml=' +
2153			encodeURIComponent(data) + ((filename != null) ?
2154			'&filename=' + encodeURIComponent(filename) : '') +
2155			'&extras=' + encodeURIComponent(JSON.stringify(extras)) +
2156			(scale != null? '&scale=' + scale : '') +
2157			(border != null? '&border=' + border : ''));
2158	};
2159
2160	/**
2161	 * Translates this point by the given vector.
2162	 *
2163	 * @param {number} dx X-coordinate of the translation.
2164	 * @param {number} dy Y-coordinate of the translation.
2165	 */
2166	EditorUi.prototype.setMode = function(mode, remember)
2167	{
2168		this.mode = mode;
2169	};
2170
2171	/**
2172	 * Loads the given file descriptor. The descriptor may define the following properties:
2173	 *
2174	 * - url: The url to load the data from (proxy is used if CORS is not enabled)
2175	 * - data: The data to be inserted. If both, data and url are defined, then the data
2176	 * is preprendended to the data returned from the given URL.
2177	 * - format: Currently, only 'csv' is supported as an optional value. Default is XML.
2178	 * - update: Optional URL to fetch updates from (POST request with the page XML).
2179	 * - interval: Optional interval for fetching updates. Default is 60000 (60 seconds).
2180	 */
2181	EditorUi.prototype.loadDescriptor = function(desc, success, error)
2182	{
2183		var hash = window.location.hash;
2184
2185		var loadData = mxUtils.bind(this, function(data)
2186		{
2187			var realData = (desc.data != null) ? desc.data : '';
2188
2189			if (data != null && data.length > 0)
2190			{
2191				if (realData.length > 0)
2192				{
2193					realData += '\n';
2194				}
2195
2196				realData += data;
2197			}
2198
2199			var xml = (desc.format != 'csv' && realData.length > 0) ? realData : this.emptyDiagramXml;
2200			var tempFile = new LocalFile(this, xml, (urlParams['title'] != null) ?
2201					decodeURIComponent(urlParams['title']) : this.defaultFilename, true);
2202			tempFile.getHash = function()
2203			{
2204				return hash;
2205			};
2206			this.fileLoaded(tempFile);
2207
2208			if (desc.format == 'csv')
2209			{
2210				this.importCsv(realData, mxUtils.bind(this, function(cells)
2211				{
2212					this.editor.undoManager.clear();
2213					this.editor.setModified(false);
2214					this.editor.setStatus('');
2215				}));
2216			}
2217
2218			// Installs updates
2219			if (desc.update != null)
2220			{
2221				var interval = (desc.interval != null) ? parseInt(desc.interval) : 60000;
2222				var currentThread = null;
2223
2224				var doUpdate = mxUtils.bind(this, function()
2225				{
2226					var page = this.currentPage;
2227
2228					mxUtils.post(desc.update, 'xml=' + encodeURIComponent(
2229						mxUtils.getXml(this.editor.getGraphXml())),
2230						mxUtils.bind(this, function(req)
2231					{
2232						if (page === this.currentPage)
2233						{
2234							if (req.getStatus() >= 200 && req.getStatus() <= 300)
2235							{
2236								var doc = this.updateDiagram(req.getText());
2237								schedule();
2238							}
2239							else
2240							{
2241								this.handleError({message: mxResources.get('error') + ' ' + req.getStatus()});
2242							}
2243						}
2244					}), mxUtils.bind(this, function(err)
2245					{
2246						this.handleError(err);
2247					}));
2248				});
2249
2250				var schedule = mxUtils.bind(this, function()
2251				{
2252					window.clearTimeout(currentThread);
2253					currentThread = window.setTimeout(doUpdate, interval);
2254				});
2255
2256				this.editor.addListener('pageSelected', mxUtils.bind(this, function()
2257				{
2258					schedule();
2259					doUpdate();
2260				}));
2261
2262				schedule();
2263				doUpdate();
2264			}
2265
2266    		if (success != null)
2267    		{
2268    			success();
2269    		}
2270		});
2271
2272		if (desc.url != null && desc.url.length > 0)
2273		{
2274			// Cannot use proxy here as it will block unknown text content
2275            // LATER: Remove cache-control header
2276            this.editor.loadUrl(desc.url, mxUtils.bind(this, function(data)
2277            {
2278            	loadData(data);
2279            }), mxUtils.bind(this, function(err)
2280            {
2281            	if (error != null)
2282            	{
2283            		error(err)
2284            	}
2285            }));
2286		}
2287		else
2288		{
2289			loadData('');
2290		}
2291	};
2292
2293	/**
2294	 * Translates this point by the given vector.
2295	 *
2296	 * @param {number} dx X-coordinate of the translation.
2297	 * @param {number} dy Y-coordinate of the translation.
2298	 */
2299	EditorUi.prototype.updateDiagram = function(xml)
2300	{
2301		var doc = null;
2302		var ui = this;
2303
2304		function createOverlay(desc)
2305		{
2306			var overlay = new mxCellOverlay(desc.image || graph.warningImage,
2307				desc.tooltip, desc.align, desc.valign, desc.offset);
2308
2309			// Installs a handler for clicks on the overlay
2310			overlay.addListener(mxEvent.CLICK, function(sender, evt)
2311			{
2312				ui.alert(desc.tooltip);
2313			});
2314
2315			return overlay;
2316		};
2317
2318		if (xml != null && xml.length > 0)
2319		{
2320			doc = mxUtils.parseXml(xml);
2321			var node = (doc != null) ? doc.documentElement : null;
2322
2323			if (node != null && node.nodeName == 'updates')
2324			{
2325				var graph = this.editor.graph;
2326				var model = graph.getModel();
2327				model.beginUpdate();
2328				var fit = null;
2329
2330				try
2331				{
2332					node = node.firstChild;
2333
2334					while (node != null)
2335					{
2336						if (node.nodeName == 'update')
2337						{
2338							// Resolves the cell ID
2339							var cell = model.getCell(node.getAttribute('id'));
2340
2341							if (cell != null)
2342							{
2343								// Changes the value
2344								try
2345								{
2346									var value = node.getAttribute('value');
2347
2348									if (value != null)
2349									{
2350										var valueNode = mxUtils.parseXml(value).documentElement;
2351
2352										if (valueNode != null)
2353										{
2354											if (valueNode.getAttribute('replace-value') == '1')
2355											{
2356												model.setValue(cell, valueNode);
2357											}
2358											else
2359											{
2360												var attrs = valueNode.attributes;
2361
2362												for (var j = 0; j < attrs.length; j++)
2363												{
2364													graph.setAttributeForCell(cell, attrs[j].nodeName,
2365														(attrs[j].nodeValue.length > 0) ? attrs[j].nodeValue : null);
2366												}
2367											}
2368										}
2369									}
2370								}
2371								catch (e)
2372								{
2373									if (window.console != null)
2374									{
2375										console.log('Error in value for ' + cell.id + ': ' + e);
2376									}
2377								}
2378
2379								// Changes the style
2380								try
2381								{
2382									var style = node.getAttribute('style');
2383
2384									if (style != null)
2385									{
2386										graph.model.setStyle(cell, style);
2387									}
2388								}
2389								catch (e)
2390								{
2391									if (window.console != null)
2392									{
2393										console.log('Error in style for ' + cell.id + ': ' + e);
2394									}
2395								}
2396
2397								// Adds or removes an overlay icon
2398								try
2399								{
2400									var icon = node.getAttribute('icon');
2401
2402									if (icon != null)
2403									{
2404										var desc = (icon.length > 0) ? JSON.parse(icon) : null;
2405
2406										if (desc == null || !desc.append)
2407										{
2408											graph.removeCellOverlays(cell);
2409										}
2410
2411										if (desc != null)
2412										{
2413											graph.addCellOverlay(cell, createOverlay(desc));
2414										}
2415									}
2416								}
2417								catch (e)
2418								{
2419									if (window.console != null)
2420									{
2421										console.log('Error in icon for ' + cell.id + ': ' + e);
2422									}
2423								}
2424
2425								// Replaces the geometry
2426								try
2427								{
2428									var geo = node.getAttribute('geometry');
2429
2430									if (geo != null)
2431									{
2432										geo = JSON.parse(geo);
2433										var curr = graph.getCellGeometry(cell);
2434
2435										if (curr != null)
2436										{
2437											curr = curr.clone();
2438
2439											// Partially overwrites geometry
2440											for (key in geo)
2441											{
2442												var val = parseFloat(geo[key]);
2443
2444												if (key == 'dx')
2445												{
2446													curr.x += val;
2447												}
2448												else if (key == 'dy')
2449												{
2450													curr.y += val;
2451												}
2452												else if (key == 'dw')
2453												{
2454													curr.width += val;
2455												}
2456												else if (key == 'dh')
2457												{
2458													curr.height += val;
2459												}
2460												else
2461												{
2462													curr[key] = parseFloat(geo[key]);
2463												}
2464											}
2465
2466											graph.model.setGeometry(cell, curr);
2467										}
2468									}
2469								}
2470								catch (e)
2471								{
2472									if (window.console != null)
2473									{
2474										console.log('Error in icon for ' + cell.id + ': ' + e);
2475									}
2476								}
2477							} // if cell != null
2478						} // if node.nodeName == 'update
2479						else if (node.nodeName == 'model')
2480						{
2481							// Finds first child element
2482							var dataNode = node.firstChild;
2483
2484							while (dataNode != null && dataNode.nodeType != mxConstants.NODETYPE_ELEMENT)
2485							{
2486								dataNode = dataNode.nextSibling;
2487							}
2488
2489							if (dataNode != null)
2490							{
2491								var dec = new mxCodec(node.firstChild);
2492								dec.decode(dataNode, model);
2493							}
2494						}
2495						else if (node.nodeName == 'view')
2496						{
2497							if (node.hasAttribute('scale'))
2498							{
2499								graph.view.scale = parseFloat(node.getAttribute('scale'));
2500							}
2501
2502							if (node.hasAttribute('dx') || node.hasAttribute('dy'))
2503							{
2504								graph.view.translate = new mxPoint(parseFloat(node.getAttribute('dx') || 0),
2505									parseFloat(node.getAttribute('dy') || 0));
2506							}
2507						}
2508						else if (node.nodeName == 'fit')
2509						{
2510							if (node.hasAttribute('max-scale'))
2511							{
2512								fit = parseFloat(node.getAttribute('max-scale'));
2513							}
2514							else
2515							{
2516								fit = 1;
2517							}
2518						}
2519
2520						node = node.nextSibling;
2521					} // end of while
2522				}
2523				finally
2524				{
2525					model.endUpdate();
2526				}
2527
2528				if (fit != null && this.chromelessResize)
2529				{
2530					this.chromelessResize(true, fit);
2531				}
2532			}
2533		}
2534
2535		return doc;
2536	};
2537
2538	/**
2539	 * Constructs a filename for a copy of the given file.
2540	 */
2541	EditorUi.prototype.getCopyFilename = function(file, timestamp)
2542	{
2543		var title = (file != null && file.getTitle() != null) ?
2544			file.getTitle() : this.defaultFilename;
2545
2546		// Handles extension
2547		var extension = '';
2548		var dot = title.lastIndexOf('.');
2549
2550		if (dot >= 0)
2551		{
2552			extension = title.substring(dot);
2553			title = title.substring(0, dot);
2554		}
2555
2556		if (timestamp)
2557		{
2558			function getFormattedTime()
2559			{
2560			    var today = new Date();
2561			    var y = today.getFullYear();
2562			    // JavaScript months are 0-based.
2563			    var m = today.getMonth() + 1;
2564			    var d = today.getDate();
2565			    var h = today.getHours();
2566			    var mi = today.getMinutes();
2567			    var s = today.getSeconds();
2568
2569			    return y + "-" + m + "-" + d + "-" + h + "-" + mi + "-" + s;
2570			}
2571
2572			var ts = new Date();
2573			title += ' ' + getFormattedTime();
2574		}
2575
2576		title = mxResources.get('copyOf', [title]) + extension;
2577
2578		return title;
2579	};
2580
2581	/**
2582	 * Translates this point by the given vector.
2583	 *
2584	 * @param {number} dx X-coordinate of the translation.
2585	 * @param {number} dy Y-coordinate of the translation.
2586	 */
2587	EditorUi.prototype.fileLoaded = function(file, noDialogs)
2588	{
2589		var oldFile = this.getCurrentFile();
2590		this.fileLoadedError = null;
2591		this.fileEditable = null;
2592		this.setCurrentFile(null);
2593		var result = false;
2594		this.hideDialog();
2595
2596		if (oldFile != null)
2597		{
2598			EditorUi.debug('File.closed', [oldFile]);
2599			oldFile.removeListener(this.descriptorChangedListener);
2600			oldFile.close();
2601		}
2602
2603		this.editor.graph.model.clear();
2604		this.editor.undoManager.clear();
2605
2606		var noFile = mxUtils.bind(this, function()
2607		{
2608			this.setGraphEnabled(false);
2609			this.setCurrentFile(null);
2610
2611			// Keeps initial title if no file existed before
2612			if (oldFile != null)
2613			{
2614				this.updateDocumentTitle();
2615			}
2616
2617			// File might have been loaded halfway
2618			this.editor.graph.model.clear();
2619			this.editor.undoManager.clear();
2620			this.setBackgroundImage(null);
2621
2622			// Avoids empty hash with no value
2623			if (!noDialogs && window.location.hash != null && window.location.hash.length > 0)
2624			{
2625				window.location.hash = '';
2626			}
2627
2628			if (this.fname != null)
2629			{
2630				this.fnameWrapper.style.display = 'none';
2631				this.fname.innerHTML = '';
2632				this.fname.setAttribute('title', mxResources.get('rename'));
2633			}
2634
2635			this.editor.setStatus('');
2636			this.updateUi();
2637
2638			if (!noDialogs)
2639			{
2640				this.showSplash();
2641			}
2642		});
2643
2644		if (file != null)
2645		{
2646			try
2647			{
2648				// Workaround for delayed scroll repaint with min UI in Safari
2649				if (mxClient.IS_SF && uiTheme == 'min')
2650				{
2651					this.diagramContainer.style.visibility = '';
2652				}
2653
2654				// Order is significant, current file needed for correct
2655				// file format for initial save after starting realtime
2656				this.openingFile = true;
2657				this.setCurrentFile(file);
2658				file.addListener('descriptorChanged', this.descriptorChangedListener);
2659				file.addListener('contentChanged', this.descriptorChangedListener);
2660				file.open();
2661				delete this.openingFile;
2662
2663				// DescriptorChanged updates the enabled state of the graph
2664				this.setGraphEnabled(true);
2665				this.setMode(file.getMode());
2666				this.editor.graph.model.prefix = Editor.guid() + '-';
2667				this.editor.undoManager.clear();
2668				this.descriptorChanged();
2669				this.updateUi();
2670
2671				// Realtime files have a valid status message
2672				if (!file.isEditable())
2673				{
2674					this.editor.setStatus('<span class="geStatusAlert">' +
2675						mxUtils.htmlEntities(mxResources.get('readOnly')) + '</span>');
2676				}
2677				// Handles modified state after error of loading new file
2678				else if (file.isModified())
2679				{
2680					file.addUnsavedStatus();
2681
2682					// Restores unsaved data
2683					if (file.backupPatch != null)
2684					{
2685						file.patch([file.backupPatch]);
2686					}
2687				}
2688				else
2689				{
2690					this.editor.setStatus('');
2691				}
2692
2693				if (!this.editor.isChromelessView() || this.editor.editable)
2694				{
2695					this.editor.graph.selectUnlockedLayer();
2696					this.showLayersDialog();
2697					this.restoreLibraries();
2698
2699					// Workaround for no initial focus in FF
2700					if (window.self !== window.top)
2701					{
2702						window.focus();
2703					}
2704				}
2705				else if (this.editor.graph.isLightboxView())
2706				{
2707					this.lightboxFit();
2708				}
2709
2710				if (this.chromelessResize)
2711				{
2712					this.chromelessResize();
2713				}
2714
2715				this.editor.fireEvent(new mxEventObject('fileLoaded'));
2716				result = true;
2717
2718				if (!this.isOffline() && file.getMode() != null)
2719				{
2720					EditorUi.logEvent({category: file.getMode().toUpperCase() + '-OPEN-FILE-' + file.getHash(),
2721						action: 'size_' + file.getSize(),
2722						label: 'autosave_' + ((this.editor.autosave) ? 'on' : 'off')});
2723				}
2724
2725				EditorUi.debug('File.opened', [file]);
2726
2727				//Notify users that editing is disabled within mobile apps (mainly for MS Teams)
2728				if (urlParams['viewerOnlyMsg'] == '1')
2729				{
2730					this.showAlert(mxResources.get('viewerOnlyMsg'));
2731				}
2732
2733				if (this.editor.editable && this.mode == file.getMode() &&
2734					file.getMode() != App.MODE_DEVICE && file.getMode() != null)
2735				{
2736					try
2737					{
2738						this.addRecent({id: file.getHash(), title: file.getTitle(), mode: file.getMode()});
2739					}
2740					catch (e)
2741					{
2742						// ignore
2743					}
2744				}
2745
2746				try
2747				{
2748					mxSettings.setOpenCounter(mxSettings.getOpenCounter() + 1);
2749					mxSettings.save();
2750				}
2751				catch (e)
2752				{
2753					// ignore
2754				}
2755			}
2756			catch (e)
2757			{
2758				this.fileLoadedError = e;
2759
2760				if (EditorUi.enableLogging && !this.isOffline())
2761				{
2762		        	try
2763		        	{
2764		        		EditorUi.logEvent({category: 'ERROR-LOAD-FILE-' +
2765		        			((file != null) ? file.getHash() : 'none'),
2766		        			action: 'message_' + e.message,
2767		        			label: 'stack_' + e.stack});
2768		        	}
2769		        	catch (e)
2770		        	{
2771		        		// ignore
2772		        	}
2773				}
2774
2775				// Asynchronous handling of errors
2776				var fn = mxUtils.bind(this, function()
2777				{
2778					// Removes URL parameter and reloads the page
2779					if (urlParams['url'] != null && this.spinner.spin(document.body, mxResources.get('reconnecting')))
2780					{
2781						window.location.search = this.getSearch(['url']);
2782					}
2783					else if (oldFile != null)
2784					{
2785						this.fileLoaded(oldFile);
2786					}
2787					else
2788					{
2789						noFile();
2790					}
2791				});
2792
2793				if (!noDialogs)
2794				{
2795					this.handleError(e, mxResources.get('errorLoadingFile'), fn, true, null, null, true);
2796				}
2797				else
2798				{
2799					fn();
2800				}
2801			}
2802		}
2803		else
2804		{
2805			noFile();
2806		}
2807
2808		return result;
2809	};
2810
2811	/**
2812	 * Creates a hash value for the current file.
2813	 */
2814	EditorUi.prototype.getHashValueForPages = function(pages, details)
2815	{
2816		// TODO: Avoid encoding to XML to make it faster
2817		var hash = 0;
2818		var model = new mxGraphModel();
2819		var codec = new mxCodec();
2820
2821		if (details != null)
2822		{
2823			details.byteCount = 0;
2824			details.attrCount = 0;
2825			details.eltCount = 0;
2826			details.nodeCount = 0;
2827		}
2828
2829		for (var i = 0; i < pages.length; i++)
2830		{
2831			this.updatePageRoot(pages[i]);
2832			var diagram = pages[i].node.cloneNode(false);
2833
2834			// FIXME: Check why names can be null in newer files
2835			// ignore in hash and do not diff null names for now
2836			diagram.removeAttribute('name');
2837
2838			// Model is only a holder for the root
2839			model.root = pages[i].root;
2840			var xmlNode = codec.encode(model);
2841			this.editor.graph.saveViewState(pages[i].viewState, xmlNode, true);
2842
2843			// Local defaults may be different in files so ignore
2844			xmlNode.removeAttribute('pageWidth');
2845			xmlNode.removeAttribute('pageHeight');
2846
2847			diagram.appendChild(xmlNode);
2848
2849			if (details != null)
2850			{
2851				details.eltCount += diagram.getElementsByTagName('*').length;
2852				details.nodeCount += diagram.getElementsByTagName('mxCell').length;
2853			}
2854
2855			hash = ((hash << 5) - hash + this.hashValue(diagram, function(obj, key, value, isXml)
2856			{
2857				// Ignores JS machine rounding errors in known numeric attributes
2858				// eg. 412.33333333333326 (Webkit/FF) == 412.33333333333325 (Edge/IE11)
2859				if (isXml && (obj.nodeName == 'mxGeometry' || obj.nodeName == 'mxPoint') &&
2860					(key == 'x' || key == 'y' || key == 'width' || key == 'height'))
2861				{
2862					return Math.round(value);
2863				}
2864				// Workaround for previous in patch written to mxCell in 10.0.23
2865				else if (isXml && obj.nodeName == 'mxCell' && key == 'previous')
2866				{
2867					return null;
2868				}
2869				else
2870				{
2871					return value;
2872				}
2873			}, details)) << 0;
2874		}
2875
2876		return hash;
2877	};
2878
2879	/**
2880	 * Creates a hash value for the given object. Replacer returns the value of the
2881	 * property or attribute for the given object or XML node.
2882	 */
2883	EditorUi.prototype.hashValue = function(obj, replacer, details)
2884	{
2885		var hash = 0;
2886
2887		// Checks for XML nodes
2888		if (obj != null && typeof obj === 'object' && typeof obj.nodeType === 'number' &&
2889			typeof obj.nodeName === 'string' && typeof obj.getAttribute === 'function')
2890		{
2891			if (obj.nodeName != null)
2892			{
2893				hash = hash ^ this.hashValue(obj.nodeName, replacer, details);
2894			}
2895
2896			if (obj.attributes != null)
2897			{
2898				if (details != null)
2899				{
2900					details.attrCount += obj.attributes.length;
2901				}
2902
2903				for (var i = 0; i < obj.attributes.length; i++)
2904				{
2905					var key = obj.attributes[i].name;
2906					var value = (replacer != null) ? replacer(obj, key, obj.attributes[i].value, true) : obj.attributes[i].value;
2907
2908					if (value != null)
2909					{
2910						hash = hash ^ (this.hashValue(key, replacer, details) +
2911							this.hashValue(value, replacer, details));
2912					}
2913				}
2914			}
2915
2916			if (obj.childNodes != null)
2917			{
2918				for (var i = 0; i < obj.childNodes.length; i++)
2919				{
2920					hash = ((hash << 5) - hash + this.hashValue(
2921						obj.childNodes[i], replacer, details)) << 0;
2922				}
2923			}
2924		}
2925		else if (obj != null && typeof obj !== 'function')
2926		{
2927			var str = String(obj);
2928			var temp = 0;
2929
2930			if (details != null)
2931			{
2932				details.byteCount += str.length;
2933			}
2934
2935			for (var i = 0; i < str.length; i++)
2936			{
2937		    	temp = ((temp << 5) - temp + str.charCodeAt(i)) << 0;
2938			}
2939
2940			hash = hash ^ temp;
2941		}
2942
2943	    return hash;
2944	};
2945
2946	/**
2947	 * Adds empty implementation
2948	 */
2949	EditorUi.prototype.descriptorChanged = function()
2950	{
2951		// empty
2952	};
2953
2954	/**
2955	 * Hook for subclassers.
2956	 */
2957	EditorUi.prototype.restoreLibraries = function() { };
2958
2959	/**
2960	 * Hook for subclassers.
2961	 */
2962	EditorUi.prototype.saveLibrary = function(name, images, file, mode, noSpin, noReload, fn) { };
2963
2964	/**
2965	 *
2966	 */
2967	EditorUi.prototype.isScratchpadEnabled = function()
2968	{
2969		return isLocalStorage || mxClient.IS_CHROMEAPP;
2970	};
2971
2972	/**
2973	 * Shows or hides the scratchpad library.
2974	 */
2975	EditorUi.prototype.toggleScratchpad = function()
2976	{
2977		if (this.isScratchpadEnabled())
2978		{
2979			if (this.scratchpad == null)
2980			{
2981				StorageFile.getFileContent(this, '.scratchpad', mxUtils.bind(this, function(xml)
2982				{
2983					if (xml == null)
2984					{
2985						xml = this.emptyLibraryXml;
2986					}
2987
2988					this.loadLibrary(new StorageLibrary(this, xml, '.scratchpad'));
2989				}));
2990			}
2991			else
2992			{
2993				this.closeLibrary(this.scratchpad);
2994			}
2995		}
2996	};
2997
2998	/**
2999	 * Translates this point by the given vector.
3000	 *
3001	 * @param {number} dx X-coordinate of the translation.
3002	 * @param {number} dy Y-coordinate of the translation.
3003	 */
3004	EditorUi.prototype.createLibraryDataFromImages = function(images)
3005	{
3006		var doc = mxUtils.createXmlDocument();
3007		var library = doc.createElement('mxlibrary');
3008		mxUtils.setTextContent(library, JSON.stringify(images));
3009		doc.appendChild(library);
3010
3011		return mxUtils.getXml(doc);
3012	};
3013
3014	/**
3015	 * Translates this point by the given vector.
3016	 *
3017	 * @param {number} dx X-coordinate of the translation.
3018	 * @param {number} dy Y-coordinate of the translation.
3019	 */
3020	EditorUi.prototype.closeLibrary = function(file)
3021	{
3022		if (file != null)
3023		{
3024			this.removeLibrarySidebar(file.getHash());
3025
3026			if (file.constructor != LocalLibrary)
3027			{
3028				mxSettings.removeCustomLibrary(file.getHash());
3029			}
3030
3031			if (file.title == '.scratchpad')
3032			{
3033				this.scratchpad = null;
3034			}
3035		}
3036	};
3037
3038	/**
3039	 * Translates this point by the given vector.
3040	 *
3041	 * @param {number} dx X-coordinate of the translation.
3042	 * @param {number} dy Y-coordinate of the translation.
3043	 */
3044	EditorUi.prototype.removeLibrarySidebar = function(id)
3045	{
3046		var elts = this.sidebar.palettes[id];
3047
3048		if (elts != null)
3049		{
3050			for (var i = 0; i < elts.length; i++)
3051			{
3052				elts[i].parentNode.removeChild(elts[i]);
3053			}
3054
3055			delete this.sidebar.palettes[id];
3056		}
3057	};
3058
3059	/**
3060	 * Changes the position of the library in the sidebar
3061	 */
3062	EditorUi.prototype.repositionLibrary = function(nextChild)
3063	{
3064	    var c = this.sidebar.container;
3065
3066	    if (nextChild == null)
3067	    {
3068	    	var elts = this.sidebar.palettes['L.scratchpad'];
3069
3070	    	if (elts == null)
3071	    	{
3072	    		elts = this.sidebar.palettes['search'];
3073	    	}
3074
3075	    	if (elts != null)
3076	    	{
3077	    		nextChild = elts[elts.length - 1].nextSibling;
3078	    	}
3079	    }
3080
3081		nextChild = (nextChild != null) ? nextChild : c.firstChild.nextSibling.nextSibling;
3082
3083		var content = c.lastChild;
3084		var title = content.previousSibling;
3085
3086	    c.insertBefore(content, nextChild);
3087	    c.insertBefore(title, content);
3088	}
3089
3090	/**
3091	 * Translates this point by the given vector.
3092	 *
3093	 * @param {number} dx X-coordinate of the translation.
3094	 * @param {number} dy Y-coordinate of the translation.
3095	 */
3096	EditorUi.prototype.loadLibrary = function(file, expand)
3097	{
3098		var doc = mxUtils.parseXml(file.getData());
3099
3100		if (doc.documentElement.nodeName == 'mxlibrary')
3101		{
3102			var images = JSON.parse(mxUtils.getTextContent(doc.documentElement));
3103			this.libraryLoaded(file, images, doc.documentElement.getAttribute('title'), expand);
3104		}
3105		else
3106		{
3107			throw {message: mxResources.get('notALibraryFile')};
3108		}
3109	};
3110
3111	/**
3112	 * Translates this point by the given vector.
3113	 *
3114	 * @param {number} dx X-coordinate of the translation.
3115	 * @param {number} dy Y-coordinate of the translation.
3116	 */
3117	EditorUi.prototype.getLibraryStorageHint = function(file)
3118	{
3119		return '';
3120	};
3121
3122	/**
3123	 * Translates this point by the given vector.
3124	 *
3125	 * @param {number} dx X-coordinate of the translation.
3126	 * @param {number} dy Y-coordinate of the translation.
3127	 */
3128	EditorUi.prototype.libraryLoaded = function(file, images, optionalTitle, expand)
3129	{
3130		if (this.sidebar == null)
3131		{
3132			return;
3133		}
3134
3135		if (file.constructor != LocalLibrary)
3136		{
3137			mxSettings.addCustomLibrary(file.getHash());
3138		}
3139
3140		if (file.title == '.scratchpad')
3141		{
3142			this.scratchpad = file;
3143		}
3144
3145		var elts = this.sidebar.palettes[file.getHash()];
3146		var nextSibling = (elts != null) ? elts[elts.length - 1].nextSibling : null;
3147
3148		// Removes existing sidebar entry for this library
3149		this.removeLibrarySidebar(file.getHash());
3150		var dropTarget = null;
3151
3152		var addImages = mxUtils.bind(this, function(imgs, content)
3153		{
3154			if (imgs.length == 0 && file.isEditable())
3155			{
3156				if (dropTarget == null)
3157				{
3158					dropTarget = document.createElement('div');
3159					dropTarget.className = 'geDropTarget';
3160					mxUtils.write(dropTarget, mxResources.get('dragElementsHere'));
3161				}
3162
3163				content.appendChild(dropTarget);
3164			}
3165			else
3166			{
3167				this.addLibraryEntries(imgs, content);
3168			}
3169		});
3170
3171		// Adds entries to search index
3172		// KNOWN: Existing entries are not replaced after edit of custom library
3173		if (this.sidebar != null && images != null)
3174		{
3175			this.sidebar.addEntries(images);
3176		}
3177
3178		// Adds new sidebar entry for this library
3179		var tmp = (optionalTitle != null && optionalTitle.length > 0) ? optionalTitle : file.getTitle();
3180		var contentDiv = this.sidebar.addPalette(file.getHash(), tmp,
3181			(expand != null) ? expand : true, mxUtils.bind(this, function(content)
3182		{
3183			addImages(images, content);
3184	    }));
3185
3186		this.repositionLibrary(nextSibling);
3187
3188		// Adds tooltip for backend
3189		var title = contentDiv.parentNode.previousSibling;
3190	    var tip = title.getAttribute('title');
3191
3192	    if (tip != null && tip.length > 0 && file.title != '.scratchpad')
3193	    {
3194	    	title.setAttribute('title', this.getLibraryStorageHint(file) + '\n' + tip);
3195	    }
3196
3197	    var buttons = document.createElement('div');
3198	    buttons.style.position = 'absolute';
3199	    buttons.style.right = '0px';
3200	    buttons.style.top = '0px';
3201	    buttons.style.padding = '8px'
3202	    buttons.style.backgroundColor = 'inherit';
3203
3204	    title.style.position = 'relative';
3205
3206	    var btnWidth = 18;
3207		var btn = document.createElement('img');
3208		btn.setAttribute('src', Editor.crossImage);
3209		btn.setAttribute('title', mxResources.get('close'));
3210		btn.setAttribute('valign', 'absmiddle');
3211		btn.setAttribute('border', '0');
3212		btn.style.position = 'relative';
3213		btn.style.top = '2px';
3214		btn.style.width = '14px';
3215		btn.style.cursor = 'pointer';
3216		btn.style.margin = '0 3px';
3217
3218		if (Editor.isDarkMode())
3219		{
3220			btn.style.filter = 'invert(100%)';
3221		}
3222
3223		var saveBtn = null;
3224
3225	    if (file.title != '.scratchpad' || this.closableScratchpad)
3226	    {
3227			buttons.appendChild(btn);
3228
3229			mxEvent.addListener(btn, 'click', mxUtils.bind(this, function(evt)
3230			{
3231				// Workaround for close after any button click in IE8
3232				if (!mxEvent.isConsumed(evt))
3233				{
3234					var fn = mxUtils.bind(this, function()
3235					{
3236						this.closeLibrary(file);
3237					});
3238
3239					if (saveBtn != null)
3240					{
3241						this.confirm(mxResources.get('allChangesLost'), null, fn,
3242							mxResources.get('cancel'), mxResources.get('discardChanges'));
3243					}
3244					else
3245					{
3246						fn();
3247					}
3248
3249					mxEvent.consume(evt);
3250				}
3251			}));
3252	    }
3253
3254		if (file.isEditable())
3255		{
3256			var graph = this.editor.graph;
3257			var spinBtn = null;
3258
3259			var editLibrary = mxUtils.bind(this, function(evt)
3260			{
3261				this.showLibraryDialog(file.getTitle(), contentDiv, images, file, file.getMode());
3262				mxEvent.consume(evt);
3263			});
3264
3265			var saveLibrary = mxUtils.bind(this, function(evt)
3266			{
3267				file.setModified(true);
3268
3269				if (file.isAutosave())
3270				{
3271					if (spinBtn != null && spinBtn.parentNode != null)
3272					{
3273						spinBtn.parentNode.removeChild(spinBtn);
3274					}
3275
3276					spinBtn = btn.cloneNode(false);
3277					spinBtn.setAttribute('src', Editor.spinImage);
3278					spinBtn.setAttribute('title', mxResources.get('saving'));
3279					spinBtn.style.cursor = 'default';
3280					spinBtn.style.marginRight = '2px';
3281					spinBtn.style.marginTop = '-2px';
3282					buttons.insertBefore(spinBtn, buttons.firstChild);
3283					title.style.paddingRight = (buttons.childNodes.length * btnWidth) + 'px';
3284
3285					this.saveLibrary(file.getTitle(), images, file, file.getMode(), true, true, function()
3286					{
3287						if (spinBtn != null && spinBtn.parentNode != null)
3288						{
3289							spinBtn.parentNode.removeChild(spinBtn);
3290							title.style.paddingRight = (buttons.childNodes.length * btnWidth) + 'px';
3291						}
3292					});
3293				}
3294				else if (saveBtn == null)
3295				{
3296					saveBtn = btn.cloneNode(false);
3297					saveBtn.setAttribute('src', Editor.saveImage);
3298					saveBtn.setAttribute('title', mxResources.get('save'));
3299					buttons.insertBefore(saveBtn, buttons.firstChild);
3300
3301					mxEvent.addListener(saveBtn, 'click', mxUtils.bind(this, function(evt)
3302					{
3303						this.saveLibrary(file.getTitle(), images, file, file.getMode(),
3304							file.constructor == LocalLibrary, true, function()
3305							{
3306								if (saveBtn != null && !file.isModified())
3307								{
3308									title.style.paddingRight = (buttons.childNodes.length * btnWidth) + 'px';
3309									saveBtn.parentNode.removeChild(saveBtn);
3310									saveBtn = null;
3311								}
3312							});
3313
3314						mxEvent.consume(evt);
3315					}));
3316
3317					title.style.paddingRight = (buttons.childNodes.length * btnWidth) + 'px';
3318				}
3319			});
3320
3321			var addCells = mxUtils.bind(this, function(cells, bounds, evt, title)
3322			{
3323				cells = graph.cloneCells(mxUtils.sortCells(graph.model.getTopmostCells(cells)));
3324
3325				// Translates cells to origin
3326				for (var i = 0; i < cells.length; i++)
3327				{
3328					var geo = graph.getCellGeometry(cells[i]);
3329
3330					if (geo != null)
3331					{
3332						geo.translate(-bounds.x, -bounds.y);
3333					}
3334				}
3335
3336				contentDiv.appendChild(this.sidebar.createVertexTemplateFromCells(
3337					cells, bounds.width, bounds.height, title || '', true, false, false));
3338
3339				var xml = Graph.compress(mxUtils.getXml(this.editor.graph.encodeCells(cells)));
3340				var entry = {xml: xml, w: bounds.width, h: bounds.height};
3341
3342				if (title != null)
3343				{
3344					entry.title = title;
3345				}
3346
3347				images.push(entry);
3348				saveLibrary(evt);
3349
3350				if (dropTarget != null && dropTarget.parentNode != null && images.length > 0)
3351				{
3352					dropTarget.parentNode.removeChild(dropTarget);
3353					dropTarget = null;
3354				}
3355			});
3356
3357			var addSelection = mxUtils.bind(this, function(evt)
3358			{
3359				if (!graph.isSelectionEmpty())
3360				{
3361					var cells = graph.getSelectionCells();
3362					var bounds = graph.view.getBounds(cells);
3363
3364					var s = graph.view.scale;
3365
3366					bounds.x /= s;
3367					bounds.y /= s;
3368					bounds.width /= s;
3369					bounds.height /= s;
3370
3371					bounds.x -= graph.view.translate.x;
3372					bounds.y -= graph.view.translate.y;
3373
3374					addCells(cells, bounds);
3375				}
3376				else if (graph.getRubberband().isActive())
3377				{
3378					graph.getRubberband().execute(evt);
3379					graph.getRubberband().reset();
3380				}
3381				else
3382				{
3383					this.showError(mxResources.get('error'), mxResources.get('nothingIsSelected'), mxResources.get('ok'));
3384				}
3385
3386				mxEvent.consume(evt);
3387			});
3388
3389			// Adds drop handler from graph
3390			mxEvent.addGestureListeners(contentDiv, function(){}, mxUtils.bind(this, function(evt)
3391			{
3392				if (graph.isMouseDown && graph.panningManager != null && graph.graphHandler.first != null)
3393				{
3394					graph.graphHandler.suspend();
3395
3396					if (graph.graphHandler.hint != null)
3397					{
3398						graph.graphHandler.hint.style.visibility = 'hidden';
3399					}
3400
3401					contentDiv.style.backgroundColor = '#f1f3f4';
3402					contentDiv.style.cursor = 'copy';
3403					graph.panningManager.stop();
3404					graph.autoScroll = false;
3405
3406					mxEvent.consume(evt);
3407				}
3408			}), mxUtils.bind(this, function(evt)
3409			{
3410				if (graph.isMouseDown && graph.panningManager != null && graph.graphHandler != null)
3411				{
3412					contentDiv.style.backgroundColor = '';
3413					contentDiv.style.cursor = 'default';
3414					this.sidebar.showTooltips = true;
3415					graph.panningManager.stop();
3416
3417					graph.graphHandler.reset();
3418					graph.isMouseDown = false;
3419					graph.autoScroll = true;
3420
3421					addSelection(evt);
3422					mxEvent.consume(evt);
3423				}
3424			}));
3425
3426			// Handles mouse leaving the library and restoring move
3427			mxEvent.addListener(contentDiv, 'mouseleave', mxUtils.bind(this, function(evt)
3428			{
3429				if (graph.isMouseDown && graph.graphHandler.first != null)
3430				{
3431					graph.graphHandler.resume();
3432
3433					if (graph.graphHandler.hint != null)
3434					{
3435						graph.graphHandler.hint.style.visibility = 'visible';
3436					}
3437
3438					contentDiv.style.backgroundColor = '';
3439					contentDiv.style.cursor = '';
3440					graph.autoScroll = true;
3441				}
3442			}));
3443
3444			// Adds drop handler from filesystem
3445			if (Graph.fileSupport)
3446			{
3447				mxEvent.addListener(contentDiv, 'dragover', mxUtils.bind(this, function(evt)
3448				{
3449					contentDiv.style.backgroundColor = '#f1f3f4';
3450					evt.dataTransfer.dropEffect = 'copy';
3451					contentDiv.style.cursor = 'copy';
3452					this.sidebar.hideTooltip();
3453					evt.stopPropagation();
3454					evt.preventDefault();
3455				}));
3456
3457				mxEvent.addListener(contentDiv, 'drop', mxUtils.bind(this, function(evt)
3458				{
3459					contentDiv.style.cursor = '';
3460					contentDiv.style.backgroundColor = '';
3461
3462				    if (evt.dataTransfer.files.length > 0)
3463				    {
3464				    	this.importFiles(evt.dataTransfer.files, 0, 0, this.maxImageSize, mxUtils.bind(this, function(data, mimeType, x, y, w, h, img, doneFn, file)
3465				    	{
3466							if (data != null && mimeType.substring(0, 6) == 'image/')
3467							{
3468								var style = 'shape=image;verticalLabelPosition=bottom;verticalAlign=top;imageAspect=0;aspect=fixed;image=' +
3469									this.convertDataUri(data);
3470								var cells = [new mxCell('', new mxGeometry(0, 0, w, h), style)];
3471								cells[0].vertex = true;
3472
3473								addCells(cells, new mxRectangle(0, 0, w, h), evt, (mxEvent.isAltDown(evt)) ? null : img.substring(0, img.lastIndexOf('.')).replace(/_/g, ' '));
3474
3475								if (dropTarget != null && dropTarget.parentNode != null && images.length > 0)
3476								{
3477									dropTarget.parentNode.removeChild(dropTarget);
3478									dropTarget = null;
3479								}
3480							}
3481							else
3482							{
3483								var done = false;
3484
3485								var doImport = mxUtils.bind(this, function(theData, theMimeType)
3486								{
3487									if (theData != null && theMimeType == 'application/pdf')
3488									{
3489										var xml = Editor.extractGraphModelFromPdf(theData);
3490
3491										if (xml != null && xml.length > 0)
3492										{
3493											theMimeType = 'text/xml';
3494											theData = xml;
3495										}
3496									}
3497
3498									if (theData != null) //Try to parse the file as xml (can be a library or mxfile). Otherwise, an error will be shown
3499									{
3500										var doc = mxUtils.parseXml(theData);
3501
3502										if (doc.documentElement.nodeName == 'mxlibrary')
3503										{
3504											try
3505											{
3506												var temp = JSON.parse(mxUtils.getTextContent(doc.documentElement));
3507												addImages(temp, contentDiv);
3508												images = images.concat(temp);
3509												saveLibrary(evt);
3510												this.spinner.stop();
3511												done = true;
3512											}
3513											catch (e)
3514											{
3515												// ignore
3516											}
3517										}
3518										else if (doc.documentElement.nodeName == 'mxfile')
3519										{
3520											try
3521											{
3522												var pages = doc.documentElement.getElementsByTagName('diagram');
3523
3524												for (var i = 0; i < pages.length; i++)
3525												{
3526													var cells = this.stringToCells(Editor.getDiagramNodeXml(pages[i]));
3527													var size = this.editor.graph.getBoundingBoxFromGeometry(cells);
3528													addCells(cells, new mxRectangle(0, 0, size.width, size.height), evt);
3529												}
3530
3531												done = true;
3532											}
3533											catch (e)
3534											{
3535												if (window.console != null)
3536												{
3537													console.log('error in drop handler:', e);
3538												}
3539											}
3540										}
3541									}
3542
3543									if (!done)
3544									{
3545										this.spinner.stop();
3546										this.handleError({message: mxResources.get('errorLoadingFile')})
3547									}
3548
3549									if (dropTarget != null && dropTarget.parentNode != null && images.length > 0)
3550									{
3551										dropTarget.parentNode.removeChild(dropTarget);
3552										dropTarget = null;
3553									}
3554								});
3555
3556								if (file != null && img != null && ((/(\.v(dx|sdx?))($|\?)/i.test(img)) || /(\.vs(x|sx?))($|\?)/i.test(img)))
3557								{
3558									this.importVisio(file, function(xml)
3559									{
3560										doImport(xml, 'text/xml');
3561									}, null, img);
3562								}
3563								else if (!this.isOffline() && new XMLHttpRequest().upload && this.isRemoteFileFormat(data, img) && file != null)
3564								{
3565									this.parseFile(file, mxUtils.bind(this, function(xhr)
3566									{
3567										if (xhr.readyState == 4)
3568										{
3569											this.spinner.stop();
3570
3571											if (xhr.status >= 200 && xhr.status <= 299)
3572											{
3573												doImport(xhr.responseText, 'text/xml');
3574											}
3575											else
3576											{
3577												this.handleError({message: mxResources.get((xhr.status == 413) ?
3578				            						'drawingTooLarge' : 'invalidOrMissingFile')},
3579				            						mxResources.get('errorLoadingFile'));
3580											}
3581										}
3582									}));
3583								}
3584								else
3585								{
3586									doImport(data, mimeType);
3587								}
3588							}
3589				    	}));
3590					}
3591
3592				    evt.stopPropagation();
3593				    evt.preventDefault();
3594				}));
3595
3596				mxEvent.addListener(contentDiv, 'dragleave', function(evt)
3597				{
3598					contentDiv.style.cursor = '';
3599					contentDiv.style.backgroundColor = '';
3600					evt.stopPropagation();
3601					evt.preventDefault();
3602				});
3603			}
3604
3605			btn = btn.cloneNode(false);
3606			btn.setAttribute('src', Editor.editImage);
3607			btn.setAttribute('title', mxResources.get('edit'));
3608			buttons.insertBefore(btn, buttons.firstChild);
3609
3610			mxEvent.addListener(btn, 'click', editLibrary);
3611			mxEvent.addListener(contentDiv, 'dblclick', function(evt)
3612			{
3613				if (mxEvent.getSource(evt) == contentDiv)
3614				{
3615					editLibrary(evt);
3616				}
3617			});
3618
3619			var btn2 = btn.cloneNode(false);
3620			btn2.setAttribute('src', Editor.plusImage);
3621			btn2.setAttribute('title', mxResources.get('add'));
3622			buttons.insertBefore(btn2, buttons.firstChild);
3623			mxEvent.addListener(btn2, 'click', addSelection);
3624
3625			if (!this.isOffline() && file.title == '.scratchpad' && EditorUi.scratchpadHelpLink != null)
3626			{
3627				var link = document.createElement('span');
3628				link.setAttribute('title', mxResources.get('help'));
3629				link.style.cssText = 'color:#a3a3a3;text-decoration:none;margin-right:2px;cursor:pointer;';
3630				mxUtils.write(link, '?');
3631
3632				mxEvent.addGestureListeners(link, mxUtils.bind(this, function(evt)
3633				{
3634					this.openLink(EditorUi.scratchpadHelpLink);
3635					mxEvent.consume(evt);
3636				}));
3637
3638				buttons.insertBefore(link, buttons.firstChild);
3639			}
3640		}
3641
3642		title.appendChild(buttons);
3643		title.style.paddingRight = (buttons.childNodes.length * btnWidth) + 'px';
3644	};
3645
3646	/**
3647	 * Adds the library entries to the given DOM node.
3648	 */
3649	EditorUi.prototype.addLibraryEntries = function(imgs, content)
3650	{
3651		for (var i = 0; i < imgs.length; i++)
3652		{
3653			var img = imgs[i];
3654			var data = img.data;
3655
3656			if (data != null)
3657			{
3658				data = this.convertDataUri(data);
3659				var s = 'shape=image;verticalLabelPosition=bottom;verticalAlign=top;imageAspect=0;';
3660
3661				if (img.aspect == 'fixed')
3662				{
3663					s += 'aspect=fixed;'
3664				}
3665
3666				content.appendChild(this.sidebar.createVertexTemplate(s + 'image=' +
3667					data, img.w, img.h, '', img.title || '', false, false, true));
3668			}
3669			else if (img.xml != null)
3670			{
3671				var cells = this.stringToCells(Graph.decompress(img.xml));
3672
3673				if (cells.length > 0)
3674				{
3675					content.appendChild(this.sidebar.createVertexTemplateFromCells(
3676						cells, img.w, img.h, img.title || '', true, false, true));
3677				}
3678			}
3679		}
3680	};
3681
3682	/**
3683	 * Extracts the resource for the current language from the given multi language
3684	 * resource object of the form {es: "...", de: "...", main: "..."} where the keys
3685	 * are country codes and main defines the fallback if no resource for the current
3686	 * country code exists.
3687	 */
3688	EditorUi.prototype.getResource = function(obj)
3689	{
3690		return (obj != null) ? (obj[mxLanguage] || obj.main) : null;
3691	};
3692
3693	/**
3694	 * EditorUi Overrides
3695	 */
3696	EditorUi.prototype.footerHeight = 0;
3697
3698	if (urlParams['savesidebar'] == '1')
3699	{
3700		Sidebar.prototype.thumbWidth = 64;
3701		Sidebar.prototype.thumbHeight = 64;
3702	}
3703
3704	/**
3705	 * Programmatic settings for theme.
3706	 */
3707    EditorUi.initTheme = function()
3708    {
3709    	if (uiTheme == 'atlas')
3710    	{
3711    		mxClient.link('stylesheet', STYLE_PATH + '/atlas.css');
3712
3713    		if (typeof Toolbar !== 'undefined')
3714    		{
3715    			Toolbar.prototype.unselectedBackground = 'linear-gradient(rgb(255, 255, 255) 0px, rgb(242, 242, 242) 100%)';
3716    			Toolbar.prototype.selectedBackground = 'rgb(242, 242, 242)';
3717    		}
3718
3719    		Editor.prototype.initialTopSpacing = 3;
3720    		EditorUi.prototype.menubarHeight = 41;
3721    		EditorUi.prototype.toolbarHeight = 38;
3722    	}
3723    	else if (Editor.isDarkMode())
3724    	{
3725    		mxClient.link('stylesheet', STYLE_PATH + '/dark.css');
3726
3727			Dialog.backdropColor = Editor.darkColor;
3728			Format.inactiveTabBackgroundColor = 'black';
3729	    	Graph.prototype.defaultThemeName = 'darkTheme';
3730			Graph.prototype.shapeBackgroundColor = Editor.darkColor;
3731			Graph.prototype.shapeForegroundColor = Editor.lightColor;
3732			Graph.prototype.defaultPageBackgroundColor = Editor.darkColor;
3733			Graph.prototype.defaultPageBorderColor = '#505759';
3734			BaseFormatPanel.prototype.buttonBackgroundColor = Editor.darkColor;
3735			mxGraphHandler.prototype.previewColor = '#cccccc';
3736			StyleFormatPanel.prototype.defaultStrokeColor = '#cccccc';
3737			mxConstants.DROP_TARGET_COLOR = '#00ff00';
3738    	}
3739
3740		Editor.sketchFontFamily = 'Architects Daughter';
3741		Editor.sketchFontSource = 'https%3A%2F%2Ffonts.googleapis.com%2Fcss%3Ffamily%3DArchitects%2BDaughter';
3742
3743		// Implements the sketch-min UI
3744		if (urlParams['sketch'] == '1')
3745		{
3746			Graph.prototype.defaultVertexStyle = {'hachureGap': '4'};
3747			Graph.prototype.defaultEdgeStyle = {'edgeStyle': 'none', 'rounded': '0', 'curved': '1',
3748				'jettySize': 'auto', 'orthogonalLoop': '1', 'endArrow': 'open', 'startSize': '14', 'endSize': '14',
3749				'sourcePerimeterSpacing': '8', 'targetPerimeterSpacing': '8'};
3750
3751			Editor.configurationKey = '.sketch-configuration';
3752			Editor.settingsKey = '.sketch-config';
3753			Graph.prototype.defaultGridEnabled = urlParams['grid'] == '1';
3754			Graph.prototype.defaultPageVisible = urlParams['pv'] == '1';
3755			Graph.prototype.defaultEdgeLength = 120;
3756			Editor.fitWindowBorders = new mxRectangle(60, 30, 30, 30);
3757		}
3758    };
3759
3760    EditorUi.initTheme();
3761
3762	/**
3763	 * Overrides image dialog to add image search and Google+.
3764	 */
3765    EditorUi.prototype.showImageDialog = function(title, value, fn, ignoreExisting, convertDataUri)
3766	{
3767		// KNOWN: IE+FF don't return keyboard focus after image dialog (calling focus doesn't help)
3768	    var dlg = new ImageDialog(this, title, value, fn, ignoreExisting, convertDataUri);
3769		this.showDialog(dlg.container, (Graph.fileSupport) ? 480 : 360, (Graph.fileSupport) ? 200 : 90, true, true);
3770		dlg.init();
3771	};
3772
3773	/**
3774	 * Hides the current menu.
3775	 */
3776	EditorUi.prototype.showBackgroundImageDialog = function(apply, img)
3777	{
3778		apply = (apply != null) ? apply : mxUtils.bind(this, function(image, failed)
3779		{
3780			if (!failed)
3781			{
3782				var change = new ChangePageSetup(this, null, image);
3783				change.ignoreColor = true;
3784
3785				this.editor.graph.model.execute(change);
3786			}
3787		});
3788		var dlg = new BackgroundImageDialog(this, apply, img);
3789		this.showDialog(dlg.container, 360, 200, true, true);
3790		dlg.init();
3791	};
3792
3793	/**
3794	 * Hides the current menu.
3795	 */
3796	EditorUi.prototype.showLibraryDialog = function(name, sidebar, images, file, mode)
3797	{
3798		var dlg = new LibraryDialog(this, name, sidebar, images, file, mode);
3799
3800		this.showDialog(dlg.container, 640, 440, true, false, mxUtils.bind(this, function(cancel)
3801		{
3802			if (cancel && this.getCurrentFile() == null && urlParams['embed'] != '1')
3803			{
3804				this.showSplash();
3805			}
3806		}));
3807
3808		dlg.init();
3809	};
3810
3811	/**
3812	 * Overridden to update after view state changes.
3813	 */
3814	var editorUiCreateFormat = EditorUi.prototype.createFormat;
3815
3816	EditorUi.prototype.createFormat = function(container)
3817	{
3818		var format = editorUiCreateFormat.apply(this, arguments);
3819
3820		this.editor.graph.addListener('viewStateChanged', mxUtils.bind(this, function(evt)
3821		{
3822			if (this.editor.graph.isSelectionEmpty())
3823			{
3824				format.refresh();
3825			}
3826		}));
3827
3828		return format;
3829	};
3830
3831	/**
3832	 * Hook for sidebar footer container.
3833	 */
3834	EditorUi.prototype.createSidebarFooterContainer = function()
3835	{
3836		var div =  this.createDiv('geSidebarContainer geSidebarFooter');
3837		div.style.position = 'absolute';
3838		div.style.overflow = 'hidden';
3839
3840		var elt2 = document.createElement('a');
3841		elt2.className = 'geTitle';
3842		elt2.style.color = '#DF6C0C';
3843		elt2.style.fontWeight = 'bold';
3844		elt2.style.height = '100%';
3845		elt2.style.paddingTop = '9px';
3846		elt2.innerHTML = '<span>+</span>';
3847
3848		var span = elt2.getElementsByTagName('span')[0];
3849		span.style.fontSize = '18px';
3850		span.style.marginRight = '5px';
3851
3852		mxUtils.write(elt2, mxResources.get('moreShapes') + '...');
3853
3854		// Prevents focus
3855		mxEvent.addListener(elt2, (mxClient.IS_POINTER) ? 'pointerdown' : 'mousedown',
3856			mxUtils.bind(this, function(evt)
3857		{
3858			evt.preventDefault();
3859		}));
3860
3861		mxEvent.addListener(elt2, 'click', mxUtils.bind(this, function(evt)
3862		{
3863			this.actions.get('shapes').funct();
3864			mxEvent.consume(evt);
3865		}));
3866
3867		div.appendChild(elt2);
3868
3869		return div;
3870	};
3871
3872	/**
3873	 * Translates this point by the given vector.
3874	 *
3875	 * @param {number} dx X-coordinate of the translation.
3876	 * @param {number} dy Y-coordinate of the translation.
3877	 */
3878	EditorUi.prototype.handleError = function(resp, title, fn, invokeFnOnClose, notFoundMessage, fileHash, disableLogging)
3879	{
3880		var resume = (this.spinner != null && this.spinner.pause != null) ? this.spinner.pause() : function() {};
3881		var e = (resp != null && resp.error != null) ? resp.error : resp;
3882
3883		// Logs errors and writes stack to console
3884		if (resp != null && resp.stack != null && resp.message != null)
3885		{
3886			try
3887			{
3888				if (!disableLogging)
3889				{
3890					EditorUi.logError('Caught: ' +
3891						(resp.message == '' && resp.name != null) ? resp.name : resp.message,
3892						resp.filename, resp.lineNumber, resp.columnNumber, resp, 'INFO');
3893				}
3894				else
3895				{
3896					if (window.console != null)
3897					{
3898						console.error('EditorUi.handleError:', resp);
3899					}
3900				}
3901			}
3902			catch (e)
3903			{
3904				// ignore
3905			}
3906		}
3907
3908		if (e != null || title != null)
3909		{
3910			var msg = mxUtils.htmlEntities(mxResources.get('unknownError'));
3911			var btn = mxResources.get('ok');
3912			var retry = null;
3913			title = (title != null) ? title : mxResources.get('error');
3914
3915			if (e != null)
3916			{
3917				if (e.retry != null)
3918				{
3919					btn = mxResources.get('cancel');
3920					retry = function()
3921					{
3922						resume();
3923						e.retry();
3924					};
3925				}
3926
3927				if (e.code == 404 || e.status == 404 || e.code == 403)
3928				{
3929					if (e.code == 403)
3930					{
3931						if (e.message != null)
3932						{
3933							msg = mxUtils.htmlEntities(e.message);
3934						}
3935						else
3936						{
3937							msg = mxUtils.htmlEntities(mxResources.get('accessDenied'));
3938						}
3939					}
3940					else
3941					{
3942						msg = (notFoundMessage != null) ? notFoundMessage :
3943							mxUtils.htmlEntities(mxResources.get('fileNotFoundOrDenied') +
3944							((this.drive != null && this.drive.user != null) ? ' (' + this.drive.user.displayName +
3945							', ' + this.drive.user.email+ ')' : ''));
3946					}
3947
3948					var id = (notFoundMessage != null) ? null : ((fileHash != null) ? fileHash : window.location.hash);
3949
3950					// #U handles case where we tried to fallback to Google File and
3951					// hash property still shows the public URL we tried to load
3952					if (id != null && (id.substring(0, 2) == '#G' ||
3953						id.substring(0, 45) == '#Uhttps%3A%2F%2Fdrive.google.com%2Fuc%3Fid%3D') &&
3954						((resp != null && resp.error != null && ((resp.error.errors != null &&
3955						resp.error.errors.length > 0 && resp.error.errors[0].reason == 'fileAccess') ||
3956						(resp.error.data != null && resp.error.data.length > 0 &&
3957						resp.error.data[0].reason == 'fileAccess'))) ||
3958						e.code == 404 || e.status == 404))
3959					{
3960						id = (id.substring(0, 2) == '#U') ? id.substring(45, id.lastIndexOf('%26ex')) : id.substring(2);
3961
3962						// Special case where the button must have a different label and function
3963						this.showError(title, msg, mxResources.get('openInNewWindow'), mxUtils.bind(this, function()
3964						{
3965							this.editor.graph.openLink('https://drive.google.com/open?id=' + id);
3966							this.handleError(resp, title, fn, invokeFnOnClose, notFoundMessage)
3967						}), retry, mxResources.get('changeUser'), mxUtils.bind(this, function()
3968						{
3969							var driveUsers = this.drive.getUsersList();
3970
3971							var div = document.createElement('div');
3972
3973							var title = document.createElement('span');
3974							title.style.marginTop = '6px';
3975							mxUtils.write(title, mxResources.get('changeUser') + ': ');
3976
3977							div.appendChild(title);
3978
3979							var usersSelect = document.createElement('select');
3980							usersSelect.style.width = '200px';
3981
3982							//TODO This code is similar to Dialogs.js change user part in SplashDialog
3983							function fillUsersSelect()
3984							{
3985								usersSelect.innerHTML = '';
3986
3987								for (var i = 0; i < driveUsers.length; i++)
3988								{
3989									var option = document.createElement('option');
3990									mxUtils.write(option, driveUsers[i].displayName);
3991									option.value = i;
3992									usersSelect.appendChild(option);
3993									//More info (email) about the user in a disabled option
3994									option = document.createElement('option');
3995									option.innerHTML = '&nbsp;&nbsp;&nbsp;';
3996									mxUtils.write(option, '<' + driveUsers[i].email + '>');
3997									option.setAttribute('disabled', 'disabled');
3998									usersSelect.appendChild(option);
3999								}
4000
4001								//Add account option
4002								var option = document.createElement('option');
4003								mxUtils.write(option, mxResources.get('addAccount'));
4004								option.value = driveUsers.length;
4005								usersSelect.appendChild(option);
4006							}
4007
4008							fillUsersSelect();
4009
4010							mxEvent.addListener(usersSelect, 'change', mxUtils.bind(this, function()
4011							{
4012								var userIndex = usersSelect.value;
4013								var existingAccount = driveUsers.length != userIndex;
4014
4015								if (existingAccount)
4016								{
4017									this.drive.setUser(driveUsers[userIndex]);
4018								}
4019
4020								this.drive.authorize(existingAccount, mxUtils.bind(this, function()
4021								{
4022									if (!existingAccount)
4023									{
4024										driveUsers = this.drive.getUsersList();
4025										fillUsersSelect();
4026									}
4027								}), mxUtils.bind(this, function(resp)
4028								{
4029									this.handleError(resp);
4030								}), true);
4031							}));
4032
4033							div.appendChild(usersSelect);
4034
4035							var dlg = new CustomDialog(this, div, mxUtils.bind(this, function()
4036							{
4037								this.loadFile(window.location.hash.substr(1), true);
4038							}));
4039							this.showDialog(dlg.container, 300, 100, true, true);
4040						}), mxResources.get('cancel'), mxUtils.bind(this, function()
4041						{
4042							this.hideDialog();
4043
4044							if (fn != null)
4045							{
4046								fn();
4047							}
4048						}), 480, 150);
4049
4050						return;
4051					}
4052				}
4053
4054				if (e.message != null)
4055				{
4056					if (e.message == '' && e.name != null)
4057					{
4058						msg = mxUtils.htmlEntities(e.name);
4059					}
4060					else
4061					{
4062						msg = mxUtils.htmlEntities(e.message);
4063					}
4064				}
4065				else if (e.response != null && e.response.error != null)
4066				{
4067					msg = mxUtils.htmlEntities(e.response.error);
4068				}
4069				else if (typeof window.App !== 'undefined')
4070				{
4071					if (e.code == App.ERROR_TIMEOUT)
4072					{
4073						msg = mxUtils.htmlEntities(mxResources.get('timeout'));
4074					}
4075					else if (e.code == App.ERROR_BUSY)
4076					{
4077						msg = mxUtils.htmlEntities(mxResources.get('busy'));
4078					}
4079					else if (typeof e === 'string' && e.length > 0)
4080					{
4081						msg = mxUtils.htmlEntities(e);
4082					}
4083				}
4084			}
4085
4086			var btn3 = null;
4087			var fn3 = null;
4088
4089			if (e != null && e.helpLink != null)
4090			{
4091				btn3 = mxResources.get('help');
4092
4093				fn3 = mxUtils.bind(this, function()
4094				{
4095					return this.editor.graph.openLink(e.helpLink);
4096				});
4097			}
4098			else if (e != null && e.ownerEmail != null)
4099			{
4100				btn3 = mxResources.get('contactOwner');
4101
4102				msg += mxUtils.htmlEntities(' (' + btn3 + ': ' + e.ownerEmail + ')');
4103
4104				fn3 = mxUtils.bind(this, function()
4105				{
4106					return this.openLink('mailto:' + mxUtils.htmlEntities(e.ownerEmail));
4107				});
4108			}
4109
4110			this.showError(title, msg, btn, fn, retry, null, null, btn3, fn3,
4111				null, null, null, (invokeFnOnClose) ? fn : null);
4112		}
4113		else if (fn != null)
4114		{
4115			fn();
4116		}
4117	};
4118
4119	/**
4120	 * Translates this point by the given vector.
4121	 *
4122	 * @param {number} dx X-coordinate of the translation.
4123	 * @param {number} dy Y-coordinate of the translation.
4124	 */
4125	EditorUi.prototype.alert = function(msg, fn, optionalWidth)
4126	{
4127		var dlg = new ErrorDialog(this, null, msg, mxResources.get('ok'), fn);
4128		this.showDialog(dlg.container, optionalWidth || 340, 100, true, false);
4129		dlg.init();
4130	};
4131
4132	/**
4133	 * Translates this point by the given vector.
4134	 *
4135	 * @param {number} dx X-coordinate of the translation.
4136	 * @param {number} dy Y-coordinate of the translation.
4137	 */
4138	EditorUi.prototype.confirm = function(msg, okFn, cancelFn, okLabel, cancelLabel, closable)
4139	{
4140		var resume = (this.spinner != null && this.spinner.pause != null) ? this.spinner.pause() : function() {};
4141		var height = Math.min(200, Math.ceil(msg.length / 50) * 28);
4142
4143		var dlg = new ConfirmDialog(this, msg, function()
4144		{
4145			resume();
4146
4147			if (okFn != null)
4148			{
4149				okFn();
4150			}
4151		}, function()
4152		{
4153			resume();
4154
4155			if (cancelFn != null)
4156			{
4157				cancelFn();
4158			}
4159		}, okLabel, cancelLabel, null, null, null, null, height);
4160
4161		this.showDialog(dlg.container, 340, 46 + height, true, closable);
4162		dlg.init();
4163	};
4164
4165	/**
4166	 * Creates a popup banner.
4167	 */
4168	EditorUi.prototype.showBanner = function(id, text, onclick, doNotShowAgainOnClose)
4169	{
4170		var result = false;
4171
4172		if (!this.bannerShowing && !this['hideBanner' + id] &&
4173			(!isLocalStorage || mxSettings.settings == null ||
4174			mxSettings.settings['close' + id] == null))
4175		{
4176			var banner = document.createElement('div');
4177			banner.style.cssText = 'position:absolute;bottom:10px;left:50%;max-width:90%;padding:18px 34px 12px 20px;' +
4178				'font-size:16px;font-weight:bold;white-space:nowrap;cursor:pointer;z-index:' + mxPopupMenu.prototype.zIndex + ';';
4179			mxUtils.setPrefixedStyle(banner.style, 'box-shadow', '1px 1px 2px 0px #ddd');
4180			mxUtils.setPrefixedStyle(banner.style, 'transform', 'translate(-50%,120%)');
4181			mxUtils.setPrefixedStyle(banner.style, 'transition', 'all 1s ease');
4182			banner.className = 'geBtn gePrimaryBtn';
4183
4184			var logo = document.createElement('img');
4185			logo.setAttribute('src', IMAGE_PATH + '/logo.png');
4186			logo.setAttribute('border', '0');
4187			logo.setAttribute('align', 'absmiddle');
4188			logo.style.cssText = 'margin-top:-4px;margin-left:8px;margin-right:12px;width:26px;height:26px;';
4189			banner.appendChild(logo);
4190
4191			var img = document.createElement('img');
4192			img.setAttribute('src', Dialog.prototype.closeImage);
4193			img.setAttribute('title', mxResources.get((doNotShowAgainOnClose) ? 'doNotShowAgain' : 'close'));
4194			img.setAttribute('border', '0');
4195			img.style.cssText = 'position:absolute;right:10px;top:12px;filter:invert(1);padding:6px;margin:-6px;cursor:default;';
4196			banner.appendChild(img);
4197
4198			mxUtils.write(banner, text);
4199			document.body.appendChild(banner);
4200			this.bannerShowing = true;
4201
4202			var div = document.createElement('div');
4203			div.style.cssText = 'font-size:11px;text-align:center;font-weight:normal;';
4204			var chk = document.createElement('input');
4205			chk.setAttribute('type', 'checkbox');
4206			chk.setAttribute('id', 'geDoNotShowAgainCheckbox');
4207			chk.style.marginRight = '6px';
4208
4209			if (!doNotShowAgainOnClose)
4210			{
4211				div.appendChild(chk);
4212
4213				var label = document.createElement('label');
4214				label.setAttribute('for', 'geDoNotShowAgainCheckbox');
4215				mxUtils.write(label, mxResources.get('doNotShowAgain'));
4216				div.appendChild(label);
4217				banner.style.paddingBottom = '30px';
4218				banner.appendChild(div);
4219			}
4220
4221			var onclose = mxUtils.bind(this, function()
4222			{
4223				if (banner.parentNode != null)
4224				{
4225					banner.parentNode.removeChild(banner);
4226					this.bannerShowing = false;
4227
4228					if (chk.checked || doNotShowAgainOnClose)
4229					{
4230						this['hideBanner' + id] = true;
4231
4232						if (isLocalStorage && mxSettings.settings != null)
4233						{
4234							mxSettings.settings['close' + id] = Date.now();
4235							mxSettings.save();
4236						}
4237					}
4238				}
4239			});
4240
4241			mxEvent.addListener(img, 'click', mxUtils.bind(this, function(e)
4242			{
4243				mxEvent.consume(e);
4244				onclose();
4245			}));
4246
4247			var hide = mxUtils.bind(this, function()
4248			{
4249				mxUtils.setPrefixedStyle(banner.style, 'transform', 'translate(-50%,120%)');
4250
4251				window.setTimeout(mxUtils.bind(this, function()
4252				{
4253					onclose();
4254				}), 1000);
4255			});
4256
4257			mxEvent.addListener(banner, 'click', mxUtils.bind(this, function(e)
4258			{
4259				var source = mxEvent.getSource(e);
4260
4261				if (source != chk && source != label)
4262				{
4263					if (onclick != null)
4264					{
4265						onclick();
4266					}
4267
4268					onclose();
4269					mxEvent.consume(e);
4270				}
4271				else
4272				{
4273					hide();
4274				}
4275			}));
4276
4277			window.setTimeout(mxUtils.bind(this, function()
4278			{
4279				mxUtils.setPrefixedStyle(banner.style, 'transform', 'translate(-50%,0%)');
4280			}), 500);
4281
4282			window.setTimeout(hide, 30000);
4283			result = true;
4284		}
4285
4286		return result;
4287	};
4288
4289	/**
4290	 * Translates this point by the given vector.
4291	 *
4292	 * @param {number} dx X-coordinate of the translation.
4293	 * @param {number} dy Y-coordinate of the translation.
4294	 */
4295	EditorUi.prototype.setCurrentFile = function(file)
4296	{
4297		if (file != null)
4298		{
4299			file.opened = new Date();
4300		}
4301
4302		this.currentFile = file;
4303	};
4304
4305	/**
4306	 * Translates this point by the given vector.
4307	 *
4308	 * @param {number} dx X-coordinate of the translation.
4309	 * @param {number} dy Y-coordinate of the translation.
4310	 */
4311	EditorUi.prototype.getCurrentFile = function()
4312	{
4313		return this.currentFile;
4314	};
4315
4316	/**
4317	 * Handling for canvas export.
4318	 */
4319	EditorUi.prototype.isExportToCanvas = function()
4320	{
4321		return this.editor.isExportToCanvas();
4322	};
4323
4324	/**
4325	 *
4326	 */
4327	EditorUi.prototype.createImageDataUri = function(canvas, xml, format, dpi)
4328	{
4329		var data = canvas.toDataURL('image/' + format);
4330
4331		// Checks for valid output
4332		if (data != null && data.length > 6)
4333		{
4334			if (xml != null)
4335			{
4336				data = Editor.writeGraphModelToPng(data, 'tEXt', 'mxfile', encodeURIComponent(xml));
4337			}
4338
4339			if (dpi > 0)
4340			{
4341				data = Editor.writeGraphModelToPng(data, 'pHYs', 'dpi', dpi);
4342			}
4343		}
4344		else
4345		{
4346			throw {message: mxResources.get('unknownError')};
4347		}
4348
4349		return data;
4350	};
4351
4352	/**
4353	 *
4354	 */
4355	EditorUi.prototype.saveCanvas = function(canvas, xml, format, ignorePageName, dpi)
4356	{
4357		var ext = ((format == 'jpeg') ? 'jpg' : format);
4358		var filename = this.getBaseFilename(ignorePageName) +
4359			((xml != null) ? '.drawio' : '') + '.' + ext;
4360   	    var data = this.createImageDataUri(canvas, xml, format, dpi);
4361
4362   	    this.saveData(filename, ext, data.substring(data.lastIndexOf(',') + 1), 'image/' + format, true);
4363	};
4364
4365	/**
4366	 * Returns true if files should be saved using <saveLocalFile>.
4367	 */
4368	EditorUi.prototype.isLocalFileSave = function()
4369	{
4370		return ((urlParams['save'] != 'remote' && (mxClient.IS_IE ||
4371			(typeof window.Blob !== 'undefined' && typeof window.URL !== 'undefined')) &&
4372			document.documentMode != 9 && document.documentMode != 8 &&
4373			document.documentMode != 7) ||
4374			this.isOfflineApp() || mxClient.IS_IOS);
4375	};
4376
4377	/**
4378	 * Translates this point by the given vector.
4379	 *
4380	 * @param {number} dx X-coordinate of the translation.
4381	 * @param {number} dy Y-coordinate of the translation.
4382	 */
4383	EditorUi.prototype.showTextDialog = function(title, text)
4384	{
4385    	var dlg = new TextareaDialog(this, title, text, null, null, mxResources.get('close'));
4386    	dlg.textarea.style.width = '600px';
4387    	dlg.textarea.style.height = '380px';
4388		this.showDialog(dlg.container, 620, 460, true, true, null, null, null, null, true);
4389		dlg.init();
4390		document.execCommand('selectall', false, null);
4391	};
4392
4393	/**
4394	 * Translates this point by the given vector.
4395	 *
4396	 * @param {number} dx X-coordinate of the translation.
4397	 * @param {number} dy Y-coordinate of the translation.
4398	 */
4399	EditorUi.prototype.doSaveLocalFile = function(data, filename, mimeType, base64Encoded, format, defaultExtension)
4400	{
4401		// Appends .drawio extension for XML files with no extension
4402		// to avoid the browser to automatically append .xml instead
4403		if (mimeType == 'text/xml' &&
4404			!/(\.drawio)$/i.test(filename) &&
4405			!/(\.xml)$/i.test(filename) &&
4406			!/(\.svg)$/i.test(filename) &&
4407			!/(\.html)$/i.test(filename))
4408		{
4409			defaultExtension = (defaultExtension != null) ? defaultExtension : 'drawio';
4410			filename = filename + '.' + defaultExtension;
4411		}
4412
4413		// Newer versions of IE
4414		if (window.Blob && navigator.msSaveOrOpenBlob)
4415		{
4416			var blob = (base64Encoded) ?
4417					this.base64ToBlob(data, mimeType) :
4418					new Blob([data], {type: mimeType})
4419			navigator.msSaveOrOpenBlob(blob, filename);
4420		}
4421		// Older versions of IE (binary not supported)
4422		else if (mxClient.IS_IE)
4423		{
4424			var win = window.open('about:blank', '_blank');
4425
4426			if (win == null)
4427			{
4428				mxUtils.popup(data, true);
4429			}
4430			else
4431			{
4432				win.document.write(data);
4433				win.document.close();
4434				win.document.execCommand('SaveAs', true, filename);
4435				win.close();
4436			}
4437		}
4438		else if (mxClient.IS_IOS && this.isOffline())
4439		{
4440			// Workaround for "WebKitBlobResource error 1" in mobile Safari
4441			if (!navigator.standalone && mimeType != null && mimeType.substring(0, 6) == 'image/')
4442			{
4443				this.openInNewWindow(data, mimeType, base64Encoded);
4444			}
4445			else
4446			{
4447				this.showTextDialog(filename + ':', data);
4448			}
4449		}
4450		else
4451		{
4452			var a = document.createElement('a');
4453
4454			// Workaround for mxXmlRequest.simulate no longer working in PaleMoon
4455			// if this is used (ie PNG export broken after XML export in PaleMoon)
4456			// and for "WebKitBlobResource error 1" for all browsers on iOS.
4457			var useDownload = (navigator.userAgent == null ||
4458				navigator.userAgent.indexOf("PaleMoon/") < 0) &&
4459				typeof a.download !== 'undefined';
4460
4461			// Workaround for Chromium 65 cross-domain anchor download issue
4462			if (mxClient.IS_GC && navigator.userAgent != null)
4463			{
4464				var raw = navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./)
4465				var vers = raw ? parseInt(raw[2], 10) : false;
4466				useDownload = vers == 65 ? false : useDownload;
4467			}
4468
4469			if (useDownload || this.isOffline())
4470			{
4471				a.href = URL.createObjectURL((base64Encoded) ?
4472					this.base64ToBlob(data, mimeType) :
4473					new Blob([data], {type: mimeType}));
4474
4475				if (useDownload)
4476				{
4477					a.download = filename;
4478				}
4479				else
4480				{
4481					// Workaround for same window in Safari
4482					a.setAttribute('target', '_blank');
4483				}
4484
4485				document.body.appendChild(a);
4486
4487				try
4488				{
4489					window.setTimeout(function()
4490					{
4491						URL.revokeObjectURL(a.href);
4492					}, 20000);
4493
4494					a.click();
4495					a.parentNode.removeChild(a);
4496				}
4497				catch (e)
4498				{
4499					// ignore
4500				}
4501			}
4502			else
4503			{
4504				var req = this.createEchoRequest(data, filename, mimeType, base64Encoded, format);
4505
4506				req.simulate(document, '_blank');
4507			}
4508		}
4509	};
4510
4511	/**
4512	 * Translates this point by the given vector.
4513	 *
4514	 * @param {number} dx X-coordinate of the translation.
4515	 * @param {number} dy Y-coordinate of the translation.
4516	 */
4517	EditorUi.prototype.createEchoRequest = function(data, filename, mimeType, base64Encoded, format, base64Response)
4518	{
4519		var param = (typeof(pako) === 'undefined' || true) ? 'xml=' + encodeURIComponent(data) :
4520			'data=' + encodeURIComponent(Graph.compress(data));
4521
4522		return new mxXmlRequest(SAVE_URL, param +
4523			((mimeType != null) ? '&mime=' + mimeType : '') +
4524			((format != null) ? '&format=' + format : '') +
4525			((base64Response != null) ? '&base64=' + base64Response : '') +
4526			((filename != null) ? '&filename=' + encodeURIComponent(filename) : '') +
4527			((base64Encoded) ? '&binary=1' : ''));
4528	};
4529
4530	/**
4531	 * Translates this point by the given vector.
4532	 *
4533	 * @param {number} dx X-coordinate of the translation.
4534	 * @param {number} dy Y-coordinate of the translation.
4535	 */
4536	EditorUi.prototype.base64ToBlob = function(base64Data, contentType)
4537	{
4538	    contentType = contentType || '';
4539	    var sliceSize = 1024;
4540	    var byteCharacters = atob(base64Data);
4541	    var bytesLength = byteCharacters.length;
4542	    var slicesCount = Math.ceil(bytesLength / sliceSize);
4543	    var byteArrays = new Array(slicesCount);
4544
4545	    for (var sliceIndex = 0; sliceIndex < slicesCount; ++sliceIndex)
4546	    {
4547	        var begin = sliceIndex * sliceSize;
4548	        var end = Math.min(begin + sliceSize, bytesLength);
4549
4550	        var bytes = new Array(end - begin);
4551
4552	        for (var offset = begin, i = 0 ; offset < end; ++i, ++offset)
4553	        {
4554	            bytes[i] = byteCharacters[offset].charCodeAt(0);
4555	        }
4556
4557	        byteArrays[sliceIndex] = new Uint8Array(bytes);
4558	    }
4559
4560	    return new Blob(byteArrays, {type: contentType});
4561	};
4562
4563	/**
4564	 * Translates this point by the given vector.
4565	 *
4566	 * @param {number} dx X-coordinate of the translation.
4567	 * @param {number} dy Y-coordinate of the translation.
4568	 */
4569	EditorUi.prototype.saveLocalFile = function(data, filename, mimeType, base64Encoded, format, allowBrowser, allowTab, defaultExtension)
4570	{
4571		allowBrowser = (allowBrowser != null) ? allowBrowser : false;
4572		allowTab = (allowTab != null) ? allowTab : (format != 'vsdx') && (!mxClient.IS_IOS || !navigator.standalone);
4573		var count = this.getServiceCount(allowBrowser);
4574
4575		if (isLocalStorage)
4576		{
4577			count++;
4578		}
4579
4580		var rowLimit = (count <= 4) ? 2 : (count > 6 ? 4 : 3);
4581
4582		var dlg = new CreateDialog(this, filename, mxUtils.bind(this, function(newTitle, mode)
4583		{
4584			try
4585			{
4586				// Opens a new window
4587				if (mode == '_blank')
4588				{
4589					if (mimeType != null && mimeType.substring(0, 6) == 'image/')
4590					{
4591						this.openInNewWindow(data, mimeType, base64Encoded);
4592					}
4593					else if (mimeType != null && mimeType.substring(0, 9) == 'text/html')
4594					{
4595						var dlg = new EmbedDialog(this, data);
4596						this.showDialog(dlg.container, 450, 240, true, true);
4597						dlg.init();
4598					}
4599					else
4600					{
4601						var win = window.open('about:blank');
4602
4603						if (win == null)
4604						{
4605							mxUtils.popup(data, true);
4606						}
4607						else
4608						{
4609							win.document.write('<pre>' + mxUtils.htmlEntities(data, false) + '</pre>');
4610							win.document.close();
4611						}
4612					}
4613				}
4614				else if (mode == App.MODE_DEVICE || mode == 'download')
4615				{
4616					this.doSaveLocalFile(data, newTitle, mimeType, base64Encoded, null, defaultExtension);
4617				}
4618				else if (newTitle != null && newTitle.length > 0)
4619				{
4620					this.pickFolder(mode, mxUtils.bind(this, function(folderId)
4621					{
4622						try
4623						{
4624							this.exportFile(data, newTitle, mimeType, base64Encoded, mode, folderId);
4625						}
4626						catch (e)
4627						{
4628							this.handleError(e);
4629						}
4630					}));
4631				}
4632			}
4633			catch (e)
4634			{
4635				this.handleError(e);
4636			}
4637		}), mxUtils.bind(this, function()
4638		{
4639			this.hideDialog();
4640		}), mxResources.get('saveAs'), mxResources.get('download'), false, allowBrowser, allowTab,
4641			null, count > 1, rowLimit, data, mimeType, base64Encoded);
4642		var height = (this.isServices(count)) ? ((count > rowLimit) ? 390 : 270) : 160;
4643		this.showDialog(dlg.container, 420, height, true, true);
4644		dlg.init();
4645	};
4646
4647	/**
4648	 *
4649	 */
4650	EditorUi.prototype.openInNewWindow = function(data, mimeType, base64Encoded)
4651	{
4652		var win = window.open('about:blank');
4653
4654		if (win == null || win.document == null)
4655		{
4656			mxUtils.popup(data, true);
4657		}
4658		else
4659		{
4660			if (mimeType == 'image/svg+xml' && !mxClient.IS_SVG)
4661			{
4662				// KNOWN: Output is scaled in Chrome on macOS
4663				win.document.write('<html><pre>' + mxUtils.htmlEntities(data, false) + '</pre></html>');
4664				win.document.close();
4665			}
4666			else
4667			{
4668				if (mimeType == 'image/svg+xml')
4669				{
4670					win.document.write('<html>'+ data + '</html>');
4671				}
4672				else
4673				{
4674					var temp = (base64Encoded) ? data : btoa(unescape(encodeURIComponent(data)));
4675
4676					win.document.write('<html><img style="max-width:100%;" src="data:' +
4677						mimeType  + ';base64,' + temp + '"/></html>');
4678				}
4679
4680				win.document.close();
4681			}
4682		}
4683	};
4684
4685	var editoUiAddChromelessToolbarItems = EditorUi.prototype.addChromelessToolbarItems;
4686
4687	/**
4688	 * Image export in viewer is only allowed for same domain or hosted environments but
4689	 * but disabled to avoid cross domain image export in canvas which isn't allowed.
4690	 */
4691	EditorUi.prototype.isChromelessImageExportEnabled = function()
4692	{
4693		return this.getServiceName() != 'draw.io' ||
4694			/.*\.draw\.io$/.test(window.location.hostname) ||
4695			/.*\.diagrams\.net$/.test(window.location.hostname);
4696	};
4697
4698	/**
4699	 * Creates a temporary graph instance for rendering off-screen content.
4700	 */
4701	EditorUi.prototype.addChromelessToolbarItems = function(addButton)
4702	{
4703		if (urlParams['tags'] != null)
4704		{
4705			this.tagsComponent = null;
4706			this.tagsDialog = null;
4707
4708			var tagsButton = addButton(mxUtils.bind(this, function(evt)
4709			{
4710				if (this.tagsComponent == null)
4711				{
4712					this.tagsComponent = this.editor.graph.createTagsDialog(mxUtils.bind(this, function()
4713					{
4714						return this.tagsDialog != null;
4715					}), true);
4716
4717					this.tagsComponent.div.getElementsByTagName('div')[0].style.position = '';
4718					mxUtils.setPrefixedStyle(this.tagsComponent.div.style, 'borderRadius', '5px');
4719					this.tagsComponent.div.className = 'geScrollable';
4720					this.tagsComponent.div.style.maxHeight = '160px';
4721					this.tagsComponent.div.style.maxWidth = '120px';
4722					this.tagsComponent.div.style.padding = '4px';
4723					this.tagsComponent.div.style.overflow = 'auto';
4724					this.tagsComponent.div.style.height = 'auto';
4725					this.tagsComponent.div.style.position = 'fixed';
4726					this.tagsComponent.div.style.fontFamily = Editor.defaultHtmlFont;
4727
4728					if (!mxClient.IS_IE && !mxClient.IS_IE11)
4729					{
4730						this.tagsComponent.div.style.backgroundColor = '#000000';
4731						this.tagsComponent.div.style.color = '#ffffff';
4732						mxUtils.setOpacity(this.tagsComponent.div, 80);
4733					}
4734					else
4735					{
4736						this.tagsComponent.div.style.backgroundColor = '#ffffff';
4737						this.tagsComponent.div.style.border = '2px solid black';
4738						this.tagsComponent.div.style.color = '#000000';
4739					}
4740				}
4741
4742				if (this.tagsDialog != null)
4743				{
4744					this.tagsDialog.parentNode.removeChild(this.tagsDialog);
4745					this.tagsDialog = null;
4746				}
4747				else
4748				{
4749					this.tagsDialog = this.tagsComponent.div;
4750
4751					mxEvent.addListener(this.tagsDialog, 'mouseleave', mxUtils.bind(this, function()
4752					{
4753						if (this.tagsDialog != null)
4754						{
4755							this.tagsDialog.parentNode.removeChild(this.tagsDialog);
4756							this.tagsDialog = null;
4757						}
4758					}));
4759
4760					var r = tagsButton.getBoundingClientRect();
4761					this.tagsDialog.style.left = r.left + 'px';
4762					this.tagsDialog.style.bottom = parseInt(this.chromelessToolbar.style.bottom) +
4763						this.chromelessToolbar.offsetHeight + 4 + 'px';
4764
4765					// Puts the dialog on top of the container z-index
4766					var style = mxUtils.getCurrentStyle(this.editor.graph.container);
4767					this.tagsDialog.style.zIndex = style.zIndex;
4768					document.body.appendChild(this.tagsDialog);
4769
4770					this.tagsComponent.refresh();
4771					this.editor.fireEvent(new mxEventObject('tagsDialogShown'));
4772				}
4773
4774				mxEvent.consume(evt);
4775			}), Editor.tagsImage, mxResources.get('tags'));
4776
4777			// Shows/hides tags button depending on content
4778			var model = this.editor.graph.getModel();
4779
4780			model.addListener(mxEvent.CHANGE, mxUtils.bind(this, function()
4781			{
4782				var tags = this.editor.graph.getAllTags();
4783				tagsButton.style.display = (tags.length > 0) ? '' : 'none';
4784			}));
4785		}
4786
4787		editoUiAddChromelessToolbarItems.apply(this, arguments);
4788
4789		this.editor.addListener('tagsDialogShown', mxUtils.bind(this, function()
4790		{
4791			if (this.layersDialog != null)
4792			{
4793				this.layersDialog.parentNode.removeChild(this.layersDialog);
4794				this.layersDialog = null;
4795			}
4796		}));
4797
4798		this.editor.addListener('layersDialogShown', mxUtils.bind(this, function()
4799		{
4800			if (this.tagsDialog != null)
4801			{
4802				this.tagsDialog.parentNode.removeChild(this.tagsDialog);
4803				this.tagsDialog = null;
4804			}
4805		}));
4806
4807		this.editor.addListener('pageSelected', mxUtils.bind(this, function()
4808		{
4809			if (this.tagsDialog != null)
4810			{
4811				this.tagsDialog.parentNode.removeChild(this.tagsDialog);
4812				this.tagsDialog = null;
4813			}
4814
4815			if (this.layersDialog != null)
4816			{
4817				this.layersDialog.parentNode.removeChild(this.layersDialog);
4818				this.layersDialog = null;
4819			}
4820		}));
4821
4822		mxEvent.addListener(this.editor.graph.container, 'click', mxUtils.bind(this, function()
4823		{
4824			if (this.tagsDialog != null)
4825			{
4826				this.tagsDialog.parentNode.removeChild(this.tagsDialog);
4827				this.tagsDialog = null;
4828			}
4829
4830			if (this.layersDialog != null)
4831			{
4832				this.layersDialog.parentNode.removeChild(this.layersDialog);
4833				this.layersDialog = null;
4834			}
4835		}));
4836
4837		if (this.isExportToCanvas() && this.isChromelessImageExportEnabled())
4838		{
4839			this.exportDialog = null;
4840
4841			var exportButton = addButton(mxUtils.bind(this, function(evt)
4842			{
4843				var clickHandler = mxUtils.bind(this, function()
4844				{
4845					mxEvent.removeListener(this.editor.graph.container, 'click', clickHandler);
4846
4847					if (this.exportDialog != null)
4848					{
4849						this.exportDialog.parentNode.removeChild(this.exportDialog);
4850						this.exportDialog = null;
4851					}
4852				});
4853
4854				if (this.exportDialog != null)
4855				{
4856					clickHandler.apply(this);
4857				}
4858				else
4859				{
4860					this.exportDialog = document.createElement('div');
4861					var r = exportButton.getBoundingClientRect();
4862
4863					mxUtils.setPrefixedStyle(this.exportDialog.style, 'borderRadius', '5px');
4864					this.exportDialog.style.position = 'fixed';
4865					this.exportDialog.style.textAlign = 'center';
4866					this.exportDialog.style.fontFamily = Editor.defaultHtmlFont;
4867					this.exportDialog.style.backgroundColor = '#000000';
4868					this.exportDialog.style.width = '50px';
4869					this.exportDialog.style.height = '50px';
4870					this.exportDialog.style.padding = '4px 2px 4px 2px';
4871					this.exportDialog.style.color = '#ffffff';
4872					mxUtils.setOpacity(this.exportDialog, 70);
4873					this.exportDialog.style.left = r.left + 'px';
4874					this.exportDialog.style.bottom = parseInt(this.chromelessToolbar.style.bottom) +
4875						this.chromelessToolbar.offsetHeight + 4 + 'px';
4876
4877					// Puts the dialog on top of the container z-index
4878					var style = mxUtils.getCurrentStyle(this.editor.graph.container);
4879					this.exportDialog.style.zIndex = style.zIndex;
4880
4881					var spinner = new Spinner({
4882						lines: 8, // The number of lines to draw
4883						length: 6, // The length of each line
4884						width: 5, // The line thickness
4885						radius: 6, // The radius of the inner circle
4886						rotate: 0, // The rotation offset
4887						color: '#fff', // #rgb or #rrggbb
4888						speed: 1.5, // Rounds per second
4889						trail: 60, // Afterglow percentage
4890						shadow: false, // Whether to render a shadow
4891						hwaccel: false, // Whether to use hardware acceleration
4892						top: '28px',
4893						zIndex: 2e9 // The z-index (defaults to 2000000000)
4894					});
4895					spinner.spin(this.exportDialog);
4896
4897				   	this.editor.exportToCanvas(mxUtils.bind(this, function(canvas)
4898				   	{
4899				   		spinner.stop();
4900
4901						this.exportDialog.style.width = 'auto';
4902						this.exportDialog.style.height = 'auto';
4903						this.exportDialog.style.padding = '10px';
4904
4905			   	   	    var data = this.createImageDataUri(canvas, null, 'png');
4906			   	   	    var img = document.createElement('img');
4907
4908			   	   	    img.style.maxWidth = '140px';
4909			   	   	    img.style.maxHeight = '140px';
4910			   	   	    img.style.cursor = 'pointer';
4911			   	   	    img.style.backgroundColor = 'white';
4912
4913			   	   	    img.setAttribute('title', mxResources.get('openInNewWindow'));
4914			   	   	    img.setAttribute('border', '0');
4915			   	   	    img.setAttribute('src', data);
4916
4917			   	   	    this.exportDialog.appendChild(img);
4918
4919						mxEvent.addListener(img, 'click', mxUtils.bind(this, function()
4920						{
4921							this.openInNewWindow(data.substring(data.indexOf(',') + 1), 'image/png', true);
4922							clickHandler.apply(this, arguments);
4923						}));
4924				   	}), null, this.thumbImageCache, null, mxUtils.bind(this, function(e)
4925				   	{
4926				   		this.spinner.stop();
4927				   		this.handleError(e);
4928				   	}), null, null, null, null, null, null, null, Editor.defaultBorder);
4929
4930					mxEvent.addListener(this.editor.graph.container, 'click', clickHandler);
4931				   	document.body.appendChild(this.exportDialog);
4932				}
4933
4934				mxEvent.consume(evt);
4935			}), Editor.cameraImage, mxResources.get('export'));
4936		}
4937	};
4938
4939	/**
4940	 * Translates this point by the given vector.
4941	 *
4942	 * @param {number} dx X-coordinate of the translation.
4943	 * @param {number} dy Y-coordinate of the translation.
4944	 */
4945	EditorUi.prototype.saveData = function(filename, format, data, mime, base64Encoded)
4946	{
4947		if (this.isLocalFileSave())
4948		{
4949			this.saveLocalFile(data, filename, mime, base64Encoded, format);
4950		}
4951		else
4952		{
4953			this.saveRequest(filename, format, mxUtils.bind(this, function(newTitle, base64)
4954			{
4955				return this.createEchoRequest(data, newTitle, mime, base64Encoded, format, base64);
4956			}), data, base64Encoded, mime);
4957		}
4958	};
4959
4960	/**
4961	 * Translates this point by the given vector.
4962	 *
4963	 * Last 3 argument are optional and must only be used if the data can be stored as is on the client
4964	 * side without requiring a server roundtrip.
4965	 *
4966	 * @param {number} dx X-coordinate of the translation.
4967	 * @param {number} dy Y-coordinate of the translation.
4968	 */
4969	EditorUi.prototype.saveRequest = function(filename, format, fn, data, base64Encoded, mimeType, allowTab)
4970	{
4971		allowTab = (allowTab != null) ? allowTab : !mxClient.IS_IOS || !navigator.standalone;
4972		var count = this.getServiceCount(false);
4973
4974		if (isLocalStorage)
4975		{
4976			count++;
4977		}
4978
4979		var rowLimit = (count <= 4) ? 2 : (count > 6 ? 4 : 3);
4980
4981		var dlg = new CreateDialog(this, filename, mxUtils.bind(this, function(newTitle, mode)
4982		{
4983			if (mode == '_blank' || newTitle != null && newTitle.length > 0)
4984			{
4985				var base64 = (mode == App.MODE_DEVICE || mode == 'download' || mode == null || mode == '_blank') ? '0' : '1';
4986				var xhr = fn((mode == '_blank') ? null : newTitle, base64);
4987
4988				if (xhr != null)
4989				{
4990					if (mode == App.MODE_DEVICE || mode == 'download' || mode == '_blank')
4991					{
4992						xhr.simulate(document, '_blank');
4993					}
4994					else
4995					{
4996						this.pickFolder(mode, mxUtils.bind(this, function(folderId)
4997						{
4998							mimeType = (mimeType != null) ? mimeType : ((format == 'pdf') ?
4999								'application/pdf' : 'image/' + format);
5000
5001							// Workaround for no roundtrip required if data is available on client-side
5002							// TODO: Refactor the saveData/saveRequest call chain for local data
5003							if (data != null)
5004							{
5005								try
5006								{
5007									this.exportFile(data, newTitle, mimeType, true, mode, folderId);
5008								}
5009								catch (e)
5010								{
5011									this.handleError(e);
5012								}
5013							}
5014							else if (this.spinner.spin(document.body, mxResources.get('saving')))
5015							{
5016								// LATER: Catch possible mixed content error
5017								// see http://stackoverflow.com/questions/30646417/catching-mixed-content-error
5018								xhr.send(mxUtils.bind(this, function()
5019								{
5020									this.spinner.stop();
5021
5022									if (xhr.getStatus() >= 200 && xhr.getStatus() <= 299)
5023									{
5024										try
5025										{
5026											this.exportFile(xhr.getText(), newTitle, mimeType, true, mode, folderId);
5027										}
5028										catch (e)
5029										{
5030											this.handleError(e);
5031										}
5032									}
5033									else
5034									{
5035										this.handleError({message: mxResources.get('errorSavingFile')});
5036									}
5037								}), function(resp)
5038								{
5039									this.spinner.stop();
5040									this.handleError(resp);
5041								});
5042							}
5043						}));
5044					}
5045				}
5046			}
5047		}), mxUtils.bind(this, function()
5048		{
5049			this.hideDialog();
5050		}), mxResources.get('saveAs'), mxResources.get('download'), false, false, allowTab,
5051			null, count > 1, rowLimit, data, mimeType, base64Encoded);
5052
5053		var height = (this.isServices(count)) ? ((count > 4) ? 390 : 270) : 160;
5054		this.showDialog(dlg.container, 420, height, true, true);
5055		dlg.init();
5056	};
5057
5058	/**
5059	 * Returns whether or not any services should be shown in dialogs
5060	 */
5061	EditorUi.prototype.isServices = function(count)
5062	{
5063		var noServices = 1; //(mxClient.IS_IOS) ? 0 : 1;
5064		return count != noServices;
5065	};
5066
5067	/**
5068	 *
5069	 */
5070	EditorUi.prototype.getEditBlankXml = function()
5071	{
5072		return this.getFileData(true);
5073	};
5074
5075	/**
5076	 * Hook for subclassers.
5077	 */
5078	EditorUi.prototype.exportFile = function(data, filename, mimeType, base64Encoded, mode, folderId)
5079	{
5080		// do nothing
5081	};
5082
5083	/**
5084	 * Hook for subclassers.
5085	 */
5086	EditorUi.prototype.pickFolder = function(mode, fn, enabled)
5087	{
5088		fn(null);
5089	};
5090
5091	/**
5092	 *
5093	 */
5094	EditorUi.prototype.exportSvg = function(scale, transparentBackground, ignoreSelection, addShadow,
5095		editable, embedImages, border, noCrop, currentPage, linkTarget, keepTheme, exportType,
5096		embedFonts, saveFn)
5097	{
5098		if (this.spinner.spin(document.body, mxResources.get('export')))
5099		{
5100			try
5101			{
5102				var selectionEmpty = this.editor.graph.isSelectionEmpty();
5103				ignoreSelection = (ignoreSelection != null) ? ignoreSelection : selectionEmpty;
5104				var bg = (transparentBackground) ? null : this.editor.graph.background;
5105
5106				if (bg == mxConstants.NONE)
5107				{
5108					bg = null;
5109				}
5110
5111				// Handles special case where background is null but transparent is false
5112				if (bg == null && transparentBackground == false)
5113				{
5114					bg = (keepTheme) ? this.editor.graph.defaultPageBackgroundColor : '#ffffff';
5115				}
5116
5117				// Sets or disables alternate text for foreignObjects. Disabling is needed
5118				// because PhantomJS seems to ignore switch statements and paint all text.
5119				var svgRoot = this.editor.graph.getSvg(bg, scale, border, noCrop,
5120					null, ignoreSelection, null, null, (linkTarget == 'blank') ? '_blank' :
5121					((linkTarget == 'self') ? '_top' : null), null, true, keepTheme,
5122					exportType);
5123
5124				if (addShadow)
5125				{
5126					this.editor.graph.addSvgShadow(svgRoot);
5127				}
5128
5129				var filename = this.getBaseFilename() + ((editable) ? '.drawio' : '') + '.svg';
5130
5131				saveFn = (saveFn != null) ? saveFn : mxUtils.bind(this, function(svg)
5132				{
5133		    		if (this.isLocalFileSave() || svg.length <= MAX_REQUEST_SIZE)
5134		    		{
5135		    			this.saveData(filename, 'svg', svg, 'image/svg+xml');
5136		    		}
5137		    		else
5138		    		{
5139		    			this.handleError({message: mxResources.get('drawingTooLarge')}, mxResources.get('error'), mxUtils.bind(this, function()
5140		    			{
5141		    				mxUtils.popup(svg);
5142		    			}));
5143		    		}
5144				});
5145
5146				var doSave = mxUtils.bind(this, function(svgRoot)
5147				{
5148					this.spinner.stop();
5149
5150					if (editable)
5151					{
5152						svgRoot.setAttribute('content', this.getFileData(true, null, null, null, ignoreSelection,
5153							currentPage, null, null, null, false));
5154					}
5155
5156					saveFn(Graph.xmlDeclaration + '\n' + ((editable) ? Graph.svgFileComment + '\n' : '') +
5157						Graph.svgDoctype + '\n' + mxUtils.getXml(svgRoot));
5158				});
5159
5160				// Adds CSS
5161				if (this.editor.graph.mathEnabled)
5162				{
5163					this.editor.addMathCss(svgRoot);
5164				}
5165
5166				var done = mxUtils.bind(this, function(svgRoot)
5167				{
5168					if (embedImages)
5169					{
5170						// Caches images
5171						if (this.thumbImageCache == null)
5172						{
5173							this.thumbImageCache = new Object();
5174						}
5175
5176						this.editor.convertImages(svgRoot, doSave, this.thumbImageCache);
5177					}
5178					else
5179					{
5180						doSave(svgRoot);
5181					}
5182				});
5183
5184				if (embedFonts)
5185				{
5186					this.embedFonts(svgRoot, done);
5187				}
5188				else
5189				{
5190					this.editor.addFontCss(svgRoot);
5191					done(svgRoot);
5192				}
5193			}
5194			catch (e)
5195			{
5196				this.handleError(e);
5197			}
5198		}
5199	};
5200
5201	/**
5202	 *
5203	 */
5204	EditorUi.prototype.addRadiobox = function(div, radioGroupName, label, checked, disabled, disableNewline, visible)
5205	{
5206		return this.addCheckbox(div, label, checked, disabled, disableNewline, visible, true, radioGroupName);
5207	};
5208
5209	/**
5210	 *
5211	 */
5212	EditorUi.prototype.addCheckbox = function(div, label, checked, disabled, disableNewline, visible, asRadio, radioGroupName)
5213	{
5214		visible = (visible != null) ? visible : true;
5215
5216		var cb = document.createElement('input');
5217		cb.style.marginRight = '8px';
5218		cb.style.marginTop = '16px';
5219		cb.setAttribute('type', asRadio? 'radio' : 'checkbox');
5220		var id = 'geCheckbox-' + Editor.guid();
5221		cb.id = id;
5222
5223		if (radioGroupName != null)
5224		{
5225			cb.setAttribute('name', radioGroupName);
5226		}
5227
5228		if (checked)
5229		{
5230			cb.setAttribute('checked', 'checked');
5231			cb.defaultChecked = true;
5232		}
5233
5234		if (disabled)
5235		{
5236			cb.setAttribute('disabled', 'disabled');
5237		}
5238
5239		if (visible)
5240		{
5241			div.appendChild(cb);
5242
5243			var lbl = document.createElement('label');
5244			mxUtils.write(lbl, label);
5245			lbl.setAttribute('for', id);
5246			div.appendChild(lbl);
5247
5248			if (!disableNewline)
5249			{
5250				mxUtils.br(div);
5251			}
5252		}
5253
5254		return cb;
5255	};
5256
5257	/**
5258	 *
5259	 */
5260	EditorUi.prototype.addEditButton = function(div, lightbox)
5261	{
5262		var edit = this.addCheckbox(div, mxResources.get('edit') + ':', true, null, true);
5263		edit.style.marginLeft = '24px';
5264
5265		var file = this.getCurrentFile();
5266		var editUrl = '';
5267
5268		if (file != null && file.getMode() != App.MODE_DEVICE && file.getMode() != App.MODE_BROWSER)
5269		{
5270			editUrl = window.location.href;
5271		}
5272
5273		var editSelect = document.createElement('select');
5274		editSelect.style.width = '120px';
5275		editSelect.style.marginLeft = '8px';
5276		editSelect.style.marginRight = '10px';
5277		editSelect.className = 'geBtn';
5278
5279		var blankOption = document.createElement('option');
5280		blankOption.setAttribute('value', 'blank');
5281		mxUtils.write(blankOption, mxResources.get('makeCopy'));
5282		editSelect.appendChild(blankOption);
5283
5284		var customOption = document.createElement('option');
5285		customOption.setAttribute('value', 'custom');
5286		mxUtils.write(customOption, mxResources.get('custom') + '...');
5287		editSelect.appendChild(customOption);
5288
5289		div.appendChild(editSelect);
5290
5291		mxEvent.addListener(editSelect, 'change', mxUtils.bind(this, function()
5292		{
5293			if (editSelect.value == 'custom')
5294			{
5295				var dlg2 = new FilenameDialog(this, editUrl, mxResources.get('ok'), function(value)
5296				{
5297					if (value != null)
5298					{
5299						editUrl = value;
5300					}
5301					else
5302					{
5303						editSelect.value = 'blank';
5304					}
5305				}, mxResources.get('url'), null, null, null, null, function()
5306				{
5307					editSelect.value = 'blank';
5308				});
5309				this.showDialog(dlg2.container, 300, 80, true, false);
5310				dlg2.init();
5311			}
5312		}));
5313
5314		mxEvent.addListener(edit, 'change', mxUtils.bind(this, function()
5315		{
5316			if (edit.checked && (lightbox == null || lightbox.checked))
5317			{
5318				editSelect.removeAttribute('disabled');
5319			}
5320			else
5321			{
5322				editSelect.setAttribute('disabled', 'disabled');
5323			}
5324		}));
5325
5326		mxUtils.br(div);
5327
5328		return {
5329			getLink: function()
5330			{
5331				return (edit.checked) ? ((editSelect.value === 'blank') ? '_blank' : editUrl) : null;
5332			},
5333			getEditInput: function()
5334			{
5335				return edit;
5336			},
5337			getEditSelect: function()
5338			{
5339				return editSelect;
5340			}
5341		};
5342	}
5343
5344	/**
5345	 *
5346	 */
5347	EditorUi.prototype.addLinkSection = function(div, showFrameOption)
5348	{
5349		mxUtils.write(div, mxResources.get('links') + ':');
5350
5351		var linkSelect = document.createElement('select');
5352		linkSelect.style.width = '100px';
5353		linkSelect.style.marginLeft = '8px';
5354		linkSelect.style.marginRight = '10px';
5355		linkSelect.className = 'geBtn';
5356
5357		var autoOption = document.createElement('option');
5358		autoOption.setAttribute('value', 'auto');
5359		mxUtils.write(autoOption, mxResources.get('automatic'));
5360		linkSelect.appendChild(autoOption);
5361
5362		var blankOption = document.createElement('option');
5363		blankOption.setAttribute('value', 'blank');
5364		mxUtils.write(blankOption, mxResources.get('openInNewWindow'));
5365		linkSelect.appendChild(blankOption);
5366
5367		var selfOption = document.createElement('option');
5368		selfOption.setAttribute('value', 'self');
5369		mxUtils.write(selfOption, mxResources.get('openInThisWindow'));
5370		linkSelect.appendChild(selfOption);
5371
5372		if (showFrameOption)
5373		{
5374			var frameOption = document.createElement('option');
5375			frameOption.setAttribute('value', 'frame');
5376			mxUtils.write(frameOption, mxResources.get('openInThisWindow') +
5377				' (' + mxResources.get('iframe') + ')');
5378			linkSelect.appendChild(frameOption);
5379		}
5380
5381		div.appendChild(linkSelect);
5382
5383		mxUtils.write(div, mxResources.get('borderColor') + ':');
5384		var linkColor = '#0000ff';
5385		var linkButton = null;
5386
5387		function updateLinkColor()
5388		{
5389			linkButton.innerHTML = '<div style="width:100%;height:100%;box-sizing:border-box;' +
5390				((linkColor != null && linkColor != mxConstants.NONE) ?
5391				'border:1px solid black;background-color:' + linkColor :
5392				'background-position:center center;background-repeat:no-repeat;' +
5393				'background-image:url(\'' + Dialog.prototype.closeImage + '\')') + ';"></div>';
5394		};
5395
5396		linkButton = mxUtils.button('', mxUtils.bind(this, function(evt)
5397		{
5398			this.pickColor(linkColor || 'none', function(color)
5399			{
5400				linkColor = color;
5401				updateLinkColor();
5402			});
5403
5404			mxEvent.consume(evt);
5405		}));
5406
5407		updateLinkColor();
5408		linkButton.style.padding = (mxClient.IS_FF) ? '4px 2px 4px 2px' : '4px';
5409		linkButton.style.marginLeft = '4px';
5410		linkButton.style.height = '22px';
5411		linkButton.style.width = '22px';
5412		linkButton.style.position = 'relative';
5413		linkButton.style.top = (mxClient.IS_IE || mxClient.IS_IE11 || mxClient.IS_EDGE) ? '6px' : '1px';
5414		linkButton.className = 'geColorBtn';
5415		div.appendChild(linkButton);
5416		mxUtils.br(div);
5417
5418		return {
5419			getColor: function()
5420			{
5421				return linkColor;
5422			},
5423			getTarget: function()
5424			{
5425				return linkSelect.value;
5426			},
5427			focus: function()
5428			{
5429				linkSelect.focus();
5430			}
5431		};
5432	}
5433
5434	/**
5435	 *
5436	 */
5437	EditorUi.prototype.createUrlParameters = function(linkTarget, linkColor, allPages, lightbox, editLink, layers, params)
5438	{
5439		params = (params != null) ? params : [];
5440
5441		if (lightbox)
5442		{
5443			if (EditorUi.lightboxHost != 'https://viewer.diagrams.net' || urlParams['dev'] == '1')
5444			{
5445				params.push('lightbox=1');
5446			}
5447
5448			if (linkTarget != 'auto')
5449			{
5450				params.push('target=' + linkTarget);
5451			}
5452
5453			if (linkColor != null && linkColor != mxConstants.NONE)
5454			{
5455				params.push('highlight=' + ((linkColor.charAt(0) == '#') ?
5456					linkColor.substring(1) : linkColor));
5457			}
5458
5459			if (editLink != null && editLink.length > 0)
5460			{
5461				params.push('edit=' + encodeURIComponent(editLink));
5462			}
5463
5464			if (layers)
5465			{
5466				params.push('layers=1');
5467			}
5468
5469			if (this.editor.graph.foldingEnabled)
5470			{
5471				params.push('nav=1');
5472			}
5473		}
5474
5475		if (allPages && this.currentPage != null && this.pages != null &&
5476			this.currentPage != this.pages[0])
5477		{
5478			params.push('page-id=' + this.currentPage.getId());
5479		}
5480
5481		return params;
5482	};
5483
5484	/**
5485	 *
5486	 */
5487	EditorUi.prototype.createLink = function(linkTarget, linkColor, allPages, lightbox, editLink, layers, url, ignoreFile, params, useOpenParameter)
5488	{
5489		params = this.createUrlParameters(linkTarget, linkColor, allPages, lightbox, editLink, layers, params);
5490		var file = this.getCurrentFile();
5491		var addTitle = true;
5492		var data = '';
5493
5494		if (url != null)
5495		{
5496			data = '#U' + encodeURIComponent(url);
5497		}
5498		else
5499		{
5500			var file = this.getCurrentFile();
5501
5502			// Fallback to non-public URL for Drive files
5503			if (!ignoreFile && file != null && file.constructor == window.DriveFile)
5504			{
5505				data = '#' + file.getHash();
5506				addTitle = false;
5507			}
5508			else
5509			{
5510				data = '#R' + encodeURIComponent((allPages) ?
5511					this.getFileData(true, null, null, null, null, null, null, true, null, false) :
5512					Graph.compress(mxUtils.getXml(this.editor.getGraphXml())))
5513			}
5514		}
5515
5516		if (addTitle && file != null && file.getTitle() != null && file.getTitle() != this.defaultFilename)
5517		{
5518			params.push('title=' + encodeURIComponent(file.getTitle()));
5519		}
5520
5521		if (useOpenParameter && data.length > 1)
5522		{
5523			params.push('open=' + data.substring(1));
5524			data = '';
5525		}
5526
5527		return ((lightbox && urlParams['dev'] != '1') ? EditorUi.lightboxHost :
5528			(((mxClient.IS_CHROMEAPP || EditorUi.isElectronApp ||
5529			!(/.*\.draw\.io$/.test(window.location.hostname))) ?
5530			EditorUi.drawHost : 'https://' + window.location.host))) + '/' +
5531			((params.length > 0) ? '?' + params.join('&') : '') + data;
5532	};
5533
5534	/**
5535	 *
5536	 */
5537	EditorUi.prototype.createHtml = function(publicUrl, zoomEnabled, initialZoom, linkTarget,
5538		linkColor, fit, allPages, layers, tags, lightbox, editLink, fn)
5539	{
5540		var s = this.getBasenames();
5541		var data = {};
5542
5543		if (linkColor != '' && linkColor != mxConstants.NONE)
5544		{
5545			data.highlight = linkColor;
5546		}
5547
5548		if (linkTarget !== 'auto')
5549		{
5550			data.target = linkTarget;
5551		}
5552
5553		if (!lightbox)
5554		{
5555			data.lightbox = false;
5556		}
5557
5558		data.nav = this.editor.graph.foldingEnabled;
5559		var zoom = parseInt(initialZoom);
5560
5561		if (!isNaN(zoom) && zoom != 100)
5562		{
5563			data.zoom = zoom / 100;
5564		}
5565
5566		var tb = [];
5567
5568		if (allPages)
5569		{
5570			tb.push('pages');
5571			data.resize = true;
5572
5573			if (this.pages != null && this.currentPage != null)
5574			{
5575				data.page = mxUtils.indexOf(this.pages, this.currentPage);
5576			}
5577		}
5578
5579		if (zoomEnabled)
5580		{
5581			tb.push('zoom');
5582			data.resize = true;
5583		}
5584
5585		if (layers)
5586		{
5587			tb.push('layers');
5588		}
5589
5590		if (tags)
5591		{
5592			tb.push('tags');
5593		}
5594
5595		if (tb.length > 0)
5596		{
5597			if (lightbox)
5598			{
5599				tb.push('lightbox');
5600			}
5601
5602			data.toolbar = tb.join(' ');
5603		}
5604
5605		if (editLink != null && editLink.length > 0)
5606		{
5607			data.edit = editLink;
5608		}
5609
5610		if (publicUrl != null)
5611		{
5612			data.url = publicUrl;
5613		}
5614		else
5615		{
5616			data.xml = this.getFileData(true, null, null, null, null, !allPages);
5617		}
5618
5619		var value = '<div class="mxgraph" style="' +
5620			((fit) ? 'max-width:100%;' : '') +
5621			((tb != '') ? 'border:1px solid transparent;' : '') +
5622			'" data-mxgraph="' + mxUtils.htmlEntities(JSON.stringify(data)) + '"></div>';
5623
5624		var fetchParam = (publicUrl != null) ? '&fetch=' + encodeURIComponent(publicUrl) : '';
5625		var s2 = (fetchParam.length > 0) ? (((urlParams['dev'] == '1') ?
5626			'https://test.draw.io/embed2.js?dev=1' : EditorUi.lightboxHost + '/embed2.js?')) + fetchParam :
5627			(((urlParams['dev'] == '1') ? 'https://test.draw.io/js/viewer-static.min.js' :
5628			window.DRAWIO_VIEWER_URL ? window.DRAWIO_VIEWER_URL : EditorUi.lightboxHost + '/js/viewer-static.min.js'));
5629		var src = '<script type="text/javascript" src="' + s2 + '"></script>';
5630
5631		fn(value, src);
5632	};
5633
5634	/**
5635	 *
5636	 */
5637	EditorUi.prototype.showHtmlDialog = function(btnLabel, helpLink, publicUrl, fn)
5638	{
5639		var div = document.createElement('div');
5640		div.style.whiteSpace = 'nowrap';
5641		var graph = this.editor.graph;
5642
5643		var hd = document.createElement('h3');
5644		mxUtils.write(hd, mxResources.get('html'));
5645		hd.style.cssText = 'width:100%;text-align:center;margin-top:0px;margin-bottom:12px';
5646		div.appendChild(hd);
5647
5648		var radioSection = document.createElement('div');
5649		radioSection.style.cssText = 'border-bottom:1px solid lightGray;padding-bottom:8px;margin-bottom:12px;';
5650
5651		var publicUrlRadio = document.createElement('input');
5652		publicUrlRadio.style.cssText = 'margin-right:8px;margin-top:8px;margin-bottom:8px;';
5653		publicUrlRadio.setAttribute('value', 'url');
5654		publicUrlRadio.setAttribute('type', 'radio');
5655		publicUrlRadio.setAttribute('name', 'type-embedhtmldialog');
5656
5657		var copyRadio = publicUrlRadio.cloneNode(true);
5658		copyRadio.setAttribute('value', 'copy');
5659		radioSection.appendChild(copyRadio);
5660
5661		var span = document.createElement('span');
5662		mxUtils.write(span, mxResources.get('includeCopyOfMyDiagram'));
5663		radioSection.appendChild(span);
5664
5665		mxUtils.br(radioSection);
5666		radioSection.appendChild(publicUrlRadio);
5667
5668		var span = document.createElement('span');
5669		mxUtils.write(span, mxResources.get('publicDiagramUrl'));
5670		radioSection.appendChild(span);
5671
5672		var file = this.getCurrentFile();
5673
5674		if (publicUrl == null && file != null && file.constructor == window.DriveFile)
5675		{
5676			var testLink = document.createElement('a');
5677			testLink.style.paddingLeft = '12px';
5678			testLink.style.color = 'gray';
5679			testLink.style.cursor = 'pointer';
5680			mxUtils.write(testLink, mxResources.get('share'));
5681			radioSection.appendChild(testLink);
5682
5683			mxEvent.addListener(testLink, 'click', mxUtils.bind(this, function()
5684			{
5685				this.hideDialog();
5686				this.drive.showPermissions(file.getId());
5687			}));
5688		}
5689
5690		copyRadio.setAttribute('checked', 'checked');
5691
5692		if (publicUrl == null)
5693		{
5694			publicUrlRadio.setAttribute('disabled', 'disabled');
5695		}
5696
5697		div.appendChild(radioSection);
5698
5699		var linkSection = this.addLinkSection(div);
5700		var zoom = this.addCheckbox(div, mxResources.get('zoom'), true, null, true);
5701		mxUtils.write(div, ':');
5702
5703		var zoomInput = document.createElement('input');
5704		zoomInput.setAttribute('type', 'text');
5705		zoomInput.style.marginRight = '16px';
5706		zoomInput.style.width = '60px';
5707		zoomInput.style.marginLeft = '4px';
5708		zoomInput.style.marginRight = '12px';
5709		zoomInput.value = '100%';
5710
5711		div.appendChild(zoomInput);
5712
5713		var fit = this.addCheckbox(div, mxResources.get('fit'), true);
5714		var hasPages = this.pages != null && this.pages.length > 1;
5715		var allPages = allPages = this.addCheckbox(div, mxResources.get('allPages'), hasPages, !hasPages);
5716		var layers = this.addCheckbox(div, mxResources.get('layers'), true);
5717		var tags = this.addCheckbox(div, mxResources.get('tags'), true);
5718		var lightbox = this.addCheckbox(div, mxResources.get('lightbox'), true);
5719
5720		var editSection = this.addEditButton(div, lightbox);
5721		var edit = editSection.getEditInput();
5722		edit.style.marginBottom = '16px';
5723
5724		mxEvent.addListener(lightbox, 'change', function()
5725		{
5726			if (lightbox.checked)
5727			{
5728				edit.removeAttribute('disabled');
5729			}
5730			else
5731			{
5732				edit.setAttribute('disabled', 'disabled');
5733			}
5734
5735			if (edit.checked && lightbox.checked)
5736			{
5737				editSection.getEditSelect().removeAttribute('disabled');
5738			}
5739			else
5740			{
5741				editSection.getEditSelect().setAttribute('disabled', 'disabled');
5742			}
5743		});
5744
5745		var dlg = new CustomDialog(this, div, mxUtils.bind(this, function()
5746		{
5747			fn((publicUrlRadio.checked) ? publicUrl : null, zoom.checked, zoomInput.value, linkSection.getTarget(),
5748				linkSection.getColor(), fit.checked, allPages.checked, layers.checked, tags.checked,
5749				lightbox.checked, editSection.getLink());
5750		}), null, btnLabel, helpLink);
5751		this.showDialog(dlg.container, 340, 430, true, true);
5752		copyRadio.focus();
5753	};
5754
5755	/**
5756	 *
5757	 */
5758	EditorUi.prototype.showPublishLinkDialog = function(title, hideShare, width, height, fn, showFrameOption, helpLink)
5759	{
5760		var div = document.createElement('div');
5761		div.style.whiteSpace = 'nowrap';
5762		var graph = this.editor.graph;
5763
5764		var hd = document.createElement('h3');
5765		mxUtils.write(hd, title || mxResources.get('link'));
5766		hd.style.cssText = 'width:100%;text-align:center;margin-top:0px;margin-bottom:12px';
5767		div.appendChild(hd);
5768
5769		var file = this.getCurrentFile();
5770		var dy = 0;
5771
5772		if (file != null && file.constructor == window.DriveFile && !hideShare)
5773		{
5774			dy = 80;
5775			helpLink = (helpLink != null) ? helpLink : 'https://www.diagrams.net/doc/faq/google-drive-publicly-publish-diagram';
5776			var hintSection = document.createElement('div');
5777			hintSection.style.cssText = 'border-bottom:1px solid lightGray;padding-bottom:14px;padding-top:6px;margin-bottom:14px;text-align:center;';
5778
5779			var text = document.createElement('div');
5780			text.style.whiteSpace = 'normal';
5781			mxUtils.write(text, mxResources.get('linkAccountRequired'));
5782			hintSection.appendChild(text);
5783
5784			var shareBtn = mxUtils.button(mxResources.get('share'), mxUtils.bind(this, function()
5785			{
5786				this.drive.showPermissions(file.getId());
5787			}));
5788			shareBtn.style.marginTop = '12px';
5789			shareBtn.className = 'geBtn';
5790			hintSection.appendChild(shareBtn);
5791			div.appendChild(hintSection);
5792
5793			var testLink = document.createElement('a');
5794			testLink.style.paddingLeft = '12px';
5795			testLink.style.color = 'gray';
5796			testLink.style.fontSize = '11px';
5797			testLink.style.cursor = 'pointer';
5798			mxUtils.write(testLink, mxResources.get('check'));
5799			hintSection.appendChild(testLink);
5800
5801			mxEvent.addListener(testLink, 'click', mxUtils.bind(this, function()
5802			{
5803				if (this.spinner.spin(document.body, mxResources.get('loading')))
5804				{
5805					this.getPublicUrl(this.getCurrentFile(), mxUtils.bind(this, function(url)
5806					{
5807						this.spinner.stop();
5808
5809						var dlg = new ErrorDialog(this, null, mxResources.get((url != null) ?
5810							'diagramIsPublic' : 'diagramIsNotPublic'), mxResources.get('ok'));
5811						this.showDialog(dlg.container, 300, 80, true, false);
5812						dlg.init();
5813					}));
5814				}
5815			}));
5816		}
5817		else
5818		{
5819			helpLink = (helpLink != null) ? helpLink : 'https://www.diagrams.net/doc/faq/publish-diagram-as-link';
5820		}
5821
5822		var widthInput = null;
5823		var heightInput = null;
5824
5825		if (width != null || height != null)
5826		{
5827			dy += 30;
5828			mxUtils.write(div, mxResources.get('width') + ':');
5829
5830			widthInput = document.createElement('input');
5831			widthInput.setAttribute('type', 'text');
5832			widthInput.style.marginRight = '16px';
5833			widthInput.style.width = '50px';
5834			widthInput.style.marginLeft = '6px';
5835			widthInput.style.marginRight = '16px';
5836			widthInput.style.marginBottom = '10px';
5837			widthInput.value = '100%';
5838
5839			div.appendChild(widthInput);
5840
5841			mxUtils.write(div, mxResources.get('height') + ':');
5842
5843			heightInput = document.createElement('input');
5844			heightInput.setAttribute('type', 'text');
5845			heightInput.style.width = '50px';
5846			heightInput.style.marginLeft = '6px';
5847			heightInput.style.marginBottom = '10px';
5848			heightInput.value = height + 'px';
5849
5850			div.appendChild(heightInput);
5851			mxUtils.br(div);
5852		}
5853
5854		var linkSection = this.addLinkSection(div, showFrameOption);
5855		var hasPages = this.pages != null && this.pages.length > 1;
5856		var allPages = null;
5857
5858		if (file == null || file.constructor != window.DriveFile || hideShare)
5859		{
5860			allPages = this.addCheckbox(div, mxResources.get('allPages'), hasPages, !hasPages);
5861		}
5862
5863		var lightbox = this.addCheckbox(div, mxResources.get('lightbox'), true, null, null, !showFrameOption);
5864		var editSection = this.addEditButton(div, lightbox);
5865		var edit = editSection.getEditInput();
5866
5867		// Cannot disable lightbox in iframes
5868		if (showFrameOption)
5869		{
5870			edit.style.marginLeft = lightbox.style.marginLeft;
5871			lightbox.style.display = 'none';
5872			dy -= 20;
5873		}
5874
5875		var layers = this.addCheckbox(div, mxResources.get('layers'), true);
5876		layers.style.marginLeft = edit.style.marginLeft;
5877		layers.style.marginTop = '8px';
5878
5879		var tags = this.addCheckbox(div, mxResources.get('tags'), true);
5880		tags.style.marginLeft = edit.style.marginLeft;
5881		tags.style.marginBottom = '16px';
5882		tags.style.marginTop = '16px';
5883
5884		mxEvent.addListener(lightbox, 'change', function()
5885		{
5886			if (lightbox.checked)
5887			{
5888				layers.removeAttribute('disabled');
5889				edit.removeAttribute('disabled');
5890			}
5891			else
5892			{
5893				layers.setAttribute('disabled', 'disabled');
5894				edit.setAttribute('disabled', 'disabled');
5895			}
5896
5897			if (edit.checked && lightbox.checked)
5898			{
5899				editSection.getEditSelect().removeAttribute('disabled');
5900			}
5901			else
5902			{
5903				editSection.getEditSelect().setAttribute('disabled', 'disabled');
5904			}
5905		});
5906
5907		var dlg = new CustomDialog(this, div, mxUtils.bind(this, function()
5908		{
5909			fn(linkSection.getTarget(), linkSection.getColor(),
5910				(allPages == null) ? true : allPages.checked,
5911				lightbox.checked, editSection.getLink(),
5912				layers.checked, (widthInput != null) ? widthInput.value : null,
5913				(heightInput != null) ? heightInput.value : null, tags.checked);
5914		}), null, mxResources.get('create'), helpLink);
5915		this.showDialog(dlg.container, 340, 300 + dy, true, true);
5916
5917		if (widthInput != null)
5918		{
5919			widthInput.focus();
5920
5921			if (mxClient.IS_GC || mxClient.IS_FF || document.documentMode >= 5)
5922			{
5923				widthInput.select();
5924			}
5925			else
5926			{
5927				document.execCommand('selectAll', false, null);
5928			}
5929		}
5930		else
5931		{
5932			linkSection.focus();
5933		}
5934	};
5935
5936	/**
5937	 *
5938	 */
5939	EditorUi.prototype.showRemoteExportDialog = function(btnLabel, helpLink, callback, hideInclude, showZoomBorder)
5940	{
5941		var div = document.createElement('div');
5942		div.style.whiteSpace = 'nowrap';
5943
5944		var hd = document.createElement('h3');
5945		mxUtils.write(hd, mxResources.get('image'));
5946		hd.style.cssText = 'width:100%;text-align:center;margin-top:0px;margin-bottom:' + (showZoomBorder? '10' : '4') +'px';
5947		div.appendChild(hd);
5948
5949		if (showZoomBorder)
5950		{
5951			mxUtils.write(div, mxResources.get('zoom') + ':');
5952			var zoomInput = document.createElement('input');
5953			zoomInput.setAttribute('type', 'text');
5954			zoomInput.style.marginRight = '16px';
5955			zoomInput.style.width = '60px';
5956			zoomInput.style.marginLeft = '4px';
5957			zoomInput.style.marginRight = '12px';
5958			zoomInput.value = this.lastExportZoom || '100%';
5959			div.appendChild(zoomInput);
5960
5961			mxUtils.write(div, mxResources.get('borderWidth') + ':');
5962			var borderInput = document.createElement('input');
5963			borderInput.setAttribute('type', 'text');
5964			borderInput.style.marginRight = '16px';
5965			borderInput.style.width = '60px';
5966			borderInput.style.marginLeft = '4px';
5967			borderInput.value = this.lastExportBorder || '0';
5968			div.appendChild(borderInput);
5969			mxUtils.br(div);
5970		}
5971
5972		var selection = this.addCheckbox(div, mxResources.get('selectionOnly'), false,
5973			this.editor.graph.isSelectionEmpty());
5974		var include = (hideInclude) ? null : this.addCheckbox(div, mxResources.get('includeCopyOfMyDiagram'),
5975			Editor.defaultIncludeDiagram);
5976
5977		var graph = this.editor.graph;
5978		var transparent = (hideInclude) ? null : this.addCheckbox(div, mxResources.get('transparentBackground'),
5979				graph.background == mxConstants.NONE || graph.background == null);
5980
5981		if (transparent != null)
5982		{
5983			transparent.style.marginBottom = '16px';
5984		}
5985
5986		var dlg = new CustomDialog(this, div, mxUtils.bind(this, function()
5987		{
5988			var scale = parseInt(zoomInput.value) / 100 || 1;
5989			var border = parseInt(borderInput.value) || 0;
5990
5991			callback(!selection.checked, (include != null) ? include.checked : false,
5992				(transparent != null) ? transparent.checked : false, scale, border);
5993		}), null, btnLabel, helpLink);
5994		this.showDialog(dlg.container, 300, (showZoomBorder? 25 : 0) + (hideInclude ? 125 : 210), true, true);
5995	};
5996
5997	/**
5998	 *
5999	 */
6000	EditorUi.prototype.showExportDialog = function(title, embedOption, btnLabel, helpLink, callback,
6001		cropOption, defaultInclude, format, exportOption)
6002	{
6003		defaultInclude = (defaultInclude != null) ? defaultInclude : Editor.defaultIncludeDiagram;
6004
6005		var div = document.createElement('div');
6006		div.style.whiteSpace = 'nowrap';
6007		var graph = this.editor.graph;
6008		var height = (format == 'jpeg') ? 220 : 300;
6009
6010		var hd = document.createElement('h3');
6011		mxUtils.write(hd, title);
6012		hd.style.cssText = 'width:100%;text-align:center;margin-top:0px;margin-bottom:10px';
6013		div.appendChild(hd);
6014
6015		mxUtils.write(div, mxResources.get('zoom') + ':');
6016		var zoomInput = document.createElement('input');
6017		zoomInput.setAttribute('type', 'text');
6018		zoomInput.style.marginRight = '16px';
6019		zoomInput.style.width = '60px';
6020		zoomInput.style.marginLeft = '4px';
6021		zoomInput.style.marginRight = '12px';
6022		zoomInput.value = this.lastExportZoom || '100%';
6023		div.appendChild(zoomInput);
6024
6025		mxUtils.write(div, mxResources.get('borderWidth') + ':');
6026		var borderInput = document.createElement('input');
6027		borderInput.setAttribute('type', 'text');
6028		borderInput.style.marginRight = '16px';
6029		borderInput.style.width = '60px';
6030		borderInput.style.marginLeft = '4px';
6031		borderInput.value = this.lastExportBorder || '0';
6032		div.appendChild(borderInput);
6033		mxUtils.br(div);
6034
6035		var selection = this.addCheckbox(div, mxResources.get('selectionOnly'),
6036			false, graph.isSelectionEmpty());
6037
6038		var cb6 = document.createElement('input');
6039		cb6.style.marginTop = '16px';
6040		cb6.style.marginRight = '8px';
6041		cb6.style.marginLeft = '24px';
6042		cb6.setAttribute('disabled', 'disabled');
6043		cb6.setAttribute('type', 'checkbox');
6044
6045		var exportSelect = document.createElement('select');
6046		exportSelect.style.marginTop = '16px';
6047		exportSelect.style.marginLeft = '8px';
6048
6049		var sizes = ['selectionOnly', 'diagram', 'page'];
6050
6051		for (var i = 0; i < sizes.length; i++)
6052		{
6053			if (!graph.isSelectionEmpty() || sizes[i] != 'selectionOnly')
6054			{
6055				var opt = document.createElement('option');
6056				mxUtils.write(opt, mxResources.get(sizes[i]));
6057				opt.setAttribute('value', sizes[i]);
6058				exportSelect.appendChild(opt);
6059			}
6060		}
6061
6062		if (exportOption)
6063		{
6064			mxUtils.write(div, mxResources.get('size') + ':');
6065			div.appendChild(exportSelect);
6066			mxUtils.br(div);
6067			height += 26;
6068
6069			mxEvent.addListener(exportSelect, 'change', function()
6070			{
6071				if (exportSelect.value == 'selectionOnly')
6072				{
6073					selection.checked = true;
6074				}
6075			});
6076		}
6077		else if (cropOption)
6078		{
6079			div.appendChild(cb6);
6080			mxUtils.write(div, mxResources.get('crop'));
6081			mxUtils.br(div);
6082
6083			height += 30;
6084
6085			mxEvent.addListener(selection, 'change', function()
6086			{
6087				if (selection.checked)
6088				{
6089					cb6.removeAttribute('disabled');
6090				}
6091				else
6092				{
6093					cb6.setAttribute('disabled', 'disabled');
6094				}
6095			});
6096		}
6097
6098		if (graph.isSelectionEmpty())
6099		{
6100			if (exportOption)
6101			{
6102				selection.style.display = 'none';
6103				selection.nextSibling.style.display = 'none';
6104				selection.nextSibling.nextSibling.style.display = 'none';
6105				height -= 30;
6106			}
6107		}
6108		else
6109		{
6110			exportSelect.value = 'diagram';
6111			cb6.setAttribute('checked', 'checked');
6112			cb6.defaultChecked = true;
6113
6114			mxEvent.addListener(selection, 'change', function()
6115			{
6116				if (selection.checked)
6117				{
6118					exportSelect.value = 'selectionOnly';
6119				}
6120				else
6121				{
6122					exportSelect.value = 'diagram';
6123				}
6124			});
6125		}
6126
6127		var defaultTransparent = false; /*graph.background == mxConstants.NONE || graph.background == null*/;
6128		var transparent = this.addCheckbox(div, mxResources.get('transparentBackground'),
6129			defaultTransparent, null, null, format != 'jpeg');
6130		var keepTheme = null;
6131
6132		if (Editor.isDarkMode())
6133		{
6134			keepTheme = this.addCheckbox(div, mxResources.get('dark'), true);
6135			height += 26;
6136		}
6137
6138		var shadow = this.addCheckbox(div, mxResources.get('shadow'), graph.shadowVisible);
6139
6140		var cb5 = document.createElement('input');
6141		cb5.style.marginTop = '16px';
6142		cb5.style.marginRight = '8px';
6143		cb5.setAttribute('type', 'checkbox');
6144
6145		var cb7 = document.createElement('input');
6146		cb7.style.marginTop = '16px';
6147		cb7.style.marginRight = '8px';
6148		cb7.setAttribute('type', 'checkbox');
6149
6150		if (this.isOffline() || !this.canvasSupported)
6151		{
6152			cb5.setAttribute('disabled', 'disabled');
6153		}
6154
6155		if (embedOption)
6156		{
6157			div.appendChild(cb5);
6158			mxUtils.write(div, mxResources.get('embedImages'));
6159			mxUtils.br(div);
6160
6161			div.appendChild(cb7);
6162			mxUtils.write(div, mxResources.get('embedFonts'));
6163			mxUtils.br(div);
6164
6165			height += 60;
6166		}
6167
6168		var grid = null;
6169
6170		if (format == 'png' || format == 'jpeg')
6171		{
6172			grid = this.addCheckbox(div, mxResources.get('grid'), false, this.isOffline() || !this.canvasSupported, false, true);
6173			height += 30;
6174		}
6175
6176		var include = this.addCheckbox(div, mxResources.get('includeCopyOfMyDiagram'), defaultInclude, null, null, format != 'jpeg');
6177		include.style.marginBottom = '16px';
6178
6179		var linkSelect = document.createElement('select');
6180		linkSelect.style.maxWidth = '260px';
6181		linkSelect.style.marginLeft = '8px';
6182		linkSelect.style.marginRight = '10px';
6183		linkSelect.className = 'geBtn';
6184
6185		var autoOption = document.createElement('option');
6186		autoOption.setAttribute('value', 'auto');
6187		mxUtils.write(autoOption, mxResources.get('automatic'));
6188		linkSelect.appendChild(autoOption);
6189
6190		var blankOption = document.createElement('option');
6191		blankOption.setAttribute('value', 'blank');
6192		mxUtils.write(blankOption, mxResources.get('openInNewWindow'));
6193		linkSelect.appendChild(blankOption);
6194
6195		var selfOption = document.createElement('option');
6196		selfOption.setAttribute('value', 'self');
6197		mxUtils.write(selfOption, mxResources.get('openInThisWindow'));
6198		linkSelect.appendChild(selfOption);
6199
6200		if (format == 'svg')
6201		{
6202			mxUtils.write(div, mxResources.get('links') + ':');
6203			div.appendChild(linkSelect);
6204			mxUtils.br(div);
6205			mxUtils.br(div);
6206			height += 50;
6207		}
6208
6209		var dlg = new CustomDialog(this, div, mxUtils.bind(this, function()
6210		{
6211			this.lastExportBorder = borderInput.value;
6212			this.lastExportZoom = zoomInput.value;
6213
6214			callback(zoomInput.value, transparent.checked, !selection.checked, shadow.checked,
6215				include.checked, cb5.checked, borderInput.value, cb6.checked, false,
6216				linkSelect.value, (grid != null) ? grid.checked : null, (keepTheme != null) ?
6217				keepTheme.checked : null, exportSelect.value, cb7.checked);
6218		}), null, btnLabel, helpLink);
6219		this.showDialog(dlg.container, 340, height, true, true, null, null, null, null, true);
6220		zoomInput.focus();
6221
6222		if (mxClient.IS_GC || mxClient.IS_FF || document.documentMode >= 5)
6223		{
6224			zoomInput.select();
6225		}
6226		else
6227		{
6228			document.execCommand('selectAll', false, null);
6229		}
6230	};
6231
6232	/**
6233	 *
6234	 */
6235	EditorUi.prototype.showEmbedImageDialog = function(fn, title, imageLabel, shadowEnabled, helpLink)
6236	{
6237		var div = document.createElement('div');
6238		div.style.whiteSpace = 'nowrap';
6239		var graph = this.editor.graph;
6240
6241		if (title != null)
6242		{
6243			var hd = document.createElement('h3');
6244			mxUtils.write(hd, title);
6245			hd.style.cssText = 'width:100%;text-align:center;margin-top:0px;margin-bottom:4px';
6246			div.appendChild(hd);
6247		}
6248
6249		var fit = this.addCheckbox(div, mxResources.get('fit'), true);
6250		var shadow = this.addCheckbox(div, mxResources.get('shadow'),
6251			graph.shadowVisible && shadowEnabled, !shadowEnabled);
6252		var image = this.addCheckbox(div, imageLabel);
6253		var lightbox = this.addCheckbox(div, mxResources.get('lightbox'), true);
6254		var editSection = this.addEditButton(div, lightbox);
6255		var edit = editSection.getEditInput();
6256
6257		var hasLayers = graph.model.getChildCount(graph.model.getRoot()) > 1;
6258		var layers = this.addCheckbox(div, mxResources.get('layers'), hasLayers, !hasLayers);
6259		layers.style.marginLeft = edit.style.marginLeft;
6260		layers.style.marginBottom = '12px';
6261		layers.style.marginTop = '8px';
6262
6263		mxEvent.addListener(lightbox, 'change', function()
6264		{
6265			if (lightbox.checked)
6266			{
6267				if (hasLayers)
6268				{
6269					layers.removeAttribute('disabled');
6270				}
6271
6272				edit.removeAttribute('disabled');
6273			}
6274			else
6275			{
6276				layers.setAttribute('disabled', 'disabled');
6277				edit.setAttribute('disabled', 'disabled');
6278			}
6279
6280			if (edit.checked && lightbox.checked)
6281			{
6282				editSection.getEditSelect().removeAttribute('disabled');
6283			}
6284			else
6285			{
6286				editSection.getEditSelect().setAttribute('disabled', 'disabled');
6287			}
6288		});
6289
6290		var dlg = new CustomDialog(this, div, mxUtils.bind(this, function()
6291		{
6292			fn(fit.checked, shadow.checked, image.checked, lightbox.checked,
6293				editSection.getLink(), layers.checked);
6294		}), null, mxResources.get('embed'), helpLink);
6295		this.showDialog(dlg.container, 280, 300, true, true);
6296	};
6297
6298	/**
6299	 *
6300	 */
6301	EditorUi.prototype.createEmbedImage = function(fit, shadow, retina, lightbox, edit, layers, fn, err)
6302	{
6303		var bounds = this.editor.graph.getGraphBounds();
6304		var page = this.getSelectedPageIndex();
6305
6306		function doUpdate(dataUri)
6307		{
6308   			var onclick = ' ';
6309   			var css = '';
6310
6311   			// Adds double click handling
6312			if (lightbox)
6313			{
6314				// KNOWN: Message passing does not seem to work in IE11
6315				onclick = " onclick=\"(function(img){if(img.wnd!=null&&!img.wnd.closed){img.wnd.focus();}else{var r=function(evt){if(evt.data=='ready'&&evt.source==img.wnd){img.wnd.postMessage(decodeURIComponent(" +
6316					"img.getAttribute('src')),'*');window.removeEventListener('message',r);}};window.addEventListener('message',r);img.wnd=window.open('" + EditorUi.lightboxHost + "/?client=1" +
6317					((page != null) ? ("&page=" + page) : "") +
6318					((edit) ? "&edit=_blank" : "") +
6319					((layers) ? '&layers=1' : '') + "');}})(this);\"";
6320				css += 'cursor:pointer;';
6321			}
6322
6323			if (fit)
6324			{
6325				css += 'max-width:100%;';
6326			}
6327
6328			var atts = '';
6329
6330			if (retina)
6331			{
6332				atts = ' width="' + Math.round(bounds.width) + '" height="' + Math.round(bounds.height) + '"';
6333			}
6334
6335			fn('<img src="' + dataUri + '"' + atts + ((css != '') ? ' style="' + css + '"' : '') + onclick + '/>');
6336		};
6337
6338		if (this.isExportToCanvas())
6339		{
6340			this.editor.exportToCanvas(mxUtils.bind(this, function(canvas)
6341		   	{
6342	   			var xml = (lightbox) ? this.getFileData(true) : null;
6343	   			var data = this.createImageDataUri(canvas, xml, 'png');
6344	   			doUpdate(data);
6345		   	}), null, null, null, mxUtils.bind(this, function(e)
6346		   	{
6347		   		err({message: mxResources.get('unknownError')});
6348		   	}), null, true, (retina) ? 2 : 1, null, shadow, null, null, Editor.defaultBorder);
6349		}
6350		else
6351		{
6352			var data = this.getFileData(true);
6353
6354			if (bounds.width * bounds.height <= MAX_AREA && data.length <= MAX_REQUEST_SIZE)
6355			{
6356				var size = '';
6357
6358				if (retina)
6359				{
6360					size = '&w=' + Math.round(2 * bounds.width) +
6361						'&h=' + Math.round(2 * bounds.height);
6362				}
6363
6364				var embed = (lightbox) ? '1' : '0';
6365				var req = new mxXmlRequest(EXPORT_URL, 'format=png' +
6366					'&base64=1&embedXml=' + embed + size + '&xml=' +
6367					encodeURIComponent(data));
6368
6369				// LATER: Updates on each change, add a delay
6370				req.send(mxUtils.bind(this, function()
6371				{
6372					if (req.getStatus() >= 200 && req.getStatus() <= 299)
6373					{
6374						// Fixes possible "incorrect function" for select() on
6375						// DOM node which is no longer in document with IE11
6376						doUpdate('data:image/png;base64,' + req.getText());
6377					}
6378					else
6379					{
6380						err({message: mxResources.get('unknownError')});
6381					}
6382				}));
6383			}
6384			else
6385			{
6386				err({message: mxResources.get('drawingTooLarge')});
6387			}
6388		}
6389	};
6390
6391	/**
6392	 *
6393	 */
6394	EditorUi.prototype.createEmbedSvg = function(fit, shadow, image, lightbox, edit, layers, fn)
6395	{
6396		var svgRoot = this.editor.graph.getSvg(null, null, null, null, null,
6397				null, null, null, null, null, !image);
6398
6399		// Keeps hashtag links on same page
6400		var links = svgRoot.getElementsByTagName('a');
6401
6402		if (links != null)
6403		{
6404			for (var i = 0; i < links.length; i++)
6405			{
6406				var href = links[i].getAttribute('href');
6407
6408				if (href != null && href.charAt(0) == '#' &&
6409					links[i].getAttribute('target') == '_blank')
6410				{
6411					links[i].removeAttribute('target');
6412				}
6413			}
6414		}
6415
6416		if (lightbox)
6417		{
6418			svgRoot.setAttribute('content', this.getFileData(true));
6419		}
6420
6421		// Adds shadow filter
6422		if (shadow)
6423		{
6424			this.editor.graph.addSvgShadow(svgRoot);
6425		}
6426
6427		// SVG inside image tag
6428		if (image)
6429		{
6430   			var onclick = ' ';
6431   			var css = '';
6432
6433   			// Adds double click handling
6434			if (lightbox)
6435			{
6436				// KNOWN: Message passing does not seem to work in IE11
6437				onclick = "onclick=\"(function(img){if(img.wnd!=null&&!img.wnd.closed){img.wnd.focus();}else{var r=function(evt){if(evt.data=='ready'&&evt.source==img.wnd){img.wnd.postMessage(decodeURIComponent(" +
6438					"img.getAttribute('src')),'*');window.removeEventListener('message',r);}};window.addEventListener('message',r);img.wnd=window.open('" + EditorUi.lightboxHost + "/?client=1" +
6439					((edit) ? "&edit=_blank" : "") + ((layers) ? '&layers=1' : '') + "');}})(this);\"";
6440				css += 'cursor:pointer;';
6441			}
6442
6443			if (fit)
6444			{
6445				css += 'max-width:100%;';
6446			}
6447
6448   			// Images inside IMG don't seem to work so embed them all
6449			this.editor.convertImages(svgRoot, mxUtils.bind(this, function(svgRoot)
6450			{
6451				fn('<img src="' + Editor.createSvgDataUri(mxUtils.getXml(svgRoot)) + '"' +
6452					((css != '') ? ' style="' + css + '"' : '') + onclick + '/>');
6453			}));
6454		}
6455		else
6456		{
6457			var css = '';
6458
6459			// Adds double click handling
6460			if (lightbox)
6461			{
6462				var page = this.getSelectedPageIndex();
6463
6464				// KNOWN: Message passing does not seem to work in IE11
6465				var js = "(function(svg){var src=window.event.target||window.event.srcElement;" +
6466					// Ignores link events
6467					"while (src!=null&&src.nodeName.toLowerCase()!='a'){src=src.parentNode;}if(src==null)" +
6468					// Focus existing lightbox
6469					"{if(svg.wnd!=null&&!svg.wnd.closed){svg.wnd.focus();}else{var r=function(evt){" +
6470					// Message handling
6471					"if(evt.data=='ready'&&evt.source==svg.wnd){svg.wnd.postMessage(decodeURIComponent(" +
6472					"svg.getAttribute('content')),'*');window.removeEventListener('message',r);}};" +
6473					"window.addEventListener('message',r);" +
6474					// Opens lightbox window
6475					"svg.wnd=window.open('" + EditorUi.lightboxHost + "/?client=1" +
6476					((page != null) ? ("&page=" + page) : "") +
6477					((edit) ? "&edit=_blank" : "") + ((layers) ? '&layers=1' : '') + "');}}})(this);";
6478				svgRoot.setAttribute('onclick', js);
6479				css += 'cursor:pointer;';
6480			}
6481
6482			// Adds responsive size
6483			if (fit)
6484			{
6485				var w = parseInt(svgRoot.getAttribute('width'));
6486				var h = parseInt(svgRoot.getAttribute('height'));
6487				svgRoot.setAttribute('viewBox', '-0.5 -0.5 ' + w + ' ' + h);
6488				css += 'max-width:100%;max-height:' + h + 'px;';
6489				svgRoot.removeAttribute('height');
6490			}
6491
6492			if (css != '')
6493			{
6494				svgRoot.setAttribute('style', css);
6495			}
6496
6497			// Adds CSS
6498			this.editor.addFontCss(svgRoot);
6499
6500			if (this.editor.graph.mathEnabled)
6501			{
6502				this.editor.addMathCss(svgRoot);
6503			}
6504
6505			fn(mxUtils.getXml(svgRoot));
6506		}
6507	};
6508
6509	/**
6510	 * Translates this point by the given vector.
6511	 *
6512	 * @param {number} dx X-coordinate of the translation.
6513	 * @param {number} dy Y-coordinate of the translation.
6514	 */
6515	EditorUi.prototype.timeSince = function(date)
6516	{
6517	    var seconds = Math.floor((new Date() - date) / 1000);
6518	    var interval = Math.floor(seconds / 31536000);
6519
6520	    if (interval > 1)
6521	    {
6522	        return interval + ' ' + mxResources.get('years');
6523	    }
6524
6525	    interval = Math.floor(seconds / 2592000);
6526
6527	    if (interval > 1)
6528	    {
6529	        return interval + ' ' + mxResources.get('months');
6530	    }
6531
6532	    interval = Math.floor(seconds / 86400);
6533
6534	    if (interval > 1)
6535	    {
6536	        return interval + ' ' + mxResources.get('days');
6537	    }
6538
6539	    interval = Math.floor(seconds / 3600);
6540
6541	    if (interval > 1)
6542	    {
6543	        return interval + ' ' + mxResources.get('hours');
6544	    }
6545
6546	    interval = Math.floor(seconds / 60);
6547
6548	    if (interval > 1)
6549	    {
6550	        return interval + ' ' + mxResources.get('minutes');
6551	    }
6552
6553	    if (interval == 1)
6554	    {
6555	        return interval + ' ' + mxResources.get('minute');
6556	    }
6557
6558	    return null;
6559	};
6560
6561	/**
6562	 *
6563	 */
6564	EditorUi.prototype.decodeNodeIntoGraph = function(node, graph)
6565	{
6566		if (node != null)
6567		{
6568			var diagramNode = null;
6569
6570			if (node.nodeName == 'diagram')
6571			{
6572				diagramNode = node;
6573			}
6574			else if (node.nodeName == 'mxfile')
6575			{
6576				var diagrams = node.getElementsByTagName('diagram');
6577
6578				if (diagrams.length > 0)
6579				{
6580					diagramNode = diagrams[0];
6581					var graphGetGlobalVariable = graph.getGlobalVariable;
6582
6583					graph.getGlobalVariable = function(name)
6584					{
6585						if (name == 'page')
6586						{
6587							return diagramNode.getAttribute('name') || mxResources.get('pageWithNumber', [1])
6588						}
6589						else if (name == 'pagenumber')
6590						{
6591							return 1;
6592						}
6593
6594						return graphGetGlobalVariable.apply(this, arguments);
6595					};
6596				}
6597			}
6598
6599			if (diagramNode != null)
6600			{
6601				node = Editor.parseDiagramNode(diagramNode);
6602			}
6603		}
6604
6605		// Hack to decode XML into temp graph via editor
6606		var prev = this.editor.graph;
6607
6608		try
6609		{
6610			this.editor.graph = graph;
6611			this.editor.setGraphXml(node);
6612		}
6613		catch (e)
6614		{
6615			// ignore
6616		}
6617		finally
6618		{
6619			this.editor.graph = prev;
6620		}
6621
6622		return node;
6623	};
6624
6625	/**
6626	 *
6627	 */
6628	EditorUi.prototype.getPngFileProperties = function(node)
6629	{
6630		var scale = 1;
6631		var border = 0;
6632
6633		if (node != null)
6634		{
6635			if (node.hasAttribute('scale'))
6636			{
6637				var temp = parseFloat(node.getAttribute('scale'));
6638
6639				if (!isNaN(temp) && temp > 0)
6640				{
6641					scale = temp;
6642				}
6643			}
6644
6645			if (node.hasAttribute('border'))
6646			{
6647				var temp = parseInt(node.getAttribute('border'));
6648
6649				if (!isNaN(temp) && temp > 0)
6650				{
6651					border = temp;
6652				}
6653			}
6654		}
6655
6656		return {scale: scale, border: border};
6657	};
6658
6659	/**
6660	 *
6661	 */
6662	EditorUi.prototype.getEmbeddedPng = function(success, error, optionalData, scale, border)
6663	{
6664		try
6665		{
6666			var graph = this.editor.graph;
6667			var darkTheme = graph.themes != null && graph.defaultThemeName == 'darkTheme';
6668			var diagramData = null;
6669
6670			// Exports PNG for given optional data
6671			if (optionalData != null && optionalData.length > 0)
6672			{
6673				graph = this.createTemporaryGraph((darkTheme) ?
6674					graph.getDefaultStylesheet() : graph.getStylesheet());
6675				document.body.appendChild(graph.container);
6676				this.decodeNodeIntoGraph(this.editor.extractGraphModel(
6677					mxUtils.parseXml(optionalData).documentElement, true), graph);
6678				diagramData = optionalData;
6679			}
6680			// Exports PNG for first page while other page is showing
6681			else if (darkTheme || (this.pages != null && this.currentPage != this.pages[0]))
6682			{
6683				graph = this.createTemporaryGraph((darkTheme) ?
6684					graph.getDefaultStylesheet() : graph.getStylesheet());
6685				var graphGetGlobalVariable = graph.getGlobalVariable;
6686				graph.setBackgroundImage = this.editor.graph.setBackgroundImage;
6687				var page = this.pages[0];
6688
6689				if (this.currentPage == page)
6690				{
6691					graph.setBackgroundImage(this.editor.graph.backgroundImage);
6692				}
6693				else if (page.viewState != null && page.viewState != null)
6694				{
6695					graph.setBackgroundImage(page.viewState.backgroundImage);
6696				}
6697
6698				graph.getGlobalVariable = function(name)
6699				{
6700					if (name == 'page')
6701					{
6702						return page.getName();
6703					}
6704					else if (name == 'pagenumber')
6705					{
6706						return 1;
6707					}
6708
6709					return graphGetGlobalVariable.apply(this, arguments);
6710				};
6711
6712				document.body.appendChild(graph.container);
6713				graph.model.setRoot(page.root);
6714			}
6715
6716		   	this.editor.exportToCanvas(mxUtils.bind(this, function(canvas)
6717		   	{
6718		   		try
6719		   		{
6720		   			if (diagramData == null)
6721		   			{
6722		   				diagramData = this.getFileData(true, null, null, null, null,
6723		   						null, null, null, null, false);
6724		   			}
6725
6726		   	   	    var data = canvas.toDataURL('image/png');
6727		   	   	    data = Editor.writeGraphModelToPng(data,
6728		   	   	    	'tEXt', 'mxfile', encodeURIComponent(diagramData));
6729	   	   	   		success(data.substring(data.lastIndexOf(',') + 1));
6730
6731					// Removes temporary graph from DOM
6732	   	   	   		if (graph != this.editor.graph)
6733					{
6734						graph.container.parentNode.removeChild(graph.container);
6735					}
6736		   		}
6737		   		catch (e)
6738		   		{
6739		   			if (error != null)
6740		   			{
6741		   				error(e);
6742		   			}
6743		   		}
6744		   	}), null, null, null, mxUtils.bind(this, function(e)
6745		   	{
6746		   		if (error != null)
6747	   			{
6748	   				error(e);
6749	   			}
6750		   	}), null, null, scale, null, graph.shadowVisible, null,
6751				graph, border, null, null, null, 'diagram', null);
6752		}
6753		catch (e)
6754		{
6755			if (error != null)
6756			{
6757				error(e);
6758			}
6759		}
6760	}
6761
6762	/**
6763	 * Returns the SVG of the diagram with embedded XML. If a callback function is
6764	 * used, the images are converted to data URIs.
6765	 */
6766	EditorUi.prototype.getEmbeddedSvg = function(xml, graph, url, noHeader, callback, ignoreSelection,
6767		redirect, embedImages, background, scale, border, shadow, keepTheme)
6768	{
6769		embedImages = (embedImages != null) ? embedImages : true;
6770		border = (border != null) ? border : 0;
6771
6772		var bg = (background != null) ? background : graph.background;
6773
6774		if (bg == mxConstants.NONE)
6775		{
6776			bg = null;
6777		}
6778
6779		// Sets or disables alternate text for foreignObjects. Disabling is needed
6780		// because PhantomJS seems to ignore switch statements and paint all text.
6781		var svgRoot = graph.getSvg(bg, scale, border, null, null, ignoreSelection, null,
6782			null, null, graph.shadowVisible || shadow, null, keepTheme, 'diagram');
6783
6784		if (graph.shadowVisible || shadow)
6785		{
6786			graph.addSvgShadow(svgRoot, null, null, border == 0);
6787		}
6788
6789		if (xml != null)
6790		{
6791			svgRoot.setAttribute('content', xml);
6792		}
6793
6794		if (url != null)
6795		{
6796			svgRoot.setAttribute('resource', url);
6797		}
6798
6799		// LATER: Click on SVG content to start editing
6800//		if (redirect != null)
6801//		{
6802//			// TODO: Ignore anchor tag source for click event
6803//			svgRoot.setAttribute('style', 'cursor:pointer;');
6804//			svgRoot.setAttribute('onclick', 'window.location.href=\'' + redirect + '\';');
6805//		}
6806
6807		var done = mxUtils.bind(this, function(svgRoot)
6808		{
6809			var result = ((!noHeader) ? Graph.xmlDeclaration + '\n' + Graph.svgFileComment +
6810				'\n' + Graph.svgDoctype + '\n' : '') + mxUtils.getXml(svgRoot);
6811
6812			if (callback != null)
6813			{
6814				callback(result);
6815			}
6816
6817			return result;
6818		});
6819
6820		// Adds CSS
6821		if (graph.mathEnabled)
6822		{
6823			this.editor.addMathCss(svgRoot);
6824		}
6825
6826		if (callback != null)
6827		{
6828			this.embedFonts(svgRoot, mxUtils.bind(this, function(svgRoot)
6829			{
6830				if (embedImages)
6831				{
6832					this.editor.convertImages(svgRoot, mxUtils.bind(this, function(svgRoot)
6833					{
6834						done(svgRoot);
6835					}));
6836				}
6837				else
6838				{
6839					done(svgRoot);
6840				}
6841			}));
6842		}
6843		else
6844		{
6845			return done(svgRoot);
6846		}
6847	};
6848
6849	/**
6850	 * Embeds font CSS as data URIs into the given svgRoot.
6851	 */
6852	EditorUi.prototype.embedFonts = function(svgRoot, callback)
6853	{
6854		this.editor.loadFonts(mxUtils.bind(this, function()
6855		{
6856			try
6857			{
6858				if (this.editor.resolvedFontCss != null)
6859				{
6860					this.editor.addFontCss(svgRoot, this.editor.resolvedFontCss);
6861				}
6862
6863				this.editor.embedExtFonts(mxUtils.bind(this, function(extFontsEmbeddedCss)
6864				{
6865					try
6866					{
6867						if (extFontsEmbeddedCss != null)
6868						{
6869							this.editor.addFontCss(svgRoot, extFontsEmbeddedCss);
6870						}
6871
6872						callback(svgRoot);
6873					}
6874					catch (e)
6875					{
6876						callback(svgRoot);
6877					}
6878				}));
6879			}
6880			catch (e)
6881			{
6882				callback(svgRoot);
6883			}
6884		}));
6885	};
6886
6887	/**
6888	 *
6889	 */
6890	EditorUi.prototype.exportImage = function(scale, transparentBackground, ignoreSelection, addShadow,
6891		editable, border, noCrop, currentPage, format, grid, dpi, keepTheme, exportType)
6892	{
6893		format = (format != null) ? format : 'png';
6894
6895		if (this.spinner.spin(document.body, mxResources.get('exporting')))
6896		{
6897			var selectionEmpty = this.editor.graph.isSelectionEmpty();
6898			ignoreSelection = (ignoreSelection != null) ? ignoreSelection : selectionEmpty;
6899
6900			// Caches images
6901			if (this.thumbImageCache == null)
6902			{
6903				this.thumbImageCache = new Object();
6904			}
6905
6906			try
6907			{
6908			   	this.editor.exportToCanvas(mxUtils.bind(this, function(canvas)
6909			   	{
6910			   		this.spinner.stop();
6911
6912			   		try
6913			   		{
6914			   			this.saveCanvas(canvas, (editable) ? this.getFileData(true, null,
6915			   				null, null, ignoreSelection, currentPage) : null,
6916			   				format, (this.pages == null || this.pages.length == 0), dpi);
6917			   		}
6918			   		catch (e)
6919			   		{
6920			   			this.handleError(e);
6921			   		}
6922			   	}), null, this.thumbImageCache, null, mxUtils.bind(this, function(e)
6923			   	{
6924			   		this.spinner.stop();
6925			   		this.handleError(e);
6926				}), null, ignoreSelection, scale || 1, transparentBackground, addShadow,
6927					null, null, border, noCrop, grid, keepTheme, exportType);
6928			}
6929			catch (e)
6930			{
6931				this.spinner.stop();
6932				this.handleError(e);
6933			}
6934		}
6935	};
6936
6937	/**
6938	/**
6939	 * Returns true if the given URL is known to have CORS headers.
6940	 */
6941	EditorUi.prototype.isCorsEnabledForUrl = function(url)
6942	{
6943		return this.editor.isCorsEnabledForUrl(url);
6944	};
6945
6946	/**
6947	 * Handling drag and drop and import.
6948	 */
6949
6950	/**
6951	 * Imports the given XML into the existing diagram.
6952	 */
6953	EditorUi.prototype.importXml = function(xml, dx, dy, crop, noErrorHandling, addNewPage, applyDefaultStyles)
6954	{
6955		dx = (dx != null) ? dx : 0;
6956		dy = (dy != null) ? dy : 0;
6957		var cells = []
6958
6959		try
6960		{
6961			var graph = this.editor.graph;
6962
6963			if (xml != null && xml.length > 0)
6964			{
6965				// Adds pages
6966				graph.model.beginUpdate();
6967				try
6968				{
6969					var doc = mxUtils.parseXml(xml);
6970					var mapping = {};
6971
6972					// Checks for mxfile with multiple pages
6973					var node = this.editor.extractGraphModel(doc.documentElement, this.pages != null);
6974
6975					if (node != null && node.nodeName == 'mxfile' && this.pages != null)
6976					{
6977						var diagrams = node.getElementsByTagName('diagram');
6978
6979						if (diagrams.length == 1 && !addNewPage)
6980						{
6981							node = Editor.parseDiagramNode(diagrams[0]);
6982
6983							if (this.currentPage != null)
6984							{
6985								mapping[diagrams[0].getAttribute('id')] = this.currentPage.getId();
6986
6987								// Renames page if diagram has one blank page with default name
6988								if (this.pages != null && this.pages.length == 1 &&
6989									this.isDiagramEmpty() && this.currentPage.getName() ==
6990									mxResources.get('pageWithNumber', [1]))
6991								{
6992									var name = diagrams[0].getAttribute('name');
6993
6994									if (name != null && name != '')
6995									{
6996										this.editor.graph.model.execute(new RenamePage(
6997											this, this.currentPage, name));
6998									}
6999								}
7000							}
7001						}
7002						else if (diagrams.length > 0)
7003						{
7004							var pages = [];
7005							var i0 = 0;
7006
7007							// Adds first page to current page if current page is only page and empty
7008							if (this.pages != null && this.pages.length == 1 && this.isDiagramEmpty())
7009							{
7010								mapping[diagrams[0].getAttribute('id')] = this.pages[0].getId();
7011								node = Editor.parseDiagramNode(diagrams[0]);
7012								crop = false;
7013								i0 = 1;
7014							}
7015
7016							for (var i = i0; i < diagrams.length; i++)
7017							{
7018								// Imported pages must obtain a new ID and
7019								// all links to pages must be updated below
7020								var oldId = diagrams[i].getAttribute('id')
7021								diagrams[i].removeAttribute('id');
7022
7023								var page = this.updatePageRoot(new DiagramPage(diagrams[i]));
7024								mapping[oldId] = diagrams[i].getAttribute('id');
7025								var index = this.pages.length;
7026
7027								// Checks for invalid page names
7028								if (page.getName() == null)
7029								{
7030									page.setName(mxResources.get('pageWithNumber', [index + 1]));
7031								}
7032
7033								graph.model.execute(new ChangePage(this, page, page, index, true));
7034								pages.push(page);
7035							}
7036
7037							this.updatePageLinks(mapping, pages);
7038						}
7039					}
7040
7041					if (node != null && node.nodeName === 'mxGraphModel')
7042					{
7043						cells = graph.importGraphModel(node, dx, dy, crop);
7044
7045						if (cells != null)
7046						{
7047							for (var i = 0; i < cells.length; i++)
7048							{
7049								this.updatePageLinksForCell(mapping, cells[i]);
7050							}
7051						}
7052					}
7053
7054					if (applyDefaultStyles)
7055					{
7056						this.insertHandler(cells, null, null,
7057							graph.defaultVertexStyle,
7058							graph.defaultEdgeStyle,
7059							false, true);
7060					}
7061				}
7062				finally
7063				{
7064					graph.model.endUpdate();
7065				}
7066			}
7067		}
7068		catch (e)
7069		{
7070			if (!noErrorHandling)
7071			{
7072				this.handleError(e);
7073			}
7074			else
7075			{
7076				throw e;
7077			}
7078		}
7079
7080		return cells;
7081	};
7082
7083	/**
7084	 * Updates links to pages in shapes and labels.
7085	 */
7086	EditorUi.prototype.updatePageLinks = function(mapping, pages)
7087	{
7088		for (var i = 0; i < pages.length; i++)
7089		{
7090			this.updatePageLinksForCell(mapping, pages[i].root);
7091		}
7092	};
7093
7094	/**
7095	 * Updates links to pages in shapes and labels.
7096	 */
7097	EditorUi.prototype.updatePageLinksForCell = function(mapping, cell)
7098	{
7099		var temp = document.createElement('div');
7100		var graph = this.editor.graph;
7101		var href = graph.getLinkForCell(cell);
7102
7103		if (href != null)
7104		{
7105			graph.setLinkForCell(cell, this.updatePageLink(mapping, href));
7106		}
7107
7108		if (graph.isHtmlLabel(cell))
7109		{
7110			temp.innerHTML = graph.sanitizeHtml(graph.getLabel(cell));
7111			var links = temp.getElementsByTagName('a');
7112			var changed = false;
7113
7114			for (var i = 0; i < links.length; i++)
7115			{
7116				href = links[i].getAttribute('href');
7117
7118				if (href != null)
7119				{
7120					links[i].setAttribute('href', this.updatePageLink(mapping, href));
7121					changed = true;
7122				}
7123			}
7124
7125			if (changed)
7126			{
7127				graph.labelChanged(cell, temp.innerHTML);
7128			}
7129		}
7130
7131		for (var i = 0; i < graph.model.getChildCount(cell); i++)
7132		{
7133			this.updatePageLinksForCell(mapping, graph.model.getChildAt(cell, i));
7134		}
7135	};
7136
7137	/**
7138	 * Updates links to pages in shapes and labels.
7139	 */
7140	EditorUi.prototype.updatePageLink = function(mapping, href)
7141	{
7142		if (Graph.isPageLink(href))
7143		{
7144			var newId = mapping[href.substring(href.indexOf(',') + 1)];
7145			href = (newId != null) ? 'data:page/id,' + newId : null;
7146		}
7147		else if (href.substring(0, 17) == 'data:action/json,')
7148		{
7149			try
7150			{
7151				var link = JSON.parse(href.substring(17));
7152
7153				if (link.actions != null)
7154				{
7155					for (var i = 0; i < link.actions.length; i++)
7156					{
7157						var action = link.actions[i];
7158
7159						if (action.open != null && Graph.isPageLink(action.open))
7160						{
7161							var oldId = action.open.substring(action.open.indexOf(',') + 1);
7162							var newId = mapping[oldId];
7163
7164							if (newId != null)
7165							{
7166								action.open = 'data:page/id,' + newId;
7167							}
7168							else if (this.getPageById(oldId) == null)
7169							{
7170								delete action.open;
7171							}
7172						}
7173					}
7174
7175					href = 'data:action/json,' + JSON.stringify(link);
7176				}
7177			}
7178			catch (e)
7179			{
7180				// Ignore
7181			}
7182		}
7183
7184		return href;
7185	};
7186
7187	/**
7188	 * Returns true for VSD, VDX and VSS, VSX files.
7189	 */
7190	EditorUi.prototype.isRemoteVisioFormat = function(filename)
7191	{
7192		return /(\.v(sd|dx))($|\?)/i.test(filename) || /(\.vs(s|x))($|\?)/i.test(filename);
7193	};
7194
7195	/**
7196	 * Imports the given Visio file
7197	 */
7198	EditorUi.prototype.importVisio = function(file, done, onerror, filename, customParam)
7199	{
7200		//A reduced version of this code is used in conf/jira plugins, review that code whenever this function is changed
7201		filename = (filename != null) ? filename : file.name;
7202
7203		onerror = (onerror != null) ? onerror : mxUtils.bind(this, function(e)
7204		{
7205			this.handleError(e);
7206		});
7207
7208		var delayed = mxUtils.bind(this, function()
7209		{
7210			this.loadingExtensions = false;
7211
7212			if (this.doImportVisio)
7213			{
7214				var remote = this.isRemoteVisioFormat(filename);
7215
7216				try
7217				{
7218					var ext = 'UNKNOWN-VISIO';
7219					var dot = filename.lastIndexOf('.');
7220
7221					if (dot >= 0 && dot < filename.length)
7222					{
7223						ext = filename.substring(dot + 1).toUpperCase();
7224					}
7225					else
7226					{
7227						var slash = filename.lastIndexOf('/');
7228
7229						if (slash >= 0 && slash < filename.length)
7230						{
7231							filename = filename.substring(slash + 1);
7232						}
7233					}
7234
7235					EditorUi.logEvent({category: ext + '-MS-IMPORT-FILE',
7236						action: 'filename_' + filename,
7237						label: (remote) ? 'remote' : 'local'});
7238				}
7239				catch (e)
7240				{
7241					// ignore
7242				}
7243
7244				if (remote)
7245				{
7246					if (VSD_CONVERT_URL != null && !this.isOffline())
7247					{
7248						var formData = new FormData();
7249						formData.append('file1', file, filename);
7250
7251						var xhr = new XMLHttpRequest();
7252						xhr.open('POST', VSD_CONVERT_URL + (/(\.vss|\.vsx)$/.test(filename)? '?stencil=1' : ''));
7253						xhr.responseType = 'blob';
7254						this.addRemoteServiceSecurityCheck(xhr);
7255
7256						if (customParam != null)
7257						{
7258							xhr.setRequestHeader('x-convert-custom', customParam);
7259						}
7260
7261						xhr.onreadystatechange = mxUtils.bind(this, function()
7262						{
7263							if (xhr.readyState == 4)
7264							{
7265								if (xhr.status >= 200 && xhr.status <= 299)
7266								{
7267									try
7268									{
7269										var resp = xhr.response;
7270
7271										if (resp.type == 'text/xml')
7272										{
7273											var reader = new FileReader();
7274
7275											reader.onload = mxUtils.bind(this, function(e)
7276											{
7277												try
7278												{
7279													done(e.target.result);
7280												}
7281												catch (e)
7282												{
7283													onerror({message: mxResources.get('errorLoadingFile')});
7284												}
7285											});
7286
7287											reader.readAsText(resp);
7288										}
7289										else
7290										{
7291											this.doImportVisio(resp, done, onerror, filename);
7292										}
7293									}
7294									catch (e)
7295									{
7296										onerror(e);
7297									}
7298								}
7299								else
7300								{
7301									try
7302									{
7303										if (xhr.responseType == '' || xhr.responseType == 'text')
7304										{
7305											onerror({message: xhr.responseText});
7306										}
7307										else
7308										{
7309											var reader = new FileReader();
7310											reader.onload = function()
7311											{
7312												onerror({message: JSON.parse(reader.result).Message});
7313											}
7314											reader.readAsText(xhr.response);
7315										}
7316									}
7317									catch(e)
7318									{
7319										onerror({});
7320									}
7321								}
7322							}
7323						});
7324
7325						xhr.send(formData);
7326					}
7327					else
7328					{
7329						onerror({message: this.getServiceName() == 'conf'? mxResources.get('vsdNoConfig') : mxResources.get('serviceUnavailableOrBlocked')});
7330					}
7331				}
7332				else
7333				{
7334					try
7335					{
7336						this.doImportVisio(file, done, onerror, filename);
7337					}
7338					catch (e)
7339					{
7340						onerror(e);
7341					}
7342				}
7343			}
7344			else
7345			{
7346				this.spinner.stop();
7347				this.handleError({message: mxResources.get('serviceUnavailableOrBlocked')});
7348			}
7349		});
7350
7351		if (!this.doImportVisio && !this.loadingExtensions && !this.isOffline(true))
7352		{
7353			this.loadingExtensions = true;
7354			mxscript('js/extensions.min.js', delayed);
7355		}
7356		else
7357		{
7358			delayed();
7359		}
7360	};
7361
7362	/**
7363	 * Imports the given GraphML (yEd) file
7364	 */
7365	EditorUi.prototype.importGraphML = function(xmlData, done, onerror)
7366	{
7367		onerror = (onerror != null) ? onerror : mxUtils.bind(this, function(e)
7368		{
7369			this.handleError(e);
7370		});
7371
7372		var delayed = mxUtils.bind(this, function()
7373		{
7374			this.loadingExtensions = false;
7375
7376			if (this.doImportGraphML)
7377			{
7378
7379				try
7380				{
7381					this.doImportGraphML(xmlData, done, onerror);
7382				}
7383				catch (e)
7384				{
7385					onerror(e);
7386				}
7387			}
7388			else
7389			{
7390				this.spinner.stop();
7391				this.handleError({message: mxResources.get('serviceUnavailableOrBlocked')});
7392			}
7393		});
7394
7395		if (!this.doImportGraphML && !this.loadingExtensions && !this.isOffline(true))
7396		{
7397			this.loadingExtensions = true;
7398			mxscript('js/extensions.min.js', delayed);
7399		}
7400		else
7401		{
7402			delayed();
7403		}
7404	};
7405
7406	/**
7407	 * Export the diagram to VSDX
7408	 */
7409	EditorUi.prototype.exportVisio = function(currentPage)
7410	{
7411		var delayed = mxUtils.bind(this, function()
7412		{
7413			this.loadingExtensions = false;
7414
7415			if (typeof VsdxExport  !== 'undefined')
7416			{
7417				try
7418				{
7419					var expSuccess = new VsdxExport(this).exportCurrentDiagrams(currentPage);
7420
7421					if (!expSuccess)
7422					{
7423						this.handleError({message: mxResources.get('unknownError')});
7424					}
7425				}
7426				catch (e)
7427				{
7428					this.handleError(e);
7429				}
7430			}
7431			else
7432			{
7433				this.spinner.stop();
7434				this.handleError({message: mxResources.get('serviceUnavailableOrBlocked')});
7435			}
7436		});
7437
7438		if (typeof VsdxExport === 'undefined' && !this.loadingExtensions && !this.isOffline(true))
7439		{
7440			this.loadingExtensions = true;
7441			mxscript('js/extensions.min.js', delayed);
7442		}
7443		else
7444		{
7445			delayed();
7446		}
7447	};
7448
7449	/**
7450	 * Imports the given Lucidchart data.
7451	 */
7452	EditorUi.prototype.convertLucidChart = function(data, success, error)
7453	{
7454		var delayed = mxUtils.bind(this, function()
7455		{
7456			this.loadingExtensions = false;
7457
7458			// Checks for signature method
7459			if (typeof window.LucidImporter !== 'undefined')
7460			{
7461				try
7462				{
7463					EditorUi.logEvent({category: 'LUCIDCHART-IMPORT-FILE',
7464						action: 'size_' + data.length});
7465					EditorUi.debug('convertLucidChart', data);
7466				}
7467				catch (e)
7468				{
7469					// ignore
7470				}
7471
7472				try
7473				{
7474					success(LucidImporter.importState(JSON.parse(data)));
7475				}
7476				catch (e)
7477				{
7478					if (window.console != null)
7479					{
7480						console.error(e);
7481					}
7482
7483					error(e);
7484				}
7485			}
7486			else
7487			{
7488				error({message: mxResources.get('serviceUnavailableOrBlocked')});
7489			}
7490		});
7491
7492		if (typeof window.LucidImporter === 'undefined' &&
7493			!this.loadingExtensions && !this.isOffline(true))
7494		{
7495			this.loadingExtensions = true;
7496
7497			if (urlParams['dev'] == '1')
7498			{
7499				//Lucid org chart requires orgChart layout, in production, it is part of the extemsions.min.js
7500				mxscript('js/diagramly/Extensions.js', function()
7501				{
7502					mxscript('js/orgchart/bridge.min.js', function()
7503					{
7504						mxscript('js/orgchart/bridge.collections.min.js', function()
7505						{
7506							mxscript('js/orgchart/OrgChart.Layout.min.js', function()
7507							{
7508								mxscript('js/orgchart/mxOrgChartLayout.js', delayed);
7509							});
7510						});
7511					});
7512				});
7513			}
7514			else
7515			{
7516				mxscript('js/extensions.min.js', delayed);
7517			}
7518		}
7519		else
7520		{
7521			// Async needed for selection
7522			window.setTimeout(delayed, 0);
7523		}
7524	};
7525
7526	/**
7527	 * Generates a Mermaid image.
7528	 */
7529	EditorUi.prototype.generateMermaidImage = function(data, config, success, error)
7530	{
7531		var ui = this;
7532
7533		var delayed = function()
7534		{
7535			try
7536			{
7537				this.loadingMermaid = false;
7538
7539				config = (config != null) ? config : EditorUi.defaultMermaidConfig;
7540				config.securityLevel = 'strict';
7541				config.startOnLoad = false;
7542
7543				mermaid.mermaidAPI.initialize(config);
7544
7545				mermaid.mermaidAPI.render('geMermaidOutput-' + new Date().getTime(), data, function(svg)
7546				{
7547					try
7548					{
7549						// Workaround for namespace errors in SVG output for IE
7550						if (mxClient.IS_IE || mxClient.IS_IE11)
7551						{
7552							svg = svg.replace(/ xmlns:\S*="http:\/\/www.w3.org\/XML\/1998\/namespace"/g, '').
7553								replace(/ (NS xml|\S*):space="preserve"/g, ' xml:space="preserve"');
7554						}
7555
7556						var doc = mxUtils.parseXml(svg);
7557						var svgs = doc.getElementsByTagName('svg');
7558
7559						if (svgs.length > 0)
7560						{
7561							var w = parseFloat(svgs[0].getAttribute('width'));
7562							var h = parseFloat(svgs[0].getAttribute('height'));
7563
7564							if (isNaN(w) || isNaN(h))
7565							{
7566								try
7567								{
7568									var viewBox = svgs[0].getAttribute('viewBox').split(/\s+/);
7569									w = parseFloat(viewBox[2]);
7570									h = parseFloat(viewBox[3]);
7571								}
7572								catch(e)
7573								{
7574									//Any size such that it shows up
7575									w = w || 100;
7576									h = h || 100;
7577								}
7578							}
7579
7580							success(ui.convertDataUri(Editor.createSvgDataUri(svg)), w, h);
7581						}
7582						else
7583						{
7584							error({message: mxResources.get('invalidInput')});
7585						}
7586					}
7587					catch (e)
7588					{
7589						error(e);
7590					}
7591				});
7592			}
7593			catch (e)
7594			{
7595				error(e);
7596			}
7597		};
7598
7599		if (typeof mermaid === 'undefined' && !this.loadingMermaid && !this.isOffline(true))
7600		{
7601			this.loadingMermaid = true;
7602
7603			if (urlParams['dev'] == '1')
7604			{
7605				mxscript('js/mermaid/mermaid.min.js', delayed);
7606			}
7607			else
7608			{
7609				mxscript('js/extensions.min.js', delayed);
7610			}
7611		}
7612		else
7613		{
7614			delayed();
7615		}
7616	};
7617
7618	/**
7619	 * Generates a plant UML image. Possible types are svg, png and txt.
7620	 */
7621	EditorUi.prototype.generatePlantUmlImage = function(data, type, success, error)
7622	{
7623		function encode64(data)
7624		{
7625			r = "";
7626
7627			for (i = 0; i < data.length; i += 3)
7628			{
7629				if (i + 2 == data.length)
7630				{
7631					r += append3bytes(data.charCodeAt(i), data.charCodeAt(i + 1), 0);
7632				}
7633				else if (i + 1 == data.length)
7634				{
7635					r += append3bytes(data.charCodeAt(i), 0, 0);
7636				}
7637				else
7638				{
7639					r += append3bytes(data.charCodeAt(i), data.charCodeAt(i + 1),
7640						data.charCodeAt(i + 2));
7641				}
7642			}
7643
7644			return r;
7645		}
7646
7647		function append3bytes(b1, b2, b3)
7648		{
7649			c1 = b1 >> 2;
7650			c2 = ((b1 & 0x3) << 4) | (b2 >> 4);
7651			c3 = ((b2 & 0xF) << 2) | (b3 >> 6);
7652			c4 = b3 & 0x3F;
7653			r = "";
7654			r += encode6bit(c1 & 0x3F);
7655			r += encode6bit(c2 & 0x3F);
7656			r += encode6bit(c3 & 0x3F);
7657			r += encode6bit(c4 & 0x3F);
7658
7659			return r;
7660		}
7661
7662		function encode6bit(b)
7663		{
7664			if (b < 10)
7665			{
7666				return String.fromCharCode(48 + b);
7667			}
7668
7669			b -= 10;
7670
7671			if (b < 26)
7672			{
7673				return String.fromCharCode(65 + b);
7674			}
7675
7676			b -= 26;
7677
7678			if (b < 26)
7679			{
7680				return String.fromCharCode(97 + b);
7681			}
7682
7683			b -= 26;
7684
7685			if (b == 0)
7686			{
7687				return '-';
7688			}
7689
7690			if (b == 1)
7691			{
7692				return '_';
7693			}
7694
7695			return '?';
7696		}
7697
7698		// TODO: Remove unescape, use btoa for compatibility with graph.compress
7699		function compress(s)
7700		{
7701			return encode64(Graph.arrayBufferToString(pako.deflateRaw(s)));
7702		};
7703
7704		var plantUmlServerUrl = (type == 'txt') ? PLANT_URL + '/txt/' :
7705			((type == 'png') ? PLANT_URL + '/png/' : PLANT_URL + '/svg/');
7706
7707		var xhr = new XMLHttpRequest();
7708		xhr.open('GET', plantUmlServerUrl + compress(data), true);
7709
7710		if (type != 'txt')
7711		{
7712			xhr.responseType = 'blob';
7713		}
7714
7715		xhr.onload = function(e)
7716		{
7717			if (this.status >= 200 && this.status < 300)
7718			{
7719				if (type == 'txt')
7720				{
7721					success(this.response);
7722				}
7723				else
7724				{
7725					var reader = new FileReader();
7726					reader.readAsDataURL(this.response);
7727
7728					reader.onloadend = function(e)
7729					{
7730						var img = new Image();
7731
7732						img.onload = function()
7733						{
7734							try
7735							{
7736								var w = img.width;
7737								var h = img.height;
7738
7739								// Workaround for 0 image size in IE11
7740								if (w == 0 && h == 0)
7741								{
7742									var data = reader.result;
7743									var comma = data.indexOf(',');
7744									var svgText = decodeURIComponent(escape(atob(data.substring(comma + 1))));
7745									var root = mxUtils.parseXml(svgText);
7746									var svgs = root.getElementsByTagName('svg');
7747
7748									if (svgs.length > 0)
7749									{
7750										w = parseFloat(svgs[0].getAttribute('width'));
7751										h = parseFloat(svgs[0].getAttribute('height'));
7752									}
7753								}
7754
7755								success(reader.result, w, h);
7756							}
7757							catch (e)
7758							{
7759								error(e);
7760							}
7761						};
7762
7763						img.src = reader.result;
7764					};
7765
7766					reader.onerror = function(e)
7767					{
7768						error(e);
7769					};
7770				}
7771			}
7772			else
7773			{
7774				error(e);
7775			}
7776		};
7777
7778		xhr.onerror = function(e)
7779		{
7780			error(e);
7781		};
7782
7783		xhr.send();
7784	};
7785
7786	/**
7787	 * Inserts the given text as a preformatted HTML text.
7788	 */
7789	EditorUi.prototype.insertAsPreText = function(text, x, y)
7790	{
7791		var graph = this.editor.graph;
7792		var cell = null;
7793
7794		graph.getModel().beginUpdate();
7795		try
7796		{
7797			cell = graph.insertVertex(null, null, '<pre>' + text + '</pre>',
7798				x, y, 1, 1, 'text;html=1;align=left;verticalAlign=top;');
7799			graph.updateCellSize(cell, true);
7800		}
7801		finally
7802		{
7803			graph.getModel().endUpdate();
7804		}
7805
7806		return cell;
7807	};
7808
7809	/**
7810	 * Imports the given XML into the existing diagram.
7811	 * TODO: Make this function asynchronous
7812	 */
7813	EditorUi.prototype.insertTextAt = function(text, dx, dy, html, asImage, crop, resizeImages, addNewPage)
7814	{
7815		crop = (crop != null) ? crop : true;
7816		resizeImages = (resizeImages != null) ? resizeImages : true;
7817
7818		// Handles special case for Gliffy data which requires async server-side for parsing
7819		if (text != null)
7820		{
7821			if (Graph.fileSupport && !this.isOffline() && new XMLHttpRequest().upload && this.isRemoteFileFormat(text))
7822			{
7823				// Fixes possible parsing problems with ASCII 160 (non-breaking space)
7824				this.parseFile(new Blob([text.replace(/\s+/g,' ')], {type: 'application/octet-stream'}), mxUtils.bind(this, function(xhr)
7825				{
7826					if (xhr.readyState == 4 && xhr.status >= 200 && xhr.status <= 299)
7827					{
7828						this.editor.graph.setSelectionCells(this.insertTextAt(
7829							xhr.responseText, dx, dy, true));
7830					}
7831				}));
7832
7833				// Returns empty cells array as it is aysynchronous
7834				return [];
7835			}
7836			// Handles special case of data URI which requires async loading for finding size
7837			else if (text.substring(0, 5) == 'data:' || (!this.isOffline() &&
7838				(asImage || (/\.(gif|jpg|jpeg|tiff|png|svg)$/i).test(text))))
7839			{
7840				var graph = this.editor.graph;
7841
7842				// Checks for embedded XML in PDF
7843				if (text.substring(0, 28) == 'data:application/pdf;base64,')
7844	    		{
7845					var xml = Editor.extractGraphModelFromPdf(text);
7846
7847					if (xml != null && xml.length > 0)
7848					{
7849						return this.importXml(xml, dx, dy, crop, true, addNewPage);
7850					}
7851	    		}
7852
7853				// Checks for embedded XML in PNG
7854				if (text.substring(0, 22) == 'data:image/png;base64,')
7855				{
7856					var xml = this.extractGraphModelFromPng(text);
7857
7858					if (xml != null && xml.length > 0)
7859					{
7860						return this.importXml(xml, dx, dy, crop, true, addNewPage);
7861					}
7862				}
7863
7864				// Tries to extract embedded XML from SVG data URI
7865				if (text.substring(0, 19) == 'data:image/svg+xml;')
7866				{
7867					try
7868					{
7869						var xml = null;
7870
7871						if (text.substring(0, 26) == 'data:image/svg+xml;base64,')
7872						{
7873							xml = text.substring(text.indexOf(',') + 1);
7874							xml = (window.atob && !mxClient.IS_SF) ? atob(xml) : Base64.decode(xml, true);
7875						}
7876						else
7877						{
7878							xml = decodeURIComponent(text.substring(text.indexOf(',') + 1));
7879						}
7880
7881						var result = this.importXml(xml, dx, dy, crop, true, addNewPage);
7882
7883						if (result.length > 0)
7884						{
7885							return result;
7886						}
7887					}
7888					catch (e)
7889					{
7890						// Ignore
7891					}
7892				}
7893
7894				this.loadImage(text, mxUtils.bind(this, function(img)
7895				{
7896					if (text.substring(0, 5) == 'data:')
7897					{
7898						this.resizeImage(img, text, mxUtils.bind(this, function(data2, w2, h2)
7899	    				{
7900							graph.setSelectionCell(graph.insertVertex(null, null, '', graph.snap(dx), graph.snap(dy),
7901									w2, h2, 'shape=image;verticalLabelPosition=bottom;labelBackgroundColor=#ffffff;' +
7902									'verticalAlign=top;aspect=fixed;imageAspect=0;image=' + this.convertDataUri(data2) + ';'));
7903	    				}), resizeImages, this.maxImageSize);
7904					}
7905					else
7906					{
7907						var s = Math.min(1, Math.min(this.maxImageSize / img.width, this.maxImageSize / img.height));
7908						var w = Math.round(img.width * s);
7909						var h = Math.round(img.height * s);
7910
7911						graph.setSelectionCell(graph.insertVertex(null, null, '', graph.snap(dx), graph.snap(dy),
7912								w, h, 'shape=image;verticalLabelPosition=bottom;labelBackgroundColor=#ffffff;' +
7913								'verticalAlign=top;aspect=fixed;imageAspect=0;image=' + text + ';'));
7914					}
7915				}), mxUtils.bind(this, function()
7916				{
7917					var cell = null;
7918
7919					// Inserts invalid data URIs as text
7920			    	graph.getModel().beginUpdate();
7921			    	try
7922			    	{
7923						cell = graph.insertVertex(graph.getDefaultParent(), null, text,
7924								graph.snap(dx), graph.snap(dy), 1, 1, 'text;' + ((html) ? 'html=1;' : ''));
7925						graph.updateCellSize(cell);
7926						graph.fireEvent(new mxEventObject('textInserted', 'cells', [cell]));
7927			    	}
7928			    	finally
7929			    	{
7930			    		graph.getModel().endUpdate();
7931			    	}
7932
7933					graph.setSelectionCell(cell);
7934				}));
7935
7936				return [];
7937			}
7938			else
7939			{
7940				text = Graph.zapGremlins(mxUtils.trim(text));
7941
7942				if (this.isCompatibleString(text))
7943				{
7944					return this.importXml(text, dx, dy, crop, null, addNewPage);
7945				}
7946				else if (text.length > 0)
7947				{
7948					if (this.isLucidChartData(text))
7949					{
7950						this.convertLucidChart(text, mxUtils.bind(this, function(xml)
7951						{
7952							this.editor.graph.setSelectionCells(
7953								this.importXml(xml, dx, dy, crop,
7954								null, addNewPage));
7955						}), mxUtils.bind(this, function(e)
7956						{
7957							this.handleError(e);
7958						}));
7959					}
7960					else
7961					{
7962						var graph = this.editor.graph;
7963						var cell = null;
7964
7965				    	graph.getModel().beginUpdate();
7966				    	try
7967				    	{
7968				    		// Fires cellsInserted to apply the current style to the inserted text.
7969				    		// This requires the value to be empty when the event is fired.
7970				    		cell = graph.insertVertex(graph.getDefaultParent(), null, '',
7971								graph.snap(dx), graph.snap(dy), 1, 1, 'text;whiteSpace=wrap;' + ((html) ? 'html=1;' : ''));
7972				    		graph.fireEvent(new mxEventObject('textInserted', 'cells', [cell]));
7973
7974				    		// Single tag is converted
7975				    		if (text.charAt(0) == '<' && text.indexOf('>') == text.length - 1)
7976				    		{
7977				    			text = mxUtils.htmlEntities(text);
7978				    		}
7979
7980				    		//TODO Refuse unsupported file types early as at this stage a lot of processing has beed done and time is wasted.
7981				    		//		For example, 5 MB PDF files is processed and then only 0.5 MB of meaningless text is added!
7982				    		//Limit labels to maxTextBytes
7983				    		if (text.length > this.maxTextBytes)
7984			    			{
7985				    			text = text.substring(0, this.maxTextBytes) + '...';
7986			    			}
7987
7988							// Apply value and updates the cell size to fit the text block
7989							cell.value = text;
7990							graph.updateCellSize(cell);
7991
7992							// Adds wrapping for large text blocks
7993							if (this.maxTextWidth > 0 && cell.geometry.width > this.maxTextWidth)
7994							{
7995								var size = graph.getPreferredSizeForCell(cell, this.maxTextWidth);
7996								cell.geometry.width = size.width;
7997								cell.geometry.height = size.height;
7998							}
7999
8000							// See https://stackoverflow.com/questions/5717093/check-if-a-javascript-string-is-a-url
8001							if (Graph.isLink(cell.value))
8002							{
8003								graph.setLinkForCell(cell, cell.value);
8004							}
8005
8006							// Adds spacing
8007							cell.geometry.width += graph.gridSize;
8008							cell.geometry.height += graph.gridSize;
8009				    	}
8010				    	finally
8011				    	{
8012				    		graph.getModel().endUpdate();
8013				    	}
8014
8015						return [cell];
8016					}
8017				}
8018			}
8019		}
8020
8021		return [];
8022	};
8023
8024	/**
8025	 * Formats the given file size.
8026	 */
8027	EditorUi.prototype.formatFileSize = function(size)
8028	{
8029	    var units = [' kB', ' MB', ' GB', ' TB', 'PB', 'EB', 'ZB', 'YB'];
8030		var i = -1;
8031
8032	    do
8033	    {
8034	    	size = size / 1024;
8035	        i++;
8036	    } while (size > 1024);
8037
8038	    return Math.max(size, 0.1).toFixed(1) + units[i];
8039	};
8040
8041	/**
8042	 * Imports the given XML into the existing diagram.
8043	 */
8044	EditorUi.prototype.convertDataUri = function(uri)
8045	{
8046		// Handles special case of data URI which needs to be rewritten
8047		// to be used in a cell style to remove the semicolon
8048		if (uri.substring(0, 5) == 'data:')
8049		{
8050			var semi = uri.indexOf(';');
8051
8052			if (semi > 0)
8053			{
8054				uri = uri.substring(0, semi) + uri.substring(uri.indexOf(',', semi + 1));
8055			}
8056		}
8057
8058		return uri;
8059	};
8060
8061	/**
8062	 * Returns true for Gliffy data.
8063	 */
8064	EditorUi.prototype.isRemoteFileFormat = function(data, filename)
8065	{
8066		return /(\"contentType\":\s*\"application\/gliffy\+json\")/.test(data);
8067	};
8068
8069	/**
8070	 * Returns true for Gliffy
8071	 */
8072	EditorUi.prototype.isLucidChartData = function(data)
8073	{
8074		return data != null && (data.substring(0, 26) ==
8075			'{"state":"{\\"Properties\\":' ||
8076			data.substring(0, 14) == '{"Properties":');
8077	};
8078
8079	/**
8080	 * Imports a local file from the device or local storage.
8081	 */
8082	EditorUi.prototype.importLocalFile = function(device, noSplash)
8083	{
8084		if (device && Graph.fileSupport)
8085		{
8086			if (this.importFileInputElt == null)
8087			{
8088				var input = document.createElement('input');
8089				input.setAttribute('type', 'file');
8090
8091				mxEvent.addListener(input, 'change', mxUtils.bind(this, function()
8092				{
8093					if (input.files != null)
8094					{
8095						// Using null for position will disable crop of input file
8096						this.importFiles(input.files, null, null, this.maxImageSize);
8097
8098			    		// Resets input to force change event for same file (type reset required for IE)
8099						input.type = '';
8100						input.type = 'file';
8101			    		input.value = '';
8102					}
8103				}));
8104
8105				input.style.display = 'none';
8106				document.body.appendChild(input);
8107				this.importFileInputElt = input;
8108			}
8109
8110			this.importFileInputElt.click();
8111		}
8112		else
8113		{
8114			window.openNew = false;
8115			window.openKey = 'import';
8116
8117			window.listBrowserFiles = mxUtils.bind(this, function(success, error)
8118			{
8119				StorageFile.listFiles(this, 'F', success, error);
8120			});
8121
8122			window.openBrowserFile = mxUtils.bind(this, function(title, success, error)
8123			{
8124				StorageFile.getFileContent(this, title, success, error);
8125			});
8126
8127			window.deleteBrowserFile = mxUtils.bind(this, function(title, success, error)
8128			{
8129				StorageFile.deleteFile(this, title, success, error);
8130			});
8131
8132			if (!noSplash)
8133			{
8134				var prevValue = Editor.useLocalStorage;
8135				Editor.useLocalStorage = !device;
8136			}
8137
8138			// Closes dialog after open
8139			window.openFile = new OpenFile(mxUtils.bind(this, function(cancel)
8140			{
8141				this.hideDialog(cancel);
8142			}));
8143
8144			window.openFile.setConsumer(mxUtils.bind(this, function(xml, filename)
8145			{
8146				if (filename != null && Graph.fileSupport && /(\.v(dx|sdx?))($|\?)/i.test(filename))
8147				{
8148					// "Not a UTF 8 file" when opening VSDX in IE so this is never called
8149					var file = new Blob([xml], {type: 'application/octet-stream'})
8150
8151					this.importVisio(file, mxUtils.bind(this, function(xml)
8152					{
8153						this.importXml(xml, 0, 0, true);
8154					}), null, filename);
8155				}
8156				else
8157				{
8158					this.editor.graph.setSelectionCells(this.importXml(xml, 0, 0, true));
8159				}
8160			}));
8161
8162			// Removes openFile if dialog is closed
8163			this.showDialog(new OpenDialog(this).container,  (Editor.useLocalStorage) ? 640 : 360,
8164				(Editor.useLocalStorage) ? 480 : 220, true, true, function()
8165			{
8166				window.openFile = null;
8167			});
8168
8169			// Extends dialog close to show splash screen
8170			if (!noSplash)
8171			{
8172				var dlg = this.dialog;
8173				var dlgClose = dlg.close;
8174
8175				this.dialog.close = mxUtils.bind(this, function(cancel)
8176				{
8177					Editor.useLocalStorage = prevValue;
8178					dlgClose.apply(dlg, arguments);
8179
8180					if (cancel && this.getCurrentFile() == null && urlParams['embed'] != '1')
8181					{
8182						this.showSplash();
8183					}
8184				});
8185			}
8186		}
8187	};
8188
8189	/**
8190	 * Imports the given zip file.
8191	 */
8192	EditorUi.prototype.importZipFile = function(file, success, onerror)
8193	{
8194		var ui = this;
8195
8196		var delayed = mxUtils.bind(this, function()
8197		{
8198			this.loadingExtensions = false;
8199
8200			if (typeof JSZip  !== 'undefined')
8201			{
8202				JSZip.loadAsync(file)
8203		        .then(function(zip)
8204		        {
8205		        	if (Object.keys(zip.files).length == 0)
8206		        	{
8207		        		onerror();
8208		        	}
8209		        	else
8210		        	{
8211		        		var gliffyLatestVer = {version: 0};
8212		        		var drawioFound = false;
8213
8214		                zip.forEach(function (relativePath, zipEntry)
8215		                {
8216		                	var name = zipEntry.name.toLowerCase();
8217
8218		                    if (name == 'diagram/diagram.xml') //draw.io zip format has the latest diagram version at diagram/diagram.xml
8219		                    {
8220		                    	drawioFound = true;
8221
8222			                    zipEntry.async("string").then(function(str){
8223			                    	if (str.indexOf('<mxfile ') == 0)
8224			                    	{
8225			                    		success(str);
8226			                    	}
8227			                    	else
8228		                    		{
8229			                    		onerror();
8230		                    		}
8231			                    });
8232		                    }
8233		                    else if (name.indexOf('versions/') == 0) //Gliffy zip format has the versions inside versions folder
8234		                   	{
8235		                    	var version = parseInt(name.substr(9)); //9 is the length of versions/
8236
8237		                    	if (version > gliffyLatestVer.version)
8238		                    	{
8239		                    		gliffyLatestVer = {version: version, zipEntry: zipEntry}
8240		                    	}
8241		                   	}
8242		                });
8243
8244		                if (gliffyLatestVer.version > 0)
8245		            	{
8246		                	gliffyLatestVer.zipEntry.async("string").then(function(data)
8247		                	{
8248		                		if (!ui.isOffline() && new XMLHttpRequest().upload && ui.isRemoteFileFormat(data, file.name))
8249		                		{
8250		                			ui.parseFile(new Blob([data], {type: 'application/octet-stream'}), mxUtils.bind(this, function(xhr)
8251		                			{
8252		                				if (xhr.readyState == 4)
8253		                				{
8254		                					if (xhr.status >= 200 && xhr.status <= 299)
8255		                					{
8256		                						success(xhr.responseText);
8257		                					}
8258		                					else
8259		                					{
8260		                						onerror();
8261		                					}
8262		                				}
8263		                			}), file.name);
8264		                		}
8265		                		else
8266		            			{
8267		                			onerror();
8268		            			}
8269		                	});
8270		            	}
8271		                else if (!drawioFound)
8272		            	{
8273		                	onerror();
8274		            	}
8275		        	}
8276		        }, function (e) {
8277		    		onerror(e);
8278		        });
8279			}
8280			else
8281			{
8282				onerror();
8283			}
8284		});
8285
8286		if (typeof JSZip === 'undefined' && !this.loadingExtensions && !this.isOffline(true))
8287		{
8288			this.loadingExtensions = true;
8289			mxscript('js/extensions.min.js', delayed);
8290		}
8291		else
8292		{
8293			delayed();
8294		}
8295	};
8296
8297	/**
8298	 * Imports the given XML into the existing diagram.
8299	 */
8300	EditorUi.prototype.importFile = function(data, mimeType, dx, dy, w, h, filename,
8301		done, file, crop, ignoreEmbeddedXml, evt)
8302	{
8303		crop = (crop != null) ? crop : true;
8304		var async = false;
8305		var cells = null;
8306
8307		var handleResult = mxUtils.bind(this, function(xml)
8308		{
8309			var importedCells = null;
8310
8311			if (xml != null && xml.substring(0, 10) == '<mxlibrary')
8312			{
8313				this.loadLibrary(new LocalLibrary(this, xml, filename));
8314			}
8315			else
8316			{
8317				importedCells = this.importXml(xml, dx, dy, crop, null,
8318					(evt != null) ? mxEvent.isControlDown(evt) : null);
8319			}
8320
8321			if (done != null)
8322			{
8323				done(importedCells);
8324			}
8325		});
8326
8327		if (mimeType.substring(0, 5) == 'image')
8328		{
8329			var containsModel = false;
8330
8331			if (mimeType.substring(0, 9) == 'image/png')
8332			{
8333				var xml = (ignoreEmbeddedXml) ? null : this.extractGraphModelFromPng(data);
8334
8335				if (xml != null && xml.length > 0)
8336				{
8337					cells = this.importXml(xml, dx, dy, crop, null, (evt != null) ?
8338						mxEvent.isControlDown(evt) : null);
8339					containsModel = true;
8340				}
8341			}
8342
8343			if (!containsModel)
8344			{
8345				var graph = this.editor.graph;
8346
8347				// Strips encoding bit (eg. ;base64,) for cell style
8348				var semi = data.indexOf(';');
8349
8350				if (semi > 0)
8351				{
8352					data = data.substring(0, semi) + data.substring(data.indexOf(',', semi + 1));
8353				}
8354
8355				if (crop && graph.isGridEnabled())
8356				{
8357					dx = graph.snap(dx);
8358					dy = graph.snap(dy);
8359				}
8360
8361				cells = [graph.insertVertex(null, null, '', dx, dy, w, h,
8362					'shape=image;verticalLabelPosition=bottom;labelBackgroundColor=#ffffff;' +
8363					'verticalAlign=top;aspect=fixed;imageAspect=0;image=' + data + ';')];
8364			}
8365		}
8366		else if (/(\.*<graphml )/.test(data))
8367        {
8368			async = true;
8369
8370			this.importGraphML(data, handleResult);
8371        }
8372		else if (file != null && filename != null && ((/(\.v(dx|sdx?))($|\?)/i.test(filename)) || /(\.vs(x|sx?))($|\?)/i.test(filename)))
8373		{
8374			//  LATER: done and async are a hack before making this asynchronous
8375			async = true;
8376
8377			this.importVisio(file, handleResult);
8378		}
8379		else if (!this.isOffline() && new XMLHttpRequest().upload && this.isRemoteFileFormat(data, filename))
8380		{
8381			//  LATER: done and async are a hack before making this asynchronous
8382			async = true;
8383
8384			// Returns empty cells array as it is aysynchronous
8385			this.parseFile((file != null) ? file : new Blob([data], {type: 'application/octet-stream'}), mxUtils.bind(this, function(xhr)
8386			{
8387				if (xhr.readyState == 4)
8388				{
8389					if (xhr.status >= 200 && xhr.status <= 299)
8390					{
8391						handleResult(xhr.responseText);
8392					}
8393					else if (done != null)
8394					{
8395						done(null);
8396					}
8397				}
8398			}), filename);
8399		}
8400		else if (data.indexOf('PK') == 0 && file != null)
8401		{
8402			async = true;
8403
8404			this.importZipFile(file, handleResult, mxUtils.bind(this, function()
8405			{
8406				//If importing as a zip file failed, just insert as text
8407				cells = this.insertTextAt(this.validateFileData(data), dx, dy, true, null, crop);
8408				done(cells);
8409			}));
8410		}
8411		else if (!/(\.v(sd|dx))($|\?)/i.test(filename) && !/(\.vs(s|x))($|\?)/i.test(filename))
8412		{
8413			cells = this.insertTextAt(this.validateFileData(data), dx, dy, true,
8414				null, crop, null, (evt != null) ? mxEvent.isControlDown(evt) : null);
8415		}
8416
8417		if (!async && done != null)
8418		{
8419			done(cells);
8420		}
8421
8422		return cells;
8423	};
8424
8425	/**
8426	 *
8427	 */
8428	EditorUi.prototype.importFiles = function(files, x, y, maxSize, fn, resultFn, filterFn, barrierFn,
8429		resizeDialog, maxBytes, resampleThreshold, ignoreEmbeddedXml, evt)
8430	{
8431		maxSize = (maxSize != null) ? maxSize : this.maxImageSize;
8432		maxBytes = (maxBytes != null) ? maxBytes : this.maxImageBytes;
8433
8434		var crop = x != null && y != null;
8435		var resizeImages = true;
8436		x = (x != null) ? x : 0;
8437		y = (y != null) ? y : 0;
8438
8439		// Checks if large images are imported
8440		var largeImages = false;
8441
8442		if (!mxClient.IS_CHROMEAPP && files != null)
8443		{
8444			var thresh = resampleThreshold || this.resampleThreshold;
8445
8446			for (var i = 0; i < files.length; i++)
8447			{
8448				if (files[i].type.substring(0, 6) == 'image/' && files[i].size > thresh)
8449				{
8450					largeImages = true;
8451
8452					break;
8453				}
8454			}
8455		}
8456
8457		var doImportFiles = mxUtils.bind(this, function()
8458		{
8459			var graph = this.editor.graph;
8460			var gs = graph.gridSize;
8461
8462			fn = (fn != null) ? fn : mxUtils.bind(this, function(data, mimeType, x, y, w, h, filename, done, file)
8463			{
8464				try
8465				{
8466					if (data != null && data.substring(0, 10) == '<mxlibrary')
8467					{
8468						this.spinner.stop();
8469						this.loadLibrary(new LocalLibrary(this, data, filename));
8470
8471		    			return null;
8472					}
8473					else
8474					{
8475						return this.importFile(data, mimeType, x, y, w, h, filename,
8476							done, file, crop, ignoreEmbeddedXml, evt);
8477					}
8478				}
8479				catch (e)
8480				{
8481					this.handleError(e);
8482
8483					return null;
8484				}
8485			});
8486
8487			resultFn = (resultFn != null) ? resultFn : mxUtils.bind(this, function(cells)
8488			{
8489				graph.setSelectionCells(cells);
8490			});
8491
8492			if (this.spinner.spin(document.body, mxResources.get('loading')))
8493			{
8494				var count = files.length;
8495				var remain = count;
8496				var queue = [];
8497
8498				// Barrier waits for all files to be loaded asynchronously
8499				var barrier = mxUtils.bind(this, function(index, fnc)
8500				{
8501					queue[index] = fnc;
8502
8503					if (--remain == 0)
8504					{
8505						this.spinner.stop();
8506
8507						if (barrierFn != null)
8508						{
8509							barrierFn(queue);
8510						}
8511						else
8512						{
8513							var cells = [];
8514
8515							graph.getModel().beginUpdate();
8516							try
8517							{
8518						    	for (var j = 0; j < queue.length; j++)
8519						    	{
8520						    		var tmp = queue[j]();
8521
8522						    		if (tmp != null)
8523						    		{
8524						    			cells = cells.concat(tmp);
8525						    		}
8526						    	}
8527							}
8528							finally
8529							{
8530								graph.getModel().endUpdate();
8531							}
8532						}
8533
8534						resultFn(cells);
8535					}
8536				});
8537
8538				for (var i = 0; i < count; i++)
8539				{
8540					(mxUtils.bind(this, function(index)
8541					{
8542						var file = files[index];
8543
8544						if (file != null)
8545						{
8546							var reader = new FileReader();
8547
8548							reader.onload = mxUtils.bind(this, function(e)
8549							{
8550								if (filterFn == null || filterFn(file))
8551								{
8552									if (file.type.substring(0, 6) == 'image/')
8553						    		{
8554						    			if (file.type.substring(0, 9) == 'image/svg')
8555						    			{
8556						    				// Checks if SVG contains content attribute
8557					    					var data = Graph.clipSvgDataUri(e.target.result);
8558					    					var comma = data.indexOf(',');
8559					    					var svgText = decodeURIComponent(escape(atob(data.substring(comma + 1))));
8560					    					var root = mxUtils.parseXml(svgText);
8561				    						var svgs = root.getElementsByTagName('svg');
8562
8563				    						if (svgs.length > 0)
8564					    					{
8565				    							var svgRoot = svgs[0];
8566						    					var cont = (ignoreEmbeddedXml) ? null : svgRoot.getAttribute('content');
8567
8568						    					if (cont != null && cont.charAt(0) != '<' && cont.charAt(0) != '%')
8569						    					{
8570						    						cont = unescape((window.atob) ? atob(cont) : Base64.decode(cont, true));
8571						    					}
8572
8573						    					if (cont != null && cont.charAt(0) == '%')
8574						    					{
8575						    						cont = decodeURIComponent(cont);
8576						    					}
8577
8578						    					if (cont != null && (cont.substring(0, 8) === '<mxfile ' ||
8579						    						cont.substring(0, 14) === '<mxGraphModel '))
8580						    					{
8581						    						barrier(index, mxUtils.bind(this, function()
8582								    				{
8583								    					return fn(cont, 'text/xml', x + index * gs, y + index * gs, 0, 0, file.name);
8584								    				}));
8585						    					}
8586						    					else
8587						    					{
8588								    				// SVG needs special handling to add viewbox if missing and
8589								    				// find initial size from SVG attributes (only for IE11)
8590								    				barrier(index, mxUtils.bind(this, function()
8591								    				{
8592							    						try
8593							    						{
8594									    					var prefix = data.substring(0, comma + 1);
8595
8596									    					// Parses SVG and find width and height
8597									    					if (root != null)
8598									    					{
8599									    						var svgs = root.getElementsByTagName('svg');
8600
8601									    						if (svgs.length > 0)
8602										    					{
8603									    							var svgRoot = svgs[0];
8604										    						var w = svgRoot.getAttribute('width');
8605										    						var h = svgRoot.getAttribute('height');
8606
8607										    						if (w != null && w.charAt(w.length - 1) != '%')
8608									    							{
8609									    								w = parseFloat(w);
8610									    							}
8611									    							else
8612									    							{
8613									    								w = NaN;
8614									    							}
8615
8616										    						if (h != null && h.charAt(h.length - 1) != '%')
8617									    							{
8618									    								h = parseFloat(h);
8619									    							}
8620									    							else
8621									    							{
8622									    								h = NaN;
8623									    							}
8624
8625										    						// Check if viewBox attribute already exists
8626										    						var vb = svgRoot.getAttribute('viewBox');
8627
8628										    						if (vb == null || vb.length == 0)
8629										    						{
8630										    							svgRoot.setAttribute('viewBox', '0 0 ' + w + ' ' + h);
8631										    						}
8632										    						// Uses width and height from viewbox for
8633										    						// missing width and height attributes
8634										    						else if (isNaN(w) || isNaN(h))
8635										    						{
8636										    							var tokens = vb.split(' ');
8637
8638										    							if (tokens.length > 3)
8639										    							{
8640										    								w = parseFloat(tokens[2]);
8641										    								h = parseFloat(tokens[3]);
8642										    							}
8643										    						}
8644
8645										    						data = Editor.createSvgDataUri(mxUtils.getXml(svgRoot));
8646										    						var s = Math.min(1, Math.min(maxSize / Math.max(1, w)), maxSize / Math.max(1, h));
8647										    						var cells = fn(data, file.type, x + index * gs, y + index * gs, Math.max(
8648										    							1, Math.round(w * s)), Math.max(1, Math.round(h * s)), file.name);
8649
8650										    						// Hack to fix width and height asynchronously
8651										    						if (isNaN(w) || isNaN(h))
8652										    						{
8653										    							var img = new Image();
8654
8655										    							img.onload = mxUtils.bind(this, function()
8656										    							{
8657										    								w = Math.max(1, img.width);
8658										    								h = Math.max(1, img.height);
8659
8660										    								cells[0].geometry.width = w;
8661										    								cells[0].geometry.height = h;
8662
8663										    								svgRoot.setAttribute('viewBox', '0 0 ' + w + ' ' + h);
8664										    								data = Editor.createSvgDataUri(mxUtils.getXml(svgRoot));
8665
8666										    								var semi = data.indexOf(';');
8667
8668										    								if (semi > 0)
8669										    								{
8670										    									data = data.substring(0, semi) + data.substring(data.indexOf(',', semi + 1));
8671										    								}
8672
8673										    								graph.setCellStyles('image', data, [cells[0]]);
8674										    							});
8675
8676										    							img.src = Editor.createSvgDataUri(mxUtils.getXml(svgRoot));
8677										    						}
8678
8679										    						return cells;
8680										    					}
8681									    					}
8682							    						}
8683							    						catch (e)
8684							    						{
8685							    							// ignores any SVG parsing errors
8686							    						}
8687
8688								    					return null;
8689								    				}));
8690						    					}
8691					    					}
8692				    						else
8693				    						{
8694					    						barrier(index, mxUtils.bind(this, function()
8695							    				{
8696							    					return null;
8697							    				}));
8698				    						}
8699						    			}
8700						    			else
8701						    			{
8702						    				// Checks if PNG+XML is available to bypass code below
8703						    				var containsModel = false;
8704
8705						    				if (file.type == 'image/png')
8706						    				{
8707						    					var xml = (ignoreEmbeddedXml) ? null : this.extractGraphModelFromPng(e.target.result);
8708
8709						    					if (xml != null && xml.length > 0)
8710						    					{
8711						    						var img = new Image();
8712						    						img.src = e.target.result;
8713
8714								    				barrier(index, mxUtils.bind(this, function()
8715								    				{
8716								    					return fn(xml, 'text/xml', x + index * gs, y + index * gs,
8717								    						img.width, img.height, file.name);
8718								    				}));
8719
8720						    						containsModel = true;
8721						    					}
8722						    				}
8723
8724							    			// Additional asynchronous step for finding image size
8725						    				if (!containsModel)
8726						    				{
8727						    					// Cannot load local files in Chrome App
8728						    					if (mxClient.IS_CHROMEAPP)
8729						    					{
8730						    						this.spinner.stop();
8731						    						this.showError(mxResources.get('error'), mxResources.get('dragAndDropNotSupported'),
8732						    							mxResources.get('cancel'), mxUtils.bind(this, function()
8733					    								{
8734					    									// Hides the dialog
8735					    								}), null, mxResources.get('ok'), mxUtils.bind(this, function()
8736					    								{
8737						    								// Redirects to import function
8738					    									this.actions.get('import').funct();
8739					    								})
8740					    							);
8741						    					}
8742						    					else
8743						    					{
8744									    			this.loadImage(e.target.result, mxUtils.bind(this, function(img)
8745									    			{
8746									    				this.resizeImage(img, e.target.result, mxUtils.bind(this, function(data2, w2, h2)
8747									    				{
8748										    				barrier(index, mxUtils.bind(this, function()
8749												    		{
8750										    					// Refuses to insert images above a certain size as they kill the app
8751										    					if (data2 != null && data2.length < maxBytes)
8752										    					{
8753											    					var s = (!resizeImages || !this.isResampleImageSize(file.size, resampleThreshold)) ? 1 : Math.min(1, Math.min(maxSize / w2, maxSize / h2));
8754
8755											    					return fn(data2, file.type, x + index * gs, y + index * gs, Math.round(w2 * s), Math.round(h2 * s), file.name);
8756										    					}
8757										    					else
8758										    					{
8759										    						this.handleError({message: mxResources.get('imageTooBig')});
8760
8761										    						return null;
8762										    					}
8763												    		}));
8764									    				}), resizeImages, maxSize, resampleThreshold, file.size);
8765									    			}), mxUtils.bind(this, function()
8766									    			{
8767									    				this.handleError({message: mxResources.get('invalidOrMissingFile')});
8768									    			}));
8769						    					}
8770						    				}
8771						    			}
8772						    		}
8773						    		else
8774						    		{
8775						    			var data = e.target.result;
8776
8777										fn(data, file.type, x + index * gs, y + index * gs, 240, 160, file.name, function(cells)
8778										{
8779											barrier(index, function()
8780				    	    				{
8781				    		    				return cells;
8782				    	    				});
8783										}, file);
8784						    		}
8785								}
8786							});
8787
8788							// Handles special cases
8789							if (/(\.v(dx|sdx?))($|\?)/i.test(file.name) || /(\.vs(x|sx?))($|\?)/i.test(file.name))
8790							{
8791								fn(null, file.type, x + index * gs, y + index * gs, 240, 160, file.name, function(cells)
8792								{
8793									barrier(index, function()
8794		    	    				{
8795		    		    				return cells;
8796		    	    				});
8797								}, file);
8798							}
8799							else if (file.type.substring(0, 5) == 'image' || file.type == 'application/pdf')
8800							{
8801								reader.readAsDataURL(file);
8802							}
8803							else
8804							{
8805								reader.readAsText(file);
8806							}
8807						}
8808					}))(i);
8809				}
8810			}
8811		});
8812
8813		if (largeImages)
8814		{
8815			// Workaround for lost files array in async code
8816			var tmp = [];
8817
8818			for (var i = 0; i < files.length; i++)
8819			{
8820				tmp.push(files[i]);
8821			}
8822
8823			files = tmp;
8824
8825			this.confirmImageResize(function(doResize)
8826			{
8827				resizeImages = doResize;
8828				doImportFiles();
8829			}, resizeDialog);
8830		}
8831		else
8832		{
8833			doImportFiles();
8834		}
8835	};
8836
8837	/**
8838	 * Parses the file using XHR2 via the server. File can be a blob or file object.
8839	 * Filename is an optional parameter for blobs (that do not have a filename).
8840	 */
8841	EditorUi.prototype.confirmImageResize = function(fn, force)
8842	{
8843		force = (force != null) ? force : false;
8844		var resume = (this.spinner != null && this.spinner.pause != null) ? this.spinner.pause() : function() {};
8845		var resizeImages = (isLocalStorage || mxClient.IS_CHROMEAPP) ? mxSettings.getResizeImages() : null;
8846
8847		var wrapper = function(remember, resize)
8848		{
8849			if (remember || force)
8850			{
8851				mxSettings.setResizeImages((remember) ? resize : null);
8852				mxSettings.save();
8853			}
8854
8855			resume();
8856			fn(resize);
8857		};
8858
8859		if (resizeImages != null && !force)
8860		{
8861			wrapper(false, resizeImages);
8862		}
8863		else
8864		{
8865			this.showDialog(new ConfirmDialog(this, mxResources.get('resizeLargeImages'),
8866			function(remember)
8867			{
8868				wrapper(remember, true);
8869			},
8870			function(remember)
8871			{
8872				wrapper(remember, false);
8873			}, mxResources.get('resize'), mxResources.get('actualSize'),
8874			'<img style="margin-top:8px;" src="' + Editor.loResImage + '"/>',
8875			'<img style="margin-top:8px;" src="' + Editor.hiResImage + '"/>',
8876			isLocalStorage || mxClient.IS_CHROMEAPP).container, 340,
8877			(isLocalStorage || mxClient.IS_CHROMEAPP) ? 220 : 200, true, true);
8878		}
8879	};
8880
8881	/**
8882	 * Parses the file using XHR2 via the server. File can be a blob or file object.
8883	 * Filename is an optional parameter for blobs (that do not have a filename).
8884	 */
8885	EditorUi.prototype.parseFile = function(file, fn, filename)
8886	{
8887		filename = (filename != null) ? filename : file.name;
8888
8889		var formData = new FormData();
8890		formData.append('format', 'xml');
8891		formData.append('upfile', file, filename);
8892
8893		var xhr = new XMLHttpRequest();
8894		xhr.open('POST', OPEN_URL);
8895
8896		xhr.onreadystatechange = function()
8897		{
8898			fn(xhr);
8899		};
8900
8901		xhr.send(formData);
8902
8903		try
8904		{
8905			EditorUi.logEvent({category: 'GLIFFY-IMPORT-FILE',
8906				action: 'size_' + file.size});
8907		}
8908		catch (e)
8909		{
8910			// ignore
8911		}
8912	};
8913
8914	/**
8915	 *
8916	 */
8917	EditorUi.prototype.isResampleImageSize = function(size, thresh)
8918	{
8919		thresh = (thresh != null) ? thresh : this.resampleThreshold;
8920
8921		return size > thresh;
8922	};
8923
8924	/**
8925	 * Resizes the given image if <maxImageBytes> is not null.
8926	 */
8927	EditorUi.prototype.resizeImage = function(img, data, fn, enabled, maxSize, thresh, fileSize)
8928	{
8929		maxSize = (maxSize != null) ? maxSize : this.maxImageSize;
8930		var w = Math.max(1, img.width);
8931		var h = Math.max(1, img.height);
8932
8933		if (enabled && this.isResampleImageSize((fileSize != null) ? fileSize : data.length, thresh))
8934		{
8935			try
8936			{
8937				var factor = Math.max(w / maxSize, h / maxSize);
8938
8939				if (factor > 1)
8940				{
8941					var w2 = Math.round(w / factor);
8942					var h2 = Math.round(h / factor);
8943
8944					var canvas = document.createElement('canvas');
8945				    canvas.width = w2;
8946				    canvas.height = h2;
8947
8948				    var ctx = canvas.getContext('2d');
8949				    ctx.drawImage(img, 0, 0, w2, h2);
8950
8951				    var tmp = canvas.toDataURL();
8952
8953				    // Uses new image if smaller
8954				    if (tmp.length < data.length)
8955				    {
8956				    	// Checks if the image is empty by comparing
8957				    	// with an empty image of the same size
8958				    	var canvas2 = document.createElement('canvas');
8959						canvas2.width = w2;
8960					    canvas2.height = h2;
8961					    var tmp2 = canvas2.toDataURL();
8962
8963					    if (tmp !== tmp2)
8964					    {
8965					    	data = tmp;
8966					    	w = w2;
8967					    	h = h2;
8968					    }
8969				    }
8970				}
8971			}
8972			catch (e)
8973			{
8974				// ignores image scaling errors
8975			}
8976		}
8977
8978		fn(data, w, h);
8979	};
8980
8981	/**
8982	 * Extracts the XML from the compressed or non-compressed text chunk.
8983	 */
8984	EditorUi.prototype.extractGraphModelFromPng = function(data)
8985	{
8986		return Editor.extractGraphModelFromPng(data);
8987	};
8988
8989	/**
8990	 * Loads the image from the given URI.
8991	 *
8992	 * @param {number} dx X-coordinate of the translation.
8993	 * @param {number} dy Y-coordinate of the translation.
8994	 */
8995	EditorUi.prototype.loadImage = function(uri, onload, onerror)
8996	{
8997		try
8998		{
8999			var img = new Image();
9000
9001			img.onload = function()
9002			{
9003				img.width = (img.width > 0) ? img.width : 120;
9004				img.height = (img.height > 0) ? img.height : 120;
9005
9006				onload(img);
9007			};
9008
9009			if (onerror != null)
9010			{
9011				img.onerror = onerror;
9012			};
9013
9014			img.src = uri;
9015		}
9016		catch (e)
9017		{
9018			if (onerror != null)
9019			{
9020				onerror(e);
9021			}
9022			else
9023			{
9024				throw e;
9025			}
9026		}
9027	};
9028
9029	// Initializes the user interface
9030	var editorUiInit = EditorUi.prototype.init;
9031	EditorUi.prototype.init = function()
9032	{
9033		mxStencilRegistry.allowEval = mxStencilRegistry.allowEval && !this.isOfflineApp();
9034
9035		// Must be set before UI is created in superclass
9036		if (this.isSettingsEnabled())
9037		{
9038			if (urlParams['sketch'] == '1')
9039			{
9040				this.doSetSketchMode((mxSettings.settings.sketchMode != null &&
9041					urlParams['rough'] == null) ? mxSettings.settings.sketchMode :
9042					urlParams['rough'] != '0');
9043			}
9044
9045			this.formatWidth = mxSettings.getFormatWidth();
9046		}
9047
9048		var ui = this;
9049		var graph = this.editor.graph;
9050
9051		if (Editor.isDarkMode())
9052		{
9053			graph.view.defaultGridColor = mxGraphView.prototype.defaultDarkGridColor;
9054		}
9055
9056		// Starts editing PlantUML data
9057		graph.cellEditor.editPlantUmlData = function(cell, trigger, data)
9058		{
9059			var obj = JSON.parse(data);
9060
9061	    	var dlg = new TextareaDialog(ui, mxResources.get('plantUml') + ':',
9062	    		obj.data, function(text)
9063			{
9064	    		if (text != null)
9065				{
9066	    			if (ui.spinner.spin(document.body, mxResources.get('inserting')))
9067	    			{
9068	    				ui.generatePlantUmlImage(text, obj.format, function(data, w, h)
9069	    				{
9070	    					ui.spinner.stop();
9071
9072	    					graph.getModel().beginUpdate();
9073	    					try
9074	    					{
9075	    						if (obj.format == 'txt')
9076		    					{
9077		    						graph.labelChanged(cell, '<pre>' + data + '</pre>');
9078		    						graph.updateCellSize(cell, true);
9079		    					}
9080	    						else
9081	    						{
9082	    							graph.setCellStyles('image', ui.convertDataUri(data), [cell]);
9083	    							var geo = graph.model.getGeometry(cell);
9084
9085	    							if (geo != null)
9086	    							{
9087	    								geo = geo.clone();
9088	    								geo.width = w;
9089	    								geo.height = h;
9090	    								graph.cellsResized([cell], [geo], false);
9091	    							}
9092	    						}
9093
9094	    						graph.setAttributeForCell(cell, 'plantUmlData',
9095		    						JSON.stringify({data: text, format: obj.format}));
9096	    					}
9097	    					finally
9098	    					{
9099	    						graph.getModel().endUpdate();
9100	    					}
9101	    				}, function(e)
9102	    				{
9103	    					ui.handleError(e);
9104	    				});
9105	    			}
9106				}
9107			}, null, null, 400, 220);
9108			ui.showDialog(dlg.container, 420, 300, true, true);
9109			dlg.init();
9110		};
9111
9112		// Starts editing Mermaid data
9113		graph.cellEditor.editMermaidData = function(cell, trigger, data)
9114		{
9115			var obj = JSON.parse(data);
9116
9117	    	var dlg = new TextareaDialog(ui, mxResources.get('mermaid') + ':',
9118	    		obj.data, function(text)
9119			{
9120	    		if (text != null)
9121				{
9122	    			if (ui.spinner.spin(document.body, mxResources.get('inserting')))
9123	    			{
9124	    				ui.generateMermaidImage(text, obj.config, function(data, w, h)
9125	    				{
9126	    					ui.spinner.stop();
9127
9128	    					graph.getModel().beginUpdate();
9129	    					try
9130	    					{
9131	    						graph.setCellStyles('image', data, [cell]);
9132    							var geo = graph.model.getGeometry(cell);
9133
9134    							if (geo != null)
9135    							{
9136    								geo = geo.clone();
9137    								geo.width = Math.max(geo.width, w);
9138    								geo.height = Math.max(geo.height, h);
9139    								graph.cellsResized([cell], [geo], false);
9140    							}
9141
9142	    						graph.setAttributeForCell(cell, 'mermaidData',
9143		    						JSON.stringify({data: text, config:
9144		    						obj.config}, null, 2));
9145	    					}
9146	    					finally
9147	    					{
9148	    						graph.getModel().endUpdate();
9149	    					}
9150	    				}, function(e)
9151	    				{
9152	    					ui.handleError(e);
9153	    				});
9154	    			}
9155				}
9156			}, null, null, 400, 220);
9157			ui.showDialog(dlg.container, 420, 300, true, true);
9158			dlg.init();
9159		};
9160
9161		// Overrides function to add editing for Plant UML.
9162		var cellEditorStartEditing = graph.cellEditor.startEditing;
9163		graph.cellEditor.startEditing = function(cell, trigger)
9164		{
9165			try
9166			{
9167				var data = this.graph.getAttributeForCell(cell, 'plantUmlData');
9168
9169				if (data != null)
9170				{
9171					this.editPlantUmlData(cell, trigger, data);
9172				}
9173				else
9174				{
9175					data = this.graph.getAttributeForCell(cell, 'mermaidData');
9176
9177					if (data != null)
9178					{
9179						this.editMermaidData(cell, trigger, data);
9180					}
9181					else
9182					{
9183						var style = graph.getCellStyle(cell);
9184
9185						if (mxUtils.getValue(style, 'metaEdit', '0') == '1')
9186						{
9187							ui.showDataDialog(cell);
9188						}
9189						else
9190						{
9191							cellEditorStartEditing.apply(this, arguments);
9192						}
9193					}
9194				}
9195			}
9196			catch (e)
9197			{
9198				ui.handleError(e);
9199			}
9200		};
9201
9202		// Redirects custom link title via UI for page links
9203		graph.getLinkTitle = function(href)
9204		{
9205			return ui.getLinkTitle(href);
9206		};
9207
9208		// Redirects custom link via UI for page link handling
9209		graph.customLinkClicked = function(link)
9210		{
9211			var done = false;
9212
9213			try
9214			{
9215				ui.handleCustomLink(link);
9216				done = true;
9217			}
9218			catch (e)
9219			{
9220				ui.handleError(e);
9221			}
9222
9223			return done;
9224		};
9225
9226		// Parses background page references
9227		var graphParseBackgroundImage = graph.parseBackgroundImage;
9228
9229		graph.parseBackgroundImage = function(json)
9230		{
9231			var result = graphParseBackgroundImage.apply(this, arguments);
9232
9233			if (result != null && result.src != null && Graph.isPageLink(result.src))
9234			{
9235				result = {originalSrc: result.src};
9236			}
9237
9238			return result;
9239		};
9240
9241		// Updates background page SVG
9242		var graphSetBackgroundImage = graph.setBackgroundImage;
9243
9244		graph.setBackgroundImage = function(img)
9245		{
9246			if (img != null && img.originalSrc != null)
9247			{
9248				img = ui.createImageForPageLink(img.originalSrc, ui.currentPage, this);
9249			}
9250
9251			graphSetBackgroundImage.apply(this, arguments);
9252		};
9253
9254		// Updates background to update placeholders for page title
9255		this.editor.addListener('pageRenamed', mxUtils.bind(this, function()
9256		{
9257			graph.refreshBackgroundImage();
9258		}));
9259
9260		// Updates background to update placeholders for page number
9261		this.editor.addListener('pageMoved', mxUtils.bind(this, function()
9262		{
9263			graph.refreshBackgroundImage();
9264		}));
9265
9266		// Updates background image after remote changes to the referenced page
9267		this.editor.addListener('pagesPatched', mxUtils.bind(this, function(sender, evt)
9268		{
9269			var ref = (graph.backgroundImage != null) ? graph.backgroundImage.originalSrc : null;
9270
9271			if (ref != null)
9272			{
9273				var comma = ref.indexOf(',');
9274
9275				if (comma > 0)
9276				{
9277					var id = ref.substring(comma + 1);
9278					var patches = evt.getProperty('patches');
9279
9280					for (var i = 0; i < patches.length; i++)
9281					{
9282						if (patches[i][EditorUi.DIFF_UPDATE] != null &&
9283							patches[i][EditorUi.DIFF_UPDATE][id] != null)
9284						{
9285							graph.refreshBackgroundImage();
9286
9287							break;
9288						}
9289					}
9290				}
9291			}
9292		}));
9293
9294		// Restores background page reference in output data or
9295		// replaces dark mode page image with normal mode image
9296		var graphGetBackgroundImageObject = graph.getBackgroundImageObject;
9297
9298		graph.getBackgroundImageObject = function(obj, resolveReferences)
9299		{
9300			var result = graphGetBackgroundImageObject.apply(this, arguments);
9301
9302			if (result != null && result.originalSrc != null)
9303			{
9304				if (!resolveReferences)
9305				{
9306					result = {src: result.originalSrc};
9307				}
9308				else if (resolveReferences && this.themes != null &&
9309					this.defaultThemeName == 'darkTheme')
9310				{
9311					var temp = this.stylesheet;
9312					this.stylesheet = this.getDefaultStylesheet();
9313					result = ui.createImageForPageLink(result.originalSrc);
9314					this.stylesheet = temp;
9315				}
9316			}
9317
9318			return result;
9319		};
9320
9321		// Extends clear default style to clear persisted settings
9322		var clearDefaultStyle = this.clearDefaultStyle;
9323
9324		this.clearDefaultStyle = function()
9325		{
9326			clearDefaultStyle.apply(this, arguments);
9327		};
9328
9329		// Sets help link for placeholders
9330		if (!this.isOffline() && typeof window.EditDataDialog !== 'undefined')
9331		{
9332			EditDataDialog.placeholderHelpLink = 'https://www.diagrams.net/doc/faq/predefined-placeholders';
9333		}
9334
9335		if (/viewer\.diagrams\.net$/.test(window.location.hostname) ||
9336			/embed\.diagrams\.net$/.test(window.location.hostname))
9337		{
9338			this.editor.editBlankUrl = 'https://app.diagrams.net/';
9339		}
9340
9341		// Passes dev mode to new window
9342		var editorGetEditBlankUrl = ui.editor.getEditBlankUrl;
9343
9344		this.editor.getEditBlankUrl = function(params)
9345		{
9346			params = (params != null) ? params : '';
9347
9348			if (urlParams['dev'] == '1')
9349			{
9350				params += ((params.length > 0) ? '&' : '?') + 'dev=1';
9351			}
9352
9353			return editorGetEditBlankUrl.apply(this, arguments);
9354		};
9355
9356		// For chromeless mode and lightbox mode in viewer
9357		// Must be overridden before supercall to be applied
9358		// in case of chromeless initialization
9359		var graphAddClickHandler = graph.addClickHandler;
9360
9361		graph.addClickHandler = function(highlight, beforeClick, onClick)
9362		{
9363			var tmp = beforeClick;
9364
9365			beforeClick = function(evt, href)
9366			{
9367				if (href == null)
9368				{
9369					var source = mxEvent.getSource(evt);
9370
9371					if (source.nodeName.toLowerCase() == 'a')
9372					{
9373						href = source.getAttribute('href');
9374					}
9375				}
9376
9377				if (href != null && graph.isCustomLink(href) &&
9378					(mxEvent.isTouchEvent(evt) ||
9379					!mxEvent.isPopupTrigger(evt)) &&
9380					graph.customLinkClicked(href))
9381				{
9382					mxEvent.consume(evt);
9383				}
9384
9385				if (tmp != null)
9386				{
9387					tmp(evt, href);
9388				}
9389			};
9390
9391			// For some reason, local argument override is not enough in this case...
9392			graphAddClickHandler.call(this, highlight, beforeClick, onClick);
9393		};
9394
9395		editorUiInit.apply(this, arguments);
9396
9397		if (mxClient.IS_SVG)
9398		{
9399			// LATER: Add shadow for labels in graph.container (eg. math, NO_FO), scaling
9400			this.editor.graph.addSvgShadow(graph.view.canvas.ownerSVGElement, null, true);
9401		}
9402
9403		if (this.menus != null)
9404		{
9405			var menusAddPopupMenuEditItems = Menus.prototype.addPopupMenuEditItems;
9406
9407			// Inserts copyAsImage into popup menu
9408			this.menus.addPopupMenuEditItems = function(menu, cell, evt)
9409			{
9410				if (ui.editor.graph.isSelectionEmpty())
9411				{
9412					menusAddPopupMenuEditItems.apply(this, arguments);
9413				}
9414				else
9415				{
9416					ui.menus.addMenuItems(menu, ['delete', '-', 'cut', 'copy',
9417						'copyAsImage', '-', 'duplicate'], null, evt);
9418				}
9419			};
9420		}
9421
9422		// Overrides print dialog size
9423		ui.actions.get('print').funct = function()
9424		{
9425			ui.showDialog(new PrintDialog(ui).container, 360,
9426				(ui.pages != null && ui.pages.length > 1) ?
9427				450 : 370, true, true);
9428		};
9429
9430		// Specifies the default filename
9431		this.defaultFilename = mxResources.get('untitledDiagram');
9432
9433		// Adds export for %page%, %pagenumber% and %pagecount% placeholders
9434		var graphGetExportVariables = graph.getExportVariables;
9435
9436		graph.getExportVariables = function()
9437		{
9438			var vars = graphGetExportVariables.apply(this, arguments);
9439			var file = ui.getCurrentFile();
9440
9441			if (file != null)
9442			{
9443				vars['filename'] = file.getTitle();
9444			}
9445
9446			vars['pagecount'] = (ui.pages != null) ? ui.pages.length : 1;
9447			vars['page'] = (ui.currentPage != null) ? ui.currentPage.getName() : '';
9448			vars['pagenumber'] = (ui.pages != null && ui.currentPage != null) ?
9449				mxUtils.indexOf(ui.pages, ui.currentPage) + 1 : 1;
9450
9451			return vars;
9452		};
9453
9454		// Adds %page%, %pagenumber% and %pagecount% placeholders
9455		var graphGetGlobalVariable = graph.getGlobalVariable;
9456
9457		graph.getGlobalVariable = function(name)
9458		{
9459			var file = ui.getCurrentFile();
9460
9461			if (name == 'filename' && file != null)
9462			{
9463				return file.getTitle();
9464			}
9465			else if (name == 'page' && ui.currentPage != null)
9466			{
9467				return ui.currentPage.getName();
9468			}
9469			else if (name == 'pagenumber')
9470			{
9471				if (ui.currentPage != null && ui.pages != null)
9472				{
9473					return mxUtils.indexOf(ui.pages, ui.currentPage) + 1;
9474				}
9475				else
9476				{
9477					return 1;
9478				}
9479			}
9480			else if (name == 'pagecount')
9481			{
9482				return (ui.pages != null) ? ui.pages.length : 1;
9483			}
9484
9485			return graphGetGlobalVariable.apply(this, arguments);
9486		};
9487
9488		var graphLabelLinkClicked = graph.labelLinkClicked;
9489
9490		graph.labelLinkClicked = function(state, elt, evt)
9491		{
9492			var href = elt.getAttribute('href');
9493
9494			if (href != null && graph.isCustomLink(href) &&
9495				(mxEvent.isTouchEvent(evt) ||
9496				!mxEvent.isPopupTrigger(evt)))
9497			{
9498				// Active links are moved to the hint
9499				if (!graph.isEnabled() || (state != null && graph.isCellLocked(state.cell)))
9500				{
9501					graph.customLinkClicked(href);
9502
9503					// Resets rubberband after click on locked cell
9504					graph.getRubberband().reset();
9505				}
9506
9507				mxEvent.consume(evt);
9508			}
9509			else
9510			{
9511				graphLabelLinkClicked.apply(this, arguments);
9512			}
9513		};
9514
9515		// Overrides editor filename
9516		this.editor.getOrCreateFilename = function()
9517		{
9518			var filename = ui.defaultFilename;
9519			var file = ui.getCurrentFile();
9520
9521			if (file != null)
9522			{
9523				filename = (file.getTitle() != null) ? file.getTitle() : filename;
9524			}
9525
9526			return filename;
9527		};
9528
9529		// Disables print action for standalone apps on iOS
9530		// because there is no way to close the new window
9531		// LATER: Use iframe for print, disable preview
9532		var printAction = this.actions.get('print');
9533		printAction.setEnabled(!mxClient.IS_IOS || !navigator.standalone);
9534		printAction.visible = printAction.isEnabled();
9535
9536		// Installs additional keyboard shortcuts for editor
9537		if (!this.editor.chromeless || this.editor.editable)
9538		{
9539			// Defines additional hotkeys
9540			this.keyHandler.bindAction(70, true, 'findReplace'); // Ctrl+F
9541			this.keyHandler.bindAction(67, true, 'copyStyle', true); // Ctrl+Shift+C
9542			this.keyHandler.bindAction(86, true, 'pasteStyle', true); // Ctrl+Shift+V
9543			this.keyHandler.bindAction(77, true, 'editGeometry', true); // Ctrl+Shift+M
9544			this.keyHandler.bindAction(88, true, 'insertText', true); // Ctrl+Shift+X
9545			this.keyHandler.bindAction(75, true, 'insertRectangle'); // Ctrl+K
9546			this.keyHandler.bindAction(75, true, 'insertEllipse', true); // Ctrl+Shift+K
9547			this.altShiftActions[83] = 'synchronize'; // Alt+Shift+S
9548
9549		    this.installImagePasteHandler();
9550		    this.installNativeClipboardHandler();
9551		};
9552
9553		// Creates the spinner
9554		this.spinner = this.createSpinner(null, null, 24);
9555
9556		// Installs drag and drop handler for rich text editor
9557		if (Graph.fileSupport)
9558		{
9559			graph.addListener(mxEvent.EDITING_STARTED, mxUtils.bind(this, function(evt)
9560			{
9561				// Setup the dnd listeners
9562				var textElt = graph.cellEditor.text2;
9563				var dropElt = null;
9564
9565				if (textElt != null)
9566				{
9567					mxEvent.addListener(textElt, 'dragleave', function(evt)
9568					{
9569						if (dropElt != null)
9570					    {
9571					    	dropElt.parentNode.removeChild(dropElt);
9572					    	dropElt = null;
9573					    }
9574
9575						evt.stopPropagation();
9576						evt.preventDefault();
9577					});
9578
9579					mxEvent.addListener(textElt, 'dragover', mxUtils.bind(this, function(evt)
9580					{
9581						// IE 10 does not implement pointer-events so it can't have a drop highlight
9582						if (dropElt == null && (!mxClient.IS_IE || document.documentMode > 10))
9583						{
9584							dropElt = this.highlightElement(textElt);
9585						}
9586
9587						evt.stopPropagation();
9588						evt.preventDefault();
9589					}));
9590
9591					mxEvent.addListener(textElt, 'drop', mxUtils.bind(this, function(evt)
9592					{
9593					    if (dropElt != null)
9594					    {
9595					    	dropElt.parentNode.removeChild(dropElt);
9596					    	dropElt = null;
9597					    }
9598
9599					    if (evt.dataTransfer.files.length > 0)
9600					    {
9601					    	this.importFiles(evt.dataTransfer.files, 0, 0, this.maxImageSize, function(data, mimeType, x, y, w, h)
9602					    	{
9603					    		// Inserts image into current text box
9604					    		graph.insertImage(data, w, h);
9605					    	}, function()
9606					    	{
9607					    		// No post processing
9608					    	}, function(file)
9609					    	{
9610					    		// Handles only images
9611					    		return file.type.substring(0, 6) == 'image/';
9612					    	}, function(queue)
9613					    	{
9614					    		// Invokes elements of queue in order
9615					    		for (var i = 0; i < queue.length; i++)
9616					    		{
9617					    			queue[i]();
9618					    		}
9619					    	}, mxEvent.isControlDown(evt));
9620			    		}
9621					    else if (mxUtils.indexOf(evt.dataTransfer.types, 'text/uri-list') >= 0)
9622					    {
9623					    	var uri = evt.dataTransfer.getData('text/uri-list');
9624
9625					    	if ((/\.(gif|jpg|jpeg|tiff|png|svg)$/i).test(uri))
9626							{
9627				    			this.loadImage(decodeURIComponent(uri), mxUtils.bind(this, function(img)
9628				    			{
9629				    				var w = Math.max(1, img.width);
9630			    					var h = Math.max(1, img.height);
9631			    					var maxSize = this.maxImageSize;
9632
9633				    				var s = Math.min(1, Math.min(maxSize / Math.max(1, w)), maxSize / Math.max(1, h));
9634				    				graph.insertImage(decodeURIComponent(uri), w * s, h * s);
9635				    			}));
9636							}
9637							else
9638							{
9639								document.execCommand('insertHTML', false, evt.dataTransfer.getData('text/plain'));
9640							}
9641					    }
9642					    else
9643					    {
9644					    	if (mxUtils.indexOf(evt.dataTransfer.types, 'text/html') >= 0)
9645						    {
9646					    		document.execCommand('insertHTML', false, evt.dataTransfer.getData('text/html'));
9647						    }
9648						    else if (mxUtils.indexOf(evt.dataTransfer.types, 'text/plain') >= 0)
9649						    {
9650						    	document.execCommand('insertHTML', false, evt.dataTransfer.getData('text/plain'));
9651						    }
9652					    }
9653
9654					    evt.stopPropagation();
9655					    evt.preventDefault();
9656					}));
9657				}
9658			}));
9659		}
9660
9661		// Adding mxRuler to editor
9662		if (this.isSettingsEnabled())
9663		{
9664			var view = this.editor.graph.view;
9665			view.setUnit(mxSettings.getUnit());
9666
9667			view.addListener('unitChanged', function(sender, evt)
9668			{
9669				mxSettings.setUnit(evt.getProperty('unit'));
9670				mxSettings.save();
9671			});
9672
9673			var showRuler = this.canvasSupported && document.documentMode != 9 &&
9674				(urlParams['ruler'] == '1' || mxSettings.isRulerOn()) &&
9675				(!this.editor.isChromelessView() || this.editor.editable);
9676
9677			this.ruler = (showRuler) ? new mxDualRuler(this, view.unit) : null;
9678			this.refresh();
9679		}
9680
9681		// Adds an element to edit the style in the footer in test mode
9682		if (urlParams['styledev'] == '1')
9683		{
9684			var footer = document.getElementById('geFooter');
9685
9686			if (footer != null)
9687			{
9688				this.styleInput = document.createElement('input');
9689				this.styleInput.setAttribute('type', 'text');
9690				this.styleInput.style.position = 'absolute';
9691				this.styleInput.style.top = '14px';
9692				this.styleInput.style.left = '2px';
9693				// Workaround for ignore right CSS property in FF
9694				this.styleInput.style.width = '98%';
9695				this.styleInput.style.visibility = 'hidden';
9696				this.styleInput.style.opacity = '0.9';
9697
9698				mxEvent.addListener(this.styleInput, 'change', mxUtils.bind(this, function()
9699				{
9700					this.editor.graph.getModel().setStyle(this.editor.graph.getSelectionCell(), this.styleInput.value);
9701				}));
9702
9703				footer.appendChild(this.styleInput);
9704
9705				this.editor.graph.getSelectionModel().addListener(mxEvent.CHANGE, mxUtils.bind(this, function(sender, evt)
9706				{
9707					if (this.editor.graph.getSelectionCount() > 0)
9708					{
9709						var cell = this.editor.graph.getSelectionCell();
9710						var style = this.editor.graph.getModel().getStyle(cell);
9711
9712						this.styleInput.value = style || '';
9713						this.styleInput.style.visibility = 'visible';
9714					}
9715					else
9716					{
9717						this.styleInput.style.visibility = 'hidden';
9718					}
9719				}));
9720			}
9721
9722			var isSelectionAllowed = this.isSelectionAllowed;
9723			this.isSelectionAllowed = function(evt)
9724			{
9725				if (mxEvent.getSource(evt) == this.styleInput)
9726				{
9727					return true;
9728				}
9729
9730				return isSelectionAllowed.apply(this, arguments);
9731			};
9732		}
9733
9734		// Removes info text in page
9735		var info = document.getElementById('geInfo');
9736
9737		if (info != null)
9738		{
9739			info.parentNode.removeChild(info);
9740		}
9741
9742		// Installs drag and drop handler for files
9743		// Enables dropping files
9744		if (Graph.fileSupport && (!this.editor.chromeless || this.editor.editable))
9745		{
9746			// Setup the dnd listeners
9747			var dropElt = null;
9748
9749			mxEvent.addListener(graph.container, 'dragleave', function(evt)
9750			{
9751				if (graph.isEnabled())
9752				{
9753					if (dropElt != null)
9754				    {
9755				    	dropElt.parentNode.removeChild(dropElt);
9756				    	dropElt = null;
9757				    }
9758
9759					evt.stopPropagation();
9760					evt.preventDefault();
9761				}
9762			});
9763
9764			mxEvent.addListener(graph.container, 'dragover', mxUtils.bind(this, function(evt)
9765			{
9766				// IE 10 does not implement pointer-events so it can't have a drop highlight
9767				if (dropElt == null && (!mxClient.IS_IE || document.documentMode > 10))
9768				{
9769					dropElt = this.highlightElement(graph.container);
9770				}
9771
9772				if (this.sidebar != null)
9773				{
9774					this.sidebar.hideTooltip();
9775				}
9776
9777				evt.stopPropagation();
9778				evt.preventDefault();
9779			}));
9780
9781			mxEvent.addListener(graph.container, 'drop', mxUtils.bind(this, function(evt)
9782			{
9783			    if (dropElt != null)
9784			    {
9785			    	dropElt.parentNode.removeChild(dropElt);
9786			    	dropElt = null;
9787			    }
9788
9789				if (graph.isEnabled())
9790				{
9791				    var pt = mxUtils.convertPoint(graph.container, mxEvent.getClientX(evt), mxEvent.getClientY(evt));
9792					var tr = graph.view.translate;
9793					var scale = graph.view.scale;
9794					var x = pt.x / scale - tr.x;
9795					var y = pt.y / scale - tr.y;
9796
9797				    if (evt.dataTransfer.files.length > 0)
9798				    {
9799				    	if (mxEvent.isShiftDown(evt))
9800				    	{
9801				    		this.openFiles(evt.dataTransfer.files, true);
9802				    	}
9803				    	else
9804				    	{
9805							if (mxEvent.isAltDown(evt))
9806							{
9807								x = null;
9808								y = null;
9809							}
9810
9811							this.importFiles(evt.dataTransfer.files, x, y, this.maxImageSize, null, null, null,
9812								null, mxEvent.isControlDown(evt), null, null, mxEvent.isShiftDown(evt), evt);
9813				    	}
9814		    		}
9815				    else
9816				    {
9817						if (mxEvent.isAltDown(evt))
9818						{
9819							x = 0;
9820							y = 0;
9821						}
9822
9823				    	var uri = (mxUtils.indexOf(evt.dataTransfer.types, 'text/uri-list') >= 0) ?
9824				    		evt.dataTransfer.getData('text/uri-list') : null;
9825				    	var data = this.extractGraphModelFromEvent(evt, this.pages != null);
9826
9827				    	if (data != null)
9828				    	{
9829				    		graph.setSelectionCells(this.importXml(data, x, y, true));
9830				    	}
9831				    	else if (mxUtils.indexOf(evt.dataTransfer.types, 'text/html') >= 0)
9832					    {
9833				    		var html = evt.dataTransfer.getData('text/html');
9834				    		var div = document.createElement('div');
9835				    		div.innerHTML = graph.sanitizeHtml(html);
9836
9837				    		// The default is based on the extension
9838				    		var asImage = null;
9839
9840				    		// Extracts single image
9841				    		var imgs = div.getElementsByTagName('img');
9842
9843				    		if (imgs != null && imgs.length == 1)
9844				    		{
9845				    			html = imgs[0].getAttribute('src');
9846
9847				    			if (html == null)
9848				    			{
9849				    				html = imgs[0].getAttribute('srcset');
9850				    			}
9851
9852				    			// Handles special case where the src attribute has no valid extension
9853				    			// in which case the text would be inserted as text with a link
9854				    			if (!(/\.(gif|jpg|jpeg|tiff|png|svg)$/i).test(html))
9855				    			{
9856				    				asImage = true;
9857				    			}
9858				    		}
9859				    		else
9860				    		{
9861				    			// Extracts single link
9862				    			var a = div.getElementsByTagName('a');
9863
9864				    			if (a != null && a.length == 1)
9865				    			{
9866				    				html = a[0].getAttribute('href');
9867				    			}
9868					    		else
9869					    		{
9870					    			// Extracts preformatted text
9871					    			var pre = div.getElementsByTagName('pre');
9872
9873					    			if (pre != null && pre.length == 1)
9874					    			{
9875					    				html = mxUtils.getTextContent(pre[0]);
9876					    			}
9877					    		}
9878				    		}
9879
9880				    		var resizeImages = true;
9881
9882				    		var doInsert = mxUtils.bind(this, function()
9883				    		{
9884				    			graph.setSelectionCells(this.insertTextAt(html, x, y, true,
9885				    				asImage, null, resizeImages, mxEvent.isControlDown(evt)));
9886				    		});
9887
9888				    		if (asImage && html != null && html.length > this.resampleThreshold)
9889				    		{
9890				    			this.confirmImageResize(function(doResize)
9891		    					{
9892		    						resizeImages = doResize;
9893		    						doInsert();
9894		    					}, mxEvent.isControlDown(evt));
9895				    		}
9896				    		else
9897			    			{
9898				    			doInsert();
9899			    			}
9900					    }
9901				    	else if (uri != null && (/\.(gif|jpg|jpeg|tiff|png|svg)$/i).test(uri))
9902						{
9903			    			this.loadImage(decodeURIComponent(uri), mxUtils.bind(this, function(img)
9904			    			{
9905			    				var w = Math.max(1, img.width);
9906		    					var h = Math.max(1, img.height);
9907		    					var maxSize = this.maxImageSize;
9908
9909			    				var s = Math.min(1, Math.min(maxSize / Math.max(1, w)), maxSize / Math.max(1, h));
9910
9911			    				graph.setSelectionCell(graph.insertVertex(null, null, '', x, y, w * s, h * s,
9912			    					'shape=image;verticalLabelPosition=bottom;labelBackgroundColor=#ffffff;' +
9913			    					'verticalAlign=top;aspect=fixed;imageAspect=0;image=' + uri + ';'));
9914			    			}), mxUtils.bind(this, function(img)
9915			    			{
9916			    				graph.setSelectionCells(this.insertTextAt(uri, x, y, true));
9917			    			}));
9918						}
9919					    else if (mxUtils.indexOf(evt.dataTransfer.types, 'text/plain') >= 0)
9920					    {
9921					    	graph.setSelectionCells(this.insertTextAt(evt.dataTransfer.getData('text/plain'), x, y, true));
9922					    }
9923					}
9924				}
9925
9926			    evt.stopPropagation();
9927			    evt.preventDefault();
9928			}), false);
9929		}
9930
9931		graph.enableFlowAnimation = true;
9932		this.initPages();
9933
9934		// Embedded mode
9935		if (urlParams['embed'] == '1')
9936		{
9937			this.initializeEmbedMode();
9938		}
9939
9940		this.installSettings();
9941	};
9942
9943	/**
9944	 * Installs handler for pasting image from clipboard.
9945	 */
9946	EditorUi.prototype.installImagePasteHandler = function()
9947	{
9948		if (!mxClient.IS_IE)
9949		{
9950			var graph = this.editor.graph;
9951
9952			graph.container.addEventListener('paste', mxUtils.bind(this, function(evt)
9953			{
9954				if (!mxEvent.isConsumed(evt))
9955				{
9956					try
9957					{
9958						var data = (evt.clipboardData || evt.originalEvent.clipboardData);
9959						var containsText = false;
9960
9961						// Workaround for asynchronous paste event processing in textInput
9962						// is to ignore this event if it contains text/html/rtf (see below).
9963						// NOTE: Image is not pasted into textInput so can't listen there.
9964						for (var i = 0; i < data.types.length; i++)
9965						{
9966							if (data.types[i].substring(0, 5) === 'text/')
9967							{
9968								containsText = true;
9969								break;
9970							}
9971						}
9972
9973						if (!containsText)
9974						{
9975							var items = data.items;
9976
9977							for (index in items)
9978							{
9979								var item = items[index];
9980
9981								if (item.kind === 'file')
9982								{
9983									if (graph.isEditing())
9984									{
9985								    	this.importFiles([item.getAsFile()], 0, 0, this.maxImageSize, function(data, mimeType, x, y, w, h)
9986								    	{
9987								    		// Inserts image into current text box
9988								    		graph.insertImage(data, w, h);
9989								    	}, function()
9990								    	{
9991								    		// No post processing
9992								    	}, function(file)
9993								    	{
9994								    		// Handles only images
9995								    		return file.type.substring(0, 6) == 'image/';
9996								    	}, function(queue)
9997								    	{
9998								    		// Invokes elements of queue in order
9999								    		for (var i = 0; i < queue.length; i++)
10000								    		{
10001								    			queue[i]();
10002								    		}
10003								    	});
10004									}
10005									else
10006									{
10007										var pt = this.editor.graph.getInsertPoint();
10008										this.importFiles([item.getAsFile()], pt.x, pt.y, this.maxImageSize);
10009										mxEvent.consume(evt);
10010									}
10011
10012									break;
10013								}
10014							}
10015						}
10016					}
10017					catch (e)
10018					{
10019						// ignore
10020					}
10021				}
10022			}), false);
10023		}
10024	};
10025
10026	/**
10027	 * Installs the native clipboard support.
10028	 */
10029	EditorUi.prototype.installNativeClipboardHandler = function()
10030	{
10031		var graph = this.editor.graph;
10032
10033		// Focused but invisible textarea during control or meta key events
10034		// LATER: Disable text rendering to avoid delay while keeping focus
10035		var textInput = document.createElement('div');
10036		textInput.setAttribute('autocomplete', 'off');
10037		textInput.setAttribute('autocorrect', 'off');
10038		textInput.setAttribute('autocapitalize', 'off');
10039		textInput.setAttribute('spellcheck', 'false');
10040		textInput.style.textRendering = 'optimizeSpeed';
10041		textInput.style.fontFamily = 'monospace';
10042		textInput.style.wordBreak = 'break-all';
10043		textInput.style.background = 'transparent';
10044		textInput.style.color = 'transparent';
10045		textInput.style.position = 'absolute';
10046		textInput.style.whiteSpace = 'nowrap';
10047		textInput.style.overflow = 'hidden';
10048		textInput.style.display = 'block';
10049		textInput.style.fontSize = '1';
10050		textInput.style.zIndex = '-1';
10051		textInput.style.resize = 'none';
10052		textInput.style.outline = 'none';
10053		textInput.style.width = '1px';
10054		textInput.style.height = '1px';
10055		mxUtils.setOpacity(textInput, 0);
10056		textInput.contentEditable = true;
10057		textInput.innerHTML = '&nbsp;';
10058
10059		var restoreFocus = false;
10060
10061		// Disables built-in cut, copy and paste shortcuts
10062		this.keyHandler.bindControlKey(88, null);
10063		this.keyHandler.bindControlKey(67, null);
10064		this.keyHandler.bindControlKey(86, null);
10065
10066		// Shows a textare when control/cmd is pressed to handle native clipboard actions
10067		mxEvent.addListener(document, 'keydown', mxUtils.bind(this, function(evt)
10068		{
10069			// No dialog visible
10070			var source = mxEvent.getSource(evt);
10071
10072			if (graph.container != null && graph.isEnabled() && !graph.isMouseDown && !graph.isEditing() &&
10073				this.dialog == null && source.nodeName != 'INPUT' && source.nodeName != 'TEXTAREA')
10074			{
10075				if (evt.keyCode == 224 /* FF */ || (!mxClient.IS_MAC && evt.keyCode == 17 /* Control */) ||
10076					(mxClient.IS_MAC && (evt.keyCode == 91 || evt.keyCode == 93) /* Left/Right Meta */))
10077				{
10078					// Cannot use parentNode for check in IE
10079					if (!restoreFocus)
10080					{
10081						// Avoid autoscroll but allow handling of all pass-through ctrl shortcuts
10082						textInput.style.left = (graph.container.scrollLeft + 10) + 'px';
10083						textInput.style.top = (graph.container.scrollTop + 10) + 'px';
10084
10085						graph.container.appendChild(textInput);
10086						restoreFocus = true;
10087
10088						textInput.focus();
10089						document.execCommand('selectAll', false, null);
10090					}
10091				}
10092			}
10093		}));
10094
10095		// Clears input and restores focus and selection
10096		function clearInput()
10097		{
10098			window.setTimeout(function()
10099			{
10100				textInput.innerHTML = '&nbsp;';
10101				textInput.focus();
10102				document.execCommand('selectAll', false, null);
10103			}, 0);
10104		};
10105
10106		mxEvent.addListener(document, 'keyup', mxUtils.bind(this, function(evt)
10107		{
10108			// Workaround for asynchronous event read invalid in IE quirks mode
10109			var keyCode = evt.keyCode;
10110
10111			// Asynchronous workaround for scroll to origin after paste if the
10112			// Ctrl-key is not pressed for long enough in FF on Windows
10113			window.setTimeout(mxUtils.bind(this, function()
10114			{
10115				if (restoreFocus && (keyCode == 224 /* FF */ || keyCode == 17 /* Control */ ||
10116					keyCode == 91 /* MetaLeft */ || keyCode == 93 /* MetaRight */))
10117				{
10118					restoreFocus = false;
10119
10120					if (!graph.isEditing() && this.dialog == null && graph.container != null)
10121					{
10122						graph.container.focus();
10123					}
10124
10125					textInput.parentNode.removeChild(textInput);
10126
10127					// Workaround for lost cursor in focused element
10128					if (this.dialog == null)
10129					{
10130						mxUtils.clearSelection();
10131					}
10132				}
10133			}), 0);
10134		}));
10135
10136		mxEvent.addListener(textInput, 'copy', mxUtils.bind(this, function(evt)
10137		{
10138			if (graph.isEnabled())
10139			{
10140				try
10141				{
10142					mxClipboard.copy(graph);
10143					this.copyCells(textInput);
10144					clearInput();
10145				}
10146				catch (e)
10147				{
10148					this.handleError(e);
10149				}
10150			}
10151		}));
10152
10153		mxEvent.addListener(textInput, 'cut', mxUtils.bind(this, function(evt)
10154		{
10155			if (graph.isEnabled())
10156			{
10157				try
10158				{
10159					mxClipboard.copy(graph);
10160					this.copyCells(textInput, true);
10161					clearInput();
10162				}
10163				catch (e)
10164				{
10165					this.handleError(e);
10166				}
10167			}
10168		}));
10169
10170		mxEvent.addListener(textInput, 'paste', mxUtils.bind(this, function(evt)
10171		{
10172			if (graph.isEnabled() && !graph.isCellLocked(graph.getDefaultParent()))
10173			{
10174				textInput.innerHTML = '&nbsp;';
10175				textInput.focus();
10176
10177				if (evt.clipboardData != null)
10178				{
10179					this.pasteCells(evt, textInput, true, true);
10180				}
10181
10182				if (!mxEvent.isConsumed(evt))
10183				{
10184					window.setTimeout(mxUtils.bind(this, function()
10185					{
10186						this.pasteCells(evt, textInput, false, true);
10187					}), 0);
10188				}
10189			}
10190		}), true);
10191
10192		// Needed for IE11
10193		var isSelectionAllowed2 = this.isSelectionAllowed;
10194		this.isSelectionAllowed = function(evt)
10195		{
10196			if (mxEvent.getSource(evt) == textInput)
10197			{
10198				return true;
10199			}
10200
10201			return isSelectionAllowed2.apply(this, arguments);
10202		};
10203	};
10204
10205	/**
10206	 * Overrides image dialog to add image search and Google+.
10207	 */
10208	EditorUi.prototype.setSketchMode = function(value)
10209	{
10210		if (this.spinner.spin(document.body, mxResources.get('working') + '...'))
10211		{
10212			window.setTimeout(mxUtils.bind(this, function()
10213			{
10214				this.spinner.stop();
10215				this.doSetSketchMode(value);
10216
10217				// Persist setting
10218				if (urlParams['rough'] == null)
10219				{
10220					mxSettings.settings.sketchMode = value;
10221					mxSettings.save();
10222				}
10223
10224				this.fireEvent(new mxEventObject('sketchModeChanged'));
10225			}), 0);
10226		}
10227	};
10228
10229	/**
10230	 * Changes Editor.pagesVisible.
10231	 */
10232	EditorUi.prototype.setPagesVisible = function(value)
10233	{
10234		if (Editor.pagesVisible != value)
10235		{
10236			Editor.pagesVisible = value;
10237
10238			// Persist setting
10239			mxSettings.settings.pagesVisible = value;
10240			mxSettings.save();
10241
10242			this.fireEvent(new mxEventObject('pagesVisibleChanged'));
10243		}
10244	};
10245
10246	/**
10247	 * Dynamic change of dark mode.
10248	 */
10249	EditorUi.prototype.setInlineFullscreen = function(value)
10250	{
10251		if (Editor.inlineFullscreen != value)
10252		{
10253			Editor.inlineFullscreen = value;
10254			this.fireEvent(new mxEventObject('inlineFullscreenChanged'));
10255
10256			var parent = window.opener || window.parent;
10257			parent.postMessage(JSON.stringify({
10258				event: 'resize',
10259				fullscreen: Editor.inlineFullscreen,
10260				rect: this.diagramContainer.getBoundingClientRect()
10261			}), '*');
10262
10263			window.setTimeout(mxUtils.bind(this, function()
10264			{
10265				this.refresh();
10266				this.actions.get('resetView').funct();
10267			}), 10);
10268		}
10269	};
10270
10271	/**
10272	 * Dynamic change of dark mode.
10273	 */
10274	EditorUi.prototype.doSetSketchMode = function(value)
10275	{
10276		if (Editor.sketchMode != value)
10277		{
10278			var graph = this.editor.graph;
10279			Editor.sketchMode = value;
10280
10281			function setStyle(style, key, value)
10282			{
10283				if (style[key] == null)
10284				{
10285					style[key] = value;
10286				}
10287			};
10288
10289			this.menus.defaultFonts = Menus.prototype.defaultFonts;
10290			this.menus.defaultFontSize = (value) ? 20 : 16;
10291
10292			graph.defaultVertexStyle = mxUtils.clone(Graph.prototype.defaultVertexStyle);
10293			setStyle(graph.defaultVertexStyle, 'fontSize', this.menus.defaultFontSize);
10294
10295			graph.defaultEdgeStyle = mxUtils.clone(Graph.prototype.defaultEdgeStyle);
10296			setStyle(graph.defaultEdgeStyle, 'fontSize', this.menus.defaultFontSize - 4);
10297			setStyle(graph.defaultEdgeStyle, 'edgeStyle', 'none');
10298			setStyle(graph.defaultEdgeStyle, 'rounded', '0');
10299			setStyle(graph.defaultEdgeStyle, 'curved', '1');
10300			setStyle(graph.defaultEdgeStyle, 'jettySize', 'auto');
10301			setStyle(graph.defaultEdgeStyle, 'orthogonalLoop', '1');
10302			setStyle(graph.defaultEdgeStyle, 'endArrow', 'open');
10303			setStyle(graph.defaultEdgeStyle, 'endSize', '14');
10304			setStyle(graph.defaultEdgeStyle, 'startSize', '14');
10305
10306			if (value)
10307			{
10308				setStyle(graph.defaultVertexStyle, 'fontFamily', Editor.sketchFontFamily);
10309				setStyle(graph.defaultVertexStyle, 'fontSource', Editor.sketchFontSource);
10310				setStyle(graph.defaultVertexStyle, 'hachureGap', '4');
10311				setStyle(graph.defaultVertexStyle, 'sketch', '1');
10312
10313				setStyle(graph.defaultEdgeStyle, 'fontFamily', Editor.sketchFontFamily);
10314				setStyle(graph.defaultEdgeStyle, 'fontSource', Editor.sketchFontSource);
10315				setStyle(graph.defaultEdgeStyle, 'sketch', '1');
10316				setStyle(graph.defaultEdgeStyle, 'hachureGap', '4');
10317				setStyle(graph.defaultEdgeStyle, 'sourcePerimeterSpacing', '8');
10318				setStyle(graph.defaultEdgeStyle, 'targetPerimeterSpacing', '8');
10319
10320				this.menus.defaultFonts = [{'fontFamily': Editor.sketchFontFamily,
10321						'fontUrl': decodeURIComponent(Editor.sketchFontSource)},
10322					{'fontFamily': 'Rock Salt', 'fontUrl': 'https://fonts.googleapis.com/css?family=Rock+Salt'},
10323					{'fontFamily': 'Permanent Marker', 'fontUrl': 'https://fonts.googleapis.com/css?family=Permanent+Marker'}].
10324					concat(this.menus.defaultFonts);
10325			}
10326
10327			graph.currentVertexStyle = mxUtils.clone(graph.defaultVertexStyle);
10328			graph.currentEdgeStyle = mxUtils.clone(graph.defaultEdgeStyle);
10329			this.clearDefaultStyle();
10330		}
10331	};
10332
10333	/**
10334	 *
10335	 */
10336	EditorUi.prototype.getLinkTitle = function(href)
10337	{
10338		var title = Graph.prototype.getLinkTitle.apply(this, arguments);
10339
10340		if (Graph.isPageLink(href))
10341		{
10342			var comma = href.indexOf(',');
10343
10344			if (comma > 0)
10345			{
10346				var page = this.getPageById(href.substring(comma + 1));
10347
10348				if (page != null)
10349				{
10350					title = page.getName();
10351				}
10352				else
10353				{
10354					title = mxResources.get('pageNotFound');
10355				}
10356			}
10357		}
10358		else if (href.substring(0, 5) == 'data:')
10359		{
10360			title = mxResources.get('action');
10361		}
10362
10363		return title;
10364	};
10365
10366	/**
10367	 *
10368	 */
10369	EditorUi.prototype.handleCustomLink = function(href)
10370	{
10371		if (Graph.isPageLink(href))
10372		{
10373			var comma = href.indexOf(',');
10374			var page = this.getPageById(href.substring(comma + 1));
10375
10376			if (page)
10377			{
10378				this.selectPage(page)
10379			}
10380			else
10381			{
10382				// Needs fallback for missing resource in case of viewer lightbox
10383				throw new Error(mxResources.get('pageNotFound') || 'Page not found');
10384			}
10385		}
10386		else
10387		{
10388			this.editor.graph.handleCustomLink(href);
10389		}
10390	};
10391
10392	/**
10393	 *
10394	 */
10395	EditorUi.prototype.isSettingsEnabled = function()
10396	{
10397		return typeof window.mxSettings !== 'undefined' && (isLocalStorage || mxClient.IS_CHROMEAPP);
10398	};
10399
10400	/**
10401	 * Creates the format panel and adds overrides.
10402	 */
10403	EditorUi.prototype.installSettings = function()
10404	{
10405		if (this.isSettingsEnabled())
10406		{
10407			// Sets global switch for sketch mode
10408			Editor.pagesVisible = mxSettings.settings.pagesVisible;
10409
10410			// Gets recent colors from settings
10411			ColorDialog.recentColors = mxSettings.getRecentColors();
10412
10413			// Avoids overridden values for changes in
10414			// multiple windows and updates shared values
10415			if (isLocalStorage)
10416			{
10417				try
10418				{
10419					window.addEventListener('storage', mxUtils.bind(this, function(evt)
10420					{
10421						if (evt.key == mxSettings.key)
10422						{
10423							mxSettings.load();
10424
10425							// Updates values
10426							ColorDialog.recentColors = mxSettings.getRecentColors();
10427							this.menus.customFonts = mxSettings.getCustomFonts();
10428						}
10429					}), false);
10430				}
10431				catch (e)
10432				{
10433					// ignore
10434				}
10435			}
10436
10437			// Updates UI to reflect current edge style
10438			this.fireEvent(new mxEventObject('styleChanged', 'keys', [], 'values', [], 'cells', []));
10439
10440			/**
10441			 * Persists custom fonts.
10442			 */
10443			this.menus.customFonts = mxSettings.getCustomFonts();
10444
10445			this.addListener('customFontsChanged', mxUtils.bind(this, function(sender, evt)
10446			{
10447				if (urlParams['ext-fonts'] != '1')
10448				{
10449					mxSettings.setCustomFonts(this.menus.customFonts);
10450				}
10451				else
10452				{
10453					var customFonts = evt.getProperty('customFonts');
10454					this.menus.customFonts = customFonts;
10455					mxSettings.setCustomFonts(customFonts);
10456				}
10457
10458				mxSettings.save();
10459			}));
10460
10461			/**
10462			 * Persists copy on connect switch.
10463			 */
10464			this.editor.graph.connectionHandler.setCreateTarget(mxSettings.isCreateTarget());
10465			this.fireEvent(new mxEventObject('copyConnectChanged'));
10466
10467			this.addListener('copyConnectChanged', mxUtils.bind(this, function(sender, evt)
10468			{
10469				mxSettings.setCreateTarget(this.editor.graph.connectionHandler.isCreateTarget());
10470				mxSettings.save();
10471			}));
10472
10473			/**
10474			 * Persists default page format.
10475			 */
10476			this.editor.graph.pageFormat = (this.editor.graph.defaultPageFormat != null) ?
10477				this.editor.graph.defaultPageFormat : mxSettings.getPageFormat();
10478
10479			this.addListener('pageFormatChanged', mxUtils.bind(this, function(sender, evt)
10480			{
10481				mxSettings.setPageFormat(this.editor.graph.pageFormat);
10482				mxSettings.save();
10483			}));
10484
10485			/**
10486			 * Persists default grid color.
10487			 */
10488			this.editor.graph.view.gridColor = mxSettings.getGridColor(Editor.isDarkMode());
10489
10490			this.addListener('gridColorChanged', mxUtils.bind(this, function(sender, evt)
10491			{
10492				mxSettings.setGridColor(this.editor.graph.view.gridColor, Editor.isDarkMode());
10493				mxSettings.save();
10494			}));
10495
10496			/**
10497			 * Persists autosave switch in Chrome app.
10498			 */
10499			if (mxClient.IS_CHROMEAPP || EditorUi.isElectronApp)
10500			{
10501				this.editor.addListener('autosaveChanged', mxUtils.bind(this, function(sender, evt)
10502				{
10503					mxSettings.setAutosave(this.editor.autosave);
10504					mxSettings.save();
10505				}));
10506
10507				this.editor.autosave = mxSettings.getAutosave();
10508			}
10509
10510			if (this.sidebar != null)
10511			{
10512				if (urlParams['search-shapes'] != null && this.sidebar.searchShapes != null)
10513				{
10514					this.sidebar.searchShapes(decodeURIComponent(urlParams['search-shapes']));
10515					this.sidebar.showEntries('search');
10516				}
10517				else
10518				{
10519					this.sidebar.showPalette('search', mxSettings.settings.search);
10520
10521					/**
10522					 * Shows scratchpad if never shown.
10523					 */
10524					if ((!this.editor.chromeless || this.editor.editable) && (mxSettings.settings.isNew ||
10525						parseInt(mxSettings.settings.version || 0) <= 8))
10526					{
10527						this.toggleScratchpad();
10528						mxSettings.save();
10529					}
10530				}
10531			}
10532
10533			// Saves app defaults for UI
10534			this.addListener('formatWidthChanged', function()
10535			{
10536				mxSettings.setFormatWidth(this.formatWidth);
10537				mxSettings.save();
10538			});
10539		}
10540	};
10541
10542	/**
10543	 * Copies the given cells and XML to the clipboard as an embedded image.
10544	 */
10545	EditorUi.prototype.copyImage = function(cells, xml, scale)
10546	{
10547		try
10548		{
10549			if (navigator.clipboard != null && this.spinner.spin(document.body, mxResources.get('exporting')))
10550			{
10551				this.editor.exportToCanvas(mxUtils.bind(this, function(canvas, svgRoot)
10552				{
10553					try
10554					{
10555						this.spinner.stop();
10556
10557						// KNOWN: SVG and delayed content currently not supported
10558						var dataUrl = this.createImageDataUri(canvas, xml, 'png');
10559						var w = parseInt(svgRoot.getAttribute('width'));
10560						var h = parseInt(svgRoot.getAttribute('height'));
10561						this.writeImageToClipboard(dataUrl, w, h, mxUtils.bind(this, function(e)
10562						{
10563							this.handleError(e);
10564						}));
10565					}
10566					catch (e)
10567					{
10568						this.handleError(e);
10569					}
10570				}), null, null, null, mxUtils.bind(this, function(e)
10571				{
10572					this.spinner.stop();
10573					this.handleError(e);
10574				}), null, null, (scale != null) ? scale : 4,
10575					this.editor.graph.background == null ||
10576					this.editor.graph.background == mxConstants.NONE,
10577					null, null, null, 10, null, null, false, null,
10578					(cells.length > 0) ? cells : null);
10579			}
10580		}
10581		catch (e)
10582		{
10583			this.handleError(e);
10584		}
10585	};
10586
10587	/**
10588	 * Copies the given cells and XML to the clipboard as an embedded image.
10589	 */
10590	EditorUi.prototype.writeImageToClipboard = function(dataUrl, w, h, error)
10591	{
10592		var blob = this.base64ToBlob(dataUrl.substring(dataUrl.indexOf(',') + 1), 'image/png');
10593		var html = '<img src="' + dataUrl + '" width="' + w + '" height="' + h + '">';
10594		var cbi = new ClipboardItem({'image/png': blob,
10595			'text/html': new Blob([html], {type: 'text/html'})});
10596		navigator.clipboard.write([cbi])['catch'](error);
10597	};
10598
10599	/**
10600	 * Creates the format panel and adds overrides.
10601	 */
10602	EditorUi.prototype.copyCells = function(elt, removeCells)
10603	{
10604		var graph = this.editor.graph;
10605
10606		if (!graph.isSelectionEmpty())
10607		{
10608			// Fixes cross-platform clipboard UTF8 issues by encoding as URI
10609			var cells = mxUtils.sortCells(graph.model.getTopmostCells(graph.getSelectionCells()));
10610			var xml = mxUtils.getXml(graph.encodeCells(cells));
10611			mxUtils.setTextContent(elt, encodeURIComponent(xml));
10612
10613			if (removeCells)
10614			{
10615				graph.removeCells(cells, false);
10616				graph.lastPasteXml = null;
10617			}
10618			else
10619			{
10620				graph.lastPasteXml = xml;
10621				graph.pasteCounter = 0;
10622			}
10623
10624			elt.focus();
10625			document.execCommand('selectAll', false, null);
10626		}
10627		else
10628		{
10629			// Disables copy on focused element
10630			elt.innerHTML = '';
10631		}
10632	};
10633
10634	/**
10635	 * Creates the format panel and adds overrides.
10636	 */
10637	EditorUi.prototype.copyXml = function()
10638	{
10639		var cells = null;
10640
10641		if (Editor.enableNativeCipboard)
10642		{
10643			var graph = this.editor.graph;
10644
10645			if (!graph.isSelectionEmpty())
10646			{
10647				cells = mxUtils.sortCells(graph.getExportableCells(
10648					graph.model.getTopmostCells(graph.getSelectionCells())));
10649				var xml = mxUtils.getXml(graph.encodeCells(cells));
10650				navigator.clipboard.writeText(xml);
10651			}
10652		}
10653
10654		return cells;
10655	};
10656
10657	/**
10658	 * Creates the format panel and adds overrides.
10659	 */
10660	EditorUi.prototype.pasteXml = function(xml, pasteAsLabel, compat, evt)
10661	{
10662		var graph = this.editor.graph;
10663		var cells = null;
10664
10665		if (graph.lastPasteXml == xml)
10666		{
10667			graph.pasteCounter++;
10668		}
10669		else
10670		{
10671			graph.lastPasteXml = xml;
10672			graph.pasteCounter = 0;
10673		}
10674
10675		var dx = graph.pasteCounter * graph.gridSize;
10676
10677		if (compat || this.isCompatibleString(xml))
10678		{
10679			cells = this.importXml(xml, dx, dx);
10680			graph.setSelectionCells(cells);
10681		}
10682		else if (pasteAsLabel && graph.getSelectionCount() == 1)
10683		{
10684			var cell = graph.getStartEditingCell(graph.getSelectionCell(), evt);
10685
10686			if ((/\.(gif|jpg|jpeg|tiff|png|svg)$/i).test(xml) &&
10687				graph.getCurrentCellStyle(cell)[mxConstants.STYLE_SHAPE] == 'image')
10688			{
10689				graph.setCellStyles(mxConstants.STYLE_IMAGE, xml, [cell]);
10690			}
10691			else
10692			{
10693				graph.model.beginUpdate();
10694        		try
10695        		{
10696					graph.labelChanged(cell, xml);
10697
10698					if (Graph.isLink(xml))
10699					{
10700						graph.setLinkForCell(cell, xml);
10701					}
10702				}
10703        		finally
10704        		{
10705        			graph.model.endUpdate();
10706        		}
10707			}
10708
10709			graph.setSelectionCell(cell);
10710		}
10711		else
10712		{
10713			var pt = graph.getInsertPoint();
10714
10715			if (graph.isMouseInsertPoint())
10716			{
10717				dx = 0;
10718
10719				// No offset for insert at mouse position
10720				if (graph.lastPasteXml == xml && graph.pasteCounter > 0)
10721				{
10722					graph.pasteCounter--;
10723				}
10724			}
10725
10726			cells = this.insertTextAt(xml, pt.x + dx, pt.y + dx, true);
10727			graph.setSelectionCells(cells);
10728		}
10729
10730		if (!graph.isSelectionEmpty())
10731		{
10732			graph.scrollCellToVisible(graph.getSelectionCell());
10733
10734			if (this.hoverIcons != null)
10735			{
10736				this.hoverIcons.update(graph.view.getState(graph.getSelectionCell()));
10737			}
10738		}
10739
10740		return cells;
10741	};
10742
10743	/**
10744	 * Creates the format panel and adds overrides.
10745	 */
10746	EditorUi.prototype.pasteCells = function(evt, realElt, useEvent, pasteAsLabel)
10747	{
10748		if (!mxEvent.isConsumed(evt))
10749		{
10750			var elt = realElt;
10751			var asHtml = false;
10752
10753			if (useEvent && evt.clipboardData != null && evt.clipboardData.getData)
10754			{
10755				// Workaround for paste from IE11 where the page is copied
10756				// as HTML while the data is only available via text/plain
10757				var plain = evt.clipboardData.getData('text/plain');
10758				var override = false;
10759
10760				if (plain != null && plain.length > 0 && plain.substring(0, 18) == '%3CmxGraphModel%3E')
10761				{
10762					var tmp = decodeURIComponent(plain);
10763
10764					if (this.isCompatibleString(tmp))
10765					{
10766						override = true;
10767						plain = tmp;
10768					}
10769				}
10770
10771				var data = (!override) ? evt.clipboardData.getData('text/html') : null;
10772
10773				if (data != null && data.length > 0)
10774				{
10775					elt = this.parseHtmlData(data);
10776					asHtml = elt.getAttribute('data-type') != 'text/plain';
10777				}
10778				else if (plain != null && plain.length > 0)
10779				{
10780					elt = document.createElement('div');
10781					mxUtils.setTextContent(elt, data);
10782				}
10783			}
10784
10785			var spans = elt.getElementsByTagName('span');
10786
10787			if (spans != null && spans.length > 0 && spans[0].getAttribute('data-lucid-type') ===
10788				'application/vnd.lucid.chart.objects')
10789			{
10790				var content = spans[0].getAttribute('data-lucid-content');
10791
10792				if (content != null && content.length > 0)
10793				{
10794					this.convertLucidChart(content, mxUtils.bind(this, function(xml)
10795					{
10796						var graph = this.editor.graph;
10797
10798						if (graph.lastPasteXml == xml)
10799						{
10800							graph.pasteCounter++;
10801						}
10802						else
10803						{
10804							graph.lastPasteXml = xml;
10805							graph.pasteCounter = 0;
10806						}
10807
10808						var dx = graph.pasteCounter * graph.gridSize;
10809						graph.setSelectionCells(this.importXml(xml, dx, dx));
10810						graph.scrollCellToVisible(graph.getSelectionCell());
10811					}), mxUtils.bind(this, function(e)
10812					{
10813						this.handleError(e);
10814					}));
10815
10816					mxEvent.consume(evt);
10817				}
10818			}
10819			else
10820			{
10821				// KNOWN: Paste from IE11 to other browsers on Windows
10822				// seems to paste the contents of index.html
10823				var xml = (asHtml) ? elt.innerHTML :
10824					mxUtils.trim((elt.innerText == null) ?
10825					mxUtils.getTextContent(elt) : elt.innerText);
10826				var compat = false;
10827
10828				// Workaround for junk after XML in VM
10829				try
10830				{
10831					var idx = xml.lastIndexOf('%3E');
10832
10833					if (idx >= 0 && idx < xml.length - 3)
10834					{
10835						xml = xml.substring(0, idx + 3);
10836					}
10837				}
10838				catch (e)
10839				{
10840					// ignore
10841				}
10842
10843				// Checks for embedded XML content
10844				try
10845				{
10846					var spans = elt.getElementsByTagName('span');
10847					var tmp = (spans != null && spans.length > 0) ?
10848						mxUtils.trim(decodeURIComponent(spans[0].textContent)) :
10849						decodeURIComponent(xml);
10850
10851					if (this.isCompatibleString(tmp))
10852					{
10853						compat = true;
10854						xml = tmp;
10855					}
10856				}
10857				catch (e)
10858				{
10859					// ignore
10860				}
10861
10862				try
10863				{
10864					if (xml != null && xml.length > 0)
10865					{
10866						this.pasteXml(xml, pasteAsLabel, compat, evt);
10867
10868						try
10869						{
10870							mxEvent.consume(evt);
10871						}
10872						catch (e)
10873						{
10874							// ignore event no longer exists in async handler in IE8-
10875						}
10876					}
10877					else if (!useEvent)
10878					{
10879						var graph = this.editor.graph;
10880
10881						graph.lastPasteXml = null;
10882						graph.pasteCounter = 0;
10883					}
10884				}
10885				catch (e)
10886				{
10887					this.handleError(e);
10888				}
10889			}
10890		}
10891
10892		realElt.innerHTML = '&nbsp;';
10893	};
10894
10895	/**
10896	 * Adds a file drop handler for opening local files.
10897	 */
10898	EditorUi.prototype.addFileDropHandler = function(elts)
10899	{
10900		// Installs drag and drop handler for files
10901		if (Graph.fileSupport)
10902		{
10903			var dropElt = null;
10904
10905			for (var i = 0; i < elts.length; i++)
10906			{
10907				// Setup the dnd listeners
10908				mxEvent.addListener(elts[i], 'dragleave', function(evt)
10909				{
10910					if (dropElt != null)
10911				    {
10912				    	dropElt.parentNode.removeChild(dropElt);
10913				    	dropElt = null;
10914				    }
10915
10916					evt.stopPropagation();
10917					evt.preventDefault();
10918				});
10919
10920				mxEvent.addListener(elts[i], 'dragover', mxUtils.bind(this, function(evt)
10921				{
10922					if (this.editor.graph.isEnabled() || urlParams['embed'] != '1')
10923					{
10924						// IE 10 does not implement pointer-events so it can't have a drop highlight
10925						if (dropElt == null && (!mxClient.IS_IE || (document.documentMode > 10 && document.documentMode < 12)))
10926						{
10927							dropElt = this.highlightElement();
10928						}
10929					}
10930
10931					evt.stopPropagation();
10932					evt.preventDefault();
10933				}));
10934
10935				mxEvent.addListener(elts[i], 'drop', mxUtils.bind(this, function(evt)
10936				{
10937					if (dropElt != null)
10938				    {
10939					    dropElt.parentNode.removeChild(dropElt);
10940					    dropElt = null;
10941				    }
10942
10943					if (this.editor.graph.isEnabled() || urlParams['embed'] != '1')
10944					{
10945						if (evt.dataTransfer.files.length > 0)
10946						{
10947							this.hideDialog();
10948
10949							// Never open files in embed mode
10950							if (urlParams['embed'] == '1')
10951							{
10952								this.importFiles(evt.dataTransfer.files, 0, 0, this.maxImageSize, null, null,
10953									null, null, !mxEvent.isControlDown(evt) && !mxEvent.isShiftDown(evt));
10954							}
10955							else
10956							{
10957								this.openFiles(evt.dataTransfer.files, true);
10958							}
10959						}
10960						else
10961						{
10962							// Handles open special files via text drag and drop
10963							var data = this.extractGraphModelFromEvent(evt);
10964
10965							// Tries additional and async parsing of text content such as HTML, Gliffy data
10966							if (data == null)
10967							{
10968								var provider = (evt.dataTransfer != null) ? evt.dataTransfer : evt.clipboardData;
10969
10970								if (provider != null)
10971								{
10972									if (document.documentMode == 10 || document.documentMode == 11)
10973									{
10974										data = provider.getData('Text');
10975									}
10976									else
10977									{
10978								    	var data = null;
10979
10980								    	if (mxUtils.indexOf(provider.types, 'text/uri-list') >= 0)
10981								    	{
10982								    		data = evt.dataTransfer.getData('text/uri-list');
10983								    	}
10984								    	else
10985								    	{
10986								    		data = (mxUtils.indexOf(provider.types, 'text/html') >= 0) ? provider.getData('text/html') : null;
10987								    	}
10988
10989										if (data != null && data.length > 0)
10990										{
10991											var div = document.createElement('div');
10992								    		div.innerHTML = this.editor.graph.sanitizeHtml(data);
10993
10994								    		// Extracts single image
10995								    		var imgs = div.getElementsByTagName('img');
10996
10997								    		if (imgs.length > 0)
10998								    		{
10999								    			data = imgs[0].getAttribute('src');
11000								    		}
11001										}
11002										else if (mxUtils.indexOf(provider.types, 'text/plain') >= 0)
11003										{
11004											data = provider.getData('text/plain');
11005										}
11006									}
11007
11008									if (data != null)
11009									{
11010										// Checks for embedded XML in PNG
11011										if (data.substring(0, 22) == 'data:image/png;base64,')
11012										{
11013											var xml = this.extractGraphModelFromPng(data);
11014
11015											if (xml != null && xml.length > 0)
11016											{
11017												this.openLocalFile(xml, null, true);
11018											}
11019										}
11020										else if (!this.isOffline() && this.isRemoteFileFormat(data))
11021										{
11022								    		new mxXmlRequest(OPEN_URL, 'format=xml&data=' + encodeURIComponent(data)).send(mxUtils.bind(this, function(req)
11023											{
11024								    			if (req.getStatus() >= 200 && req.getStatus() <= 299)
11025								    			{
11026								    				this.openLocalFile(req.getText(), null, true);
11027								    			}
11028											}));
11029										}
11030										else if (/^https?:\/\//.test(data))
11031										{
11032											if (this.getCurrentFile() == null)
11033											{
11034												window.location.hash = '#U' + encodeURIComponent(data);
11035											}
11036											else
11037											{
11038												window.openWindow(((mxClient.IS_CHROMEAPP) ?
11039													(EditorUi.drawHost + '/') : 'https://' + location.host + '/') +
11040													window.location.search + '#U' + encodeURIComponent(data));
11041											}
11042										}
11043									}
11044								}
11045							}
11046							else
11047							{
11048								this.openLocalFile(data, null, true);
11049							}
11050						}
11051					}
11052
11053					evt.stopPropagation();
11054					evt.preventDefault();
11055				}));
11056			}
11057		}
11058	};
11059
11060	/**
11061	 * Highlights the given element
11062	 */
11063	EditorUi.prototype.highlightElement = function(elt)
11064	{
11065		var x = 0;
11066		var y = 0;
11067		var w = 0;
11068		var h = 0;
11069
11070		if (elt == null)
11071		{
11072			var b = document.body;
11073			var d = document.documentElement;
11074
11075			w = (b.clientWidth || d.clientWidth) - 3;
11076			h = Math.max(b.clientHeight || 0, d.clientHeight) - 3;
11077		}
11078		else
11079		{
11080			x = elt.offsetTop;
11081			y = elt.offsetLeft;
11082			w = elt.clientWidth;
11083			h = elt.clientHeight;
11084		}
11085
11086		var hl = document.createElement('div');
11087		hl.style.zIndex = mxPopupMenu.prototype.zIndex + 2;
11088		hl.style.border = '3px dotted rgb(254, 137, 12)';
11089		hl.style.pointerEvents = 'none';
11090		hl.style.position = 'absolute';
11091		hl.style.top = x + 'px';
11092		hl.style.left = y + 'px';
11093		hl.style.width = Math.max(0, w - 3) + 'px';
11094		hl.style.height = Math.max(0, h - 3) + 'px';
11095
11096		if (elt != null && elt.parentNode == this.editor.graph.container)
11097		{
11098			this.editor.graph.container.appendChild(hl);
11099		}
11100		else
11101		{
11102			document.body.appendChild(hl);
11103		}
11104
11105		return hl;
11106	};
11107
11108	/**
11109	 * Highlights the given element
11110	 */
11111	EditorUi.prototype.stringToCells = function(xml)
11112	{
11113		var doc = mxUtils.parseXml(xml);
11114		var node = this.editor.extractGraphModel(doc.documentElement);
11115		var cells = [];
11116
11117		if (node != null)
11118		{
11119			var codec = new mxCodec(node.ownerDocument);
11120			var model = new mxGraphModel();
11121			codec.decode(node, model);
11122
11123			var parent = model.getChildAt(model.getRoot(), 0);
11124
11125			for (var j = 0; j < model.getChildCount(parent); j++)
11126			{
11127				cells.push(model.getChildAt(parent, j));
11128			}
11129		}
11130
11131		return cells;
11132	};
11133
11134	/**
11135	 * Opens the given files in the editor.
11136	 */
11137	EditorUi.prototype.openFileHandle = function(data, name, file, temp, fileHandle)
11138	{
11139		if (name != null && name.length > 0)
11140		{
11141			if (!this.useCanvasForExport && /(\.png)$/i.test(name))
11142			{
11143				name = name.substring(0, name.length - 4) + '.drawio';
11144			}
11145			else if (/(\.pdf)$/i.test(name))
11146			{
11147				name = name.substring(0, name.length - 4) + '.drawio';
11148			}
11149
11150			var handleResult = mxUtils.bind(this, function(xml)
11151			{
11152				var dot = name.lastIndexOf('.');
11153
11154				if (dot >= 0)
11155				{
11156					name = name.substring(0, name.lastIndexOf('.')) + '.drawio';
11157				}
11158				else
11159				{
11160					name = name + '.drawio';
11161				}
11162
11163				if (xml.substring(0, 10) == '<mxlibrary')
11164				{
11165					// Creates new temporary file if library is dropped in splash screen
11166					if (this.getCurrentFile() == null && urlParams['embed'] != '1')
11167					{
11168						this.openLocalFile(this.emptyDiagramXml, this.defaultFilename, temp);
11169					}
11170
11171    				try
11172	    			{
11173    					this.loadLibrary(new LocalLibrary(this, xml, name));
11174	    			}
11175    				catch (e)
11176	    			{
11177	    				this.handleError(e, mxResources.get('errorLoadingFile'));
11178	    			}
11179				}
11180				else
11181				{
11182					this.openLocalFile(xml, name, temp);
11183				}
11184			});
11185
11186			if  (/(\.v(dx|sdx?))($|\?)/i.test(name) || /(\.vs(x|sx?))($|\?)/i.test(name))
11187			{
11188				this.importVisio(file, mxUtils.bind(this, function(xml)
11189				{
11190					this.spinner.stop();
11191					handleResult(xml);
11192				}));
11193			}
11194			else if (/(\.*<graphml )/.test(data))
11195			{
11196				this.importGraphML(data, mxUtils.bind(this, function(xml)
11197				{
11198					this.spinner.stop();
11199					handleResult(xml);
11200				}));
11201			}
11202			else if (Graph.fileSupport && !this.isOffline() && new XMLHttpRequest().upload &&
11203				this.isRemoteFileFormat(data, name))
11204			{
11205				this.parseFile(file, mxUtils.bind(this, function(xhr)
11206				{
11207					if (xhr.readyState == 4)
11208					{
11209						this.spinner.stop();
11210
11211						if (xhr.status >= 200 && xhr.status <= 299)
11212						{
11213							handleResult(xhr.responseText);
11214						}
11215						else
11216						{
11217							this.handleError({message: mxResources.get((xhr.status == 413) ?
11218        						'drawingTooLarge' : 'invalidOrMissingFile')},
11219        						mxResources.get('errorLoadingFile'));
11220						}
11221					}
11222				}));
11223			}
11224			else if (this.isLucidChartData(data))
11225			{
11226				if (/(\.json)$/i.test(name))
11227				{
11228					name = name.substring(0, name.length - 5) + '.drawio';
11229				}
11230
11231				// LATER: Add import step that produces cells and use callback
11232				this.convertLucidChart(data, mxUtils.bind(this, function(xml)
11233				{
11234					this.spinner.stop();
11235					this.openLocalFile(xml, name, temp);
11236				}), mxUtils.bind(this, function(e)
11237				{
11238					this.spinner.stop();
11239					this.handleError(e);
11240				}));
11241			}
11242			else if (data.substring(0, 10) == '<mxlibrary')
11243			{
11244				this.spinner.stop();
11245
11246				// Creates new temporary file if library is dropped in splash screen
11247				if (this.getCurrentFile() == null && urlParams['embed'] != '1')
11248				{
11249					this.openLocalFile(this.emptyDiagramXml, this.defaultFilename, temp);
11250				}
11251
11252				try
11253    			{
11254    				this.loadLibrary(new LocalLibrary(this, data, file.name));
11255    			}
11256    			catch (e)
11257    			{
11258    				this.handleError(e, mxResources.get('errorLoadingFile'));
11259    			}
11260			}
11261			else if (data.indexOf('PK') == 0)
11262			{
11263				this.importZipFile(file, mxUtils.bind(this, function(xml)
11264				{
11265					this.spinner.stop();
11266					handleResult(xml);
11267				}), mxUtils.bind(this, function()
11268				{
11269					this.spinner.stop();
11270					this.openLocalFile(data, name, temp);
11271				}));
11272			}
11273			else
11274			{
11275				if (file.type.substring(0, 9) == 'image/png')
11276				{
11277					data = this.extractGraphModelFromPng(data);
11278				}
11279				else if (file.type == 'application/pdf')
11280	    		{
11281					var xml = Editor.extractGraphModelFromPdf(data);
11282
11283					if (xml != null)
11284					{
11285						fileHandle = null;
11286						temp = true;
11287						data = xml;
11288					}
11289	    		}
11290
11291				this.spinner.stop();
11292				this.openLocalFile(data, name, temp, fileHandle, (fileHandle != null) ? file : null);
11293			}
11294		}
11295	};
11296
11297	/**
11298	 * Opens the given files in the editor.
11299	 */
11300	EditorUi.prototype.openFiles = function(files, temp)
11301	{
11302		if (this.spinner.spin(document.body, mxResources.get('loading')))
11303		{
11304			for (var i = 0; i < files.length; i++)
11305			{
11306				(mxUtils.bind(this, function(file)
11307				{
11308					var reader = new FileReader();
11309
11310					reader.onload = mxUtils.bind(this, function(e)
11311					{
11312						try
11313						{
11314							this.openFileHandle(e.target.result, file.name, file, temp);
11315						}
11316						catch (e)
11317						{
11318							this.handleError(e);
11319						}
11320					});
11321
11322					reader.onerror = mxUtils.bind(this, function(e)
11323					{
11324						this.spinner.stop();
11325						this.handleError(e);
11326						window.openFile = null;
11327					});
11328
11329					if ((file.type.substring(0, 5) === 'image' ||
11330						file.type === 'application/pdf') &&
11331						file.type.substring(0, 9) !== 'image/svg')
11332					{
11333						reader.readAsDataURL(file);
11334					}
11335					else
11336					{
11337						reader.readAsText(file);
11338					}
11339				}))(files[i]);
11340			}
11341		}
11342	};
11343
11344	/**
11345	 * Shows the layers dialog if the graph has more than one layer.
11346	 */
11347	EditorUi.prototype.openLocalFile = function(data, name, temp, fileHandle, desc)
11348	{
11349		var currentFile = this.getCurrentFile();
11350
11351		var fn = mxUtils.bind(this, function()
11352		{
11353			window.openFile = null;
11354
11355			if (name == null && this.getCurrentFile() != null && this.isDiagramEmpty())
11356			{
11357				var doc = mxUtils.parseXml(data);
11358
11359				if (doc != null)
11360				{
11361					this.editor.setGraphXml(doc.documentElement);
11362					this.editor.graph.selectAll();
11363				}
11364			}
11365			else
11366			{
11367				this.fileLoaded(new LocalFile(this, data, name ||
11368					this.defaultFilename, temp, fileHandle, desc));
11369			}
11370		});
11371
11372		if (data != null && data.length > 0)
11373		{
11374			if (currentFile == null || (!currentFile.isModified() &&
11375				(mxClient.IS_CHROMEAPP || EditorUi.isElectronApp || fileHandle != null)))
11376			{
11377				fn();
11378			}
11379			else if ((mxClient.IS_CHROMEAPP || EditorUi.isElectronApp || fileHandle != null) &&
11380				currentFile != null && currentFile.isModified())
11381			{
11382				this.confirm(mxResources.get('allChangesLost'), null, fn,
11383					mxResources.get('cancel'), mxResources.get('discardChanges'));
11384			}
11385			else
11386			{
11387				window.openFile = new OpenFile(function()
11388				{
11389					window.openFile = null;
11390				});
11391
11392				window.openFile.setData(data, name);
11393				window.openWindow(this.getUrl(), null, mxUtils.bind(this, function()
11394				{
11395					if (currentFile != null && currentFile.isModified())
11396					{
11397						this.confirm(mxResources.get('allChangesLost'), null, fn,
11398							mxResources.get('cancel'), mxResources.get('discardChanges'));
11399					}
11400					else
11401					{
11402						fn();
11403					}
11404				}));
11405			}
11406		}
11407		else
11408		{
11409			throw new Error(mxResources.get('notADiagramFile'));
11410		}
11411	};
11412
11413	/**
11414	 * Returns a list of all shapes used in the current file.
11415	 */
11416	EditorUi.prototype.getBasenames = function()
11417	{
11418		var basenames = {};
11419
11420		if (this.pages != null)
11421		{
11422			for (var i = 0; i < this.pages.length; i++)
11423			{
11424				this.updatePageRoot(this.pages[i]);
11425				this.addBasenamesForCell(this.pages[i].root, basenames);
11426			}
11427		}
11428		else
11429		{
11430			this.addBasenamesForCell(this.editor.graph.model.getRoot(), basenames);
11431		}
11432
11433		var result = [];
11434
11435		for (var key in basenames)
11436		{
11437			result.push(key);
11438		}
11439
11440		return result;
11441	};
11442
11443	/**
11444	 * Returns a list of all shapes used in the current file.
11445	 */
11446	EditorUi.prototype.addBasenamesForCell = function(cell, basenames)
11447	{
11448		function addName(name)
11449		{
11450			if (name != null)
11451			{
11452				// LATER: Check if this case exists
11453				var dot = name.lastIndexOf('.');
11454
11455				if (dot > 0)
11456				{
11457					name = name.substring(dot + 1, name.length);
11458				}
11459
11460				if (basenames[name] == null)
11461				{
11462					basenames[name] = true;
11463				}
11464			}
11465		};
11466
11467		var graph = this.editor.graph;
11468		var style = graph.getCellStyle(cell);
11469		var shape = style[mxConstants.STYLE_SHAPE];
11470		addName(mxStencilRegistry.getBasenameForStencil(shape));
11471
11472		// Adds package names for markers in edges
11473		if (graph.model.isEdge(cell))
11474		{
11475			addName(mxMarker.getPackageForType(style[mxConstants.STYLE_STARTARROW]));
11476			addName(mxMarker.getPackageForType(style[mxConstants.STYLE_ENDARROW]));
11477		}
11478
11479		var childCount = graph.model.getChildCount(cell);
11480
11481		for (var i = 0; i < childCount; i++)
11482		{
11483			this.addBasenamesForCell(graph.model.getChildAt(cell, i), basenames);
11484		}
11485	};
11486
11487	/**
11488	 * Shows the layers dialog if the graph has more than one layer.
11489	 */
11490	EditorUi.prototype.setGraphEnabled = function(enabled)
11491	{
11492		this.diagramContainer.style.visibility = (enabled) ? '' : 'hidden';
11493		this.formatContainer.style.visibility = (enabled) ? '' : 'hidden';
11494		this.sidebarFooterContainer.style.display = (enabled) ? '' : 'none';
11495		this.sidebarContainer.style.display = (enabled) ? '' : 'none';
11496		this.hsplit.style.display = (enabled) ? '' : 'none';
11497		this.editor.graph.setEnabled(enabled);
11498
11499		if (this.ruler != null)
11500		{
11501			this.ruler.hRuler.container.style.visibility = (enabled) ? '' : 'hidden';
11502			this.ruler.vRuler.container.style.visibility = (enabled) ? '' : 'hidden';
11503		}
11504
11505		if (this.tabContainer != null)
11506		{
11507			this.tabContainer.style.visibility = (enabled) ? '' : 'hidden';
11508		}
11509
11510		if (!enabled)
11511		{
11512            if (this.actions.outlineWindow != null)
11513            {
11514            	this.actions.outlineWindow.window.setVisible(false);
11515            }
11516
11517            if (this.actions.layersWindow != null)
11518            {
11519            	this.actions.layersWindow.window.setVisible(false);
11520            }
11521
11522            if (this.menus.tagsWindow != null)
11523            {
11524            	this.menus.tagsWindow.window.setVisible(false);
11525            }
11526
11527            if (this.menus.findWindow != null)
11528            {
11529            	this.menus.findWindow.window.setVisible(false);
11530            }
11531
11532            if (this.menus.findReplaceWindow != null)
11533            {
11534            	this.menus.findReplaceWindow.window.setVisible(false);
11535            }
11536		}
11537	};
11538
11539	/**
11540	 * Shows the layers dialog if the graph has more than one layer.
11541	 */
11542	EditorUi.prototype.initializeEmbedMode = function()
11543	{
11544		this.setGraphEnabled(false);
11545		var parent = window.opener || window.parent;
11546
11547		if (parent != window)
11548		{
11549			if (urlParams['spin'] != '1' || this.spinner.spin(document.body, mxResources.get('loading')))
11550			{
11551				var initialized = false;
11552
11553				this.installMessageHandler(mxUtils.bind(this, function(xml, evt, modified, convertToSketch)
11554				{
11555					if (!initialized)
11556					{
11557						initialized = true;
11558
11559						this.spinner.stop();
11560						this.addEmbedButtons();
11561						this.setGraphEnabled(true);
11562					}
11563
11564					if (xml == null || xml.length == 0)
11565					{
11566						xml = this.emptyDiagramXml;
11567					}
11568
11569					// Creates temporary file for diff sync in embed mode
11570					this.setCurrentFile(new EmbedFile(this, xml, {}));
11571					this.mode = App.MODE_EMBED;
11572					this.setFileData(xml);
11573
11574					// TODO: Check if cellsInserted should be fired instead here
11575					if (convertToSketch)
11576					{
11577						try
11578						{
11579							//Disable grid and page view
11580							var graph = this.editor.graph;
11581							graph.setGridEnabled(false);
11582							graph.pageVisible = false;
11583							var cells = graph.model.cells;
11584
11585							//Add sketch style and font to all cells
11586							for (var id in cells)
11587							{
11588								var cell = cells[id];
11589
11590								if (cell != null && cell.style != null)
11591								{
11592									cell.style += ';sketch=1;' + (cell.style.indexOf('fontFamily=') == -1 || cell.style.indexOf('fontFamily=Helvetica;') > -1?
11593											'fontFamily=Architects Daughter;fontSource=https%3A%2F%2Ffonts.googleapis.com%2Fcss%3Ffamily%3DArchitects%2BDaughter;' : '');
11594								}
11595							}
11596						}
11597						catch(e)
11598						{
11599							console.log(e); //Ignore
11600						}
11601					}
11602
11603					if (!this.editor.isChromelessView())
11604					{
11605						this.showLayersDialog();
11606					}
11607					else if (this.editor.graph.isLightboxView())
11608					{
11609						this.lightboxFit();
11610					}
11611
11612					if (this.chromelessResize)
11613					{
11614						this.chromelessResize();
11615					}
11616
11617					this.editor.undoManager.clear();
11618					this.editor.modified = (modified != null) ? modified : false;
11619					this.updateUi();
11620
11621					// Workaround for no initial focus in FF
11622					// (does not work in Conf Cloud with FF)
11623					if (window.self !== window.top)
11624					{
11625						window.focus();
11626					}
11627
11628					if (this.format != null)
11629					{
11630						this.format.refresh();
11631					}
11632				}));
11633			}
11634		}
11635	};
11636
11637	/**
11638	 * Shows the layers dialog if the graph has more than one layer.
11639	 */
11640	EditorUi.prototype.showLayersDialog = function()
11641	{
11642		if (this.editor.graph.getModel().getChildCount(this.editor.graph.getModel().getRoot()) > 1)
11643		{
11644			if (this.actions.layersWindow == null)
11645			{
11646				this.actions.get('layers').funct();
11647			}
11648			else
11649			{
11650				this.actions.layersWindow.window.setVisible(true);
11651			}
11652		}
11653	};
11654
11655	/**
11656	 * Tries to find a public URL for the given file.
11657	 */
11658	EditorUi.prototype.getPublicUrl = function(file, fn)
11659	{
11660		if (file != null)
11661		{
11662			file.getPublicUrl(fn);
11663		}
11664		else
11665		{
11666			fn(null);
11667		}
11668	};
11669
11670	/**
11671	 * Adds the buttons for embedded mode.
11672	 */
11673	EditorUi.prototype.createLoadMessage = function(eventName)
11674	{
11675		var graph = this.editor.graph;
11676
11677		return {event: eventName, pageVisible: graph.pageVisible, translate: graph.view.translate,
11678			bounds: graph.getGraphBounds(), currentPage: this.getSelectedPageIndex(),
11679			scale: graph.view.scale, page: graph.view.getBackgroundPageBounds()};
11680	};
11681
11682	/**
11683	 * Adds the buttons for embedded mode.
11684	 */
11685	EditorUi.prototype.sendEmbeddedSvgExport = function(noExit)
11686	{
11687		var graph = this.editor.graph;
11688
11689		if (graph.isEditing())
11690		{
11691			graph.stopEditing(!graph.isInvokesStopCellEditing());
11692		}
11693
11694		var parent = window.opener || window.parent;
11695
11696		if (!this.editor.modified)
11697		{
11698			if (!noExit)
11699			{
11700				parent.postMessage(JSON.stringify({event: 'exit',
11701					point: this.embedExitPoint}), '*');
11702			}
11703		}
11704		else
11705		{
11706			var bg = graph.background;
11707
11708			if (bg == null || bg == mxConstants.NONE)
11709			{
11710				bg = this.embedExportBackground;
11711			}
11712
11713			this.getEmbeddedSvg(this.getFileData(true, null, null, null, null,
11714				null, null, null, null, false), graph, null, true,
11715				mxUtils.bind(this, function(svg)
11716			{
11717				parent.postMessage(JSON.stringify({
11718					event: 'export', point: this.embedExitPoint,
11719					exit: (noExit != null) ? !noExit : true,
11720					data: Editor.createSvgDataUri(svg)
11721				}), '*');
11722			}), null, null, true, bg, 1, this.embedExportBorder);
11723		}
11724
11725		if (!noExit)
11726		{
11727			this.diagramContainer.removeAttribute('data-bounds');
11728			Editor.inlineFullscreen = false;
11729			graph.model.clear();
11730			this.editor.undoManager.clear();
11731			this.setBackgroundImage(null);
11732			this.editor.modified = false;
11733
11734			this.fireEvent(new mxEventObject('editInlineStop'));
11735		}
11736	};
11737
11738	/**
11739	 * Adds the buttons for embedded mode.
11740	 */
11741	EditorUi.prototype.installMessageHandler = function(fn)
11742	{
11743		var changeListener = null;
11744		var ignoreChange = false;
11745		var autosave = false;
11746		var lastData = null;
11747
11748		var updateStatus = mxUtils.bind(this, function(sender, eventObject)
11749		{
11750			if (!this.editor.modified || urlParams['modified'] == '0')
11751			{
11752				this.editor.setStatus('');
11753			}
11754			else if (urlParams['modified'] != null)
11755			{
11756				this.editor.setStatus(mxUtils.htmlEntities(mxResources.get(urlParams['modified'])));
11757			}
11758		});
11759
11760		this.editor.graph.model.addListener(mxEvent.CHANGE, updateStatus);
11761
11762		// Receives XML message from opener and puts it into the graph
11763		mxEvent.addListener(window, 'message', mxUtils.bind(this, function(evt)
11764		{
11765			var validSource = window.opener || window.parent;
11766
11767			if (evt.source != validSource)
11768			{
11769				return;
11770			}
11771
11772			var data = evt.data;
11773			var afterLoad = null;
11774
11775			var extractDiagramXml = mxUtils.bind(this, function(data)
11776			{
11777				if (data != null && typeof data.charAt === 'function' && data.charAt(0) != '<')
11778				{
11779					try
11780					{
11781						if (data.substring(0, 22) == 'data:image/png;base64,')
11782						{
11783							data = this.extractGraphModelFromPng(data);
11784						}
11785						else if (data.substring(0, 26) == 'data:image/svg+xml;base64,')
11786						{
11787							data = atob(data.substring(26));
11788						}
11789						else if (data.substring(0, 24) == 'data:image/svg+xml;utf8,')
11790						{
11791							data = data.substring(24);
11792						}
11793
11794						if (data != null)
11795						{
11796							if (data.charAt(0) == '%')
11797							{
11798								data = decodeURIComponent(data);
11799							}
11800							else if (data.charAt(0) != '<')
11801							{
11802								data = Graph.decompress(data);
11803							}
11804						}
11805					}
11806					catch (e)
11807					{
11808						// ignore compression errors and use empty data
11809					}
11810				}
11811
11812				return data;
11813			});
11814
11815			if (urlParams['proto'] == 'json')
11816			{
11817				var convertToSketch = false;
11818
11819				try
11820				{
11821					data = JSON.parse(data);
11822				}
11823				catch (e)
11824				{
11825					data = null;
11826				}
11827
11828				try
11829				{
11830					if (data == null)
11831					{
11832						// Ignore
11833						return;
11834					}
11835					else if (data.action == 'dialog')
11836					{
11837						this.showError((data.titleKey != null) ? mxResources.get(data.titleKey) : data.title,
11838							(data.messageKey != null) ? mxResources.get(data.messageKey) : data.message,
11839							(data.buttonKey != null) ? mxResources.get(data.buttonKey) : data.button);
11840
11841						if (data.modified != null)
11842						{
11843							this.editor.modified = data.modified;
11844						}
11845
11846						return;
11847					}
11848					else if (data.action == 'layout')
11849					{
11850						this.executeLayoutList(data.layouts)
11851
11852						return;
11853					}
11854					else if (data.action == 'prompt')
11855					{
11856						this.spinner.stop();
11857
11858						var dlg = new FilenameDialog(this, data.defaultValue || '',
11859							(data.okKey != null) ? mxResources.get(data.okKey) : data.ok, function(value)
11860						{
11861							if (value != null)
11862							{
11863								parent.postMessage(JSON.stringify({event: 'prompt', value: value, message: data}), '*');
11864							}
11865							else
11866							{
11867								parent.postMessage(JSON.stringify({event: 'prompt-cancel', message: data}), '*');
11868							}
11869						}, (data.titleKey != null) ? mxResources.get(data.titleKey) : data.title);
11870						this.showDialog(dlg.container, 300, 80, true, false);
11871						dlg.init();
11872
11873						return;
11874					}
11875					else if (data.action == 'draft')
11876					{
11877						var tmp = extractDiagramXml(data.xml);
11878						this.spinner.stop();
11879
11880						var dlg = new DraftDialog(this, mxResources.get('draftFound',
11881								[data.name || this.defaultFilename]),
11882							tmp, mxUtils.bind(this, function()
11883						{
11884							this.hideDialog();
11885							parent.postMessage(JSON.stringify({event: 'draft',
11886								result: 'edit', message: data}), '*');
11887						}), mxUtils.bind(this, function()
11888						{
11889							this.hideDialog();
11890							parent.postMessage(JSON.stringify({event: 'draft',
11891								result: 'discard', message: data}), '*');
11892						}), (data.editKey) ? mxResources.get(data.editKey) : null,
11893							(data.discardKey) ? mxResources.get(data.discardKey) : null,
11894							(data.ignore) ? mxUtils.bind(this, function()
11895							{
11896								this.hideDialog();
11897								parent.postMessage(JSON.stringify({event: 'draft',
11898									result: 'ignore', message: data}), '*');
11899							}) : null);
11900						this.showDialog(dlg.container, 640, 480, true, false, mxUtils.bind(this, function(cancel)
11901						{
11902							if (cancel)
11903							{
11904								this.actions.get('exit').funct();
11905							}
11906						}));
11907
11908						try
11909						{
11910							dlg.init();
11911						}
11912						catch (e)
11913						{
11914							parent.postMessage(JSON.stringify({event: 'draft',
11915								error: e.toString(), message: data}), '*');
11916						}
11917
11918						return;
11919					}
11920					else if (data.action == 'template')
11921					{
11922						this.spinner.stop();
11923
11924						var enableRecentDocs = data.enableRecent == 1;
11925						var enableSearchDocs = data.enableSearch == 1;
11926						var enableCustomTemp = data.enableCustomTemp == 1;
11927
11928						if (urlParams['newTempDlg'] == '1' && !data.templatesOnly && data.callback != null)
11929						{
11930							var user = this.getCurrentUser();
11931
11932							var tempDlg = new TemplatesDialog(this, function(xml, filename, itemInfo)
11933							{
11934								xml = xml || this.emptyDiagramXml;
11935
11936								parent.postMessage(JSON.stringify({event: 'template', xml: xml,
11937									blank: xml == this.emptyDiagramXml, name: filename,
11938									tempUrl: itemInfo.url, libs: itemInfo.libs,
11939									builtIn: itemInfo.info != null && itemInfo.info.custContentId != null,
11940									message: data}), '*');
11941							}, mxUtils.bind(this, function()
11942							{
11943								this.actions.get('exit').funct();
11944							}), null, null, user != null? user.id : null,
11945							enableRecentDocs? mxUtils.bind(this, function(recentReadyCallback, error, username)
11946							{
11947								this.remoteInvoke('getRecentDiagrams', [username], null, recentReadyCallback, error);
11948							}) : null, enableSearchDocs?  mxUtils.bind(this, function(searchStr, searchReadyCallback, error, username)
11949							{
11950								this.remoteInvoke('searchDiagrams', [searchStr, username], null, searchReadyCallback, error);
11951							}) : null, mxUtils.bind(this, function(obj, callback, error)
11952							{
11953								this.remoteInvoke('getFileContent', [obj.url], null, callback, error);
11954							}), null, enableCustomTemp? mxUtils.bind(this, function(customTempCallback)
11955							{
11956								this.remoteInvoke('getCustomTemplates', null, null, customTempCallback, function()
11957								{
11958									customTempCallback({}, 0); //ignore error by sending empty templates
11959								});
11960							}) : null, false, false, true, true);
11961
11962							this.showDialog(tempDlg.container, window.innerWidth, window.innerHeight, true, false, null, false, true);
11963
11964							return;
11965						}
11966
11967						var dlg = new NewDialog(this, false, data.templatesOnly? false : data.callback != null,
11968							mxUtils.bind(this, function(xml, name, url, libs)
11969						{
11970							xml = xml || this.emptyDiagramXml;
11971
11972							// LATER: Add autosave option in template message
11973							if (data.callback != null)
11974							{
11975								parent.postMessage(JSON.stringify({event: 'template', xml: xml,
11976									blank: xml == this.emptyDiagramXml, name: name,
11977									tempUrl: url, libs: libs, builtIn: true,
11978									message: data}), '*');
11979							}
11980							else
11981							{
11982								fn(xml, evt, xml != this.emptyDiagramXml, data.toSketch);
11983
11984								// Workaround for status updated before modified applied
11985								if (!this.editor.modified)
11986								{
11987									this.editor.setStatus('');
11988								}
11989							}
11990						}), null, null, null, null, null, null, null,
11991						enableRecentDocs? mxUtils.bind(this, function(recentReadyCallback)
11992						{
11993							this.remoteInvoke('getRecentDiagrams', [null], null, recentReadyCallback, function()
11994							{
11995								recentReadyCallback(null, 'Network Error!');
11996							});
11997						}) : null,
11998						enableSearchDocs?  mxUtils.bind(this, function(searchStr, searchReadyCallback)
11999						{
12000							this.remoteInvoke('searchDiagrams', [searchStr, null], null, searchReadyCallback, function()
12001							{
12002								searchReadyCallback(null, 'Network Error!');
12003							});
12004						}) : null,
12005						mxUtils.bind(this, function(url, info, name)
12006						{
12007							//If binary files are possible, we can get the file content using remote invokation, imported it, and send final mxFile back
12008							parent.postMessage(JSON.stringify({event: 'template', docUrl: url, info: info,
12009								name: name}), '*');
12010						}), null, null,
12011						enableCustomTemp? mxUtils.bind(this, function(customTempCallback)
12012						{
12013							this.remoteInvoke('getCustomTemplates', null, null, customTempCallback, function()
12014							{
12015								customTempCallback({}, 0); //ignore error by sending empty templates
12016							});
12017						}) : null, data.withoutType == 1);
12018
12019						this.showDialog(dlg.container, 620, 460, true, false, mxUtils.bind(this, function(cancel)
12020						{
12021							this.sidebar.hideTooltip();
12022
12023							if (cancel)
12024							{
12025								this.actions.get('exit').funct();
12026							}
12027						}));
12028						dlg.init();
12029
12030						return;
12031					}
12032					else if (data.action == 'textContent')
12033					{
12034						//TODO Remove this message and use remote invokation instead
12035						var allPagesTxt = this.getDiagramTextContent();
12036						parent.postMessage(JSON.stringify({event: 'textContent',
12037							data: allPagesTxt, message: data}), '*');
12038						return;
12039					}
12040					else if (data.action == 'status')
12041					{
12042						if (data.messageKey != null)
12043						{
12044							this.editor.setStatus(mxUtils.htmlEntities(mxResources.get(data.messageKey)));
12045						}
12046						else if (data.message != null)
12047						{
12048							this.editor.setStatus(mxUtils.htmlEntities(data.message));
12049						}
12050
12051						if (data.modified != null)
12052						{
12053							this.editor.modified = data.modified;
12054						}
12055
12056						return;
12057					}
12058					else if (data.action == 'spinner')
12059					{
12060						var msg = (data.messageKey != null) ? mxResources.get(data.messageKey) : data.message;
12061
12062						if (data.show != null && !data.show)
12063						{
12064							this.spinner.stop();
12065						}
12066						else
12067						{
12068							this.spinner.spin(document.body, msg)
12069						}
12070
12071						return;
12072					}
12073					else if (data.action == 'exit')
12074					{
12075						this.actions.get('exit').funct();
12076
12077						return;
12078					}
12079					else if (data.action == 'viewport')
12080					{
12081						if (data.viewport != null)
12082						{
12083							this.embedViewport = data.viewport;
12084						}
12085
12086						return;
12087					}
12088					else if (data.action == 'snapshot')
12089					{
12090						this.sendEmbeddedSvgExport(true);
12091
12092						return;
12093					}
12094					else if (data.action == 'export')
12095					{
12096						if (data.format == 'png' || data.format == 'xmlpng')
12097						{
12098							if ((data.spin == null && data.spinKey == null) || this.spinner.spin(document.body,
12099								(data.spinKey != null) ? mxResources.get(data.spinKey) : data.spin))
12100							{
12101								var xml = (data.xml != null) ? data.xml : this.getFileData(true);
12102								this.editor.graph.setEnabled(false);
12103								var graph = this.editor.graph;
12104
12105								var postDataBack = mxUtils.bind(this, function(uri)
12106								{
12107									this.editor.graph.setEnabled(true);
12108									this.spinner.stop();
12109
12110									var msg = this.createLoadMessage('export');
12111									msg.format = data.format;
12112									msg.message = data;
12113									msg.data = uri;
12114									msg.xml = xml;
12115									parent.postMessage(JSON.stringify(msg), '*');
12116								});
12117
12118								var processUri = mxUtils.bind(this, function(uri)
12119								{
12120									if (uri == null)
12121									{
12122										uri = Editor.blankImage;
12123									}
12124
12125							   	    if (data.format == 'xmlpng')
12126							   	    {
12127							   	    	uri = Editor.writeGraphModelToPng(uri, 'tEXt', 'mxfile',
12128							   	    		encodeURIComponent(xml));
12129							   	    }
12130
12131									// Removes temporary graph from DOM
12132							   	    if (graph != this.editor.graph)
12133									{
12134										graph.container.parentNode.removeChild(graph.container);
12135									}
12136
12137							   	    postDataBack(uri);
12138								});
12139
12140								var pageId = data.pageId || (this.pages != null? ((data.currentPage) ?
12141									this.currentPage.getId() : this.pages[0].getId()) : null);
12142
12143								if (this.isExportToCanvas())
12144								{
12145									var graphReady = mxUtils.bind(this, function()
12146									{
12147										// Exports PNG for first/specific page while other page is visible by creating a graph
12148										// LATER: Add caching for the graph or SVG while not on first page
12149										if (this.pages != null && this.currentPage.getId() != pageId)
12150										{
12151											var graphGetGlobalVariable = graph.getGlobalVariable;
12152											graph = this.createTemporaryGraph(graph.getStylesheet());
12153											var page;
12154
12155											for (var i = 0; i < this.pages.length; i++)
12156											{
12157												if (this.pages[i].getId() == pageId)
12158												{
12159													page = this.updatePageRoot(this.pages[i]);
12160													break;
12161												}
12162											}
12163
12164											//If pageId info is incorrect
12165											if (page == null)
12166											{
12167												page = this.currentPage;
12168											}
12169
12170											graph.getGlobalVariable = function(name)
12171											{
12172												if (name == 'page')
12173												{
12174													return page.getName();
12175												}
12176												else if (name == 'pagenumber')
12177												{
12178													return 1;
12179												}
12180
12181												return graphGetGlobalVariable.apply(this, arguments);
12182											};
12183
12184											document.body.appendChild(graph.container);
12185											graph.model.setRoot(page.root);
12186										}
12187
12188										// Set visible layers based on message setting
12189										if (data.layerIds != null)
12190										{
12191											var graphModel = graph.model;
12192											var layers = graphModel.getChildCells(graphModel.getRoot());
12193											var layerIdsMap = {};
12194
12195											for (var i = 0; i < data.layerIds.length; i++)
12196											{
12197												layerIdsMap[data.layerIds[i]] = true;
12198											}
12199
12200											for (var i = 0; i < layers.length; i++)
12201											{
12202												graphModel.setVisible(layers[i], layerIdsMap[layers[i].id] || false);
12203											}
12204										}
12205
12206										this.editor.exportToCanvas(mxUtils.bind(this, function(canvas)
12207										{
12208											processUri(canvas.toDataURL('image/png'));
12209										}), data.width, null, data.background, mxUtils.bind(this, function()
12210										{
12211											processUri(null);
12212										}), null, null, data.scale, data.transparent, data.shadow, null,
12213											graph, data.border, null, data.grid, data.keepTheme);
12214									});
12215
12216									// Uses optional XML from incoming message
12217									if (data.xml != null && data.xml.length > 0)
12218									{
12219										ignoreChange = true;
12220										this.setFileData(xml);
12221										ignoreChange = false;
12222
12223										if (this.editor.graph.mathEnabled)
12224										{
12225											window.setTimeout(function()
12226											{
12227												window.MathJax.Hub.Queue(graphReady);
12228											}, 0);
12229										}
12230										else
12231										{
12232											graphReady();
12233										}
12234									}
12235									else
12236									{
12237										graphReady();
12238									}
12239								}
12240								else
12241								{
12242									// Data from server is base64 encoded to avoid binary XHR
12243									// Double encoding for XML arg is needed for UTF8 encoding
12244							       	var req = new mxXmlRequest(EXPORT_URL, 'format=png&embedXml=' +
12245							       		((data.format == 'xmlpng') ? '1' : '0') +
12246							       		(pageId != null? '&pageId=' + pageId : '') +
12247							       		(data.layerIds != null && data.layerIds.length > 0?
12248										'&extras=' + encodeURIComponent(JSON.stringify({layerIds: data.layerIds})) : '') +
12249							       		(data.scale != null? '&scale=' + data.scale : '') +'&base64=1&xml=' +
12250							       		encodeURIComponent(xml));
12251
12252									req.send(mxUtils.bind(this, function(req)
12253									{
12254										// Temp graph was never created at this point so we can
12255										// skip processUri since it already contains the XML
12256										if (req.getStatus() >= 200 && req.getStatus() <= 299)
12257										{
12258											postDataBack('data:image/png;base64,' + req.getText());
12259										}
12260										else
12261										{
12262											processUri(null);
12263										}
12264									}), mxUtils.bind(this, function()
12265									{
12266										processUri(null);
12267									}));
12268								}
12269							}
12270						}
12271						else
12272						{
12273							var graphReady = mxUtils.bind(this, function()
12274							{
12275								var msg = this.createLoadMessage('export');
12276
12277								// Attaches incoming message
12278								msg.message = data;
12279
12280								// Forces new HTML format if pages exists
12281								if (data.format == 'html2' || (data.format == 'html' && (urlParams['pages'] != '0' ||
12282									(this.pages != null && this.pages.length > 1))))
12283								{
12284									var node = this.getXmlFileData();
12285									msg.xml = mxUtils.getXml(node);
12286									msg.data = this.getFileData(null, null, true, null, null, null, node);
12287									msg.format = data.format;
12288								}
12289								else if (data.format == 'html')
12290								{
12291									var xml = this.editor.getGraphXml();
12292									msg.data = this.getHtml(xml, this.editor.graph);
12293									msg.xml = mxUtils.getXml(xml);
12294									msg.format = data.format;
12295								}
12296								else
12297								{
12298									// Creates a preview with no alt text for unsupported browsers
12299									mxSvgCanvas2D.prototype.foAltText = null;
12300
12301									var bg = (data.background != null) ? data.background : this.editor.graph.background;
12302
12303									if (bg == mxConstants.NONE)
12304									{
12305										bg = null;
12306									}
12307
12308									msg.xml = this.getFileData(true, null, null, null, null,
12309										null, null, null, null, false);
12310									msg.format = 'svg';
12311
12312									var postResult = mxUtils.bind(this, function(svg)
12313									{
12314										this.editor.graph.setEnabled(true);
12315										this.spinner.stop();
12316
12317										msg.data = Editor.createSvgDataUri(svg);
12318										parent.postMessage(JSON.stringify(msg), '*');
12319									});
12320
12321									if (data.format == 'xmlsvg')
12322									{
12323										if ((data.spin == null && data.spinKey == null) || this.spinner.spin(document.body,
12324											(data.spinKey != null) ? mxResources.get(data.spinKey) : data.spin))
12325										{
12326											this.getEmbeddedSvg(msg.xml, this.editor.graph, null, true, postResult, null, null,
12327												data.embedImages, bg, data.scale, data.border, data.shadow, data.keepTheme);
12328										}
12329									}
12330									else
12331									{
12332										if ((data.spin == null && data.spinKey == null) || this.spinner.spin(document.body,
12333											(data.spinKey != null) ? mxResources.get(data.spinKey) : data.spin))
12334										{
12335											this.editor.graph.setEnabled(false);
12336											var svgRoot = this.editor.graph.getSvg(bg, data.scale, data.border, null, null,
12337												null, null, null, null, this.editor.graph.shadowVisible || data.shadow,
12338												null, data.keepTheme);
12339
12340											if (this.editor.graph.shadowVisible || data.shadow)
12341											{
12342												this.editor.graph.addSvgShadow(svgRoot);
12343											}
12344
12345											this.embedFonts(svgRoot, mxUtils.bind(this, function(svgRoot)
12346											{
12347												if (data.embedImages || data.embedImages == null)
12348												{
12349													this.editor.convertImages(svgRoot, mxUtils.bind(this, function(svgRoot)
12350													{
12351														postResult(mxUtils.getXml(svgRoot));
12352													}));
12353												}
12354												else
12355												{
12356													postResult(mxUtils.getXml(svgRoot));
12357												}
12358											}));
12359										}
12360									}
12361
12362									return;
12363								}
12364
12365								parent.postMessage(JSON.stringify(msg), '*');
12366							});
12367
12368							// SVG is generated from graph so parse optional XML
12369							if (data.xml != null && data.xml.length > 0)
12370							{
12371								ignoreChange = true;
12372								this.setFileData(data.xml);
12373								ignoreChange = false;
12374
12375								if (this.editor.graph.mathEnabled)
12376								{
12377									window.setTimeout(function()
12378									{
12379										window.MathJax.Hub.Queue(graphReady);
12380									}, 0);
12381								}
12382								else
12383								{
12384									graphReady();
12385								}
12386							}
12387							else
12388							{
12389								graphReady();
12390							}
12391						}
12392
12393						return;
12394					}
12395					else if (data.action == 'load')
12396					{
12397						convertToSketch = data.toSketch;
12398						autosave = data.autosave == 1;
12399						this.hideDialog();
12400
12401						if (data.modified != null && urlParams['modified'] == null)
12402						{
12403							urlParams['modified'] = data.modified;
12404						}
12405
12406						if (data.saveAndExit != null && urlParams['saveAndExit'] == null)
12407						{
12408							urlParams['saveAndExit'] = data.saveAndExit;
12409						}
12410
12411						if (data.noSaveBtn != null && urlParams['noSaveBtn'] == null)
12412						{
12413							urlParams['noSaveBtn'] = data.noSaveBtn;
12414						}
12415
12416						if (data.rough != null)
12417						{
12418							var initial = Editor.sketchMode;
12419							this.doSetSketchMode(data.rough);
12420
12421							if (initial != Editor.sketchMode)
12422							{
12423								this.fireEvent(new mxEventObject('sketchModeChanged'));
12424							}
12425						}
12426
12427						if (data.dark != null)
12428						{
12429							var initial = Editor.darkMode;
12430							this.doSetDarkMode(data.dark);
12431
12432							if (initial != Editor.darkMode)
12433							{
12434								this.fireEvent(new mxEventObject('darkModeChanged'));
12435							}
12436						}
12437
12438						if (data.border != null)
12439						{
12440							this.embedExportBorder = data.border;
12441						}
12442
12443						if (data.background != null)
12444						{
12445							this.embedExportBackground = data.background;
12446						}
12447
12448						if (data.viewport != null)
12449						{
12450							this.embedViewport = data.viewport;
12451						}
12452
12453						this.embedExitPoint = null;
12454
12455						if (data.rect != null)
12456						{
12457							var border = this.embedExportBorder;
12458
12459							this.diagramContainer.style.border = '2px solid #295fcc';
12460							this.diagramContainer.style.top = data.rect.top + 'px';
12461							this.diagramContainer.style.left = data.rect.left + 'px';
12462							this.diagramContainer.style.height = data.rect.height + 'px';
12463							this.diagramContainer.style.width = data.rect.width + 'px';
12464							this.diagramContainer.style.bottom = '';
12465							this.diagramContainer.style.right = '';
12466
12467							afterLoad = mxUtils.bind(this, function()
12468							{
12469								var graph = this.editor.graph;
12470								var prev = graph.maxFitScale;
12471								graph.maxFitScale = data.maxFitScale;
12472								graph.fit(2 * border);
12473								graph.maxFitScale = prev;
12474								graph.container.scrollTop -= 2 * border;
12475								graph.container.scrollLeft -= 2 * border;
12476								this.fireEvent(new mxEventObject('editInlineStart', 'data', [data]));
12477							});
12478						}
12479
12480						if (data.noExitBtn != null && urlParams['noExitBtn'] == null)
12481						{
12482							urlParams['noExitBtn'] = data.noExitBtn;
12483						}
12484
12485						if (data.title != null && this.buttonContainer != null)
12486						{
12487							var tmp = document.createElement('span');
12488							mxUtils.write(tmp, data.title);
12489
12490							if (this.embedFilenameSpan != null)
12491							{
12492								this.embedFilenameSpan.parentNode.removeChild(this.embedFilenameSpan);
12493							}
12494
12495							this.buttonContainer.appendChild(tmp);
12496							this.embedFilenameSpan = tmp;
12497						}
12498
12499						try
12500						{
12501							if (data.libs)
12502							{
12503								this.sidebar.showEntries(data.libs);
12504							}
12505						}
12506						catch(e){}
12507
12508						if (data.xmlpng != null)
12509						{
12510							data = this.extractGraphModelFromPng(data.xmlpng);
12511						}
12512						else if (data.descriptor != null)
12513						{
12514							data = data.descriptor;
12515						}
12516						else
12517						{
12518							data = data.xml;
12519						}
12520					}
12521					else if (data.action == 'merge')
12522					{
12523						var file = this.getCurrentFile();
12524
12525						if (file != null)
12526						{
12527							var tmp = extractDiagramXml(data.xml);
12528
12529							if (tmp != null && tmp != '')
12530							{
12531								file.mergeFile(new LocalFile(this, tmp), function()
12532								{
12533									parent.postMessage(JSON.stringify({event: 'merge', message: data}), '*');
12534								}, function(err)
12535								{
12536									parent.postMessage(JSON.stringify({event: 'merge', message: data, error: err}), '*');
12537								});
12538							}
12539						}
12540
12541						return;
12542					}
12543					else if (data.action == 'remoteInvokeReady')
12544					{
12545						this.handleRemoteInvokeReady(parent);
12546						return;
12547					}
12548					else if (data.action == 'remoteInvoke')
12549					{
12550						this.handleRemoteInvoke(data, evt.origin);
12551						return;
12552					}
12553					else if (data.action == 'remoteInvokeResponse')
12554					{
12555						this.handleRemoteInvokeResponse(data);
12556						return;
12557					}
12558					else
12559					{
12560						// Unknown message must stop execution
12561						parent.postMessage(JSON.stringify({error: 'unknownMessage', data: JSON.stringify(data)}), '*');
12562
12563						return;
12564					}
12565				}
12566				catch (e)
12567				{
12568					// TODO: Block handling of more messages when in error state
12569					this.handleError(e);
12570				}
12571			}
12572
12573			var getData = mxUtils.bind(this, function()
12574			{
12575				return (urlParams['pages'] != '0' || (this.pages != null && this.pages.length > 1)) ?
12576					this.getFileData(true): mxUtils.getXml(this.editor.getGraphXml());
12577			});
12578
12579			var doLoad = mxUtils.bind(this, function(data, evt)
12580			{
12581				ignoreChange = true;
12582				try
12583				{
12584					fn(data, evt, null, convertToSketch);
12585				}
12586				catch (e)
12587				{
12588					this.handleError(e);
12589				}
12590				ignoreChange = false;
12591
12592				if (urlParams['modified'] != null)
12593				{
12594					this.editor.setStatus('');
12595				}
12596
12597				lastData = getData();
12598
12599				if (autosave && changeListener == null)
12600				{
12601					changeListener = mxUtils.bind(this, function(sender, eventObject)
12602					{
12603						var data = getData();
12604
12605						if (data != lastData && !ignoreChange)
12606						{
12607							var msg = this.createLoadMessage('autosave');
12608							msg.xml = data;
12609							var parent = window.opener || window.parent;
12610							parent.postMessage(JSON.stringify(msg), '*');
12611						}
12612
12613						lastData = data;
12614					});
12615
12616					this.editor.graph.model.addListener(mxEvent.CHANGE, changeListener);
12617
12618					// Some options trigger autosave
12619					this.editor.graph.addListener('gridSizeChanged', changeListener);
12620					this.editor.graph.addListener('shadowVisibleChanged', changeListener);
12621					this.addListener('pageFormatChanged', changeListener);
12622					this.addListener('pageScaleChanged', changeListener);
12623					this.addListener('backgroundColorChanged', changeListener);
12624					this.addListener('backgroundImageChanged', changeListener);
12625					this.addListener('foldingEnabledChanged', changeListener);
12626					this.addListener('mathEnabledChanged', changeListener);
12627					this.addListener('gridEnabledChanged', changeListener);
12628					this.addListener('guidesEnabledChanged', changeListener);
12629					this.addListener('pageViewChanged', changeListener);
12630				}
12631
12632				// Sends the bounds of the graph to the host after parsing
12633				if (urlParams['returnbounds'] == '1' || urlParams['proto'] == 'json')
12634				{
12635					var resp = this.createLoadMessage('load');
12636
12637					// Attaches XML to response
12638					resp.xml = data;
12639
12640					parent.postMessage(JSON.stringify(resp), '*');
12641				}
12642
12643				if (afterLoad != null)
12644				{
12645					afterLoad();
12646				}
12647			});
12648
12649			if (data != null && typeof data.substring === 'function' && data.substring(0, 34) == 'data:application/vnd.visio;base64,')
12650			{
12651				// Checks VND binary magic number in base64
12652				var filename = (data.substring(34, 45) == '0M8R4KGxGuE') ? 'raw.vsd' : 'raw.vsdx';
12653
12654				this.importVisio(this.base64ToBlob(data.substring(data.indexOf(',') + 1)), function(xml)
12655				{
12656					doLoad(xml, evt);
12657				}, mxUtils.bind(this, function(e)
12658				{
12659					this.handleError(e);
12660				}), filename);
12661			}
12662			else if (data != null && typeof data.substring === 'function' && !this.isOffline() && new XMLHttpRequest().upload && this.isRemoteFileFormat(data, ''))
12663			{
12664				// Asynchronous parsing via server
12665				this.parseFile(new Blob([data], {type: 'application/octet-stream'}), mxUtils.bind(this, function(xhr)
12666				{
12667					if (xhr.readyState == 4 && xhr.status >= 200 && xhr.status <= 299 &&
12668						xhr.responseText.substring(0, 13) == '<mxGraphModel')
12669					{
12670						doLoad(xhr.responseText, evt);
12671					}
12672				}), '');
12673			}
12674			else if (data != null && typeof data.substring === 'function' && this.isLucidChartData(data))
12675			{
12676				this.convertLucidChart(data, mxUtils.bind(this, function(xml)
12677				{
12678					doLoad(xml);
12679				}), mxUtils.bind(this, function(e)
12680				{
12681					this.handleError(e);
12682				}));
12683			}
12684			else if (data != null && typeof data === 'object' && data.format != null && (data.data != null || data.url != null))
12685			{
12686				this.loadDescriptor(data, mxUtils.bind(this, function(e)
12687				{
12688					doLoad(getData(), evt);
12689				}), mxUtils.bind(this, function(e)
12690				{
12691					this.handleError(e, mxResources.get('errorLoadingFile'));
12692				}));
12693			}
12694			else
12695			{
12696				data = extractDiagramXml(data);
12697				doLoad(data, evt);
12698			}
12699		}));
12700
12701		// Requests data from the sender. This is a workaround for not allowing
12702		// the opener to listen for the onload event if not in the same origin.
12703		var parent = window.opener || window.parent;
12704		var msg = (urlParams['proto'] == 'json') ? JSON.stringify({event: 'init'}) : (urlParams['ready'] || 'ready');
12705		parent.postMessage(msg, '*');
12706
12707		// Adds JSON event for opening links
12708		if (urlParams['proto'] == 'json')
12709		{
12710			var graphOpenLink = this.editor.graph.openLink;
12711
12712			this.editor.graph.openLink = function(href, target, allowOpener)
12713			{
12714				graphOpenLink.apply(this, arguments);
12715
12716				parent.postMessage(JSON.stringify({event: 'openLink', href: href, target: target, allowOpener: allowOpener}), '*');
12717			};
12718		}
12719	};
12720
12721	/**
12722	 * Adds the buttons for embedded mode.
12723	 */
12724	EditorUi.prototype.addEmbedButtons = function()
12725	{
12726		if (this.menubar != null && urlParams['embedInline'] != '1')
12727		{
12728			var div = document.createElement('div');
12729			div.style.display = 'inline-block';
12730			div.style.position = 'absolute';
12731			div.style.paddingTop = (uiTheme == 'atlas' || urlParams['atlas'] == '1') ? '2px' : '0px';
12732			div.style.paddingLeft = '8px';
12733			div.style.paddingBottom = '2px';
12734
12735			var button = document.createElement('button');
12736			button.className = 'geBigButton';
12737			var lastBtn = button;
12738
12739			if (urlParams['noSaveBtn'] == '1')
12740			{
12741				if (urlParams['saveAndExit'] != '0')
12742				{
12743					var saveAndExitTitle = urlParams['publishClose'] == '1' ? mxResources.get('publish') : mxResources.get('saveAndExit');
12744					mxUtils.write(button, saveAndExitTitle);
12745					button.setAttribute('title', saveAndExitTitle);
12746
12747					mxEvent.addListener(button, 'click', mxUtils.bind(this, function()
12748					{
12749						this.actions.get('saveAndExit').funct();
12750					}));
12751
12752					div.appendChild(button);
12753				}
12754			}
12755			else
12756			{
12757				mxUtils.write(button, mxResources.get('save'));
12758				button.setAttribute('title', mxResources.get('save') + ' (' + Editor.ctrlKey + '+S)');
12759
12760				mxEvent.addListener(button, 'click', mxUtils.bind(this, function()
12761				{
12762					this.actions.get('save').funct();
12763				}));
12764
12765				div.appendChild(button);
12766
12767				if (urlParams['saveAndExit'] == '1')
12768				{
12769					button = document.createElement('a');
12770					mxUtils.write(button, mxResources.get('saveAndExit'));
12771					button.setAttribute('title', mxResources.get('saveAndExit'));
12772					button.className = 'geBigButton geBigStandardButton';
12773					button.style.marginLeft = '6px';
12774
12775					mxEvent.addListener(button, 'click', mxUtils.bind(this, function()
12776					{
12777						this.actions.get('saveAndExit').funct();
12778					}));
12779
12780					div.appendChild(button);
12781					lastBtn = button;
12782				}
12783			}
12784
12785			if (urlParams['noExitBtn'] != '1')
12786			{
12787				button = document.createElement('a');
12788				var exitTitle = urlParams['publishClose'] == '1' ? mxResources.get('close') : mxResources.get('exit');
12789				mxUtils.write(button, exitTitle);
12790				button.setAttribute('title', exitTitle);
12791				button.className = 'geBigButton geBigStandardButton';
12792				button.style.marginLeft = '6px';
12793
12794				mxEvent.addListener(button, 'click', mxUtils.bind(this, function()
12795				{
12796					this.actions.get('exit').funct();
12797				}));
12798
12799				div.appendChild(button);
12800				lastBtn = button;
12801			}
12802
12803			lastBtn.style.marginRight = '20px';
12804
12805			this.toolbar.container.appendChild(div);
12806			this.toolbar.staticElements.push(div);
12807			div.style.right = (uiTheme == 'atlas' || urlParams['atlas'] == '1') ? '42px' : '52px';
12808		}
12809	};
12810
12811	/**
12812	 *
12813	 */
12814	EditorUi.prototype.showImportCsvDialog = function()
12815	{
12816		if (this.importCsvDialog == null)
12817		{
12818			this.importCsvDialog = new TextareaDialog(this, mxResources.get('csv') + ':',
12819    			Editor.defaultCsvValue, mxUtils.bind(this, function(newValue)
12820			{
12821    			this.importCsv(newValue);
12822			}), null, null, 620, 430, null, true, true, mxResources.get('import'),
12823				!this.isOffline() ? 'https://drawio-app.com/import-from-csv-to-drawio/' : null);
12824		}
12825
12826		this.showDialog(this.importCsvDialog.container, 640, 520, true, true, null, null, null, null, true);
12827		this.importCsvDialog.init();
12828	};
12829
12830
12831	/**
12832	 * Runs the layout from the given JavaScript array which is of the form [{layout: name, config: obj}, ...]
12833	 * where name is the layout constructor name and config contains the properties of the layout instance.
12834	 */
12835	EditorUi.prototype.executeLayoutList = function(layoutList, done)
12836	{
12837		var graph = this.editor.graph;
12838		var cells = graph.getSelectionCells();
12839
12840		for (var i = 0; i < layoutList.length; i++)
12841		{
12842			var layout = new window[layoutList[i].layout](graph);
12843
12844			if (layoutList[i].config != null)
12845			{
12846				for (var key in layoutList[i].config)
12847				{
12848					layout[key] = layoutList[i].config[key];
12849				}
12850			}
12851
12852			this.executeLayout(function()
12853			{
12854				layout.execute(graph.getDefaultParent(), cells.length == 0 ? null : cells);
12855			}, i == layoutList.length - 1, done);
12856		}
12857	};
12858
12859	/**
12860	 *
12861	 */
12862	EditorUi.prototype.importCsv = function(text, done)
12863	{
12864		try
12865		{
12866    		var lines = text.split('\n');
12867    		var allCells = [];
12868			var parents = [];
12869    		var cells = [];
12870    		var dups = {};
12871
12872    		if (lines.length > 0)
12873    		{
12874        		// Internal lookup table
12875        		var lookups = {};
12876
12877        		// Default values
12878        		var vars = null;
12879        		var style = null;
12880        		var styles = null;
12881        		var stylename = null;
12882        		var labelname = null;
12883        		var labels = null;
12884        		var parentstyle = 'whiteSpace=wrap;html=1;';
12885        		var identity = null;
12886        		var parent = null;
12887        		var namespace = '';
12888        		var width = 'auto';
12889        		var height = 'auto';
12890        		var left = null;
12891        		var top = null;
12892        		var edgespacing = 40;
12893        		var nodespacing = 40;
12894        		var levelspacing = 100;
12895        		var padding = 0;
12896
12897        		var graph = this.editor.graph;
12898				var view = graph.view;
12899				var bds = graph.getGraphBounds();
12900
12901				// Delayed after optional layout
12902    			var afterInsert = function()
12903    			{
12904    				if (done != null)
12905    				{
12906    					done(select);
12907    				}
12908    				else
12909    				{
12910    					graph.setSelectionCells(select);
12911    					graph.scrollCellToVisible(graph.getSelectionCell());
12912    				}
12913    			};
12914
12915    			// Computes unscaled, untranslated graph bounds
12916    			var pt = graph.getFreeInsertPoint();
12917				var x0 = pt.x;
12918				var y0 = pt.y;
12919				var y = y0;
12920
12921    			// Default label value depends on column names
12922        		var label = null;
12923
12924    			// Default layout to run.
12925        		var layout = 'auto';
12926
12927        		// Name of the attribute that contains the parent reference
12928        		var parent = null;
12929
12930        		// Name of the attribute that contains the references for creating edges
12931        		var edges = [];
12932
12933        		// Name of the column for hyperlinks
12934        		var link = null;
12935
12936        		// String array of names to remove from metadata
12937        		var ignore = null;
12938
12939        		// Read processing instructions first
12940        		var index = 0;
12941
12942        		while (index < lines.length && lines[index].charAt(0) == '#')
12943        		{
12944        			var text = lines[index];
12945        			index++;
12946
12947        			while (index < lines.length && text.charAt(text.length - 1) == '\\' &&
12948        				lines[index].charAt(0) == '#')
12949        			{
12950        				text = text.substring(0, text.length - 1) + mxUtils.trim(lines[index].substring(1));
12951        				index++;
12952        			}
12953
12954        			if (text.charAt(1) != '#')
12955        			{
12956	    				// Processing instruction
12957	    				var idx = text.indexOf(':');
12958
12959	    				if (idx > 0)
12960	    				{
12961		    				var key = mxUtils.trim(text.substring(1, idx));
12962		    				var value = mxUtils.trim(text.substring(idx + 1));
12963
12964		    				if (key == 'label')
12965		    				{
12966		    					label = graph.sanitizeHtml(value);
12967		    				}
12968		    				else if (key == 'labelname' && value.length > 0 && value != '-')
12969		    				{
12970		    					labelname = value;
12971		    				}
12972		    				else if (key == 'labels' && value.length > 0 && value != '-')
12973		    				{
12974		    					labels = JSON.parse(value);
12975		    				}
12976		    				else if (key == 'style')
12977		    				{
12978		    					style = value;
12979		    				}
12980		    				else if (key == 'parentstyle')
12981		    				{
12982		    					parentstyle = value;
12983		    				}
12984		    				else if (key == 'stylename' && value.length > 0 && value != '-')
12985		    				{
12986		    					stylename = value;
12987		    				}
12988		    				else if (key == 'styles' && value.length > 0 && value != '-')
12989		    				{
12990		    					styles = JSON.parse(value);
12991		    				}
12992		    				else if (key == 'vars' && value.length > 0 && value != '-')
12993		    				{
12994		    					vars = JSON.parse(value);
12995		    				}
12996		    				else if (key == 'identity' && value.length > 0 && value != '-')
12997		    				{
12998		    					identity = value;
12999		    				}
13000		    				else if (key == 'parent' && value.length > 0 && value != '-')
13001		    				{
13002		    					parent = value;
13003		    				}
13004		    				else if (key == 'namespace' && value.length > 0 && value != '-')
13005		    				{
13006		    					namespace = value;
13007		    				}
13008		    				else if (key == 'width')
13009		    				{
13010		    					width = value;
13011		    				}
13012		    				else if (key == 'height')
13013		    				{
13014		    					height = value;
13015		    				}
13016		    				else if (key == 'left' && value.length > 0)
13017		    				{
13018		    					left = value;
13019		    				}
13020		    				else if (key == 'top' && value.length > 0)
13021		    				{
13022		    					top = value;
13023		    				}
13024		    				else if (key == 'ignore')
13025		    				{
13026		    					ignore = value.split(',');
13027		    				}
13028		    				else if (key == 'connect')
13029		    				{
13030		    					edges.push(JSON.parse(value));
13031		    				}
13032		    				else if (key == 'link')
13033		    				{
13034		    					link = value;
13035		    				}
13036		    				else if (key == 'padding')
13037		    				{
13038		    					padding = parseFloat(value);
13039		    				}
13040		    				else if (key == 'edgespacing')
13041		    				{
13042		    					edgespacing = parseFloat(value);
13043		    				}
13044		    				else if (key == 'nodespacing')
13045		    				{
13046		    					nodespacing = parseFloat(value);
13047		    				}
13048		    				else if (key == 'levelspacing')
13049		    				{
13050		    					levelspacing = parseFloat(value);
13051		    				}
13052		    				else if (key == 'layout')
13053		    				{
13054		    					layout = value;
13055		    				}
13056	    				}
13057        			}
13058        		}
13059
13060        		if (lines[index] == null)
13061        		{
13062        			throw new Error(mxResources.get('invalidOrMissingFile'));
13063        		}
13064
13065    			// Converts identity and parent to index and validates XML attribute names
13066    			var keys = this.editor.csvToArray(lines[index]);
13067        		var identityIndex = null;
13068    			var parentIndex = null;
13069    			var attribs = [];
13070
13071				for (var i = 0; i < keys.length; i++)
13072	    		{
13073					if (identity == keys[i])
13074					{
13075						identityIndex = i;
13076					}
13077
13078					if (parent == keys[i])
13079					{
13080						parentIndex = i;
13081					}
13082
13083					attribs.push(mxUtils.trim(keys[i]).replace(/[^a-z0-9]+/ig, '_').
13084						replace(/^\d+/, '').replace(/_+$/, ''));
13085	    		}
13086
13087    			if (label == null)
13088    			{
13089    				label = '%' + attribs[0] + '%';
13090    			}
13091
13092    			if (edges != null)
13093				{
13094					for (var e = 0; e < edges.length; e++)
13095					{
13096						if (lookups[edges[e].to] == null)
13097						{
13098							lookups[edges[e].to] = {};
13099						}
13100					}
13101				}
13102
13103    			// Parse and validate input
13104    			var arrays = [];
13105
13106    			for (var i = index + 1; i < lines.length; i++)
13107	    		{
13108	    			var values = this.editor.csvToArray(lines[i]);
13109
13110	    			if (values == null)
13111	    			{
13112	    				var short = (lines[i].length > 40) ? lines[i].substring(0, 40) + '...' : lines[i];
13113
13114	    				throw new Error(short + ' (' + i + '):\n' + mxResources.get('containsValidationErrors'));
13115	    			}
13116	    			else if (values.length > 0)
13117		    		{
13118	    				arrays.push(values);
13119	    			}
13120	    		}
13121
13122        		graph.model.beginUpdate();
13123        		try
13124        		{
13125	    			for (var i = 0; i < arrays.length; i++)
13126		    		{
13127    	    			var values = arrays[i];
13128    					var cell = null;
13129    					var id = (identityIndex != null) ? namespace + values[identityIndex] : null;
13130
13131    					if (id != null)
13132    					{
13133    						cell = graph.model.getCell(id);
13134    					}
13135
13136    					var exists = cell != null;
13137    					var newCell = new mxCell(label, new mxGeometry(x0, y,
13138		    				0, 0), style || 'whiteSpace=wrap;html=1;');
13139    					newCell.vertex = true;
13140    					newCell.id = id;
13141
13142						for (var j = 0; j < values.length; j++)
13143				    	{
13144							graph.setAttributeForCell(newCell, attribs[j], values[j]);
13145				    	}
13146
13147						if (labelname != null && labels != null)
13148						{
13149							var tempLabel = labels[newCell.getAttribute(labelname)];
13150
13151							if (tempLabel != null)
13152							{
13153								graph.labelChanged(newCell, tempLabel);
13154							}
13155						}
13156
13157						if (stylename != null && styles != null)
13158						{
13159							var tempStyle = styles[newCell.getAttribute(stylename)];
13160
13161							if (tempStyle != null)
13162							{
13163								newCell.style = tempStyle;
13164							}
13165						}
13166
13167						graph.setAttributeForCell(newCell, 'placeholders', '1');
13168						newCell.style = graph.replacePlaceholders(newCell, newCell.style, vars);
13169
13170						if (exists)
13171						{
13172							graph.model.setValue(cell, newCell.value);
13173							graph.model.setStyle(cell, newCell.style);
13174
13175							if (mxUtils.indexOf(cells, cell) < 0)
13176							{
13177								cells.push(cell);
13178							}
13179
13180							graph.fireEvent(new mxEventObject('cellsInserted', 'cells', [cell]));
13181						}
13182						else
13183						{
13184							graph.fireEvent(new mxEventObject('cellsInserted', 'cells', [newCell]));
13185						}
13186
13187						cell = newCell;
13188
13189						if (!exists)
13190						{
13191	    					for (var e = 0; e < edges.length; e++)
13192	    					{
13193	    						lookups[edges[e].to][cell.getAttribute(edges[e].to)] = cell;
13194	    					}
13195						}
13196
13197						if (link != null && link != 'link')
13198						{
13199							graph.setLinkForCell(cell, cell.getAttribute(link));
13200
13201							// Removes attribute
13202							graph.setAttributeForCell(cell, link, null);
13203						}
13204
13205						// Sets the geometry
13206						var size = this.editor.graph.getPreferredSizeForCell(cell);
13207						var parent = (parentIndex != null) ? graph.model.getCell(
13208							namespace + values[parentIndex]) : null;
13209
13210						if (cell.vertex)
13211						{
13212							var originX = (parent != null) ? 0 : x0;
13213							var originY = (parent != null) ? 0 : y0;
13214
13215							if (left != null && cell.getAttribute(left) != null)
13216							{
13217								cell.geometry.x = originX + parseFloat(cell.getAttribute(left));
13218							}
13219
13220							if (top != null && cell.getAttribute(top) != null)
13221							{
13222								cell.geometry.y = originY + parseFloat(cell.getAttribute(top));
13223							}
13224
13225							var widthValue = (width.charAt(0) == '@') ? cell.getAttribute(width.substring(1)) : null;
13226
13227							if (widthValue != null && widthValue != 'auto')
13228							{
13229								cell.geometry.width = parseFloat(cell.getAttribute(width.substring(1)));
13230							}
13231							else
13232							{
13233								cell.geometry.width = (width == 'auto' || widthValue == 'auto') ?
13234									size.width + padding : parseFloat(width);
13235							}
13236
13237							var heightValue = (height.charAt(0) == '@') ? cell.getAttribute(height.substring(1)) : null;
13238
13239							if (heightValue != null && heightValue != 'auto')
13240							{
13241								cell.geometry.height = parseFloat(heightValue);
13242							}
13243							else
13244							{
13245								cell.geometry.height = (height == 'auto' || heightValue == 'auto') ?
13246									size.height + padding : parseFloat(height);
13247							}
13248
13249							y += cell.geometry.height + nodespacing;
13250						}
13251
13252						if (!exists)
13253						{
13254							allCells.push(cell);
13255
13256	    					if (parent != null)
13257	    					{
13258	    						parent.style = graph.replacePlaceholders(parent, parentstyle, vars);
13259	    						graph.addCell(cell, parent);
13260								parents.push(parent);
13261	    					}
13262	    					else
13263	    					{
13264	    						cells.push(graph.addCell(cell));
13265	    					}
13266						}
13267						else
13268						{
13269							if (dups[id] == null)
13270							{
13271								dups[id] = [];
13272							}
13273
13274							dups[id].push(cell);
13275						}
13276	    			}
13277
13278					// Process parents for autosize
13279					for (var i = 0; i < parents.length; i++)
13280					{
13281						var widthValue = (width.charAt(0) == '@') ? parents[i].getAttribute(width.substring(1)) : null;
13282						var heightValue = (height.charAt(0) == '@') ? parents[i].getAttribute(height.substring(1)) : null;
13283
13284						if ((width == 'auto' || widthValue == 'auto') &&
13285							(height == 'auto' || heightValue == 'auto'))
13286						{
13287							graph.updateGroupBounds([parents[i]], padding, true);
13288						}
13289					}
13290
13291					var roots = cells.slice();
13292					var select = cells.slice();
13293
13294					for (var e = 0; e < edges.length; e++)
13295					{
13296						var edge = edges[e];
13297
13298						for (var i = 0; i < allCells.length; i++)
13299	    				{
13300							var cell = allCells[i];
13301
13302							var insertEdge = mxUtils.bind(this, function(realCell, dataCell, edge)
13303							{
13304								var tmp = dataCell.getAttribute(edge.from);
13305
13306		    					if (tmp != null)
13307		    					{
13308			    					if (tmp != '')
13309			    					{
13310			    						var refs = tmp.split(',');
13311
13312				    					for (var j = 0; j < refs.length; j++)
13313				        				{
13314				    						var ref = lookups[edge.to][refs[j]];
13315
13316				    						if (ref != null)
13317				    						{
13318				    							var label = edge.label;
13319
13320				    							if (edge.fromlabel != null)
13321				    							{
13322				    								label = (dataCell.getAttribute(edge.fromlabel) || '') + (label || '');
13323				    							}
13324
13325				    							if (edge.sourcelabel != null)
13326				    							{
13327				    								label = graph.replacePlaceholders(dataCell,
13328														edge.sourcelabel, vars) + (label || '');
13329				    							}
13330
13331				    							if (edge.tolabel != null)
13332				    							{
13333				    								label = (label || '') + (ref.getAttribute(edge.tolabel) || '');
13334				    							}
13335
13336				    							if (edge.targetlabel != null)
13337				    							{
13338				    								label = (label || '') + graph.replacePlaceholders(
13339														ref, edge.targetlabel, vars);
13340				    							}
13341
13342				    							var placeholders = ((edge.placeholders == 'target') ==
13343				    								!edge.invert) ? ref : realCell;
13344				    							var style = (edge.style != null) ?
13345				    								graph.replacePlaceholders(placeholders, edge.style, vars) :
13346				    								graph.createCurrentEdgeStyle();
13347
13348				    							var edgeCell = graph.insertEdge(null, null, label || '', (edge.invert) ?
13349				    								ref : realCell, (edge.invert) ? realCell : ref, style);
13350
13351				    							// Adds additional edge labels
13352				    							if (edge.labels != null)
13353				    							{
13354				    								for (var k = 0; k < edge.labels.length; k++)
13355				    								{
13356				    									var def = edge.labels[k];
13357				    									var elx = (def.x != null) ? def.x : 0;
13358				    									var ely = (def.y != null) ? def.y : 0;
13359				    									var st = 'resizable=0;html=1;';
13360				    									var el = new mxCell(def.label || k,
13361				    										new mxGeometry(elx,  ely, 0, 0), st);
13362				    									el.vertex = true;
13363				    									el.connectable = false;
13364				    									el.geometry.relative = true;
13365
13366														if (def.placeholders != null)
13367														{
13368															el.value = graph.replacePlaceholders(
13369																((def.placeholders == 'target') ==
13370							    								!edge.invert) ? ref : realCell,
13371															el.value, vars)
13372														}
13373
13374				    									if (def.dx != null || def.dy != null)
13375				    									{
13376				    										el.geometry.offset = new mxPoint(
13377				    											(def.dx != null) ? def.dx : 0,
13378				    											(def.dy != null) ? def.dy : 0);
13379				    									}
13380
13381				    									edgeCell.insert(el);
13382				    								}
13383				    							}
13384
13385				    							select.push(edgeCell);
13386				    							mxUtils.remove((edge.invert) ? realCell : ref, roots);
13387				    						}
13388				        				}
13389			    					}
13390		    					}
13391							});
13392
13393							insertEdge(cell, cell, edge);
13394
13395    						// Checks more entries
13396    						if (dups[cell.id] != null)
13397    						{
13398    							for (var j = 0; j < dups[cell.id].length; j++)
13399    		    				{
13400    								insertEdge(cell, dups[cell.id][j], edge);
13401    		    				}
13402    						}
13403						}
13404					}
13405
13406					// Removes ignored attributes after processing above
13407					if (ignore != null)
13408					{
13409						for (var i = 0; i < allCells.length; i++)
13410						{
13411							var cell = allCells[i];
13412
13413							for (var j = 0; j < ignore.length; j++)
13414					    	{
13415								graph.setAttributeForCell(cell, mxUtils.trim(ignore[j]), null);
13416					    	}
13417						}
13418					}
13419
13420					if (cells.length > 0)
13421					{
13422						var edgeLayout = new mxParallelEdgeLayout(graph);
13423						edgeLayout.spacing = edgespacing;
13424						edgeLayout.checkOverlap = true;
13425
13426						var postProcess = function()
13427						{
13428							if (edgeLayout.spacing > 0)
13429							{
13430								edgeLayout.execute(graph.getDefaultParent());
13431							}
13432
13433			    			// Aligns cells to grid and/or rounds positions
13434							for (var i = 0; i < cells.length; i++)
13435		    				{
13436								var geo = graph.getCellGeometry(cells[i]);
13437								geo.x = Math.round(graph.snap(geo.x));
13438								geo.y = Math.round(graph.snap(geo.y));
13439
13440								if (width == 'auto')
13441								{
13442									geo.width = Math.round(graph.snap(geo.width));
13443								}
13444
13445								if (height == 'auto')
13446								{
13447									geo.height = Math.round(graph.snap(geo.height));
13448								}
13449		    				}
13450						};
13451
13452						if (layout.charAt(0) == '[')
13453						{
13454			    			// Required for layouts to work with new cells
13455							var temp = afterInsert;
13456			    			graph.view.validate();
13457							this.executeLayoutList(JSON.parse(layout), function()
13458							{
13459								postProcess();
13460								temp();
13461							});
13462							afterInsert = null;
13463						}
13464						else if (layout == 'circle')
13465						{
13466							var circleLayout = new mxCircleLayout(graph);
13467							circleLayout.disableEdgeStyle = false;
13468		    				circleLayout.resetEdges = false;
13469
13470		    				var circleLayoutIsVertexIgnored = circleLayout.isVertexIgnored;
13471
13472			    			// Ignore other cells
13473		    				circleLayout.isVertexIgnored = function(vertex)
13474		    				{
13475		    					return circleLayoutIsVertexIgnored.apply(this, arguments) ||
13476		    						mxUtils.indexOf(cells, vertex) < 0;
13477		    				};
13478
13479				    		this.executeLayout(function()
13480				    		{
13481				    			circleLayout.execute(graph.getDefaultParent());
13482				    			postProcess();
13483				    		}, true, afterInsert);
13484
13485				    		afterInsert = null;
13486						}
13487						else if (layout == 'horizontaltree' || layout == 'verticaltree' ||
13488								(layout == 'auto' && select.length == 2 * cells.length - 1 && roots.length == 1))
13489		    			{
13490			    			// Required for layouts to work with new cells
13491			    			graph.view.validate();
13492
13493		    				var treeLayout = new mxCompactTreeLayout(graph, layout == 'horizontaltree');
13494		    				treeLayout.levelDistance = nodespacing;
13495		    				treeLayout.edgeRouting = false;
13496		    				treeLayout.resetEdges = false;
13497
13498		    				this.executeLayout(function()
13499		    	    		{
13500		    					treeLayout.execute(graph.getDefaultParent(), (roots.length > 0) ? roots[0] : null);
13501		    	    		}, true, afterInsert);
13502
13503		    				afterInsert = null;
13504		    			}
13505		    			else if (layout == 'horizontalflow' || layout == 'verticalflow' ||
13506		    					(layout == 'auto' && roots.length == 1))
13507		    			{
13508			    			// Required for layouts to work with new cells
13509			    			graph.view.validate();
13510
13511			    			var flowLayout = new mxHierarchicalLayout(graph,
13512			    				(layout == 'horizontalflow') ? mxConstants.DIRECTION_WEST : mxConstants.DIRECTION_NORTH);
13513			    			flowLayout.intraCellSpacing = nodespacing;
13514			    			flowLayout.parallelEdgeSpacing = edgespacing;
13515			    			flowLayout.interRankCellSpacing = levelspacing;
13516			    			flowLayout.disableEdgeStyle = false;
13517
13518			        		this.executeLayout(function()
13519			        		{
13520			        			flowLayout.execute(graph.getDefaultParent(), select);
13521
13522			        			// Workaround for flow layout moving cells to origin
13523			        			graph.moveCells(select, x0, y0);
13524			        		}, true, afterInsert);
13525
13526			    			afterInsert = null;
13527			    		}
13528		    			else if (layout == 'organic' || (layout == 'auto' &&
13529		    					select.length > cells.length))
13530		    			{
13531			    			// Required for layouts to work with new cells
13532			    			graph.view.validate();
13533
13534		    				var organicLayout = new mxFastOrganicLayout(graph);
13535		    				organicLayout.forceConstant = nodespacing * 3;
13536		    				organicLayout.disableEdgeStyle = false;
13537		    				organicLayout.resetEdges = false;
13538
13539		    				var organicLayoutIsVertexIgnored = organicLayout.isVertexIgnored;
13540
13541			    				// Ignore other cells
13542		    				organicLayout.isVertexIgnored = function(vertex)
13543		    				{
13544		    					return organicLayoutIsVertexIgnored.apply(this, arguments) ||
13545		    						mxUtils.indexOf(cells, vertex) < 0;
13546		    				};
13547
13548		    	    		this.executeLayout(function()
13549		    	    		{
13550		    	    			organicLayout.execute(graph.getDefaultParent());
13551				    			postProcess();
13552		    	    		}, true, afterInsert);
13553
13554		    	    		afterInsert = null;
13555		    			}
13556					}
13557
13558	    			this.hideDialog();
13559        		}
13560        		finally
13561        		{
13562        			graph.model.endUpdate();
13563        		}
13564
13565        		if (afterInsert != null)
13566        		{
13567        			afterInsert();
13568        		}
13569    		}
13570		}
13571		catch (e)
13572		{
13573			this.handleError(e);
13574		}
13575	};
13576
13577	/**
13578	 * Translates this point by the given vector.
13579	 *
13580	 * @param {number} dx X-coordinate of the translation.
13581	 * @param {number} dy Y-coordinate of the translation.
13582	 */
13583	EditorUi.prototype.getSearch = function(exclude)
13584	{
13585		var result = '';
13586
13587		if (urlParams['offline'] != '1' && urlParams['demo'] != '1' && exclude != null && window.location.search.length > 0)
13588		{
13589			var amp = '?';
13590
13591			for (var key in urlParams)
13592			{
13593				if (mxUtils.indexOf(exclude, key) < 0 && urlParams[key] != null)
13594				{
13595					result += amp + key + '=' + urlParams[key];
13596					amp = '&';
13597				}
13598			}
13599		}
13600		else
13601		{
13602			result = window.location.search;
13603		}
13604
13605		return result;
13606	};
13607
13608	/**
13609	 * Returns the URL for a copy of this editor with no state.
13610	 */
13611	EditorUi.prototype.getUrl = function(pathname)
13612	{
13613		var href = (pathname != null) ? pathname : window.location.pathname;
13614		var parms = (href.indexOf('?') > 0) ? 1 : 0;
13615
13616		if (urlParams['offline'] == '1')
13617		{
13618			href += window.location.search;
13619		}
13620		else
13621		{
13622			var ignored = ['tmp', 'libs', 'clibs', 'state', 'fileId', 'code', 'share', 'notitle',
13623			               'data', 'url', 'embed', 'client', 'create', 'title', 'splash'];
13624
13625			// Removes template URL parameter for new blank diagram
13626			for (var key in urlParams)
13627			{
13628				if (mxUtils.indexOf(ignored, key) < 0)
13629				{
13630					if (parms == 0)
13631					{
13632						href += '?';
13633					}
13634					else
13635					{
13636						href += '&';
13637					}
13638
13639					if (urlParams[key] != null)
13640					{
13641						href += key + '=' + urlParams[key];
13642						parms++;
13643					}
13644				}
13645			}
13646		}
13647
13648		return href;
13649	};
13650
13651	/**
13652	 * Overrides link dialog.
13653	 */
13654	EditorUi.prototype.showLinkDialog = function(value, btnLabel, fn, showNewWindowOption, linkTarget)
13655	{
13656		var dlg = new LinkDialog(this, value, btnLabel, fn, true, showNewWindowOption, linkTarget);
13657		this.showDialog(dlg.container, 560, 130, true, true);
13658		dlg.init();
13659	};
13660
13661	/**
13662	 * Returns the number of storage options enabled
13663	 */
13664	EditorUi.prototype.getServiceCount = function(allowBrowser)
13665	{
13666		var serviceCount = 1;
13667
13668		if (this.drive != null || typeof window.DriveClient === 'function')
13669		{
13670			serviceCount++
13671		}
13672
13673		if	(this.dropbox != null || typeof window.DropboxClient === 'function')
13674		{
13675			serviceCount++
13676		}
13677
13678		if (this.oneDrive != null || typeof window.OneDriveClient === 'function')
13679		{
13680			serviceCount++
13681		}
13682
13683		if (this.gitHub != null)
13684		{
13685			serviceCount++
13686		}
13687
13688		if (this.gitLab != null)
13689		{
13690			serviceCount++
13691		}
13692
13693		if (this.notion != null)
13694		{
13695			serviceCount++
13696		}
13697
13698		if (allowBrowser && isLocalStorage && urlParams['browser'] == '1')
13699		{
13700			serviceCount++
13701		}
13702
13703		return serviceCount;
13704	}
13705
13706	/**
13707	 * Updates action and menu states depending on the file.
13708	 */
13709	EditorUi.prototype.updateUi = function()
13710	{
13711		this.updateButtonContainer();
13712		this.updateActionStates();
13713
13714		// Action states that only need update for new files
13715		var file = this.getCurrentFile();
13716		var active = file != null || (urlParams['embed'] == '1' &&
13717			this.editor.graph.isEnabled());
13718		this.menus.get('viewPanels').setEnabled(active);
13719		this.menus.get('viewZoom').setEnabled(active);
13720
13721		var restricted = (urlParams['embed'] != '1' ||
13722			!this.editor.graph.isEnabled()) &&
13723			(file == null || file.isRestricted());
13724		this.actions.get('makeCopy').setEnabled(!restricted);
13725		this.actions.get('print').setEnabled(!restricted);
13726		this.menus.get('exportAs').setEnabled(!restricted);
13727		this.menus.get('embed').setEnabled(!restricted);
13728
13729		// Disables libraries and extras menu in embed mode
13730		// while waiting for file data
13731		var libsEnabled = urlParams['embed'] != '1' ||
13732				this.editor.graph.isEnabled();
13733		this.menus.get('extras').setEnabled(libsEnabled);
13734
13735		if (Editor.enableCustomLibraries)
13736		{
13737			this.menus.get('openLibraryFrom').setEnabled(libsEnabled);
13738			this.menus.get('newLibrary').setEnabled(libsEnabled);
13739		}
13740
13741		// Disables actions in the toolbar
13742		var editable = (urlParams['embed'] == '1' &&
13743			this.editor.graph.isEnabled()) ||
13744			(file != null && file.isEditable());
13745		this.actions.get('image').setEnabled(active);
13746		this.actions.get('zoomIn').setEnabled(active);
13747		this.actions.get('zoomOut').setEnabled(active);
13748		this.actions.get('resetView').setEnabled(active);
13749
13750		// Updates undo history states
13751		this.actions.get('undo').setEnabled(this.canUndo() && editable);
13752		this.actions.get('redo').setEnabled(this.canRedo() && editable);
13753
13754		// Disables menus
13755		this.menus.get('edit').setEnabled(active);
13756		this.menus.get('view').setEnabled(active);
13757		this.menus.get('importFrom').setEnabled(editable);
13758		this.menus.get('arrange').setEnabled(editable);
13759
13760		// Disables connection drop downs in toolbar
13761		if (this.toolbar != null)
13762		{
13763			if (this.toolbar.edgeShapeMenu != null)
13764			{
13765				this.toolbar.edgeShapeMenu.setEnabled(editable);
13766			}
13767
13768			if (this.toolbar.edgeStyleMenu != null)
13769			{
13770				this.toolbar.edgeStyleMenu.setEnabled(editable);
13771			}
13772		}
13773
13774		this.updateUserElement();
13775	};
13776
13777	/**
13778	 * Hook for subclassers
13779	 */
13780	EditorUi.prototype.updateButtonContainer = function()
13781	{
13782		// do nothing
13783	};
13784
13785	/**
13786	 * Hook for subclassers
13787	 */
13788	EditorUi.prototype.updateUserElement = function()
13789	{
13790		// do nothing
13791	};
13792
13793	/**
13794	 * Hook for subclassers
13795	 */
13796	EditorUi.prototype.scheduleSanityCheck = function()
13797	{
13798		// do nothing
13799	};
13800
13801	/**
13802	 * Hook for subclassers
13803	 */
13804	EditorUi.prototype.stopSanityCheck = function()
13805	{
13806		// do nothing
13807	};
13808
13809	/**
13810	 * Returns true if a diagram is cative and editable.
13811	 */
13812	EditorUi.prototype.isDiagramActive = function()
13813	{
13814		var file = this.getCurrentFile();
13815
13816		return (file != null && file.isEditable()) ||
13817			(urlParams['embed'] == '1' && this.editor.graph.isEnabled());
13818	};
13819
13820	/**
13821	 * Updates action states depending on the selection.
13822	 */
13823	var editorUiUpdateActionStates = EditorUi.prototype.updateActionStates;
13824	EditorUi.prototype.updateActionStates = function()
13825	{
13826		editorUiUpdateActionStates.apply(this, arguments);
13827
13828		var graph = this.editor.graph;
13829		var file = this.getCurrentFile();
13830		var active = this.isDiagramActive();
13831		var editable = graph.getEditableCells(graph.getSelectionCells());
13832		var enabled = file != null || urlParams['embed'] == '1';
13833
13834		this.actions.get('pageSetup').setEnabled(active);
13835		this.actions.get('autosave').setEnabled(file != null && file.isEditable() && file.isAutosaveOptional());
13836		this.actions.get('guides').setEnabled(active);
13837		this.actions.get('editData').setEnabled(editable.length > 0 || graph.isSelectionEmpty());
13838		this.actions.get('shadowVisible').setEnabled(active);
13839		this.actions.get('connectionArrows').setEnabled(active);
13840		this.actions.get('connectionPoints').setEnabled(active);
13841		this.actions.get('copyStyle').setEnabled(active && !graph.isSelectionEmpty());
13842		this.actions.get('pasteStyle').setEnabled(active && editable.length > 0);
13843		this.actions.get('editGeometry').setEnabled(editable.length > 0 &&
13844			graph.getModel().isVertex(editable[0]));
13845		this.actions.get('createShape').setEnabled(active);
13846		this.actions.get('createRevision').setEnabled(active);
13847		this.actions.get('moveToFolder').setEnabled(file != null);
13848		this.actions.get('makeCopy').setEnabled(file != null && !file.isRestricted());
13849		this.actions.get('editDiagram').setEnabled(active && (file == null || !file.isRestricted()));
13850		this.actions.get('publishLink').setEnabled(file != null && !file.isRestricted());
13851		this.actions.get('tags').setEnabled(this.diagramContainer.style.visibility != 'hidden');
13852		this.actions.get('layers').setEnabled(this.diagramContainer.style.visibility != 'hidden');
13853		this.actions.get('outline').setEnabled(this.diagramContainer.style.visibility != 'hidden');
13854		this.actions.get('rename').setEnabled((file != null && file.isRenamable()) || urlParams['embed'] == '1');
13855		this.actions.get('close').setEnabled(file != null);
13856		this.menus.get('publish').setEnabled(file != null && !file.isRestricted());
13857
13858		var findReplace = this.actions.get('findReplace');
13859		findReplace.setEnabled(this.diagramContainer.style.visibility != 'hidden');
13860		findReplace.label = mxResources.get('find') + ((graph.isEnabled()) ?
13861			'/' + mxResources.get('replace') : '') + '...';
13862
13863		var state = graph.view.getState(graph.getSelectionCell());
13864		this.actions.get('editShape').setEnabled(active && state != null && state.shape != null && state.shape.stencil != null);
13865	};
13866
13867	/**
13868	 * Overridden to remove export dialog in chromeless lightbox.
13869	 */
13870	var editoUiDestroy = EditorUi.prototype.destroy;
13871
13872	EditorUi.prototype.destroy = function()
13873	{
13874		if (this.exportDialog != null)
13875		{
13876			this.exportDialog.parentNode.removeChild(this.exportDialog);
13877			this.exportDialog = null;
13878		}
13879
13880		editoUiDestroy.apply(this, arguments);
13881	};
13882
13883	/**
13884	 * Overrides export dialog for using ui functions for save and setting global switches.
13885	 */
13886	if (window.ExportDialog != null)
13887	{
13888		ExportDialog.showXmlOption = false;
13889		ExportDialog.showGifOption = false;
13890
13891		ExportDialog.exportFile = function(editorUi, name, format, bg, s, b, dpi, grid)
13892		{
13893			var graph = editorUi.editor.graph;
13894
13895			if (format == 'xml')
13896			{
13897				editorUi.hideDialog();
13898				editorUi.saveData(name, 'xml', mxUtils.getXml(editorUi.editor.getGraphXml()), 'text/xml');
13899			}
13900		    else if (format == 'svg')
13901			{
13902		    	editorUi.hideDialog();
13903				editorUi.saveData(name, 'svg', mxUtils.getXml(graph.getSvg(bg, s, b)), 'image/svg+xml');
13904			}
13905		    else
13906		    {
13907		    	var data = editorUi.getFileData(true, null, null, null, null, true);
13908		    	var bounds = graph.getGraphBounds();
13909				var w = Math.floor(bounds.width * s / graph.view.scale);
13910				var h = Math.floor(bounds.height * s / graph.view.scale);
13911
13912				if (data.length <= MAX_REQUEST_SIZE && w * h < MAX_AREA)
13913				{
13914					editorUi.hideDialog();
13915
13916					if ((format == 'png' || format == 'jpg' || format == 'jpeg') && editorUi.isExportToCanvas())
13917					{
13918						if (format == 'png')
13919						{
13920							editorUi.exportImage(s, bg == null || bg == 'none', true,
13921						   		false, false, b, true, false, null, grid, dpi);
13922						}
13923						else
13924						{
13925							editorUi.exportImage(s, false, true,
13926								false, false, b, true, false, 'jpeg', grid);
13927						}
13928					}
13929					else
13930					{
13931						var extras = {globalVars: graph.getExportVariables()};
13932
13933						if (grid)
13934						{
13935							extras.grid = {
13936								size: graph.gridSize,
13937								steps: graph.view.gridSteps,
13938								color: graph.view.gridColor
13939							};
13940						}
13941
13942						editorUi.saveRequest(name, format,
13943							function(newTitle, base64)
13944							{
13945								return new mxXmlRequest(EXPORT_URL, 'format=' + format + '&base64=' + (base64 || '0') +
13946									((newTitle != null) ? '&filename=' + encodeURIComponent(newTitle) : '') +
13947									'&extras=' + encodeURIComponent(JSON.stringify(extras)) +
13948									(dpi > 0? '&dpi=' + dpi : '') +
13949									'&bg=' + ((bg != null) ? bg : 'none') + '&w=' + w + '&h=' + h +
13950									'&border=' + b + '&xml=' + encodeURIComponent(data));
13951							});
13952					}
13953				}
13954				else
13955				{
13956					mxUtils.alert(mxResources.get('drawingTooLarge'));
13957				}
13958			}
13959		};
13960	}
13961
13962	EditorUi.prototype.getDiagramTextContent = function()
13963	{
13964		this.editor.graph.setEnabled(false);
13965		var graph = this.editor.graph;
13966
13967		var allPagesTxt = '';
13968
13969		if (this.pages != null)
13970		{
13971			for (var i = 0; i < this.pages.length; i++)
13972			{
13973				var pageGraph = graph;
13974
13975				if (this.currentPage != this.pages[i])
13976				{
13977					pageGraph = this.createTemporaryGraph(graph.getStylesheet());
13978					this.updatePageRoot(this.pages[i]);
13979					pageGraph.model.setRoot(this.pages[i].root);
13980				}
13981				allPagesTxt += this.pages[i].getName() + ' ' + pageGraph.getIndexableText() + ' ';
13982			}
13983		}
13984		else
13985		{
13986			allPagesTxt = graph.getIndexableText();
13987		}
13988
13989		this.editor.graph.setEnabled(true);
13990		return allPagesTxt;
13991	};
13992
13993	EditorUi.prototype.showRemotelyStoredLibrary = function(title)
13994	{
13995		var selectedLibs = {};
13996		var div = document.createElement('div');
13997		div.style.whiteSpace = 'nowrap';
13998		var graph = this.editor.graph;
13999
14000		var hd = document.createElement('h3');
14001		mxUtils.write(hd, mxUtils.htmlEntities(title));
14002		hd.style.cssText = 'width:100%;text-align:center;margin-top:0px;margin-bottom:12px';
14003		div.appendChild(hd);
14004
14005		var libsSection = document.createElement('div');
14006		libsSection.style.cssText = 'border:1px solid lightGray;overflow: auto;height:300px';
14007
14008		libsSection.innerHTML = '<div style="text-align:center;padding:8px;"><img src="' + IMAGE_PATH + '/spin.gif"></div>';
14009
14010		var loadedLibs = {};
14011
14012		try
14013		{
14014			var custLibs = mxSettings.getCustomLibraries();
14015
14016			for (var j = 0; j < custLibs.length; j++)
14017			{
14018				var l = custLibs[j];
14019
14020				if (l.substring(0, 1) == 'R')
14021				{
14022					var libDesc = JSON.parse(decodeURIComponent(l.substring(1)));
14023					loadedLibs[libDesc[0]] = {
14024						id: libDesc[0],
14025               			title: libDesc[1],
14026               			downloadUrl: libDesc[2]
14027					};
14028				}
14029			}
14030		}
14031		catch(e){}
14032
14033		this.remoteInvoke('getCustomLibraries', null, null, function(libsList)
14034		{
14035			libsSection.innerHTML = '';
14036
14037			if (libsList.length == 0)
14038			{
14039				libsSection.innerHTML = '<div style="text-align:center;padding-top:20px;color:gray;">' +
14040					mxUtils.htmlEntities(mxResources.get('noLibraries')) + '</div>';
14041			}
14042			else
14043			{
14044				for (var i = 0; i < libsList.length; i++)
14045				{
14046					var lib = libsList[i];
14047
14048					if (loadedLibs[lib.id])
14049					{
14050						selectedLibs[lib.id] = lib;
14051					}
14052
14053					var libCheck = this.addCheckbox(libsSection, lib.title, loadedLibs[lib.id]);
14054
14055					(function(lib2, check)
14056					{
14057						mxEvent.addListener(check, 'change', function()
14058						{
14059							if (this.checked)
14060							{
14061								selectedLibs[lib2.id] = lib2;
14062							}
14063							else
14064							{
14065								delete selectedLibs[lib2.id];
14066							}
14067						});
14068					})(lib, libCheck)
14069				}
14070			}
14071		}, mxUtils.bind(this, function(e)
14072		{
14073			libsSection.innerHTML = '';
14074			var status = document.createElement('div');
14075			status.style.padding = '8px';
14076			status.style.textAlign = 'center';
14077			mxUtils.write(status, mxResources.get('error') + ': ');
14078			mxUtils.write(status, (e != null && e.message != null) ?
14079				e.message : mxResources.get('unknownError'));
14080			libsSection.appendChild(status);
14081		}));
14082
14083		div.appendChild(libsSection);
14084
14085		var dlg = new CustomDialog(this, div, mxUtils.bind(this, function()
14086		{
14087			this.spinner.spin(document.body, mxResources.get('loading'));
14088			var pendingLibs = 0;
14089
14090			for (var id in selectedLibs)
14091			{
14092				if (loadedLibs[id] != null) continue; //already loaded!
14093
14094				pendingLibs++;
14095
14096				(mxUtils.bind(this, function(lib)
14097				{
14098					this.remoteInvoke('getFileContent', [lib.downloadUrl], null, mxUtils.bind(this, function(libContent)
14099					{
14100						pendingLibs--;
14101
14102						if (pendingLibs == 0) this.spinner.stop();
14103
14104						try
14105						{
14106							this.loadLibrary(new RemoteLibrary(this, libContent, lib));
14107						}
14108						catch (e)
14109						{
14110							this.handleError(e, mxResources.get('errorLoadingFile'));
14111						}
14112					}), mxUtils.bind(this, function()
14113					{
14114						pendingLibs--;
14115
14116						if (pendingLibs == 0) this.spinner.stop();
14117
14118						this.handleError(null, mxResources.get('errorLoadingFile'));
14119					}));
14120				}))(selectedLibs[id]);
14121			}
14122
14123			for (var id in loadedLibs)
14124			{
14125				if (!selectedLibs[id]) //Removed
14126				{
14127					this.closeLibrary(new RemoteLibrary(this, null, loadedLibs[id])); //create a dummy library such that we can call closeLibrary
14128				}
14129			}
14130
14131			if (pendingLibs == 0) this.spinner.stop();
14132		}), null, null, 'https://www.diagrams.net/doc/faq/custom-libraries-confluence-cloud');
14133		this.showDialog(dlg.container, 340, 390, true, true, null, null, null, null, true);
14134	};
14135
14136	//Remote invokation, currently limited to functions in EditorUi (and its sub objects) for security reasons
14137	//White-listed functions and some info about it
14138	EditorUi.prototype.remoteInvokableFns = {
14139		getDiagramTextContent: {isAsync: false},
14140		getLocalStorageFile: {isAsync: false, allowedDomains: ['app.diagrams.net']},
14141		getLocalStorageFileNames: {isAsync: false, allowedDomains: ['app.diagrams.net']},
14142		setMigratedFlag: {isAsync: false, allowedDomains: ['app.diagrams.net']}
14143	};
14144
14145	EditorUi.prototype.remoteInvokeCallbacks = [];
14146	EditorUi.prototype.remoteInvokeQueue = [];
14147
14148	EditorUi.prototype.handleRemoteInvokeReady = function(remoteWin)
14149	{
14150		this.remoteWin = remoteWin;
14151
14152		for (var i = 0; i < this.remoteInvokeQueue.length; i++)
14153		{
14154			remoteWin.postMessage(this.remoteInvokeQueue[i], '*');
14155		}
14156
14157		this.remoteInvokeQueue = [];
14158	};
14159
14160	EditorUi.prototype.handleRemoteInvokeResponse = function(msg)
14161	{
14162		var msgMarkers = msg.msgMarkers;
14163		var callback = this.remoteInvokeCallbacks[msgMarkers.callbackId];
14164
14165		if (callback == null)
14166		{
14167			throw new Error('No callback for ' + ((msgMarkers != null) ? msgMarkers.callbackId : 'null'));
14168		}
14169		else if (msg.error)
14170		{
14171			if (callback.error) callback.error(msg.error.errResp);
14172		}
14173		else if (callback.callback)
14174		{
14175			callback.callback.apply(this, msg.resp);
14176		}
14177
14178		this.remoteInvokeCallbacks[msgMarkers.callbackId] = null; //set it to null only to keep the index
14179	};
14180
14181	EditorUi.prototype.remoteInvoke = function(remoteFn, remoteFnArgs, msgMarkers, callback, error)
14182	{
14183		var acceptResponse = true;
14184
14185		var timeoutThread = window.setTimeout(mxUtils.bind(this, function()
14186		{
14187			acceptResponse = false;
14188			error({code: App.ERROR_TIMEOUT, message: mxResources.get('timeout')});
14189		}), this.timeout);
14190
14191		var wrapper = mxUtils.bind(this, function()
14192		{
14193	    	window.clearTimeout(timeoutThread);
14194
14195			if (acceptResponse)
14196			{
14197				callback.apply(this, arguments);
14198			}
14199		});
14200
14201		var errWrapper = mxUtils.bind(this, function()
14202		{
14203	    	window.clearTimeout(timeoutThread);
14204
14205			if (acceptResponse)
14206			{
14207				error.apply(this, arguments);
14208			}
14209		});
14210
14211		msgMarkers = msgMarkers || {};
14212		msgMarkers.callbackId = this.remoteInvokeCallbacks.length;
14213		this.remoteInvokeCallbacks.push({callback: wrapper, error: errWrapper});
14214		var msg = JSON.stringify({event: 'remoteInvoke', funtionName: remoteFn, functionArgs: remoteFnArgs, msgMarkers: msgMarkers});
14215
14216		if (this.remoteWin != null) //remote invoke is ready
14217		{
14218			this.remoteWin.postMessage(msg, '*');
14219		}
14220		else
14221		{
14222			this.remoteInvokeQueue.push(msg);
14223		}
14224	};
14225
14226	EditorUi.prototype.handleRemoteInvoke = function(msg, origin)
14227	{
14228		var sendResponse = mxUtils.bind(this, function(resp, error)
14229		{
14230			var respMsg = {event: 'remoteInvokeResponse', msgMarkers: msg.msgMarkers};
14231
14232			if (error != null)
14233			{
14234				respMsg.error = {errResp: error};
14235			}
14236			else if (resp != null)
14237			{
14238				respMsg.resp = resp;
14239			}
14240
14241			this.remoteWin.postMessage(JSON.stringify(respMsg), '*');
14242		});
14243
14244		try
14245		{
14246			//Remote invoke are allowed to call functions in AC
14247			var funtionName = msg.funtionName;
14248			var functionInfo = this.remoteInvokableFns[funtionName];
14249
14250			if (functionInfo != null && typeof this[funtionName] === 'function')
14251			{
14252				if (functionInfo.allowedDomains)
14253				{
14254					var allowed = false;
14255
14256					for (var i = 0; i < functionInfo.allowedDomains.length; i++)
14257					{
14258						if (origin == 'https://' + functionInfo.allowedDomains[i])
14259						{
14260							allowed = true;
14261							break;
14262						}
14263					}
14264
14265					if (!allowed)
14266					{
14267						sendResponse(null, 'Invalid Call: ' + funtionName + ' is not allowed.');
14268						return;
14269					}
14270				}
14271
14272				var functionArgs = msg.functionArgs;
14273
14274				//Confirm functionArgs are not null and is array, otherwise, discard it
14275				if (!Array.isArray(functionArgs))
14276				{
14277					functionArgs = [];
14278				}
14279
14280				//for functions with callbacks (async) we assume last two arguments are success, error
14281				if (functionInfo.isAsync)
14282				{
14283					//success
14284					functionArgs.push(function()
14285					{
14286						sendResponse(Array.prototype.slice.apply(arguments));
14287					});
14288
14289					//error
14290					functionArgs.push(function(err)
14291					{
14292						sendResponse(null, err || 'Unkown Error');
14293					});
14294
14295					this[funtionName].apply(this, functionArgs);
14296				}
14297				else
14298				{
14299					var resp = this[funtionName].apply(this, functionArgs);
14300
14301					sendResponse([resp]);
14302				}
14303			}
14304			else
14305			{
14306				sendResponse(null, 'Invalid Call: ' + funtionName + ' is not found.');
14307			}
14308		}
14309		catch(e)
14310		{
14311			sendResponse(null, 'Invalid Call: An error occurred, ' + e.message);
14312		}
14313	};
14314
14315	/**
14316	 * Opens the application keystore.
14317	 */
14318	EditorUi.prototype.openDatabase = function(success, error)
14319	{
14320		if (this.database == null)
14321		{
14322			var indexedDB = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB;
14323
14324			if (indexedDB != null)
14325			{
14326				try
14327				{
14328					var req = indexedDB.open('database', 2);
14329
14330					req.onupgradeneeded = function(e)
14331					{
14332						try
14333						{
14334							var db = req.result;
14335
14336							if (e.oldVersion < 1)
14337							{
14338							    // Version 1 is the first version of the database.
14339								db.createObjectStore('objects', {keyPath: 'key'});
14340							}
14341
14342							if (e.oldVersion < 2)
14343							{
14344								// Version 2 introduces browser file storage.
14345								db.createObjectStore('files', {keyPath: 'title'});
14346								db.createObjectStore('filesInfo', {keyPath: 'title'});
14347								EditorUi.migrateStorageFiles = isLocalStorage;
14348							}
14349						}
14350						catch (e)
14351						{
14352							if (error != null)
14353							{
14354								error(e);
14355							}
14356						}
14357					}
14358
14359					req.onsuccess = mxUtils.bind(this, function(e)
14360					{
14361						var db = req.result;
14362						this.database = db;
14363
14364						if (EditorUi.migrateStorageFiles)
14365						{
14366							StorageFile.migrate(db);
14367							EditorUi.migrateStorageFiles = false;
14368						}
14369
14370						if (location.host == 'app.diagrams.net' && !this.drawioMigrationStarted)
14371						{
14372							this.drawioMigrationStarted = true;
14373
14374							this.getDatabaseItem('.drawioMigrated3', mxUtils.bind(this, function(value)
14375							{
14376								if (value && urlParams['forceMigration'] != '1') //Already migrated
14377								{
14378									return;
14379								}
14380
14381								var drawioFrame = document.createElement('iframe');
14382								drawioFrame.style.display = 'none';
14383								drawioFrame.setAttribute('src', 'https://www.draw.io?embed=1&proto=json&forceMigration=' + urlParams['forceMigration']);
14384						    	document.body.appendChild(drawioFrame);
14385						    	var collectNames = true, allDone = false;
14386						    	var fileNames, index = 0;
14387
14388						    	var markAsMigrated = mxUtils.bind(this, function()
14389								{
14390						    		allDone = true;
14391									this.setDatabaseItem('.drawioMigrated3', true);
14392									drawioFrame.contentWindow.postMessage(JSON.stringify({action: 'remoteInvoke', funtionName: 'setMigratedFlag'}), '*');
14393								});
14394
14395								var next = mxUtils.bind(this, function()
14396								{
14397									index++;
14398									fetchOneFile();
14399								});
14400
14401								var fetchOneFile = mxUtils.bind(this, function()
14402								{
14403									try
14404									{
14405										if (index >= fileNames.length)
14406										{
14407											markAsMigrated();
14408											return;
14409										}
14410
14411										var fileTitle = fileNames[index];
14412
14413										StorageFile.getFileContent(this, fileTitle, mxUtils.bind(this, function(data)
14414										{
14415											if (data == null || (fileTitle == '.scratchpad' && data == this.emptyLibraryXml)) //Don't overwrite
14416											{
14417												drawioFrame.contentWindow.postMessage(JSON.stringify({action: 'remoteInvoke', funtionName: 'getLocalStorageFile', functionArgs: [fileTitle]}), '*');
14418											}
14419											else
14420											{
14421												next();
14422											}
14423										}), next);  //Ignore errors
14424									}
14425									catch(e)
14426									{
14427										//Log error
14428										console.log(e);
14429									}
14430								});
14431
14432								var importOneFile = mxUtils.bind(this, function(file)
14433								{
14434									try
14435									{
14436										this.setDatabaseItem(null, [{
14437											title: file.title,
14438											size: file.data.length,
14439											lastModified: Date.now(),
14440											type: file.isLib? 'L' : 'F'
14441										}, {
14442											title: file.title,
14443											data: file.data
14444										}], next, next /* Ignore errors */, ['filesInfo', 'files']);
14445									}
14446									catch(e)
14447									{
14448										//Log error
14449										console.log(e);
14450									}
14451								});
14452
14453						    	var messageListener = mxUtils.bind(this, function(evt)
14454								{
14455									try
14456									{
14457										//Only accept messages from migration iframe
14458										if (evt.source != drawioFrame.contentWindow)
14459										{
14460											return;
14461										}
14462
14463										var drawMsg = {};
14464
14465										try
14466										{
14467											drawMsg = JSON.parse(evt.data);
14468										}
14469										catch(e){} //Ignore
14470
14471										if (drawMsg.event == 'init')
14472										{
14473											drawioFrame.contentWindow.postMessage(JSON.stringify({action: 'remoteInvokeReady'}), '*');
14474											drawioFrame.contentWindow.postMessage(JSON.stringify({action: 'remoteInvoke', funtionName: 'getLocalStorageFileNames'}), '*');
14475										}
14476										else if (drawMsg.event == 'remoteInvokeResponse' && !allDone)
14477										{
14478											if (collectNames)
14479											{
14480												if (drawMsg.resp != null && drawMsg.resp.length > 0 && drawMsg.resp[0] != null)
14481												{
14482													fileNames = drawMsg.resp[0];
14483													collectNames = false;
14484													fetchOneFile();
14485												}
14486												else
14487												{
14488													//Nothing in draw.io localStorage
14489													markAsMigrated();
14490												}
14491											}
14492											else
14493											{
14494												//Add the file, then move to the next
14495												if (drawMsg.resp != null && drawMsg.resp.length > 0 && drawMsg.resp[0] != null)
14496												{
14497													importOneFile(drawMsg.resp[0]);
14498												}
14499												else
14500												{
14501													next();
14502												}
14503											}
14504										}
14505									}
14506									catch(e)
14507									{
14508										console.log(e);
14509									}
14510								});
14511
14512								window.addEventListener('message', messageListener);
14513							})); //Ignore errors
14514						}
14515
14516						success(db);
14517
14518						db.onversionchange = function()
14519						{
14520							//TODO Handle DB revision update while code is running
14521							//		Save open file and request a page reload before closing the DB
14522						    db.close();
14523						};
14524					});
14525
14526					req.onerror = error;
14527
14528					req.onblocked = function()
14529					{
14530						//TODO Use this when a new version is introduced
14531						// there's another open connection to same database
14532						// and it wasn't closed after db.onversionchange triggered for them
14533					};
14534				}
14535				catch (e)
14536				{
14537					if (error != null)
14538					{
14539						error(e);
14540					}
14541				}
14542			}
14543			else if (error != null)
14544			{
14545				error();
14546			}
14547		}
14548		else
14549		{
14550			success(this.database);
14551		}
14552	};
14553
14554	/**
14555	 * Add/Update item(s) in the database. It supports multiple stores transactions by sending an array of data, storeName
14556	 * (key is optional, can be an array also if multiple stores are needed)
14557	 */
14558	EditorUi.prototype.setDatabaseItem = function(key, data, success, error, storeName)
14559	{
14560		this.openDatabase(mxUtils.bind(this, function(db)
14561		{
14562			try
14563			{
14564				storeName = storeName || 'objects';
14565
14566				if (!Array.isArray(storeName))
14567				{
14568					storeName = [storeName];
14569					key = [key];
14570					data = [data];
14571				}
14572
14573				var trx = db.transaction(storeName, 'readwrite');
14574				trx.oncomplete = success;
14575				trx.onerror = error;
14576
14577				for (var i = 0; i < storeName.length; i++)
14578				{
14579					trx.objectStore(storeName[i]).put(key != null && key[i] != null? {key: key[i], data: data[i]} : data[i]);
14580				}
14581			}
14582			catch (e)
14583			{
14584				if (error != null)
14585				{
14586					error(e);
14587				}
14588			}
14589		}), error);
14590	};
14591
14592	/**
14593	 * Removes the item for the given key from the database.
14594	 */
14595	EditorUi.prototype.removeDatabaseItem = function(key, success, error, storeName)
14596	{
14597		this.openDatabase(mxUtils.bind(this, function(db)
14598		{
14599			storeName = storeName || 'objects';
14600
14601			if (!Array.isArray(storeName))
14602			{
14603				storeName = [storeName];
14604				key = [key];
14605			}
14606
14607			var trx = db.transaction(storeName, 'readwrite');
14608			trx.oncomplete = success;
14609			trx.onerror = error;
14610
14611			for (var i = 0; i < storeName.length; i++)
14612			{
14613				trx.objectStore(storeName[i]).delete(key[i]);
14614			}
14615		}), error);
14616	};
14617
14618	/**
14619	 * Returns one item from the database.
14620	 */
14621	EditorUi.prototype.getDatabaseItem = function(key, success, error, storeName)
14622	{
14623		this.openDatabase(mxUtils.bind(this, function(db)
14624		{
14625			try
14626			{
14627				storeName = storeName || 'objects';
14628				var trx = db.transaction([storeName], 'readonly');
14629				var req = trx.objectStore(storeName).get(key);
14630
14631				req.onsuccess = function()
14632				{
14633					success(req.result);
14634				};
14635
14636		        req.onerror = error;
14637			}
14638	        catch (e)
14639			{
14640				if (error != null)
14641				{
14642					error(e);
14643				}
14644			}
14645		}), error);
14646	};
14647
14648	/**
14649	 * Returns all items from the database.
14650	 */
14651	EditorUi.prototype.getDatabaseItems = function(success, error, storeName)
14652	{
14653		this.openDatabase(mxUtils.bind(this, function(db)
14654		{
14655			try
14656			{
14657				storeName = storeName || 'objects';
14658				var trx = db.transaction([storeName], 'readonly');
14659				var req = trx.objectStore(storeName).openCursor(
14660					IDBKeyRange.lowerBound(0));
14661				var items = [];
14662
14663				req.onsuccess = function(e)
14664				{
14665					if (e.target.result == null)
14666					{
14667						success(items);
14668					}
14669					else
14670					{
14671						items.push(e.target.result.value);
14672						e.target.result.continue();
14673					}
14674		        };
14675
14676		        req.onerror = error;
14677			}
14678			catch (e)
14679			{
14680				if (error != null)
14681				{
14682					error(e);
14683				}
14684			}
14685		}), error);
14686	};
14687
14688	/**
14689	 * Returns all item keys from the database.
14690	 */
14691	EditorUi.prototype.getDatabaseItemKeys = function(success, error, storeName)
14692	{
14693		this.openDatabase(mxUtils.bind(this, function(db)
14694		{
14695			try
14696			{
14697				storeName = storeName || 'objects';
14698				var trx = db.transaction([storeName], 'readonly');
14699				var req = trx.objectStore(storeName).getAllKeys();
14700
14701				req.onsuccess = function()
14702				{
14703					success(req.result);
14704		        };
14705
14706		        req.onerror = error;
14707			}
14708			catch (e)
14709			{
14710				if (error != null)
14711				{
14712					error(e);
14713				}
14714			}
14715		}), error);
14716	};
14717	/**
14718	 * Comments: We need these functions as wrapper of File functions in order to facilitate
14719	 * overriding them if comments are needed without having a file (e.g. Confluence Plugin)
14720	 */
14721
14722	/**
14723	 * Are comments supported
14724	 */
14725	EditorUi.prototype.commentsSupported = function()
14726	{
14727		var file = this.getCurrentFile();
14728
14729		return file != null? file.commentsSupported() : false;
14730	};
14731
14732	/**
14733	 * Show refresh button?
14734	 */
14735	EditorUi.prototype.commentsRefreshNeeded = function()
14736	{
14737		var file = this.getCurrentFile();
14738
14739		return file != null? file.commentsRefreshNeeded() : true;
14740	};
14741
14742	/**
14743	 * Show save button?
14744	 */
14745	EditorUi.prototype.commentsSaveNeeded = function()
14746	{
14747		var file = this.getCurrentFile();
14748
14749		return file != null? file.commentsSaveNeeded() : false;
14750	};
14751
14752	/**
14753	 * Get comments
14754	 */
14755	EditorUi.prototype.getComments = function(success, error)
14756	{
14757		var file = this.getCurrentFile();
14758
14759		if (file != null)
14760		{
14761			file.getComments(success, error);
14762		}
14763		else
14764		{
14765			success([]); //placeholder
14766		}
14767	};
14768
14769	/**
14770	 * Add a comment
14771	 */
14772	EditorUi.prototype.addComment = function(comment, success, error)
14773	{
14774		var file = this.getCurrentFile();
14775
14776		if (file != null)
14777		{
14778			file.addComment(comment, success, error);
14779		}
14780		else
14781		{
14782			success(Date.now()); //placeholder
14783		}
14784	};
14785
14786	/**
14787	 * Can add a reply to a reply
14788	 */
14789	EditorUi.prototype.canReplyToReplies = function()
14790	{
14791		var file = this.getCurrentFile();
14792
14793		return file != null? file.canReplyToReplies() : true;
14794	};
14795
14796	/**
14797	 * Can add comments (The permission to comment)
14798	 */
14799	EditorUi.prototype.canComment = function()
14800	{
14801		var file = this.getCurrentFile();
14802
14803		return file != null? file.canComment() : true;
14804	};
14805
14806	/**
14807	 * Get a new comment object
14808	 */
14809	EditorUi.prototype.newComment = function(content, user)
14810	{
14811		var file = this.getCurrentFile();
14812
14813		if (file != null)
14814		{
14815			return file.newComment(content, user)
14816		}
14817		else
14818		{
14819			return new DrawioComment(this, null, content, Date.now(), Date.now(), false, user);
14820		}
14821	};
14822
14823	//==================================================== End of comments =================================================================
14824
14825	/**
14826	 * Does revisions history available
14827	 */
14828	EditorUi.prototype.isRevisionHistorySupported = function()
14829	{
14830		var file = this.getCurrentFile();
14831
14832		return file != null && file.isRevisionHistorySupported();
14833	};
14834
14835	/**
14836	 * Get revisions of current file
14837	 */
14838	EditorUi.prototype.getRevisions = function(success, error)
14839	{
14840		var file = this.getCurrentFile();
14841
14842		if (file != null && file.getRevisions)
14843		{
14844			file.getRevisions(success, error);
14845		}
14846		else
14847		{
14848			error({message: mxResources.get('unknownError')});
14849		}
14850	};
14851
14852	/**
14853	 * Is revisions history enabled
14854	 */
14855	EditorUi.prototype.isRevisionHistoryEnabled = function()
14856	{
14857		var file = this.getCurrentFile();
14858
14859		return file != null &&
14860				((file.constructor == DriveFile && file.isEditable()) ||
14861				file.constructor == DropboxFile);
14862	};
14863
14864	//===========Adding methods to find the service running draw.io and allowing calling draw.io remote services
14865	EditorUi.prototype.getServiceName = function()
14866	{
14867		return 'draw.io';
14868	};
14869
14870	EditorUi.prototype.addRemoteServiceSecurityCheck = function(xhr)
14871	{
14872		//Using a standard header with specific sequence
14873		xhr.setRequestHeader('Content-Language', 'da, mi, en, de-DE');
14874	};
14875
14876	//===========To Be Removed Soon==========
14877	EditorUi.prototype.loadUrl = function(url, success, error, forceBinary, retry, dataUriPrefix, noBinary, headers)
14878	{
14879		EditorUi.logEvent('SHOULD NOT BE CALLED: loadUrl');
14880		return this.editor.loadUrl(url, success, error, forceBinary, retry, dataUriPrefix, noBinary, headers);
14881	};
14882
14883	EditorUi.prototype.loadFonts = function(then)
14884	{
14885		EditorUi.logEvent('SHOULD NOT BE CALLED: loadFonts');
14886		return this.editor.loadFonts(then);
14887	};
14888
14889	EditorUi.prototype.createSvgDataUri = function(svg)
14890	{
14891		EditorUi.logEvent('SHOULD NOT BE CALLED: createSvgDataUri');
14892		return Editor.createSvgDataUri(svg);
14893	};
14894
14895    EditorUi.prototype.embedCssFonts = function(fontCss, then)
14896    {
14897		EditorUi.logEvent('SHOULD NOT BE CALLED: embedCssFonts');
14898		return this.editor.embedCssFonts(fontCss, then);
14899	};
14900
14901    EditorUi.prototype.embedExtFonts = function(callback)
14902	{
14903		EditorUi.logEvent('SHOULD NOT BE CALLED: embedExtFonts');
14904		return this.editor.embedExtFonts(callback);
14905	};
14906
14907	EditorUi.prototype.exportToCanvas = function(callback, width, imageCache, background, error, limitHeight,
14908			ignoreSelection, scale, transparentBackground, addShadow, converter, graph, border, noCrop, grid, keepTheme)
14909	{
14910		EditorUi.logEvent('SHOULD NOT BE CALLED: exportToCanvas');
14911		return this.editor.exportToCanvas(callback, width, imageCache, background, error, limitHeight,
14912			ignoreSelection, scale, transparentBackground, addShadow, converter, graph, border,
14913			noCrop, grid, keepTheme);
14914	};
14915
14916	EditorUi.prototype.createImageUrlConverter = function()
14917	{
14918		EditorUi.logEvent('SHOULD NOT BE CALLED: createImageUrlConverter');
14919		return this.editor.createImageUrlConverter();
14920	};
14921
14922	EditorUi.prototype.convertImages = function(svgRoot, callback, imageCache, converter)
14923	{
14924		EditorUi.logEvent('SHOULD NOT BE CALLED: convertImages');
14925		return this.editor.convertImages(svgRoot, callback, imageCache, converter);
14926	};
14927
14928	EditorUi.prototype.convertImageToDataUri = function(url, callback)
14929	{
14930		EditorUi.logEvent('SHOULD NOT BE CALLED: convertImageToDataUri');
14931		return this.editor.convertImageToDataUri(url, callback);
14932	};
14933
14934	EditorUi.prototype.base64Encode = function(str)
14935	{
14936		EditorUi.logEvent('SHOULD NOT BE CALLED: base64Encode');
14937		return Editor.base64Encode(str);
14938	};
14939
14940	EditorUi.prototype.updateCRC = function(crc, data, off, len)
14941	{
14942		EditorUi.logEvent('SHOULD NOT BE CALLED: updateCRC');
14943		return Editor.updateCRC(crc, data, off, len);
14944	};
14945
14946	EditorUi.prototype.crc32 = function(str)
14947	{
14948		EditorUi.logEvent('SHOULD NOT BE CALLED: crc32');
14949		return Editor.crc32(str);
14950	};
14951
14952	EditorUi.prototype.writeGraphModelToPng = function(data, type, key, value, error)
14953	{
14954		EditorUi.logEvent('SHOULD NOT BE CALLED: writeGraphModelToPng');
14955		return Editor.writeGraphModelToPng(data, type, key, value, error);
14956	};
14957
14958	//=======End of To Be Removed Soon==========
14959
14960	EditorUi.prototype.getLocalStorageFileNames = function()
14961	{
14962		if (localStorage.getItem('.localStorageMigrated') == '1' && urlParams['forceMigration'] != '1')
14963		{
14964			return null;
14965		}
14966
14967		var files = [];
14968
14969		for (var i = 0; i < localStorage.length; i++)
14970		{
14971			var key = localStorage.key(i);
14972			var value = localStorage.getItem(key);
14973
14974			if (key.length > 0 && (key == '.scratchpad' || key.charAt(0) != '.') && value.length > 0)
14975			{
14976				var isFile = (value.substring(0, 8) === '<mxfile ' ||
14977							value.substring(0, 5) === '<?xml' || value.substring(0, 12) === '<!--[if IE]>');
14978				var isLib = (value.substring(0, 11) === '<mxlibrary>');
14979
14980				if (isFile || isLib)
14981				{
14982					files.push(key);
14983				}
14984			}
14985		}
14986
14987		return files;
14988	};
14989
14990	EditorUi.prototype.getLocalStorageFile = function(key)
14991	{
14992		if (localStorage.getItem('.localStorageMigrated') == '1' && urlParams['forceMigration'] != '1')
14993		{
14994			return null;
14995		}
14996
14997		var value = localStorage.getItem(key);
14998		return {title: key, data: value, isLib: value.substring(0, 11) === '<mxlibrary>'};
14999	};
15000
15001	EditorUi.prototype.setMigratedFlag = function()
15002	{
15003		localStorage.setItem('.localStorageMigrated', '1');
15004	};
15005})();
15006
15007/**
15008 * Comments Window, It is used by both editor and viewer. So, it is here in a common place
15009 */
15010var CommentsWindow = function(editorUi, x, y, w, h, saveCallback)
15011{
15012	var readOnly = !editorUi.canComment();
15013	var canReplyToReplies = editorUi.canReplyToReplies();
15014	var curEdited = null;
15015
15016	var div = document.createElement('div');
15017	div.className = 'geCommentsWin';
15018	div.style.background = (!Editor.isDarkMode()) ? 'whiteSmoke' : Dialog.backdropColor;
15019
15020	var tbarHeight = (!EditorUi.compactUi) ? '30px' : '26px';
15021
15022	var listDiv = document.createElement('div');
15023	listDiv.className = 'geCommentsList';
15024	listDiv.style.backgroundColor = (!Editor.isDarkMode()) ? 'whiteSmoke' : Dialog.backdropColor;
15025	listDiv.style.bottom = (parseInt(tbarHeight) + 7) + 'px';
15026	div.appendChild(listDiv);
15027
15028	var noComments = document.createElement('span');
15029	noComments.style.cssText = 'display:none;padding-top:10px;text-align:center;';
15030	mxUtils.write(noComments, mxResources.get('noCommentsFound'));
15031
15032	var selectionComment = null;
15033
15034	var ldiv = document.createElement('div');
15035
15036	ldiv.className = 'geToolbarContainer geCommentsToolbar';
15037	ldiv.style.height = tbarHeight;
15038	ldiv.style.padding = (!EditorUi.compactUi) ? '1px' : '4px 0px 3px 0px';
15039	ldiv.style.backgroundColor = (!Editor.isDarkMode()) ? 'whiteSmoke' : Dialog.backdropColor;
15040
15041	var link = document.createElement('a');
15042	link.className = 'geButton';
15043
15044	function updateNoComments()
15045	{
15046		var divs = listDiv.getElementsByTagName('div');
15047		var visibleCount = 0;
15048
15049		for (var i = 0; i < divs.length; i++)
15050		{
15051			if (divs[i].style.display != 'none' && divs[i].parentNode == listDiv)
15052			{
15053				visibleCount++;
15054			}
15055		}
15056
15057		noComments.style.display = (visibleCount == 0) ? 'block' : 'none';
15058	};
15059
15060	function editComment(comment, cdiv, saveCallback, deleteOnCancel)
15061	{
15062		curEdited = {div: cdiv, comment: comment, saveCallback: saveCallback, deleteOnCancel: deleteOnCancel};
15063
15064		var commentTxt = cdiv.querySelector('.geCommentTxt');
15065		var actionsDiv = cdiv.querySelector('.geCommentActionsList');
15066
15067		var textArea = document.createElement('textarea');
15068		textArea.className = 'geCommentEditTxtArea';
15069		textArea.style.minHeight = commentTxt.offsetHeight + 'px';
15070		textArea.value = comment.content;
15071		cdiv.insertBefore(textArea, commentTxt);
15072
15073		var btnDiv = document.createElement('div');
15074		btnDiv.className = 'geCommentEditBtns';
15075
15076		function reset()
15077		{
15078			cdiv.removeChild(textArea);
15079			cdiv.removeChild(btnDiv);
15080			actionsDiv.style.display = 'block';
15081			commentTxt.style.display = 'block';
15082		};
15083
15084		var cancelBtn = mxUtils.button(mxResources.get('cancel'), function()
15085		{
15086			if (deleteOnCancel)
15087			{
15088				cdiv.parentNode.removeChild(cdiv);
15089				updateNoComments();
15090			}
15091			else
15092			{
15093				reset();
15094			}
15095
15096			curEdited = null;
15097		});
15098
15099		cancelBtn.className = 'geCommentEditBtn';
15100		btnDiv.appendChild(cancelBtn);
15101
15102		var saveBtn = mxUtils.button(mxResources.get('save'), function()
15103		{
15104			commentTxt.innerHTML = '';
15105			comment.content = textArea.value;
15106			mxUtils.write(commentTxt, comment.content);
15107			reset();
15108			saveCallback(comment);
15109			curEdited = null;
15110		});
15111
15112		// Updates modified state and handles placeholder text
15113		mxEvent.addListener(textArea, 'keydown', mxUtils.bind(this, function(evt)
15114		{
15115			if (!mxEvent.isConsumed(evt))
15116			{
15117				if ((mxEvent.isControlDown(evt) || (mxClient.IS_MAC &&
15118					mxEvent.isMetaDown(evt))) && evt.keyCode == 13 /* Ctrl+Enter */)
15119				{
15120					saveBtn.click();
15121					mxEvent.consume(evt);
15122				}
15123				else if (evt.keyCode == 27 /* Escape */)
15124				{
15125					cancelBtn.click();
15126					mxEvent.consume(evt);
15127				}
15128			}
15129		}));
15130
15131		// Focused to include in viewport before focusin textbox
15132		saveBtn.focus();
15133		saveBtn.className = 'geCommentEditBtn gePrimaryBtn';
15134		btnDiv.appendChild(saveBtn);
15135
15136		cdiv.insertBefore(btnDiv, commentTxt);
15137		actionsDiv.style.display = 'none';
15138		commentTxt.style.display = 'none';
15139		textArea.focus();
15140	};
15141
15142	function writeCommentDate(comment, dateDiv)
15143	{
15144		dateDiv.innerHTML = '';
15145		var ts = new Date(comment.modifiedDate);
15146		var str = editorUi.timeSince(ts);
15147
15148		if (str == null)
15149		{
15150			str = mxResources.get('lessThanAMinute');
15151		}
15152
15153		mxUtils.write(dateDiv, mxResources.get('timeAgo', [str], '{1} ago'));
15154		dateDiv.setAttribute('title', ts.toLocaleDateString() + ' ' +
15155				ts.toLocaleTimeString());
15156	};
15157
15158	function showBusy(commentDiv)
15159	{
15160		var busyImg = document.createElement('img');
15161		busyImg.className = 'geCommentBusyImg';
15162		busyImg.src= IMAGE_PATH + '/spin.gif';
15163		commentDiv.appendChild(busyImg);
15164		commentDiv.busyImg = busyImg;
15165	};
15166
15167	function showError(commentDiv)
15168	{
15169		commentDiv.style.border = '1px solid red';
15170		commentDiv.removeChild(commentDiv.busyImg);
15171	};
15172
15173	function showDone(commentDiv)
15174	{
15175		commentDiv.style.border = '';
15176		commentDiv.removeChild(commentDiv.busyImg);
15177	};
15178
15179	function addComment(comment, parentArr, parent, level, showResolved)
15180	{
15181		//Skip resolved comments if showResolved is not set
15182		if (!showResolved && comment.isResolved)
15183		{
15184			return;
15185		}
15186
15187		noComments.style.display = 'none';
15188
15189		var cdiv = document.createElement('div');
15190		cdiv.className = 'geCommentContainer';
15191		cdiv.setAttribute('data-commentId', comment.id);
15192		cdiv.style.marginLeft = (level * 20 + 5) + 'px';
15193
15194		if (comment.isResolved && !Editor.isDarkMode())
15195		{
15196			cdiv.style.backgroundColor = 'ghostWhite';
15197		}
15198
15199		var headerDiv = document.createElement('div');
15200		headerDiv.className = 'geCommentHeader';
15201
15202		var userImg = document.createElement('img');
15203		userImg.className = 'geCommentUserImg';
15204		userImg.src = comment.user.pictureUrl || Editor.userImage;
15205		headerDiv.appendChild(userImg);
15206
15207		var headerTxt = document.createElement('div');
15208		headerTxt.className = 'geCommentHeaderTxt';
15209		headerDiv.appendChild(headerTxt);
15210
15211		var usernameDiv = document.createElement('div');
15212		usernameDiv.className = 'geCommentUsername';
15213		mxUtils.write(usernameDiv, comment.user.displayName || '');
15214		headerTxt.appendChild(usernameDiv);
15215
15216		var dateDiv = document.createElement('div');
15217		dateDiv.className = 'geCommentDate';
15218		dateDiv.setAttribute('data-commentId', comment.id);
15219		writeCommentDate(comment, dateDiv);
15220		headerTxt.appendChild(dateDiv);
15221		cdiv.appendChild(headerDiv);
15222
15223		var commentTxtDiv = document.createElement('div');
15224		commentTxtDiv.className = 'geCommentTxt';
15225		mxUtils.write(commentTxtDiv, comment.content || '');
15226		cdiv.appendChild(commentTxtDiv);
15227
15228		if (comment.isLocked)
15229		{
15230			cdiv.style.opacity = '0.5';
15231		}
15232
15233		var actionsDiv = document.createElement('div');
15234		actionsDiv.className = 'geCommentActions';
15235		var actionsList = document.createElement('ul');
15236		actionsList.className = 'geCommentActionsList';
15237		actionsDiv.appendChild(actionsList);
15238
15239		function addAction(name, evtHandler, hide)
15240		{
15241			var action = document.createElement('li');
15242			action.className = 'geCommentAction';
15243			var actionLnk = document.createElement('a');
15244			actionLnk.className = 'geCommentActionLnk';
15245			mxUtils.write(actionLnk, name);
15246			action.appendChild(actionLnk);
15247
15248			mxEvent.addListener(actionLnk, 'click', function(evt)
15249			{
15250				evtHandler(evt, comment);
15251				evt.preventDefault();
15252				mxEvent.consume(evt);
15253			});
15254
15255			actionsList.appendChild(action);
15256
15257			if (hide) action.style.display = 'none';
15258		};
15259
15260		function collectReplies()
15261		{
15262			var replies = [];
15263			var pdiv = cdiv;
15264
15265			function collectReplies(comment)
15266			{
15267				replies.push(pdiv);
15268
15269				if (comment.replies != null)
15270				{
15271					for (var i = 0; i < comment.replies.length; i++)
15272					{
15273						pdiv = pdiv.nextSibling;
15274						collectReplies(comment.replies[i]);
15275					}
15276				}
15277			}
15278
15279			collectReplies(comment);
15280
15281			return {pdiv: pdiv, replies: replies};
15282		};
15283
15284		function addReply(initContent, editIt, saveCallback, doResolve, doReopen)
15285		{
15286			var pdiv = collectReplies().pdiv;
15287
15288			var newReply = editorUi.newComment(initContent, editorUi.getCurrentUser());
15289			newReply.pCommentId = comment.id;
15290
15291			if (comment.replies == null) comment.replies = [];
15292
15293			var replyComment = addComment(newReply, comment.replies, pdiv, level + 1);
15294
15295			function doAddReply()
15296			{
15297				showBusy(replyComment);
15298
15299				comment.addReply(newReply, function(id)
15300				{
15301					newReply.id = id;
15302					comment.replies.push(newReply);
15303					showDone(replyComment);
15304
15305					if (saveCallback) saveCallback();
15306
15307				}, function(err)
15308				{
15309					doEdit();
15310					showError(replyComment);
15311					editorUi.handleError(err, null, null, null,
15312						mxUtils.htmlEntities(mxResources.get('objectNotFound')));
15313				}, doResolve, doReopen);
15314			};
15315
15316			function doEdit()
15317			{
15318				editComment(newReply, replyComment, function(newReply)
15319				{
15320					doAddReply();
15321				}, true);
15322			};
15323
15324			if (editIt)
15325			{
15326				doEdit();
15327			}
15328			else
15329			{
15330				doAddReply();
15331			}
15332		};
15333
15334		if (!readOnly && !comment.isLocked && (level == 0 || canReplyToReplies))
15335		{
15336			addAction(mxResources.get('reply'), function()
15337			{
15338				addReply('', true);
15339			}, comment.isResolved);
15340		}
15341
15342		var user = editorUi.getCurrentUser();
15343
15344		if (user != null && user.id == comment.user.id && !readOnly && !comment.isLocked)
15345		{
15346			addAction(mxResources.get('edit'), function()
15347			{
15348				function doEditComment()
15349				{
15350					editComment(comment, cdiv, function()
15351					{
15352						showBusy(cdiv);
15353
15354						comment.editComment(comment.content, function()
15355						{
15356							showDone(cdiv);
15357						}, function(err)
15358						{
15359							showError(cdiv);
15360							doEditComment();
15361							editorUi.handleError(err, null, null, null,
15362								mxUtils.htmlEntities(mxResources.get('objectNotFound')));
15363						});
15364					});
15365				};
15366
15367				doEditComment();
15368			}, comment.isResolved);
15369
15370			addAction(mxResources.get('delete'), function()
15371			{
15372				editorUi.confirm(mxResources.get('areYouSure'), function()
15373				{
15374					showBusy(cdiv);
15375
15376					comment.deleteComment(function(markedOnly)
15377					{
15378						if (markedOnly === true)
15379						{
15380							var commentTxt = cdiv.querySelector('.geCommentTxt');
15381							commentTxt.innerHTML = '';
15382							mxUtils.write(commentTxt, mxResources.get('msgDeleted'));
15383
15384							var actions = cdiv.querySelectorAll('.geCommentAction');
15385
15386							for (var i = 0; i < actions.length; i++)
15387							{
15388								actions[i].parentNode.removeChild(actions[i]);
15389							}
15390
15391							showDone(cdiv);
15392							cdiv.style.opacity = '0.5';
15393						}
15394						else
15395						{
15396							var replies = collectReplies(comment).replies;
15397
15398							for (var i = 0; i < replies.length; i++)
15399							{
15400								listDiv.removeChild(replies[i]);
15401							}
15402
15403							for (var i = 0; i < parentArr.length; i++)
15404							{
15405								if (parentArr[i] == comment)
15406								{
15407									parentArr.splice(i, 1);
15408									break;
15409								}
15410							}
15411
15412							noComments.style.display = (listDiv.getElementsByTagName('div').length == 0) ? 'block' : 'none';
15413						}
15414					}, function(err)
15415					{
15416						showError(cdiv);
15417						editorUi.handleError(err, null, null, null,
15418							mxUtils.htmlEntities(mxResources.get('objectNotFound')));
15419					});
15420				});
15421			}, comment.isResolved);
15422		}
15423
15424		if (!readOnly && !comment.isLocked && level == 0) //Resolve is a top-level action only
15425		{
15426			function toggleResolve(evt)
15427			{
15428				function doToggle()
15429				{
15430					var resolveActionLnk = evt.target;
15431					resolveActionLnk.innerHTML = '';
15432
15433					comment.isResolved = !comment.isResolved;
15434					mxUtils.write(resolveActionLnk, comment.isResolved? mxResources.get('reopen') : mxResources.get('resolve'));
15435					var actionsDisplay = comment.isResolved? 'none' : '';
15436					var replies = collectReplies(comment).replies;
15437					var color = (Editor.isDarkMode()) ? 'transparent' : (comment.isResolved? 'ghostWhite' : 'white');
15438
15439					for (var i = 0; i < replies.length; i++)
15440					{
15441						replies[i].style.backgroundColor = color;
15442
15443						var forOpenActions = replies[i].querySelectorAll('.geCommentAction');
15444
15445						for (var j = 0; j < forOpenActions.length; j ++)
15446						{
15447							if (forOpenActions[j] == resolveActionLnk.parentNode) continue;
15448
15449							forOpenActions[j].style.display = actionsDisplay;
15450						}
15451
15452						if (!resolvedChecked)
15453						{
15454							replies[i].style.display = 'none';
15455						}
15456					}
15457
15458					updateNoComments();
15459				};
15460
15461				if (comment.isResolved)
15462				{
15463					addReply(mxResources.get('reOpened') + ': ', true, doToggle, false, true);
15464				}
15465				else
15466				{
15467					addReply(mxResources.get('markedAsResolved'), false, doToggle, true);
15468				}
15469			};
15470
15471			addAction(comment.isResolved? mxResources.get('reopen') : mxResources.get('resolve'), toggleResolve);
15472		}
15473
15474		cdiv.appendChild(actionsDiv);
15475
15476		if (parent != null)
15477		{
15478			listDiv.insertBefore(cdiv, parent.nextSibling);
15479		}
15480		else
15481		{
15482			listDiv.appendChild(cdiv);
15483		}
15484
15485		for (var i = 0; comment.replies != null && i < comment.replies.length; i++)
15486		{
15487			var reply = comment.replies[i];
15488			reply.isResolved = comment.isResolved; //copy isResolved to child comments (replies)
15489			addComment(reply, comment.replies, null, level + 1, showResolved);
15490		}
15491
15492		if (curEdited != null)
15493		{
15494			if (curEdited.comment.id == comment.id)
15495			{
15496				var origContent = comment.content;
15497				comment.content = curEdited.comment.content;
15498				editComment(comment, cdiv, curEdited.saveCallback, curEdited.deleteOnCancel);
15499				comment.content = origContent;
15500			}
15501			else if (curEdited.comment.id == null && curEdited.comment.pCommentId == comment.id)
15502			{
15503				listDiv.appendChild(curEdited.div);
15504				editComment(curEdited.comment, curEdited.div, curEdited.saveCallback, curEdited.deleteOnCancel);
15505			}
15506		}
15507
15508		return cdiv;
15509	};
15510
15511	if (!readOnly)
15512	{
15513		var addLink = link.cloneNode();
15514		addLink.innerHTML = '<div class="geSprite geSprite-plus" style="display:inline-block;"></div>';
15515		addLink.setAttribute('title', mxResources.get('create') + '...');
15516
15517		mxEvent.addListener(addLink, 'click', function(evt)
15518		{
15519			var newComment = editorUi.newComment('', editorUi.getCurrentUser());
15520			var newCommentDiv = addComment(newComment, comments, null, 0);
15521
15522			function doAddComment()
15523			{
15524				editComment(newComment, newCommentDiv, function(newComment)
15525				{
15526					showBusy(newCommentDiv);
15527
15528					editorUi.addComment(newComment, function(id)
15529					{
15530						newComment.id = id;
15531						comments.push(newComment);
15532						showDone(newCommentDiv);
15533					}, function(err)
15534					{
15535						showError(newCommentDiv);
15536						doAddComment();
15537						editorUi.handleError(err, null, null, null,
15538							mxUtils.htmlEntities(mxResources.get('objectNotFound')));
15539					});
15540				}, true);
15541			}
15542
15543			doAddComment();
15544			evt.preventDefault();
15545			mxEvent.consume(evt);
15546		});
15547
15548		ldiv.appendChild(addLink);
15549	}
15550
15551	var resolvedLink = link.cloneNode();
15552	resolvedLink.innerHTML = '<img src="' + IMAGE_PATH + '/check.png" style="width: 16px; padding: 2px;">';
15553	resolvedLink.setAttribute('title', mxResources.get('showResolved'));
15554	var resolvedChecked = false;
15555
15556	if (Editor.isDarkMode())
15557	{
15558		resolvedLink.style.filter = 'invert(100%)';
15559	}
15560
15561	mxEvent.addListener(resolvedLink, 'click', function(evt)
15562	{
15563		resolvedChecked = !resolvedChecked;
15564
15565		this.className = resolvedChecked? 'geButton geCheckedBtn' : 'geButton';
15566		refresh();
15567
15568		evt.preventDefault();
15569		mxEvent.consume(evt);
15570	});
15571
15572	ldiv.appendChild(resolvedLink);
15573
15574	if (editorUi.commentsRefreshNeeded())
15575	{
15576		var refreshLink = link.cloneNode();
15577		refreshLink.innerHTML = '<img src="' + IMAGE_PATH + '/update16.png" style="width: 16px; padding: 2px;">';
15578		refreshLink.setAttribute('title', mxResources.get('refresh'));
15579
15580		if (Editor.isDarkMode())
15581		{
15582			refreshLink.style.filter = 'invert(100%)';
15583		}
15584
15585		mxEvent.addListener(refreshLink, 'click', function(evt)
15586		{
15587			refresh();
15588
15589			evt.preventDefault();
15590			mxEvent.consume(evt);
15591		});
15592
15593		ldiv.appendChild(refreshLink);
15594	}
15595
15596	if (editorUi.commentsSaveNeeded())
15597	{
15598		var saveLink = link.cloneNode();
15599		saveLink.innerHTML = '<img src="' + IMAGE_PATH + '/save.png" style="width: 20px; padding: 2px;">';
15600		saveLink.setAttribute('title', mxResources.get('save'));
15601
15602		if (Editor.isDarkMode())
15603		{
15604			saveLink.style.filter = 'invert(100%)';
15605		}
15606
15607		mxEvent.addListener(saveLink, 'click', function(evt)
15608		{
15609			saveCallback();
15610
15611			evt.preventDefault();
15612			mxEvent.consume(evt);
15613		});
15614
15615		ldiv.appendChild(saveLink);
15616	}
15617
15618	div.appendChild(ldiv);
15619
15620	var comments = [];
15621
15622	var refresh = mxUtils.bind(this, function()
15623	{
15624		this.hasError = false;
15625
15626		if (curEdited != null)
15627		{
15628			try
15629			{
15630				curEdited.div = curEdited.div.cloneNode(true);
15631				var commentEditTxt = curEdited.div.querySelector('.geCommentEditTxtArea');
15632				var commentEditBtns = curEdited.div.querySelector('.geCommentEditBtns');
15633
15634				curEdited.comment.content = commentEditTxt.value;
15635				commentEditTxt.parentNode.removeChild(commentEditTxt);
15636				commentEditBtns.parentNode.removeChild(commentEditBtns);
15637			}
15638			catch (e)
15639			{
15640				editorUi.handleError(e);
15641			}
15642		}
15643
15644		listDiv.innerHTML = '<div style="padding-top:10px;text-align:center;"><img src="' + IMAGE_PATH + '/spin.gif" valign="middle"> ' +
15645			mxUtils.htmlEntities(mxResources.get('loading')) + '...</div>';
15646
15647		canReplyToReplies = editorUi.canReplyToReplies();
15648
15649		if (editorUi.commentsSupported())
15650		{
15651			editorUi.getComments(function(list)
15652			{
15653				function sortReplies(replies)
15654				{
15655					if (replies != null)
15656					{
15657						//Sort replies old to new
15658						replies.sort(function(r1, r2)
15659						{
15660							return new Date(r1.modifiedDate) - new Date(r2.modifiedDate);
15661						});
15662
15663						for (var i = 0; i < replies.length; i++)
15664						{
15665							sortReplies(replies[i].replies);
15666						}
15667					}
15668				};
15669
15670				//Sort comments old to new
15671				list.sort(function(c1, c2)
15672				{
15673					return new Date(c1.modifiedDate) - new Date(c2.modifiedDate);
15674				});
15675
15676				listDiv.innerHTML = '';
15677				listDiv.appendChild(noComments);
15678				noComments.style.display = 'block';
15679				comments = list;
15680
15681				for (var i = 0; i < comments.length; i++)
15682				{
15683					sortReplies(comments[i].replies);
15684					addComment(comments[i], comments, null, 0, resolvedChecked);
15685				}
15686
15687				//New comment case
15688				if (curEdited != null && curEdited.comment.id == null && curEdited.comment.pCommentId == null)
15689				{
15690					listDiv.appendChild(curEdited.div);
15691					editComment(curEdited.comment, curEdited.div, curEdited.saveCallback, curEdited.deleteOnCancel);
15692				}
15693
15694			}, mxUtils.bind(this, function(err)
15695			{
15696				listDiv.innerHTML = mxUtils.htmlEntities(mxResources.get('error') + (err && err.message? ': ' + err.message : ''));
15697				this.hasError = true;
15698			}));
15699		}
15700		else
15701		{
15702			//TODO if comments are not supported, close the dialog
15703			listDiv.innerHTML = mxUtils.htmlEntities(mxResources.get('error'));
15704		}
15705	});
15706
15707	refresh();
15708
15709	this.refreshComments = refresh;
15710
15711	//Refresh the modified date of each comment if the window is visible
15712	var refreshCommentsTime = mxUtils.bind(this, function()
15713	{
15714		if (!this.window.isVisible()) return; //only update if it is visible
15715
15716		var modDateDivs = listDiv.querySelectorAll('.geCommentDate');
15717		var modDateDivsMap = {};
15718
15719		for (var i = 0; i < modDateDivs.length; i++)
15720		{
15721			var div = modDateDivs[i];
15722			modDateDivsMap[div.getAttribute('data-commentId')] = div;
15723		}
15724
15725		function processComment(comment)
15726		{
15727			var div = modDateDivsMap[comment.id];
15728
15729			if (div == null) return; //resolved comments
15730
15731			writeCommentDate(comment, div);
15732
15733			for (var i = 0; comment.replies != null && i < comment.replies.length; i++)
15734			{
15735				processComment(comment.replies[i]);
15736			}
15737		};
15738
15739		for (var i = 0; i < comments.length; i++)
15740		{
15741			processComment(comments[i]);
15742		}
15743	});
15744
15745	//Periodically refresh time every one minute
15746	setInterval(refreshCommentsTime, 60000);
15747	this.refreshCommentsTime = refreshCommentsTime;
15748
15749	this.window = new mxWindow(mxResources.get('comments'), div, x, y, w, h, true, true);
15750	this.window.minimumSize = new mxRectangle(0, 0, 300, 200);
15751	this.window.destroyOnClose = false;
15752	this.window.setMaximizable(false);
15753	this.window.setResizable(true);
15754	this.window.setClosable(true);
15755	this.window.setVisible(true);
15756
15757	this.window.addListener(mxEvent.SHOW, mxUtils.bind(this, function()
15758	{
15759		this.window.fit();
15760	}));
15761
15762	this.window.setLocation = function(x, y)
15763	{
15764		var iw = window.innerWidth || document.body.clientWidth || document.documentElement.clientWidth;
15765		var ih = window.innerHeight || document.body.clientHeight || document.documentElement.clientHeight;
15766
15767		x = Math.max(0, Math.min(x, iw - this.table.clientWidth));
15768		y = Math.max(0, Math.min(y, ih - this.table.clientHeight - 48));
15769
15770		if (this.getX() != x || this.getY() != y)
15771		{
15772			mxWindow.prototype.setLocation.apply(this, arguments);
15773		}
15774	};
15775
15776	var resizeListener = mxUtils.bind(this, function()
15777	{
15778		var x = this.window.getX();
15779		var y = this.window.getY();
15780
15781		this.window.setLocation(x, y);
15782	});
15783
15784	mxEvent.addListener(window, 'resize', resizeListener);
15785
15786	this.destroy = function()
15787	{
15788		mxEvent.removeListener(window, 'resize', resizeListener);
15789		this.window.destroy();
15790	}
15791};
15792
15793/**
15794 *
15795 */
15796var ConfirmDialog = function(editorUi, message, okFn, cancelFn, okLabel, cancelLabel,
15797		okImg, cancelImg, showRememberOption, imgSrc, maxHeight)
15798{
15799	var div = document.createElement('div');
15800	div.style.textAlign = 'center';
15801	maxHeight = (maxHeight != null) ? maxHeight : 44;
15802
15803	var p2 = document.createElement('div');
15804	p2.style.padding = '6px';
15805	p2.style.overflow = 'auto';
15806	p2.style.maxHeight = maxHeight + 'px';
15807	p2.style.lineHeight = '1.2em';
15808
15809	mxUtils.write(p2, message);
15810	div.appendChild(p2);
15811
15812	if (imgSrc != null)
15813	{
15814		var p3 = document.createElement('div');
15815		p3.style.padding = '6px 0 6px 0';
15816		var img = document.createElement('img');
15817		img.setAttribute('src', imgSrc);
15818		p3.appendChild(img);
15819		div.appendChild(p3);
15820	}
15821
15822	var btns = document.createElement('div');
15823	btns.style.textAlign = 'center';
15824	btns.style.whiteSpace = 'nowrap';
15825
15826	var cb = document.createElement('input');
15827	cb.setAttribute('type', 'checkbox');
15828
15829	var cancelBtn = mxUtils.button(cancelLabel || mxResources.get('cancel'), function()
15830	{
15831		editorUi.hideDialog();
15832
15833		if (cancelFn != null)
15834		{
15835			cancelFn(cb.checked);
15836		}
15837	});
15838	cancelBtn.className = 'geBtn';
15839
15840	if (cancelImg != null)
15841	{
15842		cancelBtn.innerHTML = cancelImg + '<br>' + cancelBtn.innerHTML;
15843		cancelBtn.style.paddingBottom = '8px';
15844		cancelBtn.style.paddingTop = '8px';
15845		cancelBtn.style.height = 'auto';
15846		cancelBtn.style.width = '40%';
15847	}
15848
15849	if (editorUi.editor.cancelFirst)
15850	{
15851		btns.appendChild(cancelBtn);
15852	}
15853
15854	var okBtn = mxUtils.button(okLabel || mxResources.get('ok'), function()
15855	{
15856		editorUi.hideDialog();
15857
15858		if (okFn != null)
15859		{
15860			okFn(cb.checked);
15861		}
15862	});
15863	btns.appendChild(okBtn);
15864
15865	if (okImg != null)
15866	{
15867		okBtn.innerHTML = okImg + '<br>' + okBtn.innerHTML + '<br>';
15868		okBtn.style.paddingBottom = '8px';
15869		okBtn.style.paddingTop = '8px';
15870		okBtn.style.height = 'auto';
15871		okBtn.className = 'geBtn';
15872		okBtn.style.width = '40%';
15873	}
15874	else
15875	{
15876		okBtn.className = 'geBtn gePrimaryBtn';
15877	}
15878
15879	if (!editorUi.editor.cancelFirst)
15880	{
15881		btns.appendChild(cancelBtn);
15882	}
15883
15884	div.appendChild(btns);
15885
15886	if (showRememberOption)
15887	{
15888		btns.style.marginTop = '10px';
15889		var p2 = document.createElement('p');
15890		p2.style.marginTop = '20px';
15891		p2.style.marginBottom = '0px';
15892		p2.appendChild(cb);
15893		var span = document.createElement('span');
15894		mxUtils.write(span, ' ' + mxResources.get('rememberThisSetting'));
15895		p2.appendChild(span);
15896		div.appendChild(p2);
15897
15898		mxEvent.addListener(span, 'click', function(evt)
15899		{
15900			cb.checked = !cb.checked;
15901			mxEvent.consume(evt);
15902		});
15903	}
15904	else
15905	{
15906		btns.style.marginTop = '12px';
15907	}
15908
15909	this.init = function()
15910	{
15911		okBtn.focus();
15912	};
15913
15914	this.container = div;
15915};
15916