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

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

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

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

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

选型

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

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中的实践》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

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

YC创业营: 硅谷顶级创业孵化器如何改变世界

YC创业营: 硅谷顶级创业孵化器如何改变世界

兰德尔·斯特罗斯 (Randall Stross) / 苏健 / 浙江人民出版社 / 2014-8-1 / CNY 52.90

在互联网创业成本日益降低、融资却越来越难的今天,硅谷的Y Combinator因何成为全世界创业者趋之若鹜的创业圣地?为什么25岁左右的青年最适合创业?创业者如何才能在遴选面试中脱颖而出?为什么YC特别看好那些主要由黑客组成的创业团队? YC真的歧视女性吗?如何想出能够赢得投资的新点子?创业者应该如何寻找联合创始人? 获准进入Y Combinator及其创业公司全程跟踪批量投资项目的第一人,......一起来看看 《YC创业营: 硅谷顶级创业孵化器如何改变世界》 这本书的介绍吧!

JS 压缩/解压工具
JS 压缩/解压工具

在线压缩/解压 JS 代码

RGB HSV 转换
RGB HSV 转换

RGB HSV 互转工具