HTML5拖放API原理——看了不后悔

297 阅读6分钟

拖放API

拖放API是HTML5提供的原生功能,允许用户通过鼠标或触摸操作拖拽元素并放置到目标区域。核心流程包括:设置draggable属性、通过dragstart事件传输数据(setData)、在目标区域阻止dragover默认行为,最后通过drop事件接收数据(getData)。适用于文件上传、任务排序等交互场景,兼容现代浏览器,是实现直观拖放操作的基础方案。

首先是对坐标系的认识:

显示器坐标系:screenX,screenY,计算机屏幕左上顶角为原点,定位x轴坐标,y轴坐标

浏览器坐标系:clientX,clientY,X,Y以浏览器窗口左上顶角为原点,定位x轴坐标,y轴坐标

文档坐标系:pageX,pageY,以document对象(即文档窗口)左上顶角为原点。定位x轴坐标,y轴坐标

元素坐标系:offsetX,offsetY,以当前事物的目标对象左上顶角为原点,定位x轴坐标,y轴坐标

layerX,layerY,最近的绝对定位的父元素(如果没有则为document对象)左上顶角为原点,定位x轴坐标,y轴坐标

js原生实现拖动操作

原理

这个JS原生拖动实现原理是:

  1. 监听鼠标按下:在目标元素上监听mousedown事件,记录鼠标点击位置与元素左上角的偏移量(disx/disy)

  2. 跟随鼠标移动

    • 在文档上添加mousemove监听
    • 计算新位置 = 鼠标当前坐标 - 初始偏移量
    • 实时更新元素的left/top样式
  3. 释放鼠标时:移除move/up事件监听,结束拖动

核心是通过计算鼠标位置与元素位置的差值实现精准跟随,利用DOM样式实时更新视觉效果。

鼠标事件

mousedown:用户按下鼠标开始操作

mousemove:如果鼠标没有松开,则开始移动操作

mouseup:释放鼠标按键

实现代码:

<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>js原生实现拖动</title>
    <style>
        .dragbox {
            width: 100px;
            height: 100px;
            background-color: red;
            position: absolute;
            left: 10px;
            top: 0;
        }
    </style>
</head>
<body>
    <div class="dragbox">
    </div>
    <script>
        //获取dragbox元素
        let dragbox=document.getElementsByClassName('dragbox')[0];
        //当鼠标在目标范围里按下后触发事件
        dragbox.addEventListener('mousedown',function(e){
            //获取点击位置离dragbox的左上角坐标的距离
            let disx=e.pageX-dragbox.offsetLeft;//注意不能用offsetx,因为offsetx不是元素的属性,offsetLeft是元素的左偏移量。
            let disy=e.pageY-dragbox.offsetTop;
            //可以用let disx=e.offsetX;
            //let disy=e.offsetY;替代
            
            //移动事件和鼠标松开事件后的操作
            document.addEventListener('mousemove',move);
            document.addEventListener('mouseup',up);
​
            //move函数,在鼠标移动时随时获得其移动坐标并渲染到页面上
            function move(e){
                let left=e.pageX-disx;
                let top=e.pageY-disy;
                dragbox.style.left=left+'px';
                dragbox.style.top=top+'px';
            }
            //up函数,鼠标松开时清除事件监听操作
            function up(){
                document.removeEventListener('mousemove',move);
                document.removeEventListener('mouseup',up);
            }
​
        });
    </script>
</body>
</html>

拖放原理

默认可拖动元素:图像img、超链接a默认可拖动,其余元素若想要被拖动必须加上draggable属性,并设置为true。

浏览器对拖放的默认行为:在用户松开鼠标完成拖放操作时,如果没有将元素拖到任何一个放置目标中,则浏览器会在浏览器窗口打开拖动元素表示的链接(如果有),可以在放置目标中调用e.preventDefault()禁用此种行为。

dragEvent

拖放事件触发条件
dragtsart准备拖动被拖动元素时触发
drag拖动的过程中触发(频繁触发)
dragend拖动结束时触发
dragenter被拖放元素进入目标元素时触发
dragleave被拖动元素离开目标元素时触发
dragover被拖放元素在目标元素内时触发(频繁触发)
dragover事件的默认行为是阻止放置目标的任何响应。如果没有在处理函数中调用event.preventDefault(),浏览器会阻止放置目标对于拖动操作的任何响应
drop当被拖动元素被放到了目标元素中时触发

被拖动元素支持的拖放事件

dragstart,drag,dragend

目标元素支持的拖放事件

dragenter,dragover,dragleave,drop

e.dataTransfer作用:从被拖放元素向目标元素传递源数据

使用方法

e.dataTransfer.setData(type,data)设置源数据,并设置数据类型,一般在dragstart事件里读写数据

data支持的数据类型: 1.字符串 2.二进制

e.dataTransfer.getData(type)通过类型获取源数据,一般在drop事件里获取数据

type的作用:

type参数即MIME类型,MIME类型是一种标化资源的类型格式,比如文本(text/plain)、图片(inage/x)、文件(application/x)等,通过这种标准化格式,就可以解析出拖拽源的类型,方便后续的处理。

