抛开插件,你真的懂拖动怎么实现吗?

大厂技术  高级前端  Node进阶
点击上方 程序员成长指北,关注公众号
回复1,加入高级Node交流群

写在开头

各位友友好吖❗

五一已过,下个期待的假期就是端午了,小长假即将到来,假期行程已经可以开始规划囖。😉

不过,对于是大小周的小编来说,整个四月都将是单休,瞬间开心减一半。。。

近来,小编断断续续抽空看了一部名叫 国王排名[1] 动漫,还挺好看的。😁

还把几年不换的微信头像换成了主角的头像,Em......从"蜡笔小新"变成了"波吉王子"。

2036bc3058c263efebfa2a4505e502e5.jpeg018268fcae7a8e4468f135ca8bc7496f.jpeg87aa8b58e1178bd2366d5edd02016ffa.jpeg

新头像,新心情,愉悦。😎

回到正题,本章将分享一些关于 Javascript 中拖动的内容,探索拖动过程的奥秘。👏

元素拖动

刚开始,咱们循序渐进,先来实现一个最简单的功能,让一个元素变成可拖动元素。

布局与样式:

<!DOCTYPE html>
<html>
<head>
  <title>元素拖动</title>
  <style>
    #drag {
      width: 100px;
      height: 100px;
      border: 1px solid #cbd5e0;
      display: flex;
      justify-content: center;
      align-items: center;
      cursor: move;
      user-select: none;
      position: absolute;
    }
  </style>
</head>
<body>
  <div id="drag">橙某人</div>
</body>

要让一个元素可拖动,我们需要处理三个事件:

  • mousedown[2]:按下。

  • mousemove[3]:移动。

  • mouseup[4]:释放。

三者都是老演员了,相信每个前端人都识得啦😋,具体详情可以点链接上MDN查阅。

具体逻辑过程:

<script>
  document.addEventListener('DOMContentLoaded', () => {
    const drag = document.getElementById('drag');
    // 添加鼠标按下事件
    drag.addEventListener('mousedown', mouseDownHandler);
    // 记录鼠标坐标信息
    let x = 0;
    let y = 0;
    // 鼠标按下事件
    function mouseDownHandler(e) {
      // 记录鼠标初始位置
      x = e.clientX;
      y = e.clientY;
      // 添加鼠标移动与释放事件
      document.addEventListener('mousemove', mouseMoveHandler);
      document.addEventListener('mouseup', mouseUpHandler);
    };
    // 鼠标移动事件
    function mouseMoveHandler(e) {
      // 计算鼠标拖动的距离
      const dx = e.clientX - x;
      const dy = e.clientY - y;
      // 将拖动距离赋值给目标元素
      drag.style.top = `${drag.offsetTop + dy}px`;
      drag.style.left = `${drag.offsetLeft + dx}px`;
      // 不断记录鼠标上一个位置
      x = e.clientX;
      y = e.clientY;
    };
    // 鼠标释放事件
    function mouseUpHandler() {
      // 重置相关变量
      x = 0;
      y = 0;
      // 移除事件
      document.removeEventListener('mousemove', mouseMoveHandler);
      document.removeEventListener('mouseup', mouseUpHandler);
    };
  });
</script>

offsetTop[5]:元素到 offsetParent 顶部的距离。

offsetParent[6]:距离元素最近的一个具有定位的祖宗元素(relative,absolute,fixed),若祖宗都不符合条件,offsetParentbody

效果:

486408c6960c856a66f98ec45bc183e9.gif

大致原理过程:计算鼠标拖动的横向与纵向距离,再结合元素本身的 offsetTopoffsetLeft 信息,就能实现元素的拖动。

没几行代码,应该不难哈,都写上详细的注释啦。

上面用 clientX/Y 获取鼠标的位置信息,那用 pageX/Y 可以不呢❓

clientX:提供了鼠标指针相对于浏览器视口(即当前可见的页面部分)左上角的水平坐标。不论页面是否滚动,clientX 的值都是相对于视口的。

