您的位置:首页 > 教程 > JavaScript > JavaScript实现拖拽排序的方法详解

JavaScript实现拖拽排序的方法详解

2022-05-12 15:52:50 来源:易采站长站 作者:

JavaScript实现拖拽排序的方法详解,砖头,属性,动画,鼠标,位置

JavaScript实现拖拽排序的方法详解

易采站长站,站长之家为您整理了JavaScript实现拖拽排序的方法详解的相关内容。

目录
实现原理概述代码实现完整代码实现

可拖拽排序的菜单效果大家想必都很熟悉,本次我们通过一个可拖拽排序的九宫格案例来演示其实现原理。 先看一下完成效果:

实现原理概述

拖拽原理

    当鼠标在【可拖拽小方块】(以下简称砖头)身上按下时,开始监听鼠标移动事件鼠标事件移动到什么位置,砖头就跟到什么位置鼠标抬起时,取消鼠标移动事件的监听

    排序原理

      提前定义好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]

      以上就是JavaScript实现拖拽排序的方法详解的详细内容,更多关于JavaScript拖拽排序的资料请关注易采站长站其它相关文章!

      以上就是关于对JavaScript实现拖拽排序的方法详解的详细介绍。欢迎大家对JavaScript实现拖拽排序的方法详解内容提出宝贵意见