浏览代码

8.9.0 release

Gaudenz Alder 7 年之前
父节点
当前提交
ffd9609dc2

+ 8 - 0
ChangeLog

@@ -1,3 +1,11 @@
+04-JUL-2018: 8.9.0
+
+- Faster preview in Chrome, Firefox and Edge (beta)
+- Adds responsive menubar in minimal UI
+- Add tickets and webcola plugins
+- Adds #C configuration switch
+- Uses mxGraph 3.9.8 beta 1
+
 26-JUN-2018: 8.8.7
 
 - Fixes custom content view in Confluence Cloud

+ 1 - 1
VERSION

@@ -1 +1 @@
-8.8.7
+8.9.0

文件差异内容过多而无法显示
+ 9 - 9
etc/mxgraph/mxClient.js


+ 0 - 1
src/main/java/com/mxgraph/online/SaveServlet.java

@@ -4,7 +4,6 @@ import java.io.IOException;
 import java.io.OutputStream;
 import java.io.UnsupportedEncodingException;
 import java.net.URLDecoder;
-import java.net.URLEncoder;
 import java.util.logging.Logger;
 
 import javax.servlet.ServletException;

+ 2 - 1
src/main/webapp/cache.manifest

@@ -1,7 +1,7 @@
 CACHE MANIFEST
 
 # THIS FILE WAS GENERATED. DO NOT MODIFY!
-# 06/26/2018 04:33 PM
+# 07/04/2018 12:24 PM
 
 app.html
 index.html?offline=1
@@ -32,6 +32,7 @@ images/delete.png
 images/droptarget.png
 images/help.png
 images/download.png
+images/drawlogo-gray.svg
 images/drawlogo-text-bottom.svg
 images/logo-flat.png
 images/osa_drive-harddisk.png

+ 1 - 1
src/main/webapp/index.html

@@ -20,7 +20,7 @@
 		/**
 		 * URL Parameters and protocol description are here:
 		 *
-		 * https://support.draw.io/pages/viewpage.action?pageId=12878136
+		 * https://desk.draw.io/support/solutions/articles/16000042546-what-url-parameters-are-supported
 		 *
 		 * Parameters for developers:
 		 *

文件差异内容过多而无法显示
+ 1200 - 1188
src/main/webapp/js/app.min.js


文件差异内容过多而无法显示
+ 1103 - 1089
src/main/webapp/js/atlas-viewer.min.js


文件差异内容过多而无法显示
+ 1272 - 1260
src/main/webapp/js/atlas.min.js


+ 32 - 3
src/main/webapp/js/diagramly/App.js

@@ -12,7 +12,9 @@
  */
 App = function(editor, container, lightbox)
 {
-	EditorUi.call(this, editor, container, (lightbox != null) ? lightbox : (urlParams['lightbox'] == '1' || uiTheme == 'min'));
+	EditorUi.call(this, editor, container, (lightbox != null) ? lightbox :
+		(urlParams['lightbox'] == '1' || (uiTheme == 'min' &&
+		urlParams['chrome'] != '0')));
 	
 	// Pre-fetches images
 	if (mxClient.IS_SVG)
@@ -199,7 +201,8 @@ App.pluginRegistry = {'4xAKTrabTpTzahoLthkwPNUn': '/plugins/explore.js',
 	'anim': '/plugins/animation.js', 'update': '/plugins/update.js',
 	'trees': '/plugins/trees/trees.js', 'import': '/plugins/import.js',
 	'replay': '/plugins/replay.js', 'anon': '/plugins/anonymize.js',
-	'tr': '/plugins/trello.js', 'f5': '/plugins/rackF5.js'};
+	'tr': '/plugins/trello.js', 'f5': '/plugins/rackF5.js',
+	'tickets': '/plugins/tickets.js', 'webcola': '/plugins/webcola/webcola.js'};
 
 /**
  * Function: authorize
@@ -434,6 +437,30 @@ App.main = function(callback, createUi)
 			}
 		};
 	}
+	
+	// Adds configuration
+	if (window.location.hash != null && window.location.hash.substring(0, 2) == '#C')
+	{
+		try
+		{
+			var config = JSON.parse(decodeURIComponent(
+					window.location.hash.substring(2)));
+			Editor.configure(config, true);
+			
+			if (config.open != null)
+			{
+				window.location.hash = config.open;
+			}
+			else
+			{
+				window.location.hash = '';
+			}
+		}
+		catch (e)
+		{
+			console.log(e);
+		}
+	}
 
 	if (window.mxscript != null)
 	{
@@ -568,7 +595,9 @@ App.main = function(callback, createUi)
 			}
 	
 			// Main
-			var ui = (createUi != null) ? createUi() : new App(new Editor(urlParams['chrome'] == '0' || uiTheme == 'min', null, null, null, urlParams['chrome'] != '0'));
+			var ui = (createUi != null) ? createUi() : new App(new Editor(
+					urlParams['chrome'] == '0' || uiTheme == 'min',
+					null, null, null, urlParams['chrome'] != '0'));
 			
 			if (window.mxscript != null)
 			{

+ 41 - 8
src/main/webapp/js/diagramly/Editor.js

@@ -164,6 +164,18 @@
 	 */
 	Editor.shadowOptionEnabled = true;
 
+	/**
+	 * Reference to the config object passed to <configure>.
+	 */
+	Editor.config = null;
+
+	/**
+	 * Reference to the version of the last config object in
+	 * <configure>. If this is different to the last version in
+	 * mxSettings.parse, then the settings are reset.
+	 */
+	Editor.configVersion = null;
+
 	/**
 	 * Global configuration of the Editor
 	 * see https://desk.draw.io/solution/articles/16000058316
@@ -171,10 +183,11 @@
 	 * For defaultVertexStyle, defaultEdgeStyle and defaultLibraries, this must be called before
 	 * mxSettings.load via global config variable window.mxLoadSettings = false.
 	 */
-	Editor.configure = function(config)
+	Editor.configure = function(config, untrusted)
 	{
 		if (config != null)
 		{
+			Editor.config = config;
 			Editor.configVersion = config.version;
 			Menus.prototype.defaultFonts = config.defaultFonts || Menus.prototype.defaultFonts;
 			ColorDialog.prototype.presetColors = config.presetColors || ColorDialog.prototype.presetColors;
@@ -260,7 +273,7 @@
 			  	Editor.prototype.fontCss = config.fontCss;
 			}
 			
-			if (config.plugins != null)
+			if (config.plugins != null && !untrusted)
 			{
 				// Required for callback
 				App.initPluginCallback();
@@ -367,7 +380,8 @@
 					this.graph.setBackgroundImage(null);
 				}
 				
-				mxClient.NO_FO = (this.graph.mathEnabled) ? true : this.originalNoForeignObject;
+				mxClient.NO_FO = ((this.graph.mathEnabled && urlParams['math-fo'] != '1') &&
+					!this.graph.useCssTransforms) ? true : this.originalNoForeignObject;
 				this.graph.setShadowVisible(node.getAttribute('shadow') == '1', false);
 			}
 	
@@ -567,6 +581,12 @@
 	
 	/**
 	 * Overrides reset graph.
+	 * 
+	 * math-fo=1 enables foreignObjects for MathJax.
+	 * 
+	 * FIXME:
+	 * - Editor shows no math without page view
+	 * - Lightbox zooming breaks math
 	 */
 	var editorResetGraph = Editor.prototype.resetGraph;	
 	Editor.prototype.resetGraph = function()
@@ -574,20 +594,22 @@
 		this.graph.mathEnabled = (urlParams['math'] == '1');
 		this.graph.view.x0 = null;
 		this.graph.view.y0 = null;
-		mxClient.NO_FO = (this.graph.mathEnabled) ? true : this.originalNoForeignObject;
+		mxClient.NO_FO = ((this.graph.mathEnabled && urlParams['math-fo'] != '1') &&
+			!this.graph.useCssTransforms) ? true :
+			this.originalNoForeignObject;
 		editorResetGraph.apply(this, arguments);
 	};
 
 	/**
 	 * Math support.
 	 */
