jsplumb.js小地图和框选实现策略
背景:
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-