拖放API中级

拖放API概述

拖放API(Drag and Drop API)是HTML5提供的原生接口,允许用户通过拖拽操作在页面元素之间移动数据。这个API让Web应用具备了类似桌面应用的拖放交互能力。

东巴文(db-w.cn) 认为:拖放API让Web交互更加直观和自然,是实现富交互应用的重要工具。

拖放API特点

核心特点

特点 说明
原生支持 HTML5原生API,无需插件
跨元素拖放 支持不同元素间拖放
数据传递 通过DataTransfer传递数据
视觉反馈 支持自定义拖放图像和效果

拖放事件

// 拖放事件列表
const dragEvents = {
    // 拖拽元素事件
    drag: '拖拽过程中持续触发',
    dragstart: '开始拖拽时触发',
    dragend: '拖拽结束时触发',
    
    // 放置目标事件
    dragenter: '拖拽元素进入目标时触发',
    dragover: '拖拽元素在目标上移动时持续触发',
    dragleave: '拖拽元素离开目标时触发',
    drop: '放置时触发'
};

基本拖放

设置可拖拽

<!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;
            border-radius: 5px;
            margin: 10px;
        }
        
        .dropzone {
            width: 300px;
            height: 200px;
            border: 2px dashed #ccc;
            border-radius: 5px;
            display: flex;
            align-items: center;
            justify-content: center;
            margin: 20px;
        }
        
        .dropzone.dragover {
            border-color: #667eea;
            background: rgba(102, 126, 234, 0.1);
        }
    </style>
</head>
<body>
    <!-- 可拖拽元素 -->
    <div class="draggable" draggable="true" id="drag1">
        拖拽我
    </div>
    
    <!-- 放置区域 -->
    <div class="dropzone" id="dropzone1">
        放置区域
    </div>
    
    <script>
        const draggable = document.getElementById('drag1');
        const dropzone = document.getElementById('dropzone1');
        
        // 拖拽开始
        draggable.addEventListener('dragstart', function(e) {
            console.log('开始拖拽');
            e.dataTransfer.setData('text/plain', this.id);
            e.dataTransfer.effectAllowed = 'move';
        });
        
        // 拖拽结束
        draggable.addEventListener('dragend', function(e) {
            console.log('拖拽结束');
        });
        
        // 拖拽进入目标
        dropzone.addEventListener('dragenter', function(e) {
            e.preventDefault();
            this.classList.add('dragover');
        });
        
        // 拖拽在目标上移动
        dropzone.addEventListener('dragover', function(e) {
            e.preventDefault();
            e.dataTransfer.dropEffect = 'move';
        });
        
        // 拖拽离开目标
        dropzone.addEventListener('dragleave', function(e) {
            this.classList.remove('dragover');
        });
        
        // 放置
        dropzone.addEventListener('drop', function(e) {
            e.preventDefault();
            this.classList.remove('dragover');
            
            const id = e.dataTransfer.getData('text/plain');
            const element = document.getElementById(id);
            this.appendChild(element);
        });
    </script>
</body>
</html>

拖放事件详解

拖拽源事件

// 拖拽元素
const draggable = document.querySelector('.draggable');

// dragstart: 开始拖拽
draggable.addEventListener('dragstart', function(e) {
    // 设置数据
    e.dataTransfer.setData('text/plain', '文本数据');
    e.dataTransfer.setData('text/html', '<b>HTML数据</b>');
    e.dataTransfer.setData('application/json', JSON.stringify({ id: 1 }));
    
    // 设置拖拽效果
    e.dataTransfer.effectAllowed = 'move'; // move | copy | link | all | none
    
    // 设置拖拽图像
    const dragImage = new Image();
    dragImage.src = 'drag-image.png';
    e.dataTransfer.setDragImage(dragImage, 0, 0);
    
    console.log('拖拽开始');
});

// drag: 拖拽过程中
draggable.addEventListener('drag', function(e) {
    // 持续触发,注意性能
    console.log('拖拽中...', e.clientX, e.clientY);
});

// dragend: 拖拽结束
draggable.addEventListener('dragend', function(e) {
    console.log('拖拽结束');
    
    // 检查是否成功放置
    if (e.dataTransfer.dropEffect === 'none') {
        console.log('拖拽被取消');
    }
});

放置目标事件

// 放置目标
const dropzone = document.querySelector('.dropzone');

