原貼:https://www.cnblogs.com/sggx/p/3836432.html
前言
這是我第一次寫博客,心情還是有點小小的激動!這次主要分享的是用jsPlumb,做一個可以給用戶自定義拖拉的流程圖,並且可以序列化保存在服務器端。
我在這次的實現上面做得比較粗糙,還有分享我在做jsPlumb流程圖遇到的一些問題。
准備工作
制作流程圖用到的相關的腳本:
1 <script src="<%= ResolveUrl("~/resources/jquery/jquery-1.11.1.min.js")%>" type="text/javascript"></script>
2 <script src="<%= ResolveUrl("~/resources/jquery-ui-1.10.4/js/jquery-ui-1.10.4.min.js") %>" type="text/javascript"></script>
3 <script src="<%= ResolveUrl("~/resources/jquery-plugins/jquery.jsPlumb-1.6.2-min.js") %>" type="text/javascript"></script>
jsPlumb-1.6.2-min.js在官網上下載,這里用得是最新版本。jquery-1.11.1.min.js等腳本百度上都能找到,這里就不多說了。
css樣式在官網里也可以搜到,這里我就貼出來。
.node { 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); border: 1px solid #346789; width: 150px; /*line-height: 40px;*/ text-align: center; z-index: 20; position: absolute; background-color: #eeeeef; color: black; padding: 10px; font-size: 9pt; cursor: pointer; height: 50px; line-height: 50px; } .radius { border-radius: 25em; } .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官網上的api全是英文的,博主我從小英文就不好,所以看里面的doc非常費勁,一般都是一邊開着金山翻譯,
一邊看着文檔,英語好的略過這段。
正文
言歸正傳,現在開始我們的jsPlumb流程圖制作,下面先附上流程圖。

