最近工作中遇到一個需求,需要將一個元素從某位置拖動到另一固定位置後執行某一交互行爲,具體效果如下:
這個看似簡單的需求,然而實現起來卻並不那麼順利。我首先想到的是如何通過哪個現有的插件來快速解決這個問題,然而找了半天,並未找到合適的原生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 上預覽。