[轉]jsPlumb插件做一個模仿viso的可拖拉流程圖


原貼: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;
                    }
                }
            });
View Code
 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 }]]
            };
View Code

 

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();
            });
View Code

 

我想在這里大家都有疑問吧,為什么用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);
                }
            });
        }
View Code

 

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();
            }
        }

    }
View Code

 

 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);
            }
        }
    }
View Code

 

以及兩個用到的類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; }
    }
View Code

 

 

結尾

 

附件下載地址:http://pan.baidu.com/s/1jGC8XM2

 

------------------------------------------------------------------------------------

 

jsPlumb之流程圖項目總結及實例

 原貼:http://blog.csdn.net/sinat_16039187/article/details/66969546

在使用jsPlumb過程中,所遇到的問題,以及解決方案,文中引用了《數據結構與算法JavaScript描述》的相關圖片和一部分代碼.截圖是有點多,有時比較懶,沒有太多的時間去詳細的編輯.

前言

首先是UML類圖
3

然后是流程圖
4

使用了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一層 數據結構解析

5

類似這種實際存在嵌套關系的數據體,有兩種方式可以進行管理,

  • 多層級嵌套:類似

        [
            {
     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,就可以轉為多層級
    6

如果想轉成html結構,只需要稍微改下函數,就可以實現了.

校驗流程是否存在死路(是否存在不能到達圖的終點的路徑的點)

這個就完全得靠算法來實現了.首先,對於圖的理解是重點
7

我也懶得打字了,直接用圖表示一下,基本的圖大致是這樣,而具體的表現形式則是
8

可以看到,基礎的圖的表現形式,可以用一個鄰接表來表示;

而實現,則可以看到下列的代碼:

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

而光構建是不夠的,所以來看下基礎的搜索方法:
深度優先搜索和廣度優先搜索;

深度優先搜索

先從初始節點開始訪問,並標記為已訪問過的狀態,再遞歸的去訪問在初始節點的鄰接表中其他沒有訪問過的節點,依次之后,就能訪問過所有的節點了
9

  /** * 深度優先搜索算法 * 這里不需要頂點,也就是鄰接表的初始點 */ this.dfs = (v) { this.marked[v] = true; for (var w of this.adj[v]) { if (!this.marked[w]) { this.dfs(w); } } }

根據圖片和上述的代碼,可以看出深度搜索其實可以做很多其他的擴展

廣度優先搜索

10

  /** * 廣度優先搜索算法 * @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描述》這本書,有興趣的可以去實現下查找最短路徑拓撲排序;

兩點之間所有路徑

這算是找到的比較能理解的方式來計算
11

以上圖為例,這是一個簡單的流程圖,可以很簡單的看出,右邊的流程實際上是未完成的,因為無法到達終點,所以是一個非法點,而通過上面的深度搜索,可以看出,只要對深度優先搜索算法進行一定的修改,那么就可以找到從開始到結束的所有的路徑,再通過對比,就可以知道哪些點無法到達終點,從而確定非法點.
上代碼:

    /** * 深度搜索,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()函數,將鄰接表中的雙向記錄改為單向記錄,可以有效避免下圖的錯誤計算:
12

只計算起點到終點的所有連線有時並不客觀,如果出現
13

這種情況的話,實際上深度遍歷並不能計算出最右邊的節點是合法的,那么就需要重新修改起點和終點,來推導是否能夠到達終點.從而判定該點是否合法.至於其他的,只是多了個返回值,存儲了一下計算出來的所有路徑.
而在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()通過傳參,將不同的函數封裝進隊列里,就可以減少耦合度.

放大縮小

這里想了想還是記錄一下,方法采用了最簡單的mousedownmousemove,讓元素在節流中動態的變化大小,就可以了,
14

只需要用一個節點,在點擊元素時,根據元素的大小來確定該輔助節點四個點的位置,就可以了,只要監聽了這四個點的位置,再同步給該定位元素,就能實現這一效果

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) - 42; _this.helperLeft = parseFloat(_this.offset(_this.org.get(0)).left); _this.helperTop = parseFloat(_this.offset(_this.org.get(0)).top) - 42; nwMove = true; $(_this.options.stage).on('mousemove', _.throttle(function(e) { if (nwMove) { var x = e.pageX - _this.ox; var y = e.pageY - _this.oy; var master = { height: (_this.oh - y) < _this.minHeight ? _this.minHeight : (_this.oh - y) > _this.maxHeight ? _this.maxHeight : (_this.oh - y), top: _this.oTop + y, width: (_this.ow - x) < _this.minWidth ? _this.minWidth : (_this.ow - x) > _this.maxWidth ? _this.maxWidth : (_this.ow - x), left: _this.oLeft + x }; var master2 = { height: master.height, top: _this.helperTop + y, width: master.width, left: _this.helperLeft + x } el.css(master2); _this.org.css(master); _this.scrollArtboard(master2, el); _this.hasBeyond(el,master); } _.isFunction(_this.options.refresh) && _this.options.refresh(); }, 50)).on('mouseup', function() { nwMove = false; $(this).off('mousemove'); $(this).off('mouseup'); _.isFunction(_this.options.dragStop) && _this.options.dragStop(_this.org); }); }); var neMove = false; el.on('mousedown', '.ne', function(e) { _this.ox = e.pageX; _this.oy = e.pageY; _this.ow = el.width(); _this.oh = el.height(); _this.oTop = _this.isConcurrenceChild ? _this.org.offset().top - _this.parent.offset().top : parseFloat(_this.offset(_this.org.get(0)).top) - 42; _this.helperTop = parseFloat(_this.offset(_this.org.get(0)).top) - 42; neMove = true; console.log("ne???"); $(_this.options.stage).on('mousemove', _.throttle(function(e) { if (neMove) { var x = e.pageX - _this.ox; var y = e.pageY - _this.oy; var master = { height: (_this.oh - y) < _this.minHeight ? _this.minHeight : (_this.oh - y) > _this.maxHeight ? _this.maxHeight : (_this.oh - y), top: _this.oTop + y, width: (_this.ow + x) < _this.minWidth ? _this.minWidth : (_this.ow + x) > _this.maxWidth ? _this.maxWidth : (_this.ow + x) }; var master2 = { height: master.height, top: _this.helperTop + y, width: master.width } el.css(master2); _this.org.css(master); _this.scrollArtboard(master2, el); _this.hasBeyond(el,master); } _.isFunction(_this.options.refresh) && _this.options.refresh(); }, 50)).on('mouseup', function() { neMove = false; $(this).off('mousemove'); $(this).off('mouseup'); _.isFunction(_this.options.dragStop) && _this.options.dragStop(_this.org); }); }); var swMove = false; el.on('mousedown', '.sw', 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.helperLeft = parseFloat(_this.offset(_this.org.get(0)).left); swMove = true; console.log("sw???"); $(_this.options.stage).on('mousemove', _.throttle(function(e) { if (swMove) { var x = e.pageX - _this.ox; var y = e.pageY - _this.oy; var master = { height: (_this.oh + y) < _this.minHeight ? _this.minHeight : (_this.oh + y) > _this.maxHeight ? _this.maxHeight : (_this.oh + y), width: (_this.ow - x) < _this.minWidth ? _this.minWidth : (_this.ow - x) > _this.maxWidth ? _this.maxWidth : (_this.ow - x), left: _this.oLeft + x }; var master2 = { height: master.height, width: master.width, left: _this.helperLeft + x } el.css(master2); _this.org.css(master); _this.scrollArtboard(master2, el); _this.hasBeyond(el,master); } _.isFunction(_this.options.refresh) && _this.options.refresh(); }, 50)).on('mouseup', function() { swMove = false; $(this).off('mousemove'); $(this).off('mouseup'); _.isFunction(_this.options.dragStop) && _this.options.dragStop(_this.org); }); }); var seMove = false; el.on('mousedown', '.se', function(e) { _this.ox = e.pageX; _this.oy = e.pageY; _this.ow = el.width(); _this.oh = el.height(); seMove = true; console.log("se???"); $(_this.options.stage).on('mousemove', _.throttle(function(e) { if (seMove) { var x = e.pageX - _this.ox; var y = e.pageY - _this.oy; var master = { height: (_this.oh + y) < _this.minHeight ? _this.minHeight : (_this.oh + y) > _this.maxHeight ? _this.maxHeight : (_this.oh + y), width: (_this.ow + x) < _this.minWidth ? _this.minWidth : (_this.ow + x) > _this.maxWidth ? _this.maxWidth : (_this.ow + x) }; el.css(master); _this.org.css(master); _this.scrollArtboard(master, el); _this.hasBeyond(el,master); } _.isFunction(_this.options.refresh) && _this.options.refresh(); }, 50)).on('mouseup', function() { seMove = false; $(this).off('mousemove'); $(this).off('mouseup'); _.isFunction(_this.options.dragStop) && _this.options.dragStop(_this.org); }); }); } } return resize; });

這里的tpl只是一個簡單的遮罩層

<div class="symbol_control"> <div class="symbol_control-shape nw"></div> <div class="symbol_control-shape ne"></div> <div class="symbol_control-shape sw"></div> <div class="symbol_control-shape se"></div> </div>

overlays沖突

在運行項目時,發現如果instance時,傳入了ConnectionOverlays,會與之后連線上的overlays沖突,因此,這里可以去除默認配置中的overlays,給全局加上importDefault方法,解決這一問題.

demo,分組及各種事件監聽

小結

這次的項目我個人還是覺得蠻有意思的,可以學習新的算法,了解新的數據結構,包括設計模式,也代入了其中,進行代碼的整合,所用到的中間件模式和發布訂閱者模式都讓我對於js有了一個新的理解.雖然已經用require來管理模塊,但結構仍然存在高度耦合的情況,應該還是被限制住了.
作為離職前的最后一次的項目來說,其實我感覺我的代碼能力仍然與年初沒有什么太大的改變,也許是時候脫離安逸的環境,重新開始了.

 

---------------------------

jsPlumb.jsAPI閱讀筆記(官方文檔翻譯)

 

jsPlumb DOCS

公司要開始做流程控制器,所以先調研下jsPlumb,下文是閱讀jsPlumb提供的document所產生的歸納總結

setup

如果不使用jQuery或者類jQuery庫,則傳入的節點得用id的形式,否則jsPlumb會為元素設置一個id。

jsPlumb.ready(function(){ ··· }); //or jsPlumb.bind("ready",function(){ ··· });

最好確認jsPlumb加載完畢之后,再開始使用相關功能。

默認情況下,jsPlumb在瀏覽器的窗口中注冊,為整個頁面提供一個靜態實例,所以也可以把它看成一個類,來實例化jsPlumb:

var firstInstance = jsPlumb.getInstance();

如果在使用過程中,元素的id產生了新的變化(多是生成了新的節點,舊的節點被刪除了)。則可以:

  • jsPlumb.setId(el,newId)
  • jsPlumb.setIdChanged(oldId,newId)

在使用過程中,每個部分的z-index需要注意,否則連線可能會被覆蓋,jsPlumb會為每個節點設置端點,用於定位端點。

jsPlumb也提供了拖動方法:

var secondInstance = jsPlumb.getInstance(); secondInstance.draggable("some element");

重繪,每次使用連線時,都會導致相關聯的元素重繪,但當加載大量數據時,可以使用:

jsPlumb.setSuspendDrawing(true); jsPlumb.setSuspendDrawing(false,true);

這里第二個參數的true,會使整個jsPlumb立即重繪。
也可以使用batch:

jsPlumb.batch(fn,[doNotRepaintAfterwards]);

這個函數也是一樣,可以先將所有的連接全部注冊好,再一次重繪。
這個方法在1.7.3版本之前名稱為doWhileSuspended.

config defaults

當然,jsPlumb會有一些默認的參數:
分為全局默認參數和連線默認參數,

Anchor : "BottomCenter",//端點的定位點的位置聲明(錨點):left,top,bottom等 Anchors : [ null, null ],//多個錨點的位置聲明 ConnectionsDetachable : true,//連接是否可以使用鼠標默認分離 ConnectionOverlays : [],//附加到每個連接的默認重疊 Connector : "Bezier",//要使用的默認連接器的類型:折線,流程等 Container : null,//設置父級的元素,一個容器 DoNotThrowErrors : false,//如果請求不存在的Anchor,Endpoint或Connector,是否會拋出 DragOptions : { },//用於配置拖拽元素的參數 DropOptions : { },//用於配置元素的drop行為的參數 Endpoint : "Dot",//端點(錨點)的樣式聲明(Dot) Endpoints : [ null, null ],//多個端點的樣式聲明(Dot) EndpointOverlays : [ ],//端點的重疊 EndpointStyle : { fill : "#456" },//端點的css樣式聲明 EndpointStyles : [ null, null ],//同上 EndpointHoverStyle : null,//鼠標經過樣式 EndpointHoverStyles : [ null, null ],//同上 HoverPaintStyle : null,//鼠標經過線的樣式 LabelStyle : { color : "black" },//標簽的默認樣式。 LogEnabled : false,//是否打開jsPlumb的內部日志記錄 Overlays : [ ],//重疊 MaxConnections : 1,//最大連接數 PaintStyle : { lineWidth : 8, stroke : "#456" },//連線樣式 ReattachConnections : false,//是否重新連接使用鼠標分離的線 RenderMode : "svg",//默認渲染模式 Scope : "jsPlumb_DefaultScope"//范圍,標識

如果是全局則可以使用jsPlumb.importDefaults({···})
也可以在實例化時,重新定義jsPlumb.getInstance({···})

Basic Concepts

jsPlumb關鍵點就是連接線,從上面也可以看出,大部分的配置項都是為了線而設。
其分為五個方面:

  • Anchor:錨點位置
  • Endpoint:端點,連接的起點或終點
  • Connector:連線,連接兩個節點的直觀表現,有四種默認類型:Bezier(貝塞爾曲線),Straight(直線),Flowchart(流程圖),State machine(狀態機)
  • Overlay:裝飾連接器的組件,類似箭頭之類
  • Group:包含在某個其他元素中的一組元素,可以折疊,導致與所有組成員的連接被合並到折疊的組容器上。

Anchor

錨點位置有四種類型:

  • Static:靜態,會固定到元素上的某個點,不會移動
  • Dynamic:動態,是靜態錨的集合,就是jsPlumb每次連接時選擇最合適的錨
  • Perimeter anchors:周邊錨,動態錨的應用。
  • Continuous anchors:連續錨

  1. Static
    jsPlumb有九個默認位置,元素的四個角,元素的中心,元素的每個邊的中點。
  • Top(TopCenter),TopRight,TopLeft
  • Right(RightMiddle)
  • Bottom(BottomCenter),BottomRight,BottomLeft
  • Left(LeftMiddle)
  • center
    可以使用基於數組的形式來定義錨點位置:[x,y,dx,dy,offsetX,offsetY]。
    [0,0]表示節點的左上角。
    x表示錨點在橫軸上的距離,y表示錨點在縱軸上的距離,這兩個值可以從0到1來設置,0.5為center。
    而dx表示錨點向橫軸射出線,dy表示錨點向縱軸射出線,有0,-1,1三個值來設置。0為不放射線。
    offsetX表示錨點偏移量x(px),offsetY表示錨點偏移量y(px)。
  1. Dynamic Anchors
    選擇每當某物移動或在UI中繪制時最合適的位置。

    var dynamicAnchors = [ [0.2,0,0,0],"Top","Bottom" ]

    在使用過程中,發現其就是指定錨點應該出現在哪個地方。jsPlumb會選取最近的點,來當作錨點。可以設置多個點,來當作可能出現的錨點。
    當然,jsPlumb自帶了默認的參數,AutoDefault。其實與["Top","Right","Bottom","Left"]相同。

  2. Perimeter Anchors
    jsPlumb提供了六種形狀:
  • Circle
  • Ellipse
  • Triangle
  • Diamond
  • Rectangle
  • Square
  1. Continuous Anchors

    anchor:"Continuous" //or anchor:["Continuous",{faces:["top","left"]}]

    faces同樣有四個值:top,left,right,bottom

將CSS類與Anchors相關聯

var ep = jsPlumb.addEndpoint("ele1",{ anchor:[0,0,0,0,0,0,"test"] });

也可以修改前綴:

jsPlumb.endpointAnchorClass="anchor_";

Connectors

連接器是實際連接UI元素的線,默認連接器是貝塞爾曲線,也就是默認值是"Bezier";
這里才是畫線的地方,

  • Bezier:它有一個配置項,curviness(彎曲度),默認為150.這定義了Bezier的控制點與錨點的距離
  • Straight:在兩個端點之間繪制一條直線,支持兩個配置參數:stub,默認為0。gap,默認為0
  • Flowchart:由一系列垂直或水平段組成的連接。支持四個參數,stub,默認為30;alwaysRespectStubs,默認為false;gap,默認為0;midpoint,默認為0.5;cornerRadius,默認為0;
  • StateMachine:狀態器,支持在同一元素上開始和結束的連接,支持的參數有:margin,默認為5;curviness,默認為10;proximityLimit,默認為80;

Endpoints

端點的配置和外觀參數。
jsPlumb帶有四個端點實現-點,矩形,空白和圖像,可以在使用connect(),addEndpoint(),makeSource()或jsPlumb.makeTarget時使用endpoint參數指定Endpoint屬性。

給端點進行配置

  • jsPlumb.connect(),創建連接的時候可以配置端點的屬性
  • jsPlumb.addEndpoint(),創建一個新的端點時配置屬性
  • jsPlumb.makeSource(),配置元素並隨后從該元素中拖動連接時,將創建並分配一個新的端點

端點的預設類型

  1. Dot:支持三個參數:
    radius,默認為10px,定義圓點的半徑
    cssClass,附加到Endpoint創建的元素的CSS類
    hoverClass,一個CSS類,當鼠標懸停在元素或連接的線上時附加到EndPoint創建的元素

  2. Rectangle:支持的參數:
    width,默認為20,定義矩形的寬度
    height,默認為20,定義矩形的高度
    cssClass,附加到Endpoint創建的元素的CSS類
    hoverClass,當鼠標懸停在元素或連接的線上時附加到EndPoint創建的元素

  3. image:從給定的URL中繪制圖像,支持三個參數:
    src,必選,指定要使用的圖像的URL,
    cssClass,附加到Endpoint創建的元素的CSS類
    hoverClass,當鼠標懸停在元素或連接的線上時附加到EndPoint創建的元素,
  4. Blank:空白

Overlays(疊加層)

jsPlumb有五種類型的疊加:

  1. Arrow:箭頭,在連接器的某個點繪制的可配置箭頭,可以控制箭頭的長度和寬度,參數有:
    width,箭頭尾部的寬度
    length,從箭頭的尾部到頭部的距離
    location,位置,建議使用0~1之間,當作百分比,便於理解
    direction,方向,默認值為1(表示向前),可選-1(表示向后)
    foldback,折回,也就是尾翼的角度,默認0.623,當為1時,為正三角
    paintStyle,樣式對象

  2. Label:在連接點的可配置標簽,參數有
    label,要顯示的文本
    cssClass,Label的可選css
    labelStyle,標簽外觀的可選參數:font,適應canvas的字體大小參數;color,標簽文本的顏色;padding,標簽的可選填充,比例而不是px;borderWidth,標簽邊框的可選參數,默認為0;borderStyle,顏色等邊框參數
    location,位置,默認0.5
    也可以使用getLabel,和setLabel,來獲取和設置label的文本,可傳函數

  3. PlainArrow:箭頭形狀為三角形
    只是Arrow的foldback為1時的例子,參數與Arrow相同

  4. Diamond:棱形
    同樣是Arrow的foldback為2時的例子,參數與Arrow相同

  5. Custom:自定義
    允許創建自定義的疊加層,需要使用create(),來返回DOM元素或者有效的選擇器(ID)
    var conn = jsPlumb.connect({ source:"d1", target:"d2", paintStyle:{ stroke:"red", strokeWidth:3 }, overlays:[ ["Custom", { create:function(component) { return $("<select id='myDropDown'><option value='foo'>foo</option><option value='bar'>bar</option></select>"); }, location:0.7, id:"customOverlay" }] ] });

作為[0,1]的小數,其表示沿着由連接器內接的路徑的一些成比例的行程,默認值為0.5。
作為大於1的整數,表示從起點沿連接器行進的某些絕對像素數。等於1時,在終點。
作為小於零的整數,其指示沿着連接器從端點向后行進的一些絕對值的像素。等於0時,在起點。

所有疊加層都支持:
getLocation-返回當前位置
setLocation-設置當前位置

添加疊加層

例子:

jsPlumb.connect({ overlays:[ "Arrow", [ "Label", { label:"foo", location:0.25, id:"myLabel" } ] ] });

而在addEndpoint和makeSource方法中,則不能使用overlays,需要使用connectOverlays.
也可以使用addOverlay:

var e = jsPlumb.addEndpoint("someElement"); e.addOverlay([ "Arrow", { width:10, height:10, id:"arrow" }]);

當然還有獲取疊加層的方法:getOverlay(id)這里的id與元素中的id不同,只是組件在jsPlumb中的唯一標識而已,在控制台打印之后,能看到內部提供了很多方法,另外注意原型鏈中的方法。
1

在官方的Hiding/Showing Overlays中,向我們展示了setVisible,showOverlay(id),hideOverlay(id)removeOverlay(id)等方法,當然,因為對象中有DOM元素,我們也可以使用其他方法來控制DOM元素。

Groups

相當於給節點之間加入了分組的概念,一旦分組,那么就可以使用組來控制組下的所有元素。
但這里的分組仍然是在jsPlumb中建立索引,當有相關事例時,再進行介紹。

Drag

如果不使用jsPlumb提供的拖動,則需要使用repaint()來對拖動之后的連線進行重繪。
而當修改了節點的層級,或者偏移則需要使用revalidate(container)來刷新。

Establishing Connections

在上面的例子中,已經介紹了基本的連接方式jsPlumb.connect({source:"element1",target:"element2"})
這種方式創建的連接線一旦移除,則創建的端點也會自動移除。如果不想端點被移除,則可以繼續加參數,將
deleteEndpointsOnDetach設為false。如果不想鼠標能夠移除連接線,則可以在局部配置中將ConnectionsDetachable設為false,或者在connect時,加入detachable:false

拖放連接

一開始就要創建一個端點來作為源點

var endpoint = jsPlumb.addEndpoint('elementId',{isSource:true})

這樣就可以從該端點拉線出去。
如果給另一個創建的點加入isTarget:true,則就可以用上面的點連入這個點。

或者使用makeSource或者makeTarget

jsPlumb.makeSource("ele1",{ anchor:"Continuous", maxConnections:1 ··· })

上述例子中,如果配置了maxConnections,則最多只能出現這個參數的連線,一旦多於這個數目的連線,就可以用onMaxConnections(params,originalEvent)這個回調函數來做其他事.
connectmakeSource,makeTarget,都可以配置第三個參數,相當於公共配置參數,與第二個參數類似。
-----------------------------------------------------------------------

connect中如果使用newConnection:true參數,則會取消makeTarget,makeSoucr,addEndpoint中所添加的配置項,重繪連接線。

makeTarget也有onMaxConnections方法。
因為makeTarget包括上面介紹的isTarget都可以指向源點元素,所以,如果不想造成回環(自己連自己),則可以在makeTarget中設置allowLoopback:false.如果只想產生一個端點,而不是多個端點,則需要使用uniqueEndpoint:true.
默認情況下,使用makeTarget創建的端點將deleteEndpointsOnDetach設置為true,即刪除連線,端點刪除;如果不要刪除,則需要手動改為false。
--------------------------------------------------------

如果既配置了元素可拖動,又設置了元素可拖放連接,那jsPlumb沒有辦法區分拖動元素和從元素中拖動連接,所以它提供了filter方法。

jsPlumb.makeSource("foo",{ filter:"span", filterExclude:true });

則除span元素的其他元素都可以創建拖放連接,filter也接受函數。filter:function(event,element).

也可以使用isTarget("id"),isSource("id")來判斷節點是否成為了源點。
如果配置了source和target之后,想切換源的激活狀態,則可以使用setTargetEnabled(id),setSourceEnabled(id)
如果想取消makeTargetmakeSource所創建的源點,可以使用:

  • unmakeTarget("id")
  • unmakeSource("id")
  • unmakeEveryTarget
  • unmakeEverySource

Drag and Drop scope

如果使用了jsPlumb自帶的drag或者drop,那么給端點配置scope是很有必要的,這意味着之后創建端點只能連接到對應scope的端點。如果不設置scope,其默認的scope是一樣的。

Removeing Nodes

移除節點沒什么好說的,關鍵還是要移除與之關聯的端點和連接線。

Removeing Connections/Endpoints

Connections

  1. detach

    var conn = jsPlumb.connect({...}); jsPlumb.detach(conn);
    如果使用該方法來刪除連接線,那么會有幾種情況:
  • 如果使用jsPlumb.connect創建的線,而且沒有設置deleteEndpointsOnDetach:false,則使用detach時,端點也會一起被移除。
  • 如果通過makeSource配置的元素創建了連接線,而且沒有設置deleteEndpointsOnDetach:false,則使用detach時,端點也會一起被移除。
  • 如果使用addEndpoint注冊的元素通過鼠標創建了連接線,則不會刪除端點。
  1. detachAllConnections(el,[params])
    用於刪除元素上的所有連接線。

  2. detachEveryConnection()
    刪除所有連接線。

Endpoints

  1. deleteEndpoint
    刪除一個端點。
  2. deleteEveryEndpoint
    刪除所有的端點

Connection and Endpoint Types

可以通過提供的方法來動態的修改連接線或端點的樣式。

Connection Type

jsPlumb.registerConnectionType("example",{ paintStyle:{stroke:"blue",strokeWidth:5}, }); var c = jsPlumb.connect({source:"someDiv",target:"someOtherDiv"}); c.bind("click",function(){ c.setType("example") });

當點擊連接線時,會替換連接線的樣式
也可以使用:

jsPlumb.registerConnectionTypes({
  "basic":{  paintStyle:{stroke:"blue",strokeWidth:7} }, "selected":{  paintStyle:{stroke:"red",strokeWidth:5} } }); c.bind("click",function(){ c.toggleType("selected"); });

而type支持的屬性都和css相關:

  • anchor
  • anchors
  • detachable
  • paintStyle
  • hoverPaintStyle
  • scope
  • cssClass
  • parameters
  • overlays
  • endpoint

Endpoint type

jsPlumb.registerEndpointTypes({
  "basic":{  paintStyle:{fill:"blue"} } });

端點的type支持的參數:

  • paintStyle
  • endpointStyle
  • hoverPaintStyle
  • endpointHoverStyle
  • maxConnections
  • connectorStyle
  • connectorHoverStyle
  • connector
  • connectionType
  • scope
  • cssClass
  • parameters
  • overlays

Events

首先看個小例子:

jsPlumb.bind("connection",function(info){ console.log(info); });

connection(info,originalEvent)即監聽所有的連接事件。info包含的信息有:

  • connection
  • sourceId
  • targetId
  • source
  • target
  • sourceEndpoint
  • targetEndpoint

connectionDetached(info,originalEvent)即監聽當連接斷掉時的事件。info類似connection.

右鍵點擊也有相應的contextmenu方法。

關於connection和endpoint的事件方法,請參考官網api。
記錄下overlay的事件。

jsPlumb.connect({ source:"el1", target:"el2", overlays:[ ["Label",{ events:{ click:function(labelOverlay,originalEvent){ console.log(labelOverlay); } } } }], ] })

同樣,使用unbind方法,可以移除上面所添加的監聽。

篩選jsPlumb

使用jsPlumb.select()方法,用於在Connections列表上做篩選,打印一下值:
2
就可以使用這些方法對於連接線來進行獲取(get)和修改(set)。
還有getConnections,getAllConnections()等方法也可以獲取到連接線,只不過這兩個方法沒有上面slect的方法,相當於靜態屬性

使用jsPlumb.selectEndpoints()方法,用於在Endpoints上做篩選,同樣有相應的方法。

select()selectEndpoints()都有一個each方法,用於對篩選出的方法進行操作。

Repainting an element or elements

當需要對修改過后的元素重新計算端點和連接線時,則可以使用

jsPlumb.repaint(el,[ui])

jsPlumb.repaintEverything().

Element Ids

當元素上的id也被改變時,可以使用

jsPlumb.setId(el,newId);
//or jsPlumb.setIdChanged(oldId,newId);

來重新對之前注冊的節點進行修改。

小結

前期調研完成,接下來開始使用jsPlumb做幾個小例子


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM