1. 程式人生 > 程式設計 >JavaScript 實現拖拽效果元件功能(相容移動端)

JavaScript 實現拖拽效果元件功能(相容移動端)

頁面元素拖拽是一種非常實用的前端效果,基於元素拖拽可以實現很多不同的功能,增加客戶端許多操作的便捷性,大大提高使用者體驗。日常生活中大家多多少少都見過這種效果,所以就不廢話了,直接開幹吧。

預期目標

實現一個 Class 類,通過該 Class,可以將任意 DOM 元素(比如 div)一鍵變為可拖拽狀態,也可以恢復成原來的狀態,例如這樣:

<!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 解除拖拽效果,恢復為原來的樣子
  // box2.dragRelease()
</script>
</html>

原本的樣子

在這裡插入圖片描述

隨意拖放

在這裡插入圖片描述

一、演算法思路

1.1 拖拽的行為描述

我們先思考如何描述拖拽這一行為。我的思路是這樣的:

  • 先對拖拽這一行為進行定義:在指定的元素上,若保持滑鼠按下狀態,則該元素將會跟隨滑鼠移動。當滑鼠鬆開,該元素將不再跟隨滑鼠移動。如果是移動端的話,滑鼠的角色改為觸控(touch)即可。

根據定義,我們可以確定幾個關鍵資訊:

  • 滑鼠移動,是拖拽演算法本身的作用範圍。
  • 滑鼠按下,開啟拖拽
  • 滑鼠鬆開,關閉拖拽

可以看到,完整的拖拽功能分為 3 個部分,分別是開啟、執行與關閉。分別對應滑鼠的按下、執行、鬆開事件。 因此我們至少需要設計相應的 3 個函式,作為事件的回撥。在這裡我分別命名為 dragStart()、dragMoving()、dragEnd()。

這裡就出現了第一個重點:如何描述拖拽功能的狀態變化?

顯然,滑鼠的按下與鬆開,將會決定DOM 元素是否能夠被拖拽,這是一種 “狀態” 的變化。這種狀態的變化,在編碼上,可以通過一個變數來實現,也可以通過不斷地新增 or 移除回撥函式來實現。如果通過變數的話,在滑鼠沒有按下時,滑鼠移動事件也會觸發進行狀態判斷,這其實是沒有必要的,因此方案上我們選擇後者,滑鼠按下與鬆開時,分別新增和移除實現拖拽的函式。

以上是拖拽本身的行為,此外,由於我們需要 DOM 元素能夠在原本的狀態和可拖拽狀態之間進行轉換,因此我們還需要 2 個函式,一個用於將 DOM 元素變為可拖拽狀態,另一個用於解除安裝這些狀態。前者我稱為 dragActive(),後者我稱為 dragRelease()。它們做的事情,就是新增和解除事件監聽。

現在第一個問題解決了,我們來解決第二個問題,那就是:拖拽函式怎麼實現?

1.2 拖拽的實現

首先看核心的,拖拽本身應該怎麼計算,如何讓元素跟著滑鼠走。

同樣的,我們繼續想象實際的場景。滑鼠按下時,我們假設滑鼠的座標處於(x0,y0) 點,滑鼠移動,假設移動到了(x1,y1) 點。那麼該元素,相對自身初始位置便移動了(x1-x0,y1-y0) 的距離。這種相對於自身移動的,在 CSS 上可以通過相對定位,也可以通過 transform: translate 或 translate3d 來實現,由於定位在佈局中很常用,我們也不知道指定的 DOM 元素到底是什麼樣式,為了儘量不影響原來的佈局,所以我們採用 transform。

再回到具體計算上,滑鼠的位置 x 和 y,可以通過事件回撥函式傳入的引數 event 得到,在 PC 端是 event.clientX 和 event.clientY,移動端是 event.changedTouches[0].pageX 和 event.changedTouches[0].pageY。而 mousemove 事件是連續觸發的,我們的拖動也要讓元素跟著滑鼠連續運動,因此需要不停更新 (x0,y0),(x1,y1) 的值,在每個細小的運動中都進行差值計算,就像微積分一樣。為了方便記錄和更新,我們不妨把拖動中需要的變數用一個物件表示,稱為 dragInfo,掛載到 document 元素上,這樣在不同的函式、物件之間都可以訪問。

class DragElement {
  constructor(element) {
    this.element = element
    document.dragInfo = {
      element: this.element,x0: 0,y0: 0,x1: 0,y1: 0
    }
  }
}

element 表示拖拽的元素,x 和 y 分別為計算所需的變數。

獲取滑鼠位置的函式:

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,而是要用三元表示式。這是因為有些情況下,上述兩者可能都不存在,比如當滑鼠移到瀏覽器左邊緣的時候,就無法獲得位置:

在這裡插入圖片描述

獲取滑鼠位置的函式寫完後,就可以寫拖拽的函數了:

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 上新增一個物件記錄每次拖拽後的位置即可,每當點選一個拖拽元素時,就將該元素的資訊注入 document.dragInfo。

this.element.dragPostion = {
	x: 0,y: 0
}

綜上,我們已經解決了最核心的流程描述與演算法部分,接下來只要編碼就可以了。

二、編碼實現

請根據之前說的思路,自行閱讀程式碼,整體邏輯還是非常清晰的,如果有一些細節不懂,可以在評論區提出,或者我有空了再補充。

class DragElement {
  constructor(element) {
    this.element = element
    document.dragInfo = {
      element: this.element,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)
    }
  }

  // 為元素配置相應的拖拽控制函式
  dragActive = () => {
    if (!this.element) return
    this.element.style.display = "block" 
    this.element.addEventListener('mousedown',this.dragStart,false)
    this.element.addEventListener('touchstart',false)
    this.element.addEventListener('mouseup',this.dragEnd,false) // 釋放
    this.element.addEventListener('touchend',false)
    this.element.addEventListener('touchcancel',false)
    // 為該元素新增一個物件,儲存當前位置
    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
  }

  // 點選捕獲拖拽元素,初始化相應資訊
  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 上,防止滑鼠過快可能導致的元素跟丟
    document.addEventListener('mousemove',this.dragMoving,false)
    document.addEventListener('touchmove',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,0)';
  }

  // 關閉拖拽
  dragEnd = () => {
    // 儲存當前位置
    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

到此這篇關於JavaScript 實現拖拽效果元件功能(相容移動端)的文章就介紹到這了,更多相關JavaScript 拖拽效果元件內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!