作者:灰兔呀

https://juejin.cn/post/7075918201359433758

前言

最近没有更新文章,因为去字节实习了一阵,实在是没有精力写东西,所以就咕咕咕了。现在回学校了,就可以继续更新啦,因为在字节做的业务和图可视化还有拖拽关系比较大,所以这次就写下拖拽相关的内容。

HTML5 Drag and Drop 接口

html5中提供了一系列Drag and Drop 接口,主要包括四部分:DragEventDataTansferDataTransferItemDataTransferItemList

DragEvent

源元素和目标元素

image-20220314095928431.png

**源元素:**即被拖拽的元素。

**目标元素:**即合法的可释放元素。

每个事件的事件主体都是两者之一。

拖拽事件

事件 事件处理程序 事件主体 触发时机
dragstart ondragstart 源元素 当源元素开始被拖拽。
drag ondrag 源元素 当源元素被拖拽(持续触发)。
dragend ondragend 源元素 当源元素拖拽结束(鼠标释放或按下esc键)
dragenter ondragenter 目标元素 当被拖拽元素进入该元素。
dragover ondragover 目标元素 当被拖拽元素停留在该元素(持续触发)。
dragleave ondragleave 目标元素 当被拖拽元素离开该元素。
drop ondrop 目标元素 当拖拽事件在合法的目标元素上释放。

触发顺序及次数

我们绑定相关的事件,拖放一次来查看相关事件的触发情况。

op.gif

我们让相应事件处理程序打印事件名称及事件触发的主体是谁,下面截取部分展示。

image-20220314103603324-16473183157994.png

我们可以看到对于被拖拽元素,事件触发顺序是 dragstart->drag->dragend;对于目标元素,事件触发的顺序是 dragenter->dragover->drop/dropleave

其中dragdragover会分别在源元素和目标元素反复触发。整个流程一定是dragstart第一个触发,dragend最后一个触发。

这里还有一个注意的点,如果某个元素同时设置了dragoverdrop的监听,那么必须阻止dragover的默认行为,否则drop将不会被触发。

image-20220314111402189.png

DataTansfer

我们先用一张图来直观的感受一下:

image-20220315122157204-16473183290157.png

我们可以看到,DataTransfer如同它的名字,作用就是在拖放过程中对数据进行传输,其中setData用来存放数据,getData用来获取数据,出于安全的考量,数据只能在drop时获取,而effectAlloweddropEffect则影响鼠标展示的样式,下面我们用一个例子来进行展示:

sourceElem.addEventListener('dragstart', (event) => {event.dataTransfer.effectAllowed = 'move';event.dataTransfer.setData('text/plain', '放进来了');
});
targetElem.addEventListener('dragover', (event) => {event.preventDefault();event.dataTransfer.dropEffect = 'move';
});
targetElem.addEventListener('drop', (event) => {event.target.innerHTML = event.dataTransfer.getData('text/plain');
});
复制代码

drag2.gif

可以看到在蓝色方块设置的数据被成功取得了。

DataTransferItemList

属性

length: 列表中拖动项的数量。

方法

add(): 向拖动项列表中添加新项 (File对象或String),该方法返回一个 DataTransferItem) 对象。

remove(): 根据索引删除拖动项列表中的对象。

clear(): 清空拖动项列表。

DataTransferItem(): 取值方法:返回给定下标的DataTransferItem对象.

DataTransferItem

属性

kind: 拖拽项的种类,string 或是 file

type: 拖拽项的类型,一般是一个MIME 类型。

方法

getAsString: 使用拖拽项的字符串作为参数执行指定回调函数。

getAsFile: 返回一个关联拖拽项的 File 对象 (当拖拽项不是一个文件时返回 null)。

实践

学习了上面的基础知识,我们从几个常见的应用场景入手,来实践上面的知识

可放置组件

知道上面几个事件后,我们来完成一个简单可放置组件,为了方便大家理解,这里不使用任何框架,以免增加不会框架同学的学习成本。

想让组件可拖行,那么就要可以改变它的位置,有两种思路:

  • pos:abs通过top/left等直接改变元素的位置

  • 使用css的transform属性中的translate对元素的位置进行改变

我推荐第二种,首先translate是基于本身的移动,因此自身的坐标就作为原点(0,0),但是第一种,元素本身的top/left等可能并不为0,计算起来比较复杂。其次,第一种是通过cpu去计算,而第二种是通过gpu去计算,并且会提升到一个新的层,这样做非常有利于页面的性能。原因是 Chrome 这样将 DOM 转变成一个屏幕图像:

  1. 获取 DOM 并将其分割为多个层

  2. 将层作为纹理上传至 GPU

  3. 复合多个层来生成最终的屏幕图像。

但更新的帧可以走捷径,不必经历所有过程:

如果某些特定 CSS 属性变化,并不需要发生重绘。Chrome 可以使用早已作为纹理而存在于 GPU 中的层来重新复合,但会使用不同的复合属性(例如,出现在不同的位置,拥有不同的透明度等等)。

