基于 jsPlumb 的流程图编辑器的实现 (一,节点的操作)

写在最前,就是第一次写博客,不免感慨,可以直接跳过 O(∩_∩)O

这是自己第一次写博客,经验不充分,如果觉得代码不详细,文章底部有源码地址,欢迎大家下载。基本的功能都实现了,测试有限,如果发现问题,欢迎反馈,一起讨论。

参考文章:

jsPlumb插件做一个模仿viso的可拖拉流程图

jsplumb 中文基础教程


功能点:

1.流程图节点可以拖拽添加

2.节点支持单击选中 backspace 和 delete 删除;双击改变内容;拖拽改变大小

3.节点支持连线

4.连线支持单击选中backspace 和 delete 删除;双击添加 label

5. label 支持单击选中 backspace 和 delete 删除;双击改变内容;拖拽改变大小

6.保存编辑好的流程图数据

7.根据已有数据绘制流程图

 

效果图:

 

准备工作:

需要用到的样式和脚本

<link href="https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.bootcss.com/jqueryui/1.12.1/jquery-ui.min.css" rel="stylesheet">

<script src="https://cdn.bootcss.com/jquery/1.12.1/jquery.min.js"></script>
<script src="https://cdn.bootcss.com/jqueryui/1.12.1/jquery-ui.min.js"></script>
<script src="https://cdn.bootcss.com/jsPlumb/1.7.6/jquery.jsPlumb.min.js"></script>

基本页面呈现:

  <div class="flow">
      <div class="flow-header">
        <div class="flow-title">流程编辑器</div>
        <div>
          <button id="delete" class="btn btn-default">清空</button>
          <button id="update" class="btn btn-default">编辑</button>
          <button id="save" class="btn btn-default">保存</button>
        </div>       
      </div>
      <div class="flow-body">
        <div class="flow-menu">
          <h5>节点类型列表</h5>
          <div id="flow-btns" class="flow-btns">
            <div class="flow-btn btn-base" data-type="base"></div>
            <div class="flow-btn btn-flow" data-type="flow"></div>
            <div class="flow-btn btn-node" data-type="node"></div>
            <div class="flow-btn btn-judge" data-type="judge"></div>
          </div> 
        </div>
        <div  class="flow-container" >
          <div class="flow-main" id="flow-main">
            
          </div>
        </div>
      </div>      
    </div>

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 = $('<div class="node-common" id="' + id + '" data-type="'+type+'"><span class="node-text"></span></div>')
                    $(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 = $('<div class="node-common" ><span class="node-text"></span></div>')
$(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 = $('<div class="node-common" id="' + id + '"><span class="node-text"></span></div>')

此外,在添加节点时,需要根据鼠标拖拽的位置,设置节点在画布中的位置。

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 = $("<input type='text' class='hide-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 = $("<input type='text' class='flow-input' value='" + text + "' />");
                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 = $("<input type='text' class='hide-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 = $("<input type='text' class='flow-input' value='" + text + "' />");
                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 的流程图编辑器的实现 (四,按钮的操作)

  • 6
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 31
    评论
JSPlumb是一个流程图库,可以帮助实现各种流程图的绘制和交互。下面我将以一个简单的例子来说明如何使用JSPlumb实现流程图。 首先,需要引入JSPlumb的库文件: ```html <script src="jsplumb.min.js"></script> ``` 接下来,创建一个容器来放置流程图的元素,例如: ```html <div id="flowchart"></div> ``` 然后,初始化JSPlumb: ```javascript jsPlumb.ready(function() { // 创建一个连接实例 var instance = jsPlumb.getInstance(); // 设置连接线的样式 var connectorStyle = { strokeWidth: 2, stroke: '#4178be', joinstyle: 'round', outlineStroke: 'white', outlineWidth: 2 }; // 设置端点的样式 var endpointStyle = { endpoint: 'Dot', paintStyle: { fill: '#4178be', radius: 4 }, isSource: true, connectorStyle: connectorStyle, isTarget: true, connectorHoverStyle: connectorStyle, hoverPaintStyle: { stroke: '#1e8151', strokeWidth: 3 } }; // 在容器中添加节点 var node1 = instance.addEndpoint('flowchart', { anchor: 'Right', endpoint: endpointStyle }); var node2 = instance.addEndpoint('flowchart', { anchor: 'Left', endpoint: endpointStyle }); // 连接两个节点 instance.connect({ source: node1, target: node2, paintStyle: connectorStyle }); }); ``` 在上面的代码中,首先创建了一个连接实例,然后设置了连接线和端点的样式。接下来,在容器中添加了两个节点,并使用`addEndpoint`方法来设置节点的锚点和端点的样式。最后,使用`connect`方法来连接两个节点。 通过以上的步骤,就可以使用JSPlumb实现一个简单的流程图。当然,JSPlumb还有更多的功能和选项可以进行配置,可以根据实际需求进行调整和扩展。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 31
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值