/** * 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, eg. * 500. */ 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)) { 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()) && !mxEvent.isAltDown(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.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) { if (!me.isConsumed() && this.isEnabled() && this.graph.isEnabled() && me.getState() != null && !mxEvent.isMultiTouchEvent(me.getEvent())) { var cell = this.getInitialCellForEvent(me); this.delayedSelection = this.isDelayedSelection(cell, me); this.cell = null; if (this.isSelectEnabled() && !this.delayedSelection) { this.graph.selectCellForEvent(cell, me.getEvent()); } if (this.isMoveEnabled()) { var model = this.graph.model; var geo = model.getGeometry(cell); if (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.graph.isCloneEvent(me.getEvent()) && this.graph.isCellsCloneable()))) { this.start(cell, me.getX(), me.getY()); } else if (this.delayedSelection) { this.cell = cell; } 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 VML or 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 = (this.graph.dialect != mxConstants.DIALECT_SVG) ? mxConstants.DIALECT_VML : 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) { 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.graph.isConstrainedEvent(me.getEvent()) : 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; if (!me.isConsumed() && graph.isMouseDown && this.cell != null && this.first != null && this.bounds != null && !this.suspended) { // Stops moving if a multi touch event is received if (mxEvent.isMultiTouchEvent(me.getEvent())) { this.reset(); return; } var delta = this.getDelta(me); var tol = graph.tolerance; if (this.shape != null || this.livePreviewActive || 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(); 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 (graph.isConstrainedEvent(me.getEvent())) { 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: 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)) { 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)) { 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.shape != null || this.livePreviewUsed) && this.currentDx != null && this.currentDy != null) { 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 clone = graph.isCloneEvent(me.getEvent()) && graph.isCellsCloneable() && this.isCloneEnabled(); 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, clone, this.target, me.getEvent()); } } } else if (this.isSelectEnabled() && this.delayedSelection && 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.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); if (target == 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(); };