diff options
author | adhitya | 2016-04-11 15:10:54 +0000 |
---|---|---|
committer | adhitya | 2016-04-11 15:10:54 +0000 |
commit | 92f3207b50a1caca07df5c5b238212af3358905b (patch) | |
tree | 38c92f9649c6f1016d2ef70fa2fd33c86b437cba /src/js/handler | |
parent | ab5fb6e125d82fdd5818aea3ce370c43c2293ddd (diff) | |
download | xcos-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.js | 271 | ||||
-rw-r--r-- | src/js/handler/mxCellMarker.js | 419 | ||||
-rw-r--r-- | src/js/handler/mxCellTracker.js | 149 | ||||
-rw-r--r-- | src/js/handler/mxConnectionHandler.js | 1969 | ||||
-rw-r--r-- | src/js/handler/mxConstraintHandler.js | 308 | ||||
-rw-r--r-- | src/js/handler/mxEdgeHandler.js | 1529 | ||||
-rw-r--r-- | src/js/handler/mxEdgeSegmentHandler.js | 284 | ||||
-rw-r--r-- | src/js/handler/mxElbowEdgeHandler.js | 248 | ||||
-rw-r--r-- | src/js/handler/mxGraphHandler.js | 916 | ||||
-rw-r--r-- | src/js/handler/mxKeyHandler.js | 402 | ||||
-rw-r--r-- | src/js/handler/mxPanningHandler.js | 390 | ||||
-rw-r--r-- | src/js/handler/mxRubberband.js | 348 | ||||
-rw-r--r-- | src/js/handler/mxSelectionCellsHandler.js | 260 | ||||
-rw-r--r-- | src/js/handler/mxTooltipHandler.js | 317 | ||||
-rw-r--r-- | src/js/handler/mxVertexHandler.js | 753 |
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; + } + } +}; |