Since weâre building a Drag and Drop component, we should use the Drag and Drop API, right?
Nope.
You can build a Drag/Drop component with Drag and Drop API, but the Drag and Drop API is NOT the best technology for a Drag/Drop component.
It sounds ironic, I know đ.
The Drag and Drop API is built to mimic dragging and dropping on a desktop. It produces a ghost image when you drag the component.
Styling is limited for this ghost image since itâs not an HTML Element. You can brute-force it with lots of hacks. But even if you brute force it, you still canât change things like transform or opacity!
So we cannot use the Drag and Drop API (which is unfortunate). We need some other method. (I didnât explain the Drag and Drop API in Learn JavaScript because weâre not going to use it).
Guess what? We can use Pointer events!
Preparations for Dragging
We need some way to indicate that our boxes are draggable. We cannot use the draggable attribute since weâre not using the Drag and Drop API.
I thought using data-draggable would be cool, so I added that for every box.
TIP: When you build components with many children elements, always work with ONE element first. It will make your life so much easier.
Preparing to drag
We need to be able to position an element at will if we want to drag it.
We can only position an element at will if we set its position to absolute. Once position is absolute, we can change the top and left values to move the element.
We will set position to absolute when the user holds down the mouse button. This prepares the element for dragging.
But we didnât break anything. If you set an elementâs position to absolute, you take it out of the natural document flow. This is expected.
But we donât want other elements to shift locations yet. Instead of dragging the actual element, we can make a clone of this element and drag that clone.
Weâll hide the original element at the same time so users wonât get confused between the original and the clone. (Ideally, users shouldnât even know thereâs a clone).
Unfortunately, the method we have is not foolproof. If the user moves too fast while dragging the element, their pointer might leave the element behind⌠đ° .
To prevent this from happening, we need to capture all subsequent pointer events on the clone. We can do this with setPointerCapture in pointerdown.
// Notice this is in pointerdown!
draggable.addEventListener('pointerdown', event => {
// ...
clone.setPointerCapture(event.pointerId)
clone.addEventListener('pointermove', event => {/* ... */})
// ...
})
Now users will not be able to fling the clone away from their pointers. You can even drag the clone outside of the screen and back. The clone will continue to follow the mouse.
Since we used setPointerCapture to capture the pointer event, we should also release the pointer with releasePointerCapture when the user releases their mouse button.
(Technically we donât need to do this since we deleted the clone⌠But take it as a good practice to clean up after yourself. Weâll need it in a later lesson).
As you can see, elementFromPoint gives you the closest element to the screen.
It returns <div data-dropzone></div> if the element was dropped into the dropzone directly.
But it returns <div class="box">..</div> if the element was dropped onto another element.
Fret not, we can still detect if the clone was dropped into the dropzone with closest.
clone.addEventListener('pointerup', event => {
// ...
const left = parseFloat(clone.style.left)
const top = parseFloat(clone.style.top)
const droppedArea = document.elementFromPoint(left, top)
const dropzone = droppedArea.closest('[data-dropzone]')
if (dropzone) {
// Dropped into Dropzone
}
})
If the clone was dropped into the dropzone, we append it to the dropzone. This automatically moves the element from its original location to the dropzone.
You wonât be able to drag elements in Safari right now.
This happens because Safari doesnât support event.movementX and event.movementY yet.
Itâs funny because if you console.log the event object in Safari, you see event.movementX and event.movementY just fine! Except⌠theyâre always 0
So weâre left in a situation where we canât use movementX and movementY, but we canât tell Safari (and mobile Safari) aside from other browsers⌠đ
Fixing movementX and movementY
Thankfully, this is a quick fix. MDN states that movementX has the following calculation:
Armed with this information, we can calculate the movementX and movementY values ourselves.
Calculating movementX and movementY
We need screenX and screenY values from the previous pointer event. For the very first event, we need to get the originating values from the pointerdown event.
draggable.addEventListener('pointerdown', event => {
let prevScreenX = event.screenX
let prevScreenY = event.screenY
// ...
})
When the user moves the pointer, we get screenX and screenY from the current pointermove event. This lets us calculate movementX and movementY.
We will then update prevScreenX and prevScreenY with the current values.