列表拖拽超简单实现
常见数组列表拖拽
先看一下例子和代码实现,下面再做讲解:
是不是就实现了vue-draggable
这个库的效果了;其核心就是vue
中自带的<transition-group>
过渡组件,加上dragover
事件,最后处理队列数据项的位置就搞定。
- 将上面的例子代码封装成钩子函数方便复用
interface ListDragOption<T> {
/** 返回列表函数 */
list(): Array<T>;
/**
* 触发更新列表函数
* @param newList 更新后的新数组
*/
update(newList: Array<T>): void;
/**
* 向上查找节点的最大层数,默认`3`
* - 目的是为了找到`:data-key`的值
*/
findLevel?: number;
/**
* 默认`"key"`,则元素绑定`<element data-key="xxx">`
* - 当有多种拖拽列表处于同一场景时,设置该值作为区分用
*/
dataKey?: string;
}
/**
* 列表拖拽
* - `item`节点一定要绑定`<element :data-key="唯一值">`
* @param option
*/
export function useListDrag<T>(option: ListDragOption<T>) {
const maxLevel = option.findLevel || 3;
const dataKey = option.dataKey || "key";
const current = {
index: -1
}
const target = {
key: ""
}
function findDataKey(el: HTMLElement, level = 1) {
const key = el.dataset[dataKey];
if (key) return key;
if (level < maxLevel && el.parentElement) {
return findDataKey(el.parentElement, level + 1);
}
// console.warn(`找不到<element :data-${dataKey}="xxx">绑定值,请检查是否在元素中设置绑定值或调整 findLevel`);
return undefined;
}
function onDragStart(index: number) {
current.index = index;
}
function onDropEnd() {
current.index = -1;
}
function onDragMove(event: DragEvent, targetIndex: number) {
event.preventDefault();
if (current.index < 0) return;
const targetKey = findDataKey(event.target as HTMLElement);
if (!targetKey || targetKey === target.key) return;
target.key = targetKey;
// 记录原始数据字符串,下面做对比用
const str = JSON.stringify(option.list());
// 拷贝响应数据
const ls: Array<T> = JSON.parse(str);
// 交替数组位置
[ls[current.index], ls[targetIndex]] = [ls[targetIndex], ls[current.index]];
// 上一次修改如果和当前数组一致则不重新赋值
if (str === JSON.stringify(ls)) return;
// 最终赋值给响应数据
option.update(ls);
// 更新当前索引,必须!!!
current.index = targetIndex;
}
return {
onDragStart,
onDragMove,
onDropEnd
}
}
- 使用方式
<template>
<transition-group name="the-group" tag="div">
<div
v-for="(item, itemIndex) in state.list"
:key="item.id"
class="item"
:data-key="item.id"
:draggable="true"
@dragstart="onDragStart(itemIndex)"
@dragover="onDragMove($event, itemIndex)"
@drop="onDropEnd()"
>
{{ item.label }}
</div>
</transition-group>
</template>
<script lang="ts" setup>
import { reactive } from "vue";
import { useListDrag } from "@/hooks/common";
const state = reactive({
list: Array.from({ length: 10 }).map((_, index) => ({ label: `item-${index}`, id: index }))
});
const { onDragStart, onDragMove, onDropEnd } = useListDrag({
list: () => state.list,
update(newList) {
state.list = newList;
},
// findLevel: 2 // 可选,根据布局嵌套层级进行设置
});
</script>
<style lang="scss">
.item {
width: 100%;
margin-bottom: 10px;
border: solid #ccc 1px;
border-radius: 2px;
font-size: 14px;
line-height: 26px;
padding: 0 12px;
}
.the-group-move,
.the-group-enter-active,
.the-group-leave-active {
transition: .3s all;
}
.the-group-enter-from,
.the-group-leave-to {
opacity: 0;
transform: translate3d(0, 30px, 0);
}
.the-group-leave-active {
position: absolute;
}
</style>
上面的处理方式还存在一些瑕疵,当列表数据包含了函数或者其他js
类型时,就会导致重新设置的数据丢失,但是用深克隆的方式代替JSON.stringify
又会产生另一个问题:当数组项内有大量数据时,深克隆会产生性能问题;所以在优化实现的设计中,将不再替换数组进行赋值处理,而是记录改变之后的队列位置,最后将原数组进行排序即可,这样确保性能最优,同时无需关心克隆问题。
优化之后的封装实现:
interface ListDragOption<T> {
/** 返回列表函数 */
list(): Array<T>;
/** 数组项唯一值 */
key: keyof T;
/**
* 向上查找节点的最大层数,默认`3`
* - 目的是为了找到`:data-key`的值
*/
findLevel?: number;
/**
* 默认`"key"`,则元素绑定`<element data-key="xxx">`
* - 当有多种拖拽列表处于同一场景时,设置该值作为区分用
*/
dataKey?: string;
}
/**
* [列表拖拽](https://juejin.cn/post/7354039500811845670)
* - `item`节点一定要绑定`<element :data-key="唯一值">`
* @param option
*/
export function useListDrag<T extends object>(option: ListDragOption<T>) {
const maxLevel = option.findLevel || 3;
const dataKey = option.dataKey || "key";
const current = {
index: -1
}
const target = {
key: ""
}
function findDataKey(el: HTMLElement, level = 1) {
const key = el.dataset[dataKey];
if (key) return key;
if (level < maxLevel && el.parentElement) {
return findDataKey(el.parentElement, level + 1);
}
// console.warn(`找不到<element :data-${dataKey}="xxx">绑定值,请检查是否在元素中设置绑定值或调整 findLevel`);
return undefined;
}
/**
* 获取排序对比对象
* @param list
*/
function getSortMap(list: Array<T[keyof T]>) {
const indexMap: Record<string, number> = {};
list.forEach((item, index) => (indexMap[item as string] = index));
return indexMap;
}
function onDragStart(index: number) {
current.index = index;
}
function onDropEnd() {
current.index = -1;
}
function onDragMove(event: DragEvent, targetIndex: number) {
event.preventDefault();
if (current.index < 0) return;
const targetKey = findDataKey(event.target as HTMLElement);
if (!targetKey || targetKey === target.key) return;
target.key = targetKey;
const optionList = option.list();
const before = optionList.map(item => item[option.key]);
// 记录原始数据字符串,下面做对比用
const beforeStr = JSON.stringify(before);
// 拷贝原来数组
const next: typeof before = JSON.parse(beforeStr);
// 交替数组位置
[next[current.index], next[targetIndex]] = [next[targetIndex], next[current.index]];
// 上一次修改如果和当前数组一致则不作处理
if (beforeStr === JSON.stringify(next)) return;
// 最后设置排序
const indexMap = getSortMap(next);
optionList.sort((a, b) => {
const key = option.key;
const valueA = indexMap[a[key] as string];
const valueB = indexMap[b[key] as string];
return valueA - valueB;
});
// 更新当前索引,必须!!!
current.index = targetIndex;
}
return {
onDragStart,
onDragMove,
onDropEnd
}
}
这样在使用时就不需要通过原先的update
函数去重新设置数组了
import { useListDrag } from "@/hooks/common";
const state = reactive({
list: Array.from({ length: 10 }).map((_, index) => ({ label: `item-${index}`, id: index }))
});
const { onDragStart, onDragMove, onDropEnd } = useListDrag({
list: () => state.list,
key: "id"
});
跨数组列表拖拽
你可能觉得上面这种太简单,使用场景偏窄,那再看看下面的这个
是不是就是常见的夸容器拖拽了,核心还是在绑定的节点dragover
事件中去处理,当有多个容器时,在对应的dragover
事件中做相应的判断处理即可,简单到不得了!!!