拖拽排序,换个思路轻松搞定
前言
-
列表排序效果图
-
2个列表互相穿梭排序效果图
下文我们将要写一个插件来完成效果
单个列表排序
功能拆分
单个列表排序,我们先对功能进行拆分
- 记录鼠标按下时的
索引
,并且高亮该索引
对应的元素 - 记录鼠标移动时经过的
索引
- 在鼠标移动时将2个记录的索引
交换
- 应用动画
- 拖拽结束后取消高亮元素
为什么要记录索引
,而不是直接记录元素
?
因为我们在实际的业务开发过程中,都是用框架的,而框架都是基于数据
的,数据改变后,视图自动更新。
以Vue3 框架
为例,比如我们想写一个列表的布局
- 写好
样式布局
- 定义
列表数据
- 通过
v-for
遍历数据
那列表排序
的本质不就是2条数据交换
么?只要能找到2条数据的索引
,我们只需要用以下代码就能实现交换
function swap(data,startIndex,targetIndex){
const temp = data[startIndex];
data[startIndex] = data[targetIndex];
data[targetIndex] = temp;
}
动画要怎么做?
数据是可以很简单的交换,但是动画呢,这种交换的动画应该怎么搞?
还是以Vue3 框架
为例,框架内提供了Transition
和 TransitionGroup
2个动画过渡组件,这2个动画过渡组件的实现是基于FLIP
动画思想的,想了解的可以参考这篇 FLIP 博客
我就要用原生写,我就是这么NB,我不用框架,请问阁下应该如何应对?
那还是可以基于FLIP
动画思想,只不过你要自己封装一个类似Transition
和 TransitionGroup
的函数或组件,我之前用原生的写过一个随机的动画,类似这样的效果,大概js
有50行代码
左右
布局代码
样式布局这部分代码没有营养价值,这里我就粘贴了
<script setup lang="tsx">
import { ref } from 'vue'
interface Item {
id: string
name: string
color: string
}
const baseBackgroundColor = 'skyblue'
const swapBackgroundColor = 'pink'
const listRef = 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 },
])
</script>
<template>
<TransitionGroup tag="div" class="container" name="list">
<div v-for="item in listRef" :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>
上面的代码运行后,页面就是这样的
这里值得注意的一点:我们给每个元素上添加了一个
id
属性,是为了后面能根据这个id
获取到数据
实现 sort 插件
插件需要做什么事情?
onStart
:拖拽开始的钩子onMove
:拖拽中的钩子onEnd
:拖拽结束的钩子onDragging
:在拖拽中的钩子
我们需要将插件实现的通用一点,那也就是说我们在内部不能实现交换索引,而是将交换的过程暴露给上层,我们在插件中只需要实现监听即可,大概分为这几步
- 监听用户按下的
event
- 监听用户移动时的
event
- 让用户实现
交换
插件实现
代码比较简单,这里我给贴上后在解释
import { ref, unref } from 'vue'
import type { DragDropPluginCtx, DrapDropEventsCallback, EnhancedMouseEvent } from '@drag-drop/core'
interface SortPluginOptions extends Partial<Omit<DrapDropEventsCallback, 'onDragging'>> {
swap: (mouseDownEvent: EnhancedMouseEvent, mouseMoveEvent: EnhancedMouseEvent) => void
}
export function sortPlugin(options: SortPluginOptions) {
return function ({ context }: DragDropPluginCtx) {
const {
swap,
onStart,
onMove,
onEnd,
} = options
const mouseDownEventRef = ref<EnhancedMouseEvent>()
const mouseMoveEventRef = ref<EnhancedMouseEvent>()
context.onStart((event) => {
mouseDownEventRef.value = event
onStart?.(event)
})
context.onMove((event) => {
mouseMoveEventRef.value = event
swap(unref(mouseDownEventRef)!, event)
onMove?.(event)
})
context.onEnd((event) => {
mouseDownEventRef.value = undefined
mouseMoveEventRef.value = undefined
onEnd?.(event)
})
}
}
这里我解释一下,因为可能没有读我之前的文章不太好理解
- 插件需要是一个函数,这个函数会被内部包装成
vue 的 setup
函数,所以可以使用响应式 Api
sortPlugin
是一个高阶函数,为了方便用户传参context 参数
是插件的上下文,封装好了一些拖拽钩子
,可以直接监听swap 参数
需要外界传递进来,让用户自己实现交换,我们提供了鼠标按下时的event
和鼠标移动时的event
参数EnhancedMouseEvent 这个类型
是增强版的MouseEvent
,因为在设计时考虑到跨Iframe
情况,暂且想象成MouseEvent
类型即可
可以看到,上面的代码就已经实现了监听排序的过程,接下来看这个插件如何在外层去使用
外层使用插件
我们需要在之前实现的布局代码中添加上我们的插件代码
<script setup lang="tsx">
import { ref } from 'vue'
import { useDragDrop } from '@drag-drop/core'
import { sortPlugin } from '@drag-drop/plugin-sort'
...
const context = useDragDrop({
canDraggable:event => !!event.target?.classList.contains('item'),
})
context.use(sortPlugin({
swap:// 这里需要用户去实现,
}))
</script>
<template>
<TransitionGroup tag="div" class="container" name="list">
<div v-for="item in listRef" :id="item.id" :key="item.id" class="item" :style="{ background: item.color }">
{{ item.name }}
</div>
</TransitionGroup>
</template>
<style scoped>
...
</style>
上述代码已经将插件引入并且使用了,用户需要实现swap
函数即可
实现swap
函数
我们只需要在swap
函数中交换鼠标按下时的索引
和鼠标移动时的索引
即可
function swap(startEvent: EnhancedMouseEvent, moveEvent: EnhancedMouseEvent) {
const startIndex = getIndexByEvent(startEvent)
const targetIndex = getIndexByEvent(moveEvent)
if ((~startIndex) && (~targetIndex) && startIndex !== targetIndex) {
const list = unref(listRef)
const temp = list[startIndex]
list[startIndex] = list[targetIndex]
list[targetIndex] = temp
}
}
function getIndexByEvent(event: EnhancedMouseEvent) {
const element = event.target!
const id = element.getAttribute('id')
return unref(id2IndexByListGetter).get(id as any) ?? -1
}
const id2IndexByListGetter = computed(() => {
const list = unref(listRef)
return list.reduce((p, c, i) => {
p.set(c.id, i)
return p
}, new Map<string, number>())
})
解释一下
getIndexByEvent 函数
:根据event
参数获取到元素在数据中的索引,因为在之前我们将每个元素上都添加了一个id
属性~startIndex
等同于startIndex !== -1
startIndex !== targetIndex
是为了屏蔽自己和自己交换
好了,上述代码运行后是这样的
添加动画
添加动画的代码比较简单,只需要给TransitionGroup
添加一个name 属性
,然后在样式中设定动画过渡即可
<template>
<TransitionGroup tag="div" class="container" name="list">
<div v-for="item in listRef" :id="item.id" :key="item.id" class="item" :style="{ background: item.color }">
{{ item.name }}
</div>
</TransitionGroup>
</template>
<style scoped>
.list-move{
transition: transform 0.1s linear;
}
...
</style>
本来以为写到这里就应该可以完成整个拖拽排序了,但是实际运行后确是这样的
有点抖动的感觉,而且动画也不流畅,明明没有动画前毫无问题,咋有动画了就出问题了?
这是因为在动画的过程中我们还在频繁的触发swap
函数,又在不停的交换位置,所以我们需要在动画的执行过程中暂停 swap 函数的触发
,动画结束后恢复 swap 函数的执行
,所以我们在稍微添加2行代码
<script setup lang="tsx">
...
const context = useDragDrop({
canDraggable,
})
const { pause, resume } = context.use(sortPlugin({
swap, // 这里需要用户去实现,
}))
function swap(startEvent: EnhancedMouseEvent, moveEvent: EnhancedMouseEvent) {
const startIndex = getIndexByEvent(startEvent)
const targetIndex = getIndexByEvent(moveEvent)
if ((~startIndex) && (~targetIndex) && startIndex !== targetIndex) {
const list = unref(listRef)
const temp = list[startIndex]
list[startIndex] = list[targetIndex]
list[targetIndex] = temp
pause() // 暂停 swap 函数的触发
}
}
useEventListener('transitionend', resume) // 动画结束恢复 swap 函数
...
</script>
我们的插件会被内部统一包装,并提供pause
和resume
函数,这个时候就派上用场了,具体这两个函数的实现大家可以自行去翻阅源码,看最终效果
这里省略了鼠标按下时添加高亮效果
及鼠标抬起时取消高亮效果
的代码,为什么省略,因为比较简单(主要因为懒🧐)
结语
如有错误之处,请指正,谢谢大家~
转载自:https://juejin.cn/post/7269042529248706618