在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
DataTransfer
HTML 拖放 API
相關文章
改進Vue.js的treeview
使用vue.js建構treeview