通过这篇文章修改在react中使用
React实现一个拖拽排序组件 - 支持多行多列、支持TypeScript、支持Flip动画、可自定义拖拽区域_react拖拽组件-CSDN博客
1、封装的拖拽排序组件
import React, { useEffect, useRef } from 'react';
import type { DragEventHandler } from 'react';
import type { DragSortProps } from './type';
import { FlipList } from './Filp';
import styles from './index.less';
/**查找符合条件的父节点
* @param node 当前节点。如果当前节点就符合条件,就会返回当前节点
* @param target 参数是当前找到的节点,返回一个布尔值,为true代表找到想要的父节点
* @returns 没找到则返回null,找到了返回Element
*/
/* eslint-disable no-param-reassign */
export function findParent(node: Element, target: (nowNode: Element) => boolean) {
while (node && !target(node)) {
if (node.parentElement) {
node = node.parentElement;
} else {
return null;
}
}
return node;
}
/**拖拽时,留在原位置的元素的样式 */
const movingClass = [styles.background]; //使用数组是为了方便以后添加其他类名
/**拖拽时,留在原位置的子元素的样式 */
const opacityClass = ['opacity-0']; //使用数组是为了方便以后添加其他类名
/**拖拽排序组件 */
const DragSort = function ({
list,
render,
afterDrag,
keyName,
cols = 1,
marginX = 16,
marginY = 10,
flipWithListChange = true,
}: DragSortProps) {
const listRef = useRef(null);
/**记录当前正在拖拽哪个元素 */
const nowDragItem = useRef();
/**存储flipList动画实例 */
const flipListRef = useRef();
// const [dragOpen, setDragOpen] = useState(false); //是否开启拖拽 (鼠标进入指定区域开启)
/**创建记录新的动画记录,并立即记录当前位置 */
const createNewFlipList = (exceptTarget?: Element) => {
if (!listRef.current) return;
//记录动画
// @ts-ignore
const listenChildren = [...listRef?.current?.children].filter((k) => k !== exceptTarget); //除了指定元素,其它的都动画
flipListRef.current = new FlipList(listenChildren, 300);
flipListRef.current.recordFirst();
};
//下面这两个是用于,当列表变化时,进行动画
useEffect(() => {
if (!flipWithListChange) return;
createNewFlipList();
}, [list, flipWithListChange]);
useEffect(() => {
if (!flipWithListChange) return;
createNewFlipList();
return () => {
flipListRef.current?.play(() => flipListRef.current?.recordFirst());
};
}, [list.length, flipWithListChange]);
/**事件委托- 监听 拖拽开始 事件,添加样式 */
const onDragStart: DragEventHandler = (e) => {
if (!listRef.current) return;
e.stopPropagation(); //阻止冒泡
/**这是当前正在被拖拽的元素 */
const target = e.target as HTMLDivElement;
//设置被拖拽元素“留在原地”的样式。为了防止设置正在拖拽的元素样式,所以用定时器,宏任务更晚执行
setTimeout(() => {
target.classList.add(...movingClass); //设置正被拖动的元素样式
target.childNodes.forEach((k) => (k as HTMLDivElement).classList?.add(...opacityClass)); //把子元素都设置为透明,避免影响
}, 0);
//记录元素的位置,用于Flip动画
createNewFlipList(target);
//记录当前拖拽的元素
nowDragItem.current = target;
//设置鼠标样式
e.dataTransfer.effectAllowed = 'move';
};
/**事件委托- 监听 拖拽进入某个元素 事件,在这里只是DOM变化,数据顺序没有变化 */
const onDragEnter: DragEventHandler = (e) => {
e.preventDefault(); //阻止默认行为,默认是不允许元素拖动到人家身上的
if (!listRef.current || !nowDragItem.current) return;
/**孩子数组,每次都会获取最新的 */
// @ts-ignore
const children = [...listRef.current.children];
/**真正会被挪动的元素(当前正悬浮在哪个元素上面) */ //找到符合条件的父节点
const realTarget = findParent(e.target as Element, (now) => children.indexOf(now) !== -1);
//边界判断
if (realTarget === listRef.current || realTarget === nowDragItem.current || !realTarget) {
// console.log("拖到自身或者拖到外面");
return;
}
if (realTarget.className.includes(FlipList.movingClass)) {
// console.log("这是正在动画的元素,跳过");
return;
}
//拿到两个元素的索引,用来判断这俩元素应该怎么移动
/**被拖拽元素在孩子数组中的索引 */
const nowDragtItemIndex = children.indexOf(nowDragItem.current);
/**被进入元素在孩子数组中的索引 */
const enterItemIndex = children.indexOf(realTarget);
//当用户选中文字,然后去拖动这个文字时,就会触发 (可以通过禁止选中文字来避免)
if (enterItemIndex === -1 || nowDragtItemIndex === -1) {
// console.log("若第二个数为-1,说明拖动的不是元素,而是“文字”", enterItemIndex, nowDragtItemIndex);
return;
}
//Flip动画 - 记录原始位置
flipListRef.current?.recordFirst();
if (nowDragtItemIndex < enterItemIndex) {
// console.log("向下移动");
listRef.current.insertBefore(nowDragItem.current, realTarget.nextElementSibling);
} else {
// console.log("向上移动");
listRef.current.insertBefore(nowDragItem.current, realTarget);
}
//Flip动画 - 播放
flipListRef.current?.play();
};
/**事件委托- 监听 拖拽结束 事件,删除样式,设置当前列表 */
const onDragEnd: DragEventHandler = (e) => {
if (!listRef.current) return;
/**当前正在被拖拽的元素 */
const target = e.target as Element;
target.classList.remove(...movingClass); //删除前面添加的 被拖拽元素的样式,回归原样式
target.childNodes.forEach((k) => (k as Element).classList?.remove(...opacityClass)); //删除所有子元素的透明样式
/**拿到当前DOM的id顺序信息 */
// @ts-ignore
const ids = [...listRef.current.children].map((k) => String(k.id)); //根据id,判断到时候应该怎么排序
//把列表按照id排序
const newList = [...list].sort(function (a, b) {
const aIndex = ids.indexOf(String(a[keyName]));
const bIndex = ids.indexOf(String(b[keyName]));
if (aIndex === -1 && bIndex === -1) return 0;
else if (aIndex === -1) return 1;
else if (bIndex === -1) return -1;
else return aIndex - bIndex;
});
afterDrag(newList); //触发外界传入的回调函数
// setDragOpen(false); //拖拽完成后,再次禁止拖拽
};
return (
e.preventDefault()} //被拖动的对象被拖到其它容器时(因为默认不能拖到其它元素上)
onDragEnd={onDragEnd}
/**拖拽按钮组件 */ //只有鼠标悬浮在这上面的时候,才开启拖拽,做到“指定区域拖拽”
// onMouseEnter={(e: any) => {
// e.stopPropagation();
// setDragOpen(true);
// }}
// onMouseLeave={(e: any) => {
// e.stopPropagation();
// setDragOpen(false)
// }}
>
{list.map((item, index) => {
const key = item[keyName] as string;
const width = `calc((100% - ${(cols - 1) * marginX}px) / ${cols})`;
return (
{render?.({ data: item, index })}
);
})}
);
};
export default DragSort;
2、Flip动画
/**位置的类型 */
interface position {
x: number;
y: number;
}
/**Flip动画 */
export class Flip {
/**dom元素 */
private dom: Element;
/**原位置 */
private firstPosition: position | null = null;
/**动画时间 */
private duration: number;
/**正在移动的动画会有一个专属的class类名,可以用于标识 */
static movingClass = '__flipMoving__';
constructor(dom: Element, duration = 500) {
this.dom = dom;
this.duration = duration;
}
/**获得元素的当前位置信息 */
private getDomPosition(): position {
const rect = this.dom.getBoundingClientRect();
return {
x: rect.left,
y: rect.top,
};
}
/**给原始位置赋值 */
/* eslint-disable no-param-reassign */
recordFirst(firstPosition?: position) {
if (!firstPosition) firstPosition = this.getDomPosition();
this.firstPosition = { ...firstPosition };
}
/**播放动画 */
play(callback?: () => any) {
if (!this.firstPosition) {
// console.warn('请先记录原始位置');
return;
}
const lastPositon = this.getDomPosition();
const dif: position = {
x: lastPositon.x - this.firstPosition.x,
y: lastPositon.y - this.firstPosition.y,
};
// console.log(this, dif);
if (!dif.x && !dif.y) return;
this.dom.classList.add(Flip.movingClass);
this.dom.animate(
[{ transform: `translate(${-dif.x}px, ${-dif.y}px)` }, { transform: `translate(0px, 0px)` }],
{ duration: this.duration },
);
setTimeout(() => {
this.dom.classList.remove(Flip.movingClass);
callback?.();
}, this.duration);
}
}
/**Flip多元素同时触发 */
export class FlipList {
/**Flip列表 */
private flips: Flip[];
/**正在移动的动画会有一个专属的class类名,可以用于标识 */
static movingClass = Flip.movingClass;
/**Flip多元素同时触发 - 构造函数
* @param domList 要监听的DOM列表
* @param duration 动画时长,默认500ms
*/
constructor(domList: Element[], duration?: number) {
this.flips = domList.map((k) => new Flip(k, duration));
}
/**记录全部初始位置 */
recordFirst() {
this.flips.forEach((flip) => flip.recordFirst());
}
/**播放全部动画 */
play(callback?: () => any) {
this.flips.forEach((flip) => flip.play(callback));
}
}
3、组件类型定义
/**有孩子的,基础的组件props,包含className style children */
interface baseChildrenProps {
/**组件最外层的className */
className?: string;
/**组件最外层的style */
style?: React.CSSProperties;
/**孩子 */
children?: React.ReactNode;
}
/**ItemRender渲染函数的参数 */
type itemProps = {
/**当前元素 */
data: T;
/**当前索引 */
index: number;
/**父元素宽度 */
width?: number;
/**可拖拽的盒子,只有在这上面才能拖拽。自由放置位置。提供了一个默认的拖拽图标。可以作为包围盒,将某块内容作为拖拽区域 */
DragBox?: (props: baseChildrenProps) => React.ReactNode;
};
/**拖拽排序组件的props */
export interface DragSortProps {
/**组件最外层的className */
className?: string;
/**组件最外层的style */
style?: React.CSSProperties;
/**列表,拖拽后会改变里面的顺序 */
list: T[];
/**用作唯一key,在list的元素中的属性名,比如id。必须传递 */
keyName: keyof T;
/**一行个数,默认1 */
cols?: number;
/**元素水平间距,单位px,默认0 (因为一行默认1) */
marginX?: number;
/**元素垂直间距,单位px,默认10 */
marginY?: number;
/**当列表长度变化时,是否需要Flip动画,默认开启 (可能有点略微的动画bug) */
flipWithListChange?: boolean;
/**每个元素的渲染函数 */
render?: (props: itemProps) => React.ReactNode;
/**拖拽结束事件,返回排序好的新数组,在里面自己调用setList */
afterDrag: (list?: T[]) => any;
}
4、样式
@import (reference) '~antd/es/style/themes/index';
.background {
background: linear-gradient(
45deg,
rgba(0, 0, 0, 0.1) 0,
rgba(0, 0, 0, 0.1) 25%,
transparent 25%,
transparent 50%,
rgba(0, 0, 0, 0.1) 50%,
rgba(0, 0, 0, 0.1) 75%,
transparent 75%,
transparent
);
background-size: 20px 20px;
border-radius: 5px;
}
.list {
display: flex;
flex-wrap: wrap;
row-gap: 10px;
.dragBox {
width: 100%;
}
.item {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
padding: 6px 10px;
overflow: hidden;
background-color: #fff;
border-radius: 2px;
cursor: pointer;
> span:first-child {
flex: 1;
width: 0;
}
}
}