元素拖拽与元素移动

369 阅读4分钟

拖拽事件

dataset

  dataset 是 HTMLElement 接口的只读属性, dataset 提供了对元素上自定义数据属性(data-*)读/写访问。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>dataset</title>
  </head>
  <body>
    <div id="user" data-id="123456"></div>
    <script>
      window.onload = () => {
        const userEle = document.getElementById("user");
        if (userEle) {
          userEle.innerText = `元素的data-id是${userEle.dataset.id}`;
          userEle.dataset.id = 7890;
          let timeId = setTimeout(() => {
            userEle.innerText = `元素的data-id是${userEle.dataset.id}`;
            clearTimeout(timeId);
          }, 1000);
        }
      };
    </script>
  </body>
</html>

前置知识

为便于理解,以下顺序与元素被拖动-放置-修改位置触发事件顺序一致:

  draggable 是一种枚举属性,用于标识元素是否允许使用浏览器原生行为或 HTML 拖放操作 API 拖动。

  dragstart 事件 在用户开始拖动元素或被选择的文本时调用。

  dragover 事件 在可拖动的元素或者被选择的文本被拖进一个有效的放置目标时(每几百毫秒)触发(该事件在放置目标上触发)。

  drop 事件 在元素或文本选择被放置到有效的放置目标上时触发(该事件在放置目标上触发)。为确保 drop 事件始终按预期触发,应当在处理 dragover 事件的代码部分始终包含 preventDefault() 调用。

  dragend 事件 在拖放操作结束时触发(通过释放鼠标按钮或单击 escape 键,该事件无法取消)。

  mousedown 事件 在定点设备(如鼠标或触摸板)按钮在元素内按下时,会在该元素上触发。

  mousemove 事件 在定点设备(通常指鼠标)的光标在元素内移动时,会在该元素上触发。

  mousedown 事件 在定点设备(如鼠标或触摸板)按钮在元素内释放时,在该元素上触发。

拖动流程

元素结构如下:

<div class="top">
  <div id="left-container">
    <div class="draggable-item" draggable="true" data-icon="camera"></div>
  </div>
  <div id="right-container"></div>
</div>

1、元素开始拖动

  获取物料区所有的物料元素添加上对应的拖动事件监听

const draggableItems = document.querySelectorAll(".draggable-item");
draggableItems.forEach((item) => {
  item.addEventListener("dragstart", (e) => {
    // 设置拖拽数据
    e.dataTransfer.setData(
      "text/plain",
      JSON.stringify({
        offsetX: e.offsetX,
        offsetY: e.offsetY,
        icon: e.target.dataset.icon,
      })
    );
  });
});

2、元素拖动中

const dragover = (e) => {
  // 阻止默认行为,允许拖放
  e.preventDefault();
};

const drop = (e) => {
  // 阻止默认行为,允许拖放
  e.preventDefault();

  // 获取拖拽数据
  const data = JSON.parse(e.dataTransfer.getData("text/plain"));

  // 创建一个新的元素,将拖拽数据显示在其中
  const newItem = document.createElement("div");
  newItem.classList.add("dropped-item");
  newItem.style.top = e.offsetY - data.offsetY + "px";
  newItem.style.left = e.offsetX - data.offsetX + "px";

  // 将新元素添加到右侧容器中
  rightContainer.appendChild(newItem);
};

// 添加拖动时的事件监听
draggableItems.forEach((item) => {
  // 此处代码省略

  // 为右侧容器添加拖放事件处理程序
  rightContainer.addEventListener("dragover", dragover);
  rightContainer.addEventListener("drop", drop);
});

3、元素拖动结束

  为提高性能,在拖动结束时应该将监听事件给移除。

draggableItems.forEach((item) => {
  // 此处代码省略

  // 拖动结束
  item.addEventListener("dragend", (e) => {
    // 为右侧容器添加拖放事件处理程序
    rightContainer.removeEventListener("dragover", dragover);
    rightContainer.removeEventListener("drop", drop);
  });
});

移动内容区元素

  完成以上的代码,会发现元素只能从物料区拖动至内容区域,内容区域的位置不能进行二次移动,这是不符合常理的。以下为内容区域的元素拖动代码:

const drop = (e) => {
  // 此处代码省略

  // 鼠标按下
  newItem.addEventListener("mousedown", (e) => {
    e.preventDefault();
    e.stopPropagation();

    // 鼠标按下时 记录点击的位置
    let disx = e.pageX - newItem.offsetLeft;
    let disy = e.pageY - newItem.offsetTop;

    // 鼠标移动
    document.onmousemove = function (e) {
      let x = e.pageX - disx;
      let y = e.pageY - disy;

      let maxX =
        parseInt(window.getComputedStyle(rightContainer).width) -
        parseInt(window.getComputedStyle(newItem).width);
      let maxY =
        parseInt(window.getComputedStyle(rightContainer).height) -
        parseInt(window.getComputedStyle(newItem).height);

      if (x < 0) {
        x = 0;
      } else if (x >= maxX) {
        x = maxX;
      }

      if (y < 0) {
        y = 0;
      } else if (y > maxY) {
        y = maxY;
      }

      newItem.style.left = x + "px";
      newItem.style.top = y + "px";
    };

    // 鼠标抬起
    document.onmouseup = function () {
      document.onmousemove = document.onmouseup = null;
    };
  });

  // 将新元素添加到右侧容器中
  rightContainer.appendChild(newItem);
};

