基於javascript的拖拽類封裝

在公司作一個h5編輯平臺,中間須要對元素進行拖拽、放大縮小、旋轉等操做,且須要對文本、圖片、音樂組件等不一樣元素均可以具有這些功能。參考了不少別人寫的代碼,最後終於弄明白了其中的原理,本身也寫了一個。css

效果圖以下:html

github地址以下:拖拽類封裝代碼前端

使用方法

引入js和對應的css。真的有須要的小夥伴只要把我對應文件夾的dragger.js和dragger.css拷進本身的項目裏就能夠用啦,不限制前端框架啊,vue、react仍是html文件裏均可以使用哦vue

import Drag from '../../static/dragger.js'
import './assets/css/dragger.css'

以後,實例化react

new Drag({
    id: 'box-dragger',
    showAngle: true,
    isScale: false,
    showBorder: false
})
new Drag({
    id: 'box-dragger2',
    canZoom: false,
    canRotate: false
})
new Drag({
    id: 'img-box',
    showAngle: true,
    showPosition: true
    })
new Drag({
    id: 'test'
})

具體實現(封裝細節)

功能細節整理:

  • 旋轉
  • 縮放
  • 平移

技術難點:

  • 旋轉時要注意盒子每個點的位置發生了變化
  • 針對拖拽後的盒子的left和top都有變化,計算其left和top時需將其按照中心軸旋轉擺正,再進行計算
  • 當且僅有一個盒子是被選中的盒子,點擊哪一個選中哪一個。(當前頁面多個實例化Drag對象時,如何保證操做互不影響)
  • 實現的兩種不一樣方式:git

    • 能夠選中某元素,直接給該元素內部加上操做的點
    • 有一個pannel,選中某元素時,將這個pannel定位到該元素的位置上

這兩種方式都實現過一次,第一種比較簡單,可是第一種,很差控制選中某個元素才讓操做點展現。github

如何封裝:

考慮如何讓用戶快速上手使用,可參考的點:前端框架

  • 用戶須要傳入什麼必須的參數
  • 暴露給用戶什麼可設置的參數和方法

實現過程:

可配置參數

字段 說明 是否必填 默認值
id 目標元素id
container 父容器id body
canRotate 是否能夠旋轉 true
canZoom 是否能夠縮放 true
canPull 是否能夠拉昇 true
canMove 是否能夠平移 true
showAngle 展現角度 false
showPosition 展現位置 false
isScale 是否等比例縮放 true
showBorder 是否展現pannel的border false

屬性

  • canRotate
  • canZoom
  • canPull
  • canMove
  • showAngle
  • isScale
  • id
  • container
  • targetObj
  • pannelDom 操做divdom
  • ...

具體看圖:
app

