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
相关推荐
安木夕14 分钟前
C#-Visual Studio宇宙第一IDE使用实践
前端·c#·.net
努力敲代码呀~16 分钟前
前端高频面试题2:浏览器/计算机网络
前端·计算机网络·html
高山我梦口香糖37 分钟前
[electron]预脚本不显示内联script
前端·javascript·electron
神探小白牙38 分钟前
vue-video-player视频保活成功确无法推送问题
前端·vue.js·音视频
Angel_girl3191 小时前
vue项目使用svg图标
前端·vue.js
難釋懷1 小时前
vue 项目中常用的 2 个 Ajax 库
前端·vue.js·ajax
Qian Xiaoo1 小时前
Ajax入门
前端·ajax·okhttp
爱生活的苏苏2 小时前
vue生成二维码图片+文字说明
前端·vue.js
拉不动的猪2 小时前
安卓和ios小程序开发中的兼容性问题举例
前端·javascript·面试
炫彩@之星2 小时前
Chrome书签的导出与导入:步骤图
前端·chrome