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>
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));
}
}
🖱️ 拖放开发建议
- 视觉反馈:提供清晰的拖放状态指示
- 触摸支持:移动端需要额外处理触摸事件
- 键盘支持:提供键盘操作替代方案
- 撤销功能:支持撤销拖放操作
📱 移动端兼容
原生拖放API在移动端支持有限,可以使用:
- react-dnd
- SortableJS
- dragula
- 自定义触摸事件处理
下一章将探讨 [File API](file:///e:/db-w.cn/md_data/javascript/80_File API.md),学习如何在Web应用中处理文件。