背景:
jsplumb.js官方開源免費版不支持內置小部件功能,比如:小地圖、框選、編輯等快捷功能,因此參考收費版功能自定義設置。
一、小地圖:
選用vue編輯畫布流程圖+自定義點擊節點彈框設置內容:以下是生成后的結構
<div class="jtk-demo-main"> <!-- this is the main drawing area --> <div class="jtk-demo-canvas canvas-wide jtk-surface"> <!-- miniview --> <div class="miniview jtk-miniview" style="display: block;"> </div> <!--miniview surface --> <div class="jtk-surface-canvas"></div> </div> <div class="jtk-demo-rhs"> <!-- the current dataset --> <div class="jtk-demo-dataset"></div> </div> </div>
工具版生成模板demo:
<template> <div class="jtk-demo-main"> <!-- this is the main drawing area --> <div class="jtk-demo-canvas"> <!-- miniview --> <div class="miniview"></div> </div> </div> </template> <script> import "./index.css"; import "./jsplumb.js";// 局部引入 export default { data() { return { jsPlumb: null,
jsplumToolkit:null }; }, mounted() {
this.jsplumb = this.$jsplumb || jsplumb //全局注冊或局部引入
this.jsplumbToolkit = this.$jsplumbToolkit || jsplumbToolkit //全局注冊或局部引入
if(this.jsplumb){
this.info()
} }, methods: { init() { this.jsPlumb.ready(function() { // prepare some data var data = { nodes: [ { id: "1", label: "jsPlumb" }, { id: "2", label: "Toolkit" }, { id: "3", label: "Hello" }, { id: "4", label: "World" } ], edges: [ { source: "1", target: "2" }, { source: "2", target: "3" }, { source: "3", target: "4" }, { source: "4", target: "1" } ] }; // get a new instance of the Toolkit var toolkit = this.jsPlumbToolkit.newInstance(); var mainElement = document.querySelector(".jtk-demo-main"), canvasElement = mainElement.querySelector(".jtk-demo-canvas"), miniviewElement = mainElement.querySelector(".miniview"); toolkit.render({ // 實際是js動態創建div插入綁定生成畫布 container: canvasElement, miniview: { container: miniviewElement }, layout: { type: "Spring" } }); // load the data. toolkit.load({ data: data }); }); } } }; </script>
了解原理后,自己設置一個小地圖:
1..jquery操作(只是編寫時簡化,編譯后可能更復雜,不推薦);
2.原生js操作;
<!-- 2.畫布內容:左側1.拖拽節點菜單 --> <div class="jtk-demo-main"> <!-- 2.1頂欄工具欄插口 --> <div style="padding: 5px"> <slot name="header" /> </div> <!-- 2.2畫布編輯框 --> <div style="border:1px solid #e1e1e1;position:relative;" id="flow-chart-container" > <!-- 2.3右上角圖標 --> <span>款選圖標</span> <!-- 2.4.miniview:小地圖 --> <div id="miniview" @mousewheel="miniViewRoll(this)" > <div class="miniview jtk-miniview" :class="collapsed?'jtk-miniview-collapsed':''" id="jtk-miniview" style="display: block;" > <div class="jtk-miniview-canvas" id="jtk-miniview-canvas" /> <div class="jtk-miniview-panner" /> <div class="jtk-miniview-collapse" @click="collapsed=!collapsed" /> </div> </div> <!-- 2.5自定義節點 --> <div :connections="connections" :elements="elements" :key="id" ref="chart" source-key="source" target-key="target" @element-click="handleELementClick" @container-click="onContainerClcik" @on-element-new="onElementNew" @on-element-delete="onElementDelete" @on-connection-new="onConnectionNew" @on-connection-delete="onConnectionDelete" @on-connection-drag-start="onConnectionDragStart" @on-connection-drag-end="onConnectionDragEnd" @on-connection-moved="onConnectionMoved" :left="x" :top="y" > <template slot-scope="scope"> <slot :elData="scope.elData"> <ElementSlot :el-data="scope.elData" /> </slot> </template> </div> </div> </div>
這里只分析小地圖實現邏輯,畫布繪制分:節點元素+節點連線,小地圖視圖只需要遍歷生成dom節點即可,問題是如何與實際生成的畫布綁定變動后的方位和縮放比例。
this.elements.forEach((v,i)=>{// 起始點設置為圓點 const canvasBox = document.getElementById('jtk-miniview-canvas') const nodeBox = document.createElement('div') nodeBox.className ="jtk-miniview-element" nodeBox.style =i===0?"border-radius: 50%;width: 40px; height: 40px; position: absolute;":"width: 80px; height: 30px; position: absolute;" nodeBox.style.left = v.x+'px' nodeBox.style.top = v.y+'px' canvasBox.appendChild(nodeBox) })
1.小地圖移動和縮放:
.jtk-miniview-panner { border: 5px dotted WhiteSmoke; opacity: 0.4; background-color: rgb(79, 111, 126); cursor: move; cursor: -webkit-grab;// 上面是靜態樣式,下面是動態綁定 width: 1903px; height: 937px; position: absolute; transform-origin: 0px 0px; transform: scale(0.1); left: -81.0315px; top: 8.70836px; }
解析:實際的畫布大小是1903*937,小地圖縮放倍數是0.1,定位偏差left、top是實際相反方向的取值:比如畫布向左移動,小地圖的遮罩層應該向右移動,這樣就能看清全局畫布占位。
而縮放比例改變transform的同時(也是取反),也會改變定位偏差的大小。
如果小地圖只是模仿官網節點效果,可以直接繪制節點(實際需求需要繪制線條):
mounted() { this.instance = this.$refs.chart;// 實例化流程圖對象 this.miniviewNode();// 繪制默認小地圖(只有開始節點) window.addEventListener('keyup',this.handleKeyup)// 監聽鍵盤事件 }, destroyed () {// 銷毀粘貼板內容+鍵盤監聽事件 localStorage.removeItem("kp-canvas-copy"); window.removeEventListener('keyup',this.handleKeyup) // window.removeEventListener('scroll',this.handleScroll) }, methods: { // 鍵盤事件 handleKeyup(event){ let self = this; document.onkeydown = function (e) { let evn = e || event; let key = evn.keyCode || evn.which || evn.charCode; // ctrl + v if (evn.keyCode === 86 && evn.ctrlKey) { // console.log(666) } // delete if (key === 46) { // console.log(7777) } } }, miniViewRoll() { // 小地圖滾動 if (event.wheelDelta === 120) { //±120 this.handleZoomIn();// 放大 } else { this.handleZoomOut();// 縮小 } }, miniviewNode(){// 繪制靜態節點:先移除已有節點 let childs = [1,2,3,4]; for (var i = childs.length - 1; i >= 0; i--) { canvasBox.removeChild(childs[i]); } elements.forEach((v, i) => { const nodeBox = document.createElement("div"); nodeBox.className = "jtk-miniview-element"; nodeBox.style =i === 0 ? "border-radius: 50%;width: 40px; height: 40px; position: absolute;": "width: 80px; height: 30px; position: absolute;"; nodeBox.style.left = v.x + "px"; nodeBox.style.top = v.y + "px"; canvasBox.appendChild(nodeBox); // 根據畫布大小設置0.1倍的蒙版大小 const canvasBox = document.querySelector("#flow-chart-container"); this.widthPanner = canvasBox.offsetWidth; this.heightPanner = canvasBox.offsetHeight; }); }, // 框選節點 CheckBoxNodes() { const self = this; var stateBar = document.getElementById("flow-chart-container"); stateBar.style["cursor"] = "auto"; // stateBar.style['pointer-events'] = 'none' stateBar.onmousedown = function(e) { var posx = e.clientX; var posy = e.clientY; var div = document.createElement("div"); div.id = "selectDiv"; div.className = "tempDiv"; div.style.left = e.clientX + "px"; div.style.top = e.clientY + "px"; document.body.appendChild(div); document.onmousemove = function(ev) { div.style.left = Math.min(ev.clientX, posx) + "px"; div.style.top = Math.min(ev.clientY, posy) + "px"; div.style.width = Math.abs(posx - ev.clientX) + "px"; div.style.height = Math.abs(posy - ev.clientY) + "px"; document.onmouseup = function() { var selDiv = document.getElementById("selectDiv"); var fileDivs = document.getElementsByClassName("node"); var selectedEls = []; var l = selDiv.offsetLeft; // 減去容器位置 var t = selDiv.offsetTop; // 減去容器位置 var w = selDiv.offsetWidth; var h = selDiv.offsetHeight; for (var i = 0; i < fileDivs.length; i++) { //所有節點 var sl = fileDivs[i].getBoundingClientRect().left; var st = fileDivs[i].getBoundingClientRect().top; if (sl > l && st > t && sl < l + w && st < t + h) { // 區域內節點 fileDivs[i].className += " " + "is-active"; selectedEls.push(fileDivs[i].id); } } self.selectedEls = selectedEls; if(selectedEls.length>0){ self.$message.success("已批量選中,通過ctrl+V粘貼(允許跨畫布)或delete刪除") } //********************************** */ stateBar.style.cursor = "grab"; div.parentNode.removeChild(div); stateBar.onmousedown = null; document.onmousemove = null; document.onmouseup = null; setTimeout(() => {// 設置延遲禁止移動畫布 self.checked = false; }, 200); }; }; }; }, }
頁面設置:點擊圖標、切換樣式、禁用移動畫布、添加圖標效果等
<div id="miniview" @mousewheel="miniViewRoll(this)" > <div class="miniview jtk-miniview" :class="collapsed ? 'jtk-miniview-collapsed' : ''" id="jtk-miniview" style="display: block;" > </div> <div style="position: absolute;right: 10px;bottom: 5px;z-index: 5;cursor: pointer;color: #63656E;"> <i :class="collapsed?'fa fa-eye-slash':'fa fa-eye'" @click.stop="collapsed = !collapsed" /> </div> </div>
3.使用jsplumb再實例化一個節點+連線:目的是使小地圖增加連線
<div id="miniview" @mousewheel="miniViewRoll(this)" @mousemove="miniviewMousemove" @mouseleave="miniviewMouseleave" > <div id="jtk-miniview" class="miniview jtk-miniview" :class="collapsed ? 'jtk-miniview-collapsed' : ''" :style="styleObjectMini" @mouseleave="miniViewleave" @click="handleClickMiniview" > <div id="jtk-miniview-canvas" class="jtk-miniview-canvas" :style="styleObjectCanvas" /> <div id="jtk-miniview-panner" v-drag="miniviewPannerMove" class="jtk-miniview-panner" :style="styleObjectPanner" /> </div> <div style="position: absolute;right: 10px;bottom: 5px;z-index: 5;cursor: pointer;color: #63656E;"> <i :class="collapsed?'fa fa-eye-slash':'fa fa-eye'" @click.stop="collapsed = !collapsed" /> </div> </div>
問題:因為繪制了2個實例,需要做一些優化,比如
reloadMiniview(v) { if (!this.$refs.chart) {// 實例未加載前不繪制 return; } if (this.collapsed){// 小地圖折疊后不繪制 return; } if (this.miniviewLock){// 如果上一個繪制過程未結束,不重新繪制 return; } if (this.rightMenuIsActive){ // 如果右側菜單欄打開,不重新繪制 return; } this.miniviewLock = true // 異步繪制 setTimeout(()=>{ this.drawMiniView(v) },200) this.miniviewLock = false },
繪制小地圖實例:通過css3等比縮小顯示
drawMiniView(v){ this.miniviewJsplumbInstance = jsPlumb.getInstance({// 實例化 'Connector': ['Straight'], 'Anchors': ['Bottom', 'Top'] }); const canvasBox = document.getElementById('jtk-miniview-canvas'); canvasBox.innerHTML = "" let nodes = v?v:this.$refs.chart.getAllElements(); nodes.forEach((node)=>{ this.addNodeToMiniview(node); }); this.$nextTick(()=>{ this.miniviewJsplumbInstance.setSuspendDrawing(true); const connections = this.$refs.chart.getAllRawConnectionsData(); connections.forEach((conn)=>{ this.miniviewJsplumbInstance.connect({ 'source': conn.sourceId + 'miniview', 'target': conn.targetId + 'miniview', 'paintStyle': {'stroke': 'black', 'strokeWidth': 6} }); }); this.miniviewJsplumbInstance.setSuspendDrawing(false,true); }) // 設置移動蒙版大小 this.$nextTick(() => { const canvasBox = document.querySelector('#flow-chart-container'); this.widthPanner = canvasBox.offsetWidth; this.heightPanner = canvasBox.offsetHeight; }); }, addNodeToMiniview(node){ // 將原始畫布中的節點加入到縮略圖中 const canvasBox = document.getElementById('jtk-miniview-canvas'); const nodeBox = document.createElement('div'); nodeBox.className = 'jtk-miniview-element'; nodeBox.style = this.miniviewNodeStyle(node.data.dispatch_type === 'start'); let transformedVPoint = this.transformCoordinate(node.x, node.y); nodeBox.style.left = transformedVPoint[0] + 'px'; nodeBox.style.top = transformedVPoint[1] + 'px'; nodeBox.id = node.elId + 'miniview'; canvasBox.appendChild(nodeBox); }, transformCoordinate(x, y) { return [x - this.originX, y - this.originY]; }, miniviewNodeStyle(isStart) { let style = ''; if (isStart) { style += 'border-radius: 50%;width: 40px; height: 40px; position: absolute;'; } else { style += 'width: 80px; height: 30px; position: absolute;'; } return style; },
-end-