Drag and drop sortable lists are a common design pattern nowadays. You have a list of items, and if you want to change the order of them, you just drag them with a finger, or the mouse. As you do so, the item being dragged floats, and a gap is inserted into the list indicating where the floating item will go when the user drops it.
This article explains how to implement this functionality using pointer events, a JavaScript API which unifies mouse, touch and other input devices into a single event type. Consequently, the code is pretty easy to understand, and it works well on all devices, including mobile.
Rather than explaining the code line by line, which would take quite a long time, this article describes the general structure of the code, and thought process I went through in implementing it. I've included a CodePen embed with my full code, so you can play with it.
See the Pen
drag and drop list
on CodePen.
Thinking about the problem, it can be broken down as follows:
The code is logically organised into sections which implement these aspects, and the sections below explain how they work:
Instead of explaining the basics process of implementing drag and drop using pointer events, I'm going to differ you to the article Perfecting drag and drop in pure vanilla javascript, and focus only on the issues pertaining to the specific problem here.
This aspect actually isn't that difficult, we just need to bind to 3 events, pointerdown, pointermove, pointerup:
So that animations happen in sync with the browser's render loop, the actual rendering of movements is done within a requestAnimationFrame() callback, and the pointer event movement hander just updates a variable with the current pointer location.
We also need to register a pointerleave event, so that the dragging is cancelled when the cursor leaves the browser viewport. Without it, if the user un-clicks the mouse outside the viewport, the code gets stuck in a state where there is no way to cancel dragging.
Working out how to move the item and add the gap was by far the most difficult aspect of implementing this. I initially had 3 ideas how to approach this, all of which involving directly manipulating the order of items in the DOM:
In practice none of these worked well, as they were either too slow, or resulted in visual artefacts as the browser was not performing removal and insertion of DOM nodes atomically.
What did work was to mostly leave the DOM alone, and create all visual movement using the transform: translate CSS property. In summary, this is what I ended up doing:
A bonus of this approach, moving the elements instead of restructuring the DOM, is that it's really easy to add visual transitions using CSS.
Detecting which item in the list the floating item is overlaying was done in the following way:
It may also be possible to do this using the intersection observer api, but personally I didn't like it when I looked at it, as there seemed to be no way to directly query, 'is this element over this other element right now', rather it's based on callbacks, and that wasn't what I wanted.
Finally, we want the list to scroll, when the item being dragged is close to the top or bottom of the scrollablle area. This is pretty easy to do:
Personally, I learned quite a lot about how to approach things in browsers while writing this. For example, I expected that approaches of re-ordering the DOM nodes in real-time would be fast, as I assumed the browser would be storing these internally as independent heap-allocated structures (think C structs) pointed to by references in the parent element.
I expected that re-ordering items in the DOM would amount to nothing more than rearranging pointers in memory. If that was the case we could easily re-draw the whole list at 60+ FPS on a modern computer, even if it contained thousands of elements. The browser already knows the sizes of these elements, and that we have not resized them, so there shouldn't be much work to do.
In practice, this approach was very slow and visually jittery, so I guess my mental model of browser implementation must be wrong. I have learned that the best way of moving items in real time seems to be translating them using CSS transforms, as I noted.
Finally, there are a few obvious ways that this code could be optimised:
However in practice, I find the naive approach fast enough for my needs.