前言
在网页中实现类似 Trello 或 Jira 的任务看板拖拽效果,并不一定需要复杂的第三方库。HTML5 原生提供的拖放(Drag and Drop)API 已经非常强大。本文将带你梳理拖放事件的完整生命周期,并手把手实现一个任务看板 Demo。
一、 开启拖放:draggable 属性
默认情况下,网页中只有 图片(<img>) 、链接(<a>) 和 选中的文本 是可以拖动的。若想让其他元素(如 div)可拖动,必须显式设置:
<div draggable="true">我是可拖动的任务卡片</div>
<img src="logo.png" draggable="false">
二、 拖放事件全解析
拖放过程涉及两个主体:被拖动元素(Source) 和 放置目标(Target) 。
1. 被拖动元素触发的事件
| 事件 | 触发时机 |
|---|---|
dragstart | 用户开始拖动元素的一瞬间触发。 |
drag | 元素被拖动期间持续触发(类似 mousemove)。 |
dragend | 拖动停止时触发(无论放置是否成功)。 |
2. 放置目标触发的事件
| 事件 | 触发时机 |
|---|---|
dragenter | 拖动元素进入目标区域时触发。 |
dragover | 关键:拖动元素在目标区域上方移动时持续触发。 |
dragleave | 拖动元素离开目标区域时触发。 |
drop | 拖动元素在目标上方释放时触发。 |
三、 核心对象:dataTransfer
拖放不仅是位置的移动,往往伴随着数据的传递。event.dataTransfer 对象就是这个“运载火箭”。
常用方法:
-
setData(format, data):在dragstart中调用,存储数据。- 常用格式:
text/plain或text/uri-list。
- 常用格式:
-
getData(format):在drop中调用,读取数据。
四、 实战:构建任务看板 (Kanban)
1. 核心逻辑点
- 阻止默认行为:默认情况下,浏览器禁止将数据/元素放置在其他元素上。为了允许放置,必须在
dragover事件中调用e.preventDefault()。 - 视觉反馈:通过
dragenter添加高亮类,dragleave和drop移除高亮类。
2. 代码实现
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>任务看板拖放示例</title>
<style>
.board { display: flex; gap: 20px; }
.column {
width: 200px;
padding: 15px;
background: #f5f5f5;
border-radius: 8px;
min-height: 300px;
}
.task {
padding: 12px;
margin: 10px 0;
background: white;
border: 1px solid #ddd;
border-radius: 4px;
cursor: move;
}
.highlight { border: 2px dashed #4CAF50; } /* 放置目标高亮样式 */
</style>
</head>
<body>
<div class="board">
<div class="column" id="todo">
<h3>待处理</h3>
<div class="task" draggable="true" data-id="1">任务1:设计文档</div>
<div class="task" draggable="true" data-id="2">任务2:代码评审</div>
</div>
<div class="column" id="in-progress">
<h3>进行中</h3>
</div>
<div class="column" id="done">
<h3>已完成</h3>
</div>
</div>
<script>
// 1. 监听所有可拖动元素的 dragstart 事件
document.querySelectorAll('.task').forEach(task => {
task.addEventListener('dragstart', e => {
// 保存任务ID到 dataTransfer
e.dataTransfer.setData('text/plain', e.target.dataset.id);
e.target.classList.add('dragging'); // 拖动时样式变化
});
task.addEventListener('dragend', e => {
e.target.classList.remove('dragging'); // 拖动结束还原样式
});
});
// 2. 为每个状态列设置放置目标逻辑
document.querySelectorAll('.column').forEach(column => {
// 允许放置:阻止默认行为 + 添加高亮
column.addEventListener('dragover', e => {
e.preventDefault();
column.classList.add('highlight');
});
// 移除高亮反馈
column.addEventListener('dragleave', () => {
column.classList.remove('highlight');
});
// 放置处理
column.addEventListener('drop', e => {
e.preventDefault();
column.classList.remove('highlight');
// 获取传递的任务ID
const taskId = e.dataTransfer.getData('text/plain');
const taskElement = document.querySelector(`.task[data-id="${taskId}"]`);
// 将任务移动到当前列(避免重复添加)
if (!column.contains(taskElement)) {
column.appendChild(taskElement);
// 实际项目中:此处可调用API更新任务状态
console.log(`任务 ${taskId} 移动到 ${column.id}`);
}
});
});
</script>
</body>
</html>
五、 面试模拟题
Q1:为什么要在 dragover 中执行 e.preventDefault()?
参考回答:
浏览器默认是不允许在元素上放置任何内容的(会显示一个“禁止”图标)。只有显式地在 dragover 事件中阻止默认行为,浏览器才会认为该区域是一个“有效的放置目标”,进而允许 drop 事件触发。
Q2:drop 事件和 dragend 事件哪个先触发?
参考回答:
通常情况下,drop 事件先触发(处理放置逻辑),然后 dragend 事件最后触发(处理清理逻辑,如恢复透明度)。
Q3:如何实现拖拽文件上传?
参考回答:
监听容器的 drop 事件。通过 event.dataTransfer.files 可以获取到用户拖入浏览器的文件列表(File 对象),随后可以使用 FormData 进行异步上传。