需求
一个管理后台项目用到了Antd
中的Upload
组件,需求是使用picture-card
类型,同时支持拖拽排序。
初期思路
因为项目本身就使用了react-sortable-hoc
这个拖拽组件库,所以遵循着能用就用的原则,打算使用react-sortable-hoc
配合Upload
中的itemRender
属性来完成这个需求,代码如下
typescript
import {SortableContainer, SortableElement, SortEnd} from "react-sortable-hoc";
const SortableItem = SortableElement<any>(({node}) => <div>{node}</div>);
const SortableList = SortableContainer((props) => <div {...props}/>);
const handleItemRender: UploadProps['itemRender'] = (node, file) => {
return <SortableItem index={getIndex(file)} node={node}/>
}
<SortableList ....>
<Upload
multiple
maxCount={5}
name={'file'}
listType={"picture-card"}
accept=".png, .jpg, .jpeg"
itemRender={handleItemRender}
{...restProps}>{fileList.length < 5 ? child : undefined}</Upload>
</SortableList>
看起来一切顺利,原神代码,启动!
可以看到拖拽排序的功能是实现了,但是拖拽的间距检测明显是有问题。
分析问题
首先替换了Upload
改用div
来试试基本的功能是否正常,代码如下
javascript
<SortableList ...>
{[1, 2, 3, 4, 5].map((v, i) => <SortableItem index={i} node={<div .../>)}
</SortableList>
可以看到效果是非常完美的,那问题只能是出在Upload
组件上了,当时做到这一步的时候我认为应该有很多人会碰到这样的问题,打开搜索引擎搜索后发现并没有...,很多人就直接选择不用Upload
里面自带的组件了,但是这样相当于要自己写上传进度条 状态上传失败的状态等等等orz,既然没有搜索到解决办法,只能自己动手啦,我不入地狱谁入地狱。
层级问题
首先还是打开F12 简单分析一下可能出现问题的原因,对比了一下Upload
和普通的div
能看到其实Upload
的itemRender
方法返回的Element
不是紧邻着SortableList
的,而是还包裹着一层。
如果是层级问题其实我心里大概也有了答案,不过为了确认还是要从源码入手看一看到底是怎么造成这种情况的。
源码分析
kotlin
---> SortableContainer
// 按下鼠标时触发的事件
handlePress = async (event) => {
....
....
const {index} = node.sortableInfo;
const margin = getElementMargin(node);
const gridGap = getContainerGridGap(this.container);
const containerBoundingRect = this.scrollContainer.getBoundingClientRect();
const dimensions = getHelperDimensions({index, node, collection});
this.node = node;
this.margin = margin;
this.gridGap = gridGap;
this.width = dimensions.width;
this.height = dimensions.height;
this.marginOffset = {
x: this.margin.left + this.margin.right + this.gridGap.x,
y: Math.max(this.margin.top, this.margin.bottom, this.gridGap.y),
};
this.boundingClientRect = node.getBoundingClientRect();
this.containerBoundingRect = containerBoundingRect;
this.index = index;
this.newIndex = index;
// 👆找到指定的元素并计算padding,margin等位置信息
...
...
this.helper = this.helperContainer.appendChild(cloneNode(node));
// 👆克隆一份鼠标选中的元素
...
...
events.move.forEach((eventName) => this.listenerNode.addEventListener(eventName,this.handleSortMove,false));
// 👆给克隆的元素身上绑定拖拽事件
}
OK,经过简单的分析我们大概从源码中看到了如何克隆一个元素以及元素的事件绑定,接着我们应该重点关注的就是克隆元素绑定的handleSortMove
事件
ini
---> handleSortMove
handleSortMove = (event) => {
const {onSortMove} = this.props;
// Prevent scrolling on mobile
if (typeof event.preventDefault === 'function') {
event.preventDefault();
}
this.updateHelperPosition(event);
// 👆克隆元素的拖拽坐标信息
this.animateNodes();
// 👆其他元素的对齐滚动动画事件
...
...
};
---> animateNodes
animateNodes() {
...
...
for (let i = 0, len = nodes.length; i < len; i++) {
const {node} = nodes[i];
const {index} = node.sortableInfo;
const width = node.offsetWidth;
const height = node.offsetHeight;
const offset = {
height: this.height > height ? height / 2 : this.height / 2,
width: this.width > width ? width / 2 : this.width / 2,
};
if (!edgeOffset) {
// 👇重点来了,计算元素边距距离
edgeOffset = getEdgeOffset(node, this.container);
nodes[i].edgeOffset = edgeOffset;
}
...
...
...
}
}
---> getEdgeOffset
export function getEdgeOffset(node, parent, offset = {left: 0, top: 0}) {
if (!node) {
return undefined;
}
// Get the actual offsetTop / offsetLeft value, no matter how deep the node is nested
const nodeOffset = {
left: offset.left + node.offsetLeft,
top: offset.top + node.offsetTop,
};
// 👇如果父元素不是SortContainer会一直递归直到找到SortContainer元素
if (node.parentNode === parent) {
return nodeOffset;
}
return getEdgeOffset(node.parentNode, parent, nodeOffset);
}
OOOK!果然问题是出现在了层级结构上面,因为我们现在的UploadItem
外面还会被套上一层div
所以递归的时候会重复计算offset.left
和offset.top
,这和我们的表现形式也是一致的。
解决问题
上面我们已经分析出了问题原因是因为层级 结构导致的重复计算offset
,那么解决问题的答案也很明显了,就是让子元素 的offset
为0
就可以,也就是按照CSS
规则来说
- 如果父辈元素中有定位 的元素,那么就返回距离当前元素最近的定位元素边缘的距离。
- 如果父辈元素中没有定位 元素,那么就返回相对于body左边缘距离。
所以我们给父元素一个相对定位
即可
css
:global{
.ant-upload-list-picture-card-container{
position: relative;
}
}
大功告成。