David Benson 3 лет назад
Родитель
Сommit
014eb4e9ed

+ 39 - 1
etc/build/build.xml

@@ -455,7 +455,45 @@
 		<delete file="${basedir}/.tmp1.js"/>
 	</target>
 
-	<target name="all" depends="app">
+	<target name="atlas" depends="app">
+		<copy file="${basedir}/base-viewer.min.js" tofile="${war.dir}/js/atlas-viewer.min.js" overwrite="true"/>
+		
+		<jscomp compilationLevel="simple" debug="false" forceRecompile="true" output="${basedir}/.tmp0.js">
+			<sources dir="${war.dir}/connect/common/js">
+				<file name="mxReader.js" />
+			</sources>
+		</jscomp>
+
+	    <concat destfile="${war.dir}/js/atlas.min.js" fixlastline="yes" append="no">
+			<file name="${basedir}/base.min.js" />
+			<file name="${war.dir}/js/extensions.min.js" />
+			<file name="${basedir}/.tmp0.js" />
+    	</concat>
+
+		<delete file="${basedir}/.tmp0.js"/>
+	</target>
+
+	<target name="integrate" depends="atlas">
+		<copy file="${war.dir}/js/atlas.min.js" tofile="${war.dir}/js/integrate.min.js" overwrite="true"/>
+
+		<jscomp compilationLevel="simple" debug="false" forceRecompile="true" output="${basedir}/.tmp0.js">
+			<sources dir="${basedir}/../integrate">
+				<file name="Integrate.js" />
+			</sources>
+		</jscomp>
+		
+	    <concat destfile="${war.dir}/js/integrate.min.js" fixlastline="yes" append="yes">
+			<file name="${war.dir}/js/shapes-14-6-5.min.js" />
+    		<file name="${war.dir}/js/stencils.min.js" />
+    		<file name="${basedir}/.tmp0.js" />
+   		</concat>
+
+		<delete file="${basedir}/.tmp0.js"/>
+	</target>
+
+	<target name="all" depends="app, integrate">
+		<delete file="${war.dir}/js/atlas-viewer.min.js"/>
+		<delete file="${war.dir}/js/atlas.min.js"/>
 	</target>
 
 	<!-- ================== Stand-alone war creation ============================= -->

+ 98 - 0
etc/integrate/Integrate.js

@@ -0,0 +1,98 @@
+// Disables eval for JS (uses shapes.min.js)
+mxStencilRegistry.allowEval = false;
+
+// Sets defaults
+Graph.prototype.defaultPageVisible = false;
+Graph.prototype.defaultScrollbars = false;
+EditorUi.prototype.toolbarHeight = 0;
+EditorUi.prototype.footerHeight = 0;
+EditorUi.scratchpadHelpLink = null;
+
+// Enables action states
+EditorUi.prototype.isDiagramActive = function()
+{
+    return true;
+};
+
+// Enables settings
+EditorUi.prototype.isSettingsEnabled = function()
+{
+    return true;
+};
+
+// Enables scratchpad
+EditorUi.prototype.isScratchpadEnabled = function()
+{
+    return true;
+};
+
+// Workaround for tainted canvas is to base64 encode the image on the server-side
+EditorUi.prototype.convertImageToDataUri = function(url, callback)
+{
+	if (/(\.svg)$/i.test(url))
+	{
+		mxUtils.get(url, mxUtils.bind(this, function(req)
+		{
+			callback(Editor.createSvgDataUri(req.getText()));
+		}),
+		function()
+		{
+			callback(this.svgBrokenImage.src);
+		});
+	}
+    else
+    {
+        // Workaround for tainted canvas error
+        if (url.substring(0, PROXY_URL.length) == PROXY_URL)
+        {
+            mxUtils.get(url + '&base64=1', mxUtils.bind(this, function(req)
+            {
+                callback('data:image/png;base64,' + req.getText());
+            }),
+            function()
+            {
+                callback();
+            });
+        }
+        else
+        {
+		    var img = new Image();
+		    var self = this;
+		    
+		    if (this.crossOriginImages)
+	    	{
+			    img.crossOrigin = 'anonymous';
+		    }
+		    
+		    img.onload = function()
+		    {
+		        var canvas = document.createElement('canvas');
+		        var ctx = canvas.getContext('2d');
+		        canvas.height = img.height;
+		        canvas.width = img.width;
+		        ctx.drawImage(img, 0, 0);
+		        
+		        try
+		        {
+	        		callback(canvas.toDataURL());
+		        }
+		        catch (e)
+		        {
+	        		callback(self.svgBrokenImage.src);
+		        }
+		    };
+		    
+		    img.onerror = function()
+		    {
+	    		callback(self.svgBrokenImage.src);
+		    };
+		    
+		    img.src = url;
+        }
+    }
+};
+
+if (typeof(window.mxIntegrateCallback) === 'function')
+{
+	window.mxIntegrateCallback();
+}