pageX:提供了鼠标指针相对于整个页面左上角的水平坐标,包括了任何由于滚动而不可见的部分。当你滚动页面时,pageX 的值会改变,因为它考虑了滚动的距离。

简而言之,就是要不要考虑滚动条的问题,如果你想要获取鼠标指针相对于整个页面的位置,应该使用 pageX。如果你只关心鼠标指针在当前视口内的位置,那么 clientX 就足够了。

网上找了个图,可以瞧瞧:c3403c53447aa203e76720b0ee40c56e.jpeg

列表拖动

简单的整完,咱们开始上点强度💀,这次要做的功能如下:

8344528a349517b993faa8fb2f7ad36c.gif

04092.gif

看着可能稍微有点复杂,但实际还好啦,我们一步一步来完成。

一样,先把布局与样式整一下:

<!DOCTYPE html>
<html>
<head>
  <title>列表拖动</title>
  <style>
    .item {
      width: 200px;
      height: 60px;
      border: 1px solid #cbd5e0;
      display: flex;
      justify-content: center;
      align-items: center;
      cursor: move;
      user-select: none;
      margin: 10px 0;
      box-sizing: border-box;
    }
  </style>
</head>
<body>
  <div id="list">
    <div class="item">第一个元素</div>
    <div class="item">第二个元素</div>
    <div class="item">第三个元素</div>
    <div class="item">第四个元素</div>
    <div class="item">第五个元素</div>
  </div>
</body>

接下来,让每个元素变成可拖动元素。

<script>
  document.addEventListener('DOMContentLoaded', () => {
    const list = document.getElementById('list');
    list.querySelectorAll('.item').forEach(item => {
      // 批量添加事件
      item.addEventListener('mousedown', mouseDownHandler);
    });
    // 记录鼠标在拖动元素上的位置信息
    let x = 0;
    let y = 0;
    // 记录当前的拖动元素
    let draggingElement;
    
    function mouseDownHandler(e) {
      // 记录拖动元素
      draggingElement = e.target;
      // 计算鼠标在拖动元素上的位置信息
      const rect = draggingElement.getBoundingClientRect();
      x = e.clientX - rect.left;
      y = e.clientY - rect.top;
      document.addEventListener('mousemove', mouseMoveHandler);
      document.addEventListener('mouseup', mouseUpHandler);
    };
    function mouseMoveHandler(e) {
      // 计算拖动元素的最新位置
      const left = e.clientX - x;
      const top = e.clientY - y;
      // 将移动距离赋值给目标元素
      draggingElement.style.position = 'absolute';
      draggingElement.style.top = `${top}px`;
      draggingElement.style.left = `${left}px`;
    };
    function mouseUpHandler() {
      // 布局恢复原样(列表布局肯定一直是一个样啦)
      draggingElement.style.removeProperty('top');
      draggingElement.style.removeProperty('left');
      draggingElement.style.removeProperty('position');
      x = 0;
      y = 0;
      draggingElement = null;
      document.removeEventListener('mousemove', mouseMoveHandler);
      document.removeEventListener('mouseup', mouseUpHandler);
    }
  });
</script>

与前面讲的差不多,熟悉的配方。😗

稍微有一点区别是,将元素变成可拖动的逻辑与前面讲的不太一致了。😓

这里用上了 getBoundingClientRect[7] API,其作用是为了优化前面在 mouseMoveHandler 函数中,需要不断去记录鼠标上一个位置的繁琐过程。

大概二者的区别如下:

1️⃣ 拖动元素的位置 = 拖动元素原本位置 + 拖动距离

2️⃣ 拖动元素的位置 = 根据鼠标最新位置直接计算拖动元素的最新位置 = 鼠标最新位置 - 鼠标在拖动元素上的距离

鼠标在拖动元素上的距离:

9f46f4122ce164a7b34f4967a749d451.jpeg
image.png

效果如下:

99a1449d7f877f306a5b3b18f8137c23.gif

04093.gif