功能
根據客戶的要求,我們要完成的功能點有以下幾點:
1.支持將左邊的div層復制拖拉到右邊中間的層,並且左邊同一個div拖拉沒有次數限制,如果只能拖拉一次,做這個東西就沒有什么意義了。
2.拖拉到中間的div層可以拖動,拖動不能超過中間div的邊框。
3.拖動到中間的層,四周能有4個endpoint點,可供客戶連線。
4.能支持刪除多余的div的功能。
5.支持刪除連接線。
6.能雙擊修改流程圖的文字。
7.能序列化保存流程圖。
操作
下面我們根據功能開始制作:
1.拖拉jsPlumb其實是提供draggable方法,和droppable方法官網里有介紹, 但是我這里用得是jquery里的draggable()和droppable()。
<div id="left">
<div class="node radius" id="node1">開始</div>
<div class="node" id="node2">流程</div>
<div class="node" id="node3">判斷</div>
<div class="node radius" id="node4">結束</div>
</div>
<div id="right">
<p>拖拉到此區域</p>
</div>
<div id="save">
<input type="button" value="保存" onclick="save()" />
</div>
1 $("#left").children().draggable({
2 helper: "clone",
3 scope: "ss",
4 });
helper:"clone"表示復制,scope:"ss"是一個標識為了判斷是否可以放置,主要用於droppable方法里面也設置這個標識來判斷拖放到的地方,
除非兩個都不寫scope,可以隨便拖放,但是會有一個問題,每次我從左邊拖東西到右邊,我再拖到的時候就會有div拖到不了,所以最好設置
scope:"//里面的值隨便,只是一個標識"。
下面是完整的拖放:
$("#left").children().draggable({
helper: "clone",
scope: "ss",
});
$("#right").droppable({
scope: "ss",
drop: function (event, ui) {
var left = parseInt(ui.offset.left - $(this).offset().left);
var top = parseInt(ui.offset.top - $(this).offset().top);
var name = ui.draggable[0].id;
switch (name) {
case "node1":
i++;
var id = "state_start" + i;
$(this).append('<div class="node" style="border-radius: 25em" id="' + id + '" >' + $(ui.helper).html() + '</div>');
$("#" + id).css("left", left).css("top", top);
jsPlumb.addEndpoint(id, { anchors: "TopCenter" }, hollowCircle);
jsPlumb.addEndpoint(id, { anchors: "RightMiddle" }, hollowCircle);
jsPlumb.addEndpoint(id, { anchors: "BottomCenter" }, hollowCircle);
jsPlumb.addEndpoint(id, { anchors: "LeftMiddle" }, hollowCircle);
jsPlumb.draggable(id);
$("#" + id).draggable({ containment: "parent" });
doubleclick("#" + id);
break;
case "node2":
i++;
id = "state_flow" + i;
$(this).append("<div class='node' id='" + id + "'>" + $(ui.helper).html() + "</div>");
$("#" + id).css("left", left).css("top", top);
jsPlumb.addEndpoint(id, { anchors: "TopCenter" }, hollowCircle);
jsPlumb.addEndpoint(id, { anchors: "RightMiddle" }, hollowCircle);
jsPlumb.addEndpoint(id, { anchors: "BottomCenter" }, hollowCircle);
jsPlumb.addEndpoint(id, { anchors: "LeftMiddle" }, hollowCircle);
jsPlumb.addEndpoint(id, hollowCircle);
jsPlumb.draggable(id);
$("#" + id).draggable({ containment: "parent" });
doubleclick("#" + id);
break;
case "node3":
i++;
id = "state_decide" + i;
$(this).append("<div class='node' id='" + id + "'>" + $(ui.helper).html() + "</div>");
$("#" + id).css("left", left).css("top", top);
jsPlumb.addEndpoint(id, { anchors: "TopCenter" }, hollowCircle);
jsPlumb.addEndpoint(id, { anchors: "RightMiddle" }, hollowCircle);
jsPlumb.addEndpoint(id, { anchors: "BottomCenter" }, hollowCircle);
jsPlumb.addEndpoint(id, { anchors: "LeftMiddle" }, hollowCircle);
jsPlumb.addEndpoint(id, hollowCircle);
jsPlumb.draggable(id);
$("#" + id).draggable({ containment: "parent" });
doubleclick("#" + id);
break;
case "node4":
i++;
id = "state_end" + i;
$(this).append('<div class="node" style="border-radius: 25em" id="' + id + '" >' + $(ui.helper).html() + '</div>');
$("#" + id).css("left", left).css("top", top);
jsPlumb.addEndpoint(id, { anchors: "TopCenter" }, hollowCircle);
jsPlumb.addEndpoint(id, { anchors: "RightMiddle" }, hollowCircle);
jsPlumb.addEndpoint(id, { anchors: "BottomCenter" }, hollowCircle);
jsPlumb.addEndpoint(id, { anchors: "LeftMiddle" }, hollowCircle);
jsPlumb.draggable(id);
$("#" + id).draggable({ containment: "parent" });
doubleclick("#" + id);
break;
}
}
});
1 $("#left").children().draggable({ 2 helper: "clone", 3 scope: "ss", 4 }); 5 $("#right").droppable({ 6 scope: "ss", 7 drop: function (event, ui) { 8 var left = parseInt(ui.offset.left - $(this).offset().left); 9 var top = parseInt(ui.offset.top - $(this).offset().top); 10 var name = ui.draggable[0].id; 11 switch (name) { 12 case "node1": 13 i++; 14 var id = "state_start" + i; 15 $(this).append('<div class="node" style="border-radius: 25em" id="' + id + '" >' + $(ui.helper).html() + '</div>'); 16 $("#" + id).css("left", left).css("top", top); 17 jsPlumb.addEndpoint(id, { anchors: "TopCenter" }, hollowCircle); 18 jsPlumb.addEndpoint(id, { anchors: "RightMiddle" }, hollowCircle); 19 jsPlumb.addEndpoint(id, { anchors: "BottomCenter" }, hollowCircle); 20 jsPlumb.addEndpoint(id, { anchors: "LeftMiddle" }, hollowCircle); 21 jsPlumb.draggable(id); 22 $("#" + id).draggable({ containment: "parent" }); 23 doubleclick("#" + id); 24 break; 25 case "node2": 26 i++; 27 id = "state_flow" + i; 28 $(this).append("<div class='node' id='" + id + "'>" + $(ui.helper).html() + "</div>"); 29 $("#" + id).css("left", left).css("top", top); 30 jsPlumb.addEndpoint(id, { anchors: "TopCenter" }, hollowCircle); 31 jsPlumb.addEndpoint(id, { anchors: "RightMiddle" }, hollowCircle); 32 jsPlumb.addEndpoint(id, { anchors: "BottomCenter" }, hollowCircle); 33 jsPlumb.addEndpoint(id, { anchors: "LeftMiddle" }, hollowCircle); 34 jsPlumb.addEndpoint(id, hollowCircle); 35 jsPlumb.draggable(id); 36 $("#" + id).draggable({ containment: "parent" }); 37 doubleclick("#" + id); 38 break; 39 case "node3": 40 i++; 41 id = "state_decide" + i; 42 $(this).append("<div class='node' id='" + id + "'>" + $(ui.helper).html() + "</div>"); 43 $("#" + id).css("left", left).css("top", top); 44 jsPlumb.addEndpoint(id, { anchors: "TopCenter" }, hollowCircle); 45 jsPlumb.addEndpoint(id, { anchors: "RightMiddle" }, hollowCircle); 46 jsPlumb.addEndpoint(id, { anchors: "BottomCenter" }, hollowCircle); 47 jsPlumb.addEndpoint(id, { anchors: "LeftMiddle" }, hollowCircle); 48 jsPlumb.addEndpoint(id, hollowCircle); 49 jsPlumb.draggable(id); 50 $("#" + id).draggable({ containment: "parent" }); 51 doubleclick("#" + id); 52 break; 53 case "node4": 54 i++; 55 id = "state_end" + i; 56 $(this).append('<div class="node" style="border-radius: 25em" id="' + id + '" >' + $(ui.helper).html() + '</div>'); 57 $("#" + id).css("left", left).css("top", top); 58 jsPlumb.addEndpoint(id, { anchors: "TopCenter" }, hollowCircle); 59 jsPlumb.addEndpoint(id, { anchors: "RightMiddle" }, hollowCircle); 60 jsPlumb.addEndpoint(id, { anchors: "BottomCenter" }, hollowCircle); 61 jsPlumb.addEndpoint(id, { anchors: "LeftMiddle" }, hollowCircle); 62 jsPlumb.draggable(id); 63 $("#" + id).draggable({ containment: "parent" }); 64 doubleclick("#" + id); 65 break; 66 } 67 } 68 });
怎么樣把左邊的層復制到右邊的層,我的做法是這樣的:
1 $(this).append('<div class="node" style="border-radius: 25em" id="' + id + '" >' + $(ui.helper).html() + '</div>');
做到這里會有人奇怪,怎么做到左邊能拉無數次append到右邊,id這樣不會沖突嗎?我就在外面var i=0; 當有元素拖放到右邊的div時,i++;
然后var id="state_start"+i;拼接起來,這樣你的id就不會一樣了。
然后再設置div的left和top:
drop: function (event, ui) {
var left = parseInt(ui.offset.left - $(this).offset().left);
var top = parseInt(ui.offset.top - $(this).offset().top);
$("#" + id).css("left", left).css("top", top);
2.拖拉到中間的div層可以拖動,拖動不能超過中間div的邊框:
jsPlumb.draggable(id);
$("#" + id).draggable({ containment: "parent" });
3.拖動到中間的層,四周能有4個endpoint點,可供客戶連線:
這個功能是本文的重點,如何通過jsPlumb初始化端點和構造端點(endpoint)。
3.1 初始化端點樣式設置:主要設置一些基本的端點,連接線的樣式,里面的屬性不設置,默認使用默認值
//基本連接線樣式 var connectorPaintStyle = { lineWidth: 4, strokeStyle: "#61B7CF", joinstyle: "round", outlineColor: "white", outlineWidth: 2 }; // 鼠標懸浮在連接線上的樣式 var connectorHoverStyle = { lineWidth: 4, strokeStyle: "#216477", outlineWidth: 2, outlineColor: "white" }; var hollowCircle = { endpoint: ["Dot", { radius: 8 }], //端點的形狀 connectorStyle: connectorPaintStyle,//連接線的顏色,大小樣式 connectorHoverStyle: connectorHoverStyle, paintStyle: { strokeStyle: "#1e8151", fillStyle: "transparent", radius: 2, lineWidth: 2 }, //端點的顏色樣式 //anchor: "AutoDefault", isSource: true, //是否可以拖動(作為連線起點) connector: ["Flowchart", { stub: [40, 60], gap: 10, cornerRadius: 5, alwaysRespectStubs: true }], //連接線的樣式種類有[Bezier],[Flowchart],[StateMachine ],[Straight ] isTarget: true, //是否可以放置(連線終點) maxConnections: -1, // 設置連接點最多可以連接幾條線 connectorOverlays: [["Arrow", { width: 10, length: 10, location: 1 }]] };
3.2 構造端點(endpoint):怎樣將端點添加到div的四周?
jsPlumb.addEndpoint(id, { anchors: "TopCenter" }, hollowCircle);
jsPlumb.addEndpoint(id, { anchors: "RightMiddle" }, hollowCircle);
jsPlumb.addEndpoint(id, { anchors: "BottomCenter" }, hollowCircle);
jsPlumb.addEndpoint(id, { anchors: "LeftMiddle" }, hollowCircle);
通過jsPlumb.addEndpoint(a,b,c)里面有三個參數,a:要添加端點的div的id;b:設置端點放置的位置("TopCenter","RightMiddle","BottomCenter","LeftMiddle")
四個初始位置;c:端點和連接線的樣式。b,c(可選).
添加多個端點:jsPlumb.addEndpoints(a,b,c)三個參數 c(可選),a:要添加端點的div的id;b:含端點的構造函數參數的對象列表;
舉個例子:
4.支持刪除多余的div的功能:
有時候拖拉div經常會發生拖多了等問題,所有需要刪除功能。我要做的刪除效果是:鼠標放到div上面,div的右上角會出現一個紅色的刪除圖標,鼠標移走就消失。如下圖:

我是通過以下代碼實現的:
$("#right").on("mouseenter", ".node", function () {
$(this).append('<img src="../../resources/images/close2.png" style="position: absolute;" />');
if ($(this).text() == "開始" || $(this).text() == "結束") {
$("img").css("left", 158).css("top", 0);
} else {
$("img").css("left", 158).css("top", -10);
}
});
$("#right").on("mouseleave", ".node", function () {
$("img").remove();
});
我想在這里大家都有疑問吧,為什么用on()事件委托。因為<img />是后添加進來的元素,前面頁面已經完成了初始化,所以你用$("img")根本找不到這個元素,
因為img是在頁面初始化后,才添加的元素。這里就提到了live()為什么不用這個,jquery1.7.2才有這個方法,這里用的是jquery1.11.1 已經沒有live()方法了,
取而代之的是on()方法。(live()有許多缺點,所以在新的版本被摒棄了)
后面刪除比較簡單:
1 $("#right").on("click", "img",function () {
2 if (confirm("確定要刪除嗎?")) {
3 jsPlumb.removeAllEndpoints($(this).parent().attr("id"));
4 $(this).parent().remove();
5
6 }
7 });
注明:這里我遇到一個問題,你刪除了那個div,你還得把它周圍的4個端點(endpoint)刪除,這個問題剛開始我想了很多,一直沒做出來,后來去jsPlumb官網查看相關的資料,
發現jsPlumb提供一個方法能刪除div四周的端點。方法如下:
jsPlumb.removeAllEndpoints($(this).parent().attr("id"));//刪除指定id的所有端點
5.支持刪除連接線:
1 jsPlumb.bind("click", function (conn, originalEvent) {
2 if (confirm("確定刪除嗎? "))
3 jsPlumb.detach(conn);
4 });
6. 能雙擊修改流程圖的文字:
1 function doubleclick(id) {
2 $(id).dblclick(function () {
3 var text = $(this).text();
4 $(this).html("");
5 $(this).append("<input type='text' value='" + text + "' />");
6 $(this).mouseleave(function () {
7 $(this).html($("input[type='text']").val());
8 });
9 });
10 }
7.能序列化保存流程圖:
我的思路是這樣的,將中間div里所有的"流程圖div信息和連接線兩端的信息"保存到數組里,然后序列化成json數據,通過ajax傳到asp.net 后台,將json寫入到txt文檔里保存到服務器端。
(其實保存到數據庫里是最好的,后面會考慮保存到數據庫),下次展示頁面的時候,只要讀取txt文檔里的json,然后再轉成泛型集合。
將頁面上的div信息,和連線信息轉成json跳轉到ajax.aspx頁面:
function save() { var connects = []; $.each(jsPlumb.getAllConnections(), function (idx, connection) { connects.push({ ConnectionId: connection.id, PageSourceId: connection.sourceId, PageTargetId: connection.targetId, SourceText: connection.source.innerText, TargetText: connection.target.innerText, }); }); var blocks = []; $("#right .node").each(function (idx, elem) { var $elem = $(elem); blocks.push({ BlockId: $elem.attr('id'), BlockContent: $elem.html(), BlockX: parseInt($elem.css("left"), 10), BlockY: parseInt($elem.css("top"), 10) }); }); var serliza = JSON.stringify(connects) + "&" + JSON.stringify(blocks); $.ajax({ type: "post", url: "ajax.aspx", data: { id: serliza }, success: function (filePath) { window.open("show-flowChart.aspx?path=" + filePath); } }); }
ajax.aspx頁面將前台傳過來的json保存到服務器端,並跳轉至 show-flowChart.aspx:
protected void Page_Load(object sender, EventArgs e) { if (!IsPostBack) { string str = Request["id"]; string filePath = Server.MapPath("~/prototype/project-reply")+"\\json"+DateTime.Now.ToString("yyyyMMddhhmmss")+".txt"; WriteToFile(filePath,str,false); //Response.Redirect("show-flowChart.aspx?path="+filePath); Response.Write(filePath); } } public static void WriteToFile(string name, string content, bool isCover) { FileStream fs = null; try { if (!isCover && File.Exists(name)) { fs = new FileStream(name, FileMode.Append, FileAccess.Write); StreamWriter sw = new StreamWriter(fs, Encoding.UTF8); sw.WriteLine(content); sw.Flush(); sw.Close(); } else { File.WriteAllText(name, content, Encoding.UTF8); } } finally { if (fs != null) { fs.Close(); } } }
show-flowChart.aspx頁面:
protected void Page_Load(object sender, EventArgs e) { if (!IsPostBack) { string str = Request["path"]; StreamReader sr = new StreamReader(str); string jsonText = sr.ReadToEnd(); List<JsPlumbConnect> list = new JavaScriptSerializer().Deserialize<List<JsPlumbConnect>>(jsonText.Split('&')[0]); List<JsPlumbBlock> blocks = new JavaScriptSerializer().Deserialize<List<JsPlumbBlock>>(jsonText.Split('&')[1]); string htmlText = ""; string conn = ""; if (blocks.Count > 0) { foreach (JsPlumbBlock block in blocks) { if(block.BlockContent=="開始"||block.BlockContent=="結束") htmlText += "<div class='node radius' id='" + block.BlockId + "'style='left:"+block.BlockX+"px;top:"+block.BlockY+"px;' >" + block.BlockContent + "</div>"; else htmlText += "<div class='node' id='" + block.BlockId + "'style='left:" + block.BlockX + "px;top:" + block.BlockY + "px;' >" + block.BlockContent + "</div>"; } foreach (JsPlumbConnect jsplum in list) conn += "jsPlumb.connect({ source: \"" + jsplum.PageSourceId + "\", target: \"" + jsplum.PageTargetId + "\" }, flowConnector);"; Literal1.Text = htmlText; string script = "jsPlumb.ready(function () {" + conn + "});"; ClientScript.RegisterStartupScript(this.GetType(), "myscript", script, true); } } }
以及兩個用到的類JsPlumbConnect類和JsPlumbBlock類:
/// <summary> /// 連接線信息 /// </summary> public class JsPlumbConnect { public string ConnectionId { get; set; } public string PageSourceId { get; set; } public string PageTargetId { get; set; } public string SourceText { get; set; } public string TargetText { get; set; } } /// <summary> /// 流程圖的所有div /// </summary> public class JsPlumbBlock { /// <summary> /// div Id /// </summary> public string BlockId { get; set; } /// <summary> /// div里面的內容 /// </summary> public string BlockContent { get; set; } public int BlockX { get; set; } public int BlockY { get; set; } }
結尾
附件下載地址:http://pan.baidu.com/s/1jGC8XM2
------------------------------------------------------------------------------------
jsPlumb之流程圖項目總結及實例
在使用jsPlumb過程中,所遇到的問題,以及解決方案,文中引用了《數據結構與算法JavaScript描述》的相關圖片和一部分代碼.截圖是有點多,有時比較懶,沒有太多的時間去詳細的編輯.
前言
首先是UML類圖
然后是流程圖
使用了jsPlumb的相關功能,初版是可以看到雛形了,差不多用了兩個月的時間,中間斷斷續續的又有其它工作穿插,但還是把基本功能做出來了.
其實做完了之后,才發現jsPlumb的功能,只用到了很少的一部分,更多的是對於內部數據結構的理解和實現,只能說做到了數據同步更新,距離數據驅動仍然有一定的距離.
這里會總結和記錄一下項目中遇到的問題,和解決的方法,如果有更好的方法,歡迎指出.
對於連線上的多個標簽的處理
如上圖所示,一開始是認為是否是要在連線時,配置兩個overlays,
var j = jsPlumb.getInstance();
j.connect({
source:source, target:target, overlays:[ "Arrow", ["label",{label:"foo1",location:0.25,id:"m1"}], ["label",{label:"foo2",location:0.75,id:"m2"}] ] })
當然,這里也有坑,如果id重復,那么會使用最后一個,而不會重合,包括jsPlumb內部緩存的數據都只會剩下最后的那個.
后面發現,其實也可以通過importDefaults函數來動態修改配置項.
j.importDefaults({
ConnectionOverlays: [ ["Arrow", { location: 1, id: "arrow", length: 10, foldback: 0, width: 10 }], ["Label", { label: "n", id: "label-n", location: 0.25, cssClass: "jspl-label" }], ["Label", { label: "1", id: "label-1", location: 0.75, cssClass: "jspl-label" }] ] })
只不過這樣,只會在運行了函數之后的連線里,才能有兩個標簽顯示,而之前的則無法一起變化.
所以為了方便,直接在初始化里將其給修改了.
Groups的使用
在做流程圖時,Group確實是個問題,如上圖的無限嵌套層級中,就無法使用jsPlumb提供的Groups功能.
按照文檔中來說,如果標識一個元素為組,則該組中的元素則會跟隨組的移動而移動,連線也是,但問題就是一旦一個元素成為組了,那就不能接受其它組元素了,換句話說,它所提供的的Groups方法只有一層,自然無法滿足要求.
先把總結的組的用法貼出來:
j.addGroup({
el:el, id:"one" constrain:true, // 子元素僅限在元素內拖動 droppable:true, // 子元素是否可以放置其他元素 draggable:true, // 默認為true,組是否可以拖動 dropOverride:true ,// 組中的元素是否可以拓展到其他組,為true時表示否,這里的拓展會對dom結構進行修改,而非單純的位置移動 ghost:true, // 是否創建一個子元素的副本元素 revert:true, // 元素是否可以拖到只有邊框可以重合 })
后面采用了新的方式,在節點移動時,動態刷新連線
j.repaintEverything();
而為了不阻塞頁面,需要用到函數節流throttle()
function throttle(fn,interval){ var canRun = true; return function(){ if(!canRun) return; canRun = false; setTimeout(function(){ fn.apply(this,arguments); canRun = true; },interval ? interval : 300); }; };
這是一個簡單的實現方式,主要就是為了減少dom中事件移動時重復調用的事件,同時達到執行事件的目的(只允許一個函數在x毫秒內執行一次);
當然,也可以使用underscore.js中自帶的_.throttle()函數,同樣可以達到目的.
這里的html結構就使用了嵌套的層級,將父級和子級使用這種層級保存到內部的數據源里
多層or一層 數據結構解析

類似這種實際存在嵌套關系的數據體,有兩種方式可以進行管理,
-
多層級嵌套:類似
[ { id:"1", child:{ id:"2", child:{ id:"3", child:{} } } } ]用來進行管理的話,優點是直觀,能根據層級就知道整體結構大概是多少,轉換成xml或者html也很方便.
但缺點就是進行查找和修改,並不是那么方便. -
一層展示所有節點:類似
[ { id:"1", child:[{ id:"2" }] }, { id:"2", parentId:"1", child:[{ id:"3" }] }, { id:"3", parentId:"2", child:[] } ]這種結構好處就是全部在一個層級中,查找起來和修改數據非常方便,而如果想要解析成多層級的結構,只需要運用遞歸,來生成新結構:
function mt(){ var OBJ; this.root = null; this.Node = function(e) { this.id = e.id; this.name = e.name; this.parentId = e.parentId; this.children = []; }; this.insert=function(e,key){ function add(obj,e){ if(obj.id == e.parentId){ obj.children.push(e); } else { for (var i = 0; i < obj.children.length; i++) { add(obj.children[i], e); } } } if (e != undefined) { e = new this.Node(e); } else { return; } if (this.root == null) { this.root = e; } else { OBJ = this.root; add(OBJ, e); } } this.init = function(data){ var _this = this; for(var i = 0;i<data.length;i++){ _this.insert(data[i]); } return OBJ; } }將一層的數組通過初始化函數
init,就可以轉為多層級
如果想轉成html結構,只需要稍微改下函數,就可以實現了.
校驗流程是否存在死路(是否存在不能到達圖的終點的路徑的點)
這個就完全得靠算法來實現了.首先,對於圖的理解是重點
我也懶得打字了,直接用圖表示一下,基本的圖大致是這樣,而具體的表現形式則是
可以看到,基礎的圖的表現形式,可以用一個鄰接表來表示;
而實現,則可以看到下列的代碼:
function Graph1(v) { this.vertices = v; // 總頂點 this.edges = 0; // 圖的邊數 this.adj = []; // 通過 for 循環為數組中的每個元素添加一個子數組來存儲所有的相鄰頂點,[並將所有元素初始化為空字符串。]? for (var i = 0; i < this.vertices; ++i) { this.adj[i] = []; } /** * 當調用這個函數並傳入頂點 v 和 w 時,函數會先查找頂點 v 的鄰接表,將頂點 w 添加到列表中 * 然后再查找頂點 w 的鄰接表,將頂點 v 加入列表。最后,這個函數會將邊數加 1。 * @param {[type]} v [第一個頂點] * @param {[type]} w [第二個頂點] */ this.addEdge = function(v, w) { this.adj[v].push(w); this.adj[w].push(v); this.edges++; } /** * 打印所有頂點的關系簡單表現形式 * @return {[type]} [description] */ this.showGraph = function() { for (var i = 0; i < this.vertices; ++i) { var str = i + " ->"; for (var j = 0; j < this.vertices; ++j) { if (this.adj[i][j] != undefined) { str += this.adj[i][j] + ' ' } } console.log("表現形式為:" + str); } console.log(this.adj); } }
而光構建是不夠的,所以來看下基礎的搜索方法:
深度優先搜索和廣度優先搜索;
深度優先搜索
先從初始節點開始訪問,並標記為已訪問過的狀態,再遞歸的去訪問在初始節點的鄰接表中其他沒有訪問過的節點,依次之后,就能訪問過所有的節點了
/** * 深度優先搜索算法 * 這里不需要頂點,也就是鄰接表的初始點 */ this.dfs = (v) { this.marked[v] = true; for (var w of this.adj[v]) { if (!this.marked[w]) { this.dfs(w); } } }
根據圖片和上述的代碼,可以看出深度搜索其實可以做很多其他的擴展
廣度優先搜索

/** * 廣度優先搜索算法 * @param {[type]} s [description] */ this.bfs = function(s) { var queue = []; this.marked[s] = true; queue.push(s); // 添加到隊尾 while (queue.length > 0) { var v = queue.shift(); // 從隊首移除 console.log("Visisted vertex: " + v); for (var w of this.adj[v]) { if (!this.marked[w]) { this.edgeTo[w] = v; this.marked[w] = true; queue.push(w); } } } }
而如果看了《數據結構與算法JavaScript描述》這本書,有興趣的可以去實現下查找最短路徑和拓撲排序;
兩點之間所有路徑
這算是找到的比較能理解的方式來計算
以上圖為例,這是一個簡單的流程圖,可以很簡單的看出,右邊的流程實際上是未完成的,因為無法到達終點,所以是一個非法點,而通過上面的深度搜索,可以看出,只要對深度優先搜索算法進行一定的修改,那么就可以找到從開始到結束的所有的路徑,再通過對比,就可以知道哪些點無法到達終點,從而確定非法點.
上代碼:
/** * 深度搜索,dfs,解兩點之間所有路徑 * @param {[type]} v [description] * @return {[type]} [description] */ function Graph2(v) { var _this = this; this.vertices = v; // 總頂點 this.edges = 0; //圖的起始邊數 this.adj = []; //內部鄰接表表現形式 this.marked = []; // 內部頂點訪問狀態,與鄰接表對應 this.path = []; // 路徑表示 this.lines = []; // 所有路徑匯總 for (var i = 0; i < this.vertices; ++i) { _this.adj[i] = []; } /** * 初始化訪問狀態 * @return {[type]} [description] */ this.initMarked = function() { for (var i = 0; i < _this.vertices; ++i) { _this.marked[i] = false; } }; /** * 在鄰接表中增加節點 * @param {[type]} v [description] * @param {[type]} w [description] */ this.addEdge = function(v, w) { this.adj[v].push(w); this.edges++; }; /** * 返回生成的鄰接表 * @return {[type]} [description] */ this.showGraph = function() { return this.adj; }; /** * 深度搜索算法 * @param {[type]} v [起點] * @param {[type]} d [終點] * @param {[type]} path [路徑] * @return {[type]} [description] */ this.dfs = function(v, d, path) { var _this = this; this.marked[v] = true; path.push(v); if (v == d) { var arr = []; for (var i = 0; i < path.length; i++) { arr.push(path[i]); } _this.lines.push(arr); } else { for (var w of this.adj[v]) { if (!this.marked[w]) { this.dfs(w, d, path); } } } path.pop(); this.marked[v] = false; }; this.verify = function(arr, start, end) { this.initMarked(); for (var i = 0; i < arr.length; i++) { _this.addEdge(arr[i].from, arr[i].to); } this.dfs(start, end, this.path); return this.lines; }; }
可以看出修改了addEdge()函數,將鄰接表中的雙向記錄改為單向記錄,可以有效避免下圖的錯誤計算:
只計算起點到終點的所有連線有時並不客觀,如果出現
這種情況的話,實際上深度遍歷並不能計算出最右邊的節點是合法的,那么就需要重新修改起點和終點,來推導是否能夠到達終點.從而判定該點是否合法.至於其他的,只是多了個返回值,存儲了一下計算出來的所有路徑.
而在dfs函數中,當滿足能夠從起點走到終點的,則記錄下當前的path中的值,保存到lines中去,而每一次對於path的推入或者推出,保證了只有滿足條件的點,才能被返回;
而this.marked[v] = false,則確保了,在每一次重新計算路徑時,都會驗證每個點是否存在不同的相對於終點能夠到達的路徑是否存在.
當然,一定會有更加簡單的方法,我這里只是稍微修改了下基礎的代碼!
redo和undo
這是我覺得最簡單卻耗時最久的功能,思路都知道:創建一個隊列,記錄每一次創建一個流程節點,刪除一個流程節點,建立一個新的關聯關系,刪除一個新的關聯關系等,都需要記錄下來,再通過統一的接口來訪問隊列,執行操作.
但在具體實現上,jsPlumb的remove確實需要注意一下:
首先,如果需要刪除連線,那么使用jsPlumb提供的detach()方法,就可以刪除連線,注意,傳入的數據應該是connection對象.
當然,也可以使用remove()方法,參數為選擇器或者element對象都可以,這個方法刪除的是一個節點,包括節點上所有的線.
而jsPlumb中會內部緩存所有的數據,用於刷新,和重連.
那么當我移除一個多層級且內部有連線的情況時,如果只刪除最外層的元素,那么內部的連線實際上並沒有清除,所以當redo或者移動時,會出現連線的端點有一端會跑到坐標原點,也就是div上(0,0)的地方去.所以清除時,需要注意,要把內部的所有節點依次清除,才不會發生一些莫名其妙的bug.
而在刪除和連接連線上,我使用了jsPlumb提供的事件bind('connection')和bind("connectionDetached"),用於判斷一條連線被連接或者刪除.而在記錄這里的redo和undo事件時,尤其要注意,需要首先確定刪除和連接時的連線的類型,否則會產生額外的隊列事件.
因此,在使用連接事件時,就可以使用
jsPlumb.connect({ source:"foo", target:"bar", parameters:{ "p1":34, "p2":new Date(), "p3":function() { console.log("i am p3"); } } });
來進行類型的傳參,這樣事件觸發時就可以分類處理.
也可以使用connection.setData()事件,參數可以指定任意的值,通過connection.getData()方法,就可以拿到相應的數據了.
而redo和undo本身確實沒有什么東西
var defaults = { 'name': "mutation", 'afterAddServe':$.noop, 'afterUndo':$.noop, 'afterRedo':$.noop } var mutation = function(options){ this.options = $.extend(true,{},defaults,options); this.list = []; this.index = 0; }; mutation.prototype = { addServe:function(undo,redo){ if(!_.isFunction(undo) || !_.isFunction(redo)) return false; // 說明是在有后續操作時,更新了隊列 if(this.canRedo){ this.splice(this.index+1); }; this.list.push({ undo:undo, redo:redo }); console.log(this.list); this.index = this.list.length - 1; _.isFunction(this.options.afterAddServe) && this.options.afterAddServe(this.canUndo(),this.canRedo()); }, /** * 相當於保存之后清空之前的所有保存的操作 * @return {[type]} [description] */ reset:function(){ this.list = []; this.index = 0; }, /** * 當破壞原來隊列時,需要對隊列進行修改, * index開始的所有存儲值都沒有用了 * @param {[type]} index [description] * @return {[type]} [description] */ splice:function(index){ this.list.splice(index); }, /** * 撤銷操作 * @return {[type]} [description] */ undo:function(){ if(this.canUndo()){ this.list[this.index].undo(); this.index--; _.isFunction(this.options.afterUndo) && this.options.afterUndo(this.canUndo(),this.canRedo()); } }, /** * 重做操作 * @return {[type]} [description] */ redo:function(){ if(this.canRedo()){ this.index++; this.list[this.index].redo(); _.isFunction(this.options.afterRedo) && this.options.afterRedo(this.canUndo(),this.canRedo()); } }, canUndo:function(){ return this.index !== -1; }, canRedo:function(){ return this.list.length - 1 !== this.index; } } return mutation;
每次在使用redo或者undo時,只需要判斷當前是否是隊列的尾端或者起始端,再確定是否redo或者undo就可以了.
調用時的undo()和redo()通過傳參,將不同的函數封裝進隊列里,就可以減少耦合度.
放大縮小
這里想了想還是記錄一下,方法采用了最簡單的mousedown和mousemove,讓元素在節流中動態的變化大小,就可以了,
只需要用一個節點,在點擊元素時,根據元素的大小來確定該輔助節點四個點的位置,就可以了,只要監聽了這四個點的位置,再同步給該定位元素,就能實現這一效果
define([
'text!textPath/tpl.flow.control.html', ], function(flowControl) { var defaults = { stage: document, //舞台 root: null, refresh: null, dragStop: null } var resize = function(el, options) { this.options = $.extend(true, {}, defaults, options); this.target = el instanceof jQuery ? el : $(el); this.init(); }; resize.prototype = { init: function() { this.initResizeBox(); }, renderTpl: function(tpl, data) { if (!_.isFunction(tpl)) tpl = _.template(tpl); return tpl(data) }, initResizeBox: function() { var _this = this; this.ox = 0; // 初始位置x this.oy = 0; // 初始位置y this.ow = 0; // 初始寬度 this.oh = 0; // 初始高度 this.oLeft = 0; // 初始元素left定位 this.oTop = 0; // 初始元素top定位 this.helperLeft = 0; // 初始助手left定位 this.helperTop = 0; // 初始助手top定位 this.org = null; // 映射元素 this.parent = ''; // 父元素 this.orgItem = null; // 映射子元素,用於計算范圍 this.minWidth = 0; // 映射元素最小寬度 this.minHeight = 0; // 映射元素最小高度 this.maxWidth = 0; // 映射元素最大寬度 this.maxHeight = 0; // 映射元素最大高度 this.helper = $(this.renderTpl(flowControl)).appendTo(this.target); // 縮放助手 this.bindResizeEvent(this.helper); }, offset: function(curEle) { var totalLeft = null, totalTop = null, par = curEle.offsetParent; //首先加自己本身的左偏移和上偏移 totalLeft += curEle.offsetLeft; totalTop += curEle.offsetTop //只要沒有找到body,我們就把父級參照物的邊框和偏移也進行累加 while (par) { if (navigator.userAgent.indexOf("MSIE 8.0") === -1) { //累加父級參照物的邊框 totalLeft += par.clientLeft; totalTop += par.clientTop } //累加父級參照物本身的偏移 totalLeft += par.offsetLeft; totalTop += par.offsetTop par = par.offsetParent; } return { left: totalLeft, top: totalTop } }, scrollArtboard: function(pos, el) { var _this = this; var artboardWidth = $(".artboard.flow").outerWidth(), artboardHeight = parseFloat($(".artboard.flow").outerHeight()) - 42, elWidth = el.outerWidth(), elHeight = el.outerHeight(), isConcurrenceChild = el.parent('.symbol_flow-concurrence').length > 0 ? true : false; if (isConcurrenceChild) { if (_this.offset(el.get(0)).left + elWidth > artboardWidth) { console.log("並發體越界"); $(".artboard.flow").scrollLeft(_this.offset(el.get(0)).left + elWidth); } if (_this.offset(el.get(0)).top + elHeight > artboardHeight) { console.log("並發體越界"); $(".artboard.flow").scrollTop(_this.offset(el.get(0)).top + elHeight); } } else { // 長度長於畫布 if (pos.left + elWidth > artboardWidth) { $(".artboard.flow").scrollLeft(pos.left + elWidth); } if (pos.top + elHeight > artboardHeight) { $(".artboard.flow").scrollTop(pos.top + elHeight); } } }, hasBeyond: function(el,master) { var _this = this; if (_this.isConcurrenceChild) { var parentOffset = _this.offset(_this.parent.get(0)); parentOffset.height = parentOffset.top + _this.parent.outerHeight(); parentOffset.width = parentOffset.left + _this.parent.outerWidth(); var elOffset = _this.offset(el.get(0)); elOffset.height = elOffset.top + el.outerHeight(); elOffset.width = elOffset.left + el.outerWidth(); if (master.left < 0 || master.top < 0) { $(_this.options.stage).trigger('mouseup'); } if (parentOffset.height < elOffset.height || parentOffset.width < elOffset.width) { $(_this.options.stage).trigger('mouseup'); } } }, /** * 根據傳入的操作節點進行定位 * 新增根據parentId來判斷並發體中的定位校准 * @param {[type]} target [description] * @return {[type]} [description] */ position: function(target, parentId) { var _this = this; this.org = target; this.parent = $("#" + parentId); this.orgItem = target.children('.symbol_flow-concurrence'); this.oLeft = (this.org.offset().left - this.options.root.offset().left) < parseFloat(this.org.css('left')) ? parseFloat(this.org.css('left')) : (this.org.offset().left - this.options.root.offset().left); this.oTop = (this.org.offset().top - this.options.root.offset().top) < parseFloat(this.org.css('top')) ? parseFloat(this.org.css('top')) : (this.org.offset().top - this.options.root.offset().top); this.minWidth = parseFloat(this.orgItem.css('minWidth').replace('px', '')); this.minHeight = parseFloat(this.orgItem.css('minHeight').replace('px', '')); this.maxHeight = parseFloat(this.org.closest('.symbol_flow-concurrence').outerHeight()); this.maxWidth = parseFloat(this.org.closest('.symbol_flow-concurrence').outerWidth()); this.helperLeft = parseFloat(this.offset(target.get(0)).left); this.helperTop = parseFloat(this.offset(target.get(0)).top) - 42; // 頂部偏移 this.isConcurrenceChild = parentId == "artboard" ? false : true; this.helper.css({ width: _this.orgItem.outerWidth(), height: _this.orgItem.outerHeight(), left: _this.helperLeft, top: _this.helperTop }) _this.show(); }, show: function() { this.helper.css("display", "block"); }, hide: function() { this.helper.css("display", "none"); }, bindResizeEvent: function(el) { var _this = this; var nwMove = false; el.on('mousedown', '.nw', function(e) { _this.ox = e.pageX; _this.oy = e.pageY; _this.ow = el.width(); _this.oh = el.height(); _this.oLeft = _this.isConcurrenceChild ? _this.org.offset().left - _this.parent.offset().left : _this.offset(_this.org.get(0)).left; _this.oTop = _this.isConcurrenceChild ? _this.org.offset().top - _this.parent.offset().top : parseFloat(_this.offset(_this.org.get(0)).top) - 
