基于 Mouse Event API
一、认识
Javascript
本身是具有原生拖放功能的,但是由于兼容性问题,以及功能实现的方式,用的不是很广泛。Javascript
动画广泛使用的还是模拟拖拽
二、细节
2.1 记录拖拽状态
定义 isDragging
来跟踪拖拽状态。状态逻辑如下:
-
mousedown
:当用户按下鼠标按钮时,设置isDragging
为true
,并记录初始位置。 -
mousemove
:当用户移动鼠标时,如果isDragging
为true
,更新元素的位置。 -
mouseup
:当用户释放鼠标按钮时,设置isDragging
为false
。
2.2 防止默认行为
在 mousedown
和 mousemove
事件中调用 event.preventDefault()
,以防止浏览器的默认行为(例如文本选择)。
2.3 阻止事件传播
在 mousedown
和 mousemove
事件中调用 e.stopPropagation()
,阻止事件传播
2.4 计算相对位置
1. 记录鼠标按下时,鼠标箭头在拖动元素中的位置
function computeMousedownXY(e, draggable) {
const rect = draggable.getBoundingClientRect();
const normalizedE = e.type.startsWith("mouse") ? e : e.targetTouches[0];
let x = normalizedE.pageX - rect.left - window.scrollX;
let y = normalizedE.pageY - rect.top - window.scrollY;
return [x, y];
}
2. 鼠标移动中,计算元素的新位置
function computeMousemoveXY({ e, downXY, container, draggable }) {
const containerRect = container.getBoundingClientRect();
const normalizedE = e.type.startsWith("mouse") ? e : e.targetTouches[0];
const x = normalizedE.pageX - downXY.x - containerRect.left;
const y = normalizedE.pageY - downXY.y - containerRect.top;
return [x, y];
}
2.5 限制拖拽范围
如果需要,可以在 mousemove
事件中添加逻辑,限制元素的拖拽范围,防止其移出容器边界。
// 限制拖拽范围的示例
function limitDrag(element, container) {
const rect = element.getBoundingClientRect();
const containerRect = container.getBoundingClientRect();
if (rect.left < containerRect.left) {
element.style.left = '0px';
}
if (rect.top < containerRect.top) {
element.style.top = '0px';
}
if (rect.right > containerRect.right) {
element.style.left = `${containerRect.width - rect.width}px`;
}
if (rect.bottom > containerRect.bottom) {
element.style.top = `${containerRect.height - rect.height}px`;
}
}
或者
function limitDrag({ x, y, draggable, container }) {
const rect = draggable.getBoundingClientRect();
const containerRect = container.getBoundingClientRect();
x = Math.max(Math.min(x, containerRect.width - rect.width), 0);
y = Math.max(Math.min(y, containerRect.height - rect.height), 0);
draggable.style.left = x + "px";
draggable.style.top = y + "px";
}
2.6 捕获鼠标事件
使用 document.addEventListener('mousemove')
、document.addEventListener('mouseup')
。
使用 document.addEventListener
而不是 element.addEventListener
来捕获 mousemove
和 mouseup
事件,以确保即使鼠标移出元素,拖拽操作仍然有效。同时可以解决 拖动太快,导致鼠标脱离的问题,当鼠标拖动的太快,比mousemove
事件的触发间隔还要快时,鼠标就会从元素上离开。这样就停止了元素的拖拽过程。此时,如果把mousemove
和mouseup
事件都加在document
上时,即可解决
2.7 优化拖拽性能
使用 requestAnimationFrame
来优化拖拽过程中频繁的 DOM
操作,减少重绘和重排的次数。
function onMouseMove(e) {
if (!isDragging) return;
e.preventDefault();
e.stopPropagation();
const [x, y] = computeMousemoveXY({ e, downXY, container, draggable });
requestAnimationFrame(() =>
limitDrag({
x,
y,
draggable,
container,
})
);
}
三、实现
3.1 HTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<style>
.container {
width: 800px;
height: 800px;
position: relative;
border: 1px solid #000;
}
.draggable {
width: 100px;
height: 100px;
background-color: red;
position: absolute;
cursor: grab;
}
</style>
</head>
<body>
<div class="container">
<div class="draggable"></div>
</div>
<script>
let isDragging = false;
let downXY = { x: 0, y: 0 };
const container = document.querySelector(".container");
const draggable = document.querySelector(".draggable");
function computeMousedownXY(e, draggable) {
const rect = draggable.getBoundingClientRect();
const normalizedE = e.type.startsWith("mouse") ? e : e.targetTouches[0];
let x = normalizedE.pageX - rect.left - window.scrollX;
let y = normalizedE.pageY - rect.top - window.scrollY;
return [x, y];
}
function computeMousemoveXY({ e, downXY, container, draggable }) {
const containerRect = container.getBoundingClientRect();
const normalizedE = e.type.startsWith("mouse") ? e : e.targetTouches[0];
const x = normalizedE.pageX - downXY.x - containerRect.left;
const y = normalizedE.pageY - downXY.y - containerRect.top;
return [x, y];
}
function limitDrag({ x, y, draggable, container }) {
const rect = draggable.getBoundingClientRect();
const containerRect = container.getBoundingClientRect();
x = Math.max(Math.min(x, containerRect.width - rect.width), 0);
y = Math.max(Math.min(y, containerRect.height - rect.height), 0);
draggable.style.left = x + "px";
draggable.style.top = y + "px";
}
function onMouseDown(e) {
e.preventDefault();
e.stopPropagation();
const [x, y] = computeMousedownXY(e, draggable);
downXY = { x, y };
isDragging = true;
draggable.style.cursor = "grabbing";
}
function onMouseMove(e) {
if (!isDragging) return;
e.preventDefault();
e.stopPropagation();
const [x, y] = computeMousemoveXY({ e, downXY, container, draggable });
requestAnimationFrame(() =>
limitDrag({
x,
y,
draggable,
container,
})
);
}
function onMouseUp(e) {
if (!isDragging) return;
e.preventDefault();
isDragging = false;
draggable.style.cursor = "grab";
}
function run() {
draggable.addEventListener("mousedown", onMouseDown);
document.addEventListener("mousemove", onMouseMove);
document.addEventListener("mouseup", onMouseUp);
draggable.addEventListener("touchstart", onMouseDown);
document.addEventListener("touchmove", onMouseMove);
document.addEventListener("touchend", onMouseUp);
}
run();
</script>
</body>
</html>
3.2 React
drag.tsx
import "./drag.css";
import { useRef, useEffect } from "react";
const Drag = () => {
const isDraggingRef = useRef(false);
const downXYRef = useRef({ x: 0, y: 0 });
const containerRef = useRef<HTMLDivElement>(null);
const draggableRef = useRef<HTMLDivElement>(null);
const computeMousedownXY = (e: any) => {
const rect = draggableRef.current?.getBoundingClientRect();
const normalizedE = e.type.startsWith("mouse") ? e : e.touches[0];
if (rect) {
const x = normalizedE.pageX - rect.left - window.scrollX;
const y = normalizedE.pageY - rect.top - window.scrollY;
return { x, y };
}
return { x: 0, y: 0 };
};
const computeMousemoveXY = (e: any) => {
const containerRect = containerRef.current?.getBoundingClientRect();
const normalizedE = e.type.startsWith("mouse") ? e : e.touches[0];
if (containerRect) {
const x = normalizedE.pageX - downXYRef.current.x - containerRect.left;
const y = normalizedE.pageY - downXYRef.current.y - containerRect.top;
return { x, y };
}
return { x: 0, y: 0 };
};
const limitDrag = (x: number, y: number) => {
const rect = draggableRef.current?.getBoundingClientRect();
const containerRect = containerRef.current?.getBoundingClientRect();
if (rect && containerRect) {
x = Math.max(0, Math.min(x, containerRect.width - rect.width));
y = Math.max(0, Math.min(y, containerRect.height - rect.height));
if (draggableRef.current) {
draggableRef.current.style.left = `${x}px`;
draggableRef.current.style.top = `${y}px`;
}
}
};
const onMouseDown = (e: any) => {
e.preventDefault();
e.stopPropagation();
isDraggingRef.current = true;
downXYRef.current = computeMousedownXY(e);
if (draggableRef.current) {
draggableRef.current.style.cursor = "grabbing";
}
};
const onMouseMove = (e: any) => {
if (!isDraggingRef.current) return;
e.preventDefault();
e.stopPropagation();
const { x, y } = computeMousemoveXY(e);
requestAnimationFrame(() => limitDrag(x, y));
};
const onMouseUp = (e: any) => {
if (!isDraggingRef.current) return;
e.preventDefault();
isDraggingRef.current = false;
if (draggableRef.current) {
draggableRef.current.style.cursor = "grab";
}
};
useEffect(() => {
const draggable = draggableRef.current;
if (draggable) {
draggable.addEventListener("mousedown", onMouseDown);
draggable.addEventListener("touchstart", onMouseDown);
}
document.addEventListener("mouseup", onMouseUp);
document.addEventListener("touchend", onMouseUp);
document.addEventListener("mousemove", onMouseMove);
document.addEventListener("touchmove", onMouseMove);
return () => {
if (draggable) {
draggable.removeEventListener("mousedown", onMouseDown);
draggable.removeEventListener("touchstart", onMouseDown);
}
document.removeEventListener("mouseup", onMouseUp);
document.removeEventListener("touchend", onMouseUp);
document.removeEventListener("mousemove", onMouseMove);
document.removeEventListener("touchmove", onMouseMove);
};
}, []);
return (
<div
className="container"
ref={containerRef}
onMouseDown={onMouseDown}
onTouchStart={onMouseDown}
>
<div className="draggable" ref={draggableRef}></div>
</div>
);
};
export default Drag;
drag.css
.container {
width: 800px;
height: 800px;
position: relative;
border: 1px solid #000;
}
.draggable {
width: 100px;
height: 100px;
background-color: red;
position: absolute;
cursor: grab;
}