代碼解說

  1. 初始化參數
  2. 初始化目標dom對象的位置:記錄其:框架

    • left平距左
    • top
    • width
    • height
    • angle
    • rightBottomPoint 目標dom對象右下座標
    • rightTopPoint 目標dom對象右上座標
    • leftTopPoint 目標dom對象左上座標
    • leftBottomPoint 目標dom對象左下座標
    • leftMiddlePoint 目標dom對象左中座標
    • rightMiddlePoint 目標dom對象右中座標
    • topMiddlePoint 目標dom對象上中座標
    • bottomMiddlePoint 目標dom對象下中座標
    • centerPos 目標dom對象中心點座標
  3. 初始化pannel結構
    當前的父容器中只有一個pannel結構,每次實例化對象時,會判斷一下若是當前這個父容器裏已經存在id爲pannel的結構,就將其子節點清空,按照當前實例化對象傳進來的屬性從新渲染pannel子結構。若是沒有id爲pannel的結構,就建立。
  4. 初始化事件

    • 給pannelDom和targetObj綁定mousedown事件
    • 給document綁定mousemove和mouseup事件

      initEvent () {
          document.addEventListener('mousemove', e => {
              e.preventDefault && e.preventDefault()
              this.moveChange(e, this.targetObj)
          })
          document.addEventListener('mouseup', e => {
              this.moveLeave(this.targetObj)
          })
          if (this.canMove) {
              // 外層給this.pannelDom添加mousedown事件,是在全部實例化結束後,panneldom被展現在最後一個實例化對象上,鼠標按下它時,觸發moveInit事件
              this.pannelDom.onmousedown = e => {
                  e.stopPropagation()
                  this.moveInit(9, e, this.targetObj)
              }
              this.targetObj.onmousedown = e => {
                  e.stopPropagation()
                  this.moveInit(9, e, this.targetObj)
                  this.initPannel()
                  // 在點擊其餘未被選中元素時,pannel定位到該元素上,重寫pannelDom事件,由於此時的this.pannelDom已經根據新的目標元素被重寫
                  this.pannelDom.onmousedown= e => {
                      this.moveInit(9, e, this.targetObj)
                  }
              }
          }
      }
  5. dom操做

    • 旋轉操做

      • 鼠標按下時,記錄當前鼠標位置距離box中心位置的y/x的反正切函數A1。

        this.mouseInit = {
            x: Math.floor(e.clientX),
            y: Math.floor(e.clientY)
        }
        this.preRadian = Math.atan2(this.mouseInit.y - this.centerPos.y, this.mouseInit.x - this.centerPos.x)
      • 鼠標移動時,記錄再次計算鼠標位置距離box中心位置的y/x的反正切函數A2。

        this.rotateCurrent = {
            x: Math.floor(e.clientX),
            y: Math.floor(e.clientY)
        }
        this.curRadian = Math.atan2(this.rotateCurrent.y - this.centerPos.y, this.rotateCurrent.x - this.centerPos.x)
      • 求A2-A1,求出移動的弧度

        this.tranformRadian = this.curRadian - this.preRadian
      • 求出最後box的旋轉角度,this.getRotate(target)是js中獲取某dom元素的旋轉角度的方法(粘貼過來的,親測好使)

        this.angle = this.getRotate(target) +  Math.round(this.tranformRadian * 180 / Math.PI)
        this.preRadian = this.curRadian //鼠標移動的每一下都計算這個角度,因此每一下移動前的弧度值都上一次移動後的弧度值
      • 計算旋轉後box每一個點的座標,根據餘弦公式,傳入:旋轉前每點座標,旋轉中心座標和旋轉角度

        let disAngle = this.angle - this.initAngle
        this.rightBottomPoint = this.getRotatedPoint(this.initRightBottomPoint, this.centerPos, disAngle)
        this.rightTopPoint = this.getRotatedPoint(this.initRightTopPoint, this.centerPos, disAngle)
        this.leftTopPoint = this.getRotatedPoint(this.initLeftTopPoint, this.centerPos, disAngle)
        this.leftBottomPoint = this.getRotatedPoint(this.initLeftBottomPoint, this.centerPos, disAngle)
        this.leftMiddlePoint = this.getRotatedPoint(this.initLeftMiddlePoint, this.centerPos, disAngle)
        this.rightMiddlePoint = this.getRotatedPoint(this.initRightMiddlePoint, this.centerPos, disAngle)
        this.topMiddlePoint = this.getRotatedPoint(this.initTopMiddlePoint, this.centerPos, disAngle)
        this.bottomMiddlePoint = this.getRotatedPoint(this.initBottomMiddlePoint, this.centerPos, disAngle)
    • 沿着一個方向拉昇操做。
    • 沿着一個角縮放操做。
  6. 優化,mousemove事件添加節流函數

    function throttle(fn, interval) {
        let canRun = true;
        return function () {
            if (!canRun) return;
            canRun = false;
            setTimeout(() => {
                fn.apply(this, arguments);
                canRun = true;
            }, interval);
        };
    }
    let that = this
    document.addEventListener('mousemove', throttle(function (e) {
        e.preventDefault && e.preventDefault()
        that.moveChange(e, that.targetObj)
    }, 10))

中間的沿着一個方向的拉昇操做和沿着一個角的縮放操做主要是參考了一個大佬的拖拽思想實現的 github wiki地址

補充節流函數和防抖函數的區別:

  • 節流函數:在必定規定時間段觸發過的事件就再也不觸發
  • 防抖函數:觸發了一次事件,若是在必定時間段內又觸發一次,那麼取消上一次觸發,執行此次觸發
相關文章
相關標籤/搜索