Yahoo user interface - Add drag/drop behavior to YUI tree

My mission was to create a tree where nodes can be dragged and dropped from branch to branch.

This is based off of code from sonjayatandon.com which you can find here

I was surprised to find out the the yui library didn't have this by default because most features I have needed was already done for me. I need this for a clients project that I was working on.

So the first thing I did was add a function to check to see if a node is a descendant of another node. The isChildOf worked great except it went only 1 child deep. So I created my own function that would check all children. This is because if you try to append a parent node to a descendant node it goes into infinite recursion.


YAHOO.widget.Node.prototype.isDescendant = function(node) {

var me = this;
while(me.parent != null)
{
if(me.parent == node)
{
return true;
}

me = me.parent;
}

return false;
}

The next thing was to modify the code from sonjaya a little bit for my issue because their's is based on ruby on rails.


DDSend = function(id, sGroup, config) {

if (id) {
// bind this drag drop object to the
// drag source object
this.init(id, sGroup, config);
this.initFrame();
}

var s = this.getDragEl().style;
s.borderColor = "transparent";
s.backgroundColor = "#f6f5e5";
s.opacity = 0.76;
s.filter = "alpha(opacity=76)";
};

// extend proxy so we don't move the whole object around
DDSend.prototype = new YAHOO.util.DDProxy();

DDSend.prototype.onDragDrop = function(e, id) {
// this is called when the source object dropped
// on a valid target
//alert("dd " + this.id + " was dropped on " + id);
// send a request to the server to handle the drag drop

var src = tree.getNodeByElement(document.getElementById(this.getEl().id));

var dest = tree.getNodeByElement(document.getElementById(id));

this.onDragOut(e, id);

if(dest.isDescendant(src) == false)
{
var p = src.parent;

tree.popNode(src);
src.appendTo(dest);
p.refresh();
dest.refresh();
el = document.getElementById(this.id);
tree.expandAll();
}
new DDSend(src.getContentEl());
for(i=0; i<src.children.length; i++)
{
new DDSend(src.children[i].getContentEl());
}
new DDSend(dest.getContentEl());
for(i=0; i<dest.children.length; i++)
{
new DDSend(dest.children[i].getContentEl());
}
}

DDSend.prototype.startDrag = function(x, y) {
// called when source object first selected for dragging
// draw a red border around the drag object we create
var dragEl = this.getDragEl();
var clickEl = this.getEl();

dragEl.innerHTML = clickEl.innerHTML;
dragEl.className = clickEl.className;
dragEl.style.color = clickEl.style.color;
dragEl.style.border = "1px solid red";

};

DDSend.prototype.onDragEnter = function(e, id) {
var src = this.getEl();
var el;

// this is called anytime we drag over
// a potential valid target
// highlight the target in red
if ("string" == typeof id) {
el = YAHOO.util.DDM.getElement(id);
} else {
el = YAHOO.util.DDM.getBestMatch(id).getEl();
}

var node = tree.getNodeByElement(el);
var srcnode = tree.getNodeByElement(src);

if(node.isDescendant(srcnode) == false)
el.style.border = "1px solid red";
};

DDSend.prototype.onDragOut = function(e, id) {
var el;

// this is called anytime we drag out of
// a potential valid target
// remove the highlight
if ("string" == typeof id) {
el = YAHOO.util.DDM.getElement(id);
} else {
el = YAHOO.util.DDM.getBestMatch(id).getEl();
}

el.style.border = "";
}

DDSend.prototype.endDrag = function(e) {
// override so source object doesn't move when we are done
// called when source object first selected for dragging
// draw a red border around the drag object we create
var dragEl = this.getDragEl();
var clickEl = this.getEl();

dragEl.innerHTML = "";
}

Here is a few of the things I had to change:
Add a method to get rid of border after drop in endDrag function.
Check if the node is a descendant of the currently dragged node for onDragOver and onDragDrop. Nodes cannot be dropped onto descendant nodes.
After drag drop reinitiate the drag drop functionality

And I used YAHOO.widget.HTMLNode type for simplicity reasons since I can wrap a div around my label. So all that is left is to create the tree and the node and make it draggable.


tree = new YAHOO.widget.TreeView('tree');

node1 = new YAHOO.widget.HTMLNode('<div id="node1">node 1</div>',
tree.getRoot(), false, true)

node2 = new YAHOO.widget.HTMLNode('<div id="node2">node 2</div>', tree.getRoot(), false, true)

node3 = new YAHOO.widget.HTMLNode('<div id="node3">node 3</div>', node2, false, true)

new DDSend('node1');
new DDSend('node2');

This biggest challenge that I had is after the branch a node is dragged from one branch to another the tree is completely redrawn and all elements are wiped out. Which means the nodes are no longer draggable. So if you notice in the onDragDrop function we have to reinitiate the drag drop functionality. Like so:


