绑定完请刷新页面
取消
刷新

分享好友

×
取消 复制
看完就懂的前端拖拽那些事
2022-04-24 16:09:47


作者:灰兔呀

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[]} ${sourceNode.position[1]} 
            C
            ${(targetNode.position[]  + sourceNode.position[])/2} ${sourceNode.position[1]}
            ${(targetNode.position[]  + sourceNode.position[])/2} ${targetNode.position[1]}
            ${targetNode.position[]} ${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]



分享好友

分享这个小栈给你的朋友们,一起进步吧。

趣谈前端
创建时间:2020-07-15 17:32:01
一个重度代码洁癖者,有对前端生态的总结,思考和探索。内容涵盖了笔者多年对vue,react,node,webpack以及javascript框架设计的探索和经验。公众号 - 趣谈前端
展开
订阅须知

• 所有用户可根据关注领域订阅专区或所有专区

• 付费订阅:虚拟交易,一经交易不退款;若特殊情况,可3日内客服咨询

• 专区发布评论属默认订阅所评论专区(除付费小栈外)

栈主、嘉宾

查看更多
  • xujiang
    栈主

小栈成员

查看更多
  • ?
  • victoria_ltt
  • asdjlk1
  • LCR_
戳我,来吐槽~