现在每个元素都能拖动了,只是还没有加上交换的逻辑。

但,这看着是不是有点奇怪?当拖动一个元素,列表下面的元素就顶上来了,这与咱们一开始看到的效果不太一致吖❗

这是因为缺少了一个占位元素,当在拖动元素时,需要自动插入一个占位元素,保持列表布局不会变化,拖动交换元素时,也应该是占位元素与其他元素进行交换,拖动结束时,再将占位元素给删除,将位置让给拖动元素。

下面,我们来看看如何解决这个占位元素的问题。

<!DOCTYPE html>
<html>
<head>
  <style>
    ...
    /* 占位元素样式 */
    .placeholder {
      box-sizing: border-box;
      background-color: #edf2f7;
      margin: 10px 0;
      border: 2px dashed #cbd5e0;
    }
  </style>
</head>

<script>
document.addEventListener('DOMContentLoaded', () => {
  ...
  let draggingElement;
  // 站位元素
  let placeholder;
  // 是否正在拖动中
  let isDraggingStarted = false;
  
  function mouseMoveHandler(e) {
    const draggingRect = draggingElement.getBoundingClientRect();
    // 仅在移动中初次创建一次
    if (!isDraggingStarted) {
      isDraggingStarted = true;
      // 创建占位元素
      placeholder = document.createElement('div');
      // 给占位元素添加class
      placeholder.classList.add('placeholder');
      // 插入占位元素
      draggingElement.parentNode.insertBefore(placeholder, draggingElement.nextSibling);
      // 保持占位元素与拖动元素一样大小
      placeholder.style.width = draggingRect.width + 'px';
      placeholder.style.height = draggingRect.height + 'px';
    }
    
    const left = e.clientX - x;
    ...
  };
  function mouseUpHandler() {
    // 拖动结束清除占位元素
    placeholder && placeholder.parentNode.removeChild(placeholder);
    isDraggingStarted = false;
    
    draggingElement.style.removeProperty('top');
    ...
  };
});
</script>

加了大概十行代码,都是比较基础的DOM操作。👀

瞧瞧效果:

c2497571cf24a962fd1e33bd9c7689e9.gif
04101.gif

有那味了。🤡

最后,咱们差一步了,就是根据拖动方向进行元素之间的交换。

看到"拖动方向"加粗没😯?这是关键点,我们要如何知道拖动元素是往上还是往下呢❓并且交换元素位置的时机如何把握呢❓

看如下图,假设了中间三个元素的中心点坐标分别如下图。

dbc393e770898e541cee32dea29cb893.jpeg
image.png

当我们拖动第三个元素,往上,第三个元素的中心点坐标会不断变化,可能会变成(10,19)、(10,18)、(10,17)、......。

当继续往上拖动,第三个元素中心点坐标变成(10,9)时,它的纵坐标比第二个元素中心点纵坐标小了,这个时候就需要交换位置❗

根据这个原理过程,咱们先来写一个判断拖动方向的函数,如下:

// 检测拖动元素是否向上拖动
function isAbove(nodeA, nodeB) {
  const rectA = nodeA.getBoundingClientRect();
  const rectB = nodeB.getBoundingClientRect();
  // 计算中心点纵坐标
  const centerPointA = rectA.top + rectA.height / 2;
  const centerPointB = rectB.top + rectB.height / 2;
  return centerPointA < centerPointB;
};

应该很好理解吧?🌟

交换元素的过程,咱们也可以单独写一个函数,如下:

// 交换两个相邻的元素位置
function swap(nodeA, nodeB) {
  // 获取父节点,为后续插入提供一个支点
  const parentA = nodeA.parentNode;
  // 获取参照节点
  const siblingA = nodeA.nextSibling === nodeB ? nodeA : nodeA.nextSibling;
  // 将A节点移动到参照节点之前
  nodeB.parentNode.insertBefore(nodeA, nodeB);
  // 将B节点移动到参照节点之前
  parentA.insertBefore(nodeB, siblingA);
};

