Skip to content
基础模板
html
<!doctype html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>拖拽API</title>
    <style>
      * {
        box-sizing: border-box;
      }
      h1 {
        text-align: center;
      }
      .container {
        display: flex;
        gap: 10px;
      }
      .left,
      .right {
        background: #fbfbfb;
      }
      .right {
        flex: 1;
      }
      ul,
      li {
        margin: 0;
        padding: 0;
        list-style: none;
      }
      ul {
        padding: 10px;
      }
      li {
        margin-bottom: 10px;
      }
      li:last-child {
        margin-bottom: 0;
      }
      .subject {
        padding: 8px 15px;
        color: #fff;
        font-size: 16px;
        font-weight: 500;
        border-radius: 2px;
        min-height: 0;
        min-width: 65px;
        text-align: center;
      }
      .chinese {
        background: #ff4c4c;
      }
      .maths {
        background: #0abf53;
      }
      .english {
        background: #f47721;
      }
      .physics {
        background: #0099e5;
      }
      .chemistry {
        background: #d20962;
      }
      .organism {
        background: #7d3f98;
      }
      .sport {
        background: #8a7967;
      }
      .right {
        padding: 10px;
      }
      table {
        width: 100%;
        border: none;
        border-spacing: 1px;
      }
      tr.divide {
        height: 40px;
      }
      th,
      td {
        min-width: 80px;
        min-height: 40px;
        padding: 10px 15px;
        text-align: center;
        background: #caccd1;
        border-radius: 2px;
        border: 1px solid #9f9fa3;
      }
      thead td {
        background: inherit;
        min-width: 0;
        min-height: 0;
        width: 10px;
        border: none;
      }
      tbody td {
        height: 50px;
        background: inherit;
      }
      .span {
        min-width: 0;
        min-height: 0;
        padding: 10px;
        font-weight: bold;
        background: #caccd1;
        border: 1px solid #9f9fa3;
      }
    </style>
  </head>
  <body>
    <div>
      <h1>课程表</h1>
      <div class="container">
        <div class="left">
          <ul>
            <li>
              <div class="subject chinese">语文</div>
            </li>
            <li>
              <div class="subject maths">数学</div>
            </li>
            <li>
              <div class="subject english">英语</div>
            </li>
            <li>
              <div class="subject physics">物理</div>
            </li>
            <li>
              <div class="subject chemistry">化学</div>
            </li>
            <li>
              <div class="subject organism">生物</div>
            </li>
            <li>
              <div class="subject sport">体育</div>
            </li>
          </ul>
        </div>
        <div class="right">
          <table>
            <!-- 列的分组 -->
            <colgroup>
              <col />
              <col />
              <col />
              <col />
              <col />
              <col />
              <col />
              <col />
            </colgroup>
            <thead>
              <tr>
                <td></td>
                <th>星期一</th>
                <th>星期二</th>
                <th>星期三</th>
                <th>星期四</th>
                <th>星期五</th>
                <th>星期六</th>
                <th>星期日</th>
              </tr>
            </thead>
            <tbody>
              <tr>
                <td class="span" rowspan="4">上午</td>
                <td></td>
                <td></td>
                <td></td>
                <td></td>
                <td></td>
                <td></td>
                <td></td>
              </tr>
              <tr>
                <td></td>
                <td></td>
                <td></td>
                <td></td>
                <td></td>
                <td></td>
                <td></td>
              </tr>
              <tr>
                <td></td>
                <td></td>
                <td></td>
                <td></td>
                <td></td>
                <td></td>
                <td></td>
              </tr>
              <tr>
                <td></td>
                <td></td>
                <td></td>
                <td></td>
                <td></td>
                <td></td>
                <td></td>
              </tr>
              <tr class="divide"></tr>
              <tr>
                <td class="span" rowspan="3">下午</td>
                <td></td>
                <td></td>
                <td></td>
                <td></td>
                <td></td>
                <td></td>
                <td></td>
              </tr>
              <tr>
                <td></td>
                <td></td>
                <td></td>
                <td></td>
                <td></td>
                <td></td>
                <td></td>
              </tr>
              <tr>
                <td></td>
                <td></td>
                <td></td>
                <td></td>
                <td></td>
                <td></td>
                <td></td>
              </tr>
            </tbody>
          </table>
        </div>
      </div>
    </div>
  </body>
</html>

课程表实战

查看完整示例

拖拽API 本身并不复杂,难的是结合到具体的应用场景时涉及很多的知识点,考验的是对各种知识点的掌控能力

MDN上的详细文档

给元素加上拖拽标识

  • 给科目元素加上一个draggable属性,用来表示该元素可以被拖拽,例如
html
<div draggable="true" class="subject english">英语</div>

添加事件

  • 由于可以被拖拽的元素可能有多个,为了更好的监控这些元素的事件,使用事件委托的方式来实现。找到它们的父元素,例如