// dragenter: 进入目标
dropzone.addEventListener('dragenter', function(e) {
    e.preventDefault(); // 必须阻止默认行为
    this.classList.add('drag-over');
    console.log('进入目标');
});

// dragover: 在目标上移动
dropzone.addEventListener('dragover', function(e) {
    e.preventDefault(); // 必须阻止默认行为,否则无法drop
    e.dataTransfer.dropEffect = 'move';
    console.log('在目标上移动');
});

// dragleave: 离开目标
dropzone.addEventListener('dragleave', function(e) {
    this.classList.remove('drag-over');
    console.log('离开目标');
});

// drop: 放置
dropzone.addEventListener('drop', function(e) {
    e.preventDefault(); // 阻止默认行为
    this.classList.remove('drag-over');
    
    // 获取数据
    const text = e.dataTransfer.getData('text/plain');
    const html = e.dataTransfer.getData('text/html');
    const json = e.dataTransfer.getData('application/json');
    
    console.log('放置数据:', { text, html, json });
});

DataTransfer对象

DataTransfer属性

// DataTransfer对象属性
const dataTransfer = e.dataTransfer;

// dropEffect: 放置效果
dataTransfer.dropEffect = 'none'; // none | copy | move | link

// effectAllowed: 允许的效果
dataTransfer.effectAllowed = 'all'; // none | copy | copyLink | copyMove | link | linkMove | move | all | uninitialized

// files: 文件列表
const files = dataTransfer.files;

// items: DataTransferItemList
const items = dataTransfer.items;

// types: 数据类型列表
const types = dataTransfer.types;

DataTransfer方法

// 设置数据
e.dataTransfer.setData('text/plain', '文本数据');
e.dataTransfer.setData('text/html', '<b>HTML数据</b>');

// 获取数据
const text = e.dataTransfer.getData('text/plain');
const html = e.dataTransfer.getData('text/html');

// 清除数据
e.dataTransfer.clearData();
e.dataTransfer.clearData('text/plain');

// 设置拖拽图像
const img = new Image();
img.src = 'drag-image.png';
e.dataTransfer.setDragImage(img, 10, 10); // 图像和偏移量

文件拖放

拖放文件

<!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: 800px;
            margin: 50px auto;
            padding: 20px;
        }
        
        .dropzone {
            width: 100%;
            height: 300px;
            border: 3px dashed #ccc;
            border-radius: 10px;
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            transition: all 0.3s;
            background: #f9f9f9;
        }
        
        .dropzone.dragover {
            border-color: #667eea;
            background: rgba(102, 126, 234, 0.1);
            transform: scale(1.02);
        }
        
        .dropzone-icon {
            font-size: 64px;
            margin-bottom: 20px;
        }
        
        .file-list {
            margin-top: 20px;
        }
        
        .file-item {
            display: flex;
            align-items: center;
            padding: 10px;
            background: white;
            border: 1px solid #ddd;
            border-radius: 5px;
            margin: 10px 0;
        }
        
        .file-icon {
            font-size: 32px;
            margin-right: 15px;
        }
        
        .file-info {
            flex: 1;
        }
        
        .file-name {
            font-weight: bold;
            margin-bottom: 5px;
        }
        
        .file-size {
            color: #666;
            font-size: 14px;
        }
        
        .image-preview {
            max-width: 100px;
            max-height: 100px;
            border-radius: 5px;
        }
    </style>
