Antd Upload组件实现拖拽排序的方法

455 阅读3分钟

需求

一个管理后台项目用到了Antd中的Upload组件,需求是使用picture-card类型,同时支持拖拽排序。

初期思路

因为项目本身就使用了react-sortable-hoc这个拖拽组件库,所以遵循着能用就用的原则,打算使用react-sortable-hoc配合Upload中的itemRender属性来完成这个需求,代码如下


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>

看起来一切顺利,原神代码,启动!

拖拽-修改前.gif 可以看到拖拽排序的功能是实现了,但是拖拽的间距检测明显是有问题。

分析问题

首先替换了Upload改用div来试试基本的功能是否正常,代码如下


<SortableList  ...>  
    {[1, 2, 3, 4, 5].map((v, i) => <SortableItem index={i} node={<div .../>)}  
</SortableList>

拖拽-黑块.gif 可以看到效果是非常完美的,那问题只能是出在Upload组件上了,当时做到这一步的时候我认为应该有很多人会碰到这样的问题,打开搜索引擎搜索后发现并没有...,很多人就直接选择不用Upload里面自带的组件了,但是这样相当于要自己写上传进度条状态上传失败的状态等等等orz,既然没有搜索到解决办法,只能自己动手啦,我不入地狱谁入地狱。

层级问题

首先还是打开F12简单分析一下可能出现问题的原因,对比了一下Upload和普通的div能看到其实UploaditemRender方法返回的Element不是紧邻着SortableList的,而是还包裹着一层。

div.jpg

upload.jpg

如果是层级问题其实我心里大概也有了答案,不过为了确认还是要从源码入手看一看到底是怎么造成这种情况的。

源码分析

---> 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事件

---> 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,那么解决问题的答案也很明显了,就是让子元素offset0就可以,也就是按照CSS规则来说

  1. 如果父辈元素中有定位的元素,那么就返回距离当前元素最近的定位元素边缘的距离
  2. 如果父辈元素中没有定位元素,那么就返回相对于body左边缘距离。

所以我们给父元素一个相对定位即可


:global{  
    .ant-upload-list-picture-card-container{  
        position: relative;  
    }  
}

拖拽-完成.gif

大功告成。

封面

pixiv