拖拽排序是很常见的功能,HTML 也提供了拖放接口使得 web 应用能够在网页中拖放文件。这篇文章介绍如何通过数据驱动的方式来实现 vue3 的拖拽排序
定义数据和排序方式
vue 的开发是采用数据驱动的方式,首先让我们定义好数据,以便 demo 使用,可以看到,我们定义了一些编程语言的名称
import { reactive } from "vue";
interface Language {
name: string;
}
const languages = reactive<Language[]>([
{
name: "typescript",
},
{
name: "javascript",
},
{
name: "C++",
},
{
name: "java",
},
{
name: "rust",
},
{
name: "golang",
},
]);
很多拖拽框架都支持你定义排序方式,但最基本的,你需要两个索引,当前拖拽的索引以及要替换的索引,因此我们定义以下规则,当前拖拽的 item 插入到要替换的索引下面
// 排序方式
const reorder = (languages: Language[], current: number, replace: number) => {
if (current === -1 || replace === -1) {
return;
}
const [removedLanguage] = languages.splice(current, 1);
languages.splice(replace, 0, removedLanguage);
};
页面 UI 和结构
UI 我使用了 tailwind css 框架,其中 draggable
是一个枚举类型的属性,用于标识元素是否允许使用拖放操作 API 拖动,详情可参考 MDN
<template>
<div class="dropzone">
<h2>Languages</h2>
<div
v-for="item in languages"
:key="item.name"
class="draggable"
draggable="true"
>
{{ item.name }}
</div>
</div>
</template>
<style scoped>
.draggable {
@apply bg-white my-3 p-3 text-center text-black dark:text-white dark:bg-black;
}
.dropzone {
@apply bg-green-600 p-3 m-2 w-96;
}
<style>
实现
上面我们提到需要两个索引,当前拖拽的索引和待替换的索引,我们用一个 reactive 变量表示
const record = reactive({
current: -1,
replace: -1,
});
当用户开始拖动一个元素或者一个选择文本的时候 dragstart
事件就会触发,我们可以在 dragstart
事件中赋值 current
const dragStart = (index: number) => {
record.current = index;
};
当拖动的元素或被选择的文本进入有效的放置目标时,dragenter
事件被触发,我们可以在 dragenter
事件赋值 replace
const dragEnter = (index: number) => {
record.replace = index;
};
最后我们还得实现 dragleave
事件,dragleave
事件在拖动的元素或选中的文本离开一个有效的放置目标时被触发,利用这个事件,可将 replace 置为 -1
const dragLeave = () => {
record.replace = -1;
};
最后还得实现两个事件,drop
和 dragover
,才能使得拖拽生效。在 drop 事件中,我们更改数据来实现排序并触发渲染
// drop 事件在元素或选中的文本被放置在有效的放置目标上时被触发
const drop = (event: DragEvent) => {
event.preventDefault();
reorder(languages, record.current, record.replace);
};
// 当元素或者选择的文本被拖拽到一个有效的放置目标上时,触发 dragover 事件(每几百毫秒触发一次)
const dragOver = (event: DragEvent) => {
event.preventDefault();
};
此时,template 就变成了下面这样
<template>
<div class="dropzone" @drop="drop($event)" @dragover="dragOver($event)">
<h2>Languages</h2>
<div
v-for="(item, index) in languages"
:key="item.name"
class="draggable"
draggable="true"
@dragstart="dragStart(index)"
@dragenter="dragEnter(index)"
@dragleave="dragLeave()"
>
{{ item.name }}
</div>
</div>
</template>
最后来看看效果
美化
上图可以看到,拖拽的时候,可以替换的 item 并没有高亮显示,视觉上不明显。因此我们来添加高亮显示,我们做两种区分,可替换的 hint 以及当前可替换的 alternative
修改 Language
接口
interface Language {
name: string;
hint: boolean;
alternative: boolean;
}
添加样式,并添加样式绑定 :class="{ hint: item.hint, alternative: item.alternative }"
.hint {
@apply bg-sky-300;
}
.alternative {
@apply bg-sky-700;
}
改造 drawEnter
方法
const dragEnter = (index: number) => {
record.replace = index;
languages.forEach((language, i) => {
if (i === index) {
language.hint = false;
language.alternative = true;
} else if (i === record.current) {
language.hint = false;
language.alternative = false;
} else {
language.hint = true;
language.alternative = false;
}
});
};
改造 dragLeave
方法
const dragLeave = (index: number) => {
record.replace = -1;
languages[index].hint = true;
languages[index].alternative = false;
languages[record.current].hint = false;
languages[record.current].alternative = false;
};
再添加一个 drawEnd
方法,当放置完成时,对数据和样式进行重置
const dragEnd = () => {
languages.forEach((language) => {
language.hint = false;
language.alternative = false;
});
record.current = -1;
record.replace = -1;
};
此时,template
变成了下面这样
<template>
<div class="dropzone" @drop="drop($event)" @dragover="dragOver($event)">
<h2>Languages</h2>
<div
v-for="(item, index) in languages"
:key="item.name"
class="draggable"
:class="{ hint: item.hint, alternative: item.alternative }"
draggable="true"
@dragstart="dragStart(index)"
@dragenter="dragEnter(index)"
@dragleave="dragLeave(index)"
@dragend="dragEnd()"
>
{{ item.name }}
</div>
</div>
</template>
最后来看看效果
总结
拖拽排序的实现不是一件很简单的事情,通过一番动手折腾也勉强实现了。可以看到,即便实现,代码仍非常多。在实际项目中,我更推荐使用社区成熟的轮子,欢迎在评论区讨论