Common Pitfalls with the HTML5 Drag ‘n’ Drop API

An article describing all the issues and workarounds we’ve encountered while implementing HTML5’s native Drag ‘n’ Drop capabilities into Gravit

Gravit is a free online design tool like Adobe Illustrator and uses the latest bleeding edge technologies of the web. As of such, we’ve enjoyed a long journey of surprises and pitfalls around HTML5 which I want you to (hopefully) learn from.

As you may know, HTML5 introduced a native Drag ‘n’ Drop API which originated from Microsoft’s IE 5.5 implementation. Let me say that it is a really, really, bad implementation, caused — I guess — by Microsoft’s initially confusing implementation. Anyway, with a few tricks it is easy to master the API for your own applications.

First, you might ask why you should ever use the native DND when there are so many libraries (like JQuery-UI) providing DND out-of-the-box — especially considering that these alternative libraries are way easier to handle, and are mostly cross-browser. However, let me assure you that once you’ve navigated the possible pitfalls of the native HTML5 DND API, you’ll never want to use a non-native implementation again. The biggest advantage of the native API is that some supported behaviour, e.g. file-dropping into the browser, is handled natively by the browser — so you won’t get any bad behaviors or unexpected events that are common when using non-native libraries. Most of the time, the non-native libraries are restricted to a certain area of interest; whereas when using the native DND, you can hook into the DND-Events globally and handle them well throughout your whole application.

This article will only describe the common pitfalls when using the native DND API. To learn the basics on how to use it, refer to this page.

Correctly starting a drag

To make any element draggable, you simply set the draggable attribute to true:

<div draggable=”true”/>

To listen for whenever dragging starts on the element, simply subscribe to the dragstart event:

element.addEventListener(‘dragstart’, function (evt) {})

And here comes the interesting stuff. Firstly, I strongly advise you to call stopPropagation() on each and every drag event you handle before doing anything else. This has been one of our main issues; sometimes we’ve had different drag events on children and their parent elements, and not calling stopPropagation() was causing the events to trigger on other elements, too.

The next thing we’ve discovered is that if you don’t provide any drag-data in dragstart, some browsers will not do any dragging at all (i.e. Firefox). So even if you don’t want to put any data into the drag event, you should still put some dummy text into it to ensure cross-browser dragging.

The next issue we’ve had have was to do with the drag-image. While starting a drag, you can provide a custom image you want to use for dragging. Usually, the browser takes an image shot from the element currently dragged. However, if you want to have a custom element, you can use the setDragImage function. And here comes the next pitfall — if you think you could simply create a dummy element in memory and set it as the dragImage, you’ll be lost. If you think you’re smart and append your dragImage element to the dom and make it invisible either via opacity=0 or visibility=hidden or display=none, you’re still lost, and the dragImage won’t be shown. If you’re even smarter like us, and try to position the dragImage element absolutely and move it outside the view, i.e. by using top=-1000px and left=-1000px, you’re lost again — as the dragImage needs to be visible in the current view to be used as dragImage. Great, isn’t it?

So what we’ve ended up with is actually two solutions: Either you are able to absolutely position your dragImage element at (0,0) and give it a lower z-index like -1 to set it behind your other stuff (this is what we do) or you’re able to put another element on top of it, using the background color to hide it (ouch!). At least that works, and you can set up your own dragImage easily.

So, now that you know about all those pitfalls, let’s see it in code:

element.addEventListener(‘dragstart’, function (evt) {

// We'll handle this event so first stop bubbling up

evt.stopPropagation(); // If we'd like to prevent dragging because of some condition,

// we'd simply call preventDefault() and be done here

if (some_condition) {

evt.preventDefault();

return;

} // Now setup our dataTransfer object properly

// First we'll allow a move action — this is used for the cursor

evt.dataTransfer.effectAllowed = 'move'; // Setup some dummy drag-data to ensure dragging

evt.dataTransfer.setData('text/plain', 'some_dummy_data'); // Now we'll create a dummy image for our dragImage

var dragImage = document.createElement('div');

dragImage.setAttribute('style', 'position: absolute; left: 0px; top: 0px; width: 40px; height: 40px; background: red; z-index: -1');

document.body.appendChild(dragImage); // And finally we assign the dragImage and center it on cursor

evt.dataTransfer.setDragImage(dragImage, 20, 20);

});

I’ve added a JSFiddle here running the actual code.

Correctly handling dragEnter, dragLeave and dragOver

There’s one common pitfall of all of these events — they fire like crazy if you have child elements in the target element in which the drop occurs. The easiest thing we’ve figured out:

Whenever dragEnter on your target element fires, give it a css class like ‘.drag’

Whenever dragLeave on your target element fires, remove the css class

Add the css class to your document that looks like this:

.drag * {

pointer-events: none;

}

What this does is that whenever the element is a drag target, it’ll say that all child elements of hit can not be hit by the cursor — and as such, won’t fire any drag events. This works for most modern browsers.