Back to the index of articles and examples

Drag and Drop within a TreeView

Many people has asked how to drag and drop nodes on a TreeView. Though this would seem a natural feature on a TreeView, it simply is not that easy to define. First of all, depending on the meaning of the nodes, not all of them can be dragged so that is a first issue, though it could be easily if a tree Node had a 'draggable' property.

Most of the problems lie with dropping. Both the Favorites list on a browser and a window showing the file system of a disk show their information as a tree. In both, only folders can be drop targets, however, files or folders dropped in the file system will be displayed wherever they fit in the preset sort order (usually alphabetically) with folders first. Other trees have other rules. For example, departments and personnel. You can drag and drop people into departments but if you drop a department into another department, you are dropping the people in the dragged department into the target department.

In the end, there are so many possible alternatives that there is no way to easily describe all of them via a simple set of properties. In the end, the only way would be to provide hooks to plug in custom functions to do all the analizing of what can be dropped where and how. However, this doesn't make much sense because, after all, the Drag and Drop utility already provides events to signal when interesting things happen and TreeView provides enough methods to manipulate its nodes so, there would be little to add to these.

This example shows how to match TreeView with Drag and Drop using an arbitrary way of behaving. I'll point out the places where such behaviour can be changed to suit a concrete need. The whole code is contained within this very page so anything not mentioned in the following code boxes can be seen right in this page source.

First of all, I will use a drag proxy to move nodes around, a single proxy for all DD instances so I'll create that first:

DDProxy needs a container, usually a DIV element, which can be empty. My proxy will contain a TreeView instance instead. This is an option, you could simply use an empty outline as DDProxy uses by default, I just do this to show how it can be done. DDProxy asks for the id of the container so I give it one.

I create a DDNode class which is my special variety of DDProxy:

There is nothing new here, I just use the standard way of subclassing within YUI. The commented line is where the functions that handle the behavior of the D&D operations go. First, what to do when dragging starts:

When dragging starts, I first find which tree Node is actually being dragged by using the getNodeByElement method with the dragged HTML element returned by D&D getEl method. Do not confuse Node's getEl with D&D's method of the same name. Here, this points to the DDNode instance. I make invisible the HTML element representing the Node. I use visibility:hidden instead of display:none so that the space it occupies is preserved, you might choose otherwise, for example, dimming the existing nodes by using a mask or suitable CSS styles.

I said I would drag the branch of the tree in the proxy element. In fact, I will drag a clone of that tree, I will not move the original node and its children until the element is dropped so, I use getNodeDefinition to get all the information for the tree that I might need to make a copy under the dragTree which I created within my drag proxy, which I then render.

While the object is being dragged, I check the potential destination. I use getNodeByElement again to find out the actual node which is being the potential target. Since onDragOver will be called many times, I don't want to repeat the same code over and over so I first check if the current target node is the same as the one already reported, if it isn't, then I add a className to the new target after removing that same className from the previous one, if there was any, and make this new target the good one. How you define the className is up to you, I went for a yellow background.

The important matter here is not so much the style but the decisions you can make here. You have two nodes, the srcNode which we saved in the startDrag method and the potential target in tmpTarget. It is up to you and your application whether the source node can be dropped into this target so, in most cases, you would put some more logic here to decide. Both source and target nodes are actual tree Node instances (TextNode, HTMLNode or whatever) and you can store extra custom information in them to help you make that decission. In the end, you have to either save into destNode a reference to the node if it is valid, or null if not.

Finally, you get to the drop part. We had the potential target in destNode or null if it is invalid. One final check (which could also be done in onDragOver) we have to make sure not to drop the node into itself or any of its children so we make sure the source node is not an ancestor of the destination node. We then pop (remove without destroying it) the source node out of the tree and append it to the destination node. This is where most of the possible alternatives come into play. I decided to append them as children, your application will probably require otherwise. TreeView has plenty of methods, appendTo, insertBefore or insertAfter to do as required.

Finally I re-render the tree. The changes done by popping and appending the node will not be reflected on the screen until the tree is refreshed. Unfortunatly, this redraws the tree from scratch so all event listeners are removed, including those set by the Drag and Drop utility, so I need to make those nodes draggable again.

Some clean up is in order. Whether the source node is dropped or not, we need to clean some of the changes we made to the tree, we use the endDrag method to do that:

We haven't made the tree yet, but that is up to you. Here I build a tree at random and you can see the code in the page source, but it will be irrelevant to whatever you actually do. The important part is how to make the nodes draggable:

As I said, when the tree is redrawn, the draggable and target elements are lost so we have to recreate them. However, the Drag and Drop Manager will still hold references to the elements. I keep a copy of the DDNode elements in the ddNodes array so I go around it unregistering all those elements.

To set the draggable nodes, I use getNodesBy using a function that always returns true so that I am actually making all nodes draggable and targets. You would probably use a more selective method such as getNodesByProperty or a more selective function within getNodesBy. Anyway, once you have your pick of nodes, you create your own instances of our DDNode class, which we push into ddNodes to clean up in further calls.

The constructor of DDNode (which is a kind of DDProxy) takes, first, the HTML element that is to be dragged or that can be a drop target. For the nodes, we want the content element (getContentEl), not the whole node (getEl) because that would include the children and we will probably want to be more selective than that.

The second argument is the drag group. We might want to have different sets of nodes that can interact each with other elements in the same group. We use the 'default' group here, but you might want to use separate groups.

Finally, in the configuration options, we tell DDProxy that we mean to use our container which includes a TreeView, instead of the empty box it uses by default

In this description I have omitted the piece of code that resizes the container of the proxy to hold the tree brach being dragged, the code can be seen in the page source