From b4c1e7050e77117d8d9a89ada955c725d82a7178 Mon Sep 17 00:00:00 2001 From: sbosse Date: Mon, 21 Jul 2025 23:41:04 +0200 Subject: [PATCH] Mon 21 Jul 22:43:21 CEST 2025 --- .../mxgraph/src/js/handler/mxVertexHandler.js | 1938 +++++++++++++++++ 1 file changed, 1938 insertions(+) create mode 100644 js/ui/mxgraph/src/js/handler/mxVertexHandler.js diff --git a/js/ui/mxgraph/src/js/handler/mxVertexHandler.js b/js/ui/mxgraph/src/js/handler/mxVertexHandler.js new file mode 100644 index 0000000..630ee5c --- /dev/null +++ b/js/ui/mxgraph/src/js/handler/mxVertexHandler.js @@ -0,0 +1,1938 @@ +/** + * 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: 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: 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); + // VML dialect required here for event transparency in IE + this.selectionBorder.dialect = (this.graph.dialect != mxConstants.DIALECT_SVG) ? mxConstants.DIALECT_VML : mxConstants.DIALECT_SVG; + 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.selectionBorder.setCursor(mxConstants.CURSOR_MOVABLE_VERTEX); + } + + // Adds the sizer handles + if (mxGraphHandler.prototype.maxCells <= 0 || this.graph.getSelectionCount() < mxGraphHandler.prototype.maxCells) + { + var resizable = this.graph.isCellResizable(this.state.cell); + this.sizers = []; + + if (resizable || (this.graph.isLabelMovable(this.state.cell) && + this.state.width >= 2 && this.state.height >= 2)) + { + var i = 0; + + if (resizable) + { + if (!this.singleSizer) + { + this.sizers.push(this.createSizer('nw-resize', i++)); + this.sizers.push(this.createSizer('n-resize', i++)); + this.sizers.push(this.createSizer('ne-resize', i++)); + this.sizers.push(this.createSizer('w-resize', i++)); + this.sizers.push(this.createSizer('e-resize', i++)); + this.sizers.push(this.createSizer('sw-resize', i++)); + this.sizers.push(this.createSizer('s-resize', i++)); + } + + this.sizers.push(this.createSizer('se-resize', i++)); + } + + var geo = this.graph.model.getGeometry(this.state.cell); + + if (geo != null && !geo.relative && !this.graph.isSwimlane(this.state.cell) && + this.graph.isLabelMovable(this.state.cell)) + { + // Marks this as the label handle for getHandleForEvent + this.labelShape = this.createSizer(mxConstants.CURSOR_LABEL_HANDLE, mxEvent.LABEL_HANDLE, mxConstants.LABEL_HANDLE_SIZE, mxConstants.LABEL_HANDLE_FILLCOLOR); + this.sizers.push(this.labelShape); + } + } + else if (this.graph.isCellMovable(this.state.cell) && !this.graph.isCellResizable(this.state.cell) && + this.state.width < 2 && this.state.height < 2) + { + this.labelShape = this.createSizer(mxConstants.CURSOR_MOVABLE_VERTEX, + mxEvent.LABEL_HANDLE, null, mxConstants.LABEL_HANDLE_FILLCOLOR); + this.sizers.push(this.labelShape); + } + } + + // Adds the rotation handler + if (this.isRotationHandleVisible()) + { + this.rotationShape = this.createSizer(this.rotationCursor, mxEvent.ROTATION_HANDLE, + mxConstants.HANDLE_SIZE + 3, mxConstants.HANDLE_FILLCOLOR); + this.sizers.push(this.rotationShape); + } + + this.customHandles = this.createCustomHandles(); + this.redraw(); + + if (this.constrainGroupByChildren) + { + this.updateMinBounds(); + } +}; + +/** + * 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) && + (mxGraphHandler.prototype.maxCells <= 0 || this.graph.getSelectionCount() < mxGraphHandler.prototype.maxCells) && + this.state.width >= 2 && this.state.height >= 2; +}; + +/** + * 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 aspect ratio if the cell should be maintained. + */ +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(bounds, null, this.getSelectionColor()); + shape.strokewidth = this.getSelectionStrokeWidth(); + shape.isDashed = this.isSelectionDashed(); + + return shape; +}; + +/** + * Function: getSelectionColor + * + * Returns . + */ +mxVertexHandler.prototype.getSelectionColor = function() +{ + return mxConstants.VERTEX_SELECTION_COLOR; +}; + +/** + * 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); + + 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) +{ + if (this.handleImage != null) + { + bounds = new mxRectangle(bounds.x, bounds.y, this.handleImage.width, this.handleImage.height); + var shape = new mxImageShape(bounds, this.handleImage.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 VML + 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; + + function checkShape(shape) + { + return shape != null && (me.isSource(shape) || (hit != null && mxUtils.intersects(shape.bounds, hit) && + shape.node.style.display != 'none' && shape.node.style.visibility != 'hidden')); + } + + 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 (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; + } + } + } + + 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) +{ + var tol = (!mxEvent.isMouseEvent(me.getEvent())) ? this.tolerance : 0; + + if (!me.isConsumed() && this.graph.isEnabled() && (tol > 0 || me.getState() == this.state)) + { + 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) +{ + this.inTolerance = true; + this.childOffsetX = 0; + this.childOffsetY = 0; + this.index = index; + this.startX = x; + this.startY = y; + + // 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.livePreview || 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 = (this.graph.dialect != mxConstants.DIALECT_SVG) ? + mxConstants.DIALECT_VML : mxConstants.DIALECT_SVG; + this.preview.init(this.graph.view.getOverlayPane()); + } + } + + // Prepares the handles for live preview + if (this.livePreview) + { + 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].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: hideHandles + * + * Shortcut to . + */ +mxVertexHandler.prototype.setHandlesVisible = function(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++) + { + 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); +}; + +/** + * 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].processEvent(me); + } + } + 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: rotateVertex + * + * Rotates the vertex. + */ +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; + } + + // 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.abs(Math.sqrt(dx * dx + dy * dy) - 20) * 3; + var raster = Math.max(1, 5 * Math.min(3, Math.max(0, Math.round(80 / Math.abs(dist))))); + + 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.livePreview) + { + this.redrawHandles(); + } +}; + +/** + * Function: rotateVertex + * + * Rotates 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; + } + } + } + + 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 (this.livePreview) + { + this.updateLivePreview(me); + } + + if (this.preview != null) + { + this.drawPreview(); + } +}; + +/** + * 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; + + // Needed to force update of text bounds + this.state.unscaledWidth = null; + + // 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(); + + // Restores current state + this.state.setState(tempState); +}; + +/** + * 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()); + + this.graph.getModel().beginUpdate(); + try + { + if (this.index <= mxEvent.CUSTOM_HANDLE) + { + if (this.customHandles != null) + { + this.customHandles[mxEvent.CUSTOM_HANDLE - this.index].execute(); + } + } + else if (this.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), + this.index, gridEnabled, this.isConstrainedEvent(me), recurse); + } + } + finally + { + this.graph.getModel().endUpdate(); + } + + me.consume(); + this.reset(); + } +}; + +/** + * Function: rotateCell + * + * Rotates the given cell to the given rotation. + */ +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 state = this.graph.view.getState(cell); + var style = (state != null) ? state.style : this.graph.getCellStyle(cell); + + if (style != null) + { + 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.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.customHandles != null) + { + for (var i = 0; i < this.customHandles.length; i++) + { + this.customHandles[i].reset(); + } + } + + // 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.unscaledBounds = 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 scale = this.graph.view.scale; + dx = Math.round((this.labelShape.bounds.getCenterX() - this.startX) / scale); + dy = Math.round((this.labelShape.bounds.getCenterY() - this.startY) / scale); + + geo = geo.clone(); + + if (geo.offset == null) + { + geo.offset = new mxPoint(dx, dy); + } + else + { + geo.offset.x += dx; + geo.offset.y += dy; + } + + this.graph.model.setGeometry(cell, geo); + } + else 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) +{ + 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 if (index < 3 /* Top Row */) + { + top = top + dy; + + if (gridEnabled) + { + top = this.graph.snap(top / scale) * scale; + } + } + + if (index == 0 || index == 3 || index == 5 /* Left */) + { + left += dx; + + if (gridEnabled) + { + left = this.graph.snap(left / scale) * scale; + } + } + else if (index == 2 || index == 4 || index == 7 /* Right */) + { + right += dx; + + if (gridEnabled) + { + right = this.graph.snap(right / scale) * scale; + } + } + + var width = right - left; + var height = bottom - top; + + 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() +{ + this.selectionBounds = this.getSelectionBounds(this.state); + this.bounds = new mxRectangle(this.selectionBounds.x, this.selectionBounds.y, this.selectionBounds.width, this.selectionBounds.height); + + this.redrawHandles(); + this.drawPreview(); +}; + +/** + * 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: 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 tol = this.tolerance; + this.horizontalOffset = 0; + this.verticalOffset = 0; + var s = this.bounds; + + 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 + { + 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)]); + + this.moveSizerTo(this.sizers[8], cx + this.state.absoluteOffset.x, cy + this.state.absoluteOffset.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.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(new mxPoint(s.x + s.width / 2, s.y + this.rotationHandleVSpacing), cos, sin, ct); + + if (this.rotationShape.node != null) + { + this.moveSizerTo(this.rotationShape, pt.x, pt.y); + } + } + + 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(); + } + } + + if (this.customHandles != null) + { + for (var i = 0; i < this.customHandles.length; i++) + { + this.customHandles[i].redraw(); + } + } + + this.updateParentHighlight(); +}; + +/** + * Function: updateParentHighlight + * + * Updates the highlight of the parent if is true. + */ +mxVertexHandler.prototype.updateParentHighlight = function() +{ + // If not destroyed + if (this.selectionBorder != null) + { + if (this.parentHighlight != null) + { + var parent = this.graph.model.getParent(this.state.cell); + + if (this.graph.model.isVertex(parent)) + { + var pstate = this.graph.view.getState(parent); + 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 = pstate; + this.parentHighlight.redraw(); + } + } + else + { + this.parentHighlight.destroy(); + this.parentHighlight = null; + } + } + else if (this.parentHighlightEnabled) + { + var parent = this.graph.model.getParent(this.state.cell); + + if (this.graph.model.isVertex(parent)) + { + var pstate = this.graph.view.getState(parent); + + if (pstate != null) + { + this.parentHighlight = this.createParentHighlightShape(pstate); + // VML dialect required here for event transparency in IE + this.parentHighlight.dialect = (this.graph.dialect != mxConstants.DIALECT_SVG) ? mxConstants.DIALECT_VML : mxConstants.DIALECT_SVG; + this.parentHighlight.pointerEvents = false; + this.parentHighlight.rotation = Number(pstate.style[mxConstants.STYLE_ROTATION] || '0'); + this.parentHighlight.init(this.graph.getView().getOverlayPane()); + } + } + } + } +}; + +/** + * 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.bounds; + this.selectionBorder.redraw(); + + if (this.parentHighlight != null) + { + this.parentHighlight.redraw(); + } +}; + +/** + * 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.parentHighlight != null) + { + this.parentHighlight.destroy(); + this.parentHighlight = null; + } + + if (this.selectionBorder != null) + { + this.selectionBorder.destroy(); + this.selectionBorder = null; + } + + this.labelShape = null; + this.removeHint(); + + if (this.sizers != null) + { + for (var i = 0; i < this.sizers.length; i++) + { + this.sizers[i].destroy(); + } + + this.sizers = null; + } + + if (this.customHandles != null) + { + for (var i = 0; i < this.customHandles.length; i++) + { + this.customHandles[i].destroy(); + } + + this.customHandles = null; + } +};