当前使用的
element-ui-el-table-draggable 提供了对element-ui内el-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中拿到to和from的对应的dom的,所以问题就转变为了如何在将exchange函数中,操作的对象从to/from转为el-table的vue对象中的data
因为,to/from是我们传递给sortable的container这个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事件中newIndex和oldIndex的真实性(主要是因为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的行不应该被拖拽和拖入这个问题,需要
在拖动的时候,将当前行的展开给收起
禁止其他已经展开的行的展开部分拖入
好在el-table的行都带有css, 所以将sortable.js的draggable设置为.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拖拽组件了