JavaScript拖拽排序
搬砖的乔布梭 人气:0可拖拽排序的菜单效果大家想必都很熟悉,本次我们通过一个可拖拽排序的九宫格案例来演示其实现原理。 先看一下完成效果:
实现原理概述
拖拽原理
- 当鼠标在【可拖拽小方块】(以下简称砖头)身上按下时,开始监听鼠标移动事件
- 鼠标事件移动到什么位置,砖头就跟到什么位置
- 鼠标抬起时,取消鼠标移动事件的监听
排序原理
- 提前定义好9大坑位的位置(相对外层盒子的left和top)
- 将9大砖头丢入一个数组,以便后期通过splice方法随意安插和更改砖头的位置
- 当拖动某块砖头时,先将其从数组中移除(剩余的砖头在逻辑上重新排序)
- 拖动结束时,将该砖头重新插回数组的目标位置(此时实现数据上的重排)
- 数组中的9块砖头根据新的序号,对号入座到9大坑位,完成重新渲染
代码实现
页面布局
9块砖头(li元素)相对于外层盒子(ul元素)做绝对定位
<ul id="box"> <li style="background-color:black;top: 10px; left: 10px">1</li> <li style="background-color:black;top: 10px; left: 220px">2</li> <li style="background-color:black;top: 10px; left: 430px">3</li> <li style="background-color:black;top: 220px; left: 10px">4</li> <li style="background-color:black;top: 220px; left: 220px">5</li> <li style="background-color:black;top: 220px; left: 430px">6</li> <li style="background-color:black;top: 430px; left: 10px">7</li> <li style="background-color:black;top: 430px; left: 220px">8</li> <li style="background-color:black;top: 430px; left: 430px">9</li> </ul>
样式如下
* { 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<pArr.length;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 < positions.length; 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.length; i++) { lis[i].style.left = positions[i][0] + "px" lis[i].style.top = positions[i][1] + "px" // 更新自己的位置 lis[i].setAttribute("position", i) } }
完整代码实现
主程序
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>九宫格拖拽排序</title> <style> * { 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; } </style> </head> <body> <ul id="box"> <li style="background-color:black;top: 10px; left: 10px">1</li> <li style="background-color:black;top: 10px; left: 220px">2</li> <li style="background-color:black;top: 10px; left: 430px">3</li> <li style="background-color:black;top: 220px; left: 10px">4</li> <li style="background-color:black;top: 220px; left: 220px">5</li> <li style="background-color:black;top: 220px; left: 430px">6</li> <li style="background-color:black;top: 430px; left: 10px">7</li> <li style="background-color:black;top: 430px; left: 220px">8</li> <li style="background-color:black;top: 430px; left: 430px">9</li> </ul> <!-- position 位置 --> <script src="../../../tools/arr_obj_tool.js"></script> <script src="../../../tools/animtool.js"></script> <script> // 定义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) /* 给每块砖内置一个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; } ) /* 在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 < positions.length; 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.length; i++) { lis[i].style.left = positions[i][0] + "px" lis[i].style.top = positions[i][1] + "px" // 更新自己的位置 lis[i].setAttribute("position", i) } } </script> </body> </html>
动画轮子
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<pArr.length;i++){ arr.push(pArr[i]) } return arr }
这里大家也可以简单地
const arr = [...pArr]
加载全部内容