在Vue3中实现拖拽排序,可以借助一些浏览器自带的API,以及一些Vue3的特性:
使用<template>
标签中的v-for
指令渲染出一个列表,每个列表项绑定一个draggable
属性,使其能够被拖拽。
<template>
<ul>
<li v-for="(item, index) in list" :key="item.id" :draggable="true" @dragstart="dragStart(index)">
{{ item.title }}
</li>
</ul>
</template>
在<script>
标签中,定义一个list
数组,用于存储待排序的数据。同时,定义一个dragIndex
变量,记录当前正在拖拽的元素的索引位置。
<script>
import { reactive } from 'vue';
export default {
setup() {
const state = reactive({
list: [
{ id: 1, title: 'Item 1' },
{ id: 2, title: 'Item 2' },
{ id: 3, title: 'Item 3' },
{ id: 4, title: 'Item 4' },
{ id: 5, title: 'Item 5' }
],
dragIndex: null
});
const dragStart = (index) => {
state.dragIndex = index;
}
return { state, dragStart };
}
}
</script>
使用@dragenter
和@dragover
事件处理函数,阻止默认行为,避免无法放置拖拽元素。
<template>
<ul>
<li v-for="(item, index) in state.list" :key="item.id" :draggable="true" @dragstart="dragStart(index)" @dragenter.prevent @dragover.prevent>
{{ item.title }}
</li>
</ul>
</template>
在@dragenter
事件处理函数中,获取当前拖拽元素的索引位置,以及目标元素的索引位置。根据两个索引位置的大小关系,判断拖拽元素是否需要往前移动或往后移动,同时更新list
数组的顺序。
const dragEnter = (index) => {
if (state.dragIndex !== null && state.dragIndex !== index) {
// 计算拖拽元素和目标元素的位置关系
const dragItem = state.list[state.dragIndex];
const targetItem = state.list[index];
const isAfter = state.dragIndex < index;
// 更新列表顺序
state.list.splice(state.dragIndex, 1);
state.list.splice(isAfter ? index - 1 : index, 0, dragItem);
state.dragIndex = isAfter ? index : index - 1;
}
};
在<template>
标签中,绑定@dragenter
事件,同时定义一个方法,将当前目标元素的索引位置作为参数传递给该方法。
<template>
<ul>
<li v-for="(item, index) in state.list" :key="item.id" :draggable="true" @dragstart="dragStart(index)" @dragenter.prevent @dragover.prevent @dragenter="dragEnter(index)">
{{ item.title }}
</li>
</ul>
</template>
这样,就可以实现Vue3的拖拽排序功能了。完整代码如下:
<template>
<ul>
<li v-for="(item, index) in state.list" :key="item.id" :draggable="true" @dragstart="dragStart(index)" @dragenter.prevent @dragover.prevent @dragenter="dragEnter(index)">
{{ item.title }}
</li>
</ul>
</template>
<script>
import { reactive } from 'vue';
export default {
setup() {
const state = reactive({
list: [
{ id: 1, title: 'Item 1' },
{ id: 2, title: 'Item 2' },
{ id: 3, title: 'Item 3' },
{ id: 4, title: 'Item 4' },
{ id: 5, title: 'Item 5' }
],
dragIndex: null
});
const dragStart = (index) => {
state.dragIndex = index;
}
const dragEnter = (index) => {
if (state.dragIndex !== null && state.dragIndex !== index) {
const dragItem = state.list[state.dragIndex];
const targetItem = state.list[index];
const isAfter = state.dragIndex < index;
state.list.splice(state.dragIndex, 1);
state.list.splice(isAfter ? index - 1 : index, 0, dragItem);
state.dragIndex = isAfter ? index : index - 1;
}
};
return { state, dragStart, dragEnter };
}
}
</script>
HTML5 新增的可拖拽属性
draggable
属性是 HTML5 新增的可拖拽属性HTML 中,除了图像、链接和选择的文本默认可拖拽外,其他元素默认是不可拖拽的。如果想让其他元素变成可拖拽的,首先需要把
draggable
属性设置为true
<p draggable="true"> 可拖拽draggable</p>
拖拽元素的事件
事件 | 触发时机 |
---|---|
dragstart | 开始拖拽时执行 1 次 |
drag | 拖拽开始后多次触发 |
dragend | 拖动结束后触发 1 次 |
可释放目标的事件
事件 | 触发时机 |
---|---|
dragenter | 拖拽元素进入可释放目标时执行 1 次 |
dragover | 拖拽元素进入可释放目标时触发多次(100毫秒触发一次) |
drop | 拖拽元素进入可释放目标内释放时(设置了dragover此事件才会生效) |
可放置目标
dragenter 或 dragover事件可用于表示有效的放置目标,也就是被拖拽元素可能放置的地方。
设置允许被被放置还需要阻止 dragenter 和 dragover 事件的默认处理。
<div ondragenter="event.preventDefault()">
- 创建一个列表,遍历渲染到页面
- 列表项添加
draggable="true"
- 列表项添加事件
dragstart
dragenter
dragend
dragover
- 在
dragenter
事件中,需要传入列表项的下标,实时进行元素的排序。排序的核心逻辑也是在dragenter
中- 代码执行的逻辑是:列表项拖拽到可放置目标时,将该拖拽的元素从原位置删除,再将拖拽的元素插入到当前可放置目标的位置
<template>
<div>
<TransitionGroup name="list" tag="div" class="container">
<div
class="item"
v-for="(item, i) in drag.list"
:key="item.id"
:draggable="true"
@dragstart="dragstart($event, i)"
@dragenter="dragenter($event, i)"
@dragend="dragend"
@dragover="dragover"
>
{{ item.name }}
</div>
</TransitionGroup>
</div>
</template>
<script setup>
import { reactive } from 'vue';
const drag = reactive({
list: [
{ name: 'a', id: 1 },
{ name: 'b', id: 2 },
{ name: 'c', id: 3 },
{ name: 'd', id: 4 },
{ name: 'e', id: 5 }
]
});
let dragIndex = 0;
function dragstart(e, index) {
e.stopPropagation();
dragIndex = index;
setTimeout(() => {
e.target.classList.add('moveing');
}, 0);
}
function dragenter(e, index) {
e.preventDefault();
// 拖拽到原位置时不触发
if (dragIndex !== index) {
const source = drag.list[dragIndex];
drag.list.splice(dragIndex, 1);
drag.list.splice(index, 0, source);
// 更新节点位置
dragIndex = index;
}
}
function dragover(e) {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
}
function dragend(e) {
e.target.classList.remove('moveing');
}
</script>
<style lang="scss" scoped>
.item {
width: 200px;
height: 40px;
line-height: 40px;
// background-color: #f5f6f8;
background-color: skyblue;
text-align: center;
margin: 10px;
color: #fff;
font-size: 18px;
}
.container {
position: relative;
padding: 0;
}
.moveing {
opacity: 0;
}
.list-move, /* 对移动中的元素应用的过渡 */
.list-enter-active,
.list-leave-active {
transition: all 0.2s ease;
}
</style>
sortable.js-配置文档
Element Plus
组件库中使用sortable.js
进行表格排序
npm i sortablejs -S
<template>
<div>
<el-table :data="tableData" id="dragTable" border style="width: 800px;">
<el-table-column prop="date" label="Date" width="180" />
<el-table-column prop="name" label="Name" width="180" />
<el-table-column prop="address" label="Address" />
</el-table>
</div>
</template>
<script setup>
const tableData = [
{
date: '2016-05-03',
name: 'Tom',
address: 'No. 189, Grove St, Los Angeles',
},
{
date: '2016-05-02',
name: 'Cilly',
address: 'No. 189, Grove St, Los Angeles',
},
{
date: '2016-05-04',
name: 'Linda',
address: 'No. 189, Grove St, Los Angeles',
},
{
date: '2016-05-01',
name: 'John',
address: 'No. 189, Grove St, Los Angeles',
},
]
</script>
导入 sortable.js
<script setup>
import Sortable from 'sortablejs'
import { onMounted } from 'vue'
function setSort() {
const el = document.querySelector('#dragTable table tbody')
new Sortable(el, {
sort: true,
ghostClass: 'sortable-ghost',
onEnd: (e) => {
const targetRow = tableData.splice(e.oldIndex, 1)[0]
tableData.splice(e.newIndex, 0, targetRow)
console.log(tableData)
},
})
}
onMounted(() => {
setSort()
})
const tableData = [
// ...
]
</script>
在
onMounted
中,也就是组件挂载完成之后,实例化Sortable()
,传入要进行拖拽排序的节点el
和其它一些配置参数。现在可以进行拖拽了。
可能需要按住拖动图标才可以进行拖动,需要添加
handle
配置,并指定对应的样式名
<el-table :data="tableData" id="dragTable" border style="width: 600px; margin: 20px">
<!-- ...... 省略代码 -->
<el-table-column label="操作" width="100">
<template #default>
<div class="handle-drag">
<el-icon>
<Sort />
</el-icon>
</div>
</template>
</el-table-column>
</el-table>
上面代码将表格添加了一个操作列,并将操作列的图标设置一个样式类。下面的配置表示只有包含
.handle-drag
样式的元素才可以被拖动。其他位置不能被拖动
new Sortable(el, {
// ...
handle: '.handle-drag',
// ...
})
vuedraggable-配置文档-推荐
vue.draggable.next
是 Vue3 的拖拽组件,是基于 Sortable.js 实现的。可以用于拖拽列表、菜单、工作台、选项卡等常见的场景。
npm i -S vuedraggable@next
属性
参数 | 说明 | 类型 | 默认值 |
---|---|---|---|
value | 用于实现拖拽的list,通常和内部v-for循环的数组为同一数组 | Array | null |
list | 效果同value的。和v-model不能共用 | Array | null |
tag | draggable 标签在渲染后展现出来的标签类型 | String | div |
options | draggable 列表配置项 | Object | null |
emptyInsertThreshold | 拖动时,鼠标必须与空的可排序对象之间的距离 | Number | 5 |
clone | 返回值为true时克隆,可以理解为正常的拖拽变成了复制。当pull:'clone时的拖拽的回调函数’ | Function | 无处理 |
move | 如果不为空,这个函数将以类似于Sortable onMove回调的方式调用。返回false将取消拖动操作。 | Function | null |
componentData | 用来结合UI组件的,可以理解为代理了UI组件的定制信息 | Object | null |
注意:vuedraggable新版本废弃了options属性,建议使用v-bind属性作为配置项
options配置项
参数 | 说明 | 类型 |
---|---|---|
group | 用于分组,同一组的不同list可以相互拖动 | String/Array |
sort | 定义是否可以拖拽 | Boolean |
delay | 定义鼠标选中列表单元可以开始拖动的延迟时间 | Number |
disabled | 定义是否此sortable对象是否可用 | Boolean |
animation | 动画时间 单位:ms | Number |
handle | 使列表单元中符合选择器的元素成为拖动的手柄,只有按住拖动手柄才能使列表单元进行拖动 | Selector |
filter | 定义哪些列表单元不能进行拖放,可设置为多个选择器,中间用“,”分隔 | Selector |
preventOnFilter | 当拖动filter时是否触发event.preventDefault() 默认触发 | Boolean |
draggable | 定义哪些列表单元可以进行拖放 | Selector |
ghostClass | 当拖动列表单元时会生成一个副本作为影子单元来模拟被拖动单元排序的情况,此配置项就是来给这个影子单元添加一个class | Selector |
chosenClass | 目标被选中时添加 | Selector |
dragClass | 目标拖动过程中添加 | Selector |
forceFallback | 如果设置为true时,将不使用原生的html5的拖放,可以修改一些拖放中元素的样式等 | Boolean |
fallbackClass: | 当forceFallback设置为true时,拖放过程中鼠标附着单元的样式 | String |
dataIdAttr | data-id | Selector |
scroll | 当排序的容器是个可滚动的区域,拖放可以引起区域滚动 | Boolean |
scrollFn | 用于自定义滚动条的适配 | Function(offsetX, offsetY, originalEvent, touchEvt, hoverTargetEl) |
ScrollSensitivity | 就是鼠标靠近边缘多远开始滚动默认30 | Number |
scrollSpeed | 滚动速度 | Number |
事件
参数 | 说明 | 回调参数 |
---|---|---|
start | 开始拖动时的回调函数 | function({to,from,item,clone,oldIndex,newIndex}) |
add | 添加单元时的回调函数 | function({to,from,item,clone,oldIndex,newIndex}) |
remove | 单元被移动到另一个列表时的回调函数 | function({to,from,item,clone,oldIndex,newIndex}) |
update | 排序发生变化时的回调函数 | function({to,from,item,clone,oldIndex,newIndex}) |
end | 拖动结束时的回调函数 | function({to,from,item,clone,oldIndex,newIndex}) |
choose | 选择单元时的回调函数 | function({to,from,item,clone,oldIndex,newIndex}) |
sort | 排序发生变化时的回调函数 | function({to,from,item,clone,oldIndex,newIndex}) |
filter | 尝试选择一个被filter过滤的单元的回调函数 | function({to,from,item,clone,oldIndex,newIndex}) |
clone | clone时的回调函数 | function({to,from,item,clone,oldIndex,newIndex}) |
插槽
页眉或页脚插槽都不能与 tarnstion-group 一起使用。
Header
使用标题插槽在vuedraggable组件中添加不可拖动的元素。它应该与draggable选项一起使用来标记draggable元素。请注意,无论标题槽在模板中的位置如何,它总是被添加到默认槽之前。
<draggable v-model="myArray" draggable=".item">
<div v-for="element in myArray" :key="element.id" class="item">
{{element.name}}
</div>
<button slot="header" @click="addPeople">Add</button>
</draggable>
Footer
使用页脚槽在vuedraggable组件中添加不可拖动的元素。它应该与draggable选项一起使用,以标记draggable元素。请注意,无论页脚槽在模板中的位置如何,它都将始终添加到默认槽之后。
<draggable v-model="myArray" draggable=".item">
<div v-for="element in myArray" :key="element.id" class="item">
{{element.name}}
</div>
<button slot="footer" @click="addPeople">Add</button>
</draggable>
使用代码
<script setup>
import draggable from 'vuedraggable'
import { reactive } from 'vue'
const state = reactive({
list1: [1, 2, 3, 4],
list2: ['a', 'b', 'c', 'd'],
})
function onStart() {}
function onEnd() {
console.log(state)
}
</script>
导入 draggable
并定义一些基础数据
<template>
<div style="margin-left: 30px;">
<draggable
:list="state.list1"
:force-fallback="true"
chosen-class="chosen"
animation="300"
@start="onStart"
@end="onEnd"
>
<template #item="{ element }">
<div class="item">
{{ element }}
</div>
</template>
</draggable>
</div>
</template>
其中
@start
和@end
为拖拽开始和结束时的事件。chosen-class
为拖拽时的样式
为组件设置相同的
group
属性,可以实现在不同的块之间拖拽
<draggable group="group" :list="state.list1" >
<template #item="{ element }">
<div class="item bck1">
{{ element }}
</div>
</template>
</draggable>
<draggable group="group" :list="state.list2" >
<template #item="{ element }">
<div class="item bck2">
{{ element }}
</div>
</template>
</draggable>