el-table拖拽开发实践

# 目前现状

# 当前使用的

element-ui-el-table-draggable 提供了对element-uiel-table的行进行拖拽排序的能力

# 不足之处

# element-ui-el-table-draggable

只能配置两个参数,不支持列拖拽,不支持类似group等参数

# 改进和开发记录

基本属于重写了, 根据核心原理做了一个出来, 也就是,dom结构使用.el-table__body-wrapper tbody,然后直接交换el-table这个data对应index的数据

重点提示,需要给el-table增加row-key,保证交换之后重新渲染的数据正确!!!

const elTableContext = this.$children[0] // 因为是通过slot引入
const container = elTableContext.$el.querySelector('.el-table__body-wrapper tbody')
Sortable.create(container, {
 onEnd(evt) {
  let { newIndex, oldIndex, } = evt
  // 交换elTableContext.data里的位置,不展开了
  exchange(oldIndex, newIndex)
  this.$emit('sort')
 }
})

之后我们解决几个核心问题

  • 不能使用sortable.js的配置(例如group属性来多列表之间拖拽)
  • 跨表格数据更新
  • 支持列拖拽
  • expanded的row特殊处理
  • 空处理

# sortable.js配置

这个好解决,一方面是可以配置props, 另一方面,我们可以使用$attrs这个属性,将未在props内定义的属性直接获取

Sortable.create(container, {
 ...this.$attrs,
 // sortable的onXXX事件转为vue的事件格式emit掉
 ...Object.keys(this.$listeners).reduce((events, key) => {
          const handler = this.$listeners[key]
          // 首字母大写
          const eventName = `on${key.replace(/\b(\w)(\w*)/g, function($0, $1, $2) {
            return $1.toUpperCase() + $2.toLowerCase()
          })}`
          events[eventName] = (...args) => handler(...args)
          return events
        }, {}),
 onEnd(evt) {
  // 之前的处理代码
  this.$emit('end', evt)
 },
})

同时增加一个监听,自动更新对应的参数

  watch: {
    $attrs: {
      deep: true,
      handler(options) {
        // 已经创建完实例后
        if (this._sortable) {
          // 排除事件,目前sortable没有on开头的属性
          const keys = Object.keys(options).filter(key => key.indexOf("on") !== 0)
          keys.forEach(key => {
            this._sortable.option(key, options[key])
          })
        }
      }
    }
  },

# 拖拽跨表格

因为onEnd事件是可以在event中拿到tofrom的对应的dom的,所以问题就转变为了如何在将exchange函数中,操作的对象从to/from转为el-tablevue对象中的data

因为,to/from是我们传递给sortablecontainer这个dom对象,所以我们要做的就是在一个地方做一个dom => el-table的映射关系表

我的选择是在window上挂一个weakMap这样对应的dom如果销毁的话,也能够自动清除内存

  mounted() {
    if (!window.__ElTableDraggableContext) {
      window.__ElTableDraggableContext = new WeakMap()
    }
    this.init();
  },
methods: {
 init() {
   const context = window.__ElTableDraggableContext
   this.table = this.$children[0].$el.querySelector(''.el-table__body-wrapper tbody'');
   context.set(this.table, elTableContext)
 }
}

exchange中,直接const toData = context.get(to).data; const fromData = context.get(from).data 就能直接获取需要更新的数据了,之后按照之前的操作数据即可

# 支持列拖拽

这个比较简单,将交换的对象和对应的dom获取参数换成.el-table__header-wrapper thead tr即可,这样就能拖动列头交换了,唯一的问题和expanded的行一样,因为拖拽本身的限制,只能拖动自身这个dom结构,其关联的dom结构是不会动的,这个需要写判断和脚本修改,或者个通过html2canvas截图,修改dataTransfer.setDragImage来修改拖动显示的快照

# expanded行处理

这个的问题在于,使用了<el-table-column type="expanded"/>的列,如果展开了行,其实在dom结构上是在那一行tr后增加一个tr并在里面渲染对应的dom的, 形如

<tr class="expanded"></tr>
<tr>展开行内的相关dom</tr>

,所以会影响onEnd事件中newIndexoldIndex的真实性(主要是因为index是通过tr的对应位置确定的)但是我们不需要计算展开的tr

所以我们通过index需要修正一下,我们可以通过el-table组件查询到哪些行被展开了

function fixIndex(sourceIndex, context) {
  const { expandRows } = context.store.states
  const { data } = context
  const indexOfExpandedRows = expandRows
    .map(row => data.indexOf(row))
    .map((rowIndex, index) => index + rowIndex + 1) // index 之前有几个展开了, rowIndex + 1, 不算之前已经展开的话,实际应该在的位置
  const offset = indexOfExpandedRows.filter(index => index < sourceIndex).length // 偏移量,也就是有几个expand的row小于当前row
  return sourceIndex - offset
}

偏移量只需要计算那些在index之前的expandedTr 即可

同时因为dom上,expanded的行不应该被拖拽和拖入这个问题,需要

  1. 在拖动的时候,将当前行的展开给收起
  2. 禁止其他已经展开的行的展开部分拖入

好在el-table的行都带有css, 所以将sortable.jsdraggable设置为.el-table__row就行了

然后在onStart的时候,将正在拖拽的这行的expand取消,结束的时候放回来就行

// onStart
 if (item.className.includes("expanded")) {
              const expandedTr = item.nextSibling
              expandedTr.parentNode.removeChild(expandedTr)
              const sourceContext = context.get(from)
              const index = fixIndex(oldIndex, sourceContext)
              this.movingExpandedRows = sourceContext.data[index]
            }

先关闭再打开,因为之前是直接删除的dom

// onEnd
              if (this.movingExpandedRows) {
                // 缓存需要展开的row
                const row = this.movingExpandedRows
                this.$nextTick(() => {
                  tableContext.toggleRowExpansion(row, false)
                  this.$nextTick(() => {
                    tableContext.toggleRowExpansion(row, true)
                  })
                })
              }

这样,就完成了一个满足我们条件的el-table拖拽组件了