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组件:
- Raphaël:绘图功能非常强大,但是没有提供更直接的例子,需要花费较多的开发时间
- D3:理由同上
- JointJS:对IE支持不好
- JavaScript InfoVis Toolkit:操作起来比较复杂,表现形式较少
- jQuery OrgChart:只能用来呈现组织结构图,不能进行编辑
- jssvggraph:只有呈现不具备编辑功能
- JS Flowchart:操作复杂,界面不够现代
- ternlight:缺乏API文档
- Strawberry:国人开发的,运行效果似乎不太良好(ie10)
- Diagramo:功能强大、操作不简单,而且使用php
- WireIt:demo太过简单,而且在ie下表现不佳
- jGraphUI:操作不够简单$10
- creately:商业软件
- mxGraph:商业软件,价格高昂$5000+
- MindFusion:商业软件$300+
- Draw2D touch:商业软件499 €
- yworks:商业软件$5000+
- GoJS HTML5 Canvas:商业软件$2795+