| /** |
| * 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 <mxGraph>. |
| * factoryMethod - Optional function to create the edge. The function takes |
| * the source and target <mxCell> as the first and second argument and |
| * returns the <mxCell> that represents the new edge. |
| */ |
| function mxConstraintHandler(graph) |
| { |
| this.graph = graph; |
| |
| // 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, this.resetHandler); |
| this.graph.addListener(mxEvent.ROOT, this.resetHandler); |
| }; |
| |
| /** |
| * Variable: pointImage |
| * |
| * <mxImage> to be used as the image for fixed connection points. |
| */ |
| mxConstraintHandler.prototype.pointImage = new mxImage(mxClient.imageBasePath + '/point.gif', 5, 5); |
| |
| /** |
| * Variable: graph |
| * |
| * Reference to the enclosing <mxGraph>. |
| */ |
| mxConstraintHandler.prototype.graph = null; |
| |
| /** |
| * Variable: enabled |
| * |
| * Specifies if events are handled. Default is true. |
| */ |
| mxConstraintHandler.prototype.enabled = true; |
| |
| /** |
| * Variable: highlightColor |
| * |
| * Specifies the color for the highlight. Default is <mxConstants.DEFAULT_VALID_COLOR>. |
| */ |
| mxConstraintHandler.prototype.highlightColor = mxConstants.DEFAULT_VALID_COLOR; |
| |
| /** |
| * Function: isEnabled |
| * |
| * Returns true if events are handled. This implementation |
| * returns <enabled>. |
| */ |
| mxConstraintHandler.prototype.isEnabled = function() |
| { |
| return this.enabled; |
| }; |
| |
| /** |
| * Function: setEnabled |
| * |
| * Enables or disables event handling. This implementation |
| * updates <enabled>. |
| * |
| * Parameters: |
| * |
| * enabled - Boolean that specifies the new enabled state. |
| */ |
| mxConstraintHandler.prototype.setEnabled = function(enabled) |
| { |
| this.enabled = enabled; |
| }; |
| |
| /** |
| * Function: reset |
| * |
| * Resets the state of this handler. |
| */ |
| mxConstraintHandler.prototype.reset = function() |
| { |
| if (this.focusIcons != null) |
| { |
| for (var i = 0; i < this.focusIcons.length; i++) |
| { |
| this.focusIcons[i].destroy(); |
| } |
| |
| this.focusIcons = null; |
| } |
| |
| if (this.focusHighlight != null) |
| { |
| this.focusHighlight.destroy(); |
| this.focusHighlight = null; |
| } |
| |
| this.currentConstraint = null; |
| this.currentFocusArea = null; |
| this.currentPoint = null; |
| this.currentFocus = null; |
| this.focusPoints = null; |
| }; |
| |
| /** |
| * Function: getTolerance |
| * |
| * Returns the tolerance to be used for intersecting connection points. This |
| * implementation returns <mxGraph.tolerance>. |
| * |
| * Parameters: |
| * |
| * me - <mxMouseEvent> 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 <mxMouseEvent> should be ignored in <update>. 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 <focusIcons> 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 <focusHighlight> 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 and alt are pressed. |
| */ |
| mxConstraintHandler.prototype.isKeepFocusEvent = function(me) |
| { |
| return mxEvent.isShiftDown(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 cell; |
| }; |
| |
| /** |
| * Function: update |
| * |
| * Updates the state of this handler based on the given <mxMouseEvent>. |
| * Source is a boolean indicating if the cell is a source or target. |
| */ |
| mxConstraintHandler.prototype.update = function(me, source, 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); |
| |
| if (mxClient.IS_IE) |
| { |
| tmp.grow(1); |
| tmp.width -= 1; |
| tmp.height -= 1; |
| } |
| |
| if (this.focusHighlight == null) |
| { |
| var hl = this.createHighlightShape(); |
| hl.dialect = (this.graph.dialect == mxConstants.DIALECT_SVG) ? |
| mxConstants.DIALECT_SVG : mxConstants.DIALECT_VML; |
| 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()); |
| |
| // Fixes lost event tracking for images in quirks / IE8 standards |
| if (mxClient.IS_QUIRKS || document.documentMode == 8) |
| { |
| mxEvent.addListener(icon.node, 'dragstart', function(evt) |
| { |
| mxEvent.consume(evt); |
| |
| return false; |
| }); |
| } |
| |
| // 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; |
| } |
| }; |