2019年7月1日 星期一

在Vue.js的treeview增加drag功能

在Vue.js的treeview增加drag功能

前言

  還記得在使用vue.js建構treeview時說過treeview是為了給遊戲引擎用的,但前一篇雖做出了treeview但還是少了一個功能,就是drag and drop,這功能是必備的功能,這次要把這個功能實現,在此做個紀錄。

內容

  在實現drag and drop時由於之前都沒在網頁上實現過,所以到HTML5 Drag and Drop裡學習,大致上沒什麼問題,大致的流程如下:
1.在要Drag的DOMElement裡設定"draggable"為"true"。
2.在Drag的DOMElement裡新增"ondraagstart"事件,該事件需要記錄哪個物件被drag。
3.在要Drop的DOMElement裡新增"ondragover"事件,該事件會決定能不能被drop。
4.在要Drop的DOMElement裡新增"ondrop"事件,該事件會取得步驟2紀錄的drag物件,並執行  Drop。

正個過程看起來沒什麼問題,但實際用起來卻發現有個問題,在步驟2的事件紀錄drag物件,然後在步驟4取出,這個流程會利用事件的"dataTransfer"來記錄物件,而且會發現範例是用ID的方式紀錄資料,然後步驟4時取出ID後,透過"document.getElementById()"取得DOMElement。查了一下"dataTransfer",無法紀錄物件的reference,如果要用"dataTransfer"來記錄treeview的節點一定要支援可以用一個字串代表的ID來找到對應的點,這裡卡了一下子,最後想到在步驟2與步驟4的啟動事件,啟動事件會喚起控制物件的事件,並把drag的物件記錄在控制物件,說起來抽象了點,看以下範例
html的部分
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
  <title>JS Bin</title>
</head>
<body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.0.3/vue.js"></script>
<div id='app'>
  <root_treenodeview :rootnode_list='nodelist' :evtdragnode="onDragNode" :evtdropnode="onDropNode" :evtdropnodeparent="onDropNodeParent"></root_treenodeview>
</div>
</body>
</html>

javascript的部分
Vue.component('treenodeview',{
  props : ['node','evtdragnode','evtdropnode','evtdropnodeparent'],
  template : [
    '<ul>\n',
    '  <li>\n',
    '    <div @click="onClick" draggable="true" @dragstart="onDragStart" @dragover="onDragOver" @drop="onDrop">\n',
    '      {{node.name}}\n',
    '      <span v-show="isFolder">[{{isExpand?"-":"+"}}]</span>\n',
    '    </div>\n',
    '  </li>\n',
    '  <div style="height:10px;" @dragenter="onDragEnterParent" @dragover="onDragOverParent" @dragleave="onDragLeaveParent" @drop="onDropParent">\n',
    '    <hr v-show="isShowDragParent" style="margin:0px;">\n',
    '  </div>\n',
    '  <treenodeview v-show="isExpand" v-for="childNode in node.child" :node="childNode" :evtdragnode="evtdragnode" :evtdropnode="evtdropnode" :evtdropnodeparent="evtdropnodeparent"></treenodeview>\n',
    '</ul>\n'
  ].join(''),
  data : function(){
    return {
      isExpand : false,
      isShowDragParent : false,
    };
  },
  computed:{
    isFolder:function(){
      return this.node.child &&
              this.node.child.length;
    }
  },
  methods:{
    onClick : function(){
      if(this.isFolder){
        this.isExpand=!this.isExpand;
      }
    },
    onDragStart : function(evt){
      if(this.evtdragnode)
        this.evtdragnode(this.node);
    },
    onDragOver : function(evt){
      evt.preventDefault();
    },
    onDrop : function(evt){
      evt.preventDefault();
      if(this.evtdropnode){
        this.evtdropnode(this.node);
      }
    },
    onDragEnterParent : function(evt){
      this.isShowDragParent = true;
    },
    onDragOverParent : function(evt){
      evt.preventDefault();
      this.isShowDragParent = true;
    },
    onDragLeaveParent : function(evt){
      this.isShowDragParent = false;
    },
    onDropParent : function(evt){
      evt.preventDefault();
      this.isShowDragParent = false;
      if(this.evtdropnodeparent){
        this.evtdropnodeparent(this.node);
      }
    }
  }
});
Vue.component('root_treenodeview',{
  props : ['rootnode_list','evtdragnode','evtdropnode','evtdropnodeparent'],
  template:[
    '<div>\n',
    '  <treenodeview v-for="rootNode in rootnode_list" :node="rootNode" :evtdragnode="evtdragnode" :evtdropnode="evtdropnode" :evtdropnodeparent="evtdropnodeparent"></treenodeview>\n',
    '</div>\n',
  ].join(''),
});
//
function Node(opt){
  this.name = (opt.name) ? opt.name : '';
  this.parent = undefined;
  this.child = [];
}
Node.prototype.setParent = function(parentNode){
  let isFindInParent = false;
  let findNode = parentNode;
  while(findNode){
    if(findNode === this){
      isFindInParent = true;
      break;
    }
    findNode = findNode.parent;
  }
  if(isFindInParent)
    return;
  
  if(this.parent){
    //
    if(isFindInParent)
      return;
    let index=-1;
    for(let i=0;i<this.parent.child.length;i++){
      if(this.parent.child[i] === this){
        index = i;
        break;
      }
    }
    //
    if(index >= 0){
      this.parent.child.splice(index,1);
    }
  }
  //
  this.parent = parentNode;
  if(parentNode)
    parentNode.child.push(this);
};
let nodeRoot=new Node({name:"root"});
let node1=new Node({name:"node1"});
let node2=new Node({name:"node2"});
node1.setParent(nodeRoot);
node2.setParent(node1);

