当前位置:首页 > 人工智能

关于前端里的拖拖拽拽,了解一下?

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

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

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

一、解下HTML5 中的拖放

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

1.1 draggable 属性

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

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

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

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

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

1.2 Darg & Drop 事件

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

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

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

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

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

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

// 让绑定该事件的元素支持放置

function handleDragOver(e) {

// 阻止默认的重置行为

// 即可成为拖拽元素的放置区

e.preventDefault();

}

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

1.3 DataTransfer

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

这就涉及到 DataTransfer 对象:

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

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

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

(1) 属性

(2) 方法

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

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

1.4 一个案例掌握拖放 API

拖动元素 放置区域

拖动元素 放置区域

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));

}

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

效果:

演示

拖拽演示效果

1.6 兼容性

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

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

另外目前的 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

该事件作用于“源对象”,此时正处于拖拽过程中,此时可以改变源对象的 opacity、display(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 实现效果

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中使用拖拽插件的评测[4]》

四、总结

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

参考资料

[1]react-dnd - Github: https://react-dnd.github.io/react-dnd/about

[2]draggable - MDN: https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/draggable

[3]DataTransfer - MDN: https://developer.mozilla.org/en-US/docs/Web/API/DataTransfer

[4]关于react中使用拖拽插件的评测: https://juejin.cn/post/6956112150989373448

分享到:

滇ICP备2023006006号-16