JavaScript實(shí)現(xiàn)拖拽排序的方法詳解
可拖拽排序的菜單效果大家想必都很熟悉,本次我們通過一個可拖拽排序的九宮格案例來演示其實(shí)現(xiàn)原理。 先看一下完成效果:
實(shí)現(xiàn)原理概述
拖拽原理
- 當(dāng)鼠標(biāo)在【可拖拽小方塊】(以下簡稱磚頭)身上按下時,開始監(jiān)聽鼠標(biāo)移動事件
- 鼠標(biāo)事件移動到什么位置,磚頭就跟到什么位置
- 鼠標(biāo)抬起時,取消鼠標(biāo)移動事件的監(jiān)聽
排序原理
- 提前定義好9大坑位的位置(相對外層盒子的left和top)
- 將9大磚頭丟入一個數(shù)組,以便后期通過splice方法隨意安插和更改磚頭的位置
- 當(dāng)拖動某塊磚頭時,先將其從數(shù)組中移除(剩余的磚頭在邏輯上重新排序)
- 拖動結(jié)束時,將該磚頭重新插回數(shù)組的目標(biāo)位置(此時實(shí)現(xiàn)數(shù)據(jù)上的重排)
- 數(shù)組中的9塊磚頭根據(jù)新的序號,對號入座到9大坑位,完成重新渲染
代碼實(shí)現(xiàn)
頁面布局
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的預(yù)設(shè)背景色 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], ]
找出磚頭并丟入一個數(shù)組
var ulBox = document.querySelector("#box") var lis = document.querySelectorAll("#box>li") /* 將lis轉(zhuǎn)化為真數(shù)組 */ lis = toArray(lis)
這里我使用了一個將NodeList偽數(shù)組轉(zhuǎn)化為真數(shù)組的輪子:
/* 偽數(shù)組轉(zhuǎn)真數(shù)組 pseudo array */ function toArray(pArr){ var arr = [] for(var i=0;i<pArr.length;i++){ arr.push(pArr[i]) } return arr }
給所有磚頭內(nèi)置一個position屬性
/* 給每塊磚內(nèi)置一個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事件的默認(rèn)行為) */ li.addEventListener( "selectstart", function (e) { // 阻止掉拖選文本的默認(rèn)行為 e.preventDefault() } ) /* 在任意li身上按下鼠標(biāo)=我想拖動它 */ li.addEventListener( "mousedown", function (e) { draggingLi = this draggingLi.style.zIndex = maxZindex++ } ) } )
在任意位置松開鼠標(biāo)則停止拖拽
/* 在頁面的任意位置松開鼠標(biāo)=不再拖拽任何對象 */ document.addEventListener( "mouseup", function (e) { // 當(dāng)前磚頭自己進(jìn)入位置躺好 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; } )
當(dāng)前磚頭從鼠標(biāo)事件位置回歸其坑位時用到動畫效果,以下是動畫輪子
/** * 多屬性動畫 * @param {Element} element 要做動畫的元素 * @param {Object} targetObj 屬性目標(biāo)值的對象 封裝了所有要做動畫的屬性及其目標(biāo)值 * @param {number} timeCost 動畫耗時,單位毫秒 * @param {Function} callback 動畫結(jié)束的回調(diào)函數(shù) */ const move = (element, targetObj, timeCost = 1000, callback) => { const frameTimeCost = 40; // 500.00px 提取單位的正則 const regUnit = /[\d\.]+([a-z]*)/; // 計算動畫總幀數(shù) const totalFrames = Math.round(timeCost / frameTimeCost); // 動態(tài)數(shù)一數(shù)當(dāng)前動畫到了第幾幀 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]); // 動畫速度 = (目標(biāo)值 - 當(dāng)前值)/幀數(shù) 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 // 幀數(shù)+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]; // 計算出當(dāng)前幀應(yīng)該去到的屬性值 const thisFrameValue = ssObj[attr][0] + frameCount * ssObj[attr][1]; // 將元素的屬性掰到當(dāng)前幀應(yīng)該去到的目標(biāo)值 element.style[attr] = thisFrameValue + unit; } /* 當(dāng)前幀 多個屬性動畫完成 判斷是否應(yīng)該終止動畫 */ if (frameCount >= totalFrames) { // console.log(frameCount, totalFrames); clearInterval(timer); /* 強(qiáng)制矯正(反正用戶又看不出來 V) */ // for (let attr in targetObj) { // element.style[attr] = targetObj[attr]; // console.log(attr, getComputedStyle(element)[attr]); // } // 如果有callback就調(diào)用callback // if(callback){ // callback() // } callback && callback(); } }, frameTimeCost ); /* 動畫結(jié)束后再過一幀 執(zhí)行暴力校正 */ setTimeout(() => { /* 強(qiáng)制矯正(反正用戶又看不出來 V) */ for (let attr in targetObj) { element.style[attr] = targetObj[attr]; // console.log(attr, getComputedStyle(element)[attr]); } }, timeCost + frameTimeCost); // 返回正在運(yùn)行的定時器 return timer; };
移動鼠標(biāo)時 磚頭跟隨 所有磚頭實(shí)時洗牌
/* 在ul內(nèi)移動鼠標(biāo) draggingLi跟隨鼠標(biāo) */ 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 // 將該位置設(shè)置給draggingLi draggingLi.style.left = offsetX + "px" draggingLi.style.top = offsetY + "px" /* 實(shí)時檢測實(shí)時【坑位】 */ const newPosition = checkPosition([offsetX, offsetY]); // 如果當(dāng)前磚頭的position發(fā)生變化 則數(shù)據(jù)重排 const oldPosition = draggingLi.getAttribute("position") * 1 if (newPosition != -1 && newPosition != oldPosition) { console.log(oldPosition, newPosition); /* 數(shù)據(jù)重排 */ // 先將當(dāng)前磚頭拽出數(shù)組(剩余的磚頭位置自動重排) lis.splice(oldPosition, 1) // 再將當(dāng)前磚頭插回newPosition lis.splice(newPosition, 0, draggingLi) // 打印新數(shù)據(jù) // logArr(lis,"innerText") // 磚頭洗牌 shuffle() } } )
坑位檢測方法
/* 實(shí)時檢測坑位:檢測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 } } // 沒有進(jìn)入任何坑位 return -1 }
磚頭洗牌方法
/* 磚頭洗牌:lis中的每塊磚去到對應(yīng)的位置 */ 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) } }
完整代碼實(shí)現(xiàn)
主程序
<!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的預(yù)設(shè)背景色 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轉(zhuǎn)化為真數(shù)組 */ lis = toArray(lis) /* 給每塊磚內(nèi)置一個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事件的默認(rèn)行為) */ li.addEventListener( "selectstart", function (e) { // 阻止掉拖選文本的默認(rèn)行為 e.preventDefault() } ) /* 在任意li身上按下鼠標(biāo)=我想拖動它 */ li.addEventListener( "mousedown", function (e) { draggingLi = this draggingLi.style.zIndex = maxZindex++ } ) } ) /* 在頁面的任意位置松開鼠標(biāo)=不再拖拽任何對象 */ document.addEventListener( "mouseup", function (e) { // 當(dāng)前磚頭自己進(jìn)入位置躺好 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內(nèi)移動鼠標(biāo) draggingLi跟隨鼠標(biāo) */ 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 // 將該位置設(shè)置給draggingLi draggingLi.style.left = offsetX + "px" draggingLi.style.top = offsetY + "px" /* 實(shí)時檢測實(shí)時【坑位】 */ const newPosition = checkPosition([offsetX, offsetY]); // 如果當(dāng)前磚頭的position發(fā)生變化 則數(shù)據(jù)重排 const oldPosition = draggingLi.getAttribute("position") * 1 if (newPosition != -1 && newPosition != oldPosition) { console.log(oldPosition, newPosition); /* 數(shù)據(jù)重排 */ // 先將當(dāng)前磚頭拽出數(shù)組(剩余的磚頭位置自動重排) lis.splice(oldPosition, 1) // 再將當(dāng)前磚頭插回newPosition lis.splice(newPosition, 0, draggingLi) // 打印新數(shù)據(jù) // logArr(lis,"innerText") // 磚頭洗牌 shuffle() } } ) /* 實(shí)時檢測坑位:檢測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 } } // 沒有進(jìn)入任何坑位 return -1 } /* 磚頭洗牌:lis中的每塊磚去到對應(yīng)的位置 */ 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 屬性目標(biāo)值的對象 封裝了所有要做動畫的屬性及其目標(biāo)值 * @param {number} timeCost 動畫耗時,單位毫秒 * @param {Function} callback 動畫結(jié)束的回調(diào)函數(shù) */ const move = (element, targetObj, timeCost = 1000, callback) => { const frameTimeCost = 40; // 500.00px 提取單位的正則 const regUnit = /[\d\.]+([a-z]*)/; // 計算動畫總幀數(shù) const totalFrames = Math.round(timeCost / frameTimeCost); // 動態(tài)數(shù)一數(shù)當(dāng)前動畫到了第幾幀 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]); // 動畫速度 = (目標(biāo)值 - 當(dāng)前值)/幀數(shù) 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 // 幀數(shù)+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]; // 計算出當(dāng)前幀應(yīng)該去到的屬性值 const thisFrameValue = ssObj[attr][0] + frameCount * ssObj[attr][1]; // 將元素的屬性掰到當(dāng)前幀應(yīng)該去到的目標(biāo)值 element.style[attr] = thisFrameValue + unit; } /* 當(dāng)前幀 多個屬性動畫完成 判斷是否應(yīng)該終止動畫 */ if (frameCount >= totalFrames) { // console.log(frameCount, totalFrames); clearInterval(timer); /* 強(qiáng)制矯正(反正用戶又看不出來 V) */ // for (let attr in targetObj) { // element.style[attr] = targetObj[attr]; // console.log(attr, getComputedStyle(element)[attr]); // } // 如果有callback就調(diào)用callback // if(callback){ // callback() // } callback && callback(); } }, frameTimeCost ); /* 動畫結(jié)束后再過一幀 執(zhí)行暴力校正 */ setTimeout(() => { /* 強(qiáng)制矯正(反正用戶又看不出來 V) */ for (let attr in targetObj) { element.style[attr] = targetObj[attr]; // console.log(attr, getComputedStyle(element)[attr]); } }, timeCost + frameTimeCost); // 返回正在運(yùn)行的定時器 return timer; };
偽數(shù)組轉(zhuǎn)真數(shù)組輪子
/* 偽數(shù)組轉(zhuǎn)真數(shù)組 pseudo array */ function toArray(pArr){ var arr = [] for(var i=0;i<pArr.length;i++){ arr.push(pArr[i]) } return arr }
這里大家也可以簡單地
const arr = [...pArr]
以上就是JavaScript實(shí)現(xiàn)拖拽排序的方法詳解的詳細(xì)內(nèi)容,更多關(guān)于JavaScript拖拽排序的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
javascript中break,continue和return語句用法小結(jié)
break,continue和return這三個語句的用法新手們經(jīng)常弄混淆,至少在我學(xué)習(xí)c語言的時候經(jīng)常把它們的用法給搞錯。不過現(xiàn)在好了,我已徹底搞清楚它們之間的用法2012-05-05微信小程序?qū)崿F(xiàn)購物車代碼實(shí)例詳解
這篇文章主要介紹了微信小程序?qū)崿F(xiàn)購物車代碼實(shí)例,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2019-08-08微信小程序webview實(shí)現(xiàn)長按點(diǎn)擊識別二維碼功能示例
這篇文章主要介紹了微信小程序webview實(shí)現(xiàn)長按點(diǎn)擊識別二維碼功能,結(jié)合實(shí)例形式分析了webview二維碼識別相關(guān)操作技巧,需要的朋友可以參考下2019-01-01javascript for循環(huán)設(shè)法提高性能
讓你的for循環(huán)提升性能的寫法,需要的朋友可以參考下。2010-02-02JavaScript實(shí)現(xiàn)好看的跟隨彩色氣泡效果
這篇文章主要為大家詳細(xì)介紹了JavaScript實(shí)現(xiàn)好看的跟隨彩色氣泡效果,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2020-02-02javascript實(shí)現(xiàn)左右緩動動畫函數(shù)
這篇文章主要為大家詳細(xì)介紹了javascript實(shí)現(xiàn)左右緩動動畫函數(shù),文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2020-11-11