热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

JavaScript实现拖拽排序的方法详解

可拖拽排序的菜单效果大家想必都很熟悉,本次我们通过一个可拖拽排序的九宫格案例来演示其实现原理,感兴趣的小伙伴可以

可拖拽排序的菜单效果大家想必都很熟悉,本次我们通过一个可拖拽排序的九宫格案例来演示其实现原理。 先看一下完成效果:

实现原理概述

拖拽原理

  • 当鼠标在【可拖拽小方块】(以下简称砖头)身上按下时,开始监听鼠标移动事件
  • 鼠标事件移动到什么位置,砖头就跟到什么位置
  • 鼠标抬起时,取消鼠标移动事件的监听

排序原理

  • 提前定义好9大坑位的位置(相对外层盒子的left和top)
  • 将9大砖头丢入一个数组,以便后期通过splice方法随意安插和更改砖头的位置
  • 当拖动某块砖头时,先将其从数组中移除(剩余的砖头在逻辑上重新排序)
  • 拖动结束时,将该砖头重新插回数组的目标位置(此时实现数据上的重排)
  • 数组中的9块砖头根据新的序号,对号入座到9大坑位,完成重新渲染

代码实现

页面布局

9块砖头(li元素)相对于外层盒子(ul元素)做绝对定位

	
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

样式如下

	    * {
	        margin: 0;
	        padding: 0;
	    }
	
	    html,
	    body {
	        width: 100%;
	        height: 100%;
	    }
	
	    ul,
	    li {
	        list-style: none;
	    }
	
	    ul {
	        width: 640px;
	        height: 640px;
	        border: 10px solid pink;
	        border-radius: 10px;
	        margin: 50px auto;
	        position: relative;
	    }
	
	    li {
	        width: 200px;
	        height: 200px;
	        border-radius: 10px;
	        display: flex;
	        justify-content: center;
	        align-items: center;
	        color: white;
	        font-size: 100px;
	        position: absolute;
	    }

定义砖头的背景色和9大坑位位置

	// 定义9大li的预设背景色
	var colorArr = [
	    "red",
	    "orange",
	    "yellow",
	    "green",
	    "blue",
	    "cyan",
	    "purple",
	    "pink",
	    "gray",
	];
	
	/* 定义9大坑位 */
	const positiOns= [
	    [10, 10], [220, 10], [430, 10],
	    [10, 220], [220, 220], [430, 220],
	    [10, 430], [220, 430], [430, 430],
	]

找出砖头并丢入一个数组

	var ulBox = document.querySelector("#box")
	var lis = document.querySelectorAll("#box>li")
	/* 将lis转化为真数组 */
	lis = toArray(lis)

