使用jsPlumb制作流程图设计器

jsPlumb是一个比较强大的绘图组件,它提供了一种方法,主要用于连接网页上的元素。在现代浏览器中,它使用SVG或者Canvas技术,而对于IE8以下(含IE8)的古董浏览器,则使用VML技术。

项目主页:http://jsplumbtoolkit.com/

GitHub:https://github.com/sporritt/jsPlumb

作为插件,主要支持jQuery/MooTools/YUI3三种js库,目前最新版本为1.4.1。其中作为jQuery的插件需要用到jQuery、jQuery UI,建议使用最新版本的库避免一些bug。

本文主要使用jQuery 1.9.0、jQuery UI 1.9.2、jsPlumb 1.4.1来绘制流程图。

资源准备

下载jsPlumb,用到以下几个文件:

  • build/js/jquery.jsPlumb-1.4.1-all.min.js
  • build/lib/jquery-1.9.0-min.js
  • build/lib/jquery-ui-1.9.2-min.js
  • build/lib/jquery.ui.touch-punch.min.js (可选) 用于触摸支持

以及build/demo/js/demo-helper-jquery.js,主要用于绘图模式的切换,调整为如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
jsPlumb.bind( "ready" , function () {
   // chrome fix.
   document.onselectstart = function () { return  false ; };
   // render mode
   var  resetRenderMode = function (desiredMode) {
     var  newMode = jsPlumb.setRenderMode(desiredMode);
     $( ".rmode" ).removeClass( "selected" );
     $( ".rmode[mode='"  + newMode + "']" ).addClass( "selected" );
     $( ".rmode[mode='canvas']" ).attr( "disabled" , !jsPlumb.isCanvasAvailable());
     $( ".rmode[mode='svg']" ).attr( "disabled" , !jsPlumb.isSVGAvailable());
     $( ".rmode[mode='vml']" ).attr( "disabled" , !jsPlumb.isVMLAvailable());
     nodeFlow.init();
   };
   $( ".rmode" ).bind( "click" , function () {
     var  desiredMode = $( this ).attr( "mode" );
     if  (jsPlumbDemo.reset) jsPlumbDemo.reset();
     jsPlumb.reset();
     resetRenderMode(desiredMode);
   });
   resetRenderMode(jsPlumb.SVG);
});

再准备css样式(从flowchartDemo.css调整而来):