+ 827 - 0
src/main/webapp/connect/common/js/mxReader.js

@@ -0,0 +1,827 @@
+/**
+ * A Draw.io diagram viewer component with configurable toolbar buttons for editing, deleting and zooming buttons.
+ * Parameters :
+ * diagramName - name of the diagram
+ * attachmentId - ID of the diagram attachment
+ * ceoId - page ID or issue key
+ * readerOpts - JSON object with options :
+ * 		loadUrl - url from which to load the diagram
+ * 		editUrl - url of the editor
+ * 		stylePath - url from which to load the css
+ * 		stencilPath - url from which to load the stencils
+ * 		imagePath - url from which to load the graph mages
+ * 		resourcePath - path to the translations
+ * 		viewerToolbar - show toolbar or not
+ * 		autoSize - resizes the graph container to match the graph bounds
+ * 		width - width of the container
+ * 		height - height of the container
+ * 		disableButtons - disables all buttons
+ * 		center - should the diagram be horizontally centered or not
+ * 		evaluation - evaluation mode
+ * lightbox - boolean indicating if this viewer is a lightbox
+ * graphDocument - optional document containing the XML data
+ */
+function DrawioViewer(diagramName, attachmentId, ceoId, readerOpts, lightbox, graphDocument, connect)
+{
+	this.id = attachmentId;
+	this.diagramName = diagramName;
+	this.ceoId = ceoId;
+	this.options = readerOpts;
+	this.lightbox = lightbox;
+	this.graphDocument = graphDocument;
+	this.connect = connect;
+	this.paddingBottom = (connect) ? 0 : 30;
+
+	// Overrides browser language with Confluence user language
+	var lang = null;
+	
+	// Language is in the readOpts in Server and in the URL in Connect
+	if (!connect && readerOpts.language != null)
+	{
+		lang = readerOpts.language
+	}
+	else if (connect != null && urlParams['loc'] != null)
+	{
+		lang = urlParams['loc'];
+		var dash = lang.indexOf('-');
+		
+		if (dash >= 0)
+		{
+			lang = lang.substring(0, dash);
+		}
+	}
+	
+	// Only german and english supported currently. English is default.
+	if (lang == 'de')
+	{
+		mxClient.language = 'de';
+	}
+
+	// Special extension used for the message bundle. For this bundle there is only a German
+	// translation and the default English bundle so we temporarily override isLanguageSupported
+	// to return true only if German is used and fallback to English for all other languages.
+	var prevExtension = mxResources.extension;
+	var prevIsLangSupported = mxResources.isLanguageSupported;
+	mxResources.extension = '.txt';
+	
+	mxResources.isLanguageSupported = function(lan)
+	{
+		return lan == 'de';
+	};
+	
+	mxResources.add(this.options.resourcePath);
+	
+	// Restores previous settings
+	mxResources.extension = prevExtension;
+	mxResources.isLanguageSupported = prevIsLangSupported;
+	
+	this.buttons = this.createButtons();
+	
+	this.transparentImage = Editor.prototype.transparentImage;
+	this.extractGraphModel = Editor.prototype.extractGraphModel;
+	this.setGraphXml = Editor.prototype.setGraphXml;
+	this.readGraphState = Editor.prototype.readGraphState;
+	this.resetGraph = Editor.prototype.resetGraph;
+	this.decompress = Editor.prototype.decompress;
+	this.updateGraphComponents = Editor.prototype.updateGraphComponents;
+	this.fireEvent = Editor.prototype.fireEvent;
+	this.addListener = Editor.prototype.addListener;
+	this.originalNoForeignObject = Editor.prototype.originalNoForeignObject;
+	this.gridImage = '';
+	
+	this.addListener('resetGraphView', this.resetGraphView);
+}
+
+DrawioViewer.prototype = new mxEventSource();
+
+DrawioViewer.prototype.graph = null;
+DrawioViewer.prototype.id = null;
+DrawioViewer.prototype.toolbar = null;
+DrawioViewer.prototype.options = null;
+DrawioViewer.prototype.originX = 0;
+DrawioViewer.prototype.originY = 0;
+DrawioViewer.prototype.popupWindow = null;
+DrawioViewer.prototype.buttons = {};
+
+DrawioViewer.prototype.graphXmlString = null;
+
+DrawioViewer.prototype.installToolbar = function()
+{
+	this.toolbar = document.createElement('div');
+	var toolbar = this.toolbar;
+	var container = this.graph.container;
+	toolbar.id = 'diagramly-reader-toolbar-' + this.id;
+	toolbar.className = 'diagramly-reader-toolbar';
+	toolbar.style.position = 'absolute';
+	
+	container.parentNode.appendChild(toolbar);
+
+	toolbar.style.height = '30px';
+	toolbar.style.width = this.countVisibleButtons() * 29 + 'px';
+
+	// Makes sure the toolbar is always visible and
+	// disables toolbar for all overflow content
+	container.parentNode.style.overflow = 'visible';
+	
+	if (this.lightbox)
+	{
+		toolbar.style.bottom = '4px';
+		toolbar.style.left = '50%';
+		toolbar.style.width = this.countVisibleButtons() * 29 + 'px';
+		toolbar.style.marginLeft = -Math.round(this.countVisibleButtons() * 29 / 2) + 'px'
+	}
+	else
+	{
+		container.parentNode.style.paddingBottom = this.paddingBottom + 'px';
+
+		var bs = this.graph.getBorderSizes();
+		toolbar.style.bottom = (container.offsetTop + bs.y + 4) + 'px';
+		toolbar.style.left = (container.offsetLeft + bs.x) + 'px';
+	
+		if (!mxClient.IS_TOUCH) 
+		{
+			toolbar.style.display = 'none';
+			
+			$(container.parentNode).hover(function() 
+			{
+				toolbar.style.bottom = (container.offsetTop + bs.y + 4) + 'px';
+				toolbar.style.left = (container.offsetLeft + bs.x) + 'px';
+				
+				$(toolbar).fadeIn(100);
+			},
+			function() 
+			{
+				$(toolbar).fadeOut(100);
+			});
+		}
+	}
+}
+
+DrawioViewer.prototype.countVisibleButtons = function() 
+{
+	var counter = 0;
+	
+	for(var key in this.buttons) 
+	{
+		var button = this.buttons[key];
+		
+		if (button.visible)
+		{
+			counter++;
+		}
+	}
+	
+	return counter;
+}
+
+DrawioViewer.prototype.init = function()
+{
+	this.loadStylesheet();
+	
+	// Makes the shadow brighter
+	mxConstants.SHADOWCOLOR = '#000000';
+	mxConstants.SHADOW_OPACITY = 0.25;
+	this.graph.setEnabled(false);
+	this.graph.autoScroll = false;
+	this.graph.container.style.overflow = 'hidden';
+	this.graph.container.style.cursor = 'move';
+	
+	// Panning only enabled in lightbox to allow text selection in viewer
+	this.graph.setPanning(true);
+	
+	// Workaround for context trigger starting panning if ignoreCell is true
+	this.graph.panningHandler.useLeftButtonForPanning = true;
+	this.graph.panningHandler.usePopupTrigger = false;
+	this.graph.panningHandler.ignoreCell = true;
+	
+	this.graph.panningHandler.isForcePanningEvent = function(me)
+	{
+		return mxEvent.isLeftMouseButton(me.getEvent());
+	};
+
+	// Folding only enabled in lightbox
+	this.graph.foldingEnabled = this.lightbox;
+	
+	// Overrides click handler to ignore graph enabled state
+	if (this.graph.foldingEnabled)
+	{
+		this.graph.cellRenderer.createControlClickHandler = function(state)
+		{
+			var graph = state.view.graph;
+			
+			return function (evt)
+			{
+				var collapse = !graph.isCellCollapsed(state.cell);
+				graph.foldCells(collapse, false, [state.cell], null, evt);
+				mxEvent.consume(evt);
+			};
+		};
+	}
+	else
+	{
+		// Hides collapse/expand icon if folding is disabled
+		this.graph.getFoldingImage = function()
+		{
+			return null;
+		};
+	};
+
+	// HTML entities are displayed as plain text in wrapped plain text labels
+	this.graph.cellRenderer.getLabelValue = function(state)
+	{
+		var result = mxCellRenderer.prototype.getLabelValue.apply(this, arguments);
+		
+		if (state.view.graph.isHtmlLabel(state.cell))
+		{
+			if (state.style['html'] != 1)
+			{
+				result = mxUtils.htmlEntities(result, false);
+			}
+			else
+			{
+				result = state.view.graph.sanitizeHtml(result);
+			}
+		}
+		
+		return result;
+	};
+
+	// Enables links if graph is "disabled" (ie. read-only)
+	this.graph.click = function(me)
+	{
+		var cell = me.getCell();
+		
+		if (cell != null && !me.isConsumed() && (mxEvent.isTouchEvent(me.getEvent()) ||
+			mxEvent.isLeftMouseButton(me.getEvent())))
+		{
+			var href = this.getLinkForCell(cell);
+			
+			// Test cases:
+			// 1) the relative link without the Conf base path, with a leading slash, e.g. /download/attachment/....
+			// 2) the relative link with the Conf base path, with a leading slash, e.g. /confluence/download/attachmentss/...
+			// 3) the relative link without the conf base path, without a leading slash, e.g. download/attachments/...
+			// 4) the full absolute path, e.g. https://localhost:1990/confluence/download/attachments/...
+			// 5) full path without protocol, e.g. //confluence/download/attachments/...
+
+			if (href != null)
+			{
+				var r = new RegExp('^(?:[a-z]+:)?//', 'i'); // https://stackoverflow.com/questions/10687099/how-to-test-if-a-url-string-is-absolute-or-relative
+				
+				if (!r.test(href))
+				{
+					// relative link
+
+					if (href.lastIndexOf('/', 0) !== 0) // http://stackoverflow.com/a/4579228/226469 seems to be the fastest check
+					{
+						// Need a leading slash in case we need to prepend the base path
+						href = '/' + href;
+					}
+
+					// var cp = AJS.Confluence.getContextPath(); // TODO confluence call in a common module
+					
+					// Originally, links included the base path (but not the host), so there might be cases of the base path
+					// already being prepended. If the base path has changed, we can't recover those cases
+					// window.location.href = href.substring(0, cp.length) === cp ? href : cp + href; // prepends the context path if it's not already there
+					window.location.href = href;
+				}
+				else
+				{
+					// Test if it's an absolute URL, but on the same domain (i.e. open in same window)
+					// There's a security setting (unknown which) that seems to stop the IE hack below working in
+					// IE 11, https://desk.draw.io/browse/DS-175, https://desk.draw.io/browse/DFCS-52
+					// Worst case is these users will open an absolute same domain link in a window
+
+					var link = document.createElement('a');
+					link.href = href;
+					link.href = link.href; // hack to populate 'host' under IE
+				
+					if (link.host === location.host)
+					{
+						window.location.href = href;
+					}
+					else
+					{
+						window.open(href);
+					}
+				}
+			}
+			
+			me.consume();
+		}
+	};
+
+	this.graph.setTooltips(!mxClient.IS_TOUCH);
+
+	if (this.options.width != null) 
+	{
+		this.graph.container.style.width = this.options.width + 'px';
+	}
+	
+	if (this.options.height != null) 
+	{
+		this.graph.container.style.height = this.options.height + 'px';
+	}
+
+	// Accumulates the zoom factor while the rendering is taking place
+	// so that not the complete sequence of zoom steps must be painted
+	var graph = this.graph;
+	graph.updateZoomTimeout = null;
+	graph.cumulativeZoomFactor = 1;
+	
+	graph.lazyZoom = function(zoomIn)
+	{
+		if (this.updateZoomTimeout != null)
+		{
+			window.clearTimeout(this.updateZoomTimeout);
+		}
+
+		if (zoomIn)
+		{
+			this.cumulativeZoomFactor *= this.zoomFactor;
+		}
+		else
+		{
+			this.cumulativeZoomFactor /= this.zoomFactor;
+		}
+		
+		this.cumulativeZoomFactor = Math.round(this.view.scale * this.cumulativeZoomFactor * 100) / 100 / this.view.scale;
+		
+		this.updateZoomTimeout = window.setTimeout(mxUtils.bind(this, function()
+		{
+			this.zoom(this.cumulativeZoomFactor);					
+			this.cumulativeZoomFactor = 1;
+			this.updateZoomTimeout = null;
+		}), 20);
+	};
+	
+	if (this.lightbox)
+	{
+		mxEvent.addMouseWheelListener(mxUtils.bind(this, function(evt, up)
+		{
+			if (!mxClient.IS_MAC || !mxEvent.isControlDown(evt))
+			{
+				var source = mxEvent.getSource(evt);
+				
+				while (source != null)
+				{
+					if (source == graph.container)
+					{
+						graph.lazyZoom(up);
+						mxEvent.consume(evt);
+						
+						return;
+					}
+					
+					source = source.parentNode;
+				}
+			}
+		}));
+	}
+};
+
+DrawioViewer.prototype.resetGraphView = function()
+{
+	this.graph.pageBreaksVisible = false;
+	this.graph.preferPageSize = false;
+	this.graph.pageVisible = false;
+	
+	if (!this.lightbox)
+	{
+		var update = mxUtils.bind(this, function()
+		{
+			this.graph.centerZoom = this.graph.panningHandler.panningEnabled;
+			
+			// If width and height are specified the height is overridden to match the diagram size
+			var autoSizeWidth = this.options.width == null;
+			var autoSizeHeight = this.options.height == null;
+			var bounds = this.graph.getGraphBounds();
+			var ratio = bounds.width / bounds.height;
+			
+			var width = autoSizeWidth ? bounds.width + 2 : this.options.width;
+			var height = autoSizeHeight ? (width / ratio) + 1 : this.options.height;
+	
+			this.graph.container.style.width = Math.ceil(width) + 'px';
+			this.graph.container.style.height = Math.ceil(height) + 'px';
+			this.graph.container.style.maxWidth = '100%';
+			
+			if (autoSizeWidth && autoSizeHeight)
+			{
+				this.translateOrigin();
+				
+				// Used for fast restore of initial position in zoom to fit button
+				this.initialX = this.graph.view.translate.x;
+				this.initialY = this.graph.view.translate.y;
+			}
+			else if (this.options.zoomToFit)
+			{
+				this.graph.fit();
+			}
+	
+			//set the border after calling updateGraphComponnets() because the call sets it to ''
+			this.graph.container.style.border = this.options.border ? '1px solid #DDDDDD' : 'none';
+			this.graph.container.style.backgroundColor = (this.graph.background == null ||
+					this.graph.background == 'none') ? '#ffffff' : this.graph.background;
+		});
+		
+		// Workaround for invisible container is to move the container to the document body for rendering
+		if (!this.connect && (this.graph.container.clientWidth == 0 || this.graph.container.clientHeight == 0))
+		{
+			var previousParent = this.graph.container.parentNode;
+			var nextSibling = this.graph.container.nextSibling;
+			var prevPosition = this.graph.container.style.position;
+			var prevVisible = this.graph.container.style.visible;
+			
+			// Moves to document body for rendering (needed for text measuring)
+			this.graph.container.style.position = 'absolute';
+			this.graph.container.style.visible = 'hidden';
+			
+			document.body.appendChild(this.graph.container);
+			
+			// Refresh required in visible DOM to update text bounding boxes
+			this.graph.refresh();
+			update();
+			
+			// Move it back into DOM tree position
+			if (nextSibling != null)
+			{
+				nextSibling.parentNode.insertBefore(this.graph.container, nextSibling);
+			}
+			else
+			{
+				previousParent.appendChild(this.graph.container);
+			}
+			
+			// Restore position CSS
+			this.graph.container.style.visible = prevVisible;
+			this.graph.container.style.position = prevPosition;
+		}
+		else
+		{
+			update();
+		}
+	}
+	else
+	{
+		this.graph.container.style.backgroundColor = (this.graph.background == null ||
+			this.graph.background == 'none') ? '#ffffff' : this.graph.background;
+	}
+};
+
+DrawioViewer.prototype.translateOrigin = function()
+{
+	var bounds = this.graph.getGraphBounds();
+	this.graph.view.setTranslate(this.originX - Math.floor(bounds.x), this.originY - Math.floor(bounds.y));
+};
+
+DrawioViewer.prototype.loadGraph = function(diagramName, ceoId)
+{
+	var spinner = this.createSpinner(this.graph.container);
+
+	try
+	{
+		mxUtils.get(this.options.loadUrl, mxUtils.bind(this, function(req)
+		{
+			spinner.stop();
+			
+			if (req.getStatus() < 200 || req.getStatus() > 299)
+			{
+				this.showWarning(mxResources.get('error') + ' ' + req.getStatus());
+				this.graph.container.style.border = this.options.border ? '1px solid #DDDDDD' : 'none';
+				this.graph.container.style.backgroundColor = '#ffffff';
+				this.graph.container.style.height = '20px';
+			}
+			else
+			{
+				var json = JSON.parse(req.getText());
+				this.graphXmlString = json.xml;
+				var doc = mxUtils.parseXml(json.xml);
+				this.xmlDoc = doc;
+				this.filename = json.filename;
+				this.setGraphXml(doc.documentElement);
+				this.graphDocument = doc;
+			}
+		}),
+		function()
+		{
+			spinner.stop();
+		});
+	}
+	catch (e)
+	{
+		spinner.stop();
+	}
+};
+
+DrawioViewer.prototype.loadStylesheet = function()
+{
+	var node = mxUtils.load(this.options.stylePath + '/default.xml').getDocumentElement();
+	var dec = new mxCodec(node.ownerDocument);
+	dec.decode(node, this.graph.getStylesheet());
+};
+
+DrawioViewer.prototype.renderButtons = function()
+{
+	for (var key in this.buttons) 
+	{
+		var button = this.buttons[key];
+		
+		if (button.visible) 
+		{
+			this.addToolbarButton(this.toolbar, button);
+		}
+	}
+};
+
+DrawioViewer.prototype.addToolbarButton = function(toolbar, drawioButton)
+{
+	var enabled = typeof drawioButton.enabled === 'undefined' ? true : enabled;
+	var button = drawioButton.linkButton ? document.createElement('a') : document.createElement('div');
+	button.className = 'diagramly-reader-toolbar-button';
+
+	if (drawioButton.icon != null)
+	{
+		var img = document.createElement('img');
+		img.setAttribute('src', drawioButton.icon);
+		img.style.verticalAlign = 'middle';
+		img.style.marginRight = '2px';
+		button.appendChild(img);
+		button.title = drawioButton.label;
+	}
+
+	if (!drawioButton.enabled)
+	{
+		button.style.opacity = 0.2;
+
+	} else
+	{
+		if(drawioButton.linkButton) 
+		{
+			button.href = drawioButton.url;
+		}
+		else 
+		{
+			mxEvent.addListener(button, 'click', function(evt)
+			{
+				drawioButton.clickHandler.apply(this, arguments);
+			});
+		}
+		mxEvent.addListener(button, 'mouseover', function(evt)
+		{
+			button.className += ' diagramly-reader-toolbar-button-hover';
+		});
+		mxEvent.addListener(button, 'mouseout', function(evt)
+		{
+			button.className = 'diagramly-reader-toolbar-button';
+		});
+	}
+
+	toolbar.appendChild(button);
+	return button;
+};
+
+DrawioViewer.prototype.createSpinner = function(container)
+{
+	var opts =
+	{
+		lines : 12, // The number of lines to draw
+		length : 12, // The length of each line
+		width : 5, // The line thickness
+		radius : 10, // The radius of the inner circle
+		rotate : 0, // The rotation offset
+		color : '#000', // #rgb or #rrggbb
+		speed : 1, // Rounds per second
+		trail : 60, // Afterglow percentage
+		shadow : false, // Whether to render a shadow
+		hwaccel : false, // Whether to use hardware acceleration
+		className : 'spinner', // The CSS class to assign to the spinner
+		zIndex : 2e9 // The z-index (defaults to 2000000000)
+	};
+
+	return new Spinner(opts).spin(container);
+};
+
+DrawioViewer.prototype.show = function(container) 
+{
+	this.graph = new Graph(container);
+	this.graph.id = this.id;
+	this.init();
+
+	// Uses the XML document that was loaded for the viewer in the lightbox
+	if (this.graphDocument != null)
+	{
+		this.setGraphXml(this.graphDocument.documentElement);
+	}
+	else
+	{
+		this.loadGraph(this.diagramName, this.ceoId);
+	}
+	
+	if (this.options.viewerToolbar) 
+	{
+		this.installToolbar();
+		this.renderButtons();
+	}
+	
+	if (this.options.licenseStatus == 'NO_LICENSE') 
+	{
+		this.showWarning(mxResources.get('drawio.reader.noLicense'));
+	} 
+	else if (this.options.licenseStatus == 'EVAL_LICENSE') 
+	{
+		this.showWarning(mxResources.get('drawio.reader.evaluation'));
+	} 
+	else if (this.options.licenseStatus == 'EVAL_EXPIRED') 
+	{
+		this.showWarning(mxResources.get('drawio.reader.evaluationExpired'));
+	} 
+	else if (this.options.licenseStatus == 'VERSION_MISMATCH') 
+	{
+		this.showWarning(mxResources.get('drawio.reader.versionMismatch', ['https://support.draw.io/pages/viewpage.action?pageId=11829320']));
+	}
+	else if (this.options.licenseStatus == 'USER_MISMATCH') 
+	{
+		this.showWarning(mxResources.get('drawio.reader.userMismatch', ['https://support.draw.io/pages/viewpage.action?pageId=11829323']));
+	}
+	
+	
+};
+
+DrawioViewer.prototype.showWarning = function(msg) 
+{
+	var div = document.createElement('div');
+	div.style.position = 'absolute';
+	div.style.overflow = 'hidden';
+	div.style.left = '0px';
+	div.style.top = '0px';
+	div.style.right = '0px';
+	div.style.fontSize = '12px';
+	div.style.margin = '2px';
+	mxUtils.setOpacity(div, 50);
+	div.style.color = 'gray';
+	div.style.textAlign = 'center';
+	div.style.whiteSpace = 'nowrap';
+	span = document.createElement('span');
+	span.innerHTML = msg;
+	
+	div.appendChild(span);
+	
+	this.graph.container.parentNode.appendChild(div);
+};
+
+DrawioViewer.prototype.showLightbox = function() 
+{
+	console.log('Lightbox feature not implemented.');
+};
+
+DrawioViewer.prototype.createButtons = function() 
+{
+	var viewer = this;
+	var buttons = {}; 
+	
+	var enableButton = typeof this.options.disableButtons === 'undefined' ? true : !this.options.disableButtons;
+	var canEdit = this.options.userCanEdit && enableButton;
+	var canRemove = this.options.userCanRemove && enableButton;
+
+	var autoSizeWidth = this.options.width == null;
+	var autoSizeHeight = this.options.height == null;
+	
+	buttons[DrawioViewerActions.EDIT] = new DrawioViewerButton(
+	{
+		label : mxResources.get('diagramly.reader.edit'),
+		icon : viewer.options.imagePath + '/edit.png', 
+		url : viewer.options.editUrl,
+		enabled : canEdit,
+		linkButton : true
+	});
+	
+	buttons[DrawioViewerActions.REMOVE] = new DrawioViewerButton(
+	{
+		label : mxResources.get('diagramly.reader.remove'),
+		icon : viewer.options.imagePath + '/remove.png', 
+		clickHandler : function()
+		{
+			if (confirm(mxResources.get('diagramly.reader.confirmDelete')))
+			{
+				window.location.href = viewer.options.removeUrl;
+			}
+		},
+		enabled : canRemove
+	});
+	
+	buttons[DrawioViewerActions.ACTUAL_SIZE] = new DrawioViewerButton(
+	{
+		label : mxResources.get('diagramly.reader.zoomActual'),
+		icon : viewer.options.imagePath + '/zoomActual.png', 
+		clickHandler : function()
+		{
+			viewer.graph.zoomActual();
+			viewer.translateOrigin();
+		},
+		enabled : enableButton
+	});
+	
+	buttons[DrawioViewerActions.ZOOM_TO_FIT] = new DrawioViewerButton(
+	{
+		label : (this.lightbox) ? mxResources.get('diagramly.reader.zoomActual') : mxResources.get('diagramly.reader.fit'),
+		icon : (this.lightbox) ? viewer.options.imagePath + '/zoomActual.png' : viewer.options.imagePath + '/zoomFit.gif', 
+		clickHandler : mxUtils.bind(this, function()
+		{
+			if (this.lightbox)
+			{
+				// NOTE: Maxscale is 1 here so only make smaller but not larger
+				viewer.graph.fit(8);
+				viewer.graph.center(true, true, null, 0.42);
+			}
+			else
+			{
+				if (autoSizeWidth && autoSizeHeight)
+				{
+					this.graph.view.scaleAndTranslate(1, this.initialX, this.initialY);
+				}
+				else
+				{
+					this.graph.fit();
+				}
+			}
+		}),
+		enabled : enableButton
+	});
+	
+	buttons[DrawioViewerActions.ZOOM_OUT] = new DrawioViewerButton(
+	{
+		label : mxResources.get('diagramly.reader.zoomOut'),
+		icon : viewer.options.imagePath + '/zoomOut.gif', 
+		clickHandler : function()
+		{
+			viewer.graph.zoomOut();
+		},
+		enabled : enableButton
+	});
+	
+	buttons[DrawioViewerActions.ZOOM_IN] = new DrawioViewerButton(
+	{
+		label : mxResources.get('diagramly.reader.zoomIn'),
+		icon : viewer.options.imagePath + '/zoomIn.gif', 
+		clickHandler : function()
+		{
+			viewer.graph.zoomIn();
+		},
+		enabled : enableButton
+	});
+	
+	buttons[DrawioViewerActions.EXPAND] = new DrawioViewerButton(
+	{
+		label : mxResources.get('diagramly.reader.fullScreen'),
+		icon : viewer.options.imagePath + '/largeView.png', 
+		clickHandler : mxUtils.bind(this, function()
+		{
+			this.showLightbox();
+		}),
+		enabled : enableButton
+	});
+	
+	buttons[DrawioViewerActions.CLOSE] = new DrawioViewerButton(
+	{
+		label : mxResources.get('diagramly.reader.closeFullScreen'),
+		icon : viewer.options.imagePath + '/closeLargeView.gif', 
+		clickHandler : function()
+		{
+			viewer.popupWindow.remove();
+		},
+		enabled : enableButton,
+		visible : false
+	});
+	
+	return buttons;
+};
+
+DrawioViewerActions = 
+{
+	EDIT : 'edit',
+	REMOVE : 'remove',
+	ACTUAL_SIZE : 'actualSize',
+	ZOOM_TO_FIT : 'zoomToFit',
+	ZOOM_OUT : 'zoomOut',
+	ZOOM_IN : 'zoomIn',
+	EXPAND : 'expand',
+	CLOSE : 'close'
+};
+
+function DrawioViewerButton(options) 
+{
+	this.label = options.label;
+	this.clickHandler = options.clickHandler;
+	this.enabled = typeof options.enabled != 'undefined' ? options.enabled : true;
+	this.icon = options.icon;
+	this.visible = typeof options.visible != 'undefined' ? options.visible : true;
+	this.linkButton = typeof options.linkButton != 'undefined' ? options.linkButton : false;
+	this.url = options.url;
+};
+
+DrawioViewerButton.prototype.label = null;
+DrawioViewerButton.prototype.clickHandler = null;
+DrawioViewerButton.prototype.enabled = true;
+DrawioViewerButton.prototype.icon = null;
+DrawioViewerButton.prototype.visible = true;
+DrawioViewerButton.prototype.linkButton = false;
+DrawioViewerButton.prototype.url = null;

+ 7 - 5
src/main/webapp/js/diagramly/Devel.js

@@ -53,7 +53,7 @@ if (!mxIsElectron && location.protocol !== 'http:')
 			'\'unsafe-hashes\'; '; // Required for hashes for style attribute
 		
 		var directives = 'connect-src %connect-src% \'self\' https://*.draw.io https://*.diagrams.net ' +
-			'https://*.googleapis.com wss://app.diagrams.net/rt wss://*.pusher.com https://*.pusher.com ' +
+			'https://*.googleapis.com wss://app.diagrams.net wss://*.pusher.com https://*.pusher.com ' +
 			'https://api.github.com https://raw.githubusercontent.com https://gitlab.com ' +
 			'https://graph.microsoft.com https://*.sharepoint.com  https://*.1drv.com https://api.onedrive.com ' +
 			'https://dl.dropboxusercontent.com ' +
@@ -95,7 +95,7 @@ if (!mxIsElectron && location.protocol !== 'http:')
 
 			var se_diagrams_net = hashes.replace(/%script-src%/g, '') +
 				'connect-src \'self\' https://*.diagrams.net ' +
-				'https://*.googleapis.com wss://*.pusher.com https://*.pusher.com ' +
+				'https://*.googleapis.com wss://app.diagrams.net wss://*.pusher.com https://*.pusher.com ' +
 				'https://*.google.com https://fonts.gstatic.com https://fonts.googleapis.com; ' +
 				'img-src * data: blob:; media-src * data:; font-src * about:; ' +
 				'frame-src \'self\' https://viewer.diagrams.net https://*.google.com; ' +
@@ -110,14 +110,16 @@ if (!mxIsElectron && location.protocol !== 'http:')
 					replace(/%frame-src%/g, 'https://www.lucidchart.com https://app.lucidchart.com https://lucid.app blob:').
 					replace(/%style-src%/g, 'https://aui-cdn.atlassian.com https://*.atlassian.net').
 					replace(/%connect-src%/g, '').
-					replace(/  /g, ' ');
+					replace(/  /g, ' ') +
+					'worker-src https://ac.draw.io/service-worker.js;';
 			console.log('ac.draw.io:', ac_draw_io);
 
 			var aj_draw_io = csp.replace(/%script-src%/g, 'https://connect-cdn.atl-paas.net').
 					replace(/%frame-src%/g, 'blob:').
 					replace(/%style-src%/g, 'https://aui-cdn.atlassian.com https://*.atlassian.net').
 					replace(/%connect-src%/g, 'https://api.atlassian.com https://api.media.atlassian.com').
-					replace(/  /g, ' ');
+					replace(/  /g, ' ') +
+					'worker-src https://aj.draw.io/service-worker.js;';
 			console.log('aj.draw.io:', aj_draw_io);
 
 			console.log('import.diagrams.net:', 'default-src \'self\'; worker-src blob:; img-src \'self\' blob: data: https://www.lucidchart.com ' +
@@ -135,7 +137,7 @@ if (!mxIsElectron && location.protocol !== 'http:')
 					"Access-Control-Allow-Origin": "https://se.diagrams.net"
 				},
 				teams: {
-					"Content-Security-Policy" : app_diagrams_net.replace(/ 'sha256-[^']+'/g, ''),
+					"Content-Security-Policy" : app_diagrams_net.replace(/ 'sha256-[^']+'/g, '') + 'worker-src https://app.diagrams.net/service-worker.js;',
 					"Permissions-Policy" : "microphone=()"
 				},
 				jira: {

+ 2 - 1
src/main/webapp/js/diagramly/EditorUi.js

@@ -2014,7 +2014,8 @@
 					{
 						var prev = this.editor.graph.pageVisible;
 						
-						if (pageVisible != null)
+						//Only override if page is actually visible
+						if (pageVisible == false)
 						{
 							this.editor.graph.pageVisible = pageVisible;
 						}