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

相关推荐
发呆的薇薇°3 小时前
React里使用lodash工具库
javascript·react.js
李宏伟~4 小时前
通过交叉实现数据触底分页效果new IntersectionObserver()(html、react、vue2、vue3)中使用
前端·javascript·react.js
冴羽7 小时前
Solid.js 最新官方文档翻译(12)—— 派生信号与 Memos
前端·javascript·react.js
发呆的薇薇°9 小时前
React里使用uuid插件--生成随机的id
react.js·uuid·javascirpt
发呆的薇薇°9 小时前
react里使用Day.js显示时间
前端·javascript·react.js
跑跑快跑9 小时前
React vite + less
前端·react.js·less
刺客-Andy9 小时前
React 第十九节 useLayoutEffect 用途使用技巧注意事项详解
前端·javascript·react.js·typescript·前端框架
赵大仁11 小时前
深入理解 Vue 3 中的具名插槽
前端·javascript·vue.js·react.js·前端框架·ecmascript·html5
一雨方知深秋11 小时前
v-bind 操作 class(对象,数组),v-bind 操作 style
前端·css·vue.js·html·style·class·v-bind
魏时烟20 小时前
css文字折行以及双端对齐实现方式
前端·css