Drag and Drop

HTML5拖放API允许用户通过拖放操作在页面中移动元素或数据。本章将介绍拖放API的基本概念和使用方法。

拖放基础

基本概念

const dragDropConcepts = {
    draggable: '可拖动元素,设置draggable属性为true',
    dragstart: '开始拖动时触发',
    drag: '拖动过程中持续触发',
    dragend: '拖动结束时触发',
    dragenter: '拖动元素进入目标区域时触发',
    dragover: '拖动元素在目标区域上方时持续触发',
    dragleave: '拖动元素离开目标区域时触发',
    drop: '在目标区域释放时触发'
};

简单拖放

<div id="source" draggable="true">拖动我</div>
<div id="target">放置区域</div>

<script>
    const source = document.getElementById('source');
    const target = document.getElementById('target');
    
    source.addEventListener('dragstart', (e) => {
        e.dataTransfer.setData('text/plain', e.target.id);
        e.dataTransfer.effectAllowed = 'move';
    });
    
    source.addEventListener('dragend', (e) => {
        console.log('拖动结束');
    });
    
    target.addEventListener('dragenter', (e) => {
        e.preventDefault();
        target.classList.add('highlight');
    });
    
    target.addEventListener('dragover', (e) => {
        e.preventDefault();
        e.dataTransfer.dropEffect = 'move';
    });
    
    target.addEventListener('dragleave', (e) => {
        target.classList.remove('highlight');
    });
    
    target.addEventListener('drop', (e) => {
        e.preventDefault();
        target.classList.remove('highlight');
        
        const id = e.dataTransfer.getData('text/plain');
        const element = document.getElementById(id);
        target.appendChild(element);
    });
</script>

DataTransfer对象

数据传输

source.addEventListener('dragstart', (e) => {
    e.dataTransfer.setData('text/plain', '纯文本数据');
    e.dataTransfer.setData('text/html', '<b>HTML数据</b>');
    e.dataTransfer.setData('text/uri-list', 'https://example.com');
    e.dataTransfer.setData('application/json', JSON.stringify({ id: 1 }));
    
    e.dataTransfer.effectAllowed = 'copyMove';
});

target.addEventListener('drop', (e) => {
    const text = e.dataTransfer.getData('text/plain');
    const html = e.dataTransfer.getData('text/html');
    const uri = e.dataTransfer.getData('text/uri-list');
    const json = JSON.parse(e.dataTransfer.getData('application/json'));
    
    console.log('文本:', text);
    console.log('HTML:', html);
    console.log('URI:', uri);
    console.log('JSON:', json);
});

拖放效果

const dropEffects = {
    none: '不允许放置',
    copy: '复制操作',
    move: '移动操作',
    link: '创建链接'
};

source.addEventListener('dragstart', (e) => {
    e.dataTransfer.effectAllowed = 'copy';
});

target.addEventListener('dragover', (e) => {
    e.preventDefault();
    e.dataTransfer.dropEffect = 'copy';
});

拖动图像

source.addEventListener('dragstart', (e) => {
    const img = new Image();
    img.src = 'drag-image.png';
    e.dataTransfer.setDragImage(img, 10, 10);
});

source.addEventListener('dragstart', (e) => {
    const div = document.createElement('div');
    div.style.width = '100px';
    div.style.height = '50px';
    div.style.backgroundColor = 'red';
    div.style.position = 'absolute';
    div.style.top = '-1000px';
    document.body.appendChild(div);
    
    e.dataTransfer.setDragImage(div, 50, 25);
    
    setTimeout(() => document.body.removeChild(div), 0);
});

文件拖放

拖放文件

const dropZone = document.getElementById('dropZone');

dropZone.addEventListener('dragenter', (e) => {
    e.preventDefault();
    dropZone.classList.add('active');
});

dropZone.addEventListener('dragover', (e) => {
    e.preventDefault();
});

dropZone.addEventListener('dragleave', (e) => {
    dropZone.classList.remove('active');
});

dropZone.addEventListener('drop', async (e) => {
    e.preventDefault();
    dropZone.classList.remove('active');
    
    const items = e.dataTransfer.items;
    const files = e.dataTransfer.files;
    
    for (const file of files) {
        console.log('文件名:', file.name);
        console.log('文件类型:', file.type);
        console.log('文件大小:', file.size);
        
        if (file.type.startsWith('image/')) {
            const imageUrl = URL.createObjectURL(file);
            displayImage(imageUrl);
        }
    }
    
    for (const item of items) {
        if (item.kind === 'file') {
            const file = item.getAsFile();
            await processFile(file);
        } else if (item.kind === 'string') {
            item.getAsString((str) => {
                console.log('字符串数据:', str);
            });
        }
    }
});

async function processFile(file) {
    const text = await file.text();
    console.log('文件内容:', text);
}

文件夹拖放

dropZone.addEventListener('drop', async (e) => {
    e.preventDefault();
    
    const items = e.dataTransfer.items;
    
    for (const item of items) {
        if (item.webkitGetAsEntry) {
            const entry = item.webkitGetAsEntry();
            if (entry) {
                await processEntry(entry);
            }
        }
    }
});

async function processEntry(entry, path = '') {
    if (entry.isFile) {
        const file = await new Promise((resolve) => {
            entry.file(resolve);
        });
        console.log('文件:', path + entry.name, file);
    } else if (entry.isDirectory) {
        const reader = entry.createReader();
        const entries = await new Promise((resolve) => {
            reader.readEntries(resolve);
        });
        
        for (const childEntry of entries) {
            await processEntry(childEntry, path + entry.name + '/');
        }
    }
}

排序列表

class SortableList {
    constructor(container) {
        this.container = container;
        this.draggedItem = null;
        this.placeholder = null;
        
        this.init();
    }
    
    init() {
        const items = this.container.querySelectorAll('li');
        
        items.forEach(item => {
            item.draggable = true;
            item.addEventListener('dragstart', this.onDragStart.bind(this));
            item.addEventListener('dragend', this.onDragEnd.bind(this));
            item.addEventListener('dragover', this.onDragOver.bind(this));
            item.addEventListener('drop', this.onDrop.bind(this));
        });
    }
    
    onDragStart(e) {
        this.draggedItem = e.target;
        e.target.classList.add('dragging');
        
        e.dataTransfer.effectAllowed = 'move';
        e.dataTransfer.setData('text/plain', '');
        
        this.placeholder = document.createElement('li');
        this.placeholder.className = 'placeholder';
    }
    
    onDragEnd(e) {
        e.target.classList.remove('dragging');
        this.placeholder?.remove();
        this.draggedItem = null;
        this.placeholder = null;
    }
    
    onDragOver(e) {
        e.preventDefault();
        
        const target = e.target;
        if (target === this.draggedItem || target === this.placeholder) return;
        
        const rect = target.getBoundingClientRect();
        const midY = rect.top + rect.height / 2;
        
        if (e.clientY < midY) {
            this.container.insertBefore(this.placeholder, target);
        } else {
            this.container.insertBefore(this.placeholder, target.nextSibling);
        }
    }
    
    onDrop(e) {
        e.preventDefault();
        
        const target = e.target;
        if (target === this.draggedItem) return;
        
        const rect = target.getBoundingClientRect();
        const midY = rect.top + rect.height / 2;
        
        if (e.clientY < midY) {
            this.container.insertBefore(this.draggedItem, target);
        } else {
            this.container.insertBefore(this.draggedItem, target.nextSibling);
        }
    }
}

const list = document.getElementById('sortableList');
new SortableList(list);

看板拖放

class KanbanBoard {
    constructor() {
        this.columns = document.querySelectorAll('.column');
        this.init();
    }
    
    init() {
        this.columns.forEach(column => {
            column.addEventListener('dragover', this.onDragOver.bind(this));
            column.addEventListener('drop', this.onDrop.bind(this));
            column.addEventListener('dragenter', this.onDragEnter.bind(this));
            column.addEventListener('dragleave', this.onDragLeave.bind(this));
        });
        
        document.querySelectorAll('.card').forEach(card => {
            card.draggable = true;
            card.addEventListener('dragstart', this.onDragStart.bind(this));
            card.addEventListener('dragend', this.onDragEnd.bind(this));
        });
    }
    
    onDragStart(e) {
        e.target.classList.add('dragging');
        e.dataTransfer.setData('text/plain', e.target.dataset.id);
        e.dataTransfer.effectAllowed = 'move';
    }
    
    onDragEnd(e) {
        e.target.classList.remove('dragging');
        this.columns.forEach(col => col.classList.remove('drag-over'));
    }
    
    onDragOver(e) {
        e.preventDefault();
        e.dataTransfer.dropEffect = 'move';
    }
    
    onDragEnter(e) {
        e.preventDefault();
        const column = e.target.closest('.column');
        if (column) {
            column.classList.add('drag-over');
        }
    }
    
    onDragLeave(e) {
        const column = e.target.closest('.column');
        if (column && !column.contains(e.relatedTarget)) {
            column.classList.remove('drag-over');
        }
    }
    
    onDrop(e) {
        e.preventDefault();
        
        const column = e.target.closest('.column');
        if (!column) return;
        
        column.classList.remove('drag-over');
        
        const cardId = e.dataTransfer.getData('text/plain');
        const card = document.querySelector(`[data-id="${cardId}"]`);
        
        if (card) {
            const cards = column.querySelector('.cards');
            cards.appendChild(card);
            
            this.saveState();
        }
    }
    
    saveState() {
        const state = {};
        this.columns.forEach(column => {
            const columnId = column.dataset.column;
            const cards = column.querySelectorAll('.card');
            state[columnId] = Array.from(cards).map(card => card.dataset.id);
        });
        
        localStorage.setItem('kanbanState', JSON.stringify(state));
    }
}

东巴文小贴士

🖱️ 拖放开发建议

  1. 视觉反馈:提供清晰的拖放状态指示
  2. 触摸支持:移动端需要额外处理触摸事件
  3. 键盘支持:提供键盘操作替代方案
  4. 撤销功能:支持撤销拖放操作

📱 移动端兼容

原生拖放API在移动端支持有限,可以使用:

  • react-dnd
  • SortableJS
  • dragula
  • 自定义触摸事件处理

下一步

下一章将探讨 [File API](file:///e:/db-w.cn/md_data/javascript/80_File API.md),学习如何在Web应用中处理文件。