背景需求
目前系統中對於工藝流程的展示是純粹的瀑布式流程,如圖所示:
導致的問題是1)沒有辦法展示覆雜的工藝關聯 2)在展現上比較簡陋不符合客戶的審美。
基於以上問題,決定基於jsplumb開發用於複雜工藝流程展示的控件,預計效果圖如下:
技術介紹
jsplumb是一款有條件開源的javascript類庫,基於SVG提供頁面元素的連接。之所以說有條件開源,是因爲jsplumb存在兩個分支:
1) Toolkit Edition商用版,基於社區版本進行封裝提供更豐富的API支持
2) Community Edition社區版,基於MIT和GPL2協議進行開源,提供基礎的API功能
jsplumb的核心思想是對於頁面元素的連接,在這個思想上對於連接進行了抽象,從而形成了jsplumb的幾個基本要素:
² Anchor – 錨
錨點主要用來定位一個端點的位置,更多的是一個邏輯上而非實體的概念,用戶不可以直接創建,而是通過內部的機制生成
² Endpoint – 端點
作爲每一個連接的終點而存在,可以通過編程來顯示的創建
² Connector – 連接器
連接器作爲連接的抽象,提供了兩個元素之間進行連接的方式
² Overlay – 鍍層
jsplumb通過鍍層的方式給爲連接器進行用戶友好的展示,如通過label的方式
² Group – 分組
通過分組可以將一組元素作爲一個整理,從而進行整體的拖拽和收縮等
一般來說兩個端點,一個連接器,0到多個鍍層一起工作共同組成了一次連接。每一個端點都有一個關聯的錨點。
主要難題
在熟悉了jsplumb主要的概念後,可以通過官方API瞭解一些主要功能。同時可以通過github(https://github.com/jsplumb/jsplumb/tree/master/demo)下載官方demo進行學習。
通過觀察,我們發現官方demo中的flowchart比較符合我們的需求,於是我們下載flowchart的demo源碼進行研究改造。原始的demo效果圖如下:
下面是幾個主要的改造點:
1, 代碼合併壓縮
我們發現在demo中引入了一堆js和css,我們通過合併壓縮最後形成了下面三個文件
<script src="jsplumb-link.min.js"></script>
包含jsbezier.js,mottle.js,biltong.js和katavorio.js
<script src="jsplumb-lib.min.js"></script>
包含jsplumb核心類庫相關的16個js文件,注意合併的順序
<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並沒有對於連接的enable和disable的定義。
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獲取每一個元素的id和name並通過相對位置在容器中繪製出來。
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>
反思
通過以上幾點擴展基本能滿足複雜流程圖的展示,如下:
我們仍然需要解決的問題是流程圖初始位置的計算。
目前可以通過一些簡單的算法來保證現有流程圖不出現重疊,但是如何幫助用戶最大程度上節省拖動,還需繼續研究。