虽然只有短短四行代码,但这个时候就非常考验你的 Javascript 基础了。

(当然,这种东西你也可以直接问GPT,捡现成的。👻)

最后,结合具体逻辑:

function mouseMoveHandler() {
  ...
  
  // 获取拖动时的上下元素
  const prevEle = draggingElement.previousElementSibling;
  const nextEle = placeholder.nextElementSibling;
  // 向上移动(没上一个元素了,代表到边界了,不用处理)
  if (prevEle && isAbove(draggingElement, prevEle)) {
    // 占位元素要先与拖动元素交换位置❗
    swap(placeholder, draggingElement);
    // 占位元素与上一个元素交换位置
    swap(placeholder, prevEle);
    return;
  }
  // 向下移动
  if (nextEle && !isAbove(draggingElement, nextEle)) {
    swap(nextEle, placeholder);
    swap(nextEle, draggingElement);
  }
}

我们仅需在 mouseMoveHandler 函数中添加交换逻辑即可。

这里要注意"占位元素要先与拖动元素交换位置",可能你会有疑问?不是直接交换占位元素与上一个元素(或下一个元素)就行咩?😢

我们可以看看实际的DOM结构,第二个元素与占位元素中间还隔着拖动元素呢,注意我们是要交换两个相邻的元素,不是随便两个相隔遥远的元素哦。

6a138b4d0bfaa32ef5c187bff3969bb5.jpeg
image.png

好,到此完毕,列表拖动就完成啦。👻

表格拖动-列

接下来要做的是表格上的拖动,也是比较常见的功能了,话不多说,先看效果图:

5efe71fa27c7f4efc13ab4e685952e3d.gif
04102.gif

做之前咱们先来分析一波,由于我们要拖动的是列,是竖着纵向排列的,而表格可是按照横向进行布局的❗

表格的布局结构:

<table id="table" class="table">
  <thead>
    <tr>
      <th>序号</th>
      <th>日期</th>
      <th>姓名</th>
      <th>省份</th>
      <th>城市</th>
      <th>地址</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>1</td>
      <td>2024-04-01</td>
      <td>小小红</td>
      <td>广东</td>
      <td>广州市</td>
      <td>广东省广州市xxxxxx</td>
    </tr>
    <tr>
      <td>2</td>
      <td>2024-04-02</td>
      <td>小小绿</td>
      <td>广东</td>
      <td>深圳市</td>
      <td>广东省深圳市xxxxxx</td>
    </tr>
    <tr>
      <td>3</td>
      <td>2024-04-03</td>
      <td>小小黄</td>
      <td>广东</td>
      <td>中山市</td>
      <td>广东省中山市xxxxx</td>
    </tr>
    <tr>
      <td>4</td>
      <td>2024-04-04</td>
      <td>小小白</td>
      <td>广东</td>
      <td>佛山市</td>
      <td>广东省佛山市xxxx</td>
    </tr>
    <tr>
      <td>5</td>
      <td>2024-04-05</td>
      <td>小小黑</td>
      <td>广东</td>
      <td>汕头市</td>
      <td>广东省汕头市xxxxxx</td>
    </tr>
  </tbody>
</table>
31fa68ec71350cd45305c821b777a5aa.jpeg
image.png

这...可不利于我们拖动处理呀❗我们要拖动的列必须是一个整体、是整个块,要是跨元素就麻烦了。

🌟这里咱们就要换个思路了,在要开始拖动时,动态创建一个纵向的列表,列表的每一子项就是表格的列,其实就是将表格转成我们上面已经讲过的列表拖动来进行操作;然后隐藏原表格,操作这个新列表,当拖动结束的时候,我们再通过列表的索引信息来交换表格的格子就行啦,是不是手拿把掐。😁(注意是拖动列表的项!!!)

那咱们先来看看如何动态创建出这个列表叭。😉

相关 HTML 结构就是上面那个表格布局了,没了。

相关 CSS 样式:

<!DOCTYPE html>
<html>
<head>
  <style>
    .table {
      border: 1px solid #ccc;
      border-collapse: collapse;
    }
    .table th,
    .table td {
      border: 1px solid #ccc;
    }
    .table th,
    .table td {
      padding: 10px;
      text-align: center;
      box-sizing: border-box;
    }
    .draggable {
      cursor: move;
      user-select: none;
    }
    .list {
      border-left: 1px solid #ccc;
      border-top: 1px solid #ccc;
      display: flex;
    }
    .list__table {
      border-collapse: collapse;
      border: none;
    }
    .list__table th,
    .list__table td {
      border: 1px solid #ccc;
      border-left: none;
      border-top: none;
      box-sizing: border-box;
      padding: 10px;
      text-align: center;
      color: red; /* 为了演示,加个红色 */
    }
    .placeholder {
      background-color: #edf2f7;
      border: 2px dashed #cbd5e0;
      box-sizing: border-box;
    }
    .dragging {
      background: #fff;
      border-left: 1px solid #ccc;
      border-top: 1px solid #ccc;
      z-index: 999;
    }
  </style>
</head>

上面列出了所有样式,对照着看看就行,样式不是重点,但是其中值得关注的是动态创建的列表,它的 border 是如何变成和表格的一样?因为稍微缺失一个格子的边框,可能就会造成列表和原表格不重合,容易露馅,这点你可以仔细琢磨一下。

<script>
document.addEventListener('DOMContentLoaded', () => {
  const table = document.getElementById('table');
  table.querySelectorAll('th').forEach(headerCell => {
    // 给可拖动元素添加一个样式类
    headerCell.classList.add('draggable');
    headerCell.addEventListener('mousedown', mouseDownHandler);
  });
  
  let draggingElement;
  // 记录拖动列的索引
  let draggingColumnIndex;
  let x = 0;
  let y = 0;
  let isDraggingStarted = false;
  
  function mouseDownHandler(e) {
    // 找到拖动列的索引
    draggingColumnIndex = [].slice.call(table.querySelectorAll('th')).indexOf(e.target);
    x = e.clientX - e.target.offsetLeft;
    y = e.clientY - e.target.offsetTop;
    document.addEventListener('mousemove', mouseMoveHandler);
    document.addEventListener('mouseup', mouseUpHandler);
  };
  function mouseMoveHandler(e) {
    if (!isDraggingStarted) {
      isDraggingStarted = true;
      // 动态创建列表
      createList();
    }
  }
  // 创建一个表格列表
  function createList() {
    const rect = table.getBoundingClientRect();
    list = document.createElement('div');
    list.classList.add('list');
    // 覆盖在表格上,如果是在局部,需要在共同的父元素上加上relative,小编这里父元素是body,就不用了。
    list.style.position = 'absolute';
    list.style.left = rect.left + 'px';
    list.style.top = rect.top + 'px';
    // 列表插入文档中
    table.parentNode.insertBefore(list, table);
    // 隐藏原表格
    table.style.visibility = 'hidden';
    // 获取表格所有的格子
    const originalCells = [].slice.call(table.querySelectorAll('tbody td'));
    // 获取表格的表头格子
    const originalHeaderCells = [].slice.call(table.querySelectorAll('th'));
    const numColumns = originalHeaderCells.length;
    // 循环列
    originalHeaderCells.forEach((headerCell, headerIndex) => {
      const { width } = window.getComputedStyle(headerCell);
      // 创建列表子项,也就是新的列
      const item = document.createElement('div');
      item.classList.add('draggable');
      // 子项是一个只有单列的表格,这就是上面样式中提到的列表的border如何保持和表格的边框一样的技巧
      const newTable = document.createElement('table');
      newTable.setAttribute('class', 'list__table');
      newTable.style.width = width;
      // 子项表格的表头
      const th = headerCell.cloneNode(true);
      let newRow = document.createElement('tr');
      newRow.appendChild(th);
      newTable.appendChild(newRow);
      // 子项表格的数据,在所有格子中找到属于这一列的格子
      const cells = originalCells.filter((c, idx) => {
        // 代入几个格子索引算算就清楚啦😉
        return (idx - headerIndex) % numColumns === 0;
      });
      // 找到这列的格子后,给格子加上对应列的宽度,再把它们包装成一个行tr,再插入就可以了
      cells.forEach(cell => {
        const newCell = cell.cloneNode(true);
        newCell.style.width = width + 'px';
        newRow = document.createElement('tr');
        newRow.appendChild(newCell);
        newTable.appendChild(newRow);
      });
      // 把子项表格追加到新列中,再把新列追加到列表中,完事
      item.appendChild(newTable);
      list.appendChild(item);
    });
  }
  function mouseUpHandler() {
    document.removeEventListener('mousemove', mouseMoveHandler);
    document.removeEventListener('mouseup', mouseUpHandler);
  }
});
</script>