new DDSend(src.getContentEl());
for(i=0; i<src.children.length; i++)
{
new DDSend(src.children[i].getContentEl());
}
new DDSend(dest.getContentEl());
for(i=0; i<dest.children.length; i++)
{
new DDSend(dest.children[i].getContentEl());
}

This is currently a work in progress and any comments or additions is welcomed. Here is the full source for my test. You will just have to change the directory to your yui library.


<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
<title>Untitled Document</title>
<link rel="stylesheet" type="text/css" href="lib/yui/build/treeview/assets/skins/sam/treeview.css">
<script type="text/javascript" src="lib/yui/build/yahoo-dom-event/yahoo-dom-event.js"></script>
<script type="text/javascript" src="lib/yui/build/dragdrop/dragdrop.js"></script>
<script type="text/javascript" src="lib/yui/build/treeview/treeview.js"></script>
</head>

<body>
<div id='tree'></div>
<script type='text/javascript'>
DDSend = function(id, sGroup, config) {

if (id) {
// bind this drag drop object to the
// drag source object
this.init(id, sGroup, config);
this.initFrame();
}

var s = this.getDragEl().style;
s.borderColor = "transparent";
s.backgroundColor = "#f6f5e5";
s.opacity = 0.76;
s.filter = "alpha(opacity=76)";
};

// extend proxy so we don't move the whole object around
DDSend.prototype = new YAHOO.util.DDProxy();

DDSend.prototype.onDragDrop = function(e, id) {
// this is called when the source object dropped
// on a valid target
//alert("dd " + this.id + " was dropped on " + id);
// send a request to the server to handle the drag drop

var src = tree.getNodeByElement(document.getElementById(this.getEl().id));

var dest = tree.getNodeByElement(document.getElementById(id));

this.onDragOut(e, id);

if(dest.isDescendant(src) == false)
{
var p = src.parent;

tree.popNode(src);
src.appendTo(dest);
p.refresh();
dest.refresh();
el = document.getElementById(this.id);
tree.expandAll();
}
new DDSend(src.getContentEl());
for(i=0; i<src.children.length; i++)
{
new DDSend(src.children[i].getContentEl());
}
new DDSend(dest.getContentEl());
for(i=0; i<dest.children.length; i++)
{
new DDSend(dest.children[i].getContentEl());
}
}

DDSend.prototype.startDrag = function(x, y) {
// called when source object first selected for dragging
// draw a red border around the drag object we create
var dragEl = this.getDragEl();
var clickEl = this.getEl();

dragEl.innerHTML = clickEl.innerHTML;
dragEl.className = clickEl.className;
dragEl.style.color = clickEl.style.color;
dragEl.style.border = "1px solid red";

};

DDSend.prototype.onDragEnter = function(e, id) {
var src = this.getEl();
var el;

// this is called anytime we drag over
// a potential valid target
// highlight the target in red
if ("string" == typeof id) {
el = YAHOO.util.DDM.getElement(id);
} else {
el = YAHOO.util.DDM.getBestMatch(id).getEl();
}

var node = tree.getNodeByElement(el);
var srcnode = tree.getNodeByElement(src);

if(node.isDescendant(srcnode) == false)
el.style.border = "1px solid red";
};

DDSend.prototype.onDragOut = function(e, id) {
var el;

// this is called anytime we drag out of
// a potential valid target
// remove the highlight
if ("string" == typeof id) {
el = YAHOO.util.DDM.getElement(id);
} else {
el = YAHOO.util.DDM.getBestMatch(id).getEl();
}

el.style.border = "";
}

DDSend.prototype.endDrag = function(e) {
// override so source object doesn't move when we are done
// called when source object first selected for dragging
// draw a red border around the drag object we create
var dragEl = this.getDragEl();
var clickEl = this.getEl();

dragEl.innerHTML = "";
}

YAHOO.widget.Node.prototype.isDescendant = function(node) {

var me = this;
while(me.parent != null)
{
if(me.parent == node)
{
return true;
}

me = me.parent;
}

return false;
}
</script>

<script type='text/javascript'>

(function() {
var Dom = YAHOO.util.Dom,
Event = YAHOO.util.Event;

tree = new YAHOO.widget.TreeView('tree');

node1 = new YAHOO.widget.HTMLNode({html:'<div id="node1">node 1</div>'},
tree.getRoot(), false, true)

node2 = new YAHOO.widget.HTMLNode({html:'<div id="node2">node 2</div>'}, tree.getRoot(), false, true)

node3 = new YAHOO.widget.HTMLNode({html:'<div id="node3">node 3</div>'}, node2, false, true)

tree.draw();
new DDSend('ygtvcontentel1');
new DDSend('ygtvcontentel2');
new DDSend('ygtvcontentel3');

})();

</script>
</body>
</html>