头图

让拖拽更加人性化?如何自定义dragover样式

欢迎关注微信公众号: 前端侦探

在 web 开发中,经常会碰到需要拖拽的场景。为了更好的体验,拖拽区域需要有一定的变化提示,告诉用户:"现在可以放在这里了~",例如这样的

dragover效果

之前在这篇文章讲述了如何自定义 drag 样式,这次接着探索一下如何自定义 dragover 样式。

一、dragenter 和 dragleave

要实现这样的效果,少不了和dragenterdragleave打交道。

当拖动的元素进入有效的放置目标时, 将会触发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的过程中,肯定会触发dragenterdragleave这两个事件,如下

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][over] *{
  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][over]>*: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 的配合,同样重要

[allowdrop]:empty::after{
  content: '拖放此处';
}
[allowdrop][over]:empty::after{
  content: '松开放置';
}
[allowdrop][over]{
  /*自定义样式*/
}
[allowdrop][over]>*:not([allowdrop]){
  pointer-events: none;
}
这里有个 CSS 小技巧,上面例子在拖放过程中的文字提示变化其实是通过伪元素实时变化的~

你也可以查看在线链接:自定义 dragover (codepen.io)或者自定义 dragover (juejin.cn)

另外,如果需要完全自定义拖拽,可以参考这个项目:https://github.com/XboxYan/draggable-polyfill,非常轻量,100 来行代码,不影响业务逻辑,非常适合学习和时使用,欢迎 star~

五、总结和说明

以上就是自定义 dragover 效果的完整实现了,不算复杂,但也有一些小技巧,特别是借助了 CSS 的能力。其实在这一版实现之前,我还尝试过很多别的实现,但都不如这种方式简洁明了,下面总结一下:

  1. 为了更好的体验,可以在拖拽过程中给与用户适当的变化提示
  2. 主要实现方法在于 dragenter 和 dragleave
  3. 当放置目标存在子元素时,也会触发 dragleave 事件,干扰原有逻辑
  4. 可以移除 dragleave 去除子元素的干扰,dragenter 需要先移除再添加 over
  5. 通过 CSS pointer-events 可以去除子元素的干扰
  6. 如果有多层可放置结构,可以通过 :not 过滤可放置目标
  7. 通过鼠标指针也可以改善交互体验
  8. 在 DOM 操作中千万不要忘记了 CSS,这点很重要

当然,拖拽在页面中的交互细节还有很多,比如拖拽排序过程中的挤压动画效果,后面有空再研究吧,争取出一个通用的解决方案。最后,如果觉得还不错,对你有帮助的话,欢迎点赞、收藏、转发❤❤❤

欢迎关注微信公众号: 前端侦探

前端侦探
致力于有趣的前端探索~
14.1k 声望
13.6k 粉丝
0 条评论
推荐阅读
突发奇想!借助CSS自定义彩色字体来实现多行文本展开收起
之前写过这样一篇文章:CSS 实现多行文本“展开收起”,介绍了一些纯 CSS 实现多行文本展开收起的小技巧,特别是右下角的“展开收起”按钮,用到了浮动布局,非常巧妙,有兴趣的可以回顾一下。

XboxYan7阅读 442

封面图
正则表达式实例
收集在业务中经常使用的正则表达式实例,方便以后进行查找,减少工作量。常用正则表达式实例1. 校验基本日期格式 {代码...} {代码...} 2. 校验密码强度密码的强度必须是包含大小写字母和数字的组合,不能使用特殊...

寒青57阅读 8.6k评论 11

JavaScript有用的代码片段和trick
平时工作过程中可以用到的实用代码集棉。判断对象否为空 {代码...} 浮点数取整 {代码...} 注意:前三种方法只适用于32个位整数,对于负数的处理上和Math.floor是不同的。 {代码...} 生成6位数字验证码 {代码...} ...

jenemy49阅读 7.3k评论 12

再也不学AJAX了!(二)使用AJAX ① XMLHttpRequest
「再也不学 AJAX 了」是一个以 AJAX 为主题的系列文章,希望读者通过阅读本系列文章,能够对 AJAX 技术有更加深入的认识和理解,从此能够再也不用专门学习 AJAX。本篇文章为该系列的第二篇,最近更新于 2023 年 1...

libinfs42阅读 7k评论 12

封面图
CSS 绘制一只思否猫
欢迎关注我的公众号:前端侦探练习 CSS 有一个比较有趣的方式,就是发挥想象,绘制各式各样的图案,比如来绘制一只思否猫?思否猫,SegmentFault 思否的吉祥物,是一只独一无二、特立独行、热爱自由的(&gt;^ω^&lt...

XboxYan48阅读 3.3k评论 14

封面图
「多图预警」完美实现一个@功能
一天产品大大向 boss 汇报完研发成果和产品业绩产出,若有所思的走出来,劲直向我走过来,嘴角微微上扬。产品大大:boss 对我们的研发成果挺满意的,balabala...(内心 OS:不听,讲重点)产品大大:咱们的客服 I...

wuwhs32阅读 3.5k评论 5

封面图
还在用 JS 做节流吗?CSS 也可以防止按钮重复点击
举个例子:一个保存按钮,为了避免重复提交或者服务器考虑,往往需要对点击行为做一定的限制,比如只允许每300ms提交一次,这时候我想大部分同学都会到网上直接拷贝一段throttle函数,或者直接引用lodash工具库

XboxYan35阅读 2.7k评论 2

封面图
14.1k 声望
13.6k 粉丝
宣传栏