原生实现拖拽的原理

186 阅读5分钟

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

本篇文章会讲述如何用原生的方式实现元素的拖拽和放置功能,涉及到的API主要就是几个拖拽相关的事件,只要搞清楚这几个事件的触发时机,那么拖拽和放置就不难实现了

Drag & Drop API介绍

前置知识

拖拽这一动作涉及到两个对象,一个是被拖拽元素,另一个是拖拽元素最终被放到的目标元素,这里我叫它为可放置元素 image.png 比如上面这幅图中,左边这个元素允许用户拖拽,是可拖拽元素,而右边这个元素是可以放置可拖拽元素的,称它为可放置元素

  • 对于被拖拽元素,需要给该元素添加上draggable="true"的属性
  • 而对于目标元素,需要监听dragoverdrop事件,只要设置了这两个事件的监听器,该元素就自动变成可以存放拖拽元素的容器元素了

但是有一点需要注意,在监听器中需要阻止dragover事件的默认行为,这是因为浏览器默认是会阻止可拖拽元素放到容器上的行为的,只有阻止默认行为后才能够实现可拖拽元素放到目标元素中的效果 基于以上认识,我们可以写出下面的代码:

<div id="draggable" class="draggable" draggable="true"></div>
<div id="droppable" class="droppable"></div>
const logic = (appContainer: HTMLElement) => {
  // 获取元素
  const oDraggable = appContainer.querySelector<HTMLDivElement>('#draggable')!
  const oDroppable = appContainer.querySelector<HTMLDivElement>('#droppable')!

  const handleDragover = (e: DragEvent) => {
    e.preventDefault()
  }

  const handleDrop = (e: DragEvent) => {
    e.preventDefault()
  }

  const bindEvent = () => {
    oDroppable.addEventListener('dragover', handleDragover)
    oDroppable.addEventListener('drop', handleDrop)
  }

  bindEvent()
}

如果没有设置dragover的监听器,或者设置了监听器而没有阻止其默认行为,就会导致#droppable元素不可放置,如下图所示: 未监听dragover事件时元素不可放置.gif 而设置了dragover并阻止默认行为时,效果如下: 监听dragover事件并阻止默认行为后元素可放置.gif 注意看拖拽过去之后的鼠标指针变化

相关事件的作用

知道了如何让元素变得可拖拽以及可接收可拖拽元素还不够,我们还需要清楚拖拽整个过程中涉及到哪些事件,这样才能很好地去在合适的事件中完成自己想要的效果

事件名描述备注
dragstart当用户开始拖动一个元素或者一个选择文本的时候触发针对被拖拽元素
drag当元素或者选择的文本被拖动时触发针对被拖拽元素
dragend拖放事件在拖放操作结束时触发针对被拖拽元素
dragenter当拖动的元素或被选择的文本进入有效的放置目标时触发针对可放置元素
dragover当元素或者选择的文本被拖拽到一个有效的放置目标上时触发针对可放置元素,可放置元素必须要监听该事件并阻止默认行为才能成为可放置元素
dragleave当一个被拖动的元素或者被选择的文本离开一个有效的拖放目标时触发针对可放置元素
drop当一个元素或是选中的文字被拖拽释放到一个有效的释放目标位置时针对可放置元素

这些事件其实就对应了整个拖拽过程中涉及的两个元素的整个生命周期,通过下面这个小例子可以更形象地了解到各个事件的触发时机

const logic = (appContainer: HTMLElement) => {
  // 获取元素
  const oDraggable = appContainer.querySelector<HTMLDivElement>('#draggable')!
  const oDroppable = appContainer.querySelector<HTMLDivElement>('#droppable')!

  const handleDragStart = (e: DragEvent) => {
    console.log('可拖拽元素开始被拖拽...')
  }

  const handleDrag = (e: DragEvent) => {
    console.log('可拖拽元素正在被拖拽中...')
  }

  const handleDragEnd = (e: DragEvent) => {
    console.log('可拖拽元素被释放...')
  }

  const handleDragEnter = (e: DragEvent) => {
    console.log('可放置元素检测到有可拖拽元素开始进入...')
  }

  // 元素要成为 droppable 的必要条件
  const handleDragover = (e: DragEvent) => {
    e.preventDefault()
    console.log('可放置元素检测到有可拖拽元素正在移动...')
  }

  const handleDragLeave = (e: DragEvent) => {
    console.log('可放置元素检测到有可拖拽元素离开可放置区域...')
  }

  // 元素要成为 droppable 的必要条件
  const handleDrop = (e: DragEvent) => {
    e.preventDefault()
    console.log('可放置元素检测到有可拖拽元素被放置到可放置区域中...')
  }

  const bindEvent = () => {
    // 针对可拖拽元素 draggable 的事件监听器
    oDraggable.addEventListener('dragstart', handleDragStart)
    oDraggable.addEventListener('drag', handleDrag)
    oDraggable.addEventListener('dragend', handleDragEnd)

    // 针对可放置元素 droppable 的事件监听器
    oDroppable.addEventListener('dragenter', handleDragEnter)
    oDroppable.addEventListener('dragover', handleDragover)
    oDroppable.addEventListener('dragleave', handleDragLeave)
    oDroppable.addEventListener('drop', handleDrop)
  }

  bindEvent()
}

首先看看draggable相关事件的效果: draggable相关事件监听器效果.gif 接下来看看droppable相关事件的效果,为了不被draggable相关事件监听器的输出影响,我先注释掉对draggable的相关监听器 droppable相关事件监听器效果.gif

完成一个拖拽小案例

首先在拖拽元素的一瞬间,我们就需要做一些事情,最起码要在事件监听回调中的事件对象DragEvent中记录一下有哪些元素是可拖拽的,因为站在可放置元素的角度,它并不知道如何获取到当前的可拖拽元素DOM对象 那么我们可以在dragstart的时候就将被拖拽元素的id放到DragEvent对象中,当dragenter事件触发时,就能够从DragEvent中获取到id进而获取到DOM元素做一些操作 那么要怎么往DragEvent中存放数据呢?这就要用到DragEvent对象的dataTransfer属性了,该属性也是一个对象,它里面有一个setData方法,我们可以把它看成是一个Map,当dragstart触发时设定一个指定的key,值为当前拖拽元素的id,然后再在dragenter中通过该key获取到id即可 这里setDatakey可以是MIME类型,比如text/plain,也可以任何自定义的key

const handleDragStart = (e: DragEvent) => {
  e.dataTransfer?.setData('text/plain', e.target!.id)
}

const handleDragover = (e: DragEvent) => {
  e.preventDefault()
  oDroppable.classList.add('dragover')
}

const handleDragLeave = (e: DragEvent) => {
  oDroppable.classList.remove('dragover')
}

const handleDrop = (e: DragEvent) => {
  e.preventDefault()
  const draggedId = e.dataTransfer?.getData('text/plain')!
  const draggedEl = document.getElementById(draggedId)!
  oDroppable.appendChild(draggedEl)
  oDroppable.classList.add('dropped')
}

添加这四个监听器后的效果如下 拖拽小案例.gif