Skip to content

Latest commit

 

History

History
197 lines (149 loc) · 5.05 KB

在列表中拖放元素.md

File metadata and controls

197 lines (149 loc) · 5.05 KB

在列表中拖放元素

在这个例子中,我们将创建一个可排序的列表,其项目可以在其中拖放:

<div id="list">
  <div class="draggable">A</div>
  <div class="draggable">B</div>
  <div class="draggable">C</div>
  <div class="draggable">D</div>
  <div class="draggable">E</div>
</div>

每个项目都有 draggable 指示用户可以拖动它的类:

.draggable {
  cursor: move;
  user-select: none;
}

使项目可拖动

为了使其可拖动,我们需要处理两个事件:

  • mousemove(在 document 上):计算鼠标移动了多远,并确定元素的位置
  • mouseup(在 document 上):删除 document 上的事件处理程序
// 当前拖动项
let draggingEle

// 鼠标相对于拖动元素的当前位置
let x = 0
let y = 0

const mouseDownHandler = function (e) {
  draggingEle = e.target

  // 计算鼠标位置
  const rect = draggingEle.getBoundingClientRect()
  x = e.pageX - rect.left
  y = e.pageY - rect.top

  // 将监听器附加到 document
  document.addEventListener('mousemove', mouseMoveHandler)
  document.addEventListener('mouseup', mouseUpHandler)
}

const mouseMoveHandler = function (e) {
  // 设置拖动元素的位置
  draggingEle.style.position = 'absolute'
  draggingEle.style.top = `${e.pageY - y}px`
  draggingEle.style.left = `${e.pageX - x}px`
}

mouseup 事件处理程序会删除拖动的项目,并清理事件处理程序的位置样式:

const mouseUpHandler = function () {
  // 移除位置样式
  draggingEle.style.removeProperty('top')
  draggingEle.style.removeProperty('left')
  draggingEle.style.removeProperty('position')

  x = null
  y = null
  draggingEle = null

  // 移除 mousemove 和 mouseup 的处理程序
  document.removeEventListener('mousemove', mouseMoveHandler)
  document.removeEventListener('mouseup', mouseUpHandler)
}

添加占位符

我们现在有以下几个项目:

ABCDE

例如,当我们拖动一个项目 C 时,下一个项目(D)将向上移动到顶部,并占据拖动元素(C)的区域。为了解决这个问题,我们创建了一个动态占位符元素,并将其插入到拖动元素之前。占位符的高度必须与拖动元素的高度相同。

占位符在鼠标移动期间创建一次,因此我们添加一个新标志 isDraggingStarted 来跟踪它:

let placeholder
let isDraggingStarted = false

const mouseMoveHandler = function (e) {
  const draggingRect = draggingEle.getBoundingClientRect()

  if (!isDraggingStarted) {
    // 更新标志
    isDraggingStarted = true

    // 让占位符占据拖动元素的高度
    // 所以下一个元素不会向上移动
    placeholder = document.createElement('div')
    placeholder.classList.add('placeholder')
    draggingEle.parentNode.insertBefore(placeholder, draggingEle.nextSibling)

    // 设置占位符的高度
    placeholder.style.height = `${draggingRect.height}px`
  }

  // ...
}

一旦用户放下项目,占位符将被删除:

const mouseUpHandler = function () {
  // 移除占位符
  placeholder && placeholder.parentNode.removeChild(placeholder)
  // 重置标志
  isDraggingStarted = false

  // ...
}

确定用户是向上还是向下移动项目

首先,我们需要一个辅助函数来检查一个项目是在另一个项目之上还是之下。

如果 nodeA 的水平中心点小于 nodeB,则 nodeA 被视为与 nodeB 相同。节点的中心点可以通过取其顶部和高度的一半之和来计算:

const isAbove = function (nodeA, nodeB) {
  // 获取节点的边界矩形
  const rectA = nodeA.getBoundingClientRect()
  const rectB = nodeB.getBoundingClientRect()

  return rectA.top + rectA.height / 2 < rectB.top + rectB.height / 2
}

当用户移动项目时,我们定义上一个和下一个同级项目:

const mouseMoveHandler = function (e) {
  const prevEle = draggingEle.previousElementSiblin
  const nextEle = placeholder.nextElementSibling
}

如果用户将项目移动到顶部,我们将交换占位符和上一个项目:

const mouseMoveHandler = function (e) {
  // ...
  // 用户将项目移动到顶部
  if (prevEle && isAbove(draggingEle, prevEle)) {
    swap(placeholder, draggingEle)
    swap(placeholder, prevEle)
    return
  }
}

同样,如果我们检测到用户将项目向下移动到底部,我们将交换下一个和拖动项目:

const mouseMoveHandler = function (e) {
  // ...

  // 用户将拖动元素移动到底部
  if (nextEle && isAbove(nextEle, draggingEle)) {
    swap(nextEle, placeholder)
    swap(nextEle, draggingEle)
  }
}

在这里,swap 是一个用于交换两个节点:

const swap = function (nodeA, nodeB) {
  const parentA = nodeA.parentNode
  const siblingA = nodeA.nextSibling === nodeB ? nodeA : nodeA.nextSibling

  // 将 nodeA 移到 nodeB 之前
  nodeB.parentNode.insertBefore(nodeA, nodeB)

  // 将 nodeB 移到 nodeA 的兄弟节点之前
  parentA.insertBefore(nodeB, siblingA)
}

查看效果