实现拖拽来调整表格顺序

1,555 阅读5分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第6天,点击查看活动详情

之前实习的时候,有一个需求就是:在表格中需要可以实现拖拽调整顺序,当时是依靠的vuedraggable这个包实现的。那现在呢我就想看看原生怎么实现拖拽调整顺序。

先定义一个静态的结构:(这就是我们要拖拽的结构)

    <ul>
        <li data-index="1"></li>
        <li data-index="2"></li>
        <li data-index="3"></li>
        <li data-index="4"></li>
        <li data-index="5"></li>
        <li data-index="6"></li>
        <li data-index="7"></li>
        <li data-index="8"></li>
    </ul>

image.png

draggable

默认html的所有标签基本都不可以拖拽,除了图片、链接和已经选中的文字。如果我们想除了这些之外的标签也可以拖拽,就得用draggable这个标签属性,将其设置为true

    <ul>
        <li data-index="1" draggable="true"></li>
        <li data-index="2" draggable="true"></li>
        <li data-index="3" draggable="true"></li>
        <li data-index="4" draggable="true"></li>
        <li data-index="5" draggable="true"></li>
        <li data-index="6" draggable="true"></li>
        <li data-index="7" draggable="true"></li>
        <li data-index="8" draggable="true"></li>
    </ul>

注意,一旦某个元素节点的draggable属性设为true,就无法再用鼠标选中该节点内部的文字或子节点了。

拖拽事件

我们设置了draggable仅仅是可以将其拖拽了,但是松开鼠标这个元素还在是原位置的。这是因为浏览器页面默认是阻止元素在其上面拖拽的,如果我们要让其允许在上面拖拽,是需要去进行设置的。但是在设置之前,我们得先了解一下这个拖拽的相关事件。

  • drag:拖拉过程中,在被拖拉的节点上持续触发(相隔几百毫秒)。
  • dragstart:用户开始拖拉时,在被拖拉的节点上触发,该事件的target属性是被拖拉的节点。通常应该在这个事件的监听函数中,指定拖拉的数据。
  • dragend:拖拉结束时(释放鼠标键或按下 ESC 键)在被拖拉的节点上触发,该事件的target属性是被拖拉的节点。它与dragstart事件,在同一个节点上触发。不管拖拉是否跨窗口,或者中途被取消,dragend事件总是会触发的。

(以上是被拖拽节点的事件,就是我们要移动的元素的事件,以下是拖拽节点要放置的目标节点的事件)


  • dragenter:拖拉进入当前节点时,在当前节点上触发一次,该事件的target属性是当前节点。通常应该在这个事件的监听函数中,指定是否允许在当前节点放下(drop)拖拉的数据。如果当前节点没有该事件的监听函数,或者监听函数不执行任何操作,就意味着不允许在当前节点放下数据。在视觉上显示拖拉进入当前节点,也是在这个事件的监听函数中设置。
  • dragover:拖拉到当前节点上方时,在当前节点上持续触发(相隔几百毫秒),该事件的target属性是当前节点。该事件与dragenter事件的区别是,dragenter事件在进入该节点时触发,然后只要没有离开这个节点,dragover事件会持续触发。
  • dragleave:拖拉操作离开当前节点范围时,在当前节点上触发,该事件的target属性是当前节点。如果要在视觉上显示拖拉离开操作当前节点,就在这个事件的监听函数中设置。
  • drop:被拖拉的节点或选中的文本,释放到目标节点时,在目标节点上触发。注意,如果当前节点不允许drop,即使在该节点上方松开鼠标键,也不会触发该事件。如果用户按下 ESC 键,取消这个操作,也不会触发该事件。该事件的监听函数负责取出拖拉数据,并进行相关处理。

此外我们要注意一个点是,dragoverdragenterdragleave是会进行冒泡的,我们得阻止其冒泡,不然整个页面都会触发这几个事件了(当时做的时候坑了我半天)

