记录一次element-plus拖拽上传的有趣bug

329 阅读3分钟

起因是群里一个小伙伴吐槽elp的拖拽上传会闪烁几年了都没有修复,我看了一下发的视频也是一个大无语住,谁家好人没事拖着文件在上传区域一直晃,正常不是拖进去就松手上传了嘛,然后本着实践是检验真理的唯一标准动手尝试了一下,发现拖拽的文件在中心区域的icon和text上时会闪烁,四周则是拖拽到区域内一直上下移动也会闪烁

old-drag.gif
elp拖拽文件处于上传区域时会有边框与背景色的变换,离开上传区域和结束上传会移除相关样式,这段逻辑是通过onDragoveronDragleave来触发dragover这个变量的响应式变化,元素上则是动态class绑定dragover来添加类名,源码如下,隐去了不相干部分

<template>
  <div
    :class="[ns.b('dragger'), ns.is('dragover', dragover)]"
    @drop.prevent="onDrop"
    @dragover.prevent="onDragover"
    @dragleave.prevent="dragover = false"
  >
    <slot />
  </div>
</template>

<script lang="ts" setup>
import { inject, ref } from 'vue'
import { useNamespace } from '@element-plus/hooks'
import { useFormDisabled } from '@element-plus/components/form'

const ns = useNamespace('upload')
const dragover = ref(false)
const disabled = useFormDisabled()

const onDrop = (e: DragEvent) => {
  if (disabled.value) return
  dragover.value = false

  e.stopPropagation()
  // ...省略其余逻辑
}

const onDragover = () => {
  if (!disabled.value) dragover.value = true
}
</script>

// 位于theme-chalk下的样式文件,省略其余部分,只关注dragover
@include when(dragover) {
  padding: calc(#{getCssVar('upload-dragger-padding-horizontal')} - 1px)
    calc(#{getCssVar('upload-dragger-padding-vertical')} - 1px);
  background-color: getCssVar('color', 'primary', 'light-9');
  border: 2px dashed getCssVar('color-primary');
}

逻辑很直观,通过查阅 MDN 文档,dragleave 事件在拖动的元素或选中的文本离开一个有效的放置目标时被触发,dragover 事件在可拖动的元素或者被选择的文本被拖进一个有效的放置目标时(每几百毫秒)触发,简单的理解就是拖进去添加样式,离开和上传完成时移除样式,而之所以会闪烁就是因为在拖拽过程中滑过子元素和区域内滑动会频繁触发 dragoverdragleave,即使鼠标并没有真正离开整个上传区域。这是因为这些事件是针对不同的 DOM 元素触发的,而不是整个容器。所以解决思路也很简单,只要判断还在容器内则不移除样式即可,即不将dragover.value置为false

<template>
  <div
    ref="dragRef"
    :class="[ns.b('dragger'), ns.is('dragover', dragover)]"
    @drop.prevent="onDrop"
    @dragover.prevent="onDragover"
    @dragleave.prevent="onDragleave"
  >
    <slot />
  </div>
</template>

<script lang="ts" setup>
// ...省略其余部分
const dragRef = ref()
const onDragleave = (e: DragEvent) => {
  const rect = dragRef.value.getBoundingClientRect()
  const { clientX: x, clientY: y } = e
  if (x <= rect.left || x >= rect.right || y <= rect.top || y >= rect.bottom) {
    // leave the container
    dragover.value = false
  }
}
</script>

添加一个ref和onDragleave函数,通过ref拿到元素的dom获取元素的大小及其相对于视口的位置,然后取出event中的水平坐标判断其是否处于上传区域中来处置dragover.value,如下图,不再频繁的添加、移除类名就不会闪烁

new-drag.gif
能解决自然要混个PR的,详见fix(components): [upload-dragger] Blink when sliding over sub elements by selicens · Pull Request #21006 · element-plus/element-plus
有小伙伴指出使用relatedTarget更方便,这个我还真往这个方向延申过,不是我事后找补,是被GPT误导了,如图:

image.png 它信誓旦旦的告诉我relatedTarget在拖拽事件中通常为 null ,我没有怀疑它,选择了获取元素rect来判断的方案,看来对于 GPT 还是要保持质疑,最终我实际测试了下relatedTarget是可行的,这样写相比于判断坐标要更简洁一点

  if ((e.currentTarget as Element).contains(e.relatedTarget as Element)) return
  dragover.value = false

这就是此次排查element-plus拖拽上传bug的分享,希望对大家有帮助