1/**
2 * Copyright (c) 2006-2016, JGraph Ltd
3 */
4/**
5 * No CSS and resources available in embed mode. Parameters and docs:
6 * https://www.diagrams.net/doc/faq/embed-html-options
7 */
8GraphViewer = function(container, xmlNode, graphConfig)
9{
10	this.init(container, xmlNode, graphConfig);
11};
12
13// Editor inherits from mxEventSource
14mxUtils.extend(GraphViewer, mxEventSource);
15
16/**
17 * Redirects editing to absolue URLs.
18 */
19GraphViewer.prototype.editBlankUrl = 'https://app.diagrams.net/';
20
21/**
22 * Base URL for relative images.
23 */
24GraphViewer.prototype.imageBaseUrl = 'https://viewer.diagrams.net/';
25
26/**
27 * Redirects editing to absolue URLs.
28 */
29GraphViewer.prototype.toolbarHeight = (document.compatMode == 'BackCompat') ? 24 : 26;
30
31/**
32 * Redirects editing to absolue URLs.
33 */
34GraphViewer.prototype.lightboxChrome = true;
35
36/**
37 * Redirects editing to absolue URLs.
38 */
39GraphViewer.prototype.lightboxZIndex = 999;
40
41/**
42 * Redirects editing to absolue URLs.
43 */
44GraphViewer.prototype.toolbarZIndex = 999;
45
46/**
47 * If automatic fit should be enabled if zoom is disabled. Default is true.
48 */
49GraphViewer.prototype.autoFit = false;
50
51/**
52 * If automatic crop should be enabled when layers are toggled. Default is false.
53 */
54GraphViewer.prototype.autoCrop = false;
55
56/**
57 * Specifies if the graph should be moved if a layer is made visible that
58 * extends the graph beyong the top left corner. Default is true. Is this is
59 * false then the viewport of the viewer will include all cells in all layers
60 * regardless of their initial visible state.
61 */
62GraphViewer.prototype.autoOrigin = true;
63
64/**
65 * If the diagram should be centered. Default is false.
66 */
67GraphViewer.prototype.center = false;
68
69/**
70 * Force centering of the diagram. Default is false.
71 */
72GraphViewer.prototype.forceCenter = false;
73
74/**
75 * Specifies if zooming in for auto fit is allowed. Default is false.
76 */
77GraphViewer.prototype.allowZoomIn = false;
78
79/**
80 * Specifies if zooming out for auto fit is allowed. Default is true.
81 * If toolbar-nohide is true then overflow content is visible.
82 */
83GraphViewer.prototype.allowZoomOut = true;
84
85/**
86 * Whether the title should be shown as a tooltip if the toolbar is disabled.
87 * Default is false.
88 */
89GraphViewer.prototype.showTitleAsTooltip = false;
90
91/**
92 * Specifies if the constructur should delay the rendering if the container
93 * is not visible by default.
94 */
95GraphViewer.prototype.checkVisibleState = true;
96
97/**
98 * Defines the minimum height of the container. Default is 28.
99 */
100GraphViewer.prototype.minHeight = 28;
101
102/**
103 * Defines the minimum width of the container. Default is 100.
104 */
105GraphViewer.prototype.minWidth = 100;
106
107/**
108 * Implements viewBox to keep the contents inside the bounding box
109 * of the container. This is currently not supported in Safari (due
110 * to clipping in labels with viewBox) and all browsers that do not
111 * support foreignObjects (eg. IE11).
112 */
113GraphViewer.prototype.responsive = false;
114
115/**
116 * Initializes the viewer.
117 */
118GraphViewer.prototype.init = function(container, xmlNode, graphConfig)
119{
120	this.graphConfig = (graphConfig != null) ? graphConfig : {};
121	this.autoFit = (this.graphConfig['auto-fit'] != null) ?
122		this.graphConfig['auto-fit'] : this.autoFit;
123	this.autoCrop = (this.graphConfig['auto-crop'] != null) ?
124		this.graphConfig['auto-crop'] : this.autoCrop;
125	this.autoOrigin = (this.graphConfig['auto-origin'] != null) ?
126		this.graphConfig['auto-origin'] : this.autoOrigin;
127	this.allowZoomOut = (this.graphConfig['allow-zoom-out'] != null) ?
128		this.graphConfig['allow-zoom-out'] : this.allowZoomOut;
129	this.allowZoomIn = (this.graphConfig['allow-zoom-in'] != null) ?
130		this.graphConfig['allow-zoom-in'] : this.allowZoomIn;
131	this.forceCenter = (this.graphConfig['forceCenter'] != null) ?
132		this.graphConfig['forceCenter'] : this.forceCenter;
133	this.center = (this.graphConfig['center'] != null) ?
134		this.graphConfig['center'] : (this.center || this.forceCenter);
135	this.checkVisibleState = (this.graphConfig['check-visible-state'] != null) ?
136		this.graphConfig['check-visible-state'] : this.checkVisibleState;
137	this.toolbarItems = (this.graphConfig.toolbar != null) ?
138		this.graphConfig.toolbar.split(' ') : [];
139	this.zoomEnabled = mxUtils.indexOf(this.toolbarItems, 'zoom') >= 0;
140	this.layersEnabled = mxUtils.indexOf(this.toolbarItems, 'layers') >= 0;
141	this.tagsEnabled = mxUtils.indexOf(this.toolbarItems, 'tags') >= 0;
142	this.lightboxEnabled = mxUtils.indexOf(this.toolbarItems, 'lightbox') >= 0;
143	this.lightboxClickEnabled = this.graphConfig.lightbox != false;
144	this.initialWidth = (container != null) ? container.style.width : null;
145	this.widthIsEmpty = (this.initialWidth != null) ? this.initialWidth == '' : true;
146	this.currentPage = parseInt(this.graphConfig.page) || 0;
147	this.responsive = ((this.graphConfig['responsive'] != null) ?
148		this.graphConfig['responsive'] : this.responsive) &&
149		!this.zoomEnabled && !mxClient.NO_FO && !mxClient.IS_SF;
150	this.pageId = this.graphConfig.pageId;
151	this.editor = null;
152	var self = this;
153
154	if (this.graphConfig['toolbar-position'] == 'inline')
155	{
156		this.minHeight += this.toolbarHeight;
157	}
158
159	if (xmlNode != null)
160	{
161		this.xmlDocument = xmlNode.ownerDocument;
162		this.xmlNode = xmlNode;
163		this.xml = mxUtils.getXml(xmlNode);
164
165		if (container != null)
166		{
167			var render = mxUtils.bind(this, function()
168			{
169				this.graph = new Graph(container);
170				this.graph.enableFlowAnimation = true;
171				this.graph.defaultPageBackgroundColor = 'transparent';
172				this.graph.transparentBackground = false;
173
174				if (this.responsive && this.graph.dialect == mxConstants.DIALECT_SVG)
175				{
176					var root = this.graph.view.getDrawPane().ownerSVGElement;
177					var canvas = this.graph.view.getCanvas();
178
179					if (this.graphConfig.border != null)
180					{
181						root.style.padding = this.graphConfig.border + 'px';
182					}
183					else if (container.style.padding == '')
184					{
185						root.style.padding = '8px';
186					}
187
188					root.style.boxSizing = 'border-box';
189					root.style.overflow = 'visible';
190
191					this.graph.fit = function()
192					{
193						// Automatic
194					};
195
196					this.graph.sizeDidChange = function()
197					{
198						var bounds = this.view.graphBounds;
199						var tr = this.view.translate;
200
201						root.setAttribute('viewBox',
202							(bounds.x + tr.x - this.panDx) + ' ' +
203							(bounds.y + tr.y - this.panDy) + ' ' +
204							(bounds.width + 1) + ' ' +
205							(bounds.height + 1));
206						this.container.style.backgroundColor =
207							root.style.backgroundColor;
208
209						this.fireEvent(new mxEventObject(mxEvent.SIZE, 'bounds', bounds));
210					};
211				}
212
213				if (this.graphConfig.move)
214				{
215					this.graph.isMoveCellsEvent = function(evt)
216					{
217						return true;
218					};
219				}
220
221				// Adds lightbox and link handling for shapes
222				if (this.lightboxClickEnabled)
223				{
224					container.style.cursor = 'pointer';
225				}
226
227				// Hack for using EditorUi methods on the graph instance
228				this.editor = new Editor(true, null, null, this.graph);
229				this.editor.editBlankUrl = this.editBlankUrl;
230				this.graph.lightbox = true;
231				this.graph.centerZoom = false;
232				this.graph.autoExtend = false;
233				this.graph.autoScroll = false;
234				this.graph.setEnabled(false);
235
236				if (this.graphConfig['toolbar-nohide'] == true)
237				{
238					this.editor.defaultGraphOverflow = 'visible';
239				}
240
241				//Extract graph model from html & svg formats
242				this.xmlNode = this.editor.extractGraphModel(this.xmlNode, true);
243
244				if (this.xmlNode != xmlNode)
245				{
246					this.xml = mxUtils.getXml(this.xmlNode);
247					this.xmlDocument = this.xmlNode.ownerDocument;
248				}
249
250				// Handles relative images
251				var self = this;
252
253				this.graph.getImageFromBundles = function(key)
254				{
255					return self.getImageUrl(key);
256				};
257
258				if (mxClient.IS_SVG)
259				{
260					// LATER: Add shadow for labels in graph.container (eg. math, NO_FO), scaling
261					this.graph.addSvgShadow(this.graph.view.canvas.ownerSVGElement, null, true);
262				}
263
264				// Adds page placeholders
265				if (this.xmlNode.nodeName == 'mxfile')
266				{
267					var diagrams = this.xmlNode.getElementsByTagName('diagram');
268
269					if (diagrams.length > 0)
270					{
271						//Find the page index if the pageId is provided
272						if (this.pageId != null)
273						{
274							for (var i = 0; i < diagrams.length; i++)
275							{
276								if (this.pageId == diagrams[i].getAttribute('id'))
277								{
278									this.currentPage = i;
279									break;
280								}
281							}
282						}
283
284						var graphGetGlobalVariable = this.graph.getGlobalVariable;
285						var self = this;
286
287						this.graph.getGlobalVariable = function(name)
288						{
289							var diagram = diagrams[self.currentPage];
290
291							if (name == 'page')
292							{
293								return diagram.getAttribute('name') || 'Page-' + (self.currentPage + 1);
294							}
295							else if (name == 'pagenumber')
296							{
297								return self.currentPage + 1;
298							}
299							else if (name == 'pagecount')
300							{
301								return diagrams.length;
302							}
303
304							return graphGetGlobalVariable.apply(this, arguments);
305						};
306					}
307				}
308
309				this.diagrams = [];
310				var lastXmlNode = null;
311
312				this.selectPage = function(number)
313				{
314					if(this.handlingResize)
315						return;
316
317					this.currentPage = mxUtils.mod(number, this.diagrams.length);
318					this.updateGraphXml(Editor.parseDiagramNode(this.diagrams[this.currentPage]));
319				};
320
321				this.selectPageById = function(id)
322				{
323					var index = this.getIndexById(id);
324					var found = index >= 0;
325
326					if (found)
327					{
328						this.selectPage(index);
329					}
330
331					return found;
332				};
333
334				var update = mxUtils.bind(this, function()
335				{
336					if (this.xmlNode == null || this.xmlNode.nodeName != 'mxfile')
337					{
338						this.diagrams = [];
339					}
340					if (this.xmlNode != lastXmlNode)
341					{
342						this.diagrams = this.xmlNode.getElementsByTagName('diagram');
343						lastXmlNode = this.xmlNode;
344					}
345				});
346
347				// Replaces background page reference with SVG
348				var graphSetBackgroundImage = this.graph.setBackgroundImage;
349
350				this.graph.setBackgroundImage = function(img)
351				{
352					if (img != null && Graph.isPageLink(img.src))
353					{
354						var src = img.src;
355						var comma = src.indexOf(',');
356
357						if (comma > 0)
358						{
359							var index = self.getIndexById(src.substring(comma + 1));
360
361							if (index >= 0)
362							{
363								img = self.getImageForGraphModel(
364									Editor.parseDiagramNode(
365									self.diagrams[index]));
366								img.originalSrc = src;
367							}
368						}
369					}
370
371					graphSetBackgroundImage.apply(this, arguments);
372				};
373
374				// Overrides graph bounds to include background pages
375				var graphGetGraphBounds = this.graph.getGraphBounds;
376
377				this.graph.getGraphBounds = function(img)
378				{
379					var bounds = graphGetGraphBounds.apply(this, arguments);
380					var img = this.backgroundImage;
381
382					// Check img.originalSrc to ignore background
383					// images but not background pages
384					if (img != null)
385					{
386						var t = this.view.translate;
387						var s = this.view.scale;
388
389						bounds = mxRectangle.fromRectangle(bounds);
390						bounds.add(new mxRectangle(
391							(t.x + img.x) * s, (t.y + img.y) * s,
392							img.width * s, img.height * s));
393					}
394
395					return bounds;
396				};
397
398				// LATER: Add event for setGraphXml
399				this.addListener('xmlNodeChanged', update);
400				update();
401
402				// Passes current page via urlParams global variable
403				// to let the parser know which page we're using
404				urlParams['page'] = self.currentPage;
405				var visible = null;
406
407				this.graph.getModel().beginUpdate();
408				try
409				{
410					// Required for correct parsing of fold parameter
411					urlParams['nav'] = (this.graphConfig.nav != false) ? '1' : '0';
412
413					this.editor.setGraphXml(this.xmlNode);
414					this.graph.view.scale = this.graphConfig.zoom || 1;
415					visible = this.setLayersVisible();
416
417					if (!this.responsive)
418					{
419						this.graph.border = (this.graphConfig.border != null) ? this.graphConfig.border : 8;
420					}
421				}
422				finally
423				{
424					this.graph.getModel().endUpdate();
425				}
426
427				// Adds left-button panning only if scrollbars are visible
428				if (!this.responsive)
429				{
430					this.graph.panningHandler.isForcePanningEvent = function(me)
431					{
432						return !mxEvent.isPopupTrigger(me.getEvent()) &&
433							this.graph.container.style.overflow == 'auto';
434					};
435
436					this.graph.panningHandler.useLeftButtonForPanning = true;
437					this.graph.panningHandler.ignoreCell = true;
438					this.graph.panningHandler.usePopupTrigger = false;
439					this.graph.panningHandler.pinchEnabled = false;
440				}
441
442				this.graph.setPanning(false);
443
444				if (this.graphConfig.toolbar != null)
445				{
446					this.addToolbar();
447				}
448				else if (this.graphConfig.title != null && this.showTitleAsTooltip)
449				{
450					container.setAttribute('title', this.graphConfig.title);
451				}
452
453				if (!this.responsive)
454				{
455					this.addSizeHandler();
456				}
457
458				// Crops to visible layers if no layers toolbar button
459				if (this.showLayers(this.graph) && !this.forceCenter && (!this.layersEnabled || this.autoCrop))
460				{
461					this.crop();
462				}
463
464				this.addClickHandler(this.graph);
465				this.graph.setTooltips(this.graphConfig.tooltips != false);
466				this.graph.initialViewState = {
467					translate: this.graph.view.translate.clone(),
468					scale: this.graph.view.scale
469				};
470
471				if (visible != null)
472				{
473					this.setLayersVisible(visible);
474				}
475
476				this.graph.customLinkClicked = function(href)
477				{
478					if (Graph.isPageLink(href))
479					{
480						var comma = href.indexOf(',');
481
482						if (!self.selectPageById(href.substring(comma + 1)))
483						{
484							alert(mxResources.get('pageNotFound') || 'Page not found');
485						}
486					}
487					else
488					{
489						this.handleCustomLink(href);
490					}
491
492					return true;
493				};
494
495				// Updates origin after tree cell folding
496				var graphFoldTreeCell = this.graph.foldTreeCell;
497
498				this.graph.foldTreeCell = mxUtils.bind(this, function()
499				{
500					this.treeCellFolded = true;
501
502					return graphFoldTreeCell.apply(this.graph, arguments);
503				});
504
505				this.fireEvent(new mxEventObject('render'));
506			});
507
508			var MutObs = window.MutationObserver ||
509				window.WebKitMutationObserver ||
510				window.MozMutationObserver;
511
512			if (this.checkVisibleState && container.offsetWidth == 0 && typeof MutObs !== 'undefined')
513			{
514				// Delayed rendering if inside hidden container and event available
515				var par = this.getObservableParent(container);
516
517				var observer = new MutObs(mxUtils.bind(this, function(mutation)
518				{
519					if (container.offsetWidth > 0)
520					{
521						observer.disconnect();
522						render();
523					}
524				}));
525
526				observer.observe(par, {attributes: true});
527			}
528			else
529			{
530				// Immediate rendering in all other cases
531				render();
532			}
533		}
534	}
535};
536
537/**
538 *
539 */
540GraphViewer.prototype.getObservableParent = function(container)
541{
542	var node = container.parentNode;
543
544	while (node != document.body && node.parentNode != null &&
545		mxUtils.getCurrentStyle(node).display !== 'none')
546	{
547		node = node.parentNode;
548	}
549
550	return node;
551};
552
553/**
554 *
555 */
556GraphViewer.prototype.getImageUrl = function(url)
557{
558	if (url != null && url.substring(0, 7) != 'http://' &&
559		url.substring(0, 8) != 'https://' && url.substring(0, 10) != 'data:image')
560	{
561		if (url.charAt(0) == '/')
562		{
563			url = url.substring(1, url.length);
564		}
565
566		url = this.imageBaseUrl + url;
567	}
568
569	return url;
570};
571
572/**
573 *
574 */
575GraphViewer.prototype.getImageForGraphModel = function(node)
576{
577	var graph = Graph.createOffscreenGraph(this.graph.getStylesheet());
578	graph.getGlobalVariable = this.graph.getGlobalVariable;
579	document.body.appendChild(graph.container);
580
581	var codec = new mxCodec(node.ownerDocument);
582	var root = codec.decode(node).root;
583	graph.model.setRoot(root);
584
585	var svgRoot = graph.getSvg();
586	var bounds = graph.getGraphBounds();
587	document.body.removeChild(graph.container);
588
589	return new mxImage(Editor.createSvgDataUri(mxUtils.getXml(svgRoot)),
590		bounds.width, bounds.height, bounds.x, bounds.y);
591};
592
593/**
594 *
595 */
596GraphViewer.prototype.getIndexById = function(id)
597{
598	if (this.diagrams != null)
599	{
600		for (var i = 0; i < this.diagrams.length; i++)
601		{
602			if (this.diagrams[i].getAttribute('id') == id)
603			{
604				return i;
605			}
606		}
607	}
608
609	return -1;
610};
611
612/**
613 *
614 */
615GraphViewer.prototype.setXmlNode = function(xmlNode)
616{
617	//Extract graph model from html & svg formats
618	xmlNode = this.editor.extractGraphModel(xmlNode, true);
619
620	this.xmlDocument = xmlNode.ownerDocument;
621	this.xml = mxUtils.getXml(xmlNode);
622	this.xmlNode = xmlNode;
623
624	this.updateGraphXml(xmlNode);
625	this.fireEvent(new mxEventObject('xmlNodeChanged'));
626};
627
628/**
629 *
630 */
631GraphViewer.prototype.setFileNode = function(xmlNode)
632{
633	if (this.xmlNode == null)
634	{
635		this.xmlDocument = xmlNode.ownerDocument;
636		this.xml = mxUtils.getXml(xmlNode);
637		this.xmlNode = xmlNode;
638	}
639
640	this.setGraphXml(xmlNode);
641};
642
643/**
644 *
645 */
646GraphViewer.prototype.updateGraphXml = function(xmlNode)
647{
648	this.setGraphXml(xmlNode);
649	this.fireEvent(new mxEventObject('graphChanged'));
650};
651
652/**
653 *
654 */
655GraphViewer.prototype.setLayersVisible = function(visible)
656{
657	var allVisible = true;
658
659	if (!this.autoOrigin)
660	{
661		var result = [];
662		var model = this.graph.getModel();
663
664		model.beginUpdate();
665		try
666		{
667			for (var i = 0; i < model.getChildCount(model.root); i++)
668			{
669				var layer = model.getChildAt(model.root, i);
670				allVisible = allVisible && model.isVisible(layer);
671				result.push(model.isVisible(layer));
672				model.setVisible(layer, (visible != null) ? visible[i] : true);
673			}
674		}
675		finally
676		{
677			model.endUpdate();
678		}
679	}
680
681	return (allVisible) ? null : result;
682};
683
684/**
685 *
686 */
687GraphViewer.prototype.setGraphXml = function(xmlNode)
688{
689	if (this.graph != null)
690	{
691		this.graph.view.translate = new mxPoint();
692		this.graph.view.scale = 1;
693		var visible = null;
694
695		this.graph.getModel().beginUpdate();
696		try
697		{
698			this.graph.getModel().clear();
699			this.editor.setGraphXml(xmlNode);
700			visible = this.setLayersVisible(true);
701		}
702		finally
703		{
704			this.graph.getModel().endUpdate();
705		}
706
707		if (!this.responsive)
708		{
709			// Restores initial CSS state
710			if (this.widthIsEmpty)
711			{
712				this.graph.container.style.width = '';
713				this.graph.container.style.height = '';
714			}
715			else
716			{
717				this.graph.container.style.width = this.initialWidth;
718			}
719
720			this.positionGraph();
721		}
722
723		this.graph.initialViewState = {
724			translate: this.graph.view.translate.clone(),
725			scale: this.graph.view.scale
726		};
727
728		if (visible)
729		{
730			this.setLayersVisible(visible);
731		}
732	}
733};
734
735/**
736 *
737 */
738GraphViewer.prototype.addSizeHandler = function()
739{
740	var container = this.graph.container;
741	var bounds = this.graph.getGraphBounds();
742	var updatingOverflow = false;
743
744	if (this.graphConfig['toolbar-nohide'] != true)
745	{
746		container.style.overflow = 'hidden';
747	}
748	else
749	{
750		container.style.overflow = 'visible';
751	}
752
753	var updateOverflow = mxUtils.bind(this, function()
754	{
755		if (!updatingOverflow)
756		{
757			updatingOverflow = true;
758			var tmp = this.graph.getGraphBounds();
759
760			if (this.graphConfig['toolbar-nohide'] != true)
761			{
762				// Shows scrollbars if graph is larger than available width
763				if (tmp.width + 2 * this.graph.border > container.offsetWidth - 2)
764				{
765					container.style.overflow = 'auto';
766				}
767				else
768				{
769					container.style.overflow = 'hidden';
770				}
771			}
772			else
773			{
774				container.style.overflow = 'visible';
775			}
776
777			if (this.toolbar != null && this.graphConfig['toolbar-nohide'] != true)
778			{
779				var r = container.getBoundingClientRect();
780
781				// Workaround for position:relative set in ResizeSensor
782				var origin = mxUtils.getScrollOrigin(document.body)
783				var b = (document.body.style.position === 'relative') ?
784					document.body.getBoundingClientRect() :
785					{left: -origin.x, top: -origin.y};
786				r = {left: r.left - b.left, top: r.top - b.top, bottom: r.bottom - b.top, right: r.right - b.left};
787
788				this.toolbar.style.left = r.left + 'px';
789
790				if (this.graphConfig['toolbar-position'] == 'bottom')
791				{
792					this.toolbar.style.top = r.bottom - 1 + 'px';
793				}
794				else
795				{
796					if (this.graphConfig['toolbar-position'] != 'inline')
797					{
798						this.toolbar.style.width = Math.max(this.minToolbarWidth, container.offsetWidth) + 'px';
799						this.toolbar.style.top = r.top + 1 + 'px';
800					}
801					else
802					{
803						this.toolbar.style.top = r.top + 'px';
804					}
805				}
806			}
807			else if (this.toolbar != null)
808			{
809				this.toolbar.style.width = Math.max(this.minToolbarWidth, container.offsetWidth) + 'px';
810			}
811
812			// Updates origin after tree cell folding
813			if (this.treeCellFolded)
814			{
815				this.treeCellFolded = false;
816				this.positionGraph(this.graph.view.translate);
817				this.graph.initialViewState.translate = this.graph.view.translate.clone();
818			}
819
820			updatingOverflow = false;
821		}
822	});
823
824	var lastOffsetWidth = null;
825	var cachedOffsetWidth = null;
826	this.handlingResize = false;
827
828	// Installs function on instance
829	this.fitGraph = mxUtils.bind(this, function(maxScale)
830	{
831		var cachedOffsetWidth = container.offsetWidth;
832
833		if (cachedOffsetWidth != lastOffsetWidth && !this.handlingResize)
834		{
835			this.handlingResize = true;
836
837			// Hides scrollbars to force update of translate
838			if (container.style.overflow == 'auto')
839			{
840				container.style.overflow = 'hidden';
841			}
842
843			this.graph.maxFitScale = (maxScale != null) ? maxScale : (this.graphConfig.zoom ||
844				((this.allowZoomIn) ? null : 1));
845			this.graph.fit(null, null, null, null, null, true);
846
847			if (this.center || !(this.graphConfig.resize != false || container.style.height == ''))
848			{
849				this.graph.center();
850			}
851
852			this.graph.maxFitScale = null;
853
854			if (this.graphConfig.resize != false || container.style.height == '')
855			{
856				this.updateContainerHeight(container, Math.max(this.minHeight,
857					this.graph.getGraphBounds().height +
858					2 * this.graph.border + 1));
859			}
860
861			this.graph.initialViewState = {
862				translate: this.graph.view.translate.clone(),
863				scale: this.graph.view.scale
864			};
865
866			lastOffsetWidth = cachedOffsetWidth;
867
868			// Workaround for fit triggering scrollbars triggering doResize (infinite loop)
869			window.setTimeout(mxUtils.bind(this, function()
870			{
871				this.handlingResize = false;
872			}), 0);
873		}
874	});
875
876	// Fallback for older browsers
877	if (GraphViewer.useResizeSensor)
878	{
879		if (document.documentMode <= 9)
880		{
881			mxEvent.addListener(window, 'resize', updateOverflow);
882			this.graph.addListener('size', updateOverflow);
883		}
884		else
885		{
886			new ResizeSensor(this.graph.container, updateOverflow);
887		}
888	}
889
890	if (this.graphConfig.resize || ((this.zoomEnabled || !this.autoFit) && this.graphConfig.resize != false))
891	{
892		this.graph.minimumContainerSize = new mxRectangle(0, 0, this.minWidth, this.minHeight);
893		this.graph.resizeContainer = true;
894	}
895	else
896	{
897		// Sets initial size for responsive diagram to stop at actual size
898		if (this.widthIsEmpty && !(container.style.height != '' && this.autoFit))
899		{
900			this.updateContainerWidth(container, bounds.width + 2 * this.graph.border);
901		}
902
903		if (this.graphConfig.resize != false || container.style.height == '')
904		{
905			this.updateContainerHeight(container, Math.max(this.minHeight, bounds.height + 2 * this.graph.border + 1));
906		}
907
908		if (!this.zoomEnabled && this.autoFit)
909		{
910			var lastOffsetWidth = null;
911			var scheduledResize = null;
912			var cachedOffsetWidth = null;
913
914			var doResize = mxUtils.bind(this, function()
915			{
916				window.clearTimeout(scheduledResize);
917
918				if (!this.handlingResize)
919				{
920					scheduledResize = window.setTimeout(mxUtils.bind(this, this.fitGraph), 100);
921				}
922			});
923
924			// Fallback for older browsers
925			if (GraphViewer.useResizeSensor)
926			{
927				if (document.documentMode <= 9)
928				{
929					mxEvent.addListener(window, 'resize', doResize);
930				}
931				else
932				{
933					new ResizeSensor(this.graph.container, doResize);
934				}
935			}
936		}
937		else if (!(document.documentMode <= 9))
938		{
939			this.graph.addListener('size', updateOverflow);
940		}
941	}
942
943	var positionGraph = mxUtils.bind(this, function(origin)
944	{
945		// Allocates maximum width while setting initial view state
946		var prev = container.style.minWidth;
947
948		if (this.widthIsEmpty)
949		{
950			container.style.minWidth = '100%';
951		}
952
953		var maxHeight = (this.graphConfig['max-height'] != null) ? this.graphConfig['max-height'] :
954			((container.style.height != '' && this.autoFit) ? container.offsetHeight : undefined);
955
956		if (container.offsetWidth > 0 && origin == null && this.allowZoomOut && (this.allowZoomIn ||
957			bounds.width + 2 * this.graph.border > container.offsetWidth ||
958			bounds.height + 2 * this.graph.border > maxHeight))
959		{
960			var maxScale = null;
961
962			if (maxHeight != null && bounds.height + 2 * this.graph.border > maxHeight - 2)
963			{
964				maxScale = (maxHeight - 2 * this.graph.border - 2) / bounds.height;
965			}
966
967			this.fitGraph(maxScale);
968		}
969		else if (!this.widthIsEmpty && origin == null && !(this.graphConfig.resize != false || container.style.height == ''))
970		{
971			this.graph.center((!this.widthIsEmpty || bounds.width < this.minWidth) && this.graphConfig.resize != true);
972		}
973		else
974		{
975			origin = (origin != null) ? origin : new mxPoint();
976
977			this.graph.view.setTranslate(Math.floor(this.graph.border - bounds.x / this.graph.view.scale) + origin.x,
978				Math.floor(this.graph.border - bounds.y / this.graph.view.scale) + origin.y);
979			lastOffsetWidth = container.offsetWidth;
980		}
981
982		container.style.minWidth = prev
983	});
984
985	if (document.documentMode == 8)
986	{
987		window.setTimeout(positionGraph, 0);
988	}
989	else
990	{
991		positionGraph();
992	}
993
994	// Installs function on instance
995	this.positionGraph = function(origin)
996	{
997		bounds = this.graph.getGraphBounds();
998		lastOffsetWidth = null;
999		positionGraph(origin);
1000	};
1001};
1002
1003/**
1004 * Moves the origin of the graph to the top, right corner.
1005 */
1006GraphViewer.prototype.crop = function()
1007{
1008	var graph = this.graph;
1009	var bounds = graph.getGraphBounds();
1010	var border = graph.border;
1011	var s = graph.view.scale;
1012	var x0 = (bounds.x != null) ? Math.floor(graph.view.translate.x - bounds.x / s + border) : border;
1013	var y0 = (bounds.y != null) ? Math.floor(graph.view.translate.y - bounds.y / s + border) : border;
1014
1015	graph.view.setTranslate(x0, y0);
1016};
1017
1018/**
1019 *
1020 */
1021GraphViewer.prototype.updateContainerWidth = function(container, width)
1022{
1023	container.style.width = width + 'px';
1024};
1025
1026/**
1027 *
1028 */
1029GraphViewer.prototype.updateContainerHeight = function(container, height)
1030{
1031	if (this.forceCenter || this.zoomEnabled || !this.autoFit || document.compatMode == 'BackCompat' ||
1032		document.documentMode == 8)
1033	{
1034		container.style.height = height + 'px';
1035	}
1036};
1037
1038/**
1039 * Shows the
1040 */
1041GraphViewer.prototype.showLayers = function(graph, sourceGraph)
1042{
1043	var layers = this.graphConfig.layers;
1044	var idx = (layers != null && layers.length > 0) ? layers.split(' ') : [];
1045	var layerIds = this.graphConfig.layerIds;
1046	var hasLayerIds = layerIds != null && layerIds.length > 0;
1047	var result = false;
1048
1049	if (idx.length > 0 || hasLayerIds || sourceGraph != null)
1050	{
1051		var source = (sourceGraph != null) ? sourceGraph.getModel() : null;
1052		var model = graph.getModel();
1053		model.beginUpdate();
1054
1055		try
1056		{
1057			var childCount = model.getChildCount(model.root);
1058
1059			// Shows specified layers (eg. 0 1 3)
1060			if (source == null)
1061			{
1062				var layersFound = false, visibleLayers = {};
1063
1064				if (hasLayerIds)
1065				{
1066					for (var i = 0; i < layerIds.length; i++)
1067					{
1068						var layer = model.getCell(layerIds[i]);
1069
1070						if (layer != null)
1071						{
1072							layersFound = true;
1073							visibleLayers[layer.id] = true;
1074						}
1075					}
1076				}
1077				else
1078				{
1079					for (var i = 0; i < idx.length; i++)
1080					{
1081						var layer = model.getChildAt(model.root, parseInt(idx[i]));
1082
1083						if (layer != null)
1084						{
1085							layersFound = true;
1086							visibleLayers[layer.id] = true;
1087						}
1088					}
1089				}
1090
1091				//To prevent hiding all layers, only apply if the specified layers are found
1092				//This prevents incorrect settings from showing an empty viewer
1093				for (var i = 0; layersFound && i < childCount; i++)
1094				{
1095					var layer = model.getChildAt(model.root, i);
1096					model.setVisible(layer, visibleLayers[layer.id] || false);
1097				}
1098			}
1099			else
1100			{
1101				// Match visible layers in source graph
1102				for (var i = 0; i < childCount; i++)
1103				{
1104					model.setVisible(model.getChildAt(model.root, i),
1105						source.isVisible(source.getChildAt(source.root, i)));
1106				}
1107			}
1108		}
1109		finally
1110		{
1111			model.endUpdate();
1112		}
1113
1114		result = true;
1115	}
1116
1117	return result;
1118};
1119
1120/**
1121 *
1122 */
1123GraphViewer.prototype.addToolbar = function()
1124{
1125	var container = this.graph.container;
1126	var initialCursor = this.graph.container.style.cursor;
1127
1128	if (this.graphConfig['toolbar-position'] == 'bottom')
1129	{
1130		container.style.marginBottom = this.toolbarHeight + 'px';
1131	}
1132	else if (this.graphConfig['toolbar-position'] != 'inline')
1133	{
1134		container.style.marginTop = this.toolbarHeight + 'px';
1135	}
1136
1137	// Creates toolbar for viewer
1138	var toolbar = container.ownerDocument.createElement('div');
1139	toolbar.style.position = 'absolute';
1140	toolbar.style.overflow = 'hidden';
1141	toolbar.style.boxSizing = 'border-box';
1142	toolbar.style.whiteSpace = 'nowrap';
1143	toolbar.style.textAlign = 'left';
1144	toolbar.style.zIndex = this.toolbarZIndex;
1145	toolbar.style.backgroundColor = '#eee';
1146	toolbar.style.height = this.toolbarHeight + 'px';
1147	this.toolbar = toolbar;
1148
1149	if (this.graphConfig['toolbar-position'] == 'inline')
1150	{
1151		mxUtils.setPrefixedStyle(toolbar.style, 'transition', 'opacity 100ms ease-in-out');
1152		mxUtils.setOpacity(toolbar, 30);
1153
1154		// Changes toolbar opacity on hover
1155		var fadeThread = null;
1156		var fadeThread2 = null;
1157
1158		var fadeOut = mxUtils.bind(this, function(delay)
1159		{
1160			if (fadeThread != null)
1161			{
1162				window.clearTimeout(fadeThread);
1163				fadeThead = null;
1164			}
1165
1166			if (fadeThread2 != null)
1167			{
1168				window.clearTimeout(fadeThread2);
1169				fadeThead2 = null;
1170			}
1171
1172			fadeThread = window.setTimeout(mxUtils.bind(this, function()
1173			{
1174			 	mxUtils.setOpacity(toolbar, 0);
1175				fadeThread = null;
1176
1177				fadeThread2 = window.setTimeout(mxUtils.bind(this, function()
1178				{
1179					toolbar.style.display = 'none';
1180					fadeThread2 = null;
1181				}), 100);
1182			}), delay || 200);
1183		});
1184
1185		var fadeIn = mxUtils.bind(this, function(opacity)
1186		{
1187			if (fadeThread != null)
1188			{
1189				window.clearTimeout(fadeThread);
1190				fadeThead = null;
1191			}
1192
1193			if (fadeThread2 != null)
1194			{
1195				window.clearTimeout(fadeThread2);
1196				fadeThead2 = null;
1197			}
1198
1199			toolbar.style.display = '';
1200			mxUtils.setOpacity(toolbar, opacity || 30);
1201		});
1202
1203		mxEvent.addListener(this.graph.container, (mxClient.IS_POINTER) ? 'pointermove' : 'mousemove', mxUtils.bind(this, function(evt)
1204		{
1205			if (!mxEvent.isTouchEvent(evt))
1206			{
1207				fadeIn(30);
1208				fadeOut();
1209			}
1210		}));
1211
1212		mxEvent.addListener(toolbar, (mxClient.IS_POINTER) ? 'pointermove' : 'mousemove', function(evt)
1213		{
1214			mxEvent.consume(evt);
1215		});
1216
1217		mxEvent.addListener(toolbar, 'mouseenter', mxUtils.bind(this, function(evt)
1218		{
1219			fadeIn(100);
1220		}));
1221
1222		mxEvent.addListener(toolbar, 'mousemove',  mxUtils.bind(this, function(evt)
1223		{
1224			fadeIn(100);
1225			mxEvent.consume(evt);
1226		}));
1227
1228		mxEvent.addListener(toolbar, 'mouseleave',  mxUtils.bind(this, function(evt)
1229		{
1230			if (!mxEvent.isTouchEvent(evt))
1231			{
1232				fadeIn(30);
1233			}
1234		}));
1235
1236		// Shows/hides toolbar for touch devices
1237		var graph = this.graph;
1238		var tol = graph.getTolerance();
1239
1240		graph.addMouseListener(
1241		{
1242		    startX: 0,
1243		    startY: 0,
1244		    scrollLeft: 0,
1245		    scrollTop: 0,
1246		    mouseDown: function(sender, me)
1247		    {
1248		    	this.startX = me.getGraphX();
1249		    	this.startY = me.getGraphY();
1250			    this.scrollLeft = graph.container.scrollLeft;
1251			    this.scrollTop = graph.container.scrollTop;
1252		    },
1253		    mouseMove: function(sender, me) {},
1254		    mouseUp: function(sender, me)
1255		    {
1256		    	if (mxEvent.isTouchEvent(me.getEvent()))
1257		    	{
1258			    	if ((Math.abs(this.scrollLeft - graph.container.scrollLeft) < tol &&
1259			    		Math.abs(this.scrollTop - graph.container.scrollTop) < tol) &&
1260			    		(Math.abs(this.startX - me.getGraphX()) < tol &&
1261			    		Math.abs(this.startY - me.getGraphY()) < tol))
1262			    	{
1263			    		if (parseFloat(toolbar.style.opacity || 0) > 0)
1264			    		{
1265			    			fadeOut();
1266			    		}
1267			    		else
1268			    		{
1269			    			fadeIn(30);
1270			    		}
1271					}
1272		    	}
1273		    }
1274		});
1275	}
1276
1277	var tokens = this.toolbarItems;
1278	var buttonCount = 0;
1279
1280	function addButton(fn, imgSrc, tip, enabled)
1281	{
1282		var a = document.createElement('div');
1283		a.style.borderRight = '1px solid #d0d0d0';
1284		a.style.padding = '3px 6px 3px 6px';
1285		mxEvent.addListener(a, 'click', fn);
1286
1287		if (tip != null)
1288		{
1289			a.setAttribute('title', tip);
1290		}
1291
1292		a.style.display = 'inline-block';
1293		var img = document.createElement('img');
1294		img.setAttribute('border', '0');
1295		img.setAttribute('src', imgSrc);
1296		img.style.width = '18px';
1297
1298		if (enabled == null || enabled)
1299		{
1300			mxEvent.addListener(a, 'mouseenter', function()
1301			{
1302				a.style.backgroundColor = '#ddd';
1303			});
1304
1305			mxEvent.addListener(a, 'mouseleave', function()
1306			{
1307				a.style.backgroundColor = '#eee';
1308			});
1309
1310			mxUtils.setOpacity(img, 60);
1311			a.style.cursor = 'pointer';
1312		}
1313		else
1314		{
1315			mxUtils.setOpacity(a, 30);
1316		}
1317
1318		a.appendChild(img);
1319		toolbar.appendChild(a);
1320
1321		buttonCount++;
1322
1323		return a;
1324	};
1325
1326	var layersDialog = null;
1327	var tagsComponent = null;
1328	var tagsDialog = null;
1329	var pageInfo = null;
1330
1331	for (var i = 0; i < tokens.length; i++)
1332	{
1333		var token = tokens[i];
1334
1335		if (token == 'pages')
1336		{
1337			pageInfo = container.ownerDocument.createElement('div');
1338			pageInfo.style.cssText = 'display:inline-block;position:relative;top:5px;padding:0 4px 0 4px;' +
1339				'vertical-align:top;font-family:Helvetica,Arial;font-size:12px;;cursor:default;'
1340			mxUtils.setOpacity(pageInfo, 70);
1341
1342			var prevButton = addButton(mxUtils.bind(this, function()
1343			{
1344				this.selectPage(this.currentPage - 1);
1345			}), Editor.previousImage, mxResources.get('previousPage') || 'Previous Page');
1346
1347			prevButton.style.borderRightStyle = 'none';
1348			prevButton.style.paddingLeft = '0px';
1349			prevButton.style.paddingRight = '0px';
1350			toolbar.appendChild(pageInfo);
1351
1352			var nextButton = addButton(mxUtils.bind(this, function()
1353			{
1354				this.selectPage(this.currentPage + 1);
1355			}), Editor.nextImage, mxResources.get('nextPage') || 'Next Page');
1356
1357			nextButton.style.paddingLeft = '0px';
1358			nextButton.style.paddingRight = '0px';
1359
1360			var lastXmlNode = null;
1361
1362			var update = mxUtils.bind(this, function()
1363			{
1364				pageInfo.innerHTML = '';
1365				mxUtils.write(pageInfo, (this.currentPage + 1) + ' / ' + this.diagrams.length);
1366				pageInfo.style.display = (this.diagrams.length > 1) ? 'inline-block' : 'none';
1367				prevButton.style.display = pageInfo.style.display;
1368				nextButton.style.display = pageInfo.style.display;
1369			});
1370
1371			// LATER: Add event for setGraphXml
1372			this.addListener('graphChanged', update);
1373			update();
1374		}
1375		else if (token == 'zoom')
1376		{
1377			if (this.zoomEnabled)
1378			{
1379				addButton(mxUtils.bind(this, function()
1380				{
1381					this.graph.zoomOut();
1382				}), Editor.zoomOutImage, mxResources.get('zoomOut') || 'Zoom Out');
1383
1384				addButton(mxUtils.bind(this, function()
1385				{
1386					this.graph.zoomIn();
1387				}), Editor.zoomInImage, mxResources.get('zoomIn') || 'Zoom In');
1388
1389				addButton(mxUtils.bind(this, function()
1390				{
1391					this.graph.view.scaleAndTranslate(this.graph.initialViewState.scale,
1392						this.graph.initialViewState.translate.x,
1393						this.graph.initialViewState.translate.y);
1394				}), Editor.zoomFitImage, mxResources.get('fit') || 'Fit');
1395			}
1396		}
1397		else if (token == 'layers')
1398		{
1399			if (this.layersEnabled)
1400			{
1401				var model = this.graph.getModel();
1402
1403				var layersButton = addButton(mxUtils.bind(this, function(evt)
1404				{
1405					if (layersDialog != null)
1406					{
1407						layersDialog.parentNode.removeChild(layersDialog);
1408						layersDialog = null;
1409					}
1410					else
1411					{
1412						layersDialog = this.graph.createLayersDialog(mxUtils.bind(this, function()
1413						{
1414							if (this.autoCrop)
1415							{
1416								this.crop();
1417							}
1418							else if (this.autoOrigin)
1419							{
1420								var bounds = this.graph.getGraphBounds();
1421								var v = this.graph.view;
1422
1423								if (bounds.x < 0 || bounds.y < 0)
1424								{
1425									this.crop();
1426									this.graph.originalViewState = this.graph.initialViewState;
1427
1428									this.graph.initialViewState = {
1429										translate: v.translate.clone(),
1430										scale: v.scale
1431									};
1432								}
1433								else if (this.graph.originalViewState != null &&
1434									bounds.x / v.scale + this.graph.originalViewState.translate.x - v.translate.x > 0 &&
1435									bounds.y / v.scale + this.graph.originalViewState.translate.y - v.translate.y > 0)
1436								{
1437									v.setTranslate(this.graph.originalViewState.translate.x,
1438										this.graph.originalViewState.translate.y);
1439									this.graph.originalViewState = null;
1440
1441									this.graph.initialViewState = {
1442										translate: v.translate.clone(),
1443										scale: v.scale
1444									};
1445								}
1446							}
1447						}));
1448
1449						mxEvent.addListener(layersDialog, 'mouseleave', function()
1450						{
1451							layersDialog.parentNode.removeChild(layersDialog);
1452							layersDialog = null;
1453						});
1454
1455						var r = layersButton.getBoundingClientRect();
1456
1457						layersDialog.style.width = '140px';
1458						layersDialog.style.padding = '2px 0px 2px 0px';
1459						layersDialog.style.border = '1px solid #d0d0d0';
1460						layersDialog.style.backgroundColor = '#eee';
1461						layersDialog.style.fontFamily = Editor.defaultHtmlFont;
1462						layersDialog.style.fontSize = '11px';
1463						layersDialog.style.zIndex = this.toolbarZIndex + 1;
1464						mxUtils.setOpacity(layersDialog, 80);
1465						var origin = mxUtils.getDocumentScrollOrigin(document);
1466						layersDialog.style.left = origin.x + r.left - 1 + 'px';
1467						layersDialog.style.top = origin.y + r.bottom - 2 + 'px';
1468
1469						document.body.appendChild(layersDialog);
1470					}
1471				}), Editor.layersImage, mxResources.get('layers') || 'Layers');
1472
1473				model.addListener(mxEvent.CHANGE, function()
1474				{
1475					layersButton.style.display = (model.getChildCount(model.root) > 1) ? 'inline-block' : 'none';
1476				});
1477
1478				layersButton.style.display = (model.getChildCount(model.root) > 1) ? 'inline-block' : 'none';
1479			}
1480		}
1481		else if (token == 'tags')
1482		{
1483			if (this.tagsEnabled)
1484			{
1485				var tagsButton = addButton(mxUtils.bind(this, function(evt)
1486				{
1487					if (tagsComponent == null)
1488					{
1489						tagsComponent = this.graph.createTagsDialog(mxUtils.bind(this, function()
1490						{
1491							return true;
1492						}));
1493
1494						tagsComponent.div.getElementsByTagName('div')[0].style.position = '';
1495						tagsComponent.div.style.maxHeight = '160px';
1496						tagsComponent.div.style.maxWidth = '120px';
1497						tagsComponent.div.style.padding = '2px';
1498						tagsComponent.div.style.overflow = 'auto';
1499						tagsComponent.div.style.height = 'auto';
1500						tagsComponent.div.style.position = 'fixed';
1501						tagsComponent.div.style.fontFamily = Editor.defaultHtmlFont;
1502						tagsComponent.div.style.fontSize = '11px';
1503						tagsComponent.div.style.backgroundColor = '#eee';
1504						tagsComponent.div.style.color = '#000';
1505						tagsComponent.div.style.border = '1px solid #d0d0d0';
1506						tagsComponent.div.style.zIndex = this.toolbarZIndex + 1;
1507						mxUtils.setOpacity(tagsComponent.div, 80);
1508					}
1509
1510					if (tagsDialog != null)
1511					{
1512						tagsDialog.parentNode.removeChild(tagsDialog);
1513						tagsDialog = null;
1514					}
1515					else
1516					{
1517						tagsDialog = tagsComponent.div;
1518
1519						mxEvent.addListener(tagsDialog, 'mouseleave', function()
1520						{
1521							tagsDialog.parentNode.removeChild(tagsDialog);
1522							tagsDialog = null;
1523						});
1524
1525						var r = tagsButton.getBoundingClientRect();
1526						var origin = mxUtils.getDocumentScrollOrigin(document);
1527						tagsDialog.style.left = origin.x + r.left - 1 + 'px';
1528						tagsDialog.style.top = origin.y + r.bottom - 2 + 'px';
1529						document.body.appendChild(tagsDialog);
1530						tagsComponent.refresh();
1531					}
1532				}), Editor.tagsImage, mxResources.get('tags') || 'Tags');
1533
1534				model.addListener(mxEvent.CHANGE, mxUtils.bind(this, function()
1535				{
1536					tagsButton.style.display = (this.graph.getAllTags().length > 0) ? 'inline-block' : 'none';
1537				}));
1538
1539				tagsButton.style.display = (this.graph.getAllTags().length > 0) ? 'inline-block' : 'none';
1540			}
1541		}
1542		else if (token == 'lightbox')
1543		{
1544			if (this.lightboxEnabled)
1545			{
1546				addButton(mxUtils.bind(this, function()
1547				{
1548					this.showLightbox();
1549				}), Editor.fullscreenImage, (mxResources.get('fullscreen') || 'Fullscreen'));
1550			}
1551		}
1552		else if (this.graphConfig['toolbar-buttons'] != null)
1553		{
1554			var def = this.graphConfig['toolbar-buttons'][token];
1555
1556			if (def != null)
1557			{
1558				def.elem = addButton((def.enabled == null || def.enabled) ? def.handler : function() {},
1559					def.image, def.title, def.enabled);
1560			}
1561		}
1562	}
1563
1564	if (this.graph.minimumContainerSize != null)
1565	{
1566		this.graph.minimumContainerSize.width = buttonCount * 34;
1567	}
1568
1569	if (this.graphConfig.title != null)
1570	{
1571		var filename = container.ownerDocument.createElement('div');
1572		filename.style.cssText = 'display:inline-block;position:relative;padding:3px 6px 0 6px;' +
1573			'vertical-align:top;font-family:Helvetica,Arial;font-size:12px;top:4px;cursor:default;'
1574		filename.setAttribute('title', this.graphConfig.title);
1575		mxUtils.write(filename, this.graphConfig.title);
1576		mxUtils.setOpacity(filename, 70);
1577
1578		toolbar.appendChild(filename);
1579		this.filename = filename;
1580	}
1581
1582	this.minToolbarWidth = buttonCount * 34;
1583	var prevBorder = container.style.border;
1584
1585	var enter = mxUtils.bind(this, function()
1586	{
1587		toolbar.style.width = (this.graphConfig['toolbar-position'] == 'inline') ? 'auto' :
1588			Math.max(this.minToolbarWidth, container.offsetWidth) + 'px';
1589		toolbar.style.border = '1px solid #d0d0d0';
1590
1591		if (this.graphConfig['toolbar-nohide'] != true)
1592		{
1593			var r = container.getBoundingClientRect();
1594
1595			// Workaround for position:relative set in ResizeSensor
1596			var origin = mxUtils.getScrollOrigin(document.body)
1597			var b = (document.body.style.position === 'relative') ? document.body.getBoundingClientRect() :
1598				{left: -origin.x, top: -origin.y};
1599			r = {left: r.left - b.left, top: r.top - b.top, bottom: r.bottom - b.top, right: r.right - b.left};
1600
1601			toolbar.style.left = r.left + 'px';
1602
1603			if (this.graphConfig['toolbar-position'] == 'bottom')
1604			{
1605				toolbar.style.top = r.bottom - 1 + 'px';
1606			}
1607			else
1608			{
1609				if (this.graphConfig['toolbar-position'] != 'inline')
1610				{
1611					toolbar.style.marginTop = -this.toolbarHeight + 'px';
1612					toolbar.style.top = r.top + 1 + 'px';
1613				}
1614				else
1615				{
1616					toolbar.style.top = r.top + 'px';
1617				}
1618			}
1619
1620			if (prevBorder == '1px solid transparent')
1621			{
1622				container.style.border = '1px solid #d0d0d0';
1623			}
1624
1625			document.body.appendChild(toolbar);
1626
1627			var hideToolbar = mxUtils.bind(this, function()
1628			{
1629				if (toolbar.parentNode != null)
1630				{
1631					toolbar.parentNode.removeChild(toolbar);
1632				}
1633
1634				if (layersDialog != null)
1635				{
1636					layersDialog.parentNode.removeChild(layersDialog);
1637					layersDialog = null;
1638				}
1639
1640				container.style.border = prevBorder;
1641			});
1642
1643			mxEvent.addListener(document, 'mousemove', function(evt)
1644			{
1645				var source = mxEvent.getSource(evt);
1646
1647				while (source != null)
1648				{
1649					if (source == container || source == toolbar || source == layersDialog)
1650					{
1651						return;
1652					}
1653
1654					source = source.parentNode;
1655				}
1656
1657				hideToolbar();
1658			});
1659
1660			mxEvent.addListener(document.body, 'mouseleave', function(evt)
1661			{
1662				hideToolbar();
1663			});
1664		}
1665		else
1666		{
1667			toolbar.style.top = -this.toolbarHeight + 'px';
1668			container.appendChild(toolbar);
1669		}
1670	});
1671
1672	if (this.graphConfig['toolbar-nohide'] != true)
1673	{
1674		mxEvent.addListener(container, 'mouseenter', enter);
1675	}
1676	else
1677	{
1678		enter();
1679	}
1680
1681	if (this.responsive && typeof ResizeObserver !== 'undefined')
1682	{
1683		new ResizeObserver(function()
1684		{
1685			if (toolbar.parentNode != null)
1686			{
1687				enter();
1688			}
1689		}).observe(container)
1690	}
1691};
1692
1693GraphViewer.prototype.disableButton = function(token)
1694{
1695	var def = this.graphConfig['toolbar-buttons'][token];
1696
1697	if (def != null)
1698	{
1699		mxUtils.setOpacity(def.elem, 30);
1700		mxEvent.removeListener(def.elem, 'click', def.handler);
1701		//Workaround to stop highlighting the disabled button
1702		mxEvent.addListener(def.elem, 'mouseenter', function()
1703		{
1704			def.elem.style.backgroundColor = '#eee';
1705		});
1706	}
1707};
1708
1709/**
1710 * Adds event handler for links and lightbox.
1711 */
1712GraphViewer.prototype.addClickHandler = function(graph, ui)
1713{
1714	graph.linkPolicy = this.graphConfig.target || graph.linkPolicy;
1715
1716	graph.addClickHandler(this.graphConfig.highlight, mxUtils.bind(this, function(evt, href)
1717	{
1718		if (href == null)
1719		{
1720			var source = mxEvent.getSource(evt);
1721
1722			while (source != graph.container && source != null && href == null)
1723			{
1724				if (source.nodeName.toLowerCase() == 'a')
1725				{
1726					href = source.getAttribute('href');
1727				}
1728
1729				source = source.parentNode;
1730			}
1731		}
1732
1733		if (ui != null)
1734		{
1735			if (href == null || graph.isCustomLink(href))
1736			{
1737				mxEvent.consume(evt);
1738			}
1739			else if (!graph.isExternalProtocol(href) &&
1740					!graph.isBlankLink(href))
1741			{
1742				// Hides lightbox if any links are clicked
1743				// Async handling needed for anchors to work
1744				window.setTimeout(function()
1745				{
1746					ui.destroy();
1747				}, 0);
1748			}
1749		}
1750		else if (href != null && ui == null && graph.isCustomLink(href) &&
1751			(mxEvent.isTouchEvent(evt) || !mxEvent.isPopupTrigger(evt)) &&
1752			graph.customLinkClicked(href))
1753		{
1754			// Workaround for text selection in Firefox on Windows
1755			mxUtils.clearSelection();
1756			mxEvent.consume(evt);
1757		}
1758	}), mxUtils.bind(this, function(evt)
1759	{
1760		if (ui == null && this.lightboxClickEnabled &&
1761			(!mxEvent.isTouchEvent(evt) ||
1762			this.toolbarItems.length == 0))
1763		{
1764			this.showLightbox();
1765		}
1766	}));
1767};
1768
1769/**
1770 * Adds the given array of stencils to avoid dynamic loading of shapes.
1771 */
1772GraphViewer.prototype.showLightbox = function(editable, closable, target)
1773{
1774	if (this.graphConfig.lightbox == 'open' || window.self !== window.top)
1775	{
1776		if (this.lightboxWindow != null && !this.lightboxWindow.closed)
1777		{
1778			this.lightboxWindow.focus();
1779		}
1780		else
1781		{
1782			editable = (editable != null) ? editable :
1783				((this.graphConfig.editable != null) ?
1784					this.graphConfig.editable : true);
1785			closable = (closable != null) ? closable : true;
1786			target = (target != null) ? target : 'blank';
1787
1788			var param = {'client': 1, 'target': target};
1789
1790			if (editable)
1791			{
1792				param.edit = this.graphConfig.edit || '_blank';
1793			}
1794
1795			if (closable)
1796			{
1797		    	param.close = 1;
1798			}
1799
1800			if (this.layersEnabled)
1801			{
1802		    	param.layers = 1;
1803			}
1804
1805			if (this.tagsEnabled)
1806			{
1807		    	param.tags = {};
1808			}
1809
1810			if (this.graphConfig != null && this.graphConfig.nav != false)
1811			{
1812				param.nav = 1;
1813			}
1814
1815			if (this.graphConfig != null && this.graphConfig.highlight != null)
1816			{
1817				param.highlight = this.graphConfig.highlight.substring(1);
1818			}
1819
1820			if (this.currentPage != null && this.currentPage > 0)
1821			{
1822				param.page = this.currentPage;
1823			}
1824
1825			if (typeof window.postMessage !== 'undefined' && (document.documentMode == null || document.documentMode >= 10))
1826			{
1827				if (this.lightboxWindow == null)
1828				{
1829					mxEvent.addListener(window, 'message', mxUtils.bind(this, function(evt)
1830					{
1831						if (evt.data == 'ready' && evt.source == this.lightboxWindow)
1832						{
1833							this.lightboxWindow.postMessage(this.xml, '*');
1834						}
1835					}));
1836				}
1837			}
1838			else
1839			{
1840				// Data is pulled from global variable after tab loads
1841				param.data = encodeURIComponent(this.xml);
1842			}
1843
1844			if (urlParams['dev'] == '1')
1845			{
1846				param.dev = '1';
1847			}
1848
1849		    this.lightboxWindow = window.open(((urlParams['dev'] != '1') ?
1850		    	EditorUi.lightboxHost : 'https://test.draw.io') +
1851		    	'/#P' + encodeURIComponent(JSON.stringify(param)));
1852		}
1853	}
1854	else
1855	{
1856		this.showLocalLightbox();
1857	}
1858};
1859
1860/**
1861 * Adds the given array of stencils to avoid dynamic loading of shapes.
1862 */
1863GraphViewer.prototype.showLocalLightbox = function()
1864{
1865	var origin = mxUtils.getDocumentScrollOrigin(document);
1866	var backdrop = document.createElement('div');
1867
1868	backdrop.style.cssText = 'position:fixed;top:0;left:0;bottom:0;right:0;';
1869	backdrop.style.zIndex = this.lightboxZIndex;
1870	backdrop.style.backgroundColor = '#000000';
1871	mxUtils.setOpacity(backdrop, 70);
1872
1873	document.body.appendChild(backdrop);
1874
1875	var closeImg = document.createElement('img');
1876	closeImg.setAttribute('border', '0');
1877	closeImg.setAttribute('src', Editor.closeBlackImage);
1878	closeImg.style.cssText = 'position:fixed;top:32px;right:32px;';
1879	closeImg.style.cursor = 'pointer';
1880
1881	mxEvent.addListener(closeImg, 'click', function()
1882	{
1883		ui.destroy();
1884	});
1885
1886	// LATER: Make possible to assign after instance was created
1887	urlParams['pages'] = '1';
1888	urlParams['page'] = this.currentPage;
1889	urlParams['page-id'] = this.graphConfig.pageId;
1890	urlParams['layer-ids'] = (this.graphConfig.layerIds != null && this.graphConfig.layerIds.length > 0)
1891														? this.graphConfig.layerIds.join(' ') : null;
1892	urlParams['nav'] = (this.graphConfig.nav != false) ? '1' : '0';
1893	urlParams['layers'] = (this.layersEnabled) ? '1' : '0';
1894
1895	if (this.tagsEnabled)
1896	{
1897		urlParams['tags'] = '{}';
1898	}
1899
1900	// PostMessage not working and Permission denied for opened access in IE9-
1901	if (document.documentMode == null || document.documentMode >= 10)
1902	{
1903		Editor.prototype.editButtonLink = this.graphConfig.edit;
1904		Editor.prototype.editButtonFunc = this.graphConfig.editFunc;
1905	}
1906
1907	EditorUi.prototype.updateActionStates = function() {};
1908	EditorUi.prototype.addBeforeUnloadListener = function() {};
1909	EditorUi.prototype.addChromelessClickHandler = function() {};
1910
1911	// Workaround for lost reference with same ID is to change
1912	// ID which must be done before calling EditorUi constructor
1913	var previousShadowId = Graph.prototype.shadowId;
1914	Graph.prototype.shadowId = 'lightboxDropShadow';
1915
1916	var ui = new EditorUi(new Editor(true), document.createElement('div'), true);
1917	ui.editor.editBlankUrl = this.editBlankUrl;
1918
1919	// Overrides instance variable and restores prototype state
1920	ui.editor.graph.shadowId = 'lightboxDropShadow';
1921	Graph.prototype.shadowId = previousShadowId;
1922
1923	// Disables refresh
1924	ui.refresh = function() {};
1925
1926	// Handles escape keystroke
1927	var keydownHandler = mxUtils.bind(this, function(evt)
1928	{
1929		if (evt.keyCode == 27 /* Escape */)
1930		{
1931			ui.destroy();
1932		}
1933	});
1934
1935	var destroy = ui.destroy;
1936	ui.destroy = function()
1937	{
1938		mxEvent.removeListener(document.documentElement, 'keydown', keydownHandler);
1939		document.body.removeChild(backdrop);
1940		document.body.removeChild(closeImg);
1941		document.body.style.overflow = 'auto';
1942		GraphViewer.resizeSensorEnabled = true;
1943
1944		destroy.apply(this, arguments);
1945	};
1946
1947	var graph = ui.editor.graph;
1948	var lightbox = graph.container;
1949	lightbox.style.overflow = 'hidden';
1950
1951	if (this.lightboxChrome)
1952	{
1953		lightbox.style.border = '1px solid #c0c0c0';
1954		lightbox.style.margin = '40px';
1955
1956		// Installs the keystroke listener in the target
1957		mxEvent.addListener(document.documentElement, 'keydown', keydownHandler);
1958	}
1959	else
1960	{
1961		backdrop.style.display = 'none';
1962		closeImg.style.display = 'none';
1963	}
1964
1965	// Handles relative images
1966	var self = this;
1967
1968	graph.getImageFromBundles = function(key)
1969	{
1970		return self.getImageUrl(key);
1971	};
1972
1973	// Handles relative images in print output and temporary graphs
1974	var uiCreateTemporaryGraph = ui.createTemporaryGraph;
1975
1976	ui.createTemporaryGraph = function()
1977	{
1978		var newGraph = uiCreateTemporaryGraph.apply(this, arguments);
1979
1980		newGraph.getImageFromBundles = function(key)
1981		{
1982			return self.getImageUrl(key);
1983		};
1984
1985		return newGraph;
1986	};
1987
1988	if (this.graphConfig.move)
1989	{
1990		graph.isMoveCellsEvent = function(evt)
1991		{
1992			return true;
1993		};
1994	}
1995
1996	mxUtils.setPrefixedStyle(lightbox.style, 'border-radius', '4px');
1997	lightbox.style.position = 'fixed';
1998
1999	GraphViewer.resizeSensorEnabled = false;
2000	document.body.style.overflow = 'hidden';
2001
2002	// Workaround for possible rendering issues
2003	if (!mxClient.IS_SF && !mxClient.IS_EDGE)
2004	{
2005		mxUtils.setPrefixedStyle(lightbox.style, 'transform', 'rotateY(90deg)');
2006		mxUtils.setPrefixedStyle(lightbox.style, 'transition', 'all .25s ease-in-out');
2007	}
2008
2009	this.addClickHandler(graph, ui);
2010
2011	window.setTimeout(mxUtils.bind(this, function()
2012	{
2013		// Disables focus border in Chrome
2014		lightbox.style.outline = 'none';
2015		lightbox.style.zIndex = this.lightboxZIndex;
2016		closeImg.style.zIndex = this.lightboxZIndex;
2017
2018		document.body.appendChild(lightbox);
2019		document.body.appendChild(closeImg);
2020
2021		ui.setFileData(this.xml);
2022
2023		mxUtils.setPrefixedStyle(lightbox.style, 'transform', 'rotateY(0deg)');
2024		ui.chromelessToolbar.style.bottom = 60 + 'px';
2025		ui.chromelessToolbar.style.zIndex = this.lightboxZIndex;
2026
2027		// Workaround for clipping in IE11-
2028		document.body.appendChild(ui.chromelessToolbar);
2029
2030		ui.getEditBlankXml = mxUtils.bind(this, function()
2031		{
2032			return this.xml;
2033		});
2034
2035		ui.lightboxFit();
2036		ui.chromelessResize();
2037		this.showLayers(graph, this.graph);
2038
2039		// Click on backdrop closes lightbox
2040		mxEvent.addListener(backdrop, 'click', function()
2041		{
2042			ui.destroy();
2043		});
2044	}), 0);
2045
2046	return ui;
2047};
2048
2049GraphViewer.prototype.updateTitle = function(title)
2050{
2051	title = title || '';
2052
2053	if (this.showTitleAsTooltip && this.graph != null && this.graph.container != null)
2054	{
2055		this.graph.container.setAttribute('title', title);
2056    }
2057
2058	if (this.filename != null)
2059	{
2060		this.filename.innerHTML = '';
2061		mxUtils.write(this.filename, title);
2062		this.filename.setAttribute('title', title);
2063	}
2064};
2065
2066/**
2067 *
2068 */
2069GraphViewer.processElements = function(classname)
2070{
2071	mxUtils.forEach(GraphViewer.getElementsByClassName(classname || 'mxgraph'), function(div)
2072	{
2073		try
2074		{
2075			div.innerHTML = '';
2076			GraphViewer.createViewerForElement(div);
2077		}
2078		catch (e)
2079		{
2080			div.innerHTML = e.message;
2081
2082			if (window.console != null)
2083			{
2084				console.error(e);
2085			}
2086		}
2087	});
2088};
2089
2090/**
2091 * Adds the given array of stencils to avoid dynamic loading of shapes.
2092 */
2093GraphViewer.getElementsByClassName = function(classname)
2094{
2095	if (document.getElementsByClassName)
2096	{
2097		var divs = document.getElementsByClassName(classname);
2098
2099		// Workaround for changing divs while processing
2100		var result = [];
2101
2102		for (var i = 0; i < divs.length; i++)
2103		{
2104			result.push(divs[i]);
2105		}
2106
2107		return result;
2108	}
2109	else
2110	{
2111		var tmp = document.getElementsByTagName('*');
2112		var divs = [];
2113
2114		for (var i = 0; i < tmp.length; i++)
2115		{
2116			var cls = tmp[i].className;
2117
2118			if (cls != null && cls.length > 0)
2119			{
2120				var tokens = cls.split(' ');
2121
2122				if (mxUtils.indexOf(tokens, classname) >= 0)
2123				{
2124					divs.push(tmp[i]);
2125				}
2126			}
2127		}
2128
2129		return divs;
2130	}
2131};
2132
2133/**
2134 * Adds the given array of stencils to avoid dynamic loading of shapes.
2135 */
2136GraphViewer.createViewerForElement = function(element, callback)
2137{
2138	var data = element.getAttribute('data-mxgraph');
2139
2140	if (data != null)
2141	{
2142		var config = JSON.parse(data);
2143
2144		var createViewer = function(xml)
2145		{
2146			var xmlDoc = mxUtils.parseXml(xml);
2147			var viewer = new GraphViewer(element, xmlDoc.documentElement, config);
2148
2149			if (callback != null)
2150			{
2151				callback(viewer);
2152			}
2153		};
2154
2155		if (config.url != null)
2156		{
2157			GraphViewer.getUrl(config.url, function(xml)
2158			{
2159				createViewer(xml);
2160			});
2161		}
2162		else
2163		{
2164			createViewer(config.xml);
2165		}
2166	}
2167};
2168
2169/**
2170 * Adds event if grid size is changed.
2171 */
2172GraphViewer.initCss = function()
2173{
2174	try
2175	{
2176		var style = document.createElement('style')
2177		style.type = 'text/css';
2178		style.innerHTML = ['div.mxTooltip {',
2179			'-webkit-box-shadow: 3px 3px 12px #C0C0C0;',
2180			'-moz-box-shadow: 3px 3px 12px #C0C0C0;',
2181			'box-shadow: 3px 3px 12px #C0C0C0;',
2182			'background: #FFFFCC;',
2183			'border-style: solid;',
2184			'border-width: 1px;',
2185			'border-color: black;',
2186			'font-family: Arial;',
2187			'font-size: 8pt;',
2188			'position: absolute;',
2189			'cursor: default;',
2190			'padding: 4px;',
2191			'color: black;}',
2192			'td.mxPopupMenuIcon div {',
2193			'width:16px;',
2194			'height:16px;}',
2195			'html div.mxPopupMenu {',
2196			'-webkit-box-shadow:2px 2px 3px #d5d5d5;',
2197			'-moz-box-shadow:2px 2px 3px #d5d5d5;',
2198			'box-shadow:2px 2px 3px #d5d5d5;',
2199			'_filter:progid:DXImageTransform.Microsoft.DropShadow(OffX=2, OffY=2, Color=\'#d0d0d0\',Positive=\'true\');',
2200			'background:white;',
2201			'position:absolute;',
2202			'border:3px solid #e7e7e7;',
2203			'padding:3px;}',
2204			'html table.mxPopupMenu {',
2205			'border-collapse:collapse;',
2206			'margin:0px;}',
2207			'html td.mxPopupMenuItem {',
2208			'padding:7px 30px 7px 30px;',
2209			'font-family:Helvetica Neue,Helvetica,Arial Unicode MS,Arial;',
2210			'font-size:10pt;}',
2211			'html td.mxPopupMenuIcon {',
2212			'background-color:white;',
2213			'padding:0px;}',
2214			'td.mxPopupMenuIcon .geIcon {',
2215			'padding:2px;',
2216			'padding-bottom:4px;',
2217			'margin:2px;',
2218			'border:1px solid transparent;',
2219			'opacity:0.5;',
2220			'_width:26px;',
2221			'_height:26px;}',
2222			'td.mxPopupMenuIcon .geIcon:hover {',
2223			'border:1px solid gray;',
2224			'border-radius:2px;',
2225			'opacity:1;}',
2226			'html tr.mxPopupMenuItemHover {',
2227			'background-color: #eeeeee;',
2228			'color: black;}',
2229			'table.mxPopupMenu hr {',
2230			'color:#cccccc;',
2231			'background-color:#cccccc;',
2232			'border:none;',
2233			'height:1px;}',
2234			'table.mxPopupMenu tr {	font-size:4pt;}',
2235			// Modified to only apply to the print dialog
2236			'.geDialog { font-family:Helvetica Neue,Helvetica,Arial Unicode MS,Arial;',
2237			'font-size:10pt;',
2238			'border:none;',
2239			'margin:0px;}',
2240			// These are required for the print dialog
2241			'.geDialog {	position:absolute;	background:white;	overflow:hidden;	padding:30px;	border:1px solid #acacac;	-webkit-box-shadow:0px 0px 2px 2px #d5d5d5;	-moz-box-shadow:0px 0px 2px 2px #d5d5d5;	box-shadow:0px 0px 2px 2px #d5d5d5;	_filter:progid:DXImageTransform.Microsoft.DropShadow(OffX=2, OffY=2, Color=\'#d5d5d5\', Positive=\'true\');	z-index: 2;}.geDialogClose {	position:absolute;	width:9px;	height:9px;	opacity:0.5;	cursor:pointer;	_filter:alpha(opacity=50);}.geDialogClose:hover {	opacity:1;}.geDialogTitle {	box-sizing:border-box;	white-space:nowrap;	background:rgb(229, 229, 229);	border-bottom:1px solid rgb(192, 192, 192);	font-size:15px;	font-weight:bold;	text-align:center;	color:rgb(35, 86, 149);}.geDialogFooter {	background:whiteSmoke;	white-space:nowrap;	text-align:right;	box-sizing:border-box;	border-top:1px solid #e5e5e5;	color:darkGray;}',
2242			'.geBtn {	background-color: #f5f5f5;	border-radius: 2px;	border: 1px solid #d8d8d8;	color: #333;	cursor: default;	font-size: 11px;	font-weight: bold;	height: 29px;	line-height: 27px;	margin: 0 0 0 8px;	min-width: 72px;	outline: 0;	padding: 0 8px;	cursor: pointer;}.geBtn:hover, .geBtn:focus {	-webkit-box-shadow: 0px 1px 1px rgba(0,0,0,0.1);	-moz-box-shadow: 0px 1px 1px rgba(0,0,0,0.1);	box-shadow: 0px 1px 1px rgba(0,0,0,0.1);	border: 1px solid #c6c6c6;	background-color: #f8f8f8;	background-image: linear-gradient(#f8f8f8 0px,#f1f1f1 100%);	color: #111;}.geBtn:disabled {	opacity: .5;}.gePrimaryBtn {	background-color: #4d90fe;	background-image: linear-gradient(#4d90fe 0px,#4787ed 100%);	border: 1px solid #3079ed;	color: #fff;}.gePrimaryBtn:hover, .gePrimaryBtn:focus {	background-color: #357ae8;	background-image: linear-gradient(#4d90fe 0px,#357ae8 100%);	border: 1px solid #2f5bb7;	color: #fff;}.gePrimaryBtn:disabled {	opacity: .5;}'].join('\n');
2243		document.getElementsByTagName('head')[0].appendChild(style);
2244	}
2245	catch (e)
2246	{
2247		// ignore
2248	}
2249};
2250
2251/**
2252 * Lookup for URLs.
2253 */
2254GraphViewer.cachedUrls = {};
2255
2256/**
2257 * Workaround for unsupported CORS in IE9 XHR
2258 */
2259GraphViewer.getUrl = function(url, onload, onerror)
2260{
2261	if (GraphViewer.cachedUrls[url] != null)
2262	{
2263		onload(GraphViewer.cachedUrls[url]);
2264	}
2265	else
2266	{
2267		var xhr = (navigator.userAgent != null && navigator.userAgent.indexOf('MSIE 9') > 0) ?
2268			new XDomainRequest() : new XMLHttpRequest();
2269		xhr.open('GET', url);
2270
2271	    xhr.onload = function()
2272	    {
2273	    	onload((xhr.getText != null) ? xhr.getText() : xhr.responseText);
2274		};
2275
2276	    xhr.onerror = onerror;
2277	    xhr.send();
2278	}
2279};
2280
2281/**
2282 * Redirects editing to absolue URLs.
2283 */
2284GraphViewer.resizeSensorEnabled = true;
2285
2286/**
2287 * Redirects editing to absolue URLs.
2288 */
2289GraphViewer.useResizeSensor = true;
2290
2291/**
2292 * Copyright Marc J. Schmidt. See the LICENSE file at the top-level
2293 * directory of this distribution and at
2294 * https://github.com/marcj/css-element-queries/blob/master/LICENSE.
2295 */
2296(function() {
2297
2298    // Only used for the dirty checking, so the event callback count is limted to max 1 call per fps per sensor.
2299    // In combination with the event based resize sensor this saves cpu time, because the sensor is too fast and
2300    // would generate too many unnecessary events.
2301    var requestAnimationFrame = window.requestAnimationFrame ||
2302        window.mozRequestAnimationFrame ||
2303        window.webkitRequestAnimationFrame ||
2304        function (fn) {
2305            return window.setTimeout(fn, 20);
2306        };
2307
2308    /**
2309     * Class for dimension change detection.
2310     *
2311     * @param {Element|Element[]|Elements|jQuery} element
2312     * @param {Function} callback
2313     *
2314     * @constructor
2315     */
2316    var ResizeSensor = function(element, fn) {
2317
2318    	var callback = function()
2319    	{
2320    		if (GraphViewer.resizeSensorEnabled)
2321    		{
2322    			fn();
2323    		}
2324    	};
2325
2326        /**
2327         *
2328         * @constructor
2329         */
2330        function EventQueue() {
2331            this.q = [];
2332            this.add = function(ev) {
2333                this.q.push(ev);
2334            };
2335
2336            var i, j;
2337            this.call = function() {
2338                for (i = 0, j = this.q.length; i < j; i++) {
2339                    this.q[i].call();
2340                }
2341            };
2342        }
2343
2344        /**
2345         * @param {HTMLElement} element
2346         * @param {String}      prop
2347         * @returns {String|Number}
2348         */
2349        function getComputedStyle(element, prop) {
2350            if (element.currentStyle) {
2351                return element.currentStyle[prop];
2352            } else if (window.getComputedStyle) {
2353                return window.getComputedStyle(element, null).getPropertyValue(prop);
2354            } else {
2355                return element.style[prop];
2356            }
2357        }
2358
2359        /**
2360         *
2361         * @param {HTMLElement} element
2362         * @param {Function}    resized
2363         */
2364        function attachResizeEvent(element, resized) {
2365            if (!element.resizedAttached) {
2366                element.resizedAttached = new EventQueue();
2367                element.resizedAttached.add(resized);
2368            } else if (element.resizedAttached) {
2369                element.resizedAttached.add(resized);
2370                return;
2371            }
2372
2373            element.resizeSensor = document.createElement('div');
2374            element.resizeSensor.className = 'resize-sensor';
2375            var style = 'position: absolute; left: 0; top: 0; right: 0; bottom: 0; overflow: hidden; z-index: -1; visibility: hidden;';
2376            var styleChild = 'position: absolute; left: 0; top: 0; transition: 0s;';
2377
2378            element.resizeSensor.style.cssText = style;
2379            element.resizeSensor.innerHTML =
2380                '<div class="resize-sensor-expand" style="' + style + '">' +
2381                    '<div style="' + styleChild + '"></div>' +
2382                '</div>' +
2383                '<div class="resize-sensor-shrink" style="' + style + '">' +
2384                    '<div style="' + styleChild + ' width: 200%; height: 200%"></div>' +
2385                '</div>';
2386            element.appendChild(element.resizeSensor);
2387
2388            // FIXME: Should not change element style
2389            if (getComputedStyle(element, 'position') == 'static') {
2390                element.style.position = 'relative';
2391            }
2392
2393            var expand = element.resizeSensor.childNodes[0];
2394            var expandChild = expand.childNodes[0];
2395            var shrink = element.resizeSensor.childNodes[1];
2396
2397            var reset = function() {
2398                expandChild.style.width  = 100000 + 'px';
2399                expandChild.style.height = 100000 + 'px';
2400
2401                expand.scrollLeft = 100000;
2402                expand.scrollTop = 100000;
2403
2404                shrink.scrollLeft = 100000;
2405                shrink.scrollTop = 100000;
2406            };
2407
2408            reset();
2409            var dirty = false;
2410
2411            var dirtyChecking = function(){
2412                if (!element.resizedAttached) return;
2413
2414                if (dirty) {
2415                    element.resizedAttached.call();
2416                    dirty = false;
2417                }
2418
2419                requestAnimationFrame(dirtyChecking);
2420            };
2421
2422            requestAnimationFrame(dirtyChecking);
2423            var lastWidth, lastHeight;
2424            var cachedWidth, cachedHeight; //useful to not query offsetWidth twice
2425
2426            var onScroll = function() {
2427              if ((cachedWidth = element.offsetWidth) != lastWidth || (cachedHeight = element.offsetHeight) != lastHeight) {
2428                  dirty = true;
2429
2430                  lastWidth = cachedWidth;
2431                  lastHeight = cachedHeight;
2432              }
2433              reset();
2434            };
2435
2436            var addEvent = function(el, name, cb) {
2437                if (el.attachEvent) {
2438                    el.attachEvent('on' + name, cb);
2439                } else {
2440                    el.addEventListener(name, cb);
2441                }
2442            };
2443
2444            addEvent(expand, 'scroll', onScroll);
2445            addEvent(shrink, 'scroll', onScroll);
2446        }
2447
2448        var elementType = Object.prototype.toString.call(element);
2449        var isCollectionTyped = ('[object Array]' === elementType
2450            || ('[object NodeList]' === elementType)
2451            || ('[object HTMLCollection]' === elementType)
2452            || ('undefined' !== typeof jQuery && element instanceof jQuery) //jquery
2453            || ('undefined' !== typeof Elements && element instanceof Elements) //mootools
2454        );
2455
2456        if (isCollectionTyped) {
2457            var i = 0, j = element.length;
2458            for (; i < j; i++) {
2459                attachResizeEvent(element[i], callback);
2460            }
2461        } else {
2462            attachResizeEvent(element, callback);
2463        }
2464
2465        this.detach = function() {
2466            if (isCollectionTyped) {
2467                var i = 0, j = element.length;
2468                for (; i < j; i++) {
2469                    ResizeSensor.detach(element[i]);
2470                }
2471            } else {
2472                ResizeSensor.detach(element);
2473            }
2474        };
2475    };
2476
2477    ResizeSensor.detach = function(element) {
2478        if (element.resizeSensor) {
2479            element.removeChild(element.resizeSensor);
2480            delete element.resizeSensor;
2481            delete element.resizedAttached;
2482        }
2483    };
2484
2485    window.ResizeSensor = ResizeSensor;
2486})();
2487