四兩撥千斤——Dijit Tree 拖拽(DnD, Drag & Drop)的精細控制

介紹

dijit.Tree是一款很有用的Dojo控件,用來在頁面上表示樹形數據。像Dojo其他很多控件一樣,它可以直接和後臺的數據連接,並實時顯示在頁面上。已經有很多帖子介紹如何創建樹、加入點擊事件及右鍵菜單等,這兒專門介紹dijit.Tree拖拽的精細控制(Dojo 1.7.3)。

 

先說說舉例用的應用場景。一個食品原材料供應商要建立自己的產品列表(New List),該列表包含幾個大類,大類下面又有小類,直至具體的產品。現在要做的就是,請一個專業人員,根據一個別的渠道來的列表(Old List),建立自己的產品列表。

根據需求,這兩個列表(dijit.tree)要實現的最重要的功能就是支持拖拽(Drag & drop):從左邊的列表拖動節點到右邊的列表。爲了達到目標,還需要進行諸多精細控制,列舉如下:

1.        dijit.Tree上拖拽節點

  1. 在左邊的樹上,禁止拖動根節點、已經被拖拽到右邊的節點;
  2. 設定拖拽的目標位置;
  3. 改變拖拽後的節點屬性,比如這兩棵樹都是以”id”作爲標識符,則拖拽後要保證節點的id和原來樹上的節點但不衝突;
  4. 將拖拽後原來節點的顏色變成灰色,表示已經從這棵樹上刪除;
  5. 當選中的是父親節點時,同時拖拽其孩子;
  6. 拖拽後,只在目標樹上保留不是灰色的節點;
  7. 禁止拖拽節點到某個目標位置。

2.        滾動(Scrolling)

  1. 禁止有垂直滾動條時,用鼠標點滾動條觸發拖拽。

要實現上面這麼多控制,首先要了解dijit.Tree拖拽的主要控制部件。那就是"dijit/tree/dndSource"。它實現了很多接口,在拖拽生命週期的不同階段產生相應的作用。這兒就通過繼承和修改相應接口,實現我們要的功能。


創建兩棵樹

test_tree.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
	pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<style type="text/css">
@import "../js/dijit/themes/tundra/tundra.css";
@import "../css/main.css";
</style>

</head>
<body class="tundra">
<div style="width: 100%; height: 100%"
	data-dojo-type="dijit.layout.BorderContainer"
	data-dojo-props="design:'sidebar', gutters:true, liveSplitters:true">
	<div data-dojo-type="dojox.layout.ContentPane" data-dojo-props="region:'left',splitter:true" style="width: 500px;">
		<div class="title_bar">Old List</div>
		<div id="tree_1"></div>
	</div>
	<div data-dojo-type="dojox.layout.ContentPane" data-dojo-props="region:'center',splitter:true">
		<div class="title_bar">New List</div>
		<div id="tree_2"></div>
	</div>
</div>