let app = new Vue({
  el : '#app',
  data:function(){
    return {
      nodelist:[nodeRoot],
      dragNode : null
    }
  },
  methods:{
    onDragNode:function(node){
      this.dragNode = node;
    },
    onDropNode:function(node){
      if(this.dragNode){
        if(this.dragNode.parent === undefined){
          let index=-1;
          for(let i=0;i<this.nodeList.length;i++){
            if(this.nodeList[i] === this.dragNode){
              index = i;
              break;
            }
          }
          //
          if(index>=0){
            this.nodeList.splice(index,1);
          }
        }
        this.dragNode.setParent(node);
      }
    },
    onDropNodeParent:function(node){
      if(this.dragNode && this.dragNode!==node){
        if(node.parent)
          this.dragNode.setParent(node.parent);
        else{
          this.dragNode.setParent(undefined);
          this.nodeList.push(this.dragNode);
        }
      }
    }
  }
});


這次的範例是從改進Vue.js的treeview裡改的,node的資料之前是用JSON直接給,這次寫成class,並且新增"parent"屬性,這屬性是必要的,注意在"Node.setParent()"實現時,每次都會檢查是否可以加在該點的parent,要注意Node與Node之間不能形成循環關係!控制物件指的是範例的"app"這個變數,,可以看到新增的"onDragNode()"與"onDropNode()",在html的部分要記得綁定事件,"treenodeview"與"root_treenodeview"這兩個component也要記得綁定事件,"treenodeview"的部分就和一般的drag and drop一樣,實現那4個步驟即可,並在綁定事件時喚起事件讓控制物件的事件被喚起。接著說明Drag to parent的機制,先看下圖
Drag to parent的操作
drag到結點下方的位置時會出現一條分隔線表示drag到該點的parent,這個部分的實作需要在每個節點增加"isShowDragParent",由於需要在離開drag區域時關閉顯示,所以需要用到"ondragenter"與"ondragleave"兩個事件,具體的說明可以參考HTML 拖放 API,要注意該數值在"ondragover"與"ondrop"都要設定,不然容易發生閃爍顯示分隔線的狀況,接著在控制物件的"onDropNodeParent"與"onDropNode"裡都要考慮"nodeList"會不會改變的問題,因為"root_treenodeview"是利用"nodeList"的數量來決定哪些物件是root node。

參考資料

HTML5 Drag and Drop
Data​Transfer
HTML 拖放 API

相關文章

改進Vue.js的treeview
使用vue.js建構treeview

沒有留言:

張貼留言