butterfly-dag
Version:
一个基于数据驱动的节点式编排组件库,让你有方便快捷定制可视化流程图表
1,365 lines (1,273 loc) • 149 kB
JavaScript
'use strict';
const $ = require('jquery');
const _ = require('lodash');
const domtoimage = require('dom-to-image');
import Canvas from "../interface/canvas";
import Node from '../node/baseNode';
import Edge from '../edge/baseEdge';
import Group from '../group/baseGroup';
import Endpoint from '../endpoint/baseEndpoint';
import Layout from '../utils/layout/layout';
import SelectCanvas from '../utils/selectCanvas';
// 画布和屏幕坐标地换算
import CoordinateService from '../utils/coordinate';
// scope的比较
import ScopeCompare from '../utils/scopeCompare';
// 网格模式
import GridService from '../utils/gridService';
// 辅助线模式
import GuidelineService from '../utils/guidelineService';
// 小地图模式
import Minimap from '../utils/minimap';
// 线段动画
import LinkAnimateUtil from '../utils/link/link_animate';
import './baseCanvas.less';
class BaseCanvas extends Canvas {
constructor(options) {
super(options);
this.root = options.root;
this.layout = options.layout; // layout部分也需要重新review
this.zoomable = options.zoomable || false; // 可缩放
this.moveable = options.moveable || false; // 可平移
this.draggable = options.draggable || false; // 可拖动
this.linkable = options.linkable || false; // 可连线
this.disLinkable = options.disLinkable || false; // 可拆线
this.theme = {
group: {
type: _.get(options, 'theme.group.type') || 'normal',
includeGroups: _.get(options, 'theme.group.includeGroups', false)
},
edge: {
type: _.get(options, 'theme.edge.type') || 'node',
shapeType: _.get(options, 'theme.edge.shapeType') || 'Straight',
Class: _.get(options, 'theme.edge.Class') || Edge,
arrow: _.get(options, 'theme.edge.arrow'),
arrowShapeType: _.get(options, 'theme.edge.arrowShapeType', 'default'),
arrowPosition: _.get(options, 'theme.edge.arrowPosition'),
arrowOffset: _.get(options, 'theme.edge.arrowOffset'),
draggable: _.get(options, 'theme.edge.draggable'),
label: _.get(options, 'theme.edge.label'),
labelPosition: _.get(options, 'theme.edge.labelPosition'),
labelOffset: _.get(options, 'theme.edge.labelOffset'),
isRepeat: _.get(options, 'theme.edge.isRepeat') || false,
isLinkMyself: _.get(options, 'theme.edge.isLinkMyself') || false,
isExpandWidth: _.get(options, 'theme.edge.isExpandWidth') || false,
defaultAnimate: _.get(options, 'theme.edge.defaultAnimate') || false,
},
endpoint: {
// 暂时不支持position
// position: _.get(options, 'theme.endpoint.position'),
linkableHighlight: _.get(options, 'theme.endpoint.linkableHighlight') || false,
limitNum: _.get(options, 'theme.endpoint.limitNum'),
expandArea: {
left: _.get(options, 'theme.endpoint.expandArea.left') === undefined ? 10 : _.get(options, 'theme.endpoint.expandArea.left'),
right: _.get(options, 'theme.endpoint.expandArea.right') === undefined ? 10 : _.get(options, 'theme.endpoint.expandArea.right'),
top: _.get(options, 'theme.endpoint.expandArea.top') === undefined ? 10 : _.get(options, 'theme.endpoint.expandArea.top'),
bottom: _.get(options, 'theme.endpoint.expandArea.bottom') === undefined ? 10 : _.get(options, 'theme.endpoint.expandArea.bottom'),
},
},
zoomGap: _.get(options, 'theme.zoomGap') || 0.001,
// 鼠标到达边缘画布自动移动
autoFixCanvas: {
enable: _.get(options, 'theme.autoFixCanvas.enable', false),
autoMovePadding: _.get(options, 'theme.autoFixCanvas.autoMovePadding') || [20, 20, 20, 20] // 上,右,下,左
},
// 自动适配父级div大小
autoResizeRootSize: _.get(options, 'theme.autoResizeRootSize', true)
};
// 贯穿所有对象的配置
this.global = _.get(options, 'global', {
isScopeStrict: _.get(options, 'global.isScopeStrict'), // 是否为scope的严格模式
limitQueueLen: 5, // 默认操作队列只有5步
isCloneDeep: _.get(options, 'global.isCloneDeep', true), // addNode,addEdge,addGroup传入的数据是否深拷贝一份
});
// 放大缩小和平移的数值
this._zoomData = 1;
this._moveData = [0, 0];
this._zoomTimer = null;
this.groups = [];
this.nodes = [];
this.edges = [];
// 框选模式,需要重新考虑(默认单选)
this.isSelectMode = false;
this.selecContents = [];
this.selecMode = 'include';
this.selectItem = {
nodes: [],
edges: [],
groups: [],
endpoints: []
};
// 框选前需要纪录状态
this._remarkZoom = undefined;
this._remarkMove = undefined;
this.svg = null;
this.wrapper = null;
this.canvasWrapper = null;
// 加一层wrapper方便处理缩放,平移
this._genWrapper();
// 加一层svg画线条
this._genSvgWrapper();
// 加一层canvas方便处理辅助
this._genCanvasWrapper();
// 动画初始化
LinkAnimateUtil.init(this.svg);
// 统一处理画布拖动事件
this._dragType = null;
this._dragNode = null;
this._dragEndpoint = null;
this._dragEdges = []; // 拖动连线的edge
this._dragPathEdge = null; // 拖动edge中的某段path改变路径
this._dragGroup = null;
// 初始化一些参数
this._rootWidth = $(this.root).width();
this._rootHeight = $(this.root).height();
$(this.root).css('overflow', 'hidden');
if($(this.root).css('position') === 'static') {
$(this.root).css('position', 'relative');
}
// 节点,线段,节点组z-index值,顺序:节点 > 线段 > 节点组
this._dragGroupZIndex = 50;
this._dragNodeZIndex = 250;
this._dragEdgeZindex = 499;
this._isInitEdgeZIndex = false;
// 检测节点拖动节点组的hover状态
this._hoverGroupQueue = [];
this._hoverGroupObj = undefined;
this._hoverGroupTimer = undefined;
// 网格布局
this._gridService = new GridService({
root: this.root,
canvas: this
});
// 辅助线
this._guidelineService = new GuidelineService({
root: this.root,
canvas: this
});
this._bgObjQueue = [];
this._bgObj = undefined;
this._bgTimer = undefined;
// 坐标转换服务
this._coordinateService = new CoordinateService({
canvas: this,
terOffsetX: $(this.root).offset().left,
terOffsetY: $(this.root).offset().top,
terWidth: $(this.root).width(),
terHeight: $(this.root).height(),
canOffsetX: this._moveData[0],
canOffsetY: this._moveData[1],
scale: this._zoomData
});
this._addEventListener();
this._unionData = {
__system: {
nodes: [],
edges: [],
groups: [],
endpoints: []
}
};
this._NodeClass = Node;
// undo & redo队列
this.actionQueue = [];
this.actionQueueIndex = -1;
// 画布边缘
this._autoMoveDir = [];
this._autoMoveTimer = null;
// 画布是否初始化成功
this._hasInited = false;
// 画布节点大小缓存更新
this._updateInterval = setInterval(() => {
this.nodes.forEach((item) => {
item._isForceUpdateSize = true;
});
}, 5000);
this._cache = {
nodes: {}
}
}
//===============================
//[ 画布渲染 ]
//===============================
draw(opts, callback) {
const groups = opts.groups || [];
const nodes = opts.nodes || [];
const edges = opts.edges || [];
// 自动布局需要重新review
if (this.layout) {
this._autoLayout({
groups,
nodes,
edges
});
}
let drawPromise = new Promise((resolve, reject) => {
setTimeout(() => {
// 生成groups
this.addGroups(groups);
resolve();
});
}).then(() => {
return new Promise((resolve, reject) => {
setTimeout(() => {
// 生成nodes
this.addNodes(nodes);
resolve();
}, 10);
});
}).then((resolve) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
// 生成edges
this.addEdges(edges);
resolve();
}, 20);
});
});
drawPromise.then(() => {
this.actionQueue = [];
this.actionQueueIndex = -1;
callback && callback({
nodes: this.nodes,
edges: this.edges,
groups: this.groups
});
this._hasInited = true;
});
}
redraw (opts, callback) {
this.removeNodes(this.nodes.map((item) => item.id) || []);
this.removeGroups(this.groups.map((item) => item.id) || []);
this.clearActionQueue();
this.draw(opts || {}, callback);
}
getDataMap() {
return {
nodes: this.nodes,
edges: this.edges,
groups: this.groups
};
}
_genSvgWrapper() {
function _detectMob() {
const toMatch = [
/Android/i,
/webOS/i,
/iPhone/i,
/iPad/i,
/iPod/i,
/BlackBerry/i,
/Windows Phone/i
];
return toMatch.some((toMatchItem) => {
return window.navigator.userAgent.match(toMatchItem);
});
}
let _isMobi = _detectMob();
let _SVGWidth = '100%';
let _SVGHeight = '100%';
let _detectZoom = () => {
let ratio = 0;
let screen = window.screen;
let ua = window.navigator.userAgent.toLowerCase();
if (window.devicePixelRatio !== undefined) {
ratio = window.devicePixelRatio;
}
else if (~ua.indexOf('msie')) {
if (screen.deviceXDPI && screen.logicalXDPI) {
ratio = screen.deviceXDPI / screen.logicalXDPI;
}
}
else if (window.outerWidth !== undefined && window.innerWidth !== undefined) {
ratio = window.outerWidth / window.innerWidth;
}
if (ratio) {
ratio = Math.round(ratio * 100);
}
return ratio;
};
// hack 适配浏览器的缩放比例
if (!_isMobi) {
let _scale = 1 / (_detectZoom() / 200);
_SVGWidth = (1 * _scale) + 'px';
_SVGHeight = (1 * _scale) + 'px';
}
// 生成svg的wrapper
const svg = $(document.createElementNS('http://www.w3.org/2000/svg', 'svg'))
.attr('class', 'butterfly-svg')
.attr('width', _SVGWidth)
.attr('height', _SVGHeight)
.attr('version', '1.1')
.attr('xmlns', 'http://www.w3.org/2000/svg')
.appendTo(this.wrapper);
if(!_isMobi) {
// hack 监听浏览器的缩放比例并适配
window.onresize = () => {
let _scale = 1 / (_detectZoom() / 200);
svg.attr('width', (1 * _scale) + 'px').attr('height', (1 * _scale) + 'px');
}
// hack 因为width和height为1的时候会有偏移
let wrapperOffset = $(this.wrapper)[0].getBoundingClientRect();
let svgOffset = svg[0].getBoundingClientRect();
svg.css('top', (wrapperOffset.top - svgOffset.top) + 'px').css('left', (wrapperOffset.left - svgOffset.left) + 'px');
}
return this.svg = svg;
}
_genWrapper() {
// 生成wrapper
const wrapper = $('<div class="butterfly-wrapper"></div>')
.appendTo(this.root);
return this.wrapper = wrapper[0];
}
_genCanvasWrapper() {
// 生成canvas wrapper
this.canvasWrapper = new SelectCanvas();
this.canvasWrapper.init({
root: this.root,
_on: this.on.bind(this),
_emit: this.emit.bind(this)
});
}
_addEventListener() {
if (this.zoomable) {
this.setZoomable(true);
}
if (this.moveable) {
this.setMoveable(true);
}
let _isChrome = /Chrome/.test(window.navigator.userAgent) && /Google Inc/.test(window.navigator.vendor);
let _getChromeVersion = () => {
var raw = window.navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./);
return raw ? parseInt(raw[2], 10) : false;
};
let _isHightVerChrome = _isChrome && _getChromeVersion() >= 64;
// todo:chrome64版本ResizeObserver对象不存在,但官方文档说64支持,所以加强判断下
if (_isHightVerChrome && window.ResizeObserver && this.theme.autoResizeRootSize) {
// 监听某个dom的resize事件
const _resizeObserver = new ResizeObserver(entries => {
this._rootWidth = $(this.root).width();
this._rootHeight = $(this.root).height();
this._coordinateService._changeCanvasInfo({
terOffsetX: $(this.root).offset().left,
terOffsetY: $(this.root).offset().top,
terWidth: $(this.root).width(),
terHeight: $(this.root).height()
});
this.canvasWrapper.resize({root: this.root});
this.setGridMode(true, undefined , true);
});
_resizeObserver.observe(this.root);
} else {
// 降级处理,监控窗口的resize事件
window.addEventListener('resize', () => {
this._rootWidth = $(this.root).width();
this._rootHeight = $(this.root).height();
this._coordinateService._changeCanvasInfo({
terOffsetX: $(this.root).offset().left,
terOffsetY: $(this.root).offset().top,
terWidth: $(this.root).width(),
terHeight: $(this.root).height()
});
this.canvasWrapper.resize({root: this.root});
this.setGridMode(true, undefined, true);
})
}
// 绑定一大堆事件,group:addMember,groupDragStop,group:removeMember,beforeDetach,connection,
this.on('InnerEvents', (data) => {
if (data.type === 'node:addEndpoint') {
this._addEndpoint(data.data, 'node', data.isInited);
} else if (data.type === 'node:removeEndpoint') {
let _point = data.data;
let rmEdges = this.edges.filter((item) => {
return (item.sourceNode.id === _point.nodeId && item.sourceEndpoint.id === _point.id) ||
(item.targetNode.id === _point.nodeId && item.targetEndpoint.id === _point.id);
});
this.removeEdges(rmEdges);
} else if (data.type === 'group:addEndpoint') {
this._addEndpoint(data.data, 'group', data.isInited);
} else if (data.type === 'node:dragBegin') {
this._dragType = 'node:drag';
this._dragNode = data.data;
} else if (data.type === 'node:mouseDown') {
this._dragType = 'node:mouseDown';
} else if (data.type === 'group:dragBegin') {
this._dragType = 'group:drag';
this._dragNode = data.data;
} else if (data.type === 'endpoint:drag') {
this._dragType = 'endpoint:drag';
this._dragEndpoint = data.data;
} else if (data.type === 'node:move') {
this._moveNode(data.node, data.x, data.y, data.isNotEventEmit);
} else if (data.type === 'group:move') {
this._moveGroup(data.group, data.x, data.y, data.isNotEventEmit);
} else if (data.type === 'link:mouseDown') {
this._dragType = 'link:mouseDown';
} else if (data.type === 'link:dragBegin') {
this._dragType = 'link:drag';
this._dragPathEdge = {
edge: data.edge,
path: data.path
}
} else if (data.type === 'multiple:select') {
const result = this._selectMultiplyItem(data.range, data.toDirection);
// 把框选的加到union的数组
_.assign(this._unionData['__system'], this.selectItem);
this.emit('system.multiple.select', {
data: result
});
this.emit('events', {
type: 'multiple:select',
data: result
});
this.selectItem = {
nodes: [],
edges: [],
endpoints: []
};
} else if (data.type === 'node:resize') {
this._dragType = 'node:resize';
this._dragNode = data.node;
} else if (data.type === 'group:resize') {
this._dragType = 'group:resize';
this._dragGroup = data.group;
} else if (data.type === 'node:delete') {
this.removeNode(data.data.id);
} else if (data.type === 'edge:delete') {
this.removeEdge(data.data);
} else if (data.type === 'group:delete') {
this.removeGroup(data.data.id);
} else if (data.type === 'group:addNodes') {
_.get(data, 'nodes', []).forEach((item) => {
let _hasNode = _.find(this.nodes, (_node) => {
return item.id === _node.id;
});
if (!_hasNode) {
this.addNode(item);
} else {
let neighborEdges = [];
let rmItem = this.removeNode(item.id, true, true);
let rmNode = rmItem.nodes[0];
let _group = data.group;
neighborEdges = rmItem.edges;
rmNode._init({
top: item.top - _group.top,
left: item.left - _group.left,
dom: rmNode.dom,
group: _group.id
});
this.addNode(rmNode, true);
}
});
if (!data.isNotEventEmit) {
this.emit('events', {
type: 'system.group.addMembers',
nodes: data.nodes,
group: data.group
});
this.emit('system.group.addMembers', {
nodes: data.nodes,
group: data.group
});
}
} else if (data.type === 'group:removeNodes') {
let _group = data.group;
_.get(data, 'nodes', []).forEach((item) => {
this.removeNode(item.id, true, true);
item._init({
group: undefined,
left: item.left + _group.left,
top: item.top + _group.top,
dom: item.dom,
_isDeleteGroup: true
});
this.addNode(item, true);
});
} else if (data.type === 'edge:updateLabel') {
let labelDom = data.data.labelDom;
$(this.wrapper).append(labelDom);
} else if (data.type === 'edge:setZIndex') {
this.setEdgeZIndex([data.edge], data.index);
} else if (data.type === 'endpoint:updatePos') {
let point = data.point;
let edges = this.getNeighborEdgesByEndpoint(point.nodeId, point.id);
edges.forEach((item) => {
item.redraw();
});
}
});
// 绑定拖动事件
this._attachMouseDownEvent();
}
_attachMouseDownEvent() {
let canvasOriginPos = {
x: 0,
y: 0
};
let nodeOriginPos = {
x: 0,
y: 0
};
let _isActiveEndpoint = false;
let _activeItems = [];
// 把其他可连接的point高亮
let _activeLinkableEndpoint = (target) => {
if (_isActiveEndpoint) {
return;
}
let _allPoints = this._getAllEndpoints();
_allPoints.forEach((_point) => {
if (target === _point || _point.type === 'source' || _point._tmpType === 'source') {
return;
}
if (_point.canLink && _point.canLink(target)) {
if (_point.linkable) {
_point.linkable();
_point._linkable = true;
}
return;
}
if (ScopeCompare(target.scope, _point.scope)) {
if (_point.linkable) {
_point.linkable();
_point._linkable = true;
}
_activeItems.push(_point);
return;
}
});
_isActiveEndpoint = true;
};
// 把其他可连接的point取消高亮
let _unActiveLinkableEndpoint = () => {
_isActiveEndpoint = false;
_activeItems.forEach((item) => {
item.unLinkable && item.unLinkable();
item.unHoverLinkable && item.unHoverLinkable();
item._linkable = false;
});
_activeItems = [];
};
let _oldFocusPoint = null;
let _isFocusing = false;
// 检查鼠标是否在可连的锚点上
let _focusLinkableEndpoint = (cx, cy) => {
if (!_isFocusing) {
// if (_focusPoint) {
// _focusPoint.unHoverLinkable && _focusPoint.unHoverLinkable();
// _focusPoint = null;
// }
_isFocusing = true;
setTimeout(() => {
let _points = this._getAllEndpoints();
let x = this._coordinateService._terminal2canvas('x', cx);
let y = this._coordinateService._terminal2canvas('y', cy);
let _focusPoint = null;
_points.forEach((_point) => {
const _maxX = _point._posLeft + _point._width + (_.get(_point, 'expandArea.right') || this.theme.endpoint.expandArea.right);
const _maxY = _point._posTop + _point._height + (_.get(_point, 'expandArea.bottom') || this.theme.endpoint.expandArea.bottom);
const _minX = _point._posLeft - (_.get(_point, 'expandArea.left') || this.theme.endpoint.expandArea.left);
const _minY = _point._posTop - (_.get(_point, 'expandArea.top') || this.theme.endpoint.expandArea.top);
if (x > _minX && x < _maxX && y > _minY && y < _maxY) {
_focusPoint = _point;
}
});
if (_focusPoint) {
if (_focusPoint !== _oldFocusPoint) {
_focusPoint.hoverLinkable && _focusPoint.hoverLinkable();
_oldFocusPoint = _focusPoint;
}
} else {
if (_oldFocusPoint) {
_oldFocusPoint.unHoverLinkable && _oldFocusPoint.unHoverLinkable();
_oldFocusPoint = null;
}
}
_isFocusing = false;
}, 100);
}
};
const _clearDraging = () => {
this._dragType = null;
this._dragNode = null;
this._dragEndpoint = null;
this._dragGroup = null;
this._dragPathEdge = null;
this._dragEdges = [];
nodeOriginPos = {
x: 0,
y: 0
};
canvasOriginPos = {
x: 0,
y: 0
};
this._autoMoveDir = [];
this._guidelineService.isActive && this._guidelineService.clearCanvas();
};
const mouseDownEvent = (event) => {
const LEFT_BUTTON = 0;
if (event.button !== LEFT_BUTTON) {
return;
}
if (!this._dragType && this.moveable) {
this._dragType = 'canvas:drag';
}
// 假如点击在空白地方且在框选模式下
if ((event.target === this.svg[0] || event.target === this.root) && this.isSelectMode) {
this.canvasWrapper.active();
this.canvasWrapper.dom.dispatchEvent(new MouseEvent('mousedown', {
clientX: event.clientX,
clientY: event.clientY
}));
return;
}
canvasOriginPos = {
x: event.clientX,
y: event.clientY
};
// 初始化z-index
if (!this._isInitEdgeZIndex) {
$(this.svg).css('z-index', this._dragEdgeZindex);
this.nodes.forEach((item) => {
$(item.dom).css('z-index', (this._dragNodeZIndex) * 2 - 1);
_.get(item, 'endpoints').forEach((point) => {
$(point.dom).css('z-index', this._dragNodeZIndex * 2);
});
});
this.edges.forEach((item) => {
if (item.labelDom) {
$(item.labelDom).css('z-index', this._dragEdgeZindex + 1);
}
});
this._isInitEdgeZIndex = true;
}
// 拖动的时候提高z-index
if (this._dragNode && this._dragNode.__type == 'node') {
$(this._dragNode.dom).css('z-index', (++this._dragNodeZIndex) * 2 - 1);
_.get(this._dragNode, 'endpoints').forEach((point) => {
$(point.dom).css('z-index', this._dragNodeZIndex * 2);
});
}
if (this._dragNode && this._dragNode.__type == 'group') {
$(this._dragNode.dom).css('z-index', (++this._dragGroupZIndex) * 2 - 1);
_.get(this._dragNode, 'endpoints').forEach((point) => {
$(point.dom).css('z-index', this._dragGroupZIndex * 2);
});
}
this.emit('system.drag.start', {
dragType: this._dragType,
dragNode: this._dragNode,
dragEndpoint: this._dragEndpoint,
dragEdges: this._dragEdges,
dragGroup: this._dragGroup,
position: {
clientX: event.clientX,
clientY: event.clientY,
canvasX: this._coordinateService._terminal2canvas('x', event.clientX),
canvasY: this._coordinateService._terminal2canvas('y', event.clientY)
}
});
this.emit('events', {
type: 'drag:start',
dragType: this._dragType,
dragNode: this._dragNode,
dragEndpoint: this._dragEndpoint,
dragEdges: this._dragEdges,
dragGroup: this._dragGroup,
position: {
clientX: event.clientX,
clientY: event.clientY,
canvasX: this._coordinateService._terminal2canvas('x', event.clientX),
canvasY: this._coordinateService._terminal2canvas('y', event.clientY)
}
});
this._autoMoveDir = [];
};
const mouseMoveEvent = (event) => {
const LEFT_BUTTON = 0;
if (event.button !== LEFT_BUTTON) {
return;
}
if (this._dragType) {
const canvasX = this._coordinateService._terminal2canvas('x', event.clientX);
const canvasY = this._coordinateService._terminal2canvas('y', event.clientY);
const offsetX = event.clientX - canvasOriginPos.x;
const offsetY = event.clientY - canvasOriginPos.y;
if (this._dragType === 'canvas:drag') {
this.move([offsetX + this._moveData[0], offsetY + this._moveData[1]]);
canvasOriginPos = {
x: event.clientX,
y: event.clientY
};
} else if (this._dragType === 'node:drag') {
if (nodeOriginPos.x === 0 && nodeOriginPos.y === 0) {
nodeOriginPos = {
x: canvasX,
y: canvasY
};
return;
}
if (this._dragNode) {
let moveNodes = [this._dragNode];
const unionKeys = this._findUnion('nodes', this._dragNode);
if (unionKeys && unionKeys.length > 0) {
unionKeys.forEach((key) => {
moveNodes = moveNodes.concat(this._unionData[key].nodes);
});
moveNodes = _.uniqBy(moveNodes, 'id');
} else {
this._rmSystemUnion();
}
$(this.svg).css('visibility', 'hidden');
$(this.wrapper).css('visibility', 'hidden');
moveNodes.forEach((node) => {
this._moveNode(node, node.left + (canvasX - nodeOriginPos.x), node.top + (canvasY - nodeOriginPos.y));
if (this._guidelineService.isActive) {
this._guidelineService.draw(node, 'node');
}
});
$(this.svg).css('visibility', 'visible');
$(this.wrapper).css('visibility', 'visible');
nodeOriginPos = {
x: canvasX,
y: canvasY
};
this._hoverGroup(this._dragNode);
this.emit('system.node.move', {
nodes: moveNodes
});
this.emit('events', {
type: 'node:move',
nodes: moveNodes
});
this._autoMoveCanvas(event.clientX, event.clientY, {
type: 'node:drag',
nodes: moveNodes
}, (gap) => {
nodeOriginPos.x += gap[0];
nodeOriginPos.y += gap[1];
});
}
} else if (this._dragType === 'group:drag') {
if (nodeOriginPos.x === 0 && nodeOriginPos.y === 0) {
nodeOriginPos = {
x: canvasX,
y: canvasY
};
return;
}
if (this._dragNode) {
$(this.svg).css('visibility', 'hidden');
$(this.wrapper).css('visibility', 'hidden');
const group = this._dragNode;
this._moveGroup(group, group.left + (canvasX - nodeOriginPos.x), group.top + (canvasY - nodeOriginPos.y));
if (this._guidelineService.isActive) {
this._guidelineService.draw(group, 'group');
}
$(this.svg).css('visibility', 'visible');
$(this.wrapper).css('visibility', 'visible');
nodeOriginPos = {
x: canvasX,
y: canvasY
};
this.emit('system.group.move', {
group: group
});
this.emit('events', {
type: 'group:move',
group: group
});
this._autoMoveCanvas(event.clientX, event.clientY, {
type: 'group:drag',
group: group
}, (gap) => {
nodeOriginPos.x += gap[0];
nodeOriginPos.y += gap[1];
});
}
} else if (this._dragType === 'endpoint:drag') {
const endX = this._coordinateService._terminal2canvas('x', event.clientX);
const endY = this._coordinateService._terminal2canvas('y', event.clientY);
// 明确标记source或者是没有type且没有线连上
let _isSourceEndpoint = (this._dragEndpoint.type === 'source' || this._dragEndpoint.type === 'onlyConnect' || (!this._dragEndpoint.type && (!this._dragEndpoint._tmpType || this._dragEndpoint._tmpType === 'source')));
let _isTargetEndpoint = (this._dragEndpoint.type === 'target' || (!this._dragEndpoint.type && this._dragEndpoint._tmpType === 'target')) && this._dragEndpoint.type !== 'onlyConnect';
if (_isSourceEndpoint && this.linkable) {
let unionKeys = this._findUnion('endpoints', this._dragEndpoint);
let edges = [];
if (!this._dragEdges || this._dragEdges.length === 0) {
const EdgeClass = this.theme.edge.Class;
let endpoints = [];
if (unionKeys && unionKeys.length > 0) {
unionKeys.forEach((key) => {
endpoints = endpoints.concat(this._unionData[key].endpoints);
});
endpoints = _.uniqBy(endpoints, (_point) => {return _point.nodeId + '||' + _point.id});
} else {
endpoints = [this._dragEndpoint];
}
endpoints.forEach((point) => {
let _sourceNode = point.nodeType === 'node' ? this.getNode(point.nodeId) : this.getGroup(point.nodeId);
let _sourceEndpoint = point;
let pointObj = {
type: 'endpoint',
type: this.theme.edge.type,
shapeType: this.theme.edge.shapeType,
orientationLimit: this.theme.endpoint.position,
_sourceType: point.nodeType,
sourceNode: _sourceNode,
sourceEndpoint: _sourceEndpoint,
arrow: this.theme.edge.arrow,
arrowShapeType: this.theme.edge.arrowShapeType,
arrowPosition: this.theme.edge.arrowPosition,
arrowOffset: this.theme.edge.arrowOffset,
draggable: this.theme.edge.draggable,
label: this.theme.edge.label,
labelPosition: this.theme.edge.labelPosition,
labelOffset: this.theme.edge.labelOffset,
isExpandWidth: this.theme.edge.isExpandWidth
};
pointObj['options'] = _.assign({}, pointObj, {
sourceNode: _sourceNode.id,
sourceEndpoint: _sourceEndpoint.id
});
// 检查endpoint限制连接数目
let _linkNums = this.edges.filter((_edge) => {
return _edge.sourceEndpoint.id === point.id;
}).length + 1;
if (_linkNums > point.limitNum) {
console.warn(`id为${point.id}的锚点限制了${point.limitNum}条连线`);
return;
}
let _newEdge = new EdgeClass(_.assign(pointObj, {
_global: this.global,
_on: this.on.bind(this),
_emit: this.emit.bind(this),
}));
_newEdge._init({
_coordinateService: this._coordinateService
});
$(this.svg).append(_newEdge.dom);
if (_newEdge.labelDom) {
$(this.wrapper).append(_newEdge.labelDom);
}
if (_newEdge.arrowDom) {
$(this.svg).append(_newEdge.arrowDom);
}
edges.push(_newEdge);
});
this._dragEdges = edges;
} else {
edges = this._dragEdges;
}
$(this.svg).css('visibility', 'hidden');
$(this.wrapper).css('visibility', 'hidden');
let _targetPoint = {
pos: [endX, endY],
};
edges.forEach((edge) => {
let beginX = edge.sourceEndpoint._posLeft + edge.sourceEndpoint._width / 2;
let beginY = edge.sourceEndpoint._posTop + edge.sourceEndpoint._height / 2;
const _soucePoint = {
pos: [beginX, beginY],
orientation: edge.sourceEndpoint.orientation
};
edge.redraw(_soucePoint, _targetPoint);
});
$(this.svg).css('visibility', 'visible');
$(this.wrapper).css('visibility', 'visible');
if (this.theme.endpoint.linkableHighlight) {
_activeLinkableEndpoint(this._dragEndpoint);
_focusLinkableEndpoint(event.clientX, event.clientY);
}
this._autoMoveCanvas(event.clientX, event.clientY, {
type: 'endpoint:drag',
edges: edges
}, (gap) => {
edges.forEach((edge) => {
let beginX = edge.sourceEndpoint._posLeft + edge.sourceEndpoint._width / 2;
let beginY = edge.sourceEndpoint._posTop + edge.sourceEndpoint._height / 2;
const _soucePoint = {
pos: [beginX, beginY],
orientation: edge.sourceEndpoint.orientation
};
_targetPoint.pos[0] += gap[0];
_targetPoint.pos[1] += gap[1];
edge.redraw(_soucePoint, _targetPoint);
});
});
this.emit('system.drag.move', {
dragType: this._dragType,
pos: [event.clientX, event.clientY],
dragNode: this._dragNode,
dragEndpoint: this._dragEndpoint,
dragEdges: edges
});
this.emit('events', {
type: 'drag:move',
dragType: this._dragType,
pos: [event.clientX, event.clientY],
dragNode: this._dragNode,
dragEndpoint: this._dragEndpoint,
dragEdges: edges
});
} else if (_isTargetEndpoint && this.disLinkable) {
// 从后面搜索线
let targetEdge = null;
for (let i = this.edges.length - 1; i >= 0; i--) {
if (this._dragEndpoint.id === _.get(this.edges, [i, 'targetEndpoint', 'id']) && this._dragEndpoint.nodeId === _.get(this.edges, [i, 'targetNode', 'id'])) {
targetEdge = this.edges[i];
break;
}
}
if (targetEdge && this._dragEdges.length === 0) {
targetEdge._isDeletingEdge = true;
this._dragEdges = [targetEdge];
}
if (this._dragEdges.length !== 0) {
let edge = this._dragEdges[0];
let beginX = edge.sourceEndpoint._posLeft + edge.sourceEndpoint._width / 2;
let beginY = edge.sourceEndpoint._posTop + edge.sourceEndpoint._height / 2;
const _soucePoint = {
pos: [beginX, beginY],
orientation: edge.sourceEndpoint.orientation
};
const _targetPoint = {
pos: [endX, endY],
};
edge.redraw(_soucePoint, _targetPoint);
}
if (this.theme.endpoint.linkableHighlight) {
_activeLinkableEndpoint(this._dragEndpoint);
_focusLinkableEndpoint(event.clientX, event.clientY);
}
}
} else if (this._dragType === 'link:drag') {
this._dragPathEdge.edge._updatePath(this._dragPathEdge.path, {
x: canvasX,
y: canvasY
});
} else if (this._dragType === 'node:resize') {
this._dragNode.resize(canvasX, canvasY);
} else if (this._dragType === 'group:resize') {
let pos = this._getGroupPos(this._dragGroup);
let _newWidth = canvasX - pos.left;
let _newHeight = canvasY - pos.top;
this._dragGroup.setSize(_newWidth, _newHeight);
}
}
};
const mouseEndEvent = (event) => {
const LEFT_BUTTON = 0;
if (event.button !== LEFT_BUTTON) {
return;
}
let _unionItems = [];
_unActiveLinkableEndpoint();
// 处理线条的问题
if (this._dragType === 'endpoint:drag' && this._dragEdges && this._dragEdges.length !== 0) {
// 释放对应画布上的x,y
const x = this._coordinateService._terminal2canvas('x', event.clientX);
const y = this._coordinateService._terminal2canvas('y', event.clientY);
let _targetEndpoint = null;
let _nodes = _.concat(this.nodes, this.groups);
_nodes.forEach((_node) => {
if (_node.endpoints) {
_node.endpoints.forEach((_point) => {
const _maxX = _point._posLeft + _point._width + (_.get(_point, 'expandArea.right') || this.theme.endpoint.expandArea.right);
const _maxY = _point._posTop + _point._height + (_.get(_point, 'expandArea.bottom') || this.theme.endpoint.expandArea.bottom);
const _minX = _point._posLeft - (_.get(_point, 'expandArea.left') || this.theme.endpoint.expandArea.left);
const _minY = _point._posTop - (_.get(_point, 'expandArea.top') || this.theme.endpoint.expandArea.top);
if (x > _minX && x < _maxX && y > _minY && y < _maxY) {
_targetEndpoint = _point;
}
});
}
});
let isDestoryEdges = false;
// 找不到点 或者 目标节点不是target
if (!_targetEndpoint || _targetEndpoint.type === 'source' || _targetEndpoint._tmpType === 'source') {
isDestoryEdges = true;
}
// scope不同
if (!isDestoryEdges) {
isDestoryEdges = _.some(this._dragEdges, (edge) => {
return !ScopeCompare(edge.sourceEndpoint.scope, _targetEndpoint.scope, _.get(this, 'global.isScopeStrict'));
});
}
// 检查endpoint限制连接数目
if (_targetEndpoint && _targetEndpoint.limitNum !== undefined) {
let _linkNum = this.edges.filter((_edge) => {
return _edge.targetEndpoint.id === _targetEndpoint.id;
}).length + this._dragEdges.length;
if (_linkNum > _targetEndpoint.limitNum) {
console.warn(`id为${_targetEndpoint.id}的锚点限制了${_targetEndpoint.limitNum}条连线`);
isDestoryEdges = true;
}
}
if (isDestoryEdges) {
this._dragEdges.forEach((edge) => {
if (edge._isDeletingEdge) {
this.removeEdge(edge);
} else {
edge.destroy(!edge._isDeletingEdge);
}
});
// 把endpoint重新赋值
this._dragEdges.forEach((_rmEdge) => {
if (_.get(_rmEdge, 'sourceEndpoint._tmpType') === 'source') {
let isExistEdge = _.some(this.edges, (edge) => {
return _rmEdge.sourceNode.id === edge.sourceNode.id && _rmEdge.sourceEndpoint.id === edge.sourceEndpoint.id;
});
!isExistEdge && (_rmEdge.sourceEndpoint._tmpType = undefined);
}
if (_.get(_rmEdge, 'targetEndpoint._tmpType') === 'target') {
let isExistEdge = _.some(this.edges, (edge) => {
return _rmEdge.targetNode.id === edge.targetNode.id && _rmEdge.targetEndpoint.id === edge.targetEndpoint.id;
});
!isExistEdge && (_rmEdge.targetEndpoint._tmpType = undefined);
}
});
} else {
let _delEdges = [];
let _reconnectInfo = [];
let _emitEdges = this._dragEdges.filter((edge) => {
// 线条去重
if (!this.theme.edge.isRepeat) {
let _isRepeat = _.some(this.edges, (_edge) => {
let _result = false;
if (edge.sourceNode) {
if (_edge.type === 'node') {
_result = edge.sourceNode.id === _edge.sourceNode.id;
} else {
_result = edge.sourceNode.id === _edge.sourceNode.id && edge.sourceEndpoint.id === _edge.sourceEndpoint.id;
}
}
if (_targetEndpoint.nodeId) {
if (_edge.type === 'node') {
_result = _result && (_.get(edge, 'targetNode.id') === _.get(_edge, 'targetNode.id'));
} else {
_result = _result && (_targetEndpoint.nodeId === _.get(_edge, 'targetNode.id') && _targetEndpoint.id === _.get(_edge, 'targetEndpoint.id'));
}
}
if (_result && edge._isDeletingEdge) {
_result = false;
}
return _result;
});
if (_isRepeat) {
console.warn(`id为${edge.sourceEndpoint.id}-${_targetEndpoint.id}的线条连接重复,请检查`);
edge.destroy();
return false;
}
}
let _preTargetNodeId = _.get(edge, 'targetNode.id');
let _preTargetPointId = _.get(edge, 'targetEndpoint.id');
let _currentTargetNode = _targetEndpoint.nodeType === 'node' ? this.getNode(_targetEndpoint.nodeId) : this.getGroup(_targetEndpoint.nodeId);
let _currentTargetEndpoint = _targetEndpoint;
if (_preTargetNodeId && _preTargetPointId && `${_preTargetNodeId}||${_preTargetPointId}` !== `${_currentTargetNode.id}||${_currentTargetEndpoint.id}`) {
_delEdges.push(_.cloneDeep(edge));
_reconnectInfo.push({
edge,
preTargetNodeId: _preTargetNodeId,
preTargetPointId: _preTargetPointId,
currentTargetNodeId: _currentTargetNode.id,
currentTargetPointId: _currentTargetEndpoint.id
});
// source发生变化,target未变化
edge.targetEndpoint.connectedNum -= 1;
_targetEndpoint.connectedNum += 1;
} else {
// source和target都是新增
edge.sourceEndpoint.connectedNum += 1;
_targetEndpoint.connectedNum += 1;
}
edge._create({
id: edge.id && !edge._isDeletingEdge ? edge.id : `${edge.sourceEndpoint.id}-${_targetEndpoint.id}`,
targetNode: _currentTargetNode,
_targetType: _targetEndpoint.nodeType,
targetEndpoint: _currentTargetEndpoint,
type: 'endpoint'
});
let _isConnect = edge.isConnect ? edge.isConnect() : true;
if (!_isConnect) {
console.warn(`id为${edge.sourceEndpoint.id}-${_targetEndpoint.id}的线条无法连接,请检查`);
edge.destroy();
return false;
}
// 正在删除的线重新连接
if (edge._isDeletingEdge) {
delete edge._isDeletingEdge;
} else {
edge.mounted && edge.mounted();
this.edges.push(edge);
}
// 把endpoint重新赋值
if (edge.type === 'endpoint' && !_.get(edge, 'sourceEndpoint.type') && !_.get(edge, 'sourceEndpoint._tmpType')) {
edge.sourceEndpoint._tmpType = 'source';
}
if (edge.type === 'endpoint' && !_.get(edge, 'targetEndpoint.type') && !_.get(edge, 'targetEndpoint._tmpType')) {
edge.targetEndpoint._tmpType = 'target';
}
// reconnect没前后变更
if (_preTargetNodeId && _preTargetPointId && `${_preTargetNodeId}||${_preTargetPointId}` === `${_currentTargetNode.id}||${_currentTargetEndpoint.id}`) {
return false;
}
return edge;
});
if (_delEdges.length !== 0 && _emitEdges.length !== 0) {
this.pushActionQueue({
type: 'system:reconnectEdges',
data: {
delLinks: _delEdges,
addLinks: _emitEdges,
info: _reconnectInfo
}
});
this.emit('system.link.reconnect', {
delLinks: _delEdges,
addLinks: _emitEdges,
info: _reconnectInfo
});
this.emit('events', {
type: 'link:reconnect',
delLinks: _delEdges,
addLinks: _emitEdges,
info: _reconnectInfo
});
} else {
if (_delEdges.length !== 0) {
_delEdges.forEach((_edge) => {
this.pushActionQueue({
type: 'system:removeEdges',
data: _delEdges
});
this.emit('system.link.delete', {
link: _edge
});
this.emit('events', {
type: 'link:delete',
link: _edge
});
});
}
if (_emitEdges.length !== 0) {
this.pushActionQueue({
type: 'system:addEdges',
data: this._dragEdges
});
this.emit('system.link.connect', {
links: this._dragEdges
});
this.emit('events', {
type: 'link:connect',
links: this._dragEdges
});
}
}
}
}
if ((this._dragType === 'node:drag' || this._dragType === 'group:drag') && this._dragNode) {
let _dragType = this._dragType === 'node:drag' ? 'node' : 'group';
let _dragItem = this._dragNode;
let _handleDragItem = (dragItem) => {
let sourceGroup = null;
let targetGroup = null;
let _itemLeft = dragItem.left;
let _itemRight = dragItem.left + dragItem.getWidth();
let _itemTop = dragItem.top;
let _itemBottom = dragItem.top + dragItem.getHeight();
if (dragItem.group) {
const _group = this.getGroup(dragItem.group);
const _groupLeft = _group.left;
const _groupTop = _group.top;
if (_itemRight < 0 || _itemLeft > _group.getWidth() || _itemBottom < 0 || _itemTop > _group.getHeight()) {
// 拖动到节点组外
_itemLeft += _groupLeft;
_itemTop += _groupTop;
_itemRight += _groupLeft;
_itemBottom += _groupTop;
sourceGroup = _group;
} else {
// 节点组内拖动
sourceGroup = _group;
targetGroup = _group;
}
}
this._hoverGroup(_dragItem);
if (!targetGroup) {
// 没开启group嵌套功能
if (_dragItem.__type !== 'group' || this.theme.group.includeGroups) {
targetGroup = this._findGroupByCoordinates(dragItem, _itemLeft, _itemTop, _itemRight, _itemBottom);
}
}
let neighborEdges = [];
if (sourceGroup) {
// 从源组拖动到目标组
if (sourceGroup !== targetGroup) {
// const rmItem = this.removeNode(dragNode.id, true, true);
const rmResult = _dragType === 'node' ? this.removeNode(_dragItem.id, true, true) : this.removeGroup(_dragItem.id, true);
const rmTarget = _dragType === 'node' ? rmResult.nodes[0] : rmResult.group;
neighborEdges = rmResult.edges;
const rmTargetData = {
top: _itemTop,
left: _itemLeft,
dom: rmTarget.dom,
_isDeleteGroup: true
};
let step = this.actionQueue[this.actionQueueIndex];
// todo:这块需要考虑下system:moveGroups
if (step.type === 'system:moveNodes') {
step.data._isDraging = true;
}
this.pushActionQueue({
type: 'system:groupRemoveMembers',
data: {
group: sourceGroup,
nodes: _dragType === 'node' ? [rmTarget] : [],
groups: _dragType === 'group' ? [rmTarget] : [],
_isDraging: true
}
})
this.emit('events', {
type: 'system.group.removeMembers',
group: sourceGroup,
nodes: _dragType === 'node' ? [rmTarget] : [],
groups: _dragType === 'group' ? [rmTarget] : []
});
this.emit('system.group.removeMembers', {
group: sourceGroup,
nodes: _dragType === 'node' ? [rmTarget] : [],
groups: _dragType === 'group' ? [rmTarget] : []
});
if (targetGroup) {
if (ScopeCompare(_dragItem.scope, targetGroup.scope, _.get(this, 'global.isScopeStrict'))) {
rmTargetData.top -= targetGroup.top;
rmTargetData.left -= targetGroup.left;
rmTargetData.group = targetGroup.id;
rmTargetData._isDeleteGroup = false;
this.popActionQueue();
this.pushActionQueue({
type: 'system:groupAddMembers',
data: {
sourceGroup: sourceGroup,
targetGroup: targetGroup,
nodes: _dragType === 'node' ? [rmTarget] : [],
groups: _dragType === 'group' ? [rmTarget] : [],
_isDraging: true
}
});
this.emit('events', {
type: 'system.group.addMembers',
nodes: _dragType === 'node' ? [rmTarget] : [],
groups: _dragType === 'group' ? [rmTarget] : [],
group: targetGroup
});
this.emit('system.group.addMembers', {
nodes: _dragType === 'node' ? [rmTarget] : [],
groups: _dragType === 'group' ? [rmTarget] : [],
group: targetGroup
});
this._clearHoverGroup(targetGroup);
} else {
console.warn(`nodeId为${dragNode.id}的节点和groupId${targetGroup.id}的节点组scope