拖放API(Drag and Drop API)是HTML5提供的接口,允许用户通过拖拽的方式移动元素或数据。这个API使得Web应用可以实现类似桌面应用的拖放交互体验。
东巴文(db-w.cn) 认为:拖放API让Web交互更加直观和自然,大大提升了用户体验。
| 特点 | 说明 |
|---|---|
| 原生支持 | HTML5原生API,无需额外库 |
| 跨元素 | 可以在不同元素间拖放 |
| 跨窗口 | 支持跨窗口拖放 |
| 数据传递 | 支持多种数据类型传递 |
拖动源元素 → 拖动过程 → 放置目标元素
↓ ↓ ↓
dragstart drag dragenter
drag dragover dragover
dragend drop drop
<!-- 设置元素可拖动 -->
<div draggable="true">我可以被拖动</div>
<!-- 默认可拖动的元素 -->
<img src="image.jpg" alt="图片默认可拖动">
<a href="url">链接默认可拖动</a>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>基本拖放示例</title>
<style>
.draggable {
width: 100px;
height: 100px;
background: #667eea;
color: white;
display: flex;
align-items: center;
justify-content: center;
cursor: move;
margin: 10px;
border-radius: 5px;
}
.dropzone {
width: 200px;
height: 200px;
border: 2px dashed #ccc;
display: flex;
align-items: center;
justify-content: center;
margin: 10px;
border-radius: 5px;
}
.dropzone.dragover {
border-color: #667eea;
background: rgba(102, 126, 234, 0.1);
}
</style>
</head>
<body>
<div id="draggable" class="draggable" draggable="true">
拖动我
</div>
<div id="dropzone" class="dropzone">
放置区域
</div>
<script>
const draggable = document.getElementById('draggable');
const dropzone = document.getElementById('dropzone');
// 拖动开始
draggable.addEventListener('dragstart', function(e) {
console.log('dragstart');
// 设置拖动数据
e.dataTransfer.setData('text/plain', e.target.id);
// 设置拖动效果
e.dataTransfer.effectAllowed = 'move';
// 设置拖动图像(可选)
// e.dataTransfer.setDragImage(image, xOffset, yOffset);
});
// 拖动过程
draggable.addEventListener('drag', function(e) {
console.log('drag');
});
// 拖动结束
draggable.addEventListener('dragend', function(e) {
console.log('dragend');
});
// 进入放置区域
dropzone.addEventListener('dragenter', function(e) {
console.log('dragenter');
e.preventDefault();
this.classList.add('dragover');
});
// 在放置区域上方
dropzone.addEventListener('dragover', function(e) {
console.log('dragover');
e.preventDefault(); // 必须阻止默认行为
e.dataTransfer.dropEffect = 'move';
});
// 离开放置区域
dropzone.addEventListener('dragleave', function(e) {
console.log('dragleave');
this.classList.remove('dragover');
});
// 放置
dropzone.addEventListener('drop', function(e) {
console.log('drop');
e.preventDefault();
this.classList.remove('dragover');
// 获取拖动数据
const id = e.dataTransfer.getData('text/plain');
const element = document.getElementById(id);
// 移动元素
this.appendChild(element);
});
</script>
</body>
</html>
| 事件 | 触发时机 | 说明 |
|---|---|---|
dragstart |
开始拖动 | 设置拖动数据和效果 |
drag |
拖动过程中 | 持续触发(高频) |
dragend |
拖动结束 | 清理工作 |
| 事件 | 触发时机 | 说明 |
|---|---|---|
dragenter |
进入目标 | 判断是否可放置 |
dragover |
在目标上方 | 必须阻止默认行为 |
dragleave |
离开目标 | 移除视觉反馈 |
drop |
放置 | 处理放置逻辑 |
东巴文点评:dragover事件必须调用e.preventDefault(),否则无法触发drop事件。
element.addEventListener('dragstart', function(e) {
const dt = e.dataTransfer;
// 设置拖动效果
dt.effectAllowed = 'move'; // none, copy, move, link, copyMove, copyLink, moveLink, all
// 设置数据
dt.setData('text/plain', '文本数据');
dt.setData('text/uri-list', 'http://example.com');
dt.setData('application/json', JSON.stringify({ id: 1 }));
// 设置拖动图像
const img = new Image();
img.src = 'drag-image.png';
dt.setDragImage(img, 0, 0);
});
dropzone.addEventListener('dragover', function(e) {
const dt = e.dataTransfer;
// 获取拖动效果
console.log(dt.effectAllowed);
// 设置放置效果
dt.dropEffect = 'move'; // none, copy, move, link
// 获取数据类型
console.log(dt.types);
// 获取文件列表
console.log(dt.files);
});
dropzone.addEventListener('drop', function(e) {
const dt = e.dataTransfer;
// 获取数据
const text = dt.getData('text/plain');
const url = dt.getData('text/uri-list');
const json = dt.getData('application/json');
// 获取所有数据
dt.types.forEach(type => {
console.log(type, dt.getData(type));
});
});
| 方法 | 说明 |
|---|---|
setData(format, data) |
设置拖动数据 |
getData(format) |
获取拖动数据 |
clearData([format]) |
清除数据 |
setDragImage(img, x, y) |
设置拖动图像 |
// 拖动源设置允许的效果
draggable.addEventListener('dragstart', function(e) {
e.dataTransfer.effectAllowed = 'copy'; // 只允许复制
});
// 放置目标设置实际效果
dropzone.addEventListener('dragover', function(e) {
e.preventDefault();
// 根据effectAllowed设置dropEffect
if (e.dataTransfer.effectAllowed === 'copy') {
e.dataTransfer.dropEffect = 'copy';
}
});
| effectAllowed值 | 说明 |
|---|---|
none |
不允许任何操作 |
copy |
复制 |
move |
移动 |
link |
链接 |
copyMove |
复制或移动 |
copyLink |
复制或链接 |
moveLink |
移动或链接 |
all |
所有操作 |
东巴文点评:effectAllowed和dropEffect用于控制拖放操作的视觉反馈,应该保持一致。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>文件拖放上传</title>
<style>
.dropzone {
width: 400px;
height: 200px;
border: 2px dashed #ccc;
border-radius: 10px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin: 20px;
transition: all 0.3s;
}
.dropzone.dragover {
border-color: #667eea;
background: rgba(102, 126, 234, 0.1);
transform: scale(1.02);
}
.dropzone-icon {
font-size: 48px;
color: #ccc;
margin-bottom: 10px;
}
.dropzone-text {
color: #666;
font-size: 16px;
}
.file-list {
margin: 20px;
}
.file-item {
display: flex;
align-items: center;
padding: 10px;
margin: 5px 0;
background: #f5f5f5;
border-radius: 5px;
}
.file-icon {
font-size: 24px;
margin-right: 10px;
}
.file-info {
flex: 1;
}
.file-name {
font-weight: bold;
}
.file-size {
font-size: 12px;
color: #999;
}
.file-progress {
width: 100px;
height: 5px;
background: #e0e0e0;
border-radius: 3px;
overflow: hidden;
}
.file-progress-bar {
height: 100%;
background: #667eea;
transition: width 0.3s;
}
</style>
</head>
<body>
<div id="dropzone" class="dropzone">
<div class="dropzone-icon">📁</div>
<div class="dropzone-text">拖放文件到这里上传</div>
<div class="dropzone-text" style="font-size: 12px; color: #999; margin-top: 5px;">
或点击选择文件
</div>
<input type="file" id="fileInput" multiple style="display: none;">
</div>
<div id="fileList" class="file-list"></div>
<script>
const dropzone = document.getElementById('dropzone');
const fileInput = document.getElementById('fileInput');
const fileList = document.getElementById('fileList');
// 点击选择文件
dropzone.addEventListener('click', function() {
fileInput.click();
});
fileInput.addEventListener('change', function(e) {
handleFiles(e.target.files);
});
// 拖放事件
dropzone.addEventListener('dragenter', function(e) {
e.preventDefault();
this.classList.add('dragover');
});
dropzone.addEventListener('dragover', function(e) {
e.preventDefault();
e.dataTransfer.dropEffect = 'copy';
});
dropzone.addEventListener('dragleave', function(e) {
e.preventDefault();
this.classList.remove('dragover');
});
dropzone.addEventListener('drop', function(e) {
e.preventDefault();
this.classList.remove('dragover');
const files = e.dataTransfer.files;
handleFiles(files);
});
// 处理文件
function handleFiles(files) {
Array.from(files).forEach(file => {
addFileToList(file);
uploadFile(file);
});
}
// 添加文件到列表
function addFileToList(file) {
const item = document.createElement('div');
item.className = 'file-item';
item.id = `file-${Date.now()}-${Math.random()}`;
const icon = getFileIcon(file.type);
const size = formatFileSize(file.size);
item.innerHTML = `
<div class="file-icon">${icon}</div>
<div class="file-info">
<div class="file-name">${file.name}</div>
<div class="file-size">${size}</div>
</div>
<div class="file-progress">
<div class="file-progress-bar" style="width: 0%"></div>
</div>
`;
fileList.appendChild(item);
}
// 上传文件(模拟)
function uploadFile(file) {
const formData = new FormData();
formData.append('file', file);
// 模拟上传进度
let progress = 0;
const interval = setInterval(() => {
progress += 10;
const progressBar = document.querySelector('.file-progress-bar');
if (progressBar) {
progressBar.style.width = `${progress}%`;
}
if (progress >= 100) {
clearInterval(interval);
console.log(`${file.name} 上传完成`);
}
}, 200);
// 实际上传代码
// fetch('/upload', {
// method: 'POST',
// body: formData
// });
}
// 获取文件图标
function getFileIcon(type) {
if (type.startsWith('image/')) return '🖼️';
if (type.startsWith('video/')) return '🎬';
if (type.startsWith('audio/')) return '🎵';
if (type.includes('pdf')) return '📄';
if (type.includes('word') || type.includes('document')) return '📝';
if (type.includes('excel') || type.includes('sheet')) return '📊';
if (type.includes('zip') || type.includes('rar')) return '📦';
return '📁';
}
// 格式化文件大小
function formatFileSize(bytes) {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
</script>
</body>
</html>
东巴文点评:文件拖放是拖放API最常见的应用场景,应该提供清晰的视觉反馈和进度提示。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>拖放排序</title>
<style>
.sortable-list {
list-style: none;
padding: 0;
max-width: 400px;
}
.sortable-item {
padding: 15px;
margin: 5px 0;
background: white;
border: 1px solid #ddd;
border-radius: 5px;
cursor: move;
display: flex;
align-items: center;
transition: all 0.3s;
}
.sortable-item:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.sortable-item.dragging {
opacity: 0.5;
background: #f0f0f0;
}
.sortable-item.drag-over {
border-top: 2px solid #667eea;
}
.item-handle {
margin-right: 10px;
color: #999;
}
.item-content {
flex: 1;
}
</style>
</head>
<body>
<h2>拖放排序列表</h2>
<ul id="sortableList" class="sortable-list">
<li class="sortable-item" draggable="true" data-id="1">
<span class="item-handle">☰</span>
<span class="item-content">项目 1</span>
</li>
<li class="sortable-item" draggable="true" data-id="2">
<span class="item-handle">☰</span>
<span class="item-content">项目 2</span>
</li>
<li class="sortable-item" draggable="true" data-id="3">
<span class="item-handle">☰</span>
<span class="item-content">项目 3</span>
</li>
<li class="sortable-item" draggable="true" data-id="4">
<span class="item-handle">☰</span>
<span class="item-content">项目 4</span>
</li>
<li class="sortable-item" draggable="true" data-id="5">
<span class="item-handle">☰</span>
<span class="item-content">项目 5</span>
</li>
</ul>
<script>
const list = document.getElementById('sortableList');
let draggedItem = null;
list.addEventListener('dragstart', function(e) {
if (e.target.classList.contains('sortable-item')) {
draggedItem = e.target;
e.target.classList.add('dragging');
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/plain', e.target.dataset.id);
}
});
list.addEventListener('dragend', function(e) {
if (e.target.classList.contains('sortable-item')) {
e.target.classList.remove('dragging');
// 移除所有drag-over类
document.querySelectorAll('.sortable-item').forEach(item => {
item.classList.remove('drag-over');
});
draggedItem = null;
}
});
list.addEventListener('dragover', function(e) {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
const target = e.target.closest('.sortable-item');
if (target && target !== draggedItem) {
// 移除所有drag-over类
document.querySelectorAll('.sortable-item').forEach(item => {
item.classList.remove('drag-over');
});
// 添加drag-over类到目标元素
target.classList.add('drag-over');
}
});
list.addEventListener('drop', function(e) {
e.preventDefault();
const target = e.target.closest('.sortable-item');
if (target && target !== draggedItem) {
// 获取所有列表项
const items = Array.from(list.querySelectorAll('.sortable-item'));
const draggedIndex = items.indexOf(draggedItem);
const targetIndex = items.indexOf(target);
// 重新排序
if (draggedIndex < targetIndex) {
target.parentNode.insertBefore(draggedItem, target.nextSibling);
} else {
target.parentNode.insertBefore(draggedItem, target);
}
// 保存排序
saveOrder();
}
});
// 保存排序
function saveOrder() {
const items = list.querySelectorAll('.sortable-item');
const order = Array.from(items).map(item => item.dataset.id);
console.log('新顺序:', order);
// 发送到服务器
// fetch('/api/reorder', {
// method: 'POST',
// headers: { 'Content-Type': 'application/json' },
// body: JSON.stringify({ order })
// });
}
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>拖放API综合示例 - 东巴文</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
max-width: 1200px;
margin: 0 auto;
padding: 20px;
background: #f5f5f5;
}
h1 {
text-align: center;
color: #333;
}
.section {
margin: 20px 0;
padding: 20px;
background: white;
border-radius: 10px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.section h2 {
margin-top: 0;
color: #667eea;
}
/* 看板样式 */
.kanban {
display: flex;
gap: 20px;
overflow-x: auto;
}
.kanban-column {
flex: 1;
min-width: 250px;
background: #f9f9f9;
border-radius: 8px;
padding: 15px;
}
.kanban-column h3 {
margin: 0 0 15px 0;
padding-bottom: 10px;
border-bottom: 2px solid #667eea;
}
.kanban-card {
background: white;
padding: 15px;
margin: 10px 0;
border-radius: 5px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
cursor: move;
transition: all 0.3s;
}
.kanban-card:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
transform: translateY(-2px);
}
.kanban-card.dragging {
opacity: 0.5;
}
.kanban-column.drag-over {
background: rgba(102, 126, 234, 0.1);
}
.card-title {
font-weight: bold;
margin-bottom: 8px;
}
.card-description {
font-size: 14px;
color: #666;
margin-bottom: 10px;
}
.card-tags {
display: flex;
gap: 5px;
flex-wrap: wrap;
}
.tag {
padding: 2px 8px;
border-radius: 3px;
font-size: 12px;
color: white;
}
.tag.priority-high { background: #e74c3c; }
.tag.priority-medium { background: #f39c12; }
.tag.priority-low { background: #27ae60; }
/* 文件上传样式 */
.upload-area {
border: 2px dashed #ccc;
border-radius: 10px;
padding: 40px;
text-align: center;
transition: all 0.3s;
}
.upload-area.drag-over {
border-color: #667eea;
background: rgba(102, 126, 234, 0.05);
}
.upload-icon {
font-size: 48px;
color: #ccc;
}
.upload-text {
margin: 10px 0;
color: #666;
}
.preview-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 15px;
margin-top: 20px;
}
.preview-item {
position: relative;
aspect-ratio: 1;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.preview-item img {
width: 100%;
height: 100%;
object-fit: cover;
}
.preview-item .remove-btn {
position: absolute;
top: 5px;
right: 5px;
width: 24px;
height: 24px;
background: rgba(0, 0, 0, 0.6);
color: white;
border: none;
border-radius: 50%;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
}
/* 购物车样式 */
.shop-container {
display: flex;
gap: 20px;
}
.products {
flex: 2;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 15px;
}
.product {
background: white;
padding: 15px;
border-radius: 8px;
text-align: center;
cursor: move;
transition: all 0.3s;
}
.product:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.product.dragging {
opacity: 0.5;
}
.product-icon {
font-size: 48px;
margin-bottom: 10px;
}
.product-name {
font-weight: bold;
margin-bottom: 5px;
}
.product-price {
color: #667eea;
font-weight: bold;
}
.cart {
flex: 1;
background: #f9f9f9;
border-radius: 8px;
padding: 20px;
min-height: 300px;
}
.cart.drag-over {
background: rgba(102, 126, 234, 0.1);
}
.cart h3 {
margin-top: 0;
padding-bottom: 10px;
border-bottom: 2px solid #667eea;
}
.cart-item {
display: flex;
align-items: center;
padding: 10px;
margin: 5px 0;
background: white;
border-radius: 5px;
}
.cart-item-icon {
font-size: 24px;
margin-right: 10px;
}
.cart-item-info {
flex: 1;
}
.cart-total {
margin-top: 15px;
padding-top: 15px;
border-top: 1px solid #ddd;
font-size: 18px;
font-weight: bold;
text-align: right;
}
</style>
</head>
<body>
<h1>拖放API综合示例</h1>
<!-- 看板示例 -->
<div class="section">
<h2>📋 任务看板</h2>
<div class="kanban" id="kanban">
<div class="kanban-column" data-status="todo">
<h3>待办</h3>
<div class="kanban-card" draggable="true" data-id="1">
<div class="card-title">设计首页</div>
<div class="card-description">完成首页UI设计稿</div>
<div class="card-tags">
<span class="tag priority-high">高优先级</span>
</div>
</div>
<div class="kanban-card" draggable="true" data-id="2">
<div class="card-title">编写文档</div>
<div class="card-description">编写API接口文档</div>
<div class="card-tags">
<span class="tag priority-medium">中优先级</span>
</div>
</div>
</div>
<div class="kanban-column" data-status="doing">
<h3>进行中</h3>
<div class="kanban-card" draggable="true" data-id="3">
<div class="card-title">开发登录功能</div>
<div class="card-description">实现用户登录模块</div>
<div class="card-tags">
<span class="tag priority-high">高优先级</span>
</div>
</div>
</div>
<div class="kanban-column" data-status="done">
<h3>已完成</h3>
<div class="kanban-card" draggable="true" data-id="4">
<div class="card-title">需求分析</div>
<div class="card-description">完成需求分析文档</div>
<div class="card-tags">
<span class="tag priority-low">低优先级</span>
</div>
</div>
</div>
</div>
</div>
<!-- 文件上传示例 -->
<div class="section">
<h2>📁 文件拖放上传</h2>
<div class="upload-area" id="uploadArea">
<div class="upload-icon">📤</div>
<div class="upload-text">拖放图片到这里上传</div>
<div class="upload-text" style="font-size: 12px; color: #999;">
支持 JPG、PNG、GIF 格式
</div>
</div>
<div class="preview-grid" id="previewGrid"></div>
</div>
<!-- 购物车示例 -->
<div class="section">
<h2>🛒 拖放购物车</h2>
<div class="shop-container">
<div class="products" id="products">
<div class="product" draggable="true" data-id="1" data-name="苹果" data-price="5">
<div class="product-icon">🍎</div>
<div class="product-name">苹果</div>
<div class="product-price">¥5</div>
</div>
<div class="product" draggable="true" data-id="2" data-name="香蕉" data-price="3">
<div class="product-icon">🍌</div>
<div class="product-name">香蕉</div>
<div class="product-price">¥3</div>
</div>
<div class="product" draggable="true" data-id="3" data-name="橙子" data-price="4">
<div class="product-icon">🍊</div>
<div class="product-name">橙子</div>
<div class="product-price">¥4</div>
</div>
<div class="product" draggable="true" data-id="4" data-name="葡萄" data-price="8">
<div class="product-icon">🍇</div>
<div class="product-name">葡萄</div>
<div class="product-price">¥8</div>
</div>
</div>
<div class="cart" id="cart">
<h3>购物车</h3>
<div id="cartItems"></div>
<div class="cart-total" id="cartTotal">总计: ¥0</div>
</div>
</div>
</div>
<script>
// ========== 看板拖放 ==========
const kanban = document.getElementById('kanban');
let draggedCard = null;
kanban.addEventListener('dragstart', function(e) {
if (e.target.classList.contains('kanban-card')) {
draggedCard = e.target;
e.target.classList.add('dragging');
e.dataTransfer.effectAllowed = 'move';
}
});
kanban.addEventListener('dragend', function(e) {
if (e.target.classList.contains('kanban-card')) {
e.target.classList.remove('dragging');
draggedCard = null;
document.querySelectorAll('.kanban-column').forEach(col => {
col.classList.remove('drag-over');
});
}
});
kanban.addEventListener('dragover', function(e) {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
const column = e.target.closest('.kanban-column');
if (column) {
column.classList.add('drag-over');
}
});
kanban.addEventListener('dragleave', function(e) {
const column = e.target.closest('.kanban-column');
if (column && !column.contains(e.relatedTarget)) {
column.classList.remove('drag-over');
}
});
kanban.addEventListener('drop', function(e) {
e.preventDefault();
const column = e.target.closest('.kanban-column');
if (column && draggedCard) {
column.appendChild(draggedCard);
column.classList.remove('drag-over');
console.log(`任务 ${draggedCard.dataset.id} 移动到 ${column.dataset.status}`);
}
});
// ========== 文件上传 ==========
const uploadArea = document.getElementById('uploadArea');
const previewGrid = document.getElementById('previewGrid');
uploadArea.addEventListener('dragenter', function(e) {
e.preventDefault();
this.classList.add('drag-over');
});
uploadArea.addEventListener('dragover', function(e) {
e.preventDefault();
e.dataTransfer.dropEffect = 'copy';
});
uploadArea.addEventListener('dragleave', function(e) {
e.preventDefault();
this.classList.remove('drag-over');
});
uploadArea.addEventListener('drop', function(e) {
e.preventDefault();
this.classList.remove('drag-over');
const files = e.dataTransfer.files;
handleImageFiles(files);
});
function handleImageFiles(files) {
Array.from(files).forEach(file => {
if (!file.type.startsWith('image/')) {
alert('只支持图片文件');
return;
}
const reader = new FileReader();
reader.onload = function(e) {
const preview = document.createElement('div');
preview.className = 'preview-item';
preview.innerHTML = `
<img src="${e.target.result}" alt="${file.name}">
<button class="remove-btn" onclick="this.parentElement.remove()">×</button>
`;
previewGrid.appendChild(preview);
};
reader.readAsDataURL(file);
});
}
// ========== 购物车 ==========
const products = document.getElementById('products');
const cart = document.getElementById('cart');
const cartItems = document.getElementById('cartItems');
const cartTotal = document.getElementById('cartTotal');
let cartData = [];
let draggedProduct = null;
products.addEventListener('dragstart', function(e) {
if (e.target.classList.contains('product')) {
draggedProduct = e.target;
e.target.classList.add('dragging');
e.dataTransfer.effectAllowed = 'copy';
}
});
products.addEventListener('dragend', function(e) {
if (e.target.classList.contains('product')) {
e.target.classList.remove('dragging');
draggedProduct = null;
}
});
cart.addEventListener('dragover', function(e) {
e.preventDefault();
e.dataTransfer.dropEffect = 'copy';
this.classList.add('drag-over');
});
cart.addEventListener('dragleave', function(e) {
this.classList.remove('drag-over');
});
cart.addEventListener('drop', function(e) {
e.preventDefault();
this.classList.remove('drag-over');
if (draggedProduct) {
const id = draggedProduct.dataset.id;
const name = draggedProduct.dataset.name;
const price = parseFloat(draggedProduct.dataset.price);
// 添加到购物车
const existingItem = cartData.find(item => item.id === id);
if (existingItem) {
existingItem.quantity++;
} else {
cartData.push({ id, name, price, quantity: 1 });
}
updateCartDisplay();
}
});
function updateCartDisplay() {
cartItems.innerHTML = cartData.map(item => `
<div class="cart-item">
<span class="cart-item-icon">${getProductIcon(item.name)}</span>
<div class="cart-item-info">
<div>${item.name} × ${item.quantity}</div>
<div style="color: #667eea;">¥${item.price * item.quantity}</div>
</div>
</div>
`).join('');
const total = cartData.reduce((sum, item) => sum + item.price * item.quantity, 0);
cartTotal.textContent = `总计: ¥${total}`;
}
function getProductIcon(name) {
const icons = {
'苹果': '🍎',
'香蕉': '🍌',
'橙子': '🍊',
'葡萄': '🍇'
};
return icons[name] || '📦';
}
</script>
</body>
</html>
// 推荐:提供清晰的视觉反馈
element.addEventListener('dragenter', function(e) {
this.classList.add('drag-over');
});
element.addEventListener('dragleave', function(e) {
this.classList.remove('drag-over');
});
// 不推荐:没有视觉反馈
element.addEventListener('dragover', function(e) {
e.preventDefault();
});
// 推荐:自定义拖动图像
element.addEventListener('dragstart', function(e) {
const img = new Image();
img.src = 'drag-preview.png';
e.dataTransfer.setDragImage(img, 0, 0);
});
// 不推荐:使用默认拖动图像
element.addEventListener('dragstart', function(e) {
// 没有设置拖动图像
});
// 推荐:检查文件类型
dropzone.addEventListener('drop', function(e) {
const files = e.dataTransfer.files;
Array.from(files).forEach(file => {
if (file.type.startsWith('image/')) {
processImage(file);
} else {
alert('只支持图片文件');
}
});
});
// 不推荐:不检查文件类型
dropzone.addEventListener('drop', function(e) {
const files = e.dataTransfer.files;
processFiles(files); // 可能处理了不支持的文件类型
});
东巴文点评:良好的用户体验需要提供清晰的视觉反馈和友好的错误提示。
问题1:以下哪个事件必须调用preventDefault()才能触发drop事件?
A. dragstart
B. dragenter
C. dragover
D. dragend
答案:C
东巴文解释:dragover事件必须调用e.preventDefault(),否则无法触发drop事件。这是因为默认情况下,大多数元素不允许放置。
问题2:dataTransfer.effectAllowed的作用是?
A. 设置放置效果 B. 设置允许的拖动效果 C. 设置拖动数据 D. 设置拖动图像
<details> <summary>点击查看答案</summary>答案:B
东巴文解释:effectAllowed用于设置拖动源允许的操作类型(如copy、move、link),而dropEffect用于设置放置目标的实际操作效果。
任务:创建一个待办事项列表,支持拖放排序和拖放到垃圾桶删除。
<details> <summary>点击查看参考答案</summary><!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: Arial, sans-serif;
max-width: 600px;
margin: 50px auto;
padding: 20px;
}
.todo-list {
list-style: none;
padding: 0;
}
.todo-item {
padding: 15px;
margin: 5px 0;
background: white;
border: 1px solid #ddd;
border-radius: 5px;
cursor: move;
display: flex;
align-items: center;
transition: all 0.3s;
}
.todo-item:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.todo-item.dragging {
opacity: 0.5;
}
.todo-item.drag-over {
border-top: 2px solid #667eea;
}
.todo-text {
flex: 1;
}
.trash {
margin-top: 20px;
padding: 30px;
border: 2px dashed #e74c3c;
border-radius: 10px;
text-align: center;
color: #e74c3c;
transition: all 0.3s;
}
.trash.drag-over {
background: rgba(231, 76, 60, 0.1);
border-color: #c0392b;
}
.trash-icon {
font-size: 48px;
}
.add-form {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
.add-form input {
flex: 1;
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
}
.add-form button {
padding: 10px 20px;
background: #667eea;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
}
</style>
</head>
<body>
<h1>待办事项拖放管理</h1>
<div class="add-form">
<input type="text" id="todoInput" placeholder="添加新任务...">
<button onclick="addTodo()">添加</button>
</div>
<ul id="todoList" class="todo-list"></ul>
<div id="trash" class="trash">
<div class="trash-icon">🗑️</div>
<div>拖放到这里删除</div>
</div>
<script>
const todoList = document.getElementById('todoList');
const trash = document.getElementById('trash');
const todoInput = document.getElementById('todoInput');
let draggedItem = null;
let todos = [
{ id: 1, text: '学习HTML5拖放API' },
{ id: 2, text: '完成项目文档' },
{ id: 3, text: '代码审查' }
];
// 渲染列表
function renderTodos() {
todoList.innerHTML = todos.map(todo => `
<li class="todo-item" draggable="true" data-id="${todo.id}">
<span class="todo-text">${todo.text}</span>
</li>
`).join('');
}
// 添加待办
function addTodo() {
const text = todoInput.value.trim();
if (text) {
todos.push({
id: Date.now(),
text
});
todoInput.value = '';
renderTodos();
}
}
// 回车添加
todoInput.addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
addTodo();
}
});
// 列表拖放事件
todoList.addEventListener('dragstart', function(e) {
if (e.target.classList.contains('todo-item')) {
draggedItem = e.target;
e.target.classList.add('dragging');
}
});
todoList.addEventListener('dragend', function(e) {
if (e.target.classList.contains('todo-item')) {
e.target.classList.remove('dragging');
draggedItem = null;
document.querySelectorAll('.todo-item').forEach(item => {
item.classList.remove('drag-over');
});
}
});
todoList.addEventListener('dragover', function(e) {
e.preventDefault();
const target = e.target.closest('.todo-item');
if (target && target !== draggedItem) {
document.querySelectorAll('.todo-item').forEach(item => {
item.classList.remove('drag-over');
});
target.classList.add('drag-over');
}
});
todoList.addEventListener('drop', function(e) {
e.preventDefault();
const target = e.target.closest('.todo-item');
if (target && target !== draggedItem) {
const items = Array.from(todoList.querySelectorAll('.todo-item'));
const draggedIndex = items.indexOf(draggedItem);
const targetIndex = items.indexOf(target);
// 更新数据
const [removed] = todos.splice(draggedIndex, 1);
todos.splice(targetIndex, 0, removed);
renderTodos();
}
});
// 垃圾桶拖放事件
trash.addEventListener('dragover', function(e) {
e.preventDefault();
this.classList.add('drag-over');
});
trash.addEventListener('dragleave', function(e) {
this.classList.remove('drag-over');
});
trash.addEventListener('drop', function(e) {
e.preventDefault();
this.classList.remove('drag-over');
if (draggedItem) {
const id = parseInt(draggedItem.dataset.id);
todos = todos.filter(todo => todo.id !== id);
renderTodos();
}
});
// 初始渲染
renderTodos();
</script>
</body>
</html>
</details>
东巴文(db-w.cn) - 让编程学习更有趣、更高效!