Drag and Drop

Trying to get drag-and-drop working across IE 9, Firefox 17 and Chrome 23 isn't trivial but it can be done. IE 9 restricts the common denominator the most, even though the drag-and-drop API originally is a descendant of Microsoft's efforts with Outlook Web Access a couple of years ago. But Firefox definitely adds some spicy problems of its own. Still this is no reason to go into a rant.

Anchors are draggable by default

Prior to IE 10 improving the situation there simply is no way in IE 9 or IE 8 for dragging any elements except anchors and images. So no whining, we'll take it from there. Standard Bootstrap buttons are usually just cleverly styled <a> elements. Let's drag them. The presentation in IE 9 and Chrome is rather subdued, while Firefox renders a semi-transparent copy of the dragged area without help of an additional plugin.

Drag Button A  Drag Button B  Drag Button C

Wiring a drop target with event handlers

This is our #droptarget.
Try dragging one of the buttons into here.

Using jQuery events

jQuery provides specific methods for a lot of the standard browser events. But that does not include the drag-and-drop events. Still jQuery's event engine can fully support the task of registering the handlers.

There is an extra property on the standard DOM event object for a couple of drag-and-drop related events. jQuery copies the information from the native event objects into a container of its own creation. The event.dataTransfer property does not get copied over unless it is made known to jQuery by this line in the script:

    $.event.props.push("dataTransfer");

While nothing needs to be done to keep the mouse pressed on any of the buttons and to start dragging, two events are vital for making the drop operation happen. When hovering over any innocent <div> on the page with a dragged anchor, the standard behavior is: Nothing happens. To indicate that something should happen, we need to indicate non-standard behavior. For events this is usually done by calling the preventDefault() method on the event container. This needs to be done in the dragover event. As a result there will be a visual feedback that it is OK to drop the link on the target. Without this prerequisite the drop event will not get fired when letting go of the mouse button over the target.

The drop event can retrieve the full URL of the dragged anchors href via the dataTransfer.getData("Text") method in order to identify who or what has been dragged onto the target. Here the call to preventDefault() is only included for Firefox, a browser that otherwise follows the URL at the time of dropping.

    $("#droptarget")
        .on("dragover", function(e) {
            e.preventDefault()
        })
        .on("drop", function(e) {
            $(this).removeClass("alert-success").text(e.dataTransfer.getData("Text"));
            e.preventDefault()
        });   

Clicking on the buttons and letting go too quickly or without any mouse movement would trigger the links to be followed at that time. So there is some scripting on the draggable elements after all, which can prevent this standard behaviour.

    $("[href^='#drag']")
        .click( function(e) {
            e.preventDefault()
        });

A less vital part of the script creates some additional visual feedback that the user is hovering over the drop target. Here the pair of the dragenter and dragleave events can be configured. Things look complicated on the dragleave event and they are. IE and Chrome are more straight-foward and they do not set the relatedTarget on the event container.

Again Firefox does a bit of devious behavior of its own, by firing and bubbling these events again while passing into and out of inner elements of the target. But it also sets the relatedTarget correctly, so that a decision can be made, whether the cursor is still within the target or one of its descendants.

    $("#droptarget")
        .on("dragenter", function(e) {
            $(this).addClass("alert-success")
        })
        .on("dragleave", function(e) {
            if (e.relatedTarget) {
                if (e.relatedTarget != this && !($.contains(this, e.relatedTarget)))
                    $(this).removeClass("alert-success");
            }
            else
                $(this).removeClass("alert-success");
        })

So there you are. It's not that difficult after all.