原生JS实现列表拖拽排序

造轮子之前

如果需要实现一个列表拖拽排序的功能,目前前端社区有比较成熟的方案,在工作中直接使用即可,但作为一个技术人,有时候还是要探究一下实现过程,造一个自娱自乐的轮子,巩固一下JS基本功,在我看来也是一个不错的学习和积累的过程。今天就从头实现一个这样的轮子,不使用其他框架,在实现之前,我也没看其他框架的实现原理,先自己撸一个,再去研究其他实现吧,先看下最终效果,点击查看完整示例

问题分析

1.拖拽

说到拖拽,有一个drag事件可以轻松胜任,除此之外,mouse相关事件,我这次就使用mouse事件来实现拖拽的效果,首先需要了解一下和mouse相关的事件:

1.1 mousedown

这个事件就是鼠标第一次点击在目标节点时触发的事件,这个比较好理解,拖拽事件也是始于mousedown;

1.2 mousemove

mousemove 事件在定点设备(通常指鼠标)的光标在元素内移动时,会在该元素上触发。简单点说就是只要鼠标在目标节点或子节点上移动,就会一直触发,这里想说一下mouseover事件,有点类似,但又很不一样,当鼠标在不同元素之间移动才会触发;

1.3 mouseup

mousedown事件在定点设备(如鼠标或触摸板)按钮在元素内释放时,在该元素上触发。 mouseup 事件与 mousedown事件相对应。

1.4 mouseleave

mouseleave事件在定点设备(通常是鼠标)的指针移出某个元素时被触发;

关于mouse相关的事件还是挺多的,结合下面这个图整理了一下事件触发情况,需要说明的是鼠标事件均绑定在parent节点上,图中原点1代表阶段1,阶段1-2表示不包含1和2):

鼠标从左向右移动过程中,可以分为如下几个阶段:

  • 阶段1

触发事件:parent mouseover parent mouseenter parent mousemove(parent的border-size比较大时也会触发多次,下面child同理)

  • 阶段(1-2)

触发事件:parent mousemove多次

  • 阶段2

触发事件:parent mouseout child mouseover child mousemove

  • 阶段(2-3)

触发事件:child mousemove多次

  • 阶段3

触发事件:child mousemove

  • 阶段(3-4)

触发事件:child mouseout parent mouseover parent mousemove多次

  • 阶段4

触发事件:parent mousemove

  • 阶段4以后

触发事件:parent mouseout parent mouseleave

总结一下:

1.) mouseenter和mouseleave在鼠标开始进入和离开parent的时候才会触发,整个过程只会触发一次;

2.) mousemove在整个过程会一直触发,只是target不同而已;

3.) mouseover和mouseout在target变化时才会触发;

2.重叠区域检查

在拖拽节点B的同时,会和其他列表项发生重叠(如下图的A),如果重叠的面积超过一定的阈值,那么底部被覆盖的列表子项A就需要移动位置,空出来的区域就是拖拽节点B可能最终存放的位置;

下图中红色区域就是重叠的部分,由于我实现的列表是垂直排列的,那么其实只要计算红色区域的高度占据A高度的比例即可,超过阈值就需要移动A的位置了(后面都是B作为被拖拽的节点,A作为可能被覆盖和需要被动移动位置的设定);

在节点B拖拽过程中,获得鼠标坐标信息,实时更新B的translate属性,然后遍历所有列表项,得出列表子项在页面中的位置,这里使用getBoundingClientRect来获取,关于这个api,MDN上的图片很好的解释了各个参数的含义:

重叠区域的计算就显得很简单了,首先判断是否重叠,然后判断是顶部还是底部重叠,得出重叠的比例,具体代码可参考完整示例;

3.实时排序

经过前一步重叠区域检查后,如果超过了阈值,那么就需要移动A节点的位置了,这里不是真正改变DOM的顺序,而是设置A的translate即可,由于拖拽在垂直方向上有两个移动方向,up和down,up时A需要向下移动,down则相反,这里有个细节就是如果B往上拖拽,而A已经向下移动了(比如说translateY = 60),而B再次往下移动和A有效重叠,那么A的translateY就要重置为0;

具体实现

讲完上面一些知识点和细节之后,我们就可以编码了,这里我主要想说一点,就是重叠区域检查的时候,需要判断拖拽的节点B是和列表中那个index的位置重叠,这里的位置必须是没有拖拽前的原始位置信息,而这里为了获取节点原始位置,需要减去translateY的移动距离,示例使用getNodeVerticalPos函数去实现;

ini 复制代码
......
function getNodeVerticalPos(node) {
    const { y, bottom } = node.getBoundingClientRect();
    const transformY = normalizeNumberValue(node, '--y');
    return {
        y,
        bottom,
        rawY: y - transformY, // 节点在列表中原始的y
        rawBottom: bottom - transformY, // 节点在列表中原始的bottom
    };
}
......
/**
* @desc 负责检查有效的重叠区域,即正在拖拽的节点B和哪个item正在重叠
*/
function checkValidOverlap(e) {
    const { y, bottom } = getNodeVerticalPos(e.target);
    for (let i = 0; i < items.length; i++) {
        const { rawY, rawBottom } = getNodeVerticalPos(items[i]);
        if (!(bottom < rawY || rawBottom < y)) {
            let overlapRatio = 0;
            if (bottom < rawBottom) {
                overlapRatio = (bottom - rawY) / itemHeight;
            } else {
                overlapRatio = (rawBottom - y) / itemHeight;
            }
            if (overlapRatio >= ratio) {
                if (dragIndex !== i) {
                    dragIndex = i;
                    updateNodesPos(e); // 需要更新items[dragIndex]位置的node位置
                }
                i = items.length;
            }
        }
    }
}
......

最后

这次实现的乞丐版本中每个item都是等高的,如果不是等高的呢,水平方向拖拽排序呢,多想想情况还蛮多,后面再补充吧。

相关推荐
m0_748247551 小时前
Web 应用项目开发全流程解析与实战经验分享
开发语言·前端·php
m0_748255022 小时前
前端常用算法集合
前端·算法
真的很上进2 小时前
如何借助 Babel+TS+ESLint 构建现代 JS 工程环境?
java·前端·javascript·css·react.js·vue·html
web130933203982 小时前
vue elementUI form组件动态添加el-form-item并且动态添加rules必填项校验方法
前端·vue.js·elementui
NiNg_1_2343 小时前
Echarts连接数据库,实时绘制图表详解
前端·数据库·echarts
如若1233 小时前
对文件内的文件名生成目录,方便查阅
java·前端·python
滚雪球~4 小时前
npm error code ETIMEDOUT
前端·npm·node.js
沙漏无语4 小时前
npm : 无法加载文件 D:\Nodejs\node_global\npm.ps1,因为在此系统上禁止运行脚本
前端·npm·node.js
supermapsupport4 小时前
iClient3D for Cesium在Vue中快速实现场景卷帘
前端·vue.js·3d·cesium·supermap
brrdg_sefg4 小时前
WEB 漏洞 - 文件包含漏洞深度解析
前端·网络·安全