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/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组件: