穿梭的拖拽排序,轻松拿捏

942 阅读4分钟

前言

上篇文章 拖拽排序,换个思路轻松搞定 实现了基础的列表拖拽排序,本篇文章我们实现一个可以穿梭的拖拽排序效果,效果如下

sort-two.gif

功能实现思路

上篇文章 拖拽排序,换个思路轻松搞定,我们说到了只要记录2个索引,鼠标按下的时的索引,鼠标移动时的索引就可以完成排序效果

穿梭的列表排序实现思路也是如此,因为不管你几个列表,你还是1对1的交换,所以还是记录2个索引即可,不同的是你要知道是哪一个列表的哪个索引,而不是简单的一个列表中的哪个索引

如何判断是哪一个列表的哪一个索引呢?

有很多思路,比如在不同的列表元素上加上不同的group,然后在对应的事件中根据group区分,这里我说下我的思路

尝试猜测,统一处理

尝试猜测的意思:我们在交换的过程中会拿到2个event,分别是鼠标按下的时的event鼠标移动时的event,我们可以根据event获取到元素id,在根据元素id2个列表中都获取一下,哪个列表能获取到数据就知道当前元素所在的列表了,伪代码如下

function getIndexAndListByEvent(event: EnhancedMouseEvent) {
  const element = event.target!
  const id = element.getAttribute('id') as string
  const list1Getter = unref(id2IndexByList1Getter) // 列表1的id和索引map表
  const list2Getter = unref(id2IndexByList2Getter) // 列表2的id和索引map表

  if (list1Getter.has(id)) {
    return {
      index: list1Getter.get(id)!,
      list: unref(list1Ref),
    }
  }

  if (list2Getter.has(id)) {
    return {
      index: list2Getter.get(id)!,
      list: unref(list2Ref),
    }
  }
}

统一处理的意思:我们已经知道了索引和列表数据,所以我们可以这样写交换方法

function swap(startEvent: EnhancedMouseEvent, moveEvent: EnhancedMouseEvent) {
  const startIndexAndList = getIndexAndListByEvent(startEvent)
  const targetIndexAndList = getIndexAndListByEvent(moveEvent)
  if (!startIndexAndList || !targetIndexAndList) return
  const { index: startIndex, list: list1 } = startIndexAndList
  const { index: targetIndex, list: list2 } = targetIndexAndList
  if (list1 === list2 && startIndex === targetIndex) return
  // swap
  const temp = list1[startIndex]
  list1[startIndex] = list2[targetIndex]
  list2[targetIndex] = temp
}

这样实现的思路优缺点我说明一下

优点:不需要判断2个索引是不是在一个列表,然后单独的处理

缺点:需要id唯一

功能实现

布局代码

在实现之前,我们可以把布局先写出来,布局代码如下

<script setup lang="tsx">
import type { EnhancedMouseEvent } from '@drag-drop/core'
import { useDragDrop } from '@drag-drop/core'
import { sortPlugin } from '@drag-drop/plugin-sort'
import { useEventListener } from '@drag-drop/shared'
import { computed, ref, unref } from 'vue'

interface Item {
  id: string
  name: string
  color: string
}

const baseBackgroundColor = 'skyblue'
const swapBackgroundColor = 'pink'

const list1Ref = ref<Item[]>([
  { id: '1', name: '1', color: baseBackgroundColor },
  { id: '2', name: '2', color: baseBackgroundColor },
  { id: '3', name: '3', color: baseBackgroundColor },
  { id: '4', name: '4', color: baseBackgroundColor },
  { id: '5', name: '5', color: baseBackgroundColor },
  { id: '6', name: '6', color: baseBackgroundColor },
  { id: '7', name: '7', color: baseBackgroundColor },
])

const list2Ref = ref<Item[]>([
  { id: '8', name: '8', color: baseBackgroundColor },
  { id: '9', name: '9', color: baseBackgroundColor },
  { id: '10', name: '10', color: baseBackgroundColor },
  { id: '11', name: '11', color: baseBackgroundColor },
  { id: '12', name: '12', color: baseBackgroundColor },
  { id: '13', name: '13', color: baseBackgroundColor },
  { id: '14', name: '14', color: baseBackgroundColor },
])

const id2IndexByList1Getter = computed(() => {
  const list = unref(list1Ref)
  return list.reduce((p, c, i) => {
    p.set(c.id, i)
    return p
  }, new Map<string, number>())
})

const id2IndexByList2Getter = computed(() => {
  const list = unref(list2Ref)
  return list.reduce((p, c, i) => {
    p.set(c.id, i)
    return p
  }, new Map<string, number>())
})

</script>

<template>
  <TransitionGroup tag="div" class="container" name="list">
    <div v-for="item in list1Ref" :id="item.id" :key="item.id" class="item" :style="{ background: item.color }">
      {{ item.name }}
    </div>
  </TransitionGroup>
  <TransitionGroup tag="div" class="container" name="list">
    <div v-for="item in list2Ref" :id="item.id" :key="item.id" class="item" :style="{ background: item.color }">
      {{ item.name }}
    </div>
  </TransitionGroup>
</template>

