前端里的“拖拖拽拽”

最近在项目中使用了 react-dnd,一个基于 HTML5 的拖拽库,“拖拽能力”丰富了前端的交互方式,基于拖拽能力,会扩展各种各样的拖拽反馈效果,因此有必要学习了解,最好的学习方式就是实操!

image

拖拽交互常见于各种前端编辑器里,而“编辑器”是一个集成前端技术能力的综合性工程,其中就会涉及到各种形式的拖拽交互,因为“拖拽”是提升用户体验的重要交互方式,所以需要对拖拽的交互效果做各种定制化,作为开发者理应熟练掌握“拖拽”的应用!

最近在开发一款低代码平台,所以借此机会分享一下关于“拖拽”这一交互的基础知识和实践经验,希望可以给有需要的同学提供一点参考。

一、HTML5 中的拖放

拖(Drag)和放(Drop)是 HTML5 标准的组成部分,了解掌握之后,举一反三,有助于提升我们在拖拽场景下技术方案的设计能力。

1.1 draggable 属性

现代浏览器中,不难发现,图片标签()是可以被长按拖拽,但如果需要自定义的 DOM 节点可以被拖拽需要配置以告诉浏览器提供对元素(Element / Tag)支持拖拽的能力。

而元素是否允许被拖放且可响应 API 操作依赖于 draggable 全局标签属性

draggable 是一个布尔值类型的标签属性:

  • true:元素可被拖拽
  • false:元素不可拖拽

当元素设置了 draggable 属性,此时长按就可以自由拖拽了:

image

1.2 Darg & Drop 事件

HTML 的 drag & drop 使用了“DOM Event”和从“Mouse Event”继承而来的“drag event” 。

一个典型的拖拽操作: 用户选中一个可拖拽的(draggable)元素,并将其拖拽(鼠标按住不放)至一个可放置的(droppable)元素上,然后松开鼠标。

在拖动元素期间,一些与拖放相关的事件会被触发,像 dragdragover 类型的事件会被频繁触发。

除了定义拖拽事件类型,每个事件类型还赋予了对应的事件处理器

事件类型 事件处理器 触发时机 绑定元素
dragstart ondragstart 当开始拖动一个元素时 拖拽
drag ondrag 当元素被拖动期间按一定频率触发 拖拽
dragend ondragend 当拖动的元素被释放(️松开、按键盘 ESC)时 拖拽
dragenter ondragenter 当拖动元素到一个可释放目标元素时 放置
dragexit ondragexit 当元素变得不再是拖动操作的选中目标时 放置
dragleave ondragleave 当拖动元素离开一个可释放目标元素 放置
dragover ondragover 当元素被拖到一个可释放目标元素上时(100 ms/次) 放置
drop ondrop 当拖动元素在可释放目标元素上释放时 放置

各个事件的时机可以用下面这个图简单表示:

image

⚠️注意: dragOver 事件的默认行为是:“Reset the current drag operation to "none"”。也就是说,如果不阻止放置元素的 dragOver 事件,则放置元素不会响应“拖动元素”的“放置行为”

// 让绑定该事件的元素支持放置
function handleDragOver(e) {
  // 阻止默认的重置行为
  // 即可成为拖拽元素的放置区
  e.preventDefault();
}

从设计事件标准来看,如果我们需要自行实现拖拽的效果,就需要从这关键的几个事件去思考设计。

1.3 DataTransfer

在上述的事件类型中,不难发现,放置元素和拖动元素分别绑定了自己的事件,可如何将拖拽元素和放置元素建立联系以及传递数据

这就涉及到 DataTransfer 对象:

DataTransfer 对象用于保存拖动并放下(drag and drop)过程中的数据。它可以保存一项或多项数据,这些数据项可以是一种或者多种数据类型。 —— DataTransfer - MDN

DataTransfer 对象在不同浏览器上因为标准可能不一样使得 API 有差异,但有几个“标准(常用)”属性和方法需要熟悉

在 Chrome 浏览器上的 DataTransfer 实例如下:

image

(1) 属性

属性 说明
dropEffect 获取当前选定的拖放操作类型或者设置的为一个新的类型。值为:none、copy、link、move
effectAllowed 提供所有可用的操作类型。值是:none、copy、copyLink、copyMove、link、linkMove、move、all、uninitialized
files 包含数据传输中可用的所有本地文件的列表。如果拖动操作不涉及拖动文件,则此属性为空列表
items (只读) 提供一个包含所有拖动数据列表的 DataTransferItemList 对象
types (只读) 提供一个 dragstart 事件中设置的格式的 strings 数组。

(2) 方法

属性 说明
setData(format, value) 设置给定类型的数据。如果该类型的数据不存在,则将其添加到末尾,以便类型列表中的最后一项将是新的格式。如果该类型的数据已经存在,则在相同位置替换现有数据。
getData(format) 检索给定类型的数据,如果该类型的数据不存在或 data transfer 不包含数据,则返回空字符串
clearData([format]) 删除与给定类型关联的数据。类型参数是可选的。如果类型为空或未指定,则删除与所有类型关联的数据。如果指定类型的数据不存在,或者 data transfer 中不包含任何数据,则该方法不会产生任何效果。
`setDragImage(img element, xOffset, yOffset)` 设置自定义的拖动图像,注意图像需要提前加载,否则会无效

在简单的拖拽场景中,其实可以类比 window.localStorage 对象的 setItem()getItem() 方法来理解记忆.

