写在最前,就是第一次写博客,不免感慨,可以直接跳过 O(∩_∩)O
这是自己第一次写博客,经验不充分,如果觉得代码不详细,文章底部有源码地址,欢迎大家下载。基本的功能都实现了,测试有限,如果发现问题,欢迎反馈,一起讨论。
参考文章:
jsPlumb插件做一个模仿viso的可拖拉流程图
jsplumb 中文基础教程
1.流程图节点可以拖拽添加
2.节点支持单击选中 backspace 和 delete 删除;双击改变内容;拖拽改变大小
3.节点支持连线
4.连线支持单击选中backspace 和 delete 删除;双击添加 label
5. label 支持单击选中 backspace 和 delete 删除;双击改变内容;拖拽改变大小
6.保存编辑好的流程图数据
7.根据已有数据绘制流程图
需要用到的样式和脚本
基本页面呈现:
流程编辑器
css部分:
*{margin:0;padding:0}
html,body,.flow{
height:100%;
font-size: 14px;
user-select: none;
}
/*layout*/
.flow-header{
width:100%;
height: 40px;
background: #1f88d6;
border-top: 1px solid #e0e0e0;
border-bottom: 1px solid #e0e0e0;
-webkit-box-shadow: 0 2px 0px #bbb;
box-shadow: 0 2px 0px #bbb;
font-size: 12px;
color:#ffffff;
padding: 0 30px;
box-sizing: border-box;
display: flex;
flex-direction:row;
justify-content:space-between;
align-items:center;
position: fixed;
top:0;
left:0;
z-index: 1000000000;
}
.flow-body{
width:100%;
height:100%;
padding-top:40px;
box-sizing: border-box;
}
.flow-menu{
float: left;
width:180px;
height:100%;
min-height: 400px;
background: whiteSmoke;
padding-top:20px;
box-sizing: border-box;
border-right: 1px solid #ddd;
overflow: auto;
-webkit-box-shadow: -1px 0px 5px #bbb inset;
box-shadow: -1px 0px 5px #bbb inset;
}
.flow-container{
height: 100%;
min-height: 400px;
overflow: auto;
background-color: #f2f2f2;
}
.flow-main{
width:5000px;
height:5000px;
background-image: url('../images/bg1.png');
background-repeat: repeat;
position: relative;
}
/*header*/
.flow-title{
font-size: 14px;
font-weight: 700;
}
.btn{
padding:3px 12px;
}
.btn-default{
color:#1f88d6;
}
.btn-default:link,.btn-default:visited,.btn-default:hover,.btn-default:active,.btn-default:focus {
color: #1f88d6;
background-color: #fdfdfd;
border-color: #adadad;
}
/*左侧菜单*/
.flow-menu>h5{
width:96%;
height:30px;
line-height: 30px;
background: #eeeeee;
text-indent: 20px;
font-size: 12px;
}
.flow-btns{
padding-left: 20px;
padding-top: 5px;
}
.flow-btn{
width:60px;
height:30px;
border:1px solid #aaaaaa;
background-color: #ffffff;
margin-bottom: 5px;
}
.flow-btns .btn-base{
border-radius: 0;
}
.flow-btns .btn-flow{
border-radius: 5px;
}
.flow-btns .btn-node{
border-radius: 15px;
}
.flow-btns .btn-judge{
border-radius: 50%;
}
.node-common{
width: 160px;
min-height:40px;
padding:8px 12px;
/*box-sizing: border-box;*/
font-size: 12px;
text-align: center;
line-height: 20px;
color:#1a1a1a;
border:2px solid #1a1a1a;
background-color: #ffffff;
position: absolute;
cursor: default;
overflow: hidden;
}
.flow-main .node-base{
border-radius: 0;
}
.flow-main .node-flow{
border-radius: 5px;
}
.flow-main .node-node{
border-radius: 18px;
}
.flow-main .node-judge{
border-radius: 50%;
}
.flow-main .node-focus{
border-color: #409eff;
}
.flow-input{
width:100%;
height: 100%;
position:absolute;
left:0;
top:0;
z-index: 100;
}
.hide-input{
position:absolute;
left:0;
top:-27px;
}
.line-label{
width: 80px;
min-height:40px;
padding:8px 12px;
font-size: 12px;
text-align: center;
line-height: 20px;
color:#1a1a1a;
background-color: #ffffff;
position: absolute;
z-index: 2000000;
cursor: default;
overflow: hidden;
}
.flow-main .label-focus{
border:1px solid #409eff;
}
.flow-main .label-blur{
border:none;
}
.label-input{
width:100%;
height: 100%;
position:absolute;
left:0;
top:0;
z-index:2000100;
}
关于 js 文件,data.js 用于存放流程图数据,config.js 用于一些基本的设置,index.js 用于主要逻辑。
config 基本设置:
/**
* 这里放置一些基本的设置
*/
// 流程图画布
var container = $('#flow-main');
// 用来区分流程图,是否为可编辑的状态。默认为可编辑
var isNew = true;
// 拖拽时,防止id重复。默认设为1
var _index = 0;
// 画布放大缩小。默认为1
var size = 1.0;
// 暂存流程图数据。
var flowData = {};
// 区分节点的单双击事件
var nodeTimes = null;
// 区分连接线的单双击事件
var lineTimes = null;
// 区分label的单双击事件
var labelTimes = null;
// 基本连接线样式
var connectorPaintStyle = {
lineWidth: 2,
strokeStyle: "#000000",
joinstyle: "round",
outlineColor: "white",
outlineWidth: 1
};
// 鼠标悬浮在连接线上的样式
var connectorHoverStyle = {
lineWidth: 2,
strokeStyle: "#000000",
outlineWidth: 1,
outlineColor: "white"
};
// 端点样式
var endpointStyle = {
endpoint: ["Dot", { radius: 8 }], //端点的形状
connectorStyle: connectorPaintStyle,//连接线的颜色,大小样式
// connectorHoverStyle: connectorHoverStyle,
paintStyle: {
strokeStyle: "#000000",
fillStyle: "transparent",
radius: 2,
lineWidth: 1
}, //端点的颜色样式
// anchor: dynamicAnchors,
isSource: true, //是否可以拖动(作为连线起点)
connector: ["Flowchart", { stub: [20, 30], gap: 5, cornerRadius: 3, alwaysRespectStubs: true }], //连接线的样式种类有[Bezier],[Flowchart],[StateMachine ],[Straight ]
isTarget: true, //是否可以放置(连线终点)
maxConnections: -1, // 设置连接点最多可以连接几条线
connectorOverlays: [["Arrow", { width: 10, length: 10, location: 1 }]]
};
1.左边的节点拖拽到右边画布。
功能实现完整代码:
/**
* 实现拖拽
*
*/
draggable:function(){
var that = this;
$("#flow-btns").children().draggable({
helper: "clone",
scope: "ss",
});
container.droppable({
scope: "ss",
drop: function (event, ui) {
if(!isNew){
return;
}
var left = parseInt(ui.offset.left - $(this).offset().left);
var top = parseInt(ui.offset.top - $(this).offset().top);
var type = ui.helper.context.dataset.type;
_index++;
var id =type + _index;
var dom = $('')
$(this).append(dom);
dom.css("left", left).css("top", top);
// 根据不同的类型,给节点设置不同的样式
switch (type) {
case "base":
dom.addClass('node-base');
break;
case "flow":
dom.addClass('node-flow');
break;
case "node":
dom.addClass('node-node');
break;
case "judge":
dom.addClass('node-judge');
break;
}
jsPlumb.addEndpoint(id, { anchors: "TopCenter" }, endpointStyle);
jsPlumb.addEndpoint(id, { anchors: "RightMiddle" }, endpointStyle);
jsPlumb.addEndpoint(id, { anchors: "BottomCenter" }, endpointStyle);
jsPlumb.addEndpoint(id, { anchors: "LeftMiddle" }, endpointStyle);
jsPlumb.draggable(id);
dom.draggable({ containment: "parent",grid: [10, 10] });
that.nodeClick(id);
}
});
return this;
},
首先,实现左边的节点可以拖拽。jquery 里的 draggable() 函数可以实现拖拽,helper:"clone"表示复制,scope:"ss"是一个标识,为了判断是否可以放置,droppable()方法里面也设置这个标识来判断拖放到的地方。
$("#flow-btns").children().draggable({
helper: "clone",
scope: "ss",
});
然后,实现节点在右边画布呈现。选择在拖拽完成时,向右边画布之中添加 dom 。
var dom = $('')
$(this).append(dom);
流程图中不同的节点设置了自己的样式,为了区分节点,在左边节点中设置了 data-type 属性,拖拽完成时,可以根据 type 的不同,为节点添加不同的类名,实现不同样式的呈现。
var type = ui.helper.context.dataset.type;
switch (type) {
case "base":
dom.addClass('node-base');
break;
case "flow":
dom.addClass('node-flow');
break;
case "node":
dom.addClass('node-node');
break;
case "judge":
dom.addClass('node-judge');
break;
}
作为节点,需要有一个唯一的标识,所以会为节点添加 id 属性。为了避免每次拖拽时 id 重复,声明 _index 变量,var _index = 0; ,每次拖拽时,_index++ 。
_index++;
var id = type + _index;
var dom = $('')
此外,在添加节点时,需要根据鼠标拖拽的位置,设置节点在画布中的位置。
var left = parseInt(ui.offset.left - $(this).offset().left);
var top = parseInt(ui.offset.top - $(this).offset().top);
dom.css("left", left).css("top", top);
最后,还需为节点添加端点,用于实现流程图里的连线功能。jsPlumb.addEndpoint(a,b,c) 方法,可以实现为节点添加端点。其中三个参数:a - 要添加端点的div的id;b - 设置端点放置的位置;c - 端点和连接线的样式。同时,画布中的节点依然支持可以拖拽,限制拖拽范围在父容器之内。{ containment: "parent" }
jsPlumb.addEndpoint(id, { anchors: "TopCenter" }, endpointStyle);
jsPlumb.addEndpoint(id, { anchors: "RightMiddle" }, endpointStyle);
jsPlumb.addEndpoint(id, { anchors: "BottomCenter" }, endpointStyle);
jsPlumb.addEndpoint(id, { anchors: "LeftMiddle" }, endpointStyle);
jsPlumb.draggable(id);
dom.draggable({ containment: "parent" });
2.添加节点操作
节点操作的完整代码:
/**
* 节点点击事件
*
*/
nodeClick:function(id){
var currentDom = $('#'+id);
// 单击选中,可删除
currentDom.click(function(){
clearTimeout(nodeTimes);
//执行延时
nodeTimes = setTimeout(function(){
container.children('.node-common').removeClass('node-focus');
currentDom.addClass('node-focus');
var input = $("");
setTimeout(function(){
input.focus();
},50)
currentDom.append(input);
currentDom.keydown(function (event) {
event=event||window.event
if(event.keyCode==8 || event.keyCode==46){ //8--backspace;46--delete
currentDom.remove();
jsPlumb.removeAllEndpoints(id);
return false;
}
});
},300);
})
// 双击添加文字
currentDom.dblclick(function () {
// 取消上次延时未执行的方法
clearTimeout(nodeTimes );
container.children('.node-common').removeClass('node-focus');
currentDom.addClass('node-focus');
var text = currentDom.children('span').text().replace(/(^\s*)|(\s*$)/g, "") || '';
currentDom.children('span').html("");
var input = $("");
setTimeout(function(){
input.focus();
},50)
currentDom.append(input);
currentDom.keydown(function (event) {
if(event.keyCode==13){
currentDom.children('span').html(currentDom.children("input.flow-input").val());
currentDom.children("input.flow-input").remove();
currentDom.removeClass('node-focus');
jsPlumb.repaintEverything();
return false;
}
});
});
// 拖拽改变大小
currentDom.resizable({
autoHide: true ,
minHeight: 36,
minWidth:150,
containment: "parent",
resize: function (event, ui) {
jsPlumb.repaint(ui.helper)
jsPlumb.repaintEverything()
}
})
},
2-1.单击选中节点,Backspace 和 Delete 可以删除节点。
思路:单击节点,通过样式改变,使节点处于选中的状态(我这里只简单的改变了边框的样式),然后给节点添加键盘事件,当按下 Backspace 和 Delete 时,移除节点 。
实现过程中,在添加键盘事件的时候,遇到了问题,键盘事件不触发。解决方法:在单击时,为节点动态的添加了 input 输入框,并且输入框自动获取焦点,同时,通过样式设置,隐藏 input 输入框。需要注意的是,自动获取焦点的时候,需要延时,否则获取焦点时,会有问题。
container.children('.node-common').removeClass('node-focus');
currentDom.addClass('node-focus');
var input = $("");
setTimeout(function(){
input.focus();
},50)
currentDom.append(input);
currentDom.keydown(function (event) {
event=event||window.event
if(event.keyCode==8 || event.keyCode==46){ //8--backspace;46--delete
currentDom.remove();
return false;
}
});
移除节点的同时,也需要将节点周围的 endPoint 移除掉。jsPlumb.removeAllEndpoints(id);可以实现移除 endPoint。
jsPlumb.removeAllEndpoints(id);
2-2.双击节点,添加/修改节点文字
思路:双击时,为节点动态添加 input 输入框,输入内容后, Enter 键,将 input 输入框的值赋给节点。
container.children('.node-common').removeClass('node-focus');
currentDom.addClass('node-focus');
var text = currentDom.children('span').text().replace(/(^\s*)|(\s*$)/g, "") || '';
currentDom.children('span').html("");
var input = $("");
setTimeout(function(){
input.focus();
},50)
currentDom.append(input);
currentDom.keydown(function (event) {
if(event.keyCode==13){
currentDom.children('span').html(currentDom.children("input.flow-input").val());
currentDom.children("input.flow-input").remove();
currentDom.removeClass('node-focus');
return false;
}
});
这里有一个问题,如果添加的文字内容过多,节点通过样式设置,实现了自适应,但是节点周围的 endPoint 的位置,没有改变。解决办法,jsplumb 提供了一个重绘的方法,故在赋值完成后,重新绘制。问题解决。
jsPlumb.repaintEverything();
节点同时拥有单双击事件,需要加以区分。解决办法:
// 单击事件中
clearTimeout(nodeTimes);
//执行延时
nodeTimes = setTimeout(function(){
// 执行单击事件
},300);
// 双击事件中
// 取消上次延时未执行的方法
clearTimeout(nodeTimes );
// 执行双击事件
2-3.拖拽改变节点大小
因为节点内容的多少是不确定的,所以,支持拖拽改变节点大小。
jQuery 的 resizable()方法可以实现改变节点大小。同时,节点大小变化之后, endpoint 的位置也需要变化,所以这里也需要重绘。
// 拖拽改变大小
currentDom.resizable({
autoHide: true ,
minHeight: 36,
minWidth:150,
containment: "parent",
resize: function (event, ui) {
jsPlumb.repaint(ui.helper)
jsPlumb.repaintEverything()
}
})
项目地址:https://github.com/smile1828/demo-jsPlumb
基于 jsPlumb 的流程图编辑器的实现 (二,连接线的操作)
基于 jsPlumb 的流程图编辑器的实现 (三,document的操作)
基于 jsPlumb 的流程图编辑器的实现 (四,按钮的操作)