@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
JavaScript
"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