百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术文章 > 正文

前端里的拖拖拽拽

zhezhongyun 2025-01-09 17:22 53 浏览

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

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

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

一、HTML5 中的拖放

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

1.1 draggable 属性

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

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

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

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

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

1.2 Drag & 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

当拖动元素在可释放目标元素上释放时

放置

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

??注意: 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 实例如下:

(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 事件中获取到值:

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>

演示案例: codepen.io/DYBOY/pen/e…

效果:

1.6 兼容性

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

相较于传统的通过鼠标事件: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 实现效果

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-dnd: github.com/react-dnd/r…
  • react-beautiful-dnd: github.com/atlassian/r…
  • sortablejs: sortablejs.github.io/Sortable/
  • react-sortable-hoc: github.com/clauderic/r…

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

四、总结

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

相关推荐

用豆包生成的BMI计算器(豆包的热量是多少?)

<!DOCTYPEhtml><htmllang="en"><head><metacharset="UTF-8&#...

Android 开发中文引导-应用小部件

应用小部件是可以嵌入其它应用(例如主屏幕)并收到定期更新的微型应用视图。这些视图在用户界面中被叫做小部件,并可以用应用小部件提供者发布。可以容纳其他应用部件的应用组件叫做应用部件的宿主(1)。下面的截...

Qt推流(视频文件/视频流/摄像头/桌面转流媒体rtmp+hls+webrtc)

一、前言说明推流直播就是把采集阶段封包好的内容传输到服务器的过程。其实就是将现场的视频信号从手机端,电脑端,摄影机端打包传到服务器的过程。“推流”对网络要求比较高,如果网络不稳定,直播效果就会很差,观...

一看就会!谷歌广告转化跟踪详细设置指南来了

在出海推广业务中,投放广告最常见的目的是获取订单,但我们怎么知道有没有达成投放目的呢?谷歌转化跟踪技术就可以做到!熟悉谷歌的卖家朋友都知道,转化跟踪在最近几年变得越来越复杂了,虽然有很多选项可以自定义...

Android原生编解码接口MediaCodec详解

作者:躬行之MediaCodec是Android中的编解码器组件,用来访问底层提供的编解码器,通常与MediaExtractor、MediaSync、MediaMuxer、MediaCrypt...

手把手搭建RTSP流媒体服务器(rtsp 流媒体)

0.引言本文主要讲解如何搭建RTSP流媒体服务器的过程,使用开源项目ZLMediaKit。通过这个开源项目,推RTSP流到服务器,然后拉流端可以拉取RTSP、RTMP等流。ZLMediaKit码云链接...

MediaInfo 24.04.0 是一个关于多媒体文件的信息提供工具

MediaInfo24.04.0是一个关于多媒体文件的信息提供工具(仅当文件中包含信息时才提供):包括常规信息(标题、作者、导演、专辑、曲目编号、日期、时长等);视频信息(编解码器、画面比例、帧率...

rmvb格式视频怎么打开,rmvb转MP4认准这个方法

 一、rmvb是什么格式?  RMVB是一种视频文件格式,其中的VB指的是可变比特率。比起上一代的RM格式,RMVB  格式的画面比较清晰,因为它是降低了静态画面下的比特率。  二、制作rmvb  ①...

教你用Plex Media Server,把铁威马变成你的“私人好莱坞”!

TNAS(铁威马NAS)中可以安装多媒体服务器、影视、PlexMediaServer、EmbyServer作为个人媒体服务器使用。PlexMediaServer可以组织整理TNAS上的媒体...

你肯定用过!经典Windows软件被抛弃

Windows系统这些年持续更新的过程中,不断融入新的软件和功能的同时,一些经典的应用也渐渐成为了历史……Windows媒体播放器被抛弃Windows系统不断地推陈出新,一些老旧的组件也难免被抛弃,在...

博思得Q8标签打印全能手(博思得标签打印机安装教程)

2014-12-0905:35:00作者:宋达希【中关村在线办公打印频道原创】服装吊牌、洗涤标签、产品说明标签等都要用到标签打印机,这些标签涵盖多种尺寸的长度和宽度以及材质。另外作为一件商品或者产...

flv文件用什么播放器打开,这样做不踩雷!

FLV是FLASHVIDEO的简称,是随着FlashMX的推出发展而来的视频格式。它的出现有效地解决了视频文件导入Flash后,使导出的SWF文件体积庞大,不能在网络上很好的使用等问题。一、...

media player怎么转换格式?音频转换神器推荐!

Windowsmediaplayer怎么转换格式?WindowsMediaPlayer是微软公司出品的一款多媒体播放器,通常简称“WMP”。提供了编辑音频和视频文件的功能。用户可以使用该软件导...

视频参数检查工具更新:MediaInfo 23.10

MediaInfo提供有关视频或音频文件的技术和标签信息。信息示例包括编解码器、比特率、每秒帧数、宽度、高度、频道数、持续时间、标题、作者、字幕语言和章节名称。多种方式可以查看信息(文本、工作表、树和...

多媒体管理软件:JRiver Media Center 31.0.68 (64位)

JRiverMediaCenter64位是适用于大量库的完整媒体解决方案。它组织、播放和标记所有类型的媒体文件,并对Xbox、PS3、UPnP、DLNA和TiVo进行翻录、刻录。JRiverM...