前言:
jsplumb 有2个版本一个Toolkit Edition(付费版),另外一个就是Community Edition(社区版本)。Toolkit Edition版本功能集成的比较丰富,社区版本的就差好多,很多功能都没有,需要我们自己去添加,当然了自己添加多多少少有些麻烦,而且也不完善。但是我们还是用Community Edition(社区版本),毕竟不收费,没办法,下边所说的版本默认都是以社区版。
最近公司项目有用到这个流程图,所以也就看到了这个框架,这个框架是英文版本的,地址:https://jsplumbtoolkit.com/community/doc/home.html(可以用浏览器翻译了看)。他的缺陷就是文档不全,api感觉也有点乱,实例交代的也不清楚,github地址是:https://github.com/jsplumb/jsplumb (里面有demo,自己可以下载运行,多动手试试)。如果只是简单的画个图,这个框架是没有什么问题的,demo里也有,但是如果要实现高级的动能呢鞥,还是得多多尝试。此文也是记录一下我自己用到的一些功能,很多我还没用到,用到了在慢慢补充。
上图也就是我这次用到的jsplumb实现的功能,连接线能够拖拽生成,也可以删除,编辑label。
1、数据结构
{
"nodes": [{ //节点集合
"icon": "el-icon-loading",
"id": "start",
"nodeStyle": {
"top": 100,
"left": 200
},
"text": "开始",
"type": "circle"
}, {
"icon": "el-icon-upload",
"id": "end",
"nodeStyle": {
"top": 300,
"left": 400
},
"text": "结束",
"type": "circle"
}] ,
"connections": [{ //连接线集合
"sourceId": "start",
"targetId": "end",
"label":"编辑"
}]
}
jsplumb实例里面的数据结构就是这样的,这里我们沿用他的数据结构,你也可以自己定义自己想的数据结构,但是对比起来这个结构是最清晰也最方便的。
2、初始化
jsplumb在DOM渲染完毕之后才会执行,所以需要为jsplumb的执行代码绑定一个ready事件:
jsPlumb.ready(function() {
// your jsPlumb related init code goes here
});
jsplumb默认是注册在浏览器窗口的,将整个页面提供给我们作为一个实例,但我们也可以使用getInstance方法建立页面中一个独立的实例:
var _instance = jsPlumb.getInstance();
3、功能实现(允许哪些元素拖拽,允许拆卸连接)
let instance = jsPlumb.getInstance({
PaintStyle:{
strokeWidth:2,
stroke:"#567567",
}
})
//拖拽功能
var els = document.querySelectorAll(".box");//.box是允许拖拽的元素class类名
instance.draggable(els,{
containment:true,
filter: ".ep",//除去不能拖拽的,这里是个class类名
});
//不允许拆卸连接,不设置的话默认是可以的
instance.importDefaults({
ConnectionsDetachable:false
});
4、连线监听事件(拖动connection 事件)
// 监听拖动connection 事件,判断是否有重复链接
instance.bind("beforeDrop", function(info) {
// info.connection.getOverlay("label").setLabel(info.connection.id);
// 判断是否已有该连接
let isSame = true;
//下边的forEach循环就是处理数据结构里的connections不能自己跟自己连线。当然你也可以处理其他
_this.chartData.connections.forEach(item => {
if ((item.targetId === info.targetId && item.sourceId === info.sourceId) || (item.targetId === info.sourceId && item.sourceId === info.targetId)) {
isSame = false;
}
});
if (isSame) {
//允许连线后处理的情况
} else {
alert("不允许重复连接!");
}
return isSame;//这里返回了ture就会自定进行连线。
});
5、上图实现的完整代码
下边代码就是实现上图的,需要指出的是运用了vue,但是里面掺杂了jquery,和jquery-ui,其实不想用这2个的,但是项目紧,之前项目也用到了,所以就延续了。还有就是上面代码是我自己的测试代码,写的可能有些杂乱,就是测试一个一个功能而写,写的有点乱。
还有一个想说的就是之前想实现,缩放,引入了panzoom.js,流程图也实现了滚动鼠标放大放小,但是有个问题就是滚动鼠标放大放小后如果拖动单个元素或者连线,你就会发现鼠标点对不齐了,这点还没有解决,如果有好的方案,可以告知我下。Toolkit Edition(付费版)的这些功能都有,就不会出现这样的问题。
<template>
<div id="test6" style="height:100%;position:relative">
<section id="focal" style="position:relative;overflow:hidden;width:610px;height:610px;background:#fff;border:1px solid red">
<div class="parent" id="parent" style="height:100%;">
<div class="panzoom" id="panzoom" style="border:1px solid blue;width:6000px;height:6000px; transform:translate(-50%, -50%);position:absolute;">
<div class="box" :id="item.id" :style="{'top':item.nodeStyle.top+'px','left':item.nodeStyle.left+'px'}" v-for="item in chartData.nodes" :key="item.id">
<i :class="item.icon" class="oldIcon" :title="item.text"></i>
<i class="el-icon-circle-close" style="display:none" :title="item.text" :id="item.id"></i>
<div class="ep"></div>
</div>
</div>
</div>
</section>
<div class="source">
<ul>
<li v-for="(item,index) in list" :id="item.id" :key="index" class="sourceLi" :disabled="true" :data-icon="item.icon" :data-text="item.text" :data-type="item.type">{{item.text}}</li>
</ul>
</div>
<el-dialog
title="修改label名称"
:visible.sync="dialogVisible"
width="30%"
:before-close="handleClose">
<el-input v-model="labelName" placeholder="请输入"></el-input>
<span slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">取 消</el-button>
<el-button type="primary" @click="changeNote">确 定</el-button>
</span>
</el-dialog>
</div>
</template>
<script>
import ChartNode from "@/components/ChartNode";
export default {
name: "test6",
data() {
return {
dialogVisible:false,
labelName:"",
curSourceId:'',
curTargetId:'',
addLabelText:'',//拖拽后生成的连线label文字
jsp:null,
myscale:1,
curScreen:[],//当前屏幕宽高
chartData: {
nodes: [],
connections: [],//{ "targetId": "box2", "sourceId": "box1" }
props: {},
screen:[610,610]//提交屏幕宽高
},
list: [
{
icon: "el-icon-goods",
text: "伴随车牌",
type: "circle",
id:'li1'
},
{
icon: "el-icon-bell",
text: "常住人口筛选",
type: "diamond",
id:"li2"
},
{
icon: "el-icon-date",
text: "伴随imsi",
type: "circle",
id:"li3"
}
]
};
},
mounted() {
let _this = this
jsPlumb.ready(function() {
var $section = $('#focal');
var $panzoom = $section.find('.panzoom').panzoom({
minScale: 0.3,
maxScale:2,
eventNamespace: ".panzoom",
$zoomRange: $(".jtk-endpoint"),
$set: $section.find('.jtk-overlay'),
eventsListenerElement: document.querySelector('.box')
});
$(document).on('mouseover','.box,.jtk-draggable,.jtk-overlay,.ep',function(){
$('.panzoom').panzoom("disable");
})
$(document).on('mouseleave','.box,.jtk-draggable,.jtk-overlay,.ep',function(){
$('.panzoom').panzoom("enable");
})
let instance = jsPlumb.getInstance({
PaintStyle:{
strokeWidth:2,
stroke:"#567567",
},
// Connector: ["Straight", { stub: [0,0], gap:[-30,-30] }],
Connector:[ "Straight", { curviness: 0 } ],
Endpoint: ["Blank",{ cssClass: "chart-dot", hoverClass: "chart-dot-hover", radius: 5 }],
EndpointStyle : { fill: "blue" },
HoverPaintStyle:{
stroke:"red",
},
DragOptions: { cursor: "pointer", zIndex: 2000 },
ConnectionOverlays: [
[
"Arrow",
{
location: 1,
visible: true,
width: 11,
length: 11,
id: "ARROW",
events: {
click: function() {
alert("you clicked on the arrow overlay");
}
}
}
],
["Label", { label: "", id: "label", cssClass: "aLabel" }]
],
Container: "panzoom"
})
_this.jsp = instance;
//拖拽功能
var els = document.querySelectorAll(".box");
instance.draggable(els,{
containment:true,
filter: ".ep",//除去不能拖拽的
grid:[50,50]
});
//不允许拆卸连接,不设置的话默认是可以的
instance.importDefaults({
ConnectionsDetachable:false
});
// 监听拖动connection 事件,判断是否有重复链接
instance.bind("beforeDrop", function(info) {
// info.connection.getOverlay("label").setLabel(info.connection.id);
console.log(info);
// 判断是否已有该连接
let isSame = true;
_this.chartData.connections.forEach(item => {
if ((item.targetId === info.targetId && item.sourceId === info.sourceId) || (item.targetId === info.sourceId && item.sourceId === info.targetId)) {
isSame = false;
}
});
if (isSame) {
_this.addLabelText = "新label"
_this.chartData.connections.push({
sourceId: info.sourceId,
targetId: info.targetId,
label:_this.addLabelText
});
} else {
alert("不允许重复连接!");
}
return isSame;
});
var initNode = function(el) {
instance.draggable(el, {
// containment: true,
start(params) {
// 拖动开始
// console.log(params);
},
drag(params) {
// 拖动中
// console.log(params);
},
stop(params) {
// 拖动结束
console.log(params);
let id = params.el.id;
_this.chartData.nodes.forEach(item => {
if (item.id === id) {
item.nodeStyle.left = params.pos[0];
item.nodeStyle.top = params.pos[1] ;
}
});
}
});
instance.makeSource(el, {
filter: ".ep",
anchor: ["Perimeter", { shape: "Rectangle" }],
// anchor: ["Perimeter", { shape: "Dot" }],
connectorStyle: {
stroke: "#5c96bc",
strokeWidth: 2,
outlineStroke: "transparent",
outlineWidth: 4
},
extract: {
action: "the-action"
},
maxConnections: -1,
onMaxConnections: function(info, e) {
alert("Maximum connections (" + info.maxConnections + ") reached");
}
});
instance.makeTarget(el, {
dropOptions: { hoverClass: "dragHover" },
anchor: ["Perimeter", { shape: "Rectangle" }],
allowLoopback: false
});
// instance.fire("jsPlumbDemoNodeAdded", el);
};
//初始化遮罩层
var init = function(connection) {
if(_this.addLabelText){
connection.getOverlay("label").setLabel(_this.addLabelText);
}else{
connection.getOverlay("label").setLabel('编辑');
}
$(connection.getOverlay("label").canvas).attr('mySourceId',connection.sourceId)
$(connection.getOverlay("label").canvas).attr('myTargetId',connection.targetId)
};
// 将模块拖入画板中
$(".sourceLi").draggable({
scope: "plant",
helper: "clone",
opacity: 0.7,
containment: $("#test1")
});
$("#panzoom").droppable({
scope: "plant",
drop: function(ev, ui) {
console.log(ev, ui);
let helper = ui.helper;
let id = jsPlumbUtil.uuid();
let item = {
id,
icon: helper.attr("data-icon"),
type: helper.attr("data-type"),
text: helper.attr("data-text"),
nodeStyle: {
top: ui.offset.top - $("#panzoom").offset().top ,
left: ui.offset.left - $("#panzoom").offset().left
}
};
console.log(ui.position)
_this.chartData.nodes.push(item);
_this.$nextTick(() => {
initNode(id);
});
}
});
instance.batch(() => {
jsPlumb.getSelector(".box").forEach(item => {
console.log(item)
initNode(item);
});
instance.bind("connection", function(connInfo, originalEvent) {
init(connInfo.connection);
//显示删除按钮
$(connInfo.connection.getOverlay("label").canvas).hover(function() {
$(this).append('<div class="x" style="position: absolute;">X</div>');
}, function() {
$(this).find(".x").stop().remove();
})
//删除连接
$(connInfo.connection.getOverlay("label").canvas).on('click','.x',function(){
console.log("shanchu")
let _connections = _this.chartData.connections;
_connections.forEach((val,index)=>{
if(val.targetId == connInfo.connection.targetId && val.sourceId == connInfo.connection.sourceId){
_connections.splice(index,1)
}
})
instance.deleteConnection(connInfo.connection);
$('.panzoom').panzoom("enable");//这个是为了杜绝删除前的禁止拖拽事件
})
//label双击事件
$(connInfo.connection.getOverlay("label").canvas).on("dblclick",function(conn, connInfo){
let _allConnections = _this.jsp.getAllConnections();
_this.dialogVisible = true
_this.curSourceId = $(conn.target).attr('mySourceId')
_this.curTargetId = $(conn.target).attr('myTargetId')
_allConnections.forEach((val,index)=>{
if(val.targetId == $(conn.target).attr('myTargetId') && val.sourceId == $(conn.target).attr('mySourceId')){
_this.labelName = val.getOverlay('label').label
}
})
})
});
});
instance.fire("jsPlumbDemoLoaded", instance);
$(document).on("dblclick",".box",function(){
$(this).find(".oldIcon").css('display','none')
$(this).find('.el-icon-circle-close').css('display','inline-block')
})
$(document).on("click",".el-icon-circle-close",function(){
let _note = _this.chartData.nodes
let _id = $(this).attr("id")
let _connections = _this.chartData.connections;
let _allConnections = instance.getAllConnections();
_this.chartData.connections = _connections.filter((val)=>{
return (val.targetId != _id && val.sourceId != _id)
})
_note.forEach((val,index)=>{
if(val.id == _id){
_note.splice(index,1)
}
})
_allConnections.forEach((val,index)=>{
if(val.targetId == _id || val.sourceId == _id){
instance.deleteConnectionsForElement(_id)
}
})
})
_this.handleClickTemp(1)
});
},
methods:{
myclick(){
alert("myclickmyclickmyclickmyclick")
},
// 初始化node节点
initNode(el) {
// initialise draggable elements.
// 元素拖动,基于 katavorio.js 插件
let _self = this;
this.jsp.draggable(el, {
// containment: true,
start(params) {
// 拖动开始
// console.log(params);
},
drag(params) {
// 拖动中
// console.log(params);
},
stop(params) {
// 拖动结束
console.log(params);
let id = params.el.id;
_self.chartData.nodes.forEach(item => {
if (item.id === id) {
item.nodeStyle.left = params.pos[0]
item.nodeStyle.top = params.pos[1]
}
});
}
});
this.jsp.makeSource(el, {
filter: ".ep",
// anchor: "Continuous",
anchor: ["Perimeter", { shape: "Rectangle" }],
connectorStyle: {
stroke: "#5c96bc",
strokeWidth: 2,
outlineStroke: "transparent",
outlineWidth: 4
},
extract: {
action: "the-action"
},
maxConnections: -1,
onMaxConnections: function(info, e) {
alert("Maximum connections (" + info.maxConnections + ") reached");
}
});
this.jsp.makeTarget(el, {
dropOptions: { hoverClass: "dragHover" },
anchor: ["Perimeter", { shape: "Rectangle" }],
allowLoopback: false
});
// this is not part of the core demo functionality; it is a means for the Toolkit edition's wrapped
// version of this demo to find out about new nodes being added.
//
this.jsp.fire("jsPlumbDemoNodeAdded", el);
},
handleClickTemp(key) {
this.chartData = {
nodes: [],
connections: [],
props: {}
};
this.jsp.empty("panzoom");
if (key) {
let url = "/static/json/" + 1 + ".json";
this.$axios
.get(url)
.then(resp => {
console.log(resp);
let _data = resp.data
let _reloatScreen = _data.screen
let _scale = $("#focal").width() / _data.screen[0]
let _focalWidth = $("#focal").width()
let _focalHeight = $("#focal").height()
let _panzoomWidth = $("#panzoom").width()
debugger
_data.nodes.forEach((val,index)=>{
val.nodeStyle.left = parseInt(val.nodeStyle.left) * _scale - (_panzoomWidth*_scale-_panzoomWidth)/2
val.nodeStyle.top = parseInt(val.nodeStyle.top) * _scale - (_panzoomWidth*_scale-_panzoomWidth)/2
})
// $("#panzoom").css({'width':_panzoomWidth*_scale+'px','height':_panzoomWidth*_scale+'px'})
this.chartData = _data;
this.$nextTick(() => {
this.chartData.nodes.forEach(item => {
this.initNode(item.id);
});
this.chartData.connections.forEach(item => {
let _connects = this.jsp.connect({
source: item.sourceId,
target: item.targetId
});
_connects.getOverlay("label").setLabel(item.label)
$(_connects.getOverlay("label").canvas).attr('mySourceId',item.sourceId)
$(_connects.getOverlay("label").canvas).attr('myTargetId',item.targetId)
});
});
})
.catch(err => {
console.log(err);
});
} else {
this.$nextTick(() => {
this.chartData.nodes.push({
id: "start",
icon: "el-icon-loading",
type: "circle",
text: "开始",
nodeStyle: {
top: "100px",
left: "300px"
}
});
this.$nextTick(() => {
this.jsp.batch(() => {
this.initNode(jsPlumb.getSelector("#start"));
});
});
});
}
},
changeNote(){//修改label
if(!this.labelName){
alert("名称没有填写")
return false
}
let _allConnections = this.jsp.getAllConnections();
_allConnections.forEach((val,index)=>{
if(val.sourceId == this.curSourceId && val.targetId == this.curTargetId ){
val.getOverlay("label").setLabel(this.labelName)
}
})
this.chartData.connections.forEach(val => {
if(val.sourceId == this.curSourceId && val.targetId == this.curTargetId ){
val.label = this.labelName
}
});
this.dialogVisible = false
},
handleClose(){
this.dialogVisible = false
}
},
components: {
ChartNode
}
};
</script>
<style lang="scss" scoped>
#test1{
position:relative;
width:90%;
height:90%;
border:1px solid #ddd;
background:#fff;
}
.box{
border-radius:50%;
text-align: center;
cursor: pointer;
background-color: white;
border: 1px solid #346789;
text-align: center;
z-index: 24;
cursor: pointer;
box-shadow: 2px 2px 19px #aaa;
-o-box-shadow: 2px 2px 19px #aaa;
-webkit-box-shadow: 2px 2px 19px #aaa;
-moz-box-shadow: 2px 2px 19px #aaa;
position: absolute;
color: black;
padding: 0.5em;
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
-webkit-transition: -webkit-box-shadow 0.15s ease-in;
-moz-transition: -moz-box-shadow 0.15s ease-in;
-o-transition: -o-box-shadow 0.15s ease-in;
transition: box-shadow 0.15s ease-in;
.ep {
opacity: 0;
position: absolute;
right: -10px;
top: 0;
width: 10px;
height: 10px;
background: #409eff;
border-radius: 5px;
}
&:hover {
.ep {
opacity: 1;
}
}
&.dragHover {
.ep {
opacity: 0;
}
}
}
.box:hover {
border: 1px solid #123456;
box-shadow: 2px 2px 19px #444;
-o-box-shadow: 2px 2px 19px #444;
-webkit-box-shadow: 2px 2px 19px #444;
-moz-box-shadow: 2px 2px 19px #fff;
opacity: 0.9;
}
.box:hover,
.box.jtk-source-hover,
.box.jtk-target-hover {
border: 1px solid orange;
color: orange;
}
.box1{
top:50px;
left:50px;
}
.box2{
top:160px;
left:250px;
}
.box3{
top:360px;
left:150px;
}
.box4{
top:350px;
left:450px;
}
.chart-dot-hover{
display: block;
background: red
}
.source{
position:absolute;
top:50px;
right:50px;
border:1px solid red;
width:200px;
height:300px;
li{
line-height:36px;
border:1px solid #ddd;
margin-bottom:10px;
cursor:pointer
}
}
</style>
<style>
.aLabel{
border: 1px solid blue;
padding: 4px;
}
.x{
top:-10px;
right:-10px;
cursor: pointer;
}
.jtk-overlay{
padding: 0
}
</style>