From f848dc3d417768687ac3bf921ea0f37eeffc3674 Mon Sep 17 00:00:00 2001 From: sbosse Date: Mon, 21 Jul 2025 23:41:08 +0200 Subject: [PATCH] Mon 21 Jul 22:43:21 CEST 2025 --- .../src/js/handler/mxConstraintHandler.js | 486 ++++++++++++++++++ 1 file changed, 486 insertions(+) create mode 100644 js/ui/mxgraph/src/js/handler/mxConstraintHandler.js diff --git a/js/ui/mxgraph/src/js/handler/mxConstraintHandler.js b/js/ui/mxgraph/src/js/handler/mxConstraintHandler.js new file mode 100644 index 0000000..e5d474f --- /dev/null +++ b/js/ui/mxgraph/src/js/handler/mxConstraintHandler.js @@ -0,0 +1,486 @@ +/** + * 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(); + } + }); + + 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 + * + * 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 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); + + 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: 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; + } +};