背景需求

目前系统中对于工艺流程的展示是纯粹的瀑布式流程,如图所示:

image.png

导致的问题是1)没有办法展示复杂的工艺关联 2)在展现上比较简陋不符合客户的审美。

基于以上问题,决定基于jsplumb开发用于复杂工艺流程展示的控件,预计效果图如下:


image.png

技术介绍

jsplumb是一款有条件开源的javascript类库,基于SVG提供页面元素的连接。之所以说有条件开源,是因为jsplumb存在两个分支:

1) Toolkit Edition商用版,基于社区版本进行封装提供更丰富的API支持

2) Community Edition社区版,基于MITGPL2协议进行开源,提供基础的API功能

 

jsplumb的核心思想是对于页面元素的连接,在这个思想上对于连接进行了抽象,从而形成了jsplumb的几个基本要素:

²  Anchor –

锚点主要用来定位一个端点的位置,更多的是一个逻辑上而非实体的概念,用户不可以直接创建,而是通过内部的机制生成

²  Endpoint – 端点

作为每一个连接的终点而存在,可以通过编程来显示的创建

²  Connector – 连接器

连接器作为连接的抽象,提供了两个元素之间进行连接的方式

²  Overlay – 镀层

jsplumb通过镀层的方式给为连接器进行用户友好的展示,如通过label的方式

²  Group – 分组

通过分组可以将一组元素作为一个整理,从而进行整体的拖拽和收缩等

 

 

一般来说两个端点,一个连接器,0到多个镀层一起工作共同组成了一次连接。每一个端点都有一个关联的锚点。

 


主要难题

在熟悉了jsplumb主要的概念后,可以通过官方API了解一些主要功能。同时可以通过githubhttps://github.com/jsplumb/jsplumb/tree/master/demo)下载官方demo进行学习。

 

通过观察,我们发现官方demo中的flowchart比较符合我们的需求,于是我们下载flowchartdemo源码进行研究改造。原始的demo效果图如下:

image.png

下面是几个主要的改造点:

1, 代码合并压缩

我们发现在demo中引入了一堆jscss,我们通过合并压缩最后形成了下面三个文件

<script src="jsplumb-link.min.js"></script>

  包含jsbezier.jsmottle.jsbiltong.jskatavorio.js

<script src="jsplumb-lib.min.js"></script>

  包含jsplumb核心类库相关的16js文件,注意合并的顺序

    <script src="jsPlumb_process.js"></script>

     和process组件相关的js,基于jsplumb的封装和客户化


2,每一个连接都有个Overlay的label来展示,实际需求不需要

connectionOverlay定义中发现了同时对于箭头和文字都做了定义,直接将对于label的定义去除即可

    ConnectionOverlays: [
            [ "Arrow", {
                location: 1,
                visible:true,
                width:11,
                length:11,
                id:"ARROW",
                events:{
                    click:function() { alert("you clicked on the arrow overlay")}
                }
            } ],
            [ "Label", {
                location: 0.1,
                id: "label",
                cssClass: "aLabel",
                events:{
                    tap:function() { alert("hey"); }
                }
            }]
        ]

3, 如何禁止新增新连接

在官方demo中可以通过鼠标拖动来新增一条连接,而API并没有对于连接的enabledisable的定义。

Github有人回复说通过设置ConnectionsDetachable 属性来实现,实际效果并不能达到目的。

 

最终在初始化方法中通过两个方法的组合实现了这个功能

instance.unmakeEveryTarget().unmakeEverySource();


4, 数据的加载和导出

数据导出功能在社区版不提供方法支持,不过我们可以通过一些简单的变通来实现;而导出功能和加载功能是相对应的,实现了数据的导出就可以基于现有的数据结构来实现数据的初始化加载。

以下是导出方法的实现:

function exportData(){
               var blocks=[];                          
                    $(".w").each(function(idx, elem){
                            var elem=$(elem);                           
                            blocks.push({
                                    BlockId:elem.attr('id'),
                                    BlockContent:elem.text(),
                                    BlockX:parseInt(elem.css("left"), 10),
                                    BlockY:parseInt(elem.css("top"), 10)
                            });                              
                    });
                    var serliza=JSON.stringify(blocks);
                    $("#outputText").text(serliza);
           }

  主要思路是获取页面元素的id和名称以及他们和容器的相对位置,最终通过json的格式进行存储。至于元素之间的关系通过业务系统保存和维护。


