UNPKG

@maxgraph/core

Version:

maxGraph is a fully client side JavaScript diagramming library that uses SVG and HTML for rendering.

1,246 lines (1,243 loc) 52.9 kB
"use strict"; /* Copyright 2021-present The maxGraph project Contributors Copyright (c) 2006-2015, JGraph Ltd Copyright (c) 2006-2015, Gaudenz Alder Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const Client_js_1 = __importDefault(require("../../Client.js")); const InternalEvent_js_1 = __importDefault(require("../event/InternalEvent.js")); const mathUtils_js_1 = require("../../util/mathUtils.js"); const styleUtils_js_1 = require("../../util/styleUtils.js"); const RectangleShape_js_1 = __importDefault(require("../shape/node/RectangleShape.js")); const Guide_js_1 = __importDefault(require("../other/Guide.js")); const Point_js_1 = __importDefault(require("../geometry/Point.js")); const Constants_js_1 = require("../../util/Constants.js"); const CellHighlight_js_1 = __importDefault(require("../cell/CellHighlight.js")); const Rectangle_js_1 = __importDefault(require("../geometry/Rectangle.js")); const EventUtils_js_1 = require("../../util/EventUtils.js"); const config_js_1 = require("../handler/config.js"); /** * Graph event handler that handles selection. * * Individual cells are handled separately by {@link SelectionCellsHandler} using {@link VertexHandler} or one of the {@link EdgeHandler}s. * When the {@link SelectionCellsHandler} plugin is registered in the {@link AbstractGraph}, {@link SelectionHandler} interacts with this plugin to propagate global selection events to individual cells. * * To avoid the container to scroll a moved cell into view, set {@link scrollOnMove} to `false`. * * @category Plugin */ class SelectionHandler { /** * Constructs an event handler that creates handles for the selection cells. * * @param graph Reference to the enclosing {@link AbstractGraph}. */ constructor(graph) { this.refreshThread = null; /** * 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. */ this.maxCells = 50; /** * Specifies if events are handled. Default is true. */ this.enabled = true; /** * Specifies if drop targets under the mouse should be enabled. Default is * true. */ this.highlightEnabled = true; /** * Specifies if cloning by control-drag is enabled. Default is true. */ this.cloneEnabled = true; /** * Specifies if moving is enabled. Default is true. */ this.moveEnabled = true; /** * Specifies if other cells should be used for snapping the right, center or * left side of the current selection. Default is false. */ this.guidesEnabled = false; /** * Whether the handles of the selection are currently visible. */ this.handlesVisible = true; /** * Holds the {@link Guide} instance that is used for alignment. */ this.guide = null; /** * Stores the x-coordinate of the current mouse move. */ this.currentDx = 0; /** * Stores the y-coordinate of the current mouse move. */ this.currentDy = 0; /** * Specifies if a move cursor should be shown if the mouse is over a movable * cell. Default is true. */ this.updateCursor = true; /** * Specifies if selecting is enabled. Default is true. */ this.selectEnabled = true; /** * Specifies if cells may be moved out of their parents. Default is true. */ this.removeCellsFromParent = true; /** * If empty parents should be removed from the model after all child cells * have been moved out. Default is true. */ this.removeEmptyParents = false; /** * Specifies if drop events are interpreted as new connections if no other * drop action is defined. Default is false. */ this.connectOnDrop = false; /** * Specifies if the view should be scrolled so that a moved cell is visible. * @default true */ this.scrollOnMove = true; /** * Specifies the minimum number of pixels for the width and height of a * selection border. Default is 6. */ this.minimumSize = 6; /** * Specifies the color of the preview shape. Default is black. */ this.previewColor = 'black'; /** * Specifies if the graph container should be used for preview. If this is used * then drop target detection relies entirely on {@link AbstractGraph.getCellAt} because * the HTML preview does not "let events through". Default is false. */ this.htmlPreview = false; /** * Reference to the {@link Shape} that represents the preview. */ this.shape = null; /** * Specifies if the grid should be scaled. Default is false. */ this.scaleGrid = false; /** * Specifies if the bounding box should allow for rotation. Default is true. */ this.rotationEnabled = true; /** * Maximum number of cells for which live preview should be used. Default is 0 which means no live preview. */ this.maxLivePreview = 0; /** * Variable allowLivePreview * * If live preview is allowed on this system. Default is true for systems with SVG support. */ this.allowLivePreview = Client_js_1.default.IS_SVG; this.cell = null; this.delayedSelection = false; this.first = null; this.cells = null; this.bounds = null; this.pBounds = null; this.allCells = new Map(); this.cellWasClicked = false; this.cloning = false; this.cellCount = 0; this.target = null; this.suspended = false; this.livePreviewActive = false; this.livePreviewUsed = false; this.highlight = null; this.graph = graph; this.graph.addMouseListener(this); // Repaints the handler after autoscroll this.panHandler = () => { if (!this.suspended) { this.updatePreview(); this.updateHint(); } }; this.graph.addListener(InternalEvent_js_1.default.PAN, this.panHandler); // Handles escape keystrokes this.escapeHandler = (sender, evt) => { this.reset(); }; this.graph.addListener(InternalEvent_js_1.default.ESCAPE, this.escapeHandler); // Updates the preview box for remote changes this.refreshHandler = (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(() => { this.refreshThread = null; if (this.first && !this.suspended && this.cells) { // Updates preview with no translate to compute bounding box const dx = this.currentDx; const 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.getSelectionCellsHandler()?.getHandledSelectionCells() ?? [], false, true); this.updatePreview(); } } } }, 0); }; this.graph.getDataModel().addListener(InternalEvent_js_1.default.CHANGE, this.refreshHandler); this.graph.addListener(InternalEvent_js_1.default.REFRESH, this.refreshHandler); this.keyHandler = (e) => { if (this.graph.container != null && this.graph.container.style.visibility !== 'hidden' && this.first != null && !this.suspended) { const clone = this.graph.isCloneEvent(e) && this.graph.isCellsCloneable() && this.isCloneEnabled(); if (clone !== this.cloning) { this.cloning = clone; this.checkPreview(); this.updatePreview(); } } }; if (typeof document !== 'undefined') { InternalEvent_js_1.default.addListener(document, 'keydown', this.keyHandler); InternalEvent_js_1.default.addListener(document, 'keyup', this.keyHandler); } } /** * Returns <enabled>. */ isEnabled() { return this.enabled; } /** * Sets <enabled>. */ setEnabled(value) { this.enabled = value; } /** * Returns <cloneEnabled>. */ isCloneEnabled() { return this.cloneEnabled; } /** * Sets <cloneEnabled>. * * @param value Boolean that specifies the new clone enabled state. */ setCloneEnabled(value) { this.cloneEnabled = value; } /** * Returns {@link oveEnabled}. */ isMoveEnabled() { return this.moveEnabled; } /** * Sets {@link oveEnabled}. */ setMoveEnabled(value) { this.moveEnabled = value; } /** * Returns <selectEnabled>. */ isSelectEnabled() { return this.selectEnabled; } /** * Sets <selectEnabled>. */ setSelectEnabled(value) { this.selectEnabled = value; } /** * Returns <removeCellsFromParent>. */ isRemoveCellsFromParent() { return this.removeCellsFromParent; } /** * Sets <removeCellsFromParent>. */ setRemoveCellsFromParent(value) { this.removeCellsFromParent = value; } /** * Returns true if the given cell and parent should propagate * selection state to the parent. */ isPropagateSelectionCell(cell, immediate, me) { const parent = cell.getParent(); if (immediate) { const geo = cell.isEdge() ? null : cell.getGeometry(); return ((!this.graph.isSiblingSelected(cell) && geo && geo.relative) || !this.graph.isSwimlane(parent)); } 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))); } /** * Hook to return initial cell for the given event. */ getInitialCellForEvent(me) { let state = me.getState(); if ((!this.graph.isToggleEvent(me.getEvent()) || !(0, EventUtils_js_1.isAltDown)(me.getEvent())) && state && !this.graph.isCellSelected(state.cell)) { let parent = state.cell.getParent(); let next = parent ? this.graph.view.getState(parent) : null; while (next && !this.graph.isCellSelected(next.cell) && (next.cell.isVertex() || next.cell.isEdge()) && this.isPropagateSelectionCell(state.cell, true, me)) { state = next; parent = state.cell.getParent(); next = parent ? this.graph.view.getState(parent) : null; } } return state ? state.cell : null; } /** * Hook to return true for delayed selections. */ isDelayedSelection(cell, me) { let c = cell; const selectionCellsHandler = this.getSelectionCellsHandler(); if (!this.graph.isToggleEvent(me.getEvent()) || !(0, EventUtils_js_1.isAltDown)(me.getEvent())) { while (c) { if (selectionCellsHandler?.isHandled(c)) { const cellEditorHandler = this.graph.getPlugin('CellEditorHandler'); return cellEditorHandler?.getEditingCell() !== c; } c = c.getParent(); } } return this.graph.isToggleEvent(me.getEvent()) && !(0, EventUtils_js_1.isAltDown)(me.getEvent()); } /** * Implements the delayed selection for the given mouse event. */ selectDelayed(me) { const popupMenuHandler = this.graph.getPlugin('PopupMenuHandler'); if (!popupMenuHandler || !popupMenuHandler.isPopupTrigger(me)) { let cell = me.getCell(); if (cell === null) { cell = this.cell; } if (cell) this.selectCellForEvent(cell, me); } } /** * Selects the given cell for the given {@link MouseEvent}. */ selectCellForEvent(cell, me) { const state = this.graph.view.getState(cell); if (state) { if (me.isSource(state.control)) { this.graph.selectCellForEvent(cell, me.getEvent()); } else { if (!this.graph.isToggleEvent(me.getEvent()) || !(0, EventUtils_js_1.isAltDown)(me.getEvent())) { let parent = cell.getParent(); while (parent && this.graph.view.getState(parent) && (parent.isVertex() || parent.isEdge()) && this.isPropagateSelectionCell(cell, false, me)) { cell = parent; parent = cell.getParent(); } } this.graph.selectCellForEvent(cell, me.getEvent()); } } return cell; } /** * 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. * * ```js * consumeMouseEvent(evtName, me) { * const source = eventUtils.getSource(me.getEvent()); * * if (!eventUtils.isTouchEvent(me.getEvent()) || source.nodeName != 'A') { * me.consume(); * } * } * ``` */ consumeMouseEvent(_evtName, me) { me.consume(); } /** * Handles the event by selecting the given cell and creating a handle for it. * By consuming the event all subsequent events of the gesture are redirected to this handler. */ mouseDown(_sender, me) { if (!me.isConsumed() && this.isEnabled() && this.graph.isEnabled() && me.getState() && !(0, EventUtils_js_1.isMultiTouchEvent)(me.getEvent())) { const cell = this.getInitialCellForEvent(me); if (cell) { this.delayedSelection = this.isDelayedSelection(cell, me); this.cell = null; if (this.isSelectEnabled() && !this.delayedSelection) { this.graph.selectCellForEvent(cell, me.getEvent()); } if (this.isMoveEnabled()) { const geo = cell.getGeometry(); if (geo && this.graph.isCellMovable(cell) && (!cell.isEdge() || this.graph.getSelectionCount() > 1 || (geo.points && geo.points.length > 0) || !cell.getTerminal(true) || !cell.getTerminal(false) || this.graph.isAllowDanglingEdges() || (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(InternalEvent_js_1.default.MOUSE_DOWN, me); } } } } /** * Creates an array of cell states which should be used as guides. */ getGuideStates() { const parent = this.graph.getDefaultParent(); const filter = (cell) => { const geo = cell.getGeometry(); return (!!this.graph.view.getState(cell) && cell.isVertex() && !!geo && !geo.relative); }; return this.graph.view.getCellStates(parent.filterDescendants(filter)); } /** * 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. * * @param initialCell <Cell> that triggered this handler. */ getCells(initialCell) { if (!this.delayedSelection && this.graph.isCellMovable(initialCell)) { return [initialCell]; } return this.graph.getMovableCells(this.graph.getSelectionCells()); } /** * Returns the {@link Rectangle} used as the preview bounds for * moving the given cells. */ getPreviewBounds(cells) { const bounds = this.getBoundingBox(cells); if (bounds) { // 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) { const 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); } if (bounds.height < this.minimumSize) { const 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; } /** * Returns the union of the {@link CellStates} for the given array of {@link Cells}. * 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: {@link GraphView#getBounds} and * {@link AbstractGraph.getBoundingBox}. * * @param cells Array of {@link Cells} whose bounding box should be returned. */ getBoundingBox(cells) { let result = null; if (cells.length > 0) { for (let i = 0; i < cells.length; i += 1) { if (cells[i].isVertex() || cells[i].isEdge()) { const state = this.graph.view.getState(cells[i]); if (state) { let bbox = null; if (cells[i].isVertex() && state.shape && state.shape.boundingBox) { bbox = state.shape.boundingBox; } if (bbox) { if (!result) { result = Rectangle_js_1.default.fromRectangle(bbox); } else { result.add(bbox); } } } } } } return result; } /** * Creates the shape used to draw the preview for the given bounds. */ createPreviewShape(bounds) { const shape = new RectangleShape_js_1.default(bounds, Constants_js_1.NONE, this.previewColor); shape.isDashed = true; if (this.htmlPreview) { shape.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 = 'svg'; shape.init(this.graph.getView().getOverlayPane()); shape.pointerEvents = false; // Workaround for artifacts on iOS if (Client_js_1.default.IS_IOS) { shape.getSvgScreenOffset = () => { return 0; }; } } return shape; } createGuide() { return new Guide_js_1.default(this.graph, this.getGuideStates()); } /** * Starts the handling of the mouse gesture. */ start(cell, x, y, cells) { this.cell = cell; this.first = (0, styleUtils_js_1.convertPoint)(this.graph.container, x, y); this.cells = cells ? cells : this.getCells(this.cell); this.bounds = this.graph.getView().getBounds(this.cells); this.pBounds = this.getPreviewBounds(this.cells); this.cloning = false; this.cellCount = 0; for (let i = 0; i < this.cells.length; i += 1) { this.cellCount += this.addStates(this.cells[i], this.allCells); } if (this.guidesEnabled) { this.guide = this.createGuide(); const parent = cell.getParent(); const ignore = parent.getChildCount() < 2; // Uses connected states as guides const connected = new Map(); const opps = this.graph.getOpposites(this.graph.getEdges(this.cell), this.cell); for (let i = 0; i < opps.length; i += 1) { const state = this.graph.view.getState(opps[i]); if (state && !connected.get(state)) { connected.set(state, true); } } this.guide.isStateIgnored = (state) => { const p = state.cell.getParent(); return (!!state.cell && ((!this.cloning && !!this.isCellMoving(state.cell)) || (state.cell !== (this.target || parent) && !ignore && !connected.get(state) && (!this.target || this.target.getChildCount() >= 2) && p !== (this.target || parent)))); }; } } /** * Adds the states for the given cell recursively to the given Map. * @param cell * @param dict */ addStates(cell, dict) { const state = this.graph.view.getState(cell); let count = 0; if (state && !dict.get(cell)) { dict.set(cell, state); count++; const childCount = cell.getChildCount(); for (let i = 0; i < childCount; i += 1) { count += this.addStates(cell.getChildAt(i), dict); } } return count; } /** * Returns true if the given cell is currently being moved. */ isCellMoving(cell) { return this.allCells.has(cell); } /** * Returns true if the guides should be used for the given {@link MouseEvent}. * This implementation returns {@link Guide#isEnabledForEvent}. */ useGuidesForEvent(me) { return this.guide ? this.guide.isEnabledForEvent(me.getEvent()) && !this.graph.isConstrainedEvent(me.getEvent()) : true; } /** * Snaps the given vector to the grid and returns the given mxPoint instance. */ snap(vector) { const 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; } /** * Returns an {@link Point} that represents the vector for moving the cells * for the given {@link MouseEvent}. */ getDelta(me) { const point = (0, styleUtils_js_1.convertPoint)(this.graph.container, me.getX(), me.getY()); if (!this.first) return new Point_js_1.default(); return new Point_js_1.default(point.x - this.first.x - this.graph.getPanDx(), point.y - this.first.y - this.graph.getPanDy()); } /** * Hook for subclassers do show details while the handler is active. */ updateHint(me) { return; } /** * Hooks for subclassers to hide details when the handler gets inactive. */ removeHint() { return; } /** * Hook for rounding the unscaled vector. This uses Math.round. */ roundLength(length) { return Math.round(length * 100) / 100; } /** * Returns true if the given cell is a valid drop target. */ isValidDropTarget(target, me) { return this.cell ? this.cell.getParent() !== target : false; } /** * Updates the preview if cloning state has changed. */ checkPreview() { 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 && this.bounds) { this.shape = this.createPreviewShape(this.bounds); } } /** * Handles the event by highlighting possible drop targets and updating the preview. */ mouseMove(_sender, me) { const { graph } = this; if (!me.isConsumed() && graph.isMouseDown && this.cell && this.first && this.bounds && !this.suspended) { // Stops moving if a multi touch event is received if ((0, EventUtils_js_1.isMultiTouchEvent)(me.getEvent())) { this.reset(); return; } let delta = this.getDelta(me); const tol = graph.getEventTolerance(); if (this.shape || this.livePreviewActive || Math.abs(delta.x) > tol || Math.abs(delta.y) > tol) { // Highlight is used for highlighting drop targets if (!this.highlight) { this.highlight = new CellHighlight_js_1.default(this.graph, Constants_js_1.DROP_TARGET_COLOR, 3); } const clone = graph.isCloneEvent(me.getEvent()) && graph.isCellsCloneable() && this.isCloneEnabled(); const gridEnabled = graph.isGridEnabledEvent(me.getEvent()); const cell = me.getCell(); let hideGuide = true; let target = null; this.cloning = clone; if (graph.isDropEnabled() && this.highlightEnabled && this.cells) { // Contains a call to getCellAt to find the cell under the mouse target = graph.getDropTarget(this.cells, me.getEvent(), cell, clone); } let state = target ? graph.getView().getState(target) : null; let highlight = false; if (state && (clone || (target && this.isValidDropTarget(target, me)))) { if (this.target !== target) { this.target = target; this.setHighlightColor(Constants_js_1.DROP_TARGET_COLOR); } highlight = true; } else { this.target = null; if (this.connectOnDrop && cell && this.cells && this.cells.length === 1 && cell.isVertex() && cell.isConnectable()) { state = graph.getView().getState(cell); if (state) { const error = graph.getEdgeValidationError(null, this.cell, cell); const color = error === null ? Constants_js_1.VALID_COLOR : Constants_js_1.INVALID_CONNECT_TARGET_COLOR; this.setHighlightColor(color); highlight = true; } } } if (state && highlight) { this.highlight.highlight(state); } else { this.highlight.hide(); } if (this.guide && 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 && 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(InternalEvent_js_1.default.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. InternalEvent_js_1.default.consume(me.getEvent()); } else if ((this.isMoveEnabled() || this.isCloneEnabled()) && this.updateCursor && !me.isConsumed() && (me.getState() || me.sourceState) && !graph.isMouseDown) { let cursor = graph.getCursorForMouseEvent(me); const cell = me.getCell(); if (!cursor && cell && graph.isEnabled() && graph.isCellMovable(cell)) { if (cell.isEdge()) { cursor = config_js_1.EdgeHandlerConfig.cursorMovable; } else { cursor = config_js_1.VertexHandlerConfig.cursorMovable; } } // Sets the cursor on the original source state under the mouse // instead of the event source state which can be the parent if (cursor && me.sourceState) { me.sourceState.setCursor(cursor); } } } /** * Updates the bounds of the preview shape. */ updatePreview(remote = false) { if (this.livePreviewUsed && !remote) { if (this.cells) { this.setHandlesVisibleForCells(this.getSelectionCellsHandler()?.getHandledSelectionCells() ?? [], false); this.updateLivePreview(this.currentDx, this.currentDy); } } else { this.updatePreviewShape(); } } /** * Updates the bounds of the preview shape. */ updatePreviewShape() { if (this.shape && this.pBounds) { this.shape.bounds = new Rectangle_js_1.default(Math.round(this.pBounds.x + this.currentDx), Math.round(this.pBounds.y + this.currentDy), this.pBounds.width, this.pBounds.height); this.shape.redraw(); } } /** * Updates the bounds of the preview shape. */ updateLivePreview(dx, dy) { if (!this.suspended) { const states = []; if (this.allCells) { this.allCells.forEach((state) => { const realState = state ? this.graph.view.getState(state.cell) : null; // Checks if cell was removed or replaced if (realState !== state && state) { state.destroy(); if (realState) { this.allCells.set(state.cell, realState); } else { this.allCells.delete(state.cell); } state = realState; } if (state) { // Saves current state const tempState = state.clone(); states.push([state, tempState]); // Makes transparent for events to detect drop targets if (state.shape) { if (state.shape.originalPointerEvents === null) { state.shape.originalPointerEvents = state.shape.pointerEvents; } state.shape.pointerEvents = false; if (state.text) { if (state.text.originalPointerEvents === null) { state.text.originalPointerEvents = state.text.pointerEvents; } state.text.pointerEvents = false; } } // Temporarily changes position if (state.cell.isVertex()) { 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 && state.control.node) { state.control.node.style.visibility = 'hidden'; } } // Clone live preview may use text bounds else if (state.text) { state.text.updateBoundingBox(); // Fixes preview box for edge labels if (state.text.boundingBox) { state.text.boundingBox.x += dx; state.text.boundingBox.y += dy; } if (state.text.unrotatedBoundingBox) { 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 const s = this.graph.view.scale; for (let i = 0; i < states.length; i += 1) { const state = states[i][0]; if (state.cell.isEdge()) { const geometry = state.cell.getGeometry(); const points = []; if (geometry && geometry.points) { for (let j = 0; j < geometry.points.length; j++) { if (geometry.points[j]) { points.push(new Point_js_1.default(geometry.points[j].x + dx / s, geometry.points[j].y + dy / s)); } } } let source = state.visibleSourceState; let target = state.visibleTargetState; const pts = states[i][1].absolutePoints; if (source == null || !this.isCellMoving(source.cell)) { const pt0 = pts[0]; if (pt0) { state.setAbsoluteTerminalPoint(new Point_js_1.default(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)) { const ptn = pts[pts.length - 1]; if (ptn) { state.setAbsoluteTerminalPoint(new Point_js_1.default(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); } } } /** * Redraws the preview shape for the given states array. */ redrawHandles(states) { const selectionCellsHandler = this.getSelectionCellsHandler(); for (let i = 0; i < states.length; i += 1) { const handler = selectionCellsHandler?.getHandler(states[i][0].cell); handler?.redraw(true); } } /** * Resets the given preview states array. */ resetPreviewStates(states) { for (let i = 0; i < states.length; i += 1) { states[i][0].setState(states[i][1]); } } /** * Suspends the livew preview. */ suspend() { if (!this.suspended) { if (this.livePreviewUsed) { this.updateLivePreview(0, 0); } if (this.shape) { this.shape.node.style.visibility = 'hidden'; } if (this.guide) { this.guide.setVisible(false); } this.suspended = true; } } /** * Suspends the livew preview. */ resume() { if (this.suspended) { this.suspended = false; if (this.livePreviewUsed) { this.livePreviewActive = true; } if (this.shape) { this.shape.node.style.visibility = 'visible'; } if (this.guide) { this.guide.setVisible(true); } } } /** * Resets the livew preview. */ resetLivePreview() { this.allCells.forEach((state) => { // Restores event handling if (state.shape && 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 && state.text.originalPointerEvents !== null) { state.text.pointerEvents = state.text.originalPointerEvents; state.text.originalPointerEvents = null; } } // Shows folding icon if (state.control && state.control.node && state.control.node.style.visibility === 'hidden') { state.control.node.style.visibility = ''; } // Fixes preview box for edge labels if (!this.cloning) { if (state.text) { state.text.updateBoundingBox(); } } // Forces repaint of connected edges state.view.invalidate(state.cell); }); // Repaints all invalid states this.graph.view.validate(); } /** * Sets whether the handles attached to the given cells are visible. * * @param cells Array of {@link Cell}s. * @param visible Boolean that specifies if the handles should be visible. * @param force Forces an update of the handler regardless of the last used value. */ setHandlesVisibleForCells(cells, visible, force = false) { if (force || this.handlesVisible !== visible) { this.handlesVisible = visible; const selectionCellsHandler = this.getSelectionCellsHandler(); for (let i = 0; i < cells.length; i += 1) { const handler = selectionCellsHandler?.getHandler(cells[i]); if (handler) { handler.setHandlesVisible(visible); if (visible) { handler.redraw(); } } } } } /** * Sets the color of the rectangle used to highlight drop targets. * * @param color String that represents the new highlight color. */ setHighlightColor(color) { if (this.highlight) { this.highlight.setHighlightColor(color); } } /** * Handles the event by applying the changes to the selection cells. */ mouseUp(_sender, me) { if (!me.isConsumed()) { if (this.livePreviewUsed) { this.resetLivePreview(); } if (this.cell && this.first && (this.shape || this.livePreviewUsed) && (0, mathUtils_js_1.isNumeric)(this.currentDx) && (0, mathUtils_js_1.isNumeric)(this.currentDy)) { const { graph } = this; const cell = me.getCell(); if (this.connectOnDrop && !this.target && cell && cell.isVertex() && cell.isConnectable() && graph.isEdgeValid(null, this.cell, cell)) { const connectionHandler = graph.getPlugin('ConnectionHandler'); connectionHandler?.connect(this.cell, cell, me.getEvent()); } else { const clone = graph.isCloneEvent(me.getEvent()) && graph.isCellsCloneable() && this.isCloneEnabled(); const { scale } = graph.getView(); const dx = this.roundLength(this.currentDx / scale); const dy = this.roundLength(this.currentDy / scale); const target = this.target; if (target && graph.isSplitEnabled() && this.cells && graph.isSplitTarget(target, this.cells, me.getEvent())) { graph.splitEdge(target, this.cells, null, dx, dy, me.getGraphX(), me.getGraphY()); } else if (this.cells) { 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(InternalEvent_js_1.default.MOUSE_UP, me); } this.reset(); } /** * Resets the state of this handler. */ reset() { if (this.livePreviewUsed) { this.resetLivePreview(); this.setHandlesVisibleForCells(this.getSelectionCellsHandler()?.getHandledSelectionCells() ?? [], true); } this.destroyShapes(); this.removeHint(); this.delayedSelection = false; this.livePreviewActive = false; this.livePreviewUsed = false; this.cellWasClicked = false; this.suspended = false; this.currentDx = 0; this.currentDy = 0; this.cellCount = 0; this.cloning = false; this.allCells.clear(); this.pBounds = null; this.target = null; this.first = null; this.cells = null; this.cell = null; } /** * Returns true if the given cells should be removed from the parent for the specified * mousereleased event. */ shouldRemoveCellsFromParent(parent, cells, evt) { if (parent.isVertex()) { const pState = this.graph.getView().getState(parent); if (pState) { let pt = (0, styleUtils_js_1.convertPoint)(this.graph.container, (0, EventUtils_js_1.getClientX)(evt), (0, EventUtils_js_1.getClientY)(evt)); const alpha = (0, mathUtils_js_1.toRadians)(pState.style.rotation ?? 0); if (alpha !== 0) { const cos = Math.cos(-alpha); const sin = Math.sin(-alpha); const cx = new Point_js_1.default(pState.getCenterX(), pState.getCenterY()); pt = (0, mathUtils_js_1.getRotatedPoint)(pt, cos, sin, cx); } return !(0, mathUtils_js_1.contains)(pState, pt.x, pt.y); } } return false; } /** * Moves the given cells by the specified amount. */ moveCells(cells, dx, dy, clone, target, evt) { if (!this.cell) return; if (clone) { cells = this.graph.getCloneableCells(cells); } // Removes cells from parent const parent = this.cell.getParent(); if (!target && parent && 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.batchUpdate(() => { const parents = []; // Removes parent if all child cells are removed if (!clone && target && this.removeEmptyParents) { // Collects all non-selected parents const dict = new Map(); for (let i = 0; i < cells.length; i += 1) { dict.set(cells[i], true); } // LATER: Recurse up the cell hierarchy for