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

JavaScript实现拖拽排序的方法详解_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 arr.push(pArr[i])
}
return arr
}

给所有砖头内置一个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 const [x, y] = positions[i]//[10,10]
const [ex, ey] = ep//[offsetX,offsetY]
const distance = Math.sqrt(Math.pow(x - ex, 2) + Math.pow(y - ey, 2))
if (distance <100) {
return i
}
}
// 没有进入任何坑位
return -1
}

砖头洗牌方法

/* 砖头洗牌:lis中的每块砖去到对应的位置 */
const shuffle = () => {
for (var i = 0; i lis[i].style.left = positions[i][0] + "px"
lis[i].style.top = positions[i][1] + "px"
// 更新自己的位置
lis[i].setAttribute("position", 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 arr.push(pArr[i])
}
return arr
}

这里大家也可以简单地

const arr = [...pArr]


推荐阅读
  • 本文讨论了一个关于cuowu类的问题,作者在使用cuowu类时遇到了错误提示和使用AdjustmentListener的问题。文章提供了16个解决方案,并给出了两个可能导致错误的原因。 ... [详细]
  • 目录实现效果:实现环境实现方法一:基本思路主要代码JavaScript代码总结方法二主要代码总结方法三基本思路主要代码JavaScriptHTML总结实 ... [详细]
  • 如何自行分析定位SAP BSP错误
    The“BSPtag”Imentionedintheblogtitlemeansforexamplethetagchtmlb:configCelleratorbelowwhichi ... [详细]
  • CSS3选择器的使用方法详解,提高Web开发效率和精准度
    本文详细介绍了CSS3新增的选择器方法,包括属性选择器的使用。通过CSS3选择器,可以提高Web开发的效率和精准度,使得查找元素更加方便和快捷。同时,本文还对属性选择器的各种用法进行了详细解释,并给出了相应的代码示例。通过学习本文,读者可以更好地掌握CSS3选择器的使用方法,提升自己的Web开发能力。 ... [详细]
  • Java容器中的compareto方法排序原理解析
    本文从源码解析Java容器中的compareto方法的排序原理,讲解了在使用数组存储数据时的限制以及存储效率的问题。同时提到了Redis的五大数据结构和list、set等知识点,回忆了作者大学时代的Java学习经历。文章以作者做的思维导图作为目录,展示了整个讲解过程。 ... [详细]
  • 本文介绍了OC学习笔记中的@property和@synthesize,包括属性的定义和合成的使用方法。通过示例代码详细讲解了@property和@synthesize的作用和用法。 ... [详细]
  • Spring源码解密之默认标签的解析方式分析
    本文分析了Spring源码解密中默认标签的解析方式。通过对命名空间的判断,区分默认命名空间和自定义命名空间,并采用不同的解析方式。其中,bean标签的解析最为复杂和重要。 ... [详细]
  • 向QTextEdit拖放文件的方法及实现步骤
    本文介绍了在使用QTextEdit时如何实现拖放文件的功能,包括相关的方法和实现步骤。通过重写dragEnterEvent和dropEvent函数,并结合QMimeData和QUrl等类,可以轻松实现向QTextEdit拖放文件的功能。详细的代码实现和说明可以参考本文提供的示例代码。 ... [详细]
  • 本文介绍了数据库的存储结构及其重要性,强调了关系数据库范例中将逻辑存储与物理存储分开的必要性。通过逻辑结构和物理结构的分离,可以实现对物理存储的重新组织和数据库的迁移,而应用程序不会察觉到任何更改。文章还展示了Oracle数据库的逻辑结构和物理结构,并介绍了表空间的概念和作用。 ... [详细]
  • 开发笔记:加密&json&StringIO模块&BytesIO模块
    篇首语:本文由编程笔记#小编为大家整理,主要介绍了加密&json&StringIO模块&BytesIO模块相关的知识,希望对你有一定的参考价值。一、加密加密 ... [详细]
  • android listview OnItemClickListener失效原因
    最近在做listview时发现OnItemClickListener失效的问题,经过查找发现是因为button的原因。不仅listitem中存在button会影响OnItemClickListener事件的失效,还会导致单击后listview每个item的背景改变,使得item中的所有有关焦点的事件都失效。本文给出了一个范例来说明这种情况,并提供了解决方法。 ... [详细]
  • 如何使用Java获取服务器硬件信息和磁盘负载率
    本文介绍了使用Java编程语言获取服务器硬件信息和磁盘负载率的方法。首先在远程服务器上搭建一个支持服务端语言的HTTP服务,并获取服务器的磁盘信息,并将结果输出。然后在本地使用JS编写一个AJAX脚本,远程请求服务端的程序,得到结果并展示给用户。其中还介绍了如何提取硬盘序列号的方法。 ... [详细]
  • 阿,里,云,物,联网,net,core,客户端,czgl,aliiotclient, ... [详细]
  • 使用Ubuntu中的Python获取浏览器历史记录原文: ... [详细]
  • 本文详细介绍了Linux中进程控制块PCBtask_struct结构体的结构和作用,包括进程状态、进程号、待处理信号、进程地址空间、调度标志、锁深度、基本时间片、调度策略以及内存管理信息等方面的内容。阅读本文可以更加深入地了解Linux进程管理的原理和机制。 ... [详细]
author-avatar
唯一的你b
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有