很熟悉吧,代码中很多都是前面讲过的了。

重点只有 createList 函数,它的作用就是创建一个与表格一样的列表,外观是一致的,只是与表格不同的是,它的布局是纵向的,就这么简单,详细的可以瞧瞧代码过程。👻

46cc6d29334fbc3ad37d02fc55149b94.jpeg
image.png

现在列表有了,操作列表的拖动这块咱熟呀,直接整上。

仅需改动 mouseMoveHandler 函数:

function mouseMoveHandler(e) {
  if (!isDraggingStarted) {
    isDraggingStarted = true;
    createList();
    // 通过索引获取拖动元素
    draggingElement = [].slice.call(list.children)[draggingColumnIndex];
    draggingElement.classList.add('dragging');
    // 继续创建占位元素
    placeholder = document.createElement('div');
    placeholder.classList.add('placeholder');
    draggingElement.parentNode.insertBefore(placeholder, draggingElement.nextSibling);
    // 因为是flex布局,不用设置高度也可以
    placeholder.style.width = draggingElement.offsetWidth + 'px';
  }

  // 和元素拖动的过程一样
  draggingElement.style.position = 'absolute';
  draggingElement.style.top = (draggingElement.offsetTop + e.clientY - y) + 'px';
  draggingElement.style.left = (draggingElement.offsetLeft + e.clientX - x) + 'px';
  x = e.clientX;
  y = e.clientY;

  // 交换元素,与列表拖动的一样
  const prevEle = draggingElement.previousElementSibling;
  const nextEle = placeholder.nextElementSibling;
  if (prevEle && isOnLeft(draggingElement, prevEle)) {
    swap(placeholder, draggingElement);
    swap(placeholder, prevEle);
    return;
  }
  if (nextEle && isOnLeft(nextEle, draggingElement)) { // 元素换个位置而已,用!取反也是一样的
    swap(nextEle, placeholder);
    swap(nextEle, draggingElement);
  }
}
function swap(nodeA, nodeB) {
  const parentA = nodeA.parentNode;
  const siblingA = nodeA.nextSibling === nodeB ? nodeA : nodeA.nextSibling;
  nodeB.parentNode.insertBefore(nodeA, nodeB);
  parentA.insertBefore(nodeB, siblingA);
};
function isOnLeft(nodeA, nodeB) {
  const rectA = nodeA.getBoundingClientRect();
  const rectB = nodeB.getBoundingClientRect();
  const centerPointA = rectA.left + rectA.width / 2;
  const centerPointB = rectB.left + rectB.width / 2;
  return centerPointA < centerPointB;
};

新增加的 swapisOnLeft 函数在列表拖动的时候都讲过啦,这里就不多说了。

做到这里,你的表格(列表)应该是可以正常拖动了,只是拖动后的效果还能不真正同步到表格中而已,差最后一步,咱也给它加上、加上。😀

咱们仅需要改动 mouseUpHandler 函数,在拖动结束的时候将列表子项的索引信息同步回原表格上,然后把列表移除就可以了。

具体如下:

function mouseUpHandler() {
  // 移除占位元素
  placeholder && placeholder.parentNode.removeChild(placeholder);
  // 恢复拖动元素样式
  draggingElement.classList.remove('dragging');
  draggingElement.style.removeProperty('top');
  draggingElement.style.removeProperty('left');
  draggingElement.style.removeProperty('position');
  // 获取拖动元素最后的索引
  const endColumnIndex = [].slice.call(list.children).indexOf(draggingElement);
  isDraggingStarted = false;
  // 移除创建的列表
  list.parentNode.removeChild(list);
  // 将列表的信息同步到原表格中
  table.querySelectorAll('tr').forEach(row => {
    // 获取每一行的格子
    const cells = [].slice.call(row.querySelectorAll('th, td'));
    if (draggingColumnIndex > endColumnIndex) { // 往左拖动
      // 将目标格子(cells[draggingColumnIndex])放到最新的位置上
      cells[endColumnIndex].parentNode.insertBefore(
        cells[draggingColumnIndex], cells[endColumnIndex]);    
    }else { // 往右拖动
      cells[endColumnIndex].parentNode.insertBefore(
        cells[draggingColumnIndex], cells[endColumnIndex].nextSibling);
    }
  });
  // 恢复原表格的展示
  table.style.removeProperty('visibility');
  document.removeEventListener('mousemove', mouseMoveHandler);
  document.removeEventListener('mouseup', mouseUpHandler);
};

应该不是很难哈,都写上了详细的注释。

好啦,就这么多,到此,咱们就完成了开头看到的表格列拖动的效果了。👻👻👻

表格拖动-行

既然讲了表格的列拖动了,那么行的拖动肯定也是不能落下啦。😁

不过现在我们有了前面的基础,这个不是洒洒水?有手就行?

HTML 结构不变。

CSS 样式略微调整一下:

...
.list {
  /* border-left: 1px solid #ccc; */
  border-top: 1px solid #ccc;
  /* display: flex; */
}
.list__table th,
.list__table td {
  border: 1px solid #ccc;
  /* border-left: none; */
  border-top: none;
  box-sizing: border-box;
  padding: 10px;
  text-align: center;
  color: red;
}
...

主要还是 JS 逻辑部分:

<script>
document.addEventListener('DOMContentLoaded', () => {
  const table = document.getElementById('table');
  table.querySelectorAll('tr').forEach((row, index) => {
    // 表格不能拖动,跳过
    if (index === 0) return;
    // 第一列的第一个格子才能拖动
    const firstCell = row.firstElementChild;
    firstCell.classList.add('draggable');
    firstCell.addEventListener('mousedown', mouseDownHandler);
  });

  // 记录拖动的行索引
  let draggingColumnIndex;
  let x = 0;
  let y = 0;
  let isDraggingStarted = false;
  let list;
  let draggingElement;
  let placeholder;
  
  function mouseDownHandler(e) {
    const originalRow = e.target.parentNode;
    draggingRowIndex = [].slice.call(table.querySelectorAll('tr')).indexOf(originalRow);
    x = e.clientX;
    y = e.clientY;
    document.addEventListener('mousemove', mouseMoveHandler);
    document.addEventListener('mouseup', mouseUpHandler);
  };
  function mouseMoveHandler(e) {
    if (!isDraggingStarted) {
      isDraggingStarted = true;
      createList();
      draggingElement = [].slice.call(list.children)[draggingRowIndex];
      draggingElement.classList.add('dragging');
      placeholder = document.createElement('div');
      placeholder.classList.add('placeholder');
      draggingElement.parentNode.insertBefore(placeholder, draggingElement.nextSibling);
      placeholder.style.height = draggingElement.offsetHeight + 'px';
    }
    draggingElement.style.position = 'absolute';
    draggingElement.style.top = (draggingElement.offsetTop + e.clientY - y) + 'px';
    draggingElement.style.left = (draggingElement.offsetLeft + e.clientX - x) + 'px';
    x = e.clientX;
    y = e.clientY;
    const prevEle = draggingElement.previousElementSibling;
    const nextEle = placeholder.nextElementSibling;
    if (prevEle && prevEle.previousElementSibling && isAbove(draggingElement, prevEle)) {
      swap(placeholder, draggingElement);
      swap(placeholder, prevEle);
      return;
    }
    if (nextEle && isAbove(nextEle, draggingElement)) {
      swap(nextEle, placeholder);
      swap(nextEle, draggingElement);
    }
  };
  function createList() {
    const rect = table.getBoundingClientRect();
    const width = window.getComputedStyle(table).width;
    list = document.createElement('div');
    list.classList.add('list');
    list.style.position = 'absolute';
    list.style.left = rect.left + 'px';
    list.style.top = rect.top + 'px';
    table.parentNode.insertBefore(list, table);
    table.style.visibility = 'hidden';
    // 循环行
    table.querySelectorAll('tr').forEach(row => {
      const item = document.createElement('div');
      item.classList.add('draggable');
      // 子项是一个只有一行的表格,这就是上面样式中提到的列表的border如何保持和表格的边框一样的技巧
      const newTable = document.createElement('table');
      newTable.setAttribute('class', 'list__table');
      newTable.style.width = width;
      const newRow = document.createElement('tr');
      const cells = [].slice.call(row.children);
      cells.forEach(cell => {
        const newCell = cell.cloneNode(true);
        // 每个格子还是原来格子的宽度
        newCell.style.width = window.getComputedStyle(cell).width;
        newRow.appendChild(newCell);
      });
      newTable.appendChild(newRow);
      item.appendChild(newTable);
      list.appendChild(item);
    });
  };
  function swap(nodeA, nodeB) {
    // ... 一样的,不写了
  };
  function isAbove(nodeA, nodeB) {
    const rectA = nodeA.getBoundingClientRect();
    const rectB = nodeB.getBoundingClientRect();
    const centerPointA = rectA.top + rectA.height / 2;
    const centerPointB = rectB.top + rectB.height / 2;
    return centerPointA < centerPointB;
  };
  function mouseUpHandler() {
    placeholder && placeholder.parentNode.removeChild(placeholder);
    draggingElement.classList.remove('dragging');
    draggingElement.style.removeProperty('top');
    draggingElement.style.removeProperty('left');
    draggingElement.style.removeProperty('position');
    const endRowIndex = [].slice.call(list.children).indexOf(draggingElement);
    isDraggingStarted = false;
    list.parentNode.removeChild(list);
    let rows = [].slice.call(table.querySelectorAll('tr'));
    // 行的交换就简单了,直接整行换就行了
    if (draggingRowIndex > endRowIndex) {
       rows[endRowIndex].parentNode.insertBefore(
         rows[draggingRowIndex], 
         rows[endRowIndex]
       );
    }else {
      rows[endRowIndex].parentNode.insertBefore(
        rows[draggingRowIndex],
        rows[endRowIndex].nextSibling
      );
    }
    table.style.removeProperty('visibility');
    document.removeEventListener('mousemove', mouseMoveHandler);
    document.removeEventListener('mouseup', mouseUpHandler);
  };
});
</script>

大部分还是与上面讲过的一样,有一些略微差别而已,需要注意方向相关的就可以了。

还有就是动态创建的列表变成如下的样子了:

3cb9862e19c4bc343bbbe6ae915e48d6.jpeg
image.png

最后,放个效果图:

fc04ce00772d038b021f615e3f813d6a.gif
04111.gif

完整源码

传送门[8] :https://gitee.com/ydydydq/dragging

链接:https://juejin.cn/post/7356326955930107904 

作者:橙某人

Node 社群

 
 

我组建了一个氛围特别好的 Node.js 社群,里面有很多 Node.js小伙伴,如果你对Node.js学习感兴趣的话(后续有计划也可以),我们可以一起进行Node.js相关的交流、学习、共建。下方加 考拉 好友回复「Node」即可。

3ed0d1d1852ed7577bd925023b96837d.png

“分享、点赞、在看” 支持一下
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值