For an interface I am working on for
Green Motion Travel, I needed to create a file organizer.
The files are presented in a tree, and the organizer allows you to drag files and folders around within the tree.
To accomplish this, I decided to use GWT's new Drag And Drop features.
I couldn't find any real documentation on how to use it, but there is a video on the subject on youtube, here:
Google I/O 2011: GWT + HTML5: A web developers dream!
The bit on Drag&Drop starts at about 21 minutes in.
It turned out to be fairly easy!
Where to start?
With GWT, you can add Drag & Drop to almost anything.
I wanted to add D&D to TreeItems. As it turns out, TreeItems don't really work well as D&D targets. The target somehow ends up being the tree itself, so that you can't really drop a file onto another treeitem.
So, instead of adding D&D to the TreeItem objects, I instead created HTML objects and set those as widgets in the tree, and then added D&D to those. Labels would work too, of course, but I wanted to add icons to represent folders and files. For this tutorial, I will just use Labels, as it is a bit cleaner.
To make a widget draggable, you must do the following:
- Set the 'draggable' property
- Add a drag start handler
Some widgets support a draggable property directly, however Label does not. So, we need to go to the element and set it there.
In the Drag Start Handler, you must set the event data, or some browsers will not allow you to drag it.
In my case, I don't really care what the data is. Instead, I will set a class field member (called '
dragging') to remember what label is being dragged.
Finally, we will copy an image of the existing label as the drag image.
Making a Label Draggable
Label draggableLabel = new Label("drag me!");
dragLabel .getElement().setDraggable(Element.DRAGGABLE_TRUE);
dragLabel.addDragStartHandler(new DragStartHandler()
{
@Override
public void onDragStart(DragStartEvent event)
{
// Remember what's being dragged
dragging = dragLabel;
// Must set anyway for FireFox
event.setData("text", "hi there");
// Copy the label as the drag image
event.getDataTransfer().setDragImage(getElement(), 10, 10);
}
});
If you were looking closely, you might have noticed that dragLabel is not
final, as it would have to be in the above code. I didn't bother, as I have wrapped everything up into a class that extends Label, as you will see below.
To make a widget droppable, you must:
- Add a DragOver handler
- Add a Drop handler
Again, some widgets support dropping in the API, however Label does not. So instead, you need to add it using addDomHandler.
No problem.
The DragOver handler doesn't have to do anything. I will use it, however, to apply a style to the label.
To counter the drag-over style, I will also add a DragLeave event.
Making a Label Droppable
Label dropLabel = new Label("Drop onto me");
dropLabel.addDomHandler(new DragOverHandler()
{
@Override
public void onDragOver(DragOverEvent event)
{
dropLabel.addStyleName("dropping");
}
}, DragOverEvent.getType());
dropLabel.addDomHandler(new DragLeaveHandler()
{
@Override
public void onDragLeave(DragLeaveEvent event)
{
dropLabel.removeStyleName("dropping");
}
}, DragLeaveEvent.getType());
dropLabel.addDomHandler(new DropHandler()
{
@Override
public void onDrop(DropEvent event)
{
event.preventDefault();
// Do something with dropLabel and dragging
etc...
}
}, DropEvent.getType());
BIG NOTE: Make sure to call event.preventDefault() in the onDrop, or else the browser might navigate away from the current page!
That's about it for making my labels draggable and droppable.
For my situation, I needed to create:
- Leaf nodes for files, that are draggable and not droppable.
- Root folders that are droppable, but not draggable
- Subfolders that are both draggable and droppable.
I wrapped all of the above up into a class, called DragDropLabel.
The DropHandler for this example will move the TreeItem of the source under the TreeItem of the drop target.
Here is the implementation of my DragDropLabel class:
class DragDropLabel extends Label
{
private static DragDropLabel dragging = null;
final boolean droppable;
public DragDropLabel(String text, boolean draggable, boolean droppable)
{
super(text);
if (draggable)
{
initDrag();
}
if (droppable)
{
initDrop();
}
this.droppable = droppable;
if (droppable)
{
addStyleName("droppable");
}
else if (draggable)
{
addStyleName("draggable");
}
}
private void initDrag()
{
getElement().setDraggable(Element.DRAGGABLE_TRUE);
addDragStartHandler(new DragStartHandler()
{
@Override
public void onDragStart(DragStartEvent event)
{
// Remember what's being dragged
dragging = DragDropLabel.this;
// Must set for FireFox
event.setData("text", "hi there");
// Copy the label image for the drag icon
// 10,10 indicates the pointer offset, not the image size.
event.getDataTransfer().setDragImage(getElement(), 10, 10);
}
});
}
private void initDrop()
{
addDomHandler(new DragOverHandler()
{
@Override
public void onDragOver(DragOverEvent event)
{
addStyleName("dropping");
}
}, DragOverEvent.getType());
addDomHandler(new DragLeaveHandler()
{
@Override
public void onDragLeave(DragLeaveEvent event)
{
removeStyleName("dropping");
}
}, DragLeaveEvent.getType());
addDomHandler(new DropHandler()
{
@Override
public void onDrop(DropEvent event)
{
event.preventDefault();
if (dragging != null)
{
// Target treeitem is found via 'this';
// Dragged treeitem is found via 'dragging'.
TreeItem dragTarget = null;
TreeItem dragSource = null;
// The parent of 'this' is not the TreeItem, as that's not a Widget.
// The parent is the tree containing the treeitem.
Tree tree = (Tree)DragDropLabel.this.getParent();
// Visit the entire tree, searching for the drag and drop TreeItems
List<TreeItem> stack = new ArrayList<TreeItem>();
stack.add(tree.getItem(0));
while(!stack.isEmpty())
{
TreeItem item = stack.remove(0);
for(int i=0;i<item.getChildCount();i++)
{
stack.add(item.getChild(i));
}
Widget w = item.getWidget();
if (w != null)
{
if (w == dragging)
{
dragSource = item;
if (dragTarget != null)
{
break;
}
}
if (w == DragDropLabel.this)
{
dragTarget = item;
w.removeStyleName("dropping");
if (dragSource != null)
{
break;
}
}
}
}
if (dragSource != null && dragTarget != null)
{
// Make sure that target is not a child of dragSource
TreeItem test = dragTarget;
while(test != null)
{
if (test == dragSource)
{
return;
}
test = test.getParentItem();
}
dragTarget.addItem(dragSource);
dragTarget.setState(true);
}
dragging = null;
}
}
}, DropEvent.getType());
}
}
And so, with that class, now all I have to do is add some of them to a tree, like so:
Tree tree = new Tree();
RootPanel.get("main").add(tree);
// root is not draggable.
TreeItem root = new TreeItem(new DragDropLabel("root", false, true));
tree.addItem(root);
// Add some draggable and droppable folders to 'root'
root.addItem(new DragDropLabel("folder1", true, true));
root.addItem(new DragDropLabel("folder2", true, true));
etc...
Here is a screenshot of the tree in action, dragging a file onto a folder.
You can download the source code for the example
here.
Select "File/Download" to save it.
Note that it does not contain the GWT libs to save space.
Please leave a comment if this has helped you out!