昨天做了一个可以拖拽td的自定义指令,实现基本功能后开始写td的排序的功能,其间发现了几个小问题。
几个小问题
-
没有返回序号
只返回 th 的内容,但是却没有返回是第几列的,这样外部程序还得去找,比较麻烦,应该内部解决 -
没有直接返回左右
想依据结束拖拽的时候鼠标指针在左还是右,以便于实现不同的功能,但是却只返回一个x坐标,这个不好判断,应该内部直接返回左还是右。 -
table重绘后th的事件也没了。
由于内部实现的原因,th是用 v-for 循环出来的,调整顺序后,需要重新绘制一遍,于是某些th的事件丢失了。
这个需要重新设置一遍。
改进
- 修改拖拽信息,dragInfo
增加需要的属性。
const dragInfo = reactive({
offsetX: 0,
isLeft: true, // 是否在 th 的左侧结束拖拽
ctrl: false, // 是否按下了ctrl
source: '',
target: '',
sourceIndex: 0, // 开始拖拽的位置
targetIndex: 0 // 结束拖拽的位置
})
- 修改自定义指令
/**
* 拖拽 table 的 th,返回拖拽信息
*/
const tableDrag = (app, options) => {
app.directive('tabledrag', {
// 指令的定义
mounted (el, binding) {
/**
* 实现 th 的拖拽
* @param {string} className 用于找到目标的 class 名称。
* @param {reactive} dragInfo reactive 返回拖拽信息。
* @returns 没有返回
* * const dragInfo = {
* * offsetX: 0,
* * isLeft: true, // th 左侧结束拖拽
* * ctrl: false, // 是否按下ctrl
* * source: '', // 开始拖拽的th
* * target: '', // 结束拖拽的th
* * sourceIndex: 0, // 开始拖拽的序号
* * targetIndex: 0 // 结束拖拽的序号
* * })
*/
const setThforDrag = (className, dragInfo) => {
const table = el.getElementsByClassName(className)[0]
const tr = table.rows[0]
const tdCount = tr.cells.length
// 记录 th 的序号和宽度
const thIndex = {}
// 记录临时的源
let src1 = ''
let src2 = 1
// 设置th的拖拽
for (let i = 0; i < tdCount; i++) {
const th = tr.cells[i]
thIndex[th.innerText] = {
index: i, // 记录th的序号
width: th.offsetWidth // 记录 th 的宽度
}
// 设置可以拖拽
th.setAttribute('draggable', true)
// 拖拽时经过
th.ondragover = (event) => {
event.preventDefault()
}
// 开始拖拽
th.ondragstart = (event) => {
src1 = event.target.innerText
src2 = thIndex[event.target.innerText].index
}
// 结束拖拽
th.ondrop = (event) => {
dragInfo.offsetX = event.offsetX
dragInfo.ctrl = event.ctrlKey
dragInfo.source = src1
dragInfo.sourceIndex = src2
dragInfo.target = event.target.innerText
// 设置 th 的序号
dragInfo.targetIndex = thIndex[event.target.innerText].index
dragInfo.isLeft = dragInfo.offsetX < thIndex[event.target.innerText].width / 2
}
}
}
binding.value.setThforDrag = setThforDrag
}
})
}
export default tableDrag
-
thIndex
在遍历的时候,记录th的位置和宽度,宽度 + x坐标 = 左右。 -
结束拖拽时统一设置拖拽信息
因为每次设置都是触发watch,而只有结束时的触发才是我们希望的,所以在开始拖拽的时候不能设置拖拽信息,只能临时保存,等到结束的时候一起设置。
实现调整 th 的排序
具体实现方式就不能在内部实现了,因为不同的项目有不同的实现方式,外面自己写就好。
这里基于自己的项目实现一下。
th 的实现方式
我比较懒,不喜欢一个一个的设置 el-table-column,所以用了v-for
<el-table-column
v-for="(id) in colOrder"
:colId="id"
:column-key="id"
:key="'grid_list_' + id"
:fixed="id < fixedIndex"
:prop="itemMeta[id].colName"
:label="itemMeta[id].label"
:width="itemMeta[id].width"
:min-width="50"
:align="itemMeta[id].align"
:header-align="itemMeta[id]['header-align']"
:filter-multiple="false"
:show-overflow-tooltip="true"
:formatter="myformatter"
>
</el-table-column>
colOrder 是一个纯数组,元素只有number,那么调整排序就是调整这个数组里面的元素。
插入 th
把 th1 插入到 th2 的前面或者后面。这个应该是比较常见的一种调整方式吧。
/**
* 插入 th 后调整顺序
*/
const _order = (cols) => {
// 判断前插、后插。后插:偏移 0;前插:偏移 1
const offsetTarget = dragInfo.isLeft ? 1 : 0
// 判断前后顺序。》 1;《 0
const offsetSource = dragInfo.sourceIndex < dragInfo.targetIndex ? 1 : 0
// 插入源
girdMeta.colOrder.splice(dragInfo.targetIndex - offsetTarget, 0, cols.id1)
nextTick(() => {
// 删除源
girdMeta.colOrder.splice(dragInfo.sourceIndex - offsetSource, 1)
// table被重置了,所以需要重新加拖拽事件,重新排序。
nextTick(() => {
tableInfo.setThforDrag(tableClass, dragInfo)
})
})
}
几次调试之后,代码写成了这样。这里使用 ES6 的 array.splice 来实现数组的调整。
他可以删除指定位置的数组元素,也可以在指定位置添加数组元素,这个就很方便了。
-
offsetSource
第一个 th 的偏移位置,删除的时候使用,因为从后面往前拖拽,和从前面往后面拖拽,位置会不一样。 -
offsetTarget
第二个 th 的偏移位置,添加的时候使用,因为在前面插入和在后面插入,位置明显不同。
位置确定好了之后就方便了,先把第一个 th 插入 第二个 th 的附近,然后删掉第一个th即可。
- nextTick
可能你会觉得奇怪,为啥要用 nextTick?
我也不想呀,但是数组的直接修改,并不会引发模板的响应,就是说虽然数组内容变了,但是v-for并没有重新绘制。(不用问我为啥,我也不知道)
所以只好先加入一个元素,这样会响应,然后在 nextTick 里面删除一个,实现拖拽后排序的功能。
交换两个th的位置
有的时候需要直接交换 th1 和 th2 的位置,其他 th 的位置不用变,这时我们直接交换就好。
/**
* 交换两个th的位置
*/
const _changeSet = (cols) => {
// 交换
girdMeta.colOrder[dragInfo.sourceIndex - 1] = cols.id2
girdMeta.colOrder[dragInfo.targetIndex - 1] = cols.id1
// 强制响应
girdMeta.colOrder.push(cols.id1)
nextTick(() => {
// 删除多余的col
girdMeta.colOrder.splice(girdMeta.colOrder.length - 1, 1)
// table被重置了,所以需要重新加拖拽事件,重新排序。
nextTick(() => {
tableInfo.setThforDrag(tableClass, dragInfo)
})
})
}
还是老问题,交换之后 v-for 不会响应重置,所以只好先加一个,然后 nextTick 里面再删掉一个。好无语。有没有更好的方法呢?
设置 th、td 的对齐方式
一般td有三种对齐方式:左、中、右。标题一般都是居中对齐,内容就不一定了,文本内容一般左对齐,数字一般右对齐,时间、状态等一般居中对齐,这时候需要设置一下。
我们可以做一个规定:
- 拖拽时没有离开 th,则表示要修改对齐方式,而不是调整排序。
- 往左拖拽表示对齐方式“向左”,向右同理。
- 按住ctrl表示,要修改 th 的对齐方式,否则表示修改td。一般表头都是居中的。
实现代码
/**
* 设置th的对齐方式
*/
const _setThAlgin = (cols) => {
// 判断 th 还是 td
const alignKind = (dragInfo.ctrl) ? 'header-align' : 'align'
const col = girdMeta.itemMeta[cols.id1]
// 判断:左中右
switch (col[alignKind]) {
case 'left':
if (dragInfo.isLeft) {
col[alignKind] = 'left'
} else {
col[alignKind] = 'center'
}
break
case 'center':
if (dragInfo.isLeft) {
col[alignKind] = 'left'
} else {
col[alignKind] = 'right'
}
break
case 'right':
if (dragInfo.isLeft) {
col[alignKind] = 'center'
} else {
col[alignKind] = 'right'
}
break
}
}
不多解释了,反正就是各种判断各种修改。
入口
上面是分别介绍功能,但是没有写入口代码。
/**
* 设置th的顺序
*/
const setThOrder = () => {
// 获取colId
const cols = {
id1: girdMeta.colOrder[dragInfo.sourceIndex - 1],
id2: girdMeta.colOrder[dragInfo.targetIndex - 1]
}
if (dragInfo.sourceIndex !== dragInfo.targetIndex) {
// 源和目标不同,排序
if (dragInfo.ctrl) {
// 交换
_changeSet(cols)
} else {
// 插入
_order(cols)
}
} else {
// 源和目标相同,设置对齐方式
_setThAlgin(cols)
}
}
这样分解一下,代码应该可以更好维护。
好吧,还有最后一个函数
const dragInfo = reactive({
offsetX: 0,
isLeft: true, // 是否在 th 的左侧结束拖拽
ctrl: false, // 是否按下了ctrl
source: '',
target: '',
sourceIndex: 0, // 开始拖拽的位置
targetIndex: 0 // 结束拖拽的位置
})
const { setThOrder } = manageTable(girdMeta, dragInfo, tableInfo)
onMounted(() => {
nextTick(() => {
tableInfo.setThforDrag(tableClass, dragInfo)
watch(() => dragInfo, () => {
// console.log('外部:', dragInfo)
setThOrder()
},
{ deep: true })
})
})
这样就ok了。
做了一个简单的演示:
拖拽效果
网友评论