需求
- 树状表格每一行支持拖拽至任意位置
- 表格拖拽时针对前,后,子集给出不同的动画效果
环境
工具包:Element-plus + Sortable.js 基础:Vue + TS + Scss
说明:这里我为了方便直接用了Sortable,如果想支持更加自由的扩展,可以直接使用原生的拖动
知识点
下面是针对上面方法的一些说明,已经了解的可以直接跳过该部分
- Sortable.js :onMove,onStart,onEnd
- 中文文档:sortable.js中文文档
- 官方文档:sortable.js官方文档 | 官方🌰
- 原生事件:dragover事件
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,newIndex
和oldIndex
则会相同,同时等于拖动开始的位置。
dragover
事件在可拖动的元素或者被选择的文本被拖进一个有效的放置目标时(每几百毫秒)触发。该事件在放置目标上触发,也就是说可以在该事件上处理拖动的动画效果。 MDN文档:dragover事件
实现步骤
- 通过
.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, // 拖动结束
})
}
- 因为拖动的元素上不会触发
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)
)
}
- 通过在
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
}
- 当拖动结束后,根据保存的拖入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()
}
}
踩到的坑
- element 的树状表格不是嵌套dom,而是把数据平铺成一个一维数组然后渲染的表格
- 一开始想着直接使用sortable的方法来实现,但是发现sortable的判定不准确,当拖动dom的时候容易飘到下一个元素,最后只使用了sortable的拖动效果,并且在onMove方法中把拖动的效果禁用掉了
- 再网上查找到的element 树状表格拖动的文章大部分都是把表格数据展开成一维数组和表格的下标匹配上,但是这样就不能处理随意处理动画了,我这里的思路是把数据的ID通过class的方式放到表格的每一行上,然后在sortable的方法中去通过获取dom元素拿到class解构出ID,同时做动画处理(ps:这个地方我发现禁用动画之后的sortable的方法和原生的拖动事件差别不大,所以我偷懒直接使用了sortable的方法,但是这个处理不是很严谨,因为我没有去研究sortable的源码)
结束
解决上面的坑基本上element树状表格的拖动基本上就实现的差不多了,这个思路可以自定义动画,同时还可以实现表格拖动的数据变化。上述思路用原生实现也可以,不过这其中可能会有一些其他的坑,等待大佬完善。当然如果不需要太复杂的交互效果,我更推荐直接把数据铺开为一维数组来进行实现,竟这样实现起来容易很多。
完整源码:element 树状表格拖动
参考: