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能看到其实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规则来说

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

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

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

大功告成。

封面

pixiv

相关推荐
古茗前端团队13 小时前
由 key 引发的思考
前端·javascript·react.js
程序员大阳3 天前
HTML+CSS+JS对话式实战(3)-活动管理页实现查询、新增、删除API后端接口
javascript·css·html·实战·删除·查询·新增
ZhengEnCi3 天前
12 种高级 UI 交互效果及对应 AI 提示词:从按压回弹到底部抽屉的物理感教学
前端·css·交互设计
ZhengEnCi4 天前
为什么苹果的圆角更高级:连续曲率圆角 Squircle 从超椭圆到 CSS 实现的完全讲解
css·设计
用户298698530144 天前
React 前端将 Excel 工作表导出为图片的技术实践
javascript·react.js·excel
用户3434612783205 天前
用户认证系统在高并发下的幂等性与限流策略踩坑实录
css
終不似少年遊*5 天前
【安全业务Agent】从场景到编排
python·react.js·ai·langchain·agent·安全架构·工作流
JavaPub-rodert5 天前
从零搭建通用管理后台:ShiyuAdmin 的 Go、React 与 RBAC 实践
开发语言·react.js·golang
Highcharts.js6 天前
React 中原生支持使用 JSX 组件配置自定义表格 Highcharts Grid
前端·javascript·react.js·前端框架·jsx·highcharts·自定义表格
JudithHuang6 天前
React 嵌套路由与布局
前端·react.js·arcgis·react·react router