I have written a patch that allows nested targets for drag and drop. The nested targets can accept different source types. I ran the test suite but I don't think that the dnd test suite is included in the default setup.
Index: dnd/Manager.js
===================================================================
--- dnd/Manager.js (revision 12205)
+++ dnd/Manager.js (working copy)
@@ -14,6 +14,9 @@
this.target = null;
this.canDropFlag = false;
this.events = [];
+ this.nestedTargets = false;
+ this.sources = new dojo.NodeList();
+ this.leftSource = false;
},
// avatar's offset from the mouse
@@ -159,6 +162,17 @@
this.updateAvatar();
dojo.removeClass(dojo.body(), "dojoDnd" + (this.copy ? "Move" : "Copy"));
dojo.addClass(dojo.body(), "dojoDnd" + (this.copy ? "Copy" : "Move"));
+ },
+ registerSource: function(source){
+ this.sources.push(source);
+ },
+ sourceById: function(id){
+ for (var i = 0; i < this.sources.length; i++){
+ if (this.sources[i].node.id == id){
+ return this.sources[i];
+ }
+ }
+ return null;
}
});
Index: dnd/Source.js
===================================================================
--- dnd/Source.js (revision 12205)
+++ dnd/Source.js (working copy)
@@ -78,6 +78,9 @@
dojo.subscribe("/dnd/drop", this, "onDndDrop"),
dojo.subscribe("/dnd/cancel", this, "onDndCancel")
];
+
+ var m = dojo.dnd.manager();
+ m.registerSource(this); //nestedTargets will be set by the client after this so always register the source
},
// methods
@@ -129,6 +132,13 @@
dojo.dnd.Source.superclass.onMouseMove.call(this, e);
var m = dojo.dnd.manager();
if(this.isDragging){
+
+ if (m.leftSource){
+ m.leftSource = false;
+ m.overSource(this);
+ return;
+ }
+
// calculate before/after
var before = false;
if(this.current){
@@ -210,8 +220,27 @@
// source: Object: the source which provides items
// nodes: Array: the list of transferred items
// copy: Boolean: copy items, if true, move items otherwise
+
+ var m = dojo.dnd.manager();
+
do{ //break box
if(this.containerState != "Over"){ break; }
+
+ /*If this source, contains any sources itself that have
+ their "containerState" set to "Over" then break*/
+ if (m.nestedTargets){
+ var sources = m.sources;
+ if (dojo.some(sources,function(item){
+ return Boolean(
+ item.node.id != this.node.id
+ && item.containerState == 'Over'
+ && dojo.isDescendant(item.node,this.node)
+ );
+ },this)){
+ break;
+ }
+ }
+
var oldCreator = this._normalizedCreator;
if(this != source){
// transferring nodes from the source to the target
@@ -314,6 +343,10 @@
},
onOutEvent: function(){
// summary: this function is called once, when mouse is out of our container
+ var m = dojo.dnd.manager();
+ if (m.nestedTargets){
+ m.leftSource = true;
+ }
dojo.dnd.Source.superclass.onOutEvent.call(this);
dojo.dnd.manager().outSource(this);
},
===================================================================
--- dnd/Manager.js (revision 12205)
+++ dnd/Manager.js (working copy)
@@ -14,6 +14,9 @@
this.target = null;
this.canDropFlag = false;
this.events = [];
+ this.nestedTargets = false;
+ this.sources = new dojo.NodeList();
+ this.leftSource = false;
},
// avatar's offset from the mouse
@@ -159,6 +162,17 @@
this.updateAvatar();
dojo.removeClass(dojo.body(), "dojoDnd" + (this.copy ? "Move" : "Copy"));
dojo.addClass(dojo.body(), "dojoDnd" + (this.copy ? "Copy" : "Move"));
+ },
+ registerSource: function(source){
+ this.sources.push(source);
+ },
+ sourceById: function(id){
+ for (var i = 0; i < this.sources.length; i++){
+ if (this.sources[i].node.id == id){
+ return this.sources[i];
+ }
+ }
+ return null;
}
});
Index: dnd/Source.js
===================================================================
--- dnd/Source.js (revision 12205)
+++ dnd/Source.js (working copy)
@@ -78,6 +78,9 @@
dojo.subscribe("/dnd/drop", this, "onDndDrop"),
dojo.subscribe("/dnd/cancel", this, "onDndCancel")
];
+
+ var m = dojo.dnd.manager();
+ m.registerSource(this); //nestedTargets will be set by the client after this so always register the source
},
// methods
@@ -129,6 +132,13 @@
dojo.dnd.Source.superclass.onMouseMove.call(this, e);
var m = dojo.dnd.manager();
if(this.isDragging){
+
+ if (m.leftSource){
+ m.leftSource = false;
+ m.overSource(this);
+ return;
+ }
+
// calculate before/after
var before = false;
if(this.current){
@@ -210,8 +220,27 @@
// source: Object: the source which provides items
// nodes: Array: the list of transferred items
// copy: Boolean: copy items, if true, move items otherwise
+
+ var m = dojo.dnd.manager();
+
do{ //break box
if(this.containerState != "Over"){ break; }
+
+ /*If this source, contains any sources itself that have
+ their "containerState" set to "Over" then break*/
+ if (m.nestedTargets){
+ var sources = m.sources;
+ if (dojo.some(sources,function(item){
+ return Boolean(
+ item.node.id != this.node.id
+ && item.containerState == 'Over'
+ && dojo.isDescendant(item.node,this.node)
+ );
+ },this)){
+ break;
+ }
+ }
+
var oldCreator = this._normalizedCreator;
if(this != source){
// transferring nodes from the source to the target
@@ -314,6 +343,10 @@
},
onOutEvent: function(){
// summary: this function is called once, when mouse is out of our container
+ var m = dojo.dnd.manager();
+ if (m.nestedTargets){
+ m.leftSource = true;
+ }
dojo.dnd.Source.superclass.onOutEvent.call(this);
dojo.dnd.manager().outSource(this);
},

Please file a CLA first
Please file a CLA first (http://dojotoolkit.org/cla). Before that we cannot review your code. Mark your ticket with [CLA] in the subject line, when you are done.
One reason we don't support nested targets right now is due to complications in the selection UI, while the linear container is easier to implement ⇒ less code in the base. Still we need to know some extra stuff like the orientation (vertical/horizontal).
But I am getting ahead of myself — CLA goes first, next we discuss merits of the implementation.
I am in need of nested
I am in need of nested targets and am getting very odd results when I use them.
I have a source with a list of items. Within the source there are two more sources but they don't accept items from the outer source, only from each other. The outer sources do not accept items from the inner source. When I try to drag and drop from inner source to inner source, the item gets placed in the outer source, the avatar gets stuck on the screen and I get the javascript error "t has no properties" I assume this is because of the complications with the selection UI?
I attempted to apply this patch but the results are the same. ( I know the patch isn't official but I wanted to see if it would work =) )
Any help on a work-around would be greatly appreciated. Thanks.
Latest patch
jbasinger, below is my latest patch (this is against 1.1.0)
You have to enable nested targets
Index: Manager.js =================================================================== --- Manager.js (revision 13152) +++ Manager.js (working copy) @@ -14,6 +14,9 @@ this.target = null; this.canDropFlag = false; this.events = []; + this.nestedTargets = false; + this.sources = new dojo.NodeList(); + this.leftSource = false; }, // avatar's offset from the mouse @@ -25,6 +28,10 @@ // summary: called when a source detected a mouse-over conditiion // source: Object: the reporter if(this.avatar){ + if (this.nestedTargets && this.target){ + this.target._unmarkTargetAnchor(); + } + this.target = (source && source.targetState != "Disabled") ? source : null; this.avatar.update(); } @@ -161,7 +168,68 @@ this.updateAvatar(); dojo.removeClass(dojo.body(), "dojoDnd" + (this.copy ? "Move" : "Copy")); dojo.addClass(dojo.body(), "dojoDnd" + (this.copy ? "Copy" : "Move")); + }, + //Register the source with the manager. + registerSource: function(source){ + this.sources.push(source); + }, + //Returns the source with the specified node id + sourceById: function(id){ + if (! id){ + return; + } + + for (var i = 0; i < this.sources.length; i++){ + if (this.sources[i].node.id == id){ + return this.sources[i]; + } + } + return null; + }, + //Returns the ultimate parent source of the specified source + ultimate_parent: function(source){ + //return the ultimate parent source of this nested source. + var current_parent = source; + var temp_parent = source; + var parent_node = source.node.parentNode; + while (parent_node){ + temp_parent = this.sourceById(parent_node.id); + parent_node = parent_node.parentNode; + if (temp_parent){ + current_parent = temp_parent; + } + } + return current_parent; + }, + //Deselects all selected nodes in the same tree as the passed source + deselectItemsInTree: function(source){ + if (! this.nestedTargets){ + return; + } + var parent_source = this.ultimate_parent(source); + dojo.forEach(this.sources,function(source){ + if ( + this.node.id != source.node.id + && dojo.isDescendant(source.node,parent_source.node) + ){ + source.selectNone(); + } + },source); } + //Returns a nodeList of sources which are inside the specified source. + ,nestedSources: function(source){ + var list = new dojo.NodeList(); + dojo.forEach(this.sources,function(s){ + if ( + this.node.id != s.node.id + && dojo.isDescendant(s.node,this.node) + ){ + list.push(s); + } + },source); + + return list; + } }); // summary: the manager singleton variable, can be overwritten, if needed Index: Selector.js =================================================================== --- Selector.js (revision 13152) +++ Selector.js (working copy) @@ -121,6 +121,9 @@ dojo.stopEvent(e); return; } + + dojo.dnd.manager().deselectItemsInTree(this); + if(!this.singular && e.shiftKey){ if(!dojo.dnd.getCopyKeyState(e)){ this._removeSelection(); Index: Source.js =================================================================== --- Source.js (revision 13152) +++ Source.js (working copy) @@ -96,6 +96,9 @@ dojo.subscribe("/dnd/drop", this, "onDndDrop"), dojo.subscribe("/dnd/cancel", this, "onDndCancel") ]; + + var m = dojo.dnd.manager(); + m.registerSource(this); //nestedTargets will be set by the client after this so always register the source }, // methods @@ -104,6 +107,18 @@ // source: Object: the source which provides items // nodes: Array: the list of transferred items if(this == source){ return true; } + + var m = dojo.dnd.manager(); + if (m.nestedTargets){ + //If one of the nodes we are dragging, contains the source we are + //trying to drop into, then do not allow. + if (dojo.some(nodes,function(node){ + return dojo.isDescendant(this.node,node); + },this)){ + return false; + } + } + for(var i = 0; i < nodes.length; ++i){ var type = source.getItem(nodes[i].id).type; // type instanceof Array @@ -138,7 +153,6 @@ params._skipStartup = true; return new dojo.dnd.Source(node, params); }, - // mouse event processors onMouseMove: function(e){ // summary: event processor for onmousemove @@ -147,6 +161,17 @@ dojo.dnd.Source.superclass.onMouseMove.call(this, e); var m = dojo.dnd.manager(); if(this.isDragging){ + + if (m.leftSource){ + m.leftSource = false; + m.overSource(this); + return; + } + + if (this._hasNestedOver()){ + return; + } + // calculate before/after var before = false; if(this.current){ @@ -230,6 +255,12 @@ // copy: Boolean: copy items, if true, move items otherwise do{ //break box if(this.containerState != "Over"){ break; } + + if (this._hasNestedOver()){ + break; + } + dojo.dnd.manager().deselectItemsInTree(this); + var oldCreator = this._normalizedCreator; if(this != source){ // transferring nodes from the source to the target @@ -308,6 +339,7 @@ } this._normalizedCreator = oldCreator; }while(false); + this.onDndCancel(); }, onDndCancel: function(){ @@ -332,6 +364,10 @@ }, onOutEvent: function(){ // summary: this function is called once, when mouse is out of our container + var m = dojo.dnd.manager(); + if (m.nestedTargets){ + m.leftSource = true; + } dojo.dnd.Source.superclass.onOutEvent.call(this); dojo.dnd.manager().outSource(this); }, @@ -369,7 +405,26 @@ if(dojo.hasClass(node, "dojoDndHandle")){ return true; } } return false; // Boolean - } + }, + //If the source has a source within that currently is being moused over, return true + _hasNestedOver: function(){ + /*If this source, contains any sources itself that have + their "containerState" set to "Over" then break*/ + var m = dojo.dnd.manager(); + if (m.nestedTargets){ + var sources = m.sources; + if (dojo.some(sources,function(item){ + return Boolean( + item.node.id != this.node.id + && item.containerState == 'Over' + && dojo.isDescendant(item.node,this.node) + ); + },this)){ + return true; + } + } + return false; + }, }); dojo.declare("dojo.dnd.Target", dojo.dnd.Source, {Bug with latest patch
There is a trailing comma at the end of that patch, after the "_hasNestedOver" method in Source.js