JS 实现拖拽元素的功能
这篇笔记比较短,主要过一遍 draggable 的事件。
首先简单看一下 HTML 实现:
<!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>Document</title>
    <style>
      ul {
        list-style: none;
        border: 1px solid #333;
        padding: 0.5em;
        margin: 1em auto;
        max-width: 720px;
        width: 80%;
      }
      li {
        cursor: grab;
      }
      .card {
        border-radius: 10px;
        border: 1px solid #bbb;
        padding: 0.5em;
        margin: 0.8em;
        box-shadow: 1px 1px 0.5em #ccc;
      }
      .dragging {
        cursor: grabbing;
      }
      .droppable {
        background-color: #ffe0ec;
      }
    </style>
  </head>
  <body>
    <ul>
      <li id="p1" class="card" draggable="true">
        <h2>Title for p1</h2>
        <p>Paragraph body for p1</p>
      </li>
      <li id="p2" class="card" draggable="true">
        <h2>Title for p2</h2>
        <p>Paragraph body for p2</p>
      </li>
    </ul>
    <ul>
      <li id="p3" class="card" draggable="true">
        <h2>Title for p3</h2>
        <p>Paragraph body for p3</p>
      </li>
      <li id="p4" class="card" draggable="true">
        <h2>Title for p4</h2>
        <p>Paragraph body for p4</p>
      </li>
    </ul>
    <script src="./draggable.js"></script>
  </body>
</html>
效果如下:

这里简单的加了一点 CSS(list 和 card 的部分),其他抓取元素后会出现的悬浮效果全都是浏览器的实现,而实现抓取的功能也挺简单的,就是在想要抓取的元素上添加 draggable="true" 这一属性。
所以接下来要做的,就是绑定正确的事件,并且实现 draggable 中对应的事件。
首先修改一下 css:
<style>
  li {
    cursor: grab;
  }
</style>
这样鼠标在进入的时候就是抓取的样式,这样也提示用户当前元素可以被抓取:

随后是在 js 里面添加最初的设定,获取所有的 list item,并且绑定事件。绑定事件的部分会在后面一个个实现,所以现在先加一个 placeholder:
const listItems = document.querySelectorAll('li');
listItems.forEach((li) => {});
dragstart
这是一个单独的事件处理,所有的内容会被绑定到 dragstart 的事件下面:
// callback 可以拉出来单独做一个 function
li.addEventListener('dragstart', (e) => {
  // code here
});
dragstart 是开始抓取的这一部分,实现的功能包括:
-  更改鼠标光标现实正在抓取的状态 这一部分依旧通过 js 实现,通过 style这一属性改变cursor的状态 代码为: li.style.cursor = 'grabbing';或者通过 class 名称修改,这需要添加对应的 css: li.classList.add('dragging');
-  更新传输数据 这里使用的是 DataTransfer对象,根据 MDN 所说DataTransfer是在实现 drag 功能中,用来保存被拖拽的数据的一个对象。所以这里主要实现的有两个部分: - 传输当前被选中的 li 的 id
- 限定 drag 的操作只有 move
 e.dataTransfer.setData('text/plain', li.id); e.dataTransfer.effectAllowed = 'move';
drop
drop 需要实现 4 个部分:dragenter、 dragover, dragleave 和 drop 。另外,drop 的这个操作也是作用于 ul 之上,而不是 li,这部分的基础代码为:
// drop
const lists = document.querySelectorAll('ul');
lists.forEach((list) => {
  // code here
});
dragenter & dragover
这里实现的功能主要是当被拖拽的部分进入到可被 drop 的地方,那么可被 drop 的部分应该会有一个颜色改变的提示。
list.addEventListener('dragenter', (e) => {
  // check only accept the correct type of data
  if (e.dataTransfer.types[0] === 'text/plain') {
    e.preventDefault();
    list.classList.add('droppable');
  }
});
这里加了一个 check e.dataTransfer.types[0] === 'text/plain',主要也是因为在真实的案例中,很可能会有抓取一些 html、DOM 结点的操作,这里想要确认被拖拽的只是字符串部分。
如果想要实现 drop 的功能,dragover 是个必须要调用 preventDefault 去阻止默认功能的实现:
list.addEventListener('dragover', (e) => {
  // prevent default to allow drop
  if (e.dataTransfer.types[0] === 'text/plain') {
    e.preventDefault();
  }
});
实现后效果如下:

dragleave
当 item 进入可被 drop 区域时添加的 class,同样也需要在 item 离开该区域时被移除,这一部分就可以在 dragleave 中实现:
list.addEventListener('dragleave', (e) => {
  list.classList.remove('droppable');
});
这时候实现完了会有一个小麻烦,那就是当被拖拽的对象进入另一个子结点的时候,它也算离开了当前结点:

这一部分的修改具体还是需要依赖实现去完成,这里主要通过寻找最近的 ul,并与当前的 ul 进行判断,如果不是同一个的话就会进行删除:
list.addEventListener('dragleave', (e) => {
  if (e.relatedTarget.closest('ul') !== list)
    list.classList.remove('droppable');
});
drop
drop 的部分就需要获取被拉动的 id,判断当前 list 是否包含对应 id:
- 是的话停止继续执行
- 否的话先删除当前元素,并且在对应的 list 中添加被删除的元素
实现如下:
list.addEventListener('drop', (e) => {
  const id = e.dataTransfer.getData('text/plain');
  const listArr = Array.from(list.children);
  if (listArr.find((li) => li.id === id)) return;
  const listItem = document.querySelector(`#${id}`);
  listItem.remove();
  list.appendChild(listItem);
  list.classList.remove('droppable');
});
效果如下:

完整 JS 代码
// drag
const listItems = document.querySelectorAll('li');
const connectDrag = (e, li) => {
  li.classList.add('dragging');
  e.dataTransfer.setData('text/plain', li.id);
  e.dataTransfer.effectAllowed = 'move';
};
listItems.forEach((li) =>
  li.addEventListener('dragstart', (e) => connectDrag(e, li))
);
// drop
const lists = document.querySelectorAll('ul');
lists.forEach((list) => {
  list.addEventListener('dragenter', (e) => {
    // check only accept the correct type of data
    if (e.dataTransfer.types[0] === 'text/plain') {
      e.preventDefault();
      list.classList.add('droppable');
    }
  });
  list.addEventListener('dragleave', (e) => {
    if (e.relatedTarget.closest('ul') !== list)
      list.classList.remove('droppable');
  });
  list.addEventListener('dragover', (e) => {
    // prevent default to allow drop
    if (e.dataTransfer.types[0] === 'text/plain') {
      e.preventDefault();
    }
  });
  list.addEventListener('drop', (e) => {
    const id = e.dataTransfer.getData('text/plain');
    const listArr = Array.from(list.children);
    if (listArr.find((li) => li.id === id)) return;
    const listItem = document.querySelector(`#${id}`);
    listItem.remove();
    list.appendChild(listItem);
    list.classList.remove('droppable');
  });
});
reference
- HTMLElement: dragover event
- DataTransfer: setData() method
- DataTransfer: effectAllowed property















![[深度好文]10张图带你轻松理解关系型数据库系统的工作原理](https://img-blog.csdnimg.cn/img_convert/4653f1d568937685ea5f45c290949cac.jpeg)