然后我们就要给每一个li都绑定dragstart事件,同时将当前移动的元素保存下来。

    let tempLi;
    let lis = ul.querySelectorAll('li');
    lis.forEach((li) => {
        li.addEventListener('dragstart', function (e) {
            tempLi = e.target; // // 保存被拖拉节点
            e.target.style.opacity = 0.5; // 被拖拉节点的背景色变透明
        })
    })

然后我们将ul添加事件dragover,因为dragover的默认行为会组织我们在其上面拖拽。同时保存目前所移动的位置和阻止其冒泡(这样当目标元素拖拽离开目标距离时不会进行拖拽)。

    ul.addEventListener('dragover', function (e) {
        e.preventDefault();// 防止拖拉效果被重置,允许被拖拉的节点放入目标节点
        e.stopPropagation(); //组织冒泡
        aim = e.target;
        move(tempLi, aim); // 改变原列表数据
    })

我们得到这个移动的位置后,就将我们要拖拽的元素移动到这里来:

let ul = document.querySelector('ul');
function move(tempLi, aim) {
    if (aim == ul) {
        aim.appendChild(tempLi);
    } else {
        aim.parentNode.insertBefore(tempLi, aim)
    }
}

因为e.target指向的是实际触发事件的目标,所以它指向的有两类:一类是li,一类是ul,如果是ul,我们将将其添加到最后面,如果是li,我们就让其添加到这个li的前面。

完整代码:

<head>
    <style>
        * {
            padding: 0;
            margin: 0;
        }

        body {
            width: 100%;
            height: 100vh;
            background-color: #fff;
            display: flex;
            justify-content: center;
            align-items: center;
        }

        ul {
            list-style: none;
            width: 300px;
            height: 500px;
            background-color: #fff;
            padding: 15px 10px;
            border: 1px solid #ccc;
        }

        ul li {
            width: 100%;
            padding: 10px 0;
            border-bottom: 1px solid #ccc;
            color: rgb(114, 114, 114);
            font-size: 16px;
        }

        ul li:last-child {
            border: none;
        }

        ul li:nth-child(odd) {
            background-color: aliceblue;
        }
    </style>
</head>

<body>
     <ul>
        <li data-index="1" draggable="true"></li>
        <li data-index="2" draggable="true"></li>
        <li data-index="3" draggable="true"></li>
        <li data-index="4" draggable="true"></li>
        <li data-index="5" draggable="true"></li>
        <li data-index="6" draggable="true"></li>
        <li data-index="7" draggable="true"></li>
        <li data-index="8" draggable="true"></li>
    </ul>
    <script>
        let tempLi, aim;
        let ul = document.querySelector('ul');
        let lis = ul.querySelectorAll('li');
        lis.forEach((li) => {
            li.addEventListener('dragstart', function (e) {
                tempLi = e.target; // // 保存被拖拉节点
                e.target.style.opacity = 0.5; // 被拖拉节点的背景色变透明
            })
            li.addEventListener('dragend', function (e) {
                // 被拖拉节点的背景色恢复正常
                e.target.style.opacity = '1';
            })
        })
        ul.addEventListener('dragover', function (e) {
            e.preventDefault();// 防止拖拉效果被重置,允许被拖拉的节点放入目标节点
            e.stopPropagation(); //组织冒泡
            aim = e.target;
            move(tempLi, aim); // 改变原列表数据
        })

        function move(tempLi, aim) {
            console.log(aim);
            if (aim == ul) {
                aim.appendChild(tempLi);
            } else {
                aim.parentNode.insertBefore(tempLi, aim)
            }
        }
    </script>
</body>

实际效果:

1661761664797 00_00_00-00_00_30.gif

此外,如果需要做一些高难度的东西的话,就可能需要拖拽的很多其他知识了,比如DataTransfer用来读写需要传递的数据等等。下面我放一个我学习这块知识的链接(我觉得写得很好的~~)

学习链接:wangdoc.com/javascript/…