就拿我们目前的产品来看,需要拖拽的场景并不少见。
有拖拽可以使列表排序的
有拖拽生成合同控件的
前置知识
要完成这个拖拽排序的效果,我们可以使用html5新增的draggable来实现。
将想要拖拽的元素的 draggable
属性设置成 "true"
,即可让该元素成为可以拖拽的对象。当draggable的属性值为true时,有关拖拽的几个事件就可以生效了。
dragstart
事件会在用户开始拖动元素时调用。dragenter
事件在可拖动的元素进入一个有效的放置目标时触发。简单的理解就是列表中序号为1的元素,移动到序号为2的元素上时就会触发dragenter
事件,此时回调函数内的event值是被进入的元素,也就是序号为2的元素。dragend
事件在拖放操作结束时触发,就是我们拖拽放手的那一刻。drop
事件在元素或文本选择被放置到有效的放置目标上时触发。dragenter
是正在进入目标元素,而drop
表示拖动元素已经被释放到目标元素上。dragover
事件在可拖动的元素或者被选择的文本被拖进一个有效的放置目标时(每几百毫秒)触发。dragleave
事件在拖动的元素或选中的文本离开一个有效的放置目标时被触发。dragover
事件在拖动元素在目标元素上移动时持续触发,即鼠标指针在目标元素上移动时触发。dragenter
事件在拖动元素进入目标元素时触发,但只触发一次。
更详细的解释可以去MDN查看
developer.mozilla.org/zh-CN/docs/...
实现一个简单的拖拽列表
有了基本的理论知识以后,我们可以试着实现一个简单的拖拽列表。
首先实现这个列表排序的方式有很多种,我的思路肯定不是最好的,这里仅仅是抛砖引入,举个例子更好的方案往往在评论区。
因为是用vue实现的,所以我在换位置的时候我不需要操作dom,只需要修改list中元素的位置即可。
我的实现思路是在dragstart开始拖拽的时候记住开始的下标currentDragDomIndex和具体的值draggedItem ,每进入一个其他元素触发dragenter时,可以得到进入元素的下标index。之后把list中下标为currentDragDomIndex的元素删除,把draggedItem插入到index的位置,最后更新currentDragDomIndex为新的下标位置即可。
开始: a b c d e
下标: 0 1 2 3 4
拖拽a时 dragstart记住a的下标是0 值是a 即 currentDragDomIndex=0; draggedItem =a
拖拽a 进入b时 dragenter里记住进入元素的下标1 即index=1 然后删除下标为currentDragDomIndex的元素
得: b c d e
然后把draggedItem插入到下标index的位置,最后currentDragDomIndex = index即可
b a c d e
具体代码如下,可直接建一个html运行。
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
<div ref="list">
<div v-for="(item, index) in list" draggable @dragstart="(e) => dragstart(e, index)"
@dragenter="(e) => dragenter(e, index)" @dragend="dragend" :class="{ 'box': true, 'moving': currentDragDom === item }">
{{ item }}
</div>
</div>
</div>
<script src='https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js'></script>
<script>
new Vue({
el: '#app',
data: {
list: [1, 2, 3, 4, 5],
currentDragDomIndex: null
},
methods: {
dragstart(e, index) {
this.currentDragDomIndex = index;
},
dragenter(e, index) {
e.preventDefault();
if (index !== this.currentDragDomIndex) {
const draggedItem = this.list[this.currentDragDomIndex];
this.list.splice(this.currentDragDomIndex, 1);
this.list.splice(index, 0, draggedItem);
this.currentDragDomIndex = index;
}
},
dragend() {
this.currentDragDomIndex = null;
}
}
})
</script>
<style>
.box {
width: 200px;
height: 25px;
background-color: rgb(192, 215, 255);
margin-bottom: 10px;
border-radius: 5px;
padding-left: 10px;
}
</style>
</body>
</html>
实现了基本的拖拽排序,拖拽api本身不难理解,难点在于对排序逻辑的处理。我是在dragenter中进行排序逻辑的处理,在松手的那一刻进行排序也是可以的,实现的方式不唯一。
封装成组件
基本的逻辑我们实现了之后,下一步就是怎么用的简单了,如果每个拖拽列表我都把拖拽的逻辑复制一份,但凡拖拽逻辑需要更改我就需要挨个都改一遍。封装成组件就只需要改一处了,并且用的简单。
这个拖拽列表组件是一个通用组件,它是一个视图组件。里面应该仅仅是对列表进行展示,在它内部不应该包含任何对业务的处理。
我决定用作用域插槽的方式来实现。目的在于可以在组件内部完成对数据的处理,处理完直接抛出了完事。展示成什么样子由外面说的算非常灵活。
至于排序的逻辑和上面的例子相同。
本例使用vue3实现,练练手
index.vue
js
<script setup>
import { ref } from 'vue';
import DraggableList from '../components/draggableList.vue';
const list = ref([{ no: 1001, name: '小明' }, { no: 1002, name: ''}, { no: 1003, name: ''}]);
const columns = [
{
label: '编号',
width: '240px'
},
{
label: '姓名',
width: '440px'
}
]
function changeListOrder(n) {
list.value = n
}
</script>
<template>
<DraggableList :list="list" :columns="columns" @change="changeListOrder">
<template v-slot="{ value }">
<div style="display: flex;">
<el-form-item style="width: 240px;">
{{ value.no }}
</el-form-item>
<el-form-item style="width: 440px;">
<el-input v-model="value.name"></el-input>
</el-form-item>
</div>
</template>
</DraggableList>
<el-button @click="list.push({ no: new Date().getTime(), name: '' })">新增</el-button>
</template>
DraggableList.vue
需要注意单向数据流,不要对传入的值进行修改。仅仅作为展示来使用
js
<script setup>
const emit = defineEmits(['change'])
const props = defineProps({
list: {
type: Array,
default: () => []
},
columns: {
type: Array,
default: () => []
}
})
let currentDragDomIndex = 0
function dragenter(e, index) {
if (index === currentDragDomIndex) {
return
}
const nList = [...props.list] // 建议深拷贝
const draggedItem = nList[currentDragDomIndex];
nList.splice(currentDragDomIndex, 1);
nList.splice(index, 0, draggedItem);
currentDragDomIndex = index;
emit('change', nList) // 最后将排好序的数组抛出去
}
function dragend() {
currentDragDomIndex = null;
}
function dragstart(e, index) {
currentDragDomIndex = index;
}
</script>
<template>
<div ref="list">
<div v-if="columns.length" style="display: flex;background:#F7FAFD">
<div :key="index" v-for="(item, index) in columns" :style="{
height: '48px',
lineHeight: '48px',
flex: item.width === 'auto' ? `1 1 auto` : `0 0 ${item.width}`
}">
{{ item.label }}
</div>
</div>
<div v-for="(item, index) in list" :key="index" :draggable="true" @dragstart="(e) => dragstart(e, index)"
@dragenter="(e) => dragenter(e, index)" @dragend="dragend" style="text-align: center;">
<slot :value="item"></slot>
</div>
</div>
</template>
<style scoped></style>
结尾
本文是我对拖拽功能学习的一个总结,水平有限,希望大家能有所收获!