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-


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM