手把手用JS撸一个拖放

1,017 阅读3分钟

前言

最近这些日子在做低代码相关的东西,拖放效果又是其中的关键交互,之前一直用得包装好的三方库,没有对拖放的 API 有过深入的了解,今天写一篇文章来聊聊其中的点,如果有任何的错误,欢迎大家指正。

了解拖放 API

首先打开mdn看看拖放相关的 API,相关的一共有七个事件:

dragdragstartdragenddragleavedragenterdragover
当拖拽元素或选中的文本时触发。当用户开始拖拽一个元素或选中的文本时触发当拖拽操作结束时触发 (比如松开鼠标按键或敲“Esc”键).当拖拽元素或选中的文本离开一个可释放目标时触发。当拖拽元素或选中的文本到一个可释放目标时触发当元素或选中的文本被拖到一个可释放目标上时触发(每 100 毫秒触发一次)。

光看文字也不知道每个事件啥时候触发,先写个 demo 来看看每个事件的触发时机。

拖放demo

<div class="dustbin">我是触发区域</div>
<div class="dragbox">
  <div class="draglist" title="拖拽我" draggable="true">列表1</div>
  <div class="draglist" title="拖拽我" draggable="true">列表2</div>
  <div class="draglist" title="拖拽我" draggable="true">列表3</div>
  <div class="draglist" title="拖拽我" draggable="true">列表4</div>
  <div class="draglist" title="拖拽我" draggable="true">列表5</div>
  <div class="draglist" title="拖拽我" draggable="true">列表6</div>
</div>
<style>
  .dustbin {
    background-color: skyblue;
    padding: 20px;
  }
</style>
<script>
  for (const ele of $(".draglist")) {
    ele.addEventListener("dragstart", (event) => {
      console.log("drag-start");
    });
    ele.addEventListener("dragend", (event) => {
      console.log("drag-end");
    });
  }
  
  $(".dustbin")[0].addEventListener("dragover", (event) => {
    console.log("drag-over");
    event.preventDefault();
  });
  $(".dustbin")[0].addEventListener("dragenter", function (event) {
    console.log("drag-enter");
    event.preventDefault();
    this.style.color = "#ffffff";
  });
  $(".dustbin")[0].addEventListener("drop", function (event) {
    console.log("drop");
  });
</script>

  1. 在点击并拖动“列表 6”的时候,触发了可拖动元素dragstart事件
  2. 在拖动“列表 6”元素时“我是触发区域”元素时,触发了可拖动区域元素dragenter事件
  3. 在拖动元素停留在可触发区域元素时,每 100ms 触发一次可拖动区域元素dragover事件
  4. 移出可触发区域元素时,触发可拖动区域元素drag-leave事件
  5. 在可触发区域放下拖动元素时,会触发可拖动区域元素drop事件
  6. 在可触发区域放下拖动元素时,会触发可拖动元素drag-end事件

这里有几个需要关注的点: 在 HTML 中,除了图像、链接和选择的默认行为外,默认情况下没有其他元素可拖动。 要使其他 HTML 元素可拖动,必须完成三件事:

  1. draggable将属性设置为"true"您希望使其可拖动的元素。
  2. dragstart事件添加一个监听器。
  3. 在上面的监听器中设置拖动数据

支持拖放并修改dom

这个demo会在拖动元素至“垃圾箱区域”时,会将这个元素从dom中移除,只是增加了一点点待代码。

<div class="dustbin">垃圾箱</div>
<div class="dragbox">
  <div class="draglist" title="拖拽我" draggable="true">列表1</div>
  <div class="draglist" title="拖拽我" draggable="true">列表2</div>
  <div class="draglist" title="拖拽我" draggable="true">列表3</div>
  <div class="draglist" title="拖拽我" draggable="true">列表4</div>
  <div class="draglist" title="拖拽我" draggable="true">列表5</div>
  <div class="draglist" title="拖拽我" draggable="true">列表6</div>