相应的我们可以实现我们的导入方法:

var loadJson = function(data){
          var unpack=JSON.parse(data);
          if(!unpack){
                   return false;
          }       
          unpack.map(function(value, index, array) {
                   var _block = eval(value);
                   newNodeWithName(_block.BlockId,_block.BlockContent, _block.BlockX, _                      block.BlockY);
          });
          return true;
          }
   
var newNodeWithName = function(id, name, x, y){
    var d = document.createElement("div");
        d.className = "w";
        d.id = id;
        d.innerHTML = name.substring(0, 7) + "<div class=\"ep\"></div>";
        d.style.left = x+ "px";
        d.style.top = y+ "px";
        instance.getContainer().appendChild(d);
        initNode(d);
        return d;
}

 

实现思路是通过解析json获取每一个元素的idname并通过相对位置在容器中绘制出来。


5, 自动对齐

通过页面拖动的元素不像传统的流程图工具提供自动对齐的功能,我们基于像素级别对元素对齐进行了基本的约束。

Def
 X(e) = 元素e的起始横坐标
 Y(e) = 元素e的起始纵坐标
 W(e) = 元素e的宽度
 H(e) = 元素e的高
 
If abs(Diff(X(a),X(b))) between (0, W(a)) set X(a) = X(b)
If abs(Diff(Y(a),Y(b))) between (0, H(a)) set Y(a) = Y(b)

 

实现代码:

window.autoAlignment = function(){
            var baseX = Number($(".w").eq(0).css("width").replace("px",""));           
            var baseY = Number($(".w").eq(0).css("height").replace("px",""));
            var thatX=0, thatY=0, thisX = 0, thisY=0, deltaX = 0, deltaY = 0;
            var index = 0;
            var eleArray = $(".w");
            for(var i =0 ; i < eleArray.length; i++){
                   thatX = Number($(eleArray[i]).css("left").replace("px",""));
                   thatY = Number($(eleArray[i]).css("top").replace("px",""));
              for(var j =i+1; j < eleArray.length; j++){
                     thisX = Number($(eleArray[j]).css("left").replace("px",""));
                     thisY = Number($(eleArray[j]).css("top").replace("px",""));
                     deltaX = Math.abs(thisX - thatX);
                     deltaY = Math.abs(thisY - thatY);
                     if(deltaX < baseX && deltaX >0 && deltaY >=baseY){
                       // 需要调整x
                           console.log("x "+ j);
             $(eleArray[j]).css("left",thatX+"px");
                     }
 
                     if(deltaY < baseY && deltaY >0 && deltaX >=baseX){
                           console.log("y "+ j);
                       $(eleArray[j]).css("top",thatY+"px");
                     }
                   }
            }
           //通过repaintEverything完成位置调整后的重绘
            instance.repaintEverything();
         }


6, Zoom

同样由于在社区版不提供zoom的接口,我们只能通过自己来实现zoom功能。

window.setZoom = function (zoom, instance0, transformOrigin, el) {
        transformOrigin = transformOrigin || [0.5, 0.5];
             instance = instance || jsPlumb;
             el = el || instance.getContainer();
             var p = ["webkit", "moz", "ms", "o"],
            s = "scale(" + zoom + ")",
            oString = (transformOrigin[0] * 100) + "% " + (transformOrigin[1] * 100) + "%";
       
             for (var i = 0; i < p.length; i++) {
                 el.style[p[i] + "Transform"] = s;
                 el.style[p[i] + "TransformOrigin"] = oString;
             }
 
             el.style["transform"] = s;
             el.style["transformOrigin"] = oString;
 
             instance.setZoom(zoom, true);
             instance.repaintEverything();
            
         };

实现思路通过监听事件来设置style属性实现滚动,最终调用重绘方法进行整体调整。需要注意的是监听事件应该绑定到容器的上一层,如下红色部分,否则缩放的是整个页面起不到zoom流程图的初衷

<div class="jtk-canvas canvas-wide process-canvas jtk-surface jtk-surface-nopan">
<div style="overflow:visible !important;" id="canvas">
                                   </div>
   </div>



反思

通过以上几点扩展基本能满足复杂流程图的展示,如下:

image.png


我们仍然需要解决的问题是流程图初始位置的计算。

目前可以通过一些简单的算法来保证现有流程图不出现重叠,但是如何帮助用户最大程度上节省拖动,还需继续研究。