Drag&Drop 拖放API简介以及在React中的实践

栏目: IOS · Android · 发布时间: 4年前

内容简介:最近有个需求,需要产品导航栏支持拖放。虽然开源社区已有不少成熟的拖放库,但考虑到代码可控性和可定制性,还是自己写吧。关于选型,前端实现拖放功能,无外乎几种:

最近有个需求,需要产品导航栏支持拖放。

虽然开源社区已有不少成熟的拖放库,但考虑到代码可控性和可定制性,还是自己写吧。

选型

关于选型,前端实现拖放功能,无外乎几种:

1、通过样式布局+鼠标事件,采用此方案的插件如: @shopify/draggable

2、Canvas绘制,插件如: konva

3、Drag&Drop接口,插件如: dragula

经过一番研究,最终选择了原生Drag&Drop的方案,原因如下:

1、原生拖放事件,顺应JS语言发展趋势;

2、兼容性符合项目要求;

3、在Can I use...中有如下描述:

Drag&Drop 拖放API简介以及在React中的实践

最少的代码,最方便的方法,就是它了。

事件

一个拖放行为,自然牵涉到两部分元素,即拖动元素和释放区域元素。

与之相关的事件总共有8个,其中绑定在拖动元素的事件有三个: dragdragstartdragend

剩下5个事件绑定在释放区域元素上: dragenterdragoverdragleavedragexitdrop

具体定义可以参考mdn

定义可拖动元素

浏览器中,有三种元素,默认是可以被拖动的,它们是:

1、被选中后的文本;

2、图片;

3、链接

其他元素要转成可拖动元素,必须添加 draggable="true" ,如:

<div draggable="true"></div>
复制代码

注意:这里不能略写,如写成:

<div draggable></div>
复制代码

是无效的。

定义可被释放区域

要使一块元素可被释放,首先需要绑定 dragenterdragover 事件,然后阻止事件,如下:

<div ondragover="return false">
<div ondragover="event.preventDefault()">
复制代码

因为,这两个事件的默认行为就是“不触发” drop 事件,所以要定义成可被释放区域,就反其道而行之即可。

DataTransfer对象

一个完整的拖放操作,除了拖动一个元素,在指定区域释放之外,还有最重要的一步,就是将元素携带的信息在被释放区域中展示。

比如,拖放一张图片,本质上就是获取到被拖动的图片 src 属性值,并在释放时,在释放区域展示一张相同 src 的图片。

而这个信息,就存储在DataTransfer对象中。

对于非默认可拖放元素来说,其包含的信息需要在 dragstart 事件中设置,使用 DataTransfer.setData() ,如:

dragItem.ondragstart = e => {
  e.dataTransfer.setData('text/plain', 'drag info');
}
复制代码

如果希望拖动时,展示自定义的图片,还可以调用 dataTransfer.setDragImage ,如:

dragItem1.ondragstart = e => {
  const img = new Image(); 
  img.src = 'img_url.jpg'; 
  e.dataTransfer.setDragImage(img, 0, 0);
}
复制代码

drop 事件中,可以取得拖放元素的信息,并将指定信息通过dom操作,展示在特定区域,如:

dropArea.ondrop = e => {
  e.preventDefault();
  const data = event.dataTransfer.getData("text/plain");
  const div = document.createElement("div");
  div.textContent = data;
  e.target.appendChild(div);
}
复制代码

在DataTransfer对象还有一对属性,用来确保释放区域只能释放特定类型的拖拽元素,即 dropEffecteffectAllowed

effectAllowed 只能在 dragstart 事件中设置,在 dragenterdragover 事件中,需要设置 dropEffect 的值与 effectAllowed 一致,才能触发 drop 事件。如:

dragItem.ondragstart = e => {
  e.dataTransfer.effectAllowed = "move";
}

dropArea.ondragover = e => {
  e.preventDefault();
  e.dataTransfer.dropEffect = "move";
}
复制代码

其他属性及方法,详细可以查看mdn

跨终端能力

跨终端能力是drag&drop最大的特点。

最常见的跨终端需求,就是从用户的本地拖放文件到浏览器中指定区域实现上传功能。

在指定区域的 drop 事件中,通过DataTransfer对象的files属性,即可获得文件列表信息,如:

dropArea.ondrop = e => {
  e.preventDefault();
  const files = e.dataTransfer.files;
  if (files.length) {
    Array.prototype.forEach.call(files, f => {
      console.log(f.name); //打印文件名
    });
  }
}
复制代码

在React中实践

在React项目中使用drag&drop,依然遵循React数据驱动的原则,即 事件->数据->DOM更新

所以,像之前提到的,通过DataTransfer对象传递数据的方式,在React项目中,可以改为操作组件对象属性,保证数据流的清晰。

但除此之外,在实际实践中,还是遇到了一些问题,需要特殊处理。具体如下:

1、必须保留 dataTransfer.setData

