pin_drop当前位置:知识文库 ❯ 图文
JS集成:HTML ondrag 事件详 - 详细教程与实战指南
教程简介
ondrag 事件是 HTML5 拖放 API 的核心组成部分,用于实现元素的拖拽交互。拖放操作涉及一系列事件的协作:dragstart、drag、dragend 在拖拽源上触发,dragenter、dragover、dragleave、drop 在放置目标上触发。理解这些事件的工作机制和数据传递方式,是构建流畅拖拽交互的基础。
本教程将深入讲解拖拽事件系列(dragstart/drag/dragend)、拖拽数据传递(dataTransfer)、拖拽源设置、拖拽视觉效果、自定义拖拽图像以及拖拽限制。
核心概念
拖拽事件系列
拖拽操作涉及以下事件,按触发顺序排列:
dataTransfer 对象
dataTransfer 对象是拖拽操作的核心,用于在拖拽源和放置目标之间传递数据。
拖拽效果类型
使元素可拖拽
代码示例
<!-- 设置 draggable 属性 -->
<div draggable="true">可拖拽元素</div>代码示例
// JavaScript 方式
element.draggable = true;语法与用法
设置拖拽源
代码示例
// 使元素可拖拽
element.draggable = true;
element.addEventListener('dragstart', function(e) {
// 设置拖拽数据
e.dataTransfer.setData('text/plain', '拖拽数据');
e.dataTransfer.setData('application/json', JSON.stringify({ id: 1 }));
// 设置拖拽效果
e.dataTransfer.effectAllowed = 'copy';
});设置放置目标
代码示例
// 必须阻止 dragover 的默认行为才能触发 drop
dropTarget.addEventListener('dragover', function(e) {
e.preventDefault();
e.dataTransfer.dropEffect = 'copy';
});
dropTarget.addEventListener('drop', function(e) {
e.preventDefault();
const data = e.dataTransfer.getData('text/plain');
// 处理放置逻辑
});dataTransfer 数据格式
代码示例
示例1:基础拖拽事件演示
代码示例
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>基础拖拽事件演示</title>
<style>
body {
font-family: "Microsoft YaHei", sans-serif;
padding: 20px;
background: #f5f5f5;
}
.demo-area {
display: flex;
gap: 30px;
margin-bottom: 30px;
}
.drag-source {
width: 150px;
height: 150px;
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
display: flex;
align-items: center;
justify-content: center;
border-radius: 12px;
font-size: 16px;
font-weight: bold;
cursor: grab;
user-select: none;
transition: opacity 0.2s, transform 0.2s;
}
.drag-source:active { cursor: grabbing; }
.drag-source.dragging {
opacity: 0.5;
transform: scale(0.95);
}
.drop-zone {
width: 250px;
height: 250px;
border: 3px dashed #ccc;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
color: #999;
transition: all 0.2s;
background: white;
}
.drop-zone.drag-over {
border-color: #667eea;
background: #e8eaf6;
color: #667eea;
transform: scale(1.02);
}
.drop-zone.dropped {
border-style: solid;
border-color: #4CAF50;
background: #e8f5e9;
color: #4CAF50;
}
.event-log {
background: white;
padding: 20px;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.event-log h3 { margin-top: 0; }
.log-entries {
max-height: 250px;
overflow-y: auto;
font-size: 13px;
font-family: monospace;
}
.log-entry {
padding: 4px 8px;
border-bottom: 1px solid #f0f0f0;
}
.log-entry.drag-source-event { color: #667eea; }
.log-entry.drop-target-event { color: #4CAF50; }
</style>
</head>
<body>
<h1>基础拖拽事件演示</h1>
<p>将紫色方块拖到右侧放置区域。</p>
<div class="demo-area">
<div class="drag-source" id="dragSource" draggable="true">
拖拽我
</div>
<div class="drop-zone" id="dropZone">
放置到这里
</div>
</div>
<div class="event-log">
<h3>事件日志</h3>
<div class="log-entries" id="logEntries"></div>
</div>
<script>
const dragSource = document.getElementById('dragSource');
const dropZone = document.getElementById('dropZone');
const logEntries = document.getElementById('logEntries');
// 拖拽源事件
dragSource.addEventListener('dragstart', function(e) {
e.dataTransfer.setData('text/plain', '这是拖拽数据');
e.dataTransfer.effectAllowed = 'copy';
this.classList.add('dragging');
addLog('dragstart', '开始拖拽', 'drag-source-event');
});
dragSource.addEventListener('drag', function(e) {
// drag 事件触发频率极高,不记录日志
});
dragSource.addEventListener('dragend', function(e) {
this.classList.remove('dragging');
const result = e.dataTransfer.dropEffect === 'none' ? '取消' : '成功';
addLog('dragend', `拖拽结束 (${result})`, 'drag-source-event');
});
// 放置目标事件
dropZone.addEventListener('dragenter', function(e) {
e.preventDefault();
this.classList.add('drag-over');
addLog('dragenter', '拖入放置区域', 'drop-target-event');
});
dropZone.addEventListener('dragover', function(e) {
e.preventDefault();
e.dataTransfer.dropEffect = 'copy';
});
dropZone.addEventListener('dragleave', function(e) {
this.classList.remove('drag-over');
addLog('dragleave', '拖出放置区域', 'drop-target-event');
});
dropZone.addEventListener('drop', function(e) {
e.preventDefault();
const data = e.dataTransfer.getData('text/plain');
this.classList.remove('drag-over');
this.classList.add('dropped');
this.textContent = '已接收: ' + data;
addLog('drop', `放置数据: "${data}"`, 'drop-target-event');
});
function addLog(event, message, className) {
const time = new Date().toLocaleTimeString('zh-CN', { hour12: false });
const entry = document.createElement('div');
entry.className = 'log-entry ' + className;
entry.textContent = `[${time}] ${event}: ${message}`;
logEntries.insertBefore(entry, logEntries.firstChild);
while (logEntries.children.length > 30) {
logEntries.removeChild(logEntries.lastChild);
}
}
</script>
</body>
</html>示例2:dataTransfer 数据传递
代码示例
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>dataTransfer 数据传递</title>
<style>
body {
font-family: "Microsoft YaHei", sans-serif;
padding: 20px;
background: #f5f5f5;
}
.items-container {
display: flex;
gap: 12px;
flex-wrap: wrap;
margin-bottom: 30px;
}
.drag-item {
padding: 12px 20px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
cursor: grab;
user-select: none;
font-size: 14px;
transition: transform 0.2s, box-shadow 0.2s;
}
.drag-item:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}
.drag-item:active { cursor: grabbing; }
.drag-item.dragging { opacity: 0.5; }
.drop-area {
min-height: 200px;
padding: 20px;
background: white;
border: 3px dashed #ccc;
border-radius: 12px;
transition: all 0.2s;
}
.drop-area.drag-over {
border-color: #2196F3;
background: #e3f2fd;
}
.drop-area h3 { margin-top: 0; color: #666; }
.received-data {
margin-top: 10px;
padding: 12px;
background: #f5f5f5;
border-radius: 8px;
font-size: 13px;
font-family: monospace;
white-space: pre-wrap;
}
.format-selector {
margin-bottom: 15px;
}
.format-selector label {
margin-right: 15px;
font-size: 14px;
cursor: pointer;
}
</style>
</head>
<body>
<h1>dataTransfer 数据传递</h1>
<p>拖拽下方项目到放置区域,查看不同格式的数据传递。</p>
<div class="items-container">
<div class="drag-item" draggable="true" data-type="text" data-value="Hello World">纯文本</div>
<div class="drag-item" draggable="true" data-type="url" data-value="https://example.com">URL</div>
<div class="drag-item" draggable="true" data-type="json" data-value='{"name":"张三","age":25,"role":"开发者"}'>JSON 对象</div>
<div class="drag-item" draggable="true" data-type="html" data-value="<strong>加粗文本</strong>">HTML</div>
</div>
<div class="format-selector">
<strong>读取格式:</strong>
<label><input type="radio" name="format" value="text/plain" checked> text/plain</label>
<label><input type="radio" name="format" value="text/uri-list"> text/uri-list</label>
<label><input type="radio" name="format" value="text/html"> text/html</label>
<label><input type="radio" name="format" value="application/json"> application/json</label>
</div>
<div class="drop-area" id="dropArea">
<h3>放置区域</h3>
<div class="received-data" id="receivedData">等待拖拽数据...</div>
</div>
<script>
const dropArea = document.getElementById('dropArea');
const receivedData = document.getElementById('receivedData');
const dragItems = document.querySelectorAll('.drag-item');
// 拖拽源
dragItems.forEach(item => {
item.addEventListener('dragstart', function(e) {
const type = this.dataset.type;
const value = this.dataset.value;
// 根据类型设置不同格式的数据
e.dataTransfer.setData('text/plain', value);
if (type === 'url') {
e.dataTransfer.setData('text/uri-list', value);
} else if (type === 'html') {
e.dataTransfer.setData('text/html', value);
} else if (type === 'json') {
e.dataTransfer.setData('application/json', value);
}
e.dataTransfer.effectAllowed = 'copy';
this.classList.add('dragging');
});
item.addEventListener('dragend', function() {
this.classList.remove('dragging');
});
});
// 放置目标
dropArea.addEventListener('dragover', function(e) {
e.preventDefault();
e.dataTransfer.dropEffect = 'copy';
});
dropArea.addEventListener('dragenter', function(e) {
e.preventDefault();
this.classList.add('drag-over');
});
dropArea.addEventListener('dragleave', function() {
this.classList.remove('drag-over');
});
dropArea.addEventListener('drop', function(e) {
e.preventDefault();
this.classList.remove('drag-over');
// 获取选中的格式
const selectedFormat = document.querySelector('input[name="format"]:checked').value;
// 尝试获取指定格式的数据
const data = e.dataTransfer.getData(selectedFormat);
// 显示所有可用的格式
const types = Array.from(e.dataTransfer.types);
let output = `可用格式: ${types.join(', ')}\n\n`;
output += `请求格式: ${selectedFormat}\n`;
output += `获取数据: ${data || '(无数据)'}\n\n`;
// 显示所有格式的数据
types.forEach(type => {
if (type !== 'Files') {
output += `[${type}]: ${e.dataTransfer.getData(type)}\n`;
}
});
receivedData.textContent = output;
});
</script>
</body>
</html>示例3:拖拽视觉效果与自定义拖拽图像
代码示例
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>拖拽视觉效果与自定义拖拽图像</title>
<style>
body {
font-family: "Microsoft YaHei", sans-serif;
padding: 20px;
background: #f5f5f5;
}
.demo-row {
display: flex;
gap: 30px;
margin-bottom: 30px;
align-items: flex-start;
flex-wrap: wrap;
}
.drag-card {
width: 180px;
padding: 20px;
background: white;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
cursor: grab;
user-select: none;
text-align: center;
transition: transform 0.2s;
}
.drag-card:hover { transform: translateY(-3px); }
.drag-card:active { cursor: grabbing; }
.drag-card.dragging { opacity: 0.4; }
.drag-card .icon { font-size: 40px; margin-bottom: 10px; }
.drag-card .name { font-weight: bold; margin-bottom: 5px; }
.drag-card .desc { font-size: 12px; color: #888; }
.drop-container {
flex: 1;
min-width: 300px;
min-height: 300px;
padding: 20px;
background: white;
border: 3px dashed #ddd;
border-radius: 12px;
transition: all 0.2s;
}
.drop-container.drag-over {
border-color: #4CAF50;
background: #e8f5e9;
}
.drop-container h3 { margin-top: 0; color: #666; }
.dropped-items {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.dropped-item {
padding: 8px 16px;
background: #e3f2fd;
border-radius: 20px;
font-size: 13px;
color: #1565C0;
}
/* 自定义拖拽图像样式 */
.custom-drag-image {
position: absolute;
top: -9999px;
padding: 10px 20px;
background: #333;
color: white;
border-radius: 8px;
font-size: 14px;
font-weight: bold;
box-shadow: 0 4px 15px rgba(0,0,0,0.3);
}
.effect-demo {
padding: 20px;
background: white;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
.effect-demo h3 { margin-top: 0; }
.effect-options {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.effect-btn {
padding: 8px 20px;
border: 2px solid #ddd;
border-radius: 8px;
background: white;
cursor: pointer;
font-size: 14px;
transition: all 0.2s;
}
.effect-btn:hover { border-color: #2196F3; }
.effect-btn.active {
border-color: #2196F3;
background: #e3f2fd;
color: #1565C0;
}
</style>
</head>
<body>
<h1>拖拽视觉效果与自定义拖拽图像</h1>
<div class="effect-demo">
<h3>拖拽效果设置</h3>
<div class="effect-options">
<button class="effect-btn active" data-effect="copy">copy (复制)</button>
<button class="effect-btn" data-effect="move">move (移动)</button>
<button class="effect-btn" data-effect="link">link (链接)</button>
<button class="effect-btn" data-effect="none">none (禁止)</button>
</div>
</div>
<div class="demo-row">
<div class="drag-card" id="card1" draggable="true" data-name="文档" data-id="1">
<div class="icon">📄</div>
<div class="name">文档</div>
<div class="desc">拖拽此卡片</div>
</div>
<div class="drag-card" id="card2" draggable="true" data-name="图片" data-id="2">
<div class="icon">📷</div>
<div class="name">图片</div>
<div class="desc">自定义拖拽图像</div>
</div>
<div class="drag-card" id="card3" draggable="true" data-name="视频" data-id="3">
<div class="icon">🎬</div>
<div class="name">视频</div>
<div class="desc">带计数拖拽</div>
</div>
<div class="drop-container" id="dropContainer">
<h3>放置区域</h3>
<div class="dropped-items" id="droppedItems"></div>
</div>
</div>
<!-- 自定义拖拽图像元素 -->
<div class="custom-drag-image" id="customDragImage">正在拖拽...</div>
<script>
let currentEffect = 'copy';
const dropContainer = document.getElementById('dropContainer');
const droppedItems = document.getElementById('droppedItems');
const customDragImage = document.getElementById('customDragImage');
// 效果选择
document.querySelectorAll('.effect-btn').forEach(btn => {
btn.addEventListener('click', function() {
document.querySelectorAll('.effect-btn').forEach(b => b.classList.remove('active'));
this.classList.add('active');
currentEffect = this.dataset.effect;
});
});
// 卡片1:默认拖拽
const card1 = document.getElementById('card1');
card1.addEventListener('dragstart', function(e) {
e.dataTransfer.setData('text/plain', JSON.stringify({ id: 1, name: '文档' }));
e.dataTransfer.effectAllowed = currentEffect;
this.classList.add('dragging');
});
card1.addEventListener('dragend', function() {
this.classList.remove('dragging');
});
// 卡片2:自定义拖拽图像
const card2 = document.getElementById('card2');
card2.addEventListener('dragstart', function(e) {
e.dataTransfer.setData('text/plain', JSON.stringify({ id: 2, name: '图片' }));
e.dataTransfer.effectAllowed = currentEffect;
// 设置自定义拖拽图像
customDragImage.textContent = '正在拖拽: 图片';
e.dataTransfer.setDragImage(customDragImage, 60, 20);
this.classList.add('dragging');
});
card2.addEventListener('dragend', function() {
this.classList.remove('dragging');
});
// 卡片3:动态创建拖拽图像
const card3 = document.getElementById('card3');
card3.addEventListener('dragstart', function(e) {
e.dataTransfer.setData('text/plain', JSON.stringify({ id: 3, name: '视频' }));
e.dataTransfer.effectAllowed = currentEffect;
// 动态创建拖拽图像
const dragImg = document.createElement('div');
dragImg.style.cssText = 'position:absolute;top:-9999px;padding:8px 16px;background:#E91E63;color:white;border-radius:6px;font-size:13px;';
dragImg.textContent = '1 个项目';
document.body.appendChild(dragImg);
e.dataTransfer.setDragImage(dragImg, 40, 15);
// 延迟移除(拖拽图像需要元素存在)
setTimeout(() => dragImg.remove(), 0);
this.classList.add('dragging');
});
card3.addEventListener('dragend', function() {
this.classList.remove('dragging');
});
// 放置目标
dropContainer.addEventListener('dragover', function(e) {
e.preventDefault();
if (currentEffect !== 'none') {
e.dataTransfer.dropEffect = currentEffect;
this.classList.add('drag-over');
}
});
dropContainer.addEventListener('dragleave', function() {
this.classList.remove('drag-over');
});
dropContainer.addEventListener('drop', function(e) {
e.preventDefault();
this.classList.remove('drag-over');
const data = e.dataTransfer.getData('text/plain');
if (data) {
const item = JSON.parse(data);
const tag = document.createElement('div');
tag.className = 'dropped-item';
tag.textContent = item.name + ' (效果: ' + e.dataTransfer.dropEffect + ')';
droppedItems.appendChild(tag);
}
});
</script>
</body>
</html>示例4:拖拽限制与条件放置
代码示例
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>拖拽限制与条件放置</title>
<style>
body {
font-family: "Microsoft YaHei", sans-serif;
padding: 20px;
background: #f5f5f5;
}
.container {
display: flex;
gap: 20px;
flex-wrap: wrap;
}
.source-panel, .target-panel {
flex: 1;
min-width: 250px;
}
.panel-title {
font-size: 16px;
font-weight: bold;
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 2px solid #eee;
}
.source-item {
padding: 10px 16px;
margin-bottom: 8px;
background: white;
border-radius: 8px;
box-shadow: 0 1px 4px rgba(0,0,0,0.1);
cursor: grab;
user-select: none;
display: flex;
align-items: center;
gap: 10px;
font-size: 14px;
transition: transform 0.2s;
}
.source-item:hover { transform: translateX(5px); }
.source-item:active { cursor: grabbing; }
.source-item.restricted {
border-left: 4px solid #f44336;
cursor: not-allowed;
}
.source-item.allowed {
border-left: 4px solid #4CAF50;
}
.source-item .type-badge {
padding: 2px 8px;
border-radius: 10px;
font-size: 11px;
font-weight: bold;
}
.type-admin { background: #ffebee; color: #c62828; }
.type-editor { background: #e8f5e9; color: #2e7d32; }
.type-viewer { background: #e3f2fd; color: #1565C0; }
.drop-zone {
min-height: 200px;
padding: 15px;
border: 3px dashed #ddd;
border-radius: 12px;
transition: all 0.2s;
}
.drop-zone.valid {
border-color: #4CAF50;
background: #e8f5e9;
}
.drop-zone.invalid {
border-color: #f44336;
background: #ffebee;
}
.zone-label {
font-size: 13px;
color: #888;
margin-bottom: 8px;
}
.zone-items {
display: flex;
flex-direction: column;
gap: 6px;
}
.zone-item {
padding: 8px 12px;
background: white;
border-radius: 6px;
font-size: 13px;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.message {
margin-top: 20px;
padding: 12px 16px;
border-radius: 8px;
font-size: 14px;
display: none;
}
.message.error { background: #ffebee; color: #c62828; display: block; }
.message.success { background: #e8f5e9; color: #2e7d32; display: block; }
</style>
</head>
<body>
<h1>拖拽限制与条件放置</h1>
<p>管理员角色不允许拖入普通区域,只有编辑者和查看者可以放入。</p>
<div class="container">
<div class="source-panel">
<div class="panel-title">可拖拽项目</div>
<div class="source-item allowed" draggable="true" data-role="editor" data-name="编辑者A">
<span class="type-badge type-editor">编辑者</span> 编辑者A
</div>
<div class="source-item restricted" draggable="true" data-role="admin" data-name="管理员B">
<span class="type-badge type-admin">管理员</span> 管理员B
</div>
<div class="source-item allowed" draggable="true" data-role="viewer" data-name="查看者C">
<span class="type-badge type-viewer">查看者</span> 查看者C
</div>
<div class="source-item allowed" draggable="true" data-role="editor" data-name="编辑者D">
<span class="type-badge type-editor">编辑者</span> 编辑者D
</div>
<div class="source-item restricted" draggable="true" data-role="admin" data-name="管理员E">
<span class="type-badge type-admin">管理员</span> 管理员E
</div>
</div>
<div class="target-panel">
<div class="panel-title">目标区域(仅限编辑者/查看者)</div>
<div class="drop-zone" id="restrictedZone">
<div class="zone-label">拖入允许的角色</div>
<div class="zone-items" id="zoneItems"></div>
</div>
</div>
</div>
<div class="message" id="message"></div>
<script>
const restrictedZone = document.getElementById('restrictedZone');
const zoneItems = document.getElementById('zoneItems');
const messageEl = document.getElementById('message');
let currentDragRole = null;
// 拖拽源
document.querySelectorAll('.source-item').forEach(item => {
item.addEventListener('dragstart', function(e) {
currentDragRole = this.dataset.role;
e.dataTransfer.setData('text/plain', JSON.stringify({
role: this.dataset.role,
name: this.dataset.name
}));
e.dataTransfer.effectAllowed = currentDragRole === 'admin' ? 'none' : 'copy';
});
item.addEventListener('dragend', function() {
currentDragRole = null;
restrictedZone.classList.remove('valid', 'invalid');
messageEl.className = 'message';
});
});
// 放置目标
restrictedZone.addEventListener('dragover', function(e) {
e.preventDefault();
if (currentDragRole === 'admin') {
e.dataTransfer.dropEffect = 'none';
this.classList.add('invalid');
this.classList.remove('valid');
} else {
e.dataTransfer.dropEffect = 'copy';
this.classList.add('valid');
this.classList.remove('invalid');
}
});
restrictedZone.addEventListener('dragleave', function() {
this.classList.remove('valid', 'invalid');
});
restrictedZone.addEventListener('drop', function(e) {
e.preventDefault();
this.classList.remove('valid', 'invalid');
const data = JSON.parse(e.dataTransfer.getData('text/plain'));
if (data.role === 'admin') {
showMessage('error', `不允许将管理员 "${data.name}" 放入此区域`);
return;
}
const item = document.createElement('div');
item.className = 'zone-item';
item.textContent = data.name + ' (' + data.role + ')';
zoneItems.appendChild(item);
showMessage('success', `成功添加 "${data.name}" 到区域`);
});
function showMessage(type, text) {
messageEl.className = 'message ' + type;
messageEl.textContent = text;
setTimeout(() => {
messageEl.className = 'message';
}, 3000);
}
</script>
</body>
</html>示例5:多项目拖拽与拖拽计数
代码示例
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>多项目拖拽与拖拽计数</title>
<style>
body {
font-family: "Microsoft YaHei", sans-serif;
padding: 20px;
background: #1a1a2e;
color: white;
}
.app {
max-width: 900px;
margin: 0 auto;
}
.task-board {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
}
.column {
background: #16213e;
border-radius: 12px;
padding: 16px;
min-height: 400px;
}
.column-header {
font-size: 16px;
font-weight: bold;
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 2px solid;
display: flex;
justify-content: space-between;
align-items: center;
}
.column:nth-child(1) .column-header { border-color: #f44336; }
.column:nth-child(2) .column-header { border-color: #FF9800; }
.column:nth-child(3) .column-header { border-color: #4CAF50; }
.column-count {
background: rgba(255,255,255,0.1);
padding: 2px 10px;
border-radius: 12px;
font-size: 13px;
}
.column.drag-over {
background: #1a2744;
outline: 2px dashed #667eea;
outline-offset: -2px;
}
.task-card {
padding: 12px;
background: #1e2a4a;
border-radius: 8px;
margin-bottom: 8px;
cursor: grab;
user-select: none;
transition: transform 0.15s, box-shadow 0.15s;
border-left: 3px solid #667eea;
}
.task-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
}
.task-card:active { cursor: grabbing; }
.task-card.dragging { opacity: 0.3; }
.task-title { font-size: 14px; margin-bottom: 6px; }
.task-meta {
font-size: 11px;
color: #888;
display: flex;
justify-content: space-between;
}
.priority-high { border-left-color: #f44336; }
.priority-medium { border-left-color: #FF9800; }
.priority-low { border-left-color: #4CAF50; }
</style>
</head>
<body>
<div class="app">
<h1>任务看板 - 拖拽排序</h1>
<div class="task-board" id="taskBoard">
<div class="column" data-column="todo">
<div class="column-header">
<span>待办</span>
<span class="column-count" id="count-todo">0</span>
</div>
<div class="task-list" id="list-todo"></div>
</div>
<div class="column" data-column="doing">
<div class="column-header">
<span>进行中</span>
<span class="column-count" id="count-doing">0</span>
</div>
<div class="task-list" id="list-doing"></div>
</div>
<div class="column" data-column="done">
<div class="column-header">
<span>已完成</span>
<span class="column-count" id="count-done">0</span>
</div>
<div class="task-list" id="list-done"></div>
</div>
</div>
</div>
<script>
const tasks = [
{ id: 1, title: '设计数据库架构', priority: 'high', column: 'todo' },
{ id: 2, title: '实现用户认证', priority: 'high', column: 'todo' },
{ id: 3, title: '编写 API 文档', priority: 'medium', column: 'todo' },
{ id: 4, title: '前端页面开发', priority: 'medium', column: 'doing' },
{ id: 5, title: '单元测试编写', priority: 'low', column: 'doing' },
{ id: 6, title: '项目初始化', priority: 'low', column: 'done' },
];
let draggedTaskId = null;
// 渲染所有任务
function renderTasks() {
['todo', 'doing', 'done'].forEach(column => {
const list = document.getElementById('list-' + column);
list.innerHTML = '';
const columnTasks = tasks.filter(t => t.column === column);
columnTasks.forEach(task => {
const card = document.createElement('div');
card.className = `task-card priority-${task.priority}`;
card.draggable = true;
card.dataset.taskId = task.id;
card.innerHTML = `
<div class="task-title">${task.title}</div>
<div class="task-meta">
<span>#${task.id}</span>
<span>${task.priority === 'high' ? '高' : task.priority === 'medium' ? '中' : '低'}</span>
</div>
`;
card.addEventListener('dragstart', function(e) {
draggedTaskId = parseInt(this.dataset.taskId);
e.dataTransfer.setData('text/plain', this.dataset.taskId);
e.dataTransfer.effectAllowed = 'move';
this.classList.add('dragging');
});
card.addEventListener('dragend', function() {
this.classList.remove('dragging');
draggedTaskId = null;
document.querySelectorAll('.column').forEach(c => c.classList.remove('drag-over'));
});
list.appendChild(card);
});
document.getElementById('count-' + column).textContent = columnTasks.length;
});
}
// 列的拖放事件
document.querySelectorAll('.column').forEach(column => {
column.addEventListener('dragover', function(e) {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
this.classList.add('drag-over');
});
column.addEventListener('dragleave', function(e) {
// 防止子元素触发 dragleave
if (!this.contains(e.relatedTarget)) {
this.classList.remove('drag-over');
}
});
column.addEventListener('drop', function(e) {
e.preventDefault();
this.classList.remove('drag-over');
const targetColumn = this.dataset.column;
const taskId = parseInt(e.dataTransfer.getData('text/plain'));
const task = tasks.find(t => t.id === taskId);
if (task && task.column !== targetColumn) {
task.column = targetColumn;
renderTasks();
}
});
});
renderTasks();
</script>
</body>
</html>浏览器兼容性
注意事项与最佳实践
1. 必须阻止 dragover 的默认行为
dragover 事件的默认行为是禁止放置,必须调用 e.preventDefault() 才能让元素成为有效的放置目标。
代码示例
dropTarget.addEventListener('dragover', function(e) {
e.preventDefault(); // 必须!
});2. 设置正确的 effectAllowed 和 dropEffect
这两个属性控制拖拽的光标样式和视觉效果,应保持一致。
代码示例
// 拖拽源
element.addEventListener('dragstart', function(e) {
e.dataTransfer.effectAllowed = 'move';
});
// 放置目标
dropTarget.addEventListener('dragover', function(e) {
e.preventDefault();
e.dataTransfer.dropEffect = 'move'; // 与 effectAllowed 一致
});3. 触摸设备兼容性
HTML5 拖放 API 在触摸设备上支持有限,需要使用 polyfill 或替代方案。
代码示例
// 检测触摸设备,使用替代方案
if ('ontouchstart' in window) {
// 使用 touch 事件实现自定义拖拽
// 或引入 polyfill 如 drag-drop-polyfill
}4. 避免在 drag 事件中执行耗时操作
drag 事件在拖拽过程中高频触发,类似于 scroll 事件,应避免在其中执行耗时操作。
5. 使用 setData 传递数据而非全局变量
始终使用 dataTransfer.setData() 传递数据,而非依赖全局变量。这确保了拖拽操作的数据完整性,也支持跨窗口拖放。
6. 处理 dragleave 的子元素问题
dragleave 会在鼠标移入子元素时触发,使用 contains() 检查 relatedTarget 来过滤。
代码规范示例
规范的拖拽管理器
代码示例
class DragManager {
constructor(options = {}) {
this.onDragStart = options.onDragStart || null;
this.onDragEnd = options.onDragEnd || null;
this.onDrop = options.onDrop || null;
this.effectAllowed = options.effectAllowed || 'copy';
this.dataFormat = options.dataFormat || 'text/plain';
}
makeDraggable(element, data) {
element.draggable = true;
element.setAttribute('data-draggable', 'true');
element.addEventListener('dragstart', (e) => {
e.dataTransfer.setData(this.dataFormat, JSON.stringify(data));
e.dataTransfer.effectAllowed = this.effectAllowed;
if (this.onDragStart) {
this.onDragStart(e, element, data);
}
});
element.addEventListener('dragend', (e) => {
if (this.onDragEnd) {
this.onDragEnd(e, element);
}
});
}
makeDropTarget(element) {
element.addEventListener('dragover', (e) => {
e.preventDefault();
e.dataTransfer.dropEffect = this.effectAllowed;
element.classList.add('drag-over');
});
element.addEventListener('dragleave', (e) => {
if (!element.contains(e.relatedTarget)) {
element.classList.remove('drag-over');
}
});
element.addEventListener('drop', (e) => {
e.preventDefault();
element.classList.remove('drag-over');
const data = e.dataTransfer.getData(this.dataFormat);
if (data && this.onDrop) {
this.onDrop(e, element, JSON.parse(data));
}
});
}
}常见问题与解决方案
问题1:drop 事件不触发
原因:没有在 dragover 事件中调用 e.preventDefault()。
解决方案:确保在放置目标的 dragover 处理程序中调用 e.preventDefault()。
问题2:Firefox 中拖拽需要 setData
原因:Firefox 要求在 dragstart 中调用 setData(),否则拖拽不会启动。
解决方案:始终在 dragstart 中设置数据。
代码示例
element.addEventListener('dragstart', function(e) {
e.dataTransfer.setData('text/plain', ''); // 即使不需要数据也要调用
});问题3:拖拽图像在 IE 中不显示
原因:IE 不支持 setDragImage() 方法。
解决方案:检测支持并提供回退方案。
代码示例
if (e.dataTransfer.setDragImage) {
e.dataTransfer.setDragImage(customImage, offsetX, offsetY);
}
// IE 中使用默认拖拽图像问题4:拖拽文本选中问题
原因:拖拽操作可能与文本选中冲突。
解决方案:对可拖拽元素设置 user-select: none。
代码示例
[draggable="true"] {
user-select: none;
-webkit-user-select: none;
}问题5:Safari 中 draggable 不生效
原因:Safari 对 draggable 属性的处理与其他浏览器不同,某些元素(如 和 )默认可拖拽,其他元素需要明确设置。
解决方案:始终显式设置 draggable="true",并为 dragstart 事件添加处理程序。
总结
ondrag 事件系列是 HTML5 拖放 API 的核心,通过 dragstart、drag、dragend 三个事件控制拖拽源的行为。以下是核心要点:
事件系列:理解 dragstart/drag/dragend 在拖拽源上的触发时机和用途。
dataTransfer:使用
setData/getData传递数据,支持多种 MIME 格式。视觉效果:通过
effectAllowed和dropEffect控制拖拽光标和视觉反馈。自定义图像:使用
setDragImage()创建自定义的拖拽预览图像。拖拽限制:通过条件判断控制哪些元素可以拖拽、哪些区域可以放置。
必须阻止默认行为:放置目标必须在
dragover中调用preventDefault()。触摸设备:HTML5 拖放 API 在触摸设备上支持有限,需要 polyfill 或自定义实现。
跨浏览器兼容:注意 Firefox 需要
setData(),IE 不支持setDragImage()。
掌握拖拽事件的正确使用方式,能够构建流畅、直观的拖拽交互体验。
常见问题
6. 处理 dragleave 的子元素问题?
dragleave 会在鼠标移入子元素时触发,使用 contains() 检查 relatedTarget 来过滤。
问题1:drop 事件不触发?
没有在 dragover 事件中调用 e.preventDefault()。 确保在放置目标的 dragover 处理程序中调用 e.preventDefault()。
问题2:Firefox 中拖拽需要 setData?
Firefox 要求在 dragstart 中调用 setData(),否则拖拽不会启动。 始终在 dragstart 中设置数据。
问题3:拖拽图像在 IE 中不显示?
IE 不支持 setDragImage() 方法。 检测支持并提供回退方案。
问题4:拖拽文本选中问题?
拖拽操作可能与文本选中冲突。 对可拖拽元素设置 user-select: none。
本文由小确幸生活整理发布,转载请注明出处
本文涉及AI创作
内容由AI创作,请仔细甄别