前端里的“拖拖拽拽”

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

image

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

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

一、HTML5 中的拖放

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

1.1 draggable 属性

现代浏览器中,不难发现,图片标签(<img />)是可以被长按拖拽,但如果需要自定义的 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

<div>
  <div class="drag" draggable="true" id="dragger" ondragstart="handleDragStart(event)">拖动元素</div>
  <div class="drop" ondrop="handleDrop(event)" ondragover="allowDrop(event)">放置区域</div>
</div>

<script>
  function handleDragStart(e) {
    e.dataTransfer.setData('DRAG_NODE_ID', e.target.id)
  }
  function handleDragOver(e) {
    e.preventDefault();
  }
  function handleDrop(e) {
    e.preventDefault();
    var data = e.dataTransfer.getData('DRAG_NODE_ID');
    e.target.appendChild(document.getElementById(data));
  }
</script>

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

效果:

演示
拖拽演示效果

1.6 兼容性

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

image

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

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

二、手搓一个

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

2.1 设计实现

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

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

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

(1) ondragstart

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

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

(2) ondragover

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

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

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

(3) ondrag

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

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

(4) ondragend

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

// 源对象被放置完成时
const handleDragEnd = (e: React.DragEvent<HTMLDivElement>) => {
  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<HTMLDivElement>) => {
  ...
  // 设置动画
  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中使用拖拽插件的评测

image

四、总结

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

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容

  • HTML5篇是拖拽学习的最后一篇了,O(∩_∩)O哈哈~。! 参考传送门:http://www.cnblogs.c...
    迷缘火叶阅读 712评论 0 3
  • https://www.cnblogs.com/moqiutao/p/6365113.html 抓取对象以后拖放到...
    skoll阅读 615评论 0 0
  • 前言 拖放(drap && drop)在我们平时的工作中,经常遇到。它表示:抓取对象以后拖放到另一个位置。目前,它...
    weiqinl阅读 1,344评论 0 3
  • 前言 本文依据半年前本人的分享《浅谈js拖拽》撰写,算是一篇迟到的文章。 基本思路 虽然现在关于拖拽的组件库到处都...
    lanberts阅读 2,417评论 0 0
  • 1.处理步骤 a.定义可拖动目标 b.定义被拖动的数据,可能为多种不同的格式 c.允许设置拖拽效果 d.定义放置区...
    阿迪呀dity阅读 1,585评论 0 3