1/**
2 * Copyright (c) 2006-2020, JGraph Ltd
3 * Copyright (c) 2006-2020, draw.io AG
4 */
5
6/**
7 * Constructs a new point for the optional x and y coordinates. If no
8 * coordinates are given, then the default values for <x> and <y> are used.
9 * @constructor
10 * @class Implements a basic 2D point. Known subclassers = {@link mxRectangle}.
11 * @param {number} x X-coordinate of the point.
12 * @param {number} y Y-coordinate of the point.
13 */
14App = function(editor, container, lightbox)
15{
16	EditorUi.call(this, editor, container, (lightbox != null) ? lightbox :
17		(urlParams['lightbox'] == '1' || (uiTheme == 'min' &&
18		urlParams['chrome'] != '0')));
19
20	// Logs unloading of window with modifications for Google Drive file
21	if (!mxClient.IS_CHROMEAPP && !EditorUi.isElectronApp)
22	{
23		window.onunload = mxUtils.bind(this, function()
24		{
25			var file = this.getCurrentFile();
26
27			if (file != null && file.isModified())
28			{
29				var evt = {category: 'DISCARD-FILE-' + file.getHash(),
30					action: ((file.savingFile) ? 'saving' : '') +
31					((file.savingFile && file.savingFileTime != null) ? '_' +
32						Math.round((Date.now() - file.savingFileTime.getTime()) / 1000) : '') +
33					((file.saveLevel != null) ? ('-sl_' + file.saveLevel) : '') +
34					'-age_' + ((file.ageStart != null) ? Math.round((Date.now() - file.ageStart.getTime()) / 1000) : 'x') +
35					((this.editor.autosave) ? '' : '-nosave') +
36					((file.isAutosave()) ? '' : '-noauto') +
37					'-open_' + ((file.opened != null) ? Math.round((Date.now() - file.opened.getTime()) / 1000) : 'x') +
38					'-save_' + ((file.lastSaved != null) ? Math.round((Date.now() - file.lastSaved.getTime()) / 1000) : 'x') +
39					'-change_' + ((file.lastChanged != null) ? Math.round((Date.now() - file.lastChanged.getTime()) / 1000) : 'x') +
40					'-alive_' + Math.round((Date.now() - App.startTime.getTime()) / 1000),
41					label: (file.sync != null) ? ('client_' + file.sync.clientId) : 'nosync'};
42
43				if (file.constructor == DriveFile && file.desc != null && this.drive != null)
44				{
45					evt.label += ((this.drive.user != null) ? ('-user_' + this.drive.user.id) : '-nouser') + '-rev_' +
46						file.desc.headRevisionId + '-mod_' + file.desc.modifiedDate + '-size_' + file.getSize() +
47						'-mime_' + file.desc.mimeType;
48				}
49
50				EditorUi.logEvent(evt);
51			}
52		});
53	}
54
55	// Logs changes to autosave
56	this.editor.addListener('autosaveChanged', mxUtils.bind(this, function()
57	{
58		var file = this.getCurrentFile();
59
60		if (file != null)
61		{
62			EditorUi.logEvent({category: ((this.editor.autosave) ? 'ON' : 'OFF') +
63				'-AUTOSAVE-FILE-' + file.getHash(), action: 'changed',
64				label: 'autosave_' + ((this.editor.autosave) ? 'on' : 'off')});
65		}
66	}));
67
68	// Pre-fetches images
69	if (mxClient.IS_SVG)
70	{
71		mxGraph.prototype.warningImage.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAE7SURBVHjaYvz//z8DJQAggBjwGXDuHMP/tWuD/uPTCxBAOA0AaQRK/f/+XeJ/cbHlf1wGAAQQTgPu3QNLgfHSpZo4DQAIIKwGwGyH4e/fFbG6AiQJEEAs2Ew2NFzH8OOHBMO6dT/A/KCg7wxGRh+wuhQggDBcALMdFIAcHBxgDGJjcwVIIUAAYbhAUXEdVos4OO4DXcGBIQ4QQCguQPY7sgtgAYruCpAgQACx4LJdU1OCwctLEcyWlLwPJF+AXQE0EMUBAAEEdwF6yMOiD4RRY0QT7gqQAEAAseDzu6XldYYPH9DD4joQa8L5AAEENgWb7SBcXa0JDQMBrK4AcQACiAlfyOMCEFdAnAYQQEz4FLa0XGf4/v0H0IIPONUABBAjyBmMjIwMS5cK/L927QORbtBkaG29DtYLEGAAH6f7oq3Zc+kAAAAASUVORK5CYII=';
72	}
73	else
74	{
75		var img = new Image();
76		img.src = mxGraph.prototype.warningImage.src;
77	}
78
79	// Global helper method to deal with popup blockers
80	window.openWindow = mxUtils.bind(this, function(url, pre, fallback)
81	{
82		if (urlParams['openInSameWin'] == '1' || navigator.standalone)
83		{
84			fallback();
85			return;
86		}
87
88		var wnd = null;
89
90		try
91		{
92			wnd = window.open(url);
93		}
94		catch (e)
95		{
96			// ignore
97		}
98
99		if (wnd == null || wnd === undefined)
100		{
101			this.showDialog(new PopupDialog(this, url, pre, fallback).container, 320, 140, true, true);
102		}
103		else if (pre != null)
104		{
105			pre();
106		}
107	});
108
109	// Initial state for toolbar items is disabled
110	this.updateDocumentTitle();
111	this.updateUi();
112
113	// Global helper method to display error messages
114	window.showOpenAlert = mxUtils.bind(this, function(message)
115	{
116		// Cancel must be called before showing error message
117		if (window.openFile != null)
118		{
119			window.openFile.cancel(true);
120		}
121
122		this.handleError(message);
123	});
124
125	// Handles opening files via drag and drop
126	if (!this.editor.chromeless || this.editor.editable)
127	{
128		this.addFileDropHandler([document]);
129	}
130
131	// Process the queue for waiting plugins
132	if (App.DrawPlugins != null)
133	{
134		for (var i = 0; i < App.DrawPlugins.length; i++)
135		{
136			try
137			{
138				App.DrawPlugins[i](this);
139			}
140			catch (e)
141			{
142				if (window.console != null)
143				{
144					console.log('Plugin Error:', e, App.DrawPlugins[i]);
145				}
146			}
147			finally
148			{
149				App.embedModePluginsCount--;
150				this.initializeEmbedMode();
151			}
152		}
153
154		// Installs global callback for plugins
155		window.Draw.loadPlugin = mxUtils.bind(this, function(callback)
156		{
157			try
158			{
159				callback(this);
160			}
161			finally
162			{
163				App.embedModePluginsCount--;
164				this.initializeEmbedMode();
165			}
166		});
167
168		//Set a timeout in case a plugin doesn't load quickly or doesn't load at all
169		setTimeout(mxUtils.bind(this, function()
170		{
171			//Force finish loading if its not yet called
172			if (App.embedModePluginsCount > 0)
173			{
174				App.embedModePluginsCount = 0;
175				this.initializeEmbedMode();
176			}
177		}), 5000); //5 sec timeout
178	}
179
180	this.load();
181};
182
183/**
184 * Timeout error
185 */
186App.ERROR_TIMEOUT = 'timeout';
187
188/**
189 * Busy error
190 */
191App.ERROR_BUSY = 'busy';
192
193/**
194 * Unknown error
195 */
196App.ERROR_UNKNOWN = 'unknown';
197
198/**
199 * Google drive mode
200 */
201App.MODE_GOOGLE = 'google';
202
203/**
204 * Dropbox mode
205 */
206App.MODE_DROPBOX = 'dropbox';
207
208/**
209 * OneDrive Mode
210 */
211App.MODE_ONEDRIVE = 'onedrive';
212
213/**
214 * Github Mode
215 */
216App.MODE_GITHUB = 'github';
217
218/**
219 * Gitlab mode
220 */
221App.MODE_GITLAB = 'gitlab';
222
223/**
224 * Device Mode
225 */
226App.MODE_DEVICE = 'device';
227
228/**
229 * Browser Mode
230 */
231App.MODE_BROWSER = 'browser';
232
233/**
234 * Trello App Mode
235 */
236App.MODE_TRELLO = 'trello';
237
238/**
239 * Notion App Mode
240 */
241App.MODE_NOTION = 'notion';
242
243/**
244 * Embed App Mode
245 */
246App.MODE_EMBED = 'embed';
247
248/**
249 * Atlas App Mode
250 */
251App.MODE_ATLAS = 'atlas';
252
253/**
254 * Sets the delay for autosave in milliseconds. Default is 2000.
255 */
256App.DROPBOX_APPKEY = window.DRAWIO_DROPBOX_ID;
257
258/**
259 * Sets URL to load the Dropbox SDK from
260 */
261App.DROPBOX_URL = window.DRAWIO_BASE_URL + '/js/dropbox/Dropbox-sdk.min.js';
262
263/**
264 * Sets URL to load the Dropbox dropins JS from.
265 */
266App.DROPINS_URL = 'https://www.dropbox.com/static/api/2/dropins.js';
267
268/**
269 * OneDrive Client JS (file/folder picker). This is a slightly modified version to allow using accessTokens
270 * But it doesn't work for IE11, so we fallback to the original one
271 */
272App.ONEDRIVE_URL = mxClient.IS_IE11? 'https://js.live.net/v7.2/OneDrive.js' : window.DRAWIO_BASE_URL + '/js/onedrive/OneDrive.js';
273
274/**
275 * Trello URL
276 */
277App.TRELLO_URL = 'https://api.trello.com/1/client.js';
278
279/**
280 * Trello JQuery dependency
281 */
282App.TRELLO_JQUERY_URL = window.DRAWIO_BASE_URL + '/js/jquery/jquery-3.3.1.min.js';
283
284/**
285 * Specifies the key for the pusher project.
286 */
287App.PUSHER_KEY = '1e756b07a690c5bdb054';
288
289/**
290 * Specifies the key for the pusher project.
291 */
292App.PUSHER_CLUSTER = 'eu';
293
294/**
295 * Specifies the URL for the pusher API.
296 */
297App.PUSHER_URL = 'https://js.pusher.com/4.3/pusher.min.js';
298
299/**
300 * Socket.io library
301 */
302App.SOCKET_IO_URL = window.DRAWIO_BASE_URL + '/js/socket.io/socket.io.min.js';
303App.SIMPLE_PEER_URL = window.DRAWIO_BASE_URL + '/js/socket.io/simplepeer9.10.0.min.js';
304App.SOCKET_IO_SRV = 'http://localhost:3030';
305
306/**
307 * Google APIs to load. The realtime API is needed to notify collaborators of conversion
308 * of the realtime files, but after Dec 11 it's read-only and hence no longer needed.
309 */
310App.GOOGLE_APIS = 'drive-share';
311
312/**
313 * Function: authorize
314 *
315 * Authorizes the client, gets the userId and calls <open>.
316 */
317App.startTime = new Date();
318
319/**
320 * Defines plugin IDs for loading via p URL parameter. Update the table at
321 * https://www.diagrams.net/doc/faq/supported-url-parameters
322 */
323App.pluginRegistry = {'4xAKTrabTpTzahoLthkwPNUn': 'plugins/explore.js',
324	'ex': 'plugins/explore.js', 'p1': 'plugins/p1.js',
325	'ac': 'plugins/connect.js', 'acj': 'plugins/connectJira.js',
326	'ac148': 'plugins/cConf-1-4-8.js', 'ac148cmnt': 'plugins/cConf-comments.js',
327	'voice': 'plugins/voice.js',
328	'tips': 'plugins/tooltips.js', 'svgdata': 'plugins/svgdata.js',
329	'electron': 'plugins/electron.js',
330	'number': 'plugins/number.js', 'sql': 'plugins/sql.js',
331	'props': 'plugins/props.js', 'text': 'plugins/text.js',
332	'anim': 'plugins/animation.js', 'update': 'plugins/update.js',
333	'trees': 'plugins/trees/trees.js', 'import': 'plugins/import.js',
334	'replay': 'plugins/replay.js', 'anon': 'plugins/anonymize.js',
335	'tr': 'plugins/trello.js', 'f5': 'plugins/rackF5.js',
336	'tickets': 'plugins/tickets.js', 'flow': 'plugins/flow.js',
337	'webcola': 'plugins/webcola/webcola.js', 'rnd': 'plugins/random.js',
338	'page': 'plugins/page.js', 'gd': 'plugins/googledrive.js',
339	'tags': 'plugins/tags.js'};
340
341App.publicPlugin = [
342	'ex',
343	'voice',
344	'tips',
345	'svgdata',
346	'number',
347	'sql',
348	'props',
349	'text',
350	'anim',
351	'update',
352	'trees',
353//	'import',
354	'replay',
355	'anon',
356	'tickets',
357	'flow',
358	'webcola',
359//	'rnd', 'page', 'gd',
360	'tags'
361];
362
363/**
364 * Loads all given scripts and invokes onload after
365 * all scripts have finished loading.
366 */
367App.loadScripts = function(scripts, onload)
368{
369	var n = scripts.length;
370
371	for (var i = 0; i < scripts.length; i++)
372	{
373		mxscript(scripts[i], function()
374		{
375			if (--n == 0 && onload != null)
376			{
377				onload();
378			}
379		});
380	}
381};
382
383/**
384 * Function: getStoredMode
385 *
386 * Returns the current mode.
387 */
388App.getStoredMode = function()
389{
390	var mode = null;
391
392	if (mode == null && isLocalStorage)
393	{
394		mode = localStorage.getItem('.mode');
395	}
396
397	if (mode == null && typeof(Storage) != 'undefined')
398	{
399		var cookies = document.cookie.split(";");
400
401		for (var i = 0; i < cookies.length; i++)
402		{
403			// Removes spaces around cookie
404			var cookie = mxUtils.trim(cookies[i]);
405
406			if (cookie.substring(0, 5) == 'MODE=')
407			{
408				mode = cookie.substring(5);
409				break;
410			}
411		}
412
413		if (mode != null && isLocalStorage)
414		{
415			// Moves to local storage
416			var expiry = new Date();
417			expiry.setYear(expiry.getFullYear() - 1);
418			document.cookie = 'MODE=; expires=' + expiry.toUTCString();
419			localStorage.setItem('.mode', mode);
420		}
421	}
422
423	return mode;
424};
425
426/**
427 * Static Application initializer executed at load-time.
428 */
429(function()
430{
431	if (!mxClient.IS_CHROMEAPP)
432	{
433		if (urlParams['offline'] != '1')
434		{
435			// Switches to dropbox mode for db.draw.io
436			if (window.location.hostname == 'db.draw.io' && urlParams['mode'] == null)
437			{
438				urlParams['mode'] = 'dropbox';
439			}
440
441			App.mode = urlParams['mode'];
442		}
443
444		if (App.mode == null)
445		{
446			// Stored mode overrides preferred mode
447			App.mode = App.getStoredMode();
448		}
449
450		/**
451		 * Lazy loading backends.
452		 */
453		if (window.mxscript != null)
454		{
455			// Loads gapi for all browsers but IE8 and below if not disabled or if enabled and in embed mode
456			if (urlParams['embed'] != '1')
457			{
458				if (typeof window.DriveClient === 'function')
459				{
460					if (urlParams['gapi'] != '0' && isSvgBrowser &&
461						(document.documentMode == null || document.documentMode >= 10))
462					{
463						// Immediately loads client
464						if (App.mode == App.MODE_GOOGLE || (urlParams['state'] != null &&
465							window.location.hash == '') || (window.location.hash != null &&
466							window.location.hash.substring(0, 2) == '#G'))
467						{
468							mxscript('https://apis.google.com/js/api.js');
469						}
470						// Keeps lazy loading for fallback to authenticated Google file if not public in loadFile
471						else if (urlParams['chrome'] == '0' && (window.location.hash == null ||
472							window.location.hash.substring(0, 45) !== '#Uhttps%3A%2F%2Fdrive.google.com%2Fuc%3Fid%3D'))
473						{
474							// Disables loading of client
475							window.DriveClient = null;
476						}
477					}
478					else
479					{
480						// Disables loading of client
481						window.DriveClient = null;
482					}
483				}
484
485				// Loads dropbox for all browsers but IE8 and below (no CORS) if not disabled or if enabled and in embed mode
486				// KNOWN: Picker does not work in IE11 (https://dropbox.zendesk.com/requests/1650781)
487				if (typeof window.DropboxClient === 'function')
488				{
489					if (urlParams['db'] != '0' && isSvgBrowser &&
490						(document.documentMode == null || document.documentMode > 9))
491					{
492						// Immediately loads client
493						if (App.mode == App.MODE_DROPBOX || (window.location.hash != null &&
494							window.location.hash.substring(0, 2) == '#D'))
495						{
496							mxscript(App.DROPBOX_URL, function()
497							{
498								// Must load this after the dropbox SDK since they use the same namespace
499								mxscript(App.DROPINS_URL, null, 'dropboxjs', App.DROPBOX_APPKEY, true);
500							});
501						}
502						else if (urlParams['chrome'] == '0')
503						{
504							window.DropboxClient = null;
505						}
506					}
507					else
508					{
509						// Disables loading of client
510						window.DropboxClient = null;
511					}
512				}
513
514				// Loads OneDrive for all browsers but IE6/IOS if not disabled or if enabled and in embed mode
515				if (typeof window.OneDriveClient === 'function')
516				{
517					if (urlParams['od'] != '0' && (navigator.userAgent == null ||
518						navigator.userAgent.indexOf('MSIE') < 0 || document.documentMode >= 10))
519					{
520						// Immediately loads client
521						if (App.mode == App.MODE_ONEDRIVE || (window.location.hash != null &&
522							window.location.hash.substring(0, 2) == '#W'))
523						{
524							//Editor.oneDriveInlinePicker can be set with configuration which is done later, so load it all time
525							mxscript(App.ONEDRIVE_URL);
526						}
527						else if (urlParams['chrome'] == '0')
528						{
529							window.OneDriveClient = null;
530						}
531					}
532					else
533					{
534						// Disables loading of client
535						window.OneDriveClient = null;
536					}
537				}
538
539				// Loads Trello for all browsers but < IE10 if not disabled or if enabled and in embed mode
540				if (typeof window.TrelloClient === 'function')
541				{
542					if (urlParams['tr'] == '1' && isSvgBrowser && !mxClient.IS_IE11 &&
543						(document.documentMode == null || document.documentMode >= 10))
544					{
545						// Immediately loads client
546						if (App.mode == App.MODE_TRELLO || (window.location.hash != null &&
547							window.location.hash.substring(0, 2) == '#T'))
548						{
549							mxscript(App.TRELLO_JQUERY_URL, function()
550							{
551								mxscript(App.TRELLO_URL);
552							});
553						}
554						else if (urlParams['chrome'] == '0')
555						{
556							window.TrelloClient = null;
557						}
558					}
559					else
560					{
561						// Disables loading of client
562						window.TrelloClient = null;
563					}
564				}
565			}
566		}
567	}
568})();
569
570/**
571 * Clears the PWA cache.
572 */
573App.clearServiceWorker = function(success)
574{
575	navigator.serviceWorker.getRegistrations().then(function(registrations)
576	{
577		if (registrations != null && registrations.length > 0)
578		{
579			for (var i = 0; i < registrations.length; i++)
580			{
581				registrations[i].unregister();
582			}
583
584			if (success != null)
585			{
586				success();
587			}
588		}
589	});
590};
591
592/**
593 * Program flow starts here.
594 *
595 * Optional callback is called with the app instance.
596 */
597App.main = function(callback, createUi)
598{
599	// Logs uncaught errors
600	window.onerror = function(message, url, linenumber, colno, err)
601	{
602		EditorUi.logError('Global: ' + ((message != null) ? message : ''),
603			url, linenumber, colno, err, null, true);
604	};
605
606	// Blocks stand-alone mode for certain subdomains
607	if (window.top == window.self &&
608		(/ac\.draw\.io$/.test(window.location.hostname) ||
609		/ac-ent\.draw\.io$/.test(window.location.hostname) ||
610		/aj\.draw\.io$/.test(window.location.hostname)))
611	{
612		document.body.innerHTML = '<div style="margin-top:10%;text-align:center;">Stand-alone mode not allowed for this domain.</div>';
613
614		return;
615	}
616
617	// Removes info text in embed mode
618	if (urlParams['embed'] == '1' || urlParams['lightbox'] == '1')
619	{
620		var geInfo = document.getElementById('geInfo');
621
622		if (geInfo != null)
623		{
624			geInfo.parentNode.removeChild(geInfo);
625		}
626	}
627
628	// Redirects to the latest AWS icons
629	if (document.referrer != null && urlParams['libs'] == 'aws3' &&
630		document.referrer.substring(0, 42) == 'https://aws.amazon.com/architecture/icons/')
631	{
632		urlParams['libs'] = 'aws4';
633	}
634
635	if (window.mxscript != null)
636	{
637		// Checks for script content changes to avoid CSP errors in production
638		if (urlParams['dev'] == '1' && CryptoJS != null && App.mode != App.MODE_DROPBOX && App.mode != App.MODE_TRELLO)
639		{
640			var scripts = document.getElementsByTagName('script');
641
642			// Checks bootstrap script
643			if (scripts != null && scripts.length > 0)
644			{
645				var content = mxUtils.getTextContent(scripts[0]);
646
647				if (CryptoJS.MD5(content).toString() != 'b02227617087e21bd49f2faa15164112')
648				{
649					console.log('Change bootstrap script MD5 in the previous line:', CryptoJS.MD5(content).toString());
650					alert('[Dev] Bootstrap script change requires update of CSP');
651				}
652			}
653
654			// Checks main script
655			if (scripts != null && scripts.length > 1)
656			{
657				var content = mxUtils.getTextContent(scripts[scripts.length - 1]);
658
659				if (CryptoJS.MD5(content).toString() != 'd53805dd6f0bbba2da4966491ca0a505')
660				{
661					console.log('Change main script MD5 in the previous line:', CryptoJS.MD5(content).toString());
662					alert('[Dev] Main script change requires update of CSP');
663				}
664			}
665		}
666
667		try
668		{
669			// Removes PWA cache on www.draw.io to force use of new domain via redirect
670			if (Editor.enableServiceWorker && (urlParams['offline'] == '0' ||
671				/www\.draw\.io$/.test(window.location.hostname) ||
672				(urlParams['offline'] != '1' && urlParams['dev'] == '1')))
673			{
674				App.clearServiceWorker(function()
675				{
676					if (urlParams['offline'] == '0')
677					{
678						alert('Cache cleared');
679					}
680				});
681			}
682			else if (Editor.enableServiceWorker)
683			{
684				// Runs as progressive web app if service workers are supported
685				navigator.serviceWorker.register('/service-worker.js');
686			}
687		}
688		catch (e)
689		{
690			if (window.console != null)
691			{
692				console.error(e);
693			}
694		}
695
696		// Loads Pusher API
697		if (('ArrayBuffer' in window) && !mxClient.IS_CHROMEAPP && !EditorUi.isElectronApp &&
698			DrawioFile.SYNC == 'auto' && (urlParams['embed'] != '1' || urlParams['embedRT'] == '1') && urlParams['local'] != '1' &&
699			(urlParams['chrome'] != '0' || urlParams['rt'] == '1') &&
700			urlParams['stealth'] != '1' && urlParams['offline'] != '1')
701		{
702			// TODO: Check if async loading is fast enough
703			mxscript(App.PUSHER_URL);
704
705			if (urlParams['rtCursors'] == '1')
706			{
707				mxscript(App.SOCKET_IO_URL);
708				mxscript(App.SIMPLE_PEER_URL);
709			}
710		}
711
712		// Loads plugins
713		if (urlParams['plugins'] != '0' && urlParams['offline'] != '1')
714		{
715			// mxSettings is not yet initialized in configure mode, redirect parameter
716			// to p URL parameter in caller for plugins in embed mode
717			var plugins = (mxSettings.settings != null) ? mxSettings.getPlugins() : null;
718
719			// Configured plugins in embed mode with configure=1 URL should be loaded so we
720			// look ahead here and parse the config to fetch the list of custom plugins
721			if (mxSettings.settings == null && isLocalStorage && typeof(JSON) !== 'undefined')
722			{
723				try
724				{
725					var temp = JSON.parse(localStorage.getItem(mxSettings.key));
726
727					if (temp != null)
728					{
729						plugins = temp.plugins;
730					}
731				}
732				catch (e)
733				{
734					// ignore
735				}
736			}
737
738			var temp = urlParams['p'];
739			App.initPluginCallback();
740
741			if (temp != null)
742			{
743				// Mapping from key to URL in App.plugins
744				App.loadPlugins(temp.split(';'));
745			}
746
747			if (plugins != null && plugins.length > 0 && urlParams['plugins'] != '0')
748			{
749				// Loading plugins inside the asynchronous block below stops the page from loading so a
750				// hardcoded message for the warning dialog is used since the resources are loadd below
751				var warning = 'The page has requested to load the following plugin(s):\n \n {1}\n \n Would you like to load these plugin(s) now?\n \n NOTE : Only allow plugins to run if you fully understand the security implications of doing so.\n';
752				var tmp = window.location.protocol + '//' + window.location.host;
753				var local = true;
754
755				for (var i = 0; i < plugins.length && local; i++)
756				{
757					if (plugins[i].charAt(0) != '/' && plugins[i].substring(0, tmp.length) != tmp)
758					{
759						local = false;
760					}
761				}
762
763				if (local || mxUtils.confirm(mxResources.replacePlaceholders(warning, [plugins.join('\n')]).replace(/\\n/g, '\n')))
764				{
765					for (var i = 0; i < plugins.length; i++)
766					{
767						try
768						{
769							if (App.pluginsLoaded[plugins[i]] == null)
770							{
771								App.pluginsLoaded[plugins[i]] = true;
772								App.embedModePluginsCount++;
773
774								if (plugins[i].charAt(0) == '/')
775								{
776									plugins[i] = PLUGINS_BASE_PATH + plugins[i];
777								}
778
779								mxscript(plugins[i]);
780							}
781						}
782						catch (e)
783						{
784							// ignore
785						}
786					}
787				}
788			}
789		}
790
791		// Loads gapi for all browsers but IE8 and below if not disabled or if enabled and in embed mode
792		// Special case: Cannot load in asynchronous code below
793		if (typeof window.DriveClient === 'function' &&
794			(typeof gapi === 'undefined' && (((urlParams['embed'] != '1' && urlParams['gapi'] != '0') ||
795			(urlParams['embed'] == '1' && urlParams['gapi'] == '1')) && isSvgBrowser &&
796			isLocalStorage && (document.documentMode == null || document.documentMode >= 10))))
797		{
798			mxscript('https://apis.google.com/js/api.js?onload=DrawGapiClientCallback', null, null, null, mxClient.IS_SVG);
799		}
800		// Disables client
801		else if (typeof window.gapi === 'undefined')
802		{
803			window.DriveClient = null;
804		}
805	}
806
807	/**
808	 * Asynchronous MathJax extension.
809	 */
810	if (urlParams['math'] != '0')
811	{
812		Editor.initMath();
813	}
814
815	function doLoad(bundle)
816	{
817		// Prefetches asynchronous requests so that below code runs synchronous
818		// Loading the correct bundle (one file) via the fallback system in mxResources. The stylesheet
819		// is compiled into JS in the build process and is only needed for local development.
820		mxUtils.getAll((urlParams['dev'] != '1') ? [bundle] : [bundle,
821			STYLE_PATH + '/default.xml'], function(xhr)
822		{
823			// Adds bundle text to resources
824			mxResources.parse(xhr[0].getText());
825
826			// Configuration mode
827			if (isLocalStorage && localStorage != null && window.location.hash != null &&
828				window.location.hash.substring(0, 9) == '#_CONFIG_')
829			{
830				try
831				{
832					var trustedPlugins = {};
833
834					for (var key in App.pluginRegistry)
835					{
836						trustedPlugins[App.pluginRegistry[key]] = true;
837					}
838
839					// Only allows trusted plugins
840					function checkPlugins(plugins)
841					{
842						if (plugins != null)
843						{
844							for (var i = 0; i < plugins.length; i++)
845							{
846								if (!trustedPlugins[plugins[i]])
847								{
848									throw new Error(mxResources.get('invalidInput') + ' "' + plugins[i]) + '"';
849								}
850							}
851						}
852
853						return true;
854					};
855
856					var value = JSON.parse(Graph.decompress(window.location.hash.substring(9)));
857
858					if (value != null && checkPlugins(value.plugins))
859					{
860						EditorUi.debug('Setting configuration', JSON.stringify(value));
861
862						if (value.merge != null)
863						{
864							var temp = localStorage.getItem(Editor.configurationKey);
865
866							if (temp != null)
867							{
868
869								try
870								{
871									var config = JSON.parse(temp);
872
873									for (var key in value.merge)
874									{
875										config[key] = value.merge[key];
876									}
877
878									value = config;
879								}
880								catch (e)
881								{
882									window.location.hash = '';
883									alert(e);
884								}
885							}
886							else
887							{
888								value = value.merge;
889							}
890						}
891
892						if (confirm(mxResources.get('configLinkWarn')) &&
893							confirm(mxResources.get('configLinkConfirm')))
894						{
895							localStorage.setItem(Editor.configurationKey, JSON.stringify(value));
896							window.location.hash = '';
897							window.location.reload();
898						}
899					}
900
901					window.location.hash = '';
902				}
903				catch (e)
904				{
905					window.location.hash = '';
906					alert(e);
907				}
908			}
909
910			// Prepares themes with mapping from old default-style to old XML file
911			if (xhr.length > 1)
912			{
913				Graph.prototype.defaultThemes['default-style2'] = xhr[1].getDocumentElement();
914	 			Graph.prototype.defaultThemes['darkTheme'] = xhr[1].getDocumentElement();
915			}
916
917			// Main
918			function realMain()
919			{
920				var ui = (createUi != null) ? createUi() : new App(new Editor(
921						urlParams['chrome'] == '0' || uiTheme == 'min',
922						null, null, null, urlParams['chrome'] != '0'));
923
924				if (window.mxscript != null)
925				{
926					// Loads dropbox for all browsers but IE8 and below (no CORS) if not disabled or if enabled and in embed mode
927					// KNOWN: Picker does not work in IE11 (https://dropbox.zendesk.com/requests/1650781)
928					if (typeof window.DropboxClient === 'function' &&
929						(window.Dropbox == null && window.DrawDropboxClientCallback != null &&
930						(((urlParams['embed'] != '1' && urlParams['db'] != '0') ||
931						(urlParams['embed'] == '1' && urlParams['db'] == '1')) &&
932						isSvgBrowser && (document.documentMode == null || document.documentMode > 9))))
933					{
934						mxscript(App.DROPBOX_URL, function()
935						{
936							// Must load this after the dropbox SDK since they use the same namespace
937							mxscript(App.DROPINS_URL, function()
938							{
939								DrawDropboxClientCallback();
940							}, 'dropboxjs', App.DROPBOX_APPKEY);
941						});
942					}
943					// Disables client
944					else if (typeof window.Dropbox === 'undefined' || typeof window.Dropbox.choose === 'undefined')
945					{
946						window.DropboxClient = null;
947					}
948
949					// Loads OneDrive for all browsers but IE6/IOS if not disabled or if enabled and in embed mode
950					if (typeof window.OneDriveClient === 'function' &&
951						(typeof OneDrive === 'undefined' && window.DrawOneDriveClientCallback != null &&
952						(((urlParams['embed'] != '1' && urlParams['od'] != '0') || (urlParams['embed'] == '1' &&
953						urlParams['od'] == '1')) && (navigator.userAgent == null ||
954						navigator.userAgent.indexOf('MSIE') < 0 || document.documentMode >= 10))))
955					{
956						//Editor.oneDriveInlinePicker can be set with configuration which is done later, so load it all time
957						mxscript(App.ONEDRIVE_URL, window.DrawOneDriveClientCallback);
958					}
959					// Disables client
960					else if (typeof window.OneDrive === 'undefined')
961					{
962						window.OneDriveClient = null;
963					}
964
965					// Loads Trello for all browsers but < IE10 if not disabled or if enabled and in embed mode
966					if (typeof window.TrelloClient === 'function' && !mxClient.IS_IE11 &&
967						typeof window.Trello === 'undefined' && window.DrawTrelloClientCallback != null &&
968						urlParams['tr'] == '1' && (navigator.userAgent == null ||
969						navigator.userAgent.indexOf('MSIE') < 0 || document.documentMode >= 10))
970					{
971						mxscript(App.TRELLO_JQUERY_URL, function()
972						{
973							// Must load this after the dropbox SDK since they use the same namespace
974							mxscript(App.TRELLO_URL, function()
975							{
976								DrawTrelloClientCallback();
977							});
978						});
979					}
980					// Disables client
981					else if (typeof window.Trello === 'undefined')
982					{
983						window.TrelloClient = null;
984					}
985
986				}
987
988				if (callback != null)
989				{
990					callback(ui);
991				}
992
993				/**
994				 * For developers only
995				 */
996				if (urlParams['chrome'] != '0' && urlParams['test'] == '1')
997				{
998					EditorUi.debug('App.start', [ui, (new Date().getTime() - t0.getTime()) + 'ms']);
999
1000					if (urlParams['export'] != null)
1001					{
1002						EditorUi.debug('Export:', EXPORT_URL);
1003					}
1004				}
1005			};
1006
1007			if (urlParams['dev'] == '1' || EditorUi.isElectronApp) //TODO check if we can remove these scripts loading from index.html
1008			{
1009				realMain();
1010			}
1011			else
1012			{
1013				mxStencilRegistry.allowEval = false;
1014				App.loadScripts(['js/shapes-14-6-5.min.js', 'js/stencils.min.js',
1015					'js/extensions.min.js'], realMain);
1016			}
1017		}, function(xhr)
1018		{
1019			var st = document.getElementById('geStatus');
1020
1021			if (st != null)
1022			{
1023				st.innerHTML = 'Error loading page. <a>Please try refreshing.</a>';
1024
1025				// Tries reload with default resources in case any language resources were not available
1026				st.getElementsByTagName('a')[0].onclick = function()
1027				{
1028					mxLanguage = 'en';
1029					doLoad(mxResources.getDefaultBundle(RESOURCE_BASE, mxLanguage) ||
1030							mxResources.getSpecialBundle(RESOURCE_BASE, mxLanguage));
1031				};
1032			}
1033		});
1034	};
1035
1036	function doMain()
1037	{
1038		// Optional override for autosaveDelay and defaultEdgeLength
1039		try
1040		{
1041			if (mxSettings.settings != null)
1042			{
1043				document.body.style.backgroundColor = (uiTheme == 'dark' ||
1044					mxSettings.settings.darkMode) ? Editor.darkColor : '#ffffff';
1045
1046				if (mxSettings.settings.autosaveDelay != null)
1047				{
1048					var val = parseInt(mxSettings.settings.autosaveDelay);
1049
1050					if (!isNaN(val) && val > 0)
1051					{
1052						DrawioFile.prototype.autosaveDelay = val;
1053						EditorUi.debug('Setting autosaveDelay', val);
1054					}
1055					else
1056					{
1057						EditorUi.debug('Invalid autosaveDelay', val);
1058					}
1059				}
1060
1061				if (mxSettings.settings.defaultEdgeLength != null)
1062				{
1063					var val = parseInt(mxSettings.settings.defaultEdgeLength);
1064
1065					if (!isNaN(val) && val > 0)
1066					{
1067						Graph.prototype.defaultEdgeLength = val;
1068						EditorUi.debug('Using defaultEdgeLength', val);
1069					}
1070					else
1071					{
1072						EditorUi.debug('Invalid defaultEdgeLength', val);
1073					}
1074				}
1075			}
1076		}
1077		catch (e)
1078		{
1079			if (window.console != null)
1080			{
1081				console.error(e);
1082			}
1083		}
1084
1085		// Prefetches default fonts with URLs
1086		if (Menus.prototype.defaultFonts != null)
1087		{
1088			for (var i = 0; i < Menus.prototype.defaultFonts.length; i++)
1089			{
1090				var value = Menus.prototype.defaultFonts[i];
1091
1092				if (typeof value !== 'string' &&
1093					value.fontFamily != null &&
1094					value.fontUrl != null)
1095				{
1096					Graph.addFont(value.fontFamily, value.fontUrl);
1097				}
1098			}
1099		}
1100
1101		// Adds required resources (disables loading of fallback properties, this can only
1102		// be used if we know that all keys are defined in the language specific file)
1103		mxResources.loadDefaultBundle = false;
1104		doLoad(mxResources.getDefaultBundle(RESOURCE_BASE, mxLanguage) ||
1105			mxResources.getSpecialBundle(RESOURCE_BASE, mxLanguage));
1106	};
1107
1108	// Sends load event if configuration is requested and waits for configure message
1109	if (urlParams['configure'] == '1')
1110	{
1111		var op = window.opener || window.parent;
1112
1113		var configHandler = function(evt)
1114		{
1115			if (evt.source == op)
1116			{
1117				try
1118				{
1119					var data = JSON.parse(evt.data);
1120
1121					if (data != null && data.action == 'configure')
1122					{
1123						mxEvent.removeListener(window, 'message', configHandler);
1124						Editor.configure(data.config, true);
1125						mxSettings.load();
1126						doMain();
1127					}
1128				}
1129				catch (e)
1130				{
1131					if (window.console != null)
1132					{
1133						console.log('Error in configure message: ' + e, evt.data);
1134					}
1135				}
1136			}
1137		};
1138
1139		// Receives XML message from opener and puts it into the graph
1140		mxEvent.addListener(window, 'message', configHandler);
1141		op.postMessage(JSON.stringify({event: 'configure'}), '*');
1142	}
1143	else
1144	{
1145		if (Editor.config == null)
1146		{
1147			// Loads configuration from global scope or local storage
1148			if (window.DRAWIO_CONFIG != null)
1149			{
1150				try
1151				{
1152					EditorUi.debug('Using global configuration', window.DRAWIO_CONFIG);
1153					Editor.configure(window.DRAWIO_CONFIG);
1154					mxSettings.load();
1155				}
1156				catch (e)
1157				{
1158					if (window.console != null)
1159					{
1160						console.error(e);
1161					}
1162				}
1163			}
1164
1165			// Loads configuration from local storage
1166			if (isLocalStorage && localStorage != null && urlParams['embed'] != '1')
1167			{
1168				var configData = localStorage.getItem(Editor.configurationKey);
1169
1170				if (configData != null)
1171				{
1172					try
1173					{
1174						configData = JSON.parse(configData);
1175
1176						if (configData != null)
1177						{
1178							EditorUi.debug('Using local configuration', configData);
1179							Editor.configure(configData);
1180							mxSettings.load();
1181						}
1182					}
1183					catch (e)
1184					{
1185						if (window.console != null)
1186						{
1187							console.error(e);
1188						}
1189					}
1190				}
1191			}
1192		}
1193
1194		doMain();
1195	}
1196};
1197
1198//Extends EditorUi
1199mxUtils.extend(App, EditorUi);
1200
1201/**
1202 * Executes the first step for connecting to Google Drive.
1203 */
1204App.prototype.defaultUserPicture = IMAGE_PATH + '/default-user.jpg';
1205
1206/**
1207 *
1208 */
1209App.prototype.shareImage = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA2RpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYwIDYxLjEzNDc3NywgMjAxMC8wMi8xMi0xNzozMjowMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDowOTgwMTE3NDA3MjA2ODExODhDNkFGMDBEQkQ0RTgwOSIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDoxMjU2NzdEMTcwRDIxMUUxQjc0MDkxRDhCNUQzOEFGRCIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDoxMjU2NzdEMDcwRDIxMUUxQjc0MDkxRDhCNUQzOEFGRCIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ1M1IFdpbmRvd3MiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDowNjgwMTE3NDA3MjA2ODExODcxRkM4MUY1OTFDMjQ5OCIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDowNzgwMTE3NDA3MjA2ODExODhDNkFGMDBEQkQ0RTgwOSIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PrM/fs0AAADgSURBVHjaYmDAA/7//88MwgzkAKDGFiD+BsQ/QWxSNaf9RwN37twpI8WAS+gGfP78+RpQSoRYA36iG/D379+vQClNdLVMOMz4gi7w79+/n0CKg1gD9qELvH379hzIHGK9oA508ieY8//8+fO5rq4uFCilRKwL1JmYmNhhHEZGRiZ+fn6Q2meEbDYG4u3/cYCfP38uA7kOm0ZOIJ7zn0jw48ePPiDFhmzArv8kgi9fvuwB+w5qwH9ykjswbFSZyM4sEMDPBDTlL5BxkFSd7969OwZ2BZKYGhDzkmjOJ4AAAwBhpRqGnEFb8QAAAABJRU5ErkJggg==';
1210
1211/**
1212 *
1213 */
1214App.prototype.chevronUpImage = (!mxClient.IS_SVG) ? IMAGE_PATH + '/chevron-up.png' : 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYwIDYxLjEzNDc3NywgMjAxMC8wMi8xMi0xNzozMjowMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNSBNYWNpbnRvc2giIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6NDg2NEE3NUY1MUVBMTFFM0I3MUVEMTc0N0YyOUI4QzEiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6NDg2NEE3NjA1MUVBMTFFM0I3MUVEMTc0N0YyOUI4QzEiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo0ODY0QTc1RDUxRUExMUUzQjcxRUQxNzQ3RjI5QjhDMSIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo0ODY0QTc1RTUxRUExMUUzQjcxRUQxNzQ3RjI5QjhDMSIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/Pg+qUokAAAAMUExURQAAANnZ2b+/v////5bgre4AAAAEdFJOU////wBAKqn0AAAAL0lEQVR42mJgRgMMRAswMKAKMDDARBjg8lARBoR6KImkH0wTbygT6YaS4DmAAAMAYPkClOEDDD0AAAAASUVORK5CYII=';
1215
1216/**
1217 *
1218 */
1219App.prototype.chevronDownImage = (!mxClient.IS_SVG) ? IMAGE_PATH + '/chevron-down.png' : 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYwIDYxLjEzNDc3NywgMjAxMC8wMi8xMi0xNzozMjowMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNSBNYWNpbnRvc2giIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6NDg2NEE3NUI1MUVBMTFFM0I3MUVEMTc0N0YyOUI4QzEiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6NDg2NEE3NUM1MUVBMTFFM0I3MUVEMTc0N0YyOUI4QzEiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo0ODY0QTc1OTUxRUExMUUzQjcxRUQxNzQ3RjI5QjhDMSIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo0ODY0QTc1QTUxRUExMUUzQjcxRUQxNzQ3RjI5QjhDMSIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PsCtve8AAAAMUExURQAAANnZ2b+/v////5bgre4AAAAEdFJOU////wBAKqn0AAAALUlEQVR42mJgRgMMRAkwQEXBNAOcBSPhclB1cNVwfcxI+vEZykSpoSR6DiDAAF23ApT99bZ+AAAAAElFTkSuQmCC';
1220
1221/**
1222 *
1223 */
1224App.prototype.formatShowImage = (!mxClient.IS_SVG) ? IMAGE_PATH + '/format-show.png' : 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYwIDYxLjEzNDc3NywgMjAxMC8wMi8xMi0xNzozMjowMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNSBNYWNpbnRvc2giIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6ODdCREY5REY1NkQ3MTFFNTkyNjNEMTA5NjgwODUyRTgiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6ODdCREY5RTA1NkQ3MTFFNTkyNjNEMTA5NjgwODUyRTgiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo4N0JERjlERDU2RDcxMUU1OTI2M0QxMDk2ODA4NTJFOCIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo4N0JERjlERTU2RDcxMUU1OTI2M0QxMDk2ODA4NTJFOCIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PlnMQ/8AAAAJUExURQAAAP///3FxcTfTiAsAAAACdFJOU/8A5bcwSgAAACFJREFUeNpiYEQDDEQJMMABTAAixcQ00ALoDiPRcwABBgB6DADly9Yx8wAAAABJRU5ErkJggg==';
1225
1226/**
1227 *
1228 */
1229App.prototype.formatHideImage = (!mxClient.IS_SVG) ? IMAGE_PATH + '/format-hide.png' : 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYwIDYxLjEzNDc3NywgMjAxMC8wMi8xMi0xNzozMjowMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNSBNYWNpbnRvc2giIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6ODdCREY5REI1NkQ3MTFFNTkyNjNEMTA5NjgwODUyRTgiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6ODdCREY5REM1NkQ3MTFFNTkyNjNEMTA5NjgwODUyRTgiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo4N0JERjlEOTU2RDcxMUU1OTI2M0QxMDk2ODA4NTJFOCIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo4N0JERjlEQTU2RDcxMUU1OTI2M0QxMDk2ODA4NTJFOCIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PqjT9SMAAAAGUExURQAAAP///6XZn90AAAACdFJOU/8A5bcwSgAAAB9JREFUeNpiYEQDDEQJMMABTAAmNdAC6A4j0XMAAQYAcbwA1Xvj1CgAAAAASUVORK5CYII=';
1230
1231/**
1232 *
1233 */
1234App.prototype.fullscreenImage = (!mxClient.IS_SVG) ? IMAGE_PATH + '/fullscreen.png' : 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAABlBMVEUAAAAAAAClZ7nPAAAAAXRSTlMAQObYZgAAABpJREFUCNdjgAAbGxAy4AEh5gNwBBGByoIBAIueBd12TUjqAAAAAElFTkSuQmCC';
1235
1236/**
1237 * Interval to show dialog for unsaved data if autosave is on.
1238 * Default is 300000 (5 minutes).
1239 */
1240App.prototype.warnInterval = 300000;
1241
1242/**
1243 *
1244 */
1245App.prototype.compactMode = false;
1246
1247/**
1248 *
1249 */
1250App.prototype.fullscreenMode = false;
1251
1252/**
1253 * Overriden UI settings depending on mode.
1254 */
1255if (urlParams['embed'] != '1')
1256{
1257	App.prototype.menubarHeight = 64;
1258}
1259else
1260{
1261	App.prototype.footerHeight = 0;
1262}
1263
1264/**
1265 * Queue for loading plugins and wait for UI instance
1266 */
1267App.initPluginCallback = function()
1268{
1269	if (App.DrawPlugins == null)
1270	{
1271		// Workaround for need to load plugins now but wait for UI instance
1272		App.DrawPlugins = [];
1273
1274		// Global entry point for plugins is Draw.loadPlugin. This is the only
1275		// long-term supported solution for access to the EditorUi instance.
1276		window.Draw = new Object();
1277		window.Draw.loadPlugin = function(callback)
1278		{
1279			App.DrawPlugins.push(callback);
1280		};
1281	}
1282};
1283
1284/**
1285 *
1286 */
1287App.pluginsLoaded = {};
1288App.embedModePluginsCount = 0;
1289
1290/**
1291 * Queue for loading plugins and wait for UI instance
1292 */
1293App.loadPlugins = function(plugins, useInclude)
1294{
1295	EditorUi.debug('Loading plugins', plugins);
1296
1297	for (var i = 0; i < plugins.length; i++)
1298	{
1299		if (plugins[i] != null && plugins[i].length > 0)
1300		{
1301			try
1302			{
1303				var url = PLUGINS_BASE_PATH + App.pluginRegistry[plugins[i]];
1304
1305				if (url != null)
1306				{
1307					if (App.pluginsLoaded[url] == null)
1308					{
1309						App.pluginsLoaded[url] = true;
1310						App.embedModePluginsCount++;
1311
1312						if (typeof window.drawDevUrl === 'undefined')
1313						{
1314							if (useInclude)
1315							{
1316								mxinclude(url);
1317							}
1318							else
1319							{
1320								mxscript(url);
1321							}
1322						}
1323						else
1324						{
1325							if (useInclude)
1326							{
1327								mxinclude(url);
1328							}
1329							else
1330							{
1331								mxscript(drawDevUrl + url);
1332							}
1333						}
1334					}
1335				}
1336				else if (window.console != null)
1337				{
1338					console.log('Unknown plugin:', plugins[i]);
1339				}
1340			}
1341			catch (e)
1342			{
1343				if (window.console != null)
1344				{
1345					console.log('Error loading plugin:', plugins[i], e);
1346				}
1347			}
1348		}
1349	}
1350};
1351
1352/**
1353 * Delay embed mode initialization until all plugins are loaded
1354 */
1355App.prototype.initializeEmbedMode = function()
1356{
1357	if (urlParams['embed'] == '1')
1358	{
1359		if (window.location.hostname == 'app.diagrams.net')
1360		{
1361			this.showBanner('EmbedDeprecationFooter', 'app.diagrams.net will stop working for embed mode. Please use embed.diagrams.net.');
1362		}
1363
1364		if (App.embedModePluginsCount > 0 || this.initEmbedDone)
1365		{
1366			return; //Wait for plugins to load, or this is a duplicate call due to timeout
1367		}
1368		else
1369		{
1370			this.initEmbedDone = true;
1371		}
1372
1373		EditorUi.prototype.initializeEmbedMode.apply(this, arguments);
1374	}
1375};
1376
1377/**
1378 * TODO: Define viewer protocol and implement new viewer style toolbar
1379 */
1380App.prototype.initializeViewerMode = function()
1381{
1382	var parent = window.opener || window.parent;
1383
1384	if (parent != null)
1385	{
1386		this.editor.graph.addListener(mxEvent.SIZE, mxUtils.bind(this, function()
1387		{
1388			parent.postMessage(JSON.stringify(this.createLoadMessage('size')), '*');
1389		}));
1390	}
1391};
1392
1393/**
1394 * Translates this point by the given vector.
1395 *
1396 * @param {number} dx X-coordinate of the translation.
1397 * @param {number} dy Y-coordinate of the translation.
1398 */
1399App.prototype.init = function()
1400{
1401	EditorUi.prototype.init.apply(this, arguments);
1402
1403	/**
1404	 * Specifies the default filename.
1405	 */
1406	this.defaultLibraryName = mxResources.get('untitledLibrary');
1407
1408	/**
1409	 * Holds the listener for description changes.
1410	 */
1411	this.descriptorChangedListener = mxUtils.bind(this, this.descriptorChanged);
1412
1413	/**
1414	 * Creates github client.
1415	 */
1416	this.gitHub = (!mxClient.IS_IE || document.documentMode == 10 ||
1417			mxClient.IS_IE11 || mxClient.IS_EDGE) &&
1418			(urlParams['gh'] != '0' && (urlParams['embed'] != '1' ||
1419			urlParams['gh'] == '1')) ? new GitHubClient(this) : null;
1420
1421	if (this.gitHub != null)
1422	{
1423		this.gitHub.addListener('userChanged', mxUtils.bind(this, function()
1424		{
1425			this.updateUserElement();
1426			this.restoreLibraries();
1427
1428			this.showBanner('GithubFooter', 'Click to install GitHub app', mxUtils.bind(this, function()
1429			{
1430				this.openLink('https://github.com/apps/draw-io-app');
1431			}));
1432		}));
1433	}
1434
1435	/**
1436	 * Creates gitlab client.
1437	 */
1438	this.gitLab = (!mxClient.IS_IE || document.documentMode == 10 ||
1439		mxClient.IS_IE11 || mxClient.IS_EDGE) &&
1440		(urlParams['gl'] != '0' && (urlParams['embed'] != '1' ||
1441		urlParams['gl'] == '1')) ? new GitLabClient(this) : null;
1442
1443	if (this.gitLab != null)
1444	{
1445		this.gitLab.addListener('userChanged', mxUtils.bind(this, function()
1446		{
1447			this.updateUserElement();
1448			this.restoreLibraries();
1449		}));
1450	}
1451
1452	/**
1453	 * Creates notion client.
1454	 */
1455	this.notion =
1456	//TODO disabled by default
1457		/*(!mxClient.IS_IE || document.documentMode == 10 ||
1458		mxClient.IS_IE11 || mxClient.IS_EDGE) &&
1459		(urlParams['ntn'] != '0' && (urlParams['embed'] != '1' ||
1460		urlParams['ntn'] == '1')) */
1461		urlParams['ntn'] == '1' ? new NotionClient(this) : null;
1462
1463	if (this.notion != null)
1464	{
1465		this.notion.addListener('userChanged', mxUtils.bind(this, function()
1466		{
1467			this.updateUserElement();
1468			this.restoreLibraries();
1469		}));
1470	}
1471
1472	/**
1473	 * Lazy-loading for individual backends
1474	 */
1475	if (urlParams['embed'] != '1' || urlParams['od'] == '1')
1476	{
1477		/**
1478		 * Creates onedrive client if all required libraries are available.
1479		 */
1480		var initOneDriveClient = mxUtils.bind(this, function()
1481		{
1482			if (typeof OneDrive !== 'undefined')
1483			{
1484				/**
1485				 * Holds the x-coordinate of the point.
1486				 */
1487				this.oneDrive = new OneDriveClient(this);
1488
1489				this.oneDrive.addListener('userChanged', mxUtils.bind(this, function()
1490				{
1491					this.updateUserElement();
1492					this.restoreLibraries();
1493				}));
1494
1495				// Notifies listeners of new client
1496				this.fireEvent(new mxEventObject('clientLoaded', 'client', this.oneDrive));
1497			}
1498			else if (window.DrawOneDriveClientCallback == null)
1499			{
1500				window.DrawOneDriveClientCallback = initOneDriveClient;
1501			}
1502		});
1503
1504		initOneDriveClient();
1505	}
1506
1507	/**
1508	 * Lazy-loading for Trello
1509	 */
1510	if (urlParams['embed'] != '1' || urlParams['tr'] == '1')
1511	{
1512		/**
1513		 * Creates Trello client if all required libraries are available.
1514		 */
1515		var initTrelloClient = mxUtils.bind(this, function()
1516		{
1517			if (typeof window.Trello !== 'undefined')
1518			{
1519				try
1520				{
1521					this.trello = new TrelloClient(this);
1522
1523					//TODO we have no user info from Trello so we don't set a user
1524					this.trello.addListener('userChanged', mxUtils.bind(this, function()
1525					{
1526						this.updateUserElement();
1527						this.restoreLibraries();
1528					}));
1529
1530					// Notifies listeners of new client
1531					this.fireEvent(new mxEventObject('clientLoaded', 'client', this.trello));
1532				}
1533				catch (e)
1534				{
1535					if (window.console != null)
1536					{
1537						console.error(e);
1538					}
1539				}
1540			}
1541			else if (window.DrawTrelloClientCallback == null)
1542			{
1543				window.DrawTrelloClientCallback = initTrelloClient;
1544			}
1545		});
1546
1547		initTrelloClient();
1548	}
1549
1550	/**
1551	 * Creates drive client with all required libraries are available.
1552	 */
1553	if (urlParams['embed'] != '1' || urlParams['gapi'] == '1')
1554	{
1555		var initDriveClient = mxUtils.bind(this, function()
1556		{
1557			/**
1558			 * Creates google drive client if all required libraries are available.
1559			 */
1560			if (typeof gapi !== 'undefined')
1561			{
1562				var doInit = mxUtils.bind(this, function()
1563				{
1564					this.drive = new DriveClient(this);
1565
1566					this.drive.addListener('userChanged', mxUtils.bind(this, function()
1567					{
1568						this.updateUserElement();
1569						this.restoreLibraries();
1570						this.checkLicense();
1571					}))
1572
1573					// Notifies listeners of new client
1574					this.fireEvent(new mxEventObject('clientLoaded', 'client', this.drive));
1575				});
1576
1577				if (window.DrawGapiClientCallback != null)
1578				{
1579					gapi.load(((urlParams['picker'] != '0') ? 'picker,': '') + App.GOOGLE_APIS, doInit);
1580
1581					/**
1582					 * Clears any callbacks.
1583					 */
1584					window.DrawGapiClientCallback = null;
1585				}
1586				else
1587				{
1588					doInit();
1589				}
1590			}
1591			else if (window.DrawGapiClientCallback == null)
1592			{
1593				window.DrawGapiClientCallback = initDriveClient;
1594			}
1595		});
1596
1597		initDriveClient();
1598	}
1599
1600	if (urlParams['embed'] != '1' || urlParams['db'] == '1')
1601	{
1602		/**
1603		 * Creates dropbox client if all required libraries are available.
1604		 */
1605		var initDropboxClient = mxUtils.bind(this, function()
1606		{
1607			if (typeof Dropbox === 'function' && typeof Dropbox.choose !== 'undefined')
1608			{
1609				/**
1610				 * Clears dropbox client callback.
1611				 */
1612				window.DrawDropboxClientCallback = null;
1613
1614				/**
1615				 * Holds the x-coordinate of the point.
1616				 */
1617				try
1618				{
1619					this.dropbox = new DropboxClient(this);
1620
1621					this.dropbox.addListener('userChanged', mxUtils.bind(this, function()
1622					{
1623						this.updateUserElement();
1624						this.restoreLibraries();
1625					}));
1626
1627					// Notifies listeners of new client
1628					this.fireEvent(new mxEventObject('clientLoaded', 'client', this.dropbox));
1629				}
1630				catch (e)
1631				{
1632					if (window.console != null)
1633					{
1634						console.error(e);
1635					}
1636				}
1637			}
1638			else if (window.DrawDropboxClientCallback == null)
1639			{
1640				window.DrawDropboxClientCallback = initDropboxClient;
1641			}
1642		});
1643
1644		initDropboxClient();
1645	}
1646
1647	if (urlParams['embed'] != '1')
1648	{
1649		/**
1650		 * Holds the background element.
1651		 */
1652		this.bg = this.createBackground();
1653		document.body.appendChild(this.bg);
1654		this.diagramContainer.style.visibility = 'hidden';
1655		this.formatContainer.style.visibility = 'hidden';
1656		this.hsplit.style.display = 'none';
1657		this.sidebarContainer.style.display = 'none';
1658		this.sidebarFooterContainer.style.display = 'none';
1659
1660		// Sets the initial mode
1661		if (urlParams['local'] == '1')
1662		{
1663			this.setMode(App.MODE_DEVICE);
1664		}
1665		else
1666		{
1667			this.mode = App.mode;
1668		}
1669
1670		// Add to Home Screen dialog for mobile devices
1671		if ('serviceWorker' in navigator && !this.editor.isChromelessView() &&
1672			(mxClient.IS_ANDROID || mxClient.IS_IOS))
1673		{
1674			window.addEventListener('beforeinstallprompt', mxUtils.bind(this, function(e)
1675			{
1676				this.showBanner('AddToHomeScreenFooter', mxResources.get('installApp'), function()
1677				{
1678				    e.prompt();
1679				});
1680			}));
1681		}
1682
1683		if (!mxClient.IS_CHROMEAPP && !EditorUi.isElectronApp && !this.isOffline() &&
1684			!mxClient.IS_ANDROID && !mxClient.IS_IOS && urlParams['open'] == null &&
1685			(!this.editor.chromeless || this.editor.editable))
1686		{
1687			this.editor.addListener('fileLoaded', mxUtils.bind(this, function()
1688			{
1689				var file = this.getCurrentFile();
1690				var mode = (file != null) ? file.getMode() : null;
1691
1692				if (urlParams['extAuth'] != '1' && (mode == App.MODE_DEVICE || mode == App.MODE_BROWSER))
1693				{
1694					this.showDownloadDesktopBanner();
1695				}
1696				else if (urlParams['embed'] != '1' && this.getServiceName() == 'draw.io')
1697
1698				{
1699					// just app.diagrams.net users
1700					// this.showNameConfBanner();
1701				}
1702			}));
1703		}
1704
1705		if (!mxClient.IS_CHROMEAPP && !EditorUi.isElectronApp && urlParams['embed'] != '1' && DrawioFile.SYNC == 'auto' &&
1706			urlParams['local'] != '1' && urlParams['stealth'] != '1' && !this.isOffline() &&
1707			(!this.editor.chromeless || this.editor.editable))
1708		{
1709			// Checks if the cache is alive
1710			var acceptResponse = true;
1711
1712			var timeoutThread = window.setTimeout(mxUtils.bind(this, function()
1713			{
1714				acceptResponse = false;
1715
1716				// Switches to manual sync if cache cannot be reached
1717				DrawioFile.SYNC = 'manual';
1718
1719				var file = this.getCurrentFile();
1720
1721				if (file != null && file.sync != null)
1722				{
1723					file.sync.destroy();
1724					file.sync = null;
1725
1726					var status = mxUtils.htmlEntities(mxResources.get('timeout'));
1727					this.editor.setStatus('<div title="'+ status +
1728						'" class="geStatusAlert">' + status + '</div>');
1729				}
1730
1731				EditorUi.logEvent({category: 'TIMEOUT-CACHE-CHECK', action: 'timeout', label: 408});
1732			}), Editor.cacheTimeout);
1733
1734			var t0 = new Date().getTime();
1735
1736			mxUtils.get(EditorUi.cacheUrl + '?alive', mxUtils.bind(this, function(req)
1737			{
1738				window.clearTimeout(timeoutThread);
1739			}));
1740		}
1741	}
1742	else if (this.menubar != null)
1743	{
1744		this.menubar.container.style.paddingTop = '0px';
1745	}
1746
1747	this.updateHeader();
1748
1749	if (this.menubar != null)
1750	{
1751		this.buttonContainer = document.createElement('div');
1752		this.buttonContainer.style.display = 'inline-block';
1753		this.buttonContainer.style.paddingRight = '48px';
1754		this.buttonContainer.style.position = 'absolute';
1755		this.buttonContainer.style.right = '0px';
1756
1757		this.menubar.container.appendChild(this.buttonContainer);
1758	}
1759
1760	if ((uiTheme == 'atlas' || urlParams['atlas'] == '1') && this.menubar != null)
1761	{
1762		if (this.toggleElement != null)
1763		{
1764			this.toggleElement.click();
1765			this.toggleElement.style.display = 'none';
1766		}
1767
1768		this.icon = document.createElement('img');
1769		this.icon.setAttribute('src', IMAGE_PATH + '/logo-flat-small.png');
1770		this.icon.setAttribute('title', mxResources.get('draw.io'));
1771		this.icon.style.padding = urlParams['atlas'] == '1'? '7px' : '6px';
1772		this.icon.style.cursor = 'pointer';
1773
1774		mxEvent.addListener(this.icon, 'click', mxUtils.bind(this, function(evt)
1775		{
1776			this.appIconClicked(evt);
1777		}));
1778
1779		this.menubar.container.insertBefore(this.icon, this.menubar.container.firstChild);
1780	}
1781
1782	if (this.editor.graph.isViewer())
1783	{
1784		this.initializeViewerMode();
1785	}
1786};
1787
1788/**
1789 * Schedules a sanity check.
1790 */
1791App.prototype.scheduleSanityCheck = function()
1792{
1793	if (!mxClient.IS_CHROMEAPP && !EditorUi.isElectronApp &&
1794		this.sanityCheckThread == null)
1795	{
1796		this.sanityCheckThread = window.setTimeout(mxUtils.bind(this, function()
1797		{
1798			this.sanityCheckThread = null;
1799			this.sanityCheck();
1800		}), this.warnInterval);
1801	}
1802};
1803
1804/**
1805 * Stops sanity checks.
1806 */
1807App.prototype.stopSanityCheck = function()
1808{
1809	if (this.sanityCheckThread != null)
1810	{
1811		window.clearTimeout(this.sanityCheckThread);
1812		this.sanityCheckThread = null;
1813	}
1814};
1815
1816/**
1817 * Shows a warning after some time with unsaved changes and autosave.
1818 */
1819App.prototype.sanityCheck = function()
1820{
1821	var file = this.getCurrentFile();
1822
1823	if (file != null && file.isModified() && file.isAutosave() && file.isOverdue())
1824	{
1825		var evt = {category: 'WARN-FILE-' + file.getHash(),
1826			action: ((file.savingFile) ? 'saving' : '') +
1827			((file.savingFile && file.savingFileTime != null) ? '_' +
1828				Math.round((Date.now() - file.savingFileTime.getTime()) / 1000) : '') +
1829			((file.saveLevel != null) ? ('-sl_' + file.saveLevel) : '') +
1830			'-age_' + ((file.ageStart != null) ? Math.round((Date.now() - file.ageStart.getTime()) / 1000) : 'x') +
1831			((this.editor.autosave) ? '' : '-nosave') +
1832			((file.isAutosave()) ? '' : '-noauto') +
1833			'-open_' + ((file.opened != null) ? Math.round((Date.now() - file.opened.getTime()) / 1000) : 'x') +
1834			'-save_' + ((file.lastSaved != null) ? Math.round((Date.now() - file.lastSaved.getTime()) / 1000) : 'x') +
1835			'-change_' + ((file.lastChanged != null) ? Math.round((Date.now() - file.lastChanged.getTime()) / 1000) : 'x')+
1836			'-alive_' + Math.round((Date.now() - App.startTime.getTime()) / 1000),
1837			label: (file.sync != null) ? ('client_' + file.sync.clientId) : 'nosync'};
1838
1839		if (file.constructor == DriveFile && file.desc != null && this.drive != null)
1840		{
1841			evt.label += ((this.drive.user != null) ? ('-user_' + this.drive.user.id) : '-nouser') + '-rev_' +
1842				file.desc.headRevisionId + '-mod_' + file.desc.modifiedDate + '-size_' + file.getSize() +
1843				'-mime_' + file.desc.mimeType;
1844		}
1845
1846		EditorUi.logEvent(evt);
1847
1848		var msg = mxResources.get('ensureDataSaved');
1849
1850		if (file.lastSaved != null)
1851		{
1852			var str = this.timeSince(file.lastSaved);
1853
1854			if (str == null)
1855			{
1856				str = mxResources.get('lessThanAMinute');
1857			}
1858
1859			msg = mxResources.get('lastSaved', [str]);
1860		}
1861
1862		// Resets possible stale state
1863		this.spinner.stop();
1864
1865		this.showError(mxResources.get('unsavedChanges'), msg, mxResources.get('ignore'),
1866			mxUtils.bind(this, function()
1867			{
1868				this.hideDialog();
1869			}), null, mxResources.get('save'), mxUtils.bind(this, function()
1870			{
1871				this.stopSanityCheck();
1872				this.actions.get((this.mode == null || !file.isEditable()) ?
1873					'saveAs' : 'save').funct();
1874			}), null, null, 360, 120, null, mxUtils.bind(this, function()
1875			{
1876				this.scheduleSanityCheck();
1877			}));
1878	}
1879};
1880
1881/**
1882 * Returns true if the current domain is for the new drive app.
1883 */
1884App.prototype.isDriveDomain = function()
1885{
1886	return urlParams['drive'] != '0' &&
1887		(window.location.hostname == 'test.draw.io' ||
1888		window.location.hostname == 'www.draw.io' ||
1889		window.location.hostname == 'drive.draw.io' ||
1890		window.location.hostname == 'app.diagrams.net' ||
1891		window.location.hostname == 'jgraph.github.io');
1892};
1893
1894/**
1895 * Returns the pusher instance for notifications. Creates the instance of none exists.
1896 */
1897App.prototype.getPusher = function()
1898{
1899	if (this.pusher == null && typeof window.Pusher === 'function')
1900	{
1901		this.pusher = new Pusher(App.PUSHER_KEY,
1902		{
1903			cluster: App.PUSHER_CLUSTER,
1904			encrypted: true
1905		});
1906	}
1907
1908	return this.pusher;
1909};
1910
1911/**
1912 * Shows a footer to download the desktop version once per session.
1913 */
1914App.prototype.showNameChangeBanner = function()
1915{
1916	this.showBanner('DiagramsFooter', 'draw.io is now diagrams.net', mxUtils.bind(this, function()
1917	{
1918		this.openLink('https://www.diagrams.net/blog/move-diagrams-net');
1919	}));
1920};
1921
1922/**
1923 * Shows a footer to download the desktop version once per session.
1924 */
1925App.prototype.showNameConfBanner = function()
1926{
1927	this.showBanner('ConfFooter', 'Try draw.io for Confluence', mxUtils.bind(this, function()
1928	{
1929		this.openLink('https://marketplace.atlassian.com/apps/1210933/draw-io-diagrams-for-confluence');
1930	}), true);
1931};
1932
1933/**
1934 * Shows a footer to download the desktop version once per session.
1935 */
1936App.prototype.showDownloadDesktopBanner = function()
1937{
1938	this.showBanner('DesktopFooter', mxResources.get('downloadDesktop'), mxUtils.bind(this, function()
1939	{
1940		this.openLink('https://get.diagrams.net/');
1941	}));
1942};
1943
1944/**
1945 * Shows a footer to download the desktop version once per session.
1946 */
1947App.prototype.showRatingBanner = function()
1948{
1949		if (!this.bannerShowing && !this['hideBanner' + 'ratingFooter'] &&
1950			(!isLocalStorage || mxSettings.settings == null ||
1951			mxSettings.settings['close' + 'ratingFooter'] == null))
1952		{
1953			var banner = document.createElement('div');
1954			banner.style.cssText = 'position:absolute;bottom:10px;left:50%;max-width:90%;padding:18px 34px 12px 20px;' +
1955				'font-size:16px;font-weight:bold;white-space:nowrap;cursor:pointer;z-index:' + mxPopupMenu.prototype.zIndex + ';';
1956			mxUtils.setPrefixedStyle(banner.style, 'box-shadow', '1px 1px 2px 0px #ddd');
1957			mxUtils.setPrefixedStyle(banner.style, 'transform', 'translate(-50%,120%)');
1958			mxUtils.setPrefixedStyle(banner.style, 'transition', 'all 1s ease');
1959			banner.className = 'geBtn gePrimaryBtn';
1960
1961			var img = document.createElement('img');
1962			img.setAttribute('src', Dialog.prototype.closeImage);
1963			img.setAttribute('title', mxResources.get('close'));
1964			img.setAttribute('border', '0');
1965			img.style.cssText = 'position:absolute;right:10px;top:12px;filter:invert(1);padding:6px;margin:-6px;cursor:default;';
1966			banner.appendChild(img);
1967
1968			var star = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAABx0RVh0U29mdHdhcmUAQWRvYmUgRmlyZ' +
1969				'XdvcmtzIENTM5jWRgMAAAQRdEVYdFhNTDpjb20uYWRvYmUueG1wADw/eHBhY2tldCBiZWdpbj0iICAgIiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+Cjx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8i' +
1970				'IHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDQuMS1jMDM0IDQ2LjI3Mjk3NiwgU2F0IEphbiAyNyAyMDA3IDIyOjExOjQxICAgICAgICAiPgogICA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDI' +
1971				'vMjItcmRmLXN5bnRheC1ucyMiPgogICAgICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgICAgICAgICB4bWxuczp4YXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iPgogICAgICAgICA8eGFwOkNyZW' +
1972				'F0b3JUb29sPkFkb2JlIEZpcmV3b3JrcyBDUzM8L3hhcDpDcmVhdG9yVG9vbD4KICAgICAgICAgPHhhcDpDcmVhdGVEYXRlPjIwMDgtMDItMTdUMDI6MzY6NDVaPC94YXA6Q3JlYXRlRGF0ZT4KICAgICAgICAgPHhhcDpNb2RpZ' +
1973				'nlEYXRlPjIwMDktMDMtMTdUMTQ6MTI6MDJaPC94YXA6TW9kaWZ5RGF0ZT4KICAgICAgPC9yZGY6RGVzY3JpcHRpb24+CiAgICAgIDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiCiAgICAgICAgICAgIHhtbG5zOmRjPSJo' +
1974				'dHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyI+CiAgICAgICAgIDxkYzpmb3JtYXQ+aW1hZ2UvcG5nPC9kYzpmb3JtYXQ+CiAgICAgIDwvcmRmOkRlc2NyaXB0aW9uPgogICA8L3JkZjpSREY+CjwveDp4bXBtZXRhPgo' +
1975				'gICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgIC' +
1976				'AgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI' +
1977				'CAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIIImu8AAAAAVdEVYdENyZWF0aW9uIFRpbWUAMi8xNy8wOCCcqlgAAAHuSURBVDiNlZJBi1JRGIbfk+fc0ZuMXorJe4XujWoMdREaA23HICj6AQeLINr0C4I27ab2' +
1978				'7VqOI9+q/sH8gMDceG1RkIwgClEXFMbRc5zTZgZURmG+5fu9PN/7Hg6wZohoh4h21nn4uqXW+q0xZgzg+SrPlTXX73uet+26bp6ICpcGaK1fua57M5vN3tZav7gUgIiSqVTqcRAEm0EQbCaTyQoRXb3Iy4hoG8CT6XSaY4xtMMa' +
1979				'SQohMPp8v+r7vAEC3243CMGwqpfoApsaYE8uyfgM45ABOjDEvXdfNlMvlzFINAIDneY7neZVzvdlsDgaDQYtzfsjOIjtKqU+e5+0Wi0V3VV8ACMOw3+/3v3HOX0sp/7K53te11h/S6fRuoVAIhBAL76OUOm2320dRFH0VQuxJKf' +
1980				'8BAFu+UKvVvpRKpWe2bYt5fTweq0ajQUKIN1LK43N94SMR0Y1YLLYlhBBKqQUw51wkEol7WmuzoC8FuJtIJLaUUoii6Ljb7f4yxpz6vp9zHMe2bfvacDi8BeDHKkBuNps5rVbr52QyaVuW9ZExttHpdN73ej0/Ho+nADxYCdBaV' +
1981				'0aj0RGAz5ZlHUgpx2erR/V6/d1wOHwK4CGA/QsBnPN9AN+llH+WkqFare4R0QGAO/M6M8Ysey81/wGqa8MlVvHPNAAAAABJRU5ErkJggg==';
1982
1983			mxUtils.write(banner, 'Please rate us');
1984			document.body.appendChild(banner);
1985
1986			var star1 = document.createElement('img');
1987			star1.setAttribute('border', '0');
1988			star1.setAttribute('align', 'absmiddle');
1989			star1.setAttribute('title', '1 star');
1990			star1.setAttribute('style', 'margin-top:-6px;cursor:pointer;margin-left:8px;');
1991			star1.setAttribute('src', star);
1992			banner.appendChild(star1);
1993
1994			var star2 = document.createElement('img');
1995			star2.setAttribute('border', '0');
1996			star2.setAttribute('align', 'absmiddle');
1997			star2.setAttribute('title', '2 star');
1998			star2.setAttribute('style', 'margin-top:-6px;margin-left:3px;cursor:pointer;');
1999			star2.setAttribute('src', star);
2000			banner.appendChild(star2);
2001
2002			var star3 = document.createElement('img');
2003			star3.setAttribute('border', '0');
2004			star3.setAttribute('align', 'absmiddle');
2005			star3.setAttribute('title', '3 star');
2006			star3.setAttribute('style', 'margin-top:-6px;margin-left:3px;cursor:pointer;');
2007			star3.setAttribute('src', star);
2008			banner.appendChild(star3);
2009
2010			var star4 = document.createElement('img');
2011			star4.setAttribute('border', '0');
2012			star4.setAttribute('align', 'absmiddle');
2013			star4.setAttribute('title', '4 star');
2014			star4.setAttribute('style', 'margin-top:-6px;margin-left:3px;cursor:pointer;');
2015			star4.setAttribute('src', star);
2016			banner.appendChild(star4);
2017
2018			this.bannerShowing = true;
2019
2020			var onclose = mxUtils.bind(this, function()
2021			{
2022				if (banner.parentNode != null)
2023				{
2024					banner.parentNode.removeChild(banner);
2025					this.bannerShowing = false;
2026
2027					this['hideBanner' + 'ratingFooter'] = true;
2028
2029					if (isLocalStorage && mxSettings.settings != null)
2030					{
2031						mxSettings.settings['close' + 'ratingFooter'] = Date.now();
2032						mxSettings.save();
2033					}
2034				}
2035			});
2036
2037			mxEvent.addListener(img, 'click', mxUtils.bind(this, function(e)
2038			{
2039				mxEvent.consume(e);
2040				onclose();
2041			}));
2042			mxEvent.addListener(star1, 'click', mxUtils.bind(this, function(e)
2043			{
2044				mxEvent.consume(e);
2045				onclose();
2046			}));
2047			mxEvent.addListener(star2, 'click', mxUtils.bind(this, function(e)
2048			{
2049				mxEvent.consume(e);
2050				onclose();
2051			}));
2052			mxEvent.addListener(star3, 'click', mxUtils.bind(this, function(e)
2053			{
2054				mxEvent.consume(e);
2055				onclose();
2056			}));
2057			mxEvent.addListener(star4, 'click', mxUtils.bind(this, function(e)
2058			{
2059				mxEvent.consume(e);
2060				window.open('https://marketplace.atlassian.com/apps/1210933/draw-io-diagrams-for-confluence?hosting=datacenter&tab=reviews');
2061				onclose();
2062			}));
2063
2064			var hide = mxUtils.bind(this, function()
2065			{
2066				mxUtils.setPrefixedStyle(banner.style, 'transform', 'translate(-50%,120%)');
2067
2068				window.setTimeout(mxUtils.bind(this, function()
2069				{
2070					onclose();
2071				}), 1000);
2072			});
2073
2074			window.setTimeout(mxUtils.bind(this, function()
2075			{
2076				mxUtils.setPrefixedStyle(banner.style, 'transform', 'translate(-50%,0%)');
2077			}), 500);
2078
2079			window.setTimeout(hide, 60000);
2080		}
2081};
2082
2083/**
2084 * Checks license in the case of Google Drive storage.
2085 * IMPORTANT: Do not change this function without consulting
2086 * the privacy lead. No personal information must be sent.
2087 */
2088App.prototype.checkLicense = function()
2089{
2090	var driveUser = this.drive.getUser();
2091	var email = (driveUser != null) ? driveUser.email : null;
2092
2093	if (!this.isOffline() && !this.editor.chromeless && email != null && driveUser.id != null)
2094	{
2095		// Only the domain and hashed user ID are transmitted. This code was reviewed and deemed
2096		// compliant by dbenson 2021-09-01.
2097		var at = email.lastIndexOf('@');
2098		var domain = (at >= 0) ? email.substring(at + 1) : '';
2099		var userId = Editor.crc32(driveUser.id);
2100
2101		// Timestamp is workaround for cached response in certain environments
2102		mxUtils.post('/license', 'domain=' + encodeURIComponent(domain) + '&id=' + encodeURIComponent(userId) +
2103				'&ts=' + new Date().getTime(),
2104			mxUtils.bind(this, function(req)
2105			{
2106				try
2107				{
2108					if (req.getStatus() >= 200 && req.getStatus() <= 299)
2109					{
2110						var value = req.getText();
2111
2112						if (value.length > 0)
2113						{
2114							var lic = JSON.parse(value);
2115
2116							if (lic != null)
2117							{
2118								this.handleLicense(lic, domain);
2119							}
2120						}
2121					}
2122				}
2123				catch (e)
2124				{
2125					// ignore
2126				}
2127			}));
2128	}
2129};
2130
2131/**
2132 * Returns true if the current domain is for the new drive app.
2133 */
2134App.prototype.handleLicense = function(lic, domain)
2135{
2136	if (lic != null && lic.plugins != null)
2137	{
2138		App.loadPlugins(lic.plugins.split(';'), true);
2139	}
2140};
2141
2142/**
2143 *
2144 */
2145App.prototype.getEditBlankXml = function()
2146{
2147	var file = this.getCurrentFile();
2148
2149	if (file != null && this.editor.isChromelessView() && this.editor.graph.isLightboxView())
2150	{
2151		return file.getData();
2152	}
2153	else
2154	{
2155		return this.getFileData(true);
2156	}
2157};
2158
2159/**
2160 * Updates action states depending on the selection.
2161 */
2162App.prototype.updateActionStates = function()
2163{
2164	EditorUi.prototype.updateActionStates.apply(this, arguments);
2165
2166	this.actions.get('revisionHistory').setEnabled(this.isRevisionHistoryEnabled());
2167};
2168
2169/**
2170 * Adds the specified entry to the recent file list in local storage
2171 */
2172App.prototype.addRecent = function(entry)
2173{
2174	if (isLocalStorage && localStorage != null)
2175	{
2176		var recent = this.getRecent();
2177
2178		if (recent == null)
2179		{
2180			recent = [];
2181		}
2182		else
2183		{
2184			for (var i = 0; i < recent.length; i++)
2185			{
2186				if (recent[i].id == entry.id)
2187				{
2188					recent.splice(i, 1);
2189				}
2190			}
2191		}
2192
2193		if (recent != null)
2194		{
2195			recent.unshift(entry);
2196			recent = recent.slice(0, 10);
2197			localStorage.setItem('.recent', JSON.stringify(recent));
2198		}
2199	}
2200};
2201
2202/**
2203 * Returns the recent file list from local storage
2204 */
2205App.prototype.getRecent = function()
2206{
2207	if (isLocalStorage && localStorage != null)
2208	{
2209		try
2210		{
2211			var recent = localStorage.getItem('.recent');
2212
2213			if (recent != null)
2214			{
2215				return JSON.parse(recent);
2216			}
2217		}
2218		catch (e)
2219		{
2220			// ignore
2221		}
2222
2223		return null;
2224	}
2225};
2226
2227/**
2228 * Clears the recent file list in local storage
2229 */
2230App.prototype.resetRecent = function(entry)
2231{
2232	if (isLocalStorage && localStorage != null)
2233	{
2234		try
2235		{
2236			localStorage.removeItem('.recent');
2237		}
2238		catch (e)
2239		{
2240			// ignore
2241		}
2242	}
2243};
2244
2245/**
2246 * Sets the onbeforeunload for the application
2247 */
2248App.prototype.onBeforeUnload = function()
2249{
2250	if (urlParams['embed'] == '1' && this.editor.modified)
2251	{
2252		return mxResources.get('allChangesLost');
2253	}
2254	else
2255	{
2256		var file = this.getCurrentFile();
2257
2258		if (file != null)
2259		{
2260			// KNOWN: Message is ignored by most browsers
2261			if (file.constructor == LocalFile && file.getHash() == '' && !file.isModified() &&
2262				urlParams['nowarn'] != '1' && !this.isDiagramEmpty() && urlParams['url'] == null &&
2263				!this.editor.isChromelessView() && file.fileHandle == null)
2264			{
2265				return mxResources.get('ensureDataSaved');
2266			}
2267			else if (file.isModified())
2268			{
2269				return mxResources.get('allChangesLost');
2270			}
2271			else
2272			{
2273				file.close(true);
2274			}
2275		}
2276	}
2277};
2278
2279/**
2280 * Translates this point by the given vector.
2281 *
2282 * @param {number} dx X-coordinate of the translation.
2283 * @param {number} dy Y-coordinate of the translation.
2284 */
2285App.prototype.updateDocumentTitle = function()
2286{
2287	if (!this.editor.graph.isLightboxView())
2288	{
2289		var title = this.editor.appName;
2290		var file = this.getCurrentFile();
2291
2292		if (this.isOfflineApp())
2293		{
2294			title += ' app';
2295		}
2296
2297		if (file != null)
2298		{
2299			var filename = (file.getTitle() != null) ? file.getTitle() : this.defaultFilename;
2300			title = filename + ' - ' + title;
2301		}
2302
2303		if (document.title != title)
2304		{
2305			document.title = title;
2306			var graph = this.editor.graph;
2307			graph.invalidateDescendantsWithPlaceholders(graph.model.getRoot());
2308			graph.view.validate();
2309		}
2310	}
2311};
2312
2313/**
2314 * Returns a thumbnail of the current file.
2315 */
2316App.prototype.getThumbnail = function(width, fn)
2317{
2318	var result = false;
2319
2320	try
2321	{
2322		var acceptResponse = true;
2323
2324		var timeoutThread = window.setTimeout(mxUtils.bind(this, function()
2325		{
2326			acceptResponse = false;
2327			fn(null);
2328		}), this.timeout);
2329
2330		var success = mxUtils.bind(this, function(canvas)
2331		{
2332			window.clearTimeout(timeoutThread);
2333
2334			if (acceptResponse)
2335			{
2336				fn(canvas);
2337			}
2338		});
2339
2340		if (this.thumbImageCache == null)
2341		{
2342			this.thumbImageCache = new Object();
2343		}
2344
2345		var graph = this.editor.graph;
2346		var bgImg = graph.backgroundImage;
2347
2348		// Exports PNG for first page while other page is visible by creating a graph
2349		// LATER: Add caching for the graph or SVG while not on first page
2350		// To avoid refresh during save dark theme uses separate graph instance
2351		var darkTheme = graph.themes != null && graph.defaultThemeName == 'darkTheme';
2352
2353		if (this.pages != null && (darkTheme || this.currentPage != this.pages[0]))
2354		{
2355			var graphGetGlobalVariable = graph.getGlobalVariable;
2356			graph = this.createTemporaryGraph(graph.getStylesheet());
2357			graph.setBackgroundImage = this.editor.graph.setBackgroundImage;
2358			var page = this.pages[0];
2359
2360			if (this.currentPage == page)
2361			{
2362				graph.setBackgroundImage(bgImg);
2363			}
2364			else if (page.viewState != null && page.viewState != null)
2365			{
2366				bgImg = page.viewState.backgroundImage;
2367				graph.setBackgroundImage(bgImg);
2368			}
2369
2370			graph.getGlobalVariable = function(name)
2371			{
2372				if (name == 'page')
2373				{
2374					return page.getName();
2375				}
2376				else if (name == 'pagenumber')
2377				{
2378					return 1;
2379				}
2380
2381				return graphGetGlobalVariable.apply(this, arguments);
2382			};
2383
2384			graph.getGlobalVariable = graphGetGlobalVariable;
2385			document.body.appendChild(graph.container);
2386			graph.model.setRoot(page.root);
2387		}
2388
2389		// Uses client-side canvas export
2390		if (mxClient.IS_CHROMEAPP || this.useCanvasForExport)
2391		{
2392		   	this.editor.exportToCanvas(mxUtils.bind(this, function(canvas)
2393		   	{
2394		   		try
2395		   		{
2396			   		// Removes temporary graph from DOM
2397	   	   	    	if (graph != this.editor.graph && graph.container.parentNode != null)
2398					{
2399						graph.container.parentNode.removeChild(graph.container);
2400					}
2401				}
2402				catch (e)
2403				{
2404					canvas = null;
2405				}
2406
2407		   		success(canvas);
2408		   	}), width, this.thumbImageCache, '#ffffff', function()
2409		   	{
2410		   		// Continues with null in error case
2411		   		success();
2412		   	}, null, null, null, null, null, null, graph, null, null, null,
2413			   null, 'diagram', null);
2414
2415		   	result = true;
2416		}
2417		else if (this.canvasSupported && this.getCurrentFile() != null)
2418		{
2419			var canvas = document.createElement('canvas');
2420			var bounds = graph.getGraphBounds();
2421			var t = graph.view.translate;
2422			var s = graph.view.scale;
2423
2424			if (bgImg != null)
2425			{
2426				bounds = mxRectangle.fromRectangle(bounds);
2427				bounds.add(new mxRectangle(
2428					(t.x + bgImg.x) * s, (t.y + bgImg.y) * s,
2429					bgImg.width * s, bgImg.height * s));
2430			}
2431
2432			var scale = width / bounds.width;
2433
2434			// Limits scale to 1 or 2 * width / height
2435			scale = Math.min(1, Math.min((width * 3) / (bounds.height * 4), scale));
2436
2437			var x0 = Math.floor(bounds.x);
2438			var y0 = Math.floor(bounds.y);
2439
2440			canvas.setAttribute('width', Math.ceil(scale * (bounds.width + 4)));
2441			canvas.setAttribute('height', Math.ceil(scale * (bounds.height + 4)));
2442
2443			var ctx = canvas.getContext('2d');
2444
2445			// Configures the canvas
2446			ctx.scale(scale, scale);
2447			ctx.translate(-x0, -y0);
2448
2449			// Paint white background instead of transparent
2450			var bg = graph.background;
2451
2452			if (bg == null || bg == '' || bg == mxConstants.NONE)
2453			{
2454				bg = '#ffffff';
2455			}
2456
2457			// Paints background
2458			ctx.save();
2459			ctx.fillStyle = bg;
2460			ctx.fillRect(x0, y0, Math.ceil(bounds.width + 4), Math.ceil(bounds.height + 4));
2461			ctx.restore();
2462
2463			// Paints background image
2464			if (bgImg != null)
2465			{
2466				var img = new Image();
2467				img.src = bgImg.src;
2468
2469				ctx.drawImage(img, bgImg.x * scale, bgImg.y * scale,
2470					bgImg.width * scale, bgImg.height * scale);
2471			}
2472
2473			var htmlCanvas = new mxJsCanvas(canvas);
2474
2475			// NOTE: htmlCanvas passed into async canvas is only used for image
2476			// and canvas caching (canvas caching not used in this case as we do
2477			// not render text). To reuse that cache via the thumbImageCache we
2478			// pass that into the async canvas and override the image cache in
2479			// the newly created html canvas with that of the thumbImageCache.
2480			// LATER: Is clear thumbImageCache needed if file changes?
2481			var asynCanvas = new mxAsyncCanvas(this.thumbImageCache);
2482			htmlCanvas.images = this.thumbImageCache.images;
2483
2484			// Render graph
2485			var imgExport = new mxImageExport();
2486
2487			imgExport.drawShape = function(state, canvas)
2488			{
2489				if (state.shape instanceof mxShape && state.shape.checkBounds())
2490				{
2491					canvas.save();
2492					canvas.translate(0.5, 0.5);
2493					state.shape.paint(canvas);
2494					canvas.translate(-0.5, -0.5);
2495					canvas.restore();
2496				}
2497			};
2498
2499			imgExport.drawText = function(state, canvas)
2500			{
2501				// No text output for thumbnails
2502			};
2503
2504			imgExport.drawState(graph.getView().getState(graph.model.root), asynCanvas);
2505
2506			asynCanvas.finish(mxUtils.bind(this, function()
2507			{
2508				try
2509				{
2510					imgExport.drawState(graph.getView().getState(graph.model.root), htmlCanvas);
2511
2512			   		// Removes temporary graph from DOM
2513	   	   	    	if (graph != this.editor.graph && graph.container.parentNode != null)
2514					{
2515						graph.container.parentNode.removeChild(graph.container);
2516					}
2517				}
2518				catch (e)
2519				{
2520					canvas = null;
2521				}
2522
2523				success(canvas);
2524			}));
2525
2526			result = true;
2527		}
2528	}
2529	catch (e)
2530	{
2531		result = false;
2532
2533		// Removes temporary graph from DOM
2534  	    if (graph != null && graph != this.editor.graph && graph.container.parentNode != null)
2535		{
2536			graph.container.parentNode.removeChild(graph.container);
2537		}
2538	}
2539
2540	if (!result)
2541	{
2542		window.clearTimeout(timeoutThread);
2543	}
2544
2545	return result;
2546};
2547
2548/**
2549 * Translates this point by the given vector.
2550 *
2551 * @param {number} dx X-coordinate of the translation.
2552 * @param {number} dy Y-coordinate of the translation.
2553 */
2554App.prototype.createBackground = function()
2555{
2556	var bg = this.createDiv('background');
2557	bg.style.position = 'absolute';
2558	bg.style.background = 'white';
2559	bg.style.left = '0px';
2560	bg.style.top = '0px';
2561	bg.style.bottom = '0px';
2562	bg.style.right = '0px';
2563
2564	mxUtils.setOpacity(bg, 100);
2565
2566	return bg;
2567};
2568
2569/**
2570 * Translates this point by the given vector.
2571 *
2572 * @param {number} dx X-coordinate of the translation.
2573 * @param {number} dy Y-coordinate of the translation.
2574 */
2575(function()
2576{
2577	var editorUiSetMode = EditorUi.prototype.setMode;
2578
2579	App.prototype.setMode = function(mode, remember)
2580	{
2581		editorUiSetMode.apply(this, arguments);
2582
2583		// Note: UseLocalStorage affects the file dialogs
2584		// and should not be modified if mode is undefined
2585		if (this.mode != null)
2586		{
2587			Editor.useLocalStorage = this.mode == App.MODE_BROWSER;
2588		}
2589
2590		if (this.appIcon != null)
2591		{
2592			var file = this.getCurrentFile();
2593			mode = (file != null) ? file.getMode() : mode;
2594
2595			if (mode == App.MODE_GOOGLE)
2596			{
2597				this.appIcon.setAttribute('title', mxResources.get('openIt', [mxResources.get('googleDrive')]));
2598				this.appIcon.style.cursor = 'pointer';
2599			}
2600			else if (mode == App.MODE_DROPBOX)
2601			{
2602				this.appIcon.setAttribute('title', mxResources.get('openIt', [mxResources.get('dropbox')]));
2603				this.appIcon.style.cursor = 'pointer';
2604			}
2605			else if (mode == App.MODE_ONEDRIVE)
2606			{
2607				this.appIcon.setAttribute('title', mxResources.get('openIt', [mxResources.get('oneDrive')]));
2608				this.appIcon.style.cursor = 'pointer';
2609			}
2610			else
2611			{
2612				this.appIcon.removeAttribute('title');
2613				this.appIcon.style.cursor = (mode == App.MODE_DEVICE) ? 'pointer' : 'default';
2614			}
2615		}
2616
2617		if (remember)
2618		{
2619			try
2620			{
2621				if (isLocalStorage)
2622				{
2623					localStorage.setItem('.mode', mode);
2624				}
2625				else if (typeof(Storage) != 'undefined')
2626				{
2627					var expiry = new Date();
2628					expiry.setYear(expiry.getFullYear() + 1);
2629					document.cookie = 'MODE=' + mode + '; expires=' + expiry.toUTCString();
2630				}
2631			}
2632			catch (e)
2633			{
2634				// ignore possible access denied
2635			}
2636		}
2637	};
2638})();
2639
2640/**
2641 * Function: authorize
2642 *
2643 * Authorizes the client, gets the userId and calls <open>.
2644 */
2645App.prototype.appIconClicked = function(evt)
2646{
2647	if (mxEvent.isAltDown(evt))
2648	{
2649		this.showSplash(true);
2650	}
2651	else
2652	{
2653		var file = this.getCurrentFile();
2654		var mode = (file != null) ? file.getMode() : null;
2655
2656		if (mode == App.MODE_GOOGLE)
2657		{
2658			if (file != null && file.desc != null && file.desc.parents != null &&
2659				file.desc.parents.length > 0 && !mxEvent.isShiftDown(evt))
2660			{
2661				// Opens containing folder
2662				this.openLink('https://drive.google.com/drive/folders/' + file.desc.parents[0].id);
2663			}
2664			else if (file != null && file.getId() != null)
2665			{
2666				this.openLink('https://drive.google.com/open?id=' + file.getId());
2667			}
2668			else
2669			{
2670				this.openLink('https://drive.google.com/?authuser=0');
2671			}
2672		}
2673		else if (mode == App.MODE_ONEDRIVE)
2674		{
2675			if (file != null && file.meta != null && file.meta.webUrl != null)
2676			{
2677				var url = file.meta.webUrl;
2678				var name = encodeURIComponent(file.meta.name);
2679
2680				if (url.substring(url.length - name.length, url.length) == name)
2681				{
2682					url = url.substring(0, url.length - name.length);
2683				}
2684
2685				this.openLink(url);
2686			}
2687			else
2688			{
2689				this.openLink('https://onedrive.live.com/');
2690			}
2691		}
2692		else if (mode == App.MODE_DROPBOX)
2693		{
2694			if (file != null && file.stat != null && file.stat.path_display != null)
2695			{
2696				var url = 'https://www.dropbox.com/home/Apps/drawio' + file.stat.path_display;
2697
2698				if (!mxEvent.isShiftDown(evt))
2699				{
2700					url = url.substring(0, url.length - file.stat.name.length);
2701				}
2702
2703				this.openLink(url);
2704			}
2705			else
2706			{
2707				this.openLink('https://www.dropbox.com/');
2708			}
2709		}
2710		else if (mode == App.MODE_TRELLO)
2711		{
2712			this.openLink('https://trello.com/');
2713		}
2714		else if (mode == App.MODE_NOTION)
2715		{
2716			this.openLink('https://www.notion.so/');
2717		}
2718		else if (mode == App.MODE_GITHUB)
2719		{
2720			if (file != null && file.constructor == GitHubFile)
2721			{
2722				this.openLink(file.meta.html_url);
2723			}
2724			else
2725			{
2726				this.openLink('https://github.com/');
2727			}
2728		}
2729		else if (mode == App.MODE_GITLAB)
2730		{
2731			if (file != null && file.constructor == GitLabFile)
2732			{
2733				this.openLink(file.meta.html_url);
2734			}
2735			else
2736			{
2737				this.openLink(DRAWIO_GITLAB_URL);
2738			}
2739		}
2740		else if (mode == App.MODE_DEVICE)
2741		{
2742			this.openLink('https://get.draw.io/');
2743		}
2744	}
2745
2746	mxEvent.consume(evt);
2747};
2748
2749/**
2750 * Function: authorize
2751 *
2752 * Authorizes the client, gets the userId and calls <open>.
2753 */
2754App.prototype.clearMode = function()
2755{
2756	if (isLocalStorage)
2757	{
2758		localStorage.removeItem('.mode');
2759	}
2760	else if (typeof(Storage) != 'undefined')
2761	{
2762		var expiry = new Date();
2763		expiry.setYear(expiry.getFullYear() - 1);
2764		document.cookie = 'MODE=; expires=' + expiry.toUTCString();
2765	}
2766};
2767
2768/**
2769 * Translates this point by the given vector.
2770 *
2771 * @param {number} dx X-coordinate of the translation.
2772 * @param {number} dy Y-coordinate of the translation.
2773 */
2774App.prototype.getDiagramId = function()
2775{
2776	var id = window.location.hash;
2777
2778	// Strips the hash sign
2779	if (id != null && id.length > 0)
2780	{
2781		id = id.substring(1);
2782	}
2783
2784	// Workaround for Trello client appending data after hash
2785	if (id != null && id.length > 1 && id.charAt(0) == 'T')
2786	{
2787		var idx = id.indexOf('#');
2788
2789		if (idx > 0)
2790		{
2791			id = id.substring(0, idx);
2792		}
2793	}
2794
2795	return id;
2796};
2797
2798/**
2799 * Opens any file specified in the URL parameters.
2800 */
2801App.prototype.open = function()
2802{
2803	// Cross-domain window access is not allowed in FF, so if we
2804	// were opened from another domain then this will fail.
2805	try
2806	{
2807		// If the create URL param is used in embed mode then
2808		// we try to open the XML from window.opener[value].
2809		// Use this for embedding via tab to bypass the timing
2810		// issues when passing messages without onload event.
2811		if (window.opener != null)
2812		{
2813			var value = urlParams['create'];
2814
2815			if (value != null)
2816			{
2817				value = decodeURIComponent(value);
2818			}
2819
2820			if (value != null && value.length > 0 && value.substring(0, 7) != 'http://' &&
2821				value.substring(0, 8) != 'https://')
2822			{
2823				var doc = mxUtils.parseXml(window.opener[value]);
2824				this.editor.setGraphXml(doc.documentElement);
2825			}
2826			else if (window.opener.openFile != null)
2827			{
2828				window.opener.openFile.setConsumer(mxUtils.bind(this, function(xml, filename, temp)
2829				{
2830					this.spinner.stop();
2831
2832					if (filename == null)
2833					{
2834						var title = urlParams['title'];
2835						temp = true;
2836
2837						if (title != null)
2838						{
2839							filename = decodeURIComponent(title);
2840						}
2841						else
2842						{
2843							filename = this.defaultFilename;
2844						}
2845					}
2846
2847					// Replaces PNG with XML extension
2848					var dot = (!this.useCanvasForExport) ? filename.substring(filename.length - 4) == '.png' : -1;
2849
2850					if (dot > 0)
2851					{
2852						filename = filename.substring(0, filename.length - 4) + '.drawio';
2853					}
2854
2855					this.fileLoaded((mxClient.IS_IOS) ?
2856						new StorageFile(this, xml, filename) :
2857						new LocalFile(this, xml, filename, temp));
2858				}));
2859			}
2860		}
2861	}
2862	catch(e)
2863	{
2864		// ignore
2865	}
2866};
2867
2868App.prototype.loadGapi = function(then)
2869{
2870	if (typeof gapi !== 'undefined')
2871	{
2872		gapi.load(((urlParams['picker'] != '0') ? 'picker,': '') + App.GOOGLE_APIS, then);
2873	}
2874};
2875
2876/**
2877 * Main function. Program starts here.
2878 *
2879 * @param {number} dx X-coordinate of the translation.
2880 * @param {number} dy Y-coordinate of the translation.
2881 */
2882App.prototype.load = function()
2883{
2884	// Checks if we're running in embedded mode
2885	if (urlParams['embed'] != '1')
2886	{
2887		if (this.spinner.spin(document.body, mxResources.get('starting')))
2888		{
2889			try
2890			{
2891				this.stateArg = (urlParams['state'] != null && this.drive != null) ? JSON.parse(decodeURIComponent(urlParams['state'])) : null;
2892			}
2893			catch (e)
2894			{
2895				// ignores invalid state args
2896			}
2897
2898			this.editor.graph.setEnabled(this.getCurrentFile() != null);
2899
2900			// Passes the userId from the state parameter to the client
2901			if ((window.location.hash == null || window.location.hash.length == 0) &&
2902				this.drive != null && this.stateArg != null && this.stateArg.userId != null)
2903			{
2904				this.drive.setUserId(this.stateArg.userId);
2905			}
2906
2907			// Legacy support for fileId parameter which is moved to the hash tag
2908			if (urlParams['fileId'] != null)
2909			{
2910				window.location.hash = 'G' + urlParams['fileId'];
2911				window.location.search = this.getSearch(['fileId']);
2912			}
2913			else
2914			{
2915				// Asynchronous or disabled loading of client
2916				if (this.drive == null)
2917				{
2918					if (this.mode == App.MODE_GOOGLE)
2919					{
2920						this.mode = null;
2921					}
2922
2923					this.start();
2924				}
2925				else
2926				{
2927					this.loadGapi(mxUtils.bind(this, function()
2928					{
2929						this.start();
2930					}));
2931				}
2932			}
2933		}
2934	}
2935	else
2936	{
2937		this.restoreLibraries();
2938
2939		if (urlParams['gapi'] == '1')
2940		{
2941			this.loadGapi(function() {});
2942		}
2943	}
2944};
2945
2946/**
2947 * Adds the listener for automatically saving the diagram for local changes.
2948 */
2949App.prototype.showRefreshDialog = function(title, message)
2950{
2951	if (!this.showingRefreshDialog)
2952	{
2953		this.showingRefreshDialog = true;
2954
2955		this.showError(title || mxResources.get('externalChanges'),
2956			message || mxResources.get('redirectToNewApp'),
2957			mxResources.get('refresh'), mxUtils.bind(this, function()
2958		{
2959			var file = this.getCurrentFile();
2960
2961			if (file != null)
2962			{
2963				file.setModified(false);
2964			}
2965
2966			this.spinner.spin(document.body, mxResources.get('connecting'));
2967			this.editor.graph.setEnabled(false);
2968			window.location.reload();
2969		}), null, null, null, null, null, 340, 180);
2970
2971		// Adds important notice to dialog
2972		if (this.dialog != null && this.dialog.container != null)
2973		{
2974			var alert = this.createRealtimeNotice();
2975			alert.style.left = '0';
2976			alert.style.right = '0';
2977			alert.style.borderRadius = '0';
2978			alert.style.borderLeftStyle = 'none';
2979			alert.style.borderRightStyle = 'none';
2980			alert.style.marginBottom = '26px';
2981			alert.style.padding = '8px 0 8px 0';
2982
2983			this.dialog.container.appendChild(alert);
2984		}
2985	}
2986};
2987
2988/**
2989 * Called in start after the spinner stops.
2990 */
2991App.prototype.showAlert = function(message)
2992{
2993	if (message != null && message.length > 0)
2994	{
2995		var div = document.createElement('div');
2996		div.className = 'geAlert';
2997		div.style.zIndex = 2e9;
2998		div.style.left = '50%';
2999		div.style.top = '-100%';
3000		//Limit width to 80% max with word wrapping
3001		div.style.maxWidth = '80%';
3002		div.style.width = 'max-content';
3003		div.style.whiteSpace = 'pre-wrap';
3004		mxUtils.setPrefixedStyle(div.style, 'transform', 'translate(-50%,0%)');
3005		mxUtils.setPrefixedStyle(div.style, 'transition', 'all 1s ease');
3006
3007		div.innerHTML = message;
3008
3009		var close = document.createElement('a');
3010		close.className = 'geAlertLink';
3011		close.style.textAlign = 'right';
3012		close.style.marginTop = '20px';
3013		close.style.display = 'block';
3014		close.setAttribute('title', mxResources.get('close'));
3015		close.innerHTML = mxResources.get('close');
3016		div.appendChild(close);
3017
3018		mxEvent.addListener(close, 'click', function(evt)
3019		{
3020			if (div.parentNode != null)
3021			{
3022				div.parentNode.removeChild(div);
3023				mxEvent.consume(evt);
3024			}
3025		});
3026
3027		document.body.appendChild(div);
3028
3029		// Delayed to get smoother animation after DOM rendering
3030		window.setTimeout(function()
3031		{
3032			div.style.top = '30px';
3033		}, 10);
3034
3035		// Fades out the alert after 15 secs
3036		window.setTimeout(function()
3037		{
3038			mxUtils.setPrefixedStyle(div.style, 'transition', 'all 2s ease');
3039			div.style.opacity = '0';
3040
3041			window.setTimeout(function()
3042			{
3043				if (div.parentNode != null)
3044				{
3045					div.parentNode.removeChild(div);
3046				}
3047			}, 2000);
3048		}, 15000);
3049	}
3050};
3051
3052/**
3053 * Translates this point by the given vector.
3054 *
3055 * @param {number} dx X-coordinate of the translation.
3056 * @param {number} dy Y-coordinate of the translation.
3057 */
3058App.prototype.start = function()
3059{
3060	if (this.bg != null && this.bg.parentNode != null)
3061	{
3062		this.bg.parentNode.removeChild(this.bg);
3063	}
3064
3065	this.restoreLibraries();
3066	this.spinner.stop();
3067
3068	try
3069	{
3070		// Handles all errors
3071		var ui = this;
3072
3073		window.onerror = function(message, url, linenumber, colno, err)
3074		{
3075			// Ignores Grammarly error [1344]
3076			if (message != 'ResizeObserver loop limit exceeded')
3077			{
3078				EditorUi.logError('Uncaught: ' + ((message != null) ? message : ''),
3079					url, linenumber, colno, err, null, true);
3080				ui.handleError({message: message}, mxResources.get('unknownError'),
3081					null, null, null, null, true);
3082			}
3083		};
3084
3085		// Listens to changes of the hash if not in embed or client mode
3086		if (urlParams['client'] != '1' && urlParams['embed'] != '1')
3087		{
3088			// Installs listener to claim current draft if there is one
3089			try
3090			{
3091				if (isLocalStorage)
3092				{
3093					window.addEventListener('storage', mxUtils.bind(this, function(evt)
3094					{
3095						var file = this.getCurrentFile();
3096						EditorUi.debug('storage event', evt, file);
3097
3098						if (file != null && evt.key == '.draft-alive-check' && evt.newValue != null && file.draftId != null)
3099						{
3100							this.draftAliveCheck = evt.newValue;
3101							file.saveDraft();
3102						}
3103					}));
3104				}
3105
3106				if (!mxClient.IS_CHROMEAPP && !EditorUi.isElectronApp && !this.isOfflineApp() &&
3107					urlParams['open'] == null && /www\.draw\.io$/.test(window.location.hostname) &&
3108					(!this.editor.chromeless || this.editor.editable))
3109				{
3110					this.showNameChangeBanner();
3111				}
3112			}
3113			catch (e)
3114			{
3115				// ignore
3116			}
3117
3118			mxEvent.addListener(window, 'hashchange', mxUtils.bind(this, function(evt)
3119			{
3120				try
3121				{
3122					this.hideDialog();
3123					var id = this.getDiagramId();
3124					var file = this.getCurrentFile();
3125
3126					if (file == null || file.getHash() != id)
3127					{
3128						this.loadFile(id, true);
3129					}
3130				}
3131				catch (e)
3132				{
3133					// Workaround for possible scrollWidth of null in Dialog ctor
3134					if (document.body != null)
3135					{
3136						this.handleError(e, mxResources.get('errorLoadingFile'), mxUtils.bind(this, function()
3137						{
3138							var file = this.getCurrentFile();
3139							window.location.hash = (file != null) ? file.getHash() : '';
3140						}));
3141					}
3142				}
3143			}));
3144		}
3145
3146		// Descriptor for CSV import
3147		if ((window.location.hash == null || window.location.hash.length <= 1) && urlParams['desc'] != null)
3148		{
3149			try
3150			{
3151				this.loadDescriptor(JSON.parse(Graph.decompress(urlParams['desc'])),
3152					null, mxUtils.bind(this, function(e)
3153				{
3154					this.handleError(e, mxResources.get('errorLoadingFile'));
3155				}));
3156			}
3157			catch (e)
3158			{
3159				this.handleError(e, mxResources.get('errorLoadingFile'));
3160			}
3161		}
3162		// Redirects old url URL parameter to new #U format
3163		else if ((window.location.hash == null || window.location.hash.length <= 1) && urlParams['url'] != null)
3164		{
3165			this.loadFile('U' + urlParams['url'], true);
3166		}
3167		else if (this.getCurrentFile() == null)
3168		{
3169			var done = mxUtils.bind(this, function()
3170			{
3171				// Starts in client mode and waits for data
3172				if (urlParams['client'] == '1' && (window.location.hash == null ||
3173					window.location.hash.length == 0 || window.location.hash.substring(0, 2) == '#P'))
3174				{
3175					var doLoadFile = mxUtils.bind(this, function(xml)
3176					{
3177						// Extracts graph model from PNG
3178						if (xml.substring(0, 22) == 'data:image/png;base64,')
3179						{
3180							xml = this.extractGraphModelFromPng(xml);
3181						}
3182
3183						var title = urlParams['title'];
3184
3185						if (title != null)
3186						{
3187							title = decodeURIComponent(title);
3188						}
3189						else
3190						{
3191							title = this.defaultFilename;
3192						}
3193
3194						var file = new LocalFile(this, xml, title, true);
3195
3196						if (window.location.hash != null && window.location.hash.substring(0, 2) == '#P')
3197						{
3198							file.getHash = function()
3199							{
3200								return window.location.hash.substring(1);
3201							};
3202						}
3203
3204						this.fileLoaded(file);
3205						this.getCurrentFile().setModified(!this.editor.chromeless);
3206					});
3207
3208					var parent = window.opener || window.parent;
3209
3210					if (parent != window)
3211					{
3212						var value = urlParams['create'];
3213
3214						if (value != null)
3215						{
3216							doLoadFile(parent[decodeURIComponent(value)]);
3217						}
3218						else
3219						{
3220							value = urlParams['data'];
3221
3222							if (value != null)
3223							{
3224								doLoadFile(decodeURIComponent(value));
3225							}
3226							else
3227							{
3228								this.installMessageHandler(mxUtils.bind(this, function(xml, evt)
3229								{
3230									// Ignores messages from other windows
3231									if (evt.source == parent)
3232									{
3233										doLoadFile(xml);
3234									}
3235								}));
3236							}
3237						}
3238					}
3239				}
3240				// Checks if no earlier loading errors are showing
3241				else if (this.dialog == null)
3242				{
3243					if (urlParams['demo'] == '1')
3244					{
3245						var prev = Editor.useLocalStorage;
3246						this.createFile(this.defaultFilename, null, null, null, null, null, null, true);
3247						Editor.useLocalStorage = prev;
3248					}
3249					else
3250					{
3251						var waiting = false;
3252
3253						// Checks if we're waiting for some asynchronous file to be loaded
3254						// Cross-domain window access is not allowed in FF, so if we
3255						// were opened from another domain then this will fail.
3256						try
3257						{
3258							waiting = window.opener != null && window.opener.openFile != null;
3259						}
3260						catch(e)
3261						{
3262							// ignore
3263						}
3264
3265						if (waiting)
3266						{
3267							// Spinner is stopped in App.open
3268							this.spinner.spin(document.body, mxResources.get('loading'))
3269						}
3270						else
3271						{
3272							var id = this.getDiagramId();
3273
3274
3275							if (EditorUi.enableDrafts && (urlParams['mode'] == null || EditorUi.isElectronApp) &&
3276								this.getServiceName() == 'draw.io' && (id == null || id.length == 0) &&
3277								!this.editor.isChromelessView())
3278							{
3279								this.checkDrafts();
3280							}
3281							else if (id != null && id.length > 0)
3282							{
3283								this.loadFile(id, null, null, mxUtils.bind(this, function()
3284								{
3285									var temp = decodeURIComponent(urlParams['viewbox'] || '');
3286
3287									if (temp != '')
3288									{
3289										try
3290										{
3291											var bounds = JSON.parse(temp);
3292											this.editor.graph.fitWindow(bounds, bounds.border);
3293										}
3294										catch (e)
3295										{
3296											// Ignore invalid viewport
3297											console.error(e);
3298										}
3299									}
3300								}));
3301							}
3302							else if (urlParams['splash'] != '0')
3303							{
3304								this.loadFile();
3305							}
3306							else
3307							{
3308								this.createFile(this.defaultFilename, this.getFileData(), null, null, null, null, null, true);
3309							}
3310						}
3311					}
3312				}
3313			});
3314
3315			var value = decodeURIComponent(urlParams['create'] || '');
3316
3317			if ((window.location.hash == null || window.location.hash.length <= 1) &&
3318				value != null && value.length > 0 && this.spinner.spin(document.body, mxResources.get('loading')))
3319			{
3320				var reconnect = mxUtils.bind(this, function()
3321				{
3322					// Removes URL parameter and reloads the page
3323					if (this.spinner.spin(document.body, mxResources.get('reconnecting')))
3324					{
3325						window.location.search = this.getSearch(['create', 'title']);
3326					};
3327				});
3328
3329				var showCreateDialog = mxUtils.bind(this, function(xml)
3330				{
3331					this.spinner.stop();
3332
3333					// Resets mode for dialog - local file is only for preview
3334					if (urlParams['splash'] != '0')
3335					{
3336						this.fileLoaded(new LocalFile(this, xml, null));
3337
3338						this.editor.graph.setEnabled(false);
3339						this.mode = urlParams['mode'];
3340						var title = urlParams['title'];
3341
3342						if (title != null)
3343						{
3344							title = decodeURIComponent(title);
3345						}
3346						else
3347						{
3348							title = this.defaultFilename;
3349						}
3350
3351						var serviceCount = this.getServiceCount(true);
3352
3353						if (isLocalStorage)
3354						{
3355							serviceCount++;
3356						}
3357
3358						var rowLimit = (serviceCount <= 4) ? 2 : (serviceCount > 6 ? 4 : 3);
3359
3360						var dlg = new CreateDialog(this, title, mxUtils.bind(this, function(filename, mode)
3361						{
3362							if (mode == null)
3363							{
3364								this.hideDialog();
3365								var prev = Editor.useLocalStorage;
3366								this.createFile((filename.length > 0) ? filename : this.defaultFilename,
3367									this.getFileData(), null, null, null, true, null, true);
3368								Editor.useLocalStorage = prev;
3369							}
3370							else
3371							{
3372								this.pickFolder(mode, mxUtils.bind(this, function(folderId)
3373								{
3374									this.createFile(filename, this.getFileData(true),
3375										null, mode, null, true, folderId);
3376								}));
3377							}
3378						}), null, null, null, null, urlParams['browser'] == '1',
3379							null, null, true, rowLimit, null, null, null,
3380							this.editor.fileExtensions);
3381						this.showDialog(dlg.container, 400, (serviceCount > rowLimit) ? 390 : 270,
3382							true, false, mxUtils.bind(this, function(cancel)
3383						{
3384							if (cancel && this.getCurrentFile() == null)
3385							{
3386								this.showSplash();
3387							}
3388						}));
3389						dlg.init();
3390					}
3391				});
3392
3393				value = decodeURIComponent(value);
3394
3395				if (value.substring(0, 7) != 'http://' && value.substring(0, 8) != 'https://')
3396				{
3397					// Cross-domain window access is not allowed in FF, so if we
3398					// were opened from another domain then this will fail.
3399					try
3400					{
3401						if (window.opener != null && window.opener[value] != null)
3402						{
3403							showCreateDialog(window.opener[value]);
3404						}
3405						else
3406						{
3407							this.handleError(null, mxResources.get('errorLoadingFile'));
3408						}
3409					}
3410					catch (e)
3411					{
3412						this.handleError(e, mxResources.get('errorLoadingFile'));
3413					}
3414				}
3415				else
3416				{
3417					this.loadTemplate(value, function(text)
3418					{
3419						showCreateDialog(text);
3420					}, mxUtils.bind(this, function()
3421					{
3422						this.handleError(null, mxResources.get('errorLoadingFile'), reconnect);
3423					}));
3424				}
3425			}
3426			else
3427			{
3428				// Passes the fileId from the state parameter to the hash tag and reloads
3429				// the page without the state parameter
3430				if ((window.location.hash == null || window.location.hash.length <= 1) &&
3431					urlParams['state'] != null && this.stateArg != null && this.stateArg.action == 'open')
3432				{
3433					if (this.stateArg.ids != null)
3434					{
3435						if (window.history && window.history.replaceState)
3436						{
3437							// Removes state URL parameter without reloading the page
3438							window.history.replaceState(null, null, window.location.pathname +
3439								this.getSearch(['state']));
3440						}
3441
3442						window.location.hash = 'G' + this.stateArg.ids[0];
3443					}
3444				}
3445				else if ((window.location.hash == null || window.location.hash.length <= 1) &&
3446					this.drive != null && this.stateArg != null && this.stateArg.action == 'create')
3447				{
3448					if (window.history && window.history.replaceState)
3449					{
3450						// Removes state URL parameter without reloading the page
3451						window.history.replaceState(null, null, window.location.pathname +
3452							this.getSearch(['state']));
3453					}
3454
3455					this.setMode(App.MODE_GOOGLE);
3456
3457					if (urlParams['splash'] == '0')
3458					{
3459						this.createFile((urlParams['title'] != null) ?
3460							decodeURIComponent(urlParams['title']) :
3461							this.defaultFilename);
3462					}
3463					else
3464					{
3465						this.actions.get('new').funct();
3466					}
3467				}
3468				else
3469				{
3470					// Removes open URL parameter. Hash is also updated in Init to load client.
3471					if (urlParams['open'] != null && window.history && window.history.replaceState)
3472					{
3473						window.history.replaceState(null, null, window.location.pathname +
3474							this.getSearch(['open']));
3475						window.location.hash = urlParams['open'];
3476					}
3477
3478					done();
3479				}
3480			}
3481		}
3482	}
3483	catch (e)
3484	{
3485		this.handleError(e);
3486	}
3487};
3488
3489/**
3490 * Checks for orphaned drafts.
3491 */
3492App.prototype.loadDraft = function(xml, success)
3493{
3494	this.createFile(this.defaultFilename, xml, null, null, mxUtils.bind(this, function()
3495	{
3496		window.setTimeout(mxUtils.bind(this, function()
3497		{
3498			var file = this.getCurrentFile();
3499
3500			if (file != null)
3501			{
3502				file.fileChanged();
3503
3504				if (success != null)
3505				{
3506					success();
3507				}
3508			}
3509		}), 0);
3510	}), null, null, true);
3511};
3512
3513/**
3514 * Checks for orphaned drafts.
3515 */
3516App.prototype.checkDrafts = function()
3517{
3518	try
3519	{
3520		// Triggers storage event for other windows to mark active drafts
3521		var guid = Editor.guid();
3522		localStorage.setItem('.draft-alive-check', guid);
3523
3524		window.setTimeout(mxUtils.bind(this, function()
3525		{
3526			localStorage.removeItem('.draft-alive-check');
3527
3528			this.getDatabaseItems(mxUtils.bind(this, function(items)
3529			{
3530				// Collects orphaned drafts
3531				var drafts = [];
3532
3533				for (var i = 0; i < items.length; i++)
3534				{
3535					try
3536					{
3537						var key = items[i].key;
3538
3539						if (key != null && key.substring(0, 7) == '.draft_')
3540						{
3541							var obj = JSON.parse(items[i].data);
3542
3543							if (obj != null && obj.type == 'draft' && obj.aliveCheck != guid)
3544							{
3545								obj.key = key;
3546								drafts.push(obj);
3547							}
3548						}
3549					}
3550					catch (e)
3551					{
3552						// ignore
3553					}
3554				}
3555
3556				if (drafts.length == 1)
3557				{
3558					this.loadDraft(drafts[0].data, mxUtils.bind(this, function()
3559					{
3560						this.removeDatabaseItem(drafts[0].key);
3561					}));
3562				}
3563				else if (drafts.length > 1)
3564				{
3565					var ts = new Date(drafts[0].modified);
3566
3567					var dlg = new DraftDialog(this, (drafts.length > 1) ? mxResources.get('selectDraft') :
3568						mxResources.get('draftFound', [ts.toLocaleDateString() + ' ' + ts.toLocaleTimeString()]),
3569						(drafts.length > 1) ? null : drafts[0].data, mxUtils.bind(this, function(index)
3570					{
3571						this.hideDialog();
3572						index = (index != '') ? index : 0;
3573
3574						this.loadDraft(drafts[index].data, mxUtils.bind(this, function()
3575						{
3576							this.removeDatabaseItem(drafts[index].key);
3577						}));
3578					}), mxUtils.bind(this, function(index, success)
3579					{
3580						index = (index != '') ? index : 0;
3581
3582						// Discard draft
3583						this.confirm(mxResources.get('areYouSure'), null, mxUtils.bind(this, function()
3584						{
3585							this.removeDatabaseItem(drafts[index].key);
3586
3587							if (success != null)
3588							{
3589								success();
3590							}
3591						}), mxResources.get('no'), mxResources.get('yes'));
3592					}), null, null, null, (drafts.length > 1) ? drafts : null);
3593					this.showDialog(dlg.container, 640, 480, true, false, mxUtils.bind(this, function(cancel)
3594					{
3595						if (urlParams['splash'] != '0')
3596						{
3597							this.loadFile();
3598						}
3599						else
3600						{
3601							this.createFile(this.defaultFilename, this.getFileData(), null, null, null, null, null, true);
3602						}
3603					}));
3604					dlg.init();
3605				}
3606				else if (urlParams['splash'] != '0')
3607				{
3608					this.loadFile();
3609				}
3610				else
3611				{
3612					this.createFile(this.defaultFilename, this.getFileData(), null, null, null, null, null, true);
3613				}
3614			}), mxUtils.bind(this, function()
3615			{
3616				if (urlParams['splash'] != '0')
3617				{
3618					this.loadFile();
3619				}
3620				else
3621				{
3622					this.createFile(this.defaultFilename, this.getFileData(), null, null, null, null, null, true);
3623				}
3624			}));
3625		}), 0);
3626	}
3627	catch (e)
3628	{
3629		// ignore
3630	}
3631};
3632
3633/**
3634 * Translates this point by the given vector.
3635 *
3636 * @param {number} dx X-coordinate of the translation.
3637 * @param {number} dy Y-coordinate of the translation.
3638 */
3639App.prototype.showSplash = function(force)
3640{
3641	//Splash dialog shouldn't be shownn when running without a file menu
3642	if (urlParams['noFileMenu'] == '1')
3643	{
3644		return;
3645	}
3646
3647	var serviceCount = this.getServiceCount(true);
3648
3649	var showSecondDialog = mxUtils.bind(this, function()
3650	{
3651		var dlg = new SplashDialog(this);
3652
3653		this.showDialog(dlg.container, 340, (mxClient.IS_CHROMEAPP || EditorUi.isElectronApp) ? 200 : 230, true, true,
3654			mxUtils.bind(this, function(cancel)
3655			{
3656				// Creates a blank diagram if the dialog is closed
3657				if (cancel && !mxClient.IS_CHROMEAPP)
3658				{
3659					var prev = Editor.useLocalStorage;
3660					this.createFile(this.defaultFilename + (EditorUi.isElectronApp? '.drawio' : ''), null, null, null, null, null, null,
3661						urlParams['local'] != '1');
3662					Editor.useLocalStorage = prev;
3663				}
3664			}), true);
3665	});
3666
3667	if (this.editor.isChromelessView())
3668	{
3669		this.handleError({message: mxResources.get('noFileSelected')},
3670			mxResources.get('errorLoadingFile'), mxUtils.bind(this, function()
3671		{
3672			this.showSplash();
3673		}));
3674	}
3675	else if (!mxClient.IS_CHROMEAPP && (this.mode == null || force))
3676	{
3677		var rowLimit = (serviceCount == 4) ? 2 : 3;
3678
3679		var dlg = new StorageDialog(this, mxUtils.bind(this, function()
3680		{
3681			this.hideDialog();
3682			showSecondDialog();
3683		}), rowLimit);
3684
3685		this.showDialog(dlg.container, (rowLimit < 3) ? 200 : 300,
3686			((serviceCount > 3) ? 320 : 210), true, false);
3687	}
3688	else if (urlParams['create'] == null)
3689	{
3690		showSecondDialog();
3691	}
3692};
3693
3694/**
3695 * Translates this point by the given vector.
3696 *
3697 * @param {number} dx X-coordinate of the translation.
3698 * @param {number} dy Y-coordinate of the translation.
3699 */
3700App.prototype.addLanguageMenu = function(elt, addLabel)
3701{
3702	var img = null;
3703	var langMenu = this.menus.get('language');
3704
3705	if (langMenu != null)
3706	{
3707		img = document.createElement('div');
3708		img.setAttribute('title', mxResources.get('language'));
3709		img.className = 'geIcon geSprite geSprite-globe';
3710		img.style.position = 'absolute';
3711		img.style.cursor = 'pointer';
3712		img.style.bottom = '20px';
3713		img.style.right = '20px';
3714
3715		if (addLabel)
3716		{
3717			img.style.direction = 'rtl';
3718			img.style.textAlign = 'right';
3719			img.style.right = '24px';
3720
3721			var label = document.createElement('span');
3722			label.style.display = 'inline-block';
3723			label.style.fontSize = '12px';
3724			label.style.margin = '5px 24px 0 0';
3725			label.style.color = 'gray';
3726			label.style.userSelect = 'none';
3727
3728			mxUtils.write(label, mxResources.get('language'));
3729			img.appendChild(label);
3730		}
3731
3732		mxEvent.addListener(img, 'click', mxUtils.bind(this, function(evt)
3733		{
3734			this.editor.graph.popupMenuHandler.hideMenu();
3735			var menu = new mxPopupMenu(this.menus.get('language').funct);
3736			menu.div.className += ' geMenubarMenu';
3737			menu.smartSeparators = true;
3738			menu.showDisabled = true;
3739			menu.autoExpand = true;
3740
3741			// Disables autoexpand and destroys menu when hidden
3742			menu.hideMenu = mxUtils.bind(this, function()
3743			{
3744				mxPopupMenu.prototype.hideMenu.apply(menu, arguments);
3745				menu.destroy();
3746			});
3747
3748			var offset = mxUtils.getOffset(img);
3749			menu.popup(offset.x, offset.y + img.offsetHeight, null, evt);
3750
3751			// Allows hiding by clicking on document
3752			this.setCurrentMenu(menu);
3753		}));
3754
3755		elt.appendChild(img);
3756	}
3757
3758	return img;
3759};
3760
3761/**
3762 * Loads the given file handle as a local file.
3763 */
3764App.prototype.loadFileSystemEntry = function(fileHandle, success, error)
3765{
3766	error = (error != null) ? error : mxUtils.bind(this, function(e)
3767	{
3768		this.handleError(e);
3769	});
3770
3771	try
3772	{
3773		fileHandle.getFile().then(mxUtils.bind(this, function(file)
3774		{
3775			var reader = new FileReader();
3776
3777			reader.onload = mxUtils.bind(this, function(e)
3778			{
3779				try
3780				{
3781					if (success != null)
3782					{
3783						var data = e.target.result;
3784
3785						if (file.type == 'image/png')
3786						{
3787							data = this.extractGraphModelFromPng(data);
3788						}
3789
3790						success(new LocalFile(this, data, file.name, null, fileHandle, file));
3791					}
3792					else
3793					{
3794						this.openFileHandle(e.target.result, file.name, file, false, fileHandle);
3795					}
3796				}
3797				catch(e)
3798				{
3799					error(e);
3800				}
3801			});
3802
3803			reader.onerror = error;
3804
3805			if ((file.type.substring(0, 5) === 'image' ||
3806				file.type === 'application/pdf') &&
3807				file.type.substring(0, 9) !== 'image/svg')
3808			{
3809				reader.readAsDataURL(file);
3810			}
3811			else
3812			{
3813				reader.readAsText(file);
3814			}
3815		}), error);
3816	}
3817	catch (e)
3818	{
3819		error(e);
3820	}
3821};
3822
3823/**
3824 * Loads the given file handle as a local file.
3825 */
3826App.prototype.createFileSystemOptions = function(name)
3827{
3828	var ext = [];
3829	var temp = null;
3830
3831	if (name != null)
3832	{
3833		var idx = name.lastIndexOf('.');
3834
3835		if (idx > 0)
3836		{
3837			temp = name.substring(idx + 1);
3838		}
3839	}
3840
3841	for (var i = 0; i < this.editor.diagramFileTypes.length; i++)
3842	{
3843		var obj = {description: mxResources.get(this.editor.diagramFileTypes[i].description) +
3844			((mxClient.IS_MAC) ? ' (.' + this.editor.diagramFileTypes[i].extension + ')' : ''),
3845			accept: {}};
3846		obj.accept[this.editor.diagramFileTypes[i].mimeType] = ['.' + this.editor.diagramFileTypes[i].extension];
3847
3848		if (this.editor.diagramFileTypes[i].extension == temp)
3849		{
3850			ext.splice(0, 0, obj);
3851		}
3852		else
3853		{
3854			if (this.editor.diagramFileTypes[i].extension == temp)
3855			{
3856				ext.splice(0, 0, obj);
3857			}
3858			else
3859			{
3860				ext.push(obj);
3861			}
3862		}
3863	}
3864
3865	// TODO: Specify default filename
3866	return {types: ext, fileName: name};
3867};
3868
3869/**
3870 * Loads the given file handle as a local file.
3871 */
3872App.prototype.showSaveFilePicker = function(success, error, opts)
3873{
3874	error = (error != null) ? error : mxUtils.bind(this, function(e)
3875	{
3876		if (e.name != 'AbortError')
3877		{
3878			this.handleError(e);
3879		}
3880	});
3881
3882	opts = (opts != null) ? opts : this.createFileSystemOptions();
3883
3884	window.showSaveFilePicker(opts).then(mxUtils.bind(this, function(fileHandle)
3885	{
3886		if (fileHandle != null)
3887		{
3888			fileHandle.getFile().then(mxUtils.bind(this, function(desc)
3889			{
3890				success(fileHandle, desc);
3891			}), error);
3892		}
3893	}), error);
3894};
3895
3896/**
3897 * Translates this point by the given vector.
3898 *
3899 * @param {number} dx X-coordinate of the translation.
3900 * @param {number} dy Y-coordinate of the translation.
3901 */
3902App.prototype.pickFile = function(mode)
3903{
3904	try
3905	{
3906		mode = (mode != null) ? mode : this.mode;
3907
3908		if (mode == App.MODE_GOOGLE)
3909		{
3910			if (this.drive != null && typeof(google) != 'undefined' && typeof(google.picker) != 'undefined')
3911			{
3912				this.drive.pickFile();
3913			}
3914			else
3915			{
3916				this.openLink('https://drive.google.com');
3917			}
3918		}
3919		else
3920		{
3921			var peer = this.getPeerForMode(mode);
3922
3923			if (peer != null)
3924			{
3925				peer.pickFile();
3926			}
3927			else if (mode == App.MODE_DEVICE && EditorUi.nativeFileSupport)
3928			{
3929				window.showOpenFilePicker().then(mxUtils.bind(this, function(fileHandles)
3930				{
3931					if (fileHandles != null && fileHandles.length > 0 &&
3932						this.spinner.spin(document.body, mxResources.get('loading')))
3933					{
3934						this.loadFileSystemEntry(fileHandles[0]);
3935					}
3936				}), mxUtils.bind(this, function(e)
3937				{
3938					if (e.name != 'AbortError')
3939					{
3940						this.handleError(e);
3941					}
3942				}));
3943			}
3944			else if (mode == App.MODE_DEVICE && Graph.fileSupport)
3945			{
3946				if (this.openFileInputElt == null)
3947				{
3948					var input = document.createElement('input');
3949					input.setAttribute('type', 'file');
3950
3951					mxEvent.addListener(input, 'change', mxUtils.bind(this, function()
3952					{
3953						if (input.files != null)
3954						{
3955							this.openFiles(input.files);
3956
3957				    		// Resets input to force change event for
3958							// same file (type reset required for IE)
3959							input.type = '';
3960							input.type = 'file';
3961				    		input.value = '';
3962						}
3963					}));
3964
3965					input.style.display = 'none';
3966					document.body.appendChild(input);
3967					this.openFileInputElt = input;
3968				}
3969
3970				this.openFileInputElt.click();
3971			}
3972			else
3973			{
3974				this.hideDialog();
3975				window.openNew = this.getCurrentFile() != null && !this.isDiagramEmpty();
3976				window.baseUrl = this.getUrl();
3977				window.openKey = 'open';
3978
3979				window.listBrowserFiles = mxUtils.bind(this, function(success, error)
3980				{
3981					StorageFile.listFiles(this, 'F', success, error);
3982				});
3983
3984				window.openBrowserFile = mxUtils.bind(this, function(title, success, error)
3985				{
3986					StorageFile.getFileContent(this, title, success, error);
3987				});
3988
3989				window.deleteBrowserFile = mxUtils.bind(this, function(title, success, error)
3990				{
3991					StorageFile.deleteFile(this, title, success, error);
3992				});
3993
3994				var prevValue = Editor.useLocalStorage;
3995				Editor.useLocalStorage = (mode == App.MODE_BROWSER);
3996				this.openFile();
3997
3998				// Installs local handler for opened files in same window
3999				window.openFile.setConsumer(mxUtils.bind(this, function(xml, filename)
4000				{
4001					var doOpenFile = mxUtils.bind(this, function()
4002					{
4003						// Replaces PNG with XML extension
4004						var dot = !this.useCanvasForExport && filename.substring(filename.length - 4) == '.png';
4005
4006						if (dot)
4007						{
4008							filename = filename.substring(0, filename.length - 4) + '.drawio';
4009						}
4010
4011						this.fileLoaded((mode == App.MODE_BROWSER) ?
4012							new StorageFile(this, xml, filename) :
4013							new LocalFile(this, xml, filename));
4014					});
4015
4016					var currentFile = this.getCurrentFile();
4017
4018					if (currentFile == null || !currentFile.isModified())
4019					{
4020						doOpenFile();
4021					}
4022					else
4023					{
4024						this.confirm(mxResources.get('allChangesLost'), null, doOpenFile,
4025							mxResources.get('cancel'), mxResources.get('discardChanges'));
4026					}
4027				}));
4028
4029				// Extends dialog close to show splash screen
4030				var dlg = this.dialog;
4031				var dlgClose = dlg.close;
4032
4033				this.dialog.close = mxUtils.bind(this, function(cancel)
4034				{
4035					Editor.useLocalStorage = prevValue;
4036					dlgClose.apply(dlg, arguments);
4037
4038					if (this.getCurrentFile() == null)
4039					{
4040						this.showSplash();
4041					}
4042				});
4043			}
4044		}
4045	}
4046	catch (e)
4047	{
4048		this.handleError(e);
4049	}
4050};
4051
4052/**
4053 * Translates this point by the given vector.
4054 *
4055 * @param {number} dx X-coordinate of the translation.
4056 * @param {number} dy Y-coordinate of the translation.
4057 */
4058App.prototype.pickLibrary = function(mode)
4059{
4060	mode = (mode != null) ? mode : this.mode;
4061
4062	if (mode == App.MODE_GOOGLE || mode == App.MODE_DROPBOX || mode == App.MODE_ONEDRIVE ||
4063		mode == App.MODE_GITHUB || mode == App.MODE_GITLAB || mode == App.MODE_TRELLO ||
4064		mode == App.MODE_NOTION)
4065	{
4066		var peer = (mode == App.MODE_GOOGLE) ? this.drive :
4067			((mode == App.MODE_ONEDRIVE) ? this.oneDrive :
4068			((mode == App.MODE_GITHUB) ? this.gitHub :
4069			((mode == App.MODE_GITLAB) ? this.gitLab :
4070			((mode == App.MODE_TRELLO) ? this.trello :
4071			((mode == App.MODE_NOTION) ? this.notion :
4072			this.dropbox)))));
4073
4074		if (peer != null)
4075		{
4076			peer.pickLibrary(mxUtils.bind(this, function(id, optionalFile)
4077			{
4078				if (optionalFile != null)
4079				{
4080					try
4081					{
4082						this.loadLibrary(optionalFile);
4083					}
4084					catch (e)
4085					{
4086						this.handleError(e, mxResources.get('errorLoadingFile'));
4087					}
4088				}
4089				else
4090				{
4091					if (this.spinner.spin(document.body, mxResources.get('loading')))
4092					{
4093						peer.getLibrary(id, mxUtils.bind(this, function(file)
4094						{
4095							this.spinner.stop();
4096
4097							try
4098							{
4099								this.loadLibrary(file);
4100							}
4101							catch (e)
4102							{
4103								this.handleError(e, mxResources.get('errorLoadingFile'));
4104							}
4105						}), mxUtils.bind(this, function(resp)
4106						{
4107							this.handleError(resp, (resp != null) ? mxResources.get('errorLoadingFile') : null);
4108						}));
4109					}
4110				}
4111			}));
4112		}
4113	}
4114	else if (mode == App.MODE_DEVICE && Graph.fileSupport)
4115	{
4116		if (this.libFileInputElt == null)
4117		{
4118			var input = document.createElement('input');
4119			input.setAttribute('type', 'file');
4120
4121			mxEvent.addListener(input, 'change', mxUtils.bind(this, function()
4122			{
4123				if (input.files != null)
4124				{
4125					for (var i = 0; i < input.files.length; i++)
4126					{
4127						(mxUtils.bind(this, function(file)
4128						{
4129							var reader = new FileReader();
4130
4131							reader.onload = mxUtils.bind(this, function(e)
4132							{
4133								try
4134								{
4135									this.loadLibrary(new LocalLibrary(this, e.target.result, file.name));
4136								}
4137								catch (e)
4138								{
4139									this.handleError(e, mxResources.get('errorLoadingFile'));
4140								}
4141							});
4142
4143							reader.readAsText(file);
4144						}))(input.files[i]);
4145					}
4146
4147		    		// Resets input to force change event for same file (type reset required for IE)
4148					input.type = '';
4149					input.type = 'file';
4150		    		input.value = '';
4151				}
4152			}));
4153
4154			input.style.display = 'none';
4155			document.body.appendChild(input);
4156			this.libFileInputElt = input;
4157		}
4158
4159		this.libFileInputElt.click();
4160	}
4161	else
4162	{
4163		window.openNew = false;
4164		window.openKey = 'open';
4165
4166		window.listBrowserFiles = mxUtils.bind(this, function(success, error)
4167		{
4168			StorageFile.listFiles(this, 'L', success, error);
4169		});
4170
4171		window.openBrowserFile = mxUtils.bind(this, function(title, success, error)
4172		{
4173			StorageFile.getFileContent(this, title, success, error);
4174		});
4175
4176		window.deleteBrowserFile = mxUtils.bind(this, function(title, success, error)
4177		{
4178			StorageFile.deleteFile(this, title, success, error);
4179		});
4180
4181		var prevValue = Editor.useLocalStorage;
4182		Editor.useLocalStorage = mode == App.MODE_BROWSER;
4183
4184		// Closes dialog after open
4185		window.openFile = new OpenFile(mxUtils.bind(this, function(cancel)
4186		{
4187			this.hideDialog(cancel);
4188		}));
4189
4190		window.openFile.setConsumer(mxUtils.bind(this, function(xml, filename)
4191		{
4192			try
4193			{
4194				this.loadLibrary((mode == App.MODE_BROWSER) ? new StorageLibrary(this, xml, filename) :
4195					new LocalLibrary(this, xml, filename));
4196			}
4197			catch (e)
4198			{
4199				this.handleError(e, mxResources.get('errorLoadingFile'));
4200			}
4201		}));
4202
4203		// Removes openFile if dialog is closed
4204		this.showDialog(new OpenDialog(this).container, (Editor.useLocalStorage) ? 640 : 360,
4205			(Editor.useLocalStorage) ? 480 : 220, true, true, function()
4206		{
4207			Editor.useLocalStorage = prevValue;
4208			window.openFile = null;
4209		});
4210	}
4211};
4212
4213/**
4214 * Translates this point by the given vector.
4215 *
4216 * @param {number} dx X-coordinate of the translation.
4217 * @param {number} dy Y-coordinate of the translation.
4218 */
4219App.prototype.saveLibrary = function(name, images, file, mode, noSpin, noReload, fn)
4220{
4221	try
4222	{
4223		mode = (mode != null) ? mode : this.mode;
4224		noSpin = (noSpin != null) ? noSpin : false;
4225		noReload = (noReload != null) ? noReload : false;
4226		var xml = this.createLibraryDataFromImages(images);
4227
4228		var error = mxUtils.bind(this, function(resp)
4229		{
4230			this.spinner.stop();
4231
4232			if (fn != null)
4233			{
4234				fn();
4235			}
4236
4237			this.handleError(resp, (resp != null) ? mxResources.get('errorSavingFile') : null);
4238		});
4239
4240		// Handles special case for local libraries
4241		if (file == null && mode == App.MODE_DEVICE)
4242		{
4243			file = new LocalLibrary(this, xml, name);
4244		}
4245
4246		if (file == null)
4247		{
4248			this.pickFolder(mode, mxUtils.bind(this, function(folderId)
4249			{
4250				if (mode == App.MODE_GOOGLE && this.drive != null && this.spinner.spin(document.body, mxResources.get('inserting')))
4251				{
4252					this.drive.insertFile(name, xml, folderId, mxUtils.bind(this, function(newFile)
4253					{
4254						this.spinner.stop();
4255						this.hideDialog(true);
4256						this.libraryLoaded(newFile, images);
4257					}), error, this.drive.libraryMimeType);
4258				}
4259				else if (mode == App.MODE_GITHUB && this.gitHub != null && this.spinner.spin(document.body, mxResources.get('inserting')))
4260				{
4261					this.gitHub.insertLibrary(name, xml, mxUtils.bind(this, function(newFile)
4262					{
4263						this.spinner.stop();
4264						this.hideDialog(true);
4265						this.libraryLoaded(newFile, images);
4266					}), error, folderId);
4267				}
4268				else if (mode == App.MODE_GITLAB && this.gitLab != null && this.spinner.spin(document.body, mxResources.get('inserting')))
4269				{
4270					this.gitLab.insertLibrary(name, xml, mxUtils.bind(this, function(newFile)
4271					{
4272						this.spinner.stop();
4273						this.hideDialog(true);
4274						this.libraryLoaded(newFile, images);
4275					}), error, folderId);
4276				}
4277				else if (mode == App.MODE_NOTION && this.notion != null && this.spinner.spin(document.body, mxResources.get('inserting')))
4278				{
4279					this.notion.insertLibrary(name, xml, mxUtils.bind(this, function(newFile)
4280					{
4281						this.spinner.stop();
4282						this.hideDialog(true);
4283						this.libraryLoaded(newFile, images);
4284					}), error, folderId);
4285				}
4286				else if (mode == App.MODE_TRELLO && this.trello != null && this.spinner.spin(document.body, mxResources.get('inserting')))
4287				{
4288					this.trello.insertLibrary(name, xml, mxUtils.bind(this, function(newFile)
4289					{
4290						this.spinner.stop();
4291						this.hideDialog(true);
4292						this.libraryLoaded(newFile, images);
4293					}), error, folderId);
4294				}
4295				else if (mode == App.MODE_DROPBOX && this.dropbox != null && this.spinner.spin(document.body, mxResources.get('inserting')))
4296				{
4297					this.dropbox.insertLibrary(name, xml, mxUtils.bind(this, function(newFile)
4298					{
4299						this.spinner.stop();
4300						this.hideDialog(true);
4301						this.libraryLoaded(newFile, images);
4302					}), error, folderId);
4303				}
4304				else if (mode == App.MODE_ONEDRIVE && this.oneDrive != null && this.spinner.spin(document.body, mxResources.get('inserting')))
4305				{
4306					this.oneDrive.insertLibrary(name, xml, mxUtils.bind(this, function(newFile)
4307					{
4308						this.spinner.stop();
4309						this.hideDialog(true);
4310						this.libraryLoaded(newFile, images);
4311					}), error, folderId);
4312				}
4313				else if (mode == App.MODE_BROWSER)
4314				{
4315					var fn = mxUtils.bind(this, function()
4316					{
4317						var file = new StorageLibrary(this, xml, name);
4318
4319						// Inserts data into local storage
4320						file.saveFile(name, false, mxUtils.bind(this, function()
4321						{
4322							this.hideDialog(true);
4323							this.libraryLoaded(file, images);
4324						}), error);
4325					});
4326
4327					if (localStorage.getItem(name) == null)
4328					{
4329						fn();
4330					}
4331					else
4332					{
4333						this.confirm(mxResources.get('replaceIt', [name]), fn);
4334					}
4335				}
4336				else
4337				{
4338					this.handleError({message: mxResources.get('serviceUnavailableOrBlocked')});
4339				}
4340			}));
4341		}
4342		else if (noSpin || this.spinner.spin(document.body, mxResources.get('saving')))
4343		{
4344			file.setData(xml);
4345
4346			var doSave = mxUtils.bind(this, function()
4347			{
4348				file.save(true, mxUtils.bind(this, function(resp)
4349				{
4350					this.spinner.stop();
4351					this.hideDialog(true);
4352
4353					if (!noReload)
4354					{
4355						this.libraryLoaded(file, images);
4356					}
4357
4358					if (fn != null)
4359					{
4360						fn();
4361					}
4362				}), error);
4363			});
4364
4365			if (name != file.getTitle())
4366			{
4367				var oldHash = file.getHash();
4368
4369				file.rename(name, mxUtils.bind(this, function(resp)
4370				{
4371					// Change hash in stored settings
4372					if (file.constructor != LocalLibrary && oldHash != file.getHash())
4373					{
4374						mxSettings.removeCustomLibrary(oldHash);
4375						mxSettings.addCustomLibrary(file.getHash());
4376					}
4377
4378					// Workaround for library files changing hash so
4379					// the old library cannot be removed from the
4380					// sidebar using the updated file in libraryLoaded
4381					this.removeLibrarySidebar(oldHash);
4382
4383					doSave();
4384				}), error)
4385			}
4386			else
4387			{
4388				doSave();
4389			}
4390		}
4391	}
4392	catch (e)
4393	{
4394		this.handleError(e);
4395	}
4396};
4397
4398/**
4399 * Adds the label menu items to the given menu and parent.
4400 */
4401App.prototype.saveFile = function(forceDialog, success)
4402{
4403	var file = this.getCurrentFile();
4404
4405	if (file != null)
4406	{
4407		// FIXME: Invoke for local files
4408		var done = mxUtils.bind(this, function()
4409		{
4410			if (EditorUi.enableDrafts)
4411			{
4412				file.removeDraft();
4413			}
4414
4415			if (this.getCurrentFile() != file && !file.isModified())
4416			{
4417				// Workaround for possible status update while save as dialog is showing
4418				// is to show no saved status for device files
4419				if (file.getMode() != App.MODE_DEVICE)
4420				{
4421					this.editor.setStatus(mxUtils.htmlEntities(mxResources.get('allChangesSaved')));
4422				}
4423				else
4424				{
4425					this.editor.setStatus('');
4426				}
4427			}
4428
4429			if (success != null)
4430			{
4431				success();
4432			}
4433		});
4434
4435		if (!forceDialog && file.getTitle() != null && file.invalidFileHandle == null && this.mode != null)
4436		{
4437			this.save(file.getTitle(), done);
4438		}
4439		else if (file != null && file.constructor == LocalFile && file.fileHandle != null)
4440		{
4441			this.showSaveFilePicker(mxUtils.bind(this, function(fileHandle, desc)
4442			{
4443				file.invalidFileHandle = null;
4444				file.fileHandle = fileHandle;
4445				file.title = desc.name;
4446				file.desc = desc;
4447				this.save(desc.name, done);
4448			}), null, this.createFileSystemOptions(file.getTitle()));
4449		}
4450		else
4451		{
4452			var filename = (file.getTitle() != null) ? file.getTitle() : this.defaultFilename;
4453			var allowTab = !mxClient.IS_IOS || !navigator.standalone;
4454			var prev = this.mode;
4455			var serviceCount = this.getServiceCount(true);
4456
4457			if (isLocalStorage)
4458			{
4459				serviceCount++;
4460			}
4461
4462			var rowLimit = (serviceCount <= 4) ? 2 : (serviceCount > 6 ? 4 : 3);
4463
4464			var dlg = new CreateDialog(this, filename, mxUtils.bind(this, function(name, mode, input)
4465			{
4466				if (name != null && name.length > 0)
4467				{
4468					// Handles special case where PDF export is detected
4469					if (/(\.pdf)$/i.test(name))
4470					{
4471						this.confirm(mxResources.get('didYouMeanToExportToPdf'), mxUtils.bind(this, function()
4472						{
4473							this.hideDialog();
4474							this.actions.get('exportPdf').funct();
4475						}), mxUtils.bind(this, function()
4476						{
4477							input.value = name.split('.').slice(0, -1).join('.');
4478							input.focus();
4479
4480							if (mxClient.IS_GC || mxClient.IS_FF || document.documentMode >= 5)
4481							{
4482								input.select();
4483							}
4484							else
4485							{
4486								document.execCommand('selectAll', false, null);
4487							}
4488						}), mxResources.get('yes'), mxResources.get('no'));
4489					}
4490					else
4491					{
4492						this.hideDialog();
4493
4494						if (prev == null && mode == App.MODE_DEVICE)
4495						{
4496							if (file != null && EditorUi.nativeFileSupport)
4497							{
4498								this.showSaveFilePicker(mxUtils.bind(this, function(fileHandle, desc)
4499								{
4500									file.fileHandle = fileHandle;
4501									file.mode = App.MODE_DEVICE;
4502									file.title = desc.name;
4503									file.desc = desc;
4504
4505									this.setMode(App.MODE_DEVICE);
4506									this.save(desc.name, done);
4507								}), mxUtils.bind(this, function(e)
4508								{
4509									if (e.name != 'AbortError')
4510									{
4511										this.handleError(e);
4512									}
4513								}), this.createFileSystemOptions(name));
4514							}
4515							else
4516							{
4517								this.setMode(App.MODE_DEVICE);
4518								this.save(name, done);
4519							}
4520						}
4521						else if (mode == 'download')
4522						{
4523							var tmp = new LocalFile(this, null, name);
4524							tmp.save();
4525						}
4526						else if (mode == '_blank')
4527						{
4528							window.openFile = new OpenFile(function()
4529							{
4530								window.openFile = null;
4531							});
4532
4533							// Do not use a filename to use undefined mode
4534							window.openFile.setData(this.getFileData(true));
4535							this.openLink(this.getUrl(window.location.pathname), null, true);
4536						}
4537						else if (prev != mode)
4538						{
4539							this.pickFolder(mode, mxUtils.bind(this, function(folderId)
4540							{
4541								this.createFile(name, this.getFileData(/(\.xml)$/i.test(name) ||
4542									name.indexOf('.') < 0 || /(\.drawio)$/i.test(name),
4543									/(\.svg)$/i.test(name), /(\.html)$/i.test(name)),
4544									null, mode, done, this.mode == null, folderId);
4545							}));
4546						}
4547						else if (mode != null)
4548						{
4549							this.save(name, done);
4550						}
4551					}
4552				}
4553			}), mxUtils.bind(this, function()
4554			{
4555				this.hideDialog();
4556			}), mxResources.get('saveAs'), mxResources.get('download'), null, null, allowTab,
4557				null, true, rowLimit, null, null, null, this.editor.fileExtensions, false);
4558			this.showDialog(dlg.container, 400, (serviceCount > rowLimit) ? 390 : 270, true, true);
4559			dlg.init();
4560		}
4561	}
4562};
4563
4564/**
4565 * Translates this point by the given vector.
4566 *
4567 * @param {number} dx X-coordinate of the translation.
4568 * @param {number} dy Y-coordinate of the translation.
4569 */
4570App.prototype.loadTemplate = function(url, onload, onerror, templateFilename, asLibrary)
4571{
4572	var base64 = false;
4573	var realUrl = url;
4574
4575	if (!this.editor.isCorsEnabledForUrl(realUrl))
4576	{
4577		// Always uses base64 response to check magic numbers for file type
4578		var nocache = 't=' + new Date().getTime();
4579		realUrl = PROXY_URL + '?url=' + encodeURIComponent(url) + '&base64=1&' + nocache;
4580		base64 = true;
4581	}
4582
4583	var filterFn = (templateFilename != null) ? templateFilename : url;
4584
4585	this.editor.loadUrl(realUrl, mxUtils.bind(this, function(responseData)
4586	{
4587		try
4588		{
4589			var data = (!base64) ? responseData : ((window.atob && !mxClient.IS_IE && !mxClient.IS_IE11) ?
4590				atob(responseData) : Base64.decode(responseData));
4591			var isVisioFilename = /(\.v(dx|sdx?))($|\?)/i.test(filterFn) ||
4592				/(\.vs(x|sx?))($|\?)/i.test(filterFn);
4593
4594			if (isVisioFilename || this.isVisioData(data))
4595			{
4596				// Adds filename to control converter code
4597				if (!isVisioFilename)
4598				{
4599					if (asLibrary)
4600					{
4601						filterFn = this.isRemoteVisioData(data) ? 'raw.vss' : 'raw.vssx';
4602					}
4603					else
4604					{
4605						filterFn = this.isRemoteVisioData(data) ? 'raw.vsd' : 'raw.vsdx';
4606					}
4607				}
4608
4609				this.importVisio(this.base64ToBlob(responseData.substring(responseData.indexOf(',') + 1)), function(xml)
4610				{
4611					onload(xml);
4612				}, onerror, filterFn);
4613			}
4614			else if (!this.isOffline() && new XMLHttpRequest().upload && this.isRemoteFileFormat(data, filterFn))
4615			{
4616				// Asynchronous parsing via server
4617				this.parseFile(new Blob([data], {type: 'application/octet-stream'}), mxUtils.bind(this, function(xhr)
4618				{
4619					if (xhr.readyState == 4 && xhr.status >= 200 && xhr.status <= 299 &&
4620						xhr.responseText.substring(0, 13) == '<mxGraphModel')
4621					{
4622						onload(xhr.responseText);
4623					}
4624				}), url);
4625			}
4626			else if (this.isLucidChartData(data))
4627			{
4628				this.convertLucidChart(data, mxUtils.bind(this, function(xml)
4629				{
4630					onload(xml);
4631				}), mxUtils.bind(this, function(e)
4632				{
4633					onerror(e);
4634				}));
4635			}
4636			else
4637			{
4638				if (/(\.png)($|\?)/i.test(filterFn) || this.isPngData(data))
4639				{
4640					data = this.extractGraphModelFromPng(responseData);
4641				}
4642
4643				onload(data);
4644			}
4645		}
4646		catch (e)
4647		{
4648			onerror(e);
4649		}
4650	}), onerror, /(\.png)($|\?)/i.test(filterFn) || /(\.v(dx|sdx?))($|\?)/i.test(filterFn) ||
4651		/(\.vs(x|sx?))($|\?)/i.test(filterFn), null, null, base64);
4652};
4653
4654/**
4655 * Translates this point by the given vector.
4656 *
4657 * @param {number} dx X-coordinate of the translation.
4658 * @param {number} dy Y-coordinate of the translation.
4659 */
4660App.prototype.getPeerForMode = function(mode)
4661{
4662	if (mode == App.MODE_GOOGLE)
4663	{
4664		return this.drive;
4665	}
4666	else if (mode == App.MODE_GITHUB)
4667	{
4668		return this.gitHub;
4669	}
4670	else if (mode == App.MODE_GITLAB)
4671	{
4672		return this.gitLab;
4673	}
4674	else if (mode == App.MODE_DROPBOX)
4675	{
4676		return this.dropbox;
4677	}
4678	else if (mode == App.MODE_ONEDRIVE)
4679	{
4680		return this.oneDrive;
4681	}
4682	else if (mode == App.MODE_TRELLO)
4683	{
4684		return this.trello;
4685	}
4686	else if (mode == App.MODE_NOTION)
4687	{
4688		return this.notion;
4689	}
4690	else
4691	{
4692		return null;
4693	}
4694};
4695
4696/**
4697 * Translates this point by the given vector.
4698 *
4699 * @param {number} dx X-coordinate of the translation.
4700 * @param {number} dy Y-coordinate of the translation.
4701 */
4702App.prototype.createFile = function(title, data, libs, mode, done, replace, folderId, tempFile, clibs)
4703{
4704	mode = (tempFile) ? null : ((mode != null) ? mode : this.mode);
4705
4706	if (title != null && this.spinner.spin(document.body, mxResources.get('inserting')))
4707	{
4708		data = (data != null) ? data : this.emptyDiagramXml;
4709
4710		var complete = mxUtils.bind(this, function()
4711		{
4712			this.spinner.stop();
4713		});
4714
4715		var error = mxUtils.bind(this, function(resp)
4716		{
4717			complete();
4718
4719			if (resp == null && this.getCurrentFile() == null && this.dialog == null)
4720			{
4721				this.showSplash();
4722			}
4723			else if (resp != null)
4724			{
4725				this.handleError(resp);
4726			}
4727		});
4728
4729		try
4730		{
4731			if (mode == App.MODE_GOOGLE && this.drive != null)
4732			{
4733				if (folderId == null && this.stateArg != null && this.stateArg.folderId != null)
4734				{
4735					folderId = this.stateArg.folderId;
4736				}
4737
4738				this.drive.insertFile(title, data, folderId, mxUtils.bind(this, function(file)
4739				{
4740					complete();
4741					this.fileCreated(file, libs, replace, done, clibs);
4742				}), error);
4743			}
4744			else if (mode == App.MODE_GITHUB && this.gitHub != null)
4745			{
4746				this.gitHub.insertFile(title, data, mxUtils.bind(this, function(file)
4747				{
4748					complete();
4749					this.fileCreated(file, libs, replace, done, clibs);
4750				}), error, false, folderId);
4751			}
4752			else if (mode == App.MODE_GITLAB && this.gitLab != null)
4753			{
4754				this.gitLab.insertFile(title, data, mxUtils.bind(this, function(file)
4755				{
4756					complete();
4757					this.fileCreated(file, libs, replace, done, clibs);
4758				}), error, false, folderId);
4759			}
4760			else if (mode == App.MODE_NOTION && this.notion != null)
4761			{
4762				this.notion.insertFile(title, data, mxUtils.bind(this, function(file)
4763				{
4764					complete();
4765					this.fileCreated(file, libs, replace, done, clibs);
4766				}), error, false, folderId);
4767			}
4768			else if (mode == App.MODE_TRELLO && this.trello != null)
4769			{
4770				this.trello.insertFile(title, data, mxUtils.bind(this, function(file)
4771				{
4772					complete();
4773					this.fileCreated(file, libs, replace, done, clibs);
4774				}), error, false, folderId);
4775			}
4776			else if (mode == App.MODE_DROPBOX && this.dropbox != null)
4777			{
4778				this.dropbox.insertFile(title, data, mxUtils.bind(this, function(file)
4779				{
4780					complete();
4781					this.fileCreated(file, libs, replace, done, clibs);
4782				}), error);
4783			}
4784			else if (mode == App.MODE_ONEDRIVE && this.oneDrive != null)
4785			{
4786				this.oneDrive.insertFile(title, data, mxUtils.bind(this, function(file)
4787				{
4788					complete();
4789					this.fileCreated(file, libs, replace, done, clibs);
4790				}), error, false, folderId);
4791			}
4792			else if (mode == App.MODE_BROWSER)
4793			{
4794				StorageFile.insertFile(this, title, data, mxUtils.bind(this, function(file)
4795				{
4796					complete();
4797					this.fileCreated(file, libs, replace, done, clibs);
4798				}), error);
4799			}
4800			else if (!tempFile && mode == App.MODE_DEVICE && EditorUi.nativeFileSupport)
4801			{
4802				complete();
4803
4804				this.showSaveFilePicker(mxUtils.bind(this, function(fileHandle, desc)
4805				{
4806					var file = new LocalFile(this, data, desc.name, null, fileHandle, desc);
4807
4808					file.saveFile(desc.name, false, mxUtils.bind(this, function()
4809					{
4810						this.fileCreated(file, libs, replace, done, clibs);
4811					}), error, true);
4812				}), mxUtils.bind(this, function(e)
4813				{
4814					if (e.name != 'AbortError')
4815					{
4816						error(e);
4817					}
4818				}), this.createFileSystemOptions(title));
4819			}
4820			else
4821			{
4822				complete();
4823				this.fileCreated(new LocalFile(this, data, title, mode == null), libs, replace, done, clibs);
4824			}
4825		}
4826		catch (e)
4827		{
4828			complete();
4829			this.handleError(e);
4830		}
4831	}
4832};
4833
4834/**
4835 * Translates this point by the given vector.
4836 *
4837 * @param {number} dx X-coordinate of the translation.
4838 * @param {number} dy Y-coordinate of the translation.
4839 */
4840App.prototype.fileCreated = function(file, libs, replace, done, clibs)
4841{
4842	var url = window.location.pathname;
4843
4844	if (libs != null && libs.length > 0)
4845	{
4846		url += '?libs=' + libs;
4847	}
4848
4849	if (clibs != null && clibs.length > 0)
4850	{
4851		url += '?clibs=' + clibs;
4852	}
4853
4854	url = this.getUrl(url);
4855
4856	// Always opens a new tab for local files to avoid losing changes
4857	if (file.getMode() != App.MODE_DEVICE)
4858	{
4859		url += '#' + file.getHash();
4860	}
4861
4862	// Makes sure to produce consistent output with finalized files via createFileData this needs
4863	// to save the file again since it needs the newly created file ID for redirecting in HTML
4864	if (this.spinner.spin(document.body, mxResources.get('inserting')))
4865	{
4866		var data = file.getData();
4867		var dataNode = (data.length > 0) ? this.editor.extractGraphModel(
4868			mxUtils.parseXml(data).documentElement, true) : null;
4869		var redirect = window.location.protocol + '//' + window.location.hostname + url;
4870		var node = dataNode;
4871		var graph = null;
4872
4873		// Handles special case where SVG files need a rendered graph to be saved
4874		if (dataNode != null && /\.svg$/i.test(file.getTitle()))
4875		{
4876			graph = this.createTemporaryGraph(this.editor.graph.getStylesheet());
4877			document.body.appendChild(graph.container);
4878			node = this.decodeNodeIntoGraph(node, graph);
4879		}
4880
4881		file.setData(this.createFileData(dataNode, graph, file, redirect));
4882
4883		if (graph != null)
4884		{
4885			graph.container.parentNode.removeChild(graph.container);
4886		}
4887
4888		var complete = mxUtils.bind(this, function()
4889		{
4890			this.spinner.stop();
4891		});
4892
4893		var fn = mxUtils.bind(this, function()
4894		{
4895			complete();
4896
4897			var currentFile = this.getCurrentFile();
4898
4899			if (replace == null && currentFile != null)
4900			{
4901				replace = !currentFile.isModified() && currentFile.getMode() == null;
4902			}
4903
4904			var fn3 = mxUtils.bind(this, function()
4905			{
4906				window.openFile = null;
4907				this.fileLoaded(file);
4908
4909				if (replace)
4910				{
4911					file.addAllSavedStatus();
4912				}
4913
4914				if (libs != null)
4915				{
4916					this.sidebar.showEntries(libs);
4917				}
4918
4919				if (clibs != null)
4920				{
4921					var temp = [];
4922					var tokens = clibs.split(';');
4923
4924					for (var i = 0; i < tokens.length; i++)
4925					{
4926						temp.push(decodeURIComponent(tokens[i]));
4927					}
4928
4929					this.loadLibraries(temp);
4930				}
4931			});
4932
4933			var fn2 = mxUtils.bind(this, function()
4934			{
4935				if (replace || currentFile == null || !currentFile.isModified())
4936				{
4937					fn3();
4938				}
4939				else
4940				{
4941					this.confirm(mxResources.get('allChangesLost'), null, fn3,
4942						mxResources.get('cancel'), mxResources.get('discardChanges'));
4943				}
4944			});
4945
4946			if (done != null)
4947			{
4948				done();
4949			}
4950
4951			// Opens the file in a new window
4952			if (replace != null && !replace)
4953			{
4954				// Opens local file in a new window
4955				if (file.constructor == LocalFile)
4956				{
4957					window.openFile = new OpenFile(function()
4958					{
4959						window.openFile = null;
4960					});
4961
4962					window.openFile.setData(file.getData(), file.getTitle(), file.getMode() == null);
4963				}
4964
4965				if (done != null)
4966				{
4967					done();
4968				}
4969
4970				window.openWindow(url, null, fn2);
4971			}
4972			else
4973			{
4974				fn2();
4975			}
4976		});
4977
4978		// Updates data in memory for local files
4979		if (file.constructor == LocalFile)
4980		{
4981			fn();
4982		}
4983		else
4984		{
4985			file.saveFile(file.getTitle(), false, mxUtils.bind(this, function()
4986			{
4987				fn();
4988			}), mxUtils.bind(this, function(resp)
4989			{
4990				complete();
4991				this.handleError(resp);
4992			}));
4993		}
4994	}
4995};
4996
4997/**
4998 * Translates this point by the given vector.
4999 *
5000 * @param {number} dx X-coordinate of the translation.
5001 * @param {number} dy Y-coordinate of the translation.
5002 */
5003App.prototype.loadFile = function(id, sameWindow, file, success, force)
5004{
5005	if (urlParams['openInSameWin'] == '1' || navigator.standalone)
5006	{
5007		sameWindow = true;
5008	}
5009
5010	this.hideDialog();
5011
5012	var fn2 = mxUtils.bind(this, function()
5013	{
5014		if (id == null || id.length == 0)
5015		{
5016			this.editor.setStatus('');
5017			this.fileLoaded(null);
5018		}
5019		else if (this.spinner.spin(document.body, mxResources.get('loading')))
5020		{
5021			// Handles files from localStorage
5022			if (id.charAt(0) == 'L')
5023			{
5024				this.spinner.stop();
5025
5026				if (!isLocalStorage)
5027				{
5028					this.handleError({message: mxResources.get('serviceUnavailableOrBlocked')}, mxResources.get('errorLoadingFile'), mxUtils.bind(this, function()
5029					{
5030						var tempFile = this.getCurrentFile();
5031						window.location.hash = (tempFile != null) ? tempFile.getHash() : '';
5032					}));
5033				}
5034				else
5035				{
5036					var error = mxUtils.bind(this, function (e)
5037					{
5038						this.handleError(e, mxResources.get('errorLoadingFile'), mxUtils.bind(this, function()
5039						{
5040							var tempFile = this.getCurrentFile();
5041							window.location.hash = (tempFile != null) ? tempFile.getHash() : '';
5042						}));
5043					});
5044
5045					id = decodeURIComponent(id.substring(1));
5046
5047					StorageFile.getFileContent(this, id, mxUtils.bind(this, function(data)
5048					{
5049						if (data != null)
5050						{
5051							this.fileLoaded(new StorageFile(this, data, id));
5052
5053							if (success != null)
5054							{
5055								success();
5056							}
5057						}
5058						else
5059						{
5060							error({message: mxResources.get('fileNotFound')});
5061						}
5062					}), error);
5063				}
5064			}
5065			else if (file != null)
5066			{
5067				// File already loaded
5068				this.spinner.stop();
5069				this.fileLoaded(file);
5070
5071				if (success != null)
5072				{
5073					success();
5074				}
5075			}
5076			else if (id.charAt(0) == 'S')
5077			{
5078				this.spinner.stop();
5079
5080				this.alert('[Deprecation] #S is no longer supported, go to https://app.diagrams.net/?desc=' + id.substring(1).substring(0, 10), mxUtils.bind(this, function()
5081				{
5082					window.location.href = 'https://app.diagrams.net/?desc=' + id.substring(1);
5083				}));
5084			}
5085			else if (id.charAt(0) == 'R')
5086			{
5087				// Raw file encoded into URL
5088				this.spinner.stop();
5089				var data = decodeURIComponent(id.substring(1));
5090
5091				if (data.charAt(0) != '<')
5092				{
5093					data = Graph.decompress(data);
5094				}
5095
5096				var tempFile = new LocalFile(this, data, (urlParams['title'] != null) ?
5097					decodeURIComponent(urlParams['title']) : this.defaultFilename, true);
5098				tempFile.getHash = function()
5099				{
5100					return id;
5101				};
5102				this.fileLoaded(tempFile);
5103
5104				if (success != null)
5105				{
5106					success();
5107				}
5108			}
5109			else if (id.charAt(0) == 'E') // Embed file
5110			{
5111				//Currently we only reload current file. Id is not used!
5112				var currentFile = this.getCurrentFile();
5113
5114				if (currentFile == null)
5115				{
5116					this.handleError({message: mxResources.get('serviceUnavailableOrBlocked')}, mxResources.get('errorLoadingFile'));
5117				}
5118				else
5119				{
5120					this.remoteInvoke('getDraftFileContent', null, null, mxUtils.bind(this, function(data, desc)
5121					{
5122						this.spinner.stop();
5123						this.fileLoaded(new EmbedFile(this, data, desc));
5124
5125						if (success != null)
5126						{
5127							success();
5128						}
5129					}), mxUtils.bind(this, function()
5130					{
5131						this.handleError({message: mxResources.get('serviceUnavailableOrBlocked')}, mxResources.get('errorLoadingFile'));
5132					}));
5133				}
5134			}
5135			else if (id.charAt(0) == 'U')
5136			{
5137				var url = decodeURIComponent(id.substring(1));
5138
5139				var doFallback = mxUtils.bind(this, function()
5140				{
5141					// Fallback for non-public Google Drive files
5142					if (url.substring(0, 31) == 'https://drive.google.com/uc?id=' &&
5143						(this.drive != null || typeof window.DriveClient === 'function'))
5144					{
5145						this.hideDialog();
5146
5147						var fallback = mxUtils.bind(this, function()
5148						{
5149							this.spinner.stop();
5150
5151							if (this.drive != null)
5152							{
5153								var tempId = url.substring(31, url.lastIndexOf('&ex'));
5154
5155								this.loadFile('G' + tempId, sameWindow, null, mxUtils.bind(this, function()
5156								{
5157									var currentFile = this.getCurrentFile();
5158
5159									if (currentFile != null && this.editor.chromeless && !this.editor.editable)
5160									{
5161										currentFile.getHash = function()
5162										{
5163											return 'G' + tempId;
5164										};
5165
5166										window.location.hash = '#' + currentFile.getHash();
5167									}
5168
5169									if (success != null)
5170									{
5171										success();
5172									}
5173								}));
5174
5175								return true;
5176							}
5177							else
5178							{
5179								return false;
5180							}
5181						});
5182
5183						if (!fallback() && this.spinner.spin(document.body, mxResources.get('loading')))
5184						{
5185							this.addListener('clientLoaded', fallback);
5186						}
5187
5188						return true;
5189					}
5190					else
5191					{
5192						return false;
5193					}
5194				});
5195
5196				this.loadTemplate(url, mxUtils.bind(this, function(text)
5197				{
5198					this.spinner.stop();
5199
5200					if (text != null && text.length > 0)
5201					{
5202						var filename = this.defaultFilename;
5203
5204						// Tries to find name from URL with valid extensions
5205						if (urlParams['title'] == null && urlParams['notitle'] != '1')
5206						{
5207							var tmp = url;
5208							var dot = url.lastIndexOf('.');
5209							var slash = tmp.lastIndexOf('/');
5210
5211							if (dot > slash && slash > 0)
5212							{
5213								tmp = tmp.substring(slash + 1, dot);
5214								var ext = url.substring(dot);
5215
5216								if (!this.useCanvasForExport && ext == '.png')
5217								{
5218									ext = '.drawio';
5219								}
5220
5221								if (ext === '.svg' || ext === '.xml' ||
5222									ext === '.html' || ext === '.png'  ||
5223									ext === '.drawio')
5224								{
5225									filename = tmp + ext;
5226								}
5227							}
5228						}
5229
5230						var tempFile = new LocalFile(this, text, (urlParams['title'] != null) ?
5231							decodeURIComponent(urlParams['title']) : filename, true);
5232						tempFile.getHash = function()
5233						{
5234							return id;
5235						};
5236
5237						if (this.fileLoaded(tempFile, true))
5238						{
5239							if (success != null)
5240							{
5241								success();
5242							}
5243						}
5244						else if (!doFallback())
5245						{
5246							this.handleError({message: mxResources.get('fileNotFound')},
5247								mxResources.get('errorLoadingFile'));
5248						}
5249					}
5250					else if (!doFallback())
5251					{
5252						this.handleError({message: mxResources.get('fileNotFound')},
5253							mxResources.get('errorLoadingFile'));
5254					}
5255				}), mxUtils.bind(this, function()
5256				{
5257					if (!doFallback())
5258					{
5259						this.spinner.stop();
5260						this.handleError({message: mxResources.get('fileNotFound')},
5261							mxResources.get('errorLoadingFile'));
5262					}
5263				}), (urlParams['template-filename'] != null) ?
5264					decodeURIComponent(urlParams['template-filename']) : null);
5265			}
5266			else
5267			{
5268				// Google Drive files are handled as default file types
5269				var peer = null;
5270
5271				if (id.charAt(0) == 'G')
5272				{
5273					peer = this.drive;
5274				}
5275				else if (id.charAt(0) == 'D')
5276				{
5277					peer = this.dropbox;
5278				}
5279				else if (id.charAt(0) == 'W')
5280				{
5281					peer = this.oneDrive;
5282				}
5283				else if (id.charAt(0) == 'H')
5284				{
5285					peer = this.gitHub;
5286				}
5287				else if (id.charAt(0) == 'A')
5288				{
5289					peer = this.gitLab;
5290				}
5291				else if (id.charAt(0) == 'T')
5292				{
5293					peer = this.trello;
5294				}
5295				else if (id.charAt(0) == 'N')
5296				{
5297					peer = this.notion;
5298				}
5299
5300				if (peer == null)
5301				{
5302					this.handleError({message: mxResources.get('serviceUnavailableOrBlocked')}, mxResources.get('errorLoadingFile'), mxUtils.bind(this, function()
5303					{
5304						var currentFile = this.getCurrentFile();
5305						window.location.hash = (currentFile != null) ? currentFile.getHash() : '';
5306					}));
5307				}
5308				else
5309				{
5310					var peerChar = id.charAt(0);
5311					id = decodeURIComponent(id.substring(1));
5312
5313					peer.getFile(id, mxUtils.bind(this, function(file)
5314					{
5315						this.spinner.stop();
5316						this.fileLoaded(file);
5317						var currentFile = this.getCurrentFile();
5318
5319						if (currentFile == null)
5320						{
5321							window.location.hash = '';
5322							this.showSplash();
5323						}
5324						else if (this.editor.chromeless && !this.editor.editable)
5325						{
5326							// Keeps ID even for converted files in chromeless mode for refresh to work
5327							currentFile.getHash = function()
5328							{
5329								return peerChar + id;
5330							};
5331
5332							window.location.hash = '#' + currentFile.getHash();
5333						}
5334						else if (file == currentFile && file.getMode() == null)
5335						{
5336							// Shows a warning if a copy was opened which happens
5337							// eg. for .png files in IE as they cannot be written
5338							var status = mxResources.get('copyCreated');
5339							this.editor.setStatus('<div title="'+ status +
5340								'" class="geStatusAlert">' + status + '</div>');
5341						}
5342
5343						if (success != null)
5344						{
5345							success();
5346						}
5347					}), mxUtils.bind(this, function(resp)
5348					{
5349						// Makes sure the file does not save the invalid UI model and overwrites anything important
5350						if (window.console != null && resp != null)
5351						{
5352							console.log('error in loadFile:', id, resp);
5353						}
5354
5355						this.handleError(resp, (resp != null) ? mxResources.get('errorLoadingFile') : null, mxUtils.bind(this, function()
5356						{
5357							var currentFile = this.getCurrentFile();
5358
5359							if (currentFile == null)
5360							{
5361								window.location.hash = '';
5362								this.showSplash();
5363							}
5364							else
5365							{
5366								window.location.hash = '#' + currentFile.getHash();
5367							}
5368						}), null, null, '#' + peerChar + id);
5369					}));
5370				}
5371			}
5372		}
5373	});
5374
5375	var currentFile = this.getCurrentFile();
5376
5377	var fn = mxUtils.bind(this, function()
5378	{
5379		if (force || currentFile == null || !currentFile.isModified())
5380		{
5381			fn2();
5382		}
5383		else
5384		{
5385			this.confirm(mxResources.get('allChangesLost'), mxUtils.bind(this, function()
5386			{
5387				if (currentFile != null)
5388				{
5389					window.location.hash = currentFile.getHash();
5390				}
5391			}), fn2, mxResources.get('cancel'), mxResources.get('discardChanges'));
5392		}
5393	});
5394
5395	if (id == null || id.length == 0)
5396	{
5397		fn();
5398	}
5399	else if (currentFile != null && !sameWindow)
5400	{
5401		this.showDialog(new PopupDialog(this, this.getUrl() + '#' + id,
5402			null, fn).container, 320, 140, true, true);
5403	}
5404	else
5405	{
5406		fn();
5407	}
5408};
5409
5410/**
5411 * Translates this point by the given vector.
5412 *
5413 * @param {number} dx X-coordinate of the translation.
5414 * @param {number} dy Y-coordinate of the translation.
5415 */
5416App.prototype.getLibraryStorageHint = function(file)
5417{
5418	var tip = file.getTitle();
5419
5420	if (file.constructor != LocalLibrary)
5421	{
5422		tip += '\n' + file.getHash();
5423	}
5424
5425	if (file.constructor == DriveLibrary)
5426	{
5427		tip += ' (' + mxResources.get('googleDrive') + ')';
5428	}
5429	else if (file.constructor == GitHubLibrary)
5430	{
5431		tip += ' (' + mxResources.get('github') + ')';
5432	}
5433	else if (file.constructor == TrelloLibrary)
5434	{
5435		tip += ' (' + mxResources.get('trello') + ')';
5436	}
5437	else if (file.constructor == DropboxLibrary)
5438	{
5439		tip += ' (' + mxResources.get('dropbox') + ')';
5440	}
5441	else if (file.constructor == OneDriveLibrary)
5442	{
5443		tip += ' (' + mxResources.get('oneDrive') + ')';
5444	}
5445	else if (file.constructor == StorageLibrary)
5446	{
5447		tip += ' (' + mxResources.get('browser') + ')';
5448	}
5449	else if (file.constructor == LocalLibrary)
5450	{
5451		tip += ' (' + mxResources.get('device') + ')';
5452	}
5453
5454	return tip;
5455};
5456
5457/**
5458 * Updates action states depending on the selection.
5459 */
5460App.prototype.restoreLibraries = function()
5461{
5462	this.loadLibraries(mxSettings.getCustomLibraries(), mxUtils.bind(this, function()
5463	{
5464		this.loadLibraries((urlParams['clibs'] || '').split(';'));
5465	}));
5466};
5467
5468/**
5469 * Updates action states depending on the selection.
5470 */
5471App.prototype.loadLibraries = function(libs, done)
5472{
5473	if (this.sidebar != null)
5474	{
5475		if (this.pendingLibraries == null)
5476		{
5477			this.pendingLibraries = new Object();
5478		}
5479
5480		// Ignores this library next time
5481		var ignore = mxUtils.bind(this, function(id, keep)
5482		{
5483			if (!keep)
5484			{
5485				mxSettings.removeCustomLibrary(id);
5486			}
5487
5488			delete this.pendingLibraries[id];
5489		});
5490
5491		var waiting = 0;
5492		var files = [];
5493
5494		// Loads in order of libs array
5495		var checkDone = mxUtils.bind(this, function()
5496		{
5497			if (waiting == 0)
5498			{
5499				if (libs != null)
5500				{
5501					for (var i = libs.length - 1; i >= 0; i--)
5502					{
5503						if (files[i] != null)
5504						{
5505							this.loadLibrary(files[i]);
5506						}
5507					}
5508				}
5509
5510				if (done != null)
5511				{
5512					done();
5513				}
5514			}
5515		});
5516
5517		if (libs != null)
5518		{
5519			for (var i = 0; i < libs.length; i++)
5520			{
5521				var name = encodeURIComponent(decodeURIComponent(libs[i]));
5522
5523				(mxUtils.bind(this, function(id, index)
5524				{
5525					if (id != null && id.length > 0 && this.pendingLibraries[id] == null &&
5526						this.sidebar.palettes[id] == null)
5527					{
5528						// Waits for all libraries to load
5529						waiting++;
5530
5531						var onload = mxUtils.bind(this, function(file)
5532						{
5533							delete this.pendingLibraries[id];
5534							files[index] = file;
5535							waiting--;
5536							checkDone();
5537						});
5538
5539						var onerror = mxUtils.bind(this, function(keep)
5540						{
5541							ignore(id, keep);
5542							waiting--;
5543							checkDone();
5544						});
5545
5546						this.pendingLibraries[id] = true;
5547						var service = id.substring(0, 1);
5548
5549						if (service == 'L')
5550						{
5551							if (isLocalStorage || mxClient.IS_CHROMEAPP)
5552							{
5553								// Make asynchronous for barrier to work
5554								window.setTimeout(mxUtils.bind(this, function()
5555								{
5556									try
5557									{
5558										var name = decodeURIComponent(id.substring(1));
5559
5560										StorageFile.getFileContent(this, name, mxUtils.bind(this, function(xml)
5561										{
5562											if (name == '.scratchpad' && xml == null)
5563											{
5564												xml = this.emptyLibraryXml;
5565											}
5566
5567											if (xml != null)
5568											{
5569												onload(new StorageLibrary(this, xml, name));
5570											}
5571											else
5572											{
5573												onerror();
5574											}
5575										}), onerror);
5576									}
5577									catch (e)
5578									{
5579										onerror();
5580									}
5581								}), 0);
5582							}
5583						}
5584						else if (service == 'U')
5585						{
5586							var url = decodeURIComponent(id.substring(1));
5587
5588							if (!this.isOffline())
5589							{
5590								this.loadTemplate(url, mxUtils.bind(this, function(text)
5591								{
5592									if (text != null && text.length > 0)
5593									{
5594										// LATER: Convert mxfile to mxlibrary using code from libraryLoaded
5595										onload(new UrlLibrary(this, text, url));
5596									}
5597									else
5598									{
5599										onerror();
5600									}
5601								}), function()
5602								{
5603									onerror();
5604								}, null, true);
5605							}
5606						}
5607						else if (service == 'R')
5608						{
5609							var libDesc = decodeURIComponent(id.substring(1));
5610
5611							try
5612							{
5613								libDesc = JSON.parse(libDesc);
5614								var libObj = {
5615									id: libDesc[0],
5616			               			title: libDesc[1],
5617			               			downloadUrl: libDesc[2]
5618								}
5619
5620								this.remoteInvoke('getFileContent', [libObj.downloadUrl], null, mxUtils.bind(this, function(libContent)
5621								{
5622									try
5623									{
5624										onload(new RemoteLibrary(this, libContent, libObj));
5625									}
5626									catch (e)
5627									{
5628										onerror();
5629									}
5630								}), function()
5631								{
5632									onerror();
5633								});
5634							}
5635							catch (e)
5636							{
5637								onerror();
5638							}
5639						}
5640						else if (service == 'S' && this.loadDesktopLib != null)
5641						{
5642							try
5643							{
5644								this.loadDesktopLib(decodeURIComponent(id.substring(1)), function(desktopLib)
5645								{
5646									onload(desktopLib);
5647								}, onerror);
5648							}
5649							catch (e)
5650							{
5651								onerror();
5652							}
5653						}
5654						else
5655						{
5656							var peer = null;
5657
5658							if (service == 'G')
5659							{
5660								if (this.drive != null && this.drive.user != null)
5661								{
5662									peer = this.drive;
5663								}
5664							}
5665							else if (service == 'H')
5666							{
5667								if (this.gitHub != null && this.gitHub.getUser() != null)
5668								{
5669									peer = this.gitHub;
5670								}
5671							}
5672							else if (service == 'T')
5673							{
5674								if (this.trello != null && this.trello.isAuthorized())
5675								{
5676									peer = this.trello;
5677								}
5678							}
5679							else if (service == 'D')
5680							{
5681								if (this.dropbox != null && this.dropbox.getUser() != null)
5682								{
5683									peer = this.dropbox;
5684								}
5685							}
5686							else if (service == 'W')
5687							{
5688								if (this.oneDrive != null && this.oneDrive.getUser() != null)
5689								{
5690									peer = this.oneDrive;
5691								}
5692							}
5693
5694							if (peer != null)
5695							{
5696								peer.getLibrary(decodeURIComponent(id.substring(1)), mxUtils.bind(this, function(file)
5697								{
5698									try
5699									{
5700										onload(file);
5701									}
5702									catch (e)
5703									{
5704										onerror();
5705									}
5706								}), function(resp)
5707								{
5708									onerror();
5709								});
5710							}
5711							else
5712							{
5713								onerror(true);
5714							}
5715						}
5716					}
5717				}))(name, i);
5718			}
5719
5720			checkDone();
5721		}
5722		else
5723		{
5724			checkDone();
5725		}
5726	}
5727};
5728
5729/**
5730 * Translates this point by the given vector.
5731 *
5732 * @param {number} dx X-coordinate of the translation.
5733 * @param {number} dy Y-coordinate of the translation.
5734 */
5735App.prototype.updateButtonContainer = function()
5736{
5737	if (this.buttonContainer != null)
5738	{
5739		var file = this.getCurrentFile();
5740
5741		if (urlParams['embed'] == '1')
5742		{
5743			if (uiTheme == 'atlas' || urlParams['atlas'] == '1')
5744			{
5745				this.buttonContainer.style.paddingRight = '12px';
5746				this.buttonContainer.style.paddingTop = '6px';
5747				this.buttonContainer.style.right = urlParams['noLangIcon'] == '1'? '0' : '25px';
5748			}
5749			else if (uiTheme != 'min')
5750			{
5751				this.buttonContainer.style.paddingRight = '38px';
5752				this.buttonContainer.style.paddingTop = '6px';
5753			}
5754		}
5755
5756		// Comments
5757		if (this.commentsSupported() && urlParams['sketch'] != '1')
5758		{
5759			if (this.commentButton == null)
5760			{
5761				this.commentButton = document.createElement('a');
5762				this.commentButton.setAttribute('title', mxResources.get('comments'));
5763				this.commentButton.className = 'geToolbarButton';
5764				this.commentButton.style.cssText = 'display:inline-block;position:relative;box-sizing:border-box;' +
5765					'margin-right:4px;float:left;cursor:pointer;width:24px;height:24px;background-size:24px 24px;' +
5766					'background-position:center center;background-repeat:no-repeat;background-image:' +
5767					'url(' + Editor.commentImage + ');';
5768
5769				if (uiTheme == 'atlas')
5770				{
5771					this.commentButton.style.marginRight = '10px';
5772					this.commentButton.style.marginTop = '-3px';
5773				}
5774				else if (uiTheme == 'min')
5775				{
5776					this.commentButton.style.marginTop = '1px';
5777				}
5778				else if (urlParams['atlas'] == '1')
5779				{
5780					this.commentButton.style.marginTop = '-2px';
5781				}
5782				else
5783				{
5784					this.commentButton.style.marginTop = '-5px';
5785				}
5786
5787				mxEvent.addListener(this.commentButton, 'click', mxUtils.bind(this, function()
5788				{
5789					this.actions.get('comments').funct();
5790				}));
5791
5792				this.buttonContainer.appendChild(this.commentButton);
5793
5794				if (uiTheme == 'dark' || uiTheme == 'atlas')
5795				{
5796					this.commentButton.style.filter = 'invert(100%)';
5797				}
5798			}
5799		}
5800		else if (this.commentButton != null)
5801		{
5802			this.commentButton.parentNode.removeChild(this.commentButton);
5803			this.commentButton = null;
5804		}
5805
5806		// Share
5807		if (urlParams['embed'] != '1' && this.getServiceName() == 'draw.io' &&
5808			!mxClient.IS_CHROMEAPP && !EditorUi.isElectronApp &&
5809			!this.isOfflineApp())
5810		{
5811			if (file != null)
5812			{
5813				if (this.shareButton == null)
5814				{
5815					this.shareButton = document.createElement('div');
5816					this.shareButton.className = 'geBtn gePrimaryBtn';
5817					this.shareButton.style.display = 'inline-block';
5818					this.shareButton.style.backgroundColor = '#F2931E';
5819					this.shareButton.style.borderColor = '#F08705';
5820					this.shareButton.style.backgroundImage = 'none';
5821					this.shareButton.style.padding = '2px 10px 0 10px';
5822					this.shareButton.style.marginTop = '-10px';
5823					this.shareButton.style.height = '28px';
5824					this.shareButton.style.lineHeight = '28px';
5825					this.shareButton.style.minWidth = '0px';
5826					this.shareButton.style.cssFloat = 'right';
5827					this.shareButton.setAttribute('title', mxResources.get('share'));
5828
5829					var icon = document.createElement('img');
5830					icon.setAttribute('src', this.shareImage);
5831					icon.setAttribute('align', 'absmiddle');
5832					icon.style.marginRight = '4px';
5833					icon.style.marginTop = '-3px';
5834					this.shareButton.appendChild(icon);
5835
5836					if (!Editor.isDarkMode() && uiTheme != 'atlas')
5837					{
5838						this.shareButton.style.color = 'black';
5839						icon.style.filter = 'invert(100%)';
5840					}
5841
5842					mxUtils.write(this.shareButton, mxResources.get('share'));
5843
5844					mxEvent.addListener(this.shareButton, 'click', mxUtils.bind(this, function()
5845					{
5846						this.actions.get('share').funct();
5847					}));
5848
5849					this.buttonContainer.appendChild(this.shareButton);
5850				}
5851			}
5852			else if (this.shareButton != null)
5853			{
5854				this.shareButton.parentNode.removeChild(this.shareButton);
5855				this.shareButton = null;
5856			}
5857
5858			//Fetch notifications
5859			if (urlParams['extAuth'] != '1') //Disable notification with external auth (e.g, Teams app)
5860			{
5861				this.fetchAndShowNotification('online', this.mode);
5862			}
5863		}
5864		else if (urlParams['notif'] != null) //Notif for embed mode
5865		{
5866			this.fetchAndShowNotification(urlParams['notif']);
5867		}
5868	}
5869};
5870
5871
5872App.prototype.fetchAndShowNotification = function(target, subtarget)
5873{
5874	if (this.fetchingNotif)
5875	{
5876		return;
5877	}
5878
5879	target = target || 'online';
5880	var cachedNotifKey = '.notifCache';
5881	var cachedNotif = null;
5882
5883	var processNotif = mxUtils.bind(this, function(notifs)
5884	{
5885		notifs = notifs.filter(function(notif)
5886		{
5887			return !notif.targets || notif.targets.indexOf(target) > -1 ||
5888						(subtarget != null && notif.targets.indexOf(subtarget) > -1);
5889		});
5890
5891		var lsReadFlag = target + 'NotifReadTS';
5892		var lastRead = (localStorage != null) ? parseInt(localStorage.getItem(lsReadFlag)) : true;
5893
5894		for (var i = 0; i < notifs.length; i++)
5895		{
5896			notifs[i].isNew = (!lastRead || notifs[i].timestamp > lastRead);
5897		}
5898
5899		this.showNotification(notifs, lsReadFlag);
5900	});
5901
5902	try
5903	{
5904		if (localStorage != null)
5905		{
5906			cachedNotif = JSON.parse(localStorage.getItem(cachedNotifKey));
5907		}
5908	}
5909	catch(e) {} //Ignore
5910
5911	if (cachedNotif == null || cachedNotif.ts + 24 * 60 * 60 * 1000 < Date.now()) //Cache for one day
5912	{
5913		this.fetchingNotif = true;
5914		//Fetch all notifications and store them, then filter client-side
5915		mxUtils.get(NOTIFICATIONS_URL, mxUtils.bind(this, function(req)
5916		{
5917			if (req.getStatus() >= 200 && req.getStatus() <= 299)
5918			{
5919			    var notifs = JSON.parse(req.getText());
5920
5921				//Process and sort
5922				notifs.sort(function(a, b)
5923				{
5924					return b.timestamp - a.timestamp;
5925				});
5926
5927				if (isLocalStorage)
5928				{
5929					localStorage.setItem(cachedNotifKey, JSON.stringify({ts: Date.now(), notifs: notifs}));
5930				}
5931
5932				this.fetchingNotif = false;
5933				processNotif(notifs);
5934			}
5935		}));
5936	}
5937	else
5938	{
5939		processNotif(cachedNotif.notifs);
5940	}
5941};
5942
5943App.prototype.showNotification = function(notifs, lsReadFlag)
5944{
5945	var newCount = notifs.length;
5946
5947	if (uiTheme == 'min')
5948	{
5949		newCount = 0;
5950
5951		for (var i = 0; i < notifs.length; i++)
5952		{
5953			if (notifs[i].isNew)
5954			{
5955				newCount++;
5956			}
5957		}
5958	}
5959
5960	if (newCount == 0)
5961	{
5962		if (this.notificationBtn != null)
5963		{
5964			this.notificationBtn.style.display = 'none';
5965			this.editor.fireEvent(new mxEventObject('statusChanged'));
5966		}
5967
5968		return;
5969	}
5970
5971	function shouldAnimate(newNotif)
5972	{
5973		var countEl = document.querySelector('.geNotification-count');
5974
5975		if (countEl == null)
5976		{
5977			return;
5978		}
5979
5980		countEl.innerHTML = newNotif;
5981		countEl.style.display = newNotif == 0? 'none' : '';
5982		var notifBell = document.querySelector('.geNotification-bell');
5983		notifBell.style.animation = newNotif == 0? 'none' : '';
5984		notifBell.className = 'geNotification-bell' + (newNotif == 0? ' geNotification-bellOff' : '');
5985		document.querySelector('.geBell-rad').style.animation = newNotif == 0? 'none' : '';
5986	}
5987
5988	var markAllAsRead = mxUtils.bind(this, function()
5989	{
5990		this.notificationWin.style.display = 'none';
5991		var unread = this.notificationWin.querySelectorAll('.circle.active');
5992
5993		for (var i = 0; i < unread.length; i++)
5994		{
5995			unread[i].className = 'circle';
5996		}
5997
5998		if (isLocalStorage && notifs[0])
5999		{
6000			localStorage.setItem(lsReadFlag, notifs[0].timestamp);
6001		}
6002	});
6003
6004	if (this.notificationBtn == null)
6005	{
6006		this.notificationBtn = document.createElement('div');
6007		this.notificationBtn.className = 'geNotification-box';
6008
6009		if (uiTheme == 'min')
6010		{
6011			this.notificationBtn.style.width = '30px';
6012			this.notificationBtn.style.top = '4px';
6013		}
6014		else if (urlParams['atlas'] == '1')
6015		{
6016			this.notificationBtn.style.top = '2px';
6017		}
6018
6019		var notifCount = document.createElement('span');
6020		notifCount.className = 'geNotification-count';
6021		this.notificationBtn.appendChild(notifCount);
6022
6023		var notifBell = document.createElement('div');
6024		notifBell.className = 'geNotification-bell';
6025		notifBell.style.opacity = uiTheme == 'min'? '0.5' : '';
6026		var bellPart = document.createElement('span');
6027		bellPart.className = 'geBell-top';
6028		notifBell.appendChild(bellPart);
6029		var bellPart = document.createElement('span');
6030		bellPart.className = 'geBell-middle';
6031		notifBell.appendChild(bellPart);
6032		var bellPart = document.createElement('span');
6033		bellPart.className = 'geBell-bottom';
6034		notifBell.appendChild(bellPart);
6035		var bellPart = document.createElement('span');
6036		bellPart.className = 'geBell-rad';
6037		notifBell.appendChild(bellPart);
6038		this.notificationBtn.appendChild(notifBell);
6039
6040		//Add as first child such that it is the left-most one
6041		this.buttonContainer.insertBefore(this.notificationBtn, this.buttonContainer.firstChild);
6042
6043		this.notificationWin = document.createElement('div');
6044		this.notificationWin.className = 'geNotifPanel';
6045		this.notificationWin.style.display = 'none';
6046		document.body.appendChild(this.notificationWin);
6047
6048		var winHeader = document.createElement('div');
6049		winHeader.className = 'header';
6050		var winTitle = document.createElement('span');
6051		winTitle.className = 'title';
6052		winTitle.textContent = mxResources.get('notifications');
6053		winHeader.appendChild(winTitle);
6054		var winClose = document.createElement('span');
6055		winClose.className = 'closeBtn';
6056		winClose.textContent = 'x';
6057		winHeader.appendChild(winClose);
6058		this.notificationWin.appendChild(winHeader);
6059
6060		var winBody = document.createElement('div');
6061		winBody.className = 'notifications clearfix';
6062		var notifList = document.createElement('div');
6063		notifList.setAttribute('id', 'geNotifList');
6064		notifList.style.position = 'relative';
6065		winBody.appendChild(notifList);
6066		this.notificationWin.appendChild(winBody);
6067
6068		mxEvent.addListener(this.notificationBtn, 'click', mxUtils.bind(this, function()
6069		{
6070			if (this.notificationWin.style.display == 'none')
6071			{
6072				this.notificationWin.style.display = '';
6073				document.querySelector('.notifications').scrollTop = 0;
6074				var r = this.notificationBtn.getBoundingClientRect();
6075				this.notificationWin.style.top = (r.top + this.notificationBtn.clientHeight) + 'px';
6076				this.notificationWin.style.left = (r.right - this.notificationWin.clientWidth) + 'px';
6077				shouldAnimate(0); //Stop animation once notifications are open
6078			}
6079			else
6080			{
6081				markAllAsRead();
6082			}
6083		}));
6084
6085		mxEvent.addListener(winClose, 'click', markAllAsRead);
6086	}
6087	else
6088	{
6089		this.notificationBtn.style.display = ''; //In case it was hidden
6090	}
6091
6092	var newNotif = 0;
6093	var notifListEl = document.getElementById('geNotifList');
6094
6095	if (notifListEl == null)
6096	{
6097		return; //This shouldn't happen and no meaning of continuing
6098	}
6099	else
6100	{
6101		notifListEl.innerHTML = '<div class="line"></div>';
6102
6103		for (var i = 0; i < notifs.length; i++)
6104		{
6105			(function(editorUi, notif)
6106			{
6107				if (notif.isNew)
6108				{
6109					newNotif++;
6110				}
6111
6112				var notifEl = document.createElement('div');
6113				notifEl.className = 'notification';
6114				var ts = new Date(notif.timestamp);
6115				var str = editorUi.timeSince(ts);
6116
6117				if (str == null)
6118				{
6119					str = mxResources.get('lessThanAMinute');
6120				}
6121
6122				notifEl.innerHTML = '<div class="circle' + (notif.isNew? ' active' : '') + '"></div><span class="time">' +
6123										mxUtils.htmlEntities(mxResources.get('timeAgo', [str], '{1} ago')) + '</span>' +
6124										'<p>' + mxUtils.htmlEntities(notif.content) + '</p>';
6125				if (notif.link)
6126				{
6127					mxEvent.addListener(notifEl, 'click', function()
6128					{
6129						window.open(notif.link, 'notifWin');
6130					});
6131				}
6132
6133				notifListEl.appendChild(notifEl);
6134			})(this, notifs[i]);
6135		}
6136	}
6137
6138	shouldAnimate(newNotif);
6139};
6140
6141/**
6142 * Translates this point by the given vector.
6143 *
6144 * @param {number} dx X-coordinate of the translation.
6145 * @param {number} dy Y-coordinate of the translation.
6146 */
6147App.prototype.save = function(name, done)
6148{
6149	var file = this.getCurrentFile();
6150
6151	if (file != null && this.spinner.spin(document.body, mxResources.get('saving')))
6152	{
6153		this.editor.setStatus('');
6154
6155		if (this.editor.graph.isEditing())
6156		{
6157			this.editor.graph.stopEditing();
6158		}
6159
6160		var success = mxUtils.bind(this, function()
6161		{
6162			file.handleFileSuccess(true);
6163
6164			if (done != null)
6165			{
6166				done();
6167			}
6168		});
6169
6170		var error = mxUtils.bind(this, function(err)
6171		{
6172			if (file.isModified())
6173			{
6174				Editor.addRetryToError(err, mxUtils.bind(this, function()
6175				{
6176					this.save(name, done);
6177				}));
6178			}
6179
6180			file.handleFileError(err, true);
6181		});
6182
6183		try
6184		{
6185			if (name == file.getTitle())
6186			{
6187				file.save(true, success, error);
6188			}
6189			else
6190			{
6191				file.saveAs(name, success, error)
6192			}
6193		}
6194		catch (err)
6195		{
6196			error(err);
6197		}
6198	}
6199};
6200
6201/**
6202 * Invokes callback with null if mode does not support folder or not null
6203 * if a valid folder was chosen for a mode that supports it. No callback
6204 * is made if no folder was chosen for a mode that supports it.
6205 */
6206App.prototype.pickFolder = function(mode, fn, enabled, direct, force)
6207{
6208	enabled = (enabled != null) ? enabled : true;
6209	var resume = this.spinner.pause();
6210
6211	if (enabled && mode == App.MODE_GOOGLE && this.drive != null)
6212	{
6213		// Shows a save dialog
6214		this.drive.pickFolder(mxUtils.bind(this, function(evt)
6215		{
6216			resume();
6217
6218			if (evt.action == google.picker.Action.PICKED)
6219			{
6220				var folderId = null;
6221
6222				if (evt.docs != null && evt.docs.length > 0 && evt.docs[0].type == 'folder')
6223				{
6224					folderId = evt.docs[0].id;
6225				}
6226
6227				fn(folderId);
6228			}
6229		}), force);
6230	}
6231	else if (enabled && mode == App.MODE_ONEDRIVE && this.oneDrive != null)
6232	{
6233		this.oneDrive.pickFolder(mxUtils.bind(this, function(files)
6234		{
6235			var folderId = null;
6236			resume();
6237
6238			if (files != null && files.value != null && files.value.length > 0)
6239			{
6240				folderId = OneDriveFile.prototype.getIdOf(files.value[0]);
6241        		fn(folderId);
6242			}
6243		}), direct);
6244	}
6245	else if (enabled && mode == App.MODE_GITHUB && this.gitHub != null)
6246	{
6247		this.gitHub.pickFolder(mxUtils.bind(this, function(folderPath)
6248		{
6249			resume();
6250			fn(folderPath);
6251		}));
6252	}
6253	else if (enabled && mode == App.MODE_GITLAB && this.gitLab != null)
6254	{
6255		this.gitLab.pickFolder(mxUtils.bind(this, function(folderPath)
6256		{
6257			resume();
6258			fn(folderPath);
6259		}));
6260	}
6261	else if (enabled && mode == App.MODE_NOTION && this.notion != null)
6262	{
6263		this.notion.pickFolder(mxUtils.bind(this, function(folderPath)
6264		{
6265			resume();
6266			fn(folderPath);
6267		}));
6268	}
6269	else if (enabled && mode == App.MODE_TRELLO && this.trello != null)
6270	{
6271		this.trello.pickFolder(mxUtils.bind(this, function(cardId)
6272		{
6273			resume();
6274			fn(cardId);
6275		}));
6276	}
6277	else
6278	{
6279		EditorUi.prototype.pickFolder.apply(this, arguments);
6280	}
6281};
6282
6283/**
6284 *
6285 */
6286App.prototype.exportFile = function(data, filename, mimeType, base64Encoded, mode, folderId)
6287{
6288	if (mode == App.MODE_DROPBOX)
6289	{
6290		if (this.dropbox != null && this.spinner.spin(document.body, mxResources.get('saving')))
6291		{
6292			// LATER: Add folder picker
6293			this.dropbox.insertFile(filename, (base64Encoded) ? this.base64ToBlob(data, mimeType) :
6294				data, mxUtils.bind(this, function()
6295			{
6296				this.spinner.stop();
6297			}), mxUtils.bind(this, function(resp)
6298			{
6299				this.spinner.stop();
6300				this.handleError(resp);
6301			}));
6302		}
6303	}
6304	else if (mode == App.MODE_GOOGLE)
6305	{
6306		if (this.drive != null && this.spinner.spin(document.body, mxResources.get('saving')))
6307		{
6308			this.drive.insertFile(filename, data, folderId, mxUtils.bind(this, function(resp)
6309			{
6310				// TODO: Add callback with url param for clickable status message
6311				// "File exported. Click here to open folder."
6312//				this.editor.setStatus('<div class="geStatusMessage">' +
6313//					mxResources.get('saved') + '</div>');
6314//
6315//				// Installs click handler for opening
6316//				if (this.statusContainer != null)
6317//				{
6318//					var links = this.statusContainer.getElementsByTagName('div');
6319//
6320//					if (links.length > 0)
6321//					{
6322//						links[0].style.cursor = 'pointer';
6323//
6324//						mxEvent.addListener(links[0], 'click', mxUtils.bind(this, function()
6325//						{
6326//							if (resp != null && resp.id != null)
6327//							{
6328//								window.open('https://drive.google.com/open?id=' + resp.id);
6329//							}
6330//						}));
6331//					}
6332//				}
6333
6334				this.spinner.stop();
6335			}), mxUtils.bind(this, function(resp)
6336			{
6337				this.spinner.stop();
6338				this.handleError(resp);
6339			}), mimeType, base64Encoded);
6340		}
6341	}
6342	else if (mode == App.MODE_ONEDRIVE)
6343	{
6344		if (this.oneDrive != null && this.spinner.spin(document.body, mxResources.get('saving')))
6345		{
6346			// KNOWN: OneDrive does not show .svg extension
6347			this.oneDrive.insertFile(filename, (base64Encoded) ? this.base64ToBlob(data, mimeType) :
6348				data, mxUtils.bind(this, function()
6349			{
6350				this.spinner.stop();
6351			}), mxUtils.bind(this, function(resp)
6352			{
6353				this.spinner.stop();
6354				this.handleError(resp);
6355			}), false, folderId);
6356		}
6357	}
6358	else if (mode == App.MODE_GITHUB)
6359	{
6360		if (this.gitHub != null && this.spinner.spin(document.body, mxResources.get('saving')))
6361		{
6362			// Must insert file as library to force the file to be written
6363			this.gitHub.insertFile(filename, data, mxUtils.bind(this, function()
6364			{
6365				this.spinner.stop();
6366			}), mxUtils.bind(this, function(resp)
6367			{
6368				this.spinner.stop();
6369				this.handleError(resp);
6370			}), true, folderId, base64Encoded);
6371		}
6372	}
6373	else if (mode == App.MODE_GITLAB)
6374	{
6375		if (this.gitHub != null && this.spinner.spin(document.body, mxResources.get('saving')))
6376		{
6377			// Must insert file as library to force the file to be written
6378			this.gitLab.insertFile(filename, data, mxUtils.bind(this, function()
6379			{
6380				this.spinner.stop();
6381			}), mxUtils.bind(this, function(resp)
6382			{
6383				this.spinner.stop();
6384				this.handleError(resp);
6385			}), true, folderId, base64Encoded);
6386		}
6387	}
6388	else if (mode == App.MODE_TRELLO)
6389	{
6390		if (this.trello != null && this.spinner.spin(document.body, mxResources.get('saving')))
6391		{
6392			this.trello.insertFile(filename, (base64Encoded) ? this.base64ToBlob(data, mimeType) :
6393				data, mxUtils.bind(this, function()
6394			{
6395				this.spinner.stop();
6396			}), mxUtils.bind(this, function(resp)
6397			{
6398				this.spinner.stop();
6399				this.handleError(resp);
6400			}), false, folderId);
6401		}
6402	}
6403	else if (mode == App.MODE_BROWSER)
6404	{
6405		var fn = mxUtils.bind(this, function()
6406		{
6407			localStorage.setItem(filename, data);
6408		});
6409
6410		if (localStorage.getItem(filename) == null)
6411		{
6412			fn();
6413		}
6414		else
6415		{
6416			this.confirm(mxResources.get('replaceIt', [filename]), fn);
6417		}
6418	}
6419};
6420
6421/**
6422 * Translates this point by the given vector.
6423 *
6424 * @param {number} dx X-coordinate of the translation.
6425 * @param {number} dy Y-coordinate of the translation.
6426 */
6427App.prototype.descriptorChanged = function()
6428{
6429	var file = this.getCurrentFile();
6430
6431	if (file != null)
6432	{
6433		if (this.fname != null)
6434		{
6435			this.fnameWrapper.style.display = 'block';
6436			this.fname.innerHTML = '';
6437			var filename = (file.getTitle() != null) ? file.getTitle() : this.defaultFilename;
6438			mxUtils.write(this.fname, filename);
6439			this.fname.setAttribute('title', filename + ' - ' + mxResources.get('rename'));
6440		}
6441
6442		var graph = this.editor.graph;
6443		var editable = file.isEditable() && !file.invalidChecksum;
6444
6445		if (graph.isEnabled() && !editable)
6446		{
6447			graph.reset();
6448		}
6449
6450		graph.setEnabled(editable);
6451
6452		// Ignores title and hash for revisions
6453		if (urlParams['rev'] == null)
6454		{
6455			this.updateDocumentTitle();
6456			var newHash = file.getHash();
6457
6458			if (newHash.length > 0)
6459			{
6460				window.location.hash = newHash;
6461			}
6462			else if (window.location.hash.length > 0)
6463			{
6464				window.location.hash = '';
6465			}
6466		}
6467	}
6468
6469	this.updateUi();
6470
6471	// Refresh if editable state has changed
6472	if (this.format != null && (file == null ||
6473		this.fileEditable != file.isEditable()) &&
6474		this.editor.graph.isSelectionEmpty())
6475	{
6476		this.format.refresh();
6477		this.fileEditable = (file != null) ? file.isEditable() : null;
6478	}
6479
6480	this.fireEvent(new mxEventObject('fileDescriptorChanged', 'file', file));
6481};
6482
6483/**
6484 * Adds the listener for automatically saving the diagram for local changes.
6485 */
6486App.prototype.showAuthDialog = function(peer, showRememberOption, fn, closeFn)
6487{
6488	var resume = this.spinner.pause();
6489
6490	this.showDialog(new AuthDialog(this, peer, showRememberOption, mxUtils.bind(this, function(remember)
6491	{
6492		try
6493		{
6494			if (fn != null)
6495			{
6496				fn(remember, mxUtils.bind(this, function()
6497				{
6498					this.hideDialog();
6499					resume();
6500				}));
6501			}
6502		}
6503		catch (e)
6504		{
6505			this.editor.setStatus(mxUtils.htmlEntities(e.message));
6506		}
6507	})).container, 300, (showRememberOption) ? 180 : 140, true, true, mxUtils.bind(this, function(cancel)
6508	{
6509		if (closeFn != null)
6510		{
6511			closeFn(cancel);
6512		}
6513
6514		if (cancel && this.getCurrentFile() == null && this.dialog == null)
6515		{
6516			this.showSplash();
6517		}
6518	}));
6519};
6520
6521/**
6522 * Checks if the client is authorized and calls the next step. The optional
6523 * readXml argument is used for import. Default is false. The optional
6524 * readLibrary argument is used for reading libraries. Default is false.
6525 */
6526App.prototype.convertFile = function(url, filename, mimeType, extension, success, error, executeRequest, headers)
6527{
6528	var name = filename;
6529
6530	// SVG file extensions are valid and needed for image import
6531	if (!/\.svg$/i.test(name))
6532	{
6533		name = name.substring(0, filename.lastIndexOf('.')) + extension;
6534	}
6535
6536	var gitHubUrl = false;
6537
6538	if (this.gitHub != null && url.substring(0, this.gitHub.baseUrl.length) == this.gitHub.baseUrl)
6539	{
6540		gitHubUrl = true;
6541	}
6542
6543	// Workaround for wrong binary response with VSD(X) & VDX files
6544	if (/\.v(dx|sdx?)$/i.test(filename) && Graph.fileSupport && new XMLHttpRequest().upload &&
6545		typeof new XMLHttpRequest().responseType === 'string')
6546	{
6547		var req = new XMLHttpRequest();
6548		req.open('GET', url, true);
6549
6550		if (!gitHubUrl)
6551		{
6552			req.responseType = 'blob';
6553		}
6554
6555		if (headers)
6556		{
6557			for (var key in headers)
6558			{
6559				req.setRequestHeader(key, headers[key]);
6560			}
6561		}
6562
6563		req.onload = mxUtils.bind(this, function()
6564		{
6565			if (req.status >= 200 && req.status <= 299)
6566			{
6567				var blob = null;
6568
6569				if (gitHubUrl)
6570				{
6571					var file = JSON.parse(req.responseText);
6572					blob = this.base64ToBlob(file.content, 'application/octet-stream');
6573				}
6574				else
6575				{
6576					blob = new Blob([req.response], {type: 'application/octet-stream'});
6577				}
6578
6579				this.importVisio(blob, mxUtils.bind(this, function(xml)
6580				{
6581					success(new LocalFile(this, xml, name, true));
6582				}), error, filename)
6583			}
6584			else if (error != null)
6585			{
6586				error({message: mxResources.get('errorLoadingFile')});
6587			}
6588		});
6589
6590		req.onerror = error;
6591		req.send();
6592	}
6593	else
6594	{
6595		var handleData = mxUtils.bind(this, function(data)
6596		{
6597			try
6598			{
6599				if (/\.pdf$/i.test(filename))
6600				{
6601					var temp = Editor.extractGraphModelFromPdf(data);
6602
6603					if (temp != null && temp.length > 0)
6604					{
6605						success(new LocalFile(this, temp, name, true));
6606					}
6607				}
6608				else if (/\.png$/i.test(filename))
6609				{
6610					var temp = this.extractGraphModelFromPng(data);
6611
6612					if (temp != null)
6613					{
6614						success(new LocalFile(this, temp, name, true));
6615					}
6616					else
6617					{
6618						success(new LocalFile(this, data, filename, true));
6619					}
6620				}
6621				else if (Graph.fileSupport && new XMLHttpRequest().upload && this.isRemoteFileFormat(data, url))
6622				{
6623					this.parseFile(new Blob([data], {type: 'application/octet-stream'}), mxUtils.bind(this, function(xhr)
6624					{
6625						if (xhr.readyState == 4)
6626						{
6627							if (xhr.status >= 200 && xhr.status <= 299)
6628							{
6629								success(new LocalFile(this, xhr.responseText, name, true));
6630							}
6631							else if (error != null)
6632							{
6633								error({message: mxResources.get('errorLoadingFile')});
6634							}
6635						}
6636					}), filename);
6637				}
6638				else
6639				{
6640					success(new LocalFile(this, data, name, true));
6641				}
6642			}
6643			catch (e)
6644			{
6645				if (error != null)
6646				{
6647					error(e);
6648				}
6649			}
6650		});
6651
6652		var binary = /\.png$/i.test(filename) || /\.jpe?g$/i.test(filename) ||
6653		 	/\.pdf$/i.test(filename) || (mimeType != null &&
6654		 	mimeType.substring(0, 6) == 'image/');
6655
6656		// NOTE: Cannot force non-binary request via loadUrl so needs separate
6657		// code as decoding twice on content with binary data did not work
6658		if (gitHubUrl)
6659		{
6660			mxUtils.get(url, mxUtils.bind(this, function(req)
6661			{
6662				if (req.getStatus() >= 200 && req.getStatus() <= 299)
6663				{
6664			    	if (success != null)
6665			    	{
6666				    	var file = JSON.parse(req.getText());
6667				    	var data = file.content;
6668
6669				    	if (file.encoding === 'base64')
6670				    	{
6671				    		if (/\.png$/i.test(filename))
6672					    	{
6673					    		data = 'data:image/png;base64,' + data;
6674					    	}
6675				    		else if (/\.pdf$/i.test(filename))
6676					    	{
6677					    		data = 'data:application/pdf;base64,' + data;
6678					    	}
6679				    		else
6680					    	{
6681					    		// Workaround for character encoding issues in IE10/11
6682					    		data = (window.atob && !mxClient.IS_IE && !mxClient.IS_IE11) ? atob(data) : Base64.decode(data);
6683					    	}
6684				    	}
6685
6686				    	handleData(data);
6687			    	}
6688				}
6689				else if (error != null)
6690		    	{
6691		    		error({code: App.ERROR_UNKNOWN});
6692		    	}
6693			}), function()
6694			{
6695		    	if (error != null)
6696		    	{
6697		    		error({code: App.ERROR_UNKNOWN});
6698		    	}
6699			}, false, this.timeout, function()
6700		    {
6701		    	if (error != null)
6702				{
6703					error({code: App.ERROR_TIMEOUT, retry: fn});
6704				}
6705		    }, headers);
6706		}
6707		else if (executeRequest != null)
6708		{
6709			executeRequest(url, handleData, error, binary);
6710		}
6711		else
6712		{
6713			this.editor.loadUrl(url, handleData, error, binary, null, null, null, headers);
6714		}
6715	}
6716};
6717
6718/**
6719 * Adds the listener for automatically saving the diagram for local changes.
6720 */
6721App.prototype.updateHeader = function()
6722{
6723	if (this.menubar != null)
6724	{
6725		this.appIcon = document.createElement('a');
6726		this.appIcon.style.display = 'block';
6727		this.appIcon.style.position = 'absolute';
6728		this.appIcon.style.width = '32px';
6729		this.appIcon.style.height = (this.menubarHeight - 28) + 'px';
6730		this.appIcon.style.margin = '14px 0px 8px 16px';
6731		this.appIcon.style.opacity = '0.85';
6732		this.appIcon.style.borderRadius = '3px';
6733
6734		if (uiTheme != 'dark')
6735		{
6736			this.appIcon.style.backgroundColor = '#f08705';
6737		}
6738
6739		mxEvent.disableContextMenu(this.appIcon);
6740
6741		mxEvent.addListener(this.appIcon, 'click', mxUtils.bind(this, function(evt)
6742		{
6743			this.appIconClicked(evt);
6744		}));
6745
6746		// LATER: Use Alpha image loader in IE6
6747		// NOTE: This uses the diagram bit of the old logo as it looks better in this case
6748		//this.appIcon.style.filter = 'progid:DXImageTransform.Microsoft.AlphaImageLoader(src=' + IMAGE_PATH + '/logo-white.png,sizingMethod=\'scale\')';
6749		var logo = (!mxClient.IS_SVG) ? 'url(\'' + IMAGE_PATH + '/logo-white.png\')' :
6750			((uiTheme == 'dark') ? 'url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIgogICB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgZW5hYmxlLWJhY2tncm91bmQ9Im5ldyAwIDAgMzA2LjE4NSAxMjAuMjk2IgogICB2aWV3Qm94PSIyNCAyNiA2OCA2OCIKICAgeT0iMHB4IgogICB4PSIwcHgiCiAgIHZlcnNpb249IjEuMSI+CiAgIAkgPGc+PGxpbmUKICAgICAgIHkyPSI3Mi4zOTQiCiAgICAgICB4Mj0iNDEuMDYxIgogICAgICAgeTE9IjQzLjM4NCIKICAgICAgIHgxPSI1OC4wNjkiCiAgICAgICBzdHJva2UtbWl0ZXJsaW1pdD0iMTAiCiAgICAgICBzdHJva2Utd2lkdGg9IjMuNTUyOCIKICAgICAgIHN0cm9rZT0iI0ZGRkZGRiIKICAgICAgIGZpbGw9Im5vbmUiIC8+PGxpbmUKICAgICAgIHkyPSI3Mi4zOTQiCiAgICAgICB4Mj0iNzUuMDc2IgogICAgICAgeTE9IjQzLjM4NCIKICAgICAgIHgxPSI1OC4wNjgiCiAgICAgICBzdHJva2UtbWl0ZXJsaW1pdD0iMTAiCiAgICAgICBzdHJva2Utd2lkdGg9IjMuNTAwOCIKICAgICAgIHN0cm9rZT0iI0ZGRkZGRiIKICAgICAgIGZpbGw9Im5vbmUiIC8+PGc+PHBhdGgKICAgICAgICAgZD0iTTUyLjc3Myw3Ny4wODRjMCwxLjk1NC0xLjU5OSwzLjU1My0zLjU1MywzLjU1M0gzNi45OTljLTEuOTU0LDAtMy41NTMtMS41OTktMy41NTMtMy41NTN2LTkuMzc5ICAgIGMwLTEuOTU0LDEuNTk5LTMuNTUzLDMuNTUzLTMuNTUzaDEyLjIyMmMxLjk1NCwwLDMuNTUzLDEuNTk5LDMuNTUzLDMuNTUzVjc3LjA4NHoiCiAgICAgICAgIGZpbGw9IiNGRkZGRkYiIC8+PC9nPjxnCiAgICAgICBpZD0iZzM0MTkiPjxwYXRoCiAgICAgICAgIGQ9Ik02Ny43NjIsNDguMDc0YzAsMS45NTQtMS41OTksMy41NTMtMy41NTMsMy41NTNINTEuOTg4Yy0xLjk1NCwwLTMuNTUzLTEuNTk5LTMuNTUzLTMuNTUzdi05LjM3OSAgICBjMC0xLjk1NCwxLjU5OS0zLjU1MywzLjU1My0zLjU1M0g2NC4yMWMxLjk1NCwwLDMuNTUzLDEuNTk5LDMuNTUzLDMuNTUzVjQ4LjA3NHoiCiAgICAgICAgIGZpbGw9IiNGRkZGRkYiIC8+PC9nPjxnPjxwYXRoCiAgICAgICAgIGQ9Ik04Mi43NTIsNzcuMDg0YzAsMS45NTQtMS41OTksMy41NTMtMy41NTMsMy41NTNINjYuOTc3Yy0xLjk1NCwwLTMuNTUzLTEuNTk5LTMuNTUzLTMuNTUzdi05LjM3OSAgICBjMC0xLjk1NCwxLjU5OS0zLjU1MywzLjU1My0zLjU1M2gxMi4yMjJjMS45NTQsMCwzLjU1MywxLjU5OSwzLjU1MywzLjU1M1Y3Ny4wODR6IgogICAgICAgICBmaWxsPSIjRkZGRkZGIiAvPjwvZz48L2c+PC9zdmc+)' :
6751			'url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPHN2ZyB2ZXJzaW9uPSIxLjEiIGlkPSJFYmVuZV8xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4PSIwcHgiIHk9IjBweCIKCSB2aWV3Qm94PSIwIDAgMjI1IDIyNSIgc3R5bGU9ImVuYWJsZS1iYWNrZ3JvdW5kOm5ldyAwIDAgMjI1IDIyNTsiIHhtbDpzcGFjZT0icHJlc2VydmUiPgo8c3R5bGUgdHlwZT0idGV4dC9jc3MiPgoJLnN0MXtmaWxsOiNERjZDMEM7fQoJLnN0MntmaWxsOiNGRkZGRkY7fQo8L3N0eWxlPgo8cGF0aCBjbGFzcz0ic3QxIiBkPSJNMjI1LDIxNS40YzAsNS4zLTQuMyw5LjYtOS41LDkuNmwwLDBINzcuMWwtNDQuOC00NS41TDYwLjIsMTM0bDgyLjctMTAyLjdsODIuMSw4NC41VjIxNS40eiIvPgo8cGF0aCBjbGFzcz0ic3QyIiBkPSJNMTg0LjYsMTI1LjhoLTIzLjdsLTI1LTQyLjdjNS43LTEuMiw5LjgtNi4yLDkuNy0xMlYzOWMwLTYuOC01LjQtMTIuMy0xMi4yLTEyLjNoLTAuMUg5MS42CgljLTYuOCwwLTEyLjMsNS40LTEyLjMsMTIuMlYzOXYzMi4xYzAsNS44LDQsMTAuOCw5LjcsMTJsLTI1LDQyLjdINDAuNGMtNi44LDAtMTIuMyw1LjQtMTIuMywxMi4ydjAuMXYzMi4xCgljMCw2LjgsNS40LDEyLjMsMTIuMiwxMi4zaDAuMWg0MS43YzYuOCwwLDEyLjMtNS40LDEyLjMtMTIuMnYtMC4xdi0zMi4xYzAtNi44LTUuNC0xMi4zLTEyLjItMTIuM2gtMC4xaC00bDI0LjgtNDIuNGgxOS4zCglsMjQuOSw0Mi40SDE0M2MtNi44LDAtMTIuMyw1LjQtMTIuMywxMi4ydjAuMXYzMi4xYzAsNi44LDUuNCwxMi4zLDEyLjIsMTIuM2gwLjFoNDEuN2M2LjgsMCwxMi4zLTUuNCwxMi4zLTEyLjJ2LTAuMXYtMzIuMQoJYzAtNi44LTUuNC0xMi4zLTEyLjItMTIuM0MxODQuNywxMjUuOCwxODQuNywxMjUuOCwxODQuNiwxMjUuOHoiLz4KPC9zdmc+Cg==)');
6752		this.appIcon.style.backgroundImage = logo;
6753		this.appIcon.style.backgroundPosition = 'center center';
6754		this.appIcon.style.backgroundSize = '100% 100%';
6755		this.appIcon.style.backgroundRepeat = 'no-repeat';
6756
6757		mxUtils.setPrefixedStyle(this.appIcon.style, 'transition', 'all 125ms linear');
6758
6759		mxEvent.addListener(this.appIcon, 'mouseover', mxUtils.bind(this, function()
6760		{
6761			var file = this.getCurrentFile();
6762
6763			if (file != null)
6764			{
6765				var mode = file.getMode();
6766
6767				if (mode == App.MODE_GOOGLE)
6768				{
6769					this.appIcon.style.backgroundImage = 'url(' + IMAGE_PATH + '/google-drive-logo-white.svg)';
6770					this.appIcon.style.backgroundSize = '70% 70%';
6771				}
6772				else if (mode == App.MODE_DROPBOX)
6773				{
6774					this.appIcon.style.backgroundImage = 'url(' + IMAGE_PATH + '/dropbox-logo-white.svg)';
6775					this.appIcon.style.backgroundSize = '70% 70%';
6776				}
6777				else if (mode == App.MODE_ONEDRIVE)
6778				{
6779					this.appIcon.style.backgroundImage = 'url(' + IMAGE_PATH + '/onedrive-logo-white.svg)';
6780					this.appIcon.style.backgroundSize = '70% 70%';
6781				}
6782				else if (mode == App.MODE_GITHUB)
6783				{
6784					this.appIcon.style.backgroundImage = 'url(' + IMAGE_PATH + '/github-logo-white.svg)';
6785					this.appIcon.style.backgroundSize = '70% 70%';
6786				}
6787				else if (mode == App.MODE_GITLAB)
6788				{
6789					this.appIcon.style.backgroundImage = 'url(' + IMAGE_PATH + '/gitlab-logo-white.svg)';
6790					this.appIcon.style.backgroundSize = '100% 100%';
6791				}
6792				else if (mode == App.MODE_NOTION)
6793				{
6794					this.appIcon.style.backgroundImage = 'url(' + IMAGE_PATH + '/notion-logo-white.svg)';
6795					this.appIcon.style.backgroundSize = '70% 70%';
6796				}
6797				else if (mode == App.MODE_TRELLO)
6798				{
6799					this.appIcon.style.backgroundImage = 'url(' + IMAGE_PATH + '/trello-logo-white-orange.svg)';
6800					this.appIcon.style.backgroundSize = '70% 70%';
6801				}
6802			}
6803		}));
6804
6805		mxEvent.addListener(this.appIcon, 'mouseout', mxUtils.bind(this, function()
6806		{
6807			this.appIcon.style.backgroundImage = logo;
6808			this.appIcon.style.backgroundSize = '90% 90%';
6809		}));
6810
6811		if (urlParams['embed'] != '1')
6812		{
6813			this.menubarContainer.appendChild(this.appIcon);
6814		}
6815
6816		this.fnameWrapper = document.createElement('div');
6817		this.fnameWrapper.style.position = 'absolute';
6818		this.fnameWrapper.style.right = '120px';
6819		this.fnameWrapper.style.left = '60px';
6820		this.fnameWrapper.style.top = '9px';
6821		this.fnameWrapper.style.height = '26px';
6822		this.fnameWrapper.style.display = 'none';
6823		this.fnameWrapper.style.overflow = 'hidden';
6824		this.fnameWrapper.style.textOverflow = 'ellipsis';
6825
6826		this.fname = document.createElement('a');
6827		this.fname.setAttribute('title', mxResources.get('rename'));
6828		this.fname.className = 'geItem';
6829		this.fname.style.padding = '2px 8px 2px 8px';
6830		this.fname.style.display = 'inline';
6831		this.fname.style.fontSize = '18px';
6832		this.fname.style.whiteSpace = 'nowrap';
6833
6834		// Prevents focus
6835        mxEvent.addListener(this.fname, (mxClient.IS_POINTER) ? 'pointerdown' : 'mousedown',
6836        	mxUtils.bind(this, function(evt)
6837        {
6838			evt.preventDefault();
6839		}));
6840
6841		mxEvent.addListener(this.fname, 'click', mxUtils.bind(this, function(evt)
6842		{
6843			var file = this.getCurrentFile();
6844
6845			if (file != null && file.isRenamable())
6846			{
6847				if (this.editor.graph.isEditing())
6848				{
6849					this.editor.graph.stopEditing();
6850				}
6851
6852				this.actions.get('rename').funct();
6853			}
6854
6855			mxEvent.consume(evt);
6856		}));
6857
6858		this.fnameWrapper.appendChild(this.fname);
6859
6860		if (urlParams['embed'] != '1')
6861		{
6862			this.menubarContainer.appendChild(this.fnameWrapper);
6863
6864			this.menubar.container.style.position = 'absolute';
6865			this.menubar.container.style.paddingLeft = '59px';
6866			this.toolbar.container.style.paddingLeft = '16px';
6867			this.menubar.container.style.boxSizing = 'border-box';
6868			this.menubar.container.style.top = '34px';
6869		}
6870
6871		/**
6872		 * Adds format panel toggle.
6873		 */
6874		this.toggleFormatElement = document.createElement('a');
6875		this.toggleFormatElement.setAttribute('title', mxResources.get('formatPanel') + ' (' + Editor.ctrlKey + '+Shift+P)');
6876		this.toggleFormatElement.style.position = 'absolute';
6877		this.toggleFormatElement.style.display = 'inline-block';
6878		this.toggleFormatElement.style.top = (uiTheme == 'atlas') ? '8px' : '6px';
6879		this.toggleFormatElement.style.right = (uiTheme != 'atlas' && urlParams['embed'] != '1') ? '30px' : '10px';
6880		this.toggleFormatElement.style.padding = '2px';
6881		this.toggleFormatElement.style.fontSize = '14px';
6882		this.toggleFormatElement.className = (uiTheme != 'atlas') ? 'geButton' : '';
6883		this.toggleFormatElement.style.width = '16px';
6884		this.toggleFormatElement.style.height = '16px';
6885		this.toggleFormatElement.style.backgroundPosition = '50% 50%';
6886		this.toggleFormatElement.style.backgroundRepeat = 'no-repeat';
6887		this.toolbarContainer.appendChild(this.toggleFormatElement);
6888
6889		if (uiTheme == 'dark')
6890		{
6891			this.toggleFormatElement.style.filter = 'invert(100%)';
6892		}
6893
6894		// Prevents focus
6895	    mxEvent.addListener(this.toggleFormatElement, (mxClient.IS_POINTER) ? 'pointerdown' : 'mousedown',
6896        	mxUtils.bind(this, function(evt)
6897    	{
6898			evt.preventDefault();
6899		}));
6900
6901		mxEvent.addListener(this.toggleFormatElement, 'click', mxUtils.bind(this, function(evt)
6902		{
6903			this.actions.get('formatPanel').funct();
6904			mxEvent.consume(evt);
6905		}));
6906
6907		var toggleFormatPanel = mxUtils.bind(this, function()
6908		{
6909			if (this.formatWidth > 0)
6910			{
6911				this.toggleFormatElement.style.backgroundImage = 'url(\'' + this.formatShowImage + '\')';
6912			}
6913			else
6914			{
6915				this.toggleFormatElement.style.backgroundImage = 'url(\'' + this.formatHideImage + '\')';
6916			}
6917		});
6918
6919		this.addListener('formatWidthChanged', toggleFormatPanel);
6920		toggleFormatPanel();
6921
6922		this.fullscreenElement = document.createElement('a');
6923		this.fullscreenElement.setAttribute('title', mxResources.get('fullscreen'));
6924		this.fullscreenElement.style.position = 'absolute';
6925		this.fullscreenElement.style.display = 'inline-block';
6926		this.fullscreenElement.style.top = (uiTheme == 'atlas') ? '8px' : '6px';
6927		this.fullscreenElement.style.right = (uiTheme != 'atlas' && urlParams['embed'] != '1') ? '50px' : '30px';
6928		this.fullscreenElement.style.padding = '2px';
6929		this.fullscreenElement.style.fontSize = '14px';
6930		this.fullscreenElement.className = (uiTheme != 'atlas') ? 'geButton' : '';
6931		this.fullscreenElement.style.width = '16px';
6932		this.fullscreenElement.style.height = '16px';
6933		this.fullscreenElement.style.backgroundPosition = '50% 50%';
6934		this.fullscreenElement.style.backgroundRepeat = 'no-repeat';
6935		this.fullscreenElement.style.backgroundImage = 'url(\'' + this.fullscreenImage + '\')';
6936		this.toolbarContainer.appendChild(this.fullscreenElement);
6937
6938		// Prevents focus
6939		mxEvent.addListener(this.fullscreenElement, (mxClient.IS_POINTER) ? 'pointerdown' : 'mousedown',
6940        	mxUtils.bind(this, function(evt)
6941    	{
6942			evt.preventDefault();
6943		}));
6944
6945		// Some style changes in Atlas theme
6946		if (uiTheme == 'atlas')
6947		{
6948			mxUtils.setOpacity(this.toggleFormatElement, 70);
6949			mxUtils.setOpacity(this.fullscreenElement, 70);
6950		}
6951
6952		var initialPosition = this.hsplitPosition;
6953
6954		if (uiTheme == 'dark')
6955		{
6956			this.fullscreenElement.style.filter = 'invert(100%)';
6957		}
6958
6959		mxEvent.addListener(this.fullscreenElement, 'click', mxUtils.bind(this, function(evt)
6960		{
6961			var visible = this.fullscreenMode;
6962
6963			if (uiTheme != 'atlas' && urlParams['embed'] != '1')
6964			{
6965				this.toggleCompactMode(visible);
6966			}
6967
6968			if (!visible)
6969			{
6970				initialPosition = this.hsplitPosition;
6971			}
6972
6973			this.hsplitPosition = (visible) ? initialPosition : 0;
6974			this.toggleFormatPanel(visible);
6975			this.fullscreenMode = !visible;
6976			mxEvent.consume(evt);
6977		}));
6978
6979		/**
6980		 * Adds compact UI toggle.
6981		 */
6982		if (urlParams['embed'] != '1')
6983		{
6984			this.toggleElement = document.createElement('a');
6985			this.toggleElement.setAttribute('title', mxResources.get('collapseExpand'));
6986			this.toggleElement.className = 'geButton';
6987			this.toggleElement.style.position = 'absolute';
6988			this.toggleElement.style.display = 'inline-block';
6989			this.toggleElement.style.width = '16px';
6990			this.toggleElement.style.height = '16px';
6991			this.toggleElement.style.color = '#666';
6992			this.toggleElement.style.top = (uiTheme == 'atlas') ? '8px' : '6px';
6993			this.toggleElement.style.right = '10px';
6994			this.toggleElement.style.padding = '2px';
6995			this.toggleElement.style.fontSize = '14px';
6996			this.toggleElement.style.textDecoration = 'none';
6997			this.toggleElement.style.backgroundImage = 'url(\'' + this.chevronUpImage + '\')';
6998
6999			this.toggleElement.style.backgroundPosition = '50% 50%';
7000			this.toggleElement.style.backgroundRepeat = 'no-repeat';
7001
7002			if (uiTheme == 'dark')
7003			{
7004				this.toggleElement.style.filter = 'invert(100%)';
7005			}
7006
7007			// Prevents focus
7008			mxEvent.addListener(this.toggleElement, (mxClient.IS_POINTER) ? 'pointerdown' : 'mousedown',
7009	        	mxUtils.bind(this, function(evt)
7010	    	{
7011				evt.preventDefault();
7012			}));
7013
7014			// Toggles compact mode
7015			mxEvent.addListener(this.toggleElement, 'click', mxUtils.bind(this, function(evt)
7016			{
7017				this.toggleCompactMode();
7018				mxEvent.consume(evt);
7019			}));
7020
7021			if (uiTheme != 'atlas')
7022			{
7023				this.toolbarContainer.appendChild(this.toggleElement);
7024			}
7025
7026			// Enable compact mode for small screens except for Firefox where the height is wrong
7027			if (!mxClient.IS_FF && screen.height <= 740 && typeof this.toggleElement.click !== 'undefined')
7028			{
7029				window.setTimeout(mxUtils.bind(this, function()
7030				{
7031					this.toggleElement.click();
7032				}), 0);
7033			}
7034		}
7035	}
7036};
7037
7038/**
7039 * Adds the listener for automatically saving the diagram for local changes.
7040 */
7041App.prototype.toggleCompactMode = function(visible)
7042{
7043	visible = (visible != null) ? visible : this.compactMode;
7044
7045	if (visible)
7046	{
7047		this.menubar.container.style.position = 'absolute';
7048		this.menubar.container.style.paddingLeft = '59px';
7049		this.menubar.container.style.paddingTop = '';
7050		this.menubar.container.style.paddingBottom = '';
7051		this.menubar.container.style.top = '34px';
7052		this.toolbar.container.style.paddingLeft = '16px';
7053		this.buttonContainer.style.visibility = 'visible';
7054		this.appIcon.style.display = 'block';
7055		this.fnameWrapper.style.display = 'block';
7056		this.fnameWrapper.style.visibility = 'visible';
7057		this.menubarHeight = App.prototype.menubarHeight;
7058		this.refresh();
7059		this.toggleElement.style.backgroundImage = 'url(\'' + this.chevronUpImage + '\')';
7060	}
7061	else
7062	{
7063		this.menubar.container.style.position = 'relative';
7064		this.menubar.container.style.paddingLeft = '4px';
7065		this.menubar.container.style.paddingTop = '0px';
7066		this.menubar.container.style.paddingBottom = '0px';
7067		this.menubar.container.style.top = '0px';
7068		this.toolbar.container.style.paddingLeft = '8px';
7069		this.buttonContainer.style.visibility = 'hidden';
7070		this.appIcon.style.display = 'none';
7071		this.fnameWrapper.style.display = 'none';
7072		this.fnameWrapper.style.visibility = 'hidden';
7073		this.menubarHeight = EditorUi.prototype.menubarHeight;
7074		this.refresh();
7075		this.toggleElement.style.backgroundImage = 'url(\'' + this.chevronDownImage + '\')';
7076	}
7077
7078	this.compactMode = !visible;
7079};
7080
7081/**
7082 * Adds the listener for automatically saving the diagram for local changes.
7083 */
7084App.prototype.updateUserElement = function()
7085{
7086	if ((this.drive == null || this.drive.getUser() == null) &&
7087		(this.oneDrive == null || this.oneDrive.getUser() == null) &&
7088		(this.dropbox == null || this.dropbox.getUser() == null) &&
7089		(this.gitHub == null || this.gitHub.getUser() == null) &&
7090		(this.gitLab == null || this.gitLab.getUser() == null) &&
7091		(this.notion == null || this.notion.getUser() == null) &&
7092		(this.trello == null || !this.trello.isAuthorized())) //TODO Trello no user issue
7093	{
7094		if (this.userElement != null)
7095		{
7096			this.userElement.parentNode.removeChild(this.userElement);
7097			this.userElement = null;
7098		}
7099	}
7100	else
7101	{
7102		if (this.userElement == null)
7103		{
7104			this.userElement = document.createElement('a');
7105			this.userElement.className = 'geItem';
7106			this.userElement.style.position = 'absolute';
7107			this.userElement.style.fontSize = '8pt';
7108			this.userElement.style.top = (uiTheme == 'atlas') ? '8px' : '2px';
7109			this.userElement.style.right = '30px';
7110			this.userElement.style.margin = '4px';
7111			this.userElement.style.padding = '2px';
7112			this.userElement.style.paddingRight = '16px';
7113			this.userElement.style.verticalAlign = 'middle';
7114			this.userElement.style.backgroundImage =  'url(' + IMAGE_PATH + '/expanded.gif)';
7115			this.userElement.style.backgroundPosition = '100% 60%';
7116			this.userElement.style.backgroundRepeat = 'no-repeat';
7117
7118			this.menubarContainer.appendChild(this.userElement);
7119
7120			// Prevents focus
7121			mxEvent.addListener(this.userElement, (mxClient.IS_POINTER) ? 'pointerdown' : 'mousedown',
7122	        	mxUtils.bind(this, function(evt)
7123	    	{
7124				evt.preventDefault();
7125			}));
7126
7127			mxEvent.addListener(this.userElement, 'click', mxUtils.bind(this, function(evt)
7128			{
7129				if (this.userPanel == null)
7130				{
7131					var div = document.createElement('div');
7132					div.className = 'geDialog';
7133					div.style.position = 'absolute';
7134					div.style.top = (this.userElement.clientTop +
7135						this.userElement.clientHeight + 6) + 'px';
7136					div.style.zIndex = 5;
7137					div.style.right = '36px';
7138					div.style.padding = '0px';
7139					div.style.cursor = 'default';
7140					div.style.minWidth = '300px';
7141
7142					this.userPanel = div;
7143				}
7144
7145				if (this.userPanel.parentNode != null)
7146				{
7147					this.userPanel.parentNode.removeChild(this.userPanel);
7148				}
7149				else
7150				{
7151					var connected = false;
7152					this.userPanel.innerHTML = '';
7153
7154					var img = document.createElement('img');
7155
7156					img.setAttribute('src', Dialog.prototype.closeImage);
7157					img.setAttribute('title', mxResources.get('close'));
7158					img.className = 'geDialogClose';
7159					img.style.top = '8px';
7160					img.style.right = '8px';
7161
7162					mxEvent.addListener(img, 'click', mxUtils.bind(this, function()
7163					{
7164						if (this.userPanel.parentNode != null)
7165						{
7166							this.userPanel.parentNode.removeChild(this.userPanel);
7167						}
7168					}));
7169
7170					this.userPanel.appendChild(img);
7171
7172					if (this.drive != null)
7173					{
7174						var driveUsers = this.drive.getUsersList();
7175
7176						if (driveUsers.length > 0)
7177						{
7178							// LATER: Cannot change user while file is open since close will not work with new
7179							// credentials and closing the file using fileLoaded(null) will show splash dialog.
7180							var closeFile = mxUtils.bind(this, function(callback, spinnerMsg)
7181							{
7182								var file = this.getCurrentFile();
7183
7184								if (file != null && file.constructor == DriveFile)
7185								{
7186									this.spinner.spin(document.body, spinnerMsg);
7187
7188//									file.close();
7189									this.fileLoaded(null);
7190
7191									// LATER: Use callback to wait for thumbnail update
7192									window.setTimeout(mxUtils.bind(this, function()
7193									{
7194										this.spinner.stop();
7195										callback();
7196									}), 2000);
7197								}
7198								else
7199								{
7200									callback();
7201								}
7202							});
7203
7204							var createUserRow = mxUtils.bind(this, function (user)
7205							{
7206								var tr = document.createElement('tr');
7207								tr.setAttribute('title', 'User ID: ' + user.id);
7208
7209								var td = document.createElement('td');
7210								td.setAttribute('valig', 'middle');
7211								td.style.height = '59px';
7212								td.style.width = '66px';
7213
7214								var img = document.createElement('img');
7215								img.setAttribute('width', '50');
7216								img.setAttribute('height', '50');
7217								img.setAttribute('border', '0');
7218								img.setAttribute('src', (user.pictureUrl != null) ? user.pictureUrl : this.defaultUserPicture);
7219								img.style.borderRadius = '50%';
7220								img.style.margin = '4px 8px 0 8px';
7221								td.appendChild(img);
7222								tr.appendChild(td);
7223
7224								var td = document.createElement('td');
7225								td.setAttribute('valign', 'middle');
7226								td.style.whiteSpace = 'nowrap';
7227								td.style.paddingTop = '4px';
7228								td.style.maxWidth = '0';
7229								td.style.overflow = 'hidden';
7230								td.style.textOverflow = 'ellipsis';
7231								mxUtils.write(td, user.displayName +
7232									((user.isCurrent && driveUsers.length > 1) ?
7233									' (' + mxResources.get('default') + ')' : ''));
7234
7235								if (user.email != null)
7236								{
7237									mxUtils.br(td);
7238
7239									var small = document.createElement('small');
7240									small.style.color = 'gray';
7241									mxUtils.write(small, user.email);
7242									td.appendChild(small);
7243								}
7244
7245								var div = document.createElement('div');
7246								div.style.marginTop = '4px';
7247
7248								var i = document.createElement('i');
7249								mxUtils.write(i, mxResources.get('googleDrive'));
7250								div.appendChild(i);
7251								td.appendChild(div);
7252								tr.appendChild(td);
7253
7254								if (!user.isCurrent)
7255								{
7256									tr.style.cursor = 'pointer';
7257									tr.style.opacity = '0.3';
7258
7259									mxEvent.addListener(tr, 'click', mxUtils.bind(this, function(evt)
7260									{
7261										closeFile(mxUtils.bind(this, function()
7262										{
7263											this.stateArg = null;
7264											this.drive.setUser(user);
7265
7266											this.drive.authorize(true, mxUtils.bind(this, function()
7267											{
7268												this.setMode(App.MODE_GOOGLE);
7269												this.hideDialog();
7270												this.showSplash();
7271											}), mxUtils.bind(this, function(resp)
7272											{
7273												this.handleError(resp);
7274											}), true); //Remember is true since add account imply keeping that account
7275										}), mxResources.get('closingFile') + '...');
7276
7277										mxEvent.consume(evt);
7278									}));
7279								}
7280
7281								return tr;
7282							});
7283
7284							connected = true;
7285
7286							var driveUserTable = document.createElement('table');
7287							driveUserTable.style.borderSpacing = '0';
7288							driveUserTable.style.fontSize = '10pt';
7289							driveUserTable.style.width = '100%';
7290							driveUserTable.style.padding = '10px';
7291
7292							for (var i = 0; i < driveUsers.length; i++)
7293							{
7294								driveUserTable.appendChild(createUserRow(driveUsers[i]));
7295							}
7296
7297							this.userPanel.appendChild(driveUserTable);
7298
7299							var div = document.createElement('div');
7300							div.style.textAlign = 'left';
7301							div.style.padding = '10px';
7302							div.style.whiteSpace = 'nowrap';
7303							div.style.borderTop = '1px solid rgb(224, 224, 224)';
7304
7305							var btn = mxUtils.button(mxResources.get('signOut'), mxUtils.bind(this, function()
7306							{
7307								this.confirm(mxResources.get('areYouSure'), mxUtils.bind(this, function()
7308								{
7309									closeFile(mxUtils.bind(this, function()
7310									{
7311										this.stateArg = null;
7312										this.drive.logout();
7313										this.setMode(App.MODE_GOOGLE);
7314										this.hideDialog();
7315										this.showSplash();
7316									}), mxResources.get('signOut'));
7317								}));
7318							}));
7319							btn.className = 'geBtn';
7320							btn.style.float = 'right';
7321							div.appendChild(btn);
7322
7323							var btn = mxUtils.button(mxResources.get('addAccount'), mxUtils.bind(this, function()
7324							{
7325								var authWin = this.drive.createAuthWin();
7326								//FIXME This doean't work to set focus back to main window until closing the file is done
7327								authWin.blur();
7328								window.focus();
7329
7330								closeFile(mxUtils.bind(this, function()
7331								{
7332									this.stateArg = null;
7333
7334									this.drive.authorize(false, mxUtils.bind(this, function()
7335									{
7336										this.setMode(App.MODE_GOOGLE);
7337										this.hideDialog();
7338										this.showSplash();
7339									}), mxUtils.bind(this, function(resp)
7340									{
7341										this.handleError(resp);
7342									}), true, authWin); //Remember is true since add account imply keeping that account
7343								}), mxResources.get('closingFile') + '...');
7344							}));
7345							btn.className = 'geBtn';
7346							btn.style.margin = '0px';
7347							div.appendChild(btn);
7348							this.userPanel.appendChild(div);
7349						}
7350					}
7351
7352					var addUser = mxUtils.bind(this, function(user, logo, logout, label)
7353					{
7354						if (user != null)
7355						{
7356							if (connected)
7357							{
7358								this.userPanel.appendChild(document.createElement('hr'));
7359							}
7360
7361							connected = true;
7362							var userTable = document.createElement('table');
7363							userTable.style.borderSpacing = '0';
7364							userTable.style.fontSize = '10pt';
7365							userTable.style.width = '100%';
7366							userTable.style.padding = '10px';
7367
7368							var tbody = document.createElement('tbody');
7369							var row = document.createElement('tr');
7370							var td = document.createElement('td');
7371							td.setAttribute('valig', 'top');
7372							td.style.width = '40px';
7373
7374							if (logo != null)
7375							{
7376								var img = document.createElement('img');
7377								img.setAttribute('width', '40');
7378								img.setAttribute('height', '40');
7379								img.setAttribute('border', '0');
7380								img.setAttribute('src', logo);
7381								img.style.marginRight = '6px';
7382
7383								td.appendChild(img);
7384							}
7385
7386							row.appendChild(td);
7387
7388							var td = document.createElement('td');
7389							td.setAttribute('valign', 'middle');
7390							td.style.whiteSpace = 'nowrap';
7391							td.style.maxWidth = '0';
7392							td.style.overflow = 'hidden';
7393							td.style.textOverflow = 'ellipsis';
7394
7395							mxUtils.write(td, user.displayName);
7396
7397							if (user.email != null)
7398							{
7399								mxUtils.br(td);
7400
7401								var small = document.createElement('small');
7402								small.style.color = 'gray';
7403								mxUtils.write(small, user.email);
7404								td.appendChild(small);
7405							}
7406
7407							if (label != null)
7408							{
7409								var div = document.createElement('div');
7410								div.style.marginTop = '4px';
7411
7412								var i = document.createElement('i');
7413								mxUtils.write(i, label);
7414								div.appendChild(i);
7415								td.appendChild(div);
7416							}
7417
7418							row.appendChild(td);
7419							tbody.appendChild(row);
7420							userTable.appendChild(tbody);
7421
7422							this.userPanel.appendChild(userTable);
7423							var div = document.createElement('div');
7424							div.style.textAlign = 'center';
7425							div.style.padding = '10px';
7426							div.style.whiteSpace = 'nowrap';
7427
7428							if (logout != null)
7429							{
7430								var btn = mxUtils.button(mxResources.get('signOut'), logout);
7431								btn.className = 'geBtn';
7432								div.appendChild(btn);
7433							}
7434
7435							this.userPanel.appendChild(div);
7436						}
7437					});
7438
7439					if (this.dropbox != null)
7440					{
7441						addUser(this.dropbox.getUser(), IMAGE_PATH + '/dropbox-logo.svg', mxUtils.bind(this, function()
7442						{
7443							var file = this.getCurrentFile();
7444
7445							if (file != null && file.constructor == DropboxFile)
7446							{
7447								var doLogout = mxUtils.bind(this, function()
7448								{
7449									this.dropbox.logout();
7450									window.location.hash = '';
7451								});
7452
7453								if (!file.isModified())
7454								{
7455									doLogout();
7456								}
7457								else
7458								{
7459									this.confirm(mxResources.get('allChangesLost'), null, doLogout,
7460										mxResources.get('cancel'), mxResources.get('discardChanges'));
7461								}
7462							}
7463							else
7464							{
7465								this.dropbox.logout();
7466							}
7467						}), mxResources.get('dropbox'));
7468					}
7469
7470					if (this.oneDrive != null)
7471					{
7472						addUser(this.oneDrive.getUser(), IMAGE_PATH + '/onedrive-logo.svg', this.oneDrive.noLogout? null : mxUtils.bind(this, function()
7473						{
7474							var file = this.getCurrentFile();
7475
7476							if (file != null && file.constructor == OneDriveFile)
7477							{
7478								var doLogout = mxUtils.bind(this, function()
7479								{
7480									this.oneDrive.logout();
7481									window.location.hash = '';
7482								});
7483
7484								if (!file.isModified())
7485								{
7486									doLogout();
7487								}
7488								else
7489								{
7490									this.confirm(mxResources.get('allChangesLost'), null, doLogout,
7491										mxResources.get('cancel'), mxResources.get('discardChanges'));
7492								}
7493							}
7494							else
7495							{
7496								this.oneDrive.logout();
7497							}
7498						}), mxResources.get('oneDrive'));
7499					}
7500
7501					if (this.gitHub != null)
7502					{
7503						addUser(this.gitHub.getUser(), IMAGE_PATH + '/github-logo.svg', mxUtils.bind(this, function()
7504						{
7505							var file = this.getCurrentFile();
7506
7507							if (file != null && file.constructor == GitHubFile)
7508							{
7509								var doLogout = mxUtils.bind(this, function()
7510								{
7511									this.gitHub.logout();
7512									window.location.hash = '';
7513								});
7514
7515								if (!file.isModified())
7516								{
7517									doLogout();
7518								}
7519								else
7520								{
7521									this.confirm(mxResources.get('allChangesLost'), null, doLogout,
7522										mxResources.get('cancel'), mxResources.get('discardChanges'));
7523								}
7524							}
7525							else
7526							{
7527								this.gitHub.logout();
7528							}
7529						}), mxResources.get('github'));
7530					}
7531
7532					if (this.gitLab != null)
7533					{
7534						addUser(this.gitLab.getUser(), IMAGE_PATH + '/gitlab-logo.svg', mxUtils.bind(this, function()
7535						{
7536							var file = this.getCurrentFile();
7537
7538							if (file != null && file.constructor == GitLabFile)
7539							{
7540								var doLogout = mxUtils.bind(this, function()
7541								{
7542									this.gitLab.logout();
7543									window.location.hash = '';
7544								});
7545
7546								if (!file.isModified())
7547								{
7548									doLogout();
7549								}
7550								else
7551								{
7552									this.confirm(mxResources.get('allChangesLost'), null, doLogout,
7553										mxResources.get('cancel'), mxResources.get('discardChanges'));
7554								}
7555							}
7556							else
7557							{
7558								this.gitLab.logout();
7559							}
7560						}), mxResources.get('gitlab'));
7561					}
7562
7563					if (this.notion != null)
7564					{
7565						addUser(this.notion.getUser(), IMAGE_PATH + '/notion-logo.svg', mxUtils.bind(this, function()
7566						{
7567							var file = this.getCurrentFile();
7568
7569							if (file != null && file.constructor == NotionFile)
7570							{
7571								var doLogout = mxUtils.bind(this, function()
7572								{
7573									this.notion.logout();
7574									window.location.hash = '';
7575								});
7576
7577								if (!file.isModified())
7578								{
7579									doLogout();
7580								}
7581								else
7582								{
7583									this.confirm(mxResources.get('allChangesLost'), null, doLogout,
7584										mxResources.get('cancel'), mxResources.get('discardChanges'));
7585								}
7586							}
7587							else
7588							{
7589								this.notion.logout();
7590							}
7591						}), mxResources.get('notion'));
7592					}
7593
7594					//TODO We have no user info from Trello, how we can create a user?
7595					if (this.trello != null)
7596					{
7597						addUser(this.trello.getUser(), IMAGE_PATH + '/trello-logo.svg', mxUtils.bind(this, function()
7598						{
7599							var file = this.getCurrentFile();
7600
7601							if (file != null && file.constructor == TrelloFile)
7602							{
7603								var doLogout = mxUtils.bind(this, function()
7604								{
7605									this.trello.logout();
7606									window.location.hash = '';
7607								});
7608
7609								if (!file.isModified())
7610								{
7611									doLogout();
7612								}
7613								else
7614								{
7615									this.confirm(mxResources.get('allChangesLost'), null, doLogout,
7616										mxResources.get('cancel'), mxResources.get('discardChanges'));
7617								}
7618							}
7619							else
7620							{
7621								this.trello.logout();
7622							}
7623						}), mxResources.get('trello'));
7624					}
7625
7626					if (!connected)
7627					{
7628						var div = document.createElement('div');
7629						div.style.textAlign = 'center';
7630						div.style.padding = '10px';
7631						div.innerHTML = mxResources.get('notConnected');
7632
7633						this.userPanel.appendChild(div);
7634					}
7635
7636					var div = document.createElement('div');
7637					div.style.textAlign = 'center';
7638					div.style.padding = '10px';
7639					div.style.background = Editor.isDarkMode() ? '' : 'whiteSmoke';
7640					div.style.borderTop = '1px solid #e0e0e0';
7641					div.style.whiteSpace = 'nowrap';
7642
7643					if (urlParams['sketch'] == '1')
7644					{
7645						var btn = mxUtils.button(mxResources.get('share'), mxUtils.bind(this, function()
7646						{
7647							this.actions.get('share').funct();
7648						}));
7649						btn.className = 'geBtn';
7650						div.appendChild(btn);
7651						this.userPanel.appendChild(div);
7652
7653						if (this.commentsSupported())
7654						{
7655							btn = mxUtils.button(mxResources.get('comments'), mxUtils.bind(this, function()
7656							{
7657								this.actions.get('comments').funct();
7658							}));
7659							btn.className = 'geBtn';
7660							div.appendChild(btn);
7661							this.userPanel.appendChild(div);
7662						}
7663					}
7664					else
7665					{
7666						var btn = mxUtils.button(mxResources.get('close'), mxUtils.bind(this, function()
7667						{
7668							if (!mxEvent.isConsumed(evt) && this.userPanel != null && this.userPanel.parentNode != null)
7669							{
7670								this.userPanel.parentNode.removeChild(this.userPanel);
7671							}
7672						}));
7673						btn.className = 'geBtn';
7674						div.appendChild(btn);
7675						this.userPanel.appendChild(div);
7676					}
7677
7678					document.body.appendChild(this.userPanel);
7679				}
7680
7681				mxEvent.consume(evt);
7682			}));
7683
7684			mxEvent.addListener(document.body, 'click', mxUtils.bind(this, function(evt)
7685			{
7686				if (!mxEvent.isConsumed(evt) && this.userPanel != null && this.userPanel.parentNode != null)
7687				{
7688					this.userPanel.parentNode.removeChild(this.userPanel);
7689				}
7690			}));
7691		}
7692
7693		var user = null;
7694
7695		if (this.drive != null && this.drive.getUser() != null)
7696		{
7697			user = this.drive.getUser();
7698		}
7699		else if (this.oneDrive != null && this.oneDrive.getUser() != null)
7700		{
7701			user = this.oneDrive.getUser();
7702		}
7703		else if (this.dropbox != null && this.dropbox.getUser() != null)
7704		{
7705			user = this.dropbox.getUser();
7706		}
7707		else if (this.gitHub != null && this.gitHub.getUser() != null)
7708		{
7709			user = this.gitHub.getUser();
7710		}
7711		else if (this.gitLab != null && this.gitLab.getUser() != null)
7712		{
7713			user = this.gitLab.getUser();
7714		}
7715		else if (this.notion != null && this.notion.getUser() != null)
7716		{
7717			user = this.notion.getUser();
7718		}
7719		//TODO Trello no user issue
7720
7721		if (user != null)
7722		{
7723			this.userElement.innerHTML = '';
7724
7725			if (screen.width > 560)
7726			{
7727				mxUtils.write(this.userElement, user.displayName);
7728				this.userElement.style.display = 'block';
7729			}
7730		}
7731		else
7732		{
7733			this.userElement.style.display = 'none';
7734		}
7735	}
7736};
7737
7738//TODO Use this function to get the currently logged in user
7739App.prototype.getCurrentUser = function()
7740{
7741	var user = null;
7742
7743	if (this.drive != null && this.drive.getUser() != null)
7744	{
7745		user = this.drive.getUser();
7746	}
7747	else if (this.oneDrive != null && this.oneDrive.getUser() != null)
7748	{
7749		user = this.oneDrive.getUser();
7750	}
7751	else if (this.dropbox != null && this.dropbox.getUser() != null)
7752	{
7753		user = this.dropbox.getUser();
7754	}
7755	else if (this.gitHub != null && this.gitHub.getUser() != null)
7756	{
7757		user = this.gitHub.getUser();
7758	}
7759	//TODO Trello no user issue
7760
7761	return user;
7762}
7763/**
7764 * Override depends on mxSettings which is not defined in the minified viewer.
7765 */
7766var editorResetGraph = Editor.prototype.resetGraph;
7767Editor.prototype.resetGraph = function()
7768{
7769	editorResetGraph.apply(this, arguments);
7770
7771	// Overrides default with persisted value
7772	if (this.graph.defaultPageFormat == null)
7773	{
7774		this.graph.pageFormat = mxSettings.getPageFormat();
7775	}
7776};
7777