jLuger.de - Implement Drag'n'Drop in ECMAScript 2017

In this post I will explain how to implement Drag'n'Drop in ECMAScript 2017. Wait, isn't there a native Drag'n'Drop in HTML 5. Yes there is but I had several reasons not to use it.

The first reason is that the Drag'n'Drop API isn't very elegant. See https://www.quirksmode.org/blog/archives/2009/09/the_html5_drag.html for a detailed description on what is wrong. But as there a lot of tutorials on how to use it this didn't stop me.
To understand what stopped me let me first explain what I've tried to implement. You know the mobile apps with lists where there is a move marker? You move the whole list item when you move along the marker but outside the marker you do just a scroll. I've tried to implement that in HTML/Javascript. So I've created a dragmarker and added the draggable="true" attribute. The result was that I've could move the dragmarker around but the rest of the list item stayed where it was. That wasn't what I had in mind. I wanted to move the whole list item.
While searching on how to solve this I've found several statements that the Drag'n'Drop API wasn't working on mobile browsers. I wanted to run the webapp on desktop and mobile. So I've tried some of the examples that worked on my desktop on my mobile. They didn't work. Maybe it was just my phone, me or the examples. But as this webapp is mainly intended to run on my devices this was an absolute show stopper.

So I've decided to implement the Drag'n'Drop myself. On a normal desktop system you would use mouse down/move/up. But this app should also support mobile devices. Luckily there is a standard to solve this: Pointer events. It unifies mouse and touch events.
But the real hero in implementing Drag'n'Drop is the method setPointerCapture(). This method redirects all pointer events to the calling component. So when the mouse moves outside the dragmarker the dragmarker component still gets the move events. Normally move event handler are tricky because you will get a lot events even when there is no drag (at least with mouse moves). The sample code for setPointerCapture shows how to solve this . The move event listener is only added after the down event was received and is removed at the up event. So moves outside a Drag'n'Drop aren't handled at all. This is the best as no handler is the fastest handler for unwanted events. While talking about clean up on up events: Don't forget to call releasePointerCapture() during the handling of the up event or you mess up your event handling.
So now you've got all the required move events but no more. How to process them? A pointer event has clientX/clientY and pageX/pageY properties to tell you where the click happened. The first pair is the x/y coordinate of the visible screen while the second one is on the whole document. Say you click two times on the same element but between the clicks you scroll the document. The first pair will change depending on the scrolled area while the second pair returns the first value again.
But all this information are useless until you have coordinates to compare with. Here comes getBoundingClientRect() to the rescue. It has x and y properties that you can compare to clientX/clientY of the pointer event.

So that's it? No. First you should set the style "cursor:grab;" on your dragmarker so that the users knows that there is something to drag.
Secondly the current solution will only work on desktop browsers. On mobile it will start as expected but after a very short moment it will just stop. The reason is that the browser interrupts to check if the user made a gesture that the browser should handle. The solution I've found was to set the CSS(!) style "touch-action: none;" on the area where the users wants to drag over. Yes over. Setting the style just on the dragmarker won't help. But this style has a side effect. The user won't be able to scroll or zoom on that area. If the area is larger than the visible document part the user will be trapped on that area. So you just set the style after a down event? Yes but this isn't sufficient. You will notice that it takes no effect. That is because according to the documentation on MDN the style change will not take effect during a gesture. But when I added a touch move listener on the dragmarker that only calls preventDefault() the style took effect and I could drag as long as I needed.

So this whole thing is easier than the native Drag'n'Drop API? No. You only want to use it when the official API isn't working or fitting your needs.