这里我使用了一个将NodeList伪数组转化为真数组的轮子:

	/* 伪数组转真数组 pseudo array */
	function toArray(pArr){
	    var arr = []
	    for(var i=0;i

给所有砖头内置一个position属性

	/* 给每块砖内置一个position属性 */
	lis.forEach(
	    (item, index) => item.setAttribute("position", index)
	)

定义正在拖动的砖头

        /* 正在拖动的Li(砖头) */
        var draggingLi = null;

        // 正在拖动的砖头的zindex不断加加,保持在最上层
        var maxZindex = 9

在身上按下 谁就是【正在拖动的砖头】

        /* 在身上按下 谁就是【正在拖动的砖头】 */
        lis.forEach(
            function (li, index) {
                li.style.backgroundColor = colorArr[index]

                /* li中的文字不可选(禁止selectstart事件的默认行为) */
                li.addEventListener(
                    "selectstart",
                    function (e) {
                        // 阻止掉拖选文本的默认行为
                        e.preventDefault()
                    }
                )

                /* 在任意li身上按下鼠标=我想拖动它 */
                li.addEventListener(
                    "mousedown",
                    function (e) {
                        draggingLi = this
                        draggingLi.style.zIndex = maxZindex++
                    }
                )
            }
        )

在任意位置松开鼠标则停止拖拽

        /* 在页面的任意位置松开鼠标=不再拖拽任何对象 */
        document.addEventListener(
            "mouseup",
            function (e) {
                // 当前砖头自己进入位置躺好
                const p = draggingLi.getAttribute("position") * 1
                // draggingLi.style.left = positions[p][0] + "px"
                // draggingLi.style.top = positions[p][1] + "px"
                move(
                    draggingLi, 
                    {
                        left:positions[p][0] + "px",
                        top:positions[p][1] + "px"
                    }, 
                    200
                    // callback
                )

                // 正在拖拽的砖头置空
                draggingLi = null;
            }
        )

当前砖头从鼠标事件位置回归其坑位时用到动画效果,以下是动画轮子

/**
 * 多属性动画
 * @param {Element} element 要做动画的元素
 * @param {Object} targetObj 属性目标值的对象 封装了所有要做动画的属性及其目标值
 * @param {number} timeCost 动画耗时,单位毫秒
 * @param {Function} callback 动画结束的回调函数
 */
const move = (element, targetObj, timeCost = 1000, callback) => {
    const frameTimeCost = 40;

    // 500.00px 提取单位的正则
    const regUnit = /[d.]+([a-z]*)/;

    // 计算动画总帧数
    const totalFrames = Math.round(timeCost / frameTimeCost);

    // 动态数一数当前动画到了第几帧
    let frameCount = 0;

    /* 查询特定属性的速度(汤鹏飞的辣鸡) */
    // const getAttrSpeed = (attr) => (parseFloat(targetObj[attr]) - parseFloat(getComputedStyle(element)[attr]))/totalFrames

    // 存储各个属性的初始值和动画速度
    const ssObj = {};

    /* 遍历targetObj的所有属性 */
    for (let attr in targetObj) {
        // 拿到元素属性的初始值
        const attrStart = parseFloat(getComputedStyle(element)[attr]);

        // 动画速度 = (目标值 - 当前值)/帧数
        const attrSpeed =
            (parseFloat(targetObj[attr]) - attrStart) / totalFrames;

        // 将【属性初始值】和【属性帧速度】存在obj中 以后obj[left]同时拿到这两个货
        // obj{ left:[0px初始值,50px每帧] }
        ssObj[attr] = [attrStart, attrSpeed];
    }

    /* 开始动画 */
    const timer = setInterval(
        () => {
            // element.style.left = parseFloat(getComputedStyle(element).left)+"px"
            // element.style.top = parseFloat(getComputedStyle(element).top)+"px"
            // element.style.opacity = getComputedStyle(element).opacity

            // 帧数+1
            frameCount++;

            /* 每个属性的值都+=动画速度 */
            for (let attr in targetObj) {
                // console.log(attr, ssObj[attr], totalFrames, frameCount);

                // 用正则分离出单位
                // console.log(regUnit.exec("500px"));
                // console.log(regUnit.exec(0));
                const unit = regUnit.exec(targetObj[attr])[1];

                // 计算出当前帧应该去到的属性值
                const thisFrameValue =
                    ssObj[attr][0] + frameCount * ssObj[attr][1];

                // 将元素的属性掰到当前帧应该去到的目标值
                element.style[attr] = thisFrameValue + unit;
            }

            /* 当前帧 多个属性动画完成 判断是否应该终止动画  */
            if (frameCount >= totalFrames) {
                // console.log(frameCount, totalFrames);
                clearInterval(timer);

                /* 强制矫正(反正用户又看不出来 V) */
                // for (let attr in targetObj) {
                //     element.style[attr] = targetObj[attr];
                //     console.log(attr, getComputedStyle(element)[attr]);
                // }

                // 如果有callback就调用callback
                // if(callback){
                //     callback()
                // }
                callback && callback();
            }
        },

        frameTimeCost
    );

    /* 动画结束后再过一帧 执行暴力校正 */
    setTimeout(() => {
        /* 强制矫正(反正用户又看不出来 V) */
        for (let attr in targetObj) {
            element.style[attr] = targetObj[attr];
            // console.log(attr, getComputedStyle(element)[attr]);
        }
    }, timeCost + frameTimeCost);

    // 返回正在运行的定时器
    return timer;
};

移动鼠标时 砖头跟随 所有砖头实时洗牌

        /* 在ul内移动鼠标 draggingLi跟随鼠标 */
        ulBox.addEventListener(
            "mousemove",
            function (e) {
                /* 如果draggingLi为空 什么也不做 直接返回 */
                if (draggingLi === null) {
                    return
                }

                // 拿到事件相对于ulBox的位置   
                var offsetX = e.pageX - ulBox.offsetLeft - 100
                var offsetY = e.pageY - ulBox.offsetTop - 100

                /* 校正砖头的偏移量 */
                offsetX = offsetX <10 ? 10 : offsetX
                offsetY = offsetY <10 ? 10 : offsetY
                offsetX = offsetX > 430 ? 430 : offsetX
                offsetY = offsetY > 430 ? 430 : offsetY

                // 将该位置设置给draggingLi
                draggingLi.style.left = offsetX + "px"
                draggingLi.style.top = offsetY + "px"

                /* 实时检测实时【坑位】 */
                const newPosition = checkPosition([offsetX, offsetY]);

                // 如果当前砖头的position发生变化 则数据重排
                const oldPosition = draggingLi.getAttribute("position") * 1
                if (newPosition != -1 && newPosition != oldPosition) {
                    console.log(oldPosition, newPosition);

                    /* 数据重排 */
                    // 先将当前砖头拽出数组(剩余的砖头位置自动重排)
                    lis.splice(oldPosition, 1)
                    // 再将当前砖头插回newPosition
                    lis.splice(newPosition, 0, draggingLi)

                    // 打印新数据
                    // logArr(lis,"innerText")

                    // 砖头洗牌
                    shuffle()
                }

            }
        )

坑位检测方法

        /* 实时检测坑位:检测ep与9大坑位的距离是否小于100 */
        const checkPosition = (ep) => {
            for (let i = 0; i 

砖头洗牌方法

        /* 砖头洗牌:lis中的每块砖去到对应的位置 */
        const shuffle = () => {
            for (var i = 0; i 

完整代码实现

主程序




    
    
    
    

    



    
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

动画轮子

function moveWithTransition(element, targetObj, duration) {
    element.style.transition = `all ${duration / 1000 + "s"} linear`;
    for (var attr in targetObj) {
        element.style[attr] = targetObj[attr];
    }
    setTimeout(() => {
        element.style.transition = "none";
    }, duration);
}

/**
 * 多属性动画
 * @param {Element} element 要做动画的元素
 * @param {Object} targetObj 属性目标值的对象 封装了所有要做动画的属性及其目标值
 * @param {number} timeCost 动画耗时,单位毫秒
 * @param {Function} callback 动画结束的回调函数
 */
const move = (element, targetObj, timeCost = 1000, callback) => {
    const frameTimeCost = 40;

    // 500.00px 提取单位的正则
    const regUnit = /[d.]+([a-z]*)/;

    // 计算动画总帧数
    const totalFrames = Math.round(timeCost / frameTimeCost);

    // 动态数一数当前动画到了第几帧
    let frameCount = 0;

    /* 查询特定属性的速度(汤鹏飞的辣鸡) */
    // const getAttrSpeed = (attr) => (parseFloat(targetObj[attr]) - parseFloat(getComputedStyle(element)[attr]))/totalFrames

    // 存储各个属性的初始值和动画速度
    const ssObj = {};

    /* 遍历targetObj的所有属性 */
    for (let attr in targetObj) {
        // 拿到元素属性的初始值
        const attrStart = parseFloat(getComputedStyle(element)[attr]);

        // 动画速度 = (目标值 - 当前值)/帧数
        const attrSpeed =
            (parseFloat(targetObj[attr]) - attrStart) / totalFrames;

        // 将【属性初始值】和【属性帧速度】存在obj中 以后obj[left]同时拿到这两个货
        // obj{ left:[0px初始值,50px每帧] }
        ssObj[attr] = [attrStart, attrSpeed];
    }

    /* 开始动画 */
    const timer = setInterval(
        () => {
            // element.style.left = parseFloat(getComputedStyle(element).left)+"px"
            // element.style.top = parseFloat(getComputedStyle(element).top)+"px"
            // element.style.opacity = getComputedStyle(element).opacity

            // 帧数+1
            frameCount++;

            /* 每个属性的值都+=动画速度 */
            for (let attr in targetObj) {
                // console.log(attr, ssObj[attr], totalFrames, frameCount);

                // 用正则分离出单位
                // console.log(regUnit.exec("500px"));
                // console.log(regUnit.exec(0));
                const unit = regUnit.exec(targetObj[attr])[1];

                // 计算出当前帧应该去到的属性值
                const thisFrameValue =
                    ssObj[attr][0] + frameCount * ssObj[attr][1];

                // 将元素的属性掰到当前帧应该去到的目标值
                element.style[attr] = thisFrameValue + unit;
            }

            /* 当前帧 多个属性动画完成 判断是否应该终止动画  */
            if (frameCount >= totalFrames) {
                // console.log(frameCount, totalFrames);
                clearInterval(timer);

                /* 强制矫正(反正用户又看不出来 V) */
                // for (let attr in targetObj) {
                //     element.style[attr] = targetObj[attr];
                //     console.log(attr, getComputedStyle(element)[attr]);
                // }

                // 如果有callback就调用callback
                // if(callback){
                //     callback()
                // }
                callback && callback();
            }
        },

        frameTimeCost
    );

    /* 动画结束后再过一帧 执行暴力校正 */
    setTimeout(() => {
        /* 强制矫正(反正用户又看不出来 V) */
        for (let attr in targetObj) {
            element.style[attr] = targetObj[attr];
            // console.log(attr, getComputedStyle(element)[attr]);
        }
    }, timeCost + frameTimeCost);

    // 返回正在运行的定时器
    return timer;
};

伪数组转真数组轮子

/* 伪数组转真数组 pseudo array */
function toArray(pArr){
    var arr = []
    for(var i=0;i

这里大家也可以简单地

const arr = [...pArr]

以上就是Javascript实现拖拽排序的方法详解的详细内容,更多关于Javascript拖拽排序的资料请关注编程笔记其它相关文章!


推荐阅读
  • 1,关于死锁的理解死锁,我们可以简单的理解为是两个线程同时使用同一资源,两个线程又得不到相应的资源而造成永无相互等待的情况。 2,模拟死锁背景介绍:我们创建一个朋友 ... [详细]
  • 本文介绍了使用Java实现大数乘法的分治算法,包括输入数据的处理、普通大数乘法的结果和Karatsuba大数乘法的结果。通过改变long类型可以适应不同范围的大数乘法计算。 ... [详细]
  • 猜字母游戏
    猜字母游戏猜字母游戏——设计数据结构猜字母游戏——设计程序结构猜字母游戏——实现字母生成方法猜字母游戏——实现字母检测方法猜字母游戏——实现主方法1猜字母游戏——设计数据结构1.1 ... [详细]
  • Redis底层数据结构之压缩列表的介绍及实现原理
    本文介绍了Redis底层数据结构之压缩列表的概念、实现原理以及使用场景。压缩列表是Redis为了节约内存而开发的一种顺序数据结构,由特殊编码的连续内存块组成。文章详细解释了压缩列表的构成和各个属性的含义,以及如何通过指针来计算表尾节点的地址。压缩列表适用于列表键和哈希键中只包含少量小整数值和短字符串的情况。通过使用压缩列表,可以有效减少内存占用,提升Redis的性能。 ... [详细]
  • 基于layUI的图片上传前预览功能的2种实现方式
    本文介绍了基于layUI的图片上传前预览功能的两种实现方式:一种是使用blob+FileReader,另一种是使用layUI自带的参数。通过选择文件后点击文件名,在页面中间弹窗内预览图片。其中,layUI自带的参数实现了图片预览功能。该功能依赖于layUI的上传模块,并使用了blob和FileReader来读取本地文件并获取图像的base64编码。点击文件名时会执行See()函数。摘要长度为169字。 ... [详细]
  • 本文介绍了C#中数据集DataSet对象的使用及相关方法详解,包括DataSet对象的概述、与数据关系对象的互联、Rows集合和Columns集合的组成,以及DataSet对象常用的方法之一——Merge方法的使用。通过本文的阅读,读者可以了解到DataSet对象在C#中的重要性和使用方法。 ... [详细]
  • 本文介绍了OC学习笔记中的@property和@synthesize,包括属性的定义和合成的使用方法。通过示例代码详细讲解了@property和@synthesize的作用和用法。 ... [详细]
  • Mac OS 升级到11.2.2 Eclipse打不开了,报错Failed to create the Java Virtual Machine
    本文介绍了在Mac OS升级到11.2.2版本后,使用Eclipse打开时出现报错Failed to create the Java Virtual Machine的问题,并提供了解决方法。 ... [详细]
  • 在说Hibernate映射前,我们先来了解下对象关系映射ORM。ORM的实现思想就是将关系数据库中表的数据映射成对象,以对象的形式展现。这样开发人员就可以把对数据库的操作转化为对 ... [详细]
  • 本文介绍了一个在线急等问题解决方法,即如何统计数据库中某个字段下的所有数据,并将结果显示在文本框里。作者提到了自己是一个菜鸟,希望能够得到帮助。作者使用的是ACCESS数据库,并且给出了一个例子,希望得到的结果是560。作者还提到自己已经尝试了使用"select sum(字段2) from 表名"的语句,得到的结果是650,但不知道如何得到560。希望能够得到解决方案。 ... [详细]
  • 高质量SQL书写的30条建议
    本文提供了30条关于优化SQL的建议,包括避免使用select *,使用具体字段,以及使用limit 1等。这些建议是基于实际开发经验总结出来的,旨在帮助读者优化SQL查询。 ... [详细]
  • 深入理解CSS中的margin属性及其应用场景
    本文主要介绍了CSS中的margin属性及其应用场景,包括垂直外边距合并、padding的使用时机、行内替换元素与费替换元素的区别、margin的基线、盒子的物理大小、显示大小、逻辑大小等知识点。通过深入理解这些概念,读者可以更好地掌握margin的用法和原理。同时,文中提供了一些相关的文档和规范供读者参考。 ... [详细]
  • 本文介绍了绕过WAF的XSS检测机制的方法,包括确定payload结构、测试和混淆。同时提出了一种构建XSS payload的方法,该payload与安全机制使用的正则表达式不匹配。通过清理用户输入、转义输出、使用文档对象模型(DOM)接收器和源、实施适当的跨域资源共享(CORS)策略和其他安全策略,可以有效阻止XSS漏洞。但是,WAF或自定义过滤器仍然被广泛使用来增加安全性。本文的方法可以绕过这种安全机制,构建与正则表达式不匹配的XSS payload。 ... [详细]
  • HTML5网页模板怎么加百度统计?
    本文介绍了如何在HTML5网页模板中加入百度统计,并对模板文件、css样式表、js插件库等内容进行了说明。同时还解答了关于HTML5网页模板的使用方法、表单提交、域名和空间的问题,并介绍了如何使用Visual Studio 2010创建HTML5模板。此外,还提到了使用Jquery编写美好的HTML5前端框架模板的方法,以及制作企业HTML5网站模板和支持HTML5的CMS。 ... [详细]
  • 本文介绍了一种在PHP中对二维数组根据某个字段进行排序的方法,以年龄字段为例,按照倒序的方式进行排序,并给出了具体的代码实现。 ... [详细]
author-avatar
zhangsheng7_215
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有