js
const container = document.querySelector('.container')
  • 给父元素添加dragstart事件,用来监控拖拽开始,例如
js
// e.target 表示被拖拽的元素
container.addEventListener('dragstart', function (e) {})
  • 添加dragover事件,用来监控拖拽过程,类似mouseover 例如
js
// e.target 表示拖拽过程中触碰到的目标元素
container.addEventListener('dragover', function (e) {})
  • 添加dragenter事件,用来监控被拖拽元素进入到哪个元素内,类似mouseenter,例如
js
// e.target 表示拖拽过程中进入了哪个目标元素
// 和dragover的e.target是同一个元素,但区别是每次进入一个元素只会触发一次
container.addEventListener('dragenter', function (e) {})
  • 添加drop事件,用来监控拖拽释放时落在了哪个元素上,例如

TIP

但实际操作时发现释放了之后没有触发事件,这是因为浏览器规定了某些 HTML 元素默认是不允许别的元素拖拽到它上面的,需要在dragover事件中阻止这些默认行为

js
container.addEventListener('drop', function (e) {})

指定拖拽类型

  • 给科目元素增加一个自定义属性data-effect,用来表示被拖拽的元素是copy还是move,例如
html
<div data-effect="copy" draggable="true" class="subject english">英语</div>
  • 在拖拽开始时,获取该元素的data-effect属性并赋值给e.dataTransfer.effectAllowed,例如
js
container.addEventListener('dragstart', function (e) {
  e.dataTransfer.effectAllowed = e.target.dataset.effect
})

dragenter的高亮提示

  • 定义一个 CSS 属性用来表示高亮,例如
css
.drag-highlight {
  background: #c4dff6;
}
  • dragenter事件中给e.target的元素添加drag-highlight类,例如
js
container.addEventListener('dragenter', function (e) {
  e.target.classList.add('drag-highlight')
})

但此时出现了一个问题,父元素的背景色被改变了。这是因为dragenter事件是先进入了父元素再进入子元素的

要修复这个问题可以通过自定义属性规定哪些元素可以接受拖拽元素的,例如这里指定表格的td可以接受的拖拽类型是copy

html
<td data-drop="copy"></td>
  • 规定科目容器只能接受move类型的拖拽元素,例如
html
<div class="left" data-drop="move"></div>
  • 定义一个方法用来找到带有data-drop属性的节点,例如
js
function getDropNode(node) {
  while (node) {
    if (node.dataset?.drop) return node
    node = node.parentNode
  }
}
  • 定义一个方法用来清除高亮样式
js
function clearDropStyle() {
  document.querySelectorAll('.drag-highlight').forEach((node) => {
    node.classList.remove('drag-highlight')
  })
}
  • dragenter事件中判断拖拽元素类型和接受类型是否一致
js
container.addEventListener('dragenter', function (e) {
  // 先清除一遍高亮样式
  clearDropStyle()
  // 只有携带 data-drop 属性的节点才有高亮提示
  const dropNode = getDropNode(e.target)
  if (dropNode && dropNode.dataset.drop === e.dataTransfer.effectAllowed) {
    e.target.classList.add('drag-highlight')
  }
})

drop事件逻辑

drop事件的处理分两种情况,一种从科目容器拖拽到课程表中的copy类型元素,另一种是从课程表中拖拽的move类型元素

先讲copy类型的情况。

  • 由于drop事件无法拿到被拖拽元素的节点,所以需要在dragstart事件中将被拖拽元素的节点保存一份
js
let source 
container.addEventListener('dragstart', function (e) {
  source = e.target 
  // ...
})
  • 接着开始编写drop事件逻辑,老样子先判断拖拽元素类型和接受类型是否一致
js
container.addEventListener('drop', function (e) {
  // 先清除一遍高亮样式
  clearDropStyle()
  const dropNode = getDropNode(e.target)
  if (dropNode && dropNode.dataset.drop === e.dataTransfer.effectAllowed) {
    // 复制的情况
    if (dropNode.dataset.drop === 'copy') {
      dropNode.innerHTML = '' // 清除掉之前的节点,防止重复
      const cloned = source.cloneNode(true)
      cloned.dataset.effect = 'move' // 将塞入的拖拽元素的类型改为move
      dropNode.appendChild(cloned)
    }
  }
})
  • move类型的情况就简单了,只需要将被拖拽的节点移除即可
js
container.addEventListener('drop', function (e) {
  // ...
  if (dropNode && dropNode.dataset.drop === e.dataTransfer.effectAllowed) {
    if (dropNode.dataset.drop === 'copy') {
      // ...
    } else {
      // 移动的情况
      source.remove() 
    }
  }
})

到这里,整个简易的拖拽课程表就完成了

MIT License