当前使用的

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拖拽组件了