HTML5 DRAG AND DROP: <sortable list>

HTML5 Drag and drop API has been there for a while now, but still not proved to be a bulletproof solution for common drag and drop tasks. You can find numerous advices in the web to use mouse events based implementation as a more consistent and easier to use alternative.

Two weeks ago I received a task to implement drag and drop in our angular application. I decided to have a look at the ready-to-use and proven solutions out there in the web. But after some research it was clear to me that I don’t want another dependency that I have to rely on and worry about when upgrading major version of a framework or facing other integration issues. There are also lots of opened and not resolved issues and it’s always easier to improve your own code rather than digging in the sources of the ready solution.

I thought: “It can’t be too hard to implement it by myself using native HTML5 Drag and drop API! 👨‍🔧”. But I faced lot’s of issues when working on it which I want to share with others struggling to find a solution.

It’s NOT another article about an API itself. You can find lots of good resources out there in the web. Especially good resource to start with is Native HTML5 Drag and drop. MDN articles are also very helpful especially when it comes to investigating drag and drop events.

In this article I will implement some drag and drop functionality and describe browser inconsistencies in drag and drop API implementation in modern browsers. It is written using angular (v.5), but can be easily applied to any implementation as is mainly about attaching event listeners to drag and drop events.

I aimed for modern browsers. In my case it is IE10+, recent versions of Chrome, Safari and Firefox. It’s not an article about styling so I’ll omit this part at all. Browser specific moments will be accented by Roman numerals I, II, III…

If you want the actual code — here it is.

So lets get started…


My task is to implement drag and sort functionality beetween the list items.

List items look like this 👇. It’s simply an element containg a caption and an image.

But what if we have list of let’s say 10 of such items in a row?

Our card could be even bigger. Will it be convenient for a user to drag such items? Definitely not.

So we decided we can simply collapse all the items in the list when user starts dragging and expand them back when user drops item anywhere. In the world of drag and drop this means we will have to process dragstart and dragend events.

Lets finally start coding. I’ll use embedded templates for simplicity.

Here is list component. It’s also very basic:

And finally the drag-sortable.directive with host listeners omitted:

Lets discuss in more details what’s happening here.

This directive expects draggable item data, its index in the list and css selector of sortable item.

Sets [draggable]=”true” attribute for the host element to enable dragging and dynamically binds to collapsed, dragging and safari-specific classes on the host element.

dragstart event

In dragstart event handler I query drag image element as we want custom drag image to be used and get its size. Next we set .dragging class on the host element to set a visual effect for a dragged item. In our case it’s dashed outline to visualize drop area.

Dragstart is also a right event to pass next dragSource value to BehaviorSubject declared in dragService. It is used to share the info among all draggable items about dragged element and its index. As element where drop event will fire differs from dragged element. One exception is when you drop draggable at its original place.

I.

Next goes first interesting browser specific part. Safari browser doesn’t allow DOM manipulations to be executed in drag start event handler. Simple workaround is to wrap this code with setTimeout.

Here is a code snippet:

if (this.config.isSafari) {
  setTimeout(() => {
    this.dragService.collapse$.next(true);
  }, 0);
} else {
  this.dragService.collapse$.next(true);
}

In this code I pass true value to collapsed BehaviorSubject in dragService to make all sortable items collapse on dragstart. Subscription is made in ngOnInit lifecycle hook of the directive.

What will happend in Safari without setTimeout? Draggable item will fire dragstart and dragend straightahead, so the item won’t be draggable at all.

Interestingly enought this issue will reproduce only starting from second draggable item. First draggable item will behave normally.

Here is some discussion on the topic: https://stackoverflow.com/questions/19639969/html5-dragend-event-firing-immediately .

I haven’t spotted this bug on Chrome, but if you face such issue look closer to your DOM manipulations in handlers.

Next goes logic for setting custom ghost image for draggable item as I don’t want to have big real size ghost images.

II.

event.dataTransfer.setDragImage() which is used to set custom drag image is not supported by IE10/11 and Edge.

For supporting browsers I simply call this method to set custom drag image and center it.

