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-

posted @ 2020-06-28 10:33  桥南小院  阅读(1423)  评论(4编辑  收藏  举报