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

需求

一个管理后台项目用到了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能看到其实UploaditemRender方法返回的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,那么解决问题的答案也很明显了,就是让子元素offset0就可以,也就是按照CSS规则来说

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

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

css 复制代码
:global{  
    .ant-upload-list-picture-card-container{  
        position: relative;  
    }  
}

大功告成。

封面

pixiv

相关推荐
鑫宝Code30 分钟前
【React】状态管理之Redux
前端·react.js·前端框架
pink大呲花41 分钟前
关于番外篇-CSS3新增特性
前端·css·css3
少年维持着烦恼.1 小时前
第八章习题
前端·css·html
我是哈哈hh1 小时前
HTML5和CSS3的进阶_HTML5和CSS3的新增特性
开发语言·前端·css·html·css3·html5·web
袋鼠云数栈前端4 小时前
如何手写实现 JSON Parser
css·sandbox
亿牛云爬虫专家4 小时前
Puppeteer教程:使用CSS选择器点击和爬取动态数据
javascript·css·爬虫·爬虫代理·puppeteer·代理ip
2401_857610035 小时前
深入探索React合成事件(SyntheticEvent):跨浏览器的事件处理利器
前端·javascript·react.js
fighting ~6 小时前
react17安装html-react-parser运行报错记录
javascript·react.js·html
老码沉思录6 小时前
React Native 全栈开发实战班 - 列表与滚动视图
javascript·react native·react.js
老码沉思录7 小时前
React Native 全栈开发实战班 - 状态管理入门(Context API)
javascript·react native·react.js