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); 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; 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+