vue3 使用原生 api 实现拖拽排序

1,388 阅读3分钟

拖拽排序是很常见的功能,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;
};

最后还得实现两个事件,dropdragover,才能使得拖拽生效。在 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>

最后来看看效果

3.gif

美化

上图可以看到,拖拽的时候,可以替换的 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>

最后来看看效果

4.gif

总结

拖拽排序的实现不是一件很简单的事情,通过一番动手折腾也勉强实现了。可以看到,即便实现,代码仍非常多。在实际项目中,我更推荐使用社区成熟的轮子,欢迎在评论区讨论