拖放 API dragstart dragover
以“待办→已完成”看板为例讲解 HTML5 拖放 API,说明卡片需设置 draggable="true",在 dragstart 中用 dataTransfer 保存被拖卡片标识,并厘清 dragstart、dragover、drop 的
学完这篇,你能从零做出一个「用鼠标把卡片从一个区域拖到另一个区域」的完整功能,并且搞清楚 dragstart、dragover、drop 这几个事件各自负责什么,为什么很多人的拖放代码写完「能拖但放不下」。
拖放(Drag and Drop)是 HTML5 里非常老的一组 API,Chrome 4+、Firefox 3.5+、Safari 3.1+ 就支持了。它不需要任何第三方库,但事件流程有点绕:一次成功的拖放要经过 7 个左右的事件,其中两个最容易出问题的就是 dragstart 和 dragover。下面用一个「待办 → 已完成」看板的例子,一步步搭起来。
第一步:搭好页面骨架,明确谁被拖、拖到哪
这一步要做的,是把 HTML 结构和样式写好,让页面上有两个区域和两张可拖的卡片。做完你会在浏览器里看到两个虚线框,左边框里有两张白卡片。
新建一个 index.html,把下面内容整个贴进去:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>拖放演示</title>
<style>
body { font-family: system-ui, sans-serif; padding: 24px; }
.list { display: flex; gap: 24px; }
.zone {
width: 220px; min-height: 180px; padding: 12px;
border: 2px dashed #b9c0cc; border-radius: 8px;
background: #f7f8fa;
}
.zone.over { border-color: #2f7cf6; background: #e8f0fe; }
.card {
padding: 10px 12px; margin-bottom: 8px;
background: #fff; border: 1px solid #dde1e8;
border-radius: 6px; cursor: grab;
}
.card.dragging { opacity: .4; }
</style>
</head>
<body>
<div class="list">
<div class="zone" id="todo">
<div class="card" draggable="true" data-id="t1">写周报</div>
<div class="card" draggable="true" data-id="t2">改 bug</div>
</div>
<div class="zone" id="done"></div>
</div>
</body>
</html>
这里唯一和拖放有关的属性是卡片上的 draggable="true"。不加它,浏览器不会把元素当成可拖对象,dragstart 一次都不会触发。反过来,<img>、<a> 这类元素默认就是可拖的,如果你不希望它们被拖走,要显式写 draggable="false"。
data-id 是我们自己加的业务标识,后面用来在 drop 时找回是哪张卡片被拖了。
第二步:给元素开启拖拽能力,在 dragstart 里存数据
这一步要做的是:用户按下鼠标开始拖动卡片时,把「被拖的是哪张卡」记录到浏览器的拖放数据仓库里。做完之后拖动卡片会有半透明效果,并且拖放数据已经挂在 dataTransfer 上了。
在 </body> 前加一段 <script>:
<script>
const zones = document.querySelectorAll('.zone');
document.addEventListener('dragstart', (e) => {
const card = e.target.closest('.card');
if (!card) return; // 拖的不是卡片,直接忽略
e.dataTransfer.setData('text/plain', card.dataset.id);
e.dataTransfer.effectAllowed = 'move'; // 告诉浏览器这是"移动"操作
card.classList.add('dragging');
});
</script>
三个关键点:
dragstart会冒泡,所以绑在document上用事件委托就够了,不用给每张卡片单独绑。e.target就是被拖的那个元素。setData(format, value)的第一个参数是 MIME 类型字符串。用标准的'text/plain'最省事,兼容性最好;也可以自定义成'text/x-card-id',好处是不和别处拖进来的纯文本冲突。effectAllowed决定这次拖拽允许的操作类型,可选值有copy、move、link、copyMove、all、none。它和后面dropEffect配合,控制鼠标光标是显示「+」还是「禁止」。
注意:
dragover和drop事件里,Chrome 出于安全考虑读不到getData()的内容,永远返回空字符串。数据只能在dragstart里写、在drop里读,中间那几个事件别指望拿到值。这是新手调试时最常见的困惑来源。
第三步:在 dragover 里放行,这是最容易卡住的一步
这一步要做的是:鼠标拖到目标区域上方时,告诉浏览器「这里可以放」。做完之后,拖到右框上会看到边框变蓝、背景变浅蓝。
zones.forEach(zone => {
zone.addEventListener('dragover', (e) => {
e.preventDefault(); // 关键,不写 drop 永远不会触发
e.dataTransfer.dropEffect = 'move'; // 光标显示为"移动"
zone.classList.add('over');
});
zone.addEventListener('dragleave', () => {
zone.classList.remove('over');
});
});
e.preventDefault() 是整篇文章最重要的一行代码。 浏览器的默认行为是「拒绝接受放置」,表现为鼠标显示一个禁止符号,并且 drop 事件根本不会派发。只要在 dragover 里调一次 preventDefault(),这个区域就变成了合法投放区。
注意:
dragover是高频触发的,鼠标每移动一像素就可能触发一次。所以这里面千万不要做重活——不要查询 DOM、不要发请求、不要console.log刷屏。只做preventDefault和切 class 这类轻量操作。
注意:
dropEffect必须和dragstart里设的effectAllowed兼容。如果effectAllowed是'copy',而你在dragover里把dropEffect设成'move',光标会变成禁止符号,看上去像「放不进去」,实际是两者冲突了。
第四步:在 drop 里取出数据,完成真正的搬运
这一步要做的是:用户松手时读取 dragstart 存下的 id,把对应卡片移动到目标区域。做完之后,卡片就真的从左边跑到右边了。
zones.forEach(zone => {
zone.addEventListener('drop', (e) => {
e.preventDefault();
zone.classList.remove('over');
const id = e.dataTransfer.getData('text/plain');
const card = document.querySelector(`.card[data-id="${id}"]`);
if (card) zone.appendChild(card);
});
});
appendChild 会自动把元素从原来的父节点里摘下来,所以不需要手动 remove(),一行就完成了「移动」。
drop 里的 preventDefault() 同样不能省。如果不写,浏览器会执行默认行为——比如你拖的是一个链接,松开后当前页面会直接跳转到那个 URL。
事件的完整顺序是这样的,可以在 Chrome DevTools 的 Sources 面板打断点验证:
dragstart → drag → dragenter → dragover(反复触发)→ drop → dragend
第五步:收尾——dragend 与视觉反馈
这一步要做的是:不管拖放成功还是失败(中途按 Esc 取消、丢到空白处),都把临时样式清掉。做完之后就不会出现「卡片一直是半透明的」这种残留状态。
document.addEventListener('dragend', (e) => {
const card = e.target.closest('.card');
if (card) card.classList.remove('dragging');
zones.forEach(z => z.classList.remove('over'));
});
dragend 是拖放流程的最后一个事件,无论成功与否都一定会触发,所以清理逻辑放这里最保险,而不是放在 drop 里。
另外,如果你不想用默认的拖拽影子(那个跟随鼠标的半透明截图),可以在 dragstart 里用 setDragImage 换成自己做的元素:
const ghost = document.createElement('div');
ghost.textContent = card.textContent;
ghost.style.cssText = 'padding:8px 12px;background:#2f7cf6;color:#fff;border-radius:6px;position:absolute;top:-9999px';
document.body.appendChild(ghost);
e.dataTransfer.setDragImage(ghost, 20, 20); // 后两个参数是热点偏移
setDragImage 的第二个、第三个参数是相对于这个元素左上角的偏移量,想让它跟着光标居中,就传元素宽高的一半。
第六步:几个必然会遇到的坑
**坑一:dragleave 在子元素上误触发。
原文链接:https://www.gj0.com/thread-1716.html
转载请注明出处并保留本声明;内容仅代表作者观点,与本站立场无关。若本文涉嫌侵权,请联系本站处理。