Mouse Capturing
Filed under: Bugs, Development, Documentation, Firefox, Internet Explorer, Issues, Technical
By Fabian Jakobs @ October 16, 2009 08:59
Yesterday we have found solution for a really annoying problem. The problem was that we didn't receive mouse events during drag operation in IE and Firefox if the cursor left the browser viewport. This was especially a problem for our scroll bars. Since qooxdoo 0.8 we render scroll bars using qooxdoo widgets. If the scrollbar was near the browser's edge and the user dragged the scroll bar knob outside of the browser window, scrolling just stopped. Thanks to qooxdoo user Petr Kobalíček, who pointed out that other frameworks can handle this situation, this issue could finally be resolved.
You can see the difference in this screen cast:
A basic building block for drag operations in qooxdoo is a concept called mouse capturing. It was fist introduced by Microsoft with Internet Explorer 5 but unfortunately no other browser vendor has implemented it (MSDN). Mouse capturing allows web developers to tell the browser that all mouse events should be dispatched on the same DOM element. This is especially useful for drag operations or menus, when all mouse events should go to the dragged element even if the mouse cursor is not directly above the element.
This can be easily demonstrated by looking at a simplistic drag and drop implementation:
function draggable(element) { var dragging = null; addListener(element, "mousedown", function() { var e = window.event; dragging = { mouseX: e.clientX, mouseY: e.clientY, startX: parseInt(element.style.left), startY: parseInt(element.style.top) }; element.setCapture(); }); addListener(element, "losecapture", function() { dragging = null; }); addListener(element, "mousemove", function() { if (!dragging) return; var e = window.event; var top = dragging.startY + (e.clientY - dragging.mouseY); var left = dragging.startX + (e.clientX - dragging.mouseX); element.style.top = (Math.max(0, top)) + "px"; element.style.left = (Math.max(0, left)) + "px"; }); }; draggable(document.getElementById("drag"));
open demo (works only in IE). While dragging try moving the cursor out of the browser window.
In the mousedown handler the current mouse and element position is stored in a drag session and then mouse capturing is started. From this point on all mouse events will be dispatched on the dragged element even if the mouse cursor is not over the element. The mouse can even leave the viewport as long as the mouse button is pressed. Mouse capturing ends when the mouse button is released, an alert box is opened or the browser loses focus. Note that all listeners can be attached directly to the element.
To get the same behavior in non IE browsers is a little bit tricky because none do support mouse capturing. For this reason we cannot attach the mousemove listener to the dragged element. Instead we need to attach it to the document. Since mouse events bubble up the DOM tree the document will receive all move events. One problem with this approach is that while bubbling up an intermediate event listener might manually stop the bubbling by calling stopPropagation. In this case the event would never reach the document and the drag would be broken. To fix this we have to attach the listeners to the event capturing phase. This can be easily confused with mouse capturing but it has nothing to do with it. In the W3C DOM event model bubbling events have two phases. First in the capturing phase the event bubbles from the document down to the event target. Afterwards in the bubbling phase it bubbles back from the target to the document. The bubbling phase is much better known because IE doesn't support the capturing phase at all. If the mousemove listener is added to the capturing phase of the document no other listener will be able to block it.
function draggable(element) { var dragging = null; addListener(element, "mousedown", function(e) { var e = window.event || e; dragging = { mouseX: e.clientX, mouseY: e.clientY, startX: parseInt(element.style.left), startY: parseInt(element.style.top) }; if (element.setCapture) element.setCapture(); }); addListener(element, "losecapture", function() { dragging = null; }); addListener(document, "mouseup", function() { dragging = null; }, true); var dragTarget = element.setCapture ? element : document; addListener(dragTarget, "mousemove", function(e) { if (!dragging) return; var e = window.event || e; var top = dragging.startY + (e.clientY - dragging.mouseY); var left = dragging.startX + (e.clientX - dragging.mouseX); element.style.top = (Math.max(0, top)) + "px"; element.style.left = (Math.max(0, left)) + "px"; }, true); }; draggable(document.getElementById("drag"));
If a mouse button is pressed and dragged out of the browser window, Firefox will continue to fire mouse events on the document. Opera, Safari and Chrome are a little more tolerant and fire the events on the document.documentElement as well. For this reason we must attach the listener to the document and not the document.documentElement or document.body.
Because if its usefulness we emulate the IE mouse capturing behavior in our cross browser event handling layer. The fix for IE was to call the native setCapture method. Since we used the emulated mouse capturing support for IE as well we've lost the side effect of receiving mouse events when the mouse left the browser window. In Firefox we just had to switch the event target from document.documentElement to document in our generic mouse event handler. With both fixes in place, mouse capturing and drag and drop operations now really work as expected on all supported browsers.

Comment by Helder Magalhães
Cool! B-)
October 16, 2009 10:40
Comment by Sebastian Werner
Wow. Really impressive work. And basically so simple to fix. Use “document” instead of “documentElement”. I had somehow played with setCapture() years ago and found that it had some crazy side effects. We’ll see how this works out, but for the moment it looks like a very nice enhancement.
October 16, 2009 10:49
Comment by Fabian Jakobs
First tests look fine with “setCapture”. I hope that we don’t find any blocker, which would force us to revert the IE fix. The FF fix however shouldn’t have any side effects.
October 16, 2009 11:26
Comment by Stefan
Good fix!
October 16, 2009 13:20
Comment by Markus Stange
Note that in Firefox 3.6 the document trick won’t work when you’re in an iframe, i.e. mouse moves outside the iframe won’t be sent to the iframe’s document anymore. Not that anybody will care, but I just thought I’d mention it
Also, Firefox 3.7 will have support for setCapture, see Neil’s post about it.
October 16, 2009 15:17
Pingback by qooxdoo » News » The week in qooxdoo (2009-10-16)
[...] can handle this situation, this issue could finally be resolved. The details are explained in an earlier blog post [...]
October 16, 2009 19:12