UNPKG

butterfly-dag

Version:

一个基于数据驱动的节点式编排组件库,让你有方便快捷定制可视化流程图表

1,365 lines (1,273 loc) 149 kB
'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