HTML5 拖放 API

在日常开发中,拖拽功能无疑是一个常见的需求场景。为了更好地满足这一需求,HTML5提供了一套便捷的拖放API。这些API不仅能够帮助开发者轻松实现拖拽效果,还可以提高排查拖拽问题的效率,甚至可以让我们更加灵活地自定义拖拽场景和设计能力。

首先,我们将通过案例演示来直观地展示拖拽功能在实际应用中的表现,帮助你快速理解拖拽的基本概念和操作流程。其次,我们将详细解析拖放流程并介绍相关的API,包括拖放事件的类型、事件处理函数以及关键属性的使用方法等。最后,我们将通过案例实现和讲解来巩固所学知识,让你能够亲自动手实践,并将所学知识应用到实际项目中。

通过本文的学习,你将能够轻松掌握HTML5拖放API的使用技巧,提高拖拽功能的开发效率,为你的项目增色添彩。

一、案例演示

实现一个可拖放自定义课程表,将左侧的科目拖拽到右侧的课程表中,支持增加、修改、删除、移动功能。

二、API 介绍

拖放 API(Drag and Drop API)的本质是提供拖放过程的控制能力和事件机制,而不是直接实现 "拖放效果" 或 "放置逻辑"。它更像是一套底层工具,需要开发者结合业务逻辑自行实现完整的拖放交互。

1. 拖放过程的事件

整个拖放过程中,存在两个关键元素:拖拽元素、放置元素

拖拽元素的事件:

  • dragstart:拖动开始时触发,可设置拖动携带的数据(如文本、URL、自定义数据)。
  • drag:拖动过程中持续触发(可用于实时更新视觉反馈)。
  • dragend:拖动结束时触发(无论是否成功放置)。

放置元素的事件:

  • dragenter:拖动元素进入放置目标时触发(可用于高亮目标区域)。
  • dragover :拖动元素在放置目标上移动时触发(关键:需阻止默认行为 e.preventDefault() 才能允许执行drop事件)。
  • dragleave:拖动元素离开放置目标时触发(可用于取消目标区域高亮)。
  • drop:拖动元素释放到放置目标时触发(执行实际放置逻辑,如数据添加、DOM 移动等)。

2. 设置可拖拽元素

在HTML中,文本、图片和链接是默认可以拖放的元素。其他元素都是默认不可拖动的,如果需要让其他非默认可拖动的HTML元素变得可拖动,比如<div><span>等,你需要明确地为这些元素设置 draggable="true" 属性。这样,这些元素就能够接受拖放操作了。

HTML 复制代码
<div draggable="true">语文</div>

3. 设置放置元素

所有HTML元素在默认情况下都不接受拖拽元素的放置,除非通过拖拽的事件来处理来允许接受。要使一个HTML元素能够接受被拖动的元素,需要对这个元素进行一些特定的设置和事件绑定

4. DataTransfer

DataTransfer 对象是拖放操作(Drag-and-Drop)中主要用于在拖放源(Draggable Source)和放置目标(Drop Target)之间共享数据的核心机制。它就像一个 "数据中转站",负责存储拖放过程中的数据(如文本、文件、URL 等),并在拖放的各个阶段(如开始拖放、移动、放置)中传递这些数据。

除了共享数据还能设置拖放时的一些效果配置

