可拖拽排序的菜单效果大家想必都很熟悉,本次我们通过一个可拖拽排序的九宫格案例来演示其实现原理。
## 实现原理概述
**拖拽原理**
- 当鼠标在【可拖拽小方块】(以下简称砖头)身上按下时,开始监听鼠标移动事件
- 鼠标事件移动到什么位置,砖头就跟到什么位置
- 鼠标抬起时,取消鼠标移动事件的监听
**排序原理**
- 提前定义好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前端培训”的问题,欢迎咨询千锋教育在线名师。千锋已有十余年的培训经验,课程大纲更科学更专业,有针对零基础的就业班,有针对想提升技术的提升班,高品质课程助理你实现梦想。