UNPKG

@maxgraph/core

Version:

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

1,143 lines (1,140 loc) 62 kB
"use strict"; /* Copyright 2021-present The maxGraph project Contributors 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 }); exports.CellsMixin = void 0; const mathUtils_js_1 = require("../../util/mathUtils.js"); const styleUtils_js_1 = require("../../util/styleUtils.js"); const Constants_js_1 = require("../../util/Constants.js"); const EventObject_js_1 = __importDefault(require("../event/EventObject.js")); const InternalEvent_js_1 = __importDefault(require("../event/InternalEvent.js")); const Rectangle_js_1 = __importDefault(require("../geometry/Rectangle.js")); const Point_js_1 = __importDefault(require("../geometry/Point.js")); const StringUtils_js_1 = require("../../util/StringUtils.js"); const cellArrayUtils_js_1 = require("../../util/cellArrayUtils.js"); // @ts-expect-error The properties of PartialGraph are defined elsewhere. exports.CellsMixin = { cellsResizable: true, cellsBendable: true, cellsSelectable: true, cellsDisconnectable: true, autoSizeCells: false, autoSizeCellsOnAdd: false, cellsLocked: false, cellsCloneable: true, cellsDeletable: true, cellsMovable: true, extendParents: true, extendParentsOnAdd: true, extendParentsOnMove: false, getBoundingBox(cells) { let result = null; if (cells.length > 0) { for (const cell of cells) { if (cell.isVertex() || cell.isEdge()) { const bbox = this.getView().getBoundingBox(this.getView().getState(cell), true); if (bbox) { if (!result) { result = Rectangle_js_1.default.fromRectangle(bbox); } else { result.add(bbox); } } } } } return result; }, removeStateForCell(cell) { for (const child of cell.getChildren()) { this.removeStateForCell(child); } this.getView().invalidate(cell, false, true); this.getView().removeState(cell); }, /***************************************************************************** * Group: Cell styles *****************************************************************************/ getCurrentCellStyle(cell, ignoreState = false) { const state = ignoreState ? null : this.getView().getState(cell); return state ? state.style : this.getCellStyle(cell); }, getCellStyle(cell) { const cellStyle = cell.getStyle(); const stylesheet = this.getStylesheet(); // Gets the default style for the cell const defaultStyle = cell.isEdge() ? stylesheet.getDefaultEdgeStyle() : stylesheet.getDefaultVertexStyle(); // Resolves the stylename using the above as the default const style = this.postProcessCellStyle(stylesheet.getCellStyle(cellStyle, defaultStyle ?? {})); return style; }, postProcessCellStyle(style) { if (!style.image) { return style; } const key = style.image; let image = this.getImageFromBundles(key); if (image) { style.image = image; } else { image = key; } // Converts short data uris to normal data uris if (image && image.substring(0, 11) === 'data:image/') { if (image.substring(0, 20) === 'data:image/svg+xml,<') { // Required for FF and IE11 image = image.substring(0, 19) + encodeURIComponent(image.substring(19)); } else if (image.substring(0, 22) !== 'data:image/svg+xml,%3C') { const comma = image.indexOf(','); // Adds base64 encoding prefix if needed if (comma > 0 && image.substring(comma - 7, comma + 1) !== ';base64,') { image = `${image.substring(0, comma)};base64,${image.substring(comma + 1)}`; } } style.image = image; } return style; }, setCellStyle(style, cells) { cells = cells ?? this.getSelectionCells(); this.batchUpdate(() => { for (const cell of cells) { this.getDataModel().setStyle(cell, style); } }); }, toggleCellStyle(key, defaultValue = false, cell) { cell = cell ?? this.getSelectionCell(); return this.toggleCellStyles(key, defaultValue, [cell]); }, toggleCellStyles(key, defaultValue = false, cells) { let value = false; cells = cells ?? this.getSelectionCells(); if (cells.length > 0) { const style = this.getCurrentCellStyle(cells[0]); value = !(style[key] ?? defaultValue); this.setCellStyles(key, value, cells); } return value; }, setCellStyles(key, value, cells) { cells = cells ?? this.getSelectionCells(); (0, styleUtils_js_1.setCellStyles)(this.getDataModel(), cells, key, value); }, toggleCellStyleFlags(key, flag, cells) { cells = cells ?? this.getSelectionCells(); this.setCellStyleFlags(key, flag, null, cells); }, setCellStyleFlags(key, flag, value = null, cells) { cells = cells ?? this.getSelectionCells(); if (cells.length > 0) { if (value === null) { const style = this.getCurrentCellStyle(cells[0]); const current = style[key] || 0; value = !((current & flag) === flag); } (0, styleUtils_js_1.setCellStyleFlags)(this.getDataModel(), cells, key, flag, value); } }, /***************************************************************************** * Group: Cell alignment and orientation *****************************************************************************/ alignCells(align, cells, param = null) { cells = cells ?? this.getSelectionCells(); if (cells.length > 1) { // Finds the required coordinate for the alignment if (param === null) { for (const cell of cells) { const state = this.getView().getState(cell); if (state && !cell.isEdge()) { if (param === null) { if (align === 'center') { param = state.x + state.width / 2; break; } else if (align === 'right') { param = state.x + state.width; } else if (align === 'top') { param = state.y; } else if (align === 'middle') { param = state.y + state.height / 2; break; } else if (align === 'bottom') { param = state.y + state.height; } else { param = state.x; } } else if (align === 'right') { param = Math.max(param, state.x + state.width); } else if (align === 'top') { param = Math.min(param, state.y); } else if (align === 'bottom') { param = Math.max(param, state.y + state.height); } else { param = Math.min(param, state.x); } } } } // Aligns the cells to the coordinate if (param !== null) { const s = this.getView().scale; this.batchUpdate(() => { const p = param; for (const cell of cells) { const state = this.getView().getState(cell); if (state != null) { let geo = cell.getGeometry(); if (geo != null && !cell.isEdge()) { geo = geo.clone(); if (align === 'center') { geo.x += (p - state.x - state.width / 2) / s; } else if (align === 'right') { geo.x += (p - state.x - state.width) / s; } else if (align === 'top') { geo.y += (p - state.y) / s; } else if (align === 'middle') { geo.y += (p - state.y - state.height / 2) / s; } else if (align === 'bottom') { geo.y += (p - state.y - state.height) / s; } else { geo.x += (p - state.x) / s; } this.resizeCell(cell, geo); } } } this.fireEvent(new EventObject_js_1.default(InternalEvent_js_1.default.ALIGN_CELLS, { align, cells })); }); } } return cells; }, /***************************************************************************** * Group: Cell cloning, insertion and removal *****************************************************************************/ cloneCell(cell, allowInvalidEdges = false, mapping = {}, keepPosition = false) { return this.cloneCells([cell], allowInvalidEdges, mapping, keepPosition)[0]; }, cloneCells(cells, allowInvalidEdges = true, mapping = {}, keepPosition = false) { let clones; // Creates a map for fast lookups const dict = new Map(); const tmp = []; for (const cell of cells) { dict.set(cell, true); tmp.push(cell); } if (tmp.length > 0) { const { scale } = this.getView(); const trans = this.getView().translate; const out = []; clones = (0, cellArrayUtils_js_1.cloneCells)(cells, true, mapping); for (let i = 0; i < cells.length; i += 1) { const cell = cells[i]; const clone = clones[i]; if (!allowInvalidEdges && clone.isEdge() && this.getEdgeValidationError(clone, clone.getTerminal(true), clone.getTerminal(false)) !== null) { //clones[i] = null; } else { out.push(clone); const g = clone.getGeometry(); if (g) { const state = this.getView().getState(cell); const parent = cell.getParent(); const pstate = parent ? this.getView().getState(parent) : null; if (state && pstate) { const dx = keepPosition ? 0 : pstate.origin.x; const dy = keepPosition ? 0 : pstate.origin.y; if (clone.isEdge()) { const pts = state.absolutePoints; // Checks if the source is cloned or sets the terminal point let src = cell.getTerminal(true); while (src && !dict.get(src)) { src = src.getParent(); } if (!src && pts[0]) { g.setTerminalPoint(new Point_js_1.default(pts[0].x / scale - trans.x, pts[0].y / scale - trans.y), true); } // Checks if the target is cloned or sets the terminal point let trg = cell.getTerminal(false); while (trg && !dict.get(trg)) { trg = trg.getParent(); } const n = pts.length - 1; const p = pts[n]; if (!trg && p) { g.setTerminalPoint(new Point_js_1.default(p.x / scale - trans.x, p.y / scale - trans.y), false); } // Translates the control points const { points } = g; if (points) { for (const point of points) { point.x += dx; point.y += dy; } } } else { g.translate(dx, dy); } } } } } clones = out; } else { clones = []; } return clones; }, addCell(cell, parent = null, index = null, source = null, target = null) { return this.addCells([cell], parent, index, source, target)[0]; }, addCells(cells, parent = null, index = null, source = null, target = null, absolute = false) { const p = parent ?? this.getDefaultParent(); const i = index ?? p.getChildCount(); this.batchUpdate(() => { this.cellsAdded(cells, p, i, source, target, absolute, true); this.fireEvent(new EventObject_js_1.default(InternalEvent_js_1.default.ADD_CELLS, { cells, p, i, source, target })); }); return cells; }, cellsAdded(cells, parent, index, source = null, target = null, absolute = false, constrain = false, extend = true) { this.batchUpdate(() => { const parentState = absolute ? this.getView().getState(parent) : null; const o1 = parentState ? parentState.origin : null; const zero = new Point_js_1.default(0, 0); cells.forEach((cell, i) => { /* Can cells include null values? if (cell == null) { index--; } else { */ const previous = cell.getParent(); // Keeps the cell at its absolute location if (o1 && cell !== parent && parent !== previous) { const oldState = previous ? this.getView().getState(previous) : null; const o2 = oldState ? oldState.origin : zero; let geo = cell.getGeometry(); if (geo) { const dx = o2.x - o1.x; const dy = o2.y - o1.y; // FIXME: Cells should always be inserted first before any other edit // to avoid forward references in sessions. geo = geo.clone(); geo.translate(dx, dy); if (!geo.relative && cell.isVertex() && !this.isAllowNegativeCoordinates()) { geo.x = Math.max(0, geo.x); geo.y = Math.max(0, geo.y); } this.getDataModel().setGeometry(cell, geo); } } // Decrements all following indices // if cell is already in parent if (parent === previous && index + i > parent.getChildCount()) { index--; } this.getDataModel().add(parent, cell, index + i); if (this.autoSizeCellsOnAdd) { this.autoSizeCell(cell, true); } // Extends the parent or constrains the child if ((!extend || extend) && this.isExtendParentsOnAdd(cell) && this.isExtendParent(cell)) { this.extendParent(cell); } // Additionally constrains the child after extending the parent if (!constrain || constrain) { this.constrainChild(cell); } // Sets the source terminal if (source) { this.cellConnected(cell, source, true); } // Sets the target terminal if (target) { this.cellConnected(cell, target, false); } /*}*/ }); this.fireEvent(new EventObject_js_1.default(InternalEvent_js_1.default.CELLS_ADDED, { cells, parent, index, source, target, absolute, })); }); }, autoSizeCell(cell, recurse = true) { if (recurse) { for (const child of cell.getChildren()) { this.autoSizeCell(child); } } if (cell.isVertex() && this.isAutoSizeCell(cell)) { this.updateCellSize(cell); } }, removeCells(cells = null, includeEdges = true) { if (!cells) { cells = this.getDeletableCells(this.getSelectionCells()); } // Adds all edges to the cells if (includeEdges) { // FIXME: Remove duplicate cells in result or do not add if // in cells or descendant of cells cells = this.getDeletableCells(this.addAllEdges(cells)); } else { cells = cells.slice(); // Removes edges that are currently not // visible as those cannot be updated const edges = this.getDeletableCells(this.getAllEdges(cells)); const dict = new Map(); for (const cell of cells) { dict.set(cell, true); } for (const edge of edges) { if (!this.getView().getState(edge) && !dict.get(edge)) { dict.set(edge, true); cells.push(edge); } } } this.batchUpdate(() => { this.cellsRemoved(cells); this.fireEvent(new EventObject_js_1.default(InternalEvent_js_1.default.REMOVE_CELLS, { cells, includeEdges })); }); return cells ?? []; }, cellsRemoved(cells) { if (cells.length > 0) { const { scale } = this.getView(); const tr = this.getView().translate; this.batchUpdate(() => { // Creates hashtable for faster lookup const dict = new Map(); for (const cell of cells) { dict.set(cell, true); } for (const cell of cells) { // Disconnects edges which are not being removed const edges = this.getAllEdges([cell]); const disconnectTerminal = (edge, source) => { let geo = edge.getGeometry(); if (geo) { // Checks if terminal is being removed const terminal = edge.getTerminal(source); let connected = false; let tmp = terminal; while (tmp) { if (cell === tmp) { connected = true; break; } tmp = tmp.getParent(); } if (connected) { geo = geo.clone(); const state = this.getView().getState(edge); if (state) { const pts = state.absolutePoints; const n = source ? 0 : pts.length - 1; const p = pts[n]; geo.setTerminalPoint(new Point_js_1.default(p.x / scale - tr.x - state.origin.x, p.y / scale - tr.y - state.origin.y), source); } else if (terminal) { // Fallback to center of terminal if routing // points are not available to add new point // KNOWN: Should recurse to find parent offset // of edge for nested groups but invisible edges // should be removed in removeCells step const tstate = this.getView().getState(terminal); if (tstate) { geo.setTerminalPoint(new Point_js_1.default(tstate.getCenterX() / scale - tr.x, tstate.getCenterY() / scale - tr.y), source); } } this.getDataModel().setGeometry(edge, geo); this.getDataModel().setTerminal(edge, null, source); } } }; for (const edge of edges) { if (!dict.get(edge)) { dict.set(edge, true); disconnectTerminal(edge, true); disconnectTerminal(edge, false); } } this.getDataModel().remove(cell); } this.fireEvent(new EventObject_js_1.default(InternalEvent_js_1.default.CELLS_REMOVED, { cells })); }); } }, /***************************************************************************** * Group: Cell visibility *****************************************************************************/ toggleCells(show = false, cells, includeEdges = true) { cells = cells ?? this.getSelectionCells(); // Adds all connected edges recursively if (includeEdges) { cells = this.addAllEdges(cells); } this.batchUpdate(() => { this.cellsToggled(cells, show); this.fireEvent(new EventObject_js_1.default(InternalEvent_js_1.default.TOGGLE_CELLS, { show, cells, includeEdges })); }); return cells; }, cellsToggled(cells, show = false) { if (cells.length > 0) { this.batchUpdate(() => { for (const cell of cells) { this.getDataModel().setVisible(cell, show); } }); } }, /***************************************************************************** * Group: Cell sizing *****************************************************************************/ updateCellSize(cell, ignoreChildren = false) { this.batchUpdate(() => { this.cellSizeUpdated(cell, ignoreChildren); this.fireEvent(new EventObject_js_1.default(InternalEvent_js_1.default.UPDATE_CELL_SIZE, { cell, ignoreChildren })); }); return cell; }, cellSizeUpdated(cell, ignoreChildren = false) { this.batchUpdate(() => { const size = this.getPreferredSizeForCell(cell); let geo = cell.getGeometry(); if (size && geo) { const collapsed = cell.isCollapsed(); geo = geo.clone(); if (this.isSwimlane(cell)) { const style = this.getCellStyle(cell); const cellStyle = cell.getStyle(); if (style.horizontal ?? true) { cellStyle.startSize = size.height + 8; if (collapsed) { geo.height = size.height + 8; } geo.width = size.width; } else { cellStyle.startSize = size.width + 8; if (collapsed) { geo.width = size.width + 8; } geo.height = size.height; } this.getDataModel().setStyle(cell, cellStyle); } else { const state = this.getView().createState(cell); const align = state.style.align ?? 'center'; if (align === 'right') { geo.x += geo.width - size.width; } else if (align === 'center') { geo.x += Math.round((geo.width - size.width) / 2); } const valign = state.getVerticalAlign(); if (valign === 'bottom') { geo.y += geo.height - size.height; } else if (valign === 'middle') { geo.y += Math.round((geo.height - size.height) / 2); } geo.width = size.width; geo.height = size.height; } if (!ignoreChildren && !collapsed) { const bounds = this.getView().getBounds(cell.getChildren()); if (bounds != null) { const tr = this.getView().translate; const { scale } = this.getView(); const width = (bounds.x + bounds.width) / scale - geo.x - tr.x; const height = (bounds.y + bounds.height) / scale - geo.y - tr.y; geo.width = Math.max(geo.width, width); geo.height = Math.max(geo.height, height); } } this.cellsResized([cell], [geo], false); } }); }, getPreferredSizeForCell(cell, textWidth = null) { let result = null; const state = this.getView().createState(cell); const { style } = state; if (!cell.isEdge()) { const fontSize = style.fontSize || Constants_js_1.DEFAULT_FONTSIZE; let dx = 0; let dy = 0; // Adds dimension of image if shape is a label if (state.getImageSrc() || style.image) { if (style.shape === 'label') { if (style.verticalAlign === 'middle') { dx += style.imageWidth || Constants_js_1.DEFAULT_IMAGESIZE; } if (style.align !== 'center') { dy += style.imageHeight || Constants_js_1.DEFAULT_IMAGESIZE; } } } // Adds spacings dx += 2 * (style.spacing || 0); dx += style.spacingLeft || 0; dx += style.spacingRight || 0; dy += 2 * (style.spacing || 0); dy += style.spacingTop || 0; dy += style.spacingBottom || 0; // Add spacing for collapse/expand icon // LATER: Check alignment and use constants // for image spacing const image = this.getFoldingImage(state); if (image) { dx += image.width + 8; } // Adds space for label let value = this.getCellRenderer().getLabelValue(state); if (value && value.length > 0) { if (!this.isHtmlLabel(state.cell)) { value = (0, StringUtils_js_1.htmlEntities)(value, false); } value = value.replace(/\n/g, '<br>'); const size = (0, styleUtils_js_1.getSizeForString)(value, fontSize, style.fontFamily, textWidth, style.fontStyle); let width = size.width + dx; let height = size.height + dy; if (!(style.horizontal ?? true)) { const tmp = height; height = width; width = tmp; } if (this.isGridEnabled()) { width = this.snap(width + this.getGridSize() / 2); height = this.snap(height + this.getGridSize() / 2); } result = new Rectangle_js_1.default(0, 0, width, height); } else { const gs2 = 4 * this.getGridSize(); result = new Rectangle_js_1.default(0, 0, gs2, gs2); } } return result; }, resizeCell(cell, bounds, recurse = false) { return this.resizeCells([cell], [bounds], recurse)[0]; }, resizeCells(cells, bounds, recurse) { recurse = recurse ?? this.isRecursiveResize(); this.batchUpdate(() => { const prev = this.cellsResized(cells, bounds, recurse); this.fireEvent(new EventObject_js_1.default(InternalEvent_js_1.default.RESIZE_CELLS, { cells, bounds, prev })); }); return cells; }, cellsResized(cells, bounds, recurse = false) { const prev = []; if (cells.length === bounds.length) { this.batchUpdate(() => { cells.forEach((cell, i) => { prev.push(this.cellResized(cell, bounds[i], false, recurse)); if (this.isExtendParent(cell)) { this.extendParent(cell); } this.constrainChild(cell); }); if (this.isResetEdgesOnResize()) { this.resetEdges(cells); } this.fireEvent(new EventObject_js_1.default(InternalEvent_js_1.default.CELLS_RESIZED, { cells, bounds, prev })); }); } return prev; }, cellResized(cell, bounds, ignoreRelative = false, recurse = false) { const prev = cell.getGeometry(); if (prev && (prev.x !== bounds.x || prev.y !== bounds.y || prev.width !== bounds.width || prev.height !== bounds.height)) { const geo = prev.clone(); if (!ignoreRelative && geo.relative) { const { offset } = geo; if (offset) { offset.x += bounds.x - geo.x; offset.y += bounds.y - geo.y; } } else { geo.x = bounds.x; geo.y = bounds.y; } geo.width = bounds.width; geo.height = bounds.height; if (!geo.relative && cell.isVertex() && !this.isAllowNegativeCoordinates()) { geo.x = Math.max(0, geo.x); geo.y = Math.max(0, geo.y); } this.batchUpdate(() => { if (recurse) { this.resizeChildCells(cell, geo); } this.getDataModel().setGeometry(cell, geo); this.constrainChildCells(cell); }); } return prev; }, resizeChildCells(cell, newGeo) { const geo = cell.getGeometry(); if (geo) { const dx = geo.width !== 0 ? newGeo.width / geo.width : 1; const dy = geo.height !== 0 ? newGeo.height / geo.height : 1; for (const child of cell.getChildren()) { this.scaleCell(child, dx, dy, true); } } }, constrainChildCells(cell) { for (const child of cell.getChildren()) { this.constrainChild(child); } }, scaleCell(cell, dx, dy, recurse = false) { let geo = cell.getGeometry(); if (geo) { const style = this.getCurrentCellStyle(cell); geo = geo.clone(); // Stores values for restoring based on style const { x } = geo; const { y } = geo; const w = geo.width; const h = geo.height; geo.scale(dx, dy, style.aspect === 'fixed'); if (style.resizeWidth) { geo.width = w * dx; } else if (!style.resizeWidth) { geo.width = w; } if (style.resizeHeight) { geo.height = h * dy; } else if (!style.resizeHeight) { geo.height = h; } if (!this.isCellMovable(cell)) { geo.x = x; geo.y = y; } if (!this.isCellResizable(cell)) { geo.width = w; geo.height = h; } if (cell.isVertex()) { this.cellResized(cell, geo, true, recurse); } else { this.getDataModel().setGeometry(cell, geo); } } }, extendParent(cell) { const parent = cell.getParent(); let p = parent ? parent.getGeometry() : null; if (parent && p && !parent.isCollapsed()) { const geo = cell.getGeometry(); if (geo && !geo.relative && (p.width < geo.x + geo.width || p.height < geo.y + geo.height)) { p = p.clone(); p.width = Math.max(p.width, geo.x + geo.width); p.height = Math.max(p.height, geo.y + geo.height); this.cellsResized([parent], [p], false); } } }, // ************************************************************************************* // Group: Cell moving // ************************************************************************************* importCells(cells, dx, dy, target = null, evt = null, mapping = {}) { return this.moveCells(cells, dx, dy, true, target, evt, mapping); }, moveCells(cells, dx = 0, dy = 0, clone = false, target = null, evt = null, mapping = {}) { if (dx !== 0 || dy !== 0 || clone || target) { // Removes descendants with ancestors in cells to avoid multiple moving cells = (0, cellArrayUtils_js_1.getTopmostCells)(cells); const origCells = cells; this.batchUpdate(() => { // Faster cell lookups to remove relative edge labels with selected // terminals to avoid explicit and implicit move at same time const dict = new Map(); for (const cell of cells) { dict.set(cell, true); } const isSelected = (cell) => { while (cell) { if (dict.get(cell)) { return true; } cell = cell.getParent(); } return false; }; // Removes relative edge labels with selected terminals const checked = []; for (const cell of cells) { const geo = cell.getGeometry(); const parent = cell.getParent(); if (!geo || !geo.relative || (parent && !parent.isEdge()) || (parent && !isSelected(parent.getTerminal(true)) && !isSelected(parent.getTerminal(false)))) { checked.push(cell); } } cells = checked; if (clone) { cells = this.cloneCells(cells, this.isCloneInvalidEdges(), mapping); if (!target) { target = this.getDefaultParent(); } } // FIXME: Cells should always be inserted first before any other edit // to avoid forward references in sessions. // Need to disable allowNegativeCoordinates if target not null to // allow for temporary negative numbers until cellsAdded is called. const previous = this.isAllowNegativeCoordinates(); if (target) { this.setAllowNegativeCoordinates(true); } this.cellsMoved(cells, dx, dy, !clone && this.isDisconnectOnMove() && this.isAllowDanglingEdges(), !target, this.isExtendParentsOnMove() && !target); this.setAllowNegativeCoordinates(previous); if (target) { const index = target.getChildCount(); this.cellsAdded(cells, target, index, null, null, true); // Restores parent edge on cloned edge labels if (clone) { cells.forEach((cell, i) => { const geo = cell.getGeometry(); const parent = origCells[i].getParent(); if (geo && geo.relative && parent && parent.isEdge() && this.getDataModel().contains(parent)) { this.getDataModel().add(parent, cell); } }); } } // Dispatches a move event this.fireEvent(new EventObject_js_1.default(InternalEvent_js_1.default.MOVE_CELLS, { cells, dx, dy, clone, target, event: evt, })); }); } return cells; }, cellsMoved(cells, dx, dy, disconnect = false, constrain = false, extend = false) { if (dx !== 0 || dy !== 0) { this.batchUpdate(() => { if (disconnect) { this.disconnectGraph(cells); } for (const cell of cells) { this.translateCell(cell, dx, dy); if (extend && this.isExtendParent(cell)) { this.extendParent(cell); } else if (constrain) { this.constrainChild(cell); } } if (this.isResetEdgesOnMove()) { this.resetEdges(cells); } this.fireEvent(new EventObject_js_1.default(InternalEvent_js_1.default.CELLS_MOVED, { cells, dx, dy, disconnect })); }); } }, translateCell(cell, dx, dy) { let geometry = cell.getGeometry(); if (geometry) { geometry = geometry.clone(); geometry.translate(dx, dy); if (!geometry.relative && cell.isVertex() && !this.isAllowNegativeCoordinates()) { geometry.x = Math.max(0, geometry.x); geometry.y = Math.max(0, geometry.y); } if (geometry.relative && !cell.isEdge()) { const parent = cell.getParent(); let angle = 0; if (parent.isVertex()) { const style = this.getCurrentCellStyle(parent); angle = style.rotation ?? 0; } if (angle !== 0) { const rad = (0, mathUtils_js_1.toRadians)(-angle); const cos = Math.cos(rad); const sin = Math.sin(rad); const pt = (0, mathUtils_js_1.getRotatedPoint)(new Point_js_1.default(dx, dy), cos, sin, new Point_js_1.default(0, 0)); dx = pt.x; dy = pt.y; } if (!geometry.offset) { geometry.offset = new Point_js_1.default(dx, dy); } else { geometry.offset.x = geometry.offset.x + dx; geometry.offset.y = geometry.offset.y + dy; } } this.getDataModel().setGeometry(cell, geometry); } }, getCellContainmentArea(cell) { if (!cell.isEdge()) { const parent = cell.getParent(); if (parent && parent !== this.getDefaultParent()) { const g = parent.getGeometry(); if (g) { let x = 0; let y = 0; let w = g.width; let h = g.height; if (this.isSwimlane(parent)) { const size = this.getStartSize(parent); const style = this.getCurrentCellStyle(parent); const dir = style.direction ?? 'east'; const flipH = style.flipH ?? false; const flipV = style.flipV ?? false; if (dir === 'south' || dir === 'north') { const tmp = size.width; size.width = size.height; size.height = tmp; } if ((dir === 'east' && !flipV) || (dir === 'north' && !flipH) || (dir === 'west' && flipV) || (dir === 'south' && flipH)) { x = size.width; y = size.height; } w -= size.width; h -= size.height; } return new Rectangle_js_1.default(x, y, w, h); } } } return null; }, constrainChild(cell, sizeFirst = true) { let geo = cell.getGeometry(); if (geo && (this.isConstrainRelativeChildren() || !geo.relative)) { const parent = cell.getParent(); let max = this.getMaximumGraphBounds(); // Finds parent offset if (max && parent) { const off = this.getBoundingBoxFromGeometry([parent], false); if (off) { max = Rectangle_js_1.default.fromRectangle(max); max.x -= off.x; max.y -= off.y; } } if (this.isConstrainChild(cell)) { let tmp = this.getCellContainmentArea(cell); if (tmp) { const overlap = this.getOverlap(cell); if (overlap > 0) { tmp = Rectangle_js_1.default.fromRectangle(tmp); tmp.x -= tmp.width * overlap; tmp.y -= tmp.height * overlap; tmp.width += 2 * tmp.width * overlap; tmp.height += 2 * tmp.height * overlap; } // Find the intersection between max and tmp if (!max) { max = tmp; } else { max = Rectangle_js_1.default.fromRectangle(max); max.intersect(tmp); } } } if (max) { const cells = [cell]; if (!cell.isCollapsed()) { const desc = cell.getDescendants(); for (const descItem of desc) { if (descItem.isVisible()) { cells.push(descItem); } } } const bbox = this.getBoundingBoxFromGeometry(cells, false); if (bbox) { geo = geo.clone(); // Cumulative horizontal movement let dx = 0; if (geo.width > max.width) { dx = geo.width - max.width; geo.width -= dx; } if (bbox.x + bbox.width > max.x + max.width) { dx -= bbox.x + bbox.width - max.x - max.width - dx; } // Cumulative vertical movement let dy = 0; if (geo.height > max.height) { dy = geo.height - max.height; geo.height -= dy; } if (bbox.y + bbox.height > max.y + max.height) { dy -= bbox.y + bbox.height - max.y - max.height - dy; } if (bbox.x < max.x) { dx -= bbox.x - max.x; } if (bbox.y < max.y) { dy -= bbox.y - max.y; } if (dx !== 0 || dy !== 0) { if (geo.relative) { // Relative geometries are moved via absolute offset if (!geo.offset) { geo.offset = new Point_js_1.default(); } geo.offset.x += dx; geo.offset.y += dy; } else { geo.x += dx; geo.y += dy; } } this.getDataModel().setGeometry(cell, geo); } } } }, /***************************************************************************** * Group: Cell retrieval *****************************************************************************/ getChildCells(parent, vertices = false, edges = false) { parent = parent ?? this.getDefaultParent(); const cells = parent.getChildCells(vertices, edges); const result = []; // Filters out the non-visible child cells for (const cell of cells) { if (cell.isVisible()) { result.push(cell); } } return result; }, getCellAt(x, y, parent = null, vertices = true, edges = true, ignoreFn = null) { if (!parent) { parent = this.getCurrentRoot(); if (!parent) { parent = this.getDataModel().getRoot(); } } if (parent) { const childCount = parent.getChildCount(); for (let i = childCount - 1; i >= 0; i--) { const cell = parent.getChildAt(i); const result = this.getCellAt(x, y, cell, vertices, edges, ignoreFn); if (result) { return result; } if (cell.isVisible() && ((edges && cell.isEdge()) || (vertices && cell.isVertex()))) { const state = this.getView().getState(cell); if (state && (!ignoreFn || !ignoreFn(state, x, y)) && this.intersects(state, x, y)) { return cell; } } } } return null; }, getCells(x, y, width, height, parent = null, result = [], intersection = null, ignoreFn = null, includeDescendants = false) { if (width > 0 || height > 0 || intersection) { const model = this.getDataModel(); const right = x + width; const bottom = y + height; if (!parent) { parent = this.getCurrentRoot(); if (!parent) { parent = model.getRoot(); } } if (parent) { for (const cell of parent.getChildren()) { const state = this.getView().getState(cell); if (state && cell.isVisible() && (!ignoreFn || !ignoreFn(state))) { const deg = state.style.rotation ?? 0; let box = state; // TODO: CHECK ME!!!! ========================================================== if (deg !== 0) { box = (0, mathUtils_js_1.getBoundingBox)(box, deg); } const hit = (intersection && cell.isVertex() && (0, mathUtils_js_1.intersects)(intersectio