1
2
3
4
5
6
7
8
9
10
11
12
.node { border : 1px  solid  #346789 ; 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 ; -moz-border-radius: 0.5em ; border-radius: 0.5em ; opacity: 0.8 ; filter: alpha(opacity= 80 ); width : 7em ; height : 5em ; line-height : 5em ; text-align : center ; z-index : 20 ; position : absolute ; background-color : #eeeeef ; color : black ; font-family : helvetica ; padding : 0.5em ; font-size : 1em ; }
   .node:hover { 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  #444 ; opacity: 0.8 ; filter: alpha(opacity= 80 ); }
 
._jsPlumb_connector { z-index : 4 ; }
._jsPlumb_endpoint { z-index : 21 ; cursor : pointer ; }
._jsPlumb_dragging { z-index : 4000 ; }
 
.dragHover { border : 1px  dotted  red ; }
 
.aLabel { background-color : white ; padding : 0.4em ; font : 12px  sans-serif ; color : #444 ; z-index : 21 ; border : 1px  dotted  gray ; opacity: 0.8 ; filter: alpha(opacity= 80 ); }
 
.ep { position : absolute ; right : 5px ; top : 5px ; width : 1em ; height : 1em ; background-color : #994466 ; cursor : pointer ; }

最终引入的资源如下:

1
2
3
4
5
6
7
< script  src='js/jquery-1.9.0.min.js'></ script >
  < script  src='js/jquery-ui-1.9.2.min.js'>
< link  href="css/demo.css" rel="stylesheet" />
< script  src="js/jquery.jsPlumb-1.4.1-all-min.js"></ script >
< script  src="js/jquery.ui.touch-punch.min.js"></ script >
< script  src="js/demo.init.js"></ script >
< script  src="js/demo-helper-jquery.js"></ script >

主要实现

参照例子中的Flowchart以及State Machine,实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
; ( function () {
   window.nodeFlow = {
     init: function () {
       // 设置点、线的默认样式
       jsPlumb.importDefaults({
         DragOptions: { cursor: 'pointer' , zIndex: 2000 },
         Endpoint: [ "Dot" , { radius: 1 }],
         HoverPaintStyle: { strokeStyle: "#42a62c" , lineWidth: 2 },
         ConnectionOverlays: [
           [ "Arrow" , { location: -7, id: "arrow" , length: 14, foldback: 0.8 }],
           [ "Label" , { location: 0.1, id: "label"  }]
         ]
       });
       // 连接事件
       jsPlumb.bind( "jsPlumbConnection" , function (conn, originalEvent) {
         if  (conn.connection.sourceId == conn.connection.targetId) {
           jsPlumb.detach(conn);
           alert( "不能连接自己!" );
         }
         $.each(jsPlumb.getEndpoints(conn.source), function (i, el) {
           if  (conn.connection != el.connections[0] &&
             (el.connections[0].targetId == conn.targetId || (el.connections[0].sourceId == conn.targetId && el.connections[0].targetId == conn.sourceId))) {
             jsPlumb.detach(conn);
             alert( "不能重复连接!" );
             return  false ;
           }
         });
 
         nodeFlow.onConnectionChange && nodeFlow.onConnectionChange(conn);
         conn.connection.bind( "editCompleted" , function (o) {
           if  ( typeof  console != "undefined" )
             console.log( "connection edited. path is now " , o.path);
         });
       });
       // 取消连接事件
       jsPlumb.bind( "jsPlumbConnectionDetached" , function (conn) {
         nodeFlow.onConnectionChange && nodeFlow.onConnectionChange(conn);
       });
       // 双击取消连接
       jsPlumb.bind( "dblclick" , function (conn, originalEvent) {
         jsPlumb.detach(conn);
       });
       // 连接的元素
       // 本例中.node既是源头又是目标
       var  nodeList = $( ".node" );
       nodeList.each( function (i, e) {
         // 设置连接的源元素
         jsPlumb.makeSource($(e), {
           filter: ".ep" , // .ep元素用于拖动连接
           anchor: "Continuous" ,
           connector: [ "Flowchart" , { curviness: 20 }], // 连接的方式为流程图
           connectorStyle: { strokeStyle: "#014ae1" , lineWidth: 2 },
           maxConnections: -1 // 最大连接数不限
         });
       });
       // 设置连接目标
       jsPlumb.makeTarget(nodeList, {
         dropOptions: { hoverClass: "dragHover"  },
         anchor: "Continuous"
       });
       // 初始化所有连接元素为可拖动
       jsPlumb.draggable(nodeList);
     }
   };
})();

 

保存及载入状态

创建如下html结构作为测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
< asp:HiddenField  runat="server" ID="connections" /> <!--保存连接-->
< asp:HiddenField  runat="server" ID="locations" /> <!--保存元素位置-->
< div  class="nodeWrapper" style="height:100%;">
   < div  class="node" id='node1' data-id="1">
     < div  class="ep"></ div >
     < strong >节点1</ strong >
   </ div >
   < div  class="node" id='node1' data-id="1">
     < div  class="ep"></ div >
     < strong >节点1</ strong >
   </ div >
   < div  class="node" id='node2' data-id="2">
     < div  class="ep"></ div >
     < strong >节点2</ strong >
   </ div >
   < div  class="node" id='node3' data-id="3">
     < div  class="ep"></ div >
     < strong >节点3</ strong >
   </ div >
</ div >

在连接状态改变、表单提交时保存连接数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 连接改变时把所有的节点位置、连接以JSON格式存入到隐藏域中
nodeFlow.onConnectionChange = function () {
   var  connections = [], locations = [], conns = jsPlumb.getAllConnections();
   $.each(conns, function (scopeName, scopeConnections) {
     $.each(scopeConnections, function (i, el) {
       locations.push($.extend(el.source.offset(), { nodeId: el.source.data( "id" ) }));
       locations.push($.extend(el.target.offset(), { nodeId: el.target.data( "id" ) }));
       connections.push({ source: el.source.data( "id" ), target: el.target.data( "id" ) });
     });
   });
   $( "input[id$=connections]" ).val(JSON.stringify(connections));
   $( "input[id$=locations]" ).val(JSON.stringify(locations));
};
// 提交表单时更新连接数据
$( ":submit" ).click(nodeFlow.onConnectionChange);

通过以上代码,即可以在表单提交时把流程图的状态保存到数据库。

载入数据

调整html代码如下:

1
2
3
4
5
6
7
8
9
10
< div  class="nodeWrapper" style="height:100%;">
   < asp:Repeater  runat="server" ID="nodeList">
     < ItemTemplate >
       < div  class="node" id='node<%#Eval("nodeId") %>' data-id="<%#Eval("nodeId") %>" style="<%#GetLocation((int)Eval("nodeId"))%>">
         < div  class="ep"></ div >
         < strong ><%#Eval("nodeName") %></ strong >
       </ div >
     </ ItemTemplate >
   </ asp:Repeater >
</ div >

从数据库获取节点、位置、连接数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
/// <summary>
/// 节点信息
/// </summary>
public  class  NodeItem
{
     public  int  NodeId { get ; set ; }
     public  string  NodeName { get ; set ; }
}
/// <summary>
/// 节点位置信息
/// </summary>
public  class  NodeLocation
{
     public  int  NodeId { get ; set ; }
     public  double  Left { get ; set ; }
     public  double  Top { get ; set ; }
}
/// <summary>
/// 节点连接信息
/// </summary>
public  class  NodeConnection
{
     public  int  Source { get ; set ; }
     public  int  Target { get ; set ; }
}
 
List<NodeLocation> locationData;
 
protected  void  Page_Load( object  sender, EventArgs e)
{
     if  (!IsPostBack)
     {
         var  nodeData = new  List<NodeItem>
         {
             new  NodeItem{NodeId=1, NodeName= "节点1" },
             new  NodeItem{NodeId=2, NodeName= "节点2" },
             new  NodeItem{NodeId=3, NodeName= "节点3" }
         };
         nodeList.DataSource = nodeData;
         nodeList.DataBind();
         
         // 从数据库获取位置以及连接
         locationData = JsonConvert.DeserializeObject<List<NodeLocation>>(locationString);
         var  connectionData = JsonConvert.DeserializeObject<List<NodeConnection>>(connectionString);
 
         // 连接所有节点
         var  builder = new  StringBuilder();
         builder.Append( "jsPlumb.bind(\"ready\", function() {" );
         connectionData.ForEach(c =>
         {
             builder.AppendFormat( "jsPlumb.connect({{source: 'node{0}', target: 'node{1}'}});" , c.Source.ToString(), c.Target.ToString());
         });
         builder.Append( "});" );
     }
}
 
/// <summary>
/// 获取位置
/// </summary>
protected  string  GetLocation( int  nodeId)
{
     var  ll = locationData.FirstOrDefault(l => l.NodeId == nodeId);
     if  (ll != null )
         return  "left:"  + ll.Left.ToString() + "px;top:"  + ll.Top.ToString() + "px;" ;
     return  string .Empty;
}

其中,每次载入时,都需要获取所有连接的数据,并通过脚本把所有节点连接起来。

 

结尾

以上功能只用到jsPlumb少量API,实现起来都比较简单。更多的功能参考官方文档API文档进行扩展。

在使用jsPlumb之前也看过一些其他的js组件:

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值