前言
原生的drag事件有许多诟病,
比如拖拽时跟随鼠标指针的那个元素样式过分简陋。
比如dataTransfer在除start和drop外无法访问,从而让drop元素无法很好的判断是否接受drop。
比如无法优雅的设置拖拽过程中的鼠标指针。。。。
比如不支持触摸拖拽。。。。
处于对多输入设备的兼容及花里胡哨的功能,我需要模拟一套drag和drop事件以实现需求。
这期主要是想安利一波我探索出的一种模拟浏览器原生的拖拽元素方案,兼容各种输入设备(PC/移动端适配)
PointerEvent
如今能跑网页的设备除了电脑,还有手机/平板等,其输入方式可能是鼠标/触摸/数位板或者什么的…
PointerEvent便是为此而生.
它的宗旨是为多种输入设备提供统一的事件模型.已是W3C标准的一部分。
现阶段各大浏览器都可以兼容.有关其详细文档请戳《MDN 指针事件》
开始
有了上述事件以后,你大概会考虑使用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,
鼠标始终只有一个pointerID,触摸或笔会在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;
}
});
至此,我们完成了全部需求.
并且在上述代码中可以看到,我们也实现了数据在各个过程的传递.
另外:本文上述代码未经运行测试,书写可能有纰漏,其逻辑已在项目上应用成功,欢迎指正!