基础
原生拖拽
被拖拽的元素需要有draggable属性。
当点击该元素,并开始拖拽,在这一瞬间,触发dragStart事件。
移动时,触发onDragOver事件以及Drag事件。
松开时,触发onDrop事件。
<div
draggable
class="max-w-sm mx-auto bg-gradient-to-br from-blue-100 to-purple-100 rounded-xl shadow-lg overflow-hidden flex items-center space-x-4 p-6 hover:transform hover:scale-105 transition-all duration-300"
onDragStart={(e) => handleDragStart(e, card.id)}
onDragOver={handleDragOver}
>
<img class="h-16 w-16 rounded-full object-cover shadow-xl" src={card.avatar} alt={`${card.name}的头像`} />
<div class="animate-fade-in">
<div class="text-xl font-bold text-indigo-600">{card.name}</div>
<div class="text-purple-500 font-medium">{card.position}</div>
<div class="mt-2 text-sm text-gray-600 hover:text-indigo-500 transition-colors duration-300">热爱编程,喜欢探索新技术。</div>
</div>
</div>
要看到拖动效果,有两个条件:
- 设置
draggable="true"。 - 触发dragOver事件时preventDefault()。
- 如果是火狐浏览器,还有第三个条件--
e.dataTransfer?.setData('text/plain', id.toString());
不过这样只是画面上来说有了拖拽效果。
使用cursor-move的tailwind样式可以在hover时让鼠标变成十字,让用户感觉到这是可以拖动的。
拖拽鼠标样式
none
copy
是一个加号,不太明显
move
默认就是move
link
拖拽样式问题
原生的拖拽,会让透明的被拖拽元素跟随鼠标。
问题就在于,这个透明度完全不能改,这样透明的拖拽非常不直观。
现在我们想要的是实心拖拽效果。
segmentfault.com/a/119000002…
这篇博客里面有提到一些思路。
- 使用下面这个方法设置空白元素作为拖拽跟随图片。
e.dataTransfer.setDragImage(clone, xOffset, yOffset); - 手动创建浮动元素跟随鼠标,在拖拽结束后手动去除。
虽然这个思路很好,但是在拖拽处理中
在被拖拽的元素上触发的事件
这些事件监听的是被拖动的元素的状态。
-
dragstart- 何时触发: 当用户开始拖拽一个元素时触发。
- 主要用途: 在这个事件中,你需要调用
event.dataTransfer.setData()来存储被拖拽元素的数据,以及设置event.dataTransfer.effectAllowed来指定允许的拖放效果(copy,move,link等)。
-
drag- 何时触发: 在整个拖拽过程中,只要鼠标移动,这个事件就会持续触发。
- 主要用途: 这个事件的开销比较大,通常用于实时更新拖拽时的视觉效果,比如自定义的浮动元素跟随鼠标移动。
-
dragend- 何时触发: 当拖拽操作结束时触发,无论是成功放置还是被取消。
- 主要用途: 在这个事件中,你可以进行清理工作,比如移除在
dragstart中添加的浮动元素或恢复样式。
在放置区域上触发的事件
这些事件监听的是鼠标悬停的元素的状态。
-
dragenter- 何时触发: 当被拖拽的元素进入一个有效的放置区域时触发。
- 主要用途: 用于给放置区域添加视觉反馈,比如改变背景颜色或边框,以提示用户可以放置在此处。
-
dragover- 何时触发: 当被拖拽的元素在放置区域内移动时,这个事件会持续触发。
- 主要用途: **这是最重要的事件之一。**你必须在这个事件中调用
event.preventDefault()来阻止浏览器的默认行为(默认是不允许放置),从而将该元素声明为一个有效的放置区域。如果不这样做,drop事件将永远不会触发。
-
dragleave- 何时触发: 当被拖拽的元素离开一个有效的放置区域时触发。
- 主要用途: 用于移除
dragenter事件中添加的视觉反馈,比如恢复放置区域的原始样式。
-
drop- 何时触发: 当用户在有效的放置区域内释放鼠标时触发。
- 主要用途: 在这个事件中,你需要调用
event.preventDefault()以避免浏览器打开拖拽内容(例如,如果拖拽的是文件)。然后,通过event.dataTransfer.getData()来获取在dragstart事件中存储的数据,并执行放置后的逻辑。
事件传染
父容器的拖拽事件自动传递给子元素,比如我的卡片可拖动,内部有两行文字一张图片,那么当我鼠标分别经过这三个子元素时,总共会触发3次DragEnter,这和我们预想中是不一样的。
这和冒泡是不一样的,冒泡是子元素绑定的事件和父容器绑定的事件一起发生,而此处我们没有设置子元素绑定事件。
因此,只能在事件绑定方法中,判断是否是由子元素触发的,如果是直接返回,啥也不做。
回退机制
当拖动到区域外时,我们默认不处理相关数据。这时我们不需要考虑回退机制,因为我们并没有做出直接修改。
合法再移除
而倘若存在两个区域,那么有两个数据列表,我们称为list1和list2。
当1移到2区域时,数据应该转移。
- 从list1中移除
- 添加到list2中
这样的逻辑是最简单的,而且不需要考虑非法区域时把元素放回去,因为我们本来就没删掉原来的元素。
而倘若存在多个区域,比如说4个。每多一个区域,就要在其他的每一个区域中加一个判断。
- 判断从哪个区域来的,从那个区域里面移除
- 添加到当前区域。
正常来说,立即移除应该比较符合用户的思路,这个元素都被我拖拽了,他就不该留在原地。
不过,如果在原地留下虚影,这也是符合用户操作逻辑的
符合逻辑的是:有合法落点时再执行移除和添加。
可以看看tiermaker的例子。
tiermaker.com/create/ceet…
核心逻辑
拖拽过程只有两个状态:
- 非法状态,当前鼠标所在位置不是落点区域。
- 合法状态,处于落点区域。
相关处理: - 非法状态,退回到最后一个合法区域。
- 合法状态,移除来源卡片元素,并将该卡片添加到目标落点区域数据中。
- 判断是否合法
- 将元素正确地放到落点区域数组中
删除来源元素:当前元素拖动到和来源区域不同的其他区域。(DragEnter)
插入元素(调顺序):当前元素在多个卡片之间来回拖动。(DragOver)
立即移除
倘若我们在元素离开区域1时,就直接把该元素去除,那么相应的,我们至少应该准备一个回退函数,用来处理拖拽到非法区域(比如未定义区域)时的回退逻辑,比如将其放回原位。
- 任意未定义区域,放回原位(执行回退逻辑)
- 任意可拖放区域,只处理新增逻辑。
好处是,落点区域完全不关心来源是什么,只关心拖拽的卡片本身。
坏处是,需要手动处理非法区域时的回退逻辑。
定义一个空的全局回退函数。
开始拖拽时(dragstart),覆盖回退函数(因为每个落点区域的逻辑不同)。
问题在于,drop是针对于某个元素监听,全局监听会影响到全部元素(感觉有性能问题)。
于是乎,我们难以检测在非法区域松开鼠标的行为。
事件
DragStart
真正的只触发一次的事件,我们可以用来自定义鼠标跟随元素。
结合Drag事件更新跟随元素的位置。
e.dataTransfer?.setData('text/plain', id.toString());
DragOver
排序功能主要依赖于DragOver事件,里面没有删除元素的逻辑。
DragEnter
删除的逻辑应该放在这里,而不是Leave,元素不该凭空消失。
由于事件会传播给子元素,因此应该判断是否处于容器分界点(而不是子元素)。
因此父容器和子元素之间最好存在空隙(padding之类的),但是子元素之间最好没有空隙。
由于存在间隙,所以从外移到内,一定是经过间隙的,而内部元素可以通过closest('.card')来排除。
然而,内部元素移到空白间隙时也会触发DragEnter事件。因此不应该把一次性执行的逻辑放在这。
因此DragEnter几乎是超低频率的DragOver了。
因此,如果要使用真正的边界进入(或者离开)的逻辑,应该直接计算坐标。
幸运的是,虽然在间隙和元素之间移动会触发Enter,但是在间隙中移动却不会,因此不会有执行多次这样的困扰。
或者,我们使用一个全局变量记录当前所在的区域,Enter触发时,判断当前区域和先前记录的区域是否一样,如果不一样,则触发某些逻辑。
也许记录区域的方法也许更好些。
我们可以直接用isValidDrag信号量来
DragLeave
由于前面提到的,子元素也会触发Leave,所以这个事件的意义不大。
只能通过计算来判断是否处于父容器边缘了。
到此处,我们通过Enter和Leave,监测鼠标是否处于非法位置。
而通过DragEnd事件监听,捕捉松开鼠标的动作。
至此,回退的逻辑已经可以使用了。
还有一些问题,当我们放在区域里面,但是又不处于任何一个卡片上,这时正确的逻辑应该是添加到卡片末尾。
如果使用状态标志,那么状态为合法、处于当前区域中、且不处于某个卡片的区域中。
此时其实可以发现,那不就是处于间隙中吗?没错就是间隙中,因此可以使用父容器的DragEnter来判断,只需要获取最近的卡片元素,如果获取不到,那么就是处在空隙中。
不过,如果要使用这个逻辑,卡片之间不能有空隙。为了美观,我们可以在外面套一层用来控制间隙。
大致是下面这个样子,外面是wrapper,内部是card,wrapper之间不能有间隙(因为间隙会被用于判断添加到末尾)
- 跟随的应该是内部的card
- 相关事件应该绑定到wrapper上。
总结
由于原生拖拽的缺陷太大,因此直接使用原生拖拽完全没有优势。
推荐使用框架生态的拖拽库来解决拖拽问题,写起来也简单,效果还好。
使用原生的写了一遍坑非常多,一边做一边写记录,导致写的很乱,因为时间跨度比较大。
gitee.com/oldsaltfish…