拖放事件
拖放事件(Drag and Drop Events)是 HTML5 原生支持的拖放交互 API,允许用户通过鼠标拖拽元素来实现数据的移动、复制等操作。拖放 API 定义了完整的事件生命周期:从拖拽开始、拖拽经过、放置到拖拽结束,每个阶段都有对应的事件处理器。
前置知识
阅读本节前,建议先了解:窗口事件
基础概念
拖放事件类型
| 阶段 | 事件 | 触发元素 | 说明 |
|---|---|---|---|
| 拖拽开始 | dragstart | 被拖元素 | 拖拽开始时 |
| 拖拽中 | drag | 被拖元素 | 持续触发 |
| 拖拽结束 | dragend | 被拖元素 | 拖拽操作结束时 |
| 进入目标 | dragenter | 放置目标 | 拖拽元素进入目标区域 |
| 在目标上方 | dragover | 放置目标 | 在目标区域内持续触发 |
| 离开目标 | dragleave | 放置目标 | 拖拽元素离开目标区域 |
| 放置 | drop | 放置目标 | 在目标上释放鼠标 |
使元素可拖拽
html
<!-- 设置 draggable 属性 -->
<div draggable="true">可拖拽的元素</div>
<!-- draggable 的三个值 -->
<div draggable="true">可拖拽</div>
<div draggable="false">不可拖拽</div>
<div draggable="auto">浏览器默认行为(图片、链接默认可拖拽)</div>语法
HTML 事件属性
html
<!-- 被拖拽元素 -->
<div draggable="true"
ondragstart="handleDragStart(event)"
ondrag="handleDrag(event)"
ondragend="handleDragEnd(event)">
拖拽我
</div>
<!-- 放置目标 -->
<div class="drop-zone"
ondragenter="handleDragEnter(event)"
ondragover="handleDragOver(event)"
ondragleave="handleDragLeave(event)"
ondrop="handleDrop(event)">
放在这里
</div>JavaScript 绑定
javascript
const draggable = document.getElementById('draggable');
const dropZone = document.getElementById('drop-zone');
// 被拖拽元素事件
draggable.addEventListener('dragstart', (e) => {
e.dataTransfer.setData('text/plain', e.target.id);
e.target.classList.add('dragging');
});
draggable.addEventListener('dragend', (e) => {
e.target.classList.remove('dragging');
});
// 放置目标事件
dropZone.addEventListener('dragover', (e) => {
e.preventDefault(); // 必须阻止默认行为才能接收 drop
});
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
const id = e.dataTransfer.getData('text/plain');
dropZone.appendChild(document.getElementById(id));
});详细说明
DataTransfer 对象
javascript
// dragstart 中设置数据
element.addEventListener('dragstart', (e) => {
// 设置文本数据
e.dataTransfer.setData('text/plain', 'Hello');
e.dataTransfer.setData('text/html', '<b>Hello</b>');
// 设置自定义 MIME 类型
e.dataTransfer.setData('application/json', JSON.stringify({ id: 1 }));
// 设置拖拽效果
e.dataTransfer.effectAllowed = 'move'; // copy, move, link, none, copyMove, etc.
// 设置拖拽图像
const img = document.createElement('div');
img.textContent = '拖拽中...';
e.dataTransfer.setDragImage(img, 0, 0);
});
// drop 中获取数据
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
// 获取文本数据
const text = e.dataTransfer.getData('text/plain');
// 获取自定义数据
const json = e.dataTransfer.getData('application/json');
// 获取文件(从文件管理器拖入)
const files = e.dataTransfer.files;
});effectAllowed 与 dropEffect
| effectAllowed (dragstart) | dropEffect (drop) | 结果 |
|---|---|---|
copy | copy | 复制操作 |
move | move | 移动操作 |
link | link | 链接操作 |
copyMove | copy / move | 对应操作 |
none | 任何 | 不允许放置 |
实战示例
完整的任务看板拖拽
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>拖放事件示例</title>
<style>
.board {
display: flex;
gap: 16px;
padding: 20px;
min-height: 80vh;
}
.column {
flex: 1;
background: #f1f5f9;
border-radius: 8px;
padding: 12px;
min-height: 200px;
}
.column h3 {
margin: 0 0 12px;
font-size: 16px;
color: #475569;
}
.column.drag-over {
background: #dbeafe;
border: 2px dashed #3b82f6;
}
.card {
padding: 12px;
background: white;
border-radius: 6px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
margin-bottom: 8px;
cursor: grab;
transition: transform 0.2s, opacity 0.2s;
}
.card:active { cursor: grabbing; }
.card.dragging { opacity: 0.4; transform: rotate(2deg); }
</style>
</head>
<body>
<div class="board">
<div class="column" id="todo">
<h3>待办</h3>
<div class="card" draggable="true" id="task-1">设计首页布局</div>
<div class="card" draggable="true" id="task-2">编写接口文档</div>
<div class="card" draggable="true" id="task-3">搭建开发环境</div>
</div>
<div class="column" id="doing">
<h3>进行中</h3>
</div>
<div class="column" id="done">
<h3>已完成</h3>
</div>
</div>
<script>
let draggedCard = null;
// 为所有卡片绑定拖拽事件
document.querySelectorAll('.card').forEach(card => {
card.addEventListener('dragstart', (e) => {
draggedCard = card;
card.classList.add('dragging');
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/plain', card.id);
});
card.addEventListener('dragend', () => {
card.classList.remove('dragging');
document.querySelectorAll('.column').forEach(col => {
col.classList.remove('drag-over');
});
draggedCard = null;
});
});
// 为所有列绑定放置事件
document.querySelectorAll('.column').forEach(column => {
column.addEventListener('dragover', (e) => {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
column.classList.add('drag-over');
});
column.addEventListener('dragleave', (e) => {
// 只在真正离开列时移除样式
if (!column.contains(e.relatedTarget)) {
column.classList.remove('drag-over');
}
});
column.addEventListener('drop', (e) => {
e.preventDefault();
column.classList.remove('drag-over');
if (draggedCard) {
// 插入到列中的最后一个卡片之后
const cards = column.querySelectorAll('.card');
const lastCard = cards[cards.length - 1];
if (lastCard && lastCard !== draggedCard) {
column.insertBefore(draggedCard, lastCard.nextSibling);
} else {
column.appendChild(draggedCard);
}
}
});
});
</script>
</body>
</html>注意事项
必须在 dragover 中调用 preventDefault
javascript
// 必须阻止 dragover 的默认行为,否则 drop 事件不会触发
dropZone.addEventListener('dragover', (e) => {
e.preventDefault(); // 关键!
});文件拖入支持
javascript
// 支持从桌面拖入文件
const dropArea = document.getElementById('drop-area');
dropArea.addEventListener('dragover', (e) => {
e.preventDefault();
dropArea.classList.add('drag-over');
});
dropArea.addEventListener('drop', (e) => {
e.preventDefault();
dropArea.classList.remove('drag-over');
const files = e.dataTransfer.files;
for (const file of files) {
console.log('文件名:', file.name, '大小:', file.size, '类型:', file.type);
}
});最佳实践
1. 提供清晰的视觉反馈
css
/* 拖拽开始时的样式 */
.dragging {
opacity: 0.5;
transform: scale(0.95);
}
/* 放置目标高亮 */
.drop-target {
outline: 3px dashed #3b82f6;
outline-offset: -3px;
background: #eff6ff;
}下一节
继续学习:剪贴板事件