Drag and Drop in GWT - The How-To
I wrote about GWT's lack of drag and drop a long time ago, and since then have done many things instead of making a generic drag-drop mechanism as promised. Luckily, some other people have, and it seems like their solution is quite good and solves a wide array of drag-drop problems.
Since it might be interesting to some, this post will be about how a generic draggable component can be written. I've coded a draggable panel at the time to help make any Widget into a draggable version of itself, and I'll describe how that was done. Warning: This is a step by step recipe on how to make a draggable widget of your own. If you're just looking for code examples or a library, you'd better take a look at the previously mentioned project's code and binaries.
The Basics
- Make an HTML item "floating", so it can be located anywhere on the screen - this is called absolute location on the web page.
- Make that element draggable by having the Widget respond to mouse events and reflect the mouse movement.
- Avoid text being selected or images being dragged off the browser window, which is the default behavior of clicking and dragging on an HTML item.
The GWT implementation
First, let's create a component that can contain any other single Widget. This component is called SimplePanel in GWT, and we'll extend that, add the widget we want to wrap, and set the type of positioning of the panel (which is a basicdiv element) to be absolute.
public final class DraggableWidgetWrapper
extends SimplePanel {
public DraggableWidgetWrapper(Widget inner) {
add(inner);
DOM.setStyleAttribute(getElement(), "position", "absolute");
}
}
The second step is, as said, to make the panel draggable. By making it draggable, everything it contains will be dragged as well. There are four steps to make a widget draggable:
- Sink events to it using the
DOMobject.public DraggableWidgetWrapper(Widget inner) { add(inner); DOM.setStyleAttribute(getElement(), "position", "absolute"); // We don't want to lose anything already sunk, // so just ORing it to what's already there. DOM.sinkEvents(getElement(), DOM.getEventsSunk(getElement()) | Event.MOUSEEVENTS); } - Implement the
SourcesMouseEventsinterface, usually by using the helper classMouseListenerCollection.public final class DraggableWidgetWrapper extends SimplePanel implements SourcesMouseEvents { private MouseListenerCollection mouseListeners = new MouseListenerCollection(); public void addMouseListener(MouseListener l) { mouseListeners.add(l); } public void removeMouseListener(MouseListener l) { mouseListeners.remove(l); } // ... } - Override the
onBrowserEventmethod and fire the mouse events accordingly.public class DraggableWidgetWrapper extends SimplePanel implements SourcesMouseEvents { // ... public void onBrowserEvent(Event event) { switch (DOM.eventGetType(event)) { case Event.ONMOUSEDOWN: case Event.ONMOUSEUP: case Event.ONMOUSEMOVE: case Event.ONMOUSEOVER: case Event.ONMOUSEOUT: mouseListeners.fireMouseEvent(this, event); break; } } } - Implement a
MouseListener(usually usingMouseListenerAdapter) to set theleftandtopattributes of thedivelement.public class DraggableWidgetWrapper extends SimplePanel implements SourcesMouseEvents { public DraggableWidgetWrapper(Widget widget) { add(inner); DOM.setStyleAttribute(getElement(), "position", "absolute"); DOM.sinkEvents(getElement(), DOM.getEventsSunk(getElement()) | Event.MOUSEEVENTS); addMouseListener(new DraggableMouseListener()); } // ... private class DraggableMouseListener extends MouseListenerAdapter { private boolean dragging = false; private int dragStartX; private int dragStartY; public void onMouseDown(Widget sender, int x, int y) { dragging = true; // capturing the mouse to the dragged widget. DOM.setCapture(getElement()); dragStartX = x; dragStartY = y; } public void onMouseUp(Widget sender, int x, int y) { dragging = false; DOM.releaseCapture(getElement()); } public void onMouseMove(Widget sender, int x, int y) { if (dragging) { // we don't want the widget to go off-screen, so the top/left // values should always remain be positive. int newX = Math.max(0, x + getAbsoluteLeft() - dragStartX); int newY = Math.max(0, y + getAbsoluteTop() - dragStartY); DOM.setStyleAttribute(getElement(), "left", newX); DOM.setStyleAttribute(getElement(), "top", newY); } } } }
EventPreview interface.
public class DraggableWidgetWrapper
extends SimplePanel
implements SourcesMouseEvents, EventPreview {
public DraggableWidgetWrapper(Widget widget) {
add(inner);
DOM.setStyleAttribute(getElement(), "position", "absolute");
DOM.sinkEvents(getElement(),
DOM.getEventsSunk(getElement()) | Event.MOUSEEVENTS);
DOM.addEventPreview(this);
addMouseListener(new DraggableMouseListener());
}
// ...
public boolean onEventPreview(Event event) {
if (DOM.eventGetType(event) == Event.ONMOUSEDOWN &&
DOM.isOrHasChild(getElement(), DOM.eventGetTarget(event))) {
DOM.eventPreventDefault(event);
}
// Always returning true as we don't want to cancel
// the event, just to prevent the default behaviour.
return true;
}
}
Some Cosmetics
Personally, I don't like the implementation details revealed from an API point of view, so I added a little factory-method to wrap things up:
public final DragDrop {
public static Widget makeDraggable(Widget widget) {
return new DraggableWidgetWrapper(widget);
}
}
And voila! Usage is as simple as DragDrop.makeDraggable(nonDraggable)! Well, since there is already a great library out there, I hope this was somewhat educational for you, if not useful for your personal project.
Comments (38)