summaryrefslogtreecommitdiff
path: root/src/js/handler
diff options
context:
space:
mode:
authoradhitya2016-04-11 15:10:54 +0000
committeradhitya2016-04-11 15:10:54 +0000
commit92f3207b50a1caca07df5c5b238212af3358905b (patch)
tree38c92f9649c6f1016d2ef70fa2fd33c86b437cba /src/js/handler
parentab5fb6e125d82fdd5818aea3ce370c43c2293ddd (diff)
downloadxcos-on-web-92f3207b50a1caca07df5c5b238212af3358905b.tar.gz
xcos-on-web-92f3207b50a1caca07df5c5b238212af3358905b.tar.bz2
xcos-on-web-92f3207b50a1caca07df5c5b238212af3358905b.zip
Revert last two commits - Keyboard shortcuts are not working
Diffstat (limited to 'src/js/handler')
-rw-r--r--src/js/handler/mxCellHighlight.js271
-rw-r--r--src/js/handler/mxCellMarker.js419
-rw-r--r--src/js/handler/mxCellTracker.js149
-rw-r--r--src/js/handler/mxConnectionHandler.js1969
-rw-r--r--src/js/handler/mxConstraintHandler.js308
-rw-r--r--src/js/handler/mxEdgeHandler.js1529
-rw-r--r--src/js/handler/mxEdgeSegmentHandler.js284
-rw-r--r--src/js/handler/mxElbowEdgeHandler.js248
-rw-r--r--src/js/handler/mxGraphHandler.js916
-rw-r--r--src/js/handler/mxKeyHandler.js402
-rw-r--r--src/js/handler/mxPanningHandler.js390
-rw-r--r--src/js/handler/mxRubberband.js348
-rw-r--r--src/js/handler/mxSelectionCellsHandler.js260
-rw-r--r--src/js/handler/mxTooltipHandler.js317
-rw-r--r--src/js/handler/mxVertexHandler.js753
15 files changed, 8563 insertions, 0 deletions
diff --git a/src/js/handler/mxCellHighlight.js b/src/js/handler/mxCellHighlight.js
new file mode 100644
index 0000000..f967f00
--- /dev/null
+++ b/src/js/handler/mxCellHighlight.js
@@ -0,0 +1,271 @@
+/**
+ * $Id: mxCellHighlight.js,v 1.25 2012-09-27 14:43:40 boris Exp $
+ * Copyright (c) 2006-2010, JGraph Ltd
+ */
+/**
+ * Class: mxCellHighlight
+ *
+ * A helper class to highlight cells. Here is an example for a given cell.
+ *
+ * (code)
+ * var highlight = new mxCellHighlight(graph, '#ff0000', 2);
+ * highlight.highlight(graph.view.getState(cell)));
+ * (end)
+ *
+ * Constructor: mxCellHighlight
+ *
+ * Constructs a cell highlight.
+ */
+function mxCellHighlight(graph, highlightColor, strokeWidth)
+{
+ if (graph != null)
+ {
+ this.graph = graph;
+ this.highlightColor = (highlightColor != null) ? highlightColor : mxConstants.DEFAULT_VALID_COLOR;
+ this.strokeWidth = (strokeWidth != null) ? strokeWidth : mxConstants.HIGHLIGHT_STROKEWIDTH;
+
+ // Updates the marker if the graph changes
+ this.repaintHandler = mxUtils.bind(this, function()
+ {
+ this.repaint();
+ });
+
+ this.graph.getView().addListener(mxEvent.SCALE, this.repaintHandler);
+ this.graph.getView().addListener(mxEvent.TRANSLATE, this.repaintHandler);
+ this.graph.getView().addListener(mxEvent.SCALE_AND_TRANSLATE, this.repaintHandler);
+ this.graph.getModel().addListener(mxEvent.CHANGE, this.repaintHandler);
+
+ // Hides the marker if the current root changes
+ this.resetHandler = mxUtils.bind(this, function()
+ {
+ this.hide();
+ });
+
+ this.graph.getView().addListener(mxEvent.DOWN, this.resetHandler);
+ this.graph.getView().addListener(mxEvent.UP, this.resetHandler);
+ }
+};
+
+/**
+ * Variable: keepOnTop
+ *
+ * Specifies if the highlights should appear on top of everything
+ * else in the overlay pane. Default is false.
+ */
+mxCellHighlight.prototype.keepOnTop = false;
+
+/**
+ * Variable: graph
+ *
+ * Reference to the enclosing <mxGraph>.
+ */
+mxCellHighlight.prototype.graph = true;
+
+/**
+ * Variable: state
+ *
+ * Reference to the <mxCellState>.
+ */
+mxCellHighlight.prototype.state = null;
+
+/**
+ * Variable: spacing
+ *
+ * Specifies the spacing between the highlight for vertices and the vertex.
+ * Default is 2.
+ */
+mxCellHighlight.prototype.spacing = 2;
+
+/**
+ * Variable: resetHandler
+ *
+ * Holds the handler that automatically invokes reset if the highlight
+ * should be hidden.
+ */
+mxCellHighlight.prototype.resetHandler = null;
+
+/**
+ * Function: setHighlightColor
+ *
+ * Sets the color of the rectangle used to highlight drop targets.
+ *
+ * Parameters:
+ *
+ * color - String that represents the new highlight color.
+ */
+mxCellHighlight.prototype.setHighlightColor = function(color)
+{
+ this.highlightColor = color;
+
+ if (this.shape != null)
+ {
+ if (this.shape.dialect == mxConstants.DIALECT_SVG)
+ {
+ this.shape.innerNode.setAttribute('stroke', color);
+ }
+ else if (this.shape.dialect == mxConstants.DIALECT_VML)
+ {
+ this.shape.node.strokecolor = color;
+ }
+ }
+};
+
+/**
+ * Function: drawHighlight
+ *
+ * Creates and returns the highlight shape for the given state.
+ */
+mxCellHighlight.prototype.drawHighlight = function()
+{
+ this.shape = this.createShape();
+ this.repaint();
+
+ if (!this.keepOnTop && this.shape.node.parentNode.firstChild != this.shape.node)
+ {
+ this.shape.node.parentNode.insertBefore(this.shape.node, this.shape.node.parentNode.firstChild);
+ }
+
+ // Workaround to force a repaint in AppleWebKit
+ if (this.graph.model.isEdge(this.state.cell))
+ {
+ mxUtils.repaintGraph(this.graph, this.shape.points[0]);
+ }
+};
+
+/**
+ * Function: createShape
+ *
+ * Creates and returns the highlight shape for the given state.
+ */
+mxCellHighlight.prototype.createShape = function()
+{
+ var shape = null;
+
+ if (this.graph.model.isEdge(this.state.cell))
+ {
+ shape = new mxPolyline(this.state.absolutePoints,
+ this.highlightColor, this.strokeWidth);
+ }
+ else
+ {
+ shape = new mxRectangleShape( new mxRectangle(),
+ null, this.highlightColor, this.strokeWidth);
+ }
+
+ shape.dialect = (this.graph.dialect != mxConstants.DIALECT_SVG) ?
+ mxConstants.DIALECT_VML : mxConstants.DIALECT_SVG;
+ shape.init(this.graph.getView().getOverlayPane());
+ mxEvent.redirectMouseEvents(shape.node, this.graph, this.state);
+
+ return shape;
+};
+
+
+/**
+ * Function: repaint
+ *
+ * Updates the highlight after a change of the model or view.
+ */
+mxCellHighlight.prototype.repaint = function()
+{
+ if (this.state != null && this.shape != null)
+ {
+ if (this.graph.model.isEdge(this.state.cell))
+ {
+ this.shape.points = this.state.absolutePoints;
+ }
+ else
+ {
+ this.shape.bounds = new mxRectangle(this.state.x - this.spacing, this.state.y - this.spacing,
+ this.state.width + 2 * this.spacing, this.state.height + 2 * this.spacing);
+ }
+
+ // Uses cursor from shape in highlight
+ if (this.state.shape != null)
+ {
+ this.shape.setCursor(this.state.shape.getCursor());
+ }
+
+ var alpha = (!this.graph.model.isEdge(this.state.cell)) ? Number(this.state.style[mxConstants.STYLE_ROTATION] || '0') : 0;
+
+ // Event-transparency
+ if (this.shape.dialect == mxConstants.DIALECT_SVG)
+ {
+ this.shape.node.setAttribute('style', 'pointer-events:none;');
+
+ if (alpha != 0)
+ {
+ var cx = this.state.getCenterX();
+ var cy = this.state.getCenterY();
+ var transform = 'rotate(' + alpha + ' ' + cx + ' ' + cy + ')';
+
+ this.shape.node.setAttribute('transform', transform);
+ }
+ }
+ else
+ {
+ this.shape.node.style.background = '';
+
+ if (alpha != 0)
+ {
+ this.shape.node.rotation = alpha;
+ }
+ }
+
+ this.shape.redraw();
+ }
+};
+
+/**
+ * Function: hide
+ *
+ * Resets the state of the cell marker.
+ */
+mxCellHighlight.prototype.hide = function()
+{
+ this.highlight(null);
+};
+
+/**
+ * Function: mark
+ *
+ * Marks the <markedState> and fires a <mark> event.
+ */
+mxCellHighlight.prototype.highlight = function(state)
+{
+ if (this.state != state)
+ {
+ if (this.shape != null)
+ {
+ this.shape.destroy();
+ this.shape = null;
+ }
+
+ this.state = state;
+
+ if (this.state != null)
+ {
+ this.drawHighlight();
+ }
+ }
+};
+
+/**
+ * Function: destroy
+ *
+ * Destroys the handler and all its resources and DOM nodes.
+ */
+mxCellHighlight.prototype.destroy = function()
+{
+ this.graph.getView().removeListener(this.repaintHandler);
+ this.graph.getModel().removeListener(this.repaintHandler);
+
+ this.graph.getView().removeListener(this.resetHandler);
+ this.graph.getModel().removeListener(this.resetHandler);
+
+ if (this.shape != null)
+ {
+ this.shape.destroy();
+ this.shape = null;
+ }
+};
diff --git a/src/js/handler/mxCellMarker.js b/src/js/handler/mxCellMarker.js
new file mode 100644
index 0000000..b336278
--- /dev/null
+++ b/src/js/handler/mxCellMarker.js
@@ -0,0 +1,419 @@
+/**
+ * $Id: mxCellMarker.js,v 1.30 2011-07-15 12:57:50 gaudenz Exp $
+ * Copyright (c) 2006-2010, JGraph Ltd
+ */
+/**
+ * Class: mxCellMarker
+ *
+ * A helper class to process mouse locations and highlight cells.
+ *
+ * Helper class to highlight cells. To add a cell marker to an existing graph
+ * for highlighting all cells, the following code is used:
+ *
+ * (code)
+ * var marker = new mxCellMarker(graph);
+ * graph.addMouseListener({
+ * mouseDown: function() {},
+ * mouseMove: function(sender, me)
+ * {
+ * marker.process(me);
+ * },
+ * mouseUp: function() {}
+ * });
+ * (end)
+ *
+ * Event: mxEvent.MARK
+ *
+ * Fires after a cell has been marked or unmarked. The <code>state</code>
+ * property contains the marked <mxCellState> or null if no state is marked.
+ *
+ * Constructor: mxCellMarker
+ *
+ * Constructs a new cell marker.
+ *
+ * Parameters:
+ *
+ * graph - Reference to the enclosing <mxGraph>.
+ * validColor - Optional marker color for valid states. Default is
+ * <mxConstants.DEFAULT_VALID_COLOR>.
+ * invalidColor - Optional marker color for invalid states. Default is
+ * <mxConstants.DEFAULT_INVALID_COLOR>.
+ * hotspot - Portion of the width and hight where a state intersects a
+ * given coordinate pair. A value of 0 means always highlight. Default is
+ * <mxConstants.DEFAULT_HOTSPOT>.
+ */
+function mxCellMarker(graph, validColor, invalidColor, hotspot)
+{
+ if (graph != null)
+ {
+ this.graph = graph;
+ this.validColor = (validColor != null) ? validColor : mxConstants.DEFAULT_VALID_COLOR;
+ this.invalidColor = (validColor != null) ? invalidColor : mxConstants.DEFAULT_INVALID_COLOR;
+ this.hotspot = (hotspot != null) ? hotspot : mxConstants.DEFAULT_HOTSPOT;
+
+ this.highlight = new mxCellHighlight(graph);
+ }
+};
+
+/**
+ * Extends mxEventSource.
+ */
+mxCellMarker.prototype = new mxEventSource();
+mxCellMarker.prototype.constructor = mxCellMarker;
+
+/**
+ * Variable: graph
+ *
+ * Reference to the enclosing <mxGraph>.
+ */
+mxCellMarker.prototype.graph = null;
+
+/**
+ * Variable: enabled
+ *
+ * Specifies if the marker is enabled. Default is true.
+ */
+mxCellMarker.prototype.enabled = true;
+
+/**
+ * Variable: hotspot
+ *
+ * Specifies the portion of the width and height that should trigger
+ * a highlight. The area around the center of the cell to be marked is used
+ * as the hotspot. Possible values are between 0 and 1. Default is
+ * mxConstants.DEFAULT_HOTSPOT.
+ */
+mxCellMarker.prototype.hotspot = mxConstants.DEFAULT_HOTSPOT;
+
+/**
+ * Variable: hotspotEnabled
+ *
+ * Specifies if the hotspot is enabled. Default is false.
+ */
+mxCellMarker.prototype.hotspotEnabled = false;
+
+/**
+ * Variable: validColor
+ *
+ * Holds the valid marker color.
+ */
+mxCellMarker.prototype.validColor = null;
+
+/**
+ * Variable: invalidColor
+ *
+ * Holds the invalid marker color.
+ */
+mxCellMarker.prototype.invalidColor = null;
+
+/**
+ * Variable: currentColor
+ *
+ * Holds the current marker color.
+ */
+mxCellMarker.prototype.currentColor = null;
+
+/**
+ * Variable: validState
+ *
+ * Holds the marked <mxCellState> if it is valid.
+ */
+mxCellMarker.prototype.validState = null;
+
+/**
+ * Variable: markedState
+ *
+ * Holds the marked <mxCellState>.
+ */
+mxCellMarker.prototype.markedState = null;
+
+/**
+ * Function: setEnabled
+ *
+ * Enables or disables event handling. This implementation
+ * updates <enabled>.
+ *
+ * Parameters:
+ *
+ * enabled - Boolean that specifies the new enabled state.
+ */
+mxCellMarker.prototype.setEnabled = function(enabled)
+{
+ this.enabled = enabled;
+};
+
+/**
+ * Function: isEnabled
+ *
+ * Returns true if events are handled. This implementation
+ * returns <enabled>.
+ */
+mxCellMarker.prototype.isEnabled = function()
+{
+ return this.enabled;
+};
+
+/**
+ * Function: setHotspot
+ *
+ * Sets the <hotspot>.
+ */
+mxCellMarker.prototype.setHotspot = function(hotspot)
+{
+ this.hotspot = hotspot;
+};
+
+/**
+ * Function: getHotspot
+ *
+ * Returns the <hotspot>.
+ */
+mxCellMarker.prototype.getHotspot = function()
+{
+ return this.hotspot;
+};
+
+/**
+ * Function: setHotspotEnabled
+ *
+ * Specifies whether the hotspot should be used in <intersects>.
+ */
+mxCellMarker.prototype.setHotspotEnabled = function(enabled)
+{
+ this.hotspotEnabled = enabled;
+};
+
+/**
+ * Function: isHotspotEnabled
+ *
+ * Returns true if hotspot is used in <intersects>.
+ */
+mxCellMarker.prototype.isHotspotEnabled = function()
+{
+ return this.hotspotEnabled;
+};
+
+/**
+ * Function: hasValidState
+ *
+ * Returns true if <validState> is not null.
+ */
+mxCellMarker.prototype.hasValidState = function()
+{
+ return this.validState != null;
+};
+
+/**
+ * Function: getValidState
+ *
+ * Returns the <validState>.
+ */
+mxCellMarker.prototype.getValidState = function()
+{
+ return this.validState;
+};
+
+/**
+ * Function: getMarkedState
+ *
+ * Returns the <markedState>.
+ */
+mxCellMarker.prototype.getMarkedState = function()
+{
+ return this.markedState;
+};
+
+/**
+ * Function: reset
+ *
+ * Resets the state of the cell marker.
+ */
+mxCellMarker.prototype.reset = function()
+{
+ this.validState = null;
+
+ if (this.markedState != null)
+ {
+ this.markedState = null;
+ this.unmark();
+ }
+};
+
+/**
+ * Function: process
+ *
+ * Processes the given event and cell and marks the state returned by
+ * <getState> with the color returned by <getMarkerColor>. If the
+ * markerColor is not null, then the state is stored in <markedState>. If
+ * <isValidState> returns true, then the state is stored in <validState>
+ * regardless of the marker color. The state is returned regardless of the
+ * marker color and valid state.
+ */
+mxCellMarker.prototype.process = function(me)
+{
+ var state = null;
+
+ if (this.isEnabled())
+ {
+ state = this.getState(me);
+ var isValid = (state != null) ? this.isValidState(state) : false;
+ var color = this.getMarkerColor(me.getEvent(), state, isValid);
+
+ if (isValid)
+ {
+ this.validState = state;
+ }
+ else
+ {
+ this.validState = null;
+ }
+
+ if (state != this.markedState || color != this.currentColor)
+ {
+ this.currentColor = color;
+
+ if (state != null && this.currentColor != null)
+ {
+ this.markedState = state;
+ this.mark();
+ }
+ else if (this.markedState != null)
+ {
+ this.markedState = null;
+ this.unmark();
+ }
+ }
+ }
+
+ return state;
+};
+
+/**
+ * Function: markCell
+ *
+ * Marks the given cell using the given color, or <validColor> if no color is specified.
+ */
+mxCellMarker.prototype.markCell = function(cell, color)
+{
+ var state = this.graph.getView().getState(cell);
+
+ if (state != null)
+ {
+ this.currentColor = (color != null) ? color : this.validColor;
+ this.markedState = state;
+ this.mark();
+ }
+};
+
+/**
+ * Function: mark
+ *
+ * Marks the <markedState> and fires a <mark> event.
+ */
+mxCellMarker.prototype.mark = function()
+{
+ this.highlight.setHighlightColor(this.currentColor);
+ this.highlight.highlight(this.markedState);
+ this.fireEvent(new mxEventObject(mxEvent.MARK, 'state', this.markedState));
+};
+
+/**
+ * Function: unmark
+ *
+ * Hides the marker and fires a <mark> event.
+ */
+mxCellMarker.prototype.unmark = function()
+{
+ this.mark();
+};
+
+/**
+ * Function: isValidState
+ *
+ * Returns true if the given <mxCellState> is a valid state. If this
+ * returns true, then the state is stored in <validState>. The return value
+ * of this method is used as the argument for <getMarkerColor>.
+ */
+mxCellMarker.prototype.isValidState = function(state)
+{
+ return true;
+};
+
+/**
+ * Function: getMarkerColor
+ *
+ * Returns the valid- or invalidColor depending on the value of isValid.
+ * The given <mxCellState> is ignored by this implementation.
+ */
+mxCellMarker.prototype.getMarkerColor = function(evt, state, isValid)
+{
+ return (isValid) ? this.validColor : this.invalidColor;
+};
+
+/**
+ * Function: getState
+ *
+ * Uses <getCell>, <getStateToMark> and <intersects> to return the
+ * <mxCellState> for the given <mxMouseEvent>.
+ */
+mxCellMarker.prototype.getState = function(me)
+{
+ var view = this.graph.getView();
+ cell = this.getCell(me);
+ var state = this.getStateToMark(view.getState(cell));
+
+ return (state != null && this.intersects(state, me)) ? state : null;
+};
+
+/**
+ * Function: getCell
+ *
+ * Returns the <mxCell> for the given event and cell. This returns the
+ * given cell.
+ */
+mxCellMarker.prototype.getCell = function(me)
+{
+ return me.getCell();
+};
+
+/**
+ * Function: getStateToMark
+ *
+ * Returns the <mxCellState> to be marked for the given <mxCellState> under
+ * the mouse. This returns the given state.
+ */
+mxCellMarker.prototype.getStateToMark = function(state)
+{
+ return state;
+};
+
+/**
+ * Function: intersects
+ *
+ * Returns true if the given coordinate pair intersects the given state.
+ * This returns true if the <hotspot> is 0 or the coordinates are inside
+ * the hotspot for the given cell state.
+ */
+mxCellMarker.prototype.intersects = function(state, me)
+{
+ if (this.hotspotEnabled)
+ {
+ return mxUtils.intersectsHotspot(state, me.getGraphX(), me.getGraphY(),
+ this.hotspot, mxConstants.MIN_HOTSPOT_SIZE,
+ mxConstants.MAX_HOTSPOT_SIZE);
+ }
+
+ return true;
+};
+
+/**
+ * Function: destroy
+ *
+ * Destroys the handler and all its resources and DOM nodes.
+ */
+mxCellMarker.prototype.destroy = function()
+{
+ this.graph.getView().removeListener(this.resetHandler);
+ this.graph.getModel().removeListener(this.resetHandler);
+ this.highlight.destroy();
+};
diff --git a/src/js/handler/mxCellTracker.js b/src/js/handler/mxCellTracker.js
new file mode 100644
index 0000000..5adcd6a
--- /dev/null
+++ b/src/js/handler/mxCellTracker.js
@@ -0,0 +1,149 @@
+/**
+ * $Id: mxCellTracker.js,v 1.9 2011-08-28 09:49:46 gaudenz Exp $
+ * Copyright (c) 2006-2010, JGraph Ltd
+ */
+/**
+ * Class: mxCellTracker
+ *
+ * Event handler that highlights cells. Inherits from <mxCellMarker>.
+ *
+ * Example:
+ *
+ * (code)
+ * new mxCellTracker(graph, '#00FF00');
+ * (end)
+ *
+ * For detecting dragEnter, dragOver and dragLeave on cells, the following
+ * code can be used:
+ *
+ * (code)
+ * graph.addMouseListener(
+ * {
+ * cell: null,
+ * mouseDown: function(sender, me) { },
+ * mouseMove: function(sender, me)
+ * {
+ * var tmp = me.getCell();
+ *
+ * if (tmp != this.cell)
+ * {
+ * if (this.cell != null)
+ * {
+ * this.dragLeave(me.getEvent(), this.cell);
+ * }
+ *
+ * this.cell = tmp;
+ *
+ * if (this.cell != null)
+ * {
+ * this.dragEnter(me.getEvent(), this.cell);
+ * }
+ * }
+ *
+ * if (this.cell != null)
+ * {
+ * this.dragOver(me.getEvent(), this.cell);
+ * }
+ * },
+ * mouseUp: function(sender, me) { },
+ * dragEnter: function(evt, cell)
+ * {
+ * mxLog.debug('dragEnter', cell.value);
+ * },
+ * dragOver: function(evt, cell)
+ * {
+ * mxLog.debug('dragOver', cell.value);
+ * },
+ * dragLeave: function(evt, cell)
+ * {
+ * mxLog.debug('dragLeave', cell.value);
+ * }
+ * });
+ * (end)
+ *
+ * Constructor: mxCellTracker
+ *
+ * Constructs an event handler that highlights cells.
+ *
+ * Parameters:
+ *
+ * graph - Reference to the enclosing <mxGraph>.
+ * color - Color of the highlight. Default is blue.
+ * funct - Optional JavaScript function that is used to override
+ * <mxCellMarker.getCell>.
+ */
+function mxCellTracker(graph, color, funct)
+{
+ mxCellMarker.call(this, graph, color);
+
+ this.graph.addMouseListener(this);
+
+ if (funct != null)
+ {
+ this.getCell = funct;
+ }
+
+ // Automatic deallocation of memory
+ if (mxClient.IS_IE)
+ {
+ mxEvent.addListener(window, 'unload', mxUtils.bind(this, function()
+ {
+ this.destroy();
+ }));
+ }
+};
+
+/**
+ * Extends mxCellMarker.
+ */
+mxCellTracker.prototype = new mxCellMarker();
+mxCellTracker.prototype.constructor = mxCellTracker;
+
+/**
+ * Function: mouseDown
+ *
+ * Ignores the event. The event is not consumed.
+ */
+mxCellTracker.prototype.mouseDown = function(sender, me) { };
+
+/**
+ * Function: mouseMove
+ *
+ * Handles the event by highlighting the cell under the mousepointer if it
+ * is over the hotspot region of the cell.
+ */
+mxCellTracker.prototype.mouseMove = function(sender, me)
+{
+ if (this.isEnabled())
+ {
+ this.process(me);
+ }
+};
+
+/**
+ * Function: mouseUp
+ *
+ * Handles the event by reseting the highlight.
+ */
+mxCellTracker.prototype.mouseUp = function(sender, me)
+{
+ this.reset();
+};
+
+/**
+ * Function: destroy
+ *
+ * Destroys the object and all its resources and DOM nodes. This doesn't
+ * normally need to be called. It is called automatically when the window
+ * unloads.
+ */
+mxCellTracker.prototype.destroy = function()
+{
+ if (!this.destroyed)
+ {
+ this.destroyed = true;
+
+ this.graph.removeMouseListener(this);
+ mxCellMarker.prototype.destroy.apply(this);
+ }
+};
diff --git a/src/js/handler/mxConnectionHandler.js b/src/js/handler/mxConnectionHandler.js
new file mode 100644
index 0000000..07daaf8
--- /dev/null
+++ b/src/js/handler/mxConnectionHandler.js
@@ -0,0 +1,1969 @@
+/**
+ * $Id: mxConnectionHandler.js,v 1.216 2012-12-07 15:17:37 gaudenz Exp $
+ * Copyright (c) 2006-2010, JGraph Ltd
+ */
+/**
+ * Class: mxConnectionHandler
+ *
+ * Graph event handler that creates new connections. Uses <mxTerminalMarker>
+ * for finding and highlighting the source and target vertices and
+ * <factoryMethod> to create the edge instance. This handler is built-into
+ * <mxGraph.connectionHandler> and enabled using <mxGraph.setConnectable>.
+ *
+ * Example:
+ *
+ * (code)
+ * new mxConnectionHandler(graph, function(source, target, style)
+ * {
+ * edge = new mxCell('', new mxGeometry());
+ * edge.setEdge(true);
+ * edge.setStyle(style);
+ * edge.geometry.relative = true;
+ * return edge;
+ * });
+ * (end)
+ *
+ * Here is an alternative solution that just sets a specific user object for
+ * new edges by overriding <insertEdge>.
+ *
+ * (code)
+ * mxConnectionHandlerInsertEdge = mxConnectionHandler.prototype.insertEdge;
+ * mxConnectionHandler.prototype.insertEdge = function(parent, id, value, source, target, style)
+ * {
+ * value = 'Test';
+ *
+ * return mxConnectionHandlerInsertEdge.apply(this, arguments);
+ * };
+ * (end)
+ *
+ * Using images to trigger connections:
+ *
+ * This handler uses mxTerminalMarker to find the source and target cell for
+ * the new connection and creates a new edge using <connect>. The new edge is
+ * created using <createEdge> which in turn uses <factoryMethod> or creates a
+ * new default edge.
+ *
+ * The handler uses a "highlight-paradigm" for indicating if a cell is being
+ * used as a source or target terminal, as seen in MS Visio and other products.
+ * In order to allow both, moving and connecting cells at the same time,
+ * <mxConstants.DEFAULT_HOTSPOT> is used in the handler to determine the hotspot
+ * of a cell, that is, the region of the cell which is used to trigger a new
+ * connection. The constant is a value between 0 and 1 that specifies the
+ * amount of the width and height around the center to be used for the hotspot
+ * of a cell and its default value is 0.5. In addition,
+ * <mxConstants.MIN_HOTSPOT_SIZE> defines the minimum number of pixels for the
+ * width and height of the hotspot.
+ *
+ * This solution, while standards compliant, may be somewhat confusing because
+ * there is no visual indicator for the hotspot and the highlight is seen to
+ * switch on and off while the mouse is being moved in and out. Furthermore,
+ * this paradigm does not allow to create different connections depending on
+ * the highlighted hotspot as there is only one hotspot per cell and it
+ * normally does not allow cells to be moved and connected at the same time as
+ * there is no clear indication of the connectable area of the cell.
+ *
+ * To come across these issues, the handle has an additional <createIcons> hook
+ * with a default implementation that allows to create one icon to be used to
+ * trigger new connections. If this icon is specified, then new connections can
+ * only be created if the image is clicked while the cell is being highlighted.
+ * The <createIcons> hook may be overridden to create more than one
+ * <mxImageShape> for creating new connections, but the default implementation
+ * supports one image and is used as follows:
+ *
+ * In order to display the "connect image" whenever the mouse is over the cell,
+ * an DEFAULT_HOTSPOT of 1 should be used:
+ *
+ * (code)
+ * mxConstants.DEFAULT_HOTSPOT = 1;
+ * (end)
+ *
+ * In order to avoid confusion with the highlighting, the highlight color
+ * should not be used with a connect image:
+ *
+ * (code)
+ * mxConstants.HIGHLIGHT_COLOR = null;
+ * (end)
+ *
+ * To install the image, the connectImage field of the mxConnectionHandler must
+ * be assigned a new <mxImage> instance:
+ *
+ * (code)
+ * mxConnectionHandler.prototype.connectImage = new mxImage('images/green-dot.gif', 14, 14);
+ * (end)
+ *
+ * This will use the green-dot.gif with a width and height of 14 pixels as the
+ * image to trigger new connections. In createIcons the icon field of the
+ * handler will be set in order to remember the icon that has been clicked for
+ * creating the new connection. This field will be available under selectedIcon
+ * in the connect method, which may be overridden to take the icon that
+ * triggered the new connection into account. This is useful if more than one
+ * icon may be used to create a connection.
+ *
+ * Group: Events
+ *
+ * Event: mxEvent.START
+ *
+ * Fires when a new connection is being created by the user. The <code>state</code>
+ * property contains the state of the source cell.
+ *
+ * Event: mxEvent.CONNECT
+ *
+ * Fires between begin- and endUpdate in <connect>. The <code>cell</code>
+ * property contains the inserted edge, the <code>event</code> and <code>target</code>
+ * properties contain the respective arguments that were passed to <connect> (where
+ * target corresponds to the dropTarget argument).
+ *
+ * Note that the target is the cell under the mouse where the mouse button was released.
+ * Depending on the logic in the handler, this doesn't necessarily have to be the target
+ * of the inserted edge. To print the source, target or any optional ports IDs that the
+ * edge is connected to, the following code can be used. To get more details about the
+ * actual connection point, <mxGraph.getConnectionConstraint> can be used. To resolve
+ * the port IDs, use <mxGraphModel.getCell>.
+ *
+ * (code)
+ * graph.connectionHandler.addListener(mxEvent.CONNECT, function(sender, evt)
+ * {
+ * var edge = evt.getProperty('cell');
+ * var source = graph.getModel().getTerminal(edge, true);
+ * var target = graph.getModel().getTerminal(edge, false);
+ *
+ * var style = graph.getCellStyle(edge);
+ * var sourcePortId = style[mxConstants.STYLE_SOURCE_PORT];
+ * var targetPortId = style[mxConstants.STYLE_TARGET_PORT];
+ *
+ * mxLog.show();
+ * mxLog.debug('connect', edge, source.id, target.id, sourcePortId, targetPortId);
+ * });
+ * (end)
+ *
+ * Event: mxEvent.RESET
+ *
+ * Fires when the <reset> method is invoked.
+ *
+ * Constructor: mxConnectionHandler
+ *
+ * Constructs an event handler that connects vertices using the specified
+ * factory method to create the new edges. Modify
+ * <mxConstants.ACTIVE_REGION> to setup the region on a cell which triggers
+ * the creation of a new connection or use connect icons as explained
+ * above.
+ *
+ * Parameters:
+ *
+ * graph - Reference to the enclosing <mxGraph>.
+ * factoryMethod - Optional function to create the edge. The function takes
+ * the source and target <mxCell> as the first and second argument and an
+ * optional cell style from the preview as the third argument. It returns
+ * the <mxCell> that represents the new edge.
+ */
+function mxConnectionHandler(graph, factoryMethod)
+{
+ if (graph != null)
+ {
+ this.graph = graph;
+ this.factoryMethod = factoryMethod;
+ this.init();
+ }
+};
+
+/**
+ * Extends mxEventSource.
+ */
+mxConnectionHandler.prototype = new mxEventSource();
+mxConnectionHandler.prototype.constructor = mxConnectionHandler;
+
+/**
+ * Variable: graph
+ *
+ * Reference to the enclosing <mxGraph>.
+ */
+mxConnectionHandler.prototype.graph = null;
+
+/**
+ * Variable: factoryMethod
+ *
+ * Function that is used for creating new edges. The function takes the
+ * source and target <mxCell> as the first and second argument and returns
+ * a new <mxCell> that represents the edge. This is used in <createEdge>.
+ */
+mxConnectionHandler.prototype.factoryMethod = true;
+
+/**
+ * Variable: moveIconFront
+ *
+ * Specifies if icons should be displayed inside the graph container instead
+ * of the overlay pane. This is used for HTML labels on vertices which hide
+ * the connect icon. This has precendence over <moveIconBack> when set
+ * to true. Default is false.
+ */
+mxConnectionHandler.prototype.moveIconFront = false;
+
+/**
+ * Variable: moveIconBack
+ *
+ * Specifies if icons should be moved to the back of the overlay pane. This can
+ * be set to true if the icons of the connection handler conflict with other
+ * handles, such as the vertex label move handle. Default is false.
+ */
+mxConnectionHandler.prototype.moveIconBack = false;
+
+/**
+ * Variable: connectImage
+ *
+ * <mxImage> that is used to trigger the creation of a new connection. This
+ * is used in <createIcons>. Default is null.
+ */
+mxConnectionHandler.prototype.connectImage = null;
+
+/**
+ * Variable: targetConnectImage
+ *
+ * Specifies if the connect icon should be centered on the target state
+ * while connections are being previewed. Default is false.
+ */
+mxConnectionHandler.prototype.targetConnectImage = false;
+
+/**
+ * Variable: enabled
+ *
+ * Specifies if events are handled. Default is true.
+ */
+mxConnectionHandler.prototype.enabled = true;
+
+/**
+ * Variable: select
+ *
+ * Specifies if new edges should be selected. Default is true.
+ */
+mxConnectionHandler.prototype.select = true;
+
+/**
+ * Variable: createTarget
+ *
+ * Specifies if <createTargetVertex> should be called if no target was under the
+ * mouse for the new connection. Setting this to true means the connection
+ * will be drawn as valid if no target is under the mouse, and
+ * <createTargetVertex> will be called before the connection is created between
+ * the source cell and the newly created vertex in <createTargetVertex>, which
+ * can be overridden to create a new target. Default is false.
+ */
+mxConnectionHandler.prototype.createTarget = false;
+
+/**
+ * Variable: marker
+ *
+ * Holds the <mxTerminalMarker> used for finding source and target cells.
+ */
+mxConnectionHandler.prototype.marker = null;
+
+/**
+ * Variable: constraintHandler
+ *
+ * Holds the <mxConstraintHandler> used for drawing and highlighting
+ * constraints.
+ */
+mxConnectionHandler.prototype.constraintHandler = null;
+
+/**
+ * Variable: error
+ *
+ * Holds the current validation error while connections are being created.
+ */
+mxConnectionHandler.prototype.error = null;
+
+/**
+ * Variable: waypointsEnabled
+ *
+ * Specifies if single clicks should add waypoints on the new edge. Default is
+ * false.
+ */
+mxConnectionHandler.prototype.waypointsEnabled = false;
+
+/**
+ * Variable: tapAndHoldEnabled
+ *
+ * Specifies if tap and hold should be used for starting connections on touch-based
+ * devices. Default is true.
+ */
+mxConnectionHandler.prototype.tapAndHoldEnabled = true;
+
+/**
+ * Variable: tapAndHoldDelay
+ *
+ * Specifies the time for a tap and hold. Default is 500 ms.
+ */
+mxConnectionHandler.prototype.tapAndHoldDelay = 500;
+
+/**
+ * Variable: tapAndHoldInProgress
+ *
+ * True if the timer for tap and hold events is running.
+ */
+mxConnectionHandler.prototype.tapAndHoldInProgress = false;
+
+/**
+ * Variable: tapAndHoldValid
+ *
+ * True as long as the timer is running and the touch events
+ * stay within the given <tapAndHoldTolerance>.
+ */
+mxConnectionHandler.prototype.tapAndHoldValid = false;
+
+/**
+ * Variable: tapAndHoldTolerance
+ *
+ * Specifies the tolerance for a tap and hold. Default is 4 pixels.
+ */
+mxConnectionHandler.prototype.tapAndHoldTolerance = 4;
+
+/**
+ * Variable: initialTouchX
+ *
+ * Holds the x-coordinate of the intial touch event for tap and hold.
+ */
+mxConnectionHandler.prototype.initialTouchX = 0;
+
+/**
+ * Variable: initialTouchY
+ *
+ * Holds the y-coordinate of the intial touch event for tap and hold.
+ */
+mxConnectionHandler.prototype.initialTouchY = 0;
+
+/**
+ * Variable: ignoreMouseDown
+ *
+ * Specifies if the connection handler should ignore the state of the mouse
+ * button when highlighting the source. Default is false, that is, the
+ * handler only highlights the source if no button is being pressed.
+ */
+mxConnectionHandler.prototype.ignoreMouseDown = false;
+
+/**
+ * Variable: first
+ *
+ * Holds the <mxPoint> where the mouseDown took place while the handler is
+ * active.
+ */
+mxConnectionHandler.prototype.first = null;
+
+/**
+ * Variable: connectIconOffset
+ *
+ * Holds the offset for connect icons during connection preview.
+ * Default is mxPoint(0, <mxConstants.TOOLTIP_VERTICAL_OFFSET>).
+ * Note that placing the icon under the mouse pointer with an
+ * offset of (0,0) will affect hit detection.
+ */
+mxConnectionHandler.prototype.connectIconOffset = new mxPoint(0, mxConstants.TOOLTIP_VERTICAL_OFFSET);
+
+/**
+ * Variable: edgeState
+ *
+ * Optional <mxCellState> that represents the preview edge while the
+ * handler is active. This is created in <createEdgeState>.
+ */
+mxConnectionHandler.prototype.edgeState = null;
+
+/**
+ * Variable: changeHandler
+ *
+ * Holds the change event listener for later removal.
+ */
+mxConnectionHandler.prototype.changeHandler = null;
+
+/**
+ * Variable: drillHandler
+ *
+ * Holds the drill event listener for later removal.
+ */
+mxConnectionHandler.prototype.drillHandler = null;
+
+/**
+ * Variable: mouseDownCounter
+ *
+ * Counts the number of mouseDown events since the start. The initial mouse
+ * down event counts as 1.
+ */
+mxConnectionHandler.prototype.mouseDownCounter = 0;
+
+/**
+ * Variable: movePreviewAway
+ *
+ * Switch to enable moving the preview away from the mousepointer. This is required in browsers
+ * where the preview cannot be made transparent to events and if the built-in hit detection on
+ * the HTML elements in the page should be used. Default is the value of <mxClient.IS_VML>.
+ */
+mxConnectionHandler.prototype.movePreviewAway = mxClient.IS_VML;
+
+/**
+ * Function: isEnabled
+ *
+ * Returns true if events are handled. This implementation
+ * returns <enabled>.
+ */
+mxConnectionHandler.prototype.isEnabled = function()
+{
+ return this.enabled;
+};
+
+/**
+ * Function: setEnabled
+ *
+ * Enables or disables event handling. This implementation
+ * updates <enabled>.
+ *
+ * Parameters:
+ *
+ * enabled - Boolean that specifies the new enabled state.
+ */
+mxConnectionHandler.prototype.setEnabled = function(enabled)
+{
+ this.enabled = enabled;
+};
+
+/**
+ * Function: isCreateTarget
+ *
+ * Returns <createTarget>.
+ */
+mxConnectionHandler.prototype.isCreateTarget = function()
+{
+ return this.createTarget;
+};
+
+/**
+ * Function: setCreateTarget
+ *
+ * Sets <createTarget>.
+ */
+mxConnectionHandler.prototype.setCreateTarget = function(value)
+{
+ this.createTarget = value;
+};
+
+/**
+ * Function: createShape
+ *
+ * Creates the preview shape for new connections.
+ */
+mxConnectionHandler.prototype.createShape = function()
+{
+ // Creates the edge preview
+ var shape = new mxPolyline([], mxConstants.INVALID_COLOR);
+ shape.isDashed = true;
+ shape.dialect = (this.graph.dialect != mxConstants.DIALECT_SVG) ?
+ mxConstants.DIALECT_VML : mxConstants.DIALECT_SVG;
+ shape.init(this.graph.getView().getOverlayPane());
+
+ // Event-transparency
+ if (this.graph.dialect == mxConstants.DIALECT_SVG)
+ {
+ // Sets event transparency on the internal shapes that represent
+ // the actual dashed line on the screen
+ shape.pipe.setAttribute('style', 'pointer-events:none;');
+ shape.innerNode.setAttribute('style', 'pointer-events:none;');
+ }
+ else
+ {
+ // Workaround no event transparency for preview in IE
+ // FIXME: 3,3 pixel offset for custom hit detection in IE
+ var getState = mxUtils.bind(this, function(evt)
+ {
+ var pt = mxUtils.convertPoint(this.graph.container, mxEvent.getClientX(evt), mxEvent.getClientY(evt));
+
+ return this.graph.view.getState(this.graph.getCellAt(pt.x, pt.y));
+ });
+
+ // Redirects events on the shape to the graph
+ mxEvent.redirectMouseEvents(shape.node, this.graph, getState);
+ }
+
+ return shape;
+};
+
+/**
+ * Function: init
+ *
+ * Initializes the shapes required for this connection handler. This should
+ * be invoked if <mxGraph.container> is assigned after the connection
+ * handler has been created.
+ */
+mxConnectionHandler.prototype.init = function()
+{
+ this.graph.addMouseListener(this);
+ this.marker = this.createMarker();
+ this.constraintHandler = new mxConstraintHandler(this.graph);
+
+ // Redraws the icons if the graph changes
+ this.changeHandler = mxUtils.bind(this, function(sender)
+ {
+ if (this.iconState != null)
+ {
+ this.iconState = this.graph.getView().getState(this.iconState.cell);
+ }
+
+ if (this.iconState != null)
+ {
+ this.redrawIcons(this.icons, this.iconState);
+ }
+ else
+ {
+ this.destroyIcons(this.icons);
+ this.previous = null;
+ }
+
+ this.constraintHandler.reset();
+ });
+
+ this.graph.getModel().addListener(mxEvent.CHANGE, this.changeHandler);
+ this.graph.getView().addListener(mxEvent.SCALE, this.changeHandler);
+ this.graph.getView().addListener(mxEvent.TRANSLATE, this.changeHandler);
+ this.graph.getView().addListener(mxEvent.SCALE_AND_TRANSLATE, this.changeHandler);
+
+ // Removes the icon if we step into/up or start editing
+ this.drillHandler = mxUtils.bind(this, function(sender)
+ {
+ this.destroyIcons(this.icons);
+ });
+
+ this.graph.addListener(mxEvent.START_EDITING, this.drillHandler);
+ this.graph.getView().addListener(mxEvent.DOWN, this.drillHandler);
+ this.graph.getView().addListener(mxEvent.UP, this.drillHandler);
+};
+
+/**
+ * Function: isConnectableCell
+ *
+ * Returns true if the given cell is connectable. This is a hook to
+ * disable floating connections. This implementation returns true.
+ */
+mxConnectionHandler.prototype.isConnectableCell = function(cell)
+{
+ return true;
+};
+
+/**
+ * Function: createMarker
+ *
+ * Creates and returns the <mxCellMarker> used in <marker>.
+ */
+mxConnectionHandler.prototype.createMarker = function()
+{
+ var marker = new mxCellMarker(this.graph);
+ marker.hotspotEnabled = true;
+
+ // Overrides to return cell at location only if valid (so that
+ // there is no highlight for invalid cells)
+ marker.getCell = mxUtils.bind(this, function(evt, cell)
+ {
+ var cell = mxCellMarker.prototype.getCell.apply(marker, arguments);
+ this.error = null;
+
+ if (!this.isConnectableCell(cell))
+ {
+ return null;
+ }
+
+ if (cell != null)
+ {
+ if (this.isConnecting())
+ {
+ if (this.previous != null)
+ {
+ this.error = this.validateConnection(this.previous.cell, cell);
+
+ if (this.error != null && this.error.length == 0)
+ {
+ cell = null;
+
+ // Enables create target inside groups
+ if (this.isCreateTarget())
+ {
+ this.error = null;
+ }
+ }
+ }
+ }
+ else if (!this.isValidSource(cell))
+ {
+ cell = null;
+ }
+ }
+ else if (this.isConnecting() && !this.isCreateTarget() &&
+ !this.graph.allowDanglingEdges)
+ {
+ this.error = '';
+ }
+
+ return cell;
+ });
+
+ // Sets the highlight color according to validateConnection
+ marker.isValidState = mxUtils.bind(this, function(state)
+ {
+ if (this.isConnecting())
+ {
+ return this.error == null;
+ }
+ else
+ {
+ return mxCellMarker.prototype.isValidState.apply(marker, arguments);
+ }
+ });
+
+ // Overrides to use marker color only in highlight mode or for
+ // target selection
+ marker.getMarkerColor = mxUtils.bind(this, function(evt, state, isValid)
+ {
+ return (this.connectImage == null || this.isConnecting()) ?
+ mxCellMarker.prototype.getMarkerColor.apply(marker, arguments) :
+ null;
+ });
+
+ // Overrides to use hotspot only for source selection otherwise
+ // intersects always returns true when over a cell
+ marker.intersects = mxUtils.bind(this, function(state, evt)
+ {
+ if (this.connectImage != null || this.isConnecting())
+ {
+ return true;
+ }
+
+ return mxCellMarker.prototype.intersects.apply(marker, arguments);
+ });
+
+ return marker;
+};
+
+/**
+ * Function: start
+ *
+ * Starts a new connection for the given state and coordinates.
+ */
+mxConnectionHandler.prototype.start = function(state, x, y, edgeState)
+{
+ this.previous = state;
+ this.first = new mxPoint(x, y);
+ this.edgeState = (edgeState != null) ? edgeState : this.createEdgeState(null);
+
+ // Marks the source state
+ this.marker.currentColor = this.marker.validColor;
+ this.marker.markedState = state;
+ this.marker.mark();
+
+ this.fireEvent(new mxEventObject(mxEvent.START, 'state', this.previous));
+};
+
+/**
+ * Function: isConnecting
+ *
+ * Returns true if the source terminal has been clicked and a new
+ * connection is currently being previewed.
+ */
+mxConnectionHandler.prototype.isConnecting = function()
+{
+ return this.first != null && this.shape != null;
+};
+
+/**
+ * Function: isValidSource
+ *
+ * Returns <mxGraph.isValidSource> for the given source terminal.
+ *
+ * Parameters:
+ *
+ * cell - <mxCell> that represents the source terminal.
+ */
+mxConnectionHandler.prototype.isValidSource = function(cell)
+{
+ return this.graph.isValidSource(cell);
+};
+
+/**
+ * Function: isValidTarget
+ *
+ * Returns true. The call to <mxGraph.isValidTarget> is implicit by calling
+ * <mxGraph.getEdgeValidationError> in <validateConnection>. This is an
+ * additional hook for disabling certain targets in this specific handler.
+ *
+ * Parameters:
+ *
+ * cell - <mxCell> that represents the target terminal.
+ */
+mxConnectionHandler.prototype.isValidTarget = function(cell)
+{
+ return true;
+};
+
+/**
+ * Function: validateConnection
+ *
+ * Returns the error message or an empty string if the connection for the
+ * given source target pair is not valid. Otherwise it returns null. This
+ * implementation uses <mxGraph.getEdgeValidationError>.
+ *
+ * Parameters:
+ *
+ * source - <mxCell> that represents the source terminal.
+ * target - <mxCell> that represents the target terminal.
+ */
+mxConnectionHandler.prototype.validateConnection = function(source, target)
+{
+ if (!this.isValidTarget(target))
+ {
+ return '';
+ }
+
+ return this.graph.getEdgeValidationError(null, source, target);
+};
+
+/**
+ * Function: getConnectImage
+ *
+ * Hook to return the <mxImage> used for the connection icon of the given
+ * <mxCellState>. This implementation returns <connectImage>.
+ *
+ * Parameters:
+ *
+ * state - <mxCellState> whose connect image should be returned.
+ */
+mxConnectionHandler.prototype.getConnectImage = function(state)
+{
+ return this.connectImage;
+};
+
+/**
+ * Function: isMoveIconToFrontForState
+ *
+ * Returns true if the state has a HTML label in the graph's container, otherwise
+ * it returns <moveIconFront>.
+ *
+ * Parameters:
+ *
+ * state - <mxCellState> whose connect icons should be returned.
+ */
+mxConnectionHandler.prototype.isMoveIconToFrontForState = function(state)
+{
+ if (state.text != null && state.text.node.parentNode == this.graph.container)
+ {
+ return true;
+ }
+
+ return this.moveIconFront;
+};
+
+/**
+ * Function: createIcons
+ *
+ * Creates the array <mxImageShapes> that represent the connect icons for
+ * the given <mxCellState>.
+ *
+ * Parameters:
+ *
+ * state - <mxCellState> whose connect icons should be returned.
+ */
+mxConnectionHandler.prototype.createIcons = function(state)
+{
+ var image = this.getConnectImage(state);
+
+ if (image != null && state != null)
+ {
+ this.iconState = state;
+ var icons = [];
+
+ // Cannot use HTML for the connect icons because the icon receives all
+ // mouse move events in IE, must use VML and SVG instead even if the
+ // connect-icon appears behind the selection border and the selection
+ // border consumes the events before the icon gets a chance
+ var bounds = new mxRectangle(0, 0, image.width, image.height);
+ var icon = new mxImageShape(bounds, image.src, null, null, 0);
+ icon.preserveImageAspect = false;
+
+ if (this.isMoveIconToFrontForState(state))
+ {
+ icon.dialect = mxConstants.DIALECT_STRICTHTML;
+ icon.init(this.graph.container);
+ }
+ else
+ {
+ icon.dialect = (this.graph.dialect == mxConstants.DIALECT_SVG) ?
+ mxConstants.DIALECT_SVG :
+ mxConstants.DIALECT_VML;
+ icon.init(this.graph.getView().getOverlayPane());
+
+ // Move the icon back in the overlay pane
+ if (this.moveIconBack && icon.node.previousSibling != null)
+ {
+ icon.node.parentNode.insertBefore(icon.node, icon.node.parentNode.firstChild);
+ }
+ }
+
+ icon.node.style.cursor = mxConstants.CURSOR_CONNECT;
+
+ // Events transparency
+ var getState = mxUtils.bind(this, function()
+ {
+ return (this.currentState != null) ? this.currentState : state;
+ });
+
+ // Updates the local icon before firing the mouse down event.
+ var mouseDown = mxUtils.bind(this, function(evt)
+ {
+ if (!mxEvent.isConsumed(evt))
+ {
+ this.icon = icon;
+ this.graph.fireMouseEvent(mxEvent.MOUSE_DOWN,
+ new mxMouseEvent(evt, getState()));
+ }
+ });
+
+ mxEvent.redirectMouseEvents(icon.node, this.graph, getState, mouseDown);
+
+ icons.push(icon);
+ this.redrawIcons(icons, this.iconState);
+
+ return icons;
+ }
+
+ return null;
+};
+
+/**
+ * Function: redrawIcons
+ *
+ * Redraws the given array of <mxImageShapes>.
+ *
+ * Parameters:
+ *
+ * icons - Optional array of <mxImageShapes> to be redrawn.
+ */
+mxConnectionHandler.prototype.redrawIcons = function(icons, state)
+{
+ if (icons != null && icons[0] != null && state != null)
+ {
+ var pos = this.getIconPosition(icons[0], state);
+ icons[0].bounds.x = pos.x;
+ icons[0].bounds.y = pos.y;
+ icons[0].redraw();
+ }
+};
+
+/**
+ * Function: redrawIcons
+ *
+ * Redraws the given array of <mxImageShapes>.
+ *
+ * Parameters:
+ *
+ * icons - Optional array of <mxImageShapes> to be redrawn.
+ */
+mxConnectionHandler.prototype.getIconPosition = function(icon, state)
+{
+ var scale = this.graph.getView().scale;
+ var cx = state.getCenterX();
+ var cy = state.getCenterY();
+
+ if (this.graph.isSwimlane(state.cell))
+ {
+ var size = this.graph.getStartSize(state.cell);
+
+ cx = (size.width != 0) ? state.x + size.width * scale / 2 : cx;
+ cy = (size.height != 0) ? state.y + size.height * scale / 2 : cy;
+ }
+
+ return new mxPoint(cx - icon.bounds.width / 2,
+ cy - icon.bounds.height / 2);
+};
+
+/**
+ * Function: destroyIcons
+ *
+ * Destroys the given array of <mxImageShapes>.
+ *
+ * Parameters:
+ *
+ * icons - Optional array of <mxImageShapes> to be destroyed.
+ */
+mxConnectionHandler.prototype.destroyIcons = function(icons)
+{
+ if (icons != null)
+ {
+ this.iconState = null;
+
+ for (var i = 0; i < icons.length; i++)
+ {
+ icons[i].destroy();
+ }
+ }
+};
+
+/**
+ * Function: isStartEvent
+ *
+ * Returns true if the given mouse down event should start this handler. The
+ * This implementation returns true if the event does not force marquee
+ * selection, and the currentConstraint and currentFocus of the
+ * <constraintHandler> are not null, or <previous> and <error> are not null and
+ * <icons> is null or <icons> and <icon> are not null.
+ */
+mxConnectionHandler.prototype.isStartEvent = function(me)
+{
+ return !this.graph.isForceMarqueeEvent(me.getEvent()) &&
+ ((this.constraintHandler.currentFocus != null &&
+ this.constraintHandler.currentConstraint != null) ||
+ (this.previous != null && this.error == null &&
+ (this.icons == null || (this.icons != null && this.icon != null))));
+};
+
+/**
+ * Function: mouseDown
+ *
+ * Handles the event by initiating a new connection.
+ */
+mxConnectionHandler.prototype.mouseDown = function(sender, me)
+{
+ this.mouseDownCounter++;
+
+ if (this.isEnabled() && this.graph.isEnabled() && !me.isConsumed() &&
+ !this.isConnecting() && this.isStartEvent(me))
+ {
+ if (this.constraintHandler.currentConstraint != null &&
+ this.constraintHandler.currentFocus != null &&
+ this.constraintHandler.currentPoint != null)
+ {
+ this.sourceConstraint = this.constraintHandler.currentConstraint;
+ this.previous = this.constraintHandler.currentFocus;
+ this.first = this.constraintHandler.currentPoint.clone();
+ }
+ else
+ {
+ // Stores the location of the initial mousedown
+ this.first = new mxPoint(me.getGraphX(), me.getGraphY());
+ }
+
+ this.edgeState = this.createEdgeState(me);
+ this.mouseDownCounter = 1;
+
+ if (this.waypointsEnabled && this.shape == null)
+ {
+ this.waypoints = null;
+ this.shape = this.createShape();
+ }
+
+ // Stores the starting point in the geometry of the preview
+ if (this.previous == null && this.edgeState != null)
+ {
+ var pt = this.graph.getPointForEvent(me.getEvent());
+ this.edgeState.cell.geometry.setTerminalPoint(pt, true);
+ }
+
+ this.fireEvent(new mxEventObject(mxEvent.START, 'state', this.previous));
+
+ me.consume();
+ }
+ // Handles connecting via tap and hold
+ else if (mxClient.IS_TOUCH && this.tapAndHoldEnabled && !this.tapAndHoldInProgress &&
+ this.isEnabled() && this.graph.isEnabled() && !this.isConnecting())
+ {
+ this.tapAndHoldInProgress = true;
+ this.initialTouchX = me.getX();
+ this.initialTouchY = me.getY();
+ var state = this.graph.view.getState(this.marker.getCell(me));
+
+ var handler = function()
+ {
+ if (this.tapAndHoldValid)
+ {
+ this.tapAndHold(me, state);
+ }
+
+ this.tapAndHoldInProgress = false;
+ this.tapAndHoldValid = false;
+ };
+
+ if (this.tapAndHoldThread)
+ {
+ window.clearTimeout(this.tapAndHoldThread);
+ }
+
+ this.tapAndHoldThread = window.setTimeout(mxUtils.bind(this, handler), this.tapAndHoldDelay);
+ this.tapAndHoldValid = true;
+ }
+
+ this.selectedIcon = this.icon;
+ this.icon = null;
+};
+
+/**
+ * Function: tapAndHold
+ *
+ * Handles the <mxMouseEvent> by highlighting the <mxCellState>.
+ *
+ * Parameters:
+ *
+ * me - <mxMouseEvent> that represents the touch event.
+ * state - Optional <mxCellState> that is associated with the event.
+ */
+mxConnectionHandler.prototype.tapAndHold = function(me, state)
+{
+ if (state != null)
+ {
+ this.marker.currentColor = this.marker.validColor;
+ this.marker.markedState = state;
+ this.marker.mark();
+
+ this.first = new mxPoint(me.getGraphX(), me.getGraphY());
+ this.edgeState = this.createEdgeState(me);
+ this.previous = state;
+ this.fireEvent(new mxEventObject(mxEvent.START, 'state', this.previous));
+ }
+};
+
+/**
+ * Function: isImmediateConnectSource
+ *
+ * Returns true if a tap on the given source state should immediately start
+ * connecting. This implementation returns true if the state is not movable
+ * in the graph.
+ */
+mxConnectionHandler.prototype.isImmediateConnectSource = function(state)
+{
+ return !this.graph.isCellMovable(state.cell);
+};
+
+/**
+ * Function: createEdgeState
+ *
+ * Hook to return an <mxCellState> which may be used during the preview.
+ * This implementation returns null.
+ *
+ * Use the following code to create a preview for an existing edge style:
+ *
+ * [code]
+ * graph.connectionHandler.createEdgeState = function(me)
+ * {
+ * var edge = graph.createEdge(null, null, null, null, null, 'edgeStyle=elbowEdgeStyle');
+ *
+ * return new mxCellState(this.graph.view, edge, this.graph.getCellStyle(edge));
+ * };
+ * [/code]
+ */
+mxConnectionHandler.prototype.createEdgeState = function(me)
+{
+ return null;
+};
+
+/**
+ * Function: updateCurrentState
+ *
+ * Updates the current state for a given mouse move event by using
+ * the <marker>.
+ */
+mxConnectionHandler.prototype.updateCurrentState = function(me)
+{
+ var state = this.marker.process(me);
+ this.constraintHandler.update(me, this.first == null);
+ this.currentState = state;
+};
+
+/**
+ * Function: convertWaypoint
+ *
+ * Converts the given point from screen coordinates to model coordinates.
+ */
+mxConnectionHandler.prototype.convertWaypoint = function(point)
+{
+ var scale = this.graph.getView().getScale();
+ var tr = this.graph.getView().getTranslate();
+
+ point.x = point.x / scale - tr.x;
+ point.y = point.y / scale - tr.y;
+};
+
+/**
+ * Function: mouseMove
+ *
+ * Handles the event by updating the preview edge or by highlighting
+ * a possible source or target terminal.
+ */
+mxConnectionHandler.prototype.mouseMove = function(sender, me)
+{
+ if (this.tapAndHoldValid)
+ {
+ this.tapAndHoldValid =
+ Math.abs(this.initialTouchX - me.getX()) < this.tapAndHoldTolerance &&
+ Math.abs(this.initialTouchY - me.getY()) < this.tapAndHoldTolerance;
+ }
+
+ if (!me.isConsumed() && (this.ignoreMouseDown || this.first != null || !this.graph.isMouseDown))
+ {
+ // Handles special case when handler is disabled during highlight
+ if (!this.isEnabled() && this.currentState != null)
+ {
+ this.destroyIcons(this.icons);
+ this.currentState = null;
+ }
+
+ if (this.first != null || (this.isEnabled() && this.graph.isEnabled()))
+ {
+ this.updateCurrentState(me);
+ }
+
+ if (this.first != null)
+ {
+ var view = this.graph.getView();
+ var scale = view.scale;
+ var point = new mxPoint(this.graph.snap(me.getGraphX() / scale) * scale,
+ this.graph.snap(me.getGraphY() / scale) * scale);
+ var constraint = null;
+ var current = point;
+
+ // Uses the current point from the constraint handler if available
+ if (this.constraintHandler.currentConstraint != null &&
+ this.constraintHandler.currentFocus != null &&
+ this.constraintHandler.currentPoint != null)
+ {
+ constraint = this.constraintHandler.currentConstraint;
+ current = this.constraintHandler.currentPoint.clone();
+ }
+
+ var pt2 = this.first;
+
+ // Moves the connect icon with the mouse
+ if (this.selectedIcon != null)
+ {
+ var w = this.selectedIcon.bounds.width;
+ var h = this.selectedIcon.bounds.height;
+
+ if (this.currentState != null && this.targetConnectImage)
+ {
+ var pos = this.getIconPosition(this.selectedIcon, this.currentState);
+ this.selectedIcon.bounds.x = pos.x;
+ this.selectedIcon.bounds.y = pos.y;
+ }
+ else
+ {
+ var bounds = new mxRectangle(me.getGraphX() + this.connectIconOffset.x,
+ me.getGraphY() + this.connectIconOffset.y, w, h);
+ this.selectedIcon.bounds = bounds;
+ }
+
+ this.selectedIcon.redraw();
+ }
+
+ // Uses edge state to compute the terminal points
+ if (this.edgeState != null)
+ {
+ this.edgeState.absolutePoints = [null, (this.currentState != null) ? null : current];
+ this.graph.view.updateFixedTerminalPoint(this.edgeState, this.previous, true, this.sourceConstraint);
+
+ if (this.currentState != null)
+ {
+ if (constraint == null)
+ {
+ constraint = this.graph.getConnectionConstraint(this.edgeState, this.previous, false);
+ }
+
+ this.edgeState.setAbsoluteTerminalPoint(null, false);
+ this.graph.view.updateFixedTerminalPoint(this.edgeState, this.currentState, false, constraint);
+ }
+
+ // Scales and translates the waypoints to the model
+ var realPoints = null;
+
+ if (this.waypoints != null)
+ {
+ realPoints = [];
+
+ for (var i = 0; i < this.waypoints.length; i++)
+ {
+ var pt = this.waypoints[i].clone();
+ this.convertWaypoint(pt);
+ realPoints[i] = pt;
+ }
+ }
+
+ this.graph.view.updatePoints(this.edgeState, realPoints, this.previous, this.currentState);
+ this.graph.view.updateFloatingTerminalPoints(this.edgeState, this.previous, this.currentState);
+ current = this.edgeState.absolutePoints[this.edgeState.absolutePoints.length - 1];
+ pt2 = this.edgeState.absolutePoints[0];
+ }
+ else
+ {
+ if (this.currentState != null)
+ {
+ if (this.constraintHandler.currentConstraint == null)
+ {
+ var tmp = this.getTargetPerimeterPoint(this.currentState, me);
+
+ if (tmp != null)
+ {
+ current = tmp;
+ }
+ }
+ }
+
+ // Computes the source perimeter point
+ if (this.sourceConstraint == null && this.previous != null)
+ {
+ var next = (this.waypoints != null && this.waypoints.length > 0) ?
+ this.waypoints[0] : current;
+ var tmp = this.getSourcePerimeterPoint(this.previous, next, me);
+
+ if (tmp != null)
+ {
+ pt2 = tmp;
+ }
+ }
+ }
+
+ // Makes sure the cell under the mousepointer can be detected
+ // by moving the preview shape away from the mouse. This
+ // makes sure the preview shape does not prevent the detection
+ // of the cell under the mousepointer even for slow gestures.
+ if (this.currentState == null && this.movePreviewAway)
+ {
+ var tmp = pt2;
+
+ if (this.edgeState != null && this.edgeState.absolutePoints.length > 2)
+ {
+ var tmp2 = this.edgeState.absolutePoints[this.edgeState.absolutePoints.length - 2];
+
+ if (tmp2 != null)
+ {
+ tmp = tmp2;
+ }
+ }
+
+ var dx = current.x - tmp.x;
+ var dy = current.y - tmp.y;
+
+ var len = Math.sqrt(dx * dx + dy * dy);
+
+ if (len == 0)
+ {
+ return;
+ }
+
+ current.x -= dx * 4 / len;
+ current.y -= dy * 4 / len;
+ }
+
+ // Creates the preview shape (lazy)
+ if (this.shape == null)
+ {
+ var dx = Math.abs(point.x - this.first.x);
+ var dy = Math.abs(point.y - this.first.y);
+
+ if (dx > this.graph.tolerance || dy > this.graph.tolerance)
+ {
+ this.shape = this.createShape();
+
+ // Revalidates current connection
+ this.updateCurrentState(me);
+ }
+ }
+
+ // Updates the points in the preview edge
+ if (this.shape != null)
+ {
+ if (this.edgeState != null)
+ {
+ this.shape.points = this.edgeState.absolutePoints;
+ }
+ else
+ {
+ var pts = [pt2];
+
+ if (this.waypoints != null)
+ {
+ pts = pts.concat(this.waypoints);
+ }
+
+ pts.push(current);
+ this.shape.points = pts;
+ }
+
+ this.drawPreview();
+ }
+
+ mxEvent.consume(me.getEvent());
+ me.consume();
+ }
+ else if(!this.isEnabled() || !this.graph.isEnabled())
+ {
+ this.constraintHandler.reset();
+ }
+ else if (this.previous != this.currentState && this.edgeState == null)
+ {
+ this.destroyIcons(this.icons);
+ this.icons = null;
+
+ // Sets the cursor on the current shape
+ if (this.currentState != null && this.error == null)
+ {
+ this.icons = this.createIcons(this.currentState);
+
+ if (this.icons == null)
+ {
+ this.currentState.setCursor(mxConstants.CURSOR_CONNECT);
+ me.consume();
+ }
+ }
+
+ this.previous = this.currentState;
+ }
+ else if (this.previous == this.currentState && this.currentState != null && this.icons == null &&
+ !this.graph.isMouseDown)
+ {
+ // Makes sure that no cursors are changed
+ me.consume();
+ }
+
+ if (this.constraintHandler.currentConstraint != null)
+ {
+ this.marker.reset();
+ }
+
+ if (!this.graph.isMouseDown && this.currentState != null && this.icons != null)
+ {
+ var hitsIcon = false;
+ var target = me.getSource();
+
+ for (var i = 0; i < this.icons.length && !hitsIcon; i++)
+ {
+ hitsIcon = target == this.icons[i].node || target.parentNode == this.icons[i].node;
+ }
+
+ if (!hitsIcon)
+ {
+ this.updateIcons(this.currentState, this.icons, me);
+ }
+ }
+ }
+ else
+ {
+ this.constraintHandler.reset();
+ }
+};
+
+/**
+ * Function: getTargetPerimeterPoint
+ *
+ * Returns the perimeter point for the given target state.
+ *
+ * Parameters:
+ *
+ * state - <mxCellState> that represents the target cell state.
+ * me - <mxMouseEvent> that represents the mouse move.
+ */
+mxConnectionHandler.prototype.getTargetPerimeterPoint = function(state, me)
+{
+ var result = null;
+ var view = state.view;
+ var targetPerimeter = view.getPerimeterFunction(state);
+
+ if (targetPerimeter != null)
+ {
+ var next = (this.waypoints != null && this.waypoints.length > 0) ?
+ this.waypoints[this.waypoints.length - 1] :
+ new mxPoint(this.previous.getCenterX(), this.previous.getCenterY());
+ var tmp = targetPerimeter(view.getPerimeterBounds(state),
+ this.edgeState, next, false);
+
+ if (tmp != null)
+ {
+ result = tmp;
+ }
+ }
+ else
+ {
+ result = new mxPoint(state.getCenterX(), state.getCenterY());
+ }
+
+ return result;
+};
+
+/**
+ * Function: getSourcePerimeterPoint
+ *
+ * Hook to update the icon position(s) based on a mouseOver event. This is
+ * an empty implementation.
+ *
+ * Parameters:
+ *
+ * state - <mxCellState> that represents the target cell state.
+ * next - <mxPoint> that represents the next point along the previewed edge.
+ * me - <mxMouseEvent> that represents the mouse move.
+ */
+mxConnectionHandler.prototype.getSourcePerimeterPoint = function(state, next, me)
+{
+ var result = null;
+ var view = state.view;
+ var sourcePerimeter = view.getPerimeterFunction(state);
+
+ if (sourcePerimeter != null)
+ {
+ var tmp = sourcePerimeter(view.getPerimeterBounds(state), state, next, false);
+
+ if (tmp != null)
+ {
+ result = tmp;
+ }
+ }
+ else
+ {
+ result = new mxPoint(state.getCenterX(), state.getCenterY());
+ }
+
+ return result;
+};
+
+
+/**
+ * Function: updateIcons
+ *
+ * Hook to update the icon position(s) based on a mouseOver event. This is
+ * an empty implementation.
+ *
+ * Parameters:
+ *
+ * state - <mxCellState> under the mouse.
+ * icons - Array of currently displayed icons.
+ * me - <mxMouseEvent> that contains the mouse event.
+ */
+mxConnectionHandler.prototype.updateIcons = function(state, icons, me)
+{
+ // empty
+};
+
+/**
+ * Function: isStopEvent
+ *
+ * Returns true if the given mouse up event should stop this handler. The
+ * connection will be created if <error> is null. Note that this is only
+ * called if <waypointsEnabled> is true. This implemtation returns true
+ * if there is a cell state in the given event.
+ */
+mxConnectionHandler.prototype.isStopEvent = function(me)
+{
+ return me.getState() != null;
+};
+
+/**
+ * Function: addWaypoint
+ *
+ * Adds the waypoint for the given event to <waypoints>.
+ */
+mxConnectionHandler.prototype.addWaypointForEvent = function(me)
+{
+ var point = mxUtils.convertPoint(this.graph.container, me.getX(), me.getY());
+ var dx = Math.abs(point.x - this.first.x);
+ var dy = Math.abs(point.y - this.first.y);
+ var addPoint = this.waypoints != null || (this.mouseDownCounter > 1 &&
+ (dx > this.graph.tolerance || dy > this.graph.tolerance));
+
+ if (addPoint)
+ {
+ if (this.waypoints == null)
+ {
+ this.waypoints = [];
+ }
+
+ var scale = this.graph.view.scale;
+ var point = new mxPoint(this.graph.snap(me.getGraphX() / scale) * scale,
+ this.graph.snap(me.getGraphY() / scale) * scale);
+ this.waypoints.push(point);
+ }
+};
+
+/**
+ * Function: mouseUp
+ *
+ * Handles the event by inserting the new connection.
+ */
+mxConnectionHandler.prototype.mouseUp = function(sender, me)
+{
+ if (!me.isConsumed() && this.isConnecting())
+ {
+ if (this.waypointsEnabled && !this.isStopEvent(me))
+ {
+ this.addWaypointForEvent(me);
+ me.consume();
+
+ return;
+ }
+
+ // Inserts the edge if no validation error exists
+ if (this.error == null)
+ {
+ var source = (this.previous != null) ? this.previous.cell : null;
+ var target = null;
+
+ if (this.constraintHandler.currentConstraint != null &&
+ this.constraintHandler.currentFocus != null)
+ {
+ target = this.constraintHandler.currentFocus.cell;
+ }
+
+ if (target == null && this.marker.hasValidState())
+ {
+ target = this.marker.validState.cell;
+ }
+
+ this.connect(source, target, me.getEvent(), me.getCell());
+ }
+ else
+ {
+ // Selects the source terminal for self-references
+ if (this.previous != null && this.marker.validState != null &&
+ this.previous.cell == this.marker.validState.cell)
+ {
+ this.graph.selectCellForEvent(this.marker.source, evt);
+ }
+
+ // Displays the error message if it is not an empty string,
+ // for empty error messages, the event is silently dropped
+ if (this.error.length > 0)
+ {
+ this.graph.validationAlert(this.error);
+ }
+ }
+
+ // Redraws the connect icons and resets the handler state
+ this.destroyIcons(this.icons);
+ me.consume();
+ }
+
+ if (this.first != null)
+ {
+ this.reset();
+ }
+
+ this.tapAndHoldInProgress = false;
+ this.tapAndHoldValid = false;
+};
+
+/**
+ * Function: reset
+ *
+ * Resets the state of this handler.
+ */
+mxConnectionHandler.prototype.reset = function()
+{
+ if (this.shape != null)
+ {
+ this.shape.destroy();
+ this.shape = null;
+ }
+
+ this.destroyIcons(this.icons);
+ this.icons = null;
+ this.marker.reset();
+ this.constraintHandler.reset();
+ this.selectedIcon = null;
+ this.edgeState = null;
+ this.previous = null;
+ this.error = null;
+ this.sourceConstraint = null;
+ this.mouseDownCounter = 0;
+ this.first = null;
+ this.icon = null;
+
+ this.fireEvent(new mxEventObject(mxEvent.RESET));
+};
+
+/**
+ * Function: drawPreview
+ *
+ * Redraws the preview edge using the color and width returned by
+ * <getEdgeColor> and <getEdgeWidth>.
+ */
+mxConnectionHandler.prototype.drawPreview = function()
+{
+ var valid = this.error == null;
+ var color = this.getEdgeColor(valid);
+
+ if (this.shape.dialect == mxConstants.DIALECT_SVG)
+ {
+ this.shape.innerNode.setAttribute('stroke', color);
+ }
+ else
+ {
+ this.shape.node.strokecolor = color;
+ }
+
+ this.shape.strokewidth = this.getEdgeWidth(valid);
+ this.shape.redraw();
+
+ // Workaround to force a repaint in AppleWebKit
+ mxUtils.repaintGraph(this.graph, this.shape.points[1]);
+};
+
+/**
+ * Function: getEdgeColor
+ *
+ * Returns the color used to draw the preview edge. This returns green if
+ * there is no edge validation error and red otherwise.
+ *
+ * Parameters:
+ *
+ * valid - Boolean indicating if the color for a valid edge should be
+ * returned.
+ */
+mxConnectionHandler.prototype.getEdgeColor = function(valid)
+{
+ return (valid) ? mxConstants.VALID_COLOR : mxConstants.INVALID_COLOR;
+};
+
+/**
+ * Function: getEdgeWidth
+ *
+ * Returns the width used to draw the preview edge. This returns 3 if
+ * there is no edge validation error and 1 otherwise.
+ *
+ * Parameters:
+ *
+ * valid - Boolean indicating if the width for a valid edge should be
+ * returned.
+ */
+mxConnectionHandler.prototype.getEdgeWidth = function(valid)
+{
+ return (valid) ? 3 : 1;
+};
+
+/**
+ * Function: connect
+ *
+ * Connects the given source and target using a new edge. This
+ * implementation uses <createEdge> to create the edge.
+ *
+ * Parameters:
+ *
+ * source - <mxCell> that represents the source terminal.
+ * target - <mxCell> that represents the target terminal.
+ * evt - Mousedown event of the connect gesture.
+ * dropTarget - <mxCell> that represents the cell under the mouse when it was
+ * released.
+ */
+mxConnectionHandler.prototype.connect = function(source, target, evt, dropTarget)
+{
+ if (target != null || this.isCreateTarget() || this.graph.allowDanglingEdges)
+ {
+ // Uses the common parent of source and target or
+ // the default parent to insert the edge
+ var model = this.graph.getModel();
+ var edge = null;
+
+ model.beginUpdate();
+ try
+ {
+ if (source != null && target == null && this.isCreateTarget())
+ {
+ target = this.createTargetVertex(evt, source);
+
+ if (target != null)
+ {
+ dropTarget = this.graph.getDropTarget([target], evt, dropTarget);
+
+ // Disables edges as drop targets if the target cell was created
+ // FIXME: Should not shift if vertex was aligned (same in Java)
+ if (dropTarget == null || !this.graph.getModel().isEdge(dropTarget))
+ {
+ var pstate = this.graph.getView().getState(dropTarget);
+
+ if (pstate != null)
+ {
+ var tmp = model.getGeometry(target);
+ tmp.x -= pstate.origin.x;
+ tmp.y -= pstate.origin.y;
+ }
+ }
+ else
+ {
+ dropTarget = this.graph.getDefaultParent();
+ }
+
+ this.graph.addCell(target, dropTarget);
+ }
+ }
+
+ var parent = this.graph.getDefaultParent();
+
+ if (source != null && target != null &&
+ model.getParent(source) == model.getParent(target) &&
+ model.getParent(model.getParent(source)) != model.getRoot())
+ {
+ parent = model.getParent(source);
+
+ if ((source.geometry != null && source.geometry.relative) &&
+ (target.geometry != null && target.geometry.relative))
+ {
+ parent = model.getParent(parent);
+ }
+ }
+
+ // Uses the value of the preview edge state for inserting
+ // the new edge into the graph
+ var value = null;
+ var style = null;
+
+ if (this.edgeState != null)
+ {
+ value = this.edgeState.cell.value;
+ style = this.edgeState.cell.style;
+ }
+
+ edge = this.insertEdge(parent, null, value, source, target, style);
+
+ if (edge != null)
+ {
+ // Updates the connection constraints
+ this.graph.setConnectionConstraint(edge, source, true, this.sourceConstraint);
+ this.graph.setConnectionConstraint(edge, target, false, this.constraintHandler.currentConstraint);
+
+ // Uses geometry of the preview edge state
+ if (this.edgeState != null)
+ {
+ model.setGeometry(edge, this.edgeState.cell.geometry);
+ }
+
+ // Makes sure the edge has a non-null, relative geometry
+ var geo = model.getGeometry(edge);
+
+ if (geo == null)
+ {
+ geo = new mxGeometry();
+ geo.relative = true;
+
+ model.setGeometry(edge, geo);
+ }
+
+ // Uses scaled waypoints in geometry
+ if (this.waypoints != null && this.waypoints.length > 0)
+ {
+ var s = this.graph.view.scale;
+ var tr = this.graph.view.translate;
+ geo.points = [];
+
+ for (var i = 0; i < this.waypoints.length; i++)
+ {
+ var pt = this.waypoints[i];
+ geo.points.push(new mxPoint(pt.x / s - tr.x, pt.y / s - tr.y));
+ }
+ }
+
+ if (target == null)
+ {
+ var pt = this.graph.getPointForEvent(evt, false);
+ pt.x -= this.graph.panDx / this.graph.view.scale;
+ pt.y -= this.graph.panDy / this.graph.view.scale;
+ geo.setTerminalPoint(pt, false);
+ }
+
+ this.fireEvent(new mxEventObject(mxEvent.CONNECT,
+ 'cell', edge, 'event', evt, 'target', dropTarget));
+ }
+ }
+ catch (e)
+ {
+ mxLog.show();
+ mxLog.debug(e.message);
+ }
+ finally
+ {
+ model.endUpdate();
+ }
+
+ if (this.select)
+ {
+ this.selectCells(edge, target);
+ }
+ }
+};
+
+/**
+ * Function: selectCells
+ *
+ * Selects the given edge after adding a new connection. The target argument
+ * contains the target vertex if one has been inserted.
+ */
+mxConnectionHandler.prototype.selectCells = function(edge, target)
+{
+ this.graph.setSelectionCell(edge);
+};
+
+/**
+ * Function: insertEdge
+ *
+ * Creates, inserts and returns the new edge for the given parameters. This
+ * implementation does only use <createEdge> if <factoryMethod> is defined,
+ * otherwise <mxGraph.insertEdge> will be used.
+ */
+mxConnectionHandler.prototype.insertEdge = function(parent, id, value, source, target, style)
+{
+ if (this.factoryMethod == null)
+ {
+ return this.graph.insertEdge(parent, id, value, source, target, style);
+ }
+ else
+ {
+ var edge = this.createEdge(value, source, target, style);
+ edge = this.graph.addEdge(edge, parent, source, target);
+
+ return edge;
+ }
+};
+
+/**
+ * Function: createTargetVertex
+ *
+ * Hook method for creating new vertices on the fly if no target was
+ * under the mouse. This is only called if <createTarget> is true and
+ * returns null.
+ *
+ * Parameters:
+ *
+ * evt - Mousedown event of the connect gesture.
+ * source - <mxCell> that represents the source terminal.
+ */
+mxConnectionHandler.prototype.createTargetVertex = function(evt, source)
+{
+ // Uses the first non-relative source
+ var geo = this.graph.getCellGeometry(source);
+
+ while (geo != null && geo.relative)
+ {
+ source = this.graph.getModel().getParent(source);
+ geo = this.graph.getCellGeometry(source);
+ }
+
+ var clone = this.graph.cloneCells([source])[0];
+ var geo = this.graph.getModel().getGeometry(clone);
+
+ if (geo != null)
+ {
+ var point = this.graph.getPointForEvent(evt);
+ geo.x = this.graph.snap(point.x - geo.width / 2) - this.graph.panDx / this.graph.view.scale;
+ geo.y = this.graph.snap(point.y - geo.height / 2) - this.graph.panDy / this.graph.view.scale;
+
+ // Aligns with source if within certain tolerance
+ if (this.first != null)
+ {
+ var sourceState = this.graph.view.getState(source);
+
+ if (sourceState != null)
+ {
+ var tol = this.getAlignmentTolerance();
+
+ if (Math.abs(this.graph.snap(this.first.x) -
+ this.graph.snap(point.x)) <= tol)
+ {
+ geo.x = sourceState.x;
+ }
+ else if (Math.abs(this.graph.snap(this.first.y) -
+ this.graph.snap(point.y)) <= tol)
+ {
+ geo.y = sourceState.y;
+ }
+ }
+ }
+ }
+
+ return clone;
+};
+
+/**
+ * Function: getAlignmentTolerance
+ *
+ * Returns the tolerance for aligning new targets to sources.
+ */
+mxConnectionHandler.prototype.getAlignmentTolerance = function()
+{
+ return (this.graph.isGridEnabled()) ?
+ this.graph.gridSize : this.graph.tolerance;
+};
+
+/**
+ * Function: createEdge
+ *
+ * Creates and returns a new edge using <factoryMethod> if one exists. If
+ * no factory method is defined, then a new default edge is returned. The
+ * source and target arguments are informal, the actual connection is
+ * setup later by the caller of this function.
+ *
+ * Parameters:
+ *
+ * value - Value to be used for creating the edge.
+ * source - <mxCell> that represents the source terminal.
+ * target - <mxCell> that represents the target terminal.
+ * style - Optional style from the preview edge.
+ */
+mxConnectionHandler.prototype.createEdge = function(value, source, target, style)
+{
+ var edge = null;
+
+ // Creates a new edge using the factoryMethod
+ if (this.factoryMethod != null)
+ {
+ edge = this.factoryMethod(source, target, style);
+ }
+
+ if (edge == null)
+ {
+ edge = new mxCell(value || '');
+ edge.setEdge(true);
+ edge.setStyle(style);
+
+ var geo = new mxGeometry();
+ geo.relative = true;
+ edge.setGeometry(geo);
+ }
+
+ return edge;
+};
+
+/**
+ * Function: destroy
+ *
+ * Destroys the handler and all its resources and DOM nodes. This should be
+ * called on all instances. It is called automatically for the built-in
+ * instance created for each <mxGraph>.
+ */
+mxConnectionHandler.prototype.destroy = function()
+{
+ this.graph.removeMouseListener(this);
+
+ if (this.shape != null)
+ {
+ this.shape.destroy();
+ this.shape = null;
+ }
+
+ if (this.marker != null)
+ {
+ this.marker.destroy();
+ this.marker = null;
+ }
+
+ if (this.constraintHandler != null)
+ {
+ this.constraintHandler.destroy();
+ this.constraintHandler = null;
+ }
+
+ if (this.changeHandler != null)
+ {
+ this.graph.getModel().removeListener(this.changeHandler);
+ this.graph.getView().removeListener(this.changeHandler);
+ this.changeHandler = null;
+ }
+
+ if (this.drillHandler != null)
+ {
+ this.graph.removeListener(this.drillHandler);
+ this.graph.getView().removeListener(this.drillHandler);
+ this.drillHandler = null;
+ }
+};
diff --git a/src/js/handler/mxConstraintHandler.js b/src/js/handler/mxConstraintHandler.js
new file mode 100644
index 0000000..39b3ab6
--- /dev/null
+++ b/src/js/handler/mxConstraintHandler.js
@@ -0,0 +1,308 @@
+/**
+ * $Id: mxConstraintHandler.js,v 1.15 2012-11-01 16:13:41 gaudenz Exp $
+ * Copyright (c) 2006-2010, JGraph Ltd
+ */
+/**
+ * Class: mxConstraintHandler
+ *
+ * Handles constraints on connection targets. This class is in charge of
+ * showing fixed points when the mouse is over a vertex and handles constraints
+ * to establish new connections.
+ *
+ * Constructor: mxConstraintHandler
+ *
+ * Constructs an new constraint handler.
+ *
+ * Parameters:
+ *
+ * graph - Reference to the enclosing <mxGraph>.
+ * factoryMethod - Optional function to create the edge. The function takes
+ * the source and target <mxCell> as the first and second argument and
+ * returns the <mxCell> that represents the new edge.
+ */
+function mxConstraintHandler(graph)
+{
+ this.graph = graph;
+};
+
+/**
+ * Variable: pointImage
+ *
+ * <mxImage> to be used as the image for fixed connection points.
+ */
+mxConstraintHandler.prototype.pointImage = new mxImage(mxClient.imageBasePath + '/point.gif', 5, 5);
+
+/**
+ * Variable: graph
+ *
+ * Reference to the enclosing <mxGraph>.
+ */
+mxConstraintHandler.prototype.graph = null;
+
+/**
+ * Variable: enabled
+ *
+ * Specifies if events are handled. Default is true.
+ */
+mxConstraintHandler.prototype.enabled = true;
+
+/**
+ * Variable: highlightColor
+ *
+ * Specifies the color for the highlight. Default is <mxConstants.DEFAULT_VALID_COLOR>.
+ */
+mxConstraintHandler.prototype.highlightColor = mxConstants.DEFAULT_VALID_COLOR;
+
+/**
+ * Function: isEnabled
+ *
+ * Returns true if events are handled. This implementation
+ * returns <enabled>.
+ */
+mxConstraintHandler.prototype.isEnabled = function()
+{
+ return this.enabled;
+};
+
+/**
+ * Function: setEnabled
+ *
+ * Enables or disables event handling. This implementation
+ * updates <enabled>.
+ *
+ * Parameters:
+ *
+ * enabled - Boolean that specifies the new enabled state.
+ */
+mxConstraintHandler.prototype.setEnabled = function(enabled)
+{
+ this.enabled = enabled;
+};
+
+/**
+ * Function: reset
+ *
+ * Resets the state of this handler.
+ */
+mxConstraintHandler.prototype.reset = function()
+{
+ if (this.focusIcons != null)
+ {
+ for (var i = 0; i < this.focusIcons.length; i++)
+ {
+ this.focusIcons[i].destroy();
+ }
+
+ this.focusIcons = null;
+ }
+
+ if (this.focusHighlight != null)
+ {
+ this.focusHighlight.destroy();
+ this.focusHighlight = null;
+ }
+
+ this.currentConstraint = null;
+ this.currentFocusArea = null;
+ this.currentPoint = null;
+ this.currentFocus = null;
+ this.focusPoints = null;
+};
+
+/**
+ * Function: getTolerance
+ *
+ * Returns the tolerance to be used for intersecting connection points.
+ */
+mxConstraintHandler.prototype.getTolerance = function()
+{
+ return this.graph.getTolerance();
+};
+
+/**
+ * Function: getImageForConstraint
+ *
+ * Returns the tolerance to be used for intersecting connection points.
+ */
+mxConstraintHandler.prototype.getImageForConstraint = function(state, constraint, point)
+{
+ return this.pointImage;
+};
+
+/**
+ * Function: isEventIgnored
+ *
+ * Returns true if the given <mxMouseEvent> should be ignored in <update>. This
+ * implementation always returns false.
+ */
+mxConstraintHandler.prototype.isEventIgnored = function(me, source)
+{
+ return false;
+};
+
+/**
+ * Function: update
+ *
+ * Updates the state of this handler based on the given <mxMouseEvent>.
+ * Source is a boolean indicating if the cell is a source or target.
+ */
+mxConstraintHandler.prototype.update = function(me, source)
+{
+ if (this.isEnabled() && !this.isEventIgnored(me))
+ {
+ var tol = this.getTolerance();
+ var mouse = new mxRectangle(me.getGraphX() - tol, me.getGraphY() - tol, 2 * tol, 2 * tol);
+ var connectable = (me.getCell() != null) ? this.graph.isCellConnectable(me.getCell()) : false;
+
+ if ((this.currentFocusArea == null || (!mxUtils.intersects(this.currentFocusArea, mouse) ||
+ (me.getState() != null && this.currentFocus != null && connectable))))
+ {
+ this.currentFocusArea = null;
+
+ if (me.getState() != this.currentFocus)
+ {
+ this.currentFocus = null;
+ this.constraints = (me.getState() != null && connectable) ?
+ this.graph.getAllConnectionConstraints(me.getState(), source) : null;
+
+ // Only uses cells which have constraints
+ if (this.constraints != null)
+ {
+ this.currentFocus = me.getState();
+ this.currentFocusArea = new mxRectangle(me.getState().x, me.getState().y, me.getState().width, me.getState().height);
+
+ if (this.focusIcons != null)
+ {
+ for (var i = 0; i < this.focusIcons.length; i++)
+ {
+ this.focusIcons[i].destroy();
+ }
+
+ this.focusIcons = null;
+ this.focusPoints = null;
+ }
+
+ this.focusIcons = [];
+ this.focusPoints = [];
+
+ for (var i = 0; i < this.constraints.length; i++)
+ {
+ var cp = this.graph.getConnectionPoint(me.getState(), this.constraints[i]);
+ var img = this.getImageForConstraint(me.getState(), this.constraints[i], cp);
+
+ var src = img.src;
+ var bounds = new mxRectangle(cp.x - img.width / 2,
+ cp.y - img.height / 2, img.width, img.height);
+ var icon = new mxImageShape(bounds, src);
+ icon.dialect = (this.graph.dialect == mxConstants.DIALECT_SVG) ?
+ mxConstants.DIALECT_SVG :
+ mxConstants.DIALECT_VML;
+ icon.init(this.graph.getView().getOverlayPane());
+
+ // Move the icon behind all other overlays
+ if (icon.node.previousSibling != null)
+ {
+ icon.node.parentNode.insertBefore(icon.node, icon.node.parentNode.firstChild);
+ }
+
+ var getState = mxUtils.bind(this, function()
+ {
+ return (this.currentFocus != null) ? this.currentFocus : me.getState();
+ });
+
+ icon.redraw();
+
+ mxEvent.redirectMouseEvents(icon.node, this.graph, getState);
+ this.currentFocusArea.add(icon.bounds);
+ this.focusIcons.push(icon);
+ this.focusPoints.push(cp);
+ }
+
+ this.currentFocusArea.grow(tol);
+ }
+ else if (this.focusIcons != null)
+ {
+ if (this.focusHighlight != null)
+ {
+ this.focusHighlight.destroy();
+ this.focusHighlight = null;
+ }
+
+ for (var i = 0; i < this.focusIcons.length; i++)
+ {
+ this.focusIcons[i].destroy();
+ }
+
+ this.focusIcons = null;
+ this.focusPoints = null;
+ }
+ }
+ }
+
+ this.currentConstraint = null;
+ this.currentPoint = null;
+
+ if (this.focusIcons != null && this.constraints != null &&
+ (me.getState() == null || this.currentFocus == me.getState()))
+ {
+ for (var i = 0; i < this.focusIcons.length; i++)
+ {
+ if (mxUtils.intersects(this.focusIcons[i].bounds, mouse))
+ {
+ this.currentConstraint = this.constraints[i];
+ this.currentPoint = this.focusPoints[i];
+
+ var tmp = this.focusIcons[i].bounds.clone();
+ tmp.grow((mxClient.IS_IE) ? 3 : 2);
+
+ if (mxClient.IS_IE)
+ {
+ tmp.width -= 1;
+ tmp.height -= 1;
+ }
+
+ if (this.focusHighlight == null)
+ {
+ var hl = new mxRectangleShape(tmp, null, this.highlightColor, 3);
+ hl.dialect = (this.graph.dialect == mxConstants.DIALECT_SVG) ?
+ mxConstants.DIALECT_SVG :
+ mxConstants.DIALECT_VML;
+ hl.init(this.graph.getView().getOverlayPane());
+ this.focusHighlight = hl;
+
+ var getState = mxUtils.bind(this, function()
+ {
+ return (this.currentFocus != null) ? this.currentFocus : me.getState();
+ });
+
+ mxEvent.redirectMouseEvents(hl.node, this.graph, getState/*, mouseDown*/);
+ }
+ else
+ {
+ this.focusHighlight.bounds = tmp;
+ this.focusHighlight.redraw();
+ }
+
+ break;
+ }
+ }
+ }
+
+ if (this.currentConstraint == null &&
+ this.focusHighlight != null)
+ {
+ this.focusHighlight.destroy();
+ this.focusHighlight = null;
+ }
+ }
+};
+
+/**
+ * Function: destroy
+ *
+ * Destroy this handler.
+ */
+mxConstraintHandler.prototype.destroy = function()
+{
+ this.reset();
+}; \ No newline at end of file
diff --git a/src/js/handler/mxEdgeHandler.js b/src/js/handler/mxEdgeHandler.js
new file mode 100644
index 0000000..2028342
--- /dev/null
+++ b/src/js/handler/mxEdgeHandler.js
@@ -0,0 +1,1529 @@
+/**
+ * $Id: mxEdgeHandler.js,v 1.178 2012-09-12 09:16:23 gaudenz Exp $
+ * Copyright (c) 2006-2010, JGraph Ltd
+ */
+/**
+ * Class: mxEdgeHandler
+ *
+ * Graph event handler that reconnects edges and modifies control points and
+ * the edge label location. Uses <mxTerminalMarker> for finding and
+ * highlighting new source and target vertices. This handler is automatically
+ * created in <mxGraph.createHandler> for each selected edge.
+ *
+ * To enable adding/removing control points, the following code can be used:
+ *
+ * (code)
+ * mxEdgeHandler.prototype.addEnabled = true;
+ * mxEdgeHandler.prototype.removeEnabled = true;
+ * (end)
+ *
+ * Note: This experimental feature is not recommended for production use.
+ *
+ * Constructor: mxEdgeHandler
+ *
+ * Constructs an edge handler for the specified <mxCellState>.
+ *
+ * Parameters:
+ *
+ * state - <mxCellState> of the cell to be handled.
+ */
+function mxEdgeHandler(state)
+{
+ if (state != null)
+ {
+ this.state = state;
+ this.init();
+ }
+};
+
+/**
+ * Variable: graph
+ *
+ * Reference to the enclosing <mxGraph>.
+ */
+mxEdgeHandler.prototype.graph = null;
+
+/**
+ * Variable: state
+ *
+ * Reference to the <mxCellState> being modified.
+ */
+mxEdgeHandler.prototype.state = null;
+
+/**
+ * Variable: marker
+ *
+ * Holds the <mxTerminalMarker> which is used for highlighting terminals.
+ */
+mxEdgeHandler.prototype.marker = null;
+
+/**
+ * Variable: constraintHandler
+ *
+ * Holds the <mxConstraintHandler> used for drawing and highlighting
+ * constraints.
+ */
+mxEdgeHandler.prototype.constraintHandler = null;
+
+/**
+ * Variable: error
+ *
+ * Holds the current validation error while a connection is being changed.
+ */
+mxEdgeHandler.prototype.error = null;
+
+/**
+ * Variable: shape
+ *
+ * Holds the <mxShape> that represents the preview edge.
+ */
+mxEdgeHandler.prototype.shape = null;
+
+/**
+ * Variable: bends
+ *
+ * Holds the <mxShapes> that represent the points.
+ */
+mxEdgeHandler.prototype.bends = null;
+
+/**
+ * Variable: labelShape
+ *
+ * Holds the <mxShape> that represents the label position.
+ */
+mxEdgeHandler.prototype.labelShape = null;
+
+/**
+ * Variable: cloneEnabled
+ *
+ * Specifies if cloning by control-drag is enabled. Default is true.
+ */
+mxEdgeHandler.prototype.cloneEnabled = true;
+
+/**
+ * Variable: addEnabled
+ *
+ * Specifies if adding bends by shift-click is enabled. Default is false.
+ * Note: This experimental feature is not recommended for production use.
+ */
+mxEdgeHandler.prototype.addEnabled = false;
+
+/**
+ * Variable: removeEnabled
+ *
+ * Specifies if removing bends by shift-click is enabled. Default is false.
+ * Note: This experimental feature is not recommended for production use.
+ */
+mxEdgeHandler.prototype.removeEnabled = false;
+
+/**
+ * Variable: preferHtml
+ *
+ * Specifies if bends should be added to the graph container. This is updated
+ * in <init> based on whether the edge or one of its terminals has an HTML
+ * label in the container.
+ */
+mxEdgeHandler.prototype.preferHtml = false;
+
+/**
+ * Variable: allowHandleBoundsCheck
+ *
+ * Specifies if the bounds of handles should be used for hit-detection in IE
+ * Default is true.
+ */
+mxEdgeHandler.prototype.allowHandleBoundsCheck = true;
+
+/**
+ * Variable: snapToTerminals
+ *
+ * Specifies if waypoints should snap to the routing centers of terminals.
+ * Default is false.
+ */
+mxEdgeHandler.prototype.snapToTerminals = false;
+
+/**
+ * Variable: crisp
+ *
+ * Specifies if the edge handles should be rendered in crisp mode. Default is
+ * true.
+ */
+mxEdgeHandler.prototype.crisp = true;
+
+/**
+ * Variable: handleImage
+ *
+ * Optional <mxImage> to be used as handles. Default is null.
+ */
+mxEdgeHandler.prototype.handleImage = null;
+
+/**
+ * Variable: tolerance
+ *
+ * Optional tolerance for hit-detection in <getHandleForEvent>. Default is 0.
+ */
+mxEdgeHandler.prototype.tolerance = 0;
+
+/**
+ * Function: init
+ *
+ * Initializes the shapes required for this edge handler.
+ */
+mxEdgeHandler.prototype.init = function()
+{
+ this.graph = this.state.view.graph;
+ this.marker = this.createMarker();
+ this.constraintHandler = new mxConstraintHandler(this.graph);
+
+ // Clones the original points from the cell
+ // and makes sure at least one point exists
+ this.points = [];
+
+ // Uses the absolute points of the state
+ // for the initial configuration and preview
+ this.abspoints = this.getSelectionPoints(this.state);
+ this.shape = this.createSelectionShape(this.abspoints);
+ this.shape.dialect = (this.graph.dialect != mxConstants.DIALECT_SVG) ?
+ mxConstants.DIALECT_VML : mxConstants.DIALECT_SVG;
+ this.shape.init(this.graph.getView().getOverlayPane());
+ this.shape.node.style.cursor = mxConstants.CURSOR_MOVABLE_EDGE;
+
+ // Event handling
+ var md = (mxClient.IS_TOUCH) ? 'touchstart' : 'mousedown';
+ var mm = (mxClient.IS_TOUCH) ? 'touchmove' : 'mousemove';
+ var mu = (mxClient.IS_TOUCH) ? 'touchend' : 'mouseup';
+
+ mxEvent.addListener(this.shape.node, 'dblclick',
+ mxUtils.bind(this, function(evt)
+ {
+ this.graph.dblClick(evt, this.state.cell);
+ })
+ );
+ mxEvent.addListener(this.shape.node, md,
+ mxUtils.bind(this, function(evt)
+ {
+ if (this.addEnabled && this.isAddPointEvent(evt))
+ {
+ this.addPoint(this.state, evt);
+ }
+ else
+ {
+ this.graph.fireMouseEvent(mxEvent.MOUSE_DOWN,
+ new mxMouseEvent(evt, this.state));
+ }
+ })
+ );
+ mxEvent.addListener(this.shape.node, mm,
+ mxUtils.bind(this, function(evt)
+ {
+ var cell = this.state.cell;
+
+ // Finds the cell under the mouse if the edge is being connected
+ // in which case the edge is never highlighted as it cannot
+ // be its own source or target terminal (transparent preview)
+ if (this.index != null)
+ {
+ var pt = mxUtils.convertPoint(this.graph.container,
+ mxEvent.getClientX(evt), mxEvent.getClientY(evt));
+ cell = this.graph.getCellAt(pt.x, pt.y);
+
+ // Swimlane content area is transparent in this case
+ if (this.graph.isSwimlane(cell) && this.graph.hitsSwimlaneContent(cell, pt.x, pt.y))
+ {
+ cell = null;
+ }
+ }
+
+ this.graph.fireMouseEvent(mxEvent.MOUSE_MOVE,
+ new mxMouseEvent(evt, this.graph.getView().getState(cell)));
+ })
+ );
+ mxEvent.addListener(this.shape.node, mu,
+ mxUtils.bind(this, function(evt)
+ {
+ this.graph.fireMouseEvent(mxEvent.MOUSE_UP,
+ new mxMouseEvent(evt, this.state));
+ })
+ );
+
+ // Updates preferHtml
+ this.preferHtml = this.state.text != null &&
+ this.state.text.node.parentNode == this.graph.container;
+
+ if (!this.preferHtml)
+ {
+ // Checks source terminal
+ var sourceState = this.state.getVisibleTerminalState(true);
+
+ if (sourceState != null)
+ {
+ this.preferHtml = sourceState.text != null &&
+ sourceState.text.node.parentNode == this.graph.container;
+ }
+
+ if (!this.preferHtml)
+ {
+ // Checks target terminal
+ var targetState = this.state.getVisibleTerminalState(false);
+
+ if (targetState != null)
+ {
+ this.preferHtml = targetState.text != null &&
+ targetState.text.node.parentNode == this.graph.container;
+ }
+ }
+ }
+
+ // Creates bends for the non-routed absolute points
+ // or bends that don't correspond to points
+ if (this.graph.getSelectionCount() < mxGraphHandler.prototype.maxCells ||
+ mxGraphHandler.prototype.maxCells <= 0)
+ {
+ this.bends = this.createBends();
+ }
+
+ // Adds a rectangular handle for the label position
+ this.label = new mxPoint(this.state.absoluteOffset.x, this.state.absoluteOffset.y);
+ this.labelShape = new mxRectangleShape(new mxRectangle(),
+ mxConstants.LABEL_HANDLE_FILLCOLOR,
+ mxConstants.HANDLE_STROKECOLOR);
+ this.initBend(this.labelShape);
+ this.labelShape.node.style.cursor = mxConstants.CURSOR_LABEL_HANDLE;
+ mxEvent.redirectMouseEvents(this.labelShape.node, this.graph, this.state);
+
+ this.redraw();
+};
+
+/**
+ * Function: isAddPointEvent
+ *
+ * Returns true if the given event is a trigger to add a new point. This
+ * implementation returns true if shift is pressed.
+ */
+mxEdgeHandler.prototype.isAddPointEvent = function(evt)
+{
+ return mxEvent.isShiftDown(evt);
+};
+
+/**
+ * Function: isRemovePointEvent
+ *
+ * Returns true if the given event is a trigger to remove a point. This
+ * implementation returns true if shift is pressed.
+ */
+mxEdgeHandler.prototype.isRemovePointEvent = function(evt)
+{
+ return mxEvent.isShiftDown(evt);
+};
+
+/**
+ * Function: getSelectionPoints
+ *
+ * Returns the list of points that defines the selection stroke.
+ */
+mxEdgeHandler.prototype.getSelectionPoints = function(state)
+{
+ return state.absolutePoints;
+};
+
+/**
+ * Function: createSelectionShape
+ *
+ * Creates the shape used to draw the selection border.
+ */
+mxEdgeHandler.prototype.createSelectionShape = function(points)
+{
+ var shape = new mxPolyline(points, this.getSelectionColor());
+ shape.strokewidth = this.getSelectionStrokeWidth();
+ shape.isDashed = this.isSelectionDashed();
+
+ return shape;
+};
+
+/**
+ * Function: getSelectionColor
+ *
+ * Returns <mxConstants.EDGE_SELECTION_COLOR>.
+ */
+mxEdgeHandler.prototype.getSelectionColor = function()
+{
+ return mxConstants.EDGE_SELECTION_COLOR;
+};
+
+/**
+ * Function: getSelectionStrokeWidth
+ *
+ * Returns <mxConstants.EDGE_SELECTION_STROKEWIDTH>.
+ */
+mxEdgeHandler.prototype.getSelectionStrokeWidth = function()
+{
+ return mxConstants.EDGE_SELECTION_STROKEWIDTH;
+};
+
+/**
+ * Function: isSelectionDashed
+ *
+ * Returns <mxConstants.EDGE_SELECTION_DASHED>.
+ */
+mxEdgeHandler.prototype.isSelectionDashed = function()
+{
+ return mxConstants.EDGE_SELECTION_DASHED;
+};
+
+/**
+ * Function: isConnectableCell
+ *
+ * Returns true if the given cell is connectable. This is a hook to
+ * disable floating connections. This implementation returns true.
+ */
+mxEdgeHandler.prototype.isConnectableCell = function(cell)
+{
+ return true;
+};
+
+/**
+ * Function: createMarker
+ *
+ * Creates and returns the <mxCellMarker> used in <marker>.
+ */
+mxEdgeHandler.prototype.createMarker = function()
+{
+ var marker = new mxCellMarker(this.graph);
+ var self = this; // closure
+
+ // Only returns edges if they are connectable and never returns
+ // the edge that is currently being modified
+ marker.getCell = function(me)
+ {
+ var cell = mxCellMarker.prototype.getCell.apply(this, arguments);
+
+ if (!self.isConnectableCell(cell))
+ {
+ return null;
+ }
+
+ var model = self.graph.getModel();
+
+ if (cell == self.state.cell || (cell != null &&
+ !self.graph.connectableEdges && model.isEdge(cell)))
+ {
+ cell = null;
+ }
+
+ return cell;
+ };
+
+ // Sets the highlight color according to validateConnection
+ marker.isValidState = function(state)
+ {
+ var model = self.graph.getModel();
+ var other = self.graph.view.getTerminalPort(state,
+ self.graph.view.getState(model.getTerminal(self.state.cell,
+ !self.isSource)), !self.isSource);
+ var otherCell = (other != null) ? other.cell : null;
+ var source = (self.isSource) ? state.cell : otherCell;
+ var target = (self.isSource) ? otherCell : state.cell;
+
+ // Updates the error message of the handler
+ self.error = self.validateConnection(source, target);
+
+ return self.error == null;
+ };
+
+ return marker;
+};
+
+/**
+ * Function: validateConnection
+ *
+ * Returns the error message or an empty string if the connection for the
+ * given source, target pair is not valid. Otherwise it returns null. This
+ * implementation uses <mxGraph.getEdgeValidationError>.
+ *
+ * Parameters:
+ *
+ * source - <mxCell> that represents the source terminal.
+ * target - <mxCell> that represents the target terminal.
+ */
+mxEdgeHandler.prototype.validateConnection = function(source, target)
+{
+ return this.graph.getEdgeValidationError(this.state.cell, source, target);
+};
+
+/**
+ * Function: createBends
+ *
+ * Creates and returns the bends used for modifying the edge. This is
+ * typically an array of <mxRectangleShapes>.
+ */
+ mxEdgeHandler.prototype.createBends = function()
+ {
+ var cell = this.state.cell;
+ var bends = [];
+
+ for (var i = 0; i < this.abspoints.length; i++)
+ {
+ if (this.isHandleVisible(i))
+ {
+ var source = i == 0;
+ var target = i == this.abspoints.length - 1;
+ var terminal = source || target;
+
+ if (terminal || this.graph.isCellBendable(cell))
+ {
+ var bend = this.createHandleShape(i);
+ this.initBend(bend);
+
+ if (mxClient.IS_TOUCH)
+ {
+ bend.node.setAttribute('pointer-events', 'none');
+ }
+
+ if (this.isHandleEnabled(i))
+ {
+ if (mxClient.IS_TOUCH)
+ {
+ var getState = mxUtils.bind(this, function(evt)
+ {
+ var pt = mxUtils.convertPoint(this.graph.container, mxEvent.getClientX(evt), mxEvent.getClientY(evt));
+
+ return this.graph.view.getState(this.graph.getCellAt(pt.x, pt.y));
+ });
+
+ mxEvent.redirectMouseEvents(bend.node, this.graph, getState);
+ }
+ else
+ {
+ bend.node.style.cursor = mxConstants.CURSOR_BEND_HANDLE;
+ mxEvent.redirectMouseEvents(bend.node, this.graph, this.state);
+ }
+ }
+
+ bends.push(bend);
+
+ if (!terminal)
+ {
+ this.points.push(new mxPoint(0,0));
+ bend.node.style.visibility = 'hidden';
+ }
+ }
+ }
+ }
+
+ return bends;
+};
+/**
+ * Function: isHandleEnabled
+ *
+ * Creates the shape used to display the given bend.
+ */
+mxEdgeHandler.prototype.isHandleEnabled = function(index)
+{
+ return true;
+};
+
+/**
+ * Function: isHandleVisible
+ *
+ * Returns true if the handle at the given index is visible.
+ */
+mxEdgeHandler.prototype.isHandleVisible = function(index)
+{
+ return true;
+};
+
+/**
+ * Function: createHandleShape
+ *
+ * Creates the shape used to display the given bend. Note that the index may be
+ * null for special cases, such as when called from
+ * <mxElbowEdgeHandler.createVirtualBend>.
+ */
+mxEdgeHandler.prototype.createHandleShape = function(index)
+{
+ if (this.handleImage != null)
+ {
+ return new mxImageShape(new mxRectangle(0, 0, this.handleImage.width, this.handleImage.height), this.handleImage.src);
+ }
+ else
+ {
+ var s = mxConstants.HANDLE_SIZE;
+
+ if (this.preferHtml)
+ {
+ s -= 1;
+ }
+
+ return new mxRectangleShape(new mxRectangle(0, 0, s, s), mxConstants.HANDLE_FILLCOLOR, mxConstants.HANDLE_STROKECOLOR);
+ }
+};
+
+/**
+ * Function: initBend
+ *
+ * Helper method to initialize the given bend.
+ *
+ * Parameters:
+ *
+ * bend - <mxShape> that represents the bend to be initialized.
+ */
+mxEdgeHandler.prototype.initBend = function(bend)
+{
+ bend.crisp = this.crisp;
+
+ if (this.preferHtml)
+ {
+ bend.dialect = mxConstants.DIALECT_STRICTHTML;
+ bend.init(this.graph.container);
+ }
+ else
+ {
+ bend.dialect = (this.graph.dialect != mxConstants.DIALECT_SVG) ?
+ mxConstants.DIALECT_VML : mxConstants.DIALECT_SVG;
+ bend.init(this.graph.getView().getOverlayPane());
+ }
+};
+
+/**
+ * Function: getHandleForEvent
+ *
+ * Returns the index of the handle for the given event.
+ */
+mxEdgeHandler.prototype.getHandleForEvent = function(me)
+{
+ // Finds the handle that triggered the event
+ if (this.bends != null)
+ {
+ // Connection highlight may consume events before they reach sizer handle
+ var tol = this.tolerance;
+ var hit = (this.allowHandleBoundsCheck && (mxClient.IS_IE || tol > 0)) ?
+ new mxRectangle(me.getGraphX() - tol, me.getGraphY() - tol, 2 * tol, 2 * tol) : null;
+
+ for (var i = 0; i < this.bends.length; i++)
+ {
+ if (me.isSource(this.bends[i]) || (hit != null &&
+ this.bends[i].node.style.visibility != 'hidden' &&
+ mxUtils.intersects(this.bends[i].bounds, hit)))
+ {
+ return i;
+ }
+ }
+ }
+
+ if (me.isSource(this.labelShape) || me.isSource(this.state.text))
+ {
+ // Workaround for SELECT element not working in Webkit
+ if ((!mxClient.IS_SF && !mxClient.IS_GC) || me.getSource().nodeName != 'SELECT')
+ {
+ return mxEvent.LABEL_HANDLE;
+ }
+ }
+
+ return null;
+};
+
+/**
+ * Function: mouseDown
+ *
+ * Handles the event by checking if a special element of the handler
+ * was clicked, in which case the index parameter is non-null. The
+ * indices may be one of <LABEL_HANDLE> or the number of the respective
+ * control point. The source and target points are used for reconnecting
+ * the edge.
+ */
+mxEdgeHandler.prototype.mouseDown = function(sender, me)
+{
+ var handle = null;
+
+ // Handles the case where the state in the event points to another
+ // cell if the cell has a HTML label which sits on top of the handles
+ // NOTE: Commented out. This should not be required as all HTML labels
+ // are in order an do not appear behind the handles.
+ //if (mxClient.IS_SVG || me.getState() == this.state)
+ {
+ handle = this.getHandleForEvent(me);
+ }
+
+ if (handle != null && !me.isConsumed() && this.graph.isEnabled() &&
+ !this.graph.isForceMarqueeEvent(me.getEvent()))
+ {
+ if (this.removeEnabled && this.isRemovePointEvent(me.getEvent()))
+ {
+ this.removePoint(this.state, handle);
+ }
+ else if (handle != mxEvent.LABEL_HANDLE || this.graph.isLabelMovable(me.getCell()))
+ {
+ this.start(me.getX(), me.getY(), handle);
+ }
+
+ me.consume();
+ }
+};
+
+/**
+ * Function: start
+ *
+ * Starts the handling of the mouse gesture.
+ */
+mxEdgeHandler.prototype.start = function(x, y, index)
+{
+ this.startX = x;
+ this.startY = y;
+
+ this.isSource = (this.bends == null) ? false : index == 0;
+ this.isTarget = (this.bends == null) ? false : index == this.bends.length - 1;
+ this.isLabel = index == mxEvent.LABEL_HANDLE;
+
+ if (this.isSource || this.isTarget)
+ {
+ var cell = this.state.cell;
+ var terminal = this.graph.model.getTerminal(cell, this.isSource);
+
+ if ((terminal == null && this.graph.isTerminalPointMovable(cell, this.isSource)) ||
+ (terminal != null && this.graph.isCellDisconnectable(cell, terminal, this.isSource)))
+ {
+ this.index = index;
+ }
+ }
+ else
+ {
+ this.index = index;
+ }
+};
+
+/**
+ * Function: clonePreviewState
+ *
+ * Returns a clone of the current preview state for the given point and terminal.
+ */
+mxEdgeHandler.prototype.clonePreviewState = function(point, terminal)
+{
+ return this.state.clone();
+};
+
+/**
+ * Function: getSnapToTerminalTolerance
+ *
+ * Returns the tolerance for the guides. Default value is
+ * gridSize * scale / 2.
+ */
+mxEdgeHandler.prototype.getSnapToTerminalTolerance = function()
+{
+ return this.graph.gridSize * this.graph.view.scale / 2;
+};
+
+/**
+ * Function: getPointForEvent
+ *
+ * Returns the point for the given event.
+ */
+mxEdgeHandler.prototype.getPointForEvent = function(me)
+{
+ var point = new mxPoint(me.getGraphX(), me.getGraphY());
+
+ var tt = this.getSnapToTerminalTolerance();
+ var view = this.graph.getView();
+ var overrideX = false;
+ var overrideY = false;
+
+ if (this.snapToTerminals && tt > 0)
+ {
+ function snapToPoint(pt)
+ {
+ if (pt != null)
+ {
+ var x = pt.x;
+
+ if (Math.abs(point.x - x) < tt)
+ {
+ point.x = x;
+ overrideX = true;
+ }
+
+ var y = pt.y;
+
+ if (Math.abs(point.y - y) < tt)
+ {
+ point.y = y;
+ overrideY = true;
+ }
+ }
+ }
+
+ // Temporary function
+ function snapToTerminal(terminal)
+ {
+ if (terminal != null)
+ {
+ snapToPoint.call(this, new mxPoint(view.getRoutingCenterX(terminal),
+ view.getRoutingCenterY(terminal)));
+ }
+ };
+
+ snapToTerminal.call(this, this.state.getVisibleTerminalState(true));
+ snapToTerminal.call(this, this.state.getVisibleTerminalState(false));
+
+ if (this.abspoints != null)
+ {
+ for (var i = 0; i < this.abspoints; i++)
+ {
+ if (i != this.index)
+ {
+ snapToPoint.call(this, this.abspoints[i]);
+ }
+ }
+ }
+ }
+
+ if (this.graph.isGridEnabledEvent(me.getEvent()))
+ {
+ var scale = view.scale;
+ var tr = view.translate;
+
+ if (!overrideX)
+ {
+ point.x = (this.graph.snap(point.x / scale - tr.x) + tr.x) * scale;
+ }
+
+ if (!overrideY)
+ {
+ point.y = (this.graph.snap(point.y / scale - tr.y) + tr.y) * scale;
+ }
+ }
+
+ return point;
+};
+
+/**
+ * Function: getPreviewTerminalState
+ *
+ * Updates the given preview state taking into account the state of the constraint handler.
+ */
+mxEdgeHandler.prototype.getPreviewTerminalState = function(me)
+{
+ this.constraintHandler.update(me, this.isSource);
+ this.marker.process(me);
+ var currentState = this.marker.getValidState();
+ var result = null;
+
+ if (this.constraintHandler.currentFocus != null &&
+ this.constraintHandler.currentConstraint != null)
+ {
+ this.marker.reset();
+ }
+
+ if (currentState != null)
+ {
+ result = currentState;
+ }
+ else if (this.constraintHandler.currentConstraint != null &&
+ this.constraintHandler.currentFocus != null)
+ {
+ result = this.constraintHandler.currentFocus;
+ }
+
+ return result;
+};
+
+/**
+ * Function: getPreviewPoints
+ *
+ * Updates the given preview state taking into account the state of the constraint handler.
+ */
+mxEdgeHandler.prototype.getPreviewPoints = function(point)
+{
+ var geometry = this.graph.getCellGeometry(this.state.cell);
+ var points = (geometry.points != null) ? geometry.points.slice() : null;
+
+ if (!this.isSource && !this.isTarget)
+ {
+ this.convertPoint(point, false);
+
+ if (points == null)
+ {
+ points = [point];
+ }
+ else
+ {
+ points[this.index - 1] = point;
+ }
+ }
+ else if (this.graph.resetEdgesOnConnect)
+ {
+ points = null;
+ }
+
+ return points;
+};
+
+/**
+ * Function: updatePreviewState
+ *
+ * Updates the given preview state taking into account the state of the constraint handler.
+ */
+mxEdgeHandler.prototype.updatePreviewState = function(edge, point, terminalState)
+{
+ // Computes the points for the edge style and terminals
+ var sourceState = (this.isSource) ? terminalState : this.state.getVisibleTerminalState(true);
+ var targetState = (this.isTarget) ? terminalState : this.state.getVisibleTerminalState(false);
+
+ var sourceConstraint = this.graph.getConnectionConstraint(edge, sourceState, true);
+ var targetConstraint = this.graph.getConnectionConstraint(edge, targetState, false);
+
+ var constraint = this.constraintHandler.currentConstraint;
+
+ if (constraint == null)
+ {
+ constraint = new mxConnectionConstraint();
+ }
+
+ if (this.isSource)
+ {
+ sourceConstraint = constraint;
+ }
+ else if (this.isTarget)
+ {
+ targetConstraint = constraint;
+ }
+
+ if (!this.isSource || sourceState != null)
+ {
+ edge.view.updateFixedTerminalPoint(edge, sourceState, true, sourceConstraint);
+ }
+
+ if (!this.isTarget || targetState != null)
+ {
+ edge.view.updateFixedTerminalPoint(edge, targetState, false, targetConstraint);
+ }
+
+ if ((this.isSource || this.isTarget) && terminalState == null)
+ {
+ edge.setAbsoluteTerminalPoint(point, this.isSource);
+
+ if (this.marker.getMarkedState() == null)
+ {
+ this.error = (this.graph.allowDanglingEdges) ? null : '';
+ }
+ }
+
+ edge.view.updatePoints(edge, this.points, sourceState, targetState);
+ edge.view.updateFloatingTerminalPoints(edge, sourceState, targetState);
+};
+
+/**
+ * Function: mouseMove
+ *
+ * Handles the event by updating the preview.
+ */
+mxEdgeHandler.prototype.mouseMove = function(sender, me)
+{
+ if (this.index != null && this.marker != null)
+ {
+ var point = this.getPointForEvent(me);
+
+ if (this.isLabel)
+ {
+ this.label.x = point.x;
+ this.label.y = point.y;
+ }
+ else
+ {
+ this.points = this.getPreviewPoints(point);
+ var terminalState = (this.isSource || this.isTarget) ? this.getPreviewTerminalState(me) : null;
+ var clone = this.clonePreviewState(point, (terminalState != null) ? terminalState.cell : null);
+ this.updatePreviewState(clone, point, terminalState);
+
+ // Sets the color of the preview to valid or invalid, updates the
+ // points of the preview and redraws
+ var color = (this.error == null) ? this.marker.validColor :
+ this.marker.invalidColor;
+ this.setPreviewColor(color);
+ this.abspoints = clone.absolutePoints;
+ this.active = true;
+ }
+
+ this.drawPreview();
+ mxEvent.consume(me.getEvent());
+ me.consume();
+ }
+ // Workaround for disabling the connect highlight when over handle
+ else if (mxClient.IS_IE && this.getHandleForEvent(me) != null)
+ {
+ me.consume(false);
+ }
+};
+
+/**
+ * Function: mouseUp
+ *
+ * Handles the event to applying the previewed changes on the edge by
+ * using <moveLabel>, <connect> or <changePoints>.
+ */
+mxEdgeHandler.prototype.mouseUp = function(sender, me)
+{
+ if (this.index != null && this.marker != null)
+ {
+ var edge = this.state.cell;
+
+ // Ignores event if mouse has not been moved
+ if (me.getX() != this.startX || me.getY() != this.startY)
+ {
+ // Displays the reason for not carriying out the change
+ // if there is an error message with non-zero length
+ if (this.error != null)
+ {
+ if (this.error.length > 0)
+ {
+ this.graph.validationAlert(this.error);
+ }
+ }
+ else if (this.isLabel)
+ {
+ this.moveLabel(this.state, this.label.x, this.label.y);
+ }
+ else if (this.isSource || this.isTarget)
+ {
+ var terminal = null;
+
+ if (this.constraintHandler.currentConstraint != null &&
+ this.constraintHandler.currentFocus != null)
+ {
+ terminal = this.constraintHandler.currentFocus.cell;
+ }
+
+ if (terminal == null && this.marker.hasValidState())
+ {
+ terminal = this.marker.validState.cell;
+ }
+
+ if (terminal != null)
+ {
+ edge = this.connect(edge, terminal, this.isSource,
+ this.graph.isCloneEvent(me.getEvent()) && this.cloneEnabled &&
+ this.graph.isCellsCloneable(), me);
+ }
+ else if (this.graph.isAllowDanglingEdges())
+ {
+ var pt = this.abspoints[(this.isSource) ? 0 : this.abspoints.length - 1];
+ pt.x = pt.x / this.graph.view.scale - this.graph.view.translate.x;
+ pt.y = pt.y / this.graph.view.scale - this.graph.view.translate.y;
+
+ var pstate = this.graph.getView().getState(
+ this.graph.getModel().getParent(edge));
+
+ if (pstate != null)
+ {
+ pt.x -= pstate.origin.x;
+ pt.y -= pstate.origin.y;
+ }
+
+ pt.x -= this.graph.panDx / this.graph.view.scale;
+ pt.y -= this.graph.panDy / this.graph.view.scale;
+
+ // Destroys and rectreates this handler
+ this.changeTerminalPoint(edge, pt, this.isSource);
+ }
+ }
+ else if (this.active)
+ {
+ this.changePoints(edge, this.points);
+ }
+ else
+ {
+ this.graph.getView().invalidate(this.state.cell);
+ this.graph.getView().revalidate(this.state.cell);
+ }
+ }
+
+ // Resets the preview color the state of the handler if this
+ // handler has not been recreated
+ if (this.marker != null)
+ {
+ this.reset();
+
+ // Updates the selection if the edge has been cloned
+ if (edge != this.state.cell)
+ {
+ this.graph.setSelectionCell(edge);
+ }
+ }
+
+ me.consume();
+ }
+};
+
+/**
+ * Function: reset
+ *
+ * Resets the state of this handler.
+ */
+mxEdgeHandler.prototype.reset = function()
+{
+ this.error = null;
+ this.index = null;
+ this.label = null;
+ this.points = null;
+ this.active = false;
+ this.isLabel = false;
+ this.isSource = false;
+ this.isTarget = false;
+ this.marker.reset();
+ this.constraintHandler.reset();
+ this.setPreviewColor(mxConstants.EDGE_SELECTION_COLOR);
+ this.redraw();
+};
+
+/**
+ * Function: setPreviewColor
+ *
+ * Sets the color of the preview to the given value.
+ */
+mxEdgeHandler.prototype.setPreviewColor = function(color)
+{
+ if (this.shape != null && this.shape.node != null)
+ {
+ if (this.shape.dialect == mxConstants.DIALECT_SVG)
+ {
+ this.shape.innerNode.setAttribute('stroke', color);
+ }
+ else
+ {
+ this.shape.node.strokecolor = color;
+ }
+ }
+};
+
+/**
+ * Function: convertPoint
+ *
+ * Converts the given point in-place from screen to unscaled, untranslated
+ * graph coordinates and applies the grid. Returns the given, modified
+ * point instance.
+ *
+ * Parameters:
+ *
+ * point - <mxPoint> to be converted.
+ * gridEnabled - Boolean that specifies if the grid should be applied.
+ */
+mxEdgeHandler.prototype.convertPoint = function(point, gridEnabled)
+{
+ var scale = this.graph.getView().getScale();
+ var tr = this.graph.getView().getTranslate();
+
+ if (gridEnabled)
+ {
+ point.x = this.graph.snap(point.x);
+ point.y = this.graph.snap(point.y);
+ }
+
+ point.x = Math.round(point.x / scale - tr.x);
+ point.y = Math.round(point.y / scale - tr.y);
+
+ var pstate = this.graph.getView().getState(
+ this.graph.getModel().getParent(this.state.cell));
+
+ if (pstate != null)
+ {
+ point.x -= pstate.origin.x;
+ point.y -= pstate.origin.y;
+ }
+
+ return point;
+};
+
+/**
+ * Function: moveLabel
+ *
+ * Changes the coordinates for the label of the given edge.
+ *
+ * Parameters:
+ *
+ * edge - <mxCell> that represents the edge.
+ * x - Integer that specifies the x-coordinate of the new location.
+ * y - Integer that specifies the y-coordinate of the new location.
+ */
+mxEdgeHandler.prototype.moveLabel = function(edgeState, x, y)
+{
+ var model = this.graph.getModel();
+ var geometry = model.getGeometry(edgeState.cell);
+
+ if (geometry != null)
+ {
+ geometry = geometry.clone();
+
+ // Resets the relative location stored inside the geometry
+ var pt = this.graph.getView().getRelativePoint(edgeState, x, y);
+ geometry.x = pt.x;
+ geometry.y = pt.y;
+
+ // Resets the offset inside the geometry to find the offset
+ // from the resulting point
+ var scale = this.graph.getView().scale;
+ geometry.offset = new mxPoint(0, 0);
+ var pt = this.graph.view.getPoint(edgeState, geometry);
+ geometry.offset = new mxPoint((x - pt.x) / scale, (y - pt.y) / scale);
+
+ model.setGeometry(edgeState.cell, geometry);
+ }
+};
+
+/**
+ * Function: connect
+ *
+ * Changes the terminal or terminal point of the given edge in the graph
+ * model.
+ *
+ * Parameters:
+ *
+ * edge - <mxCell> that represents the edge to be reconnected.
+ * terminal - <mxCell> that represents the new terminal.
+ * isSource - Boolean indicating if the new terminal is the source or
+ * target terminal.
+ * isClone - Boolean indicating if the new connection should be a clone of
+ * the old edge.
+ * me - <mxMouseEvent> that contains the mouse up event.
+ */
+mxEdgeHandler.prototype.connect = function(edge, terminal, isSource, isClone, me)
+{
+ var model = this.graph.getModel();
+ var parent = model.getParent(edge);
+
+ model.beginUpdate();
+ try
+ {
+ // Clones and adds the cell
+ if (isClone)
+ {
+ var clone = edge.clone();
+ model.add(parent, clone, model.getChildCount(parent));
+
+ var other = model.getTerminal(edge, !isSource);
+ this.graph.connectCell(clone, other, !isSource);
+
+ edge = clone;
+ }
+
+ var constraint = this.constraintHandler.currentConstraint;
+
+ if (constraint == null)
+ {
+ constraint = new mxConnectionConstraint();
+ }
+
+ this.graph.connectCell(edge, terminal, isSource, constraint);
+ }
+ finally
+ {
+ model.endUpdate();
+ }
+
+ return edge;
+};
+
+/**
+ * Function: changeTerminalPoint
+ *
+ * Changes the terminal point of the given edge.
+ */
+mxEdgeHandler.prototype.changeTerminalPoint = function(edge, point, isSource)
+{
+ var model = this.graph.getModel();
+ var geo = model.getGeometry(edge);
+
+ if (geo != null)
+ {
+ model.beginUpdate();
+ try
+ {
+ geo = geo.clone();
+ geo.setTerminalPoint(point, isSource);
+ model.setGeometry(edge, geo);
+ this.graph.connectCell(edge, null, isSource, new mxConnectionConstraint());
+ }
+ finally
+ {
+ model.endUpdate();
+ }
+ }
+};
+
+/**
+ * Function: changePoints
+ *
+ * Changes the control points of the given edge in the graph model.
+ */
+mxEdgeHandler.prototype.changePoints = function(edge, points)
+{
+ var model = this.graph.getModel();
+ var geo = model.getGeometry(edge);
+
+ if (geo != null)
+ {
+ geo = geo.clone();
+ geo.points = points;
+
+ model.setGeometry(edge, geo);
+ }
+};
+
+/**
+ * Function: addPoint
+ *
+ * Adds a control point for the given state and event.
+ */
+mxEdgeHandler.prototype.addPoint = function(state, evt)
+{
+ var geo = this.graph.getCellGeometry(state.cell);
+
+ if (geo != null)
+ {
+ geo = geo.clone();
+ var pt = mxUtils.convertPoint(this.graph.container, mxEvent.getClientX(evt),
+ mxEvent.getClientY(evt));
+ var index = mxUtils.findNearestSegment(state, pt.x, pt.y);
+ var gridEnabled = this.graph.isGridEnabledEvent(evt);
+ this.convertPoint(pt, gridEnabled);
+
+ if (geo.points == null)
+ {
+ geo.points = [pt];
+ }
+ else
+ {
+ geo.points.splice(index, 0, pt);
+ }
+
+ this.graph.getModel().setGeometry(state.cell, geo);
+ this.destroy();
+ this.init();
+ mxEvent.consume(evt);
+ }
+};
+
+/**
+ * Function: removePoint
+ *
+ * Removes the control point at the given index from the given state.
+ */
+mxEdgeHandler.prototype.removePoint = function(state, index)
+{
+ if (index > 0 && index < this.abspoints.length - 1)
+ {
+ var geo = this.graph.getCellGeometry(this.state.cell);
+
+ if (geo != null &&
+ geo.points != null)
+ {
+ geo = geo.clone();
+ geo.points.splice(index - 1, 1);
+ this.graph.getModel().setGeometry(state.cell, geo);
+ this.destroy();
+ this.init();
+ }
+ }
+};
+
+/**
+ * Function: getHandleFillColor
+ *
+ * Returns the fillcolor for the handle at the given index.
+ */
+mxEdgeHandler.prototype.getHandleFillColor = function(index)
+{
+ var isSource = index == 0;
+ var cell = this.state.cell;
+ var terminal = this.graph.getModel().getTerminal(cell, isSource);
+ var color = mxConstants.HANDLE_FILLCOLOR;
+
+ if ((terminal != null && !this.graph.isCellDisconnectable(cell, terminal, isSource)) ||
+ (terminal == null && !this.graph.isTerminalPointMovable(cell, isSource)))
+ {
+ color = mxConstants.LOCKED_HANDLE_FILLCOLOR;
+ }
+ else if (terminal != null && this.graph.isCellDisconnectable(cell, terminal, isSource))
+ {
+ color = mxConstants.CONNECT_HANDLE_FILLCOLOR;
+ }
+
+ return color;
+};
+
+/**
+ * Function: redraw
+ *
+ * Redraws the preview, and the bends- and label control points.
+ */
+mxEdgeHandler.prototype.redraw = function()
+{
+ this.abspoints = this.state.absolutePoints.slice();
+ var cell = this.state.cell;
+
+ // Updates the handle for the label position
+ var s = mxConstants.LABEL_HANDLE_SIZE;
+
+ this.label = new mxPoint(this.state.absoluteOffset.x, this.state.absoluteOffset.y);
+ this.labelShape.bounds = new mxRectangle(this.label.x - s / 2,
+ this.label.y - s / 2, s, s);
+ this.labelShape.redraw();
+
+ // Shows or hides the label handle depending on the label
+ var lab = this.graph.getLabel(cell);
+
+ if (lab != null && lab.length > 0 && this.graph.isLabelMovable(cell))
+ {
+ this.labelShape.node.style.visibility = 'visible';
+ }
+ else
+ {
+ this.labelShape.node.style.visibility = 'hidden';
+ }
+
+ if (this.bends != null && this.bends.length > 0)
+ {
+ var n = this.abspoints.length - 1;
+
+ var p0 = this.abspoints[0];
+ var x0 = this.abspoints[0].x;
+ var y0 = this.abspoints[0].y;
+
+ var b = this.bends[0].bounds;
+ this.bends[0].bounds = new mxRectangle(x0 - b.width / 2, y0 - b.height / 2, b.width, b.height);
+ this.bends[0].fill = this.getHandleFillColor(0);
+ this.bends[0].reconfigure();
+ this.bends[0].redraw();
+
+ var pe = this.abspoints[n];
+ var xn = this.abspoints[n].x;
+ var yn = this.abspoints[n].y;
+
+ var bn = this.bends.length - 1;
+ b = this.bends[bn].bounds;
+ this.bends[bn].bounds = new mxRectangle(xn - b.width / 2, yn - b.height / 2, b.width, b.height);
+ this.bends[bn].fill = this.getHandleFillColor(bn);
+ this.bends[bn].reconfigure();
+ this.bends[bn].redraw();
+
+ this.redrawInnerBends(p0, pe);
+ }
+
+ this.drawPreview();
+};
+
+/**
+ * Function: redrawInnerBends
+ *
+ * Updates and redraws the inner bends.
+ *
+ * Parameters:
+ *
+ * p0 - <mxPoint> that represents the location of the first point.
+ * pe - <mxPoint> that represents the location of the last point.
+ */
+mxEdgeHandler.prototype.redrawInnerBends = function(p0, pe)
+{
+ var g = this.graph.getModel().getGeometry(this.state.cell);
+ var pts = g.points;
+
+ if (pts != null)
+ {
+ if (this.points == null)
+ {
+ this.points = [];
+ }
+
+ for (var i = 1; i < this.bends.length-1; i++)
+ {
+ if (this.bends[i] != null)
+ {
+ if (this.abspoints[i] != null)
+ {
+ var x = this.abspoints[i].x;
+ var y = this.abspoints[i].y;
+
+ var b = this.bends[i].bounds;
+ this.bends[i].node.style.visibility = 'visible';
+ this.bends[i].bounds = new mxRectangle(x - b.width / 2, y - b.height / 2, b.width, b.height);
+ this.bends[i].redraw();
+
+ this.points[i - 1] = pts[i - 1];
+ }
+ else
+ {
+ this.bends[i].destroy();
+ this.bends[i] = null;
+ }
+ }
+ }
+ }
+};
+
+/**
+ * Function: drawPreview
+ *
+ * Redraws the preview.
+ */
+mxEdgeHandler.prototype.drawPreview = function()
+{
+ if (this.isLabel)
+ {
+ var s = mxConstants.LABEL_HANDLE_SIZE;
+
+ var bounds = new mxRectangle(this.label.x - s / 2, this.label.y - s / 2, s, s);
+ this.labelShape.bounds = bounds;
+ this.labelShape.redraw();
+ }
+ else
+ {
+ this.shape.points = this.abspoints;
+ this.shape.redraw();
+ }
+
+ // Workaround to force a repaint in AppleWebKit
+ mxUtils.repaintGraph(this.graph, this.shape.points[this.shape.points.length - 1]);
+};
+
+/**
+ * Function: destroy
+ *
+ * Destroys the handler and all its resources and DOM nodes. This does
+ * normally not need to be called as handlers are destroyed automatically
+ * when the corresponding cell is deselected.
+ */
+mxEdgeHandler.prototype.destroy = function()
+{
+ if (this.marker != null)
+ {
+ this.marker.destroy();
+ this.marker = null;
+ }
+
+ if (this.shape != null)
+ {
+ this.shape.destroy();
+ this.shape = null;
+ }
+
+ if (this.labelShape != null)
+ {
+ this.labelShape.destroy();
+ this.labelShape = null;
+ }
+
+ if (this.constraintHandler != null)
+ {
+ this.constraintHandler.destroy();
+ this.constraintHandler = null;
+ }
+
+ // Destroy the control points for the bends
+ if (this.bends != null)
+ {
+ for (var i = 0; i < this.bends.length; i++)
+ {
+ if (this.bends[i] != null)
+ {
+ this.bends[i].destroy();
+ this.bends[i] = null;
+ }
+ }
+ }
+};
diff --git a/src/js/handler/mxEdgeSegmentHandler.js b/src/js/handler/mxEdgeSegmentHandler.js
new file mode 100644
index 0000000..e14fde0
--- /dev/null
+++ b/src/js/handler/mxEdgeSegmentHandler.js
@@ -0,0 +1,284 @@
+/**
+ * $Id: mxEdgeSegmentHandler.js,v 1.14 2012-12-17 13:22:49 gaudenz Exp $
+ * Copyright (c) 2006-2010, JGraph Ltd
+ */
+function mxEdgeSegmentHandler(state)
+{
+ if (state != null)
+ {
+ this.state = state;
+ this.init();
+ }
+};
+
+/**
+ * Extends mxEdgeHandler.
+ */
+mxEdgeSegmentHandler.prototype = new mxElbowEdgeHandler();
+mxEdgeSegmentHandler.prototype.constructor = mxEdgeSegmentHandler;
+
+/**
+ * Function: getPreviewPoints
+ *
+ * Updates the given preview state taking into account the state of the constraint handler.
+ */
+mxEdgeSegmentHandler.prototype.getPreviewPoints = function(point)
+{
+ if (this.isSource || this.isTarget)
+ {
+ return mxElbowEdgeHandler.prototype.getPreviewPoints.apply(this, arguments);
+ }
+ else
+ {
+ this.convertPoint(point, false);
+ var pts = this.state.absolutePoints;
+ var last = pts[0].clone();
+ this.convertPoint(last, false);
+ var result = [];
+
+ for (var i = 1; i < pts.length; i++)
+ {
+ var pt = pts[i].clone();
+ this.convertPoint(pt, false);
+
+ if (i == this.index)
+ {
+ if (last.x == pt.x)
+ {
+ last.x = point.x;
+ pt.x = point.x;
+ }
+ else
+ {
+ last.y = point.y;
+ pt.y = point.y;
+ }
+ }
+
+ if (i < pts.length - 1)
+ {
+ result.push(pt);
+ }
+
+ last = pt;
+ }
+
+ if (result.length == 1)
+ {
+ var view = this.state.view;
+ var source = this.state.getVisibleTerminalState(true);
+ var target = this.state.getVisibleTerminalState(false);
+
+ if (target != null & source != null)
+ {
+ var dx = this.state.origin.x;
+ var dy = this.state.origin.y;
+
+ if (mxUtils.contains(target, result[0].x + dx, result[0].y + dy))
+ {
+ if (pts[1].y == pts[2].y)
+ {
+ result[0].y = view.getRoutingCenterY(source) - dy;
+ }
+ else
+ {
+ result[0].x = view.getRoutingCenterX(source) - dx;
+ }
+ }
+ else if (mxUtils.contains(source, result[0].x + dx, result[0].y + dy))
+ {
+ if (pts[1].y == pts[0].y)
+ {
+ result[0].y = view.getRoutingCenterY(target) - dy;
+ }
+ else
+ {
+ result[0].x = view.getRoutingCenterX(target) - dx;
+ }
+ }
+ }
+ }
+ else if (result.length == 0)
+ {
+ result = [point];
+ }
+
+ return result;
+ }
+};
+
+/**
+ * Function: createBends
+ *
+ * Adds custom bends for the center of each segment.
+ */
+mxEdgeSegmentHandler.prototype.createBends = function()
+{
+ var bends = [];
+
+ // Source
+ var bend = this.createHandleShape(0);
+
+ this.initBend(bend);
+ bend.node.style.cursor = mxConstants.CURSOR_BEND_HANDLE;
+ mxEvent.redirectMouseEvents(bend.node, this.graph, this.state);
+ bends.push(bend);
+
+ if (mxClient.IS_TOUCH)
+ {
+ bend.node.setAttribute('pointer-events', 'none');
+ }
+
+ var pts = this.state.absolutePoints;
+
+ // Waypoints (segment handles)
+ if (this.graph.isCellBendable(this.state.cell))
+ {
+ if (this.points == null)
+ {
+ this.points = [];
+ }
+
+ for (var i = 0; i < pts.length - 1; i++)
+ {
+ var bend = this.createVirtualBend();
+ bends.push(bend);
+ var horizontal = pts[i].x - pts[i + 1].x == 0;
+ bend.node.style.cursor = (horizontal) ? 'col-resize' : 'row-resize';
+ this.points.push(new mxPoint(0,0));
+
+ if (mxClient.IS_TOUCH)
+ {
+ bend.node.setAttribute('pointer-events', 'none');
+ }
+ }
+ }
+
+ // Target
+ var bend = this.createHandleShape(pts.length);
+
+ this.initBend(bend);
+ bend.node.style.cursor = mxConstants.CURSOR_BEND_HANDLE;
+ mxEvent.redirectMouseEvents(bend.node, this.graph, this.state);
+ bends.push(bend);
+
+ if (mxClient.IS_TOUCH)
+ {
+ bend.node.setAttribute('pointer-events', 'none');
+ }
+
+ return bends;
+};
+
+
+/**
+ * Function: redrawInnerBends
+ *
+ * Updates the position of the custom bends.
+ */
+mxEdgeSegmentHandler.prototype.redrawInnerBends = function(p0, pe)
+{
+ if (this.graph.isCellBendable(this.state.cell))
+ {
+ var s = mxConstants.HANDLE_SIZE;
+ var pts = this.state.absolutePoints;
+
+ if (pts != null && pts.length > 1)
+ {
+ for (var i = 0; i < this.state.absolutePoints.length - 1; i++)
+ {
+ if (this.bends[i + 1] != null)
+ {
+ var p0 = pts[i];
+ var pe = pts[i + 1];
+ var pt = new mxPoint(p0.x + (pe.x - p0.x) / 2, p0.y + (pe.y - p0.y) / 2);
+ this.bends[i+1].bounds = new mxRectangle(pt.x - s / 2, pt.y - s / 2, s, s);
+ this.bends[i+1].reconfigure();
+ this.bends[i+1].redraw();
+ }
+ }
+ }
+ }
+};
+
+/**
+ * Function: connect
+ *
+ * Calls <refresh> after <mxEdgeHandler.connect>.
+ */
+mxEdgeSegmentHandler.prototype.connect = function(edge, terminal, isSource, isClone, me)
+{
+ mxEdgeHandler.prototype.connect.apply(this, arguments);
+ this.refresh();
+};
+
+/**
+ * Function: changeTerminalPoint
+ *
+ * Calls <refresh> after <mxEdgeHandler.changeTerminalPoint>.
+ */
+mxEdgeSegmentHandler.prototype.changeTerminalPoint = function(edge, point, isSource)
+{
+ mxEdgeHandler.prototype.changeTerminalPoint.apply(this, arguments);
+ this.refresh();
+};
+
+/**
+ * Function: changePoints
+ *
+ * Changes the points of the given edge to reflect the current state of the handler.
+ */
+mxEdgeSegmentHandler.prototype.changePoints = function(edge, points)
+{
+ points = [];
+ var pts = this.abspoints;
+
+ if (pts.length > 1)
+ {
+ var pt0 = pts[0];
+ var pt1 = pts[1];
+
+ for (var i = 2; i < pts.length; i++)
+ {
+ var pt2 = pts[i];
+
+ if ((Math.round(pt0.x) != Math.round(pt1.x) ||
+ Math.round(pt1.x) != Math.round(pt2.x)) &&
+ (Math.round(pt0.y) != Math.round(pt1.y) ||
+ Math.round(pt1.y) != Math.round(pt2.y)))
+ {
+ pt0 = pt1;
+ pt1 = pt1.clone();
+ this.convertPoint(pt1, false);
+ points.push(pt1);
+ }
+
+ pt1 = pt2;
+ }
+ }
+
+ mxElbowEdgeHandler.prototype.changePoints.apply(this, arguments);
+ this.refresh();
+};
+
+/**
+ * Function: refresh
+ *
+ * Refreshes the bends of this handler.
+ */
+mxEdgeSegmentHandler.prototype.refresh = function()
+{
+ if (this.bends != null)
+ {
+ for (var i = 0; i < this.bends.length; i++)
+ {
+ if (this.bends[i] != null)
+ {
+ this.bends[i].destroy();
+ this.bends[i] = null;
+ }
+ }
+
+ this.bends = this.createBends();
+ }
+};
diff --git a/src/js/handler/mxElbowEdgeHandler.js b/src/js/handler/mxElbowEdgeHandler.js
new file mode 100644
index 0000000..85fbb06
--- /dev/null
+++ b/src/js/handler/mxElbowEdgeHandler.js
@@ -0,0 +1,248 @@
+/**
+ * $Id: mxElbowEdgeHandler.js,v 1.43 2012-01-06 13:06:01 gaudenz Exp $
+ * Copyright (c) 2006-2010, JGraph Ltd
+ */
+/**
+ * Class: mxElbowEdgeHandler
+ *
+ * Graph event handler that reconnects edges and modifies control points and
+ * the edge label location. Uses <mxTerminalMarker> for finding and
+ * highlighting new source and target vertices. This handler is automatically
+ * created in <mxGraph.createHandler>. It extends <mxEdgeHandler>.
+ *
+ * Constructor: mxEdgeHandler
+ *
+ * Constructs an edge handler for the specified <mxCellState>.
+ *
+ * Parameters:
+ *
+ * state - <mxCellState> of the cell to be modified.
+ */
+function mxElbowEdgeHandler(state)
+{
+ if (state != null)
+ {
+ this.state = state;
+ this.init();
+ }
+};
+
+/**
+ * Extends mxEdgeHandler.
+ */
+mxElbowEdgeHandler.prototype = new mxEdgeHandler();
+mxElbowEdgeHandler.prototype.constructor = mxElbowEdgeHandler;
+
+/**
+ * Specifies if a double click on the middle handle should call
+ * <mxGraph.flipEdge>. Default is true.
+ */
+mxElbowEdgeHandler.prototype.flipEnabled = true;
+
+/**
+ * Variable: doubleClickOrientationResource
+ *
+ * Specifies the resource key for the tooltip to be displayed on the single
+ * control point for routed edges. If the resource for this key does not
+ * exist then the value is used as the error message. Default is
+ * 'doubleClickOrientation'.
+ */
+mxElbowEdgeHandler.prototype.doubleClickOrientationResource =
+ (mxClient.language != 'none') ? 'doubleClickOrientation' : '';
+
+/**
+ * Function: createBends
+ *
+ * Overrides <mxEdgeHandler.createBends> to create custom bends.
+ */
+ mxElbowEdgeHandler.prototype.createBends = function()
+ {
+ var bends = [];
+
+ // Source
+ var bend = this.createHandleShape(0);
+
+ this.initBend(bend);
+ bend.node.style.cursor = mxConstants.CURSOR_BEND_HANDLE;
+ mxEvent.redirectMouseEvents(bend.node, this.graph, this.state);
+ bends.push(bend);
+
+ if (mxClient.IS_TOUCH)
+ {
+ bend.node.setAttribute('pointer-events', 'none');
+ }
+
+ // Virtual
+ bends.push(this.createVirtualBend());
+ this.points.push(new mxPoint(0,0));
+
+ // Target
+ bend = this.createHandleShape(2);
+
+ this.initBend(bend);
+ bend.node.style.cursor = mxConstants.CURSOR_BEND_HANDLE;
+ mxEvent.redirectMouseEvents(bend.node, this.graph, this.state);
+ bends.push(bend);
+
+ if (mxClient.IS_TOUCH)
+ {
+ bend.node.setAttribute('pointer-events', 'none');
+ }
+
+ return bends;
+ };
+
+/**
+ * Function: createVirtualBend
+ *
+ * Creates a virtual bend that supports double clicking and calls
+ * <mxGraph.flipEdge>.
+ */
+mxElbowEdgeHandler.prototype.createVirtualBend = function()
+{
+ var bend = this.createHandleShape();
+ this.initBend(bend);
+
+ var crs = this.getCursorForBend();
+ bend.node.style.cursor = crs;
+
+ // Double-click changes edge style
+ var dblClick = mxUtils.bind(this, function(evt)
+ {
+ if (!mxEvent.isConsumed(evt) &&
+ this.flipEnabled)
+ {
+ this.graph.flipEdge(this.state.cell, evt);
+ mxEvent.consume(evt);
+ }
+ });
+
+ mxEvent.redirectMouseEvents(bend.node, this.graph, this.state,
+ null, null, null, dblClick);
+
+ if (!this.graph.isCellBendable(this.state.cell))
+ {
+ bend.node.style.visibility = 'hidden';
+ }
+
+ return bend;
+};
+
+/**
+ * Function: getCursorForBend
+ *
+ * Returns the cursor to be used for the bend.
+ */
+mxElbowEdgeHandler.prototype.getCursorForBend = function()
+{
+ return (this.state.style[mxConstants.STYLE_EDGE] == mxEdgeStyle.TopToBottom ||
+ this.state.style[mxConstants.STYLE_EDGE] == mxConstants.EDGESTYLE_TOPTOBOTTOM ||
+ ((this.state.style[mxConstants.STYLE_EDGE] == mxEdgeStyle.ElbowConnector ||
+ this.state.style[mxConstants.STYLE_EDGE] == mxConstants.EDGESTYLE_ELBOW)&&
+ this.state.style[mxConstants.STYLE_ELBOW] == mxConstants.ELBOW_VERTICAL)) ?
+ 'row-resize' : 'col-resize';
+};
+
+/**
+ * Function: getTooltipForNode
+ *
+ * Returns the tooltip for the given node.
+ */
+mxElbowEdgeHandler.prototype.getTooltipForNode = function(node)
+{
+ var tip = null;
+
+ if (this.bends != null &&
+ this.bends[1] != null &&
+ (node == this.bends[1].node ||
+ node.parentNode == this.bends[1].node))
+ {
+ tip = this.doubleClickOrientationResource;
+ tip = mxResources.get(tip) || tip; // translate
+ }
+
+ return tip;
+};
+
+/**
+ * Function: convertPoint
+ *
+ * Converts the given point in-place from screen to unscaled, untranslated
+ * graph coordinates and applies the grid.
+ *
+ * Parameters:
+ *
+ * point - <mxPoint> to be converted.
+ * gridEnabled - Boolean that specifies if the grid should be applied.
+ */
+mxElbowEdgeHandler.prototype.convertPoint = function(point, gridEnabled)
+{
+ var scale = this.graph.getView().getScale();
+ var tr = this.graph.getView().getTranslate();
+ var origin = this.state.origin;
+
+ if (gridEnabled)
+ {
+ point.x = this.graph.snap(point.x);
+ point.y = this.graph.snap(point.y);
+ }
+
+ point.x = Math.round(point.x / scale - tr.x - origin.x);
+ point.y = Math.round(point.y / scale - tr.y - origin.y);
+};
+
+/**
+ * Function: redrawInnerBends
+ *
+ * Updates and redraws the inner bends.
+ *
+ * Parameters:
+ *
+ * p0 - <mxPoint> that represents the location of the first point.
+ * pe - <mxPoint> that represents the location of the last point.
+ */
+mxElbowEdgeHandler.prototype.redrawInnerBends = function(p0, pe)
+{
+ var g = this.graph.getModel().getGeometry(this.state.cell);
+ var pts = g.points;
+
+ var pt = (pts != null) ? pts[0] : null;
+
+ if (pt == null)
+ {
+ pt = new mxPoint(p0.x + (pe.x - p0.x) / 2, p0.y + (pe.y - p0.y) / 2);
+ }
+ else
+ {
+ pt = new mxPoint(this.graph.getView().scale*(pt.x +
+ this.graph.getView().translate.x + this.state.origin.x),
+ this.graph.getView().scale*(pt.y + this.graph.getView().translate.y +
+ this.state.origin.y));
+ }
+
+ // Makes handle slightly bigger if the yellow label handle
+ // exists and intersects this green handle
+ var b = this.bends[1].bounds;
+ var w = b.width;
+ var h = b.height;
+
+ if (this.handleImage == null)
+ {
+ w = mxConstants.HANDLE_SIZE;
+ h = mxConstants.HANDLE_SIZE;
+ }
+
+ var bounds = new mxRectangle(pt.x - w / 2, pt.y - h / 2, w, h);
+
+ if (this.handleImage == null && this.labelShape.node.style.visibility != 'hidden' &&
+ mxUtils.intersects(bounds, this.labelShape.bounds))
+ {
+ w += 3;
+ h += 3;
+ bounds = new mxRectangle(pt.x - w / 2, pt.y - h / 2, w, h);
+ }
+
+ this.bends[1].bounds = bounds;
+ this.bends[1].reconfigure();
+ this.bends[1].redraw();
+};
diff --git a/src/js/handler/mxGraphHandler.js b/src/js/handler/mxGraphHandler.js
new file mode 100644
index 0000000..57e27a1
--- /dev/null
+++ b/src/js/handler/mxGraphHandler.js
@@ -0,0 +1,916 @@
+/**
+ * $Id: mxGraphHandler.js,v 1.129 2012-04-13 12:53:30 gaudenz Exp $
+ * Copyright (c) 2006-2010, JGraph Ltd
+ */
+/**
+ * Class: mxGraphHandler
+ *
+ * Graph event handler that handles selection. Individual cells are handled
+ * separately using <mxVertexHandler> or one of the edge handlers. These
+ * handlers are created using <mxGraph.createHandler> in
+ * <mxGraphSelectionModel.cellAdded>.
+ *
+ * To avoid the container to scroll a moved cell into view, set
+ * <scrollAfterMove> to false.
+ *
+ * Constructor: mxGraphHandler
+ *
+ * Constructs an event handler that creates handles for the
+ * selection cells.
+ *
+ * Parameters:
+ *
+ * graph - Reference to the enclosing <mxGraph>.
+ */
+function mxGraphHandler(graph)
+{
+ this.graph = graph;
+ this.graph.addMouseListener(this);
+
+ // Repaints the handler after autoscroll
+ this.panHandler = mxUtils.bind(this, function()
+ {
+ this.updatePreviewShape();
+ });
+
+ this.graph.addListener(mxEvent.PAN, this.panHandler);
+};
+
+/**
+ * Variable: graph
+ *
+ * Reference to the enclosing <mxGraph>.
+ */
+mxGraphHandler.prototype.graph = null;
+
+/**
+ * Variable: maxCells
+ *
+ * Defines the maximum number of cells to paint subhandles
+ * for. Default is 50 for Firefox and 20 for IE. Set this
+ * to 0 if you want an unlimited number of handles to be
+ * displayed. This is only recommended if the number of
+ * cells in the graph is limited to a small number, eg.
+ * 500.
+ */
+mxGraphHandler.prototype.maxCells = (mxClient.IS_IE) ? 20 : 50;
+
+/**
+ * Variable: enabled
+ *
+ * Specifies if events are handled. Default is true.
+ */
+mxGraphHandler.prototype.enabled = true;
+
+/**
+ * Variable: highlightEnabled
+ *
+ * Specifies if drop targets under the mouse should be enabled. Default is
+ * true.
+ */
+mxGraphHandler.prototype.highlightEnabled = true;
+
+/**
+ * Variable: cloneEnabled
+ *
+ * Specifies if cloning by control-drag is enabled. Default is true.
+ */
+mxGraphHandler.prototype.cloneEnabled = true;
+
+/**
+ * Variable: moveEnabled
+ *
+ * Specifies if moving is enabled. Default is true.
+ */
+mxGraphHandler.prototype.moveEnabled = true;
+
+/**
+ * Variable: guidesEnabled
+ *
+ * Specifies if other cells should be used for snapping the right, center or
+ * left side of the current selection. Default is false.
+ */
+mxGraphHandler.prototype.guidesEnabled = false;
+
+/**
+ * Variable: guide
+ *
+ * Holds the <mxGuide> instance that is used for alignment.
+ */
+mxGraphHandler.prototype.guide = null;
+
+/**
+ * Variable: currentDx
+ *
+ * Stores the x-coordinate of the current mouse move.
+ */
+mxGraphHandler.prototype.currentDx = null;
+
+/**
+ * Variable: currentDy
+ *
+ * Stores the y-coordinate of the current mouse move.
+ */
+mxGraphHandler.prototype.currentDy = null;
+
+/**
+ * Variable: updateCursor
+ *
+ * Specifies if a move cursor should be shown if the mouse is ove a movable
+ * cell. Default is true.
+ */
+mxGraphHandler.prototype.updateCursor = true;
+
+/**
+ * Variable: selectEnabled
+ *
+ * Specifies if selecting is enabled. Default is true.
+ */
+mxGraphHandler.prototype.selectEnabled = true;
+
+/**
+ * Variable: removeCellsFromParent
+ *
+ * Specifies if cells may be moved out of their parents. Default is true.
+ */
+mxGraphHandler.prototype.removeCellsFromParent = true;
+
+/**
+ * Variable: connectOnDrop
+ *
+ * Specifies if drop events are interpreted as new connections if no other
+ * drop action is defined. Default is false.
+ */
+mxGraphHandler.prototype.connectOnDrop = false;
+
+/**
+ * Variable: scrollOnMove
+ *
+ * Specifies if the view should be scrolled so that a moved cell is
+ * visible. Default is true.
+ */
+mxGraphHandler.prototype.scrollOnMove = true;
+
+/**
+ * Variable: minimumSize
+ *
+ * Specifies the minimum number of pixels for the width and height of a
+ * selection border. Default is 6.
+ */
+mxGraphHandler.prototype.minimumSize = 6;
+
+/**
+ * Variable: previewColor
+ *
+ * Specifies the color of the preview shape. Default is black.
+ */
+mxGraphHandler.prototype.previewColor = 'black';
+
+/**
+ * Variable: htmlPreview
+ *
+ * Specifies if the graph container should be used for preview. If this is used
+ * then drop target detection relies entirely on <mxGraph.getCellAt> because
+ * the HTML preview does not "let events through". Default is false.
+ */
+mxGraphHandler.prototype.htmlPreview = false;
+
+/**
+ * Variable: shape
+ *
+ * Reference to the <mxShape> that represents the preview.
+ */
+mxGraphHandler.prototype.shape = null;
+
+/**
+ * Variable: scaleGrid
+ *
+ * Specifies if the grid should be scaled. Default is false.
+ */
+mxGraphHandler.prototype.scaleGrid = false;
+
+/**
+ * Variable: crisp
+ *
+ * Specifies if the move preview should be rendered in crisp mode if applicable.
+ * Default is true.
+ */
+mxGraphHandler.prototype.crisp = true;
+
+/**
+ * Function: isEnabled
+ *
+ * Returns <enabled>.
+ */
+mxGraphHandler.prototype.isEnabled = function()
+{
+ return this.enabled;
+};
+
+/**
+ * Function: setEnabled
+ *
+ * Sets <enabled>.
+ */
+mxGraphHandler.prototype.setEnabled = function(value)
+{
+ this.enabled = value;
+};
+
+/**
+ * Function: isCloneEnabled
+ *
+ * Returns <cloneEnabled>.
+ */
+mxGraphHandler.prototype.isCloneEnabled = function()
+{
+ return this.cloneEnabled;
+};
+
+/**
+ * Function: setCloneEnabled
+ *
+ * Sets <cloneEnabled>.
+ *
+ * Parameters:
+ *
+ * value - Boolean that specifies the new clone enabled state.
+ */
+mxGraphHandler.prototype.setCloneEnabled = function(value)
+{
+ this.cloneEnabled = value;
+};
+
+/**
+ * Function: isMoveEnabled
+ *
+ * Returns <moveEnabled>.
+ */
+mxGraphHandler.prototype.isMoveEnabled = function()
+{
+ return this.moveEnabled;
+};
+
+/**
+ * Function: setMoveEnabled
+ *
+ * Sets <moveEnabled>.
+ */
+mxGraphHandler.prototype.setMoveEnabled = function(value)
+{
+ this.moveEnabled = value;
+};
+
+/**
+ * Function: isSelectEnabled
+ *
+ * Returns <selectEnabled>.
+ */
+mxGraphHandler.prototype.isSelectEnabled = function()
+{
+ return this.selectEnabled;
+};
+
+/**
+ * Function: setSelectEnabled
+ *
+ * Sets <selectEnabled>.
+ */
+mxGraphHandler.prototype.setSelectEnabled = function(value)
+{
+ this.selectEnabled = value;
+};
+
+/**
+ * Function: isRemoveCellsFromParent
+ *
+ * Returns <removeCellsFromParent>.
+ */
+mxGraphHandler.prototype.isRemoveCellsFromParent = function()
+{
+ return this.removeCellsFromParent;
+};
+
+/**
+ * Function: setRemoveCellsFromParent
+ *
+ * Sets <removeCellsFromParent>.
+ */
+mxGraphHandler.prototype.setRemoveCellsFromParent = function(value)
+{
+ this.removeCellsFromParent = value;
+};
+
+/**
+ * Function: getInitialCellForEvent
+ *
+ * Hook to return initial cell for the given event.
+ */
+mxGraphHandler.prototype.getInitialCellForEvent = function(me)
+{
+ return me.getCell();
+};
+
+/**
+ * Function: isDelayedSelection
+ *
+ * Hook to return true for delayed selections.
+ */
+mxGraphHandler.prototype.isDelayedSelection = function(cell)
+{
+ return this.graph.isCellSelected(cell);
+};
+
+/**
+ * Function: mouseDown
+ *
+ * Handles the event by selecing the given cell and creating a handle for
+ * it. By consuming the event all subsequent events of the gesture are
+ * redirected to this handler.
+ */
+mxGraphHandler.prototype.mouseDown = function(sender, me)
+{
+ if (!me.isConsumed() && this.isEnabled() && this.graph.isEnabled() &&
+ !this.graph.isForceMarqueeEvent(me.getEvent()) && me.getState() != null)
+ {
+ var cell = this.getInitialCellForEvent(me);
+ this.cell = null;
+ this.delayedSelection = this.isDelayedSelection(cell);
+
+ if (this.isSelectEnabled() && !this.delayedSelection)
+ {
+ this.graph.selectCellForEvent(cell, me.getEvent());
+ }
+
+ if (this.isMoveEnabled())
+ {
+ var model = this.graph.model;
+ var geo = model.getGeometry(cell);
+
+ if (this.graph.isCellMovable(cell) && ((!model.isEdge(cell) || this.graph.getSelectionCount() > 1 ||
+ (geo.points != null && geo.points.length > 0) || model.getTerminal(cell, true) == null ||
+ model.getTerminal(cell, false) == null) || this.graph.allowDanglingEdges ||
+ (this.graph.isCloneEvent(me.getEvent()) && this.graph.isCellsCloneable())))
+ {
+ this.start(cell, me.getX(), me.getY());
+ }
+
+ this.cellWasClicked = true;
+
+ // Workaround for SELECT element not working in Webkit, this blocks moving
+ // of the cell if the select element is clicked in Safari which is needed
+ // because Safari doesn't seem to route the subsequent mouseUp event via
+ // this handler which leads to an inconsistent state (no reset called).
+ // Same for cellWasClicked which will block clearing the selection when
+ // clicking the background after clicking on the SELECT element in Safari.
+ if ((!mxClient.IS_SF && !mxClient.IS_GC) || me.getSource().nodeName != 'SELECT')
+ {
+ me.consume();
+ }
+ else if (mxClient.IS_SF && me.getSource().nodeName == 'SELECT')
+ {
+ this.cellWasClicked = false;
+ this.first = null;
+ }
+ }
+ }
+};
+
+/**
+ * Function: getGuideStates
+ *
+ * Creates an array of cell states which should be used as guides.
+ */
+mxGraphHandler.prototype.getGuideStates = function()
+{
+ var parent = this.graph.getDefaultParent();
+ var model = this.graph.getModel();
+
+ var filter = mxUtils.bind(this, function(cell)
+ {
+ return this.graph.view.getState(cell) != null &&
+ model.isVertex(cell) &&
+ model.getGeometry(cell) != null &&
+ !model.getGeometry(cell).relative;
+ });
+
+ return this.graph.view.getCellStates(model.filterDescendants(filter, parent));
+};
+
+/**
+ * Function: getCells
+ *
+ * Returns the cells to be modified by this handler. This implementation
+ * returns all selection cells that are movable, or the given initial cell if
+ * the given cell is not selected and movable. This handles the case of moving
+ * unselectable or unselected cells.
+ *
+ * Parameters:
+ *
+ * initialCell - <mxCell> that triggered this handler.
+ */
+mxGraphHandler.prototype.getCells = function(initialCell)
+{
+ if (!this.delayedSelection && this.graph.isCellMovable(initialCell))
+ {
+ return [initialCell];
+ }
+ else
+ {
+ return this.graph.getMovableCells(this.graph.getSelectionCells());
+ }
+};
+
+/**
+ * Function: getPreviewBounds
+ *
+ * Returns the <mxRectangle> used as the preview bounds for
+ * moving the given cells.
+ */
+mxGraphHandler.prototype.getPreviewBounds = function(cells)
+{
+ var bounds = this.graph.getView().getBounds(cells);
+
+ if (bounds != null)
+ {
+ if (bounds.width < this.minimumSize)
+ {
+ var dx = this.minimumSize - bounds.width;
+ bounds.x -= dx / 2;
+ bounds.width = this.minimumSize;
+ }
+
+ if (bounds.height < this.minimumSize)
+ {
+ var dy = this.minimumSize - bounds.height;
+ bounds.y -= dy / 2;
+ bounds.height = this.minimumSize;
+ }
+ }
+
+ return bounds;
+};
+
+/**
+ * Function: createPreviewShape
+ *
+ * Creates the shape used to draw the preview for the given bounds.
+ */
+mxGraphHandler.prototype.createPreviewShape = function(bounds)
+{
+ var shape = new mxRectangleShape(bounds, null, this.previewColor);
+ shape.isDashed = true;
+ shape.crisp = this.crisp;
+
+ if (this.htmlPreview)
+ {
+ shape.dialect = mxConstants.DIALECT_STRICTHTML;
+ shape.init(this.graph.container);
+ }
+ else
+ {
+ // Makes sure to use either VML or SVG shapes in order to implement
+ // event-transparency on the background area of the rectangle since
+ // HTML shapes do not let mouseevents through even when transparent
+ shape.dialect = (this.graph.dialect != mxConstants.DIALECT_SVG) ?
+ mxConstants.DIALECT_VML : mxConstants.DIALECT_SVG;
+ shape.init(this.graph.getView().getOverlayPane());
+
+ // Event-transparency
+ if (shape.dialect == mxConstants.DIALECT_SVG)
+ {
+ shape.node.setAttribute('style', 'pointer-events:none;');
+ }
+ else
+ {
+ shape.node.style.background = '';
+ }
+ }
+
+ return shape;
+};
+
+/**
+ * Function: start
+ *
+ * Starts the handling of the mouse gesture.
+ */
+mxGraphHandler.prototype.start = function(cell, x, y)
+{
+ this.cell = cell;
+ this.first = mxUtils.convertPoint(this.graph.container, x, y);
+ this.cells = this.getCells(this.cell);
+ this.bounds = this.getPreviewBounds(this.cells);
+
+ if (this.guidesEnabled)
+ {
+ this.guide = new mxGuide(this.graph, this.getGuideStates());
+ }
+};
+
+/**
+ * Function: useGuidesForEvent
+ *
+ * Returns true if the guides should be used for the given <mxMouseEvent>.
+ * This implementation returns <mxGuide.isEnabledForEvent>.
+ */
+mxGraphHandler.prototype.useGuidesForEvent = function(me)
+{
+ return (this.guide != null) ? this.guide.isEnabledForEvent(me.getEvent()) : true;
+};
+
+
+/**
+ * Function: snap
+ *
+ * Snaps the given vector to the grid and returns the given mxPoint instance.
+ */
+mxGraphHandler.prototype.snap = function(vector)
+{
+ var scale = (this.scaleGrid) ? this.graph.view.scale : 1;
+
+ vector.x = this.graph.snap(vector.x / scale) * scale;
+ vector.y = this.graph.snap(vector.y / scale) * scale;
+
+ return vector;
+};
+
+/**
+ * Function: mouseMove
+ *
+ * Handles the event by highlighting possible drop targets and updating the
+ * preview.
+ */
+mxGraphHandler.prototype.mouseMove = function(sender, me)
+{
+ var graph = this.graph;
+
+ if (!me.isConsumed() && graph.isMouseDown && this.cell != null &&
+ this.first != null && this.bounds != null)
+ {
+ var point = mxUtils.convertPoint(graph.container, me.getX(), me.getY());
+ var dx = point.x - this.first.x;
+ var dy = point.y - this.first.y;
+ var tol = graph.tolerance;
+
+ if (this.shape!= null || Math.abs(dx) > tol || Math.abs(dy) > tol)
+ {
+ // Highlight is used for highlighting drop targets
+ if (this.highlight == null)
+ {
+ this.highlight = new mxCellHighlight(this.graph,
+ mxConstants.DROP_TARGET_COLOR, 3);
+ }
+
+ if (this.shape == null)
+ {
+ this.shape = this.createPreviewShape(this.bounds);
+ }
+
+ var gridEnabled = graph.isGridEnabledEvent(me.getEvent());
+ var hideGuide = true;
+
+ if (this.guide != null && this.useGuidesForEvent(me))
+ {
+ var delta = this.guide.move(this.bounds, new mxPoint(dx, dy), gridEnabled);
+ hideGuide = false;
+ dx = delta.x;
+ dy = delta.y;
+ }
+ else if (gridEnabled)
+ {
+ var trx = graph.getView().translate;
+ var scale = graph.getView().scale;
+
+ var tx = this.bounds.x - (graph.snap(this.bounds.x / scale - trx.x) + trx.x) * scale;
+ var ty = this.bounds.y - (graph.snap(this.bounds.y / scale - trx.y) + trx.y) * scale;
+ var v = this.snap(new mxPoint(dx, dy));
+
+ dx = v.x - tx;
+ dy = v.y - ty;
+ }
+
+ if (this.guide != null && hideGuide)
+ {
+ this.guide.hide();
+ }
+
+ // Constrained movement if shift key is pressed
+ if (graph.isConstrainedEvent(me.getEvent()))
+ {
+ if (Math.abs(dx) > Math.abs(dy))
+ {
+ dy = 0;
+ }
+ else
+ {
+ dx = 0;
+ }
+ }
+
+ this.currentDx = dx;
+ this.currentDy = dy;
+ this.updatePreviewShape();
+
+ var target = null;
+ var cell = me.getCell();
+
+ if (graph.isDropEnabled() && this.highlightEnabled)
+ {
+ // Contains a call to getCellAt to find the cell under the mouse
+ target = graph.getDropTarget(this.cells, me.getEvent(), cell);
+ }
+
+ // Checks if parent is dropped into child
+ var parent = target;
+ var model = graph.getModel();
+
+ while (parent != null && parent != this.cells[0])
+ {
+ parent = model.getParent(parent);
+ }
+
+ var clone = graph.isCloneEvent(me.getEvent()) && graph.isCellsCloneable() && this.isCloneEnabled();
+ var state = graph.getView().getState(target);
+ var highlight = false;
+
+ if (state != null && parent == null && (model.getParent(this.cell) != target || clone))
+ {
+ if (this.target != target)
+ {
+ this.target = target;
+ this.setHighlightColor(mxConstants.DROP_TARGET_COLOR);
+ }
+
+ highlight = true;
+ }
+ else
+ {
+ this.target = null;
+
+ if (this.connectOnDrop && cell != null && this.cells.length == 1 &&
+ graph.getModel().isVertex(cell) && graph.isCellConnectable(cell))
+ {
+ state = graph.getView().getState(cell);
+
+ if (state != null)
+ {
+ var error = graph.getEdgeValidationError(null, this.cell, cell);
+ var color = (error == null) ?
+ mxConstants.VALID_COLOR :
+ mxConstants.INVALID_CONNECT_TARGET_COLOR;
+ this.setHighlightColor(color);
+ highlight = true;
+ }
+ }
+ }
+
+ if (state != null && highlight)
+ {
+ this.highlight.highlight(state);
+ }
+ else
+ {
+ this.highlight.hide();
+ }
+ }
+
+ me.consume();
+
+ // Cancels the bubbling of events to the container so
+ // that the droptarget is not reset due to an mouseMove
+ // fired on the container with no associated state.
+ mxEvent.consume(me.getEvent());
+ }
+ else if ((this.isMoveEnabled() || this.isCloneEnabled()) && this.updateCursor &&
+ !me.isConsumed() && me.getState() != null && !graph.isMouseDown)
+ {
+ var cursor = graph.getCursorForCell(me.getCell());
+
+ if (cursor == null && graph.isEnabled() && graph.isCellMovable(me.getCell()))
+ {
+ if (graph.getModel().isEdge(me.getCell()))
+ {
+ cursor = mxConstants.CURSOR_MOVABLE_EDGE;
+ }
+ else
+ {
+ cursor = mxConstants.CURSOR_MOVABLE_VERTEX;
+ }
+ }
+
+ me.getState().setCursor(cursor);
+ me.consume();
+ }
+};
+
+/**
+ * Function: updatePreviewShape
+ *
+ * Updates the bounds of the preview shape.
+ */
+mxGraphHandler.prototype.updatePreviewShape = function()
+{
+ if (this.shape != null)
+ {
+ this.shape.bounds = new mxRectangle(this.bounds.x + this.currentDx - this.graph.panDx,
+ this.bounds.y + this.currentDy - this.graph.panDy, this.bounds.width, this.bounds.height);
+ this.shape.redraw();
+ }
+};
+
+/**
+ * Function: setHighlightColor
+ *
+ * Sets the color of the rectangle used to highlight drop targets.
+ *
+ * Parameters:
+ *
+ * color - String that represents the new highlight color.
+ */
+mxGraphHandler.prototype.setHighlightColor = function(color)
+{
+ if (this.highlight != null)
+ {
+ this.highlight.setHighlightColor(color);
+ }
+};
+
+/**
+ * Function: mouseUp
+ *
+ * Handles the event by applying the changes to the selection cells.
+ */
+mxGraphHandler.prototype.mouseUp = function(sender, me)
+{
+ if (!me.isConsumed())
+ {
+ var graph = this.graph;
+
+ if (this.cell != null && this.first != null && this.shape != null &&
+ this.currentDx != null && this.currentDy != null)
+ {
+ var scale = graph.getView().scale;
+ var clone = graph.isCloneEvent(me.getEvent()) && graph.isCellsCloneable() && this.isCloneEnabled();
+ var dx = this.currentDx / scale;
+ var dy = this.currentDy / scale;
+
+ var cell = me.getCell();
+
+ if (this.connectOnDrop && this.target == null && cell != null && graph.getModel().isVertex(cell) &&
+ graph.isCellConnectable(cell) && graph.isEdgeValid(null, this.cell, cell))
+ {
+ graph.connectionHandler.connect(this.cell, cell, me.getEvent());
+ }
+ else
+ {
+ var target = this.target;
+
+ if (graph.isSplitEnabled() && graph.isSplitTarget(target, this.cells, me.getEvent()))
+ {
+ graph.splitEdge(target, this.cells, null, dx, dy);
+ }
+ else
+ {
+ this.moveCells(this.cells, dx, dy, clone, this.target, me.getEvent());
+ }
+ }
+ }
+ else if (this.isSelectEnabled() && this.delayedSelection && this.cell != null)
+ {
+ this.selectDelayed(me);
+ }
+ }
+
+ // Consumes the event if a cell was initially clicked
+ if (this.cellWasClicked)
+ {
+ me.consume();
+ }
+
+ this.reset();
+};
+
+/**
+ * Function: selectDelayed
+ *
+ * Implements the delayed selection for the given mouse event.
+ */
+mxGraphHandler.prototype.selectDelayed = function(me)
+{
+ this.graph.selectCellForEvent(this.cell, me.getEvent());
+};
+
+/**
+ * Function: reset
+ *
+ * Resets the state of this handler.
+ */
+mxGraphHandler.prototype.reset = function()
+{
+ this.destroyShapes();
+ this.cellWasClicked = false;
+ this.delayedSelection = false;
+ this.currentDx = null;
+ this.currentDy = null;
+ this.guides = null;
+ this.first = null;
+ this.cell = null;
+ this.target = null;
+};
+
+/**
+ * Function: shouldRemoveCellsFromParent
+ *
+ * Returns true if the given cells should be removed from the parent for the specified
+ * mousereleased event.
+ */
+mxGraphHandler.prototype.shouldRemoveCellsFromParent = function(parent, cells, evt)
+{
+ if (this.graph.getModel().isVertex(parent))
+ {
+ var pState = this.graph.getView().getState(parent);
+ var pt = mxUtils.convertPoint(this.graph.container,
+ mxEvent.getClientX(evt), mxEvent.getClientY(evt));
+
+ return pState != null && !mxUtils.contains(pState, pt.x, pt.y);
+ }
+
+ return false;
+};
+
+/**
+ * Function: moveCells
+ *
+ * Moves the given cells by the specified amount.
+ */
+mxGraphHandler.prototype.moveCells = function(cells, dx, dy, clone, target, evt)
+{
+ if (clone)
+ {
+ cells = this.graph.getCloneableCells(cells);
+ }
+
+ // Removes cells from parent
+ if (target == null && this.isRemoveCellsFromParent() &&
+ this.shouldRemoveCellsFromParent(this.graph.getModel().getParent(this.cell), cells, evt))
+ {
+ target = this.graph.getDefaultParent();
+ }
+
+ // Passes all selected cells in order to correctly clone or move into
+ // the target cell. The method checks for each cell if its movable.
+ cells = this.graph.moveCells(cells, dx - this.graph.panDx / this.graph.view.scale,
+ dy - this.graph.panDy / this.graph.view.scale, clone, target, evt);
+
+ if (this.isSelectEnabled() && this.scrollOnMove)
+ {
+ this.graph.scrollCellToVisible(cells[0]);
+ }
+
+ // Selects the new cells if cells have been cloned
+ if (clone)
+ {
+ this.graph.setSelectionCells(cells);
+ }
+};
+
+/**
+ * Function: destroyShapes
+ *
+ * Destroy the preview and highlight shapes.
+ */
+mxGraphHandler.prototype.destroyShapes = function()
+{
+ // Destroys the preview dashed rectangle
+ if (this.shape != null)
+ {
+ this.shape.destroy();
+ this.shape = null;
+ }
+
+ if (this.guide != null)
+ {
+ this.guide.destroy();
+ this.guide = null;
+ }
+
+ // Destroys the drop target highlight
+ if (this.highlight != null)
+ {
+ this.highlight.destroy();
+ this.highlight = null;
+ }
+};
+
+/**
+ * Function: destroy
+ *
+ * Destroys the handler and all its resources and DOM nodes.
+ */
+mxGraphHandler.prototype.destroy = function()
+{
+ this.graph.removeMouseListener(this);
+ this.graph.removeListener(this.panHandler);
+ this.destroyShapes();
+};
diff --git a/src/js/handler/mxKeyHandler.js b/src/js/handler/mxKeyHandler.js
new file mode 100644
index 0000000..cc07e51
--- /dev/null
+++ b/src/js/handler/mxKeyHandler.js
@@ -0,0 +1,402 @@
+/**
+ * $Id: mxKeyHandler.js,v 1.48 2012-03-30 08:30:41 gaudenz Exp $
+ * Copyright (c) 2006-2010, JGraph Ltd
+ */
+/**
+ * Class: mxKeyHandler
+ *
+ * Event handler that listens to keystroke events. This is not a singleton,
+ * however, it is normally only required once if the target is the document
+ * element (default).
+ *
+ * This handler installs a key event listener in the topmost DOM node and
+ * processes all events that originate from descandants of <mxGraph.container>
+ * or from the topmost DOM node. The latter means that all unhandled keystrokes
+ * are handled by this object regardless of the focused state of the <graph>.
+ *
+ * Example:
+ *
+ * The following example creates a key handler that listens to the delete key
+ * (46) and deletes the selection cells if the graph is enabled.
+ *
+ * (code)
+ * var keyHandler = new mxKeyHandler(graph);
+ * keyHandler.bindKey(46, function(evt)
+ * {
+ * if (graph.isEnabled())
+ * {
+ * graph.removeCells();
+ * }
+ * });
+ * (end)
+ *
+ * Keycodes:
+ *
+ * See http://tinyurl.com/yp8jgl or http://tinyurl.com/229yqw for a list of
+ * keycodes or install a key event listener into the document element and print
+ * the key codes of the respective events to the console.
+ *
+ * To support the Command key and the Control key on the Mac, the following
+ * code can be used.
+ *
+ * (code)
+ * keyHandler.getFunction = function(evt)
+ * {
+ * if (evt != null)
+ * {
+ * return (mxEvent.isControlDown(evt) || (mxClient.IS_MAC && evt.metaKey)) ? this.controlKeys[evt.keyCode] : this.normalKeys[evt.keyCode];
+ * }
+ *
+ * return null;
+ * };
+ * (end)
+ *
+ * Constructor: mxKeyHandler
+ *
+ * Constructs an event handler that executes functions bound to specific
+ * keystrokes.
+ *
+ * Parameters:
+ *
+ * graph - Reference to the associated <mxGraph>.
+ * target - Optional reference to the event target. If null, the document
+ * element is used as the event target, that is, the object where the key
+ * event listener is installed.
+ */
+function mxKeyHandler(graph, target)
+{
+ if (graph != null)
+ {
+ this.graph = graph;
+ this.target = target || document.documentElement;
+
+ // Creates the arrays to map from keycodes to functions
+ this.normalKeys = [];
+ this.shiftKeys = [];
+ this.controlKeys = [];
+ this.controlShiftKeys = [];
+
+ // Installs the keystroke listener in the target
+ mxEvent.addListener(this.target, "keydown",
+ mxUtils.bind(this, function(evt)
+ {
+ this.keyDown(evt);
+ })
+ );
+
+ // Automatically deallocates memory in IE
+ if (mxClient.IS_IE)
+ {
+ mxEvent.addListener(window, 'unload',
+ mxUtils.bind(this, function()
+ {
+ this.destroy();
+ })
+ );
+ }
+ }
+};
+
+/**
+ * Variable: graph
+ *
+ * Reference to the <mxGraph> associated with this handler.
+ */
+mxKeyHandler.prototype.graph = null;
+
+/**
+ * Variable: target
+ *
+ * Reference to the target DOM, that is, the DOM node where the key event
+ * listeners are installed.
+ */
+mxKeyHandler.prototype.target = null;
+
+/**
+ * Variable: normalKeys
+ *
+ * Maps from keycodes to functions for non-pressed control keys.
+ */
+mxKeyHandler.prototype.normalKeys = null;
+
+/**
+ * Variable: shiftKeys
+ *
+ * Maps from keycodes to functions for pressed shift keys.
+ */
+mxKeyHandler.prototype.shiftKeys = null;
+
+/**
+ * Variable: controlKeys
+ *
+ * Maps from keycodes to functions for pressed control keys.
+ */
+mxKeyHandler.prototype.controlKeys = null;
+
+/**
+ * Variable: controlShiftKeys
+ *
+ * Maps from keycodes to functions for pressed control and shift keys.
+ */
+mxKeyHandler.prototype.controlShiftKeys = null;
+
+/**
+ * Variable: enabled
+ *
+ * Specifies if events are handled. Default is true.
+ */
+mxKeyHandler.prototype.enabled = true;
+
+/**
+ * Function: isEnabled
+ *
+ * Returns true if events are handled. This implementation returns
+ * <enabled>.
+ */
+mxKeyHandler.prototype.isEnabled = function()
+{
+ return this.enabled;
+};
+
+/**
+ * Function: setEnabled
+ *
+ * Enables or disables event handling by updating <enabled>.
+ *
+ * Parameters:
+ *
+ * enabled - Boolean that specifies the new enabled state.
+ */
+mxKeyHandler.prototype.setEnabled = function(enabled)
+{
+ this.enabled = enabled;
+};
+
+/**
+ * Function: bindKey
+ *
+ * Binds the specified keycode to the given function. This binding is used
+ * if the control key is not pressed.
+ *
+ * Parameters:
+ *
+ * code - Integer that specifies the keycode.
+ * funct - JavaScript function that takes the key event as an argument.
+ */
+mxKeyHandler.prototype.bindKey = function(code, funct)
+{
+ this.normalKeys[code] = funct;
+};
+
+/**
+ * Function: bindShiftKey
+ *
+ * Binds the specified keycode to the given function. This binding is used
+ * if the shift key is pressed.
+ *
+ * Parameters:
+ *
+ * code - Integer that specifies the keycode.
+ * funct - JavaScript function that takes the key event as an argument.
+ */
+mxKeyHandler.prototype.bindShiftKey = function(code, funct)
+{
+ this.shiftKeys[code] = funct;
+};
+
+/**
+ * Function: bindControlKey
+ *
+ * Binds the specified keycode to the given function. This binding is used
+ * if the control key is pressed.
+ *
+ * Parameters:
+ *
+ * code - Integer that specifies the keycode.
+ * funct - JavaScript function that takes the key event as an argument.
+ */
+mxKeyHandler.prototype.bindControlKey = function(code, funct)
+{
+ this.controlKeys[code] = funct;
+};
+
+/**
+ * Function: bindControlShiftKey
+ *
+ * Binds the specified keycode to the given function. This binding is used
+ * if the control and shift key are pressed.
+ *
+ * Parameters:
+ *
+ * code - Integer that specifies the keycode.
+ * funct - JavaScript function that takes the key event as an argument.
+ */
+mxKeyHandler.prototype.bindControlShiftKey = function(code, funct)
+{
+ this.controlShiftKeys[code] = funct;
+};
+
+/**
+ * Function: isControlDown
+ *
+ * Returns true if the control key is pressed. This uses <mxEvent.isControlDown>.
+ *
+ * Parameters:
+ *
+ * evt - Key event whose control key pressed state should be returned.
+ */
+mxKeyHandler.prototype.isControlDown = function(evt)
+{
+ return mxEvent.isControlDown(evt);
+};
+
+/**
+ * Function: getFunction
+ *
+ * Returns the function associated with the given key event or null if no
+ * function is associated with the given event.
+ *
+ * Parameters:
+ *
+ * evt - Key event whose associated function should be returned.
+ */
+mxKeyHandler.prototype.getFunction = function(evt)
+{
+ if (evt != null)
+ {
+ if (this.isControlDown(evt))
+ {
+ if (mxEvent.isShiftDown(evt))
+ {
+ return this.controlShiftKeys[evt.keyCode];
+ }
+ else
+ {
+ return this.controlKeys[evt.keyCode];
+ }
+ }
+ else
+ {
+ if (mxEvent.isShiftDown(evt))
+ {
+ return this.shiftKeys[evt.keyCode];
+ }
+ else
+ {
+ return this.normalKeys[evt.keyCode];
+ }
+ }
+ }
+
+ return null;
+};
+
+/**
+ * Function: isGraphEvent
+ *
+ * Returns true if the event should be processed by this handler, that is,
+ * if the event source is either the target, one of its direct children, a
+ * descendant of the <mxGraph.container>, or the <mxGraph.cellEditor> of the
+ * <graph>.
+ *
+ * Parameters:
+ *
+ * evt - Key event that represents the keystroke.
+ */
+mxKeyHandler.prototype.isGraphEvent = function(evt)
+{
+ var source = mxEvent.getSource(evt);
+
+ // Accepts events from the target object or
+ // in-place editing inside graph
+ if ((source == this.target || source.parentNode == this.target) ||
+ (this.graph.cellEditor != null && source == this.graph.cellEditor.textarea))
+ {
+ return true;
+ }
+
+ // Accepts events from inside the container
+ var elt = source;
+
+ while (elt != null)
+ {
+ if (elt == this.graph.container)
+ {
+ return true;
+ }
+
+ elt = elt.parentNode;
+ }
+
+ return false;
+};
+
+/**
+ * Function: keyDown
+ *
+ * Handles the event by invoking the function bound to the respective
+ * keystroke if <mxGraph.isEnabled>, <isEnabled> and <isGraphEvent> all
+ * return true for the given event and <mxGraph.isEditing> returns false.
+ * If the graph is editing only the <enter> and <escape> cases are handled
+ * by calling the respective hooks.
+ *
+ * Parameters:
+ *
+ * evt - Key event that represents the keystroke.
+ */
+mxKeyHandler.prototype.keyDown = function(evt)
+{
+ if (this.graph.isEnabled() && !mxEvent.isConsumed(evt) &&
+ this.isGraphEvent(evt) && this.isEnabled())
+ {
+ // Cancels the editing if escape is pressed
+ if (evt.keyCode == 27 /* Escape */)
+ {
+ this.escape(evt);
+ }
+
+ // Invokes the function for the keystroke
+ else if (!this.graph.isEditing())
+ {
+ var boundFunction = this.getFunction(evt);
+
+ if (boundFunction != null)
+ {
+ boundFunction(evt);
+ mxEvent.consume(evt);
+ }
+ }
+ }
+};
+
+/**
+ * Function: escape
+ *
+ * Hook to process ESCAPE keystrokes. This implementation invokes
+ * <mxGraph.stopEditing> to cancel the current editing, connecting
+ * and/or other ongoing modifications.
+ *
+ * Parameters:
+ *
+ * evt - Key event that represents the keystroke. Possible keycode in this
+ * case is 27 (ESCAPE).
+ */
+mxKeyHandler.prototype.escape = function(evt)
+{
+ if (this.graph.isEscapeEnabled())
+ {
+ this.graph.escape(evt);
+ }
+};
+
+/**
+ * Function: destroy
+ *
+ * Destroys the handler and all its references into the DOM. This does
+ * normally not need to be called, it is called automatically when the
+ * window unloads (in IE).
+ */
+mxKeyHandler.prototype.destroy = function()
+{
+ this.target = null;
+};
diff --git a/src/js/handler/mxPanningHandler.js b/src/js/handler/mxPanningHandler.js
new file mode 100644
index 0000000..b388144
--- /dev/null
+++ b/src/js/handler/mxPanningHandler.js
@@ -0,0 +1,390 @@
+/**
+ * $Id: mxPanningHandler.js,v 1.79 2012-07-17 14:37:41 gaudenz Exp $
+ * Copyright (c) 2006-2010, JGraph Ltd
+ */
+/**
+ * Class: mxPanningHandler
+ *
+ * Event handler that pans and creates popupmenus. To use the left
+ * mousebutton for panning without interfering with cell moving and
+ * resizing, use <isUseLeftButton> and <isIgnoreCell>. For grid size
+ * steps while panning, use <useGrid>. This handler is built-into
+ * <mxGraph.panningHandler> and enabled using <mxGraph.setPanning>.
+ *
+ * Constructor: mxPanningHandler
+ *
+ * Constructs an event handler that creates a <mxPopupMenu>
+ * and pans the graph.
+ *
+ * Event: mxEvent.PAN_START
+ *
+ * Fires when the panning handler changes its <active> state to true. The
+ * <code>event</code> property contains the corresponding <mxMouseEvent>.
+ *
+ * Event: mxEvent.PAN
+ *
+ * Fires while handle is processing events. The <code>event</code> property contains
+ * the corresponding <mxMouseEvent>.
+ *
+ * Event: mxEvent.PAN_END
+ *
+ * Fires when the panning handler changes its <active> state to false. The
+ * <code>event</code> property contains the corresponding <mxMouseEvent>.
+ */
+function mxPanningHandler(graph, factoryMethod)
+{
+ if (graph != null)
+ {
+ this.graph = graph;
+ this.factoryMethod = factoryMethod;
+ this.graph.addMouseListener(this);
+ this.init();
+ }
+};
+
+/**
+ * Extends mxPopupMenu.
+ */
+mxPanningHandler.prototype = new mxPopupMenu();
+mxPanningHandler.prototype.constructor = mxPanningHandler;
+
+/**
+ * Variable: graph
+ *
+ * Reference to the enclosing <mxGraph>.
+ */
+mxPanningHandler.prototype.graph = null;
+
+/**
+ * Variable: usePopupTrigger
+ *
+ * Specifies if the <isPopupTrigger> should also be used for panning. To
+ * avoid conflicts, the panning is only activated if the mouse was moved
+ * more than <mxGraph.tolerance>, otherwise, a single click is assumed
+ * and the popupmenu is displayed. Default is true.
+ */
+mxPanningHandler.prototype.usePopupTrigger = true;
+
+/**
+ * Variable: useLeftButtonForPanning
+ *
+ * Specifies if panning should be active for the left mouse button.
+ * Setting this to true may conflict with <mxRubberband>. Default is false.
+ */
+mxPanningHandler.prototype.useLeftButtonForPanning = false;
+
+/**
+ * Variable: selectOnPopup
+ *
+ * Specifies if cells should be selected if a popupmenu is displayed for
+ * them. Default is true.
+ */
+mxPanningHandler.prototype.selectOnPopup = true;
+
+/**
+ * Variable: clearSelectionOnBackground
+ *
+ * Specifies if cells should be deselected if a popupmenu is displayed for
+ * the diagram background. Default is true.
+ */
+mxPanningHandler.prototype.clearSelectionOnBackground = true;
+
+/**
+ * Variable: ignoreCell
+ *
+ * Specifies if panning should be active even if there is a cell under the
+ * mousepointer. Default is false.
+ */
+mxPanningHandler.prototype.ignoreCell = false;
+
+/**
+ * Variable: previewEnabled
+ *
+ * Specifies if the panning should be previewed. Default is true.
+ */
+mxPanningHandler.prototype.previewEnabled = true;
+
+/**
+ * Variable: useGrid
+ *
+ * Specifies if the panning steps should be aligned to the grid size.
+ * Default is false.
+ */
+mxPanningHandler.prototype.useGrid = false;
+
+/**
+ * Variable: panningEnabled
+ *
+ * Specifies if panning should be enabled. Default is true.
+ */
+mxPanningHandler.prototype.panningEnabled = true;
+
+/**
+ * Function: isPanningEnabled
+ *
+ * Returns <panningEnabled>.
+ */
+mxPanningHandler.prototype.isPanningEnabled = function()
+{
+ return this.panningEnabled;
+};
+
+/**
+ * Function: setPanningEnabled
+ *
+ * Sets <panningEnabled>.
+ */
+mxPanningHandler.prototype.setPanningEnabled = function(value)
+{
+ this.panningEnabled = value;
+};
+
+/**
+ * Function: init
+ *
+ * Initializes the shapes required for this vertex handler.
+ */
+mxPanningHandler.prototype.init = function()
+{
+ // Supercall
+ mxPopupMenu.prototype.init.apply(this);
+
+ // Hides the tooltip if the mouse is over
+ // the context menu
+ mxEvent.addListener(this.div, (mxClient.IS_TOUCH) ? 'touchmove' : 'mousemove',
+ mxUtils.bind(this, function(evt)
+ {
+ this.graph.tooltipHandler.hide();
+ })
+ );
+};
+
+/**
+ * Function: isPanningTrigger
+ *
+ * Returns true if the given event is a panning trigger for the optional
+ * given cell. This returns true if control-shift is pressed or if
+ * <usePopupTrigger> is true and the event is a popup trigger.
+ */
+mxPanningHandler.prototype.isPanningTrigger = function(me)
+{
+ var evt = me.getEvent();
+
+ return (this.useLeftButtonForPanning && (this.ignoreCell || me.getState() == null) &&
+ mxEvent.isLeftMouseButton(evt)) || (mxEvent.isControlDown(evt) &&
+ mxEvent.isShiftDown(evt)) || (this.usePopupTrigger &&
+ mxEvent.isPopupTrigger(evt));
+};
+
+/**
+ * Function: mouseDown
+ *
+ * Handles the event by initiating the panning. By consuming the event all
+ * subsequent events of the gesture are redirected to this handler.
+ */
+mxPanningHandler.prototype.mouseDown = function(sender, me)
+{
+ if (!me.isConsumed() && this.isEnabled())
+ {
+ // Hides the popupmenu if is is being displayed
+ this.hideMenu();
+
+ this.dx0 = -this.graph.container.scrollLeft;
+ this.dy0 = -this.graph.container.scrollTop;
+
+ // Checks the event triggers to panning and popupmenu
+ this.popupTrigger = this.isPopupTrigger(me);
+ this.panningTrigger = this.isPanningEnabled() &&
+ this.isPanningTrigger(me);
+
+ // Stores the location of the trigger event
+ this.startX = me.getX();
+ this.startY = me.getY();
+
+ // Displays popup menu on Mac after the mouse was released
+ if (this.panningTrigger)
+ {
+ this.consumePanningTrigger(me);
+ }
+ }
+};
+
+/**
+ * Function: consumePanningTrigger
+ *
+ * Consumes the given <mxMouseEvent> if it was a panning trigger in
+ * <mouseDown>. The default is to invoke <mxMouseEvent.consume>. Note that this
+ * will block any further event processing. If you haven't disabled built-in
+ * context menus and require immediate selection of the cell on mouseDown in
+ * Safari and/or on the Mac, then use the following code:
+ *
+ * (code)
+ * mxPanningHandler.prototype.consumePanningTrigger = function(me)
+ * {
+ * if (me.evt.preventDefault)
+ * {
+ * me.evt.preventDefault();
+ * }
+ *
+ * // Stops event processing in IE
+ * me.evt.returnValue = false;
+ *
+ * // Sets local consumed state
+ * if (!mxClient.IS_SF && !mxClient.IS_MAC)
+ * {
+ * me.consumed = true;
+ * }
+ * };
+ * (end)
+ */
+mxPanningHandler.prototype.consumePanningTrigger = function(me)
+{
+ me.consume();
+};
+
+/**
+ * Function: mouseMove
+ *
+ * Handles the event by updating the panning on the graph.
+ */
+mxPanningHandler.prototype.mouseMove = function(sender, me)
+{
+ var dx = me.getX() - this.startX;
+ var dy = me.getY() - this.startY;
+
+ if (this.active)
+ {
+ if (this.previewEnabled)
+ {
+ // Applies the grid to the panning steps
+ if (this.useGrid)
+ {
+ dx = this.graph.snap(dx);
+ dy = this.graph.snap(dy);
+ }
+
+ this.graph.panGraph(dx + this.dx0, dy + this.dy0);
+ }
+
+ this.fireEvent(new mxEventObject(mxEvent.PAN, 'event', me));
+ me.consume();
+ }
+ else if (this.panningTrigger)
+ {
+ var tmp = this.active;
+
+ // Panning is activated only if the mouse is moved
+ // beyond the graph tolerance
+ this.active = Math.abs(dx) > this.graph.tolerance ||
+ Math.abs(dy) > this.graph.tolerance;
+
+ if (!tmp && this.active)
+ {
+ this.fireEvent(new mxEventObject(mxEvent.PAN_START, 'event', me));
+ }
+ }
+};
+
+/**
+ * Function: mouseUp
+ *
+ * Handles the event by setting the translation on the view or showing the
+ * popupmenu.
+ */
+mxPanningHandler.prototype.mouseUp = function(sender, me)
+{
+ // Shows popup menu if mouse was not moved
+ var dx = Math.abs(me.getX() - this.startX);
+ var dy = Math.abs(me.getY() - this.startY);
+
+ if (this.active)
+ {
+ if (!this.graph.useScrollbarsForPanning || !mxUtils.hasScrollbars(this.graph.container))
+ {
+ dx = me.getX() - this.startX;
+ dy = me.getY() - this.startY;
+
+ // Applies the grid to the panning steps
+ if (this.useGrid)
+ {
+ dx = this.graph.snap(dx);
+ dy = this.graph.snap(dy);
+ }
+
+ var scale = this.graph.getView().scale;
+ var t = this.graph.getView().translate;
+
+ this.graph.panGraph(0, 0);
+ this.panGraph(t.x + dx / scale, t.y + dy / scale);
+ }
+
+ this.active = false;
+ this.fireEvent(new mxEventObject(mxEvent.PAN_END, 'event', me));
+ me.consume();
+ }
+ else if (this.popupTrigger)
+ {
+ if (dx < this.graph.tolerance && dy < this.graph.tolerance)
+ {
+ var cell = this.getCellForPopupEvent(me);
+
+ // Selects the cell for which the context menu is being displayed
+ if (this.graph.isEnabled() && this.selectOnPopup &&
+ cell != null && !this.graph.isCellSelected(cell))
+ {
+ this.graph.setSelectionCell(cell);
+ }
+ else if (this.clearSelectionOnBackground && cell == null)
+ {
+ this.graph.clearSelection();
+ }
+
+ // Hides the tooltip if there is one
+ this.graph.tooltipHandler.hide();
+ var origin = mxUtils.getScrollOrigin();
+ var point = new mxPoint(me.getX() + origin.x,
+ me.getY() + origin.y);
+
+ // Menu is shifted by 1 pixel so that the mouse up event
+ // is routed via the underlying shape instead of the DIV
+ this.popup(point.x + 1, point.y + 1, cell, me.getEvent());
+ me.consume();
+ }
+ }
+
+ this.panningTrigger = false;
+ this.popupTrigger = false;
+};
+
+/**
+ * Function: getCellForPopupEvent
+ *
+ * Hook to return the cell for the mouse up popup trigger handling.
+ */
+mxPanningHandler.prototype.getCellForPopupEvent = function(me)
+{
+ return me.getCell();
+};
+
+/**
+ * Function: panGraph
+ *
+ * Pans <graph> by the given amount.
+ */
+mxPanningHandler.prototype.panGraph = function(dx, dy)
+{
+ this.graph.getView().setTranslate(dx, dy);
+};
+
+/**
+ * Function: destroy
+ *
+ * Destroys the handler and all its resources and DOM nodes.
+ */
+mxPanningHandler.prototype.destroy = function()
+{
+ this.graph.removeMouseListener(this);
+
+ // Supercall
+ mxPopupMenu.prototype.destroy.apply(this);
+};
diff --git a/src/js/handler/mxRubberband.js b/src/js/handler/mxRubberband.js
new file mode 100644
index 0000000..f9e7187
--- /dev/null
+++ b/src/js/handler/mxRubberband.js
@@ -0,0 +1,348 @@
+/**
+ * $Id: mxRubberband.js,v 1.48 2012-04-13 12:53:30 gaudenz Exp $
+ * Copyright (c) 2006-2010, JGraph Ltd
+ */
+/**
+ * Class: mxRubberband
+ *
+ * Event handler that selects rectangular regions. This is not built-into
+ * <mxGraph>. To enable rubberband selection in a graph, use the following code.
+ *
+ * Example:
+ *
+ * (code)
+ * var rubberband = new mxRubberband(graph);
+ * (end)
+ *
+ * Constructor: mxRubberband
+ *
+ * Constructs an event handler that selects rectangular regions in the graph
+ * using rubberband selection.
+ */
+function mxRubberband(graph)
+{
+ if (graph != null)
+ {
+ this.graph = graph;
+ this.graph.addMouseListener(this);
+
+ // Repaints the marquee after autoscroll
+ this.panHandler = mxUtils.bind(this, function()
+ {
+ this.repaint();
+ });
+
+ this.graph.addListener(mxEvent.PAN, this.panHandler);
+
+ // Automatic deallocation of memory
+ if (mxClient.IS_IE)
+ {
+ mxEvent.addListener(window, 'unload',
+ mxUtils.bind(this, function()
+ {
+ this.destroy();
+ })
+ );
+ }
+ }
+};
+
+/**
+ * Variable: defaultOpacity
+ *
+ * Specifies the default opacity to be used for the rubberband div. Default
+ * is 20.
+ */
+mxRubberband.prototype.defaultOpacity = 20;
+
+/**
+ * Variable: enabled
+ *
+ * Specifies if events are handled. Default is true.
+ */
+mxRubberband.prototype.enabled = true;
+
+/**
+ * Variable: div
+ *
+ * Holds the DIV element which is currently visible.
+ */
+mxRubberband.prototype.div = null;
+
+/**
+ * Variable: sharedDiv
+ *
+ * Holds the DIV element which is used to display the rubberband.
+ */
+mxRubberband.prototype.sharedDiv = null;
+
+/**
+ * Variable: currentX
+ *
+ * Holds the value of the x argument in the last call to <update>.
+ */
+mxRubberband.prototype.currentX = 0;
+
+/**
+ * Variable: currentY
+ *
+ * Holds the value of the y argument in the last call to <update>.
+ */
+mxRubberband.prototype.currentY = 0;
+
+/**
+ * Function: isEnabled
+ *
+ * Returns true if events are handled. This implementation returns
+ * <enabled>.
+ */
+mxRubberband.prototype.isEnabled = function()
+{
+ return this.enabled;
+};
+
+/**
+ * Function: setEnabled
+ *
+ * Enables or disables event handling. This implementation updates
+ * <enabled>.
+ */
+mxRubberband.prototype.setEnabled = function(enabled)
+{
+ this.enabled = enabled;
+};
+
+/**
+ * Function: mouseDown
+ *
+ * Handles the event by initiating a rubberband selection. By consuming the
+ * event all subsequent events of the gesture are redirected to this
+ * handler.
+ */
+mxRubberband.prototype.mouseDown = function(sender, me)
+{
+ if (!me.isConsumed() && this.isEnabled() && this.graph.isEnabled() &&
+ (this.graph.isForceMarqueeEvent(me.getEvent()) || me.getState() == null))
+ {
+ var offset = mxUtils.getOffset(this.graph.container);
+ var origin = mxUtils.getScrollOrigin(this.graph.container);
+ origin.x -= offset.x;
+ origin.y -= offset.y;
+ this.start(me.getX() + origin.x, me.getY() + origin.y);
+
+ // Workaround for rubberband stopping if the mouse leaves the
+ // graph container in Firefox.
+ if (mxClient.IS_NS && !mxClient.IS_SF && !mxClient.IS_GC)
+ {
+ var container = this.graph.container;
+
+ function createMouseEvent(evt)
+ {
+ var me = new mxMouseEvent(evt);
+ var pt = mxUtils.convertPoint(container, me.getX(), me.getY());
+
+ me.graphX = pt.x;
+ me.graphY = pt.y;
+
+ return me;
+ };
+
+ this.dragHandler = mxUtils.bind(this, function(evt)
+ {
+ this.mouseMove(this.graph, createMouseEvent(evt));
+ });
+
+ this.dropHandler = mxUtils.bind(this, function(evt)
+ {
+ this.mouseUp(this.graph, createMouseEvent(evt));
+ });
+
+ mxEvent.addListener(document, 'mousemove', this.dragHandler);
+ mxEvent.addListener(document, 'mouseup', this.dropHandler);
+ }
+
+ // Does not prevent the default for this event so that the
+ // event processing chain is still executed even if we start
+ // rubberbanding. This is required eg. in ExtJs to hide the
+ // current context menu. In mouseMove we'll make sure we're
+ // not selecting anything while we're rubberbanding.
+ me.consume(false);
+ }
+};
+
+/**
+ * Function: start
+ *
+ * Sets the start point for the rubberband selection.
+ */
+mxRubberband.prototype.start = function(x, y)
+{
+ this.first = new mxPoint(x, y);
+};
+
+/**
+ * Function: mouseMove
+ *
+ * Handles the event by updating therubberband selection.
+ */
+mxRubberband.prototype.mouseMove = function(sender, me)
+{
+ if (!me.isConsumed() && this.first != null)
+ {
+ var origin = mxUtils.getScrollOrigin(this.graph.container);
+ var offset = mxUtils.getOffset(this.graph.container);
+ origin.x -= offset.x;
+ origin.y -= offset.y;
+ var x = me.getX() + origin.x;
+ var y = me.getY() + origin.y;
+ var dx = this.first.x - x;
+ var dy = this.first.y - y;
+ var tol = this.graph.tolerance;
+
+ if (this.div != null || Math.abs(dx) > tol || Math.abs(dy) > tol)
+ {
+ if (this.div == null)
+ {
+ this.div = this.createShape();
+ }
+
+ // Clears selection while rubberbanding. This is required because
+ // the event is not consumed in mouseDown.
+ mxUtils.clearSelection();
+
+ this.update(x, y);
+ me.consume();
+ }
+ }
+};
+
+/**
+ * Function: createShape
+ *
+ * Creates the rubberband selection shape.
+ */
+mxRubberband.prototype.createShape = function()
+{
+ if (this.sharedDiv == null)
+ {
+ this.sharedDiv = document.createElement('div');
+ this.sharedDiv.className = 'mxRubberband';
+ mxUtils.setOpacity(this.sharedDiv, this.defaultOpacity);
+ }
+
+ this.graph.container.appendChild(this.sharedDiv);
+
+ return this.sharedDiv;
+};
+
+/**
+ * Function: mouseUp
+ *
+ * Handles the event by selecting the region of the rubberband using
+ * <mxGraph.selectRegion>.
+ */
+mxRubberband.prototype.mouseUp = function(sender, me)
+{
+ var execute = this.div != null;
+ this.reset();
+
+ if (execute)
+ {
+ var rect = new mxRectangle(this.x, this.y, this.width, this.height);
+ this.graph.selectRegion(rect, me.getEvent());
+ me.consume();
+ }
+};
+
+/**
+ * Function: reset
+ *
+ * Resets the state of the rubberband selection.
+ */
+mxRubberband.prototype.reset = function()
+{
+ if (this.div != null)
+ {
+ this.div.parentNode.removeChild(this.div);
+ }
+
+ if (this.dragHandler != null)
+ {
+ mxEvent.removeListener(document, 'mousemove', this.dragHandler);
+ this.dragHandler = null;
+ }
+
+ if (this.dropHandler != null)
+ {
+ mxEvent.removeListener(document, 'mouseup', this.dropHandler);
+ this.dropHandler = null;
+ }
+
+ this.currentX = 0;
+ this.currentY = 0;
+ this.first = null;
+ this.div = null;
+};
+
+/**
+ * Function: update
+ *
+ * Sets <currentX> and <currentY> and calls <repaint>.
+ */
+mxRubberband.prototype.update = function(x, y)
+{
+ this.currentX = x;
+ this.currentY = y;
+
+ this.repaint();
+};
+
+/**
+ * Function: repaint
+ *
+ * Computes the bounding box and updates the style of the <div>.
+ */
+mxRubberband.prototype.repaint = function()
+{
+ if (this.div != null)
+ {
+ var x = this.currentX - this.graph.panDx;
+ var y = this.currentY - this.graph.panDy;
+
+ this.x = Math.min(this.first.x, x);
+ this.y = Math.min(this.first.y, y);
+ this.width = Math.max(this.first.x, x) - this.x;
+ this.height = Math.max(this.first.y, y) - this.y;
+
+ var dx = (mxClient.IS_VML) ? this.graph.panDx : 0;
+ var dy = (mxClient.IS_VML) ? this.graph.panDy : 0;
+
+ this.div.style.left = (this.x + dx) + 'px';
+ this.div.style.top = (this.y + dy) + 'px';
+ this.div.style.width = Math.max(1, this.width) + 'px';
+ this.div.style.height = Math.max(1, this.height) + 'px';
+ }
+};
+
+/**
+ * Function: destroy
+ *
+ * Destroys the handler and all its resources and DOM nodes. This does
+ * normally not need to be called, it is called automatically when the
+ * window unloads.
+ */
+mxRubberband.prototype.destroy = function()
+{
+ if (!this.destroyed)
+ {
+ this.destroyed = true;
+ this.graph.removeMouseListener(this);
+ this.graph.removeListener(this.panHandler);
+ this.reset();
+
+ if (this.sharedDiv != null)
+ {
+ this.sharedDiv = null;
+ }
+ }
+};
diff --git a/src/js/handler/mxSelectionCellsHandler.js b/src/js/handler/mxSelectionCellsHandler.js
new file mode 100644
index 0000000..800d718
--- /dev/null
+++ b/src/js/handler/mxSelectionCellsHandler.js
@@ -0,0 +1,260 @@
+/**
+ * $Id: mxSelectionCellsHandler.js,v 1.5 2012-08-10 11:35:06 gaudenz Exp $
+ * Copyright (c) 2006-2010, JGraph Ltd
+ */
+/**
+ * Class: mxSelectionCellsHandler
+ *
+ * An event handler that manages cell handlers and invokes their mouse event
+ * processing functions.
+ *
+ * Group: Events
+ *
+ * Event: mxEvent.ADD
+ *
+ * Fires if a cell has been added to the selection. The <code>state</code>
+ * property contains the <mxCellState> that has been added.
+ *
+ * Event: mxEvent.REMOVE
+ *
+ * Fires if a cell has been remove from the selection. The <code>state</code>
+ * property contains the <mxCellState> that has been removed.
+ *
+ * Parameters:
+ *
+ * graph - Reference to the enclosing <mxGraph>.
+ */
+function mxSelectionCellsHandler(graph)
+{
+ this.graph = graph;
+ this.handlers = new mxDictionary();
+ this.graph.addMouseListener(this);
+
+ this.refreshHandler = mxUtils.bind(this, function(sender, evt)
+ {
+ if (this.isEnabled())
+ {
+ this.refresh();
+ }
+ });
+
+ this.graph.getSelectionModel().addListener(mxEvent.CHANGE, this.refreshHandler);
+ this.graph.getModel().addListener(mxEvent.CHANGE, this.refreshHandler);
+ this.graph.getView().addListener(mxEvent.SCALE, this.refreshHandler);
+ this.graph.getView().addListener(mxEvent.TRANSLATE, this.refreshHandler);
+ this.graph.getView().addListener(mxEvent.SCALE_AND_TRANSLATE, this.refreshHandler);
+ this.graph.getView().addListener(mxEvent.DOWN, this.refreshHandler);
+ this.graph.getView().addListener(mxEvent.UP, this.refreshHandler);
+};
+
+/**
+ * Extends mxEventSource.
+ */
+mxSelectionCellsHandler.prototype = new mxEventSource();
+mxSelectionCellsHandler.prototype.constructor = mxSelectionCellsHandler;
+
+/**
+ * Variable: graph
+ *
+ * Reference to the enclosing <mxGraph>.
+ */
+mxSelectionCellsHandler.prototype.graph = null;
+
+/**
+ * Variable: enabled
+ *
+ * Specifies if events are handled. Default is true.
+ */
+mxSelectionCellsHandler.prototype.enabled = true;
+
+/**
+ * Variable: refreshHandler
+ *
+ * Keeps a reference to an event listener for later removal.
+ */
+mxSelectionCellsHandler.prototype.refreshHandler = null;
+
+/**
+ * Variable: maxHandlers
+ *
+ * Defines the maximum number of handlers to paint individually. Default is 100.
+ */
+mxSelectionCellsHandler.prototype.maxHandlers = 100;
+
+/**
+ * Variable: handlers
+ *
+ * <mxDictionary> that maps from cells to handlers.
+ */
+mxSelectionCellsHandler.prototype.handlers = null;
+
+/**
+ * Function: isEnabled
+ *
+ * Returns <enabled>.
+ */
+mxSelectionCellsHandler.prototype.isEnabled = function()
+{
+ return this.enabled;
+};
+
+/**
+ * Function: setEnabled
+ *
+ * Sets <enabled>.
+ */
+mxSelectionCellsHandler.prototype.setEnabled = function(value)
+{
+ this.enabled = value;
+};
+
+/**
+ * Function: getHandler
+ *
+ * Returns the handler for the given cell.
+ */
+mxSelectionCellsHandler.prototype.getHandler = function(cell)
+{
+ return this.handlers.get(cell);
+};
+
+/**
+ * Function: reset
+ *
+ * Resets all handlers.
+ */
+mxSelectionCellsHandler.prototype.reset = function()
+{
+ this.handlers.visit(function(key, handler)
+ {
+ handler.reset.apply(handler);
+ });
+};
+
+/**
+ * Function: refresh
+ *
+ * Reloads or updates all handlers.
+ */
+mxSelectionCellsHandler.prototype.refresh = function()
+{
+ // Removes all existing handlers
+ var oldHandlers = this.handlers;
+ this.handlers = new mxDictionary();
+
+ // Creates handles for all selection cells
+ var tmp = this.graph.getSelectionCells();
+
+ for (var i = 0; i < tmp.length; i++)
+ {
+ var state = this.graph.view.getState(tmp[i]);
+
+ if (state != null)
+ {
+ var handler = oldHandlers.remove(tmp[i]);
+
+ if (handler != null)
+ {
+ if (handler.state != state)
+ {
+ handler.destroy();
+ handler = null;
+ }
+ else
+ {
+ handler.redraw();
+ }
+ }
+
+ if (handler == null)
+ {
+ handler = this.graph.createHandler(state);
+ this.fireEvent(new mxEventObject(mxEvent.ADD, 'state', state));
+ }
+
+ if (handler != null)
+ {
+ this.handlers.put(tmp[i], handler);
+ }
+ }
+ }
+
+ // Destroys all unused handlers
+ oldHandlers.visit(mxUtils.bind(this, function(key, handler)
+ {
+ this.fireEvent(new mxEventObject(mxEvent.REMOVE, 'state', handler.state));
+ handler.destroy();
+ }));
+};
+
+/**
+ * Function: mouseDown
+ *
+ * Redirects the given event to the handlers.
+ */
+mxSelectionCellsHandler.prototype.mouseDown = function(sender, me)
+{
+ if (this.graph.isEnabled() && this.isEnabled())
+ {
+ var args = [sender, me];
+
+ this.handlers.visit(function(key, handler)
+ {
+ handler.mouseDown.apply(handler, args);
+ });
+ }
+};
+
+/**
+ * Function: mouseMove
+ *
+ * Redirects the given event to the handlers.
+ */
+mxSelectionCellsHandler.prototype.mouseMove = function(sender, me)
+{
+ if (this.graph.isEnabled() && this.isEnabled())
+ {
+ var args = [sender, me];
+
+ this.handlers.visit(function(key, handler)
+ {
+ handler.mouseMove.apply(handler, args);
+ });
+ }
+};
+
+/**
+ * Function: mouseUp
+ *
+ * Redirects the given event to the handlers.
+ */
+mxSelectionCellsHandler.prototype.mouseUp = function(sender, me)
+{
+ if (this.graph.isEnabled() && this.isEnabled())
+ {
+ var args = [sender, me];
+
+ this.handlers.visit(function(key, handler)
+ {
+ handler.mouseUp.apply(handler, args);
+ });
+ }
+};
+
+/**
+ * Function: destroy
+ *
+ * Destroys the handler and all its resources and DOM nodes.
+ */
+mxSelectionCellsHandler.prototype.destroy = function()
+{
+ this.graph.removeMouseListener(this);
+
+ if (this.refreshHandler != null)
+ {
+ this.graph.getSelectionModel().removeListener(this.refreshHandler);
+ this.graph.getModel().removeListener(this.refreshHandler);
+ this.graph.getView().removeListener(this.refreshHandler);
+ this.refreshHandler = null;
+ }
+};
diff --git a/src/js/handler/mxTooltipHandler.js b/src/js/handler/mxTooltipHandler.js
new file mode 100644
index 0000000..4e34a13
--- /dev/null
+++ b/src/js/handler/mxTooltipHandler.js
@@ -0,0 +1,317 @@
+/**
+ * $Id: mxTooltipHandler.js,v 1.51 2011-03-31 10:11:17 gaudenz Exp $
+ * Copyright (c) 2006-2010, JGraph Ltd
+ */
+/**
+ * Class: mxTooltipHandler
+ *
+ * Graph event handler that displays tooltips. <mxGraph.getTooltip> is used to
+ * get the tooltip for a cell or handle. This handler is built-into
+ * <mxGraph.tooltipHandler> and enabled using <mxGraph.setTooltips>.
+ *
+ * Example:
+ *
+ * (code>
+ * new mxTooltipHandler(graph);
+ * (end)
+ *
+ * Constructor: mxTooltipHandler
+ *
+ * Constructs an event handler that displays tooltips with the specified
+ * delay (in milliseconds). If no delay is specified then a default delay
+ * of 500 ms (0.5 sec) is used.
+ *
+ * Parameters:
+ *
+ * graph - Reference to the enclosing <mxGraph>.
+ * delay - Optional delay in milliseconds.
+ */
+function mxTooltipHandler(graph, delay)
+{
+ if (graph != null)
+ {
+ this.graph = graph;
+ this.delay = delay || 500;
+ this.graph.addMouseListener(this);
+ }
+};
+
+/**
+ * Variable: zIndex
+ *
+ * Specifies the zIndex for the tooltip and its shadow. Default is 10005.
+ */
+mxTooltipHandler.prototype.zIndex = 10005;
+
+/**
+ * Variable: graph
+ *
+ * Reference to the enclosing <mxGraph>.
+ */
+mxTooltipHandler.prototype.graph = null;
+
+/**
+ * Variable: delay
+ *
+ * Delay to show the tooltip in milliseconds. Default is 500.
+ */
+mxTooltipHandler.prototype.delay = null;
+
+/**
+ * Variable: hideOnHover
+ *
+ * Specifies if the tooltip should be hidden if the mouse is moved over the
+ * current cell. Default is false.
+ */
+mxTooltipHandler.prototype.hideOnHover = false;
+
+/**
+ * Variable: enabled
+ *
+ * Specifies if events are handled. Default is true.
+ */
+mxTooltipHandler.prototype.enabled = true;
+
+/**
+ * Function: isEnabled
+ *
+ * Returns true if events are handled. This implementation
+ * returns <enabled>.
+ */
+mxTooltipHandler.prototype.isEnabled = function()
+{
+ return this.enabled;
+};
+
+/**
+ * Function: setEnabled
+ *
+ * Enables or disables event handling. This implementation
+ * updates <enabled>.
+ */
+mxTooltipHandler.prototype.setEnabled = function(enabled)
+{
+ this.enabled = enabled;
+};
+
+/**
+ * Function: isHideOnHover
+ *
+ * Returns <hideOnHover>.
+ */
+mxTooltipHandler.prototype.isHideOnHover = function()
+{
+ return this.hideOnHover;
+};
+
+/**
+ * Function: setHideOnHover
+ *
+ * Sets <hideOnHover>.
+ */
+mxTooltipHandler.prototype.setHideOnHover = function(value)
+{
+ this.hideOnHover = value;
+};
+
+/**
+ * Function: init
+ *
+ * Initializes the DOM nodes required for this tooltip handler.
+ */
+mxTooltipHandler.prototype.init = function()
+{
+ if (document.body != null)
+ {
+ this.div = document.createElement('div');
+ this.div.className = 'mxTooltip';
+ this.div.style.visibility = 'hidden';
+ this.div.style.zIndex = this.zIndex;
+
+ document.body.appendChild(this.div);
+
+ mxEvent.addListener(this.div, 'mousedown',
+ mxUtils.bind(this, function(evt)
+ {
+ this.hideTooltip();
+ })
+ );
+ }
+};
+
+/**
+ * Function: mouseDown
+ *
+ * Handles the event by initiating a rubberband selection. By consuming the
+ * event all subsequent events of the gesture are redirected to this
+ * handler.
+ */
+mxTooltipHandler.prototype.mouseDown = function(sender, me)
+{
+ this.reset(me, false);
+ this.hideTooltip();
+};
+
+/**
+ * Function: mouseMove
+ *
+ * Handles the event by updating the rubberband selection.
+ */
+mxTooltipHandler.prototype.mouseMove = function(sender, me)
+{
+ if (me.getX() != this.lastX || me.getY() != this.lastY)
+ {
+ this.reset(me, true);
+
+ if (this.isHideOnHover() || me.getState() != this.state || (me.getSource() != this.node &&
+ (!this.stateSource || (me.getState() != null && this.stateSource ==
+ (me.isSource(me.getState().shape) || !me.isSource(me.getState().text))))))
+ {
+ this.hideTooltip();
+ }
+ }
+
+ this.lastX = me.getX();
+ this.lastY = me.getY();
+};
+
+/**
+ * Function: mouseUp
+ *
+ * Handles the event by resetting the tooltip timer or hiding the existing
+ * tooltip.
+ */
+mxTooltipHandler.prototype.mouseUp = function(sender, me)
+{
+ this.reset(me, true);
+ this.hideTooltip();
+};
+
+
+/**
+ * Function: resetTimer
+ *
+ * Resets the timer.
+ */
+mxTooltipHandler.prototype.resetTimer = function()
+{
+ if (this.thread != null)
+ {
+ window.clearTimeout(this.thread);
+ this.thread = null;
+ }
+};
+
+/**
+ * Function: reset
+ *
+ * Resets and/or restarts the timer to trigger the display of the tooltip.
+ */
+mxTooltipHandler.prototype.reset = function(me, restart)
+{
+ this.resetTimer();
+
+ if (restart && this.isEnabled() && me.getState() != null && (this.div == null ||
+ this.div.style.visibility == 'hidden'))
+ {
+ var state = me.getState();
+ var node = me.getSource();
+ var x = me.getX();
+ var y = me.getY();
+ var stateSource = me.isSource(state.shape) || me.isSource(state.text);
+
+ this.thread = window.setTimeout(mxUtils.bind(this, function()
+ {
+ if (!this.graph.isEditing() && !this.graph.panningHandler.isMenuShowing())
+ {
+ // Uses information from inside event cause using the event at
+ // this (delayed) point in time is not possible in IE as it no
+ // longer contains the required information (member not found)
+ var tip = this.graph.getTooltip(state, node, x, y);
+ this.show(tip, x, y);
+ this.state = state;
+ this.node = node;
+ this.stateSource = stateSource;
+ }
+ }), this.delay);
+ }
+};
+
+/**
+ * Function: hide
+ *
+ * Hides the tooltip and resets the timer.
+ */
+mxTooltipHandler.prototype.hide = function()
+{
+ this.resetTimer();
+ this.hideTooltip();
+};
+
+/**
+ * Function: hideTooltip
+ *
+ * Hides the tooltip.
+ */
+mxTooltipHandler.prototype.hideTooltip = function()
+{
+ if (this.div != null)
+ {
+ this.div.style.visibility = 'hidden';
+ }
+};
+
+/**
+ * Function: show
+ *
+ * Shows the tooltip for the specified cell and optional index at the
+ * specified location (with a vertical offset of 10 pixels).
+ */
+mxTooltipHandler.prototype.show = function(tip, x, y)
+{
+ if (tip != null && tip.length > 0)
+ {
+ // Initializes the DOM nodes if required
+ if (this.div == null)
+ {
+ this.init();
+ }
+
+ var origin = mxUtils.getScrollOrigin();
+
+ this.div.style.left = (x + origin.x) + 'px';
+ this.div.style.top = (y + mxConstants.TOOLTIP_VERTICAL_OFFSET +
+ origin.y) + 'px';
+
+ if (!mxUtils.isNode(tip))
+ {
+ this.div.innerHTML = tip.replace(/\n/g, '<br>');
+ }
+ else
+ {
+ this.div.innerHTML = '';
+ this.div.appendChild(tip);
+ }
+
+ this.div.style.visibility = '';
+ mxUtils.fit(this.div);
+ }
+};
+
+/**
+ * Function: destroy
+ *
+ * Destroys the handler and all its resources and DOM nodes.
+ */
+mxTooltipHandler.prototype.destroy = function()
+{
+ this.graph.removeMouseListener(this);
+ mxEvent.release(this.div);
+
+ if (this.div != null && this.div.parentNode != null)
+ {
+ this.div.parentNode.removeChild(this.div);
+ }
+
+ this.div = null;
+};
diff --git a/src/js/handler/mxVertexHandler.js b/src/js/handler/mxVertexHandler.js
new file mode 100644
index 0000000..0b12e27
--- /dev/null
+++ b/src/js/handler/mxVertexHandler.js
@@ -0,0 +1,753 @@
+/**
+ * $Id: mxVertexHandler.js,v 1.107 2012-11-20 09:06:07 gaudenz Exp $
+ * Copyright (c) 2006-2010, JGraph Ltd
+ */
+/**
+ * Class: mxVertexHandler
+ *
+ * Event handler for resizing cells. This handler is automatically created in
+ * <mxGraph.createHandler>.
+ *
+ * Constructor: mxVertexHandler
+ *
+ * Constructs an event handler that allows to resize vertices
+ * and groups.
+ *
+ * Parameters:
+ *
+ * state - <mxCellState> of the cell to be resized.
+ */
+function mxVertexHandler(state)
+{
+ if (state != null)
+ {
+ this.state = state;
+ this.init();
+ }
+};
+
+/**
+ * Variable: graph
+ *
+ * Reference to the enclosing <mxGraph>.
+ */
+mxVertexHandler.prototype.graph = null;
+
+/**
+ * Variable: state
+ *
+ * Reference to the <mxCellState> being modified.
+ */
+mxVertexHandler.prototype.state = null;
+
+/**
+ * Variable: singleSizer
+ *
+ * Specifies if only one sizer handle at the bottom, right corner should be
+ * used. Default is false.
+ */
+mxVertexHandler.prototype.singleSizer = false;
+
+/**
+ * Variable: index
+ *
+ * Holds the index of the current handle.
+ */
+mxVertexHandler.prototype.index = null;
+
+/**
+ * Variable: allowHandleBoundsCheck
+ *
+ * Specifies if the bounds of handles should be used for hit-detection in IE
+ * Default is true.
+ */
+mxVertexHandler.prototype.allowHandleBoundsCheck = true;
+
+/**
+ * Variable: crisp
+ *
+ * Specifies if the selection bounds and handles should be rendered in crisp
+ * mode. Default is true.
+ */
+mxVertexHandler.prototype.crisp = true;
+
+/**
+ * Variable: handleImage
+ *
+ * Optional <mxImage> to be used as handles. Default is null.
+ */
+mxVertexHandler.prototype.handleImage = null;
+
+/**
+ * Variable: tolerance
+ *
+ * Optional tolerance for hit-detection in <getHandleForEvent>. Default is 0.
+ */
+mxVertexHandler.prototype.tolerance = 0;
+
+/**
+ * Function: init
+ *
+ * Initializes the shapes required for this vertex handler.
+ */
+mxVertexHandler.prototype.init = function()
+{
+ this.graph = this.state.view.graph;
+ this.selectionBounds = this.getSelectionBounds(this.state);
+ this.bounds = new mxRectangle(this.selectionBounds.x, this.selectionBounds.y,
+ this.selectionBounds.width, this.selectionBounds.height);
+ this.selectionBorder = this.createSelectionShape(this.bounds);
+ this.selectionBorder.dialect =
+ (this.graph.dialect != mxConstants.DIALECT_SVG) ?
+ mxConstants.DIALECT_VML : mxConstants.DIALECT_SVG;
+ this.selectionBorder.init(this.graph.getView().getOverlayPane());
+
+ // Event-transparency
+ if (this.selectionBorder.dialect == mxConstants.DIALECT_SVG)
+ {
+ this.selectionBorder.node.setAttribute('pointer-events', 'none');
+ }
+ else
+ {
+ this.selectionBorder.node.style.background = '';
+ }
+
+ if (this.graph.isCellMovable(this.state.cell))
+ {
+ this.selectionBorder.node.style.cursor = mxConstants.CURSOR_MOVABLE_VERTEX;
+ }
+
+ mxEvent.redirectMouseEvents(this.selectionBorder.node, this.graph, this.state);
+
+ // Adds the sizer handles
+ if (mxGraphHandler.prototype.maxCells <= 0 ||
+ this.graph.getSelectionCount() < mxGraphHandler.prototype.maxCells)
+ {
+ var resizable = this.graph.isCellResizable(this.state.cell);
+ this.sizers = [];
+
+ if (resizable || (this.graph.isLabelMovable(this.state.cell) &&
+ this.state.width >= 2 && this.state.height >= 2))
+ {
+ var i = 0;
+
+ if (resizable)
+ {
+ if (!this.singleSizer)
+ {
+ this.sizers.push(this.createSizer('nw-resize', i++));
+ this.sizers.push(this.createSizer('n-resize', i++));
+ this.sizers.push(this.createSizer('ne-resize', i++));
+ this.sizers.push(this.createSizer('w-resize', i++));
+ this.sizers.push(this.createSizer('e-resize', i++));
+ this.sizers.push(this.createSizer('sw-resize', i++));
+ this.sizers.push(this.createSizer('s-resize', i++));
+ }
+
+ this.sizers.push(this.createSizer('se-resize', i++));
+ }
+
+ var geo = this.graph.model.getGeometry(this.state.cell);
+
+ if (geo != null && !geo.relative && !this.graph.isSwimlane(this.state.cell) &&
+ this.graph.isLabelMovable(this.state.cell))
+ {
+ // Marks this as the label handle for getHandleForEvent
+ this.labelShape = this.createSizer(mxConstants.CURSOR_LABEL_HANDLE,
+ mxEvent.LABEL_HANDLE, mxConstants.LABEL_HANDLE_SIZE,
+ mxConstants.LABEL_HANDLE_FILLCOLOR);
+ this.sizers.push(this.labelShape);
+ }
+ }
+ else if (this.graph.isCellMovable(this.state.cell) && !this.graph.isCellResizable(this.state.cell) &&
+ this.state.width < 2 && this.state.height < 2)
+ {
+ this.labelShape = this.createSizer(mxConstants.CURSOR_MOVABLE_VERTEX,
+ null, null, mxConstants.LABEL_HANDLE_FILLCOLOR);
+ this.sizers.push(this.labelShape);
+ }
+ }
+
+ this.redraw();
+};
+
+/**
+ * Function: getSelectionBounds
+ *
+ * Returns the mxRectangle that defines the bounds of the selection
+ * border.
+ */
+mxVertexHandler.prototype.getSelectionBounds = function(state)
+{
+ return new mxRectangle(state.x, state.y, state.width, state.height);
+};
+
+/**
+ * Function: createSelectionShape
+ *
+ * Creates the shape used to draw the selection border.
+ */
+mxVertexHandler.prototype.createSelectionShape = function(bounds)
+{
+ var shape = new mxRectangleShape(bounds, null, this.getSelectionColor());
+ shape.strokewidth = this.getSelectionStrokeWidth();
+ shape.isDashed = this.isSelectionDashed();
+ shape.crisp = this.crisp;
+
+ return shape;
+};
+
+/**
+ * Function: getSelectionColor
+ *
+ * Returns <mxConstants.VERTEX_SELECTION_COLOR>.
+ */
+mxVertexHandler.prototype.getSelectionColor = function()
+{
+ return mxConstants.VERTEX_SELECTION_COLOR;
+};
+
+/**
+ * Function: getSelectionStrokeWidth
+ *
+ * Returns <mxConstants.VERTEX_SELECTION_STROKEWIDTH>.
+ */
+mxVertexHandler.prototype.getSelectionStrokeWidth = function()
+{
+ return mxConstants.VERTEX_SELECTION_STROKEWIDTH;
+};
+
+/**
+ * Function: isSelectionDashed
+ *
+ * Returns <mxConstants.VERTEX_SELECTION_DASHED>.
+ */
+mxVertexHandler.prototype.isSelectionDashed = function()
+{
+ return mxConstants.VERTEX_SELECTION_DASHED;
+};
+
+/**
+ * Function: createSizer
+ *
+ * Creates a sizer handle for the specified cursor and index and returns
+ * the new <mxRectangleShape> that represents the handle.
+ */
+mxVertexHandler.prototype.createSizer = function(cursor, index, size, fillColor)
+{
+ size = size || mxConstants.HANDLE_SIZE;
+
+ var bounds = new mxRectangle(0, 0, size, size);
+ var sizer = this.createSizerShape(bounds, index, fillColor);
+
+ if (this.state.text != null && this.state.text.node.parentNode == this.graph.container)
+ {
+ sizer.bounds.height -= 1;
+ sizer.bounds.width -= 1;
+ sizer.dialect = mxConstants.DIALECT_STRICTHTML;
+ sizer.init(this.graph.container);
+ }
+ else
+ {
+ sizer.dialect = (this.graph.dialect != mxConstants.DIALECT_SVG) ?
+ mxConstants.DIALECT_VML : mxConstants.DIALECT_SVG;
+ sizer.init(this.graph.getView().getOverlayPane());
+ }
+
+ mxEvent.redirectMouseEvents(sizer.node, this.graph, this.state);
+
+ if (this.graph.isEnabled())
+ {
+ sizer.node.style.cursor = cursor;
+ }
+
+ if (!this.isSizerVisible(index))
+ {
+ sizer.node.style.visibility = 'hidden';
+ }
+
+ return sizer;
+};
+
+/**
+ * Function: isSizerVisible
+ *
+ * Returns true if the sizer for the given index is visible.
+ * This returns true for all given indices.
+ */
+mxVertexHandler.prototype.isSizerVisible = function(index)
+{
+ return true;
+};
+
+/**
+ * Function: createSizerShape
+ *
+ * Creates the shape used for the sizer handle for the specified bounds and
+ * index.
+ */
+mxVertexHandler.prototype.createSizerShape = function(bounds, index, fillColor)
+{
+ if (this.handleImage != null)
+ {
+ bounds.width = this.handleImage.width;
+ bounds.height = this.handleImage.height;
+
+ return new mxImageShape(bounds, this.handleImage.src);
+ }
+ else
+ {
+ var shape = new mxRectangleShape(bounds,
+ fillColor || mxConstants.HANDLE_FILLCOLOR,
+ mxConstants.HANDLE_STROKECOLOR);
+ shape.crisp = this.crisp;
+
+ return shape;
+ }
+};
+
+/**
+ * Function: createBounds
+ *
+ * Helper method to create an <mxRectangle> around the given centerpoint
+ * with a width and height of 2*s or 6, if no s is given.
+ */
+mxVertexHandler.prototype.moveSizerTo = function(shape, x, y)
+{
+ if (shape != null)
+ {
+ shape.bounds.x = x - shape.bounds.width / 2;
+ shape.bounds.y = y - shape.bounds.height / 2;
+ shape.redraw();
+ }
+};
+
+/**
+ * Function: getHandleForEvent
+ *
+ * Returns the index of the handle for the given event. This returns the index
+ * of the sizer from where the event originated or <mxEvent.LABEL_INDEX>.
+ */
+mxVertexHandler.prototype.getHandleForEvent = function(me)
+{
+ if (me.isSource(this.labelShape))
+ {
+ return mxEvent.LABEL_HANDLE;
+ }
+
+ if (this.sizers != null)
+ {
+ // Connection highlight may consume events before they reach sizer handle
+ var tol = this.tolerance;
+ var hit = (this.allowHandleBoundsCheck && (mxClient.IS_IE || tol > 0)) ?
+ new mxRectangle(me.getGraphX() - tol, me.getGraphY() - tol, 2 * tol, 2 * tol) : null;
+
+ for (var i = 0; i < this.sizers.length; i++)
+ {
+ if (me.isSource(this.sizers[i]) || (hit != null &&
+ this.sizers[i].node.style.visibility != 'hidden' &&
+ mxUtils.intersects(this.sizers[i].bounds, hit)))
+ {
+ return i;
+ }
+ }
+ }
+
+ return null;
+};
+
+/**
+ * Function: mouseDown
+ *
+ * Handles the event if a handle has been clicked. By consuming the
+ * event all subsequent events of the gesture are redirected to this
+ * handler.
+ */
+mxVertexHandler.prototype.mouseDown = function(sender, me)
+{
+ if (!me.isConsumed() && this.graph.isEnabled() && !this.graph.isForceMarqueeEvent(me.getEvent()) &&
+ (this.tolerance > 0 || me.getState() == this.state))
+ {
+ var handle = this.getHandleForEvent(me);
+
+ if (handle != null)
+ {
+ this.start(me.getX(), me.getY(), handle);
+ me.consume();
+ }
+ }
+};
+
+/**
+ * Function: start
+ *
+ * Starts the handling of the mouse gesture.
+ */
+mxVertexHandler.prototype.start = function(x, y, index)
+{
+ var pt = mxUtils.convertPoint(this.graph.container, x, y);
+ this.startX = pt.x;
+ this.startY = pt.y;
+ this.index = index;
+
+ // Creates a preview that can be on top of any HTML label
+ this.selectionBorder.node.style.visibility = 'hidden';
+ this.preview = this.createSelectionShape(this.bounds);
+
+ if (this.state.text != null && this.state.text.node.parentNode == this.graph.container)
+ {
+ this.preview.dialect = mxConstants.DIALECT_STRICTHTML;
+ this.preview.init(this.graph.container);
+ }
+ else
+ {
+ this.preview.dialect = (this.graph.dialect != mxConstants.DIALECT_SVG) ?
+ mxConstants.DIALECT_VML : mxConstants.DIALECT_SVG;
+ this.preview.init(this.graph.view.getOverlayPane());
+ }
+};
+
+/**
+ * Function: mouseMove
+ *
+ * Handles the event by updating the preview.
+ */
+mxVertexHandler.prototype.mouseMove = function(sender, me)
+{
+ if (!me.isConsumed() && this.index != null)
+ {
+ var point = new mxPoint(me.getGraphX(), me.getGraphY());
+ var gridEnabled = this.graph.isGridEnabledEvent(me.getEvent());
+ var scale = this.graph.getView().scale;
+
+ if (this.index == mxEvent.LABEL_HANDLE)
+ {
+ if (gridEnabled)
+ {
+ point.x = this.graph.snap(point.x / scale) * scale;
+ point.y = this.graph.snap(point.y / scale) * scale;
+ }
+
+ this.moveSizerTo(this.sizers[this.sizers.length - 1], point.x, point.y);
+ me.consume();
+ }
+ else if (this.index != null)
+ {
+ var dx = point.x - this.startX;
+ var dy = point.y - this.startY;
+ var tr = this.graph.view.translate;
+ this.bounds = this.union(this.selectionBounds, dx, dy, this.index, gridEnabled, scale, tr);
+ this.drawPreview();
+ me.consume();
+ }
+ }
+ // Workaround for disabling the connect highlight when over handle
+ else if (this.getHandleForEvent(me) != null)
+ {
+ me.consume(false);
+ }
+};
+
+/**
+ * Function: mouseUp
+ *
+ * Handles the event by applying the changes to the geometry.
+ */
+mxVertexHandler.prototype.mouseUp = function(sender, me)
+{
+ if (!me.isConsumed() && this.index != null && this.state != null)
+ {
+ var point = new mxPoint(me.getGraphX(), me.getGraphY());
+ var scale = this.graph.getView().scale;
+
+ var gridEnabled = this.graph.isGridEnabledEvent(me.getEvent());
+ var dx = (point.x - this.startX) / scale;
+ var dy = (point.y - this.startY) / scale;
+
+ this.resizeCell(this.state.cell, dx, dy, this.index, gridEnabled);
+ this.reset();
+ me.consume();
+ }
+};
+
+/**
+ * Function: reset
+ *
+ * Resets the state of this handler.
+ */
+mxVertexHandler.prototype.reset = function()
+{
+ this.index = null;
+
+ if (this.preview != null)
+ {
+ this.preview.destroy();
+ this.preview = null;
+ }
+
+ // Checks if handler has been destroyed
+ if (this.selectionBorder != null)
+ {
+ this.selectionBounds = this.getSelectionBounds(this.state);
+ this.selectionBorder.node.style.visibility = 'visible';
+ this.bounds = new mxRectangle(this.selectionBounds.x, this.selectionBounds.y,
+ this.selectionBounds.width, this.selectionBounds.height);
+ this.drawPreview();
+ }
+};
+
+/**
+ * Function: resizeCell
+ *
+ * Uses the given vector to change the bounds of the given cell
+ * in the graph using <mxGraph.resizeCell>.
+ */
+mxVertexHandler.prototype.resizeCell = function(cell, dx, dy, index, gridEnabled)
+{
+ var geo = this.graph.model.getGeometry(cell);
+
+ if (index == mxEvent.LABEL_HANDLE)
+ {
+ var scale = this.graph.view.scale;
+ dx = (this.labelShape.bounds.getCenterX() - this.startX) / scale;
+ dy = (this.labelShape.bounds.getCenterY() - this.startY) / scale;
+
+ geo = geo.clone();
+
+ if (geo.offset == null)
+ {
+ geo.offset = new mxPoint(dx, dy);
+ }
+ else
+ {
+ geo.offset.x += dx;
+ geo.offset.y += dy;
+ }
+
+ this.graph.model.setGeometry(cell, geo);
+ }
+ else
+ {
+ var bounds = this.union(geo, dx, dy, index, gridEnabled, 1, new mxPoint(0, 0));
+ this.graph.resizeCell(cell, bounds);
+ }
+};
+
+/**
+ * Function: union
+ *
+ * Returns the union of the given bounds and location for the specified
+ * handle index.
+ *
+ * To override this to limit the size of vertex via a minWidth/-Height style,
+ * the following code can be used.
+ *
+ * (code)
+ * var vertexHandlerUnion = mxVertexHandler.prototype.union;
+ * mxVertexHandler.prototype.union = function(bounds, dx, dy, index, gridEnabled, scale, tr)
+ * {
+ * var result = vertexHandlerUnion.apply(this, arguments);
+ *
+ * result.width = Math.max(result.width, mxUtils.getNumber(this.state.style, 'minWidth', 0));
+ * result.height = Math.max(result.height, mxUtils.getNumber(this.state.style, 'minHeight', 0));
+ *
+ * return result;
+ * };
+ * (end)
+ *
+ * The minWidth/-Height style can then be used as follows:
+ *
+ * (code)
+ * graph.insertVertex(parent, null, 'Hello,', 20, 20, 80, 30, 'minWidth=100;minHeight=100;');
+ * (end)
+ */
+mxVertexHandler.prototype.union = function(bounds, dx, dy, index, gridEnabled, scale, tr)
+{
+ if (this.singleSizer)
+ {
+ var x = bounds.x + bounds.width + dx;
+ var y = bounds.y + bounds.height + dy;
+
+ if (gridEnabled)
+ {
+ x = this.graph.snap(x / scale) * scale;
+ y = this.graph.snap(y / scale) * scale;
+ }
+
+ var rect = new mxRectangle(bounds.x, bounds.y, 0, 0);
+ rect.add(new mxRectangle(x, y, 0, 0));
+
+ return rect;
+ }
+ else
+ {
+ var left = bounds.x - tr.x * scale;
+ var right = left + bounds.width;
+ var top = bounds.y - tr.y * scale;
+ var bottom = top + bounds.height;
+
+ if (index > 4 /* Bottom Row */)
+ {
+ bottom = bottom + dy;
+
+ if (gridEnabled)
+ {
+ bottom = this.graph.snap(bottom / scale) * scale;
+ }
+ }
+ else if (index < 3 /* Top Row */)
+ {
+ top = top + dy;
+
+ if (gridEnabled)
+ {
+ top = this.graph.snap(top / scale) * scale;
+ }
+ }
+
+ if (index == 0 || index == 3 || index == 5 /* Left */)
+ {
+ left += dx;
+
+ if (gridEnabled)
+ {
+ left = this.graph.snap(left / scale) * scale;
+ }
+ }
+ else if (index == 2 || index == 4 || index == 7 /* Right */)
+ {
+ right += dx;
+
+ if (gridEnabled)
+ {
+ right = this.graph.snap(right / scale) * scale;
+ }
+ }
+
+ var width = right - left;
+ var height = bottom - top;
+
+ // Flips over left side
+ if (width < 0)
+ {
+ left += width;
+ width = Math.abs(width);
+ }
+
+ // Flips over top side
+ if (height < 0)
+ {
+ top += height;
+ height = Math.abs(height);
+ }
+
+ return new mxRectangle(left + tr.x * scale, top + tr.y * scale, width, height);
+ }
+};
+
+/**
+ * Function: redraw
+ *
+ * Redraws the handles and the preview.
+ */
+mxVertexHandler.prototype.redraw = function()
+{
+ this.selectionBounds = this.getSelectionBounds(this.state);
+ this.bounds = new mxRectangle(this.selectionBounds.x, this.selectionBounds.y,
+ this.selectionBounds.width, this.selectionBounds.height);
+
+ if (this.sizers != null)
+ {
+ var s = this.state;
+ var r = s.x + s.width;
+ var b = s.y + s.height;
+
+ if (this.singleSizer)
+ {
+ this.moveSizerTo(this.sizers[0], r, b);
+ }
+ else
+ {
+ var cx = s.x + s.width / 2;
+ var cy = s.y + s.height / 2;
+
+ if (this.sizers.length > 1)
+ {
+ this.moveSizerTo(this.sizers[0], s.x, s.y);
+ this.moveSizerTo(this.sizers[1], cx, s.y);
+ this.moveSizerTo(this.sizers[2], r, s.y);
+ this.moveSizerTo(this.sizers[3], s.x, cy);
+ this.moveSizerTo(this.sizers[4], r, cy);
+ this.moveSizerTo(this.sizers[5], s.x, b);
+ this.moveSizerTo(this.sizers[6], cx, b);
+ this.moveSizerTo(this.sizers[7], r, b);
+ this.moveSizerTo(this.sizers[8],
+ cx + s.absoluteOffset.x,
+ cy + s.absoluteOffset.y);
+ }
+ else if (this.state.width >= 2 && this.state.height >= 2)
+ {
+ this.moveSizerTo(this.sizers[0],
+ cx + s.absoluteOffset.x,
+ cy + s.absoluteOffset.y);
+ }
+ else
+ {
+ this.moveSizerTo(this.sizers[0], s.x, s.y);
+ }
+ }
+ }
+
+ this.drawPreview();
+};
+
+/**
+ * Function: drawPreview
+ *
+ * Redraws the preview.
+ */
+mxVertexHandler.prototype.drawPreview = function()
+{
+ if (this.preview != null)
+ {
+ this.preview.bounds = this.bounds;
+
+ if (this.preview.node.parentNode == this.graph.container)
+ {
+ this.preview.bounds.width = Math.max(0, this.preview.bounds.width - 1);
+ this.preview.bounds.height = Math.max(0, this.preview.bounds.height - 1);
+ }
+
+ this.preview.redraw();
+ }
+
+ this.selectionBorder.bounds = this.bounds;
+ this.selectionBorder.redraw();
+};
+
+/**
+ * Function: destroy
+ *
+ * Destroys the handler and all its resources and DOM nodes.
+ */
+mxVertexHandler.prototype.destroy = function()
+{
+ if (this.preview != null)
+ {
+ this.preview.destroy();
+ this.preview = null;
+ }
+
+ this.selectionBorder.destroy();
+ this.selectionBorder = null;
+ this.labelShape = null;
+
+ if (this.sizers != null)
+ {
+ for (var i = 0; i < this.sizers.length; i++)
+ {
+ this.sizers[i].destroy();
+ this.sizers[i] = null;
+ }
+ }
+};