@antv/x6
Version:
JavaScript diagramming library that uses SVG and HTML for rendering
1,169 lines • 45.5 kB
JavaScript
import { __decorate } from "tslib";
import { Dom, disposable, FunctionExt, isModifierKeyMatch, } from '../../common';
import { Rectangle, } from '../../geometry';
import { Cell, Collection } from '../../model';
import { routerRegistry } from '../../registry';
import { View } from '../../view';
export class SelectionImpl extends View {
get graph() {
return this.options.graph;
}
get boxClassName() {
return this.prefixClassName(classNames.box);
}
get $boxes() {
return Dom.children(this.container, this.boxClassName);
}
get handleOptions() {
return this.options;
}
constructor(options) {
super();
this.updateThrottleTimer = null;
this.isDragging = false;
this.batchUpdating = false;
// 逐帧批处理拖拽位移,降低 translate 重绘频率
this.dragRafId = null;
// 合并缩放/平移下的选择框刷新到每帧一次
this.transformRafId = null;
this.dragPendingOffset = null;
this.containerLocalOffsetX = 0;
this.containerLocalOffsetY = 0;
this.containerOffsetX = 0;
this.containerOffsetY = 0;
this.draggingPreviewMode = 'translate';
// 拖拽过程的缓存,减少每次 move 重复计算
this.translatingCache = null;
this.movingRouterRestoreCache = null;
this.movingRouterRestoreTimer = null;
this.lastMovingTs = null;
this.movingDegradeActivatedTs = null;
this.options = options;
if (this.options.model) {
this.options.collection = this.options.model.collection;
}
if (this.options.collection) {
this.collection = this.options.collection;
}
else {
this.collection = new Collection([], {
comparator: depthComparator,
});
this.options.collection = this.collection;
}
this.boxCount = 0;
this.boxesUpdated = false;
this.createContainer();
this.startListening();
}
startListening() {
const graph = this.graph;
const collection = this.collection;
this.delegateEvents({
[`mousedown .${this.boxClassName}`]: 'onSelectionBoxMouseDown',
[`touchstart .${this.boxClassName}`]: 'onSelectionBoxMouseDown',
[`mousedown .${this.prefixClassName(classNames.inner)}`]: 'onSelectionContainerMouseDown',
[`touchstart .${this.prefixClassName(classNames.inner)}`]: 'onSelectionContainerMouseDown',
}, true);
graph.on('scale', this.onGraphTransformed, this);
graph.on('translate', this.onGraphTransformed, this);
graph.model.on('updated', this.onModelUpdated, this);
collection.on('added', this.onCellAdded, this);
collection.on('removed', this.onCellRemoved, this);
collection.on('reseted', this.onReseted, this);
collection.on('updated', this.onCollectionUpdated, this);
collection.on('node:change:position', this.onNodePositionChanged, this);
collection.on('cell:changed', this.onCellChanged, this);
}
stopListening() {
const graph = this.graph;
const collection = this.collection;
this.undelegateEvents();
graph.off('scale', this.onGraphTransformed, this);
graph.off('translate', this.onGraphTransformed, this);
// 清理缩放/平移的 rAF 刷新与 throttleTimer
if (this.transformRafId != null) {
cancelAnimationFrame(this.transformRafId);
this.transformRafId = null;
}
if (this.updateThrottleTimer) {
clearTimeout(this.updateThrottleTimer);
this.updateThrottleTimer = null;
}
graph.model.off('updated', this.onModelUpdated, this);
collection.off('added', this.onCellAdded, this);
collection.off('removed', this.onCellRemoved, this);
collection.off('reseted', this.onReseted, this);
collection.off('updated', this.onCollectionUpdated, this);
collection.off('node:change:position', this.onNodePositionChanged, this);
collection.off('cell:changed', this.onCellChanged, this);
}
onRemove() {
this.stopListening();
}
onGraphTransformed() {
if (this.updateThrottleTimer) {
clearTimeout(this.updateThrottleTimer);
this.updateThrottleTimer = null;
}
// 使用 rAF 将多次 transform 合并为每帧一次刷新
if (this.transformRafId == null) {
this.transformRafId = window.requestAnimationFrame(() => {
this.transformRafId = null;
if (this.collection.length <= 0) {
return;
}
if (this.isDragging) {
this.repositionSelectionBoxesInPlace();
if (this.options.following) {
this.resetContainerPosition();
}
else {
this.syncContainerPosition();
}
return;
}
this.refreshSelectionBoxes();
});
}
}
onCellChanged() {
this.updateSelectionBoxes();
}
onNodePositionChanged({ node, options, }) {
const { showNodeSelectionBox, pointerEvents } = this.options;
const { ui, selection, translateBy, snapped } = options;
const allowTranslating = (showNodeSelectionBox !== true ||
(pointerEvents &&
this.getPointerEventsValue(pointerEvents) === 'none')) &&
!this.translating &&
!selection;
const translateByUi = ui && translateBy && node.id === translateBy;
if (allowTranslating && (translateByUi || snapped)) {
this.translating = true;
const current = node.position();
const previous = node.previous('position');
if (previous) {
const dx = current.x - previous.x;
const dy = current.y - previous.y;
if (dx !== 0 || dy !== 0) {
this.translateSelectedNodes(dx, dy, node, options);
}
}
this.translating = false;
}
}
onModelUpdated({ removed }) {
if (removed === null || removed === void 0 ? void 0 : removed.length) {
this.unselect(removed);
}
}
isEmpty() {
return this.length <= 0;
}
isSelected(cell) {
return this.collection.has(cell);
}
get length() {
return this.collection.length;
}
get cells() {
return this.collection.toArray();
}
select(cells, options = {}) {
options.dryrun = true;
const items = this.filter(Array.isArray(cells) ? cells : [cells]);
this.collection.add(items, options);
return this;
}
unselect(cells, options = {}) {
// dryrun to prevent cell be removed from graph
options.dryrun = true;
this.collection.remove(Array.isArray(cells) ? cells : [cells], options);
return this;
}
reset(cells, options = {}) {
if (cells) {
this.batchUpdating = !!options.batch;
const prev = this.cells;
const next = this.filter(Array.isArray(cells) ? cells : [cells]);
const prevMap = {};
const nextMap = {};
for (const cell of prev) {
prevMap[cell.id] = cell;
}
for (const cell of next) {
nextMap[cell.id] = cell;
}
const added = [];
const removed = [];
next.forEach((cell) => {
if (!prevMap[cell.id]) {
added.push(cell);
}
});
prev.forEach((cell) => {
if (!nextMap[cell.id]) {
removed.push(cell);
}
});
if (removed.length) {
this.unselect(removed, Object.assign(Object.assign({}, options), { ui: true }));
}
if (added.length) {
this.select(added, Object.assign(Object.assign({}, options), { ui: true }));
}
this.updateContainer();
this.batchUpdating = false;
return this;
}
return this.clean(options);
}
clean(options = {}) {
if (this.length) {
this.unselect(this.cells, options);
}
// 清理容器 transform 与位移累计
this.resetContainerPosition();
this.draggingPreviewMode = 'translate';
return this;
}
setFilter(filter) {
this.options.filter = filter;
}
setContent(content) {
this.options.content = content;
}
startSelecting(evt) {
// Flow: startSelecting => adjustSelection => stopSelecting
evt = this.normalizeEvent(evt); // eslint-disable-line
this.clean();
let x;
let y;
const graphContainer = this.graph.container;
if (evt.offsetX != null &&
evt.offsetY != null &&
graphContainer.contains(evt.target)) {
x = evt.offsetX;
y = evt.offsetY;
}
else {
const offset = Dom.offset(graphContainer);
const scrollLeft = graphContainer.scrollLeft;
const scrollTop = graphContainer.scrollTop;
x = evt.clientX - offset.left + window.pageXOffset + scrollLeft;
y = evt.clientY - offset.top + window.pageYOffset + scrollTop;
}
Dom.css(this.container, {
top: y,
left: x,
width: 1,
height: 1,
});
this.setEventData(evt, {
action: 'selecting',
clientX: evt.clientX,
clientY: evt.clientY,
offsetX: x,
offsetY: y,
scrollerX: 0,
scrollerY: 0,
moving: false,
});
const client = this.graph.snapToGrid(evt.clientX, evt.clientY);
this.notifyBoxEvent('box:mousedown', evt, client.x, client.y, []);
this.delegateDocumentEvents(documentEvents, evt.data);
}
filter(cells) {
const filter = this.options.filter;
return cells.filter((cell) => {
if (Array.isArray(filter)) {
return filter.some((item) => {
if (typeof item === 'string') {
return cell.shape === item;
}
return cell.id === item.id;
});
}
if (typeof filter === 'function') {
return FunctionExt.call(filter, this.graph, cell);
}
return true;
});
}
stopSelecting(evt) {
// 重置拖拽状态和清理定时器
this.isDragging = false;
this.boxesUpdated = false;
if (this.updateThrottleTimer) {
clearTimeout(this.updateThrottleTimer);
this.updateThrottleTimer = null;
}
const graph = this.graph;
const eventData = this.getEventData(evt);
const action = eventData.action;
switch (action) {
case 'selecting': {
const client = graph.snapToGrid(evt.clientX, evt.clientY);
const rect = this.getSelectingRect();
const cells = this.getCellsInArea(rect);
this.reset(cells, { batch: true });
this.hideRubberband();
this.notifyBoxEvent('box:mouseup', evt, client.x, client.y, cells);
break;
}
case 'translating': {
const client = graph.snapToGrid(evt.clientX, evt.clientY);
if (this.dragPendingOffset) {
const toApply = this.dragPendingOffset;
this.dragPendingOffset = null;
this.applyDraggingPreview(toApply);
}
if (this.dragRafId != null) {
cancelAnimationFrame(this.dragRafId);
this.dragRafId = null;
}
// 重置容器 transform 与累计偏移
this.resetContainerPosition();
if (this.movingRouterRestoreTimer) {
clearTimeout(this.movingRouterRestoreTimer);
this.movingRouterRestoreTimer = null;
}
this.restoreMovingRouters();
this.graph.model.stopBatch('move-selection');
// 清理本次拖拽缓存
this.translatingCache = null;
this.draggingPreviewMode = 'translate';
this.notifyBoxEvent('box:mouseup', evt, client.x, client.y);
this.repositionSelectionBoxesInPlace();
break;
}
default: {
this.clean();
break;
}
}
this.undelegateDocumentEvents();
}
onMouseUp(evt) {
const e = this.normalizeEvent(evt);
const eventData = this.getEventData(e);
if (eventData) {
this.stopSelecting(evt);
}
}
onSelectionBoxMouseDown(evt) {
this.handleSelectionMouseDown(evt, true);
}
onSelectionContainerMouseDown(evt) {
this.handleSelectionMouseDown(evt, false);
}
handleSelectionMouseDown(evt, isBox) {
var _a;
evt.stopPropagation();
(_a = evt.preventDefault) === null || _a === void 0 ? void 0 : _a.call(evt);
const e = this.normalizeEvent(evt);
const client = this.graph.snapToGrid(e.clientX, e.clientY);
// 容器内的多选切换:按下修饰键时,不拖拽,直接切换选中状态
if (!isBox &&
isModifierKeyMatch(e, this.options.multipleSelectionModifiers)) {
const viewsUnderPoint = this.graph.findViewsFromPoint(client.x, client.y);
const nodeView = viewsUnderPoint.find((v) => v.isNodeView());
if (nodeView) {
const cell = nodeView.cell;
if (this.isSelected(cell)) {
this.unselect(cell, { ui: true });
}
else {
if (this.options.multiple === false) {
this.reset(cell, { ui: true });
}
else {
this.select(cell, { ui: true });
}
}
}
return;
}
if (this.options.movable) {
this.startTranslating(e);
}
let activeView = isBox ? this.getCellViewFromElem(e.target) : null;
if (!activeView) {
const viewsUnderPoint = this.graph
.findViewsFromPoint(client.x, client.y)
.filter((view) => this.isSelected(view.cell));
activeView = viewsUnderPoint[0] || null;
if (!activeView) {
const firstSelected = this.collection.first();
if (firstSelected) {
activeView = this.graph.renderer.findViewByCell(firstSelected);
}
}
}
if (activeView) {
this.setEventData(e, { activeView });
if (isBox) {
this.notifyBoxEvent('box:mousedown', e, client.x, client.y);
}
this.delegateDocumentEvents(documentEvents, e.data);
}
}
startTranslating(evt) {
this.graph.model.startBatch('move-selection');
const client = this.graph.snapToGrid(evt.clientX, evt.clientY);
this.setEventData(evt, {
action: 'translating',
clientX: client.x,
clientY: client.y,
originX: client.x,
originY: client.y,
});
this.prepareTranslatingCache();
this.draggingPreviewMode = this.getDraggingPreviewMode();
}
getRestrictArea() {
const restrict = this.graph.options.translating.restrict;
const area = typeof restrict === 'function'
? FunctionExt.call(restrict, this.graph, null)
: restrict;
if (typeof area === 'number') {
return this.graph.transform.getGraphArea().inflate(area);
}
if (area === true) {
return this.graph.transform.getGraphArea();
}
return area || null;
}
// 根据当前选择的节点构建拖拽缓存
prepareTranslatingCache() {
const selectedNodes = this.collection
.toArray()
.filter((cell) => cell.isNode());
const nodeIdSet = new Set(selectedNodes.map((n) => n.id));
const selectedEdges = this.collection
.toArray()
.filter((cell) => cell.isEdge());
const edgesToTranslateSet = new Set();
const needsTranslate = (edge) => edge.getVertices().length > 0 ||
!edge.getSourceCellId() ||
!edge.getTargetCellId();
// 邻接边:仅当需要位移(有顶点或点端点)时加入缓存
this.graph.model.getEdges().forEach((edge) => {
const srcId = edge.getSourceCellId();
const tgtId = edge.getTargetCellId();
const isConnectedToSelectedNode = (srcId != null && nodeIdSet.has(srcId)) ||
(tgtId != null && nodeIdSet.has(tgtId));
if (isConnectedToSelectedNode && needsTranslate(edge)) {
edgesToTranslateSet.add(edge);
}
});
// 选中的边(不一定与选中节点相邻)也需要考虑
selectedEdges.forEach((edge) => {
if (needsTranslate(edge)) {
edgesToTranslateSet.add(edge);
}
});
this.translatingCache = {
selectedNodes,
nodeIdSet,
edgesToTranslate: Array.from(edgesToTranslateSet),
};
}
/**
* 在移动过程中对与当前选中节点相连的边进行临时路由降级
*/
applyMovingRouterFallback() {
var _a;
if (this.movingRouterRestoreCache)
return;
const selectedNodes = (_a = this.translatingCache) === null || _a === void 0 ? void 0 : _a.selectedNodes;
if (!selectedNodes || selectedNodes.length < 2)
return;
const fallbackRaw = this.options.movingRouterFallback;
if (!fallbackRaw || !routerRegistry.exist(fallbackRaw))
return;
const fallback = { name: fallbackRaw };
const restore = {};
const processedEdges = new Set();
selectedNodes.forEach((node) => {
this.graph.model.getConnectedEdges(node).forEach((edge) => {
if (processedEdges.has(edge.id)) {
return;
}
processedEdges.add(edge.id);
const current = edge.getRouter();
restore[edge.id] = current;
edge.setRouter(fallback, { silent: true });
});
});
this.movingRouterRestoreCache = restore;
this.movingDegradeActivatedTs = Date.now();
}
/**
* 恢复移动过程中被降级的边的原始路由:
* - 如果原始路由为空则移除路由设置
* - 完成恢复后清空缓存,等待下一次移动重新降级
*/
restoreMovingRouters() {
const restore = this.movingRouterRestoreCache;
if (!restore)
return;
Object.keys(restore).forEach((id) => {
const edge = this.graph.getCellById(id);
if (!edge || !edge.isEdge())
return;
const original = restore[id];
if (original == null) {
edge.removeRouter({ silent: true });
}
else {
edge.setRouter(original, { silent: true });
}
const view = this.graph.findViewByCell(edge);
if (view) {
this.graph.renderer.requestViewUpdate(view, view.getFlag('update'), {
async: true,
});
}
});
this.movingRouterRestoreCache = null;
}
/**
* 在移动停止后延迟恢复路由,避免连线抖动:
* - `idle`:距离上次移动的空闲时间必须超过 100ms
* - `hold`:降级保持时间必须超过 150ms
* - 若条件未满足则按最小等待时间再次调度恢复
*/
scheduleMovingRouterRestoreThrottle() {
if (this.movingRouterRestoreTimer) {
clearTimeout(this.movingRouterRestoreTimer);
this.movingRouterRestoreTimer = null;
}
this.movingRouterRestoreTimer = setTimeout(() => {
const now = Date.now();
const lastMove = this.lastMovingTs || 0;
const idle = now - lastMove;
const hold = this.movingDegradeActivatedTs != null
? now - this.movingDegradeActivatedTs
: Infinity;
if (idle < SelectionImpl.RESTORE_IDLE_TIME ||
hold < SelectionImpl.RESTORE_HOLD_TIME) {
const wait = Math.max(SelectionImpl.RESTORE_IDLE_TIME - idle, SelectionImpl.RESTORE_HOLD_TIME - hold, SelectionImpl.MIN_RESTORE_WAIT_TIME);
this.movingRouterRestoreTimer = setTimeout(() => {
this.movingRouterRestoreTimer = null;
this.restoreMovingRouters();
}, wait);
return;
}
this.movingRouterRestoreTimer = null;
this.restoreMovingRouters();
}, SelectionImpl.RESTORE_IDLE_TIME);
}
getSelectionOffset(client, data) {
let dx = client.x - data.clientX;
let dy = client.y - data.clientY;
const restrict = this.getRestrictArea();
if (restrict) {
const cells = this.collection.toArray();
const totalBBox = Cell.getCellsBBox(cells, { deep: true }) || Rectangle.create();
const minDx = restrict.x - totalBBox.x;
const minDy = restrict.y - totalBBox.y;
const maxDx = restrict.x + restrict.width - (totalBBox.x + totalBBox.width);
const maxDy = restrict.y + restrict.height - (totalBBox.y + totalBBox.height);
if (dx < minDx) {
dx = minDx;
}
if (dy < minDy) {
dy = minDy;
}
if (maxDx < dx) {
dx = maxDx;
}
if (maxDy < dy) {
dy = maxDy;
}
if (!this.options.following) {
const offsetX = client.x - data.originX;
const offsetY = client.y - data.originY;
dx = offsetX <= minDx || offsetX >= maxDx ? 0 : dx;
dy = offsetY <= minDy || offsetY >= maxDy ? 0 : dy;
}
}
return {
dx,
dy,
};
}
updateSelectedNodesPosition(offset) {
if (offset.dx === 0 && offset.dy === 0) {
return;
}
// 合并偏移并在下一帧统一应用,减少高频重绘
if (this.dragPendingOffset) {
this.dragPendingOffset.dx += offset.dx;
this.dragPendingOffset.dy += offset.dy;
}
else {
this.dragPendingOffset = { dx: offset.dx, dy: offset.dy };
}
if (this.dragRafId == null) {
this.dragRafId = requestAnimationFrame(() => {
const toApply = this.dragPendingOffset || { dx: 0, dy: 0 };
this.dragPendingOffset = null;
this.dragRafId = null;
this.applyDraggingPreview(toApply);
this.boxesUpdated = true;
this.isDragging = true;
});
}
}
autoScrollGraph(x, y) {
const scroller = this.graph.getPlugin('scroller');
if (scroller === null || scroller === void 0 ? void 0 : scroller.autoScroll) {
return scroller.autoScroll(x, y);
}
return { scrollerX: 0, scrollerY: 0 };
}
adjustSelection(evt) {
const e = this.normalizeEvent(evt);
const eventData = this.getEventData(e);
const action = eventData.action;
switch (action) {
case 'selecting': {
const data = eventData;
if (data.moving !== true) {
Dom.appendTo(this.container, this.graph.container);
this.showRubberband();
data.moving = true;
}
const { scrollerX, scrollerY } = this.autoScrollGraph(e.clientX, e.clientY);
data.scrollerX += scrollerX;
data.scrollerY += scrollerY;
const dx = e.clientX - data.clientX + data.scrollerX;
const dy = e.clientY - data.clientY + data.scrollerY;
Dom.css(this.container, {
left: dx < 0 ? data.offsetX + dx : data.offsetX,
top: dy < 0 ? data.offsetY + dy : data.offsetY,
width: Math.abs(dx),
height: Math.abs(dy),
});
const client = this.graph.snapToGrid(e.clientX, e.clientY);
const rect = this.getSelectingRect();
const cells = this.getCellsInArea(rect);
this.notifyBoxEvent('box:mousemove', evt, client.x, client.y, cells);
break;
}
case 'translating': {
this.isDragging = true;
const client = this.graph.snapToGrid(e.clientX, e.clientY);
const data = eventData;
const offset = this.getSelectionOffset(client, data);
if (this.options.following) {
this.updateSelectedNodesPosition(offset);
}
else {
this.updateContainerPosition(offset);
}
if (offset.dx) {
data.clientX = client.x;
}
if (offset.dy) {
data.clientY = client.y;
}
if (offset.dx !== 0 || offset.dy !== 0) {
this.lastMovingTs = Date.now();
this.applyMovingRouterFallback();
this.scheduleMovingRouterRestoreThrottle();
}
this.notifyBoxEvent('box:mousemove', evt, client.x, client.y);
break;
}
default:
break;
}
this.boxesUpdated = false;
}
translateSelectedNodes(dx, dy, exclude, otherOptions) {
var _a, _b;
const map = {};
const excluded = [];
if (exclude) {
map[exclude.id] = true;
}
this.collection.toArray().forEach((cell) => {
cell.getDescendants({ deep: true }).forEach((child) => {
map[child.id] = true;
});
});
if (otherOptions === null || otherOptions === void 0 ? void 0 : otherOptions.translateBy) {
const currentCell = this.graph.getCellById(otherOptions.translateBy);
if (currentCell) {
map[currentCell.id] = true;
currentCell.getDescendants({ deep: true }).forEach((child) => {
map[child.id] = true;
});
excluded.push(currentCell);
}
}
const options = Object.assign(Object.assign({}, otherOptions), { selection: this.cid, exclude: excluded });
// 移动选中的节点,避免重复和嵌套
const cachedSelectedNodes = (_a = this.translatingCache) === null || _a === void 0 ? void 0 : _a.selectedNodes;
const selectedNodes = (cachedSelectedNodes !== null && cachedSelectedNodes !== void 0 ? cachedSelectedNodes : this.collection.toArray().filter((cell) => cell.isNode())).filter((node) => !map[node.id]);
selectedNodes.forEach((node) => {
node.translate(dx, dy, options);
});
// 边移动缓存:仅移动需要位移的边(有顶点或点端点)
const cachedEdges = (_b = this.translatingCache) === null || _b === void 0 ? void 0 : _b.edgesToTranslate;
const edgesToTranslate = new Set();
if (cachedEdges) {
cachedEdges.forEach((edge) => {
edgesToTranslate.add(edge);
});
}
else {
const selectedNodeIdSet = new Set(selectedNodes.map((n) => n.id));
this.graph.model.getEdges().forEach((edge) => {
const srcId = edge.getSourceCellId();
const tgtId = edge.getTargetCellId();
const srcSelected = srcId ? selectedNodeIdSet.has(srcId) : false;
const tgtSelected = tgtId ? selectedNodeIdSet.has(tgtId) : false;
if (srcSelected || tgtSelected) {
const hasVertices = edge.getVertices().length > 0;
const pointEndpoint = !srcId || !tgtId;
if (hasVertices || pointEndpoint) {
edgesToTranslate.add(edge);
}
}
});
}
// 若选择了边(仅边、无节点),确保其也被移动(过滤无顶点且两端为节点的情况)
const selectedEdges = this.collection
.toArray()
.filter((cell) => cell.isEdge() && !map[cell.id]);
selectedEdges.forEach((edge) => {
const hasVertices = edge.getVertices().length > 0;
const pointEndpoint = !edge.getSourceCellId() || !edge.getTargetCellId();
if (hasVertices || pointEndpoint) {
edgesToTranslate.add(edge);
}
});
edgesToTranslate.forEach((edge) => {
edge.translate(dx, dy, options);
});
}
getCellViewsInArea(rect) {
const graph = this.graph;
const options = {
strict: this.options.strict,
};
let views = [];
if (this.options.rubberNode) {
views = views.concat(graph.model
.getNodesInArea(rect, options)
.map((node) => graph.renderer.findViewByCell(node))
.filter((view) => view != null));
}
if (this.options.rubberEdge) {
views = views.concat(graph.model
.getEdgesInArea(rect, options)
.map((edge) => graph.renderer.findViewByCell(edge))
.filter((view) => view != null));
}
return views;
}
getCellsInArea(rect) {
return this.filter(this.getCellViewsInArea(rect).map((view) => view.cell));
}
getSelectingRect() {
let width = Dom.width(this.container);
let height = Dom.height(this.container);
const offset = Dom.offset(this.container);
const origin = this.graph.pageToLocal(offset.left, offset.top);
const scale = this.graph.transform.getScale();
width /= scale.sx;
height /= scale.sy;
return new Rectangle(origin.x, origin.y, width, height);
}
getBoxEventCells(cells, activeView = null) {
var _a;
const nodes = [];
const edges = [];
let view = activeView;
(cells !== null && cells !== void 0 ? cells : this.cells).forEach((cell) => {
const current = this.graph.getCellById(cell.id);
if (!current) {
return;
}
if (!view) {
view = this.graph.renderer.findViewByCell(current);
}
if (current.isNode()) {
nodes.push(current);
}
else if (current.isEdge()) {
edges.push(current);
}
});
return {
view,
cell: (_a = view === null || view === void 0 ? void 0 : view.cell) !== null && _a !== void 0 ? _a : null,
nodes,
edges,
};
}
notifyBoxEvent(name, e, x, y, cells) {
var _a, _b;
const activeView = (_b = (_a = this.getEventData(e)) === null || _a === void 0 ? void 0 : _a.activeView) !== null && _b !== void 0 ? _b : null;
const { view, cell, nodes, edges } = this.getBoxEventCells(cells, activeView);
this.trigger(name, { e, view, x, y, cell, nodes, edges });
}
getSelectedClassName(cell) {
return this.prefixClassName(`${cell.isNode() ? 'node' : 'edge'}-selected`);
}
addCellSelectedClassName(cell) {
const view = this.graph.renderer.findViewByCell(cell);
if (view) {
view.addClass(this.getSelectedClassName(cell));
}
}
removeCellUnSelectedClassName(cell) {
const view = this.graph.renderer.findViewByCell(cell);
if (view) {
view.removeClass(this.getSelectedClassName(cell));
}
}
destroySelectionBox(cell) {
this.removeCellUnSelectedClassName(cell);
if (this.canShowSelectionBox(cell)) {
Dom.remove(this.container.querySelector(`[data-cell-id="${cell.id}"]`));
if (this.$boxes.length === 0) {
this.hide();
}
this.boxCount = Math.max(0, this.boxCount - 1);
}
}
destroyAllSelectionBoxes(cells) {
cells.forEach((cell) => {
this.removeCellUnSelectedClassName(cell);
});
this.hide();
Dom.remove(this.$boxes);
this.boxCount = 0;
}
hide() {
Dom.removeClass(this.container, this.prefixClassName(classNames.rubberband));
Dom.removeClass(this.container, this.prefixClassName(classNames.selected));
}
showRubberband() {
Dom.addClass(this.container, this.prefixClassName(classNames.rubberband));
}
hideRubberband() {
Dom.removeClass(this.container, this.prefixClassName(classNames.rubberband));
}
showSelected() {
Dom.removeAttribute(this.container, 'style');
Dom.addClass(this.container, this.prefixClassName(classNames.selected));
}
createContainer() {
this.container = document.createElement('div');
Dom.addClass(this.container, this.prefixClassName(classNames.root));
if (this.options.className) {
Dom.addClass(this.container, this.options.className);
}
Dom.css(this.container, {
willChange: 'transform',
});
this.selectionContainer = document.createElement('div');
Dom.addClass(this.selectionContainer, this.prefixClassName(classNames.inner));
this.selectionContent = document.createElement('div');
Dom.addClass(this.selectionContent, this.prefixClassName(classNames.content));
Dom.append(this.selectionContainer, this.selectionContent);
Dom.attr(this.selectionContainer, 'data-selection-length', this.collection.length);
Dom.prepend(this.container, this.selectionContainer);
}
getDraggingPreviewMode() {
if (!this.options.following) {
return 'translate';
}
const hasVisibleEdgeSelectionBox = this.collection
.toArray()
.some((cell) => cell.isEdge() && this.canShowSelectionBox(cell));
return hasVisibleEdgeSelectionBox ? 'geometry' : 'translate';
}
applyDraggingPreview(offset) {
if (offset.dx === 0 && offset.dy === 0) {
return;
}
if (this.options.following) {
this.translateSelectedNodes(offset.dx, offset.dy);
if (this.draggingPreviewMode === 'geometry') {
this.repositionSelectionBoxesInPlace();
this.resetContainerPosition();
return;
}
}
this.updateContainerPosition(offset);
}
resetContainerPosition() {
this.containerLocalOffsetX = 0;
this.containerLocalOffsetY = 0;
this.containerOffsetX = 0;
this.containerOffsetY = 0;
Dom.css(this.container, 'transform', '');
}
syncContainerPosition() {
const origin = this.graph.coord.localToGraphPoint(0, 0);
const offset = this.graph.coord.localToGraphPoint(this.containerLocalOffsetX, this.containerLocalOffsetY);
this.containerOffsetX = offset.x - origin.x;
this.containerOffsetY = offset.y - origin.y;
if (this.containerOffsetX === 0 && this.containerOffsetY === 0) {
Dom.css(this.container, 'transform', '');
return;
}
Dom.css(this.container, 'transform', `translate3d(${this.containerOffsetX}px, ${this.containerOffsetY}px, 0)`);
}
updateContainerPosition(offset) {
if (offset.dx || offset.dy) {
this.containerLocalOffsetX += offset.dx;
this.containerLocalOffsetY += offset.dy;
// 使用 transform,避免频繁修改 left/top
this.syncContainerPosition();
}
}
updateContainer() {
const origin = { x: Infinity, y: Infinity };
const corner = { x: 0, y: 0 };
const cells = this.collection
.toArray()
.filter((cell) => this.canShowSelectionBox(cell));
cells.forEach((cell) => {
const view = this.graph.renderer.findViewByCell(cell);
if (view) {
const bbox = view.getBBox({
useCellGeometry: true,
});
origin.x = Math.min(origin.x, bbox.x);
origin.y = Math.min(origin.y, bbox.y);
corner.x = Math.max(corner.x, bbox.x + bbox.width);
corner.y = Math.max(corner.y, bbox.y + bbox.height);
}
});
Dom.css(this.selectionContainer, {
position: 'absolute',
pointerEvents: this.options.movable ? 'auto' : 'none',
cursor: this.options.movable ? 'move' : 'default',
left: origin.x,
top: origin.y,
width: corner.x - origin.x,
height: corner.y - origin.y,
});
Dom.attr(this.selectionContainer, 'data-selection-length', this.collection.length);
const boxContent = this.options.content;
if (boxContent) {
if (typeof boxContent === 'function') {
const content = FunctionExt.call(boxContent, this.graph, this, this.selectionContent);
if (content) {
this.selectionContent.innerHTML = content;
}
}
else {
this.selectionContent.innerHTML = boxContent;
}
}
if (this.collection.length > 0 && !this.container.parentNode) {
Dom.appendTo(this.container, this.graph.container);
}
else if (this.collection.length <= 0 && this.container.parentNode) {
this.container.parentNode.removeChild(this.container);
}
}
canShowSelectionBox(cell) {
return ((cell.isNode() && this.options.showNodeSelectionBox === true) ||
(cell.isEdge() && this.options.showEdgeSelectionBox === true));
}
getPointerEventsValue(pointerEvents) {
return typeof pointerEvents === 'string'
? pointerEvents
: pointerEvents(this.cells);
}
createSelectionBox(cell) {
this.addCellSelectedClassName(cell);
if (this.canShowSelectionBox(cell)) {
const view = this.graph.renderer.findViewByCell(cell);
if (view) {
const bbox = view.getBBox({
useCellGeometry: true,
});
const className = this.boxClassName;
const box = document.createElement('div');
const pointerEvents = this.options.pointerEvents;
Dom.addClass(box, className);
Dom.addClass(box, `${className}-${cell.isNode() ? 'node' : 'edge'}`);
Dom.attr(box, 'data-cell-id', cell.id);
Dom.css(box, {
position: 'absolute',
left: bbox.x,
top: bbox.y,
width: bbox.width,
height: bbox.height,
pointerEvents: pointerEvents
? this.getPointerEventsValue(pointerEvents)
: 'auto',
});
Dom.appendTo(box, this.container);
this.showSelected();
this.boxCount += 1;
}
}
}
updateSelectionBoxes() {
if (this.collection.length > 0) {
if (this.isDragging) {
return;
}
if (this.updateThrottleTimer) {
clearTimeout(this.updateThrottleTimer);
}
// 节流:限制更新频率到60fps
this.updateThrottleTimer = setTimeout(() => {
this.refreshSelectionBoxes();
this.updateThrottleTimer = null;
}, 16);
}
}
refreshSelectionBoxes() {
Dom.remove(this.$boxes);
this.boxCount = 0;
this.collection.toArray().forEach((cell) => {
this.createSelectionBox(cell);
});
this.updateContainer();
this.boxesUpdated = true;
}
// 按当前视图几何同步每个选择框的位置与尺寸
repositionSelectionBoxesInPlace() {
const boxes = this.$boxes;
if (boxes.length === 0) {
this.refreshSelectionBoxes();
return;
}
for (const elem of boxes) {
const id = elem.getAttribute('data-cell-id');
if (!id)
continue;
const cell = this.collection.get(id);
if (!cell)
continue;
const view = this.graph.renderer.findViewByCell(cell);
if (!view)
continue;
const bbox = view.getBBox({ useCellGeometry: true });
Dom.css(elem, {
left: bbox.x,
top: bbox.y,
width: bbox.width,
height: bbox.height,
});
}
this.updateContainer();
this.boxesUpdated = true;
}
getCellViewFromElem(elem) {
const id = elem.getAttribute('data-cell-id');
if (id) {
const cell = this.collection.get(id);
if (cell) {
return this.graph.renderer.findViewByCell(cell);
}
}
return null;
}
onCellRemoved({ cell }) {
this.destroySelectionBox(cell);
if (!this.batchUpdating)
this.updateContainer();
}
onReseted({ previous, current }) {
this.destroyAllSelectionBoxes(previous);
current.forEach((cell) => {
this.listenCellRemoveEvent(cell);
this.createSelectionBox(cell);
});
this.updateContainer();
}
onCellAdded({ cell }) {
// The collection do not known the cell was removed when cell was
// removed by interaction(such as, by "delete" shortcut), so we should
// manually listen to cell's remove event.
this.listenCellRemoveEvent(cell);
this.createSelectionBox(cell);
if (!this.batchUpdating)
this.updateContainer();
}
listenCellRemoveEvent(cell) {
cell.off('removed', this.onCellRemoved, this);
cell.on('removed', this.onCellRemoved, this);
}
onCollectionUpdated({ added, removed, options, }) {
added.forEach((cell) => {
this.trigger('cell:selected', { cell, options });
if (cell.isNode()) {
this.trigger('node:selected', { cell, options, node: cell });
}
else if (cell.isEdge()) {
this.trigger('edge:selected', { cell, options, edge: cell });
}
});
removed.forEach((cell) => {
this.trigger('cell:unselected', { cell, options });
if (cell.isNode()) {
this.trigger('node:unselected', { cell, options, node: cell });
}
else if (cell.isEdge()) {
this.trigger('edge:unselected', { cell, options, edge: cell });
}
});
const args = {
added,
removed,
options,
selected: this.cells.filter((cell) => !!this.graph.getCellById(cell.id)),
};
this.trigger('selection:changed', args);
}
// #endregion
dispose() {
this.clean();
this.remove();
this.off();
}
}
SelectionImpl.RESTORE_IDLE_TIME = 100;
SelectionImpl.RESTORE_HOLD_TIME = 150;
SelectionImpl.MIN_RESTORE_WAIT_TIME = 50;
__decorate([
disposable()
], SelectionImpl.prototype, "dispose", null);
// private
// -------
const baseClassName = 'widget-selection';
export const classNames = {
root: baseClassName,
inner: `${baseClassName}-inner`,
box: `${baseClassName}-box`,
content: `${baseClassName}-content`,
rubberband: `${baseClassName}-rubberband`,
selected: `${baseClassName}-selected`,
};
export const documentEvents = {
mousemove: 'adjustSelection',
touchmove: 'adjustSelection',
mouseup: 'onMouseUp',
touchend: 'onMouseUp',
touchcancel: 'onMouseUp',
};
export function depthComparator(cell) {
return cell.getAncestors().length;
}
//# sourceMappingURL=selection.js.map