我正在参与掘金创作者训练营第6期,点击了解活动详情
前言
技术永无止尽,多看看不同风景
上周利用antv实现了可视化图表,这周又回归到了pc端UI组件库和移动端框架维护,这不,业务部门又有需求了,希望能提供一个表格拖拽排序组件,支持上移、下移、置顶、置底以及多项拖拽排序,有需求就安排上,花了点时间进行调研,因为团队是以Vue技术栈为主,所以比较适合的有原生的sortablejs,Vue进行封装的vuedraggable,最终的选择sortablejs。
Why not vuedraggable
可能有人会问,既然用了Vue,并且有对于sortablejs做进一层封装的vuedraggable,为什么还是使用sortablejs,对于有一点思考能力,乐于钻研技术的程序员,每一个选择都是深思熟虑的,有其原因,这里也不例外,我最初的选择也是vuedraggable,不过在使用过程中遇到点问题,这个库对于框架的table不怎么友好,先说一下这个库的使用方式吧,这里是参照官方示例实现。
<template>
<draggable v-model="list" tag="tbody">
<tr v-for="item in list" :key="item.name">
<td scope="row">{{ item.id }}</td>
<td>{{ item.name }}</td>
<td>{{ item.sport }}</td>
</tr>
</draggable>
<template>
<script>
import draggable from 'vuedraggable'
export default {
components: {
draggable
}
}
</script>
这个库本质是一个Vue组件,有个弊端,只能拖拽draggable组件包裹的子级,当然也只能是子级,没办法跨级,一开始我也不相信为啥是这样子,所以我就去看一下它的源码实现,来解决我的疑问,直到我在源码中看到这样一句话,决定了它只能拖拽相邻子级,从示例中可以看出,我想要拖拽table的行,就需要将当前组件替代成tbody作为拖拽的外壳,但是正常的前端UI组件库,不管是ElementUI,还是ant-design-vue对于表格的封装都会避免我们去操作tbody,这样子没办法根据顶级类型做跨级的拖拽排序,对于UI组件库,还是挺不友好的
准备
安装依赖
npm i sortablejs
常用参数文档
| 属性 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| draggable | 允许拖拽的项目类名 | String | - |
| handle | 拖拽句柄,按住才能拖动 | String | - |
| animation | 排序动画的时间 | Number | 150 |
| ghostClass | 当前拖拽项对应的类名,可以设置拖拽项高亮 | String | - |
| chosenClass | 拖拽克隆项类名,设置拖拽克隆项样式 | String | - |
| multiDrag | 是否多选 | Boolean | false |
| selectedClass | 多选拖拽类名,multiDrag 为 true 时生效 | String | sortable-selected |
| 事件 | 说明 |
|---|---|
| onEnd | 拖拽结束时触发,用来处理改变后的数据,回调参数{oldIndex: 旧索引,newIndx: 新索引, oldIndicies:旧多选项,multiDrag 时生效,多选拖拽时有值 } |
拖拽
sortablejs是很完善很优秀的拖拽排序库,实现一个简单的拖拽排序还是很简单的,需要注意的点:
- 1.因为需要涉及到
dom的获取于绑定,要在moutend生命周期时才进行初始化, - 官方示例没有体现关于数据处理,虽然页面实现对应排序,但是对应的数据没有发生相应的变化,需要我们手动处理,处理也比较简单,在监听拖拽排序结束时事件
onEnd,根据其提供的回调参数新旧索引oldIndex、newIndex做相应的数组操作就可以了。
<template>
<div class="draggable">
<el-table ref="table" :data="list" stripe style="width: 100%">
<el-table-column prop="order" label="排序"></el-table-column>
<el-table-column prop="name" label="姓名"></el-table-column>
<el-table-column prop="age" label="年龄"></el-table-column>
<el-table-column width="200" label="操作" align="center">
<template #default="{ $index }">
<div class="list">
<div class="list-item handle" title="拖动">
<img src="./components/icons/move.png" alt="" />
</div>
</div>
</template>
</el-table-column>
</el-table>
</div>
</template>
<script>
import Sortable from "sortablejs";
export default {
name: "Draggable",
data() {
return {
list: [
{ id: "1", order: 1, name: "小明1", age: 18 },
{ id: "2", order: 2, name: "小明2", age: 28 },
{ id: "3", order: 3, name: "小明3", age: 33 },
{ id: "4", order: 4, name: "小明4", age: 44 },
{ id: "5", order: 5, name: "小明5", age: 55 },
{ id: "6", order: 6, name: "小明6", age: 66 },
],
};
},
mounted() {
this.init();
},
methods: {
init() {
const ele = document.querySelector(".draggable tbody");
// eslint-disable-next-line no-unused-vars
const sortable = new Sortable(ele, {
handle: ".handle",
draggable: ".el-table__row",
ghostClass: "draggable-ghost",
onEnd: ({ oldIndex, newIndex }) => {
const { list } = this;
const item = list.splice(oldIndex, 1)[0];
list.splice(newIndex, 0, item);
},
});
},
},
};
</script>
多项拖拽
对于多项拖拽,sortablejs的文档讲的其实不是很浅显易懂,需要做一下分析,不过其实增加了两步操作。
- 引入
MultiDrag,并做相应绑定
import { Sortable, MultiDrag } from 'sortablejs';
Sortable.mount(new MultiDrag());
multiDrag属性设置为true,设置selectedClass相应类名
完整示例的代码量比较多,具体可以查看Github
置顶置底
对于置顶、置底,可能有人会惯性思维,拖拽排序,所有的排序操作都需要依赖于sortablejs,这样子就把事情复杂化了,其实置顶、置底很简单的,简单几行代码就能实现,首先判断做一下边界条件判断,第一项不需要置顶,最后一项不需要置底,置顶只需要剪切当前项,利用数组方法unshift插入到第一项,置底只需要剪切当前项,利用数组方法push插入到最后一项就可以了。
// 置顶
setTop(index) {
// 第一行,不需要改变
if (index === 0) return;
const { tableData } = this;
tableData.unshift(tableData.splice(index, 1)[0]);
this.handleEmit(tableData);
},
// 置底
setBottom(index) {
const { tableData } = this;
// 最后一行,不需要改变
if (index === tableData.length - 1) return;
tableData.push(tableData.splice(index, 1)[0]);
this.handleEmit(tableData);
},
上下移动
上移、下移比较一致,本质都是删除当前项,然后插入到新索引位置,下移是删除当前项,插入到后一位索引,上移是删除当前项,插入到前一位索引,但是要添加边界判断,第一项不需要上移,最后一项不需要下移,插入一项到数组中,可以使用splice数组API。
// 上移
moveUp(index) {
// 第一行,不需要改变
if (index === 0) return;
const { tableData } = this;
tableData[index] = tableData.splice(index - 1, 1, tableData[index])[0];
this.handleEmit(tableData);
},
// 下移
moveDown(index) {
const { tableData } = this;
// 最后一行,不需要改变
if (index === tableData.length - 1) return;
tableData[index] = tableData.splice(index + 1, 1, tableData[index])[0];
this.handleEmit(tableData);
},
多项置顶置底
多项操作,这些项的选择可能是连续的,也可能是隔开的,所以处理起来比较麻烦,养成好习惯,先做边界判断,判断是否选中行,没有选中的话,什么也不需要操作,至于置顶或置底,不去考虑用户怎么选择,连续选择,还是跨行选择,拿到选中行和完整表格数做对比,计算出差集,置顶则选择项在差集前面插入,置底则选择项在差集后面插入,这样子逻辑清晰又高效的完成了多项选择的置顶置底。
// 获取表格选中项
handleSelect() {
return this.selectList;
},
// 判断是否选中行
hasSelectRow() {
const list = this.handleSelect();
if (!list.length) {
this.$message.error("请先选择行!");
}
return list.length > 0;
},
// 处理差集
handleDiff(list, selectList) {
return [...list].filter((x) =>
[...selectList].every((y) => y.id !== x.id)
);
},
// 置顶
setMultiTop() {
// 未选择行
if (!this.hasSelectRow()) return;
const { tableData } = this;
const selectList = this.handleSelect();
const diffList = this.handleDiff(tableData, selectList);
this.handleEmit([...selectList, ...diffList]);
},
// 置底
setMultiBottom() {
// 未选择行
if (!this.hasSelectRow()) return;
const { tableData } = this;
const selectList = this.handleSelect();
const diffList = this.handleDiff(tableData, selectList);
this.handleEmit([...diffList, ...selectList]);
},
小结
文章对于每个核心点做了阐述,还是比较清晰易懂的,当然也提供了完整的示例代码,放在Github
这篇文章虽然是以Vue为示例,但是核心逻辑是不变的,Sortablejs初始化是一致的,上移、下移、置顶、置底以及多项拖拽排序的核心逻辑还是基础JS逻辑,这里只是抛砖引玉,希望大家可以举一反三,有问题欢迎评论区讨论,共同成长,fighting~