<script>
		dojoConfig = {
			isDebug : false,
			parseOnLoad : true,
			async : true,
			packages : [ {
				name : "custom",
				location : "../../js/custom"
			} ]
		};
	</script>
	<script type="text/javascript" src="../js/dojo/dojo.js"></script>
	<script>
		require(["dijit/layout/BorderContainer",
		         "dojox/layout/ContentPane",
				  "dijit/form/Button" ]);
	</script>
	<script>
		require(
				[ "dojo/ready", 
				  "dijit/registry",
				  "dojo/_base/array", 
				  "dojo/on",
				  "dojo/dom",
				  "dojo/query",
				  "dojo/data/ItemFileWriteStore", 
				  "dojo/store/Observable",
				  "dijit/tree/ForestStoreModel", 
				  "dijit/Tree", 
				  "custom/TreeDndSource",
				  "custom/TreeDndTarget"
				  ],
				function(ready, registry, array, on, dom, query, 
						ItemFileWriteStore, Observable, ForestStoreModel,
						Tree, customDndSource, customDndTarget) {
					ready(function() {
						//創建左邊的樹
						var data_1 = {
								identifier: 'id',
								label: 'name',
								items: [
									{ 
										id: '0', 
										name:'Foods', 
										type: 'level1',
										children:[ 
										           {_reference: '1'},  
										           {_reference: '2'},  
										           {_reference: '3'}
										] 
									},
									{ 
										id: '1', 
										name:'Fruits', 
										type: 'level2', 
										children:[ 
										           {_reference: '5'},
										           {_reference: '6'}
										] 
									},
									{ 
										id: '5', 
										name:'Orange',
										type:"level3"
									},
									{ 
										id: '6', 
										name:'Apple',
										type:"level3"
									},
									{ 
										id: '2', 
										name:'Vegetables', 
										type: 'level2'
									},
									{ 
										id: '3', 
										name:'Cereals', 
										type: 'level2', 
										children:[ 
										           {_reference: '4'}
										] 
									},
									{ 
										id: '4', 
										name:'Rice cereal: industrially manufactured baby food based on rice',
										type:"level3"
									}
								]
						};
						
						var store_1 = new ItemFileWriteStore({
							data : data_1
						});
						
						store_1 = new Observable(store_1);
						
						var model_1 = new ForestStoreModel({
							store : store_1,
							query : {
								type : 'level1'
							}
						});
						
						var tree_1 = new Tree({
							id : "tree_1",
							model : model_1,
							showRoot : false,
							dndController : customDndSource, //控制拖動
							dragThreshold : 8,
							betweenThreshold : 5,
							persist : true
						}, "tree_1");
						
						//創建右邊的樹
						var data_2 = {
								identifier: 'id',
								label: 'name',
								items: [
									{ 
										id: '10', 
										name:'Foods', 
										type: 'level1'
									},
									{ 
										id: '11', 
										name:'Water', 
										type: 'level1'
									}
								]
						};
						
						var store_2 = new ItemFileWriteStore({
							data : data_2
						});
						
						store_2 = new Observable(store_2);
						
						var model_2 = new ForestStoreModel({
							store : store_2,
							query : {
								type : 'level1'
							}
						});
						
						var tree_2 = new Tree({
							id : "tree_2",
							model : model_2,
							showRoot : false,
							dndController : customDndTarget, //控制拖拽
							dragThreshold : 8,
							betweenThreshold : 5,
							persist : true
						}, "tree_2");
					});
				});
	</script>
</body>

支持從左邊的樹上拖動節點,並進行精細控制

TreeDndSource.js

專門在拖動的時候進行控制。主要的接口是onMouseDown。

define([ "dojo/_base/declare", "dijit/tree/dndSource", 
         "dojo/_base/array"], function(
		declare, dndSource, array) {
	return declare("custom.TreeDndSource", [ dndSource ], {
		onMouseDown: function(e){
			//2.1 禁止有垂直滾動條時,用鼠標點滾動條觸發拖拽
			var cName = e.target.className;
			if(cName.indexOf("dijitTree ") != -1) {
				return;
			}
			
			//1.1 在左邊的樹上,禁止拖動已經被拖拽到右邊的節點
			var node = e.target;
			if(node.style.color == "rgb(187, 187, 187)"){
				return;
			}
			
			//1.1 在左邊的樹上,禁止拖動根節點
			var item = dijit.getEnclosingWidget(node).item;
			var store = dijit.getEnclosingWidget(node).tree.model.store;
			if(store.getValue(item, "type") == "level1"){
				return;
			}
			
			this.inherited(arguments);
		}
	});
});

支持在右邊的樹上放下拖拽的節點,並進行精細控制

TreeDndTarget.js

專門在拖動結束的時候進行控制。主要的接口是onDndDrop。

