最近工作中遇到一个需求,需要将一个元素从某位置拖动到另一固定位置后执行某一交互行为,具体效果如下:
将红色定位标志拖动到蓝色标志位置后弹窗这个看似简单的需求,然而实现起来却并不那么顺利。我首先想到的是如何通过哪个现有的插件来快速解决这个问题,然而找了半天,并未找到合适的原生js插件,总会在实际使用当中出现一些莫名其妙的问题。所以,一不做二不休,干脆自己封装一个得了!一方面以后可能还会遇到类似的需求,另一方面自己写的总归更加熟悉,日后也更好维护和拓展。
闲言少叙,接下来就让我们一步步用原生js来实现这个简单的拖拽插件。
一、“类”的构建
插件,当然得有插件的样子。这里我用的是ES6中的class语法糖来实现“类”的封装,这样我们之后在使用插件时只需new一个对象就可以了。
// 定义拖拽插件
class Drag{
constructor(selector, options){
}
}
// 使用拖拽插件
new Drag('.box'); // 该box元素可拖拽
二、获取元素
既然是拖拽插件,当然首先得获取需要被拖拽的元素,这里我们可以让插件使用者直接把已经获取到的元素对象传进来,也可以以CSS选择器的方式传进来,在插件内部进行元素获取。
具体可以通过类型判断来实现,代码如下:
getElement(selector){
if(typeof selector === 'string'){ // 传入css选择器
return document.querySelector(selector);
} else if(typeof selector === 'object'){ // 传入DOM对象
return selector;
} else {
throw '请传入正确的元素';
}
}
然后我们可以在constructor中调用它:
constructor(selector, options){
this.el = this.getElement(selector);
if(!this.el){
throw `未找到移动元素`;
}
}
三、通过事件对象获取鼠标(或手指)位置
在处理拖拽事件之前,我们得先知道拖拽的基本原理是什么。
拖拽,本质上是鼠标(或手指)在元素上按下后,移动鼠标(或手指)时元素跟随鼠标指针(或手指)位置移动,最后当鼠标(或手指)松开时元素停止移动。
其中最关键的部分就是鼠标(或手指)移动时,鼠标(或手指)位置的获取,这时我们就要用到 事件对象 了。
1. PC端获取鼠标位置
e.clientX // 横坐标
e.clientY // 纵坐标
2. 移动端获取手指位置
这里又得分两种情况,一种是手指移动时,一种是手指松开时。
① 手指移动时,也就是touchmove
事件
e.touches[0].clientX // 横坐标
e.touches[0].clientY // 纵坐标
为什么是touches[0]
呢?因为我们只用到了一根手指呀!
② 手指松开时,也就是touchend
事件
e.changedTouches[0].clientX // 横坐标
e.changedTouches[0].clientY // 纵坐标
看到了吗?无论是哪种方式,我们获取鼠标(或手指)的位置都是clientX
和clientY
,只不过前面的那个对象不一样而已。这时为了代码良好的复用性,我们可以对前面的那个对象进行简单的封装。
eventObj(event,isEnd = false){ // isEnd代表是否是手指松开时
return isMobile() ? (isEnd ? event.changedTouches[0] : event.touches[0]) : event;
}
// 判断是否是移动端,因为移动端才会有ontouchstart
function isMobile() {
return document.body.ontouchstart;
}
四、获取事件名称
在整个拖拽过程中,我们无非就用到三种事件:开始、移动、结束。而在PC端和移动端分别对应一组事件名称,我们将其分别用数组进行存储。
eventName(){
if(isMobile()){
return ['touchstart','touchmove','touchend'];
} else {
return ['mousedown','mousemove','mouseup'];
}
}
五、实现拖拽
前面准备工作做了这么多,就是为了实现这最最关键的一步:拖拽,也就是这三种事件(开始、移动、结束)的实现。
initData(){
// 父元素的位置
this.parentPos = {
x: this.el.parentNode.getBoundingClientRect().left,
y: this.el.parentNode.getBoundingClientRect().top,
w: this.el.parentNode.getBoundingClientRect().width,
h: this.el.parentNode.getBoundingClientRect().height,
};
// 移动元素的初始位置和大小
this.elemPos = {
x: this.el.offsetLeft,
y: this.el.offsetTop,
w: this.el.offsetWidth,
h: this.el.offsetHeight,
};
}
bindEvent(){
let eventName = this.eventName(),
status = false;
// 初始化数据
this.initData();
// 开始
this.el.addEventListener(eventName[0], e => {
status = true;
});
// 移动
document.addEventListener(eventName[1], e => {
if(status){
e = this.eventObj(e);
let left = e.clientX - this.elemPos.w / 2 - this.parentPos.x,
top = e.clientY - this.elemPos.h / 2 - this.parentPos.y;
this.el.style.cssText = `position: absolute; left: ${ left }px; top: ${ top }px;`;
}
});
// 结束
document.addEventListener(eventName[2], e => {
e = this.eventObj(e,true);
status = false;
});
}
结束语
写到这里,一个简单的PC端和移动端双端通用性JS拖拽插件就已经完成了。当然,我在此基础上还加了拖拽目标和松开反弹等功能,完整代码可在我的 Github 上预览。
网友评论