1. 程式人生 > 實用技巧 >純 JS 實現放大縮小拖拽採坑之旅

純 JS 實現放大縮小拖拽採坑之旅

前言

最近團隊需要做一個智慧客服懸浮窗功能,需要支援拖動、放大縮小等功能,因為這個是全域性外掛,為了相容性考慮全部使用原生js實現,不引用任何第三方庫或者外掛。開發過程中遇到的一些問題及解決方法,在這裡和大家分享交流一下。

注:下文出現的“採寶”二字,為這個功能的產品名。

先看效果

看這個效果,相信大部分開發都會覺得實現起來比較容易。在實際開發中,筆者總結了三個主要的坑點,及其解決方案。

三個坑點

  • 拖拽採寶時會導致採寶放大縮小
  • 採寶顯示在螢幕邊界時被遮擋顯示不全
  • 採寶放大和縮小後,位置發生變化

(一)拖拽時會導致採寶放大縮小

我們在操作採寶時,不管是滑鼠拖動還是點選放大縮小,我們的事件都需要繫結在採寶頭部的圖示上,這樣我們就需要在圖示上同時繫結點選和拖拽事件。但是當我們直接新增 click 事件和 mousedown 事件的時候,我們發現在觸發 mousedown 事件的時候,也會去觸發 click 事件。這樣就會出現在拖動採寶的時候,採寶會放大和縮小。

這個效果是我們不想看到的,所以我們就需要區分開採寶上的 click 事件和 mousedown 事件,想辦法使兩個事件的觸發相互不影響。

所以我們在同一個div上同時繫結 mousedown 事件和 click 事件,然後通過控制檯輸出每個事件,檢視過程中的每個事件的觸發順序。

const moveBox = document.querySelector('.move');
moveBox.onmousedown = function (evt) {
    console.log('觸發滑鼠按下')

  moveBox.onmousemove = function (evt) {
    console.log('觸發滑鼠拖動')
  }
}

function moveBoxClick(e) {
    console.log('觸發click')
}

moveBox.onmouseup = function () {
    console.log('觸發滑鼠抬起')
}

然後我們得到的結果是:

通過控制檯的輸出情況,我們就可以看到滑鼠點選後的各個事件觸發情況:首先執行的是 mousedown 事件,然後是 mousemove 事件,再然後是 mouseup 事件,最後是 click 事件。

知道了事件的觸發順序,我們就可以通過設定一個變數 isMove 來區分開滑鼠的拖動事件和點選事件,每次滑鼠按下的時候我們將 isMove 復原,滑鼠移動的時候將 isMove 的狀態改變。

因為每次觸發 click 事件的時候也都會去先去觸發 mousedown 事件,所以我們在 click 事件裡增加一個判斷,滑鼠移動時,不觸發 click 事件。這樣就可以把 click 事件和 mousedown 事件區分開來,實現 mousedown 和 click 事件的隔離。

click 事件增加判斷

function moveBoxClick(e) {
  // 點選採寶
  const target = document.querySelector(".move");
  const smallImg = document.querySelector(".small-img");
  const magnifyImg = document.querySelector(".magnify-img");
  // 點選move盒子
  if (!isMove) {
    if (isBig) {
      smallImg.style.display = "block";
      magnifyImg.style.display = "none";
      target.style.width = "32px";
    } else {
      smallImg.style.display = "none";
      magnifyImg.style.display = "block";
      target.style.width = "130px";
    }
    isBig = !isBig;
  }
}

mousedown 事件重置 isMove 和 mousemove 改變 isMove

let isMove = false; // 是否是拖動
let isBig = false; // 是否是變大的盒子
let isMove = false; // 判斷是否移動採寶
smallImg.onmousedown = magnifyImg.onmousedown = function(evt) {
  isMove = false; // 每次滑鼠按下時,重置isMove
  document.onmousemove = function(e) {
    isMove = true; // 每次滑鼠移動時,改變isMove
  };
};

通過 isMove 的狀態,我們就可以區分開 mousemove 事件和 click 事件,使得我們在拖動採寶的時候,可以不去觸發採寶放大縮小。

(二)採寶顯示在螢幕邊界時被遮擋顯示不全

我們在拖動採寶時,判斷採寶拖動的當前定位座標是否超出了當前顯示屏的高度和寬度,我們需要限制採寶拖動的最大距離。小採寶在點選放大時,也需要做一下處理,把採寶全部顯示出來。

拖動時

const moveBox = document.querySelector(".move");
const smallImg = document.querySelector(".move .small-img");
const magnifyImg = document.querySelector(".move .magnify-img");
let isMove = false; // 是否是拖動
let isBig = false; // 是否是變大的盒子

smallImg.onmousedown = magnifyImg.onmousedown = function(evt) {
  // 拖動div盒子
  const clientX = evt.clientX;
  const clientY = evt.clientY;
  const pageX = moveBox.offsetLeft;
  const pageY = moveBox.offsetTop;
  const x = clientX - pageX;
  const y = clientY - pageY;

  document.onmousemove = function(e) {
    // 拖動後採寶的座標
    let _x = e.clientX - x;
    let _y = e.clientY - y;
    const boxWidth = moveBox.offsetWidth;
    const boxHeight = moveBox.offsetHeight;
    if (_x < 0) {
      _x = 0;
    }
    // X座標的最大值
    if (_x > window.screen.width - boxWidth) {
      _x = window.screen.width - boxWidth;
    }
    if (_y < 0) {
      _y = 0;
    }
    // Y座標的最大值
    if (_y > document.documentElement.clientHeight - boxHeight) {
      _y = document.documentElement.clientHeight - boxHeight;
    }
  };
};