</div>
<div class="dragremind"></div>
<style>
  .dustbin {
    background-color: skyblue;
    padding: 20px;
  }
</style>
<script>
  // 整个全局变量,保存在拖动中的dom元素
  let eleDrag = null;
  for (const ele of $(".draglist")) {
    ele.addEventListener("dragstart", (event) => {
      console.log("drag-start");
      event.dataTransfer.dropEffect = "move";
      event.dataTransfer.effectAllowed = "move";
      // 拖动开始时,保存dom元素
      eleDrag = event.target;
    });

    ele.addEventListener("dragend", (event) => {
      console.log("drag-end");
      // 在拖动结束时,移除dom元素
      eleDrag = null;
    });
  }
  $(".dustbin")[0].addEventListener("dragover", (event) => {
    console.log("drag-over");
    event.preventDefault();
  });

  $(".dustbin")[0].addEventListener("dragenter", function (event) {
    console.log("drag-enter");
    event.preventDefault();
    this.style.color = "#ffffff";
  });

  $(".dustbin")[0].addEventListener("drop", function (event) {
    console.log("drop");
    // 在放下元素时,从dom中移除拖动的元素
    if (eleDrag) {
      eleDrag.parentNode.removeChild(eleDrag);
    }
  });
</script>

支持随意拖放

这个demo又稍微复杂了亿点点,添加了几行代码实现随意拖放。 其实就是在拖放结束时修改了拖放元素的top以及left值,看一下dragstart以及drop函数既就可以理解了。

<div class="container">
  <div class="dragbox">
    <div
      class="draglist"
      title="拖拽我"
      draggable="true"
      style="top: 0; left: 0"
>
      列表1
    </div>
    <div
      class="draglist"
      title="拖拽我"
      draggable="true"
      style="top: 50px; left: 50px"
>
      列表2
    </div>
    <div
      class="draglist"
      title="拖拽我"
      draggable="true"
      style="top: 100px; left: 150px"
>
      列表3
    </div>
    <div
      class="draglist"
      title="拖拽我"
      draggable="true"
      style="top: 90px; left: 190px"
>
      列表4
    </div>
    <div
      class="draglist"
      title="拖拽我"
      draggable="true"
      style="top: 10px; left: 90px"
>
      列表5
    </div>
    <div
      class="draglist"
      title="拖拽我"
      draggable="true"
      style="top: 70px; left: 167px"
>
      列表6
    </div>
  </div>
</div>
<style>
  .container {
    width: 1000px;
    height: 1000px;
    border: 1px solid gray;
    overflow: hidden;
  }
  .dragbox {
    position: relative;
  }
  .draglist {
    position: absolute;
    display: inline-block;
    width: 40px;
    height: 40px;
    border: 1px solid gray;
  }
</style>
<script>
  let targetEle = null;
  var store = {};
  for (const ele of $(".draglist")) {
    ele.addEventListener("dragstart", (event) => {
      event.dataTransfer.dropEffect = "move";
      event.dataTransfer.effectAllowed = "move";
      store.x = event.pageX;
      store.y = event.pageY;
      store.top = parseFloat(getComputedStyle(event.target).top) || 0;
      store.left = parseFloat(getComputedStyle(event.target).left) || 0;

      targetEle = event.target;
    });

    ele.addEventListener("dragend", (event) => {
      targetEle = null;
    });
  }
  $(".container")[0].addEventListener("dragover", (event) => {
    event.preventDefault();
  });

  $(".container")[0].addEventListener("dragenter", function (event) {
    event.preventDefault();
  });

  $(".container")[0].addEventListener("drop", function (event) {
    if (targetEle) {
      var containerReact = targetEle.getBoundingClientRect();
      var distanceX = event.pageX - store.x;
      var distanceY = event.pageY - store.y;

      var top = store.top + distanceY;
      var left = store.left + distanceX;

      targetEle.style.top = top + "px";
      targetEle.style.left = left + "px";
    }
  });
</script>