介紹
dijit.Tree是一款很有用的Dojo控件,用來在頁面上表示樹形數據。像Dojo其他很多控件一樣,它可以直接和後臺的數據連接,並實時顯示在頁面上。已經有很多帖子介紹如何創建樹、加入點擊事件及右鍵菜單等,這兒專門介紹dijit.Tree拖拽的精細控制(Dojo 1.7.3)。
先說說舉例用的應用場景。一個食品原材料供應商要建立自己的產品列表(New List),該列表包含幾個大類,大類下面又有小類,直至具體的產品。現在要做的就是,請一個專業人員,根據一個別的渠道來的列表(Old List),建立自己的產品列表。
根據需求,這兩個列表(dijit.tree)要實現的最重要的功能就是支持拖拽(Drag & drop):從左邊的列表拖動節點到右邊的列表。爲了達到目標,還需要進行諸多精細控制,列舉如下:
1. 在dijit.Tree上拖拽節點
- 在左邊的樹上,禁止拖動根節點、已經被拖拽到右邊的節點;
- 設定拖拽的目標位置;
- 改變拖拽後的節點屬性,比如這兩棵樹都是以”id”作爲標識符,則拖拽後要保證節點的id和原來樹上的節點但不衝突;
- 將拖拽後原來節點的顏色變成灰色,表示已經從這棵樹上刪除;
- 當選中的是父親節點時,同時拖拽其孩子;
- 拖拽後,只在目標樹上保留不是灰色的節點;
- 禁止拖拽節點到某個目標位置。
2. 滾動(Scrolling)
- 禁止有垂直滾動條時,用鼠標點滾動條觸發拖拽。
要實現上面這麼多控制,首先要了解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();
}
});
});