</head>
<body>
    <h1>文件拖放示例</h1>
    
    <div class="dropzone" id="dropzone">
        <div class="dropzone-icon">📁</div>
        <p>拖放文件到这里</p>
        <p style="color: #999; font-size: 14px;">支持图片、文本、PDF等文件</p>
    </div>
    
    <div class="file-list" id="fileList"></div>
    
    <script>
        const dropzone = document.getElementById('dropzone');
        const fileList = document.getElementById('fileList');
        
        // 阻止默认拖放行为
        ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
            dropzone.addEventListener(eventName, preventDefaults, false);
            document.body.addEventListener(eventName, preventDefaults, false);
        });
        
        function preventDefaults(e) {
            e.preventDefault();
            e.stopPropagation();
        }
        
        // 拖拽进入
        dropzone.addEventListener('dragenter', function(e) {
            this.classList.add('dragover');
        });
        
        // 拖拽离开
        dropzone.addEventListener('dragleave', function(e) {
            this.classList.remove('dragover');
        });
        
        // 放置
        dropzone.addEventListener('drop', function(e) {
            this.classList.remove('dragover');
            
            const files = e.dataTransfer.files;
            handleFiles(files);
        });
        
        // 处理文件
        function handleFiles(files) {
            [...files].forEach(file => {
                displayFile(file);
            });
        }
        
        // 显示文件
        function displayFile(file) {
            const fileItem = document.createElement('div');
            fileItem.className = 'file-item';
            
            // 文件图标
            let icon = '📄';
            if (file.type.startsWith('image/')) icon = '🖼️';
            else if (file.type.startsWith('video/')) icon = '🎬';
            else if (file.type.startsWith('audio/')) icon = '🎵';
            else if (file.type === 'application/pdf') icon = '📕';
            else if (file.type.includes('word')) icon = '📘';
            else if (file.type.includes('excel')) icon = '📗';
            
            // 文件大小格式化
            const size = formatFileSize(file.size);
            
            fileItem.innerHTML = `
                <div class="file-icon">${icon}</div>
                <div class="file-info">
                    <div class="file-name">${file.name}</div>
                    <div class="file-size">${size} | ${file.type || '未知类型'}</div>
                </div>
            `;
            
            // 图片预览
            if (file.type.startsWith('image/')) {
                const reader = new FileReader();
                reader.onload = function(e) {
                    const img = document.createElement('img');
                    img.src = e.target.result;
                    img.className = 'image-preview';
                    fileItem.appendChild(img);
                };
                reader.readAsDataURL(file);
            }
            
            fileList.appendChild(fileItem);
        }
        
        // 格式化文件大小
        function formatFileSize(bytes) {
            if (bytes === 0) return '0 Bytes';
            const k = 1024;
            const sizes = ['Bytes', 'KB', 'MB', 'GB'];
            const i = Math.floor(Math.log(bytes) / Math.log(k));
            return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
        }
    </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>拖放排序示例</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            max-width: 600px;
            margin: 50px auto;
            padding: 20px;
        }
        
        h1 {
            text-align: center;
        }
        
        .sortable-list {
            list-style: none;
            padding: 0;
        }
        
        .sortable-item {
            display: flex;
            align-items: center;
            padding: 15px;
            background: white;
            border: 1px solid #ddd;
            margin: 5px 0;
            border-radius: 5px;
            cursor: move;
            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-number {
            width: 30px;
            height: 30px;
            background: #667eea;
            color: white;
            border-radius: 50%;
            display: flex;
            align-items: center;
            justify-content: center;
            margin-right: 15px;
            font-weight: bold;
        }
        
        .item-content {
            flex: 1;
        }
        
        .item-handle {
            color: #ccc;
            font-size: 20px;
        }
    </style>
</head>
<body>
    <h1>拖放排序</h1>
    
    <ul class="sortable-list" id="sortableList">
        <li class="sortable-item" draggable="true">
            <span class="item-number">1</span>
            <span class="item-content">HTML基础教程</span>
            <span class="item-handle">⋮⋮</span>
        </li>
        <li class="sortable-item" draggable="true">
            <span class="item-number">2</span>
            <span class="item-content">CSS样式教程</span>
            <span class="item-handle">⋮⋮</span>
        </li>
        <li class="sortable-item" draggable="true">
            <span class="item-number">3</span>
            <span class="item-content">JavaScript教程</span>
            <span class="item-handle">⋮⋮</span>
        </li>
        <li class="sortable-item" draggable="true">
            <span class="item-number">4</span>
            <span class="item-content">Vue.js教程</span>
            <span class="item-handle">⋮⋮</span>
        </li>
        <li class="sortable-item" draggable="true">
            <span class="item-number">5</span>
            <span class="item-content">React教程</span>
            <span class="item-handle">⋮⋮</span>
        </li>
    </ul>
    
    <script>
        const list = document.getElementById('sortableList');
        let draggedItem = null;
        
        // 为每个列表项添加事件
        list.querySelectorAll('.sortable-item').forEach(item => {
            item.addEventListener('dragstart', handleDragStart);
            item.addEventListener('dragend', handleDragEnd);
            item.addEventListener('dragover', handleDragOver);
            item.addEventListener('drop', handleDrop);
            item.addEventListener('dragenter', handleDragEnter);
            item.addEventListener('dragleave', handleDragLeave);
        });
        
        function handleDragStart(e) {
            draggedItem = this;
            this.classList.add('dragging');
            
            e.dataTransfer.effectAllowed = 'move';
            e.dataTransfer.setData('text/html', this.innerHTML);
        }
        
        function handleDragEnd(e) {
            this.classList.remove('dragging');
            
            list.querySelectorAll('.sortable-item').forEach(item => {
                item.classList.remove('drag-over');
            });
            
            updateNumbers();
        }
        
        function handleDragOver(e) {
            e.preventDefault();
            e.dataTransfer.dropEffect = 'move';
        }
        
        function handleDragEnter(e) {
            this.classList.add('drag-over');
        }
        
        function handleDragLeave(e) {
            this.classList.remove('drag-over');
        }
        
        function handleDrop(e) {
            e.preventDefault();
            
            if (draggedItem !== this) {
                const allItems = [...list.querySelectorAll('.sortable-item')];
                const draggedIndex = allItems.indexOf(draggedItem);
                const targetIndex = allItems.indexOf(this);
                
                if (draggedIndex < targetIndex) {
                    this.parentNode.insertBefore(draggedItem, this.nextSibling);
                } else {
                    this.parentNode.insertBefore(draggedItem, this);
                }
            }
            
            this.classList.remove('drag-over');
        }
        
        // 更新序号
        function updateNumbers() {
            list.querySelectorAll('.sortable-item').forEach((item, index) => {
                item.querySelector('.item-number').textContent = index + 1;
            });
        }
    </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;
        }
        
        .container {
            display: grid;
            grid-template-columns: 1fr 1fr;
            gap: 20px;
            margin-top: 20px;
        }
        
        .section {
            background: white;
            padding: 20px;
            border-radius: 10px;
            box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
        }
        
        .section h2 {
            margin-top: 0;
            color: #667eea;
            border-bottom: 2px solid #667eea;
            padding-bottom: 10px;
        }
        
        /* 看板样式 */
        .kanban {
            display: flex;
            gap: 15px;
            min-height: 400px;
        }
        
        .kanban-column {
            flex: 1;
            background: #f9f9f9;
            border-radius: 8px;
            padding: 15px;
        }
        
        .kanban-column h3 {
            margin-top: 0;
            padding-bottom: 10px;
            border-bottom: 2px solid #ddd;
        }
        
        .kanban-column.todo h3 { border-color: #dc3545; }
        .kanban-column.doing h3 { border-color: #ffc107; }
        .kanban-column.done h3 { border-color: #28a745; }
        
        .kanban-card {
            background: white;
            padding: 15px;
            margin: 10px 0;
            border-radius: 5px;
            border-left: 4px solid #667eea;
            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: 5px;
        }
        
        .card-desc {
            font-size: 14px;
            color: #666;
            margin-bottom: 10px;
        }
        
        .card-tag {
            display: inline-block;
            padding: 3px 8px;
            background: #667eea;
            color: white;
            border-radius: 3px;
            font-size: 12px;
        }
        
        /* 文件上传区 */
        .upload-zone {
            border: 3px dashed #ddd;
            border-radius: 10px;
            padding: 40px;
            text-align: center;
            transition: all 0.3s;
            background: #fafafa;
        }
        
        .upload-zone.dragover {
            border-color: #667eea;
            background: rgba(102, 126, 234, 0.05);
        }
        
        .upload-icon {
            font-size: 64px;
            margin-bottom: 20px;
        }
        
        .upload-text {
            font-size: 18px;
            margin-bottom: 10px;
        }
        
        .upload-hint {
            color: #999;
            font-size: 14px;
        }
        
        /* 文件列表 */
        .file-list {
            margin-top: 20px;
        }
        
        .file-item {
            display: flex;
            align-items: center;
            padding: 10px;
            background: #f9f9f9;
            border-radius: 5px;
            margin: 5px 0;
        }
        
        .file-icon {
            font-size: 24px;
            margin-right: 10px;
        }
        
        .file-name {
            flex: 1;
            font-weight: bold;
        }
        
        .file-size {
            color: #666;
            font-size: 14px;
        }
        
        /* 购物车 */
        .products {
            display: grid;
            grid-template-columns: repeat(2, 1fr);
            gap: 15px;
        }
        
        .product {
            background: #f9f9f9;
            padding: 15px;
            border-radius: 8px;
            text-align: center;
            cursor: move;
            transition: all 0.3s;
        }
        
        .product:hover {
            background: #f0f0f0;
        }
        
        .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 {
            min-height: 200px;
            border: 2px dashed #ddd;
            border-radius: 8px;
            padding: 15px;
            margin-top: 20px;
            transition: all 0.3s;
        }
        
        .cart.drag-over {
            border-color: #667eea;
            background: rgba(102, 126, 234, 0.05);
        }
        
        .cart-item {
            display: flex;
            align-items: center;
            padding: 10px;
            background: white;
            border-radius: 5px;
            margin: 5px 0;
        }
        
        .cart-item-icon {
            font-size: 24px;
            margin-right: 10px;
        }
        
        .cart-item-name {
            flex: 1;
        }
        
        .cart-item-price {
            color: #667eea;
            font-weight: bold;
        }
        
        .cart-total {
            margin-top: 15px;
            padding-top: 15px;
            border-top: 2px solid #ddd;
            text-align: right;
            font-size: 18px;
            font-weight: bold;
        }
        
        /* 统计信息 */
        .stats {
            display: flex;
            gap: 20px;
            margin-top: 20px;
        }
        
        .stat-item {
            flex: 1;
            padding: 15px;
            background: #f9f9f9;
            border-radius: 8px;
            text-align: center;
        }
        
        .stat-value {
            font-size: 32px;
            font-weight: bold;
            color: #667eea;
        }
        
        .stat-label {
            color: #666;
            margin-top: 5px;
        }
    </style>
</head>
<body>
    <h1>拖放API综合示例</h1>
    
    <div class="container">
        <!-- 看板 -->
        <div class="section">
            <h2>任务看板</h2>
            <div class="kanban">
                <div class="kanban-column todo" data-status="todo">
                    <h3>待办</h3>
                    <div class="kanban-card" draggable="true" data-id="1">
                        <div class="card-title">学习HTML基础</div>
                        <div class="card-desc">完成HTML基础教程的学习</div>
                        <span class="card-tag">学习</span>
                    </div>
                    <div class="kanban-card" draggable="true" data-id="2">
                        <div class="card-title">练习CSS布局</div>
                        <div class="card-desc">完成Flexbox和Grid布局练习</div>
                        <span class="card-tag">练习</span>
                    </div>
                </div>
                
                <div class="kanban-column doing" data-status="doing">
                    <h3>进行中</h3>
                    <div class="kanban-card" draggable="true" data-id="3">
                        <div class="card-title">JavaScript教程</div>
                        <div class="card-desc">正在学习JavaScript高级特性</div>
                        <span class="card-tag">学习</span>
                    </div>
                </div>
                
                <div class="kanban-column done" data-status="done">
                    <h3>已完成</h3>
                    <div class="kanban-card" draggable="true" data-id="4">
                        <div class="card-title">环境搭建</div>
                        <div class="card-desc">开发环境已配置完成</div>
                        <span class="card-tag">完成</span>
                    </div>
                </div>
            </div>
        </div>
        
        <!-- 文件上传 -->
        <div class="section">
            <h2>文件上传</h2>
            <div class="upload-zone" id="uploadZone">
                <div class="upload-icon">📁</div>
                <div class="upload-text">拖放文件到这里上传</div>
                <div class="upload-hint">支持图片、文档、压缩包等文件</div>
            </div>
            <div class="file-list" id="fileList"></div>
        </div>
        
        <!-- 购物车 -->
        <div class="section">
            <h2>购物车</h2>
            <div class="products">
                <div class="product" draggable="true" data-name="HTML教程" data-price="99">
                    <div class="product-icon">📚</div>
                    <div class="product-name">HTML教程</div>
                    <div class="product-price">¥99</div>
                </div>
                <div class="product" draggable="true" data-name="CSS教程" data-price="99">
                    <div class="product-icon">🎨</div>
                    <div class="product-name">CSS教程</div>
                    <div class="product-price">¥99</div>
                </div>
                <div class="product" draggable="true" data-name="JavaScript教程" data-price="149">
                    <div class="product-icon"></div>
                    <div class="product-name">JavaScript教程</div>
                    <div class="product-price">¥149</div>
                </div>
                <div class="product" draggable="true" data-name="Vue教程" data-price="199">
                    <div class="product-icon">💚</div>
                    <div class="product-name">Vue教程</div>
                    <div class="product-price">¥199</div>
                </div>
            </div>
            
            <div class="cart" id="cart">
                <p style="text-align: center; color: #999;">拖放商品到这里添加到购物车</p>
            </div>
        </div>
        
        <!-- 统计 -->
        <div class="section">
            <h2>操作统计</h2>
            <div class="stats">
                <div class="stat-item">
                    <div class="stat-value" id="dragCount">0</div>
                    <div class="stat-label">拖拽次数</div>
                </div>
                <div class="stat-item">
                    <div class="stat-value" id="dropCount">0</div>
                    <div class="stat-label">放置次数</div>
                </div>
                <div class="stat-item">
                    <div class="stat-value" id="fileCount">0</div>
                    <div class="stat-label">上传文件数</div>
                </div>
            </div>
        </div>
    </div>
    
    <script>
        // 统计
        let stats = {
            dragCount: 0,
            dropCount: 0,
            fileCount: 0
        };
        
        function updateStats() {
            document.getElementById('dragCount').textContent = stats.dragCount;
            document.getElementById('dropCount').textContent = stats.dropCount;
            document.getElementById('fileCount').textContent = stats.fileCount;
        }
        
        // ========== 看板 ==========
        const kanbanCards = document.querySelectorAll('.kanban-card');
        const kanbanColumns = document.querySelectorAll('.kanban-column');
        
        kanbanCards.forEach(card => {
            card.addEventListener('dragstart', function(e) {
                this.classList.add('dragging');
                e.dataTransfer.setData('text/plain', this.dataset.id);
                stats.dragCount++;
                updateStats();
            });
            
            card.addEventListener('dragend', function() {
                this.classList.remove('dragging');
            });
        });
        
        kanbanColumns.forEach(column => {
            column.addEventListener('dragover', function(e) {
                e.preventDefault();
                e.dataTransfer.dropEffect = 'move';
            });
            
            column.addEventListener('dragenter', function(e) {
                e.preventDefault();
                this.classList.add('drag-over');
            });
            
            column.addEventListener('dragleave', function() {
                this.classList.remove('drag-over');
            });
            
            column.addEventListener('drop', function(e) {
                e.preventDefault();
                this.classList.remove('drag-over');
                
                const id = e.dataTransfer.getData('text/plain');
                const card = document.querySelector(`.kanban-card[data-id="${id}"]`);
                
                if (card) {
                    this.appendChild(card);
                    stats.dropCount++;
                    updateStats();
                }
            });
        });
        
        // ========== 文件上传 ==========
        const uploadZone = document.getElementById('uploadZone');
        const fileList = document.getElementById('fileList');
        
        ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
            uploadZone.addEventListener(eventName, preventDefaults, false);
        });
        
        function preventDefaults(e) {
            e.preventDefault();
            e.stopPropagation();
        }
        
        uploadZone.addEventListener('dragenter', function() {
            this.classList.add('dragover');
        });
        
        uploadZone.addEventListener('dragleave', function() {
            this.classList.remove('dragover');
        });
        
        uploadZone.addEventListener('drop', function(e) {
            this.classList.remove('dragover');
            
            const files = e.dataTransfer.files;
            handleFiles(files);
        });
        
        function handleFiles(files) {
            [...files].forEach(file => {
                displayFile(file);
                stats.fileCount++;
                updateStats();
            });
        }
        
        function displayFile(file) {
            const fileItem = document.createElement('div');
            fileItem.className = 'file-item';
            
            let icon = '📄';
            if (file.type.startsWith('image/')) icon = '🖼️';
            else if (file.type.startsWith('video/')) icon = '🎬';
            else if (file.type === 'application/pdf') icon = '📕';
            
            const size = formatFileSize(file.size);
            
            fileItem.innerHTML = `
                <div class="file-icon">${icon}</div>
                <div class="file-name">${file.name}</div>
                <div class="file-size">${size}</div>
            `;
            
            fileList.appendChild(fileItem);
        }
        
        function formatFileSize(bytes) {
            if (bytes === 0) return '0 Bytes';
            const k = 1024;
            const sizes = ['Bytes', 'KB', 'MB', 'GB'];
            const i = Math.floor(Math.log(bytes) / Math.log(k));
            return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
        }
        
        // ========== 购物车 ==========
        const products = document.querySelectorAll('.product');
        const cart = document.getElementById('cart');
        let cartItems = [];
        
        products.forEach(product => {
            product.addEventListener('dragstart', function(e) {
                this.classList.add('dragging');
                e.dataTransfer.setData('application/json', JSON.stringify({
                    name: this.dataset.name,
                    price: this.dataset.price
                }));
                stats.dragCount++;
                updateStats();
            });
            
            product.addEventListener('dragend', function() {
                this.classList.remove('dragging');
            });
        });
        
        cart.addEventListener('dragover', function(e) {
            e.preventDefault();
            e.dataTransfer.dropEffect = 'copy';
        });
        
        cart.addEventListener('dragenter', function(e) {
            e.preventDefault();
            this.classList.add('drag-over');
        });
        
        cart.addEventListener('dragleave', function() {
            this.classList.remove('drag-over');
        });
        
        cart.addEventListener('drop', function(e) {
            e.preventDefault();
            this.classList.remove('drag-over');
            
            const data = JSON.parse(e.dataTransfer.getData('application/json'));
            
            if (!cartItems.find(item => item.name === data.name)) {
                cartItems.push(data);
                updateCart();
                stats.dropCount++;
                updateStats();
            }
        });
        
        function updateCart() {
            if (cartItems.length === 0) {
                cart.innerHTML = '<p style="text-align: center; color: #999;">拖放商品到这里添加到购物车</p>';
                return;
            }
            
            let total = 0;
            cart.innerHTML = cartItems.map(item => {
                total += parseInt(item.price);
                return `
                    <div class="cart-item">
                        <div class="cart-item-icon">📦</div>
                        <div class="cart-item-name">${item.name}</div>
                        <div class="cart-item-price">¥${item.price}</div>
                    </div>
                `;
            }).join('');
            
            cart.innerHTML += `
                <div class="cart-total">
                    总计: ¥${total}
                </div>
            `;
        }
        
        // 初始化统计
        updateStats();
    </script>
</body>
</html>

最佳实践

1. 提供视觉反馈

// 推荐:提供清晰的视觉反馈
element.addEventListener('dragenter', function() {
    this.classList.add('drag-over');
});

element.addEventListener('dragleave', function() {
    this.classList.remove('drag-over');
});

// 不推荐:没有视觉反馈
element.addEventListener('dragover', function(e) {
    e.preventDefault();
    // 用户不知道是否可以放置
});

2. 设置正确的拖拽效果

// 推荐:设置正确的拖拽效果
element.addEventListener('dragstart', function(e) {
    e.dataTransfer.effectAllowed = 'move';
});

dropzone.addEventListener('dragover', function(e) {
    e.preventDefault();
    e.dataTransfer.dropEffect = 'move';
});

// 不推荐:不设置拖拽效果
element.addEventListener('dragstart', function(e) {
    // 没有设置effectAllowed
});

3. 处理移动端兼容性

// 推荐:提供触摸设备支持
function addTouchSupport(element) {
    let touchStartX, touchStartY;
    
    element.addEventListener('touchstart', function(e) {
        touchStartX = e.touches[0].clientX;
        touchStartY = e.touches[0].clientY;
    });
    
    element.addEventListener('touchmove', function(e) {
        e.preventDefault();
        const touch = e.touches[0];
        const moveX = touch.clientX - touchStartX;
        const moveY = touch.clientY - touchStartY;
        
        // 移动元素
        this.style.transform = `translate(${moveX}px, ${moveY}px)`;
    });
    
    element.addEventListener('touchend', function(e) {
        // 处理放置
        this.style.transform = '';
    });
}

东巴文点评:原生拖放API在移动端支持不佳,需要使用触摸事件模拟或使用第三方库。

学习检验

知识点测试

问题1:以下哪个事件在拖拽元素进入放置目标时触发?

A. dragstart B. dragenter C. dragover D. drop

<details> <summary>点击查看答案</summary>

答案:B

东巴文解释dragenter事件在拖拽元素进入放置目标时触发。dragstart在开始拖拽时触发,dragover在拖拽元素在目标上移动时持续触发,drop在放置时触发。

</details>

问题2:要允许元素被放置,必须在哪个事件中调用preventDefault()

A. dragenter B. dragover C. dragleave D. dragend

<details> <summary>点击查看答案</summary>

答案:B

东巴文解释:必须在dragover事件中调用preventDefault(),否则drop事件不会触发。虽然dragenter中也需要阻止默认行为,但dragover是关键。

</details>

实践任务

任务:创建一个图片排序应用,用户可以通过拖放重新排列图片顺序。

<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: 800px;
            margin: 0 auto;
            padding: 20px;
        }
        
        h1 {
            text-align: center;
        }
        
        .gallery {
            display: grid;
            grid-template-columns: repeat(3, 1fr);
            gap: 15px;
        }
        
        .gallery-item {
            position: relative;
            aspect-ratio: 1;
            border-radius: 8px;
            overflow: hidden;
            cursor: move;
            transition: all 0.3s;
        }
        
        .gallery-item img {
            width: 100%;
            height: 100%;
            object-fit: cover;
        }
        
        .gallery-item:hover {
            transform: scale(1.05);
            box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
        }
        
        .gallery-item.dragging {
            opacity: 0.5;
        }
        
        .gallery-item.drag-over {
            border: 3px solid #667eea;
        }
        
        .item-number {
            position: absolute;
            top: 10px;
            left: 10px;
            width: 30px;
            height: 30px;
            background: rgba(0, 0, 0, 0.7);
            color: white;
            border-radius: 50%;
            display: flex;
            align-items: center;
            justify-content: center;
            font-weight: bold;
        }
    </style>
