在 web 开发中,经常会碰到需要拖拽的场景。为了更好的体验,拖拽区域需要有一定的变化提示,告诉用户:"现在可以放在这里了~",例如这样的。
dragover效果
这次接着探索一下如何自定义 dragover 样式。
一、dragenter 和 dragleave
要实现这样的效果,少不了和dragenter和dragleave打交道。
- 当拖动的元素进入有效的放置目标时, 将会触发dragenter 事件。
- 当拖动的元素离开有效的放置目标时,将会触发dragleave 事件。
拖拽目标和放置目标
假设现在有这样一个结构,这里 img是拖拽目标,div.content是放置目标。
<img>
<div class="content"></div>
然后在document监听一下;
document.addEventListener('dragleave', function(ev) {
console.log('dragleave', ev.target)
})
document.addEventListener('dragenter', function(ev) {
console.log('dragenter', ev.target)
})
那么,将img拖入div.content的过程中,肯定会触发dragenter和dragleave这两个事件,如下:
dragenter和dragleave
如果页面比较简单,要自定义拖拽过程就比较容易了;
document.addEventListener('dragleave', function(ev) {
ev.target.toggleAttribute('over',false);
})
document.addEventListener('dragenter', function(ev) {
ev.target.toggleAttribute('over',true);
})
通过添加over属性自定义样式;
.content[over]{
outline: 4px solid slateblue;
}
效果如下:
dragover效果
是不是非常容易呢?
实际使用起来其实还存在很多局限性,下面一一介绍。
二、当放置目标有子元素时
大部分情况下,放置目标并不是空的,还有其他子元素,如果采用上面的方式就会有问题了,假设布局是这样的,为了区分,可以给需要放置的元素添加一个属性,比如allowdrop,表示允许放置;
<img>
<div class="content" allowdrop>
<div>不允许放置</div>
</div>
这里通过属性区分一下:
document.addEventListener('dragleave', function(ev) {
if (ev.target.getAttribute('allowdrop')!==null) {
ev.target.toggleAttribute('over',false);
}
})
document.addEventListener('dragenter', function(ev) {
if (ev.target.getAttribute('allowdrop')!==null) {
ev.target.toggleAttribute('over',true);
}
})
效果如下:
有子元素的情况下
可以看到,当拖拽目标经过子元素时,外面的样式已经丢失了。原因其实很简单,在经过子元素时,放置目标也触发了dragleave事件!
那有没有办法不触发呢?这里有两种方式:
首先可以取消dragleave的监听,因为在执行dragleave时,元素本身是不知道即将进入哪一个区域,很容易“误伤”。取而代之的是每次dragenter时,先移除上一次放置目标的属性,然后再添加新的,有点类似选项卡的操作,具体实现如下:
var lastDrop = null;
document.addEventListener('dragenter', function(ev) {
if (lastDrop) {
lastDrop.toggleAttribute('over',false);
}
const dropbox = ev.target.closest('[allowdrop]'); // 获取最近的放置目标
if (dropbox) {
dropbox.toggleAttribute('over',true);
lastDrop = dropbox;
}
})
还有另一种方式:借助 CSS 就非常容易了。
这里有一个非常简单粗暴的方式,直接将子元素禁用鼠标响应,如下:
.content[allowdrop](empty::after{ "allowdrop") *{
pointer-events: none;
}
这样,在滑过任何子元素都不会有响应了,完美。
有子元素的情况,完美
三、多层嵌套放置目标
上面这种方式其实可以解决大多数问题了,毕竟大部分场景都是扁平的。不过有时候也会碰到多层结构,比如那种可视化编辑工具,尤其是目前比较火的低代码平台,就会涉及到多层结构,假设 HTML 是这样的。
<img>
<div class="content" allowdrop>
<div class="content" allowdrop></div>
<div class="content">不允许拖拽</div>
<div class="content" allowdrop></div>
</div>
如果按照 CSS 的处理方式(JS 方式没有问题),由于所有子元素都被禁用,里面的结构自然也无法响应了。
多层嵌套结构无响应
那如何让里面的放置目标可以响应呢?其实只需要改一下上面的 CSS 即可,如下:
.content[allowdrop](empty::after{ "allowdrop")>*:not([allowdrop]){
pointer-events: none;
}
这里使用了>选择器,表示只选择子元素,不包含后代元素,然后排除掉放置目标,这样就能实现多层嵌套了,效果如下:
多层嵌套结构,完美
是不是出乎意料的简单呢?
四、其他交互细节
不知道大家发现没,上面的例子在拖拽开始,鼠标就一直处于这种“可放置”状态,不管是在放置目标外部还是内部,如下:
鼠标指针状态
这是因为设置了dragover属性,所以整个document都变成了可放置目标,都允许触发drop事件。
document.addEventListener('dragover', function(ev){
ev.preventDefault()
})
如果希望交互更加细腻,体验更好,那么在鼠标指示上也可以进一步的优化,可以在进入放置目标后才变成这种状态,实现如下:
document.addEventListener('dragover', function(ev){
const dropbox = ev.target.closest('[allowdrop]');
if (dropbox) {
ev.preventDefault()
}
})
效果如下(注意观察鼠标的变化🔽):
拖拽过程中的鼠标变化
除此之外,还应该在drop结束后移除掉over属性。
document.addEventListener('drop', function(ev){
const dropbox = ev.target.closest('[allowdrop]');
if (dropbox) {
dropbox.toggleAttribute('over',false);
}
})
这样就实现了一个完全通用的自定义 dragover效果,区区数十行,划重点,完整代码如下:
document.addEventListener('dragover', function(ev){
const dropbox = ev.target.closest('[allowdrop]');
if (dropbox) {
ev.preventDefault()
}
})
document.addEventListener('drop', function(ev){
ev.target.toggleAttribute('over',false);
})
document.addEventListener('dragleave', function(ev) {
if (ev.target.getAttribute('allowdrop')!==null) {
ev.target.toggleAttribute('over',false);
}
})
document.addEventListener('dragenter', function(ev) {
if (ev.target.getAttribute('allowdrop')!==null) {
ev.target.toggleAttribute('over',true);
}
})
// 或者以下方式,无需dragleave,无需额外 CSS
var lastDrop = null;
document.addEventListener('dragenter', function(ev) {
if (lastDrop) {
lastDrop.toggleAttribute('over',false);
}
const dropbox = ev.target.closest('[allowdrop]'); // 获取最近的放置目标
if (dropbox) {
dropbox.toggleAttribute('over',true);
lastDrop = dropbox;
}
})
当然还少不了 CSS 的配合,同样重要。
content: '拖放此处';
}
[allowdrop](empty::after{ "allowdrop"):empty::after{
content: '松开放置';
}
[allowdrop](empty::after{ "allowdrop"){
/*自定义样式*/
}
[allowdrop](empty::after{ "allowdrop")>*:not([allowdrop]){
pointer-events: none;
}
这里有个 CSS 小技巧,上面例子在拖放过程中的文字提示变化其实是通过伪元素实时变化的。
你也可以查看在线链接:自定义 dragover (codepen.io)[1]或者自定义 dragover (juejin.cn)[2]。
五、总结和说明
以上就是自定义 dragover 效果的完整实现了,不算复杂,但也有一些小技巧,特别是借助了 CSS 的能力。其实在这一版实现之前,我还尝试过很多别的实现,但都不如这种方式简洁明了,下面总结一下:
- 为了更好的体验,可以在拖拽过程中给与用户适当的变化提示。
- 主要实现方法在于 dragenter 和 dragleave。
- 当放置目标存在子元素时,也会触发 dragleave 事件,干扰原有逻辑。
- 可以移除 dragleave 去除子元素的干扰,dragenter 需要先移除再添加 over。
- 通过 CSS pointer-events 可以去除子元素的干扰。
- 如果有多层可放置结构,可以通过 :not 过滤可放置目标。
- 通过鼠标指针也可以改善交互体验。
- 在 DOM 操作中千万不要忘记了 CSS,这点很重要。
当然,拖拽在页面中的交互细节还有很多,比如拖拽排序过程中的挤压动画效果,后面有空再研究吧,争取出一个通用的解决方案。
参考资料
[1]自定义 dragover (codepen.io): https://codepen.io/xboxyan/pen/yLvjXdJ
[2]自定义 dragover (juejin.cn): https://code.juejin.cn/pen/7104250686161813540