-	Editor.prototype.originalNoForeignObject = mxClient.NO_FO;
-
 	var editorUpdateGraphComponents = Editor.prototype.updateGraphComponents;
 	Editor.prototype.updateGraphComponents = function()
 	{
 		editorUpdateGraphComponents.apply(this, arguments);
-		mxClient.NO_FO = (this.graph.mathEnabled && Editor.MathJaxRender != null) ? true : this.originalNoForeignObject;
+		mxClient.NO_FO = ((this.graph.mathEnabled && urlParams['math-fo'] != '1') &&
+			!this.graph.useCssTransforms && Editor.MathJaxRender != null) ? true :
+			this.originalNoForeignObject;
 	};
 		
 	/**
@@ -1401,6 +1423,17 @@
 		}
 	};
 
+	/**
+	 * Safari has problems with math typesetting inside foreignObjects.
+	 */
+	var graphIsCssTransformsSupported = Graph.prototype.isCssTransformsSupported;
+	
+	Graph.prototype.isCssTransformsSupported = function()
+	{
+		// FIXME: Safari only disabled due to mathjax rendering errors
+		return graphIsCssTransformsSupported.apply(this, arguments) && !mxClient.IS_SF;
+	};
+
 	/**
 	 * Sets default style (used in editor.get/setGraphXml below)
 	 */
@@ -2176,7 +2209,7 @@
 		
 		// Buttons
 		var buttons = document.createElement('div');
-		buttons.style.cssText = 'text-align:right;margin:62px 0 0 0;';
+		buttons.style.cssText = 'text-align:right;margin:48px 0 0 0;';
 		
 		// Overall scale for print-out to account for print borders in dialogs etc
 		function preview(print)

+ 26 - 55
src/main/webapp/js/diagramly/EditorUi.js