For IE10/11 and Edge implementation we have 2 options. Either completely remove ghost image and implement logic using mouse events or go with large real size shost images. I chose option 3 😅 — for IE10/11 remove ghost image at all and in Edge use drag events to stick element with fixed position to the mouse cursor position.

The logic for removing ghost image is pretty simple. I clone dragged element and insert it instead of the original. Original element display is set to none and in the setTimeout I get things back to the starting point. So the browser will create a ghost image of display none element, meaning there won’t be ghost image at all. Voila!!!

Not so bad, yeah?

For Edge as I mentioned before I want to stick drag image to the cursor position. That’s why I set drag image position to fixed and set its top and left values.

III.

Last thing I do in the dragstart event handler is setting actual data to the drag event. And I do that in a try/catch block as IE allows only text type to be used while other browser don’t have such restrictions.

try {
  event.dataTransfer.setData('application/json',
JSON.stringify(this.item));
} catch (e) {
  event.dataTransfer.setData('text', JSON.stringify(this.item));
}

Why not ‘text’ in all cases? When you choose ‘text’ all input type=”text” fields will become droppable by default which is not the case with application/json.

You should always use setData in dragstart event as setting draggable=”true” attribute on the element is not enough for Firefox to make element draggable.

drag event

The logic executed here is fairly simple. It is needed only for Edge to stick drag image to the cursor position while dragging.

Here is how it looks like in Edge:

dragenter event

In dragenter event handler I retrieve dragSource data if it is available. By this I guarantee that I’m processing only my draggable items and not some text or image on the page which I don’t care about.

Next I’m checking if element being dragged and element on which dragenter was fired are not the same elements. If they are I don’t want to do any processing of this event.

If they aren’t I check if dragged element is before the element on which drag event fired (sortable element) or after it. Depending on the result of such check I either put sortable element before the dragged one or after it. This way I’m sorting the list and setting new indexes for the dragged item defining its new position amoung the list items.

In the end you have to prevent the default action of dragenter event.

dragover event

Again prevent the default action to allow drop. 🤔 🤔 🤔

Yeah I know it is strange that you have to prevent default actions of dragenter and dragover events.

dragend event

In this handler I’m clearing dragSource and do the logic to remove dragging and collapsed classes from the draggable items to make them return to their idle state.

For Edge I also hide the drag image that was following the mouse moves.

drop event

Here I merge the data from the dragSource (specifically new index) and data from the dropped item and emit it in drapSortable event. In sortable-items-list component I subscribe to this event and attach a handler.

That’s it about host listeners logic.


What is also important but was not mentioned in context of directive.

IV.

Custom drag image cannot be larger than it is expected by browsers.

In Chrome/Firefox I cannot set it larger than 300px. Actually I can, but the image would then be blurred and cropped on edges.

Here is how it looks like on dark background:

So if you don’t want your custom image to be blurred you have to follow this size retrictions.

There are other restrictions for the custom drag image to be used:

  1. It has to be physically present on the DOM. Display none can’t be used for the drag image but you can safely use position absolute with index set to hide element underneath others.
  2. In Safari it has to be actually in the viewport, meaning visible without scrolling. It is a bit problematic when you have large original items and small ghost images. As ghost image can easily be left outside the current visible area. In my project I used position fixed on drag image element when working with Safari.

V.

Safari doesn’t allow animation during dragging of some item. So no animations during drag 😩.

VI.

IE10/11 doesn’t auto scroll when you drag element near it’s edges and doesn’t allow you to use scrollwheel, so if you have scrollable container you will have to implement some custom scrolling logic.

Here is the full code:

I hope this article will help somebody struggling with same issues and browser inconsistencies.


If you want some more info about browser inconsistencies this is a good article — http://mereskin.github.io/dnd/.

Another great article — The HTML5 drag and drop disaster.

Thank you for reading! Hope you liked it! 🍻 🍻 🍻

Like what you read? Give Taras Bobak a round of applause.

From a quick cheer to a standing ovation, clap to show how much you enjoyed this story.