【每日编程】- JS实现拖拽排序

147 阅读3分钟

实现元素拖拽排序,需要考虑一下几个问题

  1. 监听鼠标拖拽事件
  2. 触发监听的元素是window还是父元素
  3. DOM元素实现基本拖拽的属性 - draggable
  4. 拖拽停止后,如何对整个元素进行重新排序

关于【拖拽排序】的事件

  • dragstart :当被拖拽元素开始被拖拽时触发
  • drag :当被拖拽元素被拖拽时触发
  • dragenter :当被拖拽元素进入目标元素时触发
  • dragover :当被拖拽元素在目标元素上移动时触发
  • dragleave :当被拖拽元素离开目标元素时触发
  • drop :当被拖拽元素在目标元素上,而且释放鼠标左键时触发
  • dragend :当拖拽行为结束后触发

(当然,在这个例子中不需要监听全部的拖拽事件)

拖拽事件的【生命周期】

  • 生命周期:dragstart -> drag -> dragenter -> dragover -> dragleave -> drop -> dragend

事件监听

  • mousedown
    • 给当前将要被拖拽的元素添加【拖拽属性 - draggable】
    • 缓存当前元素
  • dragstart
    • 增加拖拽样式
    • 缓存当前拖拽元素在父元素中的下标
  • dragenter
    • 缓存目标元素在父元素中的下标
    • 对比下标,元素重排
  • dragend
    • 给当前将要被拖拽的元素移除【拖拽属性】
    • 移除拖拽样式

初始化一个对象属性 - dragSortObject

const dragSortObject = {
      dragListNode: null, // 父元素
      dragSortDom: null, // 子元素(被排序的元素)
      currentIndex: 0, // 当前元素在父元素中的下标
      tragetIndex: 0, // 目标元素在父元素中的下标

      // 初始化拖动排序函数
      initDragSort() {
        dragListNode = document.getElementById('drag-list');
        if (!dragListNode) return
           
        // 监听【mousedown】
        dragListNode.addEventListener('mousedown', event => this.handleMouseDown(event))
        // 监听【dragstart】
        dragListNode.addEventListener('dragstart', event => this.handleDragStartFunc(event))
        // 监听【dragend】
        dragListNode.addEventListener('dragend', event => this.handleDragEnd(event))
        // 监听【dragenter】
        dragListNode.addEventListener('dragenter', event => this.handleDragEnter(event))
        // 监听【dragover】
        dragListNode.addEventListener('dragover', event => this.handleDragOver(event))
      },
 }

handleMouseDown的实现

// mosueDown
handleMouseDown(event) {
    if (dragListNode != event.target) {
      dragSortDom = event.target;
      // 设置当前元素可拖动
      dragSortDom.draggable = true;
    }
},
  • 【问题】
    • 到这里有人或许会问,为什么不把在mousedown中的业务逻辑放到dragstart中去处理呢?
  • 【回答】
    • 这个 draggable属性dragstart事件 是有先后顺序的。draggable属性 触发 dragstart事件

handleDragStartFunc的实现

// dragstart
  handleDragStartFunc(event) {
    // MDN属性解释 - https://developer.mozilla.org/zh-CN/docs/Web/API/DataTransfer/effectAllowed
    // 为拖动源设置所需的拖动效果
    event.dataTransfer.effectAllowed = 'move';
    // 增加拖动样式 - 增加透明度
    dragSortDom.classList.add('moving');
    // 获取当前拖动元素在父元素中的下标
    currentIndex = this.findIndexOfdragListNode(event);
  },

在这里,寻找当前元素在父元素中的下标封装成一个公共方法 - findIndexOfdragListNode

findIndexOfdragListNode的实现

// 当前元素在父元素中的下标
  findIndexOfdragListNode(event) {
    const currentIndex = Array.prototype.indexOf.call(dragListNode.childNodes, event.target)
    return currentIndex
  },

handleDragEnter的实现

// dragenter
handleDragEnter(event) {
    event.preventDefault();

    // 如果target元素是父元素或者是被拖动元素自身, 则不做任何操作
    if (event.target == dragListNode || event.target == dragSortDom) return;
    // 记录目标元素在父元素中的下标
    const targetIndex = this.findIndexOfdragListNode(event);

    if (currentIndex < targetIndex) {
      // 下标:当前元素 < 目标元素 => 上移
      dragListNode.insertBefore(dragSortDom, event.target.nextSibling)
    } else {
      // 下标:当前元素 > 目标元素 => 下移
      dragListNode.insertBefore(dragSortDom, event.target)
    }
},

注意此处【下标:当前元素 < 目标元素 => 上移】的情况,是需要把当前元素插入到目标元素的下一个元素 - nextSibling

handleDragEnd的实现

// dragend
handleDragEnd(event) {
    dragSortDom.draggable = false
    // 给被拖动元素去除透明度
    dragSortDom.classList.remove('moving')
},

这里为什么要在 dragstartdragend 中才去给元素添加/去除 draggable,而不在HTML中直接写死呢?

直接在html中写死也可,但是从代码解析效率和可阅读性上考虑就不太友好

handleDragOver的实现

// dragover
handleDragOver(event) {
    event.preventDefault();
}

HTML

<head>
<style>
    *{
      margin: 0;
      padding: 0;
    }
    ul {
      list-style-type: none;
    }
    li {
      margin-bottom: 4px;
      width: 200px;
      padding: 15px;
      background-color: antiquewhite;
      border: 1px solid antiquewhite;
      border-radius: 4px;
      user-select: none;
    }

    .moving {
      opacity: 0.3;
    }
  </style>
</head>
<body>
  <ul id="drag-list">
    <li>Item1</li>
    <li>Item2</li>
    <li>Item3</li>
    <li>Item4</li>
    <li>Item5</li>
  </ul>
</body>

结果展示

tutieshi_640x360_9s.gif

dragenter VS dragover

  • dragenter事件
    • 是元素进入目标元素后触发
  • dragover事件
    • 是元素在目标元素上移动时触发, 100ms触发一次

代码参考