JavaScript 實現(xiàn)拖拽效果組件功能(兼容移動端)
頁面元素拖拽是一種非常實用的前端效果,基于元素拖拽可以實現(xiàn)很多不同的功能,增加客戶端許多操作的便捷性,大大提高用戶體驗。日常生活中大家多多少少都見過這種效果,所以就不廢話了,直接開干吧。
預(yù)期目標
實現(xiàn)一個 Class 類,通過該 Class,可以將任意 DOM 元素(比如 div)一鍵變?yōu)榭赏献顟B(tài),也可以恢復(fù)成原來的狀態(tài),例如這樣:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
#box1 {
height: 50px;
width: 50px;
background-color: cadetblue;
}
#box2 {
height: 50px;
width: 50px;
background-color: blue;
}
#box3 {
height: 50px;
width: 50px;
background-color: red;
}
</style>
</head>
<body>
<div id="box1">1</div>
<a id="box2">2</a>
<div id="box3">3</div>
</body>
<script type="module">
// 我們要完成的目標 Class
import DragElement from './DragElement.js'
// 使 3 個元素可拖拽
let box1 = new DragElement(document.querySelector("#box1"))
let box2 = new DragElement(document.querySelector("#box2"))
let box3 = new DragElement(document.querySelector("#box3"))
// box2 解除拖拽效果,恢復(fù)為原來的樣子
// box2.dragRelease()
</script>
</html>
原本的樣子

隨意拖放

一、算法思路
1.1 拖拽的行為描述
我們先思考如何描述拖拽這一行為。我的思路是這樣的:
- 先對拖拽這一行為進行定義:在指定的元素上,若保持鼠標按下狀態(tài),則該元素將會跟隨鼠標移動。當(dāng)鼠標松開,該元素將不再跟隨鼠標移動。如果是移動端的話,鼠標的角色改為觸摸(touch)即可。
根據(jù)定義,我們可以確定幾個關(guān)鍵信息:
- 鼠標移動,是拖拽算法本身的作用范圍。
- 鼠標按下,開啟拖拽
- 鼠標松開,關(guān)閉拖拽
可以看到,完整的拖拽功能分為 3 個部分,分別是開啟、運行與關(guān)閉。分別對應(yīng)鼠標的按下、運行、松開事件。 因此我們至少需要設(shè)計相應(yīng)的 3 個函數(shù),作為事件的回調(diào)。在這里我分別命名為 dragStart()、dragMoving()、dragEnd()。
這里就出現(xiàn)了第一個重點:如何描述拖拽功能的狀態(tài)變化?
顯然,鼠標的按下與松開,將會決定DOM 元素是否能夠被拖拽,這是一種 “狀態(tài)” 的變化。這種狀態(tài)的變化,在編碼上,可以通過一個變量來實現(xiàn),也可以通過不斷地添加 or 移除回調(diào)函數(shù)來實現(xiàn)。如果通過變量的話,在鼠標沒有按下時,鼠標移動事件也會觸發(fā)進行狀態(tài)判斷,這其實是沒有必要的,因此方案上我們選擇后者,鼠標按下與松開時,分別添加和移除實現(xiàn)拖拽的函數(shù)。
以上是拖拽本身的行為,此外,由于我們需要 DOM 元素能夠在原本的狀態(tài)和可拖拽狀態(tài)之間進行轉(zhuǎn)換,因此我們還需要 2 個函數(shù),一個用于將 DOM 元素變?yōu)榭赏献顟B(tài),另一個用于卸載這些狀態(tài)。前者我稱為 dragActive(),后者我稱為 dragRelease()。它們做的事情,就是添加和解除事件監(jiān)聽。
現(xiàn)在第一個問題解決了,我們來解決第二個問題,那就是:拖拽函數(shù)怎么實現(xiàn)?
1.2 拖拽的實現(xiàn)
首先看核心的,拖拽本身應(yīng)該怎么計算,如何讓元素跟著鼠標走。
同樣的,我們繼續(xù)想象實際的場景。鼠標按下時,我們假設(shè)鼠標的坐標處于(x0, y0) 點,鼠標移動,假設(shè)移動到了(x1, y1) 點。那么該元素,相對自身初始位置便移動了(x1-x0, y1-y0) 的距離。這種相對于自身移動的,在 CSS 上可以通過相對定位,也可以通過 transform: translate 或 translate3d 來實現(xiàn),由于定位在布局中很常用,我們也不知道指定的 DOM 元素到底是什么樣式,為了盡量不影響原來的布局,所以我們采用 transform。
再回到具體計算上,鼠標的位置 x 和 y,可以通過事件回調(diào)函數(shù)傳入的參數(shù) event 得到,在 PC 端是 event.clientX 和 event.clientY,移動端是 event.changedTouches[0].pageX 和 event.changedTouches[0].pageY。而 mousemove 事件是連續(xù)觸發(fā)的,我們的拖動也要讓元素跟著鼠標連續(xù)運動,因此需要不停更新 (x0, y0),(x1, y1) 的值,在每個細小的運動中都進行差值計算,就像微積分一樣。為了方便記錄和更新,我們不妨把拖動中需要的變量用一個對象表示,稱為 dragInfo,掛載到 document 元素上,這樣在不同的函數(shù)、對象之間都可以訪問。
class DragElement {
constructor(element) {
this.element = element
document.dragInfo = {
element: this.element,
x0: 0,
y0: 0,
x1: 0,
y1: 0
}
}
}
element 表示拖拽的元素,x 和 y 分別為計算所需的變量。
獲取鼠標位置的函數(shù):
updateDragPosition = (event) => {
return {
x: event.clientX || (event.changedTouches ? event.changedTouches[0].pageX : document.dragInfo.x0),
y: event.clientY || (event.changedTouches ? event.changedTouches[0].pageY : document.dragInfo.y0)
}
}
或許有人會有疑問,為啥不直接 event.clientX || event.changedTouches[0].pageX,而是要用三元表達式。這是因為有些情況下,上述兩者可能都不存在,比如當(dāng)鼠標移到瀏覽器左邊緣的時候,就無法獲得位置:

獲取鼠標位置的函數(shù)寫完后,就可以寫拖拽的函數(shù)了:
dragMoving = (event) => {
document.dragInfo.x1 = this.updateDragPosition(event).x - document.dragInfo.x0 + document.dragInfo.x1
document.dragInfo.y1 = this.updateDragPosition(event).y - document.dragInfo.y0 + document.dragInfo.y1
document.dragInfo.x0 = this.updateDragPosition(event).x
document.dragInfo.y0 = this.updateDragPosition(event).y
document.dragInfo.element.style.transform = 'translate3d(' + document.dragInfo.x1 + 'px, ' + document.dragInfo.y1 + 'px, 0)';
}
但此時問題就來了,由于 document 上只有一個 dragInfo,不同的組件之間坐標沖突如何解決?其實這個簡單,只需要在 this.element 上添加一個對象記錄每次拖拽后的位置即可,每當(dāng)點擊一個拖拽元素時,就將該元素的信息注入 document.dragInfo。
this.element.dragPostion = {
x: 0,
y: 0
}
綜上,我們已經(jīng)解決了最核心的流程描述與算法部分,接下來只要編碼就可以了。
二、編碼實現(xiàn)
請根據(jù)之前說的思路,自行閱讀代碼,整體邏輯還是非常清晰的,如果有一些細節(jié)不懂,可以在評論區(qū)提出,或者我有空了再補充。
class DragElement {
constructor(element) {
this.element = element
document.dragInfo = {
element: this.element,
x0: 0,
y0: 0,
x1: 0,
y1: 0
}
document.updateDragPosition = this.updateDragPosition
this.dragActive()
}
// 更新鼠標位置
updateDragPosition = (event) => {
return {
x: event.clientX || (event.changedTouches ? event.changedTouches[0].pageX : document.dragInfo.x0),
y: event.clientY || (event.changedTouches ? event.changedTouches[0].pageY : document.dragInfo.y0)
}
}
// 為元素配置相應(yīng)的拖拽控制函數(shù)
dragActive = () => {
if (!this.element) return
this.element.style.display = "block"
this.element.addEventListener('mousedown', this.dragStart, false)
this.element.addEventListener('touchstart', this.dragStart, false)
this.element.addEventListener('mouseup', this.dragEnd, false) // 釋放
this.element.addEventListener('touchend', this.dragEnd, false)
this.element.addEventListener('touchcancel', this.dragEnd, false)
// 為該元素添加一個對象,保存當(dāng)前位置
this.element.dragPostion = {
x: 0,
y: 0
}
}
// 釋放配置
dragRelease = () => {
this.element.removeEventListener('mousedown', this.dragStart)
this.element.removeEventListener('touchstart', this.dragStart)
this.element.removeEventListener('mouseup', this.dragEnd) // 釋放
this.element.removeEventListener('touchend', this.dragEnd)
this.element.removeEventListener('touchcancel', this.dragEnd)
this.element.style.display = ""
return this.element
}
// 點擊捕獲拖拽元素,初始化相應(yīng)信息
dragStart = (event) => {
document.dragInfo.element = this.element
document.dragInfo.x0 = this.updateDragPosition(event).x
document.dragInfo.y0 = this.updateDragPosition(event).y
document.dragInfo.x1 = this.element.dragPostion.x
document.dragInfo.y1 = this.element.dragPostion.y
// 屏蔽默認行為
event.preventDefault();
// mousemove 綁定在 document 上,防止鼠標過快可能導(dǎo)致的元素跟丟
document.addEventListener('mousemove', this.dragMoving, false)
document.addEventListener('touchmove', this.dragMoving, false)
}
// 實時計算、更新相對位置變化
dragMoving = (event) => {
document.dragInfo.x1 = this.updateDragPosition(event).x - document.dragInfo.x0 + document.dragInfo.x1
document.dragInfo.y1 = this.updateDragPosition(event).y - document.dragInfo.y0 + document.dragInfo.y1
document.dragInfo.x0 = this.updateDragPosition(event).x
document.dragInfo.y0 = this.updateDragPosition(event).y
document.dragInfo.element.style.transform = 'translate3d(' + document.dragInfo.x1 + 'px, ' + document.dragInfo.y1 + 'px, 0)';
}
// 關(guān)閉拖拽
dragEnd = () => {
// 保存當(dāng)前位置
this.element.dragPostion.x = document.dragInfo.x1
this.element.dragPostion.y = document.dragInfo.y1
// 解綁
document.removeEventListener('touchmove', this.dragMoving)
document.removeEventListener('mousemove', this.dragMoving)
}
}
export default DragElement
到此這篇關(guān)于JavaScript 實現(xiàn)拖拽效果組件功能(兼容移動端)的文章就介紹到這了,更多相關(guān)JavaScript 拖拽效果組件內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
微信小程序?qū)W習(xí)筆記之表單提交與PHP后臺數(shù)據(jù)交互處理圖文詳解
這篇文章主要介紹了微信小程序?qū)W習(xí)筆記之表單提交與PHP后臺數(shù)據(jù)交互處理,結(jié)合實例形式詳細分析了微信小程序前臺數(shù)據(jù)form表單提交及后臺使用php進行處理相關(guān)操作技巧,并配以圖文形式詳細說明,需要的朋友可以參考下2019-03-03
Javascript執(zhí)行流程細節(jié)原理解析
這篇文章主要介紹了Javascript執(zhí)行流程細節(jié)解析,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2020-05-05
drag-and-drop實現(xiàn)圖片瀏覽器預(yù)覽
chrome的drag and drop API,它能將本地的圖片放到瀏覽器中進行預(yù)覽,猜想一下當(dāng)我們把圖片拖拽到瀏覽器里會發(fā)生什么事情,你的瀏覽器試圖打開一個新的頁面并加載這個圖片。這篇文章給我們介紹drag-and-drop實現(xiàn)圖片瀏覽器預(yù)覽,需要的朋友可以參考下2015-08-08
使用JS組件實現(xiàn)帶ToolTip驗證框的實例代碼
這篇文章主要介紹了使用JS組件實現(xiàn)帶ToolTip驗證框的實例代碼,需要的朋友可以參考下2017-08-08
JavaScript檢測瀏覽器cookie是否已經(jīng)啟動的方法
這篇文章主要介紹了JavaScript檢測瀏覽器cookie是否已經(jīng)啟動的方法,實例分析了javascript操作cookie的技巧,具有一定參考借鑒價值,需要的朋友可以參考下2015-02-02