<style scoped>
.container{
  display:inline-flex;
  flex-direction:column;
  gap:10px;
  margin:100px 0 0 30px;
  padding:20px;
  border:1px solid #ccc;
}

.item{
  display:flex;
  align-items:center;
  justify-content:center;
  width:200px;
  height:40px;
  user-select:none;
  border-radius:5px;
}
</style>

以上代码运行后效果如下

image.png

引入 sort 插件

在这篇文章 拖拽排序,换个思路轻松搞定 中,我们实现了sort插件,这个插件的作用是整个拖拽的过程都监听了,只需要实现一个swap方法即可,我们先引入它

<script setup lang="tsx">
import type { EnhancedMouseEvent } from '@drag-drop/core'
import { useDragDrop } from '@drag-drop/core'
import { sortPlugin } from '@drag-drop/plugin-sort'

...
function canDraggable(event: EnhancedMouseEvent) {
  return !!event.target?.classList.contains('item')
}

const context = useDragDrop({
  canDraggable,
})

context.use(sortPlugin({
  swap, // 这里需要用户去实现
}))

</script>

说明一下

useDragDrop 方法: 是我们之前这篇文章 用插件化 + CompositionApi 的方式实现一个可扩展的拖拽通用库 介绍的,主要就是监听了拖拽流程,然后暴露给外面一些拖拽钩子,实现了插件化的架构

sortPlugin 方法:是我们上篇文章 拖拽排序,换个思路轻松搞定 已经实现的,内部监听了拖拽排序需要用到的钩子,然后在合适的时机调用用户的swap 函数,用户只需要在 swap 函数中实现数据的交换即可

实现 swap 函数

我们在上面的 功能实现思路 这块已经说明了应该如何编写,这里我将它引入过来,代码如下

<script setup lang="tsx">
import type { EnhancedMouseEvent } from '@drag-drop/core'
import { useDragDrop } from '@drag-drop/core'
import { sortPlugin } from '@drag-drop/plugin-sort'
import { useEventListener } from '@drag-drop/shared'
import { computed, ref, unref } from 'vue'

function canDraggable(event: EnhancedMouseEvent) {
  return !!event.target?.classList.contains('item')
}

const context = useDragDrop({
  canDraggable,
})

context.use(sortPlugin({
  swap,
}))

function getIndexAndListByEvent(event: EnhancedMouseEvent) {
  const element = event.target!
  const id = element.getAttribute('id') as string
  const list1Getter = unref(id2IndexByList1Getter)
  const list2Getter = unref(id2IndexByList2Getter)

  if (list1Getter.has(id)) {
    return {
      index: list1Getter.get(id)!,
      list: unref(list1Ref),
    }
  }

  if (list2Getter.has(id)) {
    return {
      index: list2Getter.get(id)!,
      list: unref(list2Ref),
    }
  }
}

function swap(startEvent: EnhancedMouseEvent, moveEvent: EnhancedMouseEvent) {
  const startIndexAndList = getIndexAndListByEvent(startEvent)
  const targetIndexAndList = getIndexAndListByEvent(moveEvent)
  if (!startIndexAndList || !targetIndexAndList) return
  const { index: startIndex, list: list1 } = startIndexAndList
  const { index: targetIndex, list: list2 } = targetIndexAndList
  if (list1 === list2 && startIndex === targetIndex) return
  // swap
  const temp = list1[startIndex]
  list1[startIndex] = list2[targetIndex]
  list2[targetIndex] = temp
}

</script>

上述代码实现后页面可以实现穿梭交换了,但是并没有动画效果,我们只需要在style中加一行代码即可

<template>
  <TransitionGroup ... name="list">
    ...
  </TransitionGroup>
  <TransitionGroup ... name="list">
      ...
  </TransitionGroup>
</template>

<style scoped>
.list-move{
  transition: transform 0.1s linear;
}

</style>

现在有动画效果了,但是你会发现有抖动和不流畅,原因我在 拖拽排序,换个思路轻松搞定 已经说明,是因为动画的执行过程中频繁的触发 swap 函数,所以需要在 动画的执行过程中暂停 swap 函数动画执行完毕后恢复 swap 函数,这里我贴上代码

const context = useDragDrop({
  canDraggable,
})

const { pause, resume } = context.use(sortPlugin({
  swap,
}))

function swap(startEvent: EnhancedMouseEvent, moveEvent: EnhancedMouseEvent) {
  ...
  // swap
  const temp = list1[startIndex]
  list1[startIndex] = list2[targetIndex]
  list2[targetIndex] = temp
  pause() // 暂停 swap 函数执行
}

useEventListener('transitionend', resume) // 动画结束后恢复 swap 函数执行

pauseresume函数是内部统一包装好的,感兴趣的可以去看源码,最终效果如下

sort-two.gif

这里省略了鼠标按下时添加高亮效果鼠标抬起时取消高亮效果的代码,为什么省略,因为比较简单(主要因为懒🧐)

结语

  1. 完整代码在我的仓库, 仓库地址
  2. 实现拖拽插件化架构的文章,掘金文章地址
  3. 基础版本的拖拽排序文章,掘金文章地址

如有错误之处,请指正,谢谢大家~