jsPlumb配置:
const GRAPH_CONFIG = {
Container: 'chart',
Connector: ['Flowchart', { gap: 2, cornerRadius: 5, alwaysRespectStubs: true }],
DragOptions: { cursor: 'pointer', zIndex: 2000 },
PaintStyle: { stroke: '#aab7c3', strokeWidth: 2 },
HoverPaintStyle: { stroke: '#2593fb' },
Endpoint: ['Dot', { radius: 3 }],
EndpointStyle: { stroke: '#6DC1FC', fill: '#fff' },
EndpointHoverStyle: { cursor: 'crosshair', fill: '#2593FC' },
MaxConnections: -1,
ConnectionOverlays: [
['Arrow', {
location: 1,
visible: true,
width: 11,
length: 11,
id: 'ARROW',
}],
['Label', {
id: 'label',
cssClass: 'editor-label',
visible: true,
events: {
tap() {},
},
}],
],
}
const GRAPH_CIRCLE = {
isSource: true,
isTarget: true,
}
初始化jsPlumb
componentDidMount() {
this.jsPlumb = jsPlumb.getInstance(GRAPH_CONFIG);
this.id = this.getNodeId(); //设置节点id,jsPlumb中每个节点都要有唯一id
// 避免源节点和目标节点为同一个
this.jsPlumb.bind('beforeDrop', conn => {
const { sourceId, targetId } = conn;
if (sourceId === targetId) {
return false;
}
return true;
});
this.jsPlumb.bind('dblclick', conn => {
this.jsPlumb.deleteConnection(conn); //双击连线的时候删除
});
this.jsPlumb.bind('click', conn => {
const sourceType = document.getElementById(conn.sourceId).getAttribute('type');
if (sourceType === DECISION_NODE_TYPE) {
this.selectedConnection = conn;
this.setState({ selectedConn: { name: '边', target: conn } });
} else {
this.setState({ selectedConn: { name: '画布' } });
}
});
//设置当前选中的元素,右侧编辑使用
document.getElementById('graph').onclick = event => {
const target = event.target || {};
const classList = [...get(target, 'classList', [])];
if (classList.includes('editor-node')) {
this.setState({ selectedConn: { name: '节点', target } });
}
if (classList.includes('editor-label')) {
this.setState({ selectedConn: { name: '边', target } });
}
if (classList.includes('editor-middle')) {
this.setState({ selectedConn: { name: '画布' } });
}
};
//双击节点,删除
document.getElementById('graph').ondblclick = event => {
const target = event.target || {};
this.jsPlumb.remove(target);
};
}
设置id,代码如下:
getNodeId = () => {
const nodeList = document.querySelectorAll('.editor-middle .editor-node');
const node = nodeList[nodeList.length - 1];
let id = 1;
if (nodeList.length > 1) {
id = Number(node.id) + 1;
}
return id;
}
节点列表,5种节点类型,代码如下:
{ this.handleDrag(event); }}>
用户节点
FORK
DECISION
AND
OR
节点拖拽到绘图区可以使用jquery-ui,但是使用react后不想引入,所以本项目使用的原生H5的拖拽,可以查看HTML5 拖放(Drag 和 Drop)。
拖拽,代码如下:
handleDrag = event => {
event.dataTransfer.setData('Text', event.target.id); //设置被拖数据的数据类型和值
}
绘图区,代码如下:
{ this.handleDrop(event); }}
onDragOver={event => { this.allowDrop(event); }}
>
放置,代码如下:
allowDrop = event => {
event.preventDefault(); //默认无法将数据/元素放置到其他元素中。如需要设置允许放置,必须阻止对元素的默认处理方式
}
handleDrop = event => {
event.preventDefault();
const id = event.dataTransfer.getData('Text'); //获得被拖的数据
//由于元素被拖动后,列表里就没有这个元素了,所以克隆一份添加到列表原来位置,这样频繁的操作dom不太好,但是除了jquery-ui还不知道其他解决方案
const node = document.getElementById(id).cloneNode(true);
const editorNodes = document.querySelector('.editor-nodes');
if (id === 'node5') {
editorNodes.append(node);
} else {
editorNodes.insertBefore(node, document.getElementById(`node${Number(id[4]) + 1}`));
}
//这是元素在绘图区的位置
document.getElementById(id).style.left = `${event.clientX}px`;
document.getElementById(id).style.top = `${event.clientY}px`;
this.addNode(document.getElementById(id));
}
绘图区添加节点,代码如下:
addNode = node => {
const id = this.id++;
node.id = id;
node.draggable = false;
node.style.position = 'absolute';
document.getElementById('graph').append(node);
this.addPoint({ id, type: node.getAttribute('type') }); // 添加锚点
this.jsPlumb.draggable(document.getElementById(id), { containment: 'graph' }); // 节点在绘图区可拖动
}
addPoint = ({ id, type }) => {
const element = document.getElementById(id);
const anchors = GRAPH_NODE_MAP[type].anchors || [];
anchors.forEach(item => {
this.jsPlumb.addEndpoint(element, { anchors: item, uuid: `${item}` }, GRAPH_CIRCLE);
});
}
右侧编辑区,代码如下:
名称:
{ this.handleChangeNode(event, 'textContent'); }}
/>
修改节点名称、连线label,代码如下:
handleChangeNode = (event, type) => {
const { name, target } = this.state.selectedConn;
const { value = '' } = event.target;
if (name === '边') {
this.selectedConnection.setLabel(value); //设置label
}
target[type] = value;
this.setState({ selectedConn: { name, target } });
}
保存,代码如下:
handleSave = () => {
const connectionList = this.jsPlumb.getAllConnections() || [];
const nodeList = document.querySelectorAll('.editor-middle .editor-node');
const nodes = [...nodeList].map(node => ({
id: node.id,
name: node.textContent,
type: {
code: node.getAttribute('type')
},
positionX: parseInt(node.style.left, 10),
positionY: parseInt(node.style.top, 10),
}));
const connections = connectionList.map(connection => ({
connectionId: connection.id,
sourceId: connection.sourceId,
targetId: connection.targetId,
label: connection.getLabel(),
anchors: connection.endpoints.map(endpoint => ([[endpoint.anchor.x,
endpoint.anchor.y,
endpoint.anchor.orientation[0],
endpoint.anchor.orientation[1],
endpoint.anchor.offsets[0],
endpoint.anchor.offsets[1],
endpoint._jsPlumb.overlays
]]
)),
}));
this.props.onSave({ nodes, connections });
}
保存的时候分别保存了节点信息和连线信息,加载时根据这些信息可以绘制出拓扑图。
加载,代码如下:
//渲染节点
renderNodes = () => {
const { nodes } = this.props;
const nodeList = nodes.map(node => {
const type = get(node, 'type.code');
const style = {
position: 'absolute' ,
top: `${node.positionY}px`,
left: `${node.positionX}px`
};
return (
{node.name}
);
});
return nodeList;
}
getNodeClass = type => {
switch (get(GRAPH_NODE_MAP[type], 'name')) {
case 'FORK':
return 'editor-node-triangleUp';
case 'DECISION':
return 'editor-node-rhombus';
case 'AND':
case 'OR':
return 'editor-node-triangleDown';
case 'USER':
return 'editor-node-rectangle';
case 'ROOT':
return 'editor-node-rectangle editor-node-root';
default:
break;
}
}
renderJsPlumb = () => {
const { connections = [] } = this.props;
this.jsPlumb.ready(() => {
const elementList = document.querySelectorAll('#graph .editor-node');
//设置锚点
elementList.forEach(element => {
const type = element.getAttribute('type');
const anchors = get(GRAPH_NODE_MAP[type], 'anchors', []);
anchors.forEach(anchor => {
this.jsPlumb.addEndpoint(element, { anchors: anchor, uuid: `${anchor}` });
});
});
//设置连线、label
connections.forEach(({ sourceId, targetId, anchors, label }) => {
const connection = this.jsPlumb.connect({
source: sourceId,
target: targetId,
anchors,
});
if (label) {
connection.setLabel(label);
}
});
}
参考:
https://blog.csdn.net/github_38186390/article/details/86470650
https://www.cnblogs.com/sggx/p/3836432.html
https://cloud.tencent.com/developer/ask/41429