JS-深度解析 HTML5 拖放 API

0 阅读3分钟

前言

在网页中实现类似 Trello 或 Jira 的任务看板拖拽效果,并不一定需要复杂的第三方库。HTML5 原生提供的拖放(Drag and Drop)API 已经非常强大。本文将带你梳理拖放事件的完整生命周期,并手把手实现一个任务看板 Demo。

一、 开启拖放:draggable 属性

默认情况下,网页中只有 图片(<img>链接(<a>选中的文本 是可以拖动的。若想让其他元素(如 div)可拖动,必须显式设置:

<div draggable="true">我是可拖动的任务卡片</div>

<img src="logo.png" draggable="false">

二、 拖放事件全解析

拖放过程涉及两个主体:被拖动元素(Source)放置目标(Target)

1. 被拖动元素触发的事件

事件触发时机
dragstart用户开始拖动元素的一瞬间触发。
drag元素被拖动期间持续触发(类似 mousemove)。
dragend拖动停止时触发(无论放置是否成功)。

2. 放置目标触发的事件

事件触发时机
dragenter拖动元素进入目标区域时触发。
dragover关键:拖动元素在目标区域上方移动时持续触发。
dragleave拖动元素离开目标区域时触发。
drop拖动元素在目标上方释放时触发。

三、 核心对象:dataTransfer

拖放不仅是位置的移动,往往伴随着数据的传递event.dataTransfer 对象就是这个“运载火箭”。

常用方法:

  • setData(format, data) :在 dragstart 中调用,存储数据。

    • 常用格式:text/plaintext/uri-list
  • getData(format) :在 drop 中调用,读取数据。


四、 实战:构建任务看板 (Kanban)

1. 核心逻辑点

  1. 阻止默认行为:默认情况下,浏览器禁止将数据/元素放置在其他元素上。为了允许放置,必须dragover 事件中调用 e.preventDefault()
  2. 视觉反馈:通过 dragenter 添加高亮类,dragleavedrop 移除高亮类。

2. 代码实现

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>任务看板拖放示例</title>
  <style>
    .board { display: flex; gap: 20px; }
    .column {
      width: 200px;
      padding: 15px;
      background: #f5f5f5;
      border-radius: 8px;
      min-height: 300px;
    }
    .task {
      padding: 12px;
      margin: 10px 0;
      background: white;
      border: 1px solid #ddd;
      border-radius: 4px;
      cursor: move;
    }
    .highlight { border: 2px dashed #4CAF50; } /* 放置目标高亮样式 */
  </style>
</head>
<body>
  <div class="board">
    <div class="column" id="todo">
      <h3>待处理</h3>
      <div class="task" draggable="true" data-id="1">任务1:设计文档</div>
      <div class="task" draggable="true" data-id="2">任务2:代码评审</div>
    </div>
    <div class="column" id="in-progress">
      <h3>进行中</h3>
    </div>
    <div class="column" id="done">
      <h3>已完成</h3>
    </div>
  </div>

  <script>
    // 1. 监听所有可拖动元素的 dragstart 事件
    document.querySelectorAll('.task').forEach(task => {
      task.addEventListener('dragstart', e => {
        // 保存任务ID到 dataTransfer
        e.dataTransfer.setData('text/plain', e.target.dataset.id);
        e.target.classList.add('dragging'); // 拖动时样式变化
      });

      task.addEventListener('dragend', e => {
        e.target.classList.remove('dragging'); // 拖动结束还原样式
      });
    });

    // 2. 为每个状态列设置放置目标逻辑
    document.querySelectorAll('.column').forEach(column => {
      // 允许放置:阻止默认行为 + 添加高亮
      column.addEventListener('dragover', e => {
        e.preventDefault();
        column.classList.add('highlight');
      });

      // 移除高亮反馈
      column.addEventListener('dragleave', () => {
        column.classList.remove('highlight');
      });

      // 放置处理
      column.addEventListener('drop', e => {
        e.preventDefault();
        column.classList.remove('highlight');
        
        // 获取传递的任务ID
        const taskId = e.dataTransfer.getData('text/plain');
        const taskElement = document.querySelector(`.task[data-id="${taskId}"]`);
        
        // 将任务移动到当前列(避免重复添加)
        if (!column.contains(taskElement)) {
          column.appendChild(taskElement);
          
          // 实际项目中:此处可调用API更新任务状态
          console.log(`任务 ${taskId} 移动到 ${column.id}`);
        }
      });
    });
  </script>
</body>
</html>

五、 面试模拟题

Q1:为什么要在 dragover 中执行 e.preventDefault()

参考回答:

浏览器默认是不允许在元素上放置任何内容的(会显示一个“禁止”图标)。只有显式地在 dragover 事件中阻止默认行为,浏览器才会认为该区域是一个“有效的放置目标”,进而允许 drop 事件触发。

Q2:drop 事件和 dragend 事件哪个先触发?

参考回答:

通常情况下,drop 事件先触发(处理放置逻辑),然后 dragend 事件最后触发(处理清理逻辑,如恢复透明度)。

Q3:如何实现拖拽文件上传?

参考回答:

监听容器的 drop 事件。通过 event.dataTransfer.files 可以获取到用户拖入浏览器的文件列表(File 对象),随后可以使用 FormData 进行异步上传。