</head>
<body>
    <h1>图片排序</h1>
    <p style="text-align: center; color: #666;">拖拽图片重新排列顺序</p>
    
    <div class="gallery" id="gallery">
        <div class="gallery-item" draggable="true">
            <span class="item-number">1</span>
            <img src="https://picsum.photos/300/300?random=1" alt="图片1">
        </div>
        <div class="gallery-item" draggable="true">
            <span class="item-number">2</span>
            <img src="https://picsum.photos/300/300?random=2" alt="图片2">
        </div>
        <div class="gallery-item" draggable="true">
            <span class="item-number">3</span>
            <img src="https://picsum.photos/300/300?random=3" alt="图片3">
        </div>
        <div class="gallery-item" draggable="true">
            <span class="item-number">4</span>
            <img src="https://picsum.photos/300/300?random=4" alt="图片4">
        </div>
        <div class="gallery-item" draggable="true">
            <span class="item-number">5</span>
            <img src="https://picsum.photos/300/300?random=5" alt="图片5">
        </div>
        <div class="gallery-item" draggable="true">
            <span class="item-number">6</span>
            <img src="https://picsum.photos/300/300?random=6" alt="图片6">
        </div>
    </div>
    
    <script>
        const gallery = document.getElementById('gallery');
        let draggedItem = null;
        
        gallery.querySelectorAll('.gallery-item').forEach(item => {
            item.addEventListener('dragstart', handleDragStart);
            item.addEventListener('dragend', handleDragEnd);
            item.addEventListener('dragover', handleDragOver);
            item.addEventListener('drop', handleDrop);
            item.addEventListener('dragenter', handleDragEnter);
            item.addEventListener('dragleave', handleDragLeave);
        });
        
        function handleDragStart(e) {
            draggedItem = this;
            this.classList.add('dragging');
            e.dataTransfer.effectAllowed = 'move';
        }
        
        function handleDragEnd() {
            this.classList.remove('dragging');
            gallery.querySelectorAll('.gallery-item').forEach(item => {
                item.classList.remove('drag-over');
            });
            updateNumbers();
        }
        
        function handleDragOver(e) {
            e.preventDefault();
            e.dataTransfer.dropEffect = 'move';
        }
        
        function handleDragEnter(e) {
            e.preventDefault();
            if (this !== draggedItem) {
                this.classList.add('drag-over');
            }
        }
        
        function handleDragLeave() {
            this.classList.remove('drag-over');
        }
        
        function handleDrop(e) {
            e.preventDefault();
            
            if (this !== draggedItem) {
                const allItems = [...gallery.querySelectorAll('.gallery-item')];
                const draggedIndex = allItems.indexOf(draggedItem);
                const targetIndex = allItems.indexOf(this);
                
                if (draggedIndex < targetIndex) {
                    gallery.insertBefore(draggedItem, this.nextSibling);
                } else {
                    gallery.insertBefore(draggedItem, this);
                }
            }
            
            this.classList.remove('drag-over');
        }
        
        function updateNumbers() {
            gallery.querySelectorAll('.gallery-item').forEach((item, index) => {
                item.querySelector('.item-number').textContent = index + 1;
            });
        }
    </script>
</body>
</html>
</details>

东巴文(db-w.cn) - 让编程学习更有趣、更高效!