拖拽
拖拽(Drag and Drop)是浏览器原生提供的交互能力,用于在 拖拽源 与 拖放目标 之间移动或复制内容。它适合用在看板、排序、文件投放等场景,但对移动端支持并不完美,需要提前规划兼容策略。
重要
只有在 dragover 中调用 event.preventDefault(),drop 才会触发。这是最常见的“拖不进去”原因
概念与角色
- 拖拽源(Draggable):可被拖动的元素
- 拖放目标(Drop Target):允许接收拖拽内容的区域
draggable:让元素可拖拽的属性,值为true/false
默认可拖拽元素
<img> 与 <a> 默认可拖拽,其它元素需要显式设置 draggable="true"。
DataTransfer(数据载体)
拖拽过程中会携带一个 dataTransfer 对象,用于在源与目标之间传递数据。
setData(type, value):写入数据(常用text/plain或text/uri-list)getData(type):读取数据effectAllowed:拖拽源允许的操作(copy/move/link/all)dropEffect:当前目标期望的操作类型setDragImage(img, x, y):自定义拖拽预览图
拖拽事件速查
| 事件 | 触发对象 | 说明 | 常见用途 |
|---|---|---|---|
dragstart | 拖拽源 | 开始拖拽 | 设置 dataTransfer、添加拖拽样式 |
drag | 拖拽源 | 拖拽中持续触发 | 轻量状态更新(避免重计算) |
dragend | 拖拽源 | 拖拽结束 | 清理样式、重置状态 |
dragenter | 拖放目标 | 进入目标区域 | 高亮目标、显示提示 |
dragover | 拖放目标 | 目标区域内持续触发 | preventDefault() 允许放置 |
dragleave | 拖放目标 | 离开目标区域 | 取消高亮 |
drop | 拖放目标 | 松手放置 | 读取数据并执行业务逻辑 |
拖拽事件速查
拖拽流程
设置可拖拽元素
为元素设置
draggable="true",并监听dragstart。拖拽开始
在
dragstart中写入数据与拖拽行为(effectAllowed)。进入目标区域
dragenter/dragover触发,必须在dragover中preventDefault()。放置
drop触发,读取dataTransfer并完成数据交换或 DOM 更新。拖拽结束
dragend清理样式或临时状态。可选:自定义拖拽预览
使用
setDragImage指定自定义拖拽图标。
完整示例:拖动卡片到完成区
下面是一个最小但完整的拖拽示例,包含样式、交互与必要注释。
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Drag & Drop Demo</title>
<style>
:root {
color-scheme: light;
}
body {
margin: 0;
font-family: "PingFang SC", "Microsoft YaHei", sans-serif;
background: #f6f7fb;
}
.board {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 24px;
padding: 32px;
}
.column {
background: #fff;
border: 2px dashed #e2e5ee;
border-radius: 16px;
padding: 16px;
min-height: 260px;
transition: border-color 0.2s, box-shadow 0.2s;
}
.column.over {
border-color: #3b82f6;
box-shadow: 0 8px 24px rgba(59, 130, 246, 0.2);
}
.column h2 {
margin: 0 0 12px;
font-size: 18px;
}
.card {
background: #111827;
color: #fff;
padding: 12px 14px;
border-radius: 12px;
margin-bottom: 12px;
cursor: grab;
user-select: none;
}
.card.dragging {
opacity: 0.4;
cursor: grabbing;
}
.hint {
margin-top: 8px;
color: #94a3b8;
font-size: 13px;
}
</style>
</head>
<body>
<div class="board">
<!-- 拖拽源 -->
<section class="column" id="todo">
<h2>待办</h2>
<div class="card" draggable="true" data-id="task-1">写拖拽文档</div>
<div class="card" draggable="true" data-id="task-2">补充代码示例</div>
<div class="card" draggable="true" data-id="task-3">整理最佳实践</div>
</section>
<!-- 拖放目标 -->
<section class="column dropzone" id="done">
<h2>完成</h2>
<p class="hint">将卡片拖到这里</p>
</section>
</div>
<script>
const cards = document.querySelectorAll(".card");
const dropzones = document.querySelectorAll(".dropzone");
cards.forEach((card) => {
card.addEventListener("dragstart", (event) => {
// 设置拖拽数据(Firefox 必须设置,否则无法开始拖拽)
event.dataTransfer.setData("text/plain", card.dataset.id);
event.dataTransfer.effectAllowed = "move";
card.classList.add("dragging");
});
card.addEventListener("dragend", () => {
// 拖拽结束后清理样式
card.classList.remove("dragging");
});
});
dropzones.forEach((zone) => {
zone.addEventListener("dragenter", (event) => {
event.preventDefault(); // 允许放置
zone.classList.add("over");
});
zone.addEventListener("dragover", (event) => {
// 必须阻止默认行为,否则 drop 不会触发
event.preventDefault();
event.dataTransfer.dropEffect = "move";
});
zone.addEventListener("dragleave", (event) => {
// relatedTarget 不在当前区域时才移除高亮,避免子元素闪烁
if (!zone.contains(event.relatedTarget)) {
zone.classList.remove("over");
}
});
zone.addEventListener("drop", (event) => {
event.preventDefault();
const id = event.dataTransfer.getData("text/plain");
const card = document.querySelector(`[data-id="${id}"]`);
if (card) {
zone.appendChild(card);
}
zone.classList.remove("over");
});
});
</script>
</body>
</html>最佳实践
- 在
dragstart中设置dataTransfer与effectAllowed,并在dragend清理样式。 - 在
dragover中preventDefault(),同时设置dropEffect提示用户操作类型。 - 给目标区域添加视觉反馈(高亮、阴影),提升可用性。
drag与dragover触发频率很高,避免在其中做昂贵计算。- 对复杂业务提供非拖拽的替代入口(按钮、菜单),兼顾可访问性与移动端。
常见错误
- 只监听了
drop,但忘了在dragover中preventDefault()。 - 未设置
draggable="true",导致元素无法拖动。 - 使用
event.target作为目标元素,导致子元素被错误接收,推荐用event.currentTarget。 - 忘记移除
dragging/over样式,界面状态残留。 - 在 Firefox 中未调用
setData,导致拖拽无法开始。
移动端注意
原生 HTML5 Drag and Drop 在移动端兼容性较弱,生产环境中可考虑 Pointer Events 或专门的拖拽库作为补充。
