I am trying to create a draggable preact component. My current implementation breaks if the cursor moves to fast. Here is the code.
export { Draggable };
import { h } from "preact";
import { useState } from "preact/hooks";
const Draggable = (props: any) => {
const [styles, setStyles] = useState({});
const [diffPos, setDiffPos] = useState({ diffX: 0, diffY: 0 });
const [isDragging, setIsDragging] = useState(false);
const dragStart = (e: MouseEvent): void => {
const boundingRect =
(e.currentTarget as HTMLElement).getBoundingClientRect();
setDiffPos({
diffX: e.screenX - boundingRect.left,
diffY: e.screenY - boundingRect.top,
});
setIsDragging(true);
}
const dragging = (e: MouseEvent): void => {
if (isDragging === true) {
const left = e.screenX - diffPos.diffX;
const top = e.screenY - diffPos.diffY;
setStyles({ left: left, top: top });
}
}
const dragEnd = (): void => {
setIsDragging(false);
}
return (
<div
class="draggable"
style={{ ...styles, position: "absolute" }}
onMouseDown={dragStart}
onMouseMove={dragging}
onMouseUp={dragEnd}
>
{props.children}
</div>
);
}
I tried to fix it by creating a mouseup
event listener but the element stops dragging if I move the mouse to fast.
Here is my attempted fix:
export { Draggable };
import { h } from "preact";
import { useState } from "preact/hooks";
const Draggable = (props: any) => {
const [styles, setStyles] = useState({});
const [diffPos, setDiffPos] = useState({ diffX: 0, diffY: 0 });
const [isDragging, setIsDragging] = useState(false);
const dragStart = (e: MouseEvent): void => {
const boundingRect =
(e.currentTarget as HTMLElement).getBoundingClientRect();
setDiffPos({
diffX: e.screenX - boundingRect.left,
diffY: e.screenY - boundingRect.top,
});
setIsDragging(true);
// ------------------------------------------------------------ Added an event listener
document.addEventListener("mouseup", dragEnd, { once: true });
}
const dragging = (e: MouseEvent): void => {
if (isDragging === true) {
const left = e.screenX - diffPos.diffX;
const top = e.screenY - diffPos.diffY;
setStyles({ left: left, top: top });
}
}
const dragEnd = (): void => {
setIsDragging(false);
}
return (
<div
class="draggable"
style={{ ...styles, position: "absolute" }}
onMouseDown={dragStart}
onMouseMove={dragging}
// -------------------------------------------------------- Removed onMouseUp
>
{props.children}
</div>
);
}
The problem is that onMouseMove()
triggers every time you move the mouse, so, if you move over 200 pixels very slowly, that's 200 iterations. Try instead using onDragStart
and onDragEnd
. Full working demo.
Ultimately, your change will be here in render()
...
return (
<div
class="draggable"
style={{ ...styles, position: "absolute" }}
onDragStart={(e) => dragStart(e)}
onDragEnd={(e) => dragging(e)}
draggable={true}
>
{props.children}
</div>
);
I use dragEnd()
, so only two events actually fire with dragging: start and end. MouseMove was being fired every time there was movement, which could be hundreds of times during dragging.
Also, by giving it that extra draggable={true}
param, the browser will treat it as a naturally-draggable item (i.e., a semi-opaque version of the dragged item will visually appear at the position of the cursor, as the user drags the element around).
Finally, to speed up things just a smidge, I removed the eventListener
you had for dragEnd
in dragStart()
.