起初,为保证数据流清晰,在React组件中,绑定 onDragStart ,仅负责监听事件,数据的变动和传递全部修改组件属性,但是会遇到 Firefox浏览器无法拖放 的兼容问题。经查发现,在Firefox中,可拖放元素必须满足:

1、添加 draggable="true"

2、绑定事件 dragstart

3、在dragstart中,dataTransfer.setData设置数据

所以,即使 e.dataTransfer.setData('text', ''); 设置空字符串,也必须添加上这一条。

2、防止跨终端拖拽或不合法拖拽

drop&drag跨终端能力有时也会成为干扰。在项目中,会发现,如果没有做判断,同一个页面同时打开两个浏览器tab,其拖放元素可以跨tab拖动,可能会造成意外BUG。为此,需要增加判断。

一种方式,在组件实例构建时,生成一个随机字符,借助 dataTransfer.setData ,为拖放元素打上标记。同时,在 drop 事件中执行判断。

当然,如果拖放元素和释放区域分属不同组件,则需要在他们的父组件中,生成随机字符,以 props 形式,传递到两个子组件。

3、防止Firefox自动打开新页面

在上述提到的为拖放元素打标签中,起初采用的是这样的写法:

e.dataTransfer.setData('text', uniqDataTransferTag);
复制代码

结果在Firefox中,每次 drop 事件触发时,浏览器会自动打开新tab并搜索 uniqDataTransferTag(随机字符)

根据官方解释,需要在 drop 事件中调用 e.preventDefault() ,同时阻止冒泡 e.stopPropagation() ,但经过尝试,依然不生效。初步判断,可能与React的SyntheticEvent机制有关。于是只好曲线救国,改为设置自定义的 MIME type ,如:

e.dataTransfer.setData('ucloud_drag_tag', uniqDataTransferTag);
复制代码

4、节流与避免event被回收

在项目中,需要在 onDragOver 中,判断被拖放元素当前位置,并执行DOM操作。

根据定义, dragover 事件会在被拖放元素拖到释放区域上时, 每几百毫秒 触发一次,显然不做任何处理会非常影响性能。这里,自然想到采用 节流throttle 方式优化。

由于节流是异步操作,而根据React的SyntheticEvent,event对象会在当前事件循环结束后移除,除非调用 e.persist() ,才能在异步操作中访问到。

5、HACK拖放元素拖动过程中,实现“被拖走”的视觉效果

根据设计师要求,项目中希望实现元素拖动开始后要被 拖走 ,如下图:

Drag&Drop 拖放API简介以及在React中的实践

但默认的拖放效果,其实是这样:

Drag&Drop 拖放API简介以及在React中的实践

很可惜,官方并没有提供对被拖放元素拖动开始后设置效果的接口。经过尝试,找到一个通过样式HACK方法,如下:

1、新增一个 css class

,包含样式:

transform: translateX(-9999px);
复制代码

2、对被拖放元素添加样式:

transition: transform 0.1s;
复制代码

3。在拖动开始后,添加上述第一步的 css class

6、实现长按元素激活拖放效果

根据交互设计,需要实现长按元素一定时长后才可以触发拖拽。

起初,采用的方案是,绑定鼠标事件 mousedown ,触发 setTimeout ,达到固定时长后触发 state 更新,改变拖放元素的 draggable 值。但实际测试中发现,这种方法存在一定的失败率,即明明已经达到了长按的时长,依然不能拖放。而且,在 Firefox 中这个问题更加明显。

推测,可能是 draggable 的更新偶尔会晚于 dragstart 事件,导致拖放失败。

于是转变思路,增设组件的属性作为判断标志,在 mousedown 事件中更新判断标志,而 draggable 始终设为 true 。如下:

// mousedown事件处理函数
handleLongPress = e => {
    this.resetDragTimer(); // 清除定时器

    return (this.triggerDragTimer = setTimeout(() => {
      this.isMenuDraggable = true; // 判断标志
    }, this.triggerDragInterval));
};

// dragstart事件处理函数
handleDragStart = e => {
    if (!this.isMenuDraggable) {
        e.preventDefault();
    } else {
      ...
    }
};
复制代码

总结

Drag&Drop作为原生拖放API,可以用最少代码实现拖放,看似“简单”,实际并非如此。在实践中,还是需要对官方接口定义,以及各浏览器差异有足够了解,才能避免各种未知错误。而在React这类数据驱动的框架中运用时,如何处理事件监听,同时又不打乱组件的数据流,还是需要好好设计一番。


以上所述就是小编给大家介绍的《Drag&Drop 拖放API简介以及在React中的实践》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Lighttpd

Lighttpd

Andre Bogus / Packt Publishing / 2008-10 / 39.99

This is your fast guide to getting started and getting inside the Lighttpd web server. Written from a developer's perspective, this book helps you understand Lighttpd, and get it set up as securely an......一起来看看 《Lighttpd》 这本书的介绍吧!

RGB转16进制工具
RGB转16进制工具

RGB HEX 互转工具

SHA 加密
SHA 加密

SHA 加密工具

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具