Skip to content

拖放事件

拖放事件(Drag and Drop Events)是 HTML5 原生支持的拖放交互 API,允许用户通过鼠标拖拽元素来实现数据的移动、复制等操作。拖放 API 定义了完整的事件生命周期:从拖拽开始、拖拽经过、放置到拖拽结束,每个阶段都有对应的事件处理器。

前置知识

阅读本节前,建议先了解:窗口事件

基础概念

拖放事件类型

阶段事件触发元素说明
拖拽开始dragstart被拖元素拖拽开始时
拖拽中drag被拖元素持续触发
拖拽结束dragend被拖元素拖拽操作结束时
进入目标dragenter放置目标拖拽元素进入目标区域
在目标上方dragover放置目标在目标区域内持续触发
离开目标dragleave放置目标拖拽元素离开目标区域
放置drop放置目标在目标上释放鼠标

使元素可拖拽

html
<!-- 设置 draggable 属性 -->
<div draggable="true">可拖拽的元素</div>

<!-- draggable 的三个值 -->
<div draggable="true">可拖拽</div>
<div draggable="false">不可拖拽</div>
<div draggable="auto">浏览器默认行为(图片、链接默认可拖拽)</div>

语法

HTML 事件属性

html
<!-- 被拖拽元素 -->
<div draggable="true"
     ondragstart="handleDragStart(event)"
     ondrag="handleDrag(event)"
     ondragend="handleDragEnd(event)">
  拖拽我
</div>

<!-- 放置目标 -->
<div class="drop-zone"
     ondragenter="handleDragEnter(event)"
     ondragover="handleDragOver(event)"
     ondragleave="handleDragLeave(event)"
     ondrop="handleDrop(event)">
  放在这里
</div>

JavaScript 绑定

javascript
const draggable = document.getElementById('draggable');
const dropZone = document.getElementById('drop-zone');

// 被拖拽元素事件
draggable.addEventListener('dragstart', (e) => {
  e.dataTransfer.setData('text/plain', e.target.id);
  e.target.classList.add('dragging');
});

draggable.addEventListener('dragend', (e) => {
  e.target.classList.remove('dragging');
});

// 放置目标事件
dropZone.addEventListener('dragover', (e) => {
  e.preventDefault(); // 必须阻止默认行为才能接收 drop
});

dropZone.addEventListener('drop', (e) => {
  e.preventDefault();
  const id = e.dataTransfer.getData('text/plain');
  dropZone.appendChild(document.getElementById(id));
});

详细说明

DataTransfer 对象

javascript
// dragstart 中设置数据
element.addEventListener('dragstart', (e) => {
  // 设置文本数据
  e.dataTransfer.setData('text/plain', 'Hello');
  e.dataTransfer.setData('text/html', '<b>Hello</b>');

  // 设置自定义 MIME 类型
  e.dataTransfer.setData('application/json', JSON.stringify({ id: 1 }));

  // 设置拖拽效果
  e.dataTransfer.effectAllowed = 'move'; // copy, move, link, none, copyMove, etc.

  // 设置拖拽图像
  const img = document.createElement('div');
  img.textContent = '拖拽中...';
  e.dataTransfer.setDragImage(img, 0, 0);
});

// drop 中获取数据
dropZone.addEventListener('drop', (e) => {
  e.preventDefault();

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

  // 获取自定义数据
  const json = e.dataTransfer.getData('application/json');

  // 获取文件(从文件管理器拖入)
  const files = e.dataTransfer.files;
});

effectAllowed 与 dropEffect

effectAllowed (dragstart)dropEffect (drop)结果
copycopy复制操作
movemove移动操作
linklink链接操作
copyMovecopy / move对应操作
none任何不允许放置

实战示例