如果图层中某个元素需要重绘,那么整个图层都需要重绘 。所以提升为一个新的层,可以减少重绘的次数。因为只改变位置,所以可以复用纹理,提高性能。

更详细的可以看我的另一篇文章:浏览器事件循环与渲染机制 \- 掘金 \(juejin.cn\)[1]

有了思路那么我们就开始吧!

首先我们要知道这次拖拽的向量是怎样的,因为DragEvent继承自MouseEvent ,所以我们可以通过MouseEvent接口的offsetX属性和offsetY属性获取鼠标现在相对于该物体的位置差。而transform设置多个属性值,效果就可以叠加,所以我们要获得之前的移动效果,再加上现在的移动效果即可,之前的移动效果可以通过window.getComputedStyle(e.target).transform获得。

sourceElem.addEventListener('dragend', (e) => {const startPosition = window.getComputedStyle(e.target).transform;e.target.style.transform = `${startPosition} translate(${e.offsetX}px, ${e.offsetY}px)`;
}, true);
复制代码

我们给要拖拽的元素加上这段处理程序似乎就大功告成了。

wrong.gif

但是实际使用时,这个元素并没有停在预览的位置,而是左上角移动到鼠标的位置,显然不符合预期,相信大家都能猜到,我们少考虑了鼠标在元素的位置,而鼠标初始的位置同样可以通过MouseEvent接口的offsetX属性和offsetY属性获取(dragstart)。改善如下:

function enableDrag(element) {let mouseDiff = null;element.addEventListener('dragstart', (e) => {//初始时鼠标与元素的位置差mouseDiff = `translate(${-e.offsetX}px, ${-e.offsetY}px)`}, true);element.addEventListener('dragend', (e) => {//开始时元素的位置const startPosition = window.getComputedStyle(e.target).transform;//鼠标移动的位置const mouseMove = `translate(${e.offsetX}px, ${e.offsetY}px)`;e.target.style.transform = `${mouseDiff} ${startPosition} ${mouseMove}`;}, true);
}
enableDrag(souceElement);
复制代码

drag.gif

图的连线

节点使用DOM渲染,连线我们使用SVG来渲染,框架使用React,但除了state尽量使用较少的框架相关的,以防非React技术栈的同学看不懂。

首先我们要先组织我们的state,作为一个图,显然应该由nodesedges两部分组成,我们都使用数组存储,我们给每个node一个唯一的id,使用Map去映射id与对应的positon 形如 [x,y]的关系,而edge有源端与终端的id,通过id去获得对应的坐标。

我们先假设节点可以完成所有功能了,只考虑连线,可以定义如下的组件

const Edge = ({nodes:[sourceNode,targetNode]}) =>(<svg key={sourceNode.id + targetNode.id||''} style={{position:'absolute',overflow:'visible',zIndex:'-1',transform:'translate(15px,15px)'}}><path d={`M ${sourceNode.position[0]} ${sourceNode.position[1]} C${(targetNode.position[0]  + sourceNode.position[0])/2} ${sourceNode.position[1]}${(targetNode.position[0]  + sourceNode.position[0])/2} ${targetNode.position[1]}${targetNode.position[0]} ${targetNode.position[1]} `}strokeWidth={6}stroke={'red'}fill='none'></path></svg>
)
复制代码

首先我们应该从什么时候生成一个连线呢,显然是dragstart,但这时还没有对应的终端,因此不应该通过加入edges来循环渲染,而是单独渲染一个出来。在dragstart我们设置一个虚拟节点temNode,并记录开始节点的id。

并如果有temNode则渲染一条预览的edge。

temNode && (<Edge nodes = {[sourceNode,temNode]}></Edge>)
复制代码

drag3.gif

然后加入这条edge后,我们删除虚拟节点,变为循环渲染来展示所有的边。

edges.map(([sourceId,targetId])=>{const sourceNode = getNode(sourceId);const targetNode = getNode(targetId);return (<Edge nodes = {[sourceNode,targetNode]}></Edge>);
})
复制代码

那么我们只考虑边的展示了,节点的功能应该如何完善呢?

首先是dragstart,我们要设置起始节点和虚拟节点

onDragStart={()=>{setStartNodeId(uid);setTemNode({position:[x,y]})
}}
复制代码

然后既然边可以跟着动,我们必然要在drag中动态的改变虚拟节点的位置

onDrag={(event)=>{position=[x+event.nativeEvent.offsetX,y+event.nativeEvent.offsetY];setTemNode({position})
}}
复制代码

然后drop时,我们加入一条新的边

onDrop={(event)=>{event.preventDefault();setEdges(edges.concat([[startNodeId,uid]]))}
}
复制代码

最重要的是,不论在哪里事件结束了,要删除虚拟节点

onDragEnd={()=>{setTemNode(null);}
}
复制代码

下面是最终成果:

drag4.gif

参考

  • HTML Drag and Drop API - Web APIs | MDN \(mozilla.org\)[2]

【Web技术】1305- 看完就懂的前端拖拽那些事相关推荐

  1. 网络通过猫传输到计算机,网络直接从光猫出来好还是接个路由器再接入电脑好?看完搞懂了...

    网络直接从光猫出来好还是接个路由器再接入电脑好?看完搞懂了 宽带网络现在是家家户户不可缺少的"硬件"之一,现在即便是老一辈的人家中安装宽带都成了必需品.有些偏好用电脑来上网的朋友可 ...

  2. 看完弄懂,明年至少加 5K

    看完弄懂,明年至少加 5K

  3. 高铁、动车到底啥区别?看完彻底懂了(组图)

    摘自:网易新闻 (原标题:高铁.动车到底啥区别?看完彻底懂了(组图)) 高铁与动车的区别到底在哪里?磁悬浮列车又是什么鬼?今天给你讲讲清楚! 高铁.动车到底啥区别?看完彻底懂了 一.普通列车与高铁钢轨 ...

  4. 华为mate10pro以后能上鸿蒙吗,华为Mate10和Mate10 Pro差别一览 怎么选看完就懂

    华为Mate10和Mate10 Pro差别一览 怎么选看完就懂上周五华为正式发布了今年的两款重磅旗舰Mate10和Mate10 Pro.与上代产品不同,此次Mate10系列的两款产品无论是在外观还是一 ...

  5. 新手入门,数控刀具上的代码怎么认?看完就懂了!

    新手入门,数控刀具上的代码怎么认?看完就懂了! 按照不同的刀具类型对刀具分组: 类别组1 xxyyy(铣刀类): 110 球面铣刀 (圆柱型铣刀,其后的字母y代表铣刀直径,以下略同) 120 立铣刀 ...

  6. 为什么会显示有人正在使用计算机,微信“对方正在输入”为什么有时出现?有时不出现?看完才懂了.....

    原标题:微信"对方正在输入"为什么有时出现?有时不出现?看完才懂了.. 生活中有很多美好的事情 手机电量满格 您的快递正在派送 换季衣服里翻出毛爷爷 与喜欢的人聊天显示" ...

  7. java开发用i5还是i7,i7比i5更强!为什么内行人都选i5而不选i7?看完瞬间懂了

    i7比i5更强!为什么内行人都选i5而不选i7?看完瞬间懂了 2020-11-19 11:18:08 4点赞 0收藏 0评论 许多人认为i7比i5更好,那么有什么好呢?让我们先看一下区别. i7使用四 ...

  8. Callable和Runnable的区别(面试常考),看完就懂

    Callable和Runnable的区别(面试常考),看完就懂 Callable 接口 测试类 Runnable 接口 测试类 两者的区别 补充Executor框架 Callable 接口 publi ...

  9. android 7 plus,手机别瞎买,iPhone7plus相当于什么档次的安卓机?看完就懂了!

    手机别瞎买,iPhone7plus相当于什么档次的安卓机?看完就懂了! 作为此前一直比较受欢迎的苹果手机,一直是安卓手机的大力比拼的对象,目前已经有的安卓手机能够在拍照的性能上超过苹果手机了,虽然说在 ...

最新文章

  1. Linux系统无线鼠标不能用,手把手教你win7系统无线鼠标不能用的处理方案
  2. KVM 虚拟化原理探究--启动过程及各部分虚拟化原理
  3. 微软Azure已开始支持hadoop--大数据云计算
  4. 使用github pages创建博客
  5. Nodejs学习笔记(一)——基础之全局对象、包和npm
  6. 问题 C: 判断三角形的性质
  7. 数据库原理—数据库基础(二)
  8. 一个C++的ElasticSearch Client
  9. 计算机图形学前沿领域的设想,计算机图形学
  10. Java编程语言是什么
  11. linux chm 阅读器,linux下最好的chm阅读器KchmViewer,安装使用/与oklular,xCHM,gnochm简单比较...
  12. 使用pip出现报错:Could not find a version that satisfies the...No matching distribution distributio...
  13. SpringBoot使用Jib将应用快速打包成Docker镜像
  14. 从零构建神经网络-实现异或门操作
  15. 块存储、文件存储、对象存储这三者的差别
  16. 懂计算机能驾驶的月嫂薪水高
  17. 百度云视频 在线倍速播放
  18. Python实现世界人口地图
  19. Virtual box安装回退的一系列可能的原因及解决办法
  20. 基于js的网页计算器实现

热门文章

  1. error C2226: 语法错误 : 意外的“LPSTR”类型
  2. javascript --- 设计模式之构造函数模式
  3. 青狐云网盘搭建-支持会员功能-支持对接阿里云存储
  4. 百面机器学习—7.K均值算法、EM算法与高斯混合模型要点总结
  5. 游戏史上被迫修改服务器玩家,直接改变游戏的四次更新,为了挽回败局,炸掉整个服务器...
  6. 【热文】为什么比尔盖茨,马斯克、霍金都告诉你:要警惕人工智能(下)
  7. 离散数学【02】——基本结构:集合、函数、序列等
  8. base64stego的writeup
  9. jenkins 安装及配置部署操作 (jenkins+svn+tomcat and jenkins+git+maven+tomcat)
  10. Vue. 之 报错 Uncaught (in promise)