diff --git a/ChangeLog b/ChangeLog index 28595cb5f..ab87c9eb4 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,12 @@ +28-SEP-2023: 22.0.0 + +- Adds DRAWIO_SERVER_URL as an additional URL to DRAWIO_BASE_URL. BASE_URL used to indicate + the URL both the back-end and static code were delivered on. SERVER_URL now provides a + way to delivery on one URL and server the back-end functions on a different URL. + By default the server URL value is blank. +- Fixes search for uncompressed shapes, result order +- Fixes dialog stack order [drawio-3883] + 24-SEP-2023: 21.8.2 - Confirm escape key for insert layout [drawio-3880] diff --git a/README.md b/README.md index e00e60eba..d5c3900f0 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ As well as running this project, we run a production-grade deployment of the dia License ----------------- -The source code authored by us in this repo is licensed under the Apache v2, however, we do not claim to be an open source project. +The source code authored by us in this repo is licensed under the Apache v2. The full core is open source, but the are some boundary functions that are difficult to publish in a way we can maintain them. The JGraph provided icons and diagram templates are licensed under the [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/). Additional terms may also apply where the icons are originally defined by a third-party copyright holder. We have checked in all cases that the original license allows use in this project. Also see the terms for using the draw.io logo below. @@ -40,17 +40,13 @@ Supported Browsers draw.io supports Chrome 70+, Firefox 70+, Safari 11+, Opera 50+, Native Android browser 7x+, the default browser in the current and previous major iOS versions (e.g. 11.2.x and 10.3.x) and Edge 79+. -This project is not open-source, nor open-contribution +This project is not open-contribution ------------------------------------------------------ -draw.io is not open source software. The complete source code required to build the app from scratch is not available. The Apache license allows you to deploy the project and make changes to the source code that is available on the site. - -We do not make full human readable sources available to avoid a position where another commercial product affects our revenues. +draw.io is also closed to contributions. We follow a development process compliant with our SOC 2 Type II process. We do not have a mechanism where we can accept contributions from non-staff members. draw.io is not suitable as a framework for building other products from. For this try either [Tldraw](https://github.com/tldraw/tldraw) or [Excalidraw](https://github.com/excalidraw/excalidraw). -draw.io is also closed to contributions. We follow a development process compliant with our SOC 2 Type II process. We do not have a mechanism where we can accept contributions from non-staff members. - Logo and trademark usage ------------------------ diff --git a/VERSION b/VERSION index 74ad84d13..a1e47e58d 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -21.8.2 \ No newline at end of file +22.0.0 \ No newline at end of file diff --git a/src/main/mxgraph/handler/mxCellHighlight.js b/src/main/mxgraph/handler/mxCellHighlight.js new file mode 100644 index 000000000..4353459d5 --- /dev/null +++ b/src/main/mxgraph/handler/mxCellHighlight.js @@ -0,0 +1,296 @@ +/** + * Copyright (c) 2006-2015, JGraph Ltd + * Copyright (c) 2006-2015, Gaudenz Alder + */ +/** + * 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, dashed) +{ + if (graph != null) + { + this.graph = graph; + this.highlightColor = (highlightColor != null) ? highlightColor : mxConstants.DEFAULT_VALID_COLOR; + this.strokeWidth = (strokeWidth != null) ? strokeWidth : mxConstants.HIGHLIGHT_STROKEWIDTH; + this.dashed = (dashed != null) ? dashed : false; + this.opacity = mxConstants.HIGHLIGHT_OPACITY; + + // Updates the marker if the graph changes + this.repaintHandler = mxUtils.bind(this, function() + { + // Updates reference to state + if (this.state != null) + { + var tmp = this.graph.view.getState(this.state.cell); + + if (tmp == null) + { + this.hide(); + } + else + { + this.state = tmp; + 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 . + */ +mxCellHighlight.prototype.graph = null; + +/** + * Variable: state + * + * Reference to the . + */ +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) + { + this.shape.stroke = 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); + } +}; + +/** + * Function: createShape + * + * Creates and returns the highlight shape for the given state. + */ +mxCellHighlight.prototype.createShape = function() +{ + var shape = this.graph.cellRenderer.createShape(this.state); + + shape.svgStrokeTolerance = this.graph.tolerance; + shape.points = this.state.absolutePoints; + shape.apply(this.state); + shape.stroke = this.highlightColor; + shape.opacity = this.opacity; + shape.isDashed = this.dashed; + shape.isShadow = false; + + shape.dialect = mxConstants.DIALECT_SVG; + shape.init(this.graph.getView().getOverlayPane()); + mxEvent.redirectMouseEvents(shape.node, this.graph, this.state); + + if (this.graph.dialect != mxConstants.DIALECT_SVG) + { + shape.pointerEvents = false; + } + else + { + shape.svgPointerEvents = 'stroke'; + } + + return shape; +}; + +/** + * Function: getStrokeWidth + * + * Returns the stroke width. + */ +mxCellHighlight.prototype.getStrokeWidth = function(state) +{ + return this.strokeWidth; +}; + +/** + * Function: repaint + * + * Updates the highlight after a change of the model or view. + */ +mxCellHighlight.prototype.repaint = function() +{ + if (this.state != null && this.shape != null) + { + this.shape.scale = this.state.view.scale; + + if (this.graph.model.isEdge(this.state.cell)) + { + this.shape.strokewidth = this.getStrokeWidth(); + this.shape.points = this.state.absolutePoints; + this.shape.outline = false; + } + 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); + this.shape.rotation = Number(this.state.style[mxConstants.STYLE_ROTATION] || '0'); + this.shape.strokewidth = this.getStrokeWidth() / this.state.view.scale; + this.shape.outline = true; + } + + // Uses cursor from shape in highlight + if (this.state.shape != null) + { + this.shape.setCursor(this.state.shape.getCursor()); + } + + this.shape.redraw(); + } +}; + +/** + * Function: hide + * + * Resets the state of the cell marker. + */ +mxCellHighlight.prototype.hide = function() +{ + this.highlight(null); +}; + +/** + * Function: mark + * + * Marks the and fires a 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: isHighlightAt + * + * Returns true if this highlight is at the given position. + */ +mxCellHighlight.prototype.isHighlightAt = function(x, y) +{ + var hit = false; + + // Quirks mode is currently not supported as it used a different coordinate system + if (this.shape != null && document.elementFromPoint != null) + { + var elt = document.elementFromPoint(x, y); + + while (elt != null) + { + if (elt == this.shape.node) + { + hit = true; + break; + } + + elt = elt.parentNode; + } + } + + return hit; +}; + +/** + * Function: destroy + * + * Destroys the handler and all its resources and DOM nodes. + */ +mxCellHighlight.prototype.destroy = function() +{ + this.graph.getView().removeListener(this.resetHandler); + this.graph.getView().removeListener(this.repaintHandler); + this.graph.getModel().removeListener(this.repaintHandler); + + if (this.shape != null) + { + this.shape.destroy(); + this.shape = null; + } +}; diff --git a/src/main/mxgraph/handler/mxCellMarker.js b/src/main/mxgraph/handler/mxCellMarker.js new file mode 100644 index 000000000..26e4e048b --- /dev/null +++ b/src/main/mxgraph/handler/mxCellMarker.js @@ -0,0 +1,428 @@ +/** + * Copyright (c) 2006-2015, JGraph Ltd + * Copyright (c) 2006-2015, Gaudenz Alder + */ +/** + * 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 state + * property contains the marked or null if no state is marked. + * + * Constructor: mxCellMarker + * + * Constructs a new cell marker. + * + * Parameters: + * + * graph - Reference to the enclosing . + * validColor - Optional marker color for valid states. Default is + * . + * invalidColor - Optional marker color for invalid states. Default is + * . + * hotspot - Portion of the width and hight where a state intersects a + * given coordinate pair. A value of 0 means always highlight. Default is + * . + */ +function mxCellMarker(graph, validColor, invalidColor, hotspot) +{ + mxEventSource.call(this); + + if (graph != null) + { + this.graph = graph; + this.validColor = (validColor != null) ? validColor : mxConstants.DEFAULT_VALID_COLOR; + this.invalidColor = (invalidColor != null) ? invalidColor : mxConstants.DEFAULT_INVALID_COLOR; + this.hotspot = (hotspot != null) ? hotspot : mxConstants.DEFAULT_HOTSPOT; + + this.highlight = new mxCellHighlight(graph); + } +}; + +/** + * Extends mxEventSource. + */ +mxUtils.extend(mxCellMarker, mxEventSource); + +/** + * Variable: graph + * + * Reference to the enclosing . + */ +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 if it is valid. + */ +mxCellMarker.prototype.validState = null; + +/** + * Variable: markedState + * + * Holds the marked . + */ +mxCellMarker.prototype.markedState = null; + +/** + * Function: setEnabled + * + * Enables or disables event handling. This implementation + * updates . + * + * 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 . + */ +mxCellMarker.prototype.isEnabled = function() +{ + return this.enabled; +}; + +/** + * Function: setHotspot + * + * Sets the . + */ +mxCellMarker.prototype.setHotspot = function(hotspot) +{ + this.hotspot = hotspot; +}; + +/** + * Function: getHotspot + * + * Returns the . + */ +mxCellMarker.prototype.getHotspot = function() +{ + return this.hotspot; +}; + +/** + * Function: setHotspotEnabled + * + * Specifies whether the hotspot should be used in . + */ +mxCellMarker.prototype.setHotspotEnabled = function(enabled) +{ + this.hotspotEnabled = enabled; +}; + +/** + * Function: isHotspotEnabled + * + * Returns true if hotspot is used in . + */ +mxCellMarker.prototype.isHotspotEnabled = function() +{ + return this.hotspotEnabled; +}; + +/** + * Function: hasValidState + * + * Returns true if is not null. + */ +mxCellMarker.prototype.hasValidState = function() +{ + return this.validState != null; +}; + +/** + * Function: getValidState + * + * Returns the . + */ +mxCellMarker.prototype.getValidState = function() +{ + return this.validState; +}; + +/** + * Function: getMarkedState + * + * Returns the . + */ +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 + * with the color returned by . If the + * markerColor is not null, then the state is stored in . If + * returns true, then the state is stored in + * 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); + this.setCurrentState(state, me); + } + + return state; +}; + +/** + * Function: setCurrentState + * + * Sets and marks the current valid state. + */ +mxCellMarker.prototype.setCurrentState = function(state, me, color) +{ + var isValid = (state != null) ? this.isValidState(state) : false; + color = (color != null) ? 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(); + } + } +}; + +/** + * Function: markCell + * + * Marks the given cell using the given color, or 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 and fires a 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 event. + */ +mxCellMarker.prototype.unmark = function() +{ + this.mark(); +}; + +/** + * Function: isValidState + * + * Returns true if the given is a valid state. If this + * returns true, then the state is stored in . The return value + * of this method is used as the argument for . + */ +mxCellMarker.prototype.isValidState = function(state) +{ + return true; +}; + +/** + * Function: getMarkerColor + * + * Returns the valid- or invalidColor depending on the value of isValid. + * The given is ignored by this implementation. + */ +mxCellMarker.prototype.getMarkerColor = function(evt, state, isValid) +{ + return (isValid) ? this.validColor : this.invalidColor; +}; + +/** + * Function: getState + * + * Uses , and to return the + * for the given . + */ +mxCellMarker.prototype.getState = function(me) +{ + var view = this.graph.getView(); + var cell = this.getCell(me); + var state = this.getStateToMark(view.getState(cell)); + + return (state != null && this.intersects(state, me)) ? state : null; +}; + +/** + * Function: getCell + * + * Returns the for the given event and cell. This returns the + * given cell. + */ +mxCellMarker.prototype.getCell = function(me) +{ + return me.getCell(); +}; + +/** + * Function: getStateToMark + * + * Returns the to be marked for the given 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 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.highlight.destroy(); +}; diff --git a/src/main/mxgraph/handler/mxCellTracker.js b/src/main/mxgraph/handler/mxCellTracker.js new file mode 100644 index 000000000..9f0c8bb07 --- /dev/null +++ b/src/main/mxgraph/handler/mxCellTracker.js @@ -0,0 +1,145 @@ +/** + * Copyright (c) 2006-2015, JGraph Ltd + * Copyright (c) 2006-2015, Gaudenz Alder + */ +/** + * Class: mxCellTracker + * + * Event handler that highlights cells. Inherits from . + * + * 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 . + * color - Color of the highlight. Default is blue. + * funct - Optional JavaScript function that is used to override + * . + */ +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. + */ +mxUtils.extend(mxCellTracker, mxCellMarker); + +/** + * 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) { }; + +/** + * 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/main/mxgraph/handler/mxConnectionHandler.js b/src/main/mxgraph/handler/mxConnectionHandler.js new file mode 100644 index 000000000..fae4e89a5 --- /dev/null +++ b/src/main/mxgraph/handler/mxConnectionHandler.js @@ -0,0 +1,2278 @@ +/** + * Copyright (c) 2006-2016, JGraph Ltd + * Copyright (c) 2006-2016, Gaudenz Alder + */ +/** + * Class: mxConnectionHandler + * + * Graph event handler that creates new connections. Uses + * for finding and highlighting the source and target vertices and + * to create the edge instance. This handler is built-into + * and enabled using . + * + * 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 . + * + * (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 . The new edge is + * created using which in turn uses 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 other diagramming products. + * In order to allow both, moving and connecting cells at the same time, + * 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, + * 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 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 hook may be overridden to create more than one + * 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 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 state + * property contains the state of the source cell. + * + * Event: mxEvent.CONNECT + * + * Fires between begin- and endUpdate in . The cell + * property contains the inserted edge, the event and target + * properties contain the respective arguments that were passed to (where + * target corresponds to the dropTarget argument). Finally, the terminal + * property corresponds to the target argument in or the clone of the source + * terminal if is enabled. + * + * 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, can be used. To resolve + * the port IDs, use . + * + * (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 method is invoked. + * + * Constructor: mxConnectionHandler + * + * Constructs an event handler that connects vertices using the specified + * factory method to create the new edges. Modify + * 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 . + * factoryMethod - Optional function to create the edge. The function takes + * the source and target as the first and second argument and an + * optional cell style from the preview as the third argument. It returns + * the that represents the new edge. + */ +function mxConnectionHandler(graph, factoryMethod) +{ + mxEventSource.call(this); + + if (graph != null) + { + this.graph = graph; + this.factoryMethod = factoryMethod; + this.init(); + + // Handles escape keystrokes + this.escapeHandler = mxUtils.bind(this, function(sender, evt) + { + this.reset(); + }); + + this.graph.addListener(mxEvent.ESCAPE, this.escapeHandler); + } +}; + +/** + * Extends mxEventSource. + */ +mxUtils.extend(mxConnectionHandler, mxEventSource); + +/** + * Variable: graph + * + * Reference to the enclosing . + */ +mxConnectionHandler.prototype.graph = null; + +/** + * Variable: factoryMethod + * + * Function that is used for creating new edges. The function takes the + * source and target as the first and second argument and returns + * a new that represents the edge. This is used in . + */ +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 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 + * + * that is used to trigger the creation of a new connection. This + * is used in . 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 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 + * will be called before the connection is created between + * the source cell and the newly created vertex in , which + * can be overridden to create a new target. Default is false. + */ +mxConnectionHandler.prototype.createTarget = false; + +/** + * Variable: marker + * + * Holds the used for finding source and target cells. + */ +mxConnectionHandler.prototype.marker = null; + +/** + * Variable: constraintHandler + * + * Holds the 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: 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 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, ). + * 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 that represents the preview edge while the + * handler is active. This is created in . + */ +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 false. + */ +mxConnectionHandler.prototype.movePreviewAway = false; + +/** + * Variable: outlineConnect + * + * Specifies if connections to the outline of a highlighted target should be + * enabled. This will allow to place the connection point along the outline of + * the highlighted target. Default is false. + */ +mxConnectionHandler.prototype.outlineConnect = false; + +/** + * Variable: livePreview + * + * Specifies if the actual shape of the edge state should be used for the preview. + * Default is false. (Ignored if no edge state is created in .) + */ +mxConnectionHandler.prototype.livePreview = false; + +/** + * Variable: cursor + * + * Specifies the cursor to be used while the handler is active. Default is null. + */ +mxConnectionHandler.prototype.cursor = null; + +/** + * Variable: insertBeforeSource + * + * Specifies if new edges should be inserted before the source vertex in the + * cell hierarchy. Default is false for backwards compatibility. + */ +mxConnectionHandler.prototype.insertBeforeSource = false; + +/** + * Function: isEnabled + * + * Returns true if events are handled. This implementation + * returns . + */ +mxConnectionHandler.prototype.isEnabled = function() +{ + return this.enabled; +}; + +/** + * Function: setEnabled + * + * Enables or disables event handling. This implementation + * updates . + * + * Parameters: + * + * enabled - Boolean that specifies the new enabled state. + */ +mxConnectionHandler.prototype.setEnabled = function(enabled) +{ + this.enabled = enabled; +}; + +/** + * Function: isInsertBefore + * + * Returns for non-loops and false for loops. + * + * Parameters: + * + * edge - that represents the edge to be inserted. + * source - that represents the source terminal. + * target - that represents the target terminal. + * evt - Mousedown event of the connect gesture. + * dropTarget - that represents the cell under the mouse when it was + * released. + */ +mxConnectionHandler.prototype.isInsertBefore = function(edge, source, target, evt, dropTarget) +{ + return this.insertBeforeSource && source != target && + this.graph.model.getParent(edge) == + this.graph.model.getParent(source); +}; + +/** + * Function: isCreateTarget + * + * Returns . + * + * Parameters: + * + * evt - Current active native pointer event. + */ +mxConnectionHandler.prototype.isCreateTarget = function(evt) +{ + return this.createTarget; +}; + +/** + * Function: setCreateTarget + * + * Sets . + */ +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 = (this.livePreview && this.edgeState != null) ? + this.graph.cellRenderer.createShape(this.edgeState) : + new mxPolyline([], mxConstants.INVALID_COLOR); + shape.dialect = mxConstants.DIALECT_SVG; + shape.scale = this.graph.view.scale; + shape.svgStrokeTolerance = 0; + shape.pointerEvents = false; + shape.isDashed = true; + shape.init(this.graph.getView().getOverlayPane()); + mxEvent.redirectMouseEvents(shape.node, this.graph, null); + + return shape; +}; + +/** + * Function: init + * + * Initializes the shapes required for this connection handler. This should + * be invoked if 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); + this.constraintHandler.reset(); + } + else if (this.previous != null && this.graph.view.getState(this.previous.cell) == null) + { + this.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.reset(); + }); + + 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 used in . + */ +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(me) + { + var cell = mxCellMarker.prototype.getCell.apply(marker, arguments); + this.error = null; + + // Checks for cell at preview point (with grid) + if (cell == null && this.currentPoint != null) + { + cell = this.graph.getCellAt(this.currentPoint.x, this.currentPoint.y); + } + + // Uses connectable parent vertex if one exists + if (cell != null && !this.graph.isCellConnectable(cell)) + { + var parent = this.graph.getModel().getParent(cell); + + if (this.graph.getModel().isVertex(parent) && this.graph.isCellConnectable(parent)) + { + cell = parent; + } + } + + if ((this.graph.isSwimlane(cell) && this.currentPoint != null && + this.graph.hitsSwimlaneContent(cell, this.currentPoint.x, this.currentPoint.y)) || + !this.isConnectableCell(cell)) + { + cell = 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(me.getEvent())) + { + this.error = null; + } + } + } + } + else if (!this.isValidSource(cell, me)) + { + cell = null; + } + } + else if (this.isConnecting() && !this.isCreateTarget(me.getEvent()) && + !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 for the given source terminal. + * + * Parameters: + * + * cell - that represents the source terminal. + * me - that is associated with this call. + */ +mxConnectionHandler.prototype.isValidSource = function(cell, me) +{ + return this.graph.isValidSource(cell); +}; + +/** + * Function: isValidTarget + * + * Returns true. The call to is implicit by calling + * in . This is an + * additional hook for disabling certain targets in this specific handler. + * + * Parameters: + * + * cell - 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 . + * + * Parameters: + * + * source - that represents the source terminal. + * target - 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 used for the connection icon of the given + * . This implementation returns . + * + * Parameters: + * + * state - 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 . + * + * Parameters: + * + * state - 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 that represent the connect icons for + * the given . + * + * Parameters: + * + * state - 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 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 = mxConstants.DIALECT_SVG; + 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 . + * + * Parameters: + * + * icons - Optional array of 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: getIconPosition + * + * Returns the center position of the given icon. + * + * Parameters: + * + * icon - The connect icon of with the mouse. + * state - under the mouse. + */ +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; + + var alpha = mxUtils.toRadians(mxUtils.getValue(state.style, mxConstants.STYLE_ROTATION) || 0); + + if (alpha != 0) + { + var cos = Math.cos(alpha); + var sin = Math.sin(alpha); + var ct = new mxPoint(state.getCenterX(), state.getCenterY()); + var pt = mxUtils.getRotatedPoint(new mxPoint(cx, cy), cos, sin, ct); + cx = pt.x; + cy = pt.y; + } + } + + return new mxPoint(cx - icon.bounds.width / 2, + cy - icon.bounds.height / 2); +}; + +/** + * Function: destroyIcons + * + * Destroys the connect icons and resets the respective state. + */ +mxConnectionHandler.prototype.destroyIcons = function() +{ + if (this.icons != null) + { + for (var i = 0; i < this.icons.length; i++) + { + this.icons[i].destroy(); + } + + this.icons = null; + this.icon = null; + this.selectedIcon = null; + this.iconState = null; + } +}; + +/** + * 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 + * are not null, or and are not null and + * is null or and are not null. + */ +mxConnectionHandler.prototype.isStartEvent = function(me) +{ + return ((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(); + + if (this.edgeState != null) + { + this.shape.apply(this.edgeState); + } + } + + // 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(); + } + + this.selectedIcon = this.icon; + this.icon = null; +}; + +/** + * 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 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)); + * }; + * (end) + */ +mxConnectionHandler.prototype.createEdgeState = function(me) +{ + return null; +}; + +/** + * Function: isOutlineConnectEvent + * + * Returns true if is true and the source of the event is the + * outline shape or shift is pressed. + */ +mxConnectionHandler.prototype.isOutlineConnectEvent = function(me) +{ + if (mxEvent.isShiftDown(me.getEvent()) && mxEvent.isAltDown(me.getEvent())) + { + return false; + } + else + { + var offset = mxUtils.getOffset(this.graph.container); + var evt = me.getEvent(); + + var clientX = mxEvent.getClientX(evt); + var clientY = mxEvent.getClientY(evt); + + var doc = document.documentElement; + var left = (window.pageXOffset || doc.scrollLeft) - (doc.clientLeft || 0); + var top = (window.pageYOffset || doc.scrollTop) - (doc.clientTop || 0); + + var gridX = this.currentPoint.x - this.graph.container.scrollLeft + offset.x - left; + var gridY = this.currentPoint.y - this.graph.container.scrollTop + offset.y - top; + + return this.outlineConnect && ((mxEvent.isShiftDown(me.getEvent()) && + !mxEvent.isAltDown(me.getEvent())) || (me.isSource(this.marker.highlight.shape) || + (!mxEvent.isShiftDown(me.getEvent()) && mxEvent.isAltDown(me.getEvent()) && + me.getState() != null) || this.marker.highlight.isHighlightAt(clientX, clientY) || + ((gridX != clientX || gridY != clientY) && me.getState() == null && + this.marker.highlight.isHighlightAt(gridX, gridY)))); + } +}; + +/** + * Function: updateCurrentState + * + * Updates the current state for a given mouse move event by using + * the . + */ +mxConnectionHandler.prototype.updateCurrentState = function(me, point) +{ + this.constraintHandler.update(me, this.first == null, false, (this.first == null || + me.isSource(this.marker.highlight.shape)) ? null : point); + + if (this.constraintHandler.currentFocus != null && this.constraintHandler.currentConstraint != null) + { + // Handles special case where grid is large and connection point is at actual point in which + // case the outline is not followed as long as we're < gridSize / 2 away from that point + if (this.marker.highlight != null && this.marker.highlight.state != null && + this.marker.highlight.state.cell == this.constraintHandler.currentFocus.cell) + { + // Direct repaint needed if cell already highlighted + if (this.marker.highlight.shape.stroke != 'transparent') + { + this.marker.highlight.shape.stroke = 'transparent'; + this.marker.highlight.repaint(); + } + } + else + { + this.marker.markCell(this.constraintHandler.currentFocus.cell, 'transparent'); + } + + // Updates validation state + if (this.previous != null) + { + this.error = this.validateConnection(this.previous.cell, this.constraintHandler.currentFocus.cell); + + if (this.error == null) + { + this.currentState = this.constraintHandler.currentFocus; + } + + if (this.error != null || (this.currentState != null && + !this.isCellEnabled(this.currentState.cell))) + { + this.constraintHandler.reset(); + } + } + } + else + { + if (this.graph.isIgnoreTerminalEvent(me.getEvent())) + { + this.marker.reset(); + this.currentState = null; + } + else + { + this.marker.process(me); + this.currentState = this.marker.getValidState(); + } + + if (this.currentState != null && !this.isCellEnabled(this.currentState.cell)) + { + this.constraintHandler.reset(); + this.marker.reset(); + this.currentState = null; + } + + var outline = this.isOutlineConnectEvent(me); + + if (this.currentState != null && outline) + { + // Handles special case where mouse is on outline away from actual end point + // in which case the grid is ignored and mouse point is used instead + if (me.isSource(this.marker.highlight.shape)) + { + point = new mxPoint(me.getGraphX(), me.getGraphY()); + } + + var constraint = this.graph.getOutlineConstraint(point, this.currentState, me); + this.constraintHandler.setFocus(me, this.currentState, false); + this.constraintHandler.currentConstraint = constraint; + this.constraintHandler.currentPoint = point; + } + + if (this.outlineConnect) + { + if (this.marker.highlight != null && this.marker.highlight.shape != null) + { + var s = this.graph.view.scale; + + if (this.constraintHandler.currentConstraint != null && + this.constraintHandler.currentFocus != null) + { + this.marker.highlight.shape.stroke = mxConstants.OUTLINE_HIGHLIGHT_COLOR; + this.marker.highlight.shape.strokewidth = mxConstants.OUTLINE_HIGHLIGHT_STROKEWIDTH / s / s; + this.marker.highlight.repaint(); + } + else if (this.marker.hasValidState()) + { + // Handles special case where actual end point of edge and current mouse point + // are not equal (due to grid snapping) and there is no hit on shape or highlight + // but ignores cases where parent is used for non-connectable child cells + if (this.graph.isCellConnectable(me.getCell()) && + this.marker.getValidState() != me.getState()) + { + this.marker.highlight.shape.stroke = 'transparent'; + this.currentState = null; + } + else + { + this.marker.highlight.shape.stroke = mxConstants.DEFAULT_VALID_COLOR; + } + + this.marker.highlight.shape.strokewidth = mxConstants.HIGHLIGHT_STROKEWIDTH / s / s; + this.marker.highlight.repaint(); + } + } + } + } +}; + +/** + * Function: isCellEnabled + * + * Returns true if the given cell allows new connections to be created. This implementation + * always returns true. + */ +mxConnectionHandler.prototype.isCellEnabled = function(cell) +{ + return true; +}; + +/** + * 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: snapToPreview + * + * Called to snap the given point to the current preview. This snaps to the + * first point of the preview if alt is not pressed. + */ +mxConnectionHandler.prototype.snapToPreview = function(me, point) +{ + if (!mxEvent.isAltDown(me.getEvent()) && this.previous != null) + { + var tol = this.graph.gridSize * this.graph.view.scale / 2; + var tmp = (this.sourceConstraint != null) ? this.first : + new mxPoint(this.previous.getCenterX(), this.previous.getCenterY()); + + if (Math.abs(tmp.x - me.getGraphX()) < tol) + { + point.x = tmp.x; + } + + if (Math.abs(tmp.y - me.getGraphY()) < tol) + { + point.y = tmp.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 (!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.currentState = null; + } + + var view = this.graph.getView(); + var scale = view.scale; + var tr = view.translate; + var point = new mxPoint(me.getGraphX(), me.getGraphY()); + this.error = null; + + if (this.graph.isGridEnabledEvent(me.getEvent())) + { + point = new mxPoint((this.graph.snap(point.x / scale - tr.x) + tr.x) * scale, + (this.graph.snap(point.y / scale - tr.y) + tr.y) * scale); + } + + this.snapToPreview(me, point); + this.currentPoint = point; + + if ((this.first != null || (this.isEnabled() && this.graph.isEnabled())) && + (this.shape != null || this.first == null || + Math.abs(me.getGraphX() - this.first.x) > this.graph.tolerance || + Math.abs(me.getGraphY() - this.first.y) > this.graph.tolerance)) + { + this.updateCurrentState(me, point); + } + + if (this.first != null) + { + 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(); + } + else if (this.previous != null && mxEvent.isShiftDown(me.getEvent()) && + !this.graph.isIgnoreTerminalEvent(me.getEvent())) + { + if (Math.abs(this.previous.getCenterX() - point.x) < + Math.abs(this.previous.getCenterY() - point.y)) + { + point.x = this.previous.getCenterX(); + } + else + { + point.y = this.previous.getCenterY(); + } + } + + 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.updateEdgeState(current, constraint); + 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; + } + + // Stores old point to reuse when creating edge + this.originalPoint = current.clone(); + current.x -= dx * 4 / len; + current.y -= dy * 4 / len; + } + else + { + this.originalPoint = null; + } + + // Creates the preview shape (lazy) + if (this.shape == null) + { + var dx = Math.abs(me.getGraphX() - this.first.x); + var dy = Math.abs(me.getGraphY() - this.first.y); + + if (dx > this.graph.tolerance || dy > this.graph.tolerance) + { + this.shape = this.createShape(); + + if (this.edgeState != null) + { + this.shape.apply(this.edgeState); + } + + // Revalidates current connection + this.updateCurrentState(me, point); + } + } + + // 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(); + } + + // Makes sure endpoint of edge is visible during connect + if (this.cursor != null) + { + this.graph.container.style.cursor = this.cursor; + } + + 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(); + + // Sets the cursor on the current shape + if (this.currentState != null && this.error == null && this.constraintHandler.currentConstraint == 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.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: updateEdgeState + * + * Updates . + */ +mxConnectionHandler.prototype.updateEdgeState = function(current, constraint) +{ + // TODO: Use generic method for writing constraint to style + if (this.sourceConstraint != null && this.sourceConstraint.point != null) + { + this.edgeState.style[mxConstants.STYLE_EXIT_X] = this.sourceConstraint.point.x; + this.edgeState.style[mxConstants.STYLE_EXIT_Y] = this.sourceConstraint.point.y; + } + + if (constraint != null && constraint.point != null) + { + this.edgeState.style[mxConstants.STYLE_ENTRY_X] = constraint.point.x; + this.edgeState.style[mxConstants.STYLE_ENTRY_Y] = constraint.point.y; + } + else + { + delete this.edgeState.style[mxConstants.STYLE_ENTRY_X]; + delete this.edgeState.style[mxConstants.STYLE_ENTRY_Y]; + } + + 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); +}; + +/** + * Function: getTargetPerimeterPoint + * + * Returns the perimeter point for the given target state. + * + * Parameters: + * + * state - that represents the target cell state. + * me - 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 - that represents the target cell state. + * next - that represents the next point along the previewed edge. + * me - that represents the mouse move. + */ +mxConnectionHandler.prototype.getSourcePerimeterPoint = function(state, next, me) +{ + var result = null; + var view = state.view; + var sourcePerimeter = view.getPerimeterFunction(state); + var c = new mxPoint(state.getCenterX(), state.getCenterY()); + + if (sourcePerimeter != null) + { + var theta = mxUtils.getValue(state.style, mxConstants.STYLE_ROTATION, 0); + var rad = -theta * (Math.PI / 180); + + if (theta != 0) + { + next = mxUtils.getRotatedPoint(new mxPoint(next.x, next.y), Math.cos(rad), Math.sin(rad), c); + } + + var tmp = sourcePerimeter(view.getPerimeterBounds(state), state, next, false); + + if (tmp != null) + { + if (theta != 0) + { + tmp = mxUtils.getRotatedPoint(new mxPoint(tmp.x, tmp.y), Math.cos(-rad), Math.sin(-rad), c); + } + + result = tmp; + } + } + else + { + result = c; + } + + return result; +}; + + +/** + * Function: updateIcons + * + * Hook to update the icon position(s) based on a mouseOver event. This is + * an empty implementation. + * + * Parameters: + * + * state - under the mouse. + * icons - Array of currently displayed icons. + * me - 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 is null. Note that this is only + * called if 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 . + */ +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: checkConstraints + * + * Returns true if the connection for the given constraints is valid. This + * implementation returns true if the constraints are not pointing to the + * same fixed connection point. + */ +mxConnectionHandler.prototype.checkConstraints = function(c1, c2) +{ + return (c1 == null || c2 == null || c1.point == null || c2.point == null || + !c1.point.equals(c2.point) || c1.dx != c2.dx || c1.dy != c2.dy || + c1.perimeter != c2.perimeter); +}; + +/** + * 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; + } + + var c1 = this.sourceConstraint; + var c2 = this.constraintHandler.currentConstraint; + + 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.currentState != null) + { + target = this.currentState.cell; + } + + // Inserts the edge if no validation error exists and if constraints differ + if (this.error == null && (source == null || target == null || + source != target || this.checkConstraints(c1, c2))) + { + 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, me.getEvent()); + } + + // Displays the error message if it is not an empty string, + // for empty error messages, the event is silently dropped + if (this.error != null && this.error.length > 0) + { + this.graph.validationAlert(this.error); + } + } + + // Redraws the connect icons and resets the handler state + this.destroyIcons(); + me.consume(); + } + + if (this.first != null) + { + this.reset(); + } +}; + +/** + * Function: reset + * + * Resets the state of this handler. + */ +mxConnectionHandler.prototype.reset = function() +{ + if (this.shape != null) + { + this.shape.destroy(); + this.shape = null; + } + + // Resets the cursor on the container + if (this.cursor != null && this.graph.container != null) + { + this.graph.container.style.cursor = ''; + } + + this.destroyIcons(); + this.marker.reset(); + this.constraintHandler.reset(); + this.originalPoint = null; + this.currentPoint = null; + this.edgeState = null; + this.previous = null; + this.error = null; + this.sourceConstraint = null; + this.mouseDownCounter = 0; + this.first = null; + + this.fireEvent(new mxEventObject(mxEvent.RESET)); +}; + +/** + * Function: drawPreview + * + * Redraws the preview edge using the color and width returned by + * and . + */ +mxConnectionHandler.prototype.drawPreview = function() +{ + this.updatePreview(this.error == null); + + if (this.edgeState != null) + { + this.edgeState.shape = this.shape; + this.graph.cellRenderer.postConfigureShape(this.edgeState); + this.edgeState.shape = null; + } + + this.shape.redraw(); +}; + +/** + * 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.updatePreview = function(valid) +{ + this.shape.strokewidth = this.getEdgeWidth(valid); + this.shape.stroke = this.getEdgeColor(valid); +}; + +/** + * 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 to create the edge. + * + * Parameters: + * + * source - that represents the source terminal. + * target - that represents the target terminal. + * evt - Mousedown event of the connect gesture. + * dropTarget - that represents the cell under the mouse when it was + * released. + */ +mxConnectionHandler.prototype.connect = function(source, target, evt, dropTarget) +{ + if (target != null || this.isCreateTarget(evt) || 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 terminalInserted = false; + var edge = null; + + model.beginUpdate(); + try + { + if (source != null && target == null && !this.graph.isIgnoreTerminalEvent(evt) && this.isCreateTarget(evt)) + { + target = this.createTargetVertex(evt, source); + + if (target != null) + { + dropTarget = this.graph.getDropTarget([target], evt, dropTarget); + terminalInserted = true; + + // 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(); + var refSource = this.graph.getReferenceTerminal(source); + var refTarget = this.graph.getReferenceTerminal(target); + var refParent = parent; + + if (refSource != null && refTarget != null) + { + refParent = model.getNearestCommonAncestor(refSource, refTarget); + } + else if (refSource != null) + { + refParent = model.getParent(refSource); + } + + if (refParent != null && !model.isEdge(refParent) && + refParent != model.getRoot()) + { + parent = refParent; + } + + // 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); + } + + // Inserts non-overlapping edge before source + if (this.isInsertBefore(edge, source, target, evt, dropTarget) && + (this.constraintHandler.currentConstraint == null || + this.constraintHandler.currentConstraint.perimeter)) + { + var tmp = source; + + while (tmp.parent != null && tmp.geometry != null && + tmp.geometry.relative && tmp.parent != edge.parent) + { + tmp = this.graph.model.getParent(tmp); + } + + if (tmp != null && tmp.parent != null && tmp.parent == edge.parent) + { + model.add(parent, edge, tmp.parent.getIndex(tmp)); + } + } + + // 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 t = this.graph.view.translate; + var s = this.graph.view.scale; + var pt = (this.originalPoint != null) ? + new mxPoint(this.originalPoint.x / s - t.x, this.originalPoint.y / s - t.y) : + new mxPoint(this.currentPoint.x / s - t.x, this.currentPoint.y / s - t.y); + pt.x -= this.graph.panDx / this.graph.view.scale; + pt.y -= this.graph.panDy / this.graph.view.scale; + + var pstate = this.graph.getView().getState(model.getParent(edge)) + + if (pstate != null) + { + pt.x -= pstate.origin.x; + pt.y -= pstate.origin.y; + } + + geo.setTerminalPoint(pt, false); + } + + this.fireEvent(new mxEventObject(mxEvent.CONNECT, 'cell', edge, 'terminal', target, + 'event', evt, 'target', dropTarget, 'terminalInserted', terminalInserted)); + } + } + catch (e) + { + mxLog.show(); + mxLog.debug(e.message); + } + finally + { + model.endUpdate(); + } + + if (this.select) + { + this.selectCells(edge, (terminalInserted) ? target : null); + } + } +}; + +/** + * 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 if is defined, + * otherwise 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 is true and + * returns null. + * + * Parameters: + * + * evt - Mousedown event of the connect gesture. + * source - 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.cloneCell(source); + var geo = this.graph.getModel().getGeometry(clone); + + if (geo != null) + { + var t = this.graph.view.translate; + var s = this.graph.view.scale; + var point = new mxPoint(this.currentPoint.x / s - t.x, this.currentPoint.y / s - t.y); + geo.x = Math.round(point.x - geo.width / 2 - this.graph.panDx / s); + geo.y = Math.round(point.y - geo.height / 2 - this.graph.panDy / s); + + // Aligns with source if within certain tolerance + var tol = this.getAlignmentTolerance(); + + if (tol > 0) + { + var sourceState = this.graph.view.getState(source); + + if (sourceState != null) + { + var x = sourceState.x / s - t.x; + var y = sourceState.y / s - t.y; + + if (Math.abs(x - geo.x) <= tol) + { + geo.x = Math.round(x); + } + + if (Math.abs(y - geo.y) <= tol) + { + geo.y = Math.round(y); + } + } + } + } + + return clone; +}; + +/** + * Function: getAlignmentTolerance + * + * Returns the tolerance for aligning new targets to sources. This returns the grid size / 2. + */ +mxConnectionHandler.prototype.getAlignmentTolerance = function(evt) +{ + return (this.graph.isGridEnabled()) ? this.graph.gridSize / 2 : this.graph.tolerance; +}; + +/** + * Function: createEdge + * + * Creates and returns a new edge using 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 - that represents the source terminal. + * target - 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 . + */ +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; + } + + if (this.escapeHandler != null) + { + this.graph.removeListener(this.escapeHandler); + this.escapeHandler = null; + } +}; diff --git a/src/main/mxgraph/handler/mxConstraintHandler.js b/src/main/mxgraph/handler/mxConstraintHandler.js new file mode 100644 index 000000000..e62b17124 --- /dev/null +++ b/src/main/mxgraph/handler/mxConstraintHandler.js @@ -0,0 +1,505 @@ +/** + * Copyright (c) 2006-2015, JGraph Ltd + * Copyright (c) 2006-2015, Gaudenz Alder + */ +/** + * 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 . + * factoryMethod - Optional function to create the edge. The function takes + * the source and target as the first and second argument and + * returns the that represents the new edge. + */ +function mxConstraintHandler(graph) +{ + this.graph = graph; + + // Adds a graph model listener to update the current focus on changes + this.resetHandler = mxUtils.bind(this, function(sender, evt) + { + if (this.currentFocus != null && this.graph.view.getState(this.currentFocus.cell) == null) + { + this.reset(); + } + else + { + this.redraw(); + } + }); + + this.graph.model.addListener(mxEvent.CHANGE, this.resetHandler); + this.graph.view.addListener(mxEvent.SCALE_AND_TRANSLATE, this.resetHandler); + this.graph.view.addListener(mxEvent.TRANSLATE, this.resetHandler); + this.graph.view.addListener(mxEvent.SCALE, this.resetHandler); + this.graph.addListener(mxEvent.ROOT, this.resetHandler); +}; + +/** + * Variable: pointImage + * + * 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 . + */ +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 . + */ +mxConstraintHandler.prototype.highlightColor = mxConstants.DEFAULT_VALID_COLOR; + +/** + * Function: isEnabled + * + * Returns true if events are handled. This implementation + * returns . + */ +mxConstraintHandler.prototype.isEnabled = function() +{ + return this.enabled; +}; + +/** + * Function: setEnabled + * + * Enables or disables event handling. This implementation + * updates . + * + * 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. This + * implementation returns . + * + * Parameters: + * + * me - whose tolerance should be returned. + */ +mxConstraintHandler.prototype.getTolerance = function(me) +{ + 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 should be ignored in . This + * implementation always returns false. + */ +mxConstraintHandler.prototype.isEventIgnored = function(me, source) +{ + return false; +}; + +/** + * Function: isStateIgnored + * + * Returns true if the given state should be ignored. This always returns false. + */ +mxConstraintHandler.prototype.isStateIgnored = function(state, source) +{ + return false; +}; + +/** + * Function: destroyIcons + * + * Destroys the if they exist. + */ +mxConstraintHandler.prototype.destroyIcons = function() +{ + if (this.focusIcons != null) + { + for (var i = 0; i < this.focusIcons.length; i++) + { + this.focusIcons[i].destroy(); + } + + this.focusIcons = null; + this.focusPoints = null; + } +}; + +/** + * Function: destroyFocusHighlight + * + * Destroys the if one exists. + */ +mxConstraintHandler.prototype.destroyFocusHighlight = function() +{ + if (this.focusHighlight != null) + { + this.focusHighlight.destroy(); + this.focusHighlight = null; + } +}; + +/** + * Function: isKeepFocusEvent + * + * Returns true if the current focused state should not be changed for the given event. + * This returns true if shift is pressed and alt is not pressed. + */ +mxConstraintHandler.prototype.isKeepFocusEvent = function(me) +{ + return mxEvent.isShiftDown(me.getEvent()) && !mxEvent.isAltDown(me.getEvent()); +}; + +/** + * Function: getCellForEvent + * + * Returns the cell for the given event. + */ +mxConstraintHandler.prototype.getCellForEvent = function(me, point) +{ + var cell = me.getCell(); + + // Gets cell under actual point if different from event location + if (cell == null && point != null && (me.getGraphX() != point.x || me.getGraphY() != point.y)) + { + cell = this.graph.getCellAt(point.x, point.y); + } + + // Uses connectable parent vertex if one exists + if (cell != null && !this.graph.isCellConnectable(cell)) + { + var parent = this.graph.getModel().getParent(cell); + + if (this.graph.getModel().isVertex(parent) && this.graph.isCellConnectable(parent)) + { + cell = parent; + } + } + + return (this.graph.isCellLocked(cell)) ? null : cell; +}; + +/** + * Function: update + * + * Updates the state of this handler based on the given . + * Source is a boolean indicating if the cell is a source or target. + */ +mxConstraintHandler.prototype.update = function(me, source, existingEdge, point) +{ + if (this.isEnabled() && !this.isEventIgnored(me)) + { + // Lazy installation of mouseleave handler + if (this.mouseleaveHandler == null && this.graph.container != null) + { + this.mouseleaveHandler = mxUtils.bind(this, function() + { + this.reset(); + }); + + mxEvent.addListener(this.graph.container, 'mouseleave', this.resetHandler); + } + + var tol = this.getTolerance(me); + var x = (point != null) ? point.x : me.getGraphX(); + var y = (point != null) ? point.y : me.getGraphY(); + var grid = new mxRectangle(x - tol, y - tol, 2 * tol, 2 * tol); + var mouse = new mxRectangle(me.getGraphX() - tol, me.getGraphY() - tol, 2 * tol, 2 * tol); + var state = this.graph.view.getState(this.getCellForEvent(me, point)); + + // Keeps focus icons visible while over vertex bounds and no other cell under mouse or shift is pressed + if (!this.isKeepFocusEvent(me) && (this.currentFocusArea == null || this.currentFocus == null || + (state != null) || !this.graph.getModel().isVertex(this.currentFocus.cell) || + !mxUtils.intersects(this.currentFocusArea, mouse)) && (state != this.currentFocus)) + { + this.currentFocusArea = null; + this.currentFocus = null; + this.setFocus(me, state, source); + } + + this.currentConstraint = null; + this.currentPoint = null; + var minDistSq = null; + + if (this.focusIcons != null && this.constraints != null && + (state == null || this.currentFocus == state)) + { + var cx = mouse.getCenterX(); + var cy = mouse.getCenterY(); + + for (var i = 0; i < this.focusIcons.length; i++) + { + var dx = cx - this.focusIcons[i].bounds.getCenterX(); + var dy = cy - this.focusIcons[i].bounds.getCenterY(); + var tmp = dx * dx + dy * dy; + + if ((this.intersects(this.focusIcons[i], mouse, source, existingEdge) || (point != null && + this.intersects(this.focusIcons[i], grid, source, existingEdge))) && + (minDistSq == null || tmp < minDistSq)) + { + this.currentConstraint = this.constraints[i]; + this.currentPoint = this.focusPoints[i]; + minDistSq = tmp; + + var tmp = this.focusIcons[i].bounds.clone(); + tmp.grow(mxConstants.HIGHLIGHT_SIZE + 1); + tmp.width -= 1; + tmp.height -= 1; + + if (this.focusHighlight == null) + { + var hl = this.createHighlightShape(); + hl.dialect = mxConstants.DIALECT_SVG; + hl.pointerEvents = false; + + hl.init(this.graph.getView().getOverlayPane()); + this.focusHighlight = hl; + + var getState = mxUtils.bind(this, function() + { + return (this.currentFocus != null) ? this.currentFocus : state; + }); + + mxEvent.redirectMouseEvents(hl.node, this.graph, getState); + } + + this.focusHighlight.bounds = tmp; + this.focusHighlight.redraw(); + } + } + } + + if (this.currentConstraint == null) + { + this.destroyFocusHighlight(); + } + } + else + { + this.currentConstraint = null; + this.currentFocus = null; + this.currentPoint = null; + } +}; + +/** + * Function: redraw + * + * Transfers the focus to the given state as a source or target terminal. If + * the handler is not enabled then the outline is painted, but the constraints + * are ignored. + */ +mxConstraintHandler.prototype.redraw = function() +{ + if (this.currentFocus != null && this.constraints != null && this.focusIcons != null) + { + var state = this.graph.view.getState(this.currentFocus.cell); + this.currentFocus = state; + this.currentFocusArea = new mxRectangle(state.x, state.y, state.width, state.height); + + for (var i = 0; i < this.constraints.length; i++) + { + var cp = this.graph.getConnectionPoint(state, this.constraints[i]); + var img = this.getImageForConstraint(state, this.constraints[i], cp); + + var bounds = new mxRectangle(Math.round(cp.x - img.width / 2), + Math.round(cp.y - img.height / 2), img.width, img.height); + this.focusIcons[i].bounds = bounds; + this.focusIcons[i].redraw(); + this.currentFocusArea.add(this.focusIcons[i].bounds); + this.focusPoints[i] = cp; + } + } +}; + +/** + * Function: setFocus + * + * Transfers the focus to the given state as a source or target terminal. If + * the handler is not enabled then the outline is painted, but the constraints + * are ignored. + */ +mxConstraintHandler.prototype.setFocus = function(me, state, source) +{ + this.constraints = (state != null && !this.isStateIgnored(state, source) && + this.graph.isCellConnectable(state.cell)) ? ((this.isEnabled()) ? + (this.graph.getAllConnectionConstraints(state, source) || []) : []) : null; + + // Only uses cells which have constraints + if (this.constraints != null) + { + this.currentFocus = state; + this.currentFocusArea = new mxRectangle(state.x, state.y, state.width, state.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.focusPoints = []; + this.focusIcons = []; + + for (var i = 0; i < this.constraints.length; i++) + { + var cp = this.graph.getConnectionPoint(state, this.constraints[i]); + var img = this.getImageForConstraint(state, this.constraints[i], cp); + + var src = img.src; + var bounds = new mxRectangle(Math.round(cp.x - img.width / 2), + Math.round(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_MIXEDHTML : mxConstants.DIALECT_SVG; + icon.preserveImageAspect = false; + icon.init(this.graph.getView().getDecoratorPane()); + + // 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 : state; + }); + + 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(this.getTolerance(me)); + } + else + { + this.destroyIcons(); + this.destroyFocusHighlight(); + } +}; + +/** + * Function: createHighlightShape + * + * Create the shape used to paint the highlight. + * + * Returns true if the given icon intersects the given point. + */ +mxConstraintHandler.prototype.createHighlightShape = function() +{ + var hl = new mxRectangleShape(null, this.highlightColor, this.highlightColor, mxConstants.HIGHLIGHT_STROKEWIDTH); + hl.opacity = mxConstants.HIGHLIGHT_OPACITY; + + return hl; +}; + +/** + * Function: intersects + * + * Returns true if the given icon intersects the given rectangle. + */ +mxConstraintHandler.prototype.intersects = function(icon, mouse, source, existingEdge) +{ + return mxUtils.intersects(icon.bounds, mouse); +}; + +/** + * Function: destroy + * + * Destroy this handler. + */ +mxConstraintHandler.prototype.destroy = function() +{ + this.reset(); + + if (this.resetHandler != null) + { + this.graph.model.removeListener(this.resetHandler); + this.graph.view.removeListener(this.resetHandler); + this.graph.removeListener(this.resetHandler); + this.resetHandler = null; + } + + if (this.mouseleaveHandler != null && this.graph.container != null) + { + mxEvent.removeListener(this.graph.container, 'mouseleave', this.mouseleaveHandler); + this.mouseleaveHandler = null; + } +}; diff --git a/src/main/mxgraph/handler/mxEdgeHandler.js b/src/main/mxgraph/handler/mxEdgeHandler.js new file mode 100644 index 000000000..a0171f82c --- /dev/null +++ b/src/main/mxgraph/handler/mxEdgeHandler.js @@ -0,0 +1,2688 @@ +/** + * Copyright (c) 2006-2015, JGraph Ltd + * Copyright (c) 2006-2015, Gaudenz Alder + */ +/** + * Class: mxEdgeHandler + * + * Graph event handler that reconnects edges and modifies control points and + * the edge label location. Uses for finding and + * highlighting new source and target vertices. This handler is automatically + * created in 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 . + * + * Parameters: + * + * state - of the cell to be handled. + */ +function mxEdgeHandler(state) +{ + if (state != null && state.shape != null) + { + this.state = state; + this.init(); + + // Handles escape keystrokes + this.escapeHandler = mxUtils.bind(this, function(sender, evt) + { + var dirty = this.index != null; + this.reset(); + + if (dirty) + { + this.graph.cellRenderer.redraw(this.state, false, state.view.isRendering()); + } + }); + + this.state.view.graph.addListener(mxEvent.ESCAPE, this.escapeHandler); + } +}; + +/** + * Variable: graph + * + * Reference to the enclosing . + */ +mxEdgeHandler.prototype.graph = null; + +/** + * Variable: state + * + * Reference to the being modified. + */ +mxEdgeHandler.prototype.state = null; + +/** + * Variable: marker + * + * Holds the which is used for highlighting terminals. + */ +mxEdgeHandler.prototype.marker = null; + +/** + * Variable: constraintHandler + * + * Holds the 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 that represents the preview edge. + */ +mxEdgeHandler.prototype.shape = null; + +/** + * Variable: bends + * + * Holds the that represent the points. + */ +mxEdgeHandler.prototype.bends = null; + +/** + * Variable: labelShape + * + * Holds the 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: dblClickRemoveEnabled + * + * Specifies if removing bends by double click is enabled. Default is false. + */ +mxEdgeHandler.prototype.dblClickRemoveEnabled = false; + +/** + * Variable: mergeRemoveEnabled + * + * Specifies if removing bends by dropping them on other bends is enabled. + * Default is false. + */ +mxEdgeHandler.prototype.mergeRemoveEnabled = false; + +/** + * Variable: straightRemoveEnabled + * + * Specifies if removing bends by creating straight segments should be enabled. + * If enabled, this can be overridden by holding down the alt key while moving. + * Default is false. + */ +mxEdgeHandler.prototype.straightRemoveEnabled = false; + +/** + * Variable: virtualBendsEnabled + * + * Specifies if virtual bends should be added in the center of each + * segments. These bends can then be used to add new waypoints. + * Default is false. + */ +mxEdgeHandler.prototype.virtualBendsEnabled = false; + +/** + * Variable: virtualBendOpacity + * + * Opacity to be used for virtual bends (see ). + * Default is 40. + */ +mxEdgeHandler.prototype.virtualBendOpacity = 40; + +/** + * Variable: parentHighlightEnabled + * + * Specifies if the parent should be highlighted if a child cell is selected. + * Default is false. + */ +mxEdgeHandler.prototype.parentHighlightEnabled = false; + +/** + * Variable: preferHtml + * + * Specifies if bends should be added to the graph container. This is updated + * in 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: handleImage + * + * Optional to be used as handles. Default is null. + */ +mxEdgeHandler.prototype.handleImage = null; + +/** + * Variable: tolerance + * + * Optional tolerance for hit-detection in . Default is 0. + */ +mxEdgeHandler.prototype.tolerance = 0; + +/** + * Variable: outlineConnect + * + * Specifies if connections to the outline of a highlighted target should be + * enabled. This will allow to place the connection point along the outline of + * the highlighted target. Default is false. + */ +mxEdgeHandler.prototype.outlineConnect = false; + +/** + * Variable: manageLabelHandle + * + * Specifies if the label handle should be moved if it intersects with another + * handle. Uses for checking and moving. Default is false. + */ +mxEdgeHandler.prototype.manageLabelHandle = false; + +/** + * Function: init + * + * Initializes the shapes required for this edge handler. + */ +mxEdgeHandler.prototype.init = function() +{ + this.graph = this.state.view.graph; + this.marker = this.createMarker(); + + // 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_MIXEDHTML : mxConstants.DIALECT_SVG; + this.shape.init(this.graph.getView().getOverlayPane()); + this.shape.svgStrokeTolerance = 0; + this.shape.pointerEvents = false; + mxEvent.redirectMouseEvents(this.shape.node, this.graph, this.state); + + if (this.graph.isCellMovable(this.state.cell)) + { + this.shape.setCursor(mxConstants.CURSOR_MOVABLE_EDGE); + } + + // 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; + } + } + } + + this.updateParentHighlight(); + this.refresh(); + this.redraw(); +}; + +/** + * Function: createLabelShape + * + * Creates, initializes and returns the label shape. + */ +mxEdgeHandler.prototype.createLabelShape = function() +{ + var shape = this.createLabelHandleShape(); + this.initBend(shape); + + return shape; +}; + +/** + * Function: getConstraintHandler + * + * Returns the constraint handler. This implementation creates a new + * if one does not yet exist. + */ +mxEdgeHandler.prototype.getConstraintHandler = function() +{ + if (this.constraintHandler == null) + { + this.constraintHandler = this.createConstraintHandler(); + } + + return this.constraintHandler; +}; + +/** + * Function: createConstraintHandler + * + * Creates and returns a new for this handler. + */ +mxEdgeHandler.prototype.createConstraintHandler = function() +{ + return new mxConstraintHandler(this.graph); +}; + +/** + * Function: isParentHighlightVisible + * + * Returns true if the parent highlight should be visible. This implementation + * always returns true. + */ +mxEdgeHandler.prototype.isParentHighlightVisible = mxVertexHandler.prototype.isParentHighlightVisible; + +/** + * Function: destroyParentHighlight + * + * Destroys the parent highlight. + */ +mxEdgeHandler.prototype.destroyParentHighlight = mxVertexHandler.prototype.destroyParentHighlight; + +/** + * Function: updateParentHighlight + * + * Updates the highlight of the parent if is true. + */ +mxEdgeHandler.prototype.updateParentHighlight = mxVertexHandler.prototype.updateParentHighlight; + +/** + * Function: createCustomHandles + * + * Returns an array of custom handles. This implementation returns null. + */ +mxEdgeHandler.prototype.createCustomHandles = function() +{ + return null; +}; + +/** + * Function: isVirtualBendsEnabled + * + * Returns true if virtual bends should be added. This returns true if + * is true and the current style allows and + * renders custom waypoints. + */ +mxEdgeHandler.prototype.isVirtualBendsEnabled = function(evt) +{ + return this.virtualBendsEnabled && (this.state.style[mxConstants.STYLE_EDGE] == null || + this.state.style[mxConstants.STYLE_EDGE] == mxConstants.NONE || + this.state.style[mxConstants.STYLE_NOEDGESTYLE] == 1) && + mxUtils.getValue(this.state.style, mxConstants.STYLE_SHAPE, null) != 'arrow'; +}; + +/** + * Function: isCellEnabled + * + * Returns true if the given cell allows new connections to be created. This implementation + * always returns true. + */ +mxEdgeHandler.prototype.isCellEnabled = function(cell) +{ + return true; +}; + +/** + * 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: createParentHighlightShape + * + * Creates the shape used to draw the selection border. + */ +mxEdgeHandler.prototype.createParentHighlightShape = function(bounds) +{ + var shape = new mxRectangleShape(mxRectangle.fromRectangle(bounds), + null, this.getSelectionColor()); + shape.strokewidth = this.getSelectionStrokeWidth(); + shape.isDashed = this.isSelectionDashed(); + + return shape; +}; + +/** + * Function: createSelectionShape + * + * Creates the shape used to draw the selection border. + */ +mxEdgeHandler.prototype.createSelectionShape = function(points) +{ + var shape = new this.state.shape.constructor(); + shape.outline = true; + shape.apply(this.state); + + shape.isDashed = this.isSelectionDashed(); + shape.stroke = this.getSelectionColor(); + shape.isShadow = false; + + return shape; +}; + +/** + * Function: getSelectionColor + * + * Returns . + */ +mxEdgeHandler.prototype.getSelectionColor = function() +{ + return (this.graph.isCellEditable(this.state.cell)) ? + mxConstants.EDGE_SELECTION_COLOR : + mxConstants.LOCKED_HANDLE_FILLCOLOR; +}; + +/** + * Function: getSelectionStrokeWidth + * + * Returns . + */ +mxEdgeHandler.prototype.getSelectionStrokeWidth = function() +{ + return mxConstants.EDGE_SELECTION_STROKEWIDTH; +}; + +/** + * Function: isSelectionDashed + * + * Returns . + */ +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: getCellAt + * + * Creates and returns the used in . + */ +mxEdgeHandler.prototype.getCellAt = function(x, y) +{ + return (!this.outlineConnect) ? this.graph.getCellAt(x, y) : null; +}; + +/** + * Function: createMarker + * + * Creates and returns the used in . + */ +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); + + // Checks for cell at preview point (with grid) + if ((cell == self.state.cell || cell == null) && self.currentPoint != null) + { + cell = self.graph.getCellAt(self.currentPoint.x, self.currentPoint.y); + } + + // Uses connectable parent vertex if one exists + if (cell != null && !this.graph.isCellConnectable(cell)) + { + var parent = this.graph.getModel().getParent(cell); + + if (this.graph.getModel().isVertex(parent) && this.graph.isCellConnectable(parent)) + { + cell = parent; + } + } + + var model = self.graph.getModel(); + + if ((this.graph.isSwimlane(cell) && self.currentPoint != null && + this.graph.hitsSwimlaneContent(cell, self.currentPoint.x, self.currentPoint.y)) || + (!self.isConnectableCell(cell)) || (cell == self.state.cell || + (cell != null && !self.graph.connectableEdges && model.isEdge(cell))) || + model.isAncestor(self.state.cell, cell)) + { + cell = null; + } + + if (!this.graph.isCellConnectable(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 . + * + * Parameters: + * + * source - that represents the source terminal. + * target - 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 . + */ + mxEdgeHandler.prototype.createBends = function() + { + var cell = this.state.cell; + var bends = []; + + if (this.abspoints != null) + { + 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)) + { + (mxUtils.bind(this, function(index) + { + var bend = this.createHandleShape(index, null, index == this.abspoints.length - 1); + this.initBend(bend, mxUtils.bind(this, mxUtils.bind(this, function() + { + if (this.dblClickRemoveEnabled) + { + this.removePoint(this.state, index); + } + }))); + + if (this.isHandleEnabled(i)) + { + bend.setCursor((terminal) ? mxConstants.CURSOR_TERMINAL_HANDLE : mxConstants.CURSOR_BEND_HANDLE); + } + + bends.push(bend); + + if (!terminal) + { + this.points.push(new mxPoint(0,0)); + bend.node.style.visibility = 'hidden'; + } + }))(i); + } + } + } + } + + return bends; +}; + +/** + * Function: createVirtualBends + * + * Creates and returns the bends used for modifying the edge. This is + * typically an array of . + */ + mxEdgeHandler.prototype.createVirtualBends = function() + { + var bends = []; + + if (this.abspoints != null && this.abspoints.length > 0 && + this.graph.isCellBendable(this.state.cell)) + { + for (var i = 1; i < this.abspoints.length; i++) + { + (mxUtils.bind(this, function(bend) + { + this.initBend(bend); + bend.setCursor(mxConstants.CURSOR_VIRTUAL_BEND_HANDLE); + bends.push(bend); + }))(this.createHandleShape()); + } + } + + 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) +{ + var source = this.state.getVisibleTerminalState(true); + var target = this.state.getVisibleTerminalState(false); + var geo = this.graph.getCellGeometry(this.state.cell); + var edgeStyle = (geo != null) ? this.graph.view.getEdgeStyle(this.state, geo.points, source, target) : null; + + return edgeStyle != mxEdgeStyle.EntityRelation || index == 0 || index == this.abspoints.length - 1; +}; + +/** + * 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 + * . Only images and rectangles should be + * returned if support for HTML labels with not foreign objects is required. + * Index if null for virtual handles. + */ +mxEdgeHandler.prototype.createHandleShape = function(index) +{ + if (this.handleImage != null) + { + var shape = new mxImageShape(new mxRectangle(0, 0, this.handleImage.width, this.handleImage.height), this.handleImage.src); + + // Allows HTML rendering of the images + shape.preserveImageAspect = false; + + return shape; + } + 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: createLabelHandleShape + * + * Creates the shape used to display the the label handle. + */ +mxEdgeHandler.prototype.createLabelHandleShape = function() +{ + if (this.labelHandleImage != null) + { + var shape = new mxImageShape(new mxRectangle(0, 0, this.labelHandleImage.width, this.labelHandleImage.height), this.labelHandleImage.src); + + // Allows HTML rendering of the images + shape.preserveImageAspect = false; + + return shape; + } + else + { + var s = mxConstants.LABEL_HANDLE_SIZE; + return new mxRectangleShape(new mxRectangle(0, 0, s, s), mxConstants.LABEL_HANDLE_FILLCOLOR, mxConstants.HANDLE_STROKECOLOR); + } +}; + +/** + * Function: initBend + * + * Helper method to initialize the given bend. + * + * Parameters: + * + * bend - that represents the bend to be initialized. + */ +mxEdgeHandler.prototype.initBend = function(bend, dblClick) +{ + if (this.preferHtml) + { + bend.dialect = mxConstants.DIALECT_STRICTHTML; + bend.init(this.graph.container); + } + else + { + bend.dialect = (this.graph.dialect != mxConstants.DIALECT_SVG) ? + mxConstants.DIALECT_MIXEDHTML : mxConstants.DIALECT_SVG; + bend.init(this.graph.getView().getOverlayPane()); + } + + mxEvent.redirectMouseEvents(bend.node, this.graph, this.state, + null, null, null, dblClick); + + if (mxClient.IS_TOUCH) + { + bend.node.setAttribute('pointer-events', 'none'); + } +}; + +/** + * Function: getHandleForEvent + * + * Returns the index of the handle for the given event. + */ +mxEdgeHandler.prototype.getHandleForEvent = function(me) +{ + var result = null; + + if (this.state != null) + { + // Connection highlight may consume events before they reach sizer handle + var tol = (!mxEvent.isMouseEvent(me.getEvent())) ? 2 * this.tolerance : 0; + var hit = (!this.allowHandleBoundsCheck) ? null : + new mxRectangle(me.getGraphX() - tol, me.getGraphY() - tol, tol, tol); + var minDistSq = null; + + function checkShape(shape) + { + if (shape != null && (me.isSource(shape) || + shape.intersectsRectangle(hit))) + { + var dx = me.getGraphX() - shape.bounds.getCenterX(); + var dy = me.getGraphY() - shape.bounds.getCenterY(); + var tmp = dx * dx + dy * dy; + + if (minDistSq == null || tmp <= minDistSq) + { + minDistSq = tmp; + + return true; + } + } + + return false; + } + + if (this.customHandles != null && this.isCustomHandleEvent(me)) + { + // Inverse loop order to match display order + for (var i = this.customHandles.length - 1; i >= 0; i--) + { + if (checkShape(this.customHandles[i].shape)) + { + // LATER: Return reference to active shape + return mxEvent.CUSTOM_HANDLE - i; + } + } + } + + if (this.state.text != null && (me.isSource(this.state.text) || + checkShape(this.labelShape))) + { + result = mxEvent.LABEL_HANDLE; + } + + if (this.bends != null) + { + for (var i = 0; i < this.bends.length; i++) + { + if (checkShape(this.bends[i])) + { + result = i; + } + } + } + + if (this.virtualBends != null && this.isAddVirtualBendEvent(me)) + { + for (var i = 0; i < this.virtualBends.length; i++) + { + if (checkShape(this.virtualBends[i])) + { + result = mxEvent.VIRTUAL_HANDLE - i; + } + } + } + } + + return result; +}; + +/** + * Function: isAddVirtualBendEvent + * + * Returns true if the given event allows virtual bends to be added. This + * implementation returns true. + */ +mxEdgeHandler.prototype.isAddVirtualBendEvent = function(me) +{ + return true; +}; + +/** + * Function: isCustomHandleEvent + * + * Returns true if the given event allows custom handles to be changed. This + * implementation returns true. + */ +mxEdgeHandler.prototype.isCustomHandleEvent = function(me) +{ + return true; +}; + +/** + * 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 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) +{ + if (this.graph.isCellEditable(this.state.cell)) + { + var handle = this.getHandleForEvent(me); + + if (this.bends != null && this.bends[handle] != null) + { + var b = this.bends[handle].bounds; + this.snapPoint = new mxPoint(b.getCenterX(), b.getCenterY()); + } + + if (this.addEnabled && handle == null && this.isAddPointEvent(me.getEvent())) + { + this.addPoint(this.state, me.getEvent()); + me.consume(); + } + else if (handle != null && !me.isConsumed() && this.graph.isEnabled()) + { + if (this.removeEnabled && this.isRemovePointEvent(me.getEvent())) + { + this.removePoint(this.state, handle); + } + else if (handle != mxEvent.LABEL_HANDLE || this.graph.isLabelMovable(me.getCell())) + { + if (handle <= mxEvent.VIRTUAL_HANDLE) + { + mxUtils.setOpacity(this.virtualBends[mxEvent.VIRTUAL_HANDLE - handle].node, 100); + } + + this.mouseDownX = me.getX(); + this.mouseDownY = me.getY(); + this.handle = handle; + } + + if (!mxEvent.isControlDown(me.getEvent()) && + !mxEvent.isShiftDown(me.getEvent())) + { + 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; + } + + // Hides other custom handles + if (this.index <= mxEvent.CUSTOM_HANDLE && this.index > mxEvent.VIRTUAL_HANDLE) + { + if (this.customHandles != null) + { + for (var i = 0; i < this.customHandles.length; i++) + { + if (i != mxEvent.CUSTOM_HANDLE - this.index) + { + this.customHandles[i].setVisible(false); + } + } + } + } +}; + +/** + * 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 2. + */ +mxEdgeHandler.prototype.getSnapToTerminalTolerance = function() +{ + return 2; +}; + +/** + * Function: updateHint + * + * Hook for subclassers do show details while the handler is active. + */ +mxEdgeHandler.prototype.updateHint = function(me, point) { }; + +/** + * Function: removeHint + * + * Hooks for subclassers to hide details when the handler gets inactive. + */ +mxEdgeHandler.prototype.removeHint = function() { }; + +/** + * Function: roundLength + * + * Hook for rounding the unscaled width or height. This uses Math.round. + */ +mxEdgeHandler.prototype.roundLength = function(length) +{ + return Math.round(length); +}; + +/** + * Function: isSnapToTerminalsEvent + * + * Returns true if is true and if alt is not pressed. + */ +mxEdgeHandler.prototype.isSnapToTerminalsEvent = function(me) +{ + return this.snapToTerminals && !mxEvent.isAltDown(me.getEvent()); +}; + +/** + * Function: getPointForEvent + * + * Returns the point for the given event. + */ +mxEdgeHandler.prototype.getPointForEvent = function(me) +{ + var view = this.graph.getView(); + var scale = view.scale; + var point = new mxPoint(this.roundLength(me.getGraphX() / scale) * scale, + this.roundLength(me.getGraphY() / scale) * scale); + + var tt = this.getSnapToTerminalTolerance(); + var overrideX = false; + var overrideY = false; + + if (tt > 0 && this.isSnapToTerminalsEvent(me)) + { + 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; + } + } + } + + 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)); + var pts = this.state.absolutePoints; + + if (pts != null) + { + for (var i = 0; i < pts.length; i++) + { + if ((i > 0 || !this.state.isFloatingTerminalPoint(true)) && + (i < pts.length - 1 || !this.state.isFloatingTerminalPoint(false))) + { + snapToPoint.call(this, this.state.absolutePoints[i]); + } + } + } + } + + if (this.graph.isGridEnabledEvent(me.getEvent())) + { + 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) +{ + var constraintHandler = this.getConstraintHandler(); + constraintHandler.update(me, this.isSource, true, me.isSource(this.marker.highlight.shape) ? null : this.currentPoint); + + if (constraintHandler.currentFocus != null && constraintHandler.currentConstraint != null) + { + // Handles special case where grid is large and connection point is at actual point in which + // case the outline is not followed as long as we're < gridSize / 2 away from that point + if (this.marker.highlight != null && this.marker.highlight.state != null && + this.marker.highlight.state.cell == constraintHandler.currentFocus.cell) + { + // Direct repaint needed if cell already highlighted + if (this.marker.highlight.shape.stroke != 'transparent') + { + this.marker.highlight.shape.stroke = 'transparent'; + this.marker.highlight.repaint(); + } + } + else + { + this.marker.markCell(constraintHandler.currentFocus.cell, 'transparent'); + } + + var model = this.graph.getModel(); + var other = this.graph.view.getTerminalPort(this.state, + this.graph.view.getState(model.getTerminal(this.state.cell, + !this.isSource)), !this.isSource); + var otherCell = (other != null) ? other.cell : null; + var source = (this.isSource) ? constraintHandler.currentFocus.cell : otherCell; + var target = (this.isSource) ? otherCell : constraintHandler.currentFocus.cell; + + // Updates the error message of the handler + this.error = this.validateConnection(source, target); + var result = null; + + if (this.error == null) + { + result = constraintHandler.currentFocus; + } + + if (this.error != null || (result != null && + !this.isCellEnabled(result.cell))) + { + constraintHandler.reset(); + } + + return result; + } + else if (!this.graph.isIgnoreTerminalEvent(me.getEvent())) + { + this.marker.process(me); + var state = this.marker.getValidState(); + + if (state != null && !this.isCellEnabled(state.cell)) + { + constraintHandler.reset(); + this.marker.reset(); + } + + return this.marker.getValidState(); + } + else + { + this.marker.reset(); + + return null; + } +}; + +/** + * Function: getPreviewPoints + * + * Updates the given preview state taking into account the state of the constraint handler. + * + * Parameters: + * + * pt - that contains the current pointer position. + * me - Optional that contains the current event. + */ +mxEdgeHandler.prototype.getPreviewPoints = function(pt, me) +{ + var geometry = this.graph.getCellGeometry(this.state.cell); + var points = (geometry.points != null) ? geometry.points.slice() : null; + var point = new mxPoint(pt.x, pt.y); + var result = null; + + if (!this.isSource && !this.isTarget) + { + this.convertPoint(point, false); + + if (points == null) + { + points = [point]; + } + else + { + // Adds point from virtual bend + if (this.index <= mxEvent.VIRTUAL_HANDLE) + { + points.splice(mxEvent.VIRTUAL_HANDLE - this.index, 0, point); + } + + // Removes point if dragged on terminal point + if (!this.isSource && !this.isTarget) + { + for (var i = 0; i < this.bends.length; i++) + { + if (i != this.index) + { + var bend = this.bends[i]; + + if (bend != null && mxUtils.contains(bend.bounds, pt.x, pt.y)) + { + if (this.index <= mxEvent.VIRTUAL_HANDLE) + { + points.splice(mxEvent.VIRTUAL_HANDLE - this.index, 1); + } + else + { + points.splice(this.index - 1, 1); + } + + result = points; + } + } + } + + // Removes point if user tries to straighten a segment + if (result == null && this.straightRemoveEnabled && (me == null || !mxEvent.isAltDown(me.getEvent()))) + { + var tol = this.graph.tolerance * this.graph.tolerance; + var abs = this.state.absolutePoints.slice(); + abs[this.index] = pt; + + // Handes special case where removing waypoint affects tolerance (flickering) + var src = this.state.getVisibleTerminalState(true); + + if (src != null) + { + var c = this.graph.getConnectionConstraint(this.state, src, true); + + // Checks if point is not fixed + if (c == null || this.graph.getConnectionPoint(src, c) == null) + { + abs[0] = new mxPoint(src.view.getRoutingCenterX(src), src.view.getRoutingCenterY(src)); + } + } + + var trg = this.state.getVisibleTerminalState(false); + + if (trg != null) + { + var c = this.graph.getConnectionConstraint(this.state, trg, false); + + // Checks if point is not fixed + if (c == null || this.graph.getConnectionPoint(trg, c) == null) + { + abs[abs.length - 1] = new mxPoint(trg.view.getRoutingCenterX(trg), trg.view.getRoutingCenterY(trg)); + } + } + + function checkRemove(idx, tmp) + { + if (idx > 0 && idx < abs.length - 1 && + mxUtils.ptSegDistSq(abs[idx - 1].x, abs[idx - 1].y, + abs[idx + 1].x, abs[idx + 1].y, tmp.x, tmp.y) < tol) + { + points.splice(idx - 1, 1); + result = points; + } + }; + + // LATER: Check if other points can be removed if a segment is made straight + checkRemove(this.index, pt); + } + } + + // Updates existing point + if (result == null && this.index > mxEvent.VIRTUAL_HANDLE) + { + points[this.index - 1] = point; + } + } + } + else if (this.graph.resetEdgesOnConnect) + { + points = null; + } + + return (result != null) ? result : points; +}; + +/** + * Function: isOutlineConnectEvent + * + * Returns true if is true and the source of the event is the + * outline shape or shift is pressed. + */ +mxEdgeHandler.prototype.isOutlineConnectEvent = function(me) +{ + if (mxEvent.isShiftDown(me.getEvent()) && mxEvent.isAltDown(me.getEvent())) + { + return false; + } + else + { + var offset = mxUtils.getOffset(this.graph.container); + var evt = me.getEvent(); + + var clientX = mxEvent.getClientX(evt); + var clientY = mxEvent.getClientY(evt); + + var doc = document.documentElement; + var left = (window.pageXOffset || doc.scrollLeft) - (doc.clientLeft || 0); + var top = (window.pageYOffset || doc.scrollTop) - (doc.clientTop || 0); + + var gridX = this.currentPoint.x - this.graph.container.scrollLeft + offset.x - left; + var gridY = this.currentPoint.y - this.graph.container.scrollTop + offset.y - top; + + return this.outlineConnect && ((mxEvent.isShiftDown(me.getEvent()) && + !mxEvent.isAltDown(me.getEvent())) || (me.isSource(this.marker.highlight.shape) || + (!mxEvent.isShiftDown(me.getEvent()) && mxEvent.isAltDown(me.getEvent()) && + me.getState() != null) || this.marker.highlight.isHighlightAt(clientX, clientY) || + ((gridX != clientX || gridY != clientY) && me.getState() == null && + this.marker.highlight.isHighlightAt(gridX, gridY)))); + } +}; + +/** + * Function: updatePreviewState + * + * Updates the given preview state taking into account the state of the constraint handler. + */ +mxEdgeHandler.prototype.updatePreviewState = function(edge, point, terminalState, me, outline) +{ + // 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 constraintHandler = this.getConstraintHandler(); + var constraint = constraintHandler.currentConstraint; + + if (constraint == null && outline) + { + if (terminalState != null) + { + // Handles special case where mouse is on outline away from actual end point + // in which case the grid is ignored and mouse point is used instead + if (me.isSource(this.marker.highlight.shape)) + { + point = new mxPoint(me.getGraphX(), me.getGraphY()); + } + + constraint = this.graph.getOutlineConstraint(point, terminalState, me); + constraintHandler.setFocus(me, terminalState, this.isSource); + constraintHandler.currentConstraint = constraint; + constraintHandler.currentPoint = point; + } + else + { + constraint = new mxConnectionConstraint(); + } + } + + if (this.outlineConnect && this.marker.highlight != null && this.marker.highlight.shape != null) + { + var s = this.graph.view.scale; + + if (constraintHandler.currentConstraint != null && + constraintHandler.currentFocus != null) + { + this.marker.highlight.shape.stroke = (outline) ? mxConstants.OUTLINE_HIGHLIGHT_COLOR : 'transparent'; + this.marker.highlight.shape.strokewidth = mxConstants.OUTLINE_HIGHLIGHT_STROKEWIDTH / s / s; + this.marker.highlight.repaint(); + } + else if (this.marker.hasValidState()) + { + this.marker.highlight.shape.stroke = (this.graph.isCellConnectable(me.getCell()) && + this.marker.getValidState() != me.getState()) ? + 'transparent' : mxConstants.DEFAULT_VALID_COLOR; + this.marker.highlight.shape.strokewidth = mxConstants.HIGHLIGHT_STROKEWIDTH / s / s; + this.marker.highlight.repaint(); + } + } + + if (this.isSource) + { + sourceConstraint = constraint; + } + else if (this.isTarget) + { + targetConstraint = constraint; + } + + if (this.isSource || this.isTarget) + { + if (constraint != null && constraint.point != null) + { + edge.style[(this.isSource) ? mxConstants.STYLE_EXIT_X : mxConstants.STYLE_ENTRY_X] = constraint.point.x; + edge.style[(this.isSource) ? mxConstants.STYLE_EXIT_Y : mxConstants.STYLE_ENTRY_Y] = constraint.point.y; + } + else + { + delete edge.style[(this.isSource) ? mxConstants.STYLE_EXIT_X : mxConstants.STYLE_ENTRY_X]; + delete edge.style[(this.isSource) ? mxConstants.STYLE_EXIT_Y : mxConstants.STYLE_ENTRY_Y]; + } + } + + edge.setVisibleTerminalState(sourceState, true); + edge.setVisibleTerminalState(targetState, false); + + 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 constraintHandler = this.getConstraintHandler(); + this.currentPoint = this.getPointForEvent(me); + this.error = null; + + // Uses the current point from the constraint handler if available + if (this.snapPoint != null && mxEvent.isShiftDown(me.getEvent()) && + !this.graph.isIgnoreTerminalEvent(me.getEvent()) && + constraintHandler.currentFocus == null && + constraintHandler.currentFocus != this.state) + { + if (Math.abs(this.snapPoint.x - this.currentPoint.x) < + Math.abs(this.snapPoint.y - this.currentPoint.y)) + { + this.currentPoint.x = this.snapPoint.x; + } + else + { + this.currentPoint.y = this.snapPoint.y; + } + } + + if (this.index <= mxEvent.CUSTOM_HANDLE && this.index > mxEvent.VIRTUAL_HANDLE) + { + if (this.customHandles != null) + { + this.customHandles[mxEvent.CUSTOM_HANDLE - this.index].processEvent(me); + this.customHandles[mxEvent.CUSTOM_HANDLE - this.index].positionChanged(); + + if (this.shape != null && this.shape.node != null) + { + this.shape.node.style.display = 'none'; + } + } + } + else if (this.isLabel) + { + this.label.x = this.currentPoint.x; + this.label.y = this.currentPoint.y; + } + else + { + this.points = this.getPreviewPoints(this.currentPoint, me); + var terminalState = (this.isSource || this.isTarget) ? this.getPreviewTerminalState(me) : null; + + if (constraintHandler.currentConstraint != null && + constraintHandler.currentFocus != null && + constraintHandler.currentPoint != null) + { + this.currentPoint = constraintHandler.currentPoint.clone(); + } + else if (this.outlineConnect) + { + // Need to check outline before cloning terminal state + var outline = (this.isSource || this.isTarget) ? this.isOutlineConnectEvent(me) : false + + if (outline) + { + terminalState = this.marker.highlight.state; + } + else if (terminalState != null && terminalState != me.getState() && + this.graph.isCellConnectable(me.getCell()) && + this.marker.highlight.shape != null) + { + this.marker.highlight.shape.stroke = 'transparent'; + this.marker.highlight.repaint(); + terminalState = null; + } + } + + if (terminalState != null && !this.isCellEnabled(terminalState.cell)) + { + terminalState = null; + this.marker.reset(); + } + + var clone = this.clonePreviewState(this.currentPoint, (terminalState != null) ? terminalState.cell : null); + this.updatePreviewState(clone, this.currentPoint, terminalState, me, outline); + + // 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.updateHint(me, this.currentPoint, clone); + } + + // This should go before calling isOutlineConnectEvent above. As a workaround + // we add an offset of gridSize to the hint to avoid problem with hit detection + // in highlight.isHighlightAt (which uses comonentFromPoint) + this.drawPreview(); + mxEvent.consume(me.getEvent()); + me.consume(); + } + else if (!mxEvent.isControlDown(me.getEvent()) && !mxEvent.isShiftDown(me.getEvent()) && + this.mouseDownX != null && this.mouseDownY != null && this.handle != null) + { + var tol = this.graph.tolerance; + + if ((Math.abs(this.mouseDownX - me.getX()) > tol || + Math.abs(this.mouseDownY - me.getY()) > tol)) + { + this.start(this.mouseDownX, this.mouseDownY, this.handle); + } + } +}; + +/** + * Function: mouseUp + * + * Handles the event to applying the previewed changes on the edge by + * using , or . + */ +mxEdgeHandler.prototype.mouseUp = function(sender, me) +{ + // Workaround for wrong event source in Webkit + if (this.index != null && this.marker != null) + { + if (this.shape != null && this.shape.node != null) + { + this.shape.node.style.display = ''; + } + + var edge = this.state.cell; + var index = this.index; + this.index = null; + + // Ignores event if mouse has not been moved + if (me.getX() != this.startX || me.getY() != this.startY) + { + var clone = !this.graph.isIgnoreTerminalEvent(me.getEvent()) && this.graph.isCloneEvent(me.getEvent()) && + this.cloneEnabled && this.graph.isCellsCloneable(); + + // 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 (index <= mxEvent.CUSTOM_HANDLE && index > mxEvent.VIRTUAL_HANDLE) + { + if (this.customHandles != null) + { + var model = this.graph.getModel(); + + model.beginUpdate(); + try + { + this.customHandles[mxEvent.CUSTOM_HANDLE - index].execute(me); + + if (this.shape != null && this.shape.node != null) + { + this.shape.apply(this.state); + this.shape.redraw(); + } + } + finally + { + model.endUpdate(); + } + } + } + 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 != null && + this.constraintHandler.currentConstraint != null && + this.constraintHandler.currentFocus != null) + { + terminal = this.constraintHandler.currentFocus.cell; + } + + if (terminal == null && this.marker.hasValidState() && this.marker.highlight != null && + this.marker.highlight.shape != null && + this.marker.highlight.shape.stroke != 'transparent' && + this.marker.highlight.shape.stroke != 'white') + { + terminal = this.marker.validState.cell; + } + + if (terminal != null) + { + var model = this.graph.getModel(); + var parent = model.getParent(edge); + + model.beginUpdate(); + try + { + // Clones and adds the cell + if (clone) + { + var geo = model.getGeometry(edge); + var clone = this.graph.cloneCell(edge); + model.add(parent, clone, model.getChildCount(parent)); + + if (geo != null) + { + geo = geo.clone(); + model.setGeometry(clone, geo); + } + + var other = model.getTerminal(edge, !this.isSource); + this.graph.connectCell(clone, other, !this.isSource); + + edge = clone; + } + + edge = this.connect(edge, terminal, this.isSource, clone, me); + } + finally + { + model.endUpdate(); + } + } + else if (this.graph.isAllowDanglingEdges()) + { + var pt = this.abspoints[(this.isSource) ? 0 : this.abspoints.length - 1]; + pt.x = this.roundLength(pt.x / this.graph.view.scale - this.graph.view.translate.x); + pt.y = this.roundLength(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 recreates this handler + edge = this.changeTerminalPoint(edge, pt, this.isSource, clone); + } + } + else if (this.active) + { + edge = this.changePoints(edge, this.points, clone); + } + else + { + this.graph.getView().invalidate(this.state.cell); + this.graph.getView().validate(this.state.cell); + } + } + else if (this.graph.isToggleEvent(me.getEvent())) + { + this.graph.selectCellForEvent(this.state.cell, me.getEvent()); + } + + // 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(); + } + else if (this.handle != null && this.bends != null && + !mxEvent.isAltDown(me.getEvent()) && (this.handle == 0 || + this.handle == this.bends.length - 1)) + { + var terminal = this.state.getVisibleTerminal(this.handle == 0); + + if (terminal != null) + { + this.graph.selectCellForEvent(terminal, me.getEvent()); + me.consume(); + } + } + + this.handle = null; + this.mouseDownX = null; + this.mouseDownY = null; +}; + +/** + * Function: reset + * + * Resets the state of this handler. + */ +mxEdgeHandler.prototype.reset = function() +{ + if (this.active) + { + this.refresh(); + } + + this.error = null; + this.index = null; + this.label = null; + this.points = null; + this.handle = null; + this.startX = null; + this.startY = null; + this.mouseDownX = null; + this.mouseDownY = null; + this.snapPoint = null; + this.isLabel = false; + this.isSource = false; + this.isTarget = false; + this.active = false; + + if (this.livePreview && this.sizers != null) + { + for (var i = 0; i < this.sizers.length; i++) + { + if (this.sizers[i] != null) + { + this.sizers[i].node.style.display = ''; + } + } + } + + if (this.marker != null) + { + this.marker.reset(); + } + + if (this.constraintHandler != null) + { + this.constraintHandler.reset(); + } + + if (this.customHandles != null) + { + for (var i = 0; i < this.customHandles.length; i++) + { + this.customHandles[i].reset(); + } + } + + this.setPreviewColor(mxConstants.EDGE_SELECTION_COLOR); + this.removeHint(); + 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.stroke = 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 - 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 - 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) + { + var scale = this.graph.getView().scale; + geometry = geometry.clone(); + + if (geometry.relative) + { + // Resets the relative location stored inside the geometry + var pt = this.graph.getView().getRelativePoint(edgeState, x, y); + geometry.x = Math.round(pt.x * 10000) / 10000; + geometry.y = Math.round(pt.y); + + // Resets the offset inside the geometry to find the offset + // from the resulting point + geometry.offset = new mxPoint(0, 0); + var pt = this.graph.view.getPoint(edgeState, geometry); + geometry.offset = new mxPoint(Math.round((x - pt.x) / scale), Math.round((y - pt.y) / scale)); + } + else + { + var points = edgeState.absolutePoints; + var p0 = points[0]; + var pe = points[points.length - 1]; + + if (p0 != null && pe != null) + { + var cx = p0.x + (pe.x - p0.x) / 2; + var cy = p0.y + (pe.y - p0.y) / 2; + + geometry.offset = new mxPoint(Math.round((x - cx) / scale), Math.round((y - cy) / scale)); + geometry.x = 0; + geometry.y = 0; + } + } + + model.setGeometry(edgeState.cell, geometry); + } +}; + +/** + * Function: connect + * + * Changes the terminal or terminal point of the given edge in the graph + * model. + * + * Parameters: + * + * edge - that represents the edge to be reconnected. + * terminal - 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 - that contains the mouse up event. + */ +mxEdgeHandler.prototype.connect = function(edge, terminal, isSource, isClone, me) +{ + var model = this.graph.getModel(); + + model.beginUpdate(); + try + { + var constraint = (this.constraintHandler != null) ? + this.constraintHandler.currentConstraint : null; + + 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, clone) +{ + var model = this.graph.getModel(); + + model.beginUpdate(); + try + { + if (clone) + { + var parent = model.getParent(edge); + var terminal = model.getTerminal(edge, !isSource); + edge = this.graph.cloneCell(edge); + model.add(parent, edge, model.getChildCount(parent)); + model.setTerminal(edge, terminal, !isSource); + } + + var geo = model.getGeometry(edge); + + if (geo != null) + { + geo = geo.clone(); + geo.setTerminalPoint(point, isSource); + model.setGeometry(edge, geo); + this.graph.connectCell(edge, null, isSource, new mxConnectionConstraint()); + } + } + finally + { + model.endUpdate(); + } + + return edge; +}; + +/** + * Function: changePoints + * + * Changes the control points of the given edge in the graph model. + */ +mxEdgeHandler.prototype.changePoints = function(edge, points, clone) +{ + var model = this.graph.getModel(); + model.beginUpdate(); + try + { + if (clone) + { + var parent = model.getParent(edge); + var source = model.getTerminal(edge, true); + var target = model.getTerminal(edge, false); + edge = this.graph.cloneCell(edge); + model.add(parent, edge, model.getChildCount(parent)); + model.setTerminal(edge, source, true); + model.setTerminal(edge, target, false); + } + + var geo = model.getGeometry(edge); + + if (geo != null) + { + geo = geo.clone(); + geo.points = points; + + model.setGeometry(edge, geo); + } + } + finally + { + model.endUpdate(); + } + + return edge; +}; + +/** + * Function: addPoint + * + * Adds a control point for the given state and event. + */ +mxEdgeHandler.prototype.addPoint = function(state, evt) +{ + var pt = mxUtils.convertPoint(this.graph.container, mxEvent.getClientX(evt), + mxEvent.getClientY(evt)); + var gridEnabled = this.graph.isGridEnabledEvent(evt); + this.convertPoint(pt, gridEnabled); + this.addPointAt(state, pt.x, pt.y); + mxEvent.consume(evt); +}; + +/** + * Function: addPointAt + * + * Adds a control point at the given point. + */ +mxEdgeHandler.prototype.addPointAt = function(state, x, y) +{ + var geo = this.graph.getCellGeometry(state.cell); + var pt = new mxPoint(x, y); + + if (geo != null) + { + geo = geo.clone(); + var t = this.graph.view.translate; + var s = this.graph.view.scale; + var offset = new mxPoint(t.x * s, t.y * s); + + var parent = this.graph.model.getParent(this.state.cell); + + if (this.graph.model.isVertex(parent)) + { + var pState = this.graph.view.getState(parent); + offset = new mxPoint(pState.x, pState.y); + } + + var index = mxUtils.findNearestSegment(state, pt.x * s + offset.x, pt.y * s + offset.y); + + if (geo.points == null) + { + geo.points = [pt]; + } + else + { + geo.points.splice(index, 0, pt); + } + + this.graph.getModel().setGeometry(state.cell, geo); + this.refresh(); + this.redraw(); + } +}; + +/** + * 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.refresh(); + this.redraw(); + } + } +}; + +/** + * 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(ignoreHandles) +{ + if (this.state != null && this.state.absolutePoints != null) + { + this.abspoints = this.state.absolutePoints.slice(); + var g = this.graph.getModel().getGeometry(this.state.cell); + + if (g != null) + { + var pts = g.points; + + if (this.bends != null && this.bends.length > 0) + { + if (pts != null) + { + if (this.points == null) + { + this.points = []; + } + + for (var i = 1; i < this.bends.length - 1; i++) + { + if (this.bends[i] != null && this.abspoints[i] != null) + { + this.points[i - 1] = pts[i - 1]; + } + } + } + } + } + + this.drawPreview(); + + if (!ignoreHandles) + { + this.redrawHandles(); + } + } +}; + +/** + * Function: isTerminalHandleVisible + * + * Redraws the handles. + */ +mxEdgeHandler.prototype.isTerminalHandleVisible = function(source) +{ + return true; +}; + +/** + * Function: redrawHandles + * + * Redraws the handles. + */ +mxEdgeHandler.prototype.redrawHandles = function() +{ + var cell = this.state.cell; + + // Updates the handle for the label position + if (this.labelShape != null) + { + var b = this.labelShape.bounds; + this.label = new mxPoint(this.state.absoluteOffset.x, this.state.absoluteOffset.y); + this.labelShape.bounds = new mxRectangle(Math.round(this.label.x - b.width / 2), + Math.round(this.label.y - b.height / 2), b.width, b.height); + + // Shows or hides the label handle depending on the label + var lab = this.graph.getLabel(cell); + this.labelShape.visible = lab != null && lab.length > 0 && + this.graph.isCellEditable(this.state.cell) && + this.graph.isLabelMovable(cell) && + this.isHandlesVisible(); + } + + if (this.bends != null && this.bends.length > 0) + { + var n = this.abspoints.length - 1; + + var p0 = this.abspoints[0]; + var x0 = p0.x; + var y0 = p0.y; + + b = this.bends[0].bounds; + this.bends[0].bounds = new mxRectangle(Math.floor(x0 - b.width / 2), + Math.floor(y0 - b.height / 2), b.width, b.height); + this.bends[0].fill = this.getHandleFillColor(0); + this.bends[0].redraw(); + + if (this.manageLabelHandle) + { + this.checkLabelHandle(this.bends[0].bounds); + } + + this.bends[0].node.style.visibility = (!this.isHandlesVisible() || + !this.isTerminalHandleVisible(true)) ? 'hidden' : ''; + + var pe = this.abspoints[n]; + var xn = pe.x; + var yn = pe.y; + + var bn = this.bends.length - 1; + b = this.bends[bn].bounds; + this.bends[bn].bounds = new mxRectangle(Math.floor(xn - b.width / 2), + Math.floor(yn - b.height / 2), b.width, b.height); + this.bends[bn].fill = this.getHandleFillColor(bn); + this.bends[bn].redraw(); + + if (this.manageLabelHandle) + { + this.checkLabelHandle(this.bends[bn].bounds); + } + + this.bends[bn].node.style.visibility = (!this.isHandlesVisible() || + !this.isTerminalHandleVisible(false)) ? 'hidden' : ''; + this.redrawInnerBends(p0, pe); + } + + if (this.abspoints != null && this.virtualBends != null && this.virtualBends.length > 0) + { + var last = this.abspoints[0]; + + for (var i = 0; i < this.virtualBends.length; i++) + { + if (this.virtualBends[i] != null && this.abspoints[i + 1] != null) + { + var pt = this.abspoints[i + 1]; + var b = this.virtualBends[i]; + var x = last.x + (pt.x - last.x) / 2; + var y = last.y + (pt.y - last.y) / 2; + b.bounds = new mxRectangle(Math.floor(x - b.bounds.width / 2), + Math.floor(y - b.bounds.height / 2), b.bounds.width, b.bounds.height); + b.redraw(); + mxUtils.setOpacity(b.node, this.virtualBendOpacity); + last = pt; + + if (this.manageLabelHandle) + { + this.checkLabelHandle(b.bounds); + } + + b.node.style.visibility = (!this.isHandlesVisible()) ? 'hidden' : ''; + } + } + } + + if (this.labelShape != null) + { + this.labelShape.redraw(); + } + + if (this.customHandles != null) + { + for (var i = 0; i < this.customHandles.length; i++) + { + var temp = this.customHandles[i].shape.node.style.display; + this.customHandles[i].redraw(); + this.customHandles[i].shape.node.style.display = temp; + + // Hides custom handles during text editing + this.customHandles[i].shape.node.style.visibility = + (this.graph.isEditing() || !this.isHandlesVisible() || + !this.isCustomHandleVisible(this.customHandles[i])) ? + 'hidden' : ''; + } + } +}; + +/** + * Function: isCustomHandleVisible + * + * Returns true if the given custom handle is visible. + */ +mxEdgeHandler.prototype.isCustomHandleVisible = function(handle) +{ + return this.state.view.graph.getSelectionCount() == 1; +}; + +/** + * Function: hideHandles + * + * Shortcut to . + */ +mxEdgeHandler.prototype.setHandlesVisible = function(visible) +{ + if (this.bends != null) + { + for (var i = 0; i < this.bends.length; i++) + { + if (this.bends[i] != null) + { + this.bends[i].node.style.display = (visible) ? '' : 'none'; + } + } + } + + if (this.virtualBends != null) + { + for (var i = 0; i < this.virtualBends.length; i++) + { + if (this.virtualBends[i] != null) + { + this.virtualBends[i].node.style.display = (visible) ? '' : 'none'; + } + } + } + + if (this.labelShape != null) + { + this.labelShape.node.style.display = (visible) ? '' : 'none'; + } + + if (this.customHandles != null) + { + for (var i = 0; i < this.customHandles.length; i++) + { + this.customHandles[i].setVisible(visible); + } + } +}; + +/** + * Function: redrawInnerBends + * + * Updates and redraws the inner bends. + * + * Parameters: + * + * p0 - that represents the location of the first point. + * pe - that represents the location of the last point. + */ +mxEdgeHandler.prototype.redrawInnerBends = function(p0, pe) +{ + 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].bounds = new mxRectangle(Math.round(x - b.width / 2), + Math.round(y - b.height / 2), b.width, b.height); + + if (this.manageLabelHandle) + { + this.checkLabelHandle(this.bends[i].bounds); + } + else if (this.handleImage == null && this.labelShape.visible && mxUtils.intersects(this.bends[i].bounds, this.labelShape.bounds)) + { + w = mxConstants.HANDLE_SIZE + 3; + h = mxConstants.HANDLE_SIZE + 3; + this.bends[i].bounds = new mxRectangle(Math.round(x - w / 2), Math.round(y - h / 2), w, h); + } + + this.bends[i].redraw(); + this.bends[i].node.style.visibility = (!this.isHandlesVisible()) ? 'hidden' : ''; + } + else + { + this.bends[i].destroy(); + this.bends[i] = null; + } + } + } +}; + +/** + * Function: checkLabelHandle + * + * Checks if the label handle intersects the given bounds and moves it if it + * intersects. + */ +mxEdgeHandler.prototype.checkLabelHandle = function(b) +{ + if (this.labelShape != null) + { + var b2 = this.labelShape.bounds; + + if (mxUtils.intersects(b, b2)) + { + if (b.getCenterY() < b2.getCenterY()) + { + b2.y = b.y + b.height; + } + else + { + b2.y = b.y - b2.height; + } + } + } +}; + +/** + * Function: drawPreview + * + * Redraws the preview. + */ +mxEdgeHandler.prototype.drawPreview = function() +{ + try + { + if (this.isLabel) + { + var b = this.labelShape.bounds; + var bounds = new mxRectangle(Math.round(this.label.x - b.width / 2), + Math.round(this.label.y - b.height / 2), b.width, b.height); + + if (!this.labelShape.bounds.equals(bounds)) + { + this.labelShape.bounds = bounds; + this.labelShape.redraw(); + } + } + + if (this.shape != null && !mxUtils.equalPoints(this.shape.points, this.abspoints)) + { + this.shape.apply(this.state); + this.shape.points = this.abspoints.slice(); + this.shape.scale = this.state.view.scale; + this.shape.isDashed = this.isSelectionDashed(); + this.shape.stroke = this.getSelectionColor(); + this.shape.strokewidth = this.getSelectionStrokeWidth() / this.shape.scale / this.shape.scale; + this.shape.isShadow = false; + this.shape.redraw(); + } + + this.updateParentHighlight(); + } + catch (e) + { + // ignore + } +}; + +/** + * Function: isHandlesVisible + * + * Returns true if all handles should be visible. + */ +mxEdgeHandler.prototype.isHandlesVisible = function() +{ + return !this.graph.isCellLocked(this.state.cell) && + (mxGraphHandler.prototype.maxCells <= 0 || + this.graph.getSelectionCount() <= mxGraphHandler.prototype.maxCells); +}; + +/** + * Function: refresh + * + * Refreshes the bends of this handler. + */ +mxEdgeHandler.prototype.refresh = function() +{ + if (this.state != null) + { + this.abspoints = this.getSelectionPoints(this.state); + this.points = []; + + if (this.shape != null) + { + this.shape.isDashed = this.isSelectionDashed(); + this.shape.stroke = this.getSelectionColor(); + this.shape.isShadow = false; + this.shape.redraw(); + } + + if (this.bends != null) + { + this.destroyBends(this.bends); + this.bends = null; + } + + if (this.isHandlesVisible()) + { + this.bends = this.createBends(); + } + + if (this.virtualBends != null) + { + this.destroyBends(this.virtualBends); + this.virtualBends = null; + } + + if (this.isHandlesVisible()) + { + this.virtualBends = this.createVirtualBends(); + } + + if (this.customHandles != null) + { + this.destroyBends(this.customHandles); + this.customHandles = null; + } + + if (this.isHandlesVisible()) + { + this.customHandles = this.createCustomHandles(); + } + + if (this.labelShape != null) + { + this.labelShape.destroy(); + this.labelShape = null; + } + + if (this.isHandlesVisible()) + { + this.labelShape = this.createLabelShape(); + + // Puts label node on top of bends + if (this.labelShape != null && this.labelShape.node != null && + this.labelShape.node.parentNode != null) + { + this.labelShape.node.parentNode.appendChild(this.labelShape.node); + } + } + } +}; + +/** + * Function: isDestroyed + * + * Returns true if was called. + */ +mxEdgeHandler.prototype.isDestroyed = function() +{ + return this.shape == null; +}; + +/** + * Function: destroyBends + * + * Destroys all elements in . + */ +mxEdgeHandler.prototype.destroyBends = function(bends) +{ + if (bends != null) + { + for (var i = 0; i < bends.length; i++) + { + if (bends[i] != null) + { + bends[i].destroy(); + } + } + } +}; + +/** + * 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.escapeHandler != null) + { + this.state.view.graph.removeListener(this.escapeHandler); + this.escapeHandler = null; + } + + 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; + } + + if (this.parentHighlight != null) + { + this.destroyParentHighlight(); + } + + this.destroyBends(this.virtualBends); + this.virtualBends = null; + + this.destroyBends(this.customHandles); + this.customHandles = null; + + this.destroyBends(this.bends); + this.bends = null; + + this.removeHint(); +}; diff --git a/src/main/mxgraph/handler/mxEdgeSegmentHandler.js b/src/main/mxgraph/handler/mxEdgeSegmentHandler.js new file mode 100644 index 000000000..7b0db040c --- /dev/null +++ b/src/main/mxgraph/handler/mxEdgeSegmentHandler.js @@ -0,0 +1,412 @@ +/** + * Copyright (c) 2006-2015, JGraph Ltd + * Copyright (c) 2006-2015, Gaudenz Alder + */ +function mxEdgeSegmentHandler(state) +{ + mxEdgeHandler.call(this, state); +}; + +/** + * Extends mxEdgeHandler. + */ +mxUtils.extend(mxEdgeSegmentHandler, mxElbowEdgeHandler); + +/** + * Function: getCurrentPoints + * + * Returns the current absolute points. + */ +mxEdgeSegmentHandler.prototype.getCurrentPoints = function() +{ + var pts = this.state.absolutePoints; + + if (pts != null) + { + // Special case for straight edges where we add a virtual middle handle for moving the edge + var tol = Math.max(1, this.graph.view.scale); + + if (pts.length == 2 || (pts.length == 3 && + (Math.abs(pts[0].x - pts[1].x) < tol && Math.abs(pts[1].x - pts[2].x) < tol || + Math.abs(pts[0].y - pts[1].y) < tol && Math.abs(pts[1].y - pts[2].y) < tol))) + { + var cx = pts[0].x + (pts[pts.length - 1].x - pts[0].x) / 2; + var cy = pts[0].y + (pts[pts.length - 1].y - pts[0].y) / 2; + + pts = [pts[0], new mxPoint(cx, cy), new mxPoint(cx, cy), pts[pts.length - 1]]; + } + } + + return pts; +}; + +/** + * 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 + { + var pts = this.getCurrentPoints(); + var last = this.convertPoint(pts[0].clone(), false); + point = this.convertPoint(point.clone(), false); + var result = []; + + for (var i = 1; i < pts.length; i++) + { + var pt = this.convertPoint(pts[i].clone(), false); + + if (i == this.index) + { + if (Math.round(last.x - pt.x) == 0) + { + last.x = point.x; + pt.x = point.x; + } + + if (Math.round(last.y - pt.y) == 0) + { + last.y = point.y; + pt.y = point.y; + } + } + + if (i < pts.length - 1) + { + result.push(pt); + } + + last = pt; + } + + // Replaces single point that intersects with source or target + if (result.length == 1) + { + var source = this.state.getVisibleTerminalState(true); + var target = this.state.getVisibleTerminalState(false); + var scale = this.state.view.getScale(); + var tr = this.state.view.getTranslate(); + + var x = result[0].x * scale + tr.x; + var y = result[0].y * scale + tr.y; + + if ((source != null && mxUtils.contains(source, x, y)) || + (target != null && mxUtils.contains(target, x, y))) + { + result = [point, point]; + } + } + + return result; + } +}; + +/** + * Function: updatePreviewState + * + * Overridden to perform optimization of the edge style result. + */ +mxEdgeSegmentHandler.prototype.updatePreviewState = function(edge, point, terminalState, me) +{ + mxEdgeHandler.prototype.updatePreviewState.apply(this, arguments); + + // Checks and corrects preview by running edge style again + if (!this.isSource && !this.isTarget) + { + point = this.convertPoint(point.clone(), false); + var pts = edge.absolutePoints; + var pt0 = pts[0]; + var pt1 = pts[1]; + + var result = []; + + for (var i = 2; i < pts.length; i++) + { + var pt2 = pts[i]; + + // Merges adjacent segments only if more than 2 to allow for straight edges + if ((Math.round(pt0.x - pt1.x) != 0 || Math.round(pt1.x - pt2.x) != 0) && + (Math.round(pt0.y - pt1.y) != 0 || Math.round(pt1.y - pt2.y) != 0)) + { + result.push(this.convertPoint(pt1.clone(), false)); + } + + pt0 = pt1; + pt1 = pt2; + } + + var source = this.state.getVisibleTerminalState(true); + var target = this.state.getVisibleTerminalState(false); + var rpts = this.state.absolutePoints; + + // A straight line is represented by 3 handles + if (result.length == 0 && (Math.round(pts[0].x - pts[pts.length - 1].x) == 0 || + Math.round(pts[0].y - pts[pts.length - 1].y) == 0)) + { + result = [point, point]; + } + // Handles special case of transitions from straight vertical to routed + else if (pts.length == 5 && result.length == 2 && source != null && target != null && + rpts != null && Math.round(rpts[0].x - rpts[rpts.length - 1].x) == 0) + { + var view = this.graph.getView(); + var scale = view.getScale(); + var tr = view.getTranslate(); + + var y0 = view.getRoutingCenterY(source) / scale - tr.y; + + // Use fixed connection point y-coordinate if one exists + var sc = this.graph.getConnectionConstraint(edge, source, true); + + if (sc != null) + { + var pt = this.graph.getConnectionPoint(source, sc); + + if (pt != null) + { + this.convertPoint(pt, false); + y0 = pt.y; + } + } + + var ye = view.getRoutingCenterY(target) / scale - tr.y; + + // Use fixed connection point y-coordinate if one exists + var tc = this.graph.getConnectionConstraint(edge, target, false); + + if (tc) + { + var pt = this.graph.getConnectionPoint(target, tc); + + if (pt != null) + { + this.convertPoint(pt, false); + ye = pt.y; + } + } + + result = [new mxPoint(point.x, y0), new mxPoint(point.x, ye)]; + } + + this.points = result; + + // LATER: Check if points and result are different + edge.view.updateFixedTerminalPoints(edge, source, target); + edge.view.updatePoints(edge, this.points, source, target); + edge.view.updateFloatingTerminalPoints(edge, source, target); + } +}; + +/** + * Overriden to merge edge segments. + */ +mxEdgeSegmentHandler.prototype.connect = function(edge, terminal, isSource, isClone, me) +{ + var model = this.graph.getModel(); + var geo = model.getGeometry(edge); + var result = null; + + // Merges adjacent edge segments + if (geo != null && geo.points != null && geo.points.length > 0) + { + var pts = this.abspoints; + var pt0 = pts[0]; + var pt1 = pts[1]; + result = []; + + for (var i = 2; i < pts.length; i++) + { + var pt2 = pts[i]; + + // Merges adjacent segments only if more than 2 to allow for straight edges + if ((Math.round(pt0.x - pt1.x) != 0 || Math.round(pt1.x - pt2.x) != 0) && + (Math.round(pt0.y - pt1.y) != 0 || Math.round(pt1.y - pt2.y) != 0)) + { + result.push(this.convertPoint(pt1.clone(), false)); + } + + pt0 = pt1; + pt1 = pt2; + } + } + + model.beginUpdate(); + try + { + if (result != null) + { + var geo = model.getGeometry(edge); + + if (geo != null) + { + geo = geo.clone(); + geo.points = result; + + model.setGeometry(edge, geo); + } + } + + edge = mxEdgeHandler.prototype.connect.apply(this, arguments); + } + finally + { + model.endUpdate(); + } + + return edge; +}; + +/** + * Function: getTooltipForNode + * + * Returns no tooltips. + */ +mxEdgeSegmentHandler.prototype.getTooltipForNode = function(node) +{ + return null; +}; + +/** + * Function: start + * + * Starts the handling of the mouse gesture. + */ +mxEdgeSegmentHandler.prototype.start = function(x, y, index) +{ + mxEdgeHandler.prototype.start.apply(this, arguments); + + if (this.bends != null && this.bends[index] != null && + !this.isSource && !this.isTarget) + { + mxUtils.setOpacity(this.bends[index].node, 100); + } +}; + +/** + * 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.setCursor(mxConstants.CURSOR_TERMINAL_HANDLE); + bends.push(bend); + + var pts = this.getCurrentPoints(); + + // 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++) + { + bend = this.createVirtualBend(); + bends.push(bend); + var horizontal = Math.round(pts[i].x - pts[i + 1].x) == 0; + + // Special case where dy is 0 as well + if (Math.round(pts[i].y - pts[i + 1].y) == 0 && i < pts.length - 2) + { + horizontal = Math.round(pts[i].x - pts[i + 2].x) == 0; + } + + bend.setCursor((horizontal) ? 'col-resize' : 'row-resize'); + this.points.push(new mxPoint(0,0)); + } + } + + // Target + var bend = this.createHandleShape(pts.length, null, true); + this.initBend(bend); + bend.setCursor(mxConstants.CURSOR_TERMINAL_HANDLE); + bends.push(bend); + + return bends; +}; + +/** + * Function: createVirtualBends + * + * Returns null. + */ +mxEdgeSegmentHandler.prototype.createVirtualBends = function() +{ + return null; +}; + +/** + * Function: redrawInnerBends + * + * Updates the position of the custom bends. + */ +mxEdgeSegmentHandler.prototype.redrawInnerBends = function(p0, pe) +{ + if (this.graph.isCellBendable(this.state.cell)) + { + var pts = this.getCurrentPoints(); + + if (pts != null && pts.length > 1) + { + var straight = false; + + // Puts handle in the center of straight edges + if (pts.length == 4 && Math.round(pts[1].x - pts[2].x) == 0 && Math.round(pts[1].y - pts[2].y) == 0) + { + straight = true; + + if (Math.round(pts[0].y - pts[pts.length - 1].y) == 0) + { + var cx = pts[0].x + (pts[pts.length - 1].x - pts[0].x) / 2; + pts[1] = new mxPoint(cx, pts[1].y); + pts[2] = new mxPoint(cx, pts[2].y); + } + else + { + var cy = pts[0].y + (pts[pts.length - 1].y - pts[0].y) / 2; + pts[1] = new mxPoint(pts[1].x, cy); + pts[2] = new mxPoint(pts[2].x, cy); + } + } + + for (var i = 0; i < pts.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); + var b = this.bends[i + 1].bounds; + this.bends[i + 1].bounds = new mxRectangle(Math.floor(pt.x - b.width / 2), + Math.floor(pt.y - b.height / 2), b.width, b.height); + this.bends[i + 1].redraw(); + + if (this.manageLabelHandle) + { + this.checkLabelHandle(this.bends[i + 1].bounds); + } + } + } + + if (straight) + { + mxUtils.setOpacity(this.bends[1].node, this.virtualBendOpacity); + mxUtils.setOpacity(this.bends[3].node, this.virtualBendOpacity); + } + } + } +}; diff --git a/src/main/mxgraph/handler/mxElbowEdgeHandler.js b/src/main/mxgraph/handler/mxElbowEdgeHandler.js new file mode 100644 index 000000000..1c384ddfe --- /dev/null +++ b/src/main/mxgraph/handler/mxElbowEdgeHandler.js @@ -0,0 +1,240 @@ +/** + * Copyright (c) 2006-2015, JGraph Ltd + * Copyright (c) 2006-2015, Gaudenz Alder + */ +/** + * Class: mxElbowEdgeHandler + * + * Graph event handler that reconnects edges and modifies control points and + * the edge label location. Uses for finding and + * highlighting new source and target vertices. This handler is automatically + * created in . It extends . + * + * Constructor: mxEdgeHandler + * + * Constructs an edge handler for the specified . + * + * Parameters: + * + * state - of the cell to be modified. + */ +function mxElbowEdgeHandler(state) +{ + mxEdgeHandler.call(this, state); +}; + +/** + * Extends mxEdgeHandler. + */ +mxUtils.extend(mxElbowEdgeHandler, mxEdgeHandler); + +/** + * Specifies if a double click on the middle handle should call + * . 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 to create custom bends. + */ +mxElbowEdgeHandler.prototype.createBends = function() +{ + var bends = []; + + // Source + var bend = this.createHandleShape(0); + this.initBend(bend); + bend.setCursor(mxConstants.CURSOR_TERMINAL_HANDLE); + bends.push(bend); + + // Virtual + bends.push(this.createVirtualBend(mxUtils.bind(this, function(evt) + { + if (!mxEvent.isConsumed(evt) && this.flipEnabled) + { + this.graph.flipEdge(this.state.cell, evt); + mxEvent.consume(evt); + } + }))); + + this.points.push(new mxPoint(0,0)); + + // Target + bend = this.createHandleShape(2, null, true); + this.initBend(bend); + bend.setCursor(mxConstants.CURSOR_TERMINAL_HANDLE); + bends.push(bend); + + return bends; +}; + +/** + * Function: createVirtualBends + * + * Returns null. + */ +mxElbowEdgeHandler.prototype.createVirtualBends = function() +{ + return null; +}; + +/** + * Function: createVirtualBend + * + * Creates a virtual bend that supports double clicking and calls + * . + */ +mxElbowEdgeHandler.prototype.createVirtualBend = function(dblClickHandler) +{ + var bend = this.createHandleShape(); + this.initBend(bend, dblClickHandler); + + bend.setCursor(this.getCursorForBend()); + + if (!this.graph.isCellBendable(this.state.cell)) + { + bend.node.style.display = 'none'; + } + + 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 - 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); + + return point; +}; + +/** + * Function: redrawInnerBends + * + * Updates and redraws the inner bends. + * + * Parameters: + * + * p0 - that represents the location of the first point. + * pe - 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 = this.state.absolutePoints; + var pt = null; + + // Keeps the virtual bend on the edge shape + if (pts.length > 1) + { + p0 = pts[1]; + pe = pts[pts.length - 2]; + } + else if (g.points != null && g.points.length > 0) + { + pt = pts[0]; + } + + 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; + var bounds = new mxRectangle(Math.round(pt.x - w / 2), Math.round(pt.y - h / 2), w, h); + + if (this.manageLabelHandle) + { + this.checkLabelHandle(bounds); + } + else if (this.handleImage == null && this.labelShape.visible && mxUtils.intersects(bounds, this.labelShape.bounds)) + { + w = mxConstants.HANDLE_SIZE + 3; + h = mxConstants.HANDLE_SIZE + 3; + bounds = new mxRectangle(Math.floor(pt.x - w / 2), Math.floor(pt.y - h / 2), w, h); + } + + this.bends[1].bounds = bounds; + this.bends[1].redraw(); + + if (this.manageLabelHandle) + { + this.checkLabelHandle(this.bends[1].bounds); + } +}; diff --git a/src/main/mxgraph/handler/mxGraphHandler.js b/src/main/mxgraph/handler/mxGraphHandler.js new file mode 100644 index 000000000..7504b2f99 --- /dev/null +++ b/src/main/mxgraph/handler/mxGraphHandler.js @@ -0,0 +1,1938 @@ +/** + * Copyright (c) 2006-2015, JGraph Ltd + * Copyright (c) 2006-2015, Gaudenz Alder + */ +/** + * Class: mxGraphHandler + * + * Graph event handler that handles selection. Individual cells are handled + * separately using or one of the edge handlers. These + * handlers are created using in + * . + * + * To avoid the container to scroll a moved cell into view, set + * to false. + * + * Constructor: mxGraphHandler + * + * Constructs an event handler that creates handles for the + * selection cells. + * + * Parameters: + * + * graph - Reference to the enclosing . + */ +function mxGraphHandler(graph) +{ + this.graph = graph; + this.graph.addMouseListener(this); + + // Repaints the handler after autoscroll + this.panHandler = mxUtils.bind(this, function() + { + if (!this.suspended) + { + this.updatePreview(); + this.updateHint(); + } + }); + + this.graph.addListener(mxEvent.PAN, this.panHandler); + + // Handles escape keystrokes + this.escapeHandler = mxUtils.bind(this, function(sender, evt) + { + this.reset(); + }); + + this.graph.addListener(mxEvent.ESCAPE, this.escapeHandler); + + // Updates the preview box for remote changes + this.refreshHandler = mxUtils.bind(this, function(sender, evt) + { + // Merges multiple pending calls + if (this.refreshThread) + { + window.clearTimeout(this.refreshThread); + } + + // Waits for the states and handlers to be updated + this.refreshThread = window.setTimeout(mxUtils.bind(this, function() + { + this.refreshThread = null; + + if (this.first != null && !this.suspended) + { + // Updates preview with no translate to compute bounding box + var dx = this.currentDx; + var dy = this.currentDy; + this.currentDx = 0; + this.currentDy = 0; + this.updatePreview(); + this.bounds = this.graph.getView().getBounds(this.cells); + this.pBounds = this.getPreviewBounds(this.cells); + + if (this.pBounds == null && !this.livePreviewUsed) + { + this.reset(); + } + else + { + // Restores translate and updates preview + this.currentDx = dx; + this.currentDy = dy; + this.updatePreview(); + this.updateHint(); + + if (this.livePreviewUsed) + { + // Forces update to ignore last visible state + this.setHandlesVisibleForCells( + this.graph.selectionCellsHandler. + getHandledSelectionCells(), false, true); + this.updatePreview(); + } + } + } + }), 0); + }); + + this.graph.getModel().addListener(mxEvent.CHANGE, this.refreshHandler); + this.graph.addListener(mxEvent.REFRESH, this.refreshHandler); + + this.keyHandler = mxUtils.bind(this, function(e) + { + if (this.graph.container != null && this.graph.container.style.visibility != 'hidden' && + this.first != null && !this.suspended) + { + var clone = this.graph.isCloneEvent(e) && + this.graph.isCellsCloneable() && + this.isCloneEnabled(); + + if (clone != this.cloning) + { + this.cloning = clone; + this.checkPreview(); + this.updatePreview(); + } + } + }); + + mxEvent.addListener(document, 'keydown', this.keyHandler); + mxEvent.addListener(document, 'keyup', this.keyHandler); +}; + +/** + * Variable: graph + * + * Reference to the enclosing . + */ +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. + */ +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: handlesVisible + * + * Whether the handles of the selection are currently visible. + */ +mxGraphHandler.prototype.handlesVisible = true; + +/** + * Variable: guide + * + * Holds the 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 over 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: removeEmptyParents + * + * If empty parents should be removed from the model after all child cells + * have been moved out. Default is true. + */ +mxGraphHandler.prototype.removeEmptyParents = false; + +/** + * 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 because + * the HTML preview does not "let events through". Default is false. + */ +mxGraphHandler.prototype.htmlPreview = false; + +/** + * Variable: shape + * + * Reference to the 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: rotationEnabled + * + * Specifies if the bounding box should allow for rotation. Default is true. + */ +mxGraphHandler.prototype.rotationEnabled = true; + +/** + * Variable: maxLivePreview + * + * Maximum number of cells for which live preview should be used. Default is 0 + * which means no live preview. + */ +mxGraphHandler.prototype.maxLivePreview = 0; + +/** + * Variable: allowLivePreview + * + * If live preview is allowed on this system. Default is true for systems with + * SVG support. + */ +mxGraphHandler.prototype.allowLivePreview = mxClient.IS_SVG; + +/** + * Function: isEnabled + * + * Returns . + */ +mxGraphHandler.prototype.isEnabled = function() +{ + return this.enabled; +}; + +/** + * Function: setEnabled + * + * Sets . + */ +mxGraphHandler.prototype.setEnabled = function(value) +{ + this.enabled = value; +}; + +/** + * Function: isCloneEnabled + * + * Returns . + */ +mxGraphHandler.prototype.isCloneEnabled = function() +{ + return this.cloneEnabled; +}; + +/** + * Function: setCloneEnabled + * + * Sets . + * + * Parameters: + * + * value - Boolean that specifies the new clone enabled state. + */ +mxGraphHandler.prototype.setCloneEnabled = function(value) +{ + this.cloneEnabled = value; +}; + +/** + * Function: isMoveEnabled + * + * Returns . + */ +mxGraphHandler.prototype.isMoveEnabled = function() +{ + return this.moveEnabled; +}; + +/** + * Function: setMoveEnabled + * + * Sets . + */ +mxGraphHandler.prototype.setMoveEnabled = function(value) +{ + this.moveEnabled = value; +}; + +/** + * Function: isSelectEnabled + * + * Returns . + */ +mxGraphHandler.prototype.isSelectEnabled = function() +{ + return this.selectEnabled; +}; + +/** + * Function: setSelectEnabled + * + * Sets . + */ +mxGraphHandler.prototype.setSelectEnabled = function(value) +{ + this.selectEnabled = value; +}; + +/** + * Function: isRemoveCellsFromParent + * + * Returns . + */ +mxGraphHandler.prototype.isRemoveCellsFromParent = function() +{ + return this.removeCellsFromParent; +}; + +/** + * Function: setRemoveCellsFromParent + * + * Sets . + */ +mxGraphHandler.prototype.setRemoveCellsFromParent = function(value) +{ + this.removeCellsFromParent = value; +}; + +/** + * Function: isPropagateSelectionCell + * + * Returns true if the given cell and parent should propagate + * selection state to the parent. + */ +mxGraphHandler.prototype.isPropagateSelectionCell = function(cell, immediate, me) +{ + var parent = this.graph.model.getParent(cell); + + if (immediate) + { + var geo = (this.graph.model.isEdge(cell)) ? null : + this.graph.getCellGeometry(cell); + + return !this.graph.isSiblingSelected(cell) && + ((geo != null && geo.relative) || + !this.graph.isSwimlane(parent)); + } + else + { + return (!this.graph.isToggleEvent(me.getEvent()) || + (!this.graph.isSiblingSelected(cell) && + !this.graph.isCellSelected(cell) && + (!this.graph.isSwimlane(parent)) || + this.graph.isCellSelected(parent))) && + (this.graph.isToggleEvent(me.getEvent()) || + !this.graph.isCellSelected(parent)); + } +}; + +/** + * Function: getInitialCellForEvent + * + * Hook to return initial cell for the given event. This returns + * the topmost cell that is not a swimlane or is selected. + */ +mxGraphHandler.prototype.getInitialCellForEvent = function(me) +{ + var state = me.getState(); + + if ((!this.graph.isToggleEvent(me.getEvent()) || !mxEvent.isAltDown(me.getEvent())) && + state != null && (!this.graph.isCellSelected(state.cell) || + this.graph.isPart(state.cell))) + { + var model = this.graph.model; + var next = this.graph.view.getState(model.getParent(state.cell)); + + while (next != null && !this.graph.isCellSelected(next.cell) && + (model.isVertex(next.cell) || model.isEdge(next.cell)) && + this.isPropagateSelectionCell(state.cell, true, me)) + { + state = next; + next = this.graph.view.getState(this.graph.getModel().getParent(state.cell)); + } + } + + return (state != null) ? state.cell : null; +}; + +/** + * Function: isDelayedSelection + * + * Returns true if the cell or one of its ancestors is selected. + */ +mxGraphHandler.prototype.isDelayedSelection = function(cell, me) +{ + if (!this.graph.isToggleEvent(me.getEvent()) || !mxEvent.isAltDown(me.getEvent())) + { + while (cell != null) + { + if (this.graph.selectionCellsHandler.isHandled(cell)) + { + return this.graph.cellEditor.getEditingCell() != cell; + } + + cell = this.graph.model.getParent(cell); + } + } + + return this.graph.isToggleEvent(me.getEvent()); +}; + +/** + * Function: selectDelayed + * + * Implements the delayed selection for the given mouse event. + */ +mxGraphHandler.prototype.selectDelayed = function(me) +{ + if (!this.graph.popupMenuHandler.isPopupTrigger(me)) + { + var cell = me.getCell(); + + if (cell == null) + { + cell = this.cell; + } + + this.selectCellForEvent(cell, me); + } +}; + +/** + * Function: selectCellForEvent + * + * Selects the given cell for the given . + */ +mxGraphHandler.prototype.selectCellForEvent = function(cell, me) +{ + var state = this.graph.view.getState(cell); + + if (state != null) + { + if (me.isSource(state.control)) + { + this.graph.selectCellForEvent(cell, me.getEvent()); + } + else + { + if (!this.graph.isToggleEvent(me.getEvent()) || + !mxEvent.isAltDown(me.getEvent())) + { + var model = this.graph.getModel(); + var parent = model.getParent(cell); + + while (this.graph.view.getState(parent) != null && + (model.isVertex(parent) || (model.isEdge(parent) && + !this.graph.isToggleEvent(me.getEvent()))) && + this.isPropagateSelectionCell(cell, false, me)) + { + cell = parent; + parent = model.getParent(cell); + } + } + + this.graph.selectCellForEvent(cell, me.getEvent()); + } + } + + return cell; +}; + +/** + * Function: consumeMouseEvent + * + * Consumes the given mouse event. NOTE: This may be used to enable click + * events for links in labels on iOS as follows as consuming the initial + * touchStart disables firing the subsequent click event on the link. + * + * + * mxGraphHandler.prototype.consumeMouseEvent = function(evtName, me) + * { + * var source = mxEvent.getSource(me.getEvent()); + * + * if (!mxEvent.isTouchEvent(me.getEvent()) || source.nodeName != 'A') + * { + * me.consume(); + * } + * } + * + */ +mxGraphHandler.prototype.consumeMouseEvent = function(evtName, me) +{ + me.consume(); +}; + +/** + * 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) +{ + this.mouseDownX = me.getX(); + this.mouseDownY = me.getY(); + var evt = me.getEvent(); + + var forceMove = mxEvent.isAltDown(evt) && mxEvent.isShiftDown(evt) && + !this.graph.isSelectionEmpty(); + + if (!me.isConsumed() && this.isEnabled() && this.graph.isEnabled() && + (me.getState() != null || forceMove) && !mxEvent.isMultiTouchEvent(evt)) + { + var cell = this.getInitialCellForEvent(me); + this.delayedSelection = this.isDelayedSelection(cell, me); + this.cell = null; + + if (cell == null && forceMove) + { + cell = this.graph.getSelectionCell(); + } + + var selectionCount = this.graph.getSelectionCount(); + + if (this.isSelectEnabled() && !this.delayedSelection) + { + this.graph.selectCellForEvent(cell, evt); + } + + if (mxEvent.isTouchEvent(me.getEvent()) && this.graph.isCellSelected(cell) && + selectionCount > 0) + { + this.blockDelayedSelection = true; + this.delayedSelection = true; + } + + if (this.isMoveEnabled()) + { + if (this.delayedSelection) + { + this.cell = cell; + } + else + { + this.start(cell, me.getX(), me.getY()); + } + + this.cellWasClicked = true; + this.consumeMouseEvent(mxEvent.MOUSE_DOWN, me); + } + } +}; + +/** + * 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 - 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 used as the preview bounds for + * moving the given cells. + */ +mxGraphHandler.prototype.getPreviewBounds = function(cells) +{ + var bounds = this.getBoundingBox(cells); + + if (bounds != null) + { + // Corrects width and height + bounds.width = Math.max(0, bounds.width - 1); + bounds.height = Math.max(0, bounds.height - 1); + + if (bounds.width < this.minimumSize) + { + var dx = this.minimumSize - bounds.width; + bounds.x -= dx / 2; + bounds.width = this.minimumSize; + } + else + { + bounds.x = Math.round(bounds.x); + bounds.width = Math.ceil(bounds.width); + } + + var tr = this.graph.view.translate; + var s = this.graph.view.scale; + + if (bounds.height < this.minimumSize) + { + var dy = this.minimumSize - bounds.height; + bounds.y -= dy / 2; + bounds.height = this.minimumSize; + } + else + { + bounds.y = Math.round(bounds.y); + bounds.height = Math.ceil(bounds.height); + } + } + + return bounds; +}; + +/** + * Function: getBoundingBox + * + * Returns the union of the for the given array of . + * For vertices, this method uses the bounding box of the corresponding shape + * if one exists. The bounding box of the corresponding text label and all + * controls and overlays are ignored. See also: and + * . + * + * Parameters: + * + * cells - Array of whose bounding box should be returned. + */ +mxGraphHandler.prototype.getBoundingBox = function(cells) +{ + var result = null; + + if (cells != null && cells.length > 0) + { + var model = this.graph.getModel(); + + for (var i = 0; i < cells.length; i++) + { + if (model.isVertex(cells[i]) || model.isEdge(cells[i])) + { + var state = this.graph.view.getState(cells[i]); + + if (state != null) + { + var bbox = state; + + if (model.isVertex(cells[i]) && state.shape != null && state.shape.boundingBox != null) + { + bbox = state.shape.boundingBox; + } + + if (result == null) + { + result = mxRectangle.fromRectangle(bbox); + } + else + { + result.add(bbox); + } + } + } + } + } + + return result; +}; + +/** + * 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; + + if (this.htmlPreview) + { + shape.dialect = mxConstants.DIALECT_STRICTHTML; + shape.init(this.graph.container); + } + else + { + // Makes sure to use either 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 = mxConstants.DIALECT_SVG; + shape.init(this.graph.getView().getOverlayPane()); + shape.pointerEvents = false; + + // Workaround for artifacts on iOS + if (mxClient.IS_IOS) + { + shape.getSvgScreenOffset = function() + { + return 0; + }; + } + } + + return shape; +}; + +/** + * Function: start + * + * Starts the handling of the mouse gesture. + */ +mxGraphHandler.prototype.start = function(cell, x, y, cells) +{ + var model = this.graph.model; + var geo = model.getGeometry(cell); + + if (this.first == null && (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.cell = cell; + this.first = mxUtils.convertPoint(this.graph.container, x, y); + this.cells = (cells != null) ? cells : this.getCells(this.cell); + this.bounds = this.graph.getView().getBounds(this.cells); + this.pBounds = this.getPreviewBounds(this.cells); + this.allCells = new mxDictionary(); + this.cloning = false; + this.cellCount = 0; + + for (var i = 0; i < this.cells.length; i++) + { + this.cellCount += this.addStates(this.cells[i], this.allCells); + } + + if (this.guidesEnabled) + { + this.guide = new mxGuide(this.graph, this.getGuideStates()); + var parent = this.graph.model.getParent(cell); + var ignore = this.graph.model.getChildCount(parent) < 2; + + // Uses connected states as guides + var connected = new mxDictionary(); + var opps = this.graph.getOpposites(this.graph.getEdges(this.cell), this.cell); + + for (var i = 0; i < opps.length; i++) + { + var state = this.graph.view.getState(opps[i]); + + if (state != null && !connected.get(state)) + { + connected.put(state, true); + } + } + + this.guide.isStateIgnored = mxUtils.bind(this, function(state) + { + var p = this.graph.model.getParent(state.cell); + + return state.cell != null && ((!this.cloning && + this.isCellMoving(state.cell)) || + (state.cell != (this.target || parent) && !ignore && + !connected.get(state) && + (this.target == null || this.graph.model.getChildCount( + this.target) >= 2) && p != (this.target || parent))); + }); + } + } +}; + +/** + * Function: addStates + * + * Adds the states for the given cell recursively to the given dictionary. + */ +mxGraphHandler.prototype.addStates = function(cell, dict) +{ + var state = this.graph.view.getState(cell); + var count = 0; + + if (state != null && dict.get(cell) == null) + { + dict.put(cell, state); + count++; + + var childCount = this.graph.model.getChildCount(cell); + + for (var i = 0; i < childCount; i++) + { + count += this.addStates(this.graph.model.getChildAt(cell, i), dict); + } + } + + return count; +}; + +/** + * Function: isCellMoving + * + * Returns true if the given cell is currently being moved. + */ +mxGraphHandler.prototype.isCellMoving = function(cell) +{ + return this.allCells.get(cell) != null; +}; + +/** + * Function: useGuidesForEvent + * + * Returns true if the guides should be used for the given . + * This implementation returns . + */ +mxGraphHandler.prototype.useGuidesForEvent = function(me) +{ + return (this.guide != null) ? this.guide.isEnabledForEvent(me.getEvent()) && + !this.isConstrainedEvent(me) : 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: getDelta + * + * Returns an that represents the vector for moving the cells + * for the given . + */ +mxGraphHandler.prototype.getDelta = function(me) +{ + var point = mxUtils.convertPoint(this.graph.container, me.getX(), me.getY()); + + return new mxPoint(point.x - this.first.x - this.graph.panDx, + point.y - this.first.y - this.graph.panDy); +}; + +/** + * Function: updateHint + * + * Hook for subclassers do show details while the handler is active. + */ +mxGraphHandler.prototype.updateHint = function(me) { }; + +/** + * Function: removeHint + * + * Hooks for subclassers to hide details when the handler gets inactive. + */ +mxGraphHandler.prototype.removeHint = function() { }; + +/** + * Function: roundLength + * + * Hook for rounding the unscaled vector. Allows for half steps in the raster so + * numbers coming in should be rounded if no half steps are allowed (ie for non + * aligned standard moving where pixel steps should be preferred). + */ +mxGraphHandler.prototype.roundLength = function(length) +{ + return Math.round(length * 100) / 100; +}; + +/** + * Function: isValidDropTarget + * + * Returns true if the given cell is a valid drop target. + */ +mxGraphHandler.prototype.isValidDropTarget = function(target, me) +{ + return this.graph.model.getParent(this.cell) != target; +}; + +/** + * Function: checkPreview + * + * Updates the preview if cloning state has changed. + */ +mxGraphHandler.prototype.checkPreview = function() +{ + if (this.livePreviewActive && this.cloning) + { + this.resetLivePreview(); + this.livePreviewActive = false; + } + else if (this.maxLivePreview >= this.cellCount && !this.livePreviewActive && this.allowLivePreview) + { + if (!this.cloning || !this.livePreviewActive) + { + this.livePreviewActive = true; + this.livePreviewUsed = true; + } + } + else if (!this.livePreviewUsed && this.shape == null) + { + this.shape = this.createPreviewShape(this.bounds); + } +}; + +/** + * Function: mouseMove + * + * Handles the event by highlighting possible drop targets and updating the + * preview. + */ +mxGraphHandler.prototype.mouseMove = function(sender, me) +{ + var graph = this.graph; + var tol = graph.tolerance; + + // Adds cell to selection and start moving cells + if (this.first == null && this.delayedSelection && this.cell != null && + this.mouseDownX != null && this.mouseDownY != null && + (Math.abs(this.mouseDownX - me.getX()) > tol || + Math.abs(this.mouseDownY - me.getY()) > tol)) + { + this.delayedSelection = false; + + if (!mxEvent.isAltDown(me.getEvent())) + { + graph.addSelectionCell(this.cell); + } + + this.start(this.cell, this.mouseDownX, this.mouseDownY, + graph.getMovableCells(graph.getSelectionCells())); + } + + var delta = (this.first != null) ? this.getDelta(me) : null; + + if (!me.isConsumed() && graph.isMouseDown && this.cell != null && + delta != null && this.bounds != null && !this.suspended) + { + // Stops moving if a multi touch event is received + if (mxEvent.isMultiTouchEvent(me.getEvent())) + { + this.reset(); + return; + } + + if (this.shape != null || this.livePreviewActive || this.cloning || + Math.abs(delta.x) > tol || Math.abs(delta.y) > tol) + { + // Highlight is used for highlighting drop targets + if (this.highlight == null) + { + this.highlight = new mxCellHighlight(this.graph, + mxConstants.DROP_TARGET_COLOR, 3); + } + + var clone = graph.isCloneEvent(me.getEvent()) && + graph.isCellsCloneable() && + this.isCloneEnabled(); + var gridEnabled = graph.isGridEnabledEvent(me.getEvent()); + var cell = me.getCell(); + cell = (cell != null && mxUtils.indexOf(this.cells, cell) < 0) ? cell : + graph.getCellAt(me.getGraphX(), me.getGraphY(), null, null, null, + mxUtils.bind(this, function(state, x, y) + { + return mxUtils.indexOf(this.cells, state.cell) >= 0; + })); + var hideGuide = true; + var target = null; + this.cloning = clone; + + 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, clone); + } + + var state = graph.getView().getState(target); + var highlight = false; + + if (state != null && (clone || this.isValidDropTarget(target, me))) + { + 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(); + } + + if (this.guide != null && this.useGuidesForEvent(me)) + { + delta = this.guide.move(this.bounds, delta, gridEnabled, clone); + hideGuide = false; + } + else + { + delta = this.graph.snapDelta(delta, this.bounds, !gridEnabled, false, false); + } + + if (this.guide != null && hideGuide) + { + this.guide.hide(); + } + + // Constrained movement if shift key is pressed + if (this.isConstrainedEvent(me)) + { + if (Math.abs(delta.x) > Math.abs(delta.y)) + { + delta.y = 0; + } + else + { + delta.x = 0; + } + } + + this.checkPreview(); + + if (this.currentDx != delta.x || this.currentDy != delta.y) + { + this.currentDx = delta.x; + this.currentDy = delta.y; + this.updatePreview(); + } + } + + this.updateHint(me); + this.consumeMouseEvent(mxEvent.MOUSE_MOVE, me); + + // 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 || me.sourceState != null) && !graph.isMouseDown) + { + var cursor = graph.getCursorForMouseEvent(me); + + 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; + } + } + + // Sets the cursor on the original source state under the mouse + // instead of the event source state which can be the parent + if (cursor != null && me.sourceState != null) + { + me.sourceState.setCursor(cursor); + } + } +}; + +/** + * Function: isConstrainedEvent + * + * Returns true if the given event is constrained. + */ +mxGraphHandler.prototype.isConstrainedEvent = function(me) +{ + return (this.target == null || this.graph.isCloneEvent(me.getEvent())) && + this.graph.isConstrainedEvent(me.getEvent()); +}; + +/** + * Function: updatePreview + * + * Updates the bounds of the preview shape. + */ +mxGraphHandler.prototype.updatePreview = function(remote) +{ + if (this.livePreviewUsed && !remote) + { + if (this.cells != null) + { + this.setHandlesVisibleForCells( + this.graph.selectionCellsHandler. + getHandledSelectionCells(), false); + this.updateLivePreview(this.currentDx, this.currentDy); + } + } + else + { + this.updatePreviewShape(); + } +}; + +/** + * Function: updatePreviewShape + * + * Updates the bounds of the preview shape. + */ +mxGraphHandler.prototype.updatePreviewShape = function() +{ + if (this.shape != null && this.pBounds != null) + { + this.shape.bounds = new mxRectangle(Math.round(this.pBounds.x + this.currentDx), + Math.round(this.pBounds.y + this.currentDy), this.pBounds.width, this.pBounds.height); + this.shape.redraw(); + } +}; + +/** + * Function: updateLivePreview + * + * Updates the bounds of the preview shape. + */ +mxGraphHandler.prototype.updateLivePreview = function(dx, dy) +{ + if (!this.suspended) + { + var states = []; + + if (this.allCells != null) + { + this.allCells.visit(mxUtils.bind(this, function(key, state) + { + var realState = this.graph.view.getState(state.cell); + + // Checks if cell was removed or replaced + if (realState != state) + { + state.destroy(); + + if (realState != null) + { + this.allCells.put(state.cell, realState); + } + else + { + this.allCells.remove(state.cell); + } + + state = realState; + } + + if (state != null) + { + // Saves current state + var tempState = state.clone(); + states.push([state, tempState]); + + // Makes transparent for events to detect drop targets + if (state.shape != null) + { + if (state.shape.originalPointerEvents == null) + { + state.shape.originalPointerEvents = state.shape.pointerEvents; + } + + state.shape.pointerEvents = false; + + if (state.text != null) + { + if (state.text.originalPointerEvents == null) + { + state.text.originalPointerEvents = state.text.pointerEvents; + } + + state.text.pointerEvents = false; + } + } + + // Temporarily changes position + if (this.graph.model.isVertex(state.cell)) + { + if (!this.cloning || this.graph.isCellCloneable(state.cell)) + { + state.x += dx; + state.y += dy; + } + + // Draws the live preview + if (!this.cloning) + { + state.view.graph.cellRenderer.redraw(state, true); + + // Forces redraw of connected edges after all states + // have been updated but avoids update of state + state.view.invalidate(state.cell); + state.invalid = false; + + // Hides folding icon + if (state.control != null && state.control.node != null) + { + state.control.node.style.visibility = 'hidden'; + } + } + // Clone live preview may use text bounds + else if (state.text != null) + { + state.text.updateBoundingBox(); + + // Fixes preview box for edge labels + if (state.text.boundingBox != null) + { + state.text.boundingBox.x += dx; + state.text.boundingBox.y += dy; + } + + if (state.text.unrotatedBoundingBox != null) + { + state.text.unrotatedBoundingBox.x += dx; + state.text.unrotatedBoundingBox.y += dy; + } + } + } + } + })); + } + + // Resets the handler if everything was removed + if (states.length == 0) + { + this.reset(); + } + else + { + // Redraws connected edges + var s = this.graph.view.scale; + + for (var i = 0; i < states.length; i++) + { + var state = states[i][0]; + + if (this.graph.model.isEdge(state.cell) && (!this.cloning || + this.graph.isCellCloneable(state.cell))) + { + var geometry = this.graph.getCellGeometry(state.cell); + var points = []; + + if (geometry != null && geometry.points != null) + { + for (var j = 0; j < geometry.points.length; j++) + { + if (geometry.points[j] != null) + { + points.push(new mxPoint( + geometry.points[j].x + dx / s, + geometry.points[j].y + dy / s)); + } + } + } + + var source = state.visibleSourceState; + var target = state.visibleTargetState; + var pts = states[i][1].absolutePoints; + + if (source == null || !this.isCellMoving(source.cell)) + { + var pt0 = pts[0]; + state.setAbsoluteTerminalPoint(new mxPoint(pt0.x + dx, pt0.y + dy), true); + source = null; + } + else + { + state.view.updateFixedTerminalPoint(state, source, true, + this.graph.getConnectionConstraint(state, source, true)); + } + + if (target == null || !this.isCellMoving(target.cell)) + { + var ptn = pts[pts.length - 1]; + state.setAbsoluteTerminalPoint(new mxPoint(ptn.x + dx, ptn.y + dy), false); + target = null; + } + else + { + state.view.updateFixedTerminalPoint(state, target, false, + this.graph.getConnectionConstraint(state, target, false)); + } + + state.view.updatePoints(state, points, source, target); + state.view.updateFloatingTerminalPoints(state, source, target); + state.view.updateEdgeLabelOffset(state); + state.invalid = false; + + // Draws the live preview but avoids update of state + if (!this.cloning) + { + state.view.graph.cellRenderer.redraw(state, true); + } + } + } + + this.graph.view.validate(); + this.redrawHandles(states); + this.resetPreviewStates(states); + } + } +}; + +/** + * Function: redrawHandles + * + * Redraws the preview shape for the given states array. + */ +mxGraphHandler.prototype.redrawHandles = function(states) +{ + for (var i = 0; i < states.length; i++) + { + var handler = this.graph.selectionCellsHandler.getHandler(states[i][0].cell); + + if (handler != null) + { + handler.redraw(true); + } + } +}; + +/** + * Function: resetPreviewStates + * + * Resets the given preview states array. + */ +mxGraphHandler.prototype.resetPreviewStates = function(states) +{ + for (var i = 0; i < states.length; i++) + { + states[i][0].setState(states[i][1]); + } +}; + +/** + * Function: suspend + * + * Suspends the livew preview. + */ +mxGraphHandler.prototype.suspend = function() +{ + if (!this.suspended) + { + if (this.livePreviewUsed) + { + this.updateLivePreview(0, 0); + } + + if (this.shape != null) + { + this.shape.node.style.visibility = 'hidden'; + } + + if (this.guide != null) + { + this.guide.setVisible(false); + } + + this.suspended = true; + } +}; + +/** + * Function: resume + * + * Suspends the livew preview. + */ +mxGraphHandler.prototype.resume = function() +{ + if (this.suspended) + { + this.suspended = null; + + if (this.livePreviewUsed) + { + this.livePreviewActive = true; + } + + if (this.shape != null) + { + this.shape.node.style.visibility = 'visible'; + } + + if (this.guide != null) + { + this.guide.setVisible(true); + } + } +}; + +/** + * Function: resetLivePreview + * + * Resets the livew preview. + */ +mxGraphHandler.prototype.resetLivePreview = function() +{ + if (this.allCells != null) + { + this.allCells.visit(mxUtils.bind(this, function(key, state) + { + // Restores event handling + if (state.shape != null && state.shape.originalPointerEvents != null) + { + state.shape.pointerEvents = state.shape.originalPointerEvents; + state.shape.originalPointerEvents = null; + + // Forces repaint even if not moved to update pointer events + state.shape.bounds = null; + + if (state.text != null) + { + state.text.pointerEvents = state.text.originalPointerEvents; + state.text.originalPointerEvents = null; + } + } + + // Shows folding icon + if (state.control != null && state.control.node != null && + state.control.node.style.visibility == 'hidden') + { + state.control.node.style.visibility = ''; + } + + // Fixes preview box for edge labels + if (!this.cloning) + { + if (state.text != null) + { + state.text.updateBoundingBox(); + } + } + + // Forces repaint of connected edges + state.view.invalidate(state.cell); + })); + + // Repaints all invalid states + this.graph.view.validate(); + } +}; + +/** + * Function: setHandlesVisibleForCells + * + * Sets wether the handles attached to the given cells are visible. + * + * Parameters: + * + * cells - Array of . + * visible - Boolean that specifies if the handles should be visible. + * force - Forces an update of the handler regardless of the last used value. + */ +mxGraphHandler.prototype.setHandlesVisibleForCells = function(cells, visible, force) +{ + if (force || this.handlesVisible != visible) + { + this.handlesVisible = visible; + + for (var i = 0; i < cells.length; i++) + { + var handler = this.graph.selectionCellsHandler.getHandler(cells[i]); + + if (handler != null) + { + handler.setHandlesVisible(visible); + + if (visible) + { + handler.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()) + { + if (this.livePreviewUsed) + { + this.resetLivePreview(); + } + + if (this.cell != null && this.first != null && + this.currentDx != null && this.currentDy != null && + (this.shape != null || this.livePreviewUsed || this.cloning)) + { + var graph = this.graph; + 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 scale = graph.getView().scale; + var dx = this.roundLength(this.currentDx / scale); + var dy = this.roundLength(this.currentDy / scale); + var target = this.target; + + if (graph.isSplitEnabled() && graph.isSplitTarget(target, this.cells, me.getEvent())) + { + graph.splitEdge(target, this.cells, null, dx, dy, + me.getGraphX(), me.getGraphY()); + } + else + { + this.moveCells(this.cells, dx, dy, this.cloning, this.target, me.getEvent()); + } + } + } + else if (this.isSelectEnabled() && this.delayedSelection && + !this.blockDelayedSelection && this.cell != null) + { + this.selectDelayed(me); + } + } + + // Consumes the event if a cell was initially clicked + if (this.cellWasClicked) + { + this.consumeMouseEvent(mxEvent.MOUSE_UP, me); + } + + this.reset(); +}; + +/** + * Function: reset + * + * Resets the state of this handler. + */ +mxGraphHandler.prototype.reset = function() +{ + if (this.livePreviewUsed) + { + this.resetLivePreview(); + this.setHandlesVisibleForCells( + this.graph.selectionCellsHandler. + getHandledSelectionCells(), true); + } + + this.destroyShapes(); + this.removeHint(); + + this.blockDelayedSelection = false; + this.delayedSelection = false; + this.livePreviewActive = null; + this.livePreviewUsed = null; + this.cellWasClicked = false; + this.suspended = null; + this.currentDx = null; + this.currentDy = null; + this.cellCount = null; + this.cloning = false; + this.allCells = null; + this.pBounds = null; + this.guides = null; + this.target = null; + this.first = null; + this.cells = null; + this.cell = 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); + + if (pState != null) + { + var pt = mxUtils.convertPoint(this.graph.container, + mxEvent.getClientX(evt), mxEvent.getClientY(evt)); + var alpha = mxUtils.toRadians(mxUtils.getValue(pState.style, mxConstants.STYLE_ROTATION) || 0); + + if (alpha != 0) + { + var cos = Math.cos(-alpha); + var sin = Math.sin(-alpha); + var cx = new mxPoint(pState.getCenterX(), pState.getCenterY()); + pt = mxUtils.getRotatedPoint(pt, cos, sin, cx); + } + + return !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 + var parent = this.graph.getModel().getParent(this.cell); + + // Handles transparent group being dragged via child cells + if (!this.graph.isCellSelected(this.cell) && this.graph.isCellSelected(parent)) + { + parent = this.graph.getModel().getParent(parent); + } + + if (target == null && evt != null && this.isRemoveCellsFromParent() && + this.shouldRemoveCellsFromParent(parent, cells, evt)) + { + target = this.graph.getDefaultParent(); + } + + // Cloning into locked cells is not allowed + clone = clone && !this.graph.isCellLocked(target || this.graph.getDefaultParent()); + + this.graph.getModel().beginUpdate(); + try + { + var parents = []; + + // Removes parent if all child cells are removed + if (!clone && target != null && this.removeEmptyParents) + { + // Collects all non-selected parents + var dict = new mxDictionary(); + + for (var i = 0; i < cells.length; i++) + { + dict.put(cells[i], true); + } + + // LATER: Recurse up the cell hierarchy + for (var i = 0; i < cells.length; i++) + { + var par = this.graph.model.getParent(cells[i]); + + if (par != null && !dict.get(par)) + { + dict.put(par, true); + parents.push(par); + } + } + } + + // 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, dy, clone, target, evt); + + // Removes parent if all child cells are removed + var temp = []; + + for (var i = 0; i < parents.length; i++) + { + if (this.shouldRemoveParent(parents[i])) + { + temp.push(parents[i]); + } + } + + this.graph.removeCells(temp, false); + } + finally + { + this.graph.getModel().endUpdate(); + } + + // Selects the new cells if cells have been cloned + if (clone) + { + this.graph.setSelectionCells(cells); + } + + if (this.isSelectEnabled() && this.scrollOnMove) + { + this.graph.scrollCellToVisible(cells[0]); + } +}; + +/** + * Function: shouldRemoveParent + * + * Returns true if the given parent should be removed after removal of child cells. + */ +mxGraphHandler.prototype.shouldRemoveParent = function(parent) +{ + var state = this.graph.view.getState(parent); + + return state != null && (this.graph.model.isEdge(state.cell) || this.graph.model.isVertex(state.cell)) && + this.graph.isCellDeletable(state.cell) && this.graph.model.getChildCount(state.cell) == 0 && + this.graph.isTransparentState(state); +}; + +/** + * 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); + + if (this.escapeHandler != null) + { + this.graph.removeListener(this.escapeHandler); + this.escapeHandler = null; + } + + if (this.refreshHandler != null) + { + this.graph.getModel().removeListener(this.refreshHandler); + this.graph.removeListener(this.refreshHandler); + this.refreshHandler = null; + } + + mxEvent.removeListener(document, 'keydown', this.keyHandler); + mxEvent.removeListener(document, 'keyup', this.keyHandler); + + this.destroyShapes(); + this.removeHint(); +}; diff --git a/src/main/mxgraph/handler/mxHandle.js b/src/main/mxgraph/handler/mxHandle.js new file mode 100644 index 000000000..560971a24 --- /dev/null +++ b/src/main/mxgraph/handler/mxHandle.js @@ -0,0 +1,352 @@ +/** + * Copyright (c) 2006-2015, JGraph Ltd + * Copyright (c) 2006-2015, Gaudenz Alder + */ +/** + * Class: mxHandle + * + * Implements a single custom handle for vertices. + * + * Constructor: mxHandle + * + * Constructs a new handle for the given state. + * + * Parameters: + * + * state - of the cell to be handled. + */ +function mxHandle(state, cursor, image, shape) +{ + this.graph = state.view.graph; + this.state = state; + this.cursor = (cursor != null) ? cursor : this.cursor; + this.image = (image != null) ? image : this.image; + this.shape = (shape != null) ? shape : null; + this.init(); +}; + +/** + * Variable: cursor + * + * Specifies the cursor to be used for this handle. Default is 'default'. + */ +mxHandle.prototype.cursor = 'default'; + +/** + * Variable: image + * + * Specifies the to be used to render the handle. Default is null. + */ +mxHandle.prototype.image = null; + +/** + * Variable: ignoreGrid + * + * Default is false. + */ +mxHandle.prototype.ignoreGrid = false; + +/** + * Function: getPosition + * + * Hook for subclassers to return the current position of the handle. + */ +mxHandle.prototype.getPosition = function(bounds) { }; + +/** + * Function: setPosition + * + * Hooks for subclassers to update the style in the . + */ +mxHandle.prototype.setPosition = function(bounds, pt, me) { }; + +/** + * Function: execute + * + * Hook for subclassers to execute the handle. + */ +mxHandle.prototype.execute = function(me) { }; + +/** + * Function: copyStyle + * + * Sets the cell style with the given name to the corresponding value in . + */ +mxHandle.prototype.copyStyle = function(key) +{ + this.graph.setCellStyles(key, this.state.style[key], [this.state.cell]); +}; + +/** + * Function: processEvent + * + * Processes the given and invokes . + */ +mxHandle.prototype.processEvent = function(me) +{ + var scale = this.graph.view.scale; + var tr = this.graph.view.translate; + var pt = new mxPoint(me.getGraphX() / scale - tr.x, me.getGraphY() / scale - tr.y); + + // Center shape on mouse cursor + if (this.shape != null && this.shape.bounds != null) + { + pt.x -= this.shape.bounds.width / scale / 4; + pt.y -= this.shape.bounds.height / scale / 4; + } + + // Snaps to grid for the rotated position then applies the rotation for the direction after that + var alpha1 = -mxUtils.toRadians(this.getRotation()); + var alpha2 = -mxUtils.toRadians(this.getTotalRotation()) - alpha1; + pt = this.flipPoint(this.rotatePoint(this.snapPoint(this.rotatePoint(pt, alpha1), + this.ignoreGrid || !this.graph.isGridEnabledEvent(me.getEvent())), alpha2)); + this.setPosition(this.state.getPaintBounds(), pt, me); + this.redraw(); +}; + +/** + * Function: positionChanged + * + * Should be called after in . + * This repaints the state using . + */ +mxHandle.prototype.positionChanged = function() +{ + if (this.state.text != null) + { + this.state.text.apply(this.state); + } + + if (this.state.shape != null) + { + this.graph.cellRenderer.configureShape(this.state); + } + + this.graph.cellRenderer.redraw(this.state, true); +}; + +/** + * Function: getRotation + * + * Returns the rotation defined in the style of the cell. + */ +mxHandle.prototype.getRotation = function() +{ + if (this.state.shape != null) + { + return this.state.shape.getRotation(); + } + + return 0; +}; + +/** + * Function: getTotalRotation + * + * Returns the rotation from the style and the rotation from the direction of + * the cell. + */ +mxHandle.prototype.getTotalRotation = function() +{ + if (this.state.shape != null) + { + return this.state.shape.getShapeRotation(); + } + + return 0; +}; + +/** + * Function: init + * + * Creates and initializes the shapes required for this handle. + */ +mxHandle.prototype.init = function() +{ + var html = this.isHtmlRequired(); + + if (this.image != null) + { + this.shape = new mxImageShape(new mxRectangle(0, 0, this.image.width, this.image.height), this.image.src); + this.shape.preserveImageAspect = false; + } + else if (this.shape == null) + { + this.shape = this.createShape(html); + } + + this.initShape(html); +}; + +/** + * Function: createShape + * + * Creates and returns the shape for this handle. + */ +mxHandle.prototype.createShape = function(html) +{ + var bounds = new mxRectangle(0, 0, mxConstants.HANDLE_SIZE, mxConstants.HANDLE_SIZE); + + return new mxRectangleShape(bounds, mxConstants.HANDLE_FILLCOLOR, mxConstants.HANDLE_STROKECOLOR); +}; + +/** + * Function: initShape + * + * Initializes and sets its cursor. + */ +mxHandle.prototype.initShape = function(html) +{ + if (html && this.shape.isHtmlAllowed()) + { + this.shape.dialect = mxConstants.DIALECT_STRICTHTML; + this.shape.init(this.graph.container); + } + else + { + this.shape.dialect = (this.graph.dialect != mxConstants.DIALECT_SVG) ? + mxConstants.DIALECT_MIXEDHTML : mxConstants.DIALECT_SVG; + + if (this.cursor != null) + { + this.shape.init(this.graph.getView().getOverlayPane()); + } + } + + mxEvent.redirectMouseEvents(this.shape.node, this.graph, this.state); + this.shape.node.style.cursor = this.cursor; +}; + +/** + * Function: redraw + * + * Renders the shape for this handle. + */ +mxHandle.prototype.redraw = function() +{ + if (this.shape != null && this.state.shape != null) + { + var pt = this.getPosition(this.state.getPaintBounds()); + + if (pt != null) + { + var alpha = mxUtils.toRadians(this.getTotalRotation()); + pt = this.rotatePoint(this.flipPoint(pt), alpha); + + var scale = this.graph.view.scale; + var tr = this.graph.view.translate; + this.shape.bounds.x = Math.floor((pt.x + tr.x) * scale - this.shape.bounds.width / 2); + this.shape.bounds.y = Math.floor((pt.y + tr.y) * scale - this.shape.bounds.height / 2); + + // Needed to force update of text bounds + this.shape.redraw(); + } + } +}; + +/** + * Function: isHtmlRequired + * + * Returns true if this handle should be rendered in HTML. This returns true if + * the text node is in the graph container. + */ +mxHandle.prototype.isHtmlRequired = function() +{ + return this.state.text != null && this.state.text.node.parentNode == this.graph.container; +}; + +/** + * Function: rotatePoint + * + * Rotates the point by the given angle. + */ +mxHandle.prototype.rotatePoint = function(pt, alpha) +{ + var bounds = this.state.getCellBounds(); + var cx = new mxPoint(bounds.getCenterX(), bounds.getCenterY()); + var cos = Math.cos(alpha); + var sin = Math.sin(alpha); + + return mxUtils.getRotatedPoint(pt, cos, sin, cx); +}; + +/** + * Function: flipPoint + * + * Flips the given point vertically and/or horizontally. + */ +mxHandle.prototype.flipPoint = function(pt) +{ + if (this.state.shape != null) + { + var bounds = this.state.getCellBounds(); + + if (this.state.shape.flipH) + { + pt.x = 2 * bounds.x + bounds.width - pt.x; + } + + if (this.state.shape.flipV) + { + pt.y = 2 * bounds.y + bounds.height - pt.y; + } + } + + return pt; +}; + +/** + * Function: snapPoint + * + * Snaps the given point to the grid if ignore is false. This modifies + * the given point in-place and also returns it. + */ +mxHandle.prototype.snapPoint = function(pt, ignore) +{ + if (!ignore) + { + pt.x = this.graph.snap(pt.x); + pt.y = this.graph.snap(pt.y); + } + + return pt; +}; + +/** + * Function: setVisible + * + * Shows or hides this handle. + */ +mxHandle.prototype.setVisible = function(visible) +{ + if (this.shape != null && this.shape.node != null) + { + this.shape.node.style.display = (visible) ? '' : 'none'; + } +}; + +/** + * Function: reset + * + * Resets the state of this handle by setting its visibility to true. + */ +mxHandle.prototype.reset = function() +{ + this.setVisible(true); + this.state.style = this.graph.getCellStyle(this.state.cell); + this.positionChanged(); +}; + +/** + * Function: destroy + * + * Destroys this handle. + */ +mxHandle.prototype.destroy = function() +{ + if (this.shape != null) + { + this.shape.destroy(); + this.shape = null; + } +}; diff --git a/src/main/mxgraph/handler/mxKeyHandler.js b/src/main/mxgraph/handler/mxKeyHandler.js new file mode 100644 index 000000000..6a391f04a --- /dev/null +++ b/src/main/mxgraph/handler/mxKeyHandler.js @@ -0,0 +1,428 @@ +/** + * Copyright (c) 2006-2015, JGraph Ltd + * Copyright (c) 2006-2015, Gaudenz Alder + */ +/** + * 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 + * 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 . + * + * 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 . + * 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 = []; + + this.keydownHandler = mxUtils.bind(this, function(evt) + { + this.keyDown(evt); + }); + + // Installs the keystroke listener in the target + mxEvent.addListener(this.target, 'keydown', this.keydownHandler); + + // Automatically deallocates memory in IE + if (mxClient.IS_IE) + { + mxEvent.addListener(window, 'unload', + mxUtils.bind(this, function() + { + this.destroy(); + }) + ); + } + } +}; + +/** + * Variable: graph + * + * Reference to the 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 + * . + */ +mxKeyHandler.prototype.isEnabled = function() +{ + return this.enabled; +}; + +/** + * Function: setEnabled + * + * Enables or disables event handling by updating . + * + * 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 . + * + * 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 && !mxEvent.isAltDown(evt)) + { + 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 , or the of the + * . + * + * 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 && this.graph.cellEditor.isEventSource(evt))) + { + return true; + } + + // Accepts events from inside the container + return mxUtils.isAncestorNode(this.graph.container, source); +}; + +/** + * Function: keyDown + * + * Handles the event by invoking the function bound to the respective keystroke + * if returns true for the given event and if + * returns false, except for escape for which + * is not invoked. + * + * Parameters: + * + * evt - Key event that represents the keystroke. + */ +mxKeyHandler.prototype.keyDown = function(evt) +{ + if (this.isEnabledForEvent(evt)) + { + // Cancels the editing if escape is pressed + if (evt.keyCode == 27 /* Escape */) + { + this.escape(evt); + } + + // Invokes the function for the keystroke + else if (!this.isEventIgnored(evt)) + { + var boundFunction = this.getFunction(evt); + + if (boundFunction != null) + { + boundFunction(evt); + mxEvent.consume(evt); + } + } + } +}; + +/** + * Function: isEnabledForEvent + * + * Returns true if the given event should be handled. is + * called later if the event is not an escape key stroke, in which case + * is called. This implementation returns true if + * returns true for both, this handler and , if the event is not + * consumed and if returns true. + * + * Parameters: + * + * evt - Key event that represents the keystroke. + */ +mxKeyHandler.prototype.isEnabledForEvent = function(evt) +{ + return (this.graph.isEnabled() && !mxEvent.isConsumed(evt) && + this.isGraphEvent(evt) && this.isEnabled()); +}; + +/** + * Function: isEventIgnored + * + * Returns true if the given keystroke should be ignored. This returns + * graph.isEditing(). + * + * Parameters: + * + * evt - Key event that represents the keystroke. + */ +mxKeyHandler.prototype.isEventIgnored = function(evt) +{ + return this.graph.isEditing(); +}; + +/** + * Function: escape + * + * Hook to process ESCAPE keystrokes. This implementation invokes + * 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() +{ + if (this.target != null && this.keydownHandler != null) + { + mxEvent.removeListener(this.target, 'keydown', this.keydownHandler); + this.keydownHandler = null; + } + + this.target = null; +}; diff --git a/src/main/mxgraph/handler/mxPanningHandler.js b/src/main/mxgraph/handler/mxPanningHandler.js new file mode 100644 index 000000000..26de99ecc --- /dev/null +++ b/src/main/mxgraph/handler/mxPanningHandler.js @@ -0,0 +1,499 @@ +/** + * Copyright (c) 2006-2015, JGraph Ltd + * Copyright (c) 2006-2015, Gaudenz Alder + */ +/** + * Class: mxPanningHandler + * + * Event handler that pans and creates popupmenus. To use the left + * mousebutton for panning without interfering with cell moving and + * resizing, use and . For grid size + * steps while panning, use . This handler is built-into + * and enabled using . + * + * Constructor: mxPanningHandler + * + * Constructs an event handler that creates a + * and pans the graph. + * + * Event: mxEvent.PAN_START + * + * Fires when the panning handler changes its state to true. The + * event property contains the corresponding . + * + * Event: mxEvent.PAN + * + * Fires while handle is processing events. The event property contains + * the corresponding . + * + * Event: mxEvent.PAN_END + * + * Fires when the panning handler changes its state to false. The + * event property contains the corresponding . + */ +function mxPanningHandler(graph) +{ + if (graph != null) + { + this.graph = graph; + this.graph.addMouseListener(this); + + // Handles force panning event + this.forcePanningHandler = mxUtils.bind(this, function(sender, evt) + { + var evtName = evt.getProperty('eventName'); + var me = evt.getProperty('event'); + + if (evtName == mxEvent.MOUSE_DOWN && this.isForcePanningEvent(me)) + { + this.start(me); + this.active = true; + this.fireEvent(new mxEventObject(mxEvent.PAN_START, 'event', me)); + me.consume(); + } + }); + + this.graph.addListener(mxEvent.FIRE_MOUSE_EVENT, this.forcePanningHandler); + + // Handles pinch gestures + this.gestureHandler = mxUtils.bind(this, function(sender, eo) + { + if (this.isPinchEnabled()) + { + var evt = eo.getProperty('event'); + + if (!mxEvent.isConsumed(evt) && evt.type == 'gesturestart') + { + this.initialScale = this.graph.view.scale; + + // Forces start of panning when pinch gesture starts + if (!this.active && this.mouseDownEvent != null) + { + this.start(this.mouseDownEvent); + this.mouseDownEvent = null; + } + } + else if (evt.type == 'gestureend' && this.initialScale != null) + { + this.initialScale = null; + } + + if (this.initialScale != null) + { + this.zoomGraph(evt); + } + } + }); + + this.graph.addListener(mxEvent.GESTURE, this.gestureHandler); + + this.mouseUpListener = mxUtils.bind(this, function() + { + if (this.active) + { + this.reset(); + } + }); + + // Stops scrolling on every mouseup anywhere in the + // document and when the mouse leaves the window + mxEvent.addGestureListeners(document, null, null, this.mouseUpListener); + mxEvent.addListener(document, 'mouseleave',this.mouseUpListener); + } +}; + +/** + * Extends mxEventSource. + */ +mxPanningHandler.prototype = new mxEventSource(); +mxPanningHandler.prototype.constructor = mxPanningHandler; + +/** + * Variable: graph + * + * Reference to the enclosing . + */ +mxPanningHandler.prototype.graph = null; + +/** + * Variable: useLeftButtonForPanning + * + * Specifies if panning should be active for the left mouse button. + * Setting this to true may conflict with . Default is false. + */ +mxPanningHandler.prototype.useLeftButtonForPanning = false; + +/** + * Variable: usePopupTrigger + * + * Specifies if should also be used for panning. + */ +mxPanningHandler.prototype.usePopupTrigger = 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; + +/** + * Variable: pinchEnabled + * + * Specifies if pinch gestures should be handled as zoom. Default is true. + */ +mxPanningHandler.prototype.pinchEnabled = true; + +/** + * Variable: maxScale + * + * Specifies the maximum scale. Default is 8. + */ +mxPanningHandler.prototype.maxScale = 8; + +/** + * Variable: minScale + * + * Specifies the minimum scale. Default is 0.01. + */ +mxPanningHandler.prototype.minScale = 0.01; + +/** + * Variable: dx + * + * Holds the current horizontal offset. + */ +mxPanningHandler.prototype.dx = null; + +/** + * Variable: dy + * + * Holds the current vertical offset. + */ +mxPanningHandler.prototype.dy = null; + +/** + * Variable: startX + * + * Holds the x-coordinate of the start point. + */ +mxPanningHandler.prototype.startX = 0; + +/** + * Variable: startY + * + * Holds the y-coordinate of the start point. + */ +mxPanningHandler.prototype.startY = 0; + +/** + * Function: isActive + * + * Returns true if the handler is currently active. + */ +mxPanningHandler.prototype.isActive = function() +{ + return this.active || this.initialScale != null; +}; + +/** + * Function: isPanningEnabled + * + * Returns . + */ +mxPanningHandler.prototype.isPanningEnabled = function() +{ + return this.panningEnabled; +}; + +/** + * Function: setPanningEnabled + * + * Sets . + */ +mxPanningHandler.prototype.setPanningEnabled = function(value) +{ + this.panningEnabled = value; +}; + +/** + * Function: isPinchEnabled + * + * Returns . + */ +mxPanningHandler.prototype.isPinchEnabled = function() +{ + return this.pinchEnabled; +}; + +/** + * Function: setPinchEnabled + * + * Sets . + */ +mxPanningHandler.prototype.setPinchEnabled = function(value) +{ + this.pinchEnabled = value; +}; + +/** + * 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 + * is true and the event is a popup trigger. + */ +mxPanningHandler.prototype.isPanningTrigger = function(me) +{ + var evt = me.getEvent(); + + return (this.useLeftButtonForPanning && me.getState() == null && + mxEvent.isLeftMouseButton(evt)) || (mxEvent.isControlDown(evt) && + mxEvent.isShiftDown(evt)) || (this.usePopupTrigger && mxEvent.isPopupTrigger(evt)); +}; + +/** + * Function: isForcePanningEvent + * + * Returns true if the given should start panning. This + * implementation always returns true if is true or for + * multi touch events. + */ +mxPanningHandler.prototype.isForcePanningEvent = function(me) +{ + return this.ignoreCell || mxEvent.isMultiTouchEvent(me.getEvent()); +}; + +/** + * 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) +{ + this.mouseDownEvent = me; + + if (!me.isConsumed() && this.isPanningEnabled() && + !this.active && this.isPanningTrigger(me)) + { + this.start(me); + this.consumePanningTrigger(me); + } +}; + +/** + * Function: start + * + * Starts panning at the given event. + */ +mxPanningHandler.prototype.start = function(me) +{ + this.dx0 = -this.graph.container.scrollLeft; + this.dy0 = -this.graph.container.scrollTop; + + // Stores the location of the trigger event + this.startX = me.getX(); + this.startY = me.getY(); + this.dx = null; + this.dy = null; + + this.panningTrigger = true; +}; + +/** + * Function: consumePanningTrigger + * + * Consumes the given if it was a panning trigger in + * . The default is to invoke . 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) +{ + this.dx = me.getX() - this.startX; + this.dy = me.getY() - this.startY; + + if (this.active) + { + if (this.previewEnabled) + { + // Applies the grid to the panning steps + if (this.useGrid) + { + this.dx = this.graph.snap(this.dx); + this.dy = this.graph.snap(this.dy); + } + + this.graph.panGraph(this.dx + this.dx0, this.dy + this.dy0); + } + + this.fireEvent(new mxEventObject(mxEvent.PAN, 'event', me)); + } + 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(this.dx) > this.graph.tolerance || Math.abs(this.dy) > this.graph.tolerance; + + if (!tmp && this.active) + { + this.fireEvent(new mxEventObject(mxEvent.PAN_START, 'event', me)); + } + } + + if (this.active || this.panningTrigger) + { + me.consume(); + } +}; + +/** + * Function: mouseUp + * + * Handles the event by setting the translation on the view or showing the + * popupmenu. + */ +mxPanningHandler.prototype.mouseUp = function(sender, me) +{ + if (this.active) + { + if (this.dx != null && this.dy != null) + { + // Ignores if scrollbars have been used for panning + if (!this.graph.useScrollbarsForPanning || !mxUtils.hasScrollbars(this.graph.container)) + { + var scale = this.graph.getView().scale; + var t = this.graph.getView().translate; + this.graph.panGraph(0, 0); + this.panGraph(t.x + this.dx / scale, t.y + this.dy / scale); + } + + me.consume(); + } + + this.fireEvent(new mxEventObject(mxEvent.PAN_END, 'event', me)); + } + + this.reset(); +}; + +/** + * Function: zoomGraph + * + * Zooms the graph to the given value and consumed the event if needed. + */ +mxPanningHandler.prototype.zoomGraph = function(evt) +{ + var value = Math.round(this.initialScale * evt.scale * 100) / 100; + + if (this.minScale != null) + { + value = Math.max(this.minScale, value); + } + + if (this.maxScale != null) + { + value = Math.min(this.maxScale, value); + } + + if (this.graph.view.scale != value) + { + this.graph.zoomTo(value); + mxEvent.consume(evt); + } +}; + +/** + * Function: reset + * + * Resets the state of this handler. + */ +mxPanningHandler.prototype.reset = function() +{ + this.graph.isMouseDown = false + this.panningTrigger = false; + this.mouseDownEvent = null; + this.active = false; + this.dx = null; + this.dy = null; +}; + +/** + * Function: panGraph + * + * Pans 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); + this.graph.removeListener(this.forcePanningHandler); + this.graph.removeListener(this.gestureHandler); + mxEvent.removeGestureListeners(document, null, null, this.mouseUpListener); + mxEvent.removeListener(document, 'mouseleave',this.mouseUpListener); +}; diff --git a/src/main/mxgraph/handler/mxPopupMenuHandler.js b/src/main/mxgraph/handler/mxPopupMenuHandler.js new file mode 100644 index 000000000..285c84993 --- /dev/null +++ b/src/main/mxgraph/handler/mxPopupMenuHandler.js @@ -0,0 +1,229 @@ +/** + * Copyright (c) 2006-2015, JGraph Ltd + * Copyright (c) 2006-2015, Gaudenz Alder + */ +/** + * Class: mxPopupMenuHandler + * + * Event handler that creates popupmenus. + * + * Constructor: mxPopupMenuHandler + * + * Constructs an event handler that creates a . + */ +function mxPopupMenuHandler(graph, factoryMethod) +{ + if (graph != null) + { + this.graph = graph; + this.factoryMethod = factoryMethod; + this.graph.addMouseListener(this); + + // Does not show menu if any touch gestures take place after the trigger + this.gestureHandler = mxUtils.bind(this, function(sender, eo) + { + this.inTolerance = false; + }); + + this.graph.addListener(mxEvent.GESTURE, this.gestureHandler); + + this.init(); + } +}; + +/** + * Extends mxPopupMenu. + */ +mxPopupMenuHandler.prototype = new mxPopupMenu(); +mxPopupMenuHandler.prototype.constructor = mxPopupMenuHandler; + +/** + * Variable: graph + * + * Reference to the enclosing . + */ +mxPopupMenuHandler.prototype.graph = null; + +/** + * Variable: selectOnPopup + * + * Specifies if cells should be selected if a popupmenu is displayed for + * them. Default is true. + */ +mxPopupMenuHandler.prototype.selectOnPopup = true; + +/** + * Variable: clearSelectionOnBackground + * + * Specifies if cells should be deselected if a popupmenu is displayed for + * the diagram background. Default is true. + */ +mxPopupMenuHandler.prototype.clearSelectionOnBackground = true; + +/** + * Variable: triggerX + * + * X-coordinate of the mouse down event. + */ +mxPopupMenuHandler.prototype.triggerX = null; + +/** + * Variable: triggerY + * + * Y-coordinate of the mouse down event. + */ +mxPopupMenuHandler.prototype.triggerY = null; + +/** + * Variable: screenX + * + * Screen X-coordinate of the mouse down event. + */ +mxPopupMenuHandler.prototype.screenX = null; + +/** + * Variable: screenY + * + * Screen Y-coordinate of the mouse down event. + */ +mxPopupMenuHandler.prototype.screenY = null; + +/** + * Function: init + * + * Initializes the shapes required for this vertex handler. + */ +mxPopupMenuHandler.prototype.init = function() +{ + // Supercall + mxPopupMenu.prototype.init.apply(this); + + // Hides the tooltip if the mouse is over + // the context menu + mxEvent.addGestureListeners(this.div, mxUtils.bind(this, function(evt) + { + this.graph.tooltipHandler.hide(); + })); +}; + +/** + * Function: isSelectOnPopup + * + * Hook for returning if a cell should be selected for a given . + * This implementation returns . + */ +mxPopupMenuHandler.prototype.isSelectOnPopup = function(me) +{ + return this.selectOnPopup; +}; + +/** + * Function: mouseDown + * + * Handles the event by initiating the panning. By consuming the event all + * subsequent events of the gesture are redirected to this handler. + */ +mxPopupMenuHandler.prototype.mouseDown = function(sender, me) +{ + if (this.isEnabled() && !mxEvent.isMultiTouchEvent(me.getEvent())) + { + // Hides the popupmenu if is is being displayed + this.hideMenu(); + this.triggerX = me.getGraphX(); + this.triggerY = me.getGraphY(); + this.screenX = mxEvent.getMainEvent(me.getEvent()).screenX; + this.screenY = mxEvent.getMainEvent(me.getEvent()).screenY; + this.popupTrigger = this.isPopupTrigger(me); + this.inTolerance = true; + } +}; + +/** + * Function: mouseMove + * + * Handles the event by updating the panning on the graph. + */ +mxPopupMenuHandler.prototype.mouseMove = function(sender, me) +{ + // Popup trigger may change on mouseUp so ignore it + if (this.inTolerance && this.screenX != null && this.screenY != null) + { + if (Math.abs(mxEvent.getMainEvent(me.getEvent()).screenX - this.screenX) > this.graph.tolerance || + Math.abs(mxEvent.getMainEvent(me.getEvent()).screenY - this.screenY) > this.graph.tolerance) + { + this.inTolerance = false; + } + } +}; + +/** + * Function: mouseUp + * + * Handles the event by setting the translation on the view or showing the + * popupmenu. + */ +mxPopupMenuHandler.prototype.mouseUp = function(sender, me, popup) +{ + var doConsume = popup == null; + + popup = (popup != null) ? popup : mxUtils.bind(this, function(cell) + { + var origin = mxUtils.getScrollOrigin(); + this.popup(me.getX() + origin.x + 1, me.getY() + origin.y + 1, cell, me.getEvent()); + }); + + if (this.popupTrigger && this.inTolerance && this.triggerX != null && this.triggerY != null) + { + var cell = this.getCellForPopupEvent(me); + + // Selects the cell for which the context menu is being displayed + if (this.graph.isEnabled() && this.isSelectOnPopup(me) && + 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(); + + // Menu is shifted by 1 pixel so that the mouse up event + // is routed via the underlying shape instead of the DIV + popup(cell); + + if (doConsume) + { + me.consume(); + } + } + + this.popupTrigger = false; + this.inTolerance = false; +}; + +/** + * Function: getCellForPopupEvent + * + * Hook to return the cell for the mouse up popup trigger handling. + */ +mxPopupMenuHandler.prototype.getCellForPopupEvent = function(me) +{ + return me.getCell(); +}; + +/** + * Function: destroy + * + * Destroys the handler and all its resources and DOM nodes. + */ +mxPopupMenuHandler.prototype.destroy = function() +{ + this.graph.removeMouseListener(this); + this.graph.removeListener(this.gestureHandler); + + // Supercall + mxPopupMenu.prototype.destroy.apply(this); +}; diff --git a/src/main/mxgraph/handler/mxRubberband.js b/src/main/mxgraph/handler/mxRubberband.js new file mode 100644 index 000000000..cdf57d0a4 --- /dev/null +++ b/src/main/mxgraph/handler/mxRubberband.js @@ -0,0 +1,429 @@ +/** + * Copyright (c) 2006-2016, JGraph Ltd + * Copyright (c) 2006-2016, Gaudenz Alder + */ +/** + * Class: mxRubberband + * + * Event handler that selects rectangular regions. This is not built-into + * . 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); + + // Handles force rubberband event + this.forceRubberbandHandler = mxUtils.bind(this, function(sender, evt) + { + var evtName = evt.getProperty('eventName'); + var me = evt.getProperty('event'); + + if (evtName == mxEvent.MOUSE_DOWN && this.isForceRubberbandEvent(me)) + { + 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); + me.consume(false); + } + }); + + this.graph.addListener(mxEvent.FIRE_MOUSE_EVENT, this.forceRubberbandHandler); + + // Repaints the marquee after autoscroll + this.panHandler = mxUtils.bind(this, function() + { + this.repaint(); + }); + + this.graph.addListener(mxEvent.PAN, this.panHandler); + + // Does not show menu if any touch gestures take place after the trigger + this.gestureHandler = mxUtils.bind(this, function(sender, eo) + { + if (this.first != null) + { + this.reset(); + } + }); + + this.graph.addListener(mxEvent.GESTURE, this.gestureHandler); + + // 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 . + */ +mxRubberband.prototype.currentX = 0; + +/** + * Variable: currentY + * + * Holds the value of the y argument in the last call to . + */ +mxRubberband.prototype.currentY = 0; + +/** + * Variable: fadeOut + * + * Optional fade out effect. Default is false. + */ +mxRubberband.prototype.fadeOut = false; + +/** + * Function: isEnabled + * + * Returns true if events are handled. This implementation returns + * . + */ +mxRubberband.prototype.isEnabled = function() +{ + return this.enabled; +}; + +/** + * Function: setEnabled + * + * Enables or disables event handling. This implementation updates + * . + */ +mxRubberband.prototype.setEnabled = function(enabled) +{ + this.enabled = enabled; +}; + +/** + * Function: isForceRubberbandEvent + * + * Returns true if the given should start rubberband selection. + * This implementation returns true if the alt key is pressed. + */ +mxRubberband.prototype.isForceRubberbandEvent = function(me) +{ + return mxEvent.isAltDown(me.getEvent()) && !mxEvent.isShiftDown(me.getEvent()); +}; + +/** + * 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() && + me.getState() == null && !mxEvent.isMultiTouchEvent(me.getEvent())) + { + 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); + + // 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); + + 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)); + }); + + // Workaround for rubberband stopping if the mouse leaves the container in Firefox + if (mxClient.IS_FF) + { + mxEvent.addGestureListeners(document, null, this.dragHandler, this.dropHandler); + } +}; + +/** + * 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); + var result = this.sharedDiv; + + if (mxClient.IS_SVG && (!mxClient.IS_IE || document.documentMode >= 10) && this.fadeOut) + { + this.sharedDiv = null; + } + + return result; +}; + +/** + * Function: isActive + * + * Returns true if this handler is active. + */ +mxRubberband.prototype.isActive = function(sender, me) +{ + return this.div != null && this.div.style.display != 'none'; +}; + +/** + * Function: mouseUp + * + * Handles the event by selecting the region of the rubberband using + * . + */ +mxRubberband.prototype.mouseUp = function(sender, me) +{ + var active = this.isActive(); + this.reset(); + + if (active) + { + this.execute(me.getEvent()); + me.consume(); + } +}; + +/** + * Function: execute + * + * Resets the state of this handler and selects the current region + * for the given event. + */ +mxRubberband.prototype.execute = function(evt) +{ + var rect = new mxRectangle(this.x, this.y, this.width, this.height); + this.graph.selectRegion(rect, evt); +}; + +/** + * Function: reset + * + * Resets the state of the rubberband selection. + */ +mxRubberband.prototype.reset = function() +{ + if (this.div != null) + { + if (mxClient.IS_SVG && (!mxClient.IS_IE || document.documentMode >= 10) && this.fadeOut) + { + var temp = this.div; + mxUtils.setPrefixedStyle(temp.style, 'transition', 'all 0.2s linear'); + temp.style.pointerEvents = 'none'; + temp.style.opacity = 0; + + window.setTimeout(function() + { + temp.parentNode.removeChild(temp); + }, 200); + } + else + { + this.div.parentNode.removeChild(this.div); + } + } + + mxEvent.removeGestureListeners(document, null, this.dragHandler, this.dropHandler); + this.dragHandler = null; + this.dropHandler = null; + + this.currentX = 0; + this.currentY = 0; + this.first = null; + this.div = null; +}; + +/** + * Function: update + * + * Sets and and calls . + */ +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
. + */ +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 = 0; + var dy = 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.forceRubberbandHandler); + this.graph.removeListener(this.panHandler); + this.reset(); + + if (this.sharedDiv != null) + { + this.sharedDiv = null; + } + } +}; diff --git a/src/main/mxgraph/handler/mxSelectionCellsHandler.js b/src/main/mxgraph/handler/mxSelectionCellsHandler.js new file mode 100644 index 000000000..b3b768cd6 --- /dev/null +++ b/src/main/mxgraph/handler/mxSelectionCellsHandler.js @@ -0,0 +1,369 @@ +/** + * Copyright (c) 2006-2015, JGraph Ltd + * Copyright (c) 2006-2015, Gaudenz Alder + */ +/** + * 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 state + * property contains the that has been added. + * + * Event: mxEvent.REMOVE + * + * Fires if a cell has been remove from the selection. The state + * property contains the that has been removed. + * + * Parameters: + * + * graph - Reference to the enclosing . + */ +function mxSelectionCellsHandler(graph) +{ + mxEventSource.call(this); + + this.graph = graph; + this.handlers = new mxDictionary(); + this.graph.addMouseListener(this); + + this.redrawHandler = mxUtils.bind(this, function(sender, evt) + { + if (this.isEnabled()) + { + this.refresh(false); + } + }); + + this.refreshHandler = mxUtils.bind(this, function(sender, evt) + { + if (this.isEnabled()) + { + this.refresh(true); + } + }); + + this.graph.addListener(mxEvent.EDITING_STOPPED, this.redrawHandler); + this.graph.addListener(mxEvent.EDITING_STARTED, this.redrawHandler); + this.graph.getSelectionModel().addListener(mxEvent.CHANGE, this.redrawHandler); + 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. + */ +mxUtils.extend(mxSelectionCellsHandler, mxEventSource); + +/** + * Variable: graph + * + * Reference to the enclosing . + */ +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 + * + * that maps from cells to handlers. + */ +mxSelectionCellsHandler.prototype.handlers = null; + +/** + * Function: isEnabled + * + * Returns . + */ +mxSelectionCellsHandler.prototype.isEnabled = function() +{ + return this.enabled; +}; + +/** + * Function: setEnabled + * + * Sets . + */ +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: isHandled + * + * Returns true if the given cell has a handler. + */ +mxSelectionCellsHandler.prototype.isHandled = function(cell) +{ + return this.getHandler(cell) != null; +}; + +/** + * Function: reset + * + * Resets all handlers. + */ +mxSelectionCellsHandler.prototype.reset = function() +{ + this.handlers.visit(function(key, handler) + { + handler.reset.apply(handler); + }); +}; + +/** + * Function: getHandledSelectionCells + * + * Reloads or updates all handlers. + */ +mxSelectionCellsHandler.prototype.getHandledSelectionCells = function() +{ + return this.graph.getSelectionCells(); +}; + +/** + * Function: refresh + * + * Reloads or updates all handlers. + */ +mxSelectionCellsHandler.prototype.refresh = function(refreshHandlers) +{ + // Removes all existing handlers + var oldHandlers = this.handlers; + this.handlers = new mxDictionary(); + + // Creates handles for all selection cells + var tmp = mxUtils.sortCells(this.getHandledSelectionCells(), false); + + // Forces refresh if old/new count is below/above max cells + if (!refreshHandlers && this.graph.graphHandler.maxCells > 0 && + this.graph.getSelectionCount() > 0) + { + var oldCount = oldHandlers.getCount(); + + if (oldCount > 0) + { + refreshHandlers = (oldCount <= this.graph.graphHandler.maxCells) != + (this.graph.getSelectionCount() <= this.graph.graphHandler.maxCells); + } + } + + // Destroys or updates old handlers + 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 if (!this.isHandlerActive(handler)) + { + if (refreshHandlers) + { + handler.refresh(); + } + + handler.redraw(); + } + } + + if (handler != null) + { + this.handlers.put(tmp[i], handler); + } + } + } + + // Destroys unused handlers + oldHandlers.visit(mxUtils.bind(this, function(key, handler) + { + this.fireEvent(new mxEventObject(mxEvent.REMOVE, 'state', handler.state)); + handler.destroy(); + })); + + // Creates new handlers and updates parent highlight on existing handlers + for (var i = 0; i < tmp.length; i++) + { + var state = this.graph.view.getState(tmp[i]); + + if (state != null) + { + var handler = this.handlers.get(tmp[i]); + + if (handler == null) + { + handler = this.graph.createHandler(state); + this.fireEvent(new mxEventObject(mxEvent.ADD, 'state', state)); + this.handlers.put(tmp[i], handler); + } + else + { + handler.updateParentHighlight(); + } + } + } +}; + +/** + * Function: isHandlerActive + * + * Returns true if the given handler is active and should not be redrawn. + */ +mxSelectionCellsHandler.prototype.isHandlerActive = function(handler) +{ + return handler.index != null; +}; + +/** + * Function: updateHandler + * + * Updates the handler for the given shape if one exists. + */ +mxSelectionCellsHandler.prototype.updateHandler = function(state) +{ + var handler = this.handlers.remove(state.cell); + + if (handler != null) + { + // Transfers the current state to the new handler + var index = handler.index; + var x = handler.startX; + var y = handler.startY; + + handler.destroy(); + handler = this.graph.createHandler(state); + + if (handler != null) + { + this.handlers.put(state.cell, handler); + + if (index != null && x != null && y != null) + { + handler.start(x, y, index); + } + } + } +}; + +/** + * 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.redrawHandler); + this.graph.getModel().removeListener(this.refreshHandler); + this.graph.getView().removeListener(this.refreshHandler); + this.graph.removeListener(this.redrawHandler); + this.redrawHandler = null; + this.refreshHandler = null; + } +}; diff --git a/src/main/mxgraph/handler/mxTooltipHandler.js b/src/main/mxgraph/handler/mxTooltipHandler.js new file mode 100644 index 000000000..ee363405f --- /dev/null +++ b/src/main/mxgraph/handler/mxTooltipHandler.js @@ -0,0 +1,355 @@ +/** + * Copyright (c) 2006-2015, JGraph Ltd + * Copyright (c) 2006-2015, Gaudenz Alder + */ +/** + * Class: mxTooltipHandler + * + * Graph event handler that displays tooltips. is used to + * get the tooltip for a cell or handle. This handler is built-into + * and enabled using . + * + * 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 . + * 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 . + */ +mxTooltipHandler.prototype.graph = null; + +/** + * Variable: delay + * + * Delay to show the tooltip in milliseconds. Default is 500. + */ +mxTooltipHandler.prototype.delay = null; + +/** + * Variable: ignoreTouchEvents + * + * Specifies if touch and pen events should be ignored. Default is true. + */ +mxTooltipHandler.prototype.ignoreTouchEvents = true; + +/** + * 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: destroyed + * + * True if this handler was destroyed using . + */ +mxTooltipHandler.prototype.destroyed = 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 . + */ +mxTooltipHandler.prototype.isEnabled = function() +{ + return this.enabled; +}; + +/** + * Function: setEnabled + * + * Enables or disables event handling. This implementation + * updates . + */ +mxTooltipHandler.prototype.setEnabled = function(enabled) +{ + this.enabled = enabled; +}; + +/** + * Function: isHideOnHover + * + * Returns . + */ +mxTooltipHandler.prototype.isHideOnHover = function() +{ + return this.hideOnHover; +}; + +/** + * Function: setHideOnHover + * + * Sets . + */ +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'; + + document.body.appendChild(this.div); + + mxEvent.addGestureListeners(this.div, mxUtils.bind(this, function(evt) + { + var source = mxEvent.getSource(evt); + + if (source.nodeName != 'A') + { + this.hideTooltip(); + } + })); + } +}; + +/** + * Function: getStateForEvent + * + * Returns the to be used for showing a tooltip for this event. + */ +mxTooltipHandler.prototype.getStateForEvent = function(me) +{ + return me.getState(); +}; + +/** + * 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); + var state = this.getStateForEvent(me); + + if (this.isHideOnHover() || state != this.state || (me.getSource() != this.node && + (!this.stateSource || (state != null && this.stateSource == + (me.isSource(state.shape) || !me.isSource(state.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, state) +{ + if (!this.ignoreTouchEvents || mxEvent.isMouseEvent(me.getEvent())) + { + this.resetTimer(); + state = (state != null) ? state : this.getStateForEvent(me); + + if (restart && this.isEnabled() && state != null && (this.div == null || + this.div.style.visibility == 'hidden')) + { + 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.popupMenuHandler.isMenuShowing() && !this.graph.isMouseDown) + { + // 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'; + this.div.innerText = ''; + } +}; + +/** + * 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 (!this.destroyed && tip != null && tip.length > 0) + { + // Initializes the DOM nodes if required + if (this.div == null) + { + this.init(); + } + + var origin = mxUtils.getScrollOrigin(); + + this.div.style.zIndex = this.zIndex; + 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.style.whiteSpace = 'pre-line'; + this.div.innerHTML = tip; + } + else + { + this.div.style.whiteSpace = ''; + this.div.innerText = ''; + 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() +{ + if (!this.destroyed) + { + this.graph.removeMouseListener(this); + mxEvent.release(this.div); + + if (this.div != null && this.div.parentNode != null) + { + this.div.parentNode.removeChild(this.div); + } + + this.destroyed = true; + this.div = null; + } +}; diff --git a/src/main/mxgraph/handler/mxVertexHandler.js b/src/main/mxgraph/handler/mxVertexHandler.js new file mode 100644 index 000000000..489461674 --- /dev/null +++ b/src/main/mxgraph/handler/mxVertexHandler.js @@ -0,0 +1,2369 @@ +/** + * Copyright (c) 2006-2015, JGraph Ltd + * Copyright (c) 2006-2015, Gaudenz Alder + */ +/** + * Class: mxVertexHandler + * + * Event handler for resizing cells. This handler is automatically created in + * . + * + * Constructor: mxVertexHandler + * + * Constructs an event handler that allows to resize vertices + * and groups. + * + * Parameters: + * + * state - of the cell to be resized. + */ +function mxVertexHandler(state) +{ + if (state != null) + { + this.state = state; + this.init(); + + // Handles escape keystrokes + this.escapeHandler = mxUtils.bind(this, function(sender, evt) + { + if (this.livePreview && this.index != null) + { + // Redraws the live preview + this.state.view.graph.cellRenderer.redraw(this.state, true); + + // Redraws connected edges + this.state.view.invalidate(this.state.cell); + this.state.invalid = false; + this.state.view.validate(); + } + + this.reset(); + }); + + this.state.view.graph.addListener(mxEvent.ESCAPE, this.escapeHandler); + } +}; + +/** + * Variable: graph + * + * Reference to the enclosing . + */ +mxVertexHandler.prototype.graph = null; + +/** + * Variable: state + * + * Reference to the 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 or + * if > 0. Default is true. + */ +mxVertexHandler.prototype.allowHandleBoundsCheck = true; + +/** + * Variable: handleImage + * + * Optional to be used as handles. Default is null. + */ +mxVertexHandler.prototype.handleImage = null; + +/** + * Variable: handlesVisible + * + * If handles are currently visible. + */ +mxVertexHandler.prototype.handlesVisible = true; + +/** + * Variable: tolerance + * + * Optional tolerance for hit-detection in . Default is 0. + */ +mxVertexHandler.prototype.tolerance = 0; + +/** + * Variable: rotationEnabled + * + * Specifies if a rotation handle should be visible. Default is false. + */ +mxVertexHandler.prototype.rotationEnabled = false; + +/** + * Variable: parentHighlightEnabled + * + * Specifies if the parent should be highlighted if a child cell is selected. + * Default is false. + */ +mxVertexHandler.prototype.parentHighlightEnabled = false; + +/** + * Variable: rotationRaster + * + * Specifies if rotation steps should be "rasterized" depening on the distance + * to the handle. Default is true. + */ +mxVertexHandler.prototype.rotationRaster = true; + +/** + * Variable: rotationCursor + * + * Specifies the cursor for the rotation handle. Default is 'crosshair'. + */ +mxVertexHandler.prototype.rotationCursor = 'crosshair'; + +/** + * Variable: livePreview + * + * Specifies if resize should change the cell in-place. This is an experimental + * feature for non-touch devices. Default is false. + */ +mxVertexHandler.prototype.livePreview = false; + +/** + * Variable: movePreviewToFront + * + * Specifies if the live preview should be moved to the front. + */ +mxVertexHandler.prototype.movePreviewToFront = false; + +/** + * Variable: manageSizers + * + * Specifies if sizers should be hidden and spaced if the vertex is small. + * Default is false. + */ +mxVertexHandler.prototype.manageSizers = false; + +/** + * Variable: constrainGroupByChildren + * + * Specifies if the size of groups should be constrained by the children. + * Default is false. + */ +mxVertexHandler.prototype.constrainGroupByChildren = false; + +/** + * Variable: rotationHandleVSpacing + * + * Vertical spacing for rotation icon. Default is -16. + */ +mxVertexHandler.prototype.rotationHandleVSpacing = -16; + +/** + * Variable: horizontalOffset + * + * The horizontal offset for the handles. This is updated in + * if is true and the sizers are offset horizontally. + */ +mxVertexHandler.prototype.horizontalOffset = 0; + +/** + * Variable: verticalOffset + * + * The horizontal offset for the handles. This is updated in + * if is true and the sizers are offset vertically. + */ +mxVertexHandler.prototype.verticalOffset = 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 = mxConstants.DIALECT_SVG; + this.selectionBorder.svgStrokeTolerance = 0; + this.selectionBorder.pointerEvents = false; + this.selectionBorder.rotation = Number(this.state.style[mxConstants.STYLE_ROTATION] || '0'); + this.selectionBorder.init(this.graph.getView().getOverlayPane()); + mxEvent.redirectMouseEvents(this.selectionBorder.node, this.graph, this.state); + + if (this.graph.isCellMovable(this.state.cell) && !this.graph.isCellLocked(this.state.cell)) + { + this.selectionBorder.setCursor(mxConstants.CURSOR_MOVABLE_VERTEX); + } + + this.refresh(); + this.redraw(); + + if (this.constrainGroupByChildren) + { + this.updateMinBounds(); + } +}; + +/** + * Function: isHandlesVisible + * + * Returns true if all handles should be visible. + */ +mxVertexHandler.prototype.isHandlesVisible = function() +{ + return !this.graph.isCellLocked(this.state.cell) && + (mxGraphHandler.prototype.maxCells <= 0 || + this.graph.getSelectionCount() <= mxGraphHandler.prototype.maxCells); +}; + +/** + * Function: refresh + * + * Initializes the shapes required for this vertex handler. + */ +mxVertexHandler.prototype.refresh = function() +{ + if (this.selectionBorder != null) + { + this.selectionBorder.strokewidth = this.getSelectionStrokeWidth(); + this.selectionBorder.isDashed = this.isSelectionDashed(); + this.selectionBorder.stroke = this.getSelectionColor(); + this.selectionBorder.redraw(); + } + + if (this.sizers != null) + { + this.destroySizers(); + } + + if (this.isHandlesVisible()) + { + this.sizers = this.createSizers(); + } + + if (this.customHandles != null) + { + this.destroyCustomHandles(); + } + + if (this.isHandlesVisible()) + { + this.customHandles = this.createCustomHandles(); + } +}; + +/** + * Function: isRotationHandleVisible + * + * Returns true if the rotation handle should be showing. + */ +mxVertexHandler.prototype.isRotationHandleVisible = function() +{ + return this.graph.isEnabled() && this.rotationEnabled && + this.graph.isCellRotatable(this.state.cell); +}; + +/** + * Function: isConstrainedEvent + * + * Returns true if the aspect ratio if the cell should be maintained. + */ +mxVertexHandler.prototype.isConstrainedEvent = function(me) +{ + return mxEvent.isShiftDown(me.getEvent()) || this.state.style[mxConstants.STYLE_ASPECT] == 'fixed'; +}; + +/** + * Function: isCenteredEvent + * + * Returns true if the center of the vertex should be maintained during the resize. + */ +mxVertexHandler.prototype.isCenteredEvent = function(state, me) +{ + return false; +}; + +/** + * Function: createCustomHandles + * + * Returns an array of custom handles. This implementation returns null. + */ +mxVertexHandler.prototype.createCustomHandles = function() +{ + return null; +}; + +/** + * Function: updateMinBounds + * + * Initializes the shapes required for this vertex handler. + */ +mxVertexHandler.prototype.updateMinBounds = function() +{ + var children = this.graph.getChildCells(this.state.cell); + + if (children.length > 0) + { + this.minBounds = this.graph.view.getBounds(children); + + if (this.minBounds != null) + { + var s = this.state.view.scale; + var t = this.state.view.translate; + + this.minBounds.x -= this.state.x; + this.minBounds.y -= this.state.y; + this.minBounds.x /= s; + this.minBounds.y /= s; + this.minBounds.width /= s; + this.minBounds.height /= s; + this.x0 = this.state.x / s - t.x; + this.y0 = this.state.y / s - t.y; + } + } +}; + +/** + * Function: getSelectionBounds + * + * Returns the mxRectangle that defines the bounds of the selection + * border. + */ +mxVertexHandler.prototype.getSelectionBounds = function(state) +{ + return new mxRectangle(Math.round(state.x), Math.round(state.y), Math.round(state.width), Math.round(state.height)); +}; + +/** + * Function: createParentHighlightShape + * + * Creates the shape used to draw the selection border. + */ +mxVertexHandler.prototype.createParentHighlightShape = function(bounds) +{ + return this.createSelectionShape(bounds); +}; + +/** + * Function: createSelectionShape + * + * Creates the shape used to draw the selection border. + */ +mxVertexHandler.prototype.createSelectionShape = function(bounds) +{ + var shape = new mxRectangleShape( + mxRectangle.fromRectangle(bounds), + null, this.getSelectionColor()); + shape.strokewidth = this.getSelectionStrokeWidth(); + shape.isDashed = this.isSelectionDashed(); + + return shape; +}; + +/** + * Function: getSelectionColor + * + * Returns . + */ +mxVertexHandler.prototype.getSelectionColor = function() +{ + return (this.graph.isCellEditable(this.state.cell)) ? + mxConstants.VERTEX_SELECTION_COLOR : + mxConstants.LOCKED_HANDLE_FILLCOLOR; +}; + +/** + * Function: getSelectionStrokeWidth + * + * Returns . + */ +mxVertexHandler.prototype.getSelectionStrokeWidth = function() +{ + return mxConstants.VERTEX_SELECTION_STROKEWIDTH; +}; + +/** + * Function: isSelectionDashed + * + * Returns . + */ +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 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, this.handleImage); + + if (sizer.isHtmlAllowed() && 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_MIXEDHTML : mxConstants.DIALECT_SVG; + sizer.init(this.graph.getView().getOverlayPane()); + } + + mxEvent.redirectMouseEvents(sizer.node, this.graph, this.state); + + if (this.graph.isEnabled()) + { + sizer.setCursor(cursor); + } + + if (!this.isSizerVisible(index)) + { + sizer.visible = false; + } + + 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 an + * index. Only images and rectangles should be returned if support for HTML + * labels with not foreign objects is required. + */ +mxVertexHandler.prototype.createSizerShape = function(bounds, index, fillColor, image) +{ + if (image != null) + { + bounds = new mxRectangle(bounds.x, bounds.y, image.width, image.height); + var shape = new mxImageShape(bounds, image.src); + + // Allows HTML rendering of the images + shape.preserveImageAspect = false; + + return shape; + } + else if (index == mxEvent.ROTATION_HANDLE) + { + return new mxEllipse(bounds, fillColor || mxConstants.HANDLE_FILLCOLOR, mxConstants.HANDLE_STROKECOLOR); + } + else + { + return new mxRectangleShape(bounds, fillColor || mxConstants.HANDLE_FILLCOLOR, mxConstants.HANDLE_STROKECOLOR); + } +}; + +/** + * Function: createBounds + * + * Helper method to create an 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 = Math.floor(x - shape.bounds.width / 2); + shape.bounds.y = Math.floor(y - shape.bounds.height / 2); + + // Fixes visible inactive handles in . TODO, remove? + if (shape.node != null && shape.node.style.display != 'none') + { + 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 . + */ +mxVertexHandler.prototype.getHandleForEvent = function(me) +{ + // Connection highlight may consume events before they reach sizer handle + var tol = (!mxEvent.isMouseEvent(me.getEvent())) ? this.tolerance : 1; + var hit = (this.allowHandleBoundsCheck && (mxClient.IS_IE || tol > 0)) ? + new mxRectangle(me.getGraphX() - tol, me.getGraphY() - tol, 2 * tol, 2 * tol) : null; + + var checkShape = mxUtils.bind(this, function(shape) + { + var st = (shape != null && shape.constructor != mxImageShape && + this.allowHandleBoundsCheck) ? shape.strokewidth + shape.svgStrokeTolerance : null; + var real = (st != null) ? new mxRectangle(me.getGraphX() - Math.floor(st / 2), + me.getGraphY() - Math.floor(st / 2), st, st) : hit; + + return shape != null && (me.isSource(shape) || shape.intersectsRectangle(real)); + }); + + if (checkShape(this.rotationShape)) + { + return mxEvent.ROTATION_HANDLE; + } + else if (checkShape(this.labelShape)) + { + return mxEvent.LABEL_HANDLE; + } + + if (this.sizers != null) + { + for (var i = 0; i < this.sizers.length; i++) + { + if (checkShape(this.sizers[i])) + { + return i; + } + } + } + + if (this.customHandles != null && this.isCustomHandleEvent(me)) + { + // Inverse loop order to match display order + for (var i = this.customHandles.length - 1; i >= 0; i--) + { + if (this.customHandles[i] != null && + checkShape(this.customHandles[i].shape)) + { + // LATER: Return reference to active shape + return mxEvent.CUSTOM_HANDLE - i; + } + } + } + + return null; +}; + +/** + * Function: isCustomHandleEvent + * + * Returns true if the given event allows custom handles to be changed. This + * implementation returns true. + */ +mxVertexHandler.prototype.isCustomHandleEvent = function(me) +{ + return true; +}; + +/** + * 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() && + (!mxEvent.isAltDown(me.getEvent()) || + !mxEvent.isShiftDown(me.getEvent()))) + { + var handle = this.getHandleForEvent(me); + + if (handle != null) + { + this.start(me.getGraphX(), me.getGraphY(), handle); + me.consume(); + } + } +}; + +/** + * Function: isLivePreviewBorder + * + * Called if is enabled to check if a border should be painted. + * This implementation returns true if the shape is transparent. + */ +mxVertexHandler.prototype.isLivePreviewBorder = function() +{ + return this.state.shape != null && this.state.shape.fill == null && this.state.shape.stroke == null; +}; + +/** + * Function: start + * + * Starts the handling of the mouse gesture. + */ +mxVertexHandler.prototype.start = function(x, y, index) +{ + if (this.selectionBorder != null) + { + this.livePreviewActive = this.livePreview && this.graph.model.getChildCount(this.state.cell) == 0; + this.inTolerance = true; + this.childOffsetX = 0; + this.childOffsetY = 0; + this.index = index; + this.startX = x; + this.startY = y; + + if (this.index <= mxEvent.CUSTOM_HANDLE && this.isGhostPreview()) + { + this.ghostPreview = this.createGhostPreview(); + } + else + { + // Saves reference to parent state + var model = this.state.view.graph.model; + var parent = model.getParent(this.state.cell); + + if (this.state.view.currentRoot != parent && (model.isVertex(parent) || model.isEdge(parent))) + { + this.parentState = this.state.view.graph.view.getState(parent); + } + + // Creates a preview that can be on top of any HTML label + this.selectionBorder.node.style.display = (index == mxEvent.ROTATION_HANDLE) ? 'inline' : 'none'; + + // Creates the border that represents the new bounds + if (!this.livePreviewActive || this.isLivePreviewBorder()) + { + this.preview = this.createSelectionShape(this.bounds); + + if (!(mxClient.IS_SVG && Number(this.state.style[mxConstants.STYLE_ROTATION] || '0') != 0) && + 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 = mxConstants.DIALECT_SVG; + this.preview.init(this.graph.view.getOverlayPane()); + } + } + + if (index == mxEvent.ROTATION_HANDLE) + { + // With the rotation handle in a corner, need the angle and distance + var pos = this.getRotationHandlePosition(); + + var dx = pos.x - this.state.getCenterX(); + var dy = pos.y - this.state.getCenterY(); + + this.startAngle = (dx != 0) ? Math.atan(dy / dx) * 180 / Math.PI + 90 : 0; + this.startDist = Math.sqrt(dx * dx + dy * dy); + } + + // Prepares the handles for live preview + if (this.livePreviewActive) + { + this.hideSizers(); + + if (index == mxEvent.ROTATION_HANDLE) + { + this.rotationShape.node.style.display = ''; + } + else if (index == mxEvent.LABEL_HANDLE) + { + this.labelShape.node.style.display = ''; + } + else if (this.sizers != null && this.sizers[index] != null) + { + this.sizers[index].node.style.display = ''; + } + else if (index <= mxEvent.CUSTOM_HANDLE && this.customHandles != null && + this.customHandles[mxEvent.CUSTOM_HANDLE - index] != null) + { + this.customHandles[mxEvent.CUSTOM_HANDLE - index].setVisible(true); + } + + // Gets the array of connected edge handlers for redrawing + var edges = this.graph.getEdges(this.state.cell); + this.edgeHandlers = []; + + for (var i = 0; i < edges.length; i++) + { + var handler = this.graph.selectionCellsHandler.getHandler(edges[i]); + + if (handler != null) + { + this.edgeHandlers.push(handler); + } + } + } + } + } +}; + +/** + * Function: createGhostPreview + * + * Starts the handling of the mouse gesture. + */ +mxVertexHandler.prototype.createGhostPreview = function() +{ + var shape = this.graph.cellRenderer.createShape(this.state); + shape.init(this.graph.view.getOverlayPane()); + shape.scale = this.state.view.scale; + shape.bounds = this.bounds; + shape.outline = true; + + return shape; +}; + +/** + * Function: hideHandles + * + * Shortcut to . + */ +mxVertexHandler.prototype.setHandlesVisible = function(visible) +{ + this.handlesVisible = visible; + + if (this.sizers != null) + { + for (var i = 0; i < this.sizers.length; i++) + { + this.sizers[i].node.style.display = (visible) ? '' : 'none'; + } + } + + if (this.customHandles != null) + { + for (var i = 0; i < this.customHandles.length; i++) + { + if (this.customHandles[i] != null) + { + this.customHandles[i].setVisible(visible); + } + } + } +}; + +/** + * Function: hideSizers + * + * Hides all sizers except. + * + * Starts the handling of the mouse gesture. + */ +mxVertexHandler.prototype.hideSizers = function() +{ + this.setHandlesVisible(false); +}; + +/** + * Function: checkTolerance + * + * Checks if the coordinates for the given event are within the + * . If the event is a mouse event then the tolerance is + * ignored. + */ +mxVertexHandler.prototype.checkTolerance = function(me) +{ + if (this.inTolerance && this.startX != null && this.startY != null) + { + if (mxEvent.isMouseEvent(me.getEvent()) || + Math.abs(me.getGraphX() - this.startX) > this.graph.tolerance || + Math.abs(me.getGraphY() - this.startY) > this.graph.tolerance) + { + this.inTolerance = false; + } + } +}; + +/** + * Function: updateHint + * + * Hook for subclassers do show details while the handler is active. + */ +mxVertexHandler.prototype.updateHint = function(me) { }; + +/** + * Function: removeHint + * + * Hooks for subclassers to hide details when the handler gets inactive. + */ +mxVertexHandler.prototype.removeHint = function() { }; + +/** + * Function: roundAngle + * + * Hook for rounding the angle. This uses Math.round. + */ +mxVertexHandler.prototype.roundAngle = function(angle) +{ + return Math.round(angle * 10) / 10; +}; + +/** + * Function: roundLength + * + * Hook for rounding the unscaled width or height. This uses Math.round. + */ +mxVertexHandler.prototype.roundLength = function(length) +{ + return Math.round(length * 100) / 100; +}; + +/** + * Function: mouseMove + * + * Handles the event by updating the preview. + */ +mxVertexHandler.prototype.mouseMove = function(sender, me) +{ + if (!me.isConsumed() && this.index != null) + { + // Checks tolerance for ignoring single clicks + this.checkTolerance(me); + + if (!this.inTolerance) + { + if (this.index <= mxEvent.CUSTOM_HANDLE) + { + if (this.customHandles != null && this.customHandles[mxEvent.CUSTOM_HANDLE - this.index] != null) + { + this.customHandles[mxEvent.CUSTOM_HANDLE - this.index].processEvent(me); + this.customHandles[mxEvent.CUSTOM_HANDLE - this.index].active = true; + + if (this.ghostPreview != null) + { + this.ghostPreview.apply(this.state); + this.ghostPreview.strokewidth = this.getSelectionStrokeWidth() / + this.ghostPreview.scale / this.ghostPreview.scale; + this.ghostPreview.isDashed = this.isSelectionDashed(); + this.ghostPreview.stroke = this.getSelectionColor(); + this.ghostPreview.redraw(); + + if (this.selectionBounds != null) + { + this.selectionBorder.node.style.display = 'none'; + } + } + else + { + if (this.movePreviewToFront) + { + this.moveToFront(); + } + + this.customHandles[mxEvent.CUSTOM_HANDLE - this.index].positionChanged(); + } + } + } + else if (this.index == mxEvent.LABEL_HANDLE) + { + this.moveLabel(me); + } + else + { + if (this.index == mxEvent.ROTATION_HANDLE) + { + this.rotateVertex(me); + } + else + { + this.resizeVertex(me); + } + + this.updateHint(me); + } + } + + me.consume(); + } + // Workaround for disabling the connect highlight when over handle + else if (!this.graph.isMouseDown && this.getHandleForEvent(me) != null) + { + me.consume(false); + } +}; + +/** + * Function: isGhostPreview + * + * Returns true if a ghost preview should be used for custom handles. + */ +mxVertexHandler.prototype.isGhostPreview = function() +{ + return this.state.view.graph.model.getChildCount(this.state.cell) > 0; +}; + +/** + * Function: moveLabel + * + * Moves the label. + */ +mxVertexHandler.prototype.moveLabel = function(me) +{ + var point = new mxPoint(me.getGraphX(), me.getGraphY()); + var tr = this.graph.view.translate; + var scale = this.graph.view.scale; + + if (this.graph.isGridEnabledEvent(me.getEvent())) + { + point.x = (this.graph.snap(point.x / scale - tr.x) + tr.x) * scale; + point.y = (this.graph.snap(point.y / scale - tr.y) + tr.y) * scale; + } + + var index = (this.rotationShape != null) ? this.sizers.length - 2 : this.sizers.length - 1; + this.moveSizerTo(this.sizers[index], point.x, point.y); +}; + +/** + * Function: rotateVertex + * + * Rotates the vertex. + */ +mxVertexHandler.prototype.rotateVertex = function(me) +{ + var point = new mxPoint(me.getGraphX(), me.getGraphY()); + var dx = this.state.x + this.state.width / 2 - point.x; + var dy = this.state.y + this.state.height / 2 - point.y; + this.currentAlpha = (dx != 0) ? Math.atan(dy / dx) * 180 / Math.PI + 90 : ((dy < 0) ? 180 : 0); + + if (dx > 0) + { + this.currentAlpha -= 180; + } + + this.currentAlpha -= this.startAngle; + + // Rotation raster + if (this.rotationRaster && this.graph.isGridEnabledEvent(me.getEvent())) + { + var dx = point.x - this.state.getCenterX(); + var dy = point.y - this.state.getCenterY(); + var dist = Math.sqrt(dx * dx + dy * dy); + + if (dist - this.startDist < 2) + { + raster = 15; + } + else if (dist - this.startDist < 25) + { + raster = 5; + } + else + { + raster = 1; + } + + this.currentAlpha = Math.round(this.currentAlpha / raster) * raster; + } + else + { + this.currentAlpha = this.roundAngle(this.currentAlpha); + } + + this.selectionBorder.rotation = this.currentAlpha; + this.selectionBorder.redraw(); + + if (this.livePreviewActive) + { + this.redrawHandles(); + } +}; + +/** + * Function: resizeVertex + * + * Risizes the vertex. + */ +mxVertexHandler.prototype.resizeVertex = function(me) +{ + var ct = new mxPoint(this.state.getCenterX(), this.state.getCenterY()); + var alpha = mxUtils.toRadians(this.state.style[mxConstants.STYLE_ROTATION] || '0'); + var point = new mxPoint(me.getGraphX(), me.getGraphY()); + var tr = this.graph.view.translate; + var scale = this.graph.view.scale; + var cos = Math.cos(-alpha); + var sin = Math.sin(-alpha); + + var dx = point.x - this.startX; + var dy = point.y - this.startY; + + // Rotates vector for mouse gesture + var tx = cos * dx - sin * dy; + var ty = sin * dx + cos * dy; + + dx = tx; + dy = ty; + + var geo = this.graph.getCellGeometry(this.state.cell); + this.unscaledBounds = this.union(geo, dx / scale, dy / scale, this.index, + this.graph.isGridEnabledEvent(me.getEvent()), 1, + new mxPoint(0, 0), this.isConstrainedEvent(me), + this.isCenteredEvent(this.state, me)); + + // Keeps vertex within maximum graph or parent bounds + if (!geo.relative) + { + var max = this.graph.getMaximumGraphBounds(); + + // Handles child cells + if (max != null && this.parentState != null) + { + max = mxRectangle.fromRectangle(max); + + max.x -= (this.parentState.x - tr.x * scale) / scale; + max.y -= (this.parentState.y - tr.y * scale) / scale; + } + + if (this.graph.isConstrainChild(this.state.cell)) + { + var tmp = this.graph.getCellContainmentArea(this.state.cell); + + if (tmp != null) + { + var overlap = this.graph.getOverlap(this.state.cell); + + if (overlap > 0) + { + tmp = mxRectangle.fromRectangle(tmp); + + tmp.x -= tmp.width * overlap; + tmp.y -= tmp.height * overlap; + tmp.width += 2 * tmp.width * overlap; + tmp.height += 2 * tmp.height * overlap; + } + + if (max == null) + { + max = tmp; + } + else + { + max = mxRectangle.fromRectangle(max); + max.intersect(tmp); + } + } + } + + if (max != null) + { + if (this.unscaledBounds.x < max.x) + { + this.unscaledBounds.width -= max.x - this.unscaledBounds.x; + this.unscaledBounds.x = max.x; + } + + if (this.unscaledBounds.y < max.y) + { + this.unscaledBounds.height -= max.y - this.unscaledBounds.y; + this.unscaledBounds.y = max.y; + } + + if (this.unscaledBounds.x + this.unscaledBounds.width > max.x + max.width) + { + this.unscaledBounds.width -= this.unscaledBounds.x + + this.unscaledBounds.width - max.x - max.width; + } + + if (this.unscaledBounds.y + this.unscaledBounds.height > max.y + max.height) + { + this.unscaledBounds.height -= this.unscaledBounds.y + + this.unscaledBounds.height - max.y - max.height; + } + } + } + + var old = this.bounds; + this.bounds = new mxRectangle(((this.parentState != null) ? this.parentState.x : tr.x * scale) + + (this.unscaledBounds.x) * scale, ((this.parentState != null) ? this.parentState.y : tr.y * scale) + + (this.unscaledBounds.y) * scale, this.unscaledBounds.width * scale, this.unscaledBounds.height * scale); + + if (geo.relative && this.parentState != null) + { + this.bounds.x += this.state.x - this.parentState.x; + this.bounds.y += this.state.y - this.parentState.y; + } + + cos = Math.cos(alpha); + sin = Math.sin(alpha); + + var c2 = new mxPoint(this.bounds.getCenterX(), this.bounds.getCenterY()); + + var dx = c2.x - ct.x; + var dy = c2.y - ct.y; + + var dx2 = cos * dx - sin * dy; + var dy2 = sin * dx + cos * dy; + + var dx3 = dx2 - dx; + var dy3 = dy2 - dy; + + var dx4 = this.bounds.x - this.state.x; + var dy4 = this.bounds.y - this.state.y; + + var dx5 = cos * dx4 - sin * dy4; + var dy5 = sin * dx4 + cos * dy4; + + this.bounds.x += dx3; + this.bounds.y += dy3; + + // Rounds unscaled bounds to int + this.unscaledBounds.x = this.roundLength(this.unscaledBounds.x + dx3 / scale); + this.unscaledBounds.y = this.roundLength(this.unscaledBounds.y + dy3 / scale); + this.unscaledBounds.width = this.roundLength(this.unscaledBounds.width); + this.unscaledBounds.height = this.roundLength(this.unscaledBounds.height); + + // Shifts the children according to parent offset + if (!this.graph.isCellCollapsed(this.state.cell) && (dx3 != 0 || dy3 != 0)) + { + this.childOffsetX = this.state.x - this.bounds.x + dx5; + this.childOffsetY = this.state.y - this.bounds.y + dy5; + } + else + { + this.childOffsetX = 0; + this.childOffsetY = 0; + } + + if (!old.equals(this.bounds)) + { + if (this.livePreviewActive) + { + this.updateLivePreview(me); + } + + if (this.preview != null) + { + this.drawPreview(); + } + else + { + this.updateParentHighlight(); + } + } +}; + +/** + * Function: updateLivePreview + * + * Repaints the live preview. + */ +mxVertexHandler.prototype.updateLivePreview = function(me) +{ + // TODO: Apply child offset to children in live preview + var scale = this.graph.view.scale; + var tr = this.graph.view.translate; + + // Saves current state + var tempState = this.state.clone(); + + // Temporarily changes size and origin + this.state.x = this.bounds.x; + this.state.y = this.bounds.y; + this.state.origin = new mxPoint(this.state.x / scale - tr.x, this.state.y / scale - tr.y); + this.state.width = this.bounds.width; + this.state.height = this.bounds.height; + + // Redraws cell and handles + var off = this.state.absoluteOffset; + off = new mxPoint(off.x, off.y); + + // Required to store and reset absolute offset for updating label position + this.state.absoluteOffset.x = 0; + this.state.absoluteOffset.y = 0; + var geo = this.graph.getCellGeometry(this.state.cell); + + if (geo != null) + { + var offset = geo.offset || this.EMPTY_POINT; + + if (offset != null && !geo.relative) + { + this.state.absoluteOffset.x = this.state.view.scale * offset.x; + this.state.absoluteOffset.y = this.state.view.scale * offset.y; + } + + this.state.view.updateVertexLabelOffset(this.state); + } + + // Draws the live preview + this.state.view.graph.cellRenderer.redraw(this.state, true); + + // Redraws connected edges TODO: Include child edges + this.state.view.invalidate(this.state.cell); + this.state.invalid = false; + this.state.view.validate(); + this.redrawHandles(); + + // Moves live preview to front + if (this.movePreviewToFront) + { + this.moveToFront(); + } + + // Hides folding icon + if (this.state.control != null && this.state.control.node != null) + { + this.state.control.node.style.visibility = 'hidden'; + } + + // Restores current state + this.state.setState(tempState); +}; + +/** + * Function: moveToFront + * + * Handles the event by applying the changes to the geometry. + */ +mxVertexHandler.prototype.moveToFront = function() +{ + if ((this.state.text != null && this.state.text.node != null && + this.state.text.node.nextSibling != null) || + (this.state.shape != null && this.state.shape.node != null && + this.state.shape.node.nextSibling != null && (this.state.text == null || + this.state.shape.node.nextSibling != this.state.text.node))) + { + if (this.state.shape != null && this.state.shape.node != null) + { + this.state.shape.node.parentNode.appendChild(this.state.shape.node); + } + + if (this.state.text != null && this.state.text.node != null) + { + this.state.text.node.parentNode.appendChild(this.state.text.node); + } + } +}; + +/** + * Function: mouseUp + * + * Handles the event by applying the changes to the geometry. + */ +mxVertexHandler.prototype.mouseUp = function(sender, me) +{ + if (this.index != null && this.state != null) + { + var point = new mxPoint(me.getGraphX(), me.getGraphY()); + var index = this.index; + this.index = null; + + if (this.ghostPreview == null) + { + // Marks as invalid to ensure reset of order + this.state.view.invalidate(this.state.cell, false, false); + } + + this.graph.getModel().beginUpdate(); + try + { + if (index <= mxEvent.CUSTOM_HANDLE) + { + if (this.customHandles != null && this.customHandles[mxEvent.CUSTOM_HANDLE - index] != null) + { + // Creates style before changing cell state + var style = this.state.view.graph.getCellStyle(this.state.cell); + + this.customHandles[mxEvent.CUSTOM_HANDLE - index].active = false; + this.customHandles[mxEvent.CUSTOM_HANDLE - index].execute(me); + + // Sets style and apply on shape to force repaint and + // check if execute has removed custom handles + if (this.customHandles != null && + this.customHandles[mxEvent.CUSTOM_HANDLE - index] != null) + { + this.state.style = style; + this.customHandles[mxEvent.CUSTOM_HANDLE - index].positionChanged(); + } + } + } + else if (index == mxEvent.ROTATION_HANDLE) + { + if (this.currentAlpha != null) + { + var delta = this.currentAlpha - (this.state.style[mxConstants.STYLE_ROTATION] || 0); + + if (delta != 0) + { + this.rotateCell(this.state.cell, delta); + } + } + else + { + this.rotateClick(); + } + } + else + { + var gridEnabled = this.graph.isGridEnabledEvent(me.getEvent()); + var alpha = mxUtils.toRadians(this.state.style[mxConstants.STYLE_ROTATION] || '0'); + var cos = Math.cos(-alpha); + var sin = Math.sin(-alpha); + + var dx = point.x - this.startX; + var dy = point.y - this.startY; + + // Rotates vector for mouse gesture + var tx = cos * dx - sin * dy; + var ty = sin * dx + cos * dy; + + dx = tx; + dy = ty; + + var s = this.graph.view.scale; + var recurse = this.isRecursiveResize(this.state, me); + this.resizeCell(this.state.cell, this.roundLength(dx / s), this.roundLength(dy / s), + index, gridEnabled, this.isConstrainedEvent(me), recurse); + } + } + finally + { + this.graph.getModel().endUpdate(); + } + + // Restores order if cell wasn't changed in model + if (this.state.invalid) + { + this.state.view.validate(); + } + + me.consume(); + this.reset(); + this.redrawHandles(); + } +}; + +/** + * Function: isRecursiveResize + * + * Returns the recursiveResize of the give state. + * + * Parameters: + * + * state - the given . This implementation takes + * the value of this state. + * me - the mouse event. + */ +mxVertexHandler.prototype.isRecursiveResize = function(state, me) +{ + return this.graph.isRecursiveResize(this.state); +}; + +/** + * Function: rotateClick + * + * Hook for subclassers to implement a single click on the rotation handle. + * This code is executed as part of the model transaction. This implementation + * is empty. + */ +mxVertexHandler.prototype.rotateClick = function() { }; + +/** + * Function: rotateCell + * + * Rotates the given cell and its children by the given angle in degrees. + * + * Parameters: + * + * cell - to be rotated. + * angle - Angle in degrees. + */ +mxVertexHandler.prototype.rotateCell = function(cell, angle, parent) +{ + if (angle != 0) + { + var model = this.graph.getModel(); + + if (model.isVertex(cell) || model.isEdge(cell)) + { + if (!model.isEdge(cell)) + { + var style = this.graph.getCurrentCellStyle(cell); + var total = (style[mxConstants.STYLE_ROTATION] || 0) + angle; + this.graph.setCellStyles(mxConstants.STYLE_ROTATION, total, [cell]); + } + + var geo = this.graph.getCellGeometry(cell); + + if (geo != null) + { + var pgeo = this.graph.getCellGeometry(parent); + + if (pgeo != null && !model.isEdge(parent)) + { + geo = geo.clone(); + geo.rotate(angle, new mxPoint(pgeo.width / 2, pgeo.height / 2)); + model.setGeometry(cell, geo); + } + + if ((model.isVertex(cell) && !geo.relative) || model.isEdge(cell)) + { + // Recursive rotation + var childCount = model.getChildCount(cell); + + for (var i = 0; i < childCount; i++) + { + this.rotateCell(model.getChildAt(cell, i), angle, cell); + } + } + } + } + } +}; + +/** + * Function: reset + * + * Resets the state of this handler. + */ +mxVertexHandler.prototype.reset = function() +{ + if (this.sizers != null && this.index != null && this.sizers[this.index] != null && + this.sizers[this.index].node.style.display == 'none') + { + this.sizers[this.index].node.style.display = ''; + } + + this.currentAlpha = null; + this.inTolerance = null; + this.index = null; + + // TODO: Reset and redraw cell states for live preview + if (this.preview != null) + { + this.preview.destroy(); + this.preview = null; + } + + if (this.ghostPreview != null) + { + this.ghostPreview.destroy(); + this.ghostPreview = null; + } + + if (this.livePreviewActive && this.sizers != null) + { + for (var i = 0; i < this.sizers.length; i++) + { + if (this.sizers[i] != null) + { + this.sizers[i].node.style.display = ''; + } + } + + // Shows folding icon + if (this.state.control != null && this.state.control.node != null) + { + this.state.control.node.style.visibility = ''; + } + } + + if (this.customHandles != null) + { + for (var i = 0; i < this.customHandles.length; i++) + { + if (this.customHandles[i] != null) + { + if (this.customHandles[i].active) + { + this.customHandles[i].active = false; + this.customHandles[i].reset(); + } + else + { + this.customHandles[i].setVisible(true); + } + } + } + } + + // Checks if handler has been destroyed + if (this.selectionBorder != null) + { + this.selectionBorder.node.style.display = 'inline'; + this.selectionBounds = this.getSelectionBounds(this.state); + this.bounds = new mxRectangle(this.selectionBounds.x, this.selectionBounds.y, + this.selectionBounds.width, this.selectionBounds.height); + this.drawPreview(); + } + + this.removeHint(); + this.redrawHandles(); + this.edgeHandlers = null; + this.handlesVisible = true; + this.unscaledBounds = null; + this.livePreviewActive = null; +}; + +/** + * Function: resizeCell + * + * Uses the given vector to change the bounds of the given cell + * in the graph using . + */ +mxVertexHandler.prototype.resizeCell = function(cell, dx, dy, index, gridEnabled, constrained, recurse) +{ + var geo = this.graph.model.getGeometry(cell); + + if (geo != null) + { + if (index == mxEvent.LABEL_HANDLE) + { + var alpha = -mxUtils.toRadians(this.state.style[mxConstants.STYLE_ROTATION] || '0'); + var horz = mxUtils.getValue(this.state.style, mxConstants.STYLE_HORIZONTAL, true) == 1; + var cos = Math.cos(alpha); + var sin = Math.sin(alpha); + var scale = this.graph.view.scale; + var pt = mxUtils.getRotatedPoint(new mxPoint( + Math.round((this.labelShape.bounds.getCenterX() - this.startX) / scale), + Math.round((this.labelShape.bounds.getCenterY() - this.startY) / scale)), + cos, sin); + + if (!horz) + { + pt.y = -pt.y; + } + + geo = geo.clone(); + + if (geo.offset == null) + { + geo.offset = pt; + } + else + { + geo.offset.x += pt.x; + geo.offset.y += pt.y; + } + + this.graph.model.setGeometry(cell, geo); + } + else if (this.unscaledBounds != null) + { + var scale = this.graph.view.scale; + + if (this.childOffsetX != 0 || this.childOffsetY != 0) + { + this.moveChildren(cell, Math.round(this.childOffsetX / scale), Math.round(this.childOffsetY / scale)); + } + + this.graph.resizeCell(cell, this.unscaledBounds, recurse); + } + } +}; + +/** + * Function: moveChildren + * + * Moves the children of the given cell by the given vector. + */ +mxVertexHandler.prototype.moveChildren = function(cell, dx, dy) +{ + var model = this.graph.getModel(); + var childCount = model.getChildCount(cell); + + for (var i = 0; i < childCount; i++) + { + var child = model.getChildAt(cell, i); + var geo = this.graph.getCellGeometry(child); + + if (geo != null) + { + geo = geo.clone(); + geo.translate(dx, dy); + model.setGeometry(child, geo); + } + } +}; +/** + * 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, constrained) + * { + * 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) + * + * To override this to update the height for a wrapped text if the width of a vertex is + * changed, the following can be used. + * + * (code) + * var mxVertexHandlerUnion = mxVertexHandler.prototype.union; + * mxVertexHandler.prototype.union = function(bounds, dx, dy, index, gridEnabled, scale, tr, constrained) + * { + * var result = mxVertexHandlerUnion.apply(this, arguments); + * var s = this.state; + * + * if (this.graph.isHtmlLabel(s.cell) && (index == 3 || index == 4) && + * s.text != null && s.style[mxConstants.STYLE_WHITE_SPACE] == 'wrap') + * { + * var label = this.graph.getLabel(s.cell); + * var fontSize = mxUtils.getNumber(s.style, mxConstants.STYLE_FONTSIZE, mxConstants.DEFAULT_FONTSIZE); + * var ww = result.width / s.view.scale - s.text.spacingRight - s.text.spacingLeft + * + * result.height = mxUtils.getSizeForString(label, fontSize, s.style[mxConstants.STYLE_FONTFAMILY], ww).height; + * } + * + * return result; + * }; + * (end) + */ +mxVertexHandler.prototype.union = function(bounds, dx, dy, index, gridEnabled, scale, tr, constrained, centered) +{ + gridEnabled = (gridEnabled != null) ? gridEnabled && this.graph.gridEnabled : this.graph.gridEnabled; + + 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 w0 = bounds.width; + var h0 = bounds.height; + var left = bounds.x - tr.x * scale; + var right = left + w0; + var top = bounds.y - tr.y * scale; + var bottom = top + h0; + + var cx = left + w0 / 2; + var cy = top + h0 / 2; + + if (index > 4 /* Bottom Row */) + { + bottom = bottom + dy; + + if (gridEnabled) + { + bottom = this.graph.snap(bottom / scale) * scale; + } + else + { + bottom = Math.round(bottom / scale) * scale; + } + } + else if (index < 3 /* Top Row */) + { + top = top + dy; + + if (gridEnabled) + { + top = this.graph.snap(top / scale) * scale; + } + else + { + top = Math.round(top / scale) * scale; + } + } + + if (index == 0 || index == 3 || index == 5 /* Left */) + { + left += dx; + + if (gridEnabled) + { + left = this.graph.snap(left / scale) * scale; + } + else + { + left = Math.round(left / scale) * scale; + } + } + else if (index == 2 || index == 4 || index == 7 /* Right */) + { + right += dx; + + if (gridEnabled) + { + right = this.graph.snap(right / scale) * scale; + } + else + { + right = Math.round(right / scale) * scale; + } + } + + var width = right - left; + var height = bottom - top; + + if (constrained) + { + var geo = this.graph.getCellGeometry(this.state.cell); + + if (geo != null) + { + var aspect = geo.width / geo.height; + + if (index== 1 || index== 2 || index == 7 || index == 6) + { + width = height * aspect; + } + else + { + height = width / aspect; + } + + if (index == 0) + { + left = right - width; + top = bottom - height; + } + } + } + + if (centered) + { + width += (width - w0); + height += (height - h0); + + var cdx = cx - (left + width / 2); + var cdy = cy - (top + height / 2); + + left += cdx; + top += cdy; + right += cdx; + bottom += cdy; + } + + // 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); + } + + var result = new mxRectangle(left + tr.x * scale, top + tr.y * scale, width, height); + + if (this.minBounds != null) + { + result.width = Math.max(result.width, this.minBounds.x * scale + this.minBounds.width * scale + + Math.max(0, this.x0 * scale - result.x)); + result.height = Math.max(result.height, this.minBounds.y * scale + this.minBounds.height * scale + + Math.max(0, this.y0 * scale - result.y)); + } + + return result; + } +}; + +/** + * Function: redraw + * + * Redraws the handles and the preview. + */ +mxVertexHandler.prototype.redraw = function(ignoreHandles) +{ + this.selectionBounds = this.getSelectionBounds(this.state); + this.bounds = new mxRectangle(this.selectionBounds.x, this.selectionBounds.y, + this.selectionBounds.width, this.selectionBounds.height); + this.drawPreview(); + + if (!ignoreHandles) + { + this.redrawHandles(); + } +}; + +/** + * Returns the padding to be used for drawing handles for the current . + */ +mxVertexHandler.prototype.getHandlePadding = function() +{ + // KNOWN: Tolerance depends on event type (eg. 0 for mouse events) + var result = new mxPoint(0, 0); + var tol = this.tolerance; + + if (this.sizers != null && this.sizers.length > 0 && this.sizers[0] != null && + (this.bounds.width < 2 * this.sizers[0].bounds.width + 2 * tol || + this.bounds.height < 2 * this.sizers[0].bounds.height + 2 * tol)) + { + tol /= 2; + + result.x = this.sizers[0].bounds.width + tol; + result.y = this.sizers[0].bounds.height + tol; + } + + return result; +}; + +/** + * Function: getSizerBounds + * + * Returns the bounds used to paint the resize handles. + */ +mxVertexHandler.prototype.getSizerBounds = function() +{ + return this.bounds; +}; + +/** + * Function: redrawHandles + * + * Redraws the handles. To hide certain handles the following code can be used. + * + * (code) + * mxVertexHandler.prototype.redrawHandles = function() + * { + * mxVertexHandlerRedrawHandles.apply(this, arguments); + * + * if (this.sizers != null && this.sizers.length > 7) + * { + * this.sizers[1].node.style.display = 'none'; + * this.sizers[6].node.style.display = 'none'; + * } + * }; + * (end) + */ +mxVertexHandler.prototype.redrawHandles = function() +{ + var s = this.getSizerBounds(); + var tol = this.tolerance; + this.horizontalOffset = 0; + this.verticalOffset = 0; + + if (this.customHandles != null) + { + for (var i = 0; i < this.customHandles.length; i++) + { + if (this.customHandles[i] != null) + { + var temp = this.customHandles[i].shape.node.style.display; + this.customHandles[i].redraw(); + this.customHandles[i].shape.node.style.display = temp; + + // Hides custom handles during text editing + this.customHandles[i].shape.node.style.visibility = + (!this.handlesVisible || !this.isHandlesVisible() || + !this.isCustomHandleVisible(this.customHandles[i]) || + this.graph.isEditing()) ? + 'hidden' : ''; + } + } + } + + if (this.sizers != null && this.sizers.length > 0 && this.sizers[0] != null) + { + if (this.index == null && this.manageSizers && this.sizers.length >= 8) + { + // KNOWN: Tolerance depends on event type (eg. 0 for mouse events) + var padding = this.getHandlePadding(); + this.horizontalOffset = padding.x; + this.verticalOffset = padding.y; + + if (this.horizontalOffset != 0 || this.verticalOffset != 0) + { + s = new mxRectangle(s.x, s.y, s.width, s.height); + + s.x -= this.horizontalOffset / 2; + s.width += this.horizontalOffset; + s.y -= this.verticalOffset / 2; + s.height += this.verticalOffset; + } + + if (this.sizers.length >= 8) + { + if ((s.width < 2 * this.sizers[0].bounds.width + 2 * tol) || + (s.height < 2 * this.sizers[0].bounds.height + 2 * tol)) + { + this.sizers[0].node.style.display = 'none'; + this.sizers[2].node.style.display = 'none'; + this.sizers[5].node.style.display = 'none'; + this.sizers[7].node.style.display = 'none'; + } + else if (this.handlesVisible) + { + this.sizers[0].node.style.display = ''; + this.sizers[2].node.style.display = ''; + this.sizers[5].node.style.display = ''; + this.sizers[7].node.style.display = ''; + } + } + } + + 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 >= 8) + { + var crs = ['nw-resize', 'n-resize', 'ne-resize', 'e-resize', 'se-resize', 's-resize', 'sw-resize', 'w-resize']; + + var alpha = mxUtils.toRadians(this.state.style[mxConstants.STYLE_ROTATION] || '0'); + var cos = Math.cos(alpha); + var sin = Math.sin(alpha); + + var da = Math.round(alpha * 4 / Math.PI); + + var ct = new mxPoint(s.getCenterX(), s.getCenterY()); + var pt = mxUtils.getRotatedPoint(new mxPoint(s.x, s.y), cos, sin, ct); + + this.moveSizerTo(this.sizers[0], pt.x, pt.y); + this.sizers[0].setCursor(crs[mxUtils.mod(0 + da, crs.length)]); + + pt.x = cx; + pt.y = s.y; + pt = mxUtils.getRotatedPoint(pt, cos, sin, ct); + + this.moveSizerTo(this.sizers[1], pt.x, pt.y); + this.sizers[1].setCursor(crs[mxUtils.mod(1 + da, crs.length)]); + + pt.x = r; + pt.y = s.y; + pt = mxUtils.getRotatedPoint(pt, cos, sin, ct); + + this.moveSizerTo(this.sizers[2], pt.x, pt.y); + this.sizers[2].setCursor(crs[mxUtils.mod(2 + da, crs.length)]); + + pt.x = s.x; + pt.y = cy; + pt = mxUtils.getRotatedPoint(pt, cos, sin, ct); + + this.moveSizerTo(this.sizers[3], pt.x, pt.y); + this.sizers[3].setCursor(crs[mxUtils.mod(7 + da, crs.length)]); + + pt.x = r; + pt.y = cy; + pt = mxUtils.getRotatedPoint(pt, cos, sin, ct); + + this.moveSizerTo(this.sizers[4], pt.x, pt.y); + this.sizers[4].setCursor(crs[mxUtils.mod(3 + da, crs.length)]); + + pt.x = s.x; + pt.y = b; + pt = mxUtils.getRotatedPoint(pt, cos, sin, ct); + + this.moveSizerTo(this.sizers[5], pt.x, pt.y); + this.sizers[5].setCursor(crs[mxUtils.mod(6 + da, crs.length)]); + + pt.x = cx; + pt.y = b; + pt = mxUtils.getRotatedPoint(pt, cos, sin, ct); + + this.moveSizerTo(this.sizers[6], pt.x, pt.y); + this.sizers[6].setCursor(crs[mxUtils.mod(5 + da, crs.length)]); + + pt.x = r; + pt.y = b; + pt = mxUtils.getRotatedPoint(pt, cos, sin, ct); + + this.moveSizerTo(this.sizers[7], pt.x, pt.y); + this.sizers[7].setCursor(crs[mxUtils.mod(4 + da, crs.length)]); + + var horz = mxUtils.getValue(this.state.style, mxConstants.STYLE_HORIZONTAL, true) == 1; + pt.x = cx + this.state.absoluteOffset.x; + pt.y = cy + ((horz ? 1 : -1) * this.state.absoluteOffset.y); + pt = mxUtils.getRotatedPoint(pt, cos, sin, ct); + this.moveSizerTo(this.sizers[8], pt.x, pt.y); + } + else if (this.state.width >= 2 && this.state.height >= 2) + { + this.moveSizerTo(this.sizers[0], cx + this.state.absoluteOffset.x, cy + this.state.absoluteOffset.y); + } + else + { + this.moveSizerTo(this.sizers[0], this.state.x, this.state.y); + } + } + } + + if (this.sizers != null) + { + for (var i = 0; i < this.sizers.length; i++) + { + this.sizers[i].node.style.visibility = (this.isHandlesVisible()) ? '' : 'hidden'; + } + } + + if (this.rotationShape != null) + { + var alpha = mxUtils.toRadians((this.currentAlpha != null) ? this.currentAlpha : this.state.style[mxConstants.STYLE_ROTATION] || '0'); + var cos = Math.cos(alpha); + var sin = Math.sin(alpha); + + var ct = new mxPoint(this.state.getCenterX(), this.state.getCenterY()); + var pt = mxUtils.getRotatedPoint(this.getRotationHandlePosition(), cos, sin, ct); + + if (this.rotationShape.node != null) + { + this.moveSizerTo(this.rotationShape, pt.x, pt.y); + + // Hides rotation handle during text editing + this.rotationShape.node.style.visibility = + (this.state.view.graph.isEditing() || + !this.handlesVisible || !this.isHandlesVisible() || + !this.isRotationHandleVisible()) ? + 'hidden' : ''; + } + } + + if (this.selectionBorder != null) + { + this.selectionBorder.rotation = Number(this.state.style[mxConstants.STYLE_ROTATION] || '0'); + } + + if (this.edgeHandlers != null) + { + for (var i = 0; i < this.edgeHandlers.length; i++) + { + this.edgeHandlers[i].redraw(); + } + } +}; + +/** + * Function: isCustomHandleVisible + * + * Returns true if the given custom handle is visible. + */ +mxVertexHandler.prototype.isCustomHandleVisible = function(handle) +{ + return this.state.view.graph.getSelectionCount() == 1; +}; + +/** + * Function: getRotationHandlePosition + * + * Returns an that defines the rotation handle position. + */ +mxVertexHandler.prototype.getRotationHandlePosition = function() +{ + return new mxPoint(this.bounds.x + this.bounds.width / 2, this.bounds.y + this.rotationHandleVSpacing) +}; + +/** + * Function: isParentHighlightVisible + * + * Returns true if the parent highlight should be visible. This implementation + * always returns true. + */ +mxVertexHandler.prototype.isParentHighlightVisible = function() +{ + return !this.graph.isCellSelected(this.graph.model.getParent(this.state.cell)); +}; + +/** + * Function: destroyParentHighlight + * + * Destroys the parent highlight. + */ +mxVertexHandler.prototype.destroyParentHighlight = function() +{ + if (this.parentHighlight.state != null) + { + delete this.parentHighlight.state.parentHighlight; + delete this.parentHighlight.state; + } + + this.parentHighlight.destroy(); + this.parentHighlight = null; +}; + +/** + * Function: updateParentHighlight + * + * Updates the highlight of the parent if is true. + */ +mxVertexHandler.prototype.updateParentHighlight = function() +{ + if (!this.isDestroyed()) + { + var visible = this.isParentHighlightVisible(); + var parent = this.graph.model.getParent(this.state.cell); + var pstate = this.graph.view.getState(parent); + + if (this.parentHighlight != null) + { + if (this.graph.model.isVertex(parent) && visible) + { + var b = this.parentHighlight.bounds; + + if (pstate != null && (b.x != pstate.x || b.y != pstate.y || + b.width != pstate.width || b.height != pstate.height)) + { + this.parentHighlight.bounds = mxRectangle.fromRectangle(pstate); + this.parentHighlight.redraw(); + } + } + else + { + this.destroyParentHighlight(); + } + } + else if (this.parentHighlightEnabled && visible) + { + if (this.graph.model.isVertex(parent) && pstate != null && + pstate.parentHighlight == null) + { + this.parentHighlight = this.createParentHighlightShape(pstate); + this.parentHighlight.dialect = mxConstants.DIALECT_SVG; + this.parentHighlight.svgStrokeTolerance = 0; + this.parentHighlight.pointerEvents = false; + this.parentHighlight.rotation = Number(pstate.style[mxConstants.STYLE_ROTATION] || '0'); + this.parentHighlight.init(this.graph.getView().getOverlayPane()); + this.parentHighlight.redraw(); + + // Shows highlight once per parent + pstate.parentHighlight = this.parentHighlight; + this.parentHighlight.state = pstate; + } + } + } +}; + +/** + * 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.rotation = Number(this.state.style[mxConstants.STYLE_ROTATION] || '0'); + this.preview.redraw(); + } + + this.selectionBorder.bounds = this.getSelectionBorderBounds(); + this.selectionBorder.redraw(); + this.updateParentHighlight(); +}; + +/** + * Function: getSelectionBorderBounds + * + * Returns the bounds for the selection border. + */ +mxVertexHandler.prototype.getSelectionBorderBounds = function() +{ + return this.bounds; +}; + +/** + * Function: createSizers + * + * Destroys the handler and all its resources and DOM nodes. + */ +mxVertexHandler.prototype.createSizers = function() +{ + var resizable = this.graph.isCellResizable(this.state.cell) && + !this.graph.isCellLocked(this.state.cell); + var 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) + { + sizers.push(this.createSizer('nw-resize', i++)); + sizers.push(this.createSizer('n-resize', i++)); + sizers.push(this.createSizer('ne-resize', i++)); + sizers.push(this.createSizer('w-resize', i++)); + sizers.push(this.createSizer('e-resize', i++)); + sizers.push(this.createSizer('sw-resize', i++)); + sizers.push(this.createSizer('s-resize', i++)); + } + + 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); + sizers.push(this.labelShape); + } + } + else if (this.graph.isCellMovable(this.state.cell) && !resizable && + this.state.width < 2 && this.state.height < 2) + { + this.labelShape = this.createSizer(mxConstants.CURSOR_MOVABLE_VERTEX, + mxEvent.LABEL_HANDLE, null, mxConstants.LABEL_HANDLE_FILLCOLOR); + sizers.push(this.labelShape); + } + + // Adds the rotation handler + if (this.rotationShape == null) + { + this.rotationShape = this.createSizer(this.rotationCursor, mxEvent.ROTATION_HANDLE, + mxConstants.HANDLE_SIZE + 3, mxConstants.HANDLE_FILLCOLOR); + sizers.push(this.rotationShape); + } + + return sizers; +}; + +/** + * Function: destroyCustomHandles + * + * Destroys the handler and all its resources and DOM nodes. + */ +mxVertexHandler.prototype.destroyCustomHandles = function() +{ + if (this.customHandles != null) + { + for (var i = 0; i < this.customHandles.length; i++) + { + if (this.customHandles[i] != null) + { + this.customHandles[i].destroy(); + } + } + + this.customHandles = null; + } +}; + +/** + * Function: destroySizers + * + * Destroys the handler and all its resources and DOM nodes. + */ +mxVertexHandler.prototype.destroySizers = function() +{ + if (this.sizers != null) + { + for (var i = 0; i < this.sizers.length; i++) + { + this.sizers[i].destroy(); + } + + this.sizers = null; + this.rotationShape = null; + } +}; + +/** + * Function: isDestroyed + * + * Returns true if this handler was destroyed or not initialized. + */ +mxVertexHandler.prototype.isDestroyed = function() +{ + return this.selectionBorder == null; +}; + +/** + * Function: destroy + * + * Destroys the handler and all its resources and DOM nodes. + */ +mxVertexHandler.prototype.destroy = function() +{ + if (this.escapeHandler != null) + { + this.state.view.graph.removeListener(this.escapeHandler); + this.escapeHandler = null; + } + + if (this.preview != null) + { + this.preview.destroy(); + this.preview = null; + } + + if (this.ghostPreview != null) + { + this.ghostPreview.destroy(); + this.ghostPreview = null; + } + + if (this.selectionBorder != null) + { + this.selectionBorder.destroy(); + this.selectionBorder = null; + } + + if (this.parentHighlight != null) + { + this.destroyParentHighlight(); + } + + this.labelShape = null; + this.removeHint(); + this.destroySizers(); + this.destroyCustomHandles(); +}; diff --git a/src/main/mxgraph/index.txt b/src/main/mxgraph/index.txt new file mode 100644 index 000000000..f3631d6a0 --- /dev/null +++ b/src/main/mxgraph/index.txt @@ -0,0 +1,316 @@ +Document: API Specification + +Overview: + + This JavaScript library is divided into 8 packages. The top-level + class includes (or dynamically imports) everything else. The current version + is stored in . + + The *editor* package provides the classes required to implement a diagram + editor. The main class in this package is . + + The *view* and *model* packages implement the graph component, represented + by . It refers to a which contains s and + caches the state of the cells in a . The cells are painted + using a based on the appearance defined in . + Undo history is implemented in . To display an icon on the + graph, may be used. Validation rules are defined with + . + + The *handler*, *layout* and *shape* packages contain event listeners, + layout algorithms and shapes, respectively. The graph event listeners + include for rubberband selection, + for tooltips and for basic cell modifications. + implements a tree layout algorithm, and the + shape package provides various shapes, which are subclasses of + . + + The *util* package provides utility classes including for + copy-paste, for drag-and-drop, for keys and + values of stylesheets, and for cross-browser + event-handling and general purpose functions, for + internationalization and for console output. + + The *io* package implements a generic for turning + JavaScript objects into XML. The main class is . + is the global registry for custom codecs. + +Events: + + There are three different types of events, namely native DOM events, + which are fired in an , and + which are fired in . + + Some helper methods for handling native events are provided in . It + also takes care of resolving cycles between DOM nodes and JavaScript event + handlers, which can lead to memory leaks in IE6. + + Most custom events in mxGraph are implemented using . Its + listeners are functions that take a sender and . Additionally, + the class fires special which are handled using + mouse listeners, which are objects that provide a mousedown, mousemove and + mouseup method. + + Events in are fired using . + Listeners are added and removed using and + . in are fired using + . Listeners are added and removed using + and , respectively. + +Key bindings: + + The following key bindings are defined for mouse events in the client across + all browsers and platforms: + + - Control-Drag: Duplicates (clones) selected cells + - Shift-Rightlick: Shows the context menu + - Alt-Click: Forces rubberband (aka. marquee) + - Control-Select: Toggles the selection state + - Shift-Drag: Constrains the offset to one direction + - Shift-Control-Drag: Panning (also Shift-Rightdrag) + +Configuration: + + The following global variables may be defined before the client is loaded to + specify its language or base path, respectively. + + - mxBasePath: Specifies the path in . + - mxImageBasePath: Specifies the path in . + - mxLanguage: Specifies the language for resources in . + - mxDefaultLanguage: Specifies the default language in . + - mxLoadResources: Specifies if any resources should be loaded. Default is true. + - mxLoadStylesheets: Specifies if any stylesheets should be loaded. Default is true. + +Reserved Words: + + The mx prefix is used for all classes and objects in mxGraph. The mx prefix + can be seen as the global namespace for all JavaScript code in mxGraph. The + following fieldnames should not be used in objects. + + - *mxObjectId*: If the object is used with mxObjectIdentity + - *as*: If the object is a field of another object + - *id*: If the object is an idref in a codec + - *mxListenerList*: Added to DOM nodes when used with + - *window._mxDynamicCode*: Temporarily used to load code in Safari and Chrome + (see ). + - *_mxJavaScriptExpression*: Global variable that is temporarily used to + evaluate code in Safari, Opera, Firefox 3 and IE (see ). + +Files: + + The library contains these relative filenames. All filenames are relative + to . + +Built-in Images: + + All images are loaded from the , + which you can change to reflect your environment. The image variables can + also be changed individually. + + - mxGraph.prototype.collapsedImage + - mxGraph.prototype.expandedImage + - mxGraph.prototype.warningImage + - mxWindow.prototype.closeImage + - mxWindow.prototype.minimizeImage + - mxWindow.prototype.normalizeImage + - mxWindow.prototype.maximizeImage + - mxWindow.prototype.resizeImage + - mxPopupMenu.prototype.submenuImage + - mxUtils.errorImage + - mxConstraintHandler.prototype.pointImage + + The basename of the warning image (images/warning without extension) used in + is defined in . + +Resources: + + The and classes add the following resources to + at class loading time: + + - resources/editor*.properties + - resources/graph*.properties + + By default, the library ships with English and German resource files. + +Images: + + Recommendations for using images. Use GIF images (256 color palette) in HTML + elements (such as the toolbar and context menu), and PNG images (24 bit) for + all images which appear inside the graph component. + + - For PNG images inside HTML elements, Internet Explorer will ignore any + transparency information. + - For GIF images inside the graph, Firefox on the Mac will display strange + colors. Furthermore, only the first image for animated GIFs is displayed + on the Mac. + + For faster image rendering during application runtime, images can be + prefetched using the following code: + + (code) + var image = new Image(); + image.src = url_to_image; + (end) + +Deployment: + + The client is added to the page using the following script tag inside the + head of a document: + + (code) + + (end) + + The deployment version of the mxClient.js file contains all required code + in a single file. For deployment, the complete javascript/src directory is + required. + +Source Code: + + If you are a source code customer and you wish to develop using the + full source code, the commented source code is shipped in the + javascript/devel/source.zip file. It contains one file for each class + in mxGraph. To use the source code the source.zip file must be + uncompressed and the mxClient.js URL in the HTML page must be changed + to reference the uncompressed mxClient.js from the source.zip file. + +Compression: + + When using Apache2 with mod_deflate, you can use the following directive + in src/js/.htaccess to speedup the loading of the JavaScript sources: + + (code) + SetOutputFilter DEFLATE + (end) + +Classes: + + There are two types of "classes" in mxGraph: classes and singletons (where + only one instance exists). Singletons are mapped to global objects where the + variable name equals the classname. For example mxConstants is an object with + all the constants defined as object fields. Normal classes are mapped to a + constructor function and a prototype which defines the instance fields and + methods. For example, is a function and mxEditor.prototype is the + prototype for the object that the mxEditor function creates. The mx prefix is + a convention that is used for all classes in the mxGraph package to avoid + conflicts with other objects in the global namespace. + +Subclassing: + + For subclassing, the superclass must provide a constructor that is either + parameterless or handles an invocation with no arguments. Furthermore, the + special constructor field must be redefined after extending the prototype. + For example, the superclass of mxEditor is . This is + represented in JavaScript by first "inheriting" all fields and methods from + the superclass by assigning the prototype to an instance of the superclass, + eg. mxEditor.prototype = new mxEventSource() and redefining the constructor + field using mxEditor.prototype.constructor = mxEditor. The latter rule is + applied so that the type of an object can be retrieved via the name of it’s + constructor using mxUtils.getFunctionName(obj.constructor). + +Constructor: + + For subclassing in mxGraph, the same scheme should be applied. For example, + for subclassing the class, first a constructor must be defined for + the new class. The constructor calls the super constructor with any arguments + that it may have using the call function on the mxGraph function object, + passing along explitely each argument: + + (code) + function MyGraph(container) + { + mxGraph.call(this, container); + } + (end) + + The prototype of MyGraph inherits from mxGraph as follows. As usual, the + constructor is redefined after extending the superclass: + + (code) + MyGraph.prototype = new mxGraph(); + MyGraph.prototype.constructor = MyGraph; + (end) + + You may want to define the codec associated for the class after the above + code. This code will be executed at class loading time and makes sure the + same codec is used to encode instances of mxGraph and MyGraph. + + (code) + var codec = mxCodecRegistry.getCodec(mxGraph); + codec.template = new MyGraph(); + mxCodecRegistry.register(codec); + (end) + +Functions: + + In the prototype for MyGraph, functions of mxGraph can then be extended as + follows. + + (code) + MyGraph.prototype.isCellSelectable = function(cell) + { + var selectable = mxGraph.prototype.isSelectable.apply(this, arguments); + + var geo = this.model.getGeometry(cell); + return selectable && (geo == null || !geo.relative); + } + (end) + + The supercall in the first line is optional. It is done using the apply + function on the isSelectable function object of the mxGraph prototype, using + the special this and arguments variables as parameters. Calls to the + superclass function are only possible if the function is not replaced in the + superclass as follows, which is another way of “subclassing” in JavaScript. + + (code) + mxGraph.prototype.isCellSelectable = function(cell) + { + var geo = this.model.getGeometry(cell); + return selectable && + (geo == null || + !geo.relative); + } + (end) + + The above scheme is useful if a function definition needs to be replaced + completely. + + In order to add new functions and fields to the subclass, the following code + is used. The example below adds a new function to return the XML + representation of the graph model: + + (code) + MyGraph.prototype.getXml = function() + { + var enc = new mxCodec(); + return enc.encode(this.getModel()); + } + (end) + +Variables: + + Likewise, a new field is declared and defined as follows. + + (code) + MyGraph.prototype.myField = 'Hello, World!'; + (end) + + Note that the value assigned to myField is created only once, that is, all + instances of MyGraph share the same value. If you require instance-specific + values, then the field must be defined in the constructor instead. + + (code) + function MyGraph(container) + { + mxGraph.call(this, container); + + this.myField = new Array(); + } + (end) + + Finally, a new instance of MyGraph is created using the following code, where + container is a DOM node that acts as a container for the graph view: + + (code) + var graph = new MyGraph(container); + (end) diff --git a/src/main/mxgraph/io/mxCellCodec.js b/src/main/mxgraph/io/mxCellCodec.js new file mode 100644 index 000000000..75c5bb7bb --- /dev/null +++ b/src/main/mxgraph/io/mxCellCodec.js @@ -0,0 +1,188 @@ +/** + * Copyright (c) 2006-2015, JGraph Ltd + * Copyright (c) 2006-2015, Gaudenz Alder + */ +mxCodecRegistry.register(function() +{ + /** + * Class: mxCellCodec + * + * Codec for s. This class is created and registered + * dynamically at load time and used implicitly via + * and the . + * + * Transient Fields: + * + * - children + * - edges + * - overlays + * - mxTransient + * + * Reference Fields: + * + * - parent + * - source + * - target + * + * Transient fields can be added using the following code: + * + * mxCodecRegistry.getCodec(mxCell).exclude.push('name_of_field'); + * + * To subclass , replace the template and add an alias as + * follows. + * + * (code) + * function CustomCell(value, geometry, style) + * { + * mxCell.apply(this, arguments); + * } + * + * mxUtils.extend(CustomCell, mxCell); + * + * mxCodecRegistry.getCodec(mxCell).template = new CustomCell(); + * mxCodecRegistry.addAlias('CustomCell', 'mxCell'); + * (end) + */ + var codec = new mxObjectCodec(new mxCell(), + ['children', 'edges', 'overlays', 'mxTransient'], + ['parent', 'source', 'target']); + + /** + * Function: isCellCodec + * + * Returns true since this is a cell codec. + */ + codec.isCellCodec = function() + { + return true; + }; + + /** + * Overidden to disable conversion of value to number. + */ + codec.isNumericAttribute = function(dec, attr, obj) + { + return attr.nodeName !== 'value' && mxObjectCodec.prototype.isNumericAttribute.apply(this, arguments); + }; + + /** + * Function: isExcluded + * + * Excludes user objects that are XML nodes. + */ + codec.isExcluded = function(obj, attr, value, isWrite) + { + return mxObjectCodec.prototype.isExcluded.apply(this, arguments) || + (isWrite && attr == 'value' && mxUtils.isNode(value)); + }; + + /** + * Function: afterEncode + * + * Encodes an and wraps the XML up inside the + * XML of the user object (inversion). + */ + codec.afterEncode = function(enc, obj, node) + { + if (obj.value != null && mxUtils.isNode(obj.value)) + { + // Wraps the graphical annotation up in the user object (inversion) + // by putting the result of the default encoding into a clone of the + // user object (node type 1) and returning this cloned user object. + var tmp = node; + node = mxUtils.importNode(enc.document, obj.value, true); + node.appendChild(tmp); + + // Moves the id attribute to the outermost XML node, namely the + // node which denotes the object boundaries in the file. + var id = tmp.getAttribute('id'); + node.setAttribute('id', id); + tmp.removeAttribute('id'); + } + + return node; + }; + + /** + * Function: beforeDecode + * + * Decodes an and uses the enclosing XML node as + * the user object for the cell (inversion). + */ + codec.beforeDecode = function(dec, node, obj) + { + var inner = node.cloneNode(true); + var classname = this.getName(); + + if (node.nodeName != classname) + { + // Passes the inner graphical annotation node to the + // object codec for further processing of the cell. + var tmp = node.getElementsByTagName(classname)[0]; + + if (tmp != null && tmp.parentNode == node) + { + mxUtils.removeWhitespace(tmp, true); + mxUtils.removeWhitespace(tmp, false); + tmp.parentNode.removeChild(tmp); + inner = tmp; + } + else + { + inner = null; + } + + // Creates the user object out of the XML node + obj.value = node.cloneNode(true); + var id = obj.value.getAttribute('id'); + + if (id != null) + { + obj.setId(id); + obj.value.removeAttribute('id'); + } + } + else + { + // Uses ID from XML file as ID for cell in model + obj.setId(node.getAttribute('id')); + } + + // Preprocesses and removes all Id-references in order to use the + // correct encoder (this) for the known references to cells (all). + if (inner != null) + { + for (var i = 0; i < this.idrefs.length; i++) + { + var attr = this.idrefs[i]; + var ref = inner.getAttribute(attr); + + if (ref != null) + { + inner.removeAttribute(attr); + var object = dec.objects[ref] || dec.lookup(ref); + + if (object == null) + { + // Needs to decode forward reference + var element = dec.getElementById(ref); + + if (element != null) + { + var decoder = mxCodecRegistry.codecs[element.nodeName] || this; + object = decoder.decode(dec, element); + } + } + + obj[attr] = object; + } + } + } + + return inner; + }; + + // Returns the codec into the registry + return codec; + +}()); diff --git a/src/main/mxgraph/io/mxChildChangeCodec.js b/src/main/mxgraph/io/mxChildChangeCodec.js new file mode 100644 index 000000000..5cff378dd --- /dev/null +++ b/src/main/mxgraph/io/mxChildChangeCodec.js @@ -0,0 +1,168 @@ +/** + * Copyright (c) 2006-2015, JGraph Ltd + * Copyright (c) 2006-2015, Gaudenz Alder + */ +mxCodecRegistry.register(function() +{ + /** + * Class: mxChildChangeCodec + * + * Codec for s. This class is created and registered + * dynamically at load time and used implicitly via and + * the . + * + * Transient Fields: + * + * - model + * - previous + * - previousIndex + * - child + * + * Reference Fields: + * + * - parent + */ + var codec = new mxObjectCodec(new mxChildChange(), + ['model', 'child', 'previousIndex'], + ['parent', 'previous']); + + /** + * Function: isReference + * + * Returns true for the child attribute if the child + * cell had a previous parent or if we're reading the + * child as an attribute rather than a child node, in + * which case it's always a reference. + */ + codec.isReference = function(obj, attr, value, isWrite) + { + if (attr == 'child' && (!isWrite || obj.model.contains(obj.previous))) + { + return true; + } + + return mxUtils.indexOf(this.idrefs, attr) >= 0; + }; + + /** + * Function: isExcluded + * + * Excludes references to parent or previous if not in the model. + */ + codec.isExcluded = function(obj, attr, value, write) + { + return mxObjectCodec.prototype.isExcluded.apply(this, arguments) || + (write && value != null && (attr == 'previous' || + attr == 'parent') && !obj.model.contains(value)); + }; + + /** + * Function: afterEncode + * + * Encodes the child recusively and adds the result + * to the given node. + */ + codec.afterEncode = function(enc, obj, node) + { + if (this.isReference(obj, 'child', obj.child, true)) + { + // Encodes as reference (id) + node.setAttribute('child', enc.getId(obj.child)); + } + else + { + // At this point, the encoder is no longer able to know which cells + // are new, so we have to encode the complete cell hierarchy and + // ignore the ones that are already there at decoding time. Note: + // This can only be resolved by moving the notify event into the + // execute of the edit. + enc.encodeCell(obj.child, node); + } + + return node; + }; + + /** + * Function: beforeDecode + * + * Decodes the any child nodes as using the respective + * codec from the registry. + */ + codec.beforeDecode = function(dec, node, obj) + { + if (node.firstChild != null && + node.firstChild.nodeType == mxConstants.NODETYPE_ELEMENT) + { + // Makes sure the original node isn't modified + node = node.cloneNode(true); + + var tmp = node.firstChild; + obj.child = dec.decodeCell(tmp, false); + + var tmp2 = tmp.nextSibling; + tmp.parentNode.removeChild(tmp); + tmp = tmp2; + + while (tmp != null) + { + tmp2 = tmp.nextSibling; + + if (tmp.nodeType == mxConstants.NODETYPE_ELEMENT) + { + // Ignores all existing cells because those do not need to + // be re-inserted into the model. Since the encoded version + // of these cells contains the new parent, this would leave + // to an inconsistent state on the model (ie. a parent + // change without a call to parentForCellChanged). + var id = tmp.getAttribute('id'); + + if (dec.lookup(id) == null) + { + dec.decodeCell(tmp); + } + } + + tmp.parentNode.removeChild(tmp); + tmp = tmp2; + } + } + else + { + var childRef = node.getAttribute('child'); + obj.child = dec.getObject(childRef); + } + + return node; + }; + + /** + * Function: afterDecode + * + * Restores object state in the child change. + */ + codec.afterDecode = function(dec, node, obj) + { + // Cells are decoded here after a complete transaction so the previous + // parent must be restored on the cell for the case where the cell was + // added. This is needed for the local model to identify the cell as a + // new cell and register the ID. + if (obj.child != null) + { + if (obj.child.parent != null && obj.previous != null && + obj.child.parent != obj.previous) + { + obj.previous = obj.child.parent; + } + + obj.child.parent = obj.previous; + obj.previous = obj.parent; + obj.previousIndex = obj.index; + } + + return obj; + }; + + // Returns the codec into the registry + return codec; + +}()); diff --git a/src/main/mxgraph/io/mxCodec.js b/src/main/mxgraph/io/mxCodec.js new file mode 100644 index 000000000..fbdea9ca1 --- /dev/null +++ b/src/main/mxgraph/io/mxCodec.js @@ -0,0 +1,653 @@ +/** + * Copyright (c) 2006-2015, JGraph Ltd + * Copyright (c) 2006-2015, Gaudenz Alder + */ +/** + * Class: mxCodec + * + * XML codec for JavaScript object graphs. See for a + * description of the general encoding/decoding scheme. This class uses the + * codecs registered in for encoding/decoding each object. + * + * References: + * + * In order to resolve references, especially forward references, the mxCodec + * constructor must be given the document that contains the referenced + * elements. + * + * Examples: + * + * The following code is used to encode a graph model. + * + * (code) + * var encoder = new mxCodec(); + * var result = encoder.encode(graph.getModel()); + * var xml = mxUtils.getXml(result); + * (end) + * + * Example: + * + * Using the code below, an XML document is decoded into an existing model. The + * document may be obtained using one of the functions in mxUtils for loading + * an XML file, eg. , or using for parsing an + * XML string. + * + * (code) + * var doc = mxUtils.parseXml(xmlString); + * var codec = new mxCodec(doc); + * codec.decode(doc.documentElement, graph.getModel()); + * (end) + * + * Example: + * + * This example demonstrates parsing a list of isolated cells into an existing + * graph model. Note that the cells do not have a parent reference so they can + * be added anywhere in the cell hierarchy after parsing. + * + * (code) + * var xml = ''; + * var doc = mxUtils.parseXml(xml); + * var codec = new mxCodec(doc); + * var elt = doc.documentElement.firstChild; + * var cells = []; + * + * while (elt != null) + * { + * cells.push(codec.decode(elt)); + * elt = elt.nextSibling; + * } + * + * graph.addCells(cells); + * (end) + * + * Example: + * + * Using the following code, the selection cells of a graph are encoded and the + * output is displayed in a dialog box. + * + * (code) + * var enc = new mxCodec(); + * var cells = graph.getSelectionCells(); + * mxUtils.alert(mxUtils.getPrettyXml(enc.encode(cells))); + * (end) + * + * Newlines in the XML can be converted to
, in which case a '
' argument + * must be passed to as the second argument. + * + * Debugging: + * + * For debugging I/O you can use the following code to get the sequence of + * encoded objects: + * + * (code) + * var oldEncode = mxCodec.prototype.encode; + * mxCodec.prototype.encode = function(obj) + * { + * mxLog.show(); + * mxLog.debug('mxCodec.encode: obj='+mxUtils.getFunctionName(obj.constructor)); + * + * return oldEncode.apply(this, arguments); + * }; + * (end) + * + * Note that the I/O system adds object codecs for new object automatically. For + * decoding those objects, the constructor should be written as follows: + * + * (code) + * var MyObj = function(name) + * { + * // ... + * }; + * (end) + * + * Constructor: mxCodec + * + * Constructs an XML encoder/decoder for the specified + * owner document. + * + * Parameters: + * + * document - Optional XML document that contains the data. + * If no document is specified then a new document is created + * using . + */ +function mxCodec(document) +{ + this.document = document || mxUtils.createXmlDocument(); + this.objects = []; +}; + +/** + * Variable: allowlist + * + * Array of strings that specifies the types to be decoded. Null means all + * types are allowed. Default is null. + */ +mxCodec.allowlist = null; + +/** + * Variable: document + * + * The owner document of the codec. + */ +mxCodec.prototype.document = null; + +/** + * Variable: objects + * + * Maps from IDs to objects. + */ +mxCodec.prototype.objects = null; + +/** + * Variable: elements + * + * Lookup table for resolving IDs to elements. + */ +mxCodec.prototype.elements = null; + +/** + * Variable: encodeDefaults + * + * Specifies if default values should be encoded. Default is false. + */ +mxCodec.prototype.encodeDefaults = false; + + +/** + * Function: putObject + * + * Assoiates the given object with the given ID and returns the given object. + * + * Parameters + * + * id - ID for the object to be associated with. + * obj - Object to be associated with the ID. + */ +mxCodec.prototype.putObject = function(id, obj) +{ + this.objects[id] = obj; + + return obj; +}; + +/** + * Function: getObject + * + * Returns the decoded object for the element with the specified ID in + * . If the object is not known then is used to find an + * object. If no object is found, then the element with the respective ID + * from the document is parsed using . + */ +mxCodec.prototype.getObject = function(id) +{ + var obj = null; + + if (id != null) + { + obj = this.objects[id]; + + if (obj == null) + { + obj = this.lookup(id); + + if (obj == null) + { + var node = this.getElementById(id); + + if (node != null) + { + obj = this.decode(node); + } + } + } + } + + return obj; +}; + +/** + * Function: lookup + * + * Hook for subclassers to implement a custom lookup mechanism for cell IDs. + * This implementation always returns null. + * + * Example: + * + * (code) + * var codec = new mxCodec(); + * codec.lookup = function(id) + * { + * return model.getCell(id); + * }; + * (end) + * + * Parameters: + * + * id - ID of the object to be returned. + */ +mxCodec.prototype.lookup = function(id) +{ + return null; +}; + +/** + * Function: getElementById + * + * Returns the element with the given ID from . + * + * Parameters: + * + * id - String that contains the ID. + */ +mxCodec.prototype.getElementById = function(id) +{ + this.updateElements(); + + return this.elements[id]; +}; + +/** + * Function: updateElements + * + * Returns the element with the given ID from . + * + * Parameters: + * + * id - String that contains the ID. + */ +mxCodec.prototype.updateElements = function() +{ + if (this.elements == null) + { + this.elements = new Object(); + + if (this.document.documentElement != null) + { + this.addElement(this.document.documentElement); + } + } +}; + +/** + * Function: addElement + * + * Adds the given element to if it has an ID. + */ +mxCodec.prototype.addElement = function(node) +{ + if (node.nodeType == mxConstants.NODETYPE_ELEMENT) + { + var id = node.getAttribute('id'); + + if (id != null) + { + if (this.elements[id] == null) + { + this.elements[id] = node; + } + else if (this.elements[id] != node) + { + throw new Error(id + ': Duplicate ID'); + } + } + } + + node = node.firstChild; + + while (node != null) + { + this.addElement(node); + node = node.nextSibling; + } +}; + +/** + * Function: getId + * + * Returns the ID of the specified object. This implementation + * calls first and if that returns null handles + * the object as an by returning their IDs using + * . If no ID exists for the given cell, then + * an on-the-fly ID is generated using . + * + * Parameters: + * + * obj - Object to return the ID for. + */ +mxCodec.prototype.getId = function(obj) +{ + var id = null; + + if (obj != null) + { + id = this.reference(obj); + + if (id == null && obj instanceof mxCell) + { + id = obj.getId(); + + if (id == null) + { + // Uses an on-the-fly Id + id = mxCellPath.create(obj); + + if (id.length == 0) + { + id = 'root'; + } + } + } + } + + return id; +}; + +/** + * Function: reference + * + * Hook for subclassers to implement a custom method + * for retrieving IDs from objects. This implementation + * always returns null. + * + * Example: + * + * (code) + * var codec = new mxCodec(); + * codec.reference = function(obj) + * { + * return obj.getCustomId(); + * }; + * (end) + * + * Parameters: + * + * obj - Object whose ID should be returned. + */ +mxCodec.prototype.reference = function(obj) +{ + return null; +}; + +/** + * Function: encode + * + * Encodes the specified object and returns the resulting + * XML node. + * + * Parameters: + * + * obj - Object to be encoded. + */ +mxCodec.prototype.encode = function(obj) +{ + var node = null; + + if (obj != null && obj.constructor != null) + { + var enc = mxCodecRegistry.getCodec(obj.constructor); + + if (enc != null) + { + node = enc.encode(this, obj); + } + else + { + if (mxUtils.isNode(obj)) + { + node = mxUtils.importNode(this.document, obj, true); + } + else + { + mxLog.warn('mxCodec.encode: No codec for ' + mxUtils.getFunctionName(obj.constructor)); + } + } + } + + return node; +}; + +/** + * Function: decode + * + * Decodes the given XML node. The optional "into" + * argument specifies an existing object to be + * used. If no object is given, then a new instance + * is created using the constructor from the codec. + * + * The function returns the passed in object or + * the new instance if no object was given. + * + * Parameters: + * + * node - XML node to be decoded. + * into - Optional object to be decodec into. + */ +mxCodec.prototype.decode = function(node, into) +{ + this.updateElements(); + var obj = null; + + if (node != null && node.nodeType == mxConstants.NODETYPE_ELEMENT) + { + var ctor = this.getConstructor(node.nodeName); + var dec = mxCodecRegistry.getCodec(ctor); + + if (dec != null) + { + obj = dec.decode(this, node, into); + } + else + { + obj = node.cloneNode(true); + obj.removeAttribute('as'); + } + } + + return obj; +}; + +/** + * Function: getConstructor + * + * Returns the constructor for the given object type. + * + * Parameters: + * + * name - Name of the type to be returned. + */ +mxCodec.prototype.getConstructor = function(name) +{ + var ctor = null; + + try + { + if (mxCodec.allowlist == null || mxUtils.indexOf( + mxCodec.allowlist, name) >= 0) + { + ctor = window[name]; + } + else if (window.console != null) + { + console.error('mxCodec.getConstructor: ' + name + + ' not allowed in mxCodec.allowlist'); + } + } + catch (err) + { + // ignore + } + + return ctor; +}; + +/** + * Function: encodeCell + * + * Encoding of cell hierarchies is built-into the core, but + * is a higher-level function that needs to be explicitely + * used by the respective object encoders (eg. , + * and ). This + * implementation writes the given cell and its children as a + * (flat) sequence into the given node. The children are not + * encoded if the optional includeChildren is false. The + * function is in charge of adding the result into the + * given node and has no return value. + * + * Parameters: + * + * cell - to be encoded. + * node - Parent XML node to add the encoded cell into. + * includeChildren - Optional boolean indicating if the + * function should include all descendents. Default is true. + */ +mxCodec.prototype.encodeCell = function(cell, node, includeChildren) +{ + node.appendChild(this.encode(cell)); + + if (includeChildren == null || includeChildren) + { + var childCount = cell.getChildCount(); + + for (var i = 0; i < childCount; i++) + { + this.encodeCell(cell.getChildAt(i), node); + } + } +}; + +/** + * Function: isCellCodec + * + * Returns true if the given codec is a cell codec. This uses + * to check if the codec is of the + * given type. + */ +mxCodec.prototype.isCellCodec = function(codec) +{ + if (codec != null && typeof(codec.isCellCodec) == 'function') + { + return codec.isCellCodec(); + } + + return false; +}; + +/** + * Function: decodeCell + * + * Decodes cells that have been encoded using inversion, ie. + * where the user object is the enclosing node in the XML, + * and restores the group and graph structure in the cells. + * Returns a new instance that represents the + * given node. + * + * Parameters: + * + * node - XML node that contains the cell data. + * restoreStructures - Optional boolean indicating whether + * the graph structure should be restored by calling insert + * and insertEdge on the parent and terminals, respectively. + * Default is true. + */ +mxCodec.prototype.decodeCell = function(node, restoreStructures) +{ + restoreStructures = (restoreStructures != null) ? restoreStructures : true; + var cell = null; + + if (node != null && node.nodeType == mxConstants.NODETYPE_ELEMENT) + { + // Tries to find a codec for the given node name. If that does + // not return a codec then the node is the user object (an XML node + // that contains the mxCell, aka inversion). + var decoder = mxCodecRegistry.getCodec(node.nodeName); + + // Tries to find the codec for the cell inside the user object. + // This assumes all node names inside the user object are either + // not registered or they correspond to a class for cells. + if (!this.isCellCodec(decoder)) + { + var child = node.firstChild; + + while (child != null && !this.isCellCodec(decoder)) + { + decoder = mxCodecRegistry.getCodec(child.nodeName); + child = child.nextSibling; + } + } + + if (!this.isCellCodec(decoder)) + { + decoder = mxCodecRegistry.getCodec(mxCell); + } + + cell = decoder.decode(this, node); + + if (restoreStructures) + { + this.insertIntoGraph(cell); + } + } + + return cell; +}; + +/** + * Function: insertIntoGraph + * + * Inserts the given cell into its parent and terminal cells. + */ +mxCodec.prototype.insertIntoGraph = function(cell) +{ + var parent = cell.parent; + var source = cell.getTerminal(true); + var target = cell.getTerminal(false); + + // Fixes possible inconsistencies during insert into graph + cell.setTerminal(null, false); + cell.setTerminal(null, true); + cell.parent = null; + + if (parent != null) + { + if (parent == cell) + { + throw new Error(parent.id + ': Self Reference'); + } + else + { + parent.insert(cell); + } + } + + if (source != null) + { + source.insertEdge(cell, true); + } + + if (target != null) + { + target.insertEdge(cell, false); + } +}; + +/** + * Function: setAttribute + * + * Sets the attribute on the specified node to value. This is a + * helper method that makes sure the attribute and value arguments + * are not null. + * + * Parameters: + * + * node - XML node to set the attribute for. + * attributes - Attributename to be set. + * value - New value of the attribute. + */ +mxCodec.prototype.setAttribute = function(node, attribute, value) +{ + if (attribute != null && value != null) + { + node.setAttribute(attribute, value); + } +}; diff --git a/src/main/mxgraph/io/mxCodecRegistry.js b/src/main/mxgraph/io/mxCodecRegistry.js new file mode 100644 index 000000000..42ebcd7ac --- /dev/null +++ b/src/main/mxgraph/io/mxCodecRegistry.js @@ -0,0 +1,137 @@ +/** + * Copyright (c) 2006-2015, JGraph Ltd + * Copyright (c) 2006-2015, Gaudenz Alder + */ +var mxCodecRegistry = +{ + /** + * Class: mxCodecRegistry + * + * Singleton class that acts as a global registry for codecs. + * + * Adding an : + * + * 1. Define a default codec with a new instance of the + * object to be handled. + * + * (code) + * var codec = new mxObjectCodec(new mxGraphModel()); + * (end) + * + * 2. Define the functions required for encoding and decoding + * objects. + * + * (code) + * codec.encode = function(enc, obj) { ... } + * codec.decode = function(dec, node, into) { ... } + * (end) + * + * 3. Register the codec in the . + * + * (code) + * mxCodecRegistry.register(codec); + * (end) + * + * may be used to either create a new + * instance of an object or to configure an existing instance, + * in which case the into argument points to the existing + * object. In this case, we say the codec "configures" the + * object. + * + * Variable: codecs + * + * Maps from constructor names to codecs. + */ + codecs: [], + + /** + * Variable: aliases + * + * Maps from classnames to codecnames. + */ + aliases: [], + + /** + * Function: register + * + * Registers a new codec and associates the name of the template + * constructor in the codec with the codec object. + * + * Parameters: + * + * codec - to be registered. + */ + register: function(codec) + { + if (codec != null) + { + var name = codec.getName(); + mxCodecRegistry.codecs[name] = codec; + + var classname = mxUtils.getFunctionName(codec.template.constructor); + + if (classname != name) + { + mxCodecRegistry.addAlias(classname, name); + } + } + + return codec; + }, + + /** + * Function: addAlias + * + * Adds an alias for mapping a classname to a codecname. + */ + addAlias: function(classname, codecname) + { + mxCodecRegistry.aliases[classname] = codecname; + }, + + /** + * Function: getCodec + * + * Returns a codec that handles objects that are constructed + * using the given constructor. + * + * Parameters: + * + * ctor - JavaScript constructor function. + */ + getCodec: function(ctor) + { + var codec = null; + + if (ctor != null) + { + var name = mxUtils.getFunctionName(ctor); + var tmp = mxCodecRegistry.aliases[name]; + + if (tmp != null) + { + name = tmp; + } + + codec = mxCodecRegistry.codecs[name]; + + // Registers a new default codec for the given constructor + // if no codec has been previously defined. + if (codec == null) + { + try + { + codec = new mxObjectCodec(new ctor()); + mxCodecRegistry.register(codec); + } + catch (e) + { + // ignore + } + } + } + + return codec; + } + +}; diff --git a/src/main/mxgraph/io/mxGenericChangeCodec.js b/src/main/mxgraph/io/mxGenericChangeCodec.js new file mode 100644 index 000000000..23d81b8ee --- /dev/null +++ b/src/main/mxgraph/io/mxGenericChangeCodec.js @@ -0,0 +1,64 @@ +/** + * Copyright (c) 2006-2015, JGraph Ltd + * Copyright (c) 2006-2015, Gaudenz Alder + */ +/** + * Class: mxGenericChangeCodec + * + * Codec for s, s, s, + * s and s. This class is created + * and registered dynamically at load time and used implicitly + * via and the . + * + * Transient Fields: + * + * - model + * - previous + * + * Reference Fields: + * + * - cell + * + * Constructor: mxGenericChangeCodec + * + * Factory function that creates a for + * the specified change and fieldname. + * + * Parameters: + * + * obj - An instance of the change object. + * variable - The fieldname for the change data. + */ +var mxGenericChangeCodec = function(obj, variable) +{ + var codec = new mxObjectCodec(obj, ['model', 'previous'], ['cell']); + + /** + * Function: afterDecode + * + * Restores the state by assigning the previous value. + */ + codec.afterDecode = function(dec, node, obj) + { + // Allows forward references in sessions. This is a workaround + // for the sequence of edits in mxGraph.moveCells and cellsAdded. + if (mxUtils.isNode(obj.cell)) + { + obj.cell = dec.decodeCell(obj.cell, false); + } + + obj.previous = obj[variable]; + + return obj; + }; + + return codec; +}; + +// Registers the codecs +mxCodecRegistry.register(mxGenericChangeCodec(new mxValueChange(), 'value')); +mxCodecRegistry.register(mxGenericChangeCodec(new mxStyleChange(), 'style')); +mxCodecRegistry.register(mxGenericChangeCodec(new mxGeometryChange(), 'geometry')); +mxCodecRegistry.register(mxGenericChangeCodec(new mxCollapseChange(), 'collapsed')); +mxCodecRegistry.register(mxGenericChangeCodec(new mxVisibleChange(), 'visible')); +mxCodecRegistry.register(mxGenericChangeCodec(new mxCellAttributeChange(), 'value')); diff --git a/src/main/mxgraph/io/mxGraphCodec.js b/src/main/mxgraph/io/mxGraphCodec.js new file mode 100644 index 000000000..f7f9a15ec --- /dev/null +++ b/src/main/mxgraph/io/mxGraphCodec.js @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2006-2015, JGraph Ltd + * Copyright (c) 2006-2015, Gaudenz Alder + */ +mxCodecRegistry.register(function() +{ + /** + * Class: mxGraphCodec + * + * Codec for s. This class is created and registered + * dynamically at load time and used implicitly via + * and the . + * + * Transient Fields: + * + * - graphListeners + * - eventListeners + * - view + * - container + * - cellRenderer + * - editor + * - selection + */ + return new mxObjectCodec(new mxGraph(), + ['graphListeners', 'eventListeners', 'view', 'container', + 'cellRenderer', 'editor', 'selection']); + +}()); diff --git a/src/main/mxgraph/io/mxGraphViewCodec.js b/src/main/mxgraph/io/mxGraphViewCodec.js new file mode 100644 index 000000000..5343ae055 --- /dev/null +++ b/src/main/mxgraph/io/mxGraphViewCodec.js @@ -0,0 +1,197 @@ +/** + * Copyright (c) 2006-2015, JGraph Ltd + * Copyright (c) 2006-2015, Gaudenz Alder + */ +mxCodecRegistry.register(function() +{ + /** + * Class: mxGraphViewCodec + * + * Custom encoder for s. This class is created + * and registered dynamically at load time and used implicitly via + * and the . This codec only writes views + * into a XML format that can be used to create an image for + * the graph, that is, it contains absolute coordinates with + * computed perimeters, edge styles and cell styles. + */ + var codec = new mxObjectCodec(new mxGraphView()); + + /** + * Function: encode + * + * Encodes the given using + * starting at the model's root. This returns the + * top-level graph node of the recursive encoding. + */ + codec.encode = function(enc, view) + { + return this.encodeCell(enc, view, + view.graph.getModel().getRoot()); + }; + + /** + * Function: encodeCell + * + * Recursively encodes the specifed cell. Uses layer + * as the default nodename. If the cell's parent is + * null, then graph is used for the nodename. If + * returns true for the cell, + * then edge is used for the nodename, else if + * returns true for the cell, + * then vertex is used for the nodename. + * + * is used to create the label + * attribute for the cell. For graph nodes and vertices + * the bounds are encoded into x, y, width and height. + * For edges the points are encoded into a points + * attribute as a space-separated list of comma-separated + * coordinate pairs (eg. x0,y0 x1,y1 ... xn,yn). All + * values from the cell style are added as attribute + * values to the node. + */ + codec.encodeCell = function(enc, view, cell) + { + var model = view.graph.getModel(); + var state = view.getState(cell); + var parent = model.getParent(cell); + + if (parent == null || state != null) + { + var childCount = model.getChildCount(cell); + var geo = view.graph.getCellGeometry(cell); + var name = null; + + if (parent == model.getRoot()) + { + name = 'layer'; + } + else if (parent == null) + { + name = 'graph'; + } + else if (model.isEdge(cell)) + { + name = 'edge'; + } + else if (childCount > 0 && geo != null) + { + name = 'group'; + } + else if (model.isVertex(cell)) + { + name = 'vertex'; + } + + if (name != null) + { + var node = enc.document.createElement(name); + var lab = view.graph.getLabel(cell); + + if (lab != null) + { + node.setAttribute('label', view.graph.getLabel(cell)); + + if (view.graph.isHtmlLabel(cell)) + { + node.setAttribute('html', true); + } + } + + if (parent == null) + { + var bounds = view.getGraphBounds(); + + if (bounds != null) + { + node.setAttribute('x', Math.round(bounds.x)); + node.setAttribute('y', Math.round(bounds.y)); + node.setAttribute('width', Math.round(bounds.width)); + node.setAttribute('height', Math.round(bounds.height)); + } + + node.setAttribute('scale', view.scale); + } + else if (state != null && geo != null) + { + // Writes each key, value in the style pair to an attribute + for (var i in state.style) + { + var value = state.style[i]; + + // Tries to turn objects and functions into strings + if (typeof(value) == 'function' && + typeof(value) == 'object') + { + value = mxStyleRegistry.getName(value); + } + + if (value != null && + typeof(value) != 'function' && + typeof(value) != 'object') + { + node.setAttribute(i, value); + } + } + + var abs = state.absolutePoints; + + // Writes the list of points into one attribute + if (abs != null && abs.length > 0) + { + var pts = Math.round(abs[0].x) + ',' + Math.round(abs[0].y); + + for (var i=1; is. This class is created and registered + * dynamically at load time and used implicitly via + * and the . + */ + var codec = new mxObjectCodec(new mxGraphModel()); + + /** + * Function: encodeObject + * + * Encodes the given by writing a (flat) XML sequence of + * cell nodes as produced by the . The sequence is + * wrapped-up in a node with the name root. + */ + codec.encodeObject = function(enc, obj, node) + { + var rootNode = enc.document.createElement('root'); + enc.encodeCell(obj.getRoot(), rootNode); + node.appendChild(rootNode); + }; + + /** + * Function: decodeChild + * + * Overrides decode child to handle special child nodes. + */ + codec.decodeChild = function(dec, child, obj) + { + if (child.nodeName == 'root') + { + this.decodeRoot(dec, child, obj); + } + else + { + mxObjectCodec.prototype.decodeChild.apply(this, arguments); + } + }; + + /** + * Function: decodeRoot + * + * Reads the cells into the graph model. All cells + * are children of the root element in the node. + */ + codec.decodeRoot = function(dec, root, model) + { + var rootCell = null; + var tmp = root.firstChild; + + while (tmp != null) + { + var cell = dec.decodeCell(tmp); + + if (cell != null && cell.getParent() == null) + { + rootCell = cell; + } + + tmp = tmp.nextSibling; + } + + // Sets the root on the model if one has been decoded + if (rootCell != null) + { + model.setRoot(rootCell); + } + }; + + // Returns the codec into the registry + return codec; + +}()); diff --git a/src/main/mxgraph/io/mxObjectCodec.js b/src/main/mxgraph/io/mxObjectCodec.js new file mode 100644 index 000000000..78c150ec6 --- /dev/null +++ b/src/main/mxgraph/io/mxObjectCodec.js @@ -0,0 +1,1099 @@ +/** + * Copyright (c) 2006-2015, JGraph Ltd + * Copyright (c) 2006-2015, Gaudenz Alder + */ +/** + * Class: mxObjectCodec + * + * Generic codec for JavaScript objects that implements a mapping between + * JavaScript objects and XML nodes that maps each field or element to an + * attribute or child node, and vice versa. + * + * Atomic Values: + * + * Consider the following example. + * + * (code) + * var obj = new Object(); + * obj.foo = "Foo"; + * obj.bar = "Bar"; + * (end) + * + * This object is encoded into an XML node using the following. + * + * (code) + * var enc = new mxCodec(); + * var node = enc.encode(obj); + * (end) + * + * The output of the encoding may be viewed using as follows. + * + * (code) + * mxLog.show(); + * mxLog.debug(mxUtils.getPrettyXml(node)); + * (end) + * + * Finally, the result of the encoding looks as follows. + * + * (code) + * + * (end) + * + * In the above output, the foo and bar fields have been mapped to attributes + * with the same names, and the name of the constructor was used for the + * nodename. + * + * Booleans: + * + * Since booleans are numbers in JavaScript, all boolean values are encoded + * into 1 for true and 0 for false. The decoder also accepts the string true + * and false for boolean values. + * + * Objects: + * + * The above scheme is applied to all atomic fields, that is, to all non-object + * fields of an object. For object fields, a child node is created with a + * special attribute that contains the fieldname. This special attribute is + * called "as" and hence, as is a reserved word that should not be used for a + * fieldname. + * + * Consider the following example where foo is an object and bar is an atomic + * property of foo. + * + * (code) + * var obj = {foo: {bar: "Bar"}}; + * (end) + * + * This will be mapped to the following XML structure by mxObjectCodec. + * + * (code) + * + * + * + * (end) + * + * In the above output, the inner Object node contains the as-attribute that + * specifies the fieldname in the enclosing object. That is, the field foo was + * mapped to a child node with an as-attribute that has the value foo. + * + * Arrays: + * + * Arrays are special objects that are either associative, in which case each + * key, value pair is treated like a field where the key is the fieldname, or + * they are a sequence of atomic values and objects, which is mapped to a + * sequence of child nodes. For object elements, the above scheme is applied + * without the use of the special as-attribute for creating each child. For + * atomic elements, a special add-node is created with the value stored in the + * value-attribute. + * + * For example, the following array contains one atomic value and one object + * with a field called bar. Furthermore it contains two associative entries + * called bar with an atomic value, and foo with an object value. + * + * (code) + * var obj = ["Bar", {bar: "Bar"}]; + * obj["bar"] = "Bar"; + * obj["foo"] = {bar: "Bar"}; + * (end) + * + * This array is represented by the following XML nodes. + * + * (code) + * + * + * + * + * + * (end) + * + * The Array node name is the name of the constructor. The additional + * as-attribute in the last child contains the key of the associative entry, + * whereas the second last child is part of the array sequence and does not + * have an as-attribute. + * + * References: + * + * Objects may be represented as child nodes or attributes with ID values, + * which are used to lookup the object in a table within . The + * function is in charge of deciding if a specific field should + * be encoded as a reference or not. Its default implementation returns true if + * the fieldname is in , an array of strings that is used to configure + * the . + * + * Using this approach, the mapping does not guarantee that the referenced + * object itself exists in the document. The fields that are encoded as + * references must be carefully chosen to make sure all referenced objects + * exist in the document, or may be resolved by some other means if necessary. + * + * For example, in the case of the graph model all cells are stored in a tree + * whose root is referenced by the model's root field. A tree is a structure + * that is well suited for an XML representation, however, the additional edges + * in the graph model have a reference to a source and target cell, which are + * also contained in the tree. To handle this case, the source and target cell + * of an edge are treated as references, whereas the children are treated as + * objects. Since all cells are contained in the tree and no edge references a + * source or target outside the tree, this setup makes sure all referenced + * objects are contained in the document. + * + * In the case of a tree structure we must further avoid infinite recursion by + * ignoring the parent reference of each child. This is done by returning true + * in , whose default implementation uses the array of excluded + * fieldnames passed to the mxObjectCodec constructor. + * + * References are only used for cells in mxGraph. For defining other + * referencable object types, the codec must be able to work out the ID of an + * object. This is done by implementing . For decoding a + * reference, the XML node with the respective id-attribute is fetched from the + * document, decoded, and stored in a lookup table for later reference. For + * looking up external objects, may be implemented. + * + * Expressions: + * + * For decoding JavaScript expressions, the add-node may be used with a text + * content that contains the JavaScript expression. For example, the following + * creates a field called foo in the enclosing object and assigns it the value + * of . + * + * (code) + * + * mxConstants.ALIGN_LEFT + * + * (end) + * + * The resulting object has a field called foo with the value "left". Its XML + * representation looks as follows. + * + * (code) + * + * (end) + * + * This means the expression is evaluated at decoding time and the result of + * the evaluation is stored in the respective field. Valid expressions are all + * JavaScript expressions, including function definitions, which are mapped to + * functions on the resulting object. + * + * Expressions are only evaluated if is true. + * + * Constructor: mxObjectCodec + * + * Constructs a new codec for the specified template object. + * The variables in the optional exclude array are ignored by + * the codec. Variables in the optional idrefs array are + * turned into references in the XML. The optional mapping + * may be used to map from variable names to XML attributes. + * The argument is created as follows: + * + * (code) + * var mapping = new Object(); + * mapping['variableName'] = 'attribute-name'; + * (end) + * + * Parameters: + * + * template - Prototypical instance of the object to be + * encoded/decoded. + * exclude - Optional array of fieldnames to be ignored. + * idrefs - Optional array of fieldnames to be converted to/from + * references. + * mapping - Optional mapping from field- to attributenames. + */ +function mxObjectCodec(template, exclude, idrefs, mapping) +{ + this.template = template; + + this.exclude = (exclude != null) ? exclude : []; + this.idrefs = (idrefs != null) ? idrefs : []; + this.mapping = (mapping != null) ? mapping : []; + + this.reverse = new Object(); + + for (var i in this.mapping) + { + this.reverse[this.mapping[i]] = i; + } +}; + +/** + * Variable: allowEval + * + * Static global switch that specifies if expressions in arrays are allowed. + * Default is false. NOTE: Enabling this carries a possible security risk. + */ +mxObjectCodec.allowEval = false; + +/** + * Variable: template + * + * Holds the template object associated with this codec. + */ +mxObjectCodec.prototype.template = null; + +/** + * Variable: exclude + * + * Array containing the variable names that should be + * ignored by the codec. + */ +mxObjectCodec.prototype.exclude = null; + +/** + * Variable: idrefs + * + * Array containing the variable names that should be + * turned into or converted from references. See + * and . + */ +mxObjectCodec.prototype.idrefs = null; + +/** + * Variable: mapping + * + * Maps from from fieldnames to XML attribute names. + */ +mxObjectCodec.prototype.mapping = null; + +/** + * Variable: reverse + * + * Maps from from XML attribute names to fieldnames. + */ +mxObjectCodec.prototype.reverse = null; + +/** + * Function: getName + * + * Returns the name used for the nodenames and lookup of the codec when + * classes are encoded and nodes are decoded. For classes to work with + * this the codec registry automatically adds an alias for the classname + * if that is different than what this returns. The default implementation + * returns the classname of the template class. + */ +mxObjectCodec.prototype.getName = function() +{ + return mxUtils.getFunctionName(this.template.constructor); +}; + +/** + * Function: cloneTemplate + * + * Returns a new instance of the template for this codec. + */ +mxObjectCodec.prototype.cloneTemplate = function() +{ + return new this.template.constructor(); +}; + +/** + * Function: getFieldName + * + * Returns the fieldname for the given attributename. + * Looks up the value in the mapping or returns + * the input if there is no reverse mapping for the + * given name. + */ +mxObjectCodec.prototype.getFieldName = function(attributename) +{ + if (attributename != null) + { + var mapped = this.reverse[attributename]; + + if (mapped != null) + { + attributename = mapped; + } + } + + return attributename; +}; + +/** + * Function: getAttributeName + * + * Returns the attributename for the given fieldname. + * Looks up the value in the or returns + * the input if there is no mapping for the + * given name. + */ +mxObjectCodec.prototype.getAttributeName = function(fieldname) +{ + if (fieldname != null) + { + var mapped = this.mapping[fieldname]; + + if (mapped != null) + { + fieldname = mapped; + } + } + + return fieldname; +}; + +/** + * Function: isExcluded + * + * Returns true if the given attribute is to be ignored by the codec. This + * implementation returns true if the given fieldname is in or + * if the fieldname equals . + * + * Parameters: + * + * obj - Object instance that contains the field. + * attr - Fieldname of the field. + * value - Value of the field. + * write - Boolean indicating if the field is being encoded or decoded. + * Write is true if the field is being encoded, else it is being decoded. + */ +mxObjectCodec.prototype.isExcluded = function(obj, attr, value, write) +{ + return attr == mxObjectIdentity.FIELD_NAME || + mxUtils.indexOf(this.exclude, attr) >= 0; +}; + +/** + * Function: isReference + * + * Returns true if the given fieldname is to be treated + * as a textual reference (ID). This implementation returns + * true if the given fieldname is in . + * + * Parameters: + * + * obj - Object instance that contains the field. + * attr - Fieldname of the field. + * value - Value of the field. + * write - Boolean indicating if the field is being encoded or decoded. + * Write is true if the field is being encoded, else it is being decoded. + */ +mxObjectCodec.prototype.isReference = function(obj, attr, value, write) +{ + return mxUtils.indexOf(this.idrefs, attr) >= 0; +}; + +/** + * Function: encode + * + * Encodes the specified object and returns a node + * representing then given object. Calls + * after creating the node and with the + * resulting node after processing. + * + * Enc is a reference to the calling encoder. It is used + * to encode complex objects and create references. + * + * This implementation encodes all variables of an + * object according to the following rules: + * + * - If the variable name is in then it is ignored. + * - If the variable name is in then + * is used to replace the object with its ID. + * - The variable name is mapped using . + * - If obj is an array and the variable name is numeric + * (ie. an index) then it is not encoded. + * - If the value is an object, then the codec is used to + * create a child node with the variable name encoded into + * the "as" attribute. + * - Else, if is true or the value differs + * from the template value, then ... + * - ... if obj is not an array, then the value is mapped to + * an attribute. + * - ... else if obj is an array, the value is mapped to an + * add child with a value attribute or a text child node, + * if the value is a function. + * + * If no ID exists for a variable in or if an object + * cannot be encoded, a warning is issued using . + * + * Returns the resulting XML node that represents the given + * object. + * + * Parameters: + * + * enc - that controls the encoding process. + * obj - Object to be encoded. + */ +mxObjectCodec.prototype.encode = function(enc, obj) +{ + var node = enc.document.createElement(this.getName()); + + obj = this.beforeEncode(enc, obj, node); + this.encodeObject(enc, obj, node); + + return this.afterEncode(enc, obj, node); +}; + +/** + * Function: encodeObject + * + * Encodes the value of each member in then given obj into the given node using + * . + * + * Parameters: + * + * enc - that controls the encoding process. + * obj - Object to be encoded. + * node - XML node that contains the encoded object. + */ +mxObjectCodec.prototype.encodeObject = function(enc, obj, node) +{ + enc.setAttribute(node, 'id', enc.getId(obj)); + + for (var i in obj) + { + var name = i; + var value = obj[name]; + + if (value != null && !this.isExcluded(obj, name, value, true)) + { + if (mxUtils.isInteger(name)) + { + name = null; + } + + this.encodeValue(enc, obj, name, value, node); + } + } +}; + +/** + * Function: encodeValue + * + * Converts the given value according to the mappings + * and id-refs in this codec and uses + * to write the attribute into the given node. + * + * Parameters: + * + * enc - that controls the encoding process. + * obj - Object whose property is going to be encoded. + * name - XML node that contains the encoded object. + * value - Value of the property to be encoded. + * node - XML node that contains the encoded object. + */ +mxObjectCodec.prototype.encodeValue = function(enc, obj, name, value, node) +{ + if (value != null) + { + if (this.isReference(obj, name, value, true)) + { + var tmp = enc.getId(value); + + if (tmp == null) + { + mxLog.warn('mxObjectCodec.encode: No ID for ' + + this.getName() + '.' + name + '=' + value); + return; // exit + } + + value = tmp; + } + + var defaultValue = this.template[name]; + + // Checks if the value is a default value and + // the name is correct + if (name == null || enc.encodeDefaults || defaultValue != value) + { + name = this.getAttributeName(name); + this.writeAttribute(enc, obj, name, value, node); + } + } +}; + +/** + * Function: writeAttribute + * + * Writes the given value into node using + * or depending on the type of the value. + */ +mxObjectCodec.prototype.writeAttribute = function(enc, obj, name, value, node) +{ + if (typeof(value) != 'object' /* primitive type */) + { + this.writePrimitiveAttribute(enc, obj, name, value, node); + } + else /* complex type */ + { + this.writeComplexAttribute(enc, obj, name, value, node); + } +}; + +/** + * Function: writePrimitiveAttribute + * + * Writes the given value as an attribute of the given node. + */ +mxObjectCodec.prototype.writePrimitiveAttribute = function(enc, obj, name, value, node) +{ + value = this.convertAttributeToXml(enc, obj, name, value, node); + + if (name == null) + { + var child = enc.document.createElement('add'); + + if (typeof(value) == 'function') + { + child.appendChild(enc.document.createTextNode(value)); + } + else + { + enc.setAttribute(child, 'value', value); + } + + node.appendChild(child); + } + else if (typeof(value) != 'function') + { + enc.setAttribute(node, name, value); + } +}; + +/** + * Function: writeComplexAttribute + * + * Writes the given value as a child node of the given node. + */ +mxObjectCodec.prototype.writeComplexAttribute = function(enc, obj, name, value, node) +{ + var child = enc.encode(value); + + if (child != null) + { + if (name != null) + { + child.setAttribute('as', name); + } + + node.appendChild(child); + } + else + { + mxLog.warn('mxObjectCodec.encode: No node for ' + this.getName() + '.' + name + ': ' + value); + } +}; + +/** + * Function: convertAttributeToXml + * + * Converts true to "1" and false to "0" is returns true. + * All other values are not converted. + * + * Parameters: + * + * enc - that controls the encoding process. + * obj - Objec to convert the attribute for. + * name - Name of the attribute to be converted. + * value - Value to be converted. + */ +mxObjectCodec.prototype.convertAttributeToXml = function(enc, obj, name, value) +{ + // Makes sure to encode boolean values as numeric values + if (this.isBooleanAttribute(enc, obj, name, value)) + { + // Checks if the value is true (do not use the value as is, because + // this would check if the value is not null, so 0 would be true) + value = (value == true) ? '1' : '0'; + } + + return value; +}; + +/** + * Function: isBooleanAttribute + * + * Returns true if the given object attribute is a boolean value. + * + * Parameters: + * + * enc - that controls the encoding process. + * obj - Objec to convert the attribute for. + * name - Name of the attribute to be converted. + * value - Value of the attribute to be converted. + */ +mxObjectCodec.prototype.isBooleanAttribute = function(enc, obj, name, value) +{ + return (typeof(value.length) == 'undefined' && (value == true || value == false)); +}; + +/** + * Function: convertAttributeFromXml + * + * Converts booleans and numeric values to the respective types. Values are + * numeric if returns true. + * + * Parameters: + * + * dec - that controls the decoding process. + * attr - XML attribute to be converted. + * obj - Objec to convert the attribute for. + */ +mxObjectCodec.prototype.convertAttributeFromXml = function(dec, attr, obj) +{ + var value = attr.value; + + if (this.isNumericAttribute(dec, attr, obj)) + { + value = parseFloat(value); + + if (isNaN(value) || !isFinite(value)) + { + value = 0; + } + } + + return value; +}; + +/** + * Function: isNumericAttribute + * + * Returns true if the given XML attribute is or should be a numeric value. + * + * Parameters: + * + * dec - that controls the decoding process. + * attr - XML attribute to be converted. + * obj - Objec to convert the attribute for. + */ +mxObjectCodec.prototype.isNumericAttribute = function(dec, attr, obj) +{ + // Handles known numeric attributes for generic objects + var result = (obj.constructor == mxGeometry && + (attr.name == 'x' || attr.name == 'y' || + attr.name == 'width' || attr.name == 'height')) || + (obj.constructor == mxPoint && + (attr.name == 'x' || attr.name == 'y')) || + mxUtils.isNumeric(attr.value); + + return result; +}; + +/** + * Function: beforeEncode + * + * Hook for subclassers to pre-process the object before + * encoding. This returns the input object. The return + * value of this function is used in to perform + * the default encoding into the given node. + * + * Parameters: + * + * enc - that controls the encoding process. + * obj - Object to be encoded. + * node - XML node to encode the object into. + */ +mxObjectCodec.prototype.beforeEncode = function(enc, obj, node) +{ + return obj; +}; + +/** + * Function: afterEncode + * + * Hook for subclassers to post-process the node + * for the given object after encoding and return the + * post-processed node. This implementation returns + * the input node. The return value of this method + * is returned to the encoder from . + * + * Parameters: + * + * enc - that controls the encoding process. + * obj - Object to be encoded. + * node - XML node that represents the default encoding. + */ +mxObjectCodec.prototype.afterEncode = function(enc, obj, node) +{ + return node; +}; + +/** + * Function: decode + * + * Parses the given node into the object or returns a new object + * representing the given node. + * + * Dec is a reference to the calling decoder. It is used to decode + * complex objects and resolve references. + * + * If a node has an id attribute then the object cache is checked for the + * object. If the object is not yet in the cache then it is constructed + * using the constructor of