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,主要用於繪圖模式的切換,調整為如下代碼:
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調整而來):
.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; }
最終引入的資源如下:
<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,實現如下:
; (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結構作為測試:
<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>
在連接狀態改變、表單提交時保存連接數據:
// 連接改變時把所有的節點位置、連接以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代碼如下:
<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>
從數據庫獲取節點、位置、連接數據:
/// <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+
