/** * 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 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 (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 = (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; } };