在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的操作 |
參考資料
HTML5 Drag and DropDataTransfer
HTML 拖放 API
相關文章
改進Vue.js的treeview使用vue.js建構treeview
沒有留言:
張貼留言