فهرست منبع

8.8.7 release

Former-commit-id: 9c86bba29e9ad61a6faa373776360acf86b5872c
Gaudenz Alder 7 سال پیش
والد
کامیت
ba79760406

+ 8 - 0
ChangeLog

@@ -1,3 +1,11 @@
+26-JUN-2018: 8.8.7
+
+- Fixes custom content view in Confluence Cloud
+- Disables click sidebar collapse
+- Uses inline window for text plugin
+- Adds showStartScreen in Chrome App
+- Disables plantUML when offline
+
 21-JUN-2018: 8.8.6
 
 - Improves detection of gliffy macro in conf cloud

+ 0 - 13
Dockerfile

@@ -1,13 +0,0 @@
-FROM frekele/ant:1.10.3-jdk8 as  BUILD
-RUN mkdir /usr/build
-COPY src /usr/build/src
-COPY etc /usr/build/etc
-COPY war /usr/build/war
-COPY VERSION /usr/build
-RUN cd /usr/build/etc/build/
-RUN ant -file /usr/build/etc/build/build.xml war
-
-FROM tomcat:9.0 as TARGET
-COPY --from=BUILD /usr/build/build/draw.war  /usr/local/tomcat/webapps/
-EXPOSE 8080
-CMD ["catalina.sh", "run"]

+ 0 - 10
README.md

@@ -17,16 +17,6 @@ A development guide is being started on the GitHub project wiki. There is a [dra
 
 The [mxGraph documentation](https://jgraph.github.io/mxgraph/) provides a lot of the docs for the bottom part of the stack. There is an [mxgraph tag on SO](http://stackoverflow.com/questions/tagged/mxgraph).
 
-
-Docker
-------
-After successful build, from the project directory run
-```bash
-docker build -t draw .
-docker run -d -p 8888:8080 draw
-```
-Now the app will be accessible at http://localhost:8888/draw/?https=0
-
 Running
 -------
 The simplest way to run draw.io initially is to fork this project, [publish the master branch to GitHub pages](https://help.github.com/categories/github-pages-basics/) and the [pages sites](https://jgraph.github.io/drawio/src/main/webapp/index.html) will have the full editor functionality (sans the integrations).

+ 1 - 1
VERSION

@@ -1 +1 @@
-8.8.6
+8.8.7

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

@@ -1,7 +1,7 @@
 CACHE MANIFEST
 
 # THIS FILE WAS GENERATED. DO NOT MODIFY!
-# 06/21/2018 06:20 PM
+# 06/26/2018 04:33 PM
 
 app.html
 index.html?offline=1

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 259 - 258
src/main/webapp/js/app.min.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 521 - 520
src/main/webapp/js/atlas-viewer.min.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 249 - 248
src/main/webapp/js/atlas.min.js


+ 16 - 13
src/main/webapp/js/diagramly/App.js

@@ -2164,26 +2164,29 @@ App.prototype.start = function()
 							
 							if (urlParams['splash'] == '0' && (id == null || id.length == 0))
 							{
-								var draft = this.getDraft();
-								var fileData = (draft != null) ? draft.data : this.getFileData();
-								var prev = Editor.useLocalStorage;
-								this.createFile(this.defaultFilename, fileData, null, null, null, null, null, true);
-								Editor.useLocalStorage = prev;
-								
-								// Draft was used so the user should save the file
-								if (draft != null)
+								if (!mxClient.IS_CHROMEAPP)
 								{
-									var file = this.getCurrentFile();
+									var draft = this.getDraft();
+									var fileData = (draft != null) ? draft.data : this.getFileData();
+									var prev = Editor.useLocalStorage;
+									this.createFile(this.defaultFilename, fileData, null, null, null, null, null, true);
+									Editor.useLocalStorage = prev;
 									
-									if (file != null)
+									// Draft was used so the user should save the file
+									if (draft != null)
 									{
-										file.addUnsavedStatus();
+										var file = this.getCurrentFile();
+										
+										if (file != null)
+										{
+											file.addUnsavedStatus();
+										}
 									}
 								}
 							}
 							else
 							{
-								this.loadFile(this.getDiagramId());
+								this.loadFile(id);
 							}
 						}
 					}
@@ -2364,7 +2367,7 @@ App.prototype.showSplash = function(force)
 			this.showSplash();
 		}));
 	}