属性 / 方法 作用
e.dataTransfer.types 返回拖放数据的 MIME 类型数组(如 'text/plain', 'text/html'
setData(type, data) 存储数据(type 为 MIME 类型,data 为字符串或 File 对象)
getData(type) 读取指定类型的数据(返回字符串,文件需通过 files 属性读取)
files 只读属性,返回拖放的文件列表(FileList 对象,仅在拖放文件时有效)
items 只读属性,返回 DataTransferItemList ,支持添加 / 删除多种类型数据(推荐)
clearData(type) 清除指定类型的数据,若不传入类型则清除所有数据
effectAllowed 设置拖放操作的允许效果(如 copy(默认) 、move 、link 等,影响鼠标样式)
dropEffect 设置放置时的实际效果(需与 effectAllowed配合,如最终是复制还是移动)

三、案例实现

1.第一步:把左侧科目设置为可拖拽

HTML 复制代码
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
      body {
        margin: 0;
        padding: 0;
        font-family: sans-serif;
        background: #f7f7f7;
      }

      .content {
        display: flex;
        justify-content: center;
        align-items: flex-start;
        gap: 40px;
        padding: 40px;
        max-width: 1200px;
        margin: 40px auto;
      }

      .left {
        display: flex;
        flex-direction: column;
        gap: 18px;
        background: #fff;
        padding: 24px 18px;
        border-radius: 10px;
        box-shadow: 0 2px 8px rgba(0, 0, 0, 0.07);
        min-width: 120px;
      }

      div[draggable="true"] {
        padding: 14px 0;
        text-align: center;
        border-radius: 6px;
        color: #fff;
        font-weight: bold;
        font-size: 16px;
        cursor: grab;
        box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
        user-select: none;
        transition: opacity 0.2s;
      }

      .hover-background {
        outline: 2px dashed #4f8cff;
        background: #e6f0ff !important;
      }

      .right {
        background: #fff;
        padding: 24px;
        border-radius: 10px;
        box-shadow: 0 2px 8px rgba(0, 0, 0, 0.07);
      }

      table {
        border-collapse: collapse;
        width: 600px;
      }

      thead td {
        background: #e9ecef;
        font-weight: bold;
        text-align: center;
        padding: 10px 0;
        border-bottom: 2px solid #d1d5db;
        font-size: 16px;
      }

      tbody td {
        width: 120px;
        height: 60px;
        border: 1px solid #e0e0e0;
        text-align: center;
        font-size: 15px;
        background: #fafbfc;
        transition: background 0.2s;
      }
    </style>
  </head>
  <body>
    <div class="content">
      <div class="left">
        <div draggable="true" style="background:rgb(26, 231, 156)">语文</div>
        <div draggable="true" style="background:rgb(107, 219, 15)">数学</div>
        <div draggable="true" style="background:rgb(208, 133, 13)">英语</div>
        <div draggable="true" style="background:rgb(30, 98, 246)">物理</div>
        <div draggable="true" style="background:rgb(210, 40, 113)">化学</div>
        <div draggable="true" style="background:rgb(210, 224, 26); color:#333;">生物</div>
      </div>
      <div class="right">
        <table>
          <thead>
            <tr>
              <td>星期一</td>
              <td>星期二</td>
              <td>星期三</td>
              <td>星期四</td>
              <td>星期五</td>
            </tr>
          </thead>
          <tbody>
            <tr>
              <td></td>
              <td></td>
              <td></td>
              <td></td>
              <td></td>
            </tr>
            <tr>
              <td></td>
              <td></td>
              <td></td>
              <td></td>
              <td></td>
            </tr>
            <tr>
              <td></td>
              <td></td>
              <td></td>
              <td></td>
              <td></td>
            </tr>
            <tr>
              <td></td>
              <td></td>
              <td></td>
              <td></td>
              <td></td>
            </tr>
          </tbody>
        </table>
      </div>
    </div>
  </body>
</html>

2.第二步:绑定拖放事件

这里采用事件委托的方式,对整个父元素进行事件绑定

javascript 复制代码
// 获取父元素DOM
const container = document.querySelector('.content');


// 拖拽元素开始拖动
container.addEventListener('dragstart', e => {
});

// 拖拽元素进入放置元素
container.addEventListener('dragenter', e => {
});

// 拖拽拖在放置元素上方
container.addEventListener('dragover', e => {
});

// 拖拽元素离开放置元素
container.addEventListener('dragleave', e => {
});

// 拖拽元素拖拽结束
container.addEventListener('dragend', e => {
});

// 拖拽元素放下在放置元素
container.addEventListener('drop', e => {
});

3.第三步:编写事件处理逻辑

ondragstart:拖拽开始时,需要做以下处理

  • 可以让拖拽元素原来的位置样式有些变化,这里就是整体颜色变得更透明
  • 从左侧拖到右侧,为新增,鼠标显示新增的手势。右侧拖到左侧为移除,显示普通手势。(拖拽过程中默认为新增手势)
  • 保存当前拖拽元素,后续操作需要用到

通过自定义属性给拖拽元素添加标识,标识此元素是新增还是移除,新增设置为 data-effect="copy"

HTML 复制代码
        <div class="left">
            <div data-effect="copy" draggable="true" style="background:rgb(26, 231, 156)">语文</div>
            <div data-effect="copy" draggable="true" style="background:rgb(107, 219, 15)">数学</div>
            <div data-effect="copy" draggable="true" style="background:rgb(208, 133, 13)">英语</div>
            <div data-effect="copy" draggable="true" style="background:rgb(30, 98, 246)">物理</div>
            <div data-effect="copy" draggable="true" style="background:rgb(210, 40, 113)">化学</div>
            <div data-effect="copy" draggable="true" style="background:rgb(210, 224, 26)">生物</div>
        </div>
js 复制代码
    // 声明用来保存拖拽元素DOM的变量
    let source = '';

    // 拖拽开始
    container.addEventListener('dragstart', e => {
        if (!e.target.draggable) return;
        // 如果是移除操作,则设置手势为移除
        if (e.target.dataset.effect === "move") {
            e.dataTransfer.effectAllowed = "move";
        }
        source = e.target;
        // 设置拖拽元素的样式
        source.style.opacity = '0.2'
    });

ondragenter:进入放置元素,需要做以下处理

  • 通过自定义属性进行标识 data-drop="copy",判断哪些元素可以接收
  • 查找最近的放置节点,这样做是为了如果上面已经放置了元素,也不影响可放置元素
  • 如果可以接收拖拽元素,需要设置其样式用于标识
HTML 复制代码
                <tbody>
                    <tr>
                        <td data-drop="copy"></td>
                        <td data-drop="copy"></td>
                        <td data-drop="copy"></td>
                        <td data-drop="copy"></td>
                        <td data-drop="copy"></td>
                    </tr>
                    <tr>
                        <td data-drop="copy"></td>
                        <td data-drop="copy"></td>
                        <td data-drop="copy"></td>
                        <td data-drop="copy"></td>
                        <td data-drop="copy"></td>
                    </tr>
                    <tr>
                        <td data-drop="copy"></td>
                        <td data-drop="copy"></td>
                        <td data-drop="copy"></td>
                        <td data-drop="copy"></td>
                        <td data-drop="copy"></td>
                    </tr>
                    <tr>
                        <td data-drop="copy"></td>
                        <td data-drop="copy"></td>
                        <td data-drop="copy"></td>
                        <td data-drop="copy"></td>
                        <td data-drop="copy"></td>
                    </tr>
                </tbody>
js 复制代码
    // 拖拽进入
    container.addEventListener('dragenter', e => {
        const dropNode = getDropNode(e.target);
        if (dropNode && source && dropNode.dataset.drop === source.dataset.effect) {
            dropNode.classList.add('hover-background');
        }
    });
    
    // 查找最近的放置节点
    function getDropNode(node) {
        while (node) {
            if (node.dataset && node.dataset.drop) return node;
            node = node.parentNode;
        }
        return null;
    }

此时会发现,所有经过的放置元素背景色都发生了变化

ondragover:在放置元素上方,需要做以下处理

  • 阻止放置元素的默认行为,避免放置失败
js 复制代码
    // 拖在元素上方时,阻止默认行为,避免放置失败
    container.addEventListener('dragover', e => e.preventDefault());

dragleave:离开放置元素,需要做以下处理

  • 查找最近的放置节点
  • 清除元素背景色
js 复制代码
    // 拖拽离开
    container.addEventListener('dragleave', e => {
        const dropNode = getDropNode(e.target);
        if (dropNode) dropNode.classList.remove('hover-background');
    });

ondrop:松手放置时对放置元素的处理

  • 清除hover时的背景样式
  • 查找最近的放置节点
  • 【新增课程】清除放置元素内的内容;克隆拖拽元素;设置克隆后的元素data-effect属性只为move;恢复拖拽元素本身的样式;添加到放置元素中
  • 【移除课程】将左侧区域设置自定义属性,作为为可移除区间,然后删除掉拖拽元素
HTML 复制代码
        <div class="left" data-drop="move">
            <div data-effect="copy" draggable="true" style="background:rgb(26, 231, 156)">语文</div>
            <div data-effect="copy" draggable="true" style="background:rgb(107, 219, 15)">数学</div>
            <div data-effect="copy" draggable="true" style="background:rgb(208, 133, 13)">英语</div>
            <div data-effect="copy" draggable="true" style="background:rgb(30, 98, 246)">物理</div>
            <div data-effect="copy" draggable="true" style="background:rgb(210, 40, 113)">化学</div>
            <div data-effect="copy" draggable="true" style="background:rgb(210, 224, 26)">生物</div>
        </div>
        </div>
js 复制代码
    // 放下
    container.addEventListener('drop', e => {
        // 清除hover时的样式
        clearHoverClass();
        // 获取最近的放置节点
        const dropNode = getDropNode(e.target);
        if (dropNode && source && dropNode.dataset.drop === source.dataset.effect) {
            // 如果是新增课程
            if (dropNode.dataset.drop === "copy") {
                dropNode.innerHTML = "";
                source.style.opacity = "1";
                const cloned = source.cloneNode(true);
                cloned.dataset.effect = "move";
                dropNode.appendChild(cloned);
            } else {
                // 移除课程
                source.remove();
            }
        }
    });

    function clearHoverClass() {
        document.querySelectorAll('.hover-background').forEach(ele => ele.classList.remove("hover-background"));
    }

dragend:拖拽元素结束拖拽,把保存DOM的变量变为null

js 复制代码
    // 拖拽结束
    container.addEventListener('dragend', e => {
        if (source) source = null;
    });

四、参考代码

HTML 复制代码
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>课程表拖拽</title>
    <style>
        body {
            margin: 0;
            padding: 0;
            font-family: sans-serif;
            background: #f7f7f7;
        }

        .content {
            display: flex;
            justify-content: center;
            align-items: flex-start;
            gap: 40px;
            padding: 40px;
            max-width: 1200px;
            margin: 40px auto;
        }

        .left {
            display: flex;
            flex-direction: column;
            gap: 18px;
            background: #fff;
            padding: 24px 18px;
            border-radius: 10px;
            box-shadow: 0 2px 8px rgba(0, 0, 0, 0.07);
            min-width: 120px;
        }

        div[draggable="true"] {
            padding: 14px 0;
            text-align: center;
            border-radius: 6px;
            color: #fff;
            font-weight: bold;
            font-size: 16px;
            cursor: grab;
            box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
            user-select: none;
            transition: opacity 0.2s;
        }

        .hover-background {
            outline: 2px dashed #4f8cff;
            background: #e6f0ff !important;
        }

        .right {
            background: #fff;
            padding: 24px;
            border-radius: 10px;
            box-shadow: 0 2px 8px rgba(0, 0, 0, 0.07);
        }

        table {
            border-collapse: collapse;
            width: 600px;
        }

        thead td {
            background: #e9ecef;
            font-weight: bold;
            text-align: center;
            padding: 10px 0;
            border-bottom: 2px solid #d1d5db;
            font-size: 16px;
        }

        tbody td {
            width: 120px;
            height: 60px;
            border: 1px solid #e0e0e0;
            text-align: center;
            font-size: 15px;
            background: #fafbfc;
            transition: background 0.2s;
        }
    </style>
</head>

<body>
    <div class="content">
        <div class="left" data-drop="move">
            <div data-effect="copy" draggable="true" style="background:rgb(26, 231, 156)">语文</div>
            <div data-effect="copy" draggable="true" style="background:rgb(107, 219, 15)">数学</div>
            <div data-effect="copy" draggable="true" style="background:rgb(208, 133, 13)">英语</div>
            <div data-effect="copy" draggable="true" style="background:rgb(30, 98, 246)">物理</div>
            <div data-effect="copy" draggable="true" style="background:rgb(210, 40, 113)">化学</div>
            <div data-effect="copy" draggable="true" style="background:rgb(210, 224, 26)">生物</div>
        </div>
        <div class="right">
            <table>
                <thead>
                    <tr>
                        <td>星期一</td>
                        <td>星期二</td>
                        <td>星期三</td>
                        <td>星期四</td>
                        <td>星期五</td>
                    </tr>
                </thead>
                <tbody>
                    <tr>
                        <td data-drop="copy"></td>
                        <td data-drop="copy"></td>
                        <td data-drop="copy"></td>
                        <td data-drop="copy"></td>
                        <td data-drop="copy"></td>
                    </tr>
                    <tr>
                        <td data-drop="copy"></td>
                        <td data-drop="copy"></td>
                        <td data-drop="copy"></td>
                        <td data-drop="copy"></td>
                        <td data-drop="copy"></td>
                    </tr>
                    <tr>
                        <td data-drop="copy"></td>
                        <td data-drop="copy"></td>
                        <td data-drop="copy"></td>
                        <td data-drop="copy"></td>
                        <td data-drop="copy"></td>
                    </tr>
                    <tr>
                        <td data-drop="copy"></td>
                        <td data-drop="copy"></td>
                        <td data-drop="copy"></td>
                        <td data-drop="copy"></td>
                        <td data-drop="copy"></td>
                    </tr>
                </tbody>
            </table>
        </div>
    </div>
</body>
<script>
    const container = document.querySelector('.content');
    let source = null;

    // 拖在元素上方时,阻止默认行为,避免放置失败
    container.addEventListener('dragover', e => e.preventDefault());

    // 拖拽开始
    container.addEventListener('dragstart', e => {
        if (!e.target.draggable) return;
        // 如果是移除操作,则设置手势为移除
        if (e.target.dataset.effect === "move") {
            e.dataTransfer.effectAllowed = "move";
        }
        source = e.target;
        // 设置拖拽元素的样式
        e.target.style.opacity = '0.2'
    });

    // 拖拽进入
    container.addEventListener('dragenter', e => {
        const dropNode = getDropNode(e.target);
        if (dropNode && source && dropNode.dataset.drop === source.dataset.effect) {
            dropNode.classList.add('hover-background');
        }
    });

    // 拖拽离开
    container.addEventListener('dragleave', e => {
        const dropNode = getDropNode(e.target);
        if (dropNode) dropNode.classList.remove('hover-background');
    });

    // 拖拽结束
    container.addEventListener('dragend', e => {
        if (source) source = null;
    });

    // 放下
    container.addEventListener('drop', e => {
        // 清除hover时的样式
        clearHoverClass();
        // 获取最近的放置节点
        const dropNode = getDropNode(e.target);
        if (dropNode && source && dropNode.dataset.drop === source.dataset.effect) {
            // 如果是新增课程
            if (dropNode.dataset.drop === "copy") {
                dropNode.innerHTML = "";
                source.style.opacity = "1";
                const cloned = source.cloneNode(true);
                cloned.dataset.effect = "move";
                dropNode.appendChild(cloned);
            } else {
                // 移除课程
                source.remove();
            }
        }
    });

    // 查找最近的放置节点
    function getDropNode(node) {
        while (node) {
            if (node.dataset && node.dataset.drop) return node;
            node = node.parentNode;
        }
        return null;
    }

    function clearHoverClass() {
        document.querySelectorAll('.hover-background').forEach(ele => ele.classList.remove("hover-background"));
    }
</script>

</html
相关推荐
恋猫de小郭19 分钟前
Flutter 状态管理基准测评,一个很有趣的观点
android·前端·flutter
雪芽蓝域zzs24 分钟前
前端编辑组件wangEditor
前端
风骏时光牛马1 小时前
稳定性治理及疑难问题深度剖析与实战复盘
前端
JeffongTan9 小时前
在LWC中镶嵌VF Page获取用户IP
前端·javascript·salesforce
陆枫Larry11 小时前
Astro 是什么
前端
是立不是利13 小时前
写给设计师的 CSS 博客美学指南
前端·css
默_笙14 小时前
🏝 Docker 就是"房地产开发":从施工图纸到小区物业的容器化指南
前端·javascript
计算机魔术师14 小时前
Rohan Paul 谈用时间跨度衡量智能体能力,并引用 OpenAI 自动化研究实习生里程碑
前端
kyriewen14 小时前
我手写了个极简版 React Router——才搞懂 v6 为什么砍了这么多 API
前端·javascript·程序员
a11177614 小时前
Shirone 二次元博客主题 开源
前端·开源