孪生属性:

e.dataTransfer.effectAllowed() 属性只能用于指示哪些类型的行为被允许,在源上进行设置。给用户一个标识例如e.dataTransfer.effectAllowed('link')拖进目标元素后会出现一个箭头

注意:effectAllowed属性一般只能在dragstart事件中设置

类型效果
none代表无法将收集到的数据放置到接受元素上,不允许任何操作
copy代表在接收元素上显示复制效果
move代表在目标元素上显示移动效果
link代表在目标元素上显示连接跳转效果
copyLink允许复制或链接
copyMove允许复制或移动
linkMove允许链接或移动
all允许所有操作
uninitialized尚未设置effectAllowed属性时的默认值,等同于all

e.dataTransfer.dropEffect(),只能用于指示哪些类型的行为是推荐的,在目标上进行设置。与e.dataTransfer.effectAllowed() 相对应,也可以不写,但是如果写了就必须要相同,否则就无法实现放置功能。类似于锁与钥匙,必须配对,但是不写是默认配对。

注意:dropEffect属性可以在dragenter和dragover事件中设置

拖拽流程图

拖拽流程图.jpg

实现拖放的必要操作

1.拖动源上的draggable属性为true

2.放置目标上的dragover内需执行e.preventDefault()

3.拖动源的类型必须与拖放目标能够接受的类型相匹配

4.拖动源的effectAllowed属性值必须包含拖放目标dropEffect所支持的最佳行为类型

实战练习:
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>实现拖放</title>
    <style>
        .text {
            width: 50px;
            height: 50px;
            background-color: antiquewhite;
            line-height: 50px;
            text-align: center;
        }
        .drag {
            width: 200px;
            height: 200px;
            border: 2px solid red;
            margin: 50px auto;
            display: flex;
            flex-wrap: wrap;
            padding: 10px;
            gap: 10px;/*设置元素左右间距*/
            overflow: auto;/*内容过多时显示滚动条*/
        }
    </style>
</head>
<body>
    <div class="text" draggable="true">你好</div>
    <div class="drag"></div>
    <script>
        let dragtarget=document.getElementsByClassName('drag')[0];
        let dragsource=document.getElementsByClassName('text')[0];
        var img =new Image();
        img.src="小.png";
        img.width=1;
        img.height=1;
        //dragstart拖动开始
        dragsource.addEventListener('dragstart',function(e){
            e.dataTransfer.effectAllowed='copy';
            e.dataTransfer.setData('text/plain',dragsource.outerHTML);
            console.log('start');
            e.dataTransfer.setDragImage(img,5,5);
        })
        //dragend停止拖动元素
        dragsource.addEventListener('dragend',function(){
            dragtarget.style.border='2px solid blue';
            console.log('end');
        })
        //dragover,在放目标里面
        dragtarget.addEventListener('dragover',function(e){
            e.preventDefault();
            e.dataTransfer.dropEffect='copy';
            dragtarget.style.border='2px solid green';
            // console.log('dragover');
        })
        //drop,拖动元素放到了目标框里
        dragtarget.addEventListener('drop',function(e){
            e.preventDefault();
            let data=e.dataTransfer.getData('text/plain');
            dragtarget.innerHTML+=data;
            console.log('drop');
        })
        //从目标框移除元素
        dragtarget.addEventListener('dragleave',function(e){
            if(e.target.classList.contains('text')){
                e.target.remove();
            }
        })
    </script>
</body>
</html>
文件拖放实战:
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>本地文件拖放</title>
    <style>
        #field{
            width: 200px;
            height: 100px;
            margin: 20px auto;
        }
        .staus {
            margin: 20px auto;
            width: 400px;
            text-align: center;
        }
    </style>
</head>
<body>
    <fieldset id="field">
        <legend>请将文件托放入此</legend>
​
    </fieldset>
    <div class="staus"></div>
    <script>
        //获取目标区域
        let field=document.getElementById('field');
        //阻止浏览器默认行为打开
        field.addEventListener('dragover',function(e){
            e.preventDefault();
            this.style.border='3px solid red';
        })
        //在放置结束后渲染页面,阻止默认行为,获取文件数据
        field.addEventListener('drop',function(e){
            e.preventDefault();
            let fielslist=e.dataTransfer.files;
            let fiels='';
            let fiel;
            for (let i = 0; i < fielslist.length; i++) {
                fiel=fielslist[i];
                var lastmodified=fiel.lastModifiedDate;
                var lastmodifiedstr=lastmodified.toLocaleString();
                fiels+='<li>文件名称:'+fiel.name+'<br>文件类型'+fiel.type+'<br>文件大小:'+fiel.size+'字节'+'<br>修改时间:'+lastmodifiedstr+'</li>'
                
            }
            document.getElementsByClassName('staus')[0].innerHTML+='<ul>'+fiels+'</ul>';
        })
    </script>
</body>
</html>

尾注:如有错误,还请指正