需求:
React原生拖拽API实现拖拽及带动画的排序
难点:
- React原生拖拽各个API函数的用途
- 排序动画的实现思路
实现过程:
拖拽变换位置还算比较顺利,只要拖拽API了解就问题不大,感觉难点主要在动画的实现,动画的实现主要参考了该文章:js drag拖动排序,但需求上有两点跟该文章的不太一样:一个是用React实现,还有就是该文章的动画思路是先替换dom元素的位置再进行动画效果展示。然后我打算先动画再替换位置。
该文章的动画具体实现思路如下:
先记录两个元素的位置,再将dom元素进行替换位置(是用的判断前后顺序然后用target.parentNode.insertBefore插入),然后再用translate不带transition动画效果的诺回原来的位置,然后利用setTimeout进入下一轮事件循环重置位置、打开动画、诺回目标位置
我的动画实现思路:
获取当前最新的两个元素位置,加上动画transition,用translate先挪位置,然后用定时器设置和动画同样长的时间后去变换实际的dom位置(因为是用的react即直接修改源列表数据),然后因为onDragOver是持续不断的触发,在变换位置的时候,鼠标会碰到元素(猜测是这个原因)导致动画停在中间,我是简单用了flag变量去判断何时去允许下一次动画变位置。
还有个要考虑的点是动画transition何时开始结束,因为变换dom实际位置的时候也还会带着transition
代码:
import { useState } from 'react'
import styles from './index.less';
const INIT_DATA = [
{ id: 1, name: '需求1' },
{ id: 2, name: '需求2' },
{ id: 3, name: '需求3' },
{ id: 4, name: '需求4' },
{ id: 5, name: '需求5' },
{ id: 6, name: '需求6' },
{ id: 7, name: '需求7' },
{ id: 8, name: '需求8' },
{ id: 9, name: '需求9' },
{ id: 10, name: '需求10' },
]
export default function index() {
const [list, setList] = useState(INIT_DATA)
const [curDragItem, setCurDragItem] = useState<any>({});
let animationFlag = true;
const dragStart = (e: any) => {
setCurDragItem(JSON.parse(e.target.getAttribute('drag-data')));
}
const dragEnd = (e: any) => {
setCurDragItem({});
}
const onDragOver = (e: any) => {
e.preventDefault();
let targetItem = JSON.parse(e?.target?.getAttribute('drag-data'));
if (animationFlag && curDragItem.id && targetItem?.id && curDragItem.id !== targetItem?.id) {
animationFlag = false;
// 获取当前拖拽节点最新的位置
let curNewestDragItemDom: any = getDomItem(curDragItem.id);
let curNewestDragItemDomRect: any =
curNewestDragItemDom?.getBoundingClientRect() || {};
const targetRect = e.target.getBoundingClientRect();
// 动画
e.target.style.transition = 'all 200ms';
curNewestDragItemDom.style.transition = 'all 200ms';
e.target.style.transform = `translate3d(${curNewestDragItemDomRect.left - targetRect.left
}px,${curNewestDragItemDomRect.top - targetRect.top}px,0)`;
curNewestDragItemDom.style.transform = `translate3d(${targetRect.left - curNewestDragItemDomRect.left
}px,${targetRect.top - curNewestDragItemDomRect.top}px,0)`;
setTimeout(() => {
//排序
sortPosition(curDragItem.id, targetItem.id);
}, 200);
}
}
// 获取指定id的组件元素
const getDomItem = (id: any) => {
return document.getElementsByName('item_' + id)?.[0] || {};
};
// 元素换位置
const sortPosition = (sourceId: any, targetId: any) => {
if (sourceId && targetId && targetId !== sourceId) {
// 真正的节点交换顺序
let tmpList = JSON.parse(JSON.stringify(list))
let sourceItem = tmpList.find((val: any) => val.id == sourceId);
let sourceItemIndex = tmpList.findIndex((val: any) => val.id == sourceId);
let targetItem = tmpList.find((val: any) => val.id == targetId);
let targetItemIndex = tmpList.findIndex((val: any) => val.id == targetId);
tmpList.splice(sourceItemIndex, 1, targetItem);
tmpList.splice(targetItemIndex, 1, sourceItem);
setList(tmpList);
// 清除动画及位移
let sourceDom = getDomItem(sourceId);
let targetDom = getDomItem(targetId);
sourceDom.style.transition = '';
sourceDom.style.transform = '';
targetDom.style.transition = '';
targetDom.style.transform = '';
// 允许下次交换
animationFlag = true;
}
}
return (
<div className={styles.wrap}>
<ul
className={styles.list}
onDragOver={onDragOver}
>
{
list.map((val: any) => {
return (
<li
key={val.id}
name={'item_' + val.id}
className={styles.item}
draggable={true}
onDragStart={dragStart}
onDragEnd={dragEnd}
drag-data={JSON.stringify(val)}
>
{val.name}
</li>
)
})
}
</ul>
</div>
)
}
.wrap{
height: 100%;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
.list{
width: 480px;
padding:10px;
}
.item{
display: inline-flex;
justify-content: center;
align-items: center;
height: 30px;
width: 140px;
margin-right: 20px;
margin-bottom: 10px;
border: 1px solid #eee;
border-radius: 4px;
background:#eee;
cursor: pointer;
&:nth-child(3n){
margin-right: 0;
}
}
}