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"));
 

open demo

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.