前言

原生的drag事件有许多诟病,
比如拖拽时跟随鼠标指针的那个元素样式过分简陋。
比如dataTransfer在除start和drop外无法访问,从而让drop元素无法很好的判断是否接受drop。
比如无法优雅的设置拖拽过程中的鼠标指针。。。。
比如不支持触摸拖拽。。。。

处于对多输入设备的兼容及花里胡哨的功能,我需要模拟一套drag和drop事件以实现需求。
这期主要是想安利一波我探索出的一种模拟浏览器原生的拖拽元素方案,兼容各种输入设备(PC/移动端适配)

PointerEvent

如今能跑网页的设备除了电脑,还有手机/平板等,其输入方式可能是鼠标/触摸/数位板或者什么的…
PointerEvent便是为此而生.
它的宗旨是为多种输入设备提供统一的事件模型.已是W3C标准的一部分。
现阶段各大浏览器都可以兼容.有关其详细文档请戳《MDN 指针事件》或《整合鼠标、触摸 和触控笔事件的Pointer Event Api》

我们先来看一下它提供了哪些事件

  • onpointerdown //元素的一个点被按下,可能是鼠标/触摸/压感笔
  • onpointermove //元素上某点被移动
  • onpointerup //元素的某点抬起

  • onpointerenter//某点进入了元素范围内(包括超出尺寸的子元素),且没有遮挡。
  • onpointerleave //某点离开了元素范围内(包括超出尺寸的子元素),且没有遮挡。

  • onpointerover //元素自身或子元素某点进入事件,可通过e.target区分
  • onpointerout //元素自身或子元素某点离开事件,可通过e.target区分

  • onpointercancel //当元素被按下,且浏览器认为该事件不会再有后续事件发生的时候触发.比如触摸过程手机锁屏,输入点过多等一些意外情况导致的中断(部分浏览器暂未支持)

  • 我为你提供了一个测试页面,它几乎包含了所有可能出现的情况,
    这可以帮助你更好的理解事件的触发机制!其源码放在了最后。
    Codepen - PointerEvent触发机制测试

开始

有了上述事件以后,你大概会考虑使用down move up来实现模拟drop的逻辑。

let div = document.querySelector('#a');
div.addEventListener('pointerdown', (e) => {document.addEventListener('pointerup', pointerup);document.addEventListener('pointermove', pointermove);document.addEventListener('pointercancel', pointerup);function pointerup(e) {document.removeEventListener('pointerup', pointerup);document.removeEventListener('pointermove', pointerup);document.removeEventListener('pointercancel', pointerup);}function pointermove(e) { }
});

这样很好,简单的拖拽事件完成了。

此时你可能会思考着给每个需要drop的对象注册enter和leave事件,并搞一个全局变量来标记当前激活的drop对象,并在up的时候判断这个变量。
是的,你可以这么做。对于简单的拖拽来说这足够了。

设置指针样式

但如果你需要实现一个跟随鼠标的元素并设置拖拽过程的鼠标指针样式,你就无法使用pointer-events: none来将跟随元素穿透,这会导致需要drop的元素无法触发相关事件.

那该怎么办呢
此时.setPointerCapture()函数派上了用处.
你可以在pointerdown事件内,向body创建一个隐藏的元素vdom,
让它作为全局变量的宿主对象,随后在up事件中移除它.
然后,在pointerdown事件中使用vdom.setPointerCapture(e.pointerID)来将当前pointerEvent事件的捕捉对象设置为vdom
此时,有关于pointerID的所有事件,都将绑定在vdom元素上.鼠标指针也会跟随vdom元素.

pointerID是啥
pointerID是pointerEvent事件为每个输入点生成的唯一id,
它将在事件创建时创建(比如pointerdown),并直到触摸点消失时才销毁,期间触发的的move enter out等事件的pointerID都会和down时的一致,你可大可以将他理解为手指id.这是touchEvent无法比拟的优势特性.

 let div = document.querySelector('#a');div.style.cursor = 'drag';//设置鼠标样式div.addEventListener('pointerdown', (e) => {let vdom = document.body.appendChild(document.createElement('p'));vdom.setPointerCapture(e.pointerId);vdom.style.cursor = "grabbing";//设置按下鼠标样式let dragHandle=document.body.appendChild(document.createElement('div'));dragHandle.appendChild("我是跟随鼠标的div");dragHandle.style.position = "absolute";dragHandle.style.pointerEvents = "none";//事件穿透dragHandle.style.left=e.x;dragHandle.style.top=e.y;document.addEventListener('pointerup', pointerup);document.addEventListener('pointermove', pointermove);document.addEventListener('pointercancel', pointerup);function pointerup(e) {document.body.removeChild(vdom);document.removeEventListener('pointerup', pointerup);document.removeEventListener('pointermove', pointerup);document.removeEventListener('pointercancel', pointerup);}function pointermove(e) { //更新跟随pointer元素的位置dragHandle.style.left=e.x;dragHandle.style.top=e.y;}});

此时,我们实现了拖拽过程中鼠标指针的全局变化,不受任何其他元素影响.
我们绘制的跟随元素也可以照常设置穿透了.

模拟drop

但此时我们还未能让drop元素知道自己被drop了。
由于设置了setPointerCapture函数,导致我们的其他元素无法接受到任何pointer事件,
这下尴尬了…

不过不要紧,思考一下,我们只是想要知道当前pointer位置下是不是可以drop的元素.
这就好办了,用过jq的可能知道bbox()这个函数,它是dom.getBoundingClientRect();的缩写.
它用于获取目标元素当前在浏览器的绝对位置信息.
我们可以模拟一套命中检测来实现判断.

命中检测(HitTest)

 //用于检测某个坐标是否在元素矩形范围内function hitTest(dom,{ x , y }) {let bbox = dom.getBoundingClientRect();return x > bbox.left && x < bbox.right && y > bbox.top && y < bbox.bottom;}

有了上述方法,我们可以轻松的判断当前pointer位置下是否是要drop的元素.
缺点是只能判断矩形区域,如果你的拖放对象是异形,还需自行修改…
于是,我们可以这么写…

let dropElArr = [];//自行将需要drop的元素添加进此数组
let dropHit = null; //移动过程中命中检测通过的drop元素
let dataTransfer = {};//要传递给drop的数据
let div = document.querySelector('#a');
vdom.style.cursor = 'drag';//设置鼠标样式
div.addEventListener('pointerdown', (e) => {let vdom = document.body.appendChild(document.createElement('p'));vdom.setPointerCapture(e.pointerId);this.vdom.style.cursor = "grabbing";//设置按下鼠标样式let dragHandle = document.body.appendChild(document.createElement('div'));dragHandle.appendChild("我是跟随鼠标的div");dragHandle.style.position = "absolute";dragHandle.style.pointerEvents = "none";dragHandle.getBoundingClientRect();document.addEventListener('pointerup', pointerup);document.addEventListener('pointermove', pointermove);document.addEventListener('pointercancel', pointerup);dataTransfer = {};//你想要传递给drop的数据function pointerup(e) {document.body.removeChild(vdom);vdom = null;document.removeEventListener('pointerup', pointerup);document.removeEventListener('pointermove', pointerup);document.removeEventListener('pointercancel', pointerup);if (dropHit) dropHit.drop(e, dataTransfer);//通知命中的drag元素drop事件}function pointermove(e) {//更新跟随pointer元素的位置dragHandle.style.left = e.x;dragHandle.style.top = e.y;//获取命中的drop元素for (let i = 0; i < dropElArr.length; i++) {if (hitTest(dropElArr[i], e)) {if(dropHit&&dropHit.dragleave)dropHit.dragleave(e,dataTransfer);//通知drag元素拖放离开dropHit = dropElArr[i];if(dropHit.dragenter)dropHit.dragenter(e,dataTransfer);//通知drag元素拖放进入return;}}if(dropHit&&dropHit.dragleave){dropHit.dragleave(e,dataTransfer);//通知drag元素拖放离开dropHit = null;}}function hitTest(dom, { x, y }) {let bbox = dom.getBoundingClientRect();return x > bbox.left && x < bbox.right && y > bbox.top && y < bbox.bottom;}
});

至此,我们完成了全部需求.
并且在上述代码中可以看到,我们也实现了数据在各个过程的传递.

补充

在触摸屏的情况下,我们需要对目标元素的css设置一个属性来阻止浏览器的默认触摸行为,

主要现象就是你会发现,触摸移动了不远move事件就莫名其妙的不触发了,
原因是它会让浏览器触发页面滑动事件,从而触发了pointerleave事件取消掉了move.

touch-action: none;

如果可以,你可以将它放在body的css里面

最后

另外:本文上述代码未经运行测试,书写可能有纰漏,其逻辑已在项目上应用成功,欢迎指正!

下面这个是上传到Codepen上的事件测试代码,可能有的人网络环境打不开.我在这cv一下:

<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title>
</head>
<style>html,body {width: 100%;height: 100%;user-select: none;padding: 0;margin: 0;touch-action: none;}#testHandle {background-color: rgb(255, 72, 0);width: 250px;height: 200px;border-radius: 20px;padding: 10px;}.subChild {background-color: rgba(255, 255, 255, 0.4);}.subChildOut{position: relative;left: 255px;top: -100px;width: 120px;height: 100px;background-color: rgba(255, 251, 0, 0.8);}.Occlusionlayer {position: absolute;padding: 0.2em;left: 180px;top: 200px;width: 50px;height: 50px;background-color: yellowgreen;}.Occlusionlayer.noEvent {pointer-events: none;left: 120px;}
</style><body><div id="testHandle">我是事件元素<div class="subChild">这是子元素<br>你可以将鼠标点击或触摸这里<br>通过F12控制台开查看输出事件<br><div class="subChild">这是子子元素,任何子层级的元素都会触发over/out事件</div>下面的空白部分是元素本身<br>目的是测试区分leave和out<br></div><div class="subChildOut">我是子元素,但我超出了元素范围<br>事件依旧会触发</div><script>let testHandle = document.querySelector('#testHandle');testHandle.addEventListener('pointerdown', (e) => {console.log("[testHandle]某点按下 pointerdown", e);});testHandle.addEventListener('pointerup', (e) => {console.log("[testHandle]某点抬起 pointerup", e);});testHandle.addEventListener('pointermove', (e) => {console.log("[testHandle]某点移动 pointermove", e);});testHandle.addEventListener('pointercancel', (e) => {console.log("[testHandle]某点取消 pointercancel", e);});testHandle.addEventListener('pointerover', (e) => {console.log("[testHandle]元素或子元素进入 pointerover", e);});testHandle.addEventListener('pointerout', (e) => {console.log("[testHandle]元素或子元素离开 pointerout", e);});testHandle.addEventListener('pointerenter', (e) => {console.log("[testHandle]某点进入 pointerenter", e);});testHandle.addEventListener('pointerleave', (e) => {console.log("[testHandle]某点离开 pointerleave", e);});</script></div><div class="Occlusionlayer">这是遮挡div</div><div class="Occlusionlayer noEvent">穿透测试</div>
</body></html>

H5 HTML 移动端触摸拖拽drag drop 自定义拖拽样式 使用PointerEvent模拟的拖拽方案相关推荐

  1. html dragover获得拖拽对象,突袭HTML5之Javascript API扩展4—拖拽(Drag/Drop)概述

    拖拽(Drag/Drop)是个非常普遍的功能.你可以抓住一个对象,并且拖动到你想放置的区域.很多javascript都类似实现了相关的功能,例如,jQueryUI的draganddrop组件.在HTM ...

  2. winform 文件拖拽drag\drop

    winform 重写函数,而不是控件委托事件 protected override void OnDragDrop(DragEventArgs drgevent) protected override ...

  3. h5滚动时侧滑出现_HTML5移动端触摸事件以及滑动翻页效果

    HTML5中新添加了很多事件,但是由于他们的兼容问题不是很理想,应用实战性不是太强,所以在这里基本省略,咱们只分享应用广泛兼容不错的事件,日后随着兼容情况提升以后再陆续添加分享.今天为大家介绍的事件主 ...

  4. html5拖放详解,HTML5拖拽/拖放(drag drop)详解

    H5中拖拽属性: draggable: auto | true | false 拖动事件: - dragstart 在元素开始被拖动时触发 - dragend 在拖动操作完成时触发 - drag 在元 ...

  5. HTML5原生拖拽/拖放 Drag Drop 详解

    转载自:juejin.im/post/5a169d- 前言 拖放(drap && drop)在我们平时的工作中,经常遇到.它表示:抓取对象以后拖放到另一个位置.目前,它是HTML5标准 ...

  6. HTML5 drag drop 拖拽与拖放简介

    by zhangxinxu from http://www.zhangxinxu.com 本文地址:http://www.zhangxinxu.com/wordpress/?p=1419 一.前面的话 ...

  7. HTML5原生拖拽/拖放(drag drop)详解

    前言 拖放(drap && drop)在我们平时的工作中,经常遇到.它表示:抓取对象以后拖放到另一个位置.目前,它是HTML5标准的一部分.我从几个方面学习并实践这个功能. 拖放的流程 ...

  8. vue中使用拖拽drag

    被拖拽的节点 dragable="true" @dragstart="drag" 拖入的节点 @drop="drop" @dragover= ...

  9. HTML5滑动(swipe)事件,移动端触摸(touch)事件

    目有个交互需要实现手指滑动的交互,pc端使用mousedown,mousemove,mouseup监听实现. 但在ios设备上mousemove是不好监听的,同类的方法是touchstart,touc ...

  10. H5手机移动端WEB开发资源整合 常用的标签及注意事项

    meta基础知识 H5页面窗口自动调整到设备宽度,并禁止用户缩放页面 <meta name="viewport" content="width=device-wid ...

最新文章

  1. 图片出处识别_图片模糊怎么变清晰?方法都在这里了
  2. 捣鼓 Subversion
  3. 训练作用_我们口才训练微信群有哪些重要作用?
  4. Counting Divisors HDU - 6069
  5. OpenCV gapi模块实现幻灯片滑slides soble的实例(附完整代码)
  6. c语言补全程序,跪求高手解答简单的程序补全题~!
  7. TensorFlow 2.4 发布
  8. 程序员菜鸟到高手的11个阶段,你处于哪个阶段?
  9. 易云维医院后勤管理系统浅谈医院空调设备管理的问题及优化建议
  10. 学习matlab(十七)——信号处理
  11. NPOI导出真正的电子表格,支持 自定义多行表头(表头风格设置),支持多个sheet页面导出
  12. 关于2015年春运增开旅客列车的公告
  13. mvn help:system下载包失败解决
  14. tolower()函数用法
  15. 关于更佳学术搜索及Android SDK更新问题
  16. 控制台报错:java.security.InvalidKeyException: Illegal key size
  17. java 屏幕识别_java – 检测当前屏幕边界
  18. 微博登录记录pythonurllib_Python使用cookielib和urllib2模拟登陆新浪微博并抓取数据...
  19. 沙拉翻译网页双语显示,程序员必备,是神器没错了
  20. windebug路径设置

热门文章

  1. android音乐同步到iphone,安卓手机上的音乐还能转移到iPhone,你信不信
  2. Codeforces 553A Kyoya and Colored Balls 给球涂颜色
  3. 微信推出网页版传输助手,真的好用吗?
  4. 改善民生 住有所居(加快经济发展方式转变)
  5. 微信会员卡管理系统会员充值说明
  6. python画聚类树状图_聚类分析python画树状图--Plotly(dendrogram)用法解析
  7. 软件研发的6sigma案例解析
  8. git将一个分支的提交合并到另一个分支
  9. 首批企业入驻“一县一店”:多元化方式助力农产外销
  10. 数据库定时备份linux篇