业务中要实现这样一个拖拽组件,在鼠标拖动着东西,进入被 drop 的区域(drop zone)的时候,要高亮提示用户已经进入可以释放的区域
一开始打算找找看有没有相关的css伪类可以天然支持这样的行为(就像:active那样!)但搜索一圈后
并未发现
,故只能借助 drag and drop API,结合 class 操作来进行样式的控制
查询 drag & drop 相关的事件 api 后,锁定了
dragenter
/
dragleave
/
drop
三个事件,其中前两个分别在被拖动的元素进入和离开区域的时候控制添加和删除高亮class,drop事件是为了处理拖动进入后就直接释放,而没有触发 dragleave 的情况
示意代码
大致是这样的
1 2 3 4 5 6 7 8 9
function handleDragEnter (event ) { event.target.classList.add('highlight' ) } function handleDragLeave (event ) { event.target.classList.remove('highlight' ) } function handleDrop (event ) { event.target.classList.remove('highlight' ) }
一切都是如此的顺滑,直到一个机缘巧合之下,发现如果 drop zone 有别的子元素的情况下,好像不太好使了就, 具体表现为:当被拖动元素进一步进入 drop zone 下的子元素的时候,drop zone 的激活态就消失了
经过一番探索,本着面向SO编程的思想,并装模作样查询了规范文档
Drag & Drop Specification
,终于发现了问题所在
导致bug的核心原因在于,当我拖动元素进入 Drop Zone 时,确实触发了 Drop Zone 的
dragenter
事件,但当我继续进入 Drop Zone 的子元素的时候,就触发了目标元素更替,也就是对于原先的 Drop Zone 来说,我就是已经离开它了,进一步会触发下一个目标元素(C1)的
dragenter
事件
继续面向SO,发现了有国外老哥遇到相似问题,并已经有人提出了多种解决方案,我大致浏览了一下,取一种借鉴之,问题解决。
借鉴的方案大概是增强了 dragleave 事件处理的判断能力,除了以事件被fired作为添加class的标志以外,进一步对当前元素的位置进行了判断:
1 2 3 4 5 6 7 8
function handleDragLeave (event ) { const { target } = event const rect = target.getBoundingClientRect() if (e.x > rect.left + rect.width || e.x < rect.left || e.y > rect.top + rect.height || e.y < rect.top) { event.target.classList.remove('highlight' ) } }
可以看到,从原来的无脑看到事件触发就认为被拖元素已经离开 Drop Zone,变为进一步判断当前事件触发时鼠标(也代表了被拖元素)与Drop Zone的相对位置,如果鼠标仍在 Drop Zone 元素内,那我们认为被拖元素实际上还没有离开,只是进入了另一个子节点而已,故不删除高亮态class
对于这种情况还可以用一种比较极端的 css 解法:利用 css 属性
pointer-events: none
来规定内部目标元素(C1)不触发鼠标事件(
MouseEvent
),而拖动元素的事件
DragEvent
是继承
MouseEvent
的,所以也会被屏蔽,从鼠标事件的角度看,C1那个位置就好像什么都没有
注1:event.x 表示事件触发时鼠标指针相对视口 (viewport) 左上角的位置
注2:event.x 来源于 MouseEvent.x,同时也是 MouseEvent.clientX 的别名,新的写法目前可能还存在兼容性问题