移动端拖拽终极解决方案

212 阅读3分钟

前言:

你还在为移动端拖拽而烦恼?其实很简单,看看移动端拖拽终极解决方案!

实现思路:

使用touch事件进行用户交互,配合document.elementFromPoint()判断是否移入目标元素。

  1. touchstart事件获取被拖拽元素,并克隆该元素生成一个子元素,添加至拖拽面板或者根元素,使用绝对定位或者fixed定位又或者transform来实现拖拽跟随手指移动的交互交互效果。
  2. touchmove事件使用document.elementFromPoint()获取当前手指下方的dom元素,根据实际的布局情况以及业务需求,自行向上递归寻找是否有符合拖拽进入的目标元素,并做相应的处理。注意:document.elementFromPoint()获取的元素始终为屏幕最顶层元素,所以在使用前应当先隐藏当前触点下移动的元素。
  3. touchend事件对最后的触摸元素进行判断操作,完成相应业务。

主要实现原理:

  • 移动端touch事件 ----> 用户交互
  • document.elementFromPoint() ----> 判断是否移入目标元素

实现效果:

效果演示:

QQ202463-22038.gif

源代码:

<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>移动端拖拽实现</title>
    <style>
      * {
        margin: 0;
        padding: 0;
        box-sizing: border-box;
      }
      body {
        overflow: hidden;
      }
      .panel {
        width: 100vw;
        height: 100vh;
        background-color: lightblue;
        overflow: hidden;
        position: relative;
      }
      /* 容器盒子 */
      .container {
        display: flex;
        align-items: center;
        justify-content: center;
        gap: 20px;
        flex-wrap: wrap;
        padding: 20px;
      }
      /* 目标容器盒子 */
      .target-box {
        height: 120px;
        width: 120px;
        background-color: rgb(180, 164, 196);
        display: flex;
        justify-content: space-between;
        flex-wrap: wrap;
        gap: 10px;
        flex-shrink: 0;
      }

      /* 被拖拽元素盒子 */
      .drag-box {
        height: 200px;
        display: flex;
        justify-content: center;
        align-items: center;
        flex-wrap: wrap;
        gap: 20px;
        background-color: rgb(184, 210, 210);
      }
      /* 被拖拽元素 */
      .drag-item {
        width: 50px;
        height: 50px;
        background-color: antiquewhite;
        display: flex;
        align-items: center;
        justify-content: center;
        font-size: 40px;
        font-weight: bold;
        cursor: grab;
        user-select: none;
        flex-shrink: 0;
      }

      /* 拖拽至目标容器上方移动时容器样式 */
      .dragging-move {
        border: 2px solid black;
      }
    </style>
  </head>
  <body>
    <!-- 拖拽面板 -->
    <div class="panel" id="panel">
      <!-- 拖拽容器 -->
      <div class="container">
        <!-- 目标容器盒子 -->
        <div class="target-box"></div>
        <div class="target-box"></div>
        <div class="target-box"></div>
        <div class="target-box"></div>
      </div>
      <!-- 被拖拽容器盒子 -->
      <div class="drag-box">
        <!-- 被拖拽元素 -->
        <div class="drag-item" draggable="true">1</div>
        <div class="drag-item" draggable="true">2</div>
        <div class="drag-item" draggable="true">3</div>
        <div class="drag-item" draggable="true">4</div>
        <div class="drag-item" draggable="true">5</div>
        <div class="drag-item" draggable="true">6</div>
        <div class="drag-item" draggable="true">7</div>
        <div class="drag-item" draggable="true">8</div>
        <div class="drag-item" draggable="true">9</div>
        <div class="drag-item" draggable="true">10</div>
      </div>
    </div>
    <script>
      //拖拽进入的目标盒子的类名,也可用其他自定义属性
      let targetClass = "target-box";
      //获取被拖拽的元素
      let dragItems = document.querySelectorAll(".drag-item");
      //获取拖拽进入的目标容器盒子
      let targetBoxes = document.querySelectorAll(".target-box");
      //获取拖拽面板
      let panel = document.getElementById("panel");
      //正在移动的元素
      let movingNode = null;

      //移动元素的宽高
      let height = 0;
      let width = 0;
      //拖拽进入的目标盒子元素
      let target = null;

      //初始化绑定事件
      dragItems.forEach((item) => {
        //pc端 ---> drag事件
        item.addEventListener("dragstart", (e) => {
          movingNode = e.target;
        });
        item.addEventListener("dragend", () => {
          target.classList.remove("dragging-move");
        });

        //兼容移动端 ---> touch事件 + elementFromPoint
        item.addEventListener("touchstart", (e) => {
          //克隆原始被拖拽节点作为拖拽对象
          movingNode = e.target.cloneNode(true);
          //获取宽高用于设置移动时触点位于元素中心
          height = e.target.getBoundingClientRect().height;
          width = e.target.getBoundingClientRect().width;
          //设置绝对定位,实现跟随手指移动,也可使用 transform
          movingNode.style.position = "absolute";
          movingNode.style.top = "0px";
          movingNode.style.left = "0px";
          let { clientX, clientY } = e.changedTouches[0];
          movingNode.style.transform = `translateX(${
            clientX - width / 2
          }px) translateY(${clientY - height / 2}px)`;
          //添加克隆节点,跟随手指移动
          panel.appendChild(movingNode);
        });

        item.addEventListener("touchmove", (e) => {
          let { clientX, clientY } = e.changedTouches[0];
          movingNode.style.transform = `translateX(${
            clientX - width / 2
          }px) translateY(${clientY - height / 2}px)`;
          //清除目标容器盒子高亮样式
          targetBoxes.forEach((node) => {
            node.classList.remove("dragging-move");
          });
          //记录元素开始的display样式
          let display = movingNode.style.display;
          //暂时隐藏移动元素,方便获取当前手指下方的元素(tips:document.elementFromPoint()始终获取屏幕最顶层元素)
          movingNode.style.display = "none";
          //获取手指下方的元素,并判断是否为移入的目标容器盒子
          let targetEL = document.elementFromPoint(clientX, clientY);
          //这里只向上判断了当前手指下方的元素以及他的父元素,可以根据需要使用递归函数,递归有限次
          let parent = targetEL.parentNode;
          if (targetEL.classList.value.indexOf(targetClass) > -1) {
            //添加移入后盒子的高亮样式
            targetEL.classList.add("dragging-move");
          } else if (parent.classList.value.indexOf(targetClass) > -1) {
            //添加移入后盒子的高亮样式
            parent.classList.add("dragging-move");
          }
          //还原移动节点的display样式
          movingNode.style.display = display;
        });

        item.addEventListener("touchend", (e) => {
          let { clientX, clientY } = e.changedTouches[0];
          let display = movingNode.style.display;
          movingNode.style.display = "none";
          let targetEL = document.elementFromPoint(clientX, clientY);
          let parent = targetEL.parentNode;
          //判断是否满足加入条件,满足进行对应的操作
          if (targetEL.classList.value.indexOf(targetClass) > -1) {
            targetEL.classList.remove("dragging-move");
            let children = targetEL.childNodes;
            if (children.length >= 4) {
              target = null;
              return;
            }
            targetEL.appendChild(item);
          } else if (parent.classList.value.indexOf(targetClass) > -1) {
            parent.classList.remove("dragging-move");
            let children = parent.childNodes;
            if (children.length >= 4) {
              target = null;
              return;
            }
            parent.appendChild(item);
          }
          panel.removeChild(movingNode);
        });
      });
      //处理pc端的drag事件
      targetBoxes.forEach((item) => {
        item.addEventListener("dragenter", () => {
          target = item;
        });

        item.addEventListener("dragover", (e) => {
          e.preventDefault();
          targetBoxes.forEach((node) => {
            node.classList.remove("dragging-move");
          });
          item.classList.add("dragging-move");
        });
        item.addEventListener("drop", (e) => {
          if (!target) {
            return;
          }
          let children = target.childNodes;
          if (children.length >= 4) {
            target = null;
            return;
          }
          target.appendChild(movingNode);
        });
      });
    </script>
  </body>
</html>