小採寶在邊界放大時

// 點選時,判斷採寶是否超出顯示屏
function autoPotion () {
  let x = moveBox.offsetLeft;
  let y = moveBox.offsetTop;

  if (x < 0) {
      x = 0;
  } else if (x > document.documentElement.clientWidth - moveBox.offsetWidth) {
      x = document.documentElement.clientWidth - moveBox.offsetWidth;
  }

  if (y < 0) {
      y = 0;
  } else if (y > document.documentElement.clientHeight - moveBox.offsetHeight) {
      y = document.documentElement.clientHeight - moveBox.offsetHeight;
  }

  moveBox.style.left = x + "px";
  moveBox.style.top = y + "px";
}

資源搜尋網站大全 https://www.renrenfan.com.cn 廣州VI設計公司https://www.houdianzi.com

(三)採寶放大和縮小後,位置發生變化

通過上圖,我們可以看到,當小採寶處在顯示屏邊界時,點選放大後再點選縮小,我們發現採寶的位置發生了變化。這個是因為採寶是根據左上角的座標來定位的,當小採寶移動到右下角時,點選放大以後,採寶左上角的座標發生了變化,這樣就使得采寶在放大縮小時,位置在發生變化。所以,我們在採寶移動完成時需要記錄採寶左上角的座標,在點選時,需要將採寶上次移動完成的座標重新賦值給採寶,這樣就使得采寶在放大縮小時,位置不會發生變化。

這樣,我們把每次 mouseup 事件的時候記錄下採寶的位置,這樣我們解決了採寶放大縮小時位置發生變化的問題。

完整的程式碼

html:

<div class="box">
  <div class="move">
    <img
      onclick="moveBoxClick()"
      class="small-img"
      draggable="false"
      src="https://zcy-cdn.oss-cn-shanghai.aliyuncs.com/f2e-assets/103bbf76-6248-421c-a3d6-28a525c459db.png"
      alt=""
    />
    <img
      onclick="moveBoxClick()"
      class="magnify-img"
      draggable="false"
      src="https://zcy-cdn.oss-cn-shanghai.aliyuncs.com/f2e-assets/90e26f49-9824-4443-b4aa-8aa64a3c8690.png"
      alt=""
    />
    <div class="content"></div>
  </div>
</div>

JavaScript

const moveBox = document.querySelector(".move");
const smallImg = document.querySelector(".move .small-img");
const magnifyImg = document.querySelector(".move .magnify-img");
var initX = 0; // 記錄小採寶的x座標
var initY = 0; // 記錄小採寶的y座標
let isMove = false; // 是否是拖動
let isBig = false; // 是否是變大的盒子
    
smallImg.onmousedown = magnifyImg.onmousedown = function(evt) {
      // 拖動div盒子
  const clientX = evt.clientX;
  const clientY = evt.clientY;
  const pageX = moveBox.offsetLeft;
  const pageY = moveBox.offsetTop;
  const x = clientX - pageX;
  const y = clientY - pageY;

  isMove = false;

  document.onmousemove = function(e) {
    const boxWidth = moveBox.offsetWidth;
    const boxHeight = moveBox.offsetHeight;
    let _x = e.clientX - x;
    let _y = e.clientY - y;
    if (_x < 0) {
      _x = 0;
    }
    if (_x > window.screen.width - boxWidth) {
      _x = window.screen.width - boxWidth;
    }
    if (_y < 0) {
      _y = 0;
    }
    if (_y > document.documentElement.clientHeight - boxHeight) {
      _y = document.documentElement.clientHeight - boxHeight;
    }

    if (isBig) {
      initX = _x;
      initY = _y;
    }

    moveBox.style.left = _x + "px";
    moveBox.style.top = _y + "px";

    isMove = true;
  };
};


document.onmouseup = function() {
  if (isMove) {
    initX = moveBox.offsetLeft;
    initY = moveBox.offsetTop;
  }
  document.onmousemove = null;
};

function moveBoxClick(e) {
  const target = document.querySelector(".move");
  const smallImg = document.querySelector(".small-img");
  const magnifyImg = document.querySelector(".magnify-img");
  // 點選move盒子
  if (!isMove) {
    if (isBig) {
      smallImg.style.display = "block";
      magnifyImg.style.display = "none";
      target.style.width = "32px";
      target.style.left = initX + 'px';
      target.style.top = initY + 'px';
    } else {
      smallImg.style.display = "none";
      magnifyImg.style.display = "block";
      target.style.width = "130px";
    }
    isBig = !isBig;

    setTimeout(() => {
      autoPotion();
    }, 100)
  }
}

// 點選時,判斷採寶是否超出顯示屏
function autoPotion () {
  let x = moveBox.offsetLeft;
  let y = moveBox.offsetTop;

  if (x < 0) {
    x = 0;
  } else if (x > document.documentElement.clientWidth - moveBox.offsetWidth) {
    x = document.documentElement.clientWidth - moveBox.offsetWidth;
  }

  if (y < 0) {
    y = 0;
  } else if (y > document.documentElement.clientHeight - moveBox.offsetHeight) {
    y = document.documentElement.clientHeight - moveBox.offsetHeight;
  }

  moveBox.style.left = x + "px";
  moveBox.style.top = y + "px";
}