[转]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", {  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,  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({
                 _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,
                             (_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,
                             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,
                             (_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,
                             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),
                             (_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,
                             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),
                             (_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", { 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做几个小例子

原文地址:https://www.cnblogs.com/liangqihui/p/8559672.html