jsplumb 流程图,常用功能配置记录
前言:
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>
jsplumb 流程图,常用功能配置记录的更多相关文章
- Nginx常用功能配置一
Nginx常用功能配置 参数include配置 说明:如果日常工作中server标签存在太多,可以采用include配置模式,Nginx的主配置文件包含的所有虚拟主机的子配置文件会统一放入extra目 ...
- Nginx常用功能配置二
Nginx常用功能配置二 Nginx location匹配设置 location作用:可以根据用户请求的URI来执行不同的应用,根据用户请求的网站的地址URL匹配. location语法: locat ...
- Apache运维中常用功能配置笔记梳理
Apache 是一款使用量排名第一的 web 服务器,LAMP 中的 A 指的就是它.由于其开源.稳定.安全等特性而被广泛使用.下边记录了使用 Apache 以来经常用到的功能,做此梳理,作为日常运维 ...
- nginx常用功能配置
一.规范优化nginx配置文件 nginx的主配置文件为nginx.conf,主配置文件包含的所有虚拟主机的子配置文件会统一放入extra目录中,虚拟主机的配置文件按照网站的域名或功能取名,例如www ...
- Ubuntu 常用环境配置记录
引言 经常使用 Ubuntu 虚拟机,双系统,WSL,服务器等等,每次配置常用开发环境都要去百度细节,故在此记录一下. 更换软件源 阿里云镜像 清华镜像 # 更新 sudo apt update &a ...
- vim 常用命令逐渐熟悉以及常用的配置记录
本篇博客记录的是我自己学习vim的常用命令的过程中,各个命令的逐渐熟悉的顺序(有一部分已经熟悉的命令没有列出),已经对vim编辑器本身的一些设置的记录 1.G 快速将光标切换到文件的最底部 2.u 撤 ...
- nginx下目录浏览及其验证功能配置记录
工作中常常有写不能有网页下载东西的需求,在Apache下搭建完成后直接导入文件即可达到下载/显示文件的效果;而Nginx的目录列表功能默认是关闭的,如果需要打开Nginx的目录列表功能,需要手动配置, ...
- Echarts图表常用功能配置,Demo示例
先看下效果图: 就如上图所示,都是些常用的基本配置. Legend分页,X轴设置,Y轴设置,底部缩放条设置, 数值显示样式设置,工具箱设置,自定义工具按钮, 绑定点击事件等等.这些配置代码中都做了简单 ...
- centos中apache自用常用额外配置记录(xwamp)
xwamp套件中apache配置,记录下,以免忘记. 配置路径 ${wwwroot_dir}/conf/httpd.conf 配置内容 <ifmodule mod_deflate.c> D ...
随机推荐
- vb开发最全教程
https://www.xin3721.com/eschool/VisualBasicenet/
- 003_硬件基础电路_LM2596
以下内容全部从文档中获取有用信息的 链接:https://pan.baidu.com/s/1fEbtY616bJWsuaDOZ0CUjw提取码:1byu 复制这段内容后打开百度网盘手机App,操作更方 ...
- sql server join ,inner join ,left join ,right join 的使用
测试数据脚本 CREATE TABLE Atable ( S# INT, Sname nvarchar(32), Sage INT, Sfrom nvarchar(8) ) insert into A ...
- File类的createNewFile()和mkdirs() mkdir()
createNewFile文件不存在则创建,存在则不创建并返回false,文件路径必须存在才可创建路径下的文件(注意它只能创建文件,即如果你给了/storage/emulated/0/hello/sn ...
- MongoDB空间分配
Mongodb占据的磁盘空间比MySQL大得多,可以理解文档数据如Json这种格式,存在许多冗余数据,但空间占用大得不正常,甚至是传统数据库的三四倍,不太契合工程实践,应该有改善的余地. 查阅了一些资 ...
- DOM操作2
一.API和WebAPI API就是接口,就是通道,负责一个程序和其他软件的沟通,本质是预先定义的函数. Web API是网络应用程序接口.包含了广泛的功能,网络应用通过API接口,可以实现存储服务. ...
- nginx + keepalived双活配置
一.安装nginx 我们这边默认安装的nginx的是1.12.2的版本,所以我们需要安装1.16.1版本的nginx的,才好,所以我们这边先更新yum源,步骤如下: 1.添加yum源: [root@s ...
- centos7的redis加哨兵系统
三台服务器 1.下载 wget http://download.redis.io/releases/redis-5.0.3.tar.gztar -zxvf redis-5.0.3.tar.gzcd r ...
- local模式运行spark-shell时报错 java.lang.IllegalArgumentException: Error while instantiating 'org.apache.spark.sql.hive.HiveSessionState':
先前在local模式下,什么都不做修改直接运行./spark-shell 运行什么问题都没有,然后配置过在HADOOP yarn上运行,之后再在local模式下运行出现以下错误: java.lang. ...
- luogu_P4767 [IOI2000]邮局
传送门 Description 高速公路旁边有一些村庄.高速公路表示为整数轴,每个村庄的位置用单个整数坐标标识.没有两个在同样地方的村庄.两个位置之间的距离是其整数坐标差的绝对值. 邮局将建在一些,但 ...