define([ "dojo/_base/declare", "custom/TreeDndSource",
         "dojo/_base/array", "dojo/query", "custom/util"], function(
		declare, dndSource, array, query, util) {
	return declare("custom.TreeDndTarget", [ dndSource ], {
		sourceStore: null,
		targetModel: null,
		targetStore: null,
		
		addItem: function(sourceItem, parent, insertIndex, deleteList) {
			var that = this;
			var newItem = {};
			
			//1.3 改變拖拽後的節點屬性,確保id不重複
			var keys = this.sourceStore.getAttributes(sourceItem);
			array.forEach(keys, function(key){
				if(key != "children"){
					var value = that.sourceStore.getValue(sourceItem, key);
					if(key == "id"){
						value = util.getRandomId();
					}else if(key == "parent"){
						if(dojo.isArray(parent.id)){
							value = parent.id[0];								
						}else{
							value = parent.id;
						}
					}
					newItem[key] = value;
				}
			});

		    newItem = this.targetStore.newItem(newItem);
		    this.targetModel.pasteItem(newItem, null, parent, false, insertIndex);

		    //1.5 當選中的是父親節點時,同時拖拽其孩子
		    var children = sourceItem.children;
		    if (children !== undefined && children.length > 0) {
		    	array.forEach(children, function(citem, i){
		    		//1.6 拖拽後,只在目標樹上保留不是灰色的節點
		    		if(deleteList != null && deleteList[i] == false)
		    			that.addItem(citem, newItem, i, null);
				});
		    }
		    
		    if(this.targetStore == this.sourceStore){
		    	this.targetStore.deleteItem(sourceItem);
		    }
		},
		
		onDndDrop: function(source, nodes, copy){
			this.targetModel = this.tree.model;
			this.targetStore = this.targetModel.store;
			this.sourceStore = source.tree.model.store;
			
			//1.2 設定拖拽的目標位置
			var newParentItem;
			var insertIndex;
			var target = this.targetAnchor;
			newParentItem = (target && target.item) || this.tree.item;
			if(this.dropPosition == "Before" || this.dropPosition == "After"){
				newParentItem = (target.getParent() && target.getParent().item) || this.tree.item;
				
				insertIndex = target.getIndexInParent();
				if(this.dropPosition == "After"){
					insertIndex = target.getIndexInParent() + 1;
				}
			}else{
				newParentItem = (target && target.item) || this.tree.item;
			}

			if(newParentItem == null){
				if(this.current != null){
					newParentItem = this.current.item;
				}
			}
			
			if(newParentItem != null){
				var that = this;
				array.forEach(nodes, function(node, i){
					if(insertIndex != null){
						insertIndex = insertIndex + i;
					}
															
					//1.7 禁止拖拽節點到某個目標位置
					var sourceItem = dijit.getEnclosingWidget(node).item;
					if(newParentItem.root == true && that.tree.id == "tree_2"){
						alert("The item is being dropped outside the model which is invalid.");
						that.onDndCancel();
						return;
					}
					
					//1.6 拖拽後,只在目標樹上保留不是灰色的節點
					var deleteList = {};
					array.forEach(node.children[1].children, function(treenode, j){
						deleteList[j] = false;
						var treelabels = query("span[class~='dijitTreeLabel']", treenode);
						if(treelabels.length == 1){
							var treelabel = treelabels[0];
							if(treelabel.style.color == "rgb(187, 187, 187)"){
								deleteList[j] = true;
							}
						}
					});
					
					//Create item's all children in target tree
					that.addItem(sourceItem, newParentItem, insertIndex, deleteList);
					
					//1.4 將拖拽後原來節點的顏色變成灰色,表示已經從這棵樹上刪除
					var labelnodes = dojo.query("span[class~='dijitTreeLabel']", node);
					if(labelnodes != null && labelnodes.length > 0){
						array.forEach(labelnodes, function(labelnode){
							labelnode.style.color = "#BBBBBB";							
						});
					}
				});
				util.markModelSave(false);
			}
			
			this.onDndCancel();
		}
	});
});

總結

其實和dijit Tree拖拽相關的JavaScript對象和接口很強大,但如果對它們不熟悉,上面所說的精細控制就很會讓人焦頭爛額。而一旦熟悉並領會了其中規律,就可以四兩撥千斤,快速實現所需要的功能。希望這篇文章能對dijit Tree的更多控制有所啓發。

發佈了81 篇原創文章 · 獲贊 29 · 訪問量 42萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章