getData() 在测试中发现只能在 ondrop 事件中获取到值:

image

1.4 一个案例掌握拖放 API

拖动元素
放置区域

演示案例: https://codepen.io/DYBOY/pen/eYeyvWm

效果:

演示
拖拽演示效果

1.6 兼容性

是 HTML5 标准提出的能力,因此各大浏览器厂商对于标准的支持有差异,其兼容性参考如下:

image

相较于传统的通过鼠标事件:mousedownmousemovemouseup 组合实现的拖拽要简单很多,少了放入目标边界的判断,也少了对位置的实时获取操作。

另外目前的 API 不算多,例如我们想要定制化拖拽的图片大小、鼠标样式等,目前暂时没发现比较方便的解决方式,但是从另一个角度来说,让我们对于拖拽能力的设计和标准有了一个更深切的认识,对于设计实现拖拽交互有了一个“理论”基础!

二、手搓一个

有了上面的基础知识,那么实现一个列表拖拽排序并不是什么难事。

2.1 设计实现

结合上述的 Drag & Drop 的事件类型,那么拖拽排序主要是针对“拖动对象”之间相互作用关系的逻辑梳理,此处我们暂且区分为:

  • 源对象: 拖拽列表中被拖动的单个列表项
  • 目标对象: 拖拽列表中和“源对象”产生“相互作用”的列表项

整体的交互事件的设计思路如下:

(1) ondragstart

此时开始拖拽“源对象”的时机,在此事件回调函数中改变“源对象”的样式,设置拖拽的一些传递参数等初始值。

// 源对象开始拖拽
const handleDragStart = (e: React.DragEvent) => {
  e.dataTransfer.effectAllowed = "move";
  setDragId(e.currentTarget.dataset.index); // 从 dataset 获取拖拽项的 id
};

(2) ondragover

正与拖拽中的“源对象”产生相互影响的目标对象,此时“源对象”处于“目标对象”的正上方,目标对象 100ms/次的频率调用“目标对象”的 ondragover 中声明的回调事件。

此时,我们会计算改变“源对象”和“目标对象”的位置。

// 源对象在目标对象上方时
const handleDragOver = (e: React.DragEvent) => {
  e.preventDefault(); // 允许放置,阻止默认事件
  const dropId = e.currentTarget.dataset.index;
  move(dragId, dropId); // 改变原列表数据
};

(3) ondrag

该事件作用于“源对象”,此时正处于拖拽过程中,此时可以改变源对象的 opacitydisplay(none)visiblity 样式属性,如果在 dragstart 事件改变,则会导致拖拽拷贝对象丢失。

// 源对象被拖拽过程中
const handleDrag = (e: React.DragEvent) => {
  e.currentTarget.style.opacity = "0";
};

(4) ondragend

在松手完成“源对象”的放置时,主动调用绑定在“源对象”身上的事件,此时恢复更改的样式。

// 源对象被放置完成时
const handleDragEnd = (e: React.DragEvent) => {
  e.currentTarget.style.opacity = "1";
};

2.2 实现效果

image

2.3 加点动画

上面的实现中效果还算可以,但是少了拖拽项的切换过程动画,直接在 dragover 事件中通过 move(dragId, dropId) 方法直接修改了原列表数据的排序,导致切换突变。

借助 animation 新增 CSS 帧动画:

@keyframes dropUp {
  100% {
    transform: translateY(5px);
  }
}

@keyframes dropDown {
  100% {
    transform: translateY(-5px);
  }
}

.drop-up{
  animation: dropUp 0.3s ease-in-out forwards;
}
.drop-down{
  animation: dropDown 0.3s ease-in-out forwards;
}

同样的在 dragOver 事件中处理,新增逻辑代码:

// 源对象在目标对象上方时
const handleDragOver = (e: React.DragEvent) => {
  ...
  // 设置动画
  const dropId = e.currentTarget.dataset.index;
  const dragIndex = findIndex(listData, (i) => i.id === dragId);
  const dropIndex = findIndex(listData, (i) => i.id === dropId);
  // 通过增加对应的 CSS class,实现视觉上的动画过渡
  e.currentTarget.classList.remove("drop-up", "drop-down");
  if (dragIndex < dropIndex) {
    e.currentTarget.classList.add("drop-down");
  } else if (dragIndex > dropIndex) {
    e.currentTarget.classList.add("drop-up");
  } 
  ...
};

增加了动画的效果:

增加了动画的效果

看起来似乎好一点了,当然大家可以去扩充动画的效果,亦或者借助三方动画库。

三、已有拖拽库

目前主流的拖拽库有:

  • react-dnd: https://github.com/react-dnd/react-dnd/
  • react-beautiful-dnd: https://github.com/atlassian/react-beautiful-dnd/
  • sortablejs: https://sortablejs.github.io/Sortable/
  • react-sortable-hoc: https://github.com/clauderic/react-sortable-hoc/

关于几者的差异,可以参阅:《关于react中使用拖拽插件的评测》

image

四、总结

由于低代码平台其实会有丰富的拖拽场景,从可扩展和兼容性上考虑,最终选择了 react-dnd 作为基础拖拽库,当然,在复杂的拖拽场景下,是需要自行扩展该拖拽库,上手难度相对会高一点,不过有了这些“拖拽知识”作为前置基础,那么扩展功能也就不是什么难事了。

朋友们可以关注笔者的微信公众号:DYBOY,来一起玩耍呀~

你可能感兴趣的:(前端里的“拖拖拽拽”)