-	else if (this.mode == null || force)
+	else if (!mxClient.IS_CHROMEAPP && (this.mode == null || force))
 	{
 		var rowLimit = (serviceCount == 4) ? 2 : 3;
 		

+ 10 - 3
src/main/webapp/js/diagramly/Dialogs.js

@@ -960,7 +960,7 @@ var ErrorDialog = function(editorUi, title, message, buttonText, fn, retry, butt
 /**
  * Constructs a new embed dialog
  */
-var EmbedDialog = function(editorUi, result, timeout, ignoreSize, previewFn)
+var EmbedDialog = function(editorUi, result, timeout, ignoreSize, previewFn, title)
 {
 	var div = document.createElement('div');
 	var maxSize = 500000;
@@ -970,8 +970,15 @@ var EmbedDialog = function(editorUi, result, timeout, ignoreSize, previewFn)
 	// Checks if result is a link
 	var validUrl = /^https?:\/\//.test(result) || /^mailto:\/\//.test(result);
 
-	mxUtils.write(div, mxResources.get((result.length < maxSize) ?
-		((validUrl) ? 'link' : 'mainEmbedNotice') : 'preview') + ':');
+	if (title != null)
+	{
+		mxUtils.write(div, title);
+	}
+	else
+	{
+		mxUtils.write(div, mxResources.get((result.length < maxSize) ?
+			((validUrl) ? 'link' : 'mainEmbedNotice') : 'preview') + ':');
+	}
 	mxUtils.br(div);
 	
 	var size = document.createElement('div');

+ 9 - 9
src/main/webapp/js/diagramly/Editor.js

@@ -1486,12 +1486,14 @@
 			cells = this.getCellsForAction(action.select);
 			this.setSelectionCells(cells);
 		}
-		
-		if (action.scroll != null)
+
+		if (action.highlight != null)
 		{
-			cells = this.getCellsForAction(action.scroll);
+			cells = this.getCellsForAction(action.highlight);
+			this.highlightCells(cells, action.highlight.color,
+				action.highlight.duration, action.highlight.opacity);
 		}
-		
+
 		if (action.toggle != null)
 		{
 			this.toggleCells(this.getCellsForAction(action.toggle));
@@ -1507,13 +1509,11 @@
 			this.setCellsVisible(this.getCellsForAction(action.hide), false);
 		}
 
-		if (action.highlight != null)
+		if (action.scroll != null)
 		{
-			cells = this.getCellsForAction(action.highlight);
-			this.highlightCells(cells, action.highlight.color,
-				action.highlight.duration, action.highlight.opacity);
+			cells = this.getCellsForAction(action.scroll);
 		}
-
+		
 		if (cells.length > 0)
 		{
 			this.scrollCellToVisible(cells[0]);

+ 23 - 17
src/main/webapp/js/diagramly/EditorUi.js

@@ -6190,25 +6190,31 @@
 				    	{
 				    		// Fires cellsInserted to apply the current style to the inserted text.
 				    		// This requires the value to be empty when the event is fired.
-						cell = graph.insertVertex(graph.getDefaultParent(), null, '',
+				    		cell = graph.insertVertex(graph.getDefaultParent(), null, '',
 								graph.snap(dx), graph.snap(dy), 1, 1, 'text;' + ((html) ? 'html=1;' : ''));
-						graph.fireEvent(new mxEventObject('textInserted', 'cells', [cell]));
-						
-						// Apply value and updates the cell size to fit the text block
-						cell.value = text;
-						graph.updateCellSize(cell);
-						
-						// See http://stackoverflow.com/questions/6927719/url-regex-does-not-work-in-javascript
-						var regexp = /\b((?:[a-z][\w-]+:(?:\/{1,3}|[a-z0-9%])|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}\/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'".,<>?«»“”‘’]))/i;
+				    		graph.fireEvent(new mxEventObject('textInserted', 'cells', [cell]));
 						
-						if (regexp.test(cell.value))
-						{
-							graph.setLinkForCell(cell, cell.value);
-						}
-						
-						// Adds spacing
-						cell.geometry.width += graph.gridSize;
-						cell.geometry.height += graph.gridSize;
+				    		// Single tag is converted
+				    		if (text.charAt(0) == '<' && text.indexOf('>') == text.length - 1)
+				    		{
+				    			text = mxUtils.htmlEntities(text);
+				    		}
+				    		
+							// Apply value and updates the cell size to fit the text block
+							cell.value = text;
+							graph.updateCellSize(cell);
+							
+							// See http://stackoverflow.com/questions/6927719/url-regex-does-not-work-in-javascript
+							var regexp = /\b((?:[a-z][\w-]+:(?:\/{1,3}|[a-z0-9%])|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}\/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'".,<>?«»“”‘’]))/i;
+							
+							if (regexp.test(cell.value))
+							{
+								graph.setLinkForCell(cell, cell.value);
+							}
+							
+							// Adds spacing
+							cell.geometry.width += graph.gridSize;
+							cell.geometry.height += graph.gridSize;
 				    	}
 				    	finally
 				    	{

+ 0 - 1
src/main/webapp/js/diagramly/Init.js

@@ -244,7 +244,6 @@ function setCurrentXml(data, filename)
 // Enables offline mode
 if (urlParams['offline'] == '1' || urlParams['demo'] == '1' || urlParams['stealth'] == '1' || urlParams['local'] == '1')
 {
-	urlParams['analytics'] = '0';
 	urlParams['picker'] = '0';
 	urlParams['gapi'] = '0';
 	urlParams['db'] = '0';

+ 2 - 2
src/main/webapp/js/diagramly/Menus.js

@@ -420,7 +420,7 @@
 			action.isEnabled = isGraphEnabled;
 		}
 		
-		if (isLocalStorage)
+		if (isLocalStorage || mxClient.IS_CHROMEAPP)
 		{
 			var action = editorUi.actions.addAction('showStartScreen', function()
 			{
@@ -1957,7 +1957,7 @@
 
 		var addInsertItem = function(menu, parent, title, method)
 		{
-			if (method != 'plantUml' || EditorUi.enablePlantUml)
+			if (method != 'plantUml' || (EditorUi.enablePlantUml && !editorUi.isOffline()))
 			{
 				menu.addItem(title, null, mxUtils.bind(this, function()
 				{

+ 2 - 2
src/main/webapp/js/diagramly/Minimal.js

@@ -696,7 +696,7 @@ EditorUi.initMinimalTheme = function()
         	toggleFormat(ui);
         }));
         
-        if (EditorUi.enablePlantUml)
+        if (EditorUi.enablePlantUml && !ui.isOffline())
         {
 	        ui.actions.put('plantUml', new Action(mxResources.get('plantUml') + '...', function()
 	        {
@@ -868,7 +868,7 @@ EditorUi.initMinimalTheme = function()
 
         this.put('insertAdvanced', new Menu(mxUtils.bind(this, function(menu, parent)
         {
-            ui.menus.addMenuItems(menu, ['importText', 'createShape', 'plantUml', '-', 'importCsv', 'formatSql', 'editDiagram'], parent);
+            ui.menus.addMenuItems(menu, ['importText', 'plantUml', '-', 'formatSql', 'importCsv', '-', 'createShape', 'editDiagram'], parent);
         })));
         
         mxResources.parse('insertLayout=' + mxResources.get('layout'));

+ 1 - 1
src/main/webapp/js/diagramly/sidebar/Sidebar-WebIcons.js

@@ -468,7 +468,7 @@
 		 this.createVertexTemplateEntry(s + 'drawio2;fillColor=#1A5BA3',
 				 w * 261, h * 354, '', 'Draw.io', null, null, this.getTagsForStencil(gn, 'drawio draw io draw.io', dt).join(' ')),
 		 this.createVertexTemplateEntry(s + 'drawio3;fillColor=#1A5BA3;fontSize=15;fontColor=#1A5BA3;fontStyle=1',
-				 w * 261, h * 261, 'draw<font color="#f08707">.io</font>', 'Draw.io', null, null, this.getTagsForStencil(gn, 'drawio draw io draw.io', dt).join(' ')),
+				 w * 261, h * 261, 'draw.<font color="#f08707">io</font>', 'Draw.io', null, null, this.getTagsForStencil(gn, 'drawio draw io draw.io', dt).join(' ')),
 		 this.createVertexTemplateEntry(s + 'dribbble;fillColor=#EB548D',
 				 w * 337, h * 336, '', 'Dribbble', null, null, this.getTagsForStencil(gn, 'dribbble', dt).join(' ')),
 		 this.createVertexTemplateEntry(s + 'dropbox;fillColor=#0287EA',

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 1 - 1
src/main/webapp/js/embed-static.min.js


+ 8 - 8
src/main/webapp/js/mxgraph/EditorUi.js

@@ -975,11 +975,6 @@ EditorUi.prototype.footerHeight = 28;
  */
 EditorUi.prototype.sidebarFooterHeight = 34;
 
-/**
- * Specifies the link for the edit button in chromeless mode.
- */
-EditorUi.prototype.editButtonLink = null;
-
 /**
  * Specifies the position of the horizontal split bar. Default is 208 or 118 for
  * screen widths <= 640px.
@@ -1001,6 +996,11 @@ EditorUi.prototype.lightboxMaxFitScale = 2;
  */
 EditorUi.prototype.lightboxVerticalDivider = 4;
 
+/**
+ * Specifies if single click on horizontal split should collapse sidebar. Default is false.
+ */
+EditorUi.prototype.hsplitClickEnabled = false;
+
 /**
  * Installs the listeners to update the action states.
  */
@@ -3247,16 +3247,16 @@ EditorUi.prototype.addSplitHandler = function(elt, horizontal, dx, onChange)
 		mxEvent.consume(evt);
 	});
 	
-	mxEvent.addListener(elt, 'click', function(evt)
+	mxEvent.addListener(elt, 'click', mxUtils.bind(this, function(evt)
 	{
-		if (!ignoreClick)
+		if (!ignoreClick && this.hsplitClickEnabled)
 		{
 			var next = (last != null) ? last - dx : 0;
 			last = getValue();
 			onChange(next);
 			mxEvent.consume(evt);
 		}
-	});
+	}));
 
 	mxEvent.addGestureListeners(document, null, moveHandler, dropHandler);
 	

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 1 - 1
src/main/webapp/js/reader.min.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 521 - 520
src/main/webapp/js/viewer.min.js


+ 4 - 1
src/main/webapp/plugins/text.js

@@ -9,7 +9,10 @@ Draw.loadPlugin(function(ui)
 	// Adds action
 	ui.actions.addAction('extractText', function()
 	{
-		mxUtils.popup(ui.editor.graph.getIndexableText());
+		var dlg = new EmbedDialog(ui, ui.editor.graph.getIndexableText(),
+			null, null, null, 'Extracted Text:');
+		ui.showDialog(dlg.container, 440, 240, true, true);
+		dlg.init();
 	});
 	
 	var menu = ui.menus.get('extras');

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 4 - 0
src/main/webapp/plugins/webcola/cola.min.js


+ 763 - 0
src/main/webapp/plugins/webcola/mxWebColaAdaptor.js

@@ -0,0 +1,763 @@
+/**
+ * Copyright (c) 2006-2018, JGraph Ltd
+ * Copyright (c) 2006-2018, Gaudenz Alder
+ */
+/**
+ * Class: mxWebColaAdaptor
+ *
+ * Extends WebCola's cola object to act as both adaptor and layout in WebCola for mxGraph.
+ *
+ * Constructor: mxWebColaAdaptor
+ *
+ * Constructs a new WebCola-based adaptor for given mxGraph.
+ *
+ * Arguments:
+ *
+ * graph - <mxGraph> that contains the cells.
+ * dimension - <[]> array containing [width, height] of canvas in points
+ * movableVertices - <[]> IDs of vertices that are movable; if undefined all vertices are movable
+ * options - <{}> WebCola options for layout/adapter
+ *
+ **/
+var doNothing = function()
+  /**
+   * Empty method for default event handlers
+   */
+{
+}
+
+function mxWebColaAdaptor(graph, dimension, movableVertices, options)
+/**
+ * Conatructs a WebCola adaptor for mxGraph
+ * @param graph mxGraph instance
+ * @param dimension array containing [width, height] of drawing canvas in points
+ * @param movableVertices set containing IDs of vertices that are movable; if undefined all vertices are movable
+ * @param options WebCola options for layout/adapter
+ * @constructor
+ */
+{
+  this.graph = graph;
+  this.dimension = dimension;
+  if (typeof dimension === 'undefined')
+  {
+    this.dimension = [600, 600];
+  }
+  var layoutResult = this.graphToLayout(graph, movableVertices);
+  this.nodes = layoutResult.nodes;
+  this.links = layoutResult.links;
+  this.groups = layoutResult.groups;
+  this.cellToNode = layoutResult.cellToNode;
+  this.isStopped = false;
+  this.options = {};
+  // assign default values
+  for (var key in this.defaultValues)
+  {
+    this.options[key] = this.defaultValues[key];
+  }
+  // if options were passed, override defaults for keys available in options
+  if (options != null)
+  {
+    for (var key in options)
+    {
+      this.options[key] = options[key];
+    }
+  }
+}
+
+// default layout options
+mxWebColaAdaptor.prototype.defaultValues = {
+  doAnimations: true, // whether to show the layout as it's running
+  // doAnimations: false, // whether to show the layout as it's running
+  skipFrames: 1, // number of ticks per frame; higher is faster but more jerky
+  maxSimulationTime: 4000, // max length in ms to run the layout
+  ungrabifyWhileSimulating: false, // so you can't drag nodes during layout
+  fit: true, // on every layout reposition of nodes, fit the viewport
+  padding: 30, // padding around the simulation
+  boundingBox: undefined, // constrain layout bounds; { x1, y1, x2, y2 } or { x1, y1, w, h }
+  nodeDimensionsIncludeLabels: false, // whether labels should be included in determining the space used by a node
+
+  // layout event callbacks
+  ready: function ready() {}, // on layoutready
+  stop: function stop() {}, // on layoutstop
+
+  // positioning options
+  randomize: false, // use random node positions at beginning of layout
+  avoidOverlap: true, // if true, prevents overlap of node bounding boxes
+  handleDisconnected: true, // if true, avoids disconnected components from overlapping
+  nodeSpacing: function nodeSpacing(node) {
+    return 10;
+  }, // extra spacing around nodes
+  flow: undefined, // use DAG/tree flow layout if specified, e.g. { axis: 'y', minSeparation: 30 }
+  alignment: undefined, // relative alignment constraints on nodes, e.g. function( node ){ return { x: 0, y: 1 } }
+  gapInequalities: undefined, // list of inequality constraints for the gap between the nodes, e.g. [{"axis":"y", "left":node1, "right":node2, "gap":25}]
+
+  // different methods of specifying edge length
+  // each can be a constant numerical value or a function like `function( edge ){ return 2; }`
+  edgeLength: undefined, // sets edge length directly in simulation
+  edgeSymDiffLength: undefined, // symmetric diff edge length in simulation
+  edgeJaccardLength: undefined, // jaccard edge length in simulation
+
+  // iterations of cola algorithm; uses default values on undefined
+  unconstrIter: undefined, // unconstrained initial layout iterations
+  userConstIter: undefined, // initial layout iterations with user-specified constraints
+  allConstIter: undefined, // initial layout iterations with all constraints including non-overlap
+
+  // infinite layout options
+  keepRunning: false // overrides all other options for a forces-all-the-time mode
+};
+
+mxWebColaAdaptor.prototype.updatePositions = function()
+  /**
+   * Default method for updating positions
+   * Should be overridden by the caller/user of the adaptor
+   */
+{
+  console.log("colaAdaptor: updatePositions");
+  // TODO: do all the positions here
+}
+
+mxWebColaAdaptor.prototype.kick = function (colaAdaptor)
+/**
+ * Starts WebCola computation on the given adaptor
+ */
+{
+  console.log("colaAdaptor: step");
+
+  if ('doAnimations' in this.options && this.options.doAnimations)
+  {
+    doRendering(this.callback);
+  }
+  else
+  {
+    // run until the end
+    while (!this.process(colaAdaptor))
+    {
+    }
+  }
+}
+
+mxWebColaAdaptor.prototype.step = function (colaAdaptor)
+/**
+ * Notifies about a single layout computation step on WebCola adaptor
+ */
+{
+  if ('doAnimations' in this.options && this.options.doAnimations)
+  {
+    this.updatePositions();
+  }
+}
+
+mxWebColaAdaptor.prototype.frameSteps = function(colaAdaptor)
+/**
+ * Runs multiple ticks on WebCola adaptor until finished
+ */
+{
+  var result = void 0;
+
+  for (var i = 0; i < this.options.skipFrames && !result; i++) {
+    result = result || this.process(colaAdaptor);
+  }
+  return result;
+}
+
+mxWebColaAdaptor.prototype.process = function(colaAdaptor)
+/**
+ * Executes the whole layout computation on WebCola adaptor
+ */
+{
+  if (this.isStopped)
+  {
+    this.finish();
+    return true;
+  }
+  var result = colaAdaptor.tick();
+  if (result && this.options.keepRunning) {
+    colaAdaptor.resume();
+  }
+  return result;
+}
+
+mxWebColaAdaptor.prototype.renderingChain = function(colaAdaptor)
+/**
+ * This keeps rendering new simulation frames until end is reached
+ */
+{
+  if (this.process(colaAdaptor))
+  {
+    return;
+  }
+  doRendering(this.callback);
+}
+
+mxWebColaAdaptor.prototype.finish = function()
+{
+
+}
+
+mxWebColaAdaptor.prototype.run = function()
+  /**
+   * Runs the layout computation on given nodes/links/groups
+   * @returns Nothing
+   */
+{
+  var layout = this;
+  var options = this.options;
+
+  var colaAdaptor = layout.adaptor = cola.adaptor
+  ({
+    trigger: function (evt)
+    {
+      var START = cola.EventType ? cola.EventType.start : 'start';
+      var TICK = cola.EventType ? cola.EventType.tick : 'tick';
+      var END = cola.EventType ? cola.EventType.end : 'end';
+
+      switch (evt.type)
+      {
+        case START:
+        {
+          // colaAdaptor.start();
+        }
+        break;
+        case TICK:
+        {
+          layout.step();
+        }
+        break;
+        case END:
+        {
+          console.log("colaAdaptor: end");
+          layout.updatePositions();
+          if (!options.keepRunning)
+          {
+            layout.finish();
+          }
+        }
+        break;
+      }
+    },
+
+    kick: function ()
+    {
+      layout.kick(colaAdaptor);
+    },
+
+    finish: function()
+    {
+      layout.finish();
+    },
+
+    on: doNothing,
+
+    drag: doNothing
+  });
+
+  colaAdaptor.nodes(this.nodes)
+             .links(this.links)
+             .groups(this.groups)
+             .linkDistance(function (link)
+              {
+                return link.length;
+              });
+
+  layout.callback = function()
+  {
+    layout.renderingChain(colaAdaptor);
+  }
+
+  colaAdaptor.avoidOverlaps(options.avoidOverlap)
+             .handleDisconnected(options.handleDisconnected)
+             // .constraints(constraints)
+             // .start(100, 100, 100);
+             .start();
+  return this.adaptor;
+}
+
+// module.exports = defaultValues;
+
+function getScreenConstraints(layout, width, height)
+/**
+ * Returns a set of constraints covering limits of screen
+ * @param layout
+ * @param width
+ * @param height
+ * @returns {Array}
+ */
+{
+  var gap = 20;
+  var size = layout._nodes.length;
+  var topLeft = {x: 0, y: 0, fixed: true, index: size};
+  var bottomRight = {x: width, y: height, fixed: true, index: size + 1};
+  layout._nodes.push(topLeft);
+  layout._nodes.push(bottomRight);
+  var constraints = [];
+  for (var i = 0; i < size; i++) {
+    var index = layout._nodes[i].index;
+    constraints.push({ axis: 'x', type: 'separation', left: topLeft.index, right: index, gap: gap });
+    constraints.push({ axis: 'y', type: 'separation', left: topLeft.index, right: index, gap: gap });
+    constraints.push({ axis: 'x', type: 'separation', left: index, right: bottomRight.index, gap: gap });
+    constraints.push({ axis: 'y', type: 'separation', left: index, right: bottomRight.index, gap: gap });
+  }
+  return constraints;
+}
+
+mxWebColaAdaptor.prototype.graphToLayout = function(graph, movableVertices)
+/**
+ * Returns a WebCola layout set up for the given Draw.io graph
+ * In WebCola's TypeScript source: vertex cell -> InputNode
+ *                                 edge cell -> Link
+ *                                 parent/child -> Group
+ * @param graph Draw.io graph object
+ * @param fixedVertices Vertices that shouldn't be moved (dictionary with {id: True} pairs, id is vertex id)
+ *                      optional, if undefined all vertices are considered movable
+ * @returns list of WebCola nodes, list of WebCola links, and a dictionary from Draw.io cell ID to WebCola node ID
+ *          returned as a dictionary: {nodes: ..., links: ..., cellToNode: ...}
+ */
+{
+  var activeMaps = this.findActiveVertices(graph);  // list of all active vertices, i.e. with no collapsed parents
+  var activeVertices = activeMaps.activeVertices;  // inactive vertex to its nearest active parent map
+  var inactiveToActiveMap = activeMaps.inactiveToActiveMap;
+  var cells = graph.getModel().cells;
+  var view = graph.getView();
+  var nodeCells = {};
+  var linkCells = {};
+  var cellIds = {};
+  var edgeIds = {};
+  var colaId = 0;
+  var nodes = [];
+  var links = [];
+  // process nodes first
+  for (var id in cells)
+  {
+    var cell = cells[id];
+    var state = view.getState(cell);
+    var bounds = view.getBoundingBox(state, true);
+    var isFirst = true;
+    // if (cell.isVertex() && this.isLeafOrCollapsed(cell)) {
+    // only active vertices should be considered (i.e. not hidden by a collapsed or layouted vertex)
+    // if (cell.isVertex() && activeVertices[cell.id])
+    if (cell.isVertex() && this.isLeafOrCollapsed(cell) && activeVertices[cell.id])
+    {
+      var node = {};
+      // node.x = bounds.getCenterX();
+      // node.y = bounds.getCenterY();
+      node.width = bounds.width;
+      node.height = bounds.height;
+      node.index = colaId;
+      node.name = cell.value;
+      node.fixed = false;
+      if (typeof movableVertices !== 'undefined' && !(id in movableVertices))
+      {
+        node.fixed = true;
+      }
+      nodes.push(node);
+      cellIds[id] = colaId;
+      nodeCells[colaId] = cell;
+      colaId++;
+    }
+  }
+  // now edges can be processed as well
+  for (var id in cells)
+  {
+    var cell = cells[id];
+    var state = view.getState(cell);
+    if (cell.isEdge())
+    {
+      // attach edges to lowest active vertex corresponding to each of their terminals
+      var terminal_id1 = inactiveToActiveMap[cell.source.id];
+      var terminal_id2 = inactiveToActiveMap[cell.target.id];
+      if (terminal_id1 == terminal_id2)
+      {
+        // both terminals are under the same active parent, no need to make an invisible edge
+        continue;
+      }
+      // if either of terminals are groups, we need to insert complete graph between nodes within these groups
+      var terminal1 = cells[terminal_id1];
+      var terminal2 = cells[terminal_id2];
+      var addedLinks = [];
+      if (this.isGroup(terminal1) || this.isGroup(terminal2))
+      {
+        addedLinks = this.addGroupConstraintLinks(terminal1, terminal2, activeVertices, inactiveToActiveMap, cellIds);
+      }
+      else
+      {
+        // link = {}
+        // link.source = cellIds[cell.source.id];
+        // link.target = cellIds[cell.target.id];
+        var link = this.createLink(terminal_id1, terminal_id2, cellIds);
+        addedLinks.push(link);
+      }
+      for (var i = 0; i < addedLinks.length; i++)
+      {
+        var link = addedLinks[i];
+        links.push(link);
+        edgeIds[cell] = id;
+        linkCells[link] = cell;
+      }
+    }
+  }
+  links = this.getUniqueLinks(links);
+  // finally, groups need to be extracted
+  // mxGraph.getCellsForGroup
+  // mxGraphModel.getChildCount
+  // mxGraph.getBoundsForGroup
+  // first, get all possible parents and their children
+  var groupParents = {};
+  var directParentChildren = {};
+  for (var id in cells)
+  {
+    var cell = cells[id];
+    if (!cell.isVertex() || !this.isLeafOrCollapsed(cell))
+      continue;
+    var parent = cell.getParent();
+    if (parent.isVertex())
+    {
+      groupParents[parent.id] = parent;
+      if (!(parent.id in directParentChildren))
+      {
+        directParentChildren[parent.id] = {}
+      }
+      directParentChildren[parent.id][id] = cell;
+    }
+  }
+  // now go through all parents/children and build a group hierarchy for WebCola
+  var preliminaryGroups = [];
+  var groupId = 0;
+  var groupToParent = {}
+  for (var parentId in groupParents)
+  {
+    var parentChildren = directParentChildren[parentId];
+    var groupNodes = []
+    for (var childId in parentChildren)
+    {
+      if (activeVertices[childId])
+      {
+        groupNodes.push(cellIds[childId]);
+      }
+    }
+    preliminaryGroups.push({id: groupId, parentId: parentId, nodes: parentChildren, leaves: groupNodes, groups: []});
+    groupToParent[groupId] = parentId;
+    groupId++;
+  }
+  // here scan newly formed groups if their parent is a child of any of the nodes in any of the groups
+  for (var i = 0; i < preliminaryGroups.length; i++)
+  {
+    var parentGroup = preliminaryGroups[i];
+    var parentId = parentGroup.parentId;
+    for (var j = 0; j < preliminaryGroups.length; j++)
+    {
+      if (i == j)
+        continue;
+      var groupParentId = cells[preliminaryGroups[j].parentId].getParent().id;
+      if (parentId == groupParentId)
+        parentGroup.groups.push(j);
+    }
+  }
+  // finalize groups
+  var groups = [];
+  for (var i = 0; i < preliminaryGroups.length; i++)
+  {
+    var group = preliminaryGroups[i];
+    var graphGroup = {};
+    if (group.leaves.length > 0)
+    {
+      graphGroup["leaves"] = group.leaves;
+    }
+    if (group.groups.length > 0)
+    {
+      graphGroup["groups"] = group.groups;
+    }
+    if (graphGroup.hasOwnProperty("leaves") || graphGroup.hasOwnProperty("groups"))
+    {
+      groups.push(graphGroup);
+    }
+  }
+
+  return {nodes: nodes, links: links, groups: groups, cellToNode: cellIds};
+};
+
+mxWebColaAdaptor.prototype.createLink = function(sourceId, targetId, cellIds)
+{
+  var link = {};
+  link.source = cellIds[sourceId];
+  link.target = cellIds[targetId];
+  link.weight = 0.9;
+  link.length = 100; // TODO: replace with Graph.prototype.defaultEdgeLength; once integrated in draw.io
+  return link;
+}
+
+mxWebColaAdaptor.prototype.isLeafOrCollapsed = function(cell)
+  /**
+   * Returns true if a cell is either a leaf or a collapsed group
+   * @param cell cell to investigate
+   * @returns true if a cell is either a leaf or a collapsed group, false otherwise
+   */
+{
+  if (cell.isCollapsed() ||
+      cell.children == null || cell.children.length == 0 ||
+      typeof this.graph.getCellStyle(cell)['childLayout'] != 'undefined')
+  {
+    return true;
+  }
+  return false;
+}
+
+mxWebColaAdaptor.prototype.findActiveVertices = function(graph)
+  /**
+   * Scans all groups and finds active vertices, as well as an inactive-vertex-to-active-parent map
+   * @param graph input graph
+   */
+{
+  var inactiveToActiveMap = {};
+  var activeVertices = {};
+  var root = graph.getModel().root;
+  var cellsToExplore = [{vertex: root, isActive: true, activeParent: root}]
+  while (cellsToExplore.length > 0)
+  {
+    var currentCellInfo = cellsToExplore.shift();
+    var cell = currentCellInfo.vertex;
+    if (cell.isEdge())
+    {
+      // cut at edge group, those are ignored
+      continue;
+    }
+    var isActive = currentCellInfo.isActive;
+    var activeParent = currentCellInfo.activeParent;
+    if (cell.isVertex())
+    {
+      if (isActive)
+      {
+        activeVertices[cell.id] = true;
+      }
+      else
+      {
+        activeVertices[cell.id] = false;
+      }
+    }
+    // prepare children
+    // child can be active only if any of its parents is not collapsed
+    var isActive = isActive && !this.isLeafOrCollapsed(cell);
+    var children = cell.children;
+    if (children != null && children.length > 0)
+    {
+      for (var i = 0; i < children.length; i++)
+      {
+        var child = children[i];
+        var childActiveParent = isActive? child: activeParent;
+        cellsToExplore.push({vertex: child, isActive: isActive, activeParent: childActiveParent});
+        if (child.isVertex())
+        {
+          inactiveToActiveMap[child.id] = childActiveParent.id;
+        }
+      }
+    }
+  }
+  return {activeVertices: activeVertices, inactiveToActiveMap: inactiveToActiveMap};
+}
+
+mxWebColaAdaptor.prototype.getActiveVerticesInGroup = function(groupCell, activeVertices, includeCollapsedGroups)
+  /**
+   * Scans all children in group and returns all active vertices inside group
+   * This method is for creating redundant edges between members of groups to simulate group edges in WebCola
+   * See https://github.com/tgdwyer/WebCola/issues/38
+   * @param groupCell group cell
+   */
+{
+  var activeChildren = [];
+  if (includeCollapsedGroups && this.isLeafOrCollapsed(groupCell))
+  {
+    activeChildren.push(groupCell);
+  }
+  var cellsToExplore = [groupCell];
+  while (cellsToExplore.length > 0)
+  {
+    var cell = cellsToExplore.shift();
+    if (!cell.isVertex() || !activeVertices[cell])
+    {
+      // cut at edge group, those are ignored
+      continue;
+    }
+    if (this.isLeafOrCollapsed(cell))
+    {
+      activeChildren.push(cell);
+    }
+    else
+    {
+      var children = cell.children;
+      if (children == null || children.length == 0)
+      {
+        continue;
+      }
+      cellsToExplore = cellsToExplore.concat(children);
+    }
+  }
+  return activeChildren;
+}
+
+mxWebColaAdaptor.prototype.getAllVerticesInGroup = function(groupCell, includeCollapsedGroups)
+  /**
+   * Scans all children in group and returns all active vertices inside group
+   * This method is for creating redundant edges between members of groups to simulate group edges in WebCola
+   * See https://github.com/tgdwyer/WebCola/issues/38
+   * @param groupCell group cell
+   */
+{
+  var result = [];
+  if (includeCollapsedGroups && this.isLeafOrCollapsed(groupCell))
+  {
+    result.push(groupCell);
+  }
+  var cellsToExplore = [groupCell];
+  while (cellsToExplore.length > 0)
+  {
+    var cell = cellsToExplore.shift();
+    if (!cell.isVertex())
+    {
+      // cut at edge group, those are ignored
+      continue;
+    }
+    if (this.isLeafOrCollapsed(cell))
+    {
+      result.push(cell);
+    }
+    else
+    {
+      var children = cell.children;
+      if (children == null || children.length == 0)
+      {
+        continue;
+      }
+      cellsToExplore = cellsToExplore.concat(children);
+    }
+  }
+  return result;
+}
+
+mxWebColaAdaptor.prototype.hasVertexChildren = function(cell)
+  /**
+   * Returns true if a (group) cell has vertex children in its subtree
+   * @param cell (group) cell
+   * @returns true if if a (group) cell has vertex children in its subtree, false otherwise
+   */
+{
+  if (cell.children == null || cell.children.length == 0)
+  {
+    return false;
+  }
+  var toBeExamined = []
+  toBeExamined = toBeExamined.concat(cell.children);
+  while (toBeExamined.length > 0)
+  {
+    var cell = toBeExamined.shift();
+    if (cell.isVertex())
+      return true;
+    if (cell.children != null && cell.children.length > 0)
+    {
+      toBeExamined = toBeExamined.concat(cell.children);
+    }
+  }
+  return false;
+}
+
+mxWebColaAdaptor.prototype.isInCollapsedTree = function(cell)
+{
+  // scan the material path for collapsed group node
+  while (cell != null)
+  {
+    cell = cell.getParent();
+    if (cell != null && cell.isCollapsed())
+    {
+      return true;
+    }
+  }
+  return false;
+}
+
+mxWebColaAdaptor.prototype.isGroup = function(cell)
+  /**
+   * Returns true if cell is a group (has children)
+   * @param cell cell
+   * @returns true if cell is a group (has children); false otherwise
+   */
+{
+  return cell.children != null && cell.children.length > 0;
+}
+
+mxWebColaAdaptor.prototype.addGroupConstraintLinks = function(groupA, groupB, activeVertices, inactiveToActiveMap, cellIds)
+  /**
+   * Adds edges between vertex and group or two groups. Each vertex child of a group must be connected to the vertex/
+   * group, as this way WebCola simulates edges on the group level (as groups don't exist as vertices in WebCola)
+   * @param rootCell root cell
+   */
+{
+  var result = []
+  // var childrenA = this.getActiveVerticesInGroup(groupA, activeVertices, false);
+  // var childrenB = this.getActiveVerticesInGroup(groupB, activeVertices, false);
+  var childrenA = [groupA];
+  var childrenB = [groupB];
+  if (!groupA.isCollapsed())
+  {
+    childrenA = this.getAllVerticesInGroup(groupA, activeVertices, false);
+  }
+  if (!groupB.isCollapsed())
+  {
+    childrenB = this.getAllVerticesInGroup(groupB, activeVertices, false);
+  }
+  if (childrenA == null || childrenA.length == 0 || childrenB == null || childrenB.length == 0)
+    return result;
+  for (var i = 0; i < childrenA.length; i++)
+  {
+    var childA_Id = inactiveToActiveMap[childrenA[i].id];
+    for (var j = 0; j < childrenB.length; j++)
+    {
+      var childB_Id = inactiveToActiveMap[childrenB[j].id];
+      var link = this.createLink(childA_Id, childB_Id, cellIds);
+      result.push(link);
+    }
+  }
+  return result;
+}
+
+mxWebColaAdaptor.prototype.getUniqueLinks = function(links)
+  /**
+   * Returns an array of unique links from an array of links
+   * @param links array of links containing duplicate links
+   * @returns array of unique links
+   */
+{
+  var result = [];
+  // TODO: this part is inefficient - O(n^2); Theta(n) should be possible with hashmap
+  for (var i = 0; i < links.length; i++)
+  {
+    var link = links[i];
+    var shouldBeAdded = true;
+    for (var j = 0; j < result.length; j++)
+    {
+      var existingLink = result[j];
+      if (link.source == existingLink.source && link.target == existingLink.target)
+      {
+        shouldBeAdded = false;
+        break;
+      }
+    }
+    if (shouldBeAdded)
+    {
+      result.push(link);
+    }
+  }
+  return result;
+}
+
+var doRendering = void 0;
+
+if ((typeof window === "undefined" ? "undefined" : typeof(window)) !== ( true ? "undefined" : typeof(undefined)))
+{
+  doRendering = window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.msRequestAnimationFrame;
+}
+else
+{
+  // if not available, all you get is immediate calls
+  function doRendering(callback)
+  {
+    callback();
+  };
+}

+ 303 - 0
src/main/webapp/plugins/webcola/mxWebColaLayout.js

@@ -0,0 +1,303 @@
+/**
+ * Copyright (c) 2006-2018, JGraph Ltd
+ * Copyright (c) 2006-2018, Gaudenz Alder
+ */
+/**
+ * Class: mxWebColaLayout
+ *
+ * Extends <mxGraphLayout> to implement a WebCola-based layout.
+ *
+ * Example:
+ *
+ * (code)
+ * var layout = new mxWebColaLayout(graph);
+ * layout.execute(graph.getDefaultParent());
+ * (end)
+ *
+ * Constructor: mxWebColaLayout
+ *
+ * Constructs a new WebCola-based layout for the graph.
+ *
+ * Arguments:
+ *
+ * graph - <mxGraph> that contains the cells.
+ *
+ **/
+function mxWebColaLayout(graph, layoutType)
+/**
+ * Constructs a WebCola-based layout
+ * @param graph <mxGraph> that contains the cells.
+ * @param layoutType Type of WebCola layout
+ */
+{
+  mxGraphLayout.call(this, graph);
+  this.layoutType = layoutType;
+};
+
+mxWebColaLayout.prototype = new mxGraphLayout();
+mxWebColaLayout.prototype.constructor = mxWebColaLayout;
+
+mxWebColaLayout.prototype.layoutType = null;
+
+mxWebColaLayout.prototype.execute = function(parent)
+  /**
+   * Runs re-layouting of the portion of a graph from a given starting cell
+   * @param parent starting cell
+   */
+{
+  var movableVertices = this.getReachableVertices(parent);
+  this.layout = new mxWebColaAdaptor(this.graph, [600, 600], movableVertices);
+  var self = this;
+  var update = function () {
+    console.log("mxColaLayout: update");
+    self.updateGraph();
+  };
+  this.layout.updatePositions = update;
+  this.resetGraph(this.graph);
+  var finalLayout = this.computePositions(this.layout);
+};
+
+mxWebColaLayout.prototype.getReachableVertices = function(parent)
+  /***
+   * Finds all vertices reachable from a given parent
+   * @param parent starting cell of a search
+   * @returns dictionary of vertice IDs that are reachable in the form of {id: True}
+   *          if undefined is returned, it means parent was not provided and it should be interpreted as all vertices
+   *          are reachable
+   */
+{
+  if (parent == undefined)
+    return undefined;
+  // first, get all incidental edges in a sub-tree (or a connected component if with loops)
+  var edges = this.graph.getEdges(parent, null, true, true, true, true);
+  // now all vertices that are reachable are subject to change; unreachable should be fixed
+  var reachableVertices = void(0);
+  if (edges.length != 0)
+  {
+    var reachableVertices = {};
+    for (var i = 0; i < edges.length; i++)
+    {
+      var edge = edges[i];
+      reachableVertices[edge.source.id] = true;
+      reachableVertices[edge.target.id] = true;
+    }
+  }
+  // vertices connected by returned edges must be allowed to move, rest must be fixed in place
+  return reachableVertices;
+}
+
+mxWebColaLayout.prototype.computePositions = function()
+  /**
+   * Executes layout to compute positions
+   */
+{
+  return this.layout.run();
+}
+
+mxWebColaLayout.prototype.resetGraph = function(graph)
+  /**
+   * Resets initial vertex positions
+   */
+{
+  var model = graph.getModel();
+  var cells = model.cells;
+  var view = graph.getView();
+  for (var id in cells)
+  {
+    var cell = cells[id];
+    var state = view.getState(cell);
+    var bounds = view.getBoundingBox(state, true);
+    var isFirst = true;
+    if (cell.isVertex()) {
+      var geometry = model.getGeometry(cell);
+      if (geometry != null && typeof geometry != "undefined")
+      {
+        geometry = geometry.clone();
+        geometry.offset = null;
+        model.setGeometry(cell, geometry);
+      }
+    }
+  }
+}
+
+mxWebColaLayout.prototype.getGroupBounds = function(model, groupCell)
+  /**
+   * Computes bounds of a group as boundary encompassing all children of a group
+   * @param model graph model
+   * @param groupCell starting group
+   * @returns boundaries of all children inside a group
+   */
+{
+  var minX =  1000000;
+  var minY =  1000000;
+  var maxX = -1000000;
+  var maxY = -1000000;
+  if (groupCell.children == null || groupCell.children.length == 0)
+    return null;
+  var cellsToVisit = [];
+  cellsToVisit = cellsToVisit.concat(groupCell.children);
+  while (cellsToVisit.length > 0)
+  {
+    var child = cellsToVisit.shift();
+    if (child.isVertex())
+    {
+      if (this.layout.isLeafOrCollapsed(child))
+      {
+        var geometry = model.getGeometry(child);
+        if (geometry != null && typeof geometry != "undefined")
+        {
+          if (geometry.x < minX)
+          {
+            minX = geometry.x;
+          }
+          if (geometry.y < minY)
+          {
+            minY = geometry.y;
+          }
+          if (geometry.x + geometry.width > maxX)
+          {
+            maxX = geometry.x + geometry.width;
+          }
+          if (geometry.y + geometry.height > maxY)
+          {
+            maxY = geometry.y + geometry.height;
+          }
+        }
+      }
+      else
+      {
+        cellsToVisit = cellsToVisit.concat(child.children);
+      }
+    }
+  }
+  var bounds = model.getGeometry(groupCell).clone();
+  bounds.x = minX;
+  bounds.y = minY;
+  bounds.width = maxX - minX;
+  bounds.height = maxY - minY;
+  return bounds;
+}
+
+mxWebColaLayout.prototype.adjustChildOffsets = function(model, groupCell)
+  /**
+   * Adjusts offset of child vertices to be relative to parent groups
+   * @param model graph model
+   * @param groupCell starting group cell
+   */
+{
+  if (groupCell.children == null || groupCell.children.length == 0)
+    return;
+  var groupBounds = model.getGeometry(groupCell);
+  var offsetX = groupBounds.x;
+  var offsetY = groupBounds.y;
+  var cellsToVisit = [];
+  cellsToVisit = cellsToVisit.concat(groupCell.children);
+  while (cellsToVisit.length > 0)
+  {
+    var child = cellsToVisit.shift();
+    if (child.isVertex())
+    {
+      if (this.layout.isLeafOrCollapsed(child))
+      {
+        var geometry = model.getGeometry(child);
+        if (geometry != null && typeof geometry != "undefined")
+        {
+          geometry = geometry.clone();
+          geometry.x = geometry.x - offsetX;
+          geometry.y = geometry.y - offsetY;
+          model.setGeometry(child, geometry);
+        }
+      }
+      else
+      {
+        cellsToVisit = cellsToVisit.concat(child.children);
+      }
+    }
+  }
+}
+
+mxWebColaLayout.prototype.updateGraph = function()
+  /**
+   * Updates graph based on layout's vertex/group positions
+   */
+{
+  console.log("updating graph");
+  // find X, Y ranges first
+  var minX = 1000000;
+  var maxX = -1000000;
+  var minY = 1000000;
+  var maxY = -1000000;
+
+  for (var i = 0; i < this.layout.adaptor._nodes.length; i++)
+  {
+    var node = this.layout.adaptor._nodes[i];
+    var x = node.x;
+    var y = node.y;
+    minX = Math.min(minX, x);
+    minY = Math.min(minY, y);
+    maxX = Math.max(maxX, x);
+    maxY = Math.max(maxY, y);
+  }
+
+  var spanX = maxX - minX;
+  var spanY = maxY - minY;
+
+  var model = this.graph.getModel();
+  model.beginUpdate();
+  try
+  {
+    var cells = model.cells;
+    var view = this.graph.getView();
+    // scan leaves and edges
+    for (var id in cells)
+    {
+      var cell = cells[id];
+      var state = view.getState(cell);
+      var bounds = view.getBoundingBox(state, true);
+      if (cell.isVertex() && this.layout.isLeafOrCollapsed(cell))
+      {
+        var nodeId = this.layout.cellToNode[id];
+        if (typeof nodeId == "undefined")
+          continue;
+        var node = this.layout.adaptor._nodes[nodeId];
+        var geometry = model.getGeometry(cell);
+        if (geometry != null && typeof geometry != "undefined")
+        {
+          geometry = geometry.clone();
+          geometry.x = node.x - minX;
+          geometry.y = node.y - minY;
+          model.setGeometry(cell, geometry);
+        }
+        else
+        {
+          alert("vertex cell id:" + id + " has no geometry!");
+        }
+      }
+      else if (cell.isEdge())
+      {
+        this.graph.resetEdge(cell);
+      }
+    }
+    // scan groups
+    for (var id in cells)
+    {
+      var cell = cells[id];
+      var state = view.getState(cell);
+      var bounds = view.getBoundingBox(state, true);
+      if (cell.isVertex() && !this.layout.isLeafOrCollapsed(cell))
+      {
+        var bounds = this.getGroupBounds(model, cell);
+        if (bounds != null && typeof bounds != "undefined")
+        {
+          model.setGeometry(cell, bounds);
+          this.adjustChildOffsets(model, cell);
+        }
+      }
+    }
+  }
+  finally
+  {
+    model.endUpdate();
+  }
+
+}

+ 31 - 0
src/main/webapp/plugins/webcola/webcola.js

@@ -0,0 +1,31 @@
+/**
+ * WebCola layout plugin.
+ */
+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");
+	
+	// Adds resource for action
+	mxResources.parse('webColaLayout=WebCola Layout...');
+
+	// Adds action
+	ui.actions.addAction('Apply WebCola layout', function()
+	{
+		var graph = ui.editor.graph;
+		var layout = mxWebColaLayout(graph);
+		var parent = graph.getDefaultParent(); 
+		layout.execute(parent);
+	});
+	
+	var menu = ui.menus.get('extras');
+	var oldFunct = menu.funct;
+	
+	menu.funct = function(menu, parent)
+	{
+		oldFunct.apply(this, arguments);
+		
+		ui.menus.addMenuItems(menu, ['-', 'webColaLayout'], parent);
+	};
+});