可拖拽排序的菜單效果大家想必都很熟悉,本次我們通過一個可拖拽排序的九宮格案例來演示其實現原理。
## 實現原理概述
**拖拽原理**
- 當鼠標在【可拖拽小方塊】(以下簡稱磚頭)身上按下時,開始監聽鼠標移動事件
- 鼠標事件移動到什么位置,磚頭就跟到什么位置
- 鼠標抬起時,取消鼠標移動事件的監聽
**排序原理**
- 提前定義好9大坑位的位置(相對外層盒子的left和top)
- 將9大磚頭丟入一個數組,以便后期通過splice方法隨意安插和更改磚頭的位置
- 當拖動某塊磚頭時,先將其從數組中移除(剩余的磚頭在邏輯上重新排序)
- 拖動結束時,將該磚頭重新插回數組的目標位置(此時實現數據上的重排)
- 數組中的9塊磚頭根據新的序號,對號入座到9大坑位,完成重新渲染
## 代碼實現
**頁面布局**
9塊磚頭(li元素)相對于外層盒子(ul元素)做絕對定位
```html
<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>
```
樣式如下
```css
<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>
```
**定義磚頭的背景色和9大坑位位置**
```js
// 定義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],
]
```
**找出磚頭并丟入一個數組**
```js
var ulBox = document.querySelector("#box")
var lis = document.querySelectorAll("#box>li")
/* 將lis轉化為真數組 */
lis = toArray(lis)
```
這里我使用了一個將NodeList偽數組轉化為真數組的輪子:
```js
/* 偽數組轉真數組 pseudo array */
function toArray(pArr){
var arr = []
for(var i=0;i<pArr.length;i++){
arr.push(pArr[i])
}
return arr
}
```
**給所有磚頭內置一個position屬性**
```js
/* 給每塊磚內置一個position屬性 */
lis.forEach(
(item, index) => item.setAttribute("position", index)
)
```
**定義正在拖動的磚頭**
```js
/* 正在拖動的Li(磚頭) */
var draggingLi = null;
// 正在拖動的磚頭的zindex不斷加加,保持在最上層
var maxZindex = 9
```
**在身上按下 誰就是【正在拖動的磚頭】**
```js
/* 在身上按下 誰就是【正在拖動的磚頭】 */
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++
}
)
}
)
```
**在任意位置松開鼠標則停止拖拽**
```js
/* 在頁面的任意位置松開鼠標=不再拖拽任何對象 */
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;
}
)
```
當前磚頭從鼠標事件位置回歸其坑位時用到動畫效果,以下是動畫輪子
```js
/**
* 多屬性動畫
* @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;
};
```
**移動鼠標時 磚頭跟隨 所有磚頭實時洗牌**
```js
/* 在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()
}
}
)
```
**坑位檢測方法**
```js
/* 實時檢測坑位:檢測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
}
```
**磚頭洗牌方法**
```js
/* 磚頭洗牌: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)
}
}
```
## 完整代碼實現
**主程序**
```js
<!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>
```
**動畫輪子**
```js
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;
};
```
**偽數組轉真數組輪子**
```js
/* 偽數組轉真數組 pseudo array */
function toArray(pArr){
var arr = []
for(var i=0;i<pArr.length;i++){
arr.push(pArr[i])
}
return arr
}
```
這里大家也可以簡單地
```js
const arr = [...pArr]
```
祝大家擼碼愉快,身心健康!更多關于“web前端培訓”的問題,歡迎咨詢千鋒教育在線名師。千鋒已有十余年的培訓經驗,課程大綱更科學更專業,有針對零基礎的就業班,有針對想提升技術的提升班,高品質課程助理你實現夢想。