完整代码

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>元素拖拽与元素移动</title>
    <style>
      html,
      body {
        margin: 0;
        padding: 0;
        width: 100%;
        height: 100%;
      }

      body {
        display: flex;
        flex-direction: column;
      }

      .top {
        flex: 1 0 0;
        display: flex;
        overflow: hidden;
      }

      #left-container {
        position: relative;
        width: 200px;
        background-color: #ccc;
        padding: 40px;
      }

      #left-container::after {
        position: absolute;
        inset: 0;
        display: flex;
        justify-content: center;
        align-items: center;
        content: "物料区";
        font-size: 30px;
        writing-mode: vertical-rl;
        letter-spacing: 1em;
        opacity: 0.6;
        z-index: 0;
        pointer-events: none;
      }

      #right-container {
        flex: 1 0 0;
        position: relative;
        overflow: hidden;
      }

      #right-container::after {
        position: absolute;
        inset: 0;
        display: flex;
        justify-content: center;
        align-items: center;
        content: "内容区";
        font-size: 30px;
        letter-spacing: 1em;
        opacity: 0.3;
        z-index: 0;
        pointer-events: none;
      }

      .draggable-item {
        width: 20px;
        height: 20px;
        background-color: skyblue;
        border-radius: 25%;
        z-index: 1;
      }

      .dropped-item {
        position: absolute;
        width: 20px;
        height: 20px;
        background-color: skyblue;
        border-radius: 25%;
        cursor: move;
      }
    </style>
  </head>

  <body>
    <div class="top">
      <div id="left-container">
        <div class="draggable-item" draggable="true" data-icon="camera"></div>
      </div>
      <div id="right-container"></div>
    </div>

    <script>
      // 获取左右容器和所有可拖拽的物料
      const leftContainer = document.getElementById("left-container");
      const rightContainer = document.getElementById("right-container");
      const draggableItems = document.querySelectorAll(".draggable-item");

      const dragover = (e) => {
        // 阻止默认行为,允许拖放
        e.preventDefault();
      };

      const drop = (e) => {
        // 阻止默认行为,允许拖放
        e.preventDefault();

        // 获取拖拽数据
        const data = JSON.parse(e.dataTransfer.getData("text/plain"));

        // 创建一个新的元素,将拖拽数据显示在其中
        const newItem = document.createElement("div");
        newItem.classList.add("dropped-item");
        newItem.style.top = e.offsetY - data.offsetY + "px";
        newItem.style.left = e.offsetX - data.offsetX + "px";

        newItem.addEventListener("mousedown", (e) => {
          e.preventDefault();
          e.stopPropagation();
          // 鼠标按下时 记录点击的位置
          let disx = e.pageX - newItem.offsetLeft;
          let disy = e.pageY - newItem.offsetTop;

          // 鼠标移动
          document.onmousemove = function (e) {
            let x = e.pageX - disx;
            let y = e.pageY - disy;

            let maxX =
              parseInt(window.getComputedStyle(rightContainer).width) -
              parseInt(window.getComputedStyle(newItem).width);
            let maxY =
              parseInt(window.getComputedStyle(rightContainer).height) -
              parseInt(window.getComputedStyle(newItem).height);

            if (x < 0) {
              x = 0;
            } else if (x >= maxX) {
              x = maxX;
            }

            if (y < 0) {
              y = 0;
            } else if (y > maxY) {
              y = maxY;
            }

            newItem.style.left = x + "px";
            newItem.style.top = y + "px";
          };
          document.onmouseup = function () {
            document.onmousemove = document.onmouseup = null;
          };
        });

        // 将新元素添加到右侧容器中
        rightContainer.appendChild(newItem);
      };

      // 为可拖拽的物料添加拖放事件处理程序
      draggableItems.forEach((item) => {
        item.addEventListener("dragstart", (e) => {
          // 设置拖拽数据
          e.dataTransfer.setData(
            "text/plain",
            JSON.stringify({
              offsetX: e.offsetX,
              offsetY: e.offsetY,
              icon: e.target.dataset.icon,
            })
          );

          // 为右侧容器添加拖放事件处理程序
          rightContainer.addEventListener("dragover", dragover);
          rightContainer.addEventListener("drop", drop);
        });

        item.addEventListener("dragend", (e) => {
          // 为右侧容器添加拖放事件处理程序
          rightContainer.removeEventListener("dragover", dragover);
          rightContainer.removeEventListener("drop", drop);
        });
      });
    </script>
  </body>
</html>

对应源代码链接