纯 JS 实现放大缩小拖拽踩坑之旅

原创不易,但愿能关注下咱们,再顺手点个赞~~

本文首发于政采云前端团队博客: 纯 JS 实现放大缩小拖拽踩坑之旅javascript

前言

最近团队须要作一个智能客服悬浮窗功能,须要支持拖动、放大缩小等功能,由于这个是全局插件,为了兼容性考虑所有使用原生 JS 实现,不引用任何第三方库或者插件。开发过程当中遇到的一些问题及解决方法,在这里和你们分享交流一下。html

注:下文出现的“采宝”二字,为这个功能的产品名。前端

先看效果

'实现效果'

看这个效果,相信大部分开发都会以为实现起来比较容易。在实际开发中,笔者总结了三个主要的坑点,及其解决方案。java

三个坑点

  • 拖拽采宝时会致使采宝放大缩小
  • 采宝显示在屏幕边界时被遮挡显示不全
  • 采宝放大和缩小后,位置发生变化

(一)拖拽时会致使采宝放大缩小

咱们在操做采宝时,不论是鼠标拖动仍是点击放大缩小,咱们的事件都须要绑定在采宝头部的图标上,这样咱们就须要在图标上同时绑定点击和拖拽事件。可是当咱们直接添加 click 事件和 mousedown 事件的时候,咱们发如今触发 mousedown 事件的时候,也会去触发 click 事件。这样就会出如今拖动采宝的时候,采宝会放大和缩小。post

点击盒子

这个效果是咱们不想看到的,因此咱们就须要区分开采宝上的 click 事件和 mousedown 事件,想办法使两个事件的触发相互不影响。性能

因此咱们在同一个 DIV 上同时绑定 mousedown 事件和 click 事件,而后经过控制台输出每一个事件,查看过程当中的每一个事件的触发顺序。ui

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('触发鼠标抬起')
}
复制代码

而后咱们获得的结果是:spa

点击盒子

经过控制台的输出状况,咱们就能够看到鼠标点击后的各个事件触发状况:首先执行的是 mousedown 事件,而后是 mousemove 事件,再而后是 mouseup 事件,最后是 click 事件。插件

知道了事件的触发顺序,咱们就能够经过设置一个变量 isMove 来区分开鼠标的拖动事件和点击事件,每次鼠标按下的时候咱们将 isMove 复原,鼠标移动的时候将 isMove 的状态改变。3d

由于每次触发 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";
}
复制代码

效果以下

'放大缩小'

(三)采宝放大和缩小后,位置发生变化

经过上图,咱们能够看到,当小采宝处在显示屏边界时,点击放大后再点击缩小,咱们发现采宝的位置发生了变化。这个是由于采宝是根据左上角的坐标来定位的,当小采宝移动到右下角时,点击放大之后,采宝左上角的坐标发生了变化,这样就使得采宝在放大缩小时,位置在发生变化。因此,咱们在采宝移动完成时须要记录采宝左上角的坐标,在点击时,须要将采宝上次移动完成的坐标从新赋值给采宝,这样就使得采宝在放大缩小时,位置不会发生变化。

'点击位置变化'

这样,咱们把每次 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";
}
复制代码

总结

经过开发一个很小的功能点,引出了不少须要处理的细节问题,提升用户体验。针对这些问题的解决方案,各位看官若是有更好的解决方案,欢迎留言讨论。

招贤纳士

政采云前端团队(ZooTeam),一个年轻富有激情和创造力的前端团队,隶属于政采云产品研发部,Base 在风景如画的杭州。团队现有 50 余个前端小伙伴,平均年龄 27 岁,近 3 成是全栈工程师,妥妥的青年风暴团。成员构成既有来自于阿里、网易的“老”兵,也有浙大、中科大、杭电等校的应届新人。团队在平常的业务对接以外,还在物料体系、工程平台、搭建平台、性能体验、云端应用、数据分析及可视化等方向进行技术探索和实战,推进并落地了一系列的内部技术产品,持续探索前端技术体系的新边界。

若是你想改变一直被事折腾,但愿开始能折腾事;若是你想改变一直被告诫须要多些想法,却无从破局;若是你想改变你有能力去作成那个结果,却不须要你;若是你想改变你想作成的事须要一个团队去支撑,但没你带人的位置;若是你想改变既定的节奏,将会是“ 5 年工做时间 3 年工做经验”;若是你想改变原本悟性不错,但老是有那一层窗户纸的模糊… 若是你相信相信的力量,相信平凡人能成就非凡事,相信能遇到更好的本身。若是你但愿参与到随着业务腾飞的过程,亲手推进一个有着深刻的业务理解、完善的技术体系、技术创造价值、影响力外溢的前端团队的成长历程,我以为咱们该聊聊。任什么时候间,等着你写点什么,发给 ZooTeam@cai-inc.com

推荐阅读

1024 巨献!!一文看尽前端过去一年的精华沉淀(700 篇好文大汇总)

多是最全的 “文本溢出截断省略” 方案合集

乾坤大挪移!React 也能 “用上” computed 属性

相关文章
相关标签/搜索