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 basic div 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); } } } }
The third step is a tricky one, because by using the first two steps you’ll actually get a draggable widget - but it won’t be just perfect. There will be little glitches: Dragging an image will sometimes take the image as if to copy it locally (out of the browser), dragging text will select some text, either the text dragged or something behind it, etc. To make sure these won’t happen, we need to cancel the default browser behaviour for the dragged object. This is accomplished by previewing all events coming through, using the 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.
October 13th, 2007 at 12:54 am
[...] The Chaotic Java Blog has a really nice, well-written tutorial on how to do drag and drop with GWT. 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. [...]
November 19th, 2007 at 3:37 pm
Thanks for the post! A few questions though:
1. What is DOM.getEventTarget(event) ? I looked at the DOM API and could only find DOM.eventGetCurrentTarget(event). I presume thats what you meant?
2. I’m getting some strange results with the coordinates calulated with the onMouseMove() method. I did the following in onModuleLoad():
RootPanel.get().add(DragDrop.makeDraggable(new Button(”Drag ME”)));
Whenever I drag the Button, it ends up going to position (0,0) on the screen and stays there. Am I doing something wrong?
November 27th, 2007 at 9:46 pm
Seth,
Thanks for your feedback. Yes, you are correct: there is no “DOM.getEventTarget” - I must have re-phrased the method in my head, the one I wanted to write is “DOM.eventGetTarget”.
This method differs from the one you suggested, so try using it and see if you keep having trouble with the button, as it shouldn’t be problematic. On the other hand, since making a widget draggable cancels its natural click-behavior, I wouldn’t suggest making a button draggable, rather maybe making a panel draggable and place the button on that..
December 12th, 2007 at 4:32 am
Hi.
A really nice tutorial. I tried it, and it worked with just a little bit of tweaking.
About Seth’s coordinates Problem: I experienced the same odd behaviour, but fixed it by just changing the minus operators to plus.
“dragStartX + x + getAbsoluteLeft()” and so on.
That worked just nearly perfect. The dragable moved about 10px south-east of the cursor.
My only real problem with this tutorial is, it runs very slowly on my machine. It seems to be some kind of lag in the event handeling process.
I don’t know…
I’d be gratefull for any sugestions.
greez
December 13th, 2007 at 8:05 pm
@DaPsYcHo: Does it lag when you use gwt-dnd as well, or just with the output of this tutorial? Also, what machine are you using (Windows, Linux, Mac?)
The GWT “promise” is that everything will work on any browser and platform (or at least the popular ones) - it doesn’t specify how fast it would work, though
It sounds odd to me that you had to switch the operators but I’ll check it.
December 14th, 2007 at 5:17 am
[...] Drag and Drop in GWT - The How-To DOM.sinkEvents(getElement(), DOM.getEventsSunk(getElement()) | Event.MOUSEEVENTS); (tags: gwt howto) December 13th 2007 Posted to Links [...]
January 3rd, 2008 at 4:16 pm
Great how to !
Thanks, only mistake what you have done is with counting newX and newY, there should be: getAbsoluteLeft()-dragStartX + x; and newY = getAbsoluteTop()-dragStartY +y;
Cheers from Poland
January 3rd, 2008 at 9:30 pm
Mazurek: As you can see, I’ve been commented about the mistake in the newX/Y already - I really need to sit down and see what’s wrong there and correct it.
Glad you enjoyed the how-to, and thanks for your comment!
March 31st, 2008 at 9:14 pm
Thanks for the illuminating article on the Draggable widget, it was eye opening indeed .
I however have a request to make , can you tell me how using GWT I can get a Popup to emerge in a grid when my mouse hovers over a particular column? I am sure it pretty much follows the same logic as the Draggable widget right?
March 31st, 2008 at 10:14 pm
insaneyogi: You are most likely correct. You will probably need to extend the Grid class and implement the SourceMouseEvents interface like instructed here (you need only the MouseOver and MouseOut events, but you can implement the rest as well). Then, register an event listener to the event you expose which starts and stops a dialog box when the mouse enters or exits the grid cell, respectively.
I am not a 100% sure this works, as the Grid is implemented as an html table, and it might cause problems (as the cells themselves are not the element you are adding the mouse listeners to). However, you can try it. Another issue you will need to deal with is finding out which cell the mouse is over at that moment.
Let me know how it goes!
May 2nd, 2008 at 12:59 pm
Hi!
Great how to! Congratulations! It was what I was looking for!
But I am not able to make it work properly. The widget I made ‘draggable’ is a dialog box. To open it, if I just do db.show(), in Internet Explorer it works fine, but not in Firefox (it’s like the pixels are different…). If I open it by calling db.center(), when I try to drag the dialog box, it moves ‘too much’ (the x and y coordenates are increased by the position where the dialog box was oppened).
Do you know where is the problem?
Thanks you very much!
May 2nd, 2008 at 2:39 pm
@DaPsYcHo: You are correct, there was a problem with the plus and minus signs. The equation you provided was almost correct, as the dragStartX/Y needed to be in minus. This fixes the behavior of the dragged element being a bit far from the cursor.
@Laia: The DialogBox class is already draggable by design - it might be the reason for this weird behavior. Did you try it with other classes?
May 2nd, 2008 at 2:40 pm
To everyone, in general: I’ve fixed the how-to and now it should work perfectly with no weird compilation errors or mistakes in X/Y calculations! That is why the comments above might seem odd…
July 5th, 2008 at 12:01 am
Excellent how-to: It’s tha’ bomb! Just one thing I’m really struggling to figure, and please excuse me if the answer is obvious, ’cause I just can’t seem to get it right: How do I know on which element the dragged source was actually dropped?
The main problem is this: Say I drag an image, whenever I drop it, itself will always be the target of the drop. I guess this is the case since it follows the mouse pointer, and is always directly beneath it.
What am I missing? How can I solve this? Any suggestions solutions are really appreciated.
Thanks bud!
August 9th, 2008 at 7:06 am
Thanks for your good work
Some things that I had to change in order to get a working widget:
I changed this
”
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);
”
for these lines:
”
Integer newX = Math.max(0, x + getAbsoluteLeft() - dragStartX);
Integer newY = Math.max(0, y + getAbsoluteTop() - dragStartY);
DOM.setStyleAttribute(getElement(), “left”, newX + “px”);
DOM.setStyleAttribute(getElement(), “top”, newY + “px”);
”
Because of that tiny change, made this wrapper usable or not in my project.
Hope this helps someone
September 11th, 2008 at 4:54 pm
Wonderful, it helps me design a map viewer.
September 16th, 2008 at 2:16 am
Thanks. This was a big help.
September 20th, 2008 at 7:17 pm
@Lautaro: I believe the reason your patched worked is that the signature for setStyleAttribute is (element, string, string). Since neither newX nore newY are strings, the call fails. Concatenating any string coerces the ints to strings; which conversion can also be obtained as follows:
DOM.setStyleAttribute(getElement(), “left”, Integer.toString(newX));
DOM.setStyleAttribute(getElement(), “top”, Integer.toString(newY));
It’s not clear to me how the author got this code to work w/o this coercion.
October 4th, 2008 at 8:06 am
This article is interesting. I have not been able to get it working with a TreeItem. Yup, I know there is a drap and drop tree project on google, but they seem to be doing it for D&D of items in the same trees.
What I am trying is to make a treeItem draggable onto a flexTable.
Anybody any experience to share ?
December 3rd, 2008 at 1:57 pm
Hello, Avah! Thanks for the article.
Could you give me a piece of advice about improving your DnD example? I’m going to drag and move my panel behind another panel. What should i do in this case? As I understand I should attach the same listeners to the front panel and on events change the coordinates of the back panel.
How to do it in better way?
December 3rd, 2008 at 11:50 pm
@Anton: Thanks for your comment!
I wonder if moving a panel behind another panel, and then when the user clicks that other panel it moves the behind-the-scenes panel, is intuitive - but it might be in the case of your application..?
In any case, what you suggested sounds reasonable. However, it will only work if you have only a single panel going behind that other panel; otherwise, you wouldn’t know who to delegate the movement to. I would probably try to remember which panels are behind the “invisible” panel, and then try to check if the clicked location is inside the bounds of any of them. If so, I would then delegate the mouse events to the relevant item.
December 4th, 2008 at 8:50 am
2 Aviad: Thanks for response. Yes, I’ve already implemented boundary check, but still got no understanding how to delegate events. I’m a newbie in GWT, so couldn’t you give more detailed description of events delegating ? Thanks in advance!
December 16th, 2008 at 1:27 pm
Hi Aviad,
I appreciate your efforts & expertise in developing this piece of code that explains a lot of basic GWT stuff. I’ve started learning GWT a week ago. I couldn’t find any comprehensive tutorial, only help was the StockWatcher example in code.google.com. My question is HOW & WHERE did you learn all this ? I am amazed. Can you refer me an in-depth GWT tutorial ?
1. I tried your code. I am sorry to say that it doesn’t work for me. I know it should work. I The widgets in my RootPanel doesn’t move at all, instead they show their basic behavior.
I excluded the code
public final DragDrop {
public static Widget makeDraggable(Widget widget) {
return new DraggableWidgetWrapper(widget);
}
}
because i have no idea where to place them. Eclipse gives me error when i place them any where in the code.
2. Before i saw your tutorial, i was developing a drag&drop class myself. I used the MouseListener and added it to a Widget (new HTML(”")). I could drag it but the events were triggered in a weird manner & dragging wasn’t pleasant. I saw the method setCapture & ReleaseCapture in your articles. When i used them, my class worked better but not perfect.
Can you tell me what exactly the set& release Capture do ?
3) I am developing a spreadsheet program similar to Google Spreadsheet. Do you know any custom GWT extension that provides one free of cost ?
Many Thanks,
Hazem
January 12th, 2009 at 12:32 pm
Hi,
Thanks for putting up this post. I didn’t use it to implement your work but found it as a useful reference for checking out event handling in GWT. It was very useful.
January 12th, 2009 at 9:26 pm
You welcome Saurabh! If you have any further questions, maybe they’ll be an idea for another post..
January 17th, 2009 at 10:20 pm
Nice tutorial but I just have something to say about it :
take care when you write “et voila”, because you wrote “et viola”, and “violer” means “rape” :p
I’m sure it’s not what you mean ^^
January 22nd, 2009 at 8:10 am
@Mamie: That’s not what I meant, correct. Thanks for the correction.