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.