使用sortable实现element树状表格拖动

需求

  1. 树状表格每一行支持拖拽至任意位置
  2. 表格拖拽时针对前,后,子集给出不同的动画效果

环境

工具包:Element-plus + Sortable.js 基础:Vue + TS + Scss

说明:这里我为了方便直接用了Sortable,如果想支持更加自由的扩展,可以直接使用原生的拖动

知识点

下面是针对上面方法的一些说明,已经了解的可以直接跳过该部分

onStart

开始拖拽执行的回调事件。

参数名 说明 Ts类型 参数对象结构
event 存放拖动的DOM,以及对应数据的下标 SortableEvent

在浏览器中打印可以看到回调参数放的是一些关于拖动元素的信息,其中item是拖动的dom,可以直接在上面进行事件绑定等一系列操作。oldIndex是拖动元素在数据中对应的下标。

onMove

拖拽过程中(替换其他元素位置)执行返回true可以替换位置,返回false取消替换位置,当返回true时,会再发生位置替换的时候调用函数,当返回false时,会持续调用函数。

参数名 说明 Ts类型 参数对象结构
event 存放拖动元素以及拖入元素的信息 MoveEvent
originalEvent 鼠标的信息 Event

event中dragged是被拖拽的DOM对象,draggedRect是被拖拽对象的长宽以及位置信息,related是被替换的DOM对象,relatedRect是被替换对象的长宽以及位置信息,willInsertAfter代表是在被替换对象的前面还是后面。 originalEvent中pageX``pageY是鼠标在页面的坐标,offsetX``offsetY是鼠标在替换元素的坐标,clientX``clientY是鼠标在可视区域的坐标,screenX``screenY是鼠标在屏幕的坐标。

在onMove中可以获取到很多坐标以及dom元素,可以在这里做动画处理

onEnd

拖拽结束后执行的回调事件

参数名 说明 Ts类型 参数对象结构
event 存放拖动的DOM,以及对应数据的下标 SortableEvent

结束的回调参数和onStart的参数一致,但是在结束回调中可以获取到替换元素的下标,newIndex为替换之后的新下标,oldIndex为拖动对象原来所在数据下标。如果onMove返回false,newIndexoldIndex则会相同,同时等于拖动开始的位置。

dragover

事件在可拖动的元素或者被选择的文本被拖进一个有效的放置目标时(每几百毫秒)触发。该事件在放置目标上触发,也就是说可以在该事件上处理拖动的动画效果。 MDN文档:dragover事件

实现步骤

  1. 通过.el-table__body-wrapper tbody获取element table上的tbody并设置拖动动画,同时通过elment table的row-class-name属性给每一行设置上对应的ID类
typescript 复制代码
// 表格行样式
const tableRowClassName = ({ row }: any) => {
  return `table-row drag-class-${row.id}`
}
// 设置拖动排序
const setDragSort = () => {
  // 获取容器元素
  const el = taskTableRef.value?.$el.querySelector('.el-table__body-wrapper tbody')
  if (!el) return
  sortableObj.value = new Sortable(el, {
    handle: '.drag-mark',
    forceFallback: false, // 忽略HTML5原生拖拽行为
    onMove: tableMove, // 拖动中
    onStart: tableStart, // 开始拖动
    onEnd: tableEnd, // 拖动结束
  })
}
  1. 因为拖动的元素上不会触发onMove函数,所以需要在拖动开始时针对拖动元素绑定dragover事件,用于清除掉手动设置上的css样式。同时你可以在这里记录下拖动数据的ID。
typescript 复制代码
// 表格拖动开始
const tableStart = (event: SortableEvent) => {
  // 在Sortable中onMove返回false时,拖动的那个元素不会触发onMove事件,所以手动添加上事件
  event.item.addEventListener(
    'dragover',
    useThrottleFn(() => {
      if (relatedDom.value) {
        clearDragAnimation() // 清除拖动css
        relatedDom.value = undefined
      }
    }, 300)
  )
}
  1. 通过在onMove回调函数中返回false来禁用拖动替换动画,并根据回调函数的参数中的offsetY属性添加上自己的拖动替换动画 [ 这个地方我用class的方式记录拖动的位置 ]。同时记录当前拖入的DOM
typescript 复制代码
// 表格拖动中 主要处理拖动的动画
const tableMove = (evt: MoveEvent, originalEvent: Event) => {
  if (relatedDom.value && !evt.related.isEqualNode(relatedDom.value)) {
    // 如果替换的dom不一致,则删除原有的效果
    clearDragAnimation()
    relatedDom.value = evt.related
  } else if (!relatedDom.value) {
    relatedDom.value = evt.related
  }

  if ((originalEvent as DragEvent).offsetY > 2 && (originalEvent as DragEvent).offsetY <= 10) {
    clearDragAnimation()
    // 替换dom的前面
    const div = document.createElement('div')
    div.className = 'before drag-animation'
    evt.related.appendChild(div)
  } else if ((originalEvent as DragEvent).offsetY > 10 && (originalEvent as DragEvent).offsetY <= 20) {
    clearDragAnimation()
    // 替换dom的子级
    evt.related.classList.add('son-drag-animation')
  } else if ((originalEvent as DragEvent).offsetY > 20) {
    clearDragAnimation()
    // 替换dom的后面
    const div = document.createElement('div')
    div.className = 'after drag-animation'
    evt.related.appendChild(div)
  }
  return false
}
  1. 当拖动结束后,根据保存的拖入DOM获取到对应的ID,下标以及数据,结合拖入和拖动的数据重新组装表格数据
typescript 复制代码
// 表格拖动结束 主要处理拖动后表格的数据变化
const tableEnd = () => {
  let tempRequest: { DragID: number; DropID: number; DropType: string } = {
    DragID: dragID.value,
    DropID: 0,
    DropType: '',
  }

  const oneArray = treetoarray(taskTableRef.value?.data || [], 'children')
  if (relatedDom.value) {
    const tempRelated = relatedDom.value.querySelector('.drag-animation')
    const isChild = relatedDom.value.className.includes('son-drag-animation')
    // 获取拖入方式
    if (tempRelated) {
      tempRequest.DropType = Array.from(tempRelated.classList).includes('before') ? 'before' : 'after'
    } else if (isChild) {
      tempRequest.DropType = 'inner'
    }
    // 获取拖入的ID
    relatedDom.value?.classList.forEach((item) => {
      if (item.includes('drag-class')) {
        tempRequest.DropID = Number(item.split('drag-class-')[1])
      }
    })

    // 获取拖动的下标
    const dragIndex = oneArray.findIndex((item: any) => item.id == tempRequest.DragID)
    const dragData = oneArray.find((item: any) => item.id === tempRequest.DragID)
    // 获取拖入的下标
    const dropIndex = oneArray.findIndex((item: any) => item.id == tempRequest.DropID)
    const dropData = oneArray.find((item: any) => item.id == tempRequest.DropID)
    if (dragIndex != -1 && dragData && dropIndex != -1 && dropData) {
      oneArray.splice(dragIndex, 1)
      console.log(dragData);
      const childrenIndex = childrenNodeData.value.findIndex(item => item.id === dropData.id)

      switch (tempRequest.DropType) {
        case 'before':
          oneArray.splice(dropIndex - 1, 0, dragData)
          dragData.parentID = dropData.parentID
          // 模拟拖入子节点
          if (childrenIndex != -1) {
            childrenNodeData.value.splice(childrenIndex, 0, dragData)
          }
          break;
        case 'after':
          oneArray.splice(dropIndex, 0, dragData)
          dragData.parentID = dropData.parentID
          // 模拟拖入子节点
          if (childrenIndex != -1) {
            childrenNodeData.value.splice(childrenIndex - 1, 0, dragData)
          }
          break;
        case 'inner':
          oneArray.splice(dropIndex, 0, dragData)
          dragData.parentID = dropData.id
          // 模拟拖入子节点
          if (dropData.parentID !== 0) {
            childrenNodeData.value = childrenNodeData.value.map(item => {
              item.children?.push(dragData)
              return item
            })
          }
          break;
        default:
          break;
      }
    }
    tableData.value = []
    nextTick(() => {
      tableData.value = arraytotree(oneArray)
      taskTableRef.value?.doLayout()
    })
    clearDragAnimation()
  }
}

踩到的坑

  1. element 的树状表格不是嵌套dom,而是把数据平铺成一个一维数组然后渲染的表格
  2. 一开始想着直接使用sortable的方法来实现,但是发现sortable的判定不准确,当拖动dom的时候容易飘到下一个元素,最后只使用了sortable的拖动效果,并且在onMove方法中把拖动的效果禁用掉了
  3. 再网上查找到的element 树状表格拖动的文章大部分都是把表格数据展开成一维数组和表格的下标匹配上,但是这样就不能处理随意处理动画了,我这里的思路是把数据的ID通过class的方式放到表格的每一行上,然后在sortable的方法中去通过获取dom元素拿到class解构出ID,同时做动画处理(ps:这个地方我发现禁用动画之后的sortable的方法和原生的拖动事件差别不大,所以我偷懒直接使用了sortable的方法,但是这个处理不是很严谨,因为我没有去研究sortable的源码)

结束

解决上面的坑基本上element树状表格的拖动基本上就实现的差不多了,这个思路可以自定义动画,同时还可以实现表格拖动的数据变化。上述思路用原生实现也可以,不过这其中可能会有一些其他的坑,等待大佬完善。当然如果不需要太复杂的交互效果,我更推荐直接把数据铺开为一维数组来进行实现,竟这样实现起来容易很多。

完整源码:element 树状表格拖动

参考:

相关推荐
雨 子11 分钟前
Spring Web MVC
前端·spring boot·spring·mvc·postman
计算机毕设指导620 分钟前
基于Springboot美食推荐商城系统【附源码】
java·前端·spring boot·后端·spring·tomcat·美食
!win !23 分钟前
外部H5唤起常用小程序链接规则整理
前端·小程序
染指悲剧36 分钟前
vue实现虚拟列表滚动
前端·javascript·vue.js
林涧泣1 小时前
【Uniapp-Vue3】navigator路由与页面跳转
前端·vue.js·uni-app
浩浩测试一下2 小时前
Web渗透测试之XSS跨站脚本之JS输出 以及 什么是闭合标签 一篇文章给你说明白
前端·javascript·安全·web安全·网络安全·html·系统安全
一棵开花的树,枝芽无限靠近你3 小时前
【PPTist】插入形状、插入图片、插入图表
前端·笔记·学习·编辑器·ppt·pptist
不会玩技术的技术girl3 小时前
获取淘宝商品详情高级版 API 接口 Java 示例代码
java·开发语言·前端
金州饿霸3 小时前
hadoop-yarn常用命令
大数据·前端·hadoop
前端搬运工X3 小时前
Object.keys 的原生 JS 类型之困
javascript·typescript