@@ -217,7 +217,11 @@
 	EditorUi.prototype.setLocalData = function(key, data, fn)
 	{
 		localStorage.setItem(key, data);
-		fn();
+		
+		if (fn != null)
+		{
+			fn();
+		}
 	};
 	
 	/**
@@ -1671,7 +1675,8 @@
 				this.fname.innerHTML = '';
 				this.fname.setAttribute('title', mxResources.get('rename'));
 			}
-	
+
+			this.editor.setStatus('');
 			this.updateUi();
 			this.showSplash();
 		});
@@ -3017,7 +3022,7 @@
 	 */
 
 	/**
-	 * 
+	 * See fixme in convertMath for client-side image generation with math.
 	 */
 	EditorUi.prototype.isExportToCanvas = function()
 	{
@@ -3433,6 +3438,7 @@
 			   	   	    img.style.maxWidth = '140px';
 			   	   	    img.style.maxHeight = '140px';
 			   	   	    img.style.cursor = 'pointer';
+			   	   	    img.style.backgroundColor = 'white';
 			   	   	    
 			   	   	    img.setAttribute('title', mxResources.get('openInNewWindow'));
 			   	   	    img.setAttribute('border', '0');
@@ -4890,64 +4896,29 @@
 	 */
 	EditorUi.prototype.convertMath = function(graph, svgRoot, fixPosition, callback)
 	{
-		// FIXME: Only horizontal dash in output so better no conversion at all
-		/*if (false && graph.mathEnabled && typeof(MathJax) !== 'undefined' && typeof(MathJax.Hub) !== 'undefined')
+		if (graph.mathEnabled && typeof(MathJax) !== 'undefined' && typeof(MathJax.Hub) !== 'undefined')
 		{
-			// Workaround for lost gradients in Chrome after remove from DOM
-			var elts = svgRoot.getElementsByTagName('*');
-			
-			for (var i = 0; i < elts.length; i++)
-			{
-				if (elts[i].getAttribute('id') != null)
-				{
-					elts[i].setAttribute('id', 'mxTemporaryPrefix-' + elts[i].getAttribute('id'));
-				}
-			}
-
-			// Temporarily attaches to DOM for rendering
-			svgRoot.style.visibility = 'hidden';
-			document.body.appendChild(svgRoot);
+	      	// Temporarily attaches to DOM for rendering
+			// FIXME: If adding svgRoot to body, the text
+			// value of the math is appended, if not
+			// added to DOM then LaTeX does not work.
+			// This must be fixed to enable client-side export
+			// if math is enabled.
+//			document.body.appendChild(svgRoot);
 			Editor.MathJaxRender(svgRoot);
-			
-			MathJax.Hub.Queue(mxUtils.bind(this, function ()
+	      
+			window.setTimeout(mxUtils.bind(this, function()
 			{
-				// Removes from DOM
-				svgRoot.parentNode.removeChild(svgRoot);
-				svgRoot.style.visibility = '';
-				
-				// Restores original IDs
-				for (var i = 0; i < elts.length; i++)
-				{
-					if (elts[i].getAttribute('id') != null)
-					{
-						elts[i].setAttribute('id', elts[i].getAttribute('id').substring('mxTemporaryPrefix-'.length));
-					}
-				}
-				
-				// Keeping scale but moving translate only works for image export which
-				// is fine since we do not want the SVG export to contain a workaround.
-				// See https://github.com/mathjax/MathJax/issues/279
-				if (fixPosition && navigator.userAgent.indexOf('AppleWebKit/') >= 0)
+				MathJax.Hub.Queue(mxUtils.bind(this, function ()
 				{
-					var fo = svgRoot.getElementsByTagName('foreignObject');
-					
-					for (var i = 0; i < fo.length; i++)
-					{
-						var tr = fo[i].parentNode.parentNode.getAttribute('transform');
-						var translate  = /translate\(\s*([^\s,)]+)[ ,]([^\s,)]+)/.exec(tr);
+					// Removes from DOM
+//					svgRoot.parentNode.removeChild(svgRoot);
 					
-						fo[i].setAttribute('x', Math.round(translate[1]));
-						fo[i].setAttribute('y', Math.round(translate[2]));
-						
-						// Must use translate for crisp rendering
-						fo[i].parentNode.parentNode.setAttribute('transform', 'translate(0.5,0.5)' + tr.substring(tr.indexOf(')') + 1));
-					}
-				}
-				
-				callback();
-			}));
+					callback();
+				}));
+			}), 0);
 		}
-		else*/
+		else
 		{
 			callback();
 		}

+ 15 - 1
src/main/webapp/js/diagramly/GraphViewer.js

@@ -100,7 +100,20 @@ GraphViewer.prototype.init = function(container, xmlNode, graphConfig)
 			{
 				this.graph = new Graph(container);
 				this.graph.transparentBackground = false;
+				this.graph.useCssTransforms = this.graph.isCssTransformsSupported();
 				
+			    // Required for Math workaround in Graph.updateCssTransform
+			    if (mxClient.IS_SVG && this.graph.useCssTransforms &&
+			    	this.graph.view.getDrawPane() != null)
+			    {
+			        var root = this.graph.view.getDrawPane().ownerSVGElement;
+			        
+			        if (root != null)
+			        {
+			            root.style.position = 'absolute';
+			        }
+			    }
+			    
 				if (this.graphConfig.move)
 				{
 					this.graph.isMoveCellsEvent = function(evt)
@@ -948,7 +961,7 @@ GraphViewer.prototype.addToolbar = function()
 			if (this.zoomEnabled)
 			{
 				addButton(mxUtils.bind(this, function()
-				{ 
+				{
 					this.graph.zoomOut();
 				}), Editor.zoomOutImage, mxResources.get('zoomOut') || 'Zoom Out');
 
@@ -1336,6 +1349,7 @@ GraphViewer.prototype.showLocalLightbox = function()
 	if (document.documentMode == null || document.documentMode >= 10)
 	{
 		Editor.prototype.editButtonLink = this.graphConfig.edit;
+		Editor.prototype.editButtonFunc = this.graphConfig.editFunc;
 	}
 	
 	EditorUi.prototype.updateActionStates = function() {};

文件差异内容过多而无法显示
+ 163 - 104
src/main/webapp/js/diagramly/Minimal.js


+ 3 - 1
src/main/webapp/js/diagramly/RealtimeMapping.js

@@ -210,7 +210,9 @@ RealtimeMapping.prototype.initGraph = function()
 	if (this.isActive())
 	{
 		this.activate(true);
-		mxClient.NO_FO = (this.graph.mathEnabled) ? true : Editor.prototype.originalNoForeignObject;
+		mxClient.NO_FO = (this.graph.mathEnabled &&
+			!this.graph.useCssTransforms) ? true :
+			Editor.prototype.originalNoForeignObject;
 
 		// TODO: Fixes math offset - why?
 		this.ui.editor.graph.sizeDidChange();

+ 2 - 2
src/main/webapp/js/diagramly/sidebar/Sidebar-Atlassian.js

@@ -13,9 +13,9 @@
 		var fns = [
 			this.addEntry(dt + 'issue', function()
 	   		{
-			   	var bg = new mxCell('Task description', new mxGeometry(0, 0, 200, 50), s + 'issue;issueType=story;issuePriority=blocker;issueStatus=inProgress;verticalAlign=bottom;align=left;whiteSpace=wrap;overflow=hidden;spacingTop=25;strokeColor=#A8ADB0;fillColor=#EEEEEE;fontSize=12;perimeter=rectanglePerimeter;');
+			   	var bg = new mxCell('Task description', new mxGeometry(0, 0, 200, 50), s + 'issue;issueType=story;issuePriority=blocker;issueStatus=inProgress;verticalAlign=bottom;align=left;whiteSpace=wrap;overflow=hidden;spacingTop=25;strokeColor=#A8ADB0;fillColor=#EEEEEE;fontSize=12;backgroundOutline=1;');
 			   	bg.vertex = true;
-			   	var label1 = new mxCell('ID', new mxGeometry(0, 0, 60, 20), 'strokeColor=none;fillColor=none;part=1;resizable=0;align=left;autosize=1;points=[];deletable=0;');
+			   	var label1 = new mxCell('ID', new mxGeometry(0, 0, 60, 20), 'strokeColor=none;fillColor=none;part=1;resizable=0;align=left;autosize=1;points=[];deletable=0;connectable=0;');
 			   	label1.geometry.relative = true;
 			   	label1.geometry.offset = new mxPoint(20, 0);
 			   	label1.vertex = true;

文件差异内容过多而无法显示
+ 170 - 162
src/main/webapp/js/embed-static.min.js


+ 7 - 2
src/main/webapp/js/mxgraph/Editor.js

@@ -384,6 +384,9 @@ Editor.prototype.resetGraph = function()
 	this.graph.background = this.graph.defaultGraphBackground;
 	this.graph.pageScale = mxGraph.prototype.pageScale;
 	this.graph.pageFormat = mxGraph.prototype.pageFormat;
+	this.graph.currentScale = 1;
+	this.graph.currentTranslate.x = 0;
+	this.graph.currentTranslate.y = 0;
 	this.updateGraphComponents();
 	this.graph.view.setScale(1);
 };
@@ -739,10 +742,12 @@ function Dialog(editorUi, elt, w, h, modal, closable, onClose, noScroll)
 	var w0 = w;
 	var h0 = h;
 	
-	var dh = Math.max(document.body.clientHeight, document.documentElement.clientHeight);
+	// clientHeight check is attempted fix for print dialog offset in viewer lightbox
+	var dh = (document.documentElement.clientHeight > 0) ? document.documentElement.clientHeight :
+		Math.max(document.body.clientHeight || 0, document.documentElement.clientHeight);
 	var left = Math.max(1, Math.round((document.body.clientWidth - w - 64) / 2));
 	var top = Math.max(1, Math.round((dh - h - editorUi.footerHeight) / 3));
-
+	
 	// Keeps window size inside available space
 	if (!mxClient.IS_QUIRKS)
 	{

+ 15 - 2
src/main/webapp/js/mxgraph/EditorUi.js

@@ -14,7 +14,16 @@ EditorUi = function(editor, container, lightbox)
 	
 	var graph = this.editor.graph;
 	graph.lightbox = lightbox;
+	graph.useCssTransforms =
+		this.editor.isChromelessView() &&
+		graph.isCssTransformsSupported();
 
+	// Faster scrollwheel zoom is possible with CSS transforms
+	if (graph.useCssTransforms)
+	{
+		this.lazyZoomDelay = 0;
+	}
+	
 	// Pre-fetches submenu image or replaces with embedded image if supported
 	if (mxClient.IS_SVG)
 	{
@@ -1713,11 +1722,15 @@ EditorUi.prototype.initCanvas = function()
 	
 			this.addChromelessToolbarItems(addButton);
 	
-			if (this.editor.editButtonLink != null)
+			if (this.editor.editButtonLink != null || this.editor.editButtonFunc != null)
 			{
 				addButton(mxUtils.bind(this, function(evt)
 				{
-					if (this.editor.editButtonLink == '_blank')
+					if (this.editor.editButtonFunc != null) 
+					{
+						this.editor.editButtonFunc();
+					} 
+					else if (this.editor.editButtonLink == '_blank')
 					{
 						this.editor.editAsNew(this.getEditBlankXml());
 					}

+ 13 - 8
src/main/webapp/js/mxgraph/Format.js

@@ -27,6 +27,16 @@ Format.prototype.showCloseButton = true;
  */
 Format.prototype.inactiveTabBackgroundColor = '#d7d7d7';
 
+/**
+ * Background color for inactive tabs.
+ */
+Format.prototype.roundableShapes = ['label', 'rectangle', 'internalStorage', 'corner',
+	'parallelogram', 'swimlane', 'triangle', 'trapezoid',
+	'ext', 'step', 'tee', 'process', 'link',
+	'rhombus', 'offPageConnector', 'loopLimit', 'hexagon',
+	'manualInput', 'curlyBracket', 'singleArrow', 'callout',
+	'doubleArrow', 'flexArrow', 'card', 'umlLifeline'];
+
 /**
  * Adds the label menu items to the given menu and parent.
  */
@@ -245,14 +255,9 @@ Format.prototype.isGlassState = function(state)
  */
 Format.prototype.isRoundedState = function(state)
 {
-	var shape = mxUtils.getValue(state.style, mxConstants.STYLE_SHAPE, null);
-	
-	return (shape == 'label' || shape == 'rectangle' || shape == 'internalStorage' || shape == 'corner' ||
-			shape == 'parallelogram' || shape == 'swimlane' || shape == 'triangle' || shape == 'trapezoid' ||
-			shape == 'ext' || shape == 'step' || shape == 'tee' || shape == 'process' || shape == 'link' ||
-			shape == 'rhombus' || shape == 'offPageConnector' || shape == 'loopLimit' || shape == 'hexagon' ||
-			shape == 'manualInput' || shape == 'curlyBracket' || shape == 'singleArrow' || shape == 'callout' ||
-			shape == 'doubleArrow' || shape == 'flexArrow' || shape == 'card' || shape == 'umlLifeline');
+	return (state.shape != null) ? state.shape.isRoundable() :
+		mxUtils.indexOf(this.roundableShapes, mxUtils.getValue(state.style,
+		mxConstants.STYLE_SHAPE, null)) >= 0;
 };
 
 /**

+ 453 - 146
src/main/webapp/js/mxgraph/Graph.js

@@ -889,6 +889,9 @@ Graph = function(container, model, renderHint, stylesheet, themes)
 			return me;
 		};
 	}
+	
+	//Create a unique offset object for each graph instance.
+	this.currentTranslate = new mxPoint(0, 0);
 };
 
 /**
@@ -1105,6 +1108,282 @@ Graph.prototype.init = function(container)
 	this.initLayoutManager();
 };
 
+/**
+ * Implements zoom and offset via CSS transforms. This is currently only used
+ * in read-only as there are fewer issues with the mxCellState not being scaled
+ * and translated.
+ * 
+ * KNOWN ISSUES TO FIX:
+ * - Apply CSS transforms to HTML labels in IE11
+ */
+(function()
+{
+	/**
+	 * Uses CSS transforms for scale and translate.
+	 */
+	Graph.prototype.useCssTransforms = false;
+
+	/**
+	 * Contains the scale.
+	 */
+	Graph.prototype.currentScale = 1;
+
+	/**
+	 * Contains the offset.
+	 */
+	Graph.prototype.currentTranslate = new mxPoint(0, 0);
+
+	/**
+	 * Only foreignObject supported for now (no IE11).
+	 */
+	Graph.prototype.isCssTransformsSupported = function()
+	{
+		return this.dialect == mxConstants.DIALECT_SVG && !mxClient.NO_FO;
+	};
+
+	/**
+	 * Function: getCellAt
+	 * 
+	 * Overrides to transform incoming coordinates.
+	 */
+	Graph.prototype.getCellAt = function(x, y, parent, vertices, edges, ignoreFn)
+	{
+		if (this.useCssTransforms)
+		{
+			x /= this.currentScale - this.currentTranslate.x;
+			y /= this.currentScale - this.currentTranslate.y;
+		}
+		
+		return mxGraph.prototype.getCellAt.apply(this, arguments);
+	};
+
+	/**
+	 * Function: repaint
+	 * 
+	 * Updates the highlight after a change of the model or view.
+	 */
+	mxCellHighlight.prototype.getStrokeWidth = function(state)
+	{
+		var s = this.strokeWidth;
+		
+		if (this.graph.useCssTransforms)
+		{
+			s /= this.graph.currentScale;
+		}
+
+		return s;
+	};
+
+	/**
+	 * Function: getGraphBounds
+	 * 
+	 * Overrides getGraphBounds to use bounding box from SVG.
+	 */
+	mxGraphView.prototype.getGraphBounds = function()
+	{
+		var b = this.graphBounds;
+		
+		if (this.graph.useCssTransforms)
+		{
+			var t = this.graph.currentTranslate;
+			var s = this.graph.currentScale;
+
+			b = new mxRectangle(
+				(b.x + t.x) * s, (b.y + t.y) * s,
+				b.width * s, b.height * s);
+		}
+
+		return b;
+	};
+	
+	/**
+	 * Function: viewStateChanged
+	 * 
+	 * Overrides to bypass full cell tree validation.
+	 * TODO: Check if this improves performance
+	 */
+	mxGraphView.prototype.viewStateChanged = function()
+	{
+		if (this.graph.useCssTransforms)
+		{
+			this.validate();
+			this.graph.sizeDidChange();
+		}
+		else
+		{
+			this.revalidate();
+			this.graph.sizeDidChange();
+		}
+	};
+
+	/**
+	 * Function: validate
+	 * 
+	 * Overrides validate to normalize validation view state and pass
+	 * current state to CSS transform.
+	 */
+	var graphViewValidate = mxGraphView.prototype.validate;
+	
+	mxGraphView.prototype.validate = function(cell)
+	{
+		if (this.graph.useCssTransforms)
+		{
+			this.graph.currentScale = this.scale;
+			this.graph.currentTranslate.x = this.translate.x;
+			this.graph.currentTranslate.y = this.translate.y;
+			
+			this.scale = 1;
+			this.translate.x = 0;
+			this.translate.y = 0;
+		}
+		
+		graphViewValidate.apply(this, arguments);
+		
+		if (this.graph.useCssTransforms)
+		{
+			this.graph.updateCssTransform();
+			
+			this.scale = this.graph.currentScale;
+			this.translate.x = this.graph.currentTranslate.x;
+			this.translate.y = this.graph.currentTranslate.y;
+		}
+	};
+
+	/**
+	 * Function: updateCssTransform
+	 * 
+	 * Zooms out of the graph by <zoomFactor>.
+	 */
+	Graph.prototype.updateCssTransform = function()
+	{
+		var temp = this.view.getDrawPane();
+		
+		if (temp != null)
+		{
+			var g = this.view.getDrawPane().parentNode;
+			var prev = g.getAttribute('transform');
+			g.setAttribute('transformOrigin', '0 0');
+			g.setAttribute('transform', 'scale(' + this.currentScale + ',' + this.currentScale + ')' +
+				'translate(' + this.currentTranslate.x + ',' + this.currentTranslate.y + ')');
+
+			// Applies workarounds only if translate has changed
+			if (prev != g.getAttribute('transform'))
+			{
+				try
+				{
+					// Applies transform to labels outside of the SVG DOM
+					// Excluded via isCssTransformsSupported
+//					if (mxClient.NO_FO)
+//					{
+//						var transform = 'scale(' + this.currentScale + ')' + 'translate(' +
+//							this.currentTranslate.x + 'px,' + this.currentTranslate.y + 'px)';
+//							
+//						this.view.states.visit(mxUtils.bind(this, function(cell, state)
+//						{
+//							if (state.text != null && state.text.node != null)
+//							{
+//								// Stores initial CSS transform that is used for the label alignment
+//								if (state.text.originalTransform == null)
+//								{
+//									state.text.originalTransform = state.text.node.style.transform;
+//								}
+//								
+//								state.text.node.style.transform = transform + state.text.originalTransform;
+//							}
+//						}));
+//					}
+					// Workaround for https://bugs.webkit.org/show_bug.cgi?id=93358 in WebKit
+					// Adding an absolute position DIV before the SVG seems to mitigate the problem.
+					if (mxClient.IS_GC)
+					{
+						if (this.mathEnabled && (this.webKitForceRepaintNode == null ||
+							this.webKitForceRepaintNode.parentNode == null) &&
+							this.container.firstChild.nodeName == 'svg')
+						{
+							this.webKitForceRepaintNode = document.createElement('div');
+							this.webKitForceRepaintNode.style.cssText = 'position:absolute;';
+							g.ownerSVGElement.parentNode.insertBefore(this.webKitForceRepaintNode, g.ownerSVGElement);
+						}
+						else if (this.webKitForceRepaintNode != null && (!this.mathEnabled ||
+								(this.container.firstChild.nodeName != 'svg' &&
+								this.container.firstChild != this.webKitForceRepaintNode)))
+						{
+							if (this.webKitForceRepaintNode.parentNode != null)
+							{
+								this.webKitForceRepaintNode.parentNode.removeChild(this.webKitForceRepaintNode);
+							}
+							
+							this.webKitForceRepaintNode = null;
+						}
+					}
+					// Workaround for https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/4320441/
+					else if (mxClient.IS_EDGE)
+					{
+						// Recommended workaround is to do this on all
+						// foreignObjects, but this seems to be faster
+						var val = g.style.display;
+						g.style.display = 'none';
+						g.getBBox();
+						g.style.display = val;
+					}
+				}
+				catch (e)
+				{
+					// ignore
+					console.log('err', e);
+				}
+			}
+		}
+	};
+	
+	var graphViewValidateBackgroundPage = mxGraphView.prototype.validateBackgroundPage;
+	
+	mxGraphView.prototype.validateBackgroundPage = function()
+	{
+		var useCssTranforms = this.graph.useCssTransforms, scale = this.scale, 
+			translate = this.translate;
+		
+		if (useCssTranforms)
+		{
+			this.scale = this.graph.currentScale;
+			this.translate = this.graph.currentTranslate;
+		}
+		
+		graphViewValidateBackgroundPage.apply(this, arguments);
+		
+		if (useCssTranforms)
+		{
+			this.scale = scale;
+			this.translate = translate;
+		}
+	};
+
+	var graphUpdatePageBreaks = mxGraph.prototype.updatePageBreaks;
+	
+	mxGraph.prototype.updatePageBreaks = function(visible, width, height)
+	{
+		var useCssTranforms = this.useCssTransforms, scale = this.view.scale, 
+			translate = this.view.translate;
+	
+		if (useCssTranforms)
+		{
+			this.view.scale = 1;
+			this.view.translate = new mxPoint(0, 0);
+			this.useCssTransforms = false;
+		}
+		
+		graphUpdatePageBreaks.apply(this, arguments);
+		
+		if (useCssTranforms)
+		{
+			this.view.scale = scale;
+			this.view.translate = translate;
+			this.useCssTransforms = true;
+		}
+	};
+	
+})();
+
 /**
  * Sets the XML node for the current diagram.
  */
@@ -1396,7 +1675,8 @@ Graph.prototype.isReplacePlaceholders = function(cell)
  */
 Graph.prototype.isZoomWheelEvent = function(evt)
 {
-	return mxEvent.isAltDown(evt) || (mxEvent.isControlDown(evt) && !mxClient.IS_MAC);
+	return mxEvent.isAltDown(evt) || (mxEvent.isMetaDown(evt) && mxClient.IS_MAC) ||
+		(mxEvent.isControlDown(evt) && !mxClient.IS_MAC);
 };
 
 /**
@@ -2549,7 +2829,7 @@ Graph.prototype.getTooltipForCell = function(cell)
 			{
 				if (temp[i].name != 'link' || !this.isCustomLink(temp[i].value))
 				{
-					tip += ((temp[i].name != 'link') ? temp[i].name + ':' : '') +
+					tip += ((temp[i].name != 'link') ? '<b>' + temp[i].name + ':</b> ' : '') +
 						mxUtils.htmlEntities(temp[i].value) + '\n';
 				}
 			}
@@ -2557,6 +2837,11 @@ Graph.prototype.getTooltipForCell = function(cell)
 			if (tip.length > 0)
 			{
 				tip = tip.substring(0, tip.length - 1);
+				
+				if (mxClient.IS_SVG)
+				{
+					tip = '<div style="max-width:360px;">' + tip + '</div>';
+				}
 			}
 		}
 	}
@@ -5789,178 +6074,200 @@ if (typeof mxVertexHandler != 'undefined')
 		 */
 		Graph.prototype.getSvg = function(background, scale, border, nocrop, crisp, ignoreSelection, showText, imgExport)
 		{
-			scale = (scale != null) ? scale : 1;
-			border = (border != null) ? border : 0;
-			crisp = (crisp != null) ? crisp : true;
-			ignoreSelection = (ignoreSelection != null) ? ignoreSelection : true;
-			showText = (showText != null) ? showText : true;
-
-			var bounds = (ignoreSelection || nocrop) ?
-					this.getGraphBounds() : this.getBoundingBox(this.getSelectionCells());
-
-			if (bounds == null)
+			//Disable Css Transforms if it is used
+			var origUseCssTrans = this.useCssTransforms;
+			
+			if (origUseCssTrans) 
 			{
-				throw Error(mxResources.get('drawingEmpty'));
+				this.useCssTransforms = false;
+				this.view.revalidate();
+				this.sizeDidChange();
 			}
 
-			var vs = this.view.scale;
-			
-			// Prepares SVG document that holds the output
-			var svgDoc = mxUtils.createXmlDocument();
-			var root = (svgDoc.createElementNS != null) ?
-		    		svgDoc.createElementNS(mxConstants.NS_SVG, 'svg') : svgDoc.createElement('svg');
-		    
-			if (background != null)
+			try 
 			{
-				if (root.style != null)
+				scale = (scale != null) ? scale : 1;
+				border = (border != null) ? border : 0;
+				crisp = (crisp != null) ? crisp : true;
+				ignoreSelection = (ignoreSelection != null) ? ignoreSelection : true;
+				showText = (showText != null) ? showText : true;
+	
+				var bounds = (ignoreSelection || nocrop) ?
+						this.getGraphBounds() : this.getBoundingBox(this.getSelectionCells());
+	
+				if (bounds == null)
 				{
-					root.style.backgroundColor = background;
+					throw Error(mxResources.get('drawingEmpty'));
+				}
+	
+				var vs = this.view.scale;
+				
+				// Prepares SVG document that holds the output
+				var svgDoc = mxUtils.createXmlDocument();
+				var root = (svgDoc.createElementNS != null) ?
+			    		svgDoc.createElementNS(mxConstants.NS_SVG, 'svg') : svgDoc.createElement('svg');
+			    
+				if (background != null)
+				{
+					if (root.style != null)
+					{
+						root.style.backgroundColor = background;
+					}
+					else
+					{
+						root.setAttribute('style', 'background-color:' + background);
+					}
+				}
+			    
+				if (svgDoc.createElementNS == null)
+				{
+			    	root.setAttribute('xmlns', mxConstants.NS_SVG);
+			    	root.setAttribute('xmlns:xlink', mxConstants.NS_XLINK);
 				}
 				else
 				{
-					root.setAttribute('style', 'background-color:' + background);
+					// KNOWN: Ignored in IE9-11, adds namespace for each image element instead. No workaround.
+					root.setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:xlink', mxConstants.NS_XLINK);
+				}
+				
+				var s = scale / vs;
+				root.setAttribute('width', Math.max(1, Math.ceil(bounds.width * s) + 2 * border) + 'px');
+				root.setAttribute('height', Math.max(1, Math.ceil(bounds.height * s) + 2 * border) + 'px');
+				root.setAttribute('version', '1.1');
+				
+			    // Adds group for anti-aliasing via transform
+				var node = root;
+				
+				if (crisp)
+				{
+					var group = (svgDoc.createElementNS != null) ?
+							svgDoc.createElementNS(mxConstants.NS_SVG, 'g') : svgDoc.createElement('g');
+					group.setAttribute('transform', 'translate(0.5,0.5)');
+					root.appendChild(group);
+					svgDoc.appendChild(root);
+					node = group;
+				}
+				else
+				{
+					svgDoc.appendChild(root);
 				}
-			}
-		    
-			if (svgDoc.createElementNS == null)
-			{
-		    	root.setAttribute('xmlns', mxConstants.NS_SVG);
-		    	root.setAttribute('xmlns:xlink', mxConstants.NS_XLINK);
-			}
-			else
-			{
-				// KNOWN: Ignored in IE9-11, adds namespace for each image element instead. No workaround.
-				root.setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:xlink', mxConstants.NS_XLINK);
-			}
-			
-			var s = scale / vs;
-			root.setAttribute('width', Math.max(1, Math.ceil(bounds.width * s) + 2 * border) + 'px');
-			root.setAttribute('height', Math.max(1, Math.ceil(bounds.height * s) + 2 * border) + 'px');
-			root.setAttribute('version', '1.1');
-			
-		    // Adds group for anti-aliasing via transform
-			var node = root;
-			
-			if (crisp)
-			{
-				var group = (svgDoc.createElementNS != null) ?
-						svgDoc.createElementNS(mxConstants.NS_SVG, 'g') : svgDoc.createElement('g');
-				group.setAttribute('transform', 'translate(0.5,0.5)');
-				root.appendChild(group);
-				svgDoc.appendChild(root);
-				node = group;
-			}
-			else
-			{
-				svgDoc.appendChild(root);
-			}
-		
-		    // Renders graph. Offset will be multiplied with state's scale when painting state.
-			// TextOffset only seems to affect FF output but used everywhere for consistency.
-			var svgCanvas = this.createSvgCanvas(node);
-			svgCanvas.foOffset = (crisp) ? -0.5 : 0;
-			svgCanvas.textOffset = (crisp) ? -0.5 : 0;
-			svgCanvas.imageOffset = (crisp) ? -0.5 : 0;
-			svgCanvas.translate(Math.floor((border / scale - bounds.x) / vs), Math.floor((border / scale - bounds.y) / vs));
-			
-			// Convert HTML entities
-			var htmlConverter = document.createElement('textarea');
 			
-			// Adds simple text fallback for viewers with no support for foreignObjects
-			var createAlternateContent = svgCanvas.createAlternateContent;
-			svgCanvas.createAlternateContent = function(fo, x, y, w, h, str, align, valign, wrap, format, overflow, clip, rotation)
-			{
-				var s = this.state;
-
-				// Assumes a max character width of 0.2em
-				if (this.foAltText != null && (w == 0 || (s.fontSize != 0 && str.length < (w * 5) / s.fontSize)))
+			    // Renders graph. Offset will be multiplied with state's scale when painting state.
+				// TextOffset only seems to affect FF output but used everywhere for consistency.
+				var svgCanvas = this.createSvgCanvas(node);
+				svgCanvas.foOffset = (crisp) ? -0.5 : 0;
+				svgCanvas.textOffset = (crisp) ? -0.5 : 0;
+				svgCanvas.imageOffset = (crisp) ? -0.5 : 0;
+				svgCanvas.translate(Math.floor((border / scale - bounds.x) / vs), Math.floor((border / scale - bounds.y) / vs));
+				
+				// Convert HTML entities
+				var htmlConverter = document.createElement('textarea');
+				
+				// Adds simple text fallback for viewers with no support for foreignObjects
+				var createAlternateContent = svgCanvas.createAlternateContent;
+				svgCanvas.createAlternateContent = function(fo, x, y, w, h, str, align, valign, wrap, format, overflow, clip, rotation)
 				{
-					var alt = this.createElement('text');
-					alt.setAttribute('x', Math.round(w / 2));
-					alt.setAttribute('y', Math.round((h + s.fontSize) / 2));
-					alt.setAttribute('fill', s.fontColor || 'black');
-					alt.setAttribute('text-anchor', 'middle');
-					alt.setAttribute('font-size', Math.round(s.fontSize) + 'px');
-					alt.setAttribute('font-family', s.fontFamily);
-					
-					if ((s.fontStyle & mxConstants.FONT_BOLD) == mxConstants.FONT_BOLD)
+					var s = this.state;
+	
+					// Assumes a max character width of 0.2em
+					if (this.foAltText != null && (w == 0 || (s.fontSize != 0 && str.length < (w * 5) / s.fontSize)))
 					{
-						alt.setAttribute('font-weight', 'bold');
+						var alt = this.createElement('text');
+						alt.setAttribute('x', Math.round(w / 2));
+						alt.setAttribute('y', Math.round((h + s.fontSize) / 2));
+						alt.setAttribute('fill', s.fontColor || 'black');
+						alt.setAttribute('text-anchor', 'middle');
+						alt.setAttribute('font-size', Math.round(s.fontSize) + 'px');
+						alt.setAttribute('font-family', s.fontFamily);
+						
+						if ((s.fontStyle & mxConstants.FONT_BOLD) == mxConstants.FONT_BOLD)
+						{
+							alt.setAttribute('font-weight', 'bold');
+						}
+						
+						if ((s.fontStyle & mxConstants.FONT_ITALIC) == mxConstants.FONT_ITALIC)
+						{
+							alt.setAttribute('font-style', 'italic');
+						}
+						
+						if ((s.fontStyle & mxConstants.FONT_UNDERLINE) == mxConstants.FONT_UNDERLINE)
+						{
+							alt.setAttribute('text-decoration', 'underline');
+						}
+						
+						try
+						{
+							htmlConverter.innerHTML = str;
+							alt.textContent = htmlConverter.value;
+							
+							return alt;
+						}
+						catch (e)
+						{
+							return createAlternateContent.apply(this, arguments);
+						}
 					}
-					
-					if ((s.fontStyle & mxConstants.FONT_ITALIC) == mxConstants.FONT_ITALIC)
+					else
 					{
-						alt.setAttribute('font-style', 'italic');
+						return createAlternateContent.apply(this, arguments);
 					}
+				};
+				
+				// Paints background image
+				var bgImg = this.backgroundImage;
+				
+				if (bgImg != null)
+				{
+					var s2 = vs / scale;
+					var tr = this.view.translate;
+					var tmp = new mxRectangle(tr.x * s2, tr.y * s2, bgImg.width * s2, bgImg.height * s2);
 					
-					if ((s.fontStyle & mxConstants.FONT_UNDERLINE) == mxConstants.FONT_UNDERLINE)
+					// Checks if visible
+					if (mxUtils.intersects(bounds, tmp))
 					{
-						alt.setAttribute('text-decoration', 'underline');
+						svgCanvas.image(tr.x, tr.y, bgImg.width, bgImg.height, bgImg.src, true);
 					}
+				}
+				
+				svgCanvas.scale(s);
+				svgCanvas.textEnabled = showText;
+				
+				imgExport = (imgExport != null) ? imgExport : this.createSvgImageExport();
+				var imgExportDrawCellState = imgExport.drawCellState;
+				
+				// Implements ignoreSelection flag
+				imgExport.drawCellState = function(state, canvas)
+				{
+					var graph = state.view.graph;
+					var selected = graph.isCellSelected(state.cell);
+					var parent = graph.model.getParent(state.cell);
 					
-					try
+					// Checks if parent cell is selected
+					while (!ignoreSelection && !selected && parent != null)
 					{
-						htmlConverter.innerHTML = str;
-						alt.textContent = htmlConverter.value;
-						
-						return alt;
+						selected = graph.isCellSelected(parent);
+						parent = graph.model.getParent(parent);
 					}
-					catch (e)
+					
+					if (ignoreSelection || selected)
 					{
-						return createAlternateContent.apply(this, arguments);
+						imgExportDrawCellState.apply(this, arguments);
 					}
-				}
-				else
-				{
-					return createAlternateContent.apply(this, arguments);
-				}
-			};
-			
-			// Paints background image
-			var bgImg = this.backgroundImage;
+				};
+	
+				imgExport.drawState(this.getView().getState(this.model.root), svgCanvas);
 			
-			if (bgImg != null)
-			{
-				var s2 = vs / scale;
-				var tr = this.view.translate;
-				var tmp = new mxRectangle(tr.x * s2, tr.y * s2, bgImg.width * s2, bgImg.height * s2);
-				
-				// Checks if visible
-				if (mxUtils.intersects(bounds, tmp))
-				{
-					svgCanvas.image(tr.x, tr.y, bgImg.width, bgImg.height, bgImg.src, true);
-				}
+				return root;
 			}
-			
-			svgCanvas.scale(s);
-			svgCanvas.textEnabled = showText;
-			
-			imgExport = (imgExport != null) ? imgExport : this.createSvgImageExport();
-			var imgExportDrawCellState = imgExport.drawCellState;
-			
-			// Implements ignoreSelection flag
-			imgExport.drawCellState = function(state, canvas)
+			finally
 			{
-				var graph = state.view.graph;
-				var selected = graph.isCellSelected(state.cell);
-				var parent = graph.model.getParent(state.cell);
-				
-				// Checks if parent cell is selected
-				while (!ignoreSelection && !selected && parent != null)
+				if (origUseCssTrans) 
 				{
-					selected = graph.isCellSelected(parent);
-					parent = graph.model.getParent(parent);
+					this.useCssTransforms = true;
+					this.view.revalidate();
+					this.sizeDidChange();
 				}
-				
-				if (ignoreSelection || selected)
-				{
-					imgExportDrawCellState.apply(this, arguments);
-				}
-			};
-
-			imgExport.drawState(this.getView().getState(this.model.root), svgCanvas);
-		
-			return root;
+			}
 		};
 		
 		/**

+ 10 - 3
src/main/webapp/js/mxgraph/Menus.js

@@ -1177,15 +1177,22 @@ Menubar.prototype.hideMenu = function()
 /**
  * Adds a submenu to this menubar.
  */
-Menubar.prototype.addMenu = function(label, funct)
+Menubar.prototype.addMenu = function(label, funct, before)
 {
 	var elt = document.createElement('a');
 	elt.setAttribute('href', 'javascript:void(0);');
 	elt.className = 'geItem';
 	mxUtils.write(elt, label);
-
 	this.addMenuHandler(elt, funct);
-	this.container.appendChild(elt);
+	
+    if (before != null)
+    {
+    	this.container.insertBefore(elt, before);
+    }
+    else
+    {
+    	this.container.appendChild(elt);
+    }
 	
 	return elt;
 };

+ 5 - 0
src/main/webapp/js/mxgraph/Shapes.js

@@ -3522,6 +3522,11 @@
 					}
 					
 					var fn = handleFactory[name];
+					
+					if (fn == null && this.state.shape != null && this.state.shape.isRoundable())
+					{
+						fn = handleFactory[mxConstants.SHAPE_RECTANGLE];
+					}
 				
 					if (fn != null)
 					{

文件差异内容过多而无法显示
+ 170 - 162
src/main/webapp/js/reader.min.js


文件差异内容过多而无法显示
+ 8 - 8
src/main/webapp/js/shapes.min.js


文件差异内容过多而无法显示
+ 1103 - 1089
src/main/webapp/js/viewer.min.js


+ 3 - 1
src/main/webapp/plugins/cConf-1-4-8.js

@@ -184,7 +184,9 @@ Draw.loadPlugin(function(ui)
 	menu.funct = function(menu, parent)
 	{
 		oldFunct.apply(this, arguments);
-		this.addSubmenu('viewerMenu', menu, parent);
+		
+		menu.addSeparator(parent);
+		ui.menus.addSubmenu('viewerMenu', menu, parent);
 	};
 	
 	// Returns modified macro data to client

+ 365 - 0
src/main/webapp/plugins/tickets.js

@@ -0,0 +1,365 @@
+/**
+ * Freshdesk ticket plugin. Drag tickets into the diagram. Tickets are
+ * updated on file open, page select and via Extras, Update Tickets.
+ * 
+ * Drag freshdesk tickets into the diagram. Domain must match deskDomain.freshdesk.com.
+ * 
+ * Supported URL parameters:
+ * - tickets-config=URI encoded JSON
+ * 
+ * Alternatively, to avoid logging of the URL parameter with the API key, a ticketsConfig object
+ * can be added in Editor.configure (ie. via #C hash property), eg. 
+ * 
+ * https://www.draw.io/?p=tickets#C%7B"ticketsConfig"%3A %7B"deskApiKey"%3A"YOUR_API_KEY"%2C"deskDomain"%3A"YOUR_DOMAIN"%7D%7D
+ * 
+ * Use an additional "open" variable in the config JSON to open a file after parsing the config, eg.
+ * 
+ * ...#C%7B"ticketsConfig"%3A %7B"deskApiKey"%3A"YOUR_API_KEY"%2C"deskDomain"%3A"YOUR_DOMAIN"%7D%2C"open"%3A"ID_WITH_PREFIX"%7D
+ * 
+ * Required JSON parameters:
+ * - deskApiKey=api_key (see user profile)
+ * - deskDomain=subdomain (subdomain.freshdesk.com)
+ * 
+ * Optional JSON parameters:
+ * - deskStatus: Lookup for status codes (code => string)
+ * - deskTypes: Lookup for ticket types (string => story, task, subTask, feature,
+ * bug, techTask, epic, improvement, fault, change, access, purchase or itHelp)
+ * 
+ * The current configuration is stored in localStorage under ".tickets-config". Use
+ * https://jgraph.github.io/drawio-tools/tools/convert.html for URI encoding.
+ */
+Draw.loadPlugin(function(ui)
+{
+	var config = null;
+	var deskDomain = null;
+	var deskApiKey = null;
+	var graph = ui.editor.graph;
+	
+	var deskPriority = {'1': 'minor', '2': 'major',
+		'3': 'critical', '4': 'blocker'};
+	var deskTypes = {'Question': 'story', 'Incident': 'techTask', 'Problem': 'fault',
+		'Feature Request': 'feature', 'Lead': 'purchase'};
+	var deskStatus = {'2': 'Open', '3': 'Pending', '4': 'Resolved', '5': 'Closed',
+		'6': 'Waiting on Customer', '7': 'Waiting on Third Party',
+		'8': 'Resolved Internally'};
+	var deskStatusWidth = {};
+	
+	function configure()
+	{
+		deskDomain = 'https://' + config.deskDomain + '.freshdesk.com';
+		deskApiKey = config.deskApiKey;
+		
+		deskTypes = config.deskTypes || deskTypes;
+		deskStatus = config.deskStatus || deskStatus;
+		deskStatusWidth = {};
+
+		// Precomputes text widths for custom ticket status
+		var div = document.createElement('div');
+		div.style.fontFamily = 'Arial,Helvetica';
+		div.style.visibility = 'hidden';
+		div.style.position = 'absolute';
+		div.style.fontSize = '11px';
+		
+		document.body.appendChild(div);
+		
+		for (var key in deskStatus)
+		{
+			div.innerHTML = '';
+			mxUtils.write(div, deskStatus[key]);
+			deskStatusWidth[key] = div.clientWidth + 4;
+		}
+
+		document.body.removeChild(div);
+	};
+	
+	if (urlParams['tickets-config'] != null)
+	{
+		config = JSON.parse(decodeURIComponent(urlParams['tickets-config']));
+	}
+	
+	if (config == null && Editor.config != null && Editor.config.ticketsConfig != null)
+	{
+		config = Editor.config.ticketsConfig;
+	}
+	
+	if (config != null)
+	{
+		ui.setLocalData('.tickets-config', JSON.stringify(config));
+		configure();
+	}
+	else
+	{
+		ui.getLocalData('.tickets-config', mxUtils.bind(this, function(value)
+		{
+			if (value != null)
+			{
+				config = JSON.parse(value);
+				configure();
+				updateTickets();
+			}
+		}));
+	}
+
+	function isDeskLink(link)
+	{
+		return config != null && link.substring(0, deskDomain.length) == deskDomain;
+	};
+	
+	function getIdForDeskLink(link)
+	{
+		return link.substring(link.lastIndexOf('/') + 1);
+	};
+	
+	function getDeskTicket(id, fn)
+	{
+		var xhr = new XMLHttpRequest();
+		xhr.open('GET', deskDomain + '/api/v2/tickets/' + id);
+		xhr.setRequestHeader('Authorization', 'Basic ' + btoa(deskApiKey + ':x'));
+
+		xhr.onload = function ()
+		{
+			if (xhr.status >= 200 && xhr.status <= 299)
+			{
+				fn(JSON.parse(xhr.responseText));
+			}
+			else
+			{
+				fn();
+			}
+		};
+		
+		xhr.onerror = function ()
+		{
+			fn();
+		};
+
+		xhr.send();
+	};
+	
+	function updateStyle(cell, ticket)
+	{
+		var type = (ticket.type != null) ? deskTypes[ticket.type] : 'bug';
+		var status = deskStatus[ticket.status] || 'Unknown';
+		var priority = deskPriority[ticket.priority];
+		var sw = deskStatusWidth[ticket.status];
+		var prev = cell.style;
+		
+		cell.style = mxUtils.setStyle(cell.style, 'issueType', type);
+		cell.style = mxUtils.setStyle(cell.style, 'issueStatus', status);
+		cell.style = mxUtils.setStyle(cell.style, 'issueStatusWidth', sw);
+		cell.style = mxUtils.setStyle(cell.style, 'issuePriority', priority);
+		
+		return prev != cell.style;
+	};
+	
+	function updateData(cell, ticket)
+	{
+		var changed = false;
+		
+		function setAttr(key, value)
+		{
+			var prev = cell.value.getAttribute(key);
+			value = value || '';
+			
+			if (prev != value)
+			{
+				cell.value.setAttribute(key, value);
+				
+				return true;
+			}
+			else
+			{
+				return false;
+			}
+		};
+		
+		changed = setAttr('abstract', ticket.description_text.substring(0, 600)) |
+			setAttr('email_config_id', ticket.email_config_id) |
+			setAttr('requester_id', ticket.requester_id) |
+			setAttr('group_id', ticket.group_id) |
+			setAttr('created_at', ticket.created_at) |
+			setAttr('updated_at', ticket.updated_at) |
+			setAttr('due_by', ticket.due_by) |
+			setAttr('tags', ticket.tags.join(' '));
+		
+		for (var key in ticket.custom_fields)
+		{
+			changed = changed | setAttr(key, ticket.custom_fields[key]);
+		}
+
+		return changed;
+	};
+	
+	function updateTickets(spin)
+	{
+		if (config != null && (!spin || ui.spinner.spin(document.body, mxResources.get('loading') + '...')))
+		{
+			var pending = 0;
+			
+			graph.view.states.visit(function(id, state)
+			{
+				var link = graph.getLinkForCell(state.cell);
+				
+				if (link != null && isDeskLink(link))
+				{
+					var id = getIdForDeskLink(link);
+					pending++;
+					
+					getDeskTicket(id, function(ticket)
+					{
+						pending--;
+						
+						if (ticket != null)
+						{
+							// Expression must execute both calls
+							if (updateStyle(state.cell, ticket) |
+								updateData(state.cell, ticket))
+							{
+								state.style = null;
+								graph.view.invalidate(state.cell, true, false);
+								graph.view.validate(state.cell);
+							}
+						}
+						
+						if (spin && pending == 0)
+						{
+							ui.spinner.stop();
+						}
+					})
+				}
+			});
+			
+			if (spin && pending == 0)
+			{
+				ui.spinner.stop();
+			}
+		}
+	};
+	
+	function getCellForLink(link)
+	{
+		for (var key in graph.view.states.map)
+		{
+			var cell = graph.view.states.map[key].cell;
+			
+			if (link == graph.getLinkForCell(cell))
+			{
+				return cell;
+			}
+		}
+	};
+
+	// Adds resource for action
+	mxResources.parse('updateTickets=Update Tickets...');
+
+	// Adds action
+	ui.actions.addAction('updateTickets', function()
+	{
+		updateTickets(true);
+	});
+	
+	// Updates tickets in opened files
+	ui.editor.addListener('fileLoaded', function()
+	{
+		updateTickets(false);
+	});
+
+	// Updates tickets when page changes
+	ui.editor.addListener('pageSelected', function()
+	{
+		updateTickets(false);
+	});
+
+	// Adds menu item
+	var menu = ui.menus.get('extras');
+	var oldFunct = menu.funct;
+	
+	menu.funct = function(menu, parent)
+	{
+		oldFunct.apply(this, arguments);
+		
+		ui.menus.addMenuItems(menu, ['-', 'updateTickets'], parent);
+	};
+
+	// Intercepts ticket URLs
+	var uiInsertTextAt = ui.insertTextAt;
+	
+	ui.insertTextAt = function(text, dx, dy, html, asImage, crop, resizeImages)
+	{
+		if (isDeskLink(text))
+		{
+			var cell = getCellForLink(text);
+			
+			if (cell != null)
+			{
+				// Selects existing ticket with same link
+				graph.setSelectionCell(cell);
+				graph.scrollCellToVisible(graph.getSelectionCell());
+			}
+			else if (ui.spinner.spin(document.body, mxResources.get('loading') + '...'))
+			{
+				// Creates new shape
+				var id = getIdForDeskLink(text);
+				
+				getDeskTicket(id, function(ticket)
+				{
+					ui.spinner.stop();
+					
+					if (ticket != null)
+					{
+						var cell = null;
+						
+				    	graph.getModel().beginUpdate();
+				    	try
+				    	{
+				    		cell = graph.insertVertex(graph.getDefaultParent(), null,
+				    			'%subject%\n\n<b>Updated:</b> %updated_at% ' +
+				    			'(<a href="' + deskDomain + '/contacts/%requester_id%">From</a>)',
+								graph.snap(dx), graph.snap(dy), 200, 50,
+								'html=1;whiteSpace=wrap;overflow=hidden;shape=mxgraph.atlassian.issue;' +
+								'fontSize=12;verticalAlign=top;align=left;spacingTop=25;' +
+								'strokeColor=#A8ADB0;fillColor=#EEEEEE;backgroundOutline=1;');
+				    		
+				    		graph.setLinkForCell(cell, text);
+				    		cell.value.setAttribute('subject', ticket.subject);
+							cell.value.setAttribute('placeholders', '1');
+							cell.value.setAttribute('ticket_id', id);
+				    		updateData(cell, ticket);
+							updateStyle(cell, ticket);
+
+				    		// Adds ticket ID label
+				    		var label1 = new mxCell('%ticket_id%', new mxGeometry(0, 0, 60, 20),
+						   		'strokeColor=none;fillColor=none;part=1;resizable=0;align=left;' +
+						   		'autosize=1;points=[];deletable=0;editable=0;connectable=0;');
+						   	graph.setAttributeForCell(label1, 'placeholders', '1');
+						   	label1.geometry.relative = true;
+						   	label1.geometry.offset = new mxPoint(20, 0);
+						   	label1.vertex = true;
+						   	cell.insert(label1);
+				    		
+				    		graph.updateCellSize(cell);
+				    		cell.geometry.width = Math.max(220, cell.geometry.width);
+				    		cell.geometry.height += 10;
+				    	}
+				    	finally
+				    	{
+				    		graph.getModel().endUpdate();
+				    	}
+				    	
+						graph.setSelectionCell(cell);
+					}
+					else
+					{
+						ui.handleError({message: mxResources.get('unknownError')});
+					}
+				});
+			}
+			
+	    	return null;
+		}
+		else
+		{
+			return uiInsertTextAt.apply(this, arguments);
+		}
+	};
+});

+ 9 - 6
src/main/webapp/plugins/webcola/webcola.js

@@ -3,18 +3,18 @@
  */
 Draw.loadPlugin(function(ui)
 {
-	mxscript("js/diagramly/plugins/webcola/cola.min.js");
-	mxscript("js/diagramly/plugins/webcola/mxWebColaAdaptor.js");
-	mxscript("js/diagramly/plugins/webcola/mxWebColaLayout.js");
+	mxscript("plugins/webcola/cola.min.js", null, null, null, true);
+	mxscript("plugins/webcola/mxWebColaAdaptor.js", null, null, null, true);
+	mxscript("plugins/webcola/mxWebColaLayout.js", null, null, null, true);
 	
 	// Adds resource for action
 	mxResources.parse('webColaLayout=WebCola Layout...');
 
 	// Adds action
-	ui.actions.addAction('Apply WebCola layout', function()
+	ui.actions.addAction('webColaLayout', function()
 	{
 		var graph = ui.editor.graph;
-		var layout = mxWebColaLayout(graph);
+		var layout = new mxWebColaLayout(graph);
 		var parent = graph.getDefaultParent(); 
 		layout.execute(parent);
 	});
@@ -26,6 +26,9 @@ Draw.loadPlugin(function(ui)
 	{
 		oldFunct.apply(this, arguments);
 		
-		ui.menus.addMenuItems(menu, ['-', 'webColaLayout'], parent);
+		if (typeof window.mxWebColaLayout === 'function')
+		{
+			ui.menus.addMenuItems(menu, ['-', 'webColaLayout'], parent);
+		}
 	};
 });

+ 6 - 10
src/main/webapp/shapes/mxAtlassian.js

@@ -30,7 +30,7 @@ mxAtlassianJiraIssue.prototype.cst = {ISSUE : 'mxgraph.atlassian.issue'};
 * 
 * Paints the vertex shape.
 */
-mxAtlassianJiraIssue.prototype.paintVertexShape = function(c, x, y, w, h)
+mxAtlassianJiraIssue.prototype.paintForeground = function(c, x, y, w, h)
 {
 	c.translate(x, y);
 
@@ -38,15 +38,6 @@ mxAtlassianJiraIssue.prototype.paintVertexShape = function(c, x, y, w, h)
 	var issuePriority = mxUtils.getValue(this.style, 'issuePriority', 'minor');
 	var issueStatus = mxUtils.getValue(this.style, 'issueStatus', 'todo');
 
-	
-	c.begin();
-	c.moveTo(0, 0);
-	c.lineTo(w, 0);
-	c.lineTo(w, h);
-	c.lineTo(0, h);
-	c.close();
-	c.fillAndStroke();
-
 	c.setStrokeColor('none');
 	
 	switch (issueType) {
@@ -258,6 +249,11 @@ mxAtlassianJiraIssue.prototype.paintVertexShape = function(c, x, y, w, h)
 			
 			c.text(w - 25, 15, 0, 0, 'DONE', mxConstants.ALIGN_CENTER, mxConstants.ALIGN_MIDDLE, 0, null, 0, 0, 0);
 			break;
+	    default:
+	    	var tw = mxUtils.getValue(this.style, 'issueStatusWidth', issueStatus.length * 6.5);
+			c.rect(w - tw - 5, 5, tw, 20);
+			c.fill();
+	    	c.text(w - 7, 15, 0, 0, issueStatus, mxConstants.ALIGN_RIGHT, mxConstants.ALIGN_MIDDLE, 0, null, 0, 0, 0);
 	}
 };