完整的任务看板拖拽

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>拖放事件示例</title>
  <style>
    .board {
      display: flex;
      gap: 16px;
      padding: 20px;
      min-height: 80vh;
    }
    .column {
      flex: 1;
      background: #f1f5f9;
      border-radius: 8px;
      padding: 12px;
      min-height: 200px;
    }
    .column h3 {
      margin: 0 0 12px;
      font-size: 16px;
      color: #475569;
    }
    .column.drag-over {
      background: #dbeafe;
      border: 2px dashed #3b82f6;
    }
    .card {
      padding: 12px;
      background: white;
      border-radius: 6px;
      box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
      margin-bottom: 8px;
      cursor: grab;
      transition: transform 0.2s, opacity 0.2s;
    }
    .card:active { cursor: grabbing; }
    .card.dragging { opacity: 0.4; transform: rotate(2deg); }
  </style>
</head>
<body>
  <div class="board">
    <div class="column" id="todo">
      <h3>待办</h3>
      <div class="card" draggable="true" id="task-1">设计首页布局</div>
      <div class="card" draggable="true" id="task-2">编写接口文档</div>
      <div class="card" draggable="true" id="task-3">搭建开发环境</div>
    </div>
    <div class="column" id="doing">
      <h3>进行中</h3>
    </div>
    <div class="column" id="done">
      <h3>已完成</h3>
    </div>
  </div>

  <script>
    let draggedCard = null;

    // 为所有卡片绑定拖拽事件
    document.querySelectorAll('.card').forEach(card => {
      card.addEventListener('dragstart', (e) => {
        draggedCard = card;
        card.classList.add('dragging');
        e.dataTransfer.effectAllowed = 'move';
        e.dataTransfer.setData('text/plain', card.id);
      });

      card.addEventListener('dragend', () => {
        card.classList.remove('dragging');
        document.querySelectorAll('.column').forEach(col => {
          col.classList.remove('drag-over');
        });
        draggedCard = null;
      });
    });

    // 为所有列绑定放置事件
    document.querySelectorAll('.column').forEach(column => {
      column.addEventListener('dragover', (e) => {
        e.preventDefault();
        e.dataTransfer.dropEffect = 'move';
        column.classList.add('drag-over');
      });

      column.addEventListener('dragleave', (e) => {
        // 只在真正离开列时移除样式
        if (!column.contains(e.relatedTarget)) {
          column.classList.remove('drag-over');
        }
      });

      column.addEventListener('drop', (e) => {
        e.preventDefault();
        column.classList.remove('drag-over');

        if (draggedCard) {
          // 插入到列中的最后一个卡片之后
          const cards = column.querySelectorAll('.card');
          const lastCard = cards[cards.length - 1];
          if (lastCard && lastCard !== draggedCard) {
            column.insertBefore(draggedCard, lastCard.nextSibling);
          } else {
            column.appendChild(draggedCard);
          }
        }
      });
    });
  </script>
</body>
</html>

注意事项

必须在 dragover 中调用 preventDefault

javascript
// 必须阻止 dragover 的默认行为,否则 drop 事件不会触发
dropZone.addEventListener('dragover', (e) => {
  e.preventDefault(); // 关键!
});

文件拖入支持

javascript
// 支持从桌面拖入文件
const dropArea = document.getElementById('drop-area');

dropArea.addEventListener('dragover', (e) => {
  e.preventDefault();
  dropArea.classList.add('drag-over');
});

dropArea.addEventListener('drop', (e) => {
  e.preventDefault();
  dropArea.classList.remove('drag-over');

  const files = e.dataTransfer.files;
  for (const file of files) {
    console.log('文件名:', file.name, '大小:', file.size, '类型:', file.type);
  }
});

最佳实践

1. 提供清晰的视觉反馈

css
/* 拖拽开始时的样式 */
.dragging {
  opacity: 0.5;
  transform: scale(0.95);
}

/* 放置目标高亮 */
.drop-target {
  outline: 3px dashed #3b82f6;
  outline-offset: -3px;
  background: #eff6ff;
}

下一节

继续学习:剪贴板事件

参考链接