使用原生js编写一个简单的框选功能方法


Posted in Javascript onMay 13, 2019

今天我们来聊一下怎么使用原生javascript编写一个简单的框选功能。

需求描述

  • 鼠标左键按下不放,移动鼠标出现矩形选框;
  • 鼠标左键松开,根据上边出现的矩形选框统计选框范围内的DOM元素;

嗯...上边的功能描述看着是挺简单的,但实现起来也还是会有些地方需要斟酌思考的。比如,如果我们的框选范围不是document.body,而是某一个div里边进行框选呢?而现实开发过程中,我们会遇上的应该就是第二种情况。

点击查看完整的源码

怎么实现

二话不说,咱们动手写代码吧!因为更好的兼容性,这里就避免了一些ES6的语法,如果是用的其他框架来写的话,代码上相应的也要做一些调整。

<head>
<style>
.fileDiv {
 display: inline-block;
 width: 100px;
 height: 100px;
 margin: 24px;
 background-color: blue;
}
</style>
</head>
<body>
 <div class="fileDiv"></div>
 <div class="fileDiv"></div>
 <div class="fileDiv"></div>
 <div class="fileDiv"></div>
 <div class="fileDiv"></div>
 <div class="fileDiv"></div>
 <div class="fileDiv"></div>
 <div class="fileDiv"></div>
</body>

添加鼠标事件监听

由于js自身并没有带有鼠标点击按住不放这样子的事件,这里我们不仅需要检测鼠标左键点击按下,还要加一个定时器来检测鼠标是否按住不放了。

<script>
 (function () {
  // 定时器id
  var mouseStopId;
  // 是否开启框选功能
  var mouseOn = false;
  // 用来存放鼠标点击初始位置
  var startX = 0;
  var startY = 0;
  // 添加鼠标按下监听事件
  document.body.addEventListener('mousedown', function (e) {
   // 阻止事件冒泡
   clearEventBubble(e);
   // 判断是否为鼠标左键被按下
   if (e.buttons !== 1 || e.which !== 1) return;

   mouseStopId = setTimeout(function () {
    mouseOn = true;
    startX = e.clientX;
    startY = e.clientY;
   }, 300); // 间隔300毫秒后执行,判定这时候鼠标左键被按住不放
  });

  // 添加鼠标移动事件监听
  document.body.addEventListener('mousemove', function (e) {
   // 如果并非框选开启,退出
   if (!mouseOn) return;
   // 阻止事件冒泡
   clearEventBubble(e);
   // 处理鼠标移动
   // codes
  });

  // 添加鼠标点击松开事件监听
  document.body.addEventListener('mouseup', function (e) {
   // 阻止事件冒泡
   clearEventBubble(e);
   // 处理鼠标点击松开
   // codes
  });

  function clearEventBubble (e) {
   if (e.stopPropagation) e.stopPropagation();
   else e.cancelBubble = true;

   if (e.preventDefault) e.preventDefault();
   else e.returnValue = false;
  }
 })();
</script>

添加框选可视化元素

使用原生js编写一个简单的框选功能方法

框选可视化元素示意图

我们有了事件监听还不够,为了更好的交互效果,我们需要一个随时跟随着鼠标移动的框选框元素,用于让用户随时感知框选范围。

<script>
 (function () {
  var mouseStopId;
  var mouseOn = false;
  var startX = 0;
  var startY = 0;
  document.body.addEventListener('mousedown', function (e) {
   clearEventBubble(e);
   if (e.buttons !== 1 || e.which !== 1) return;

   mouseStopId = setTimeout(function () {
    mouseOn = true;
    startX = e.clientX;
    startY = e.clientY;
    // 创建一个框选元素
    var selDiv = document.createElement('div');
    // 给框选元素添加css样式,这里使用绝对定位
    selDiv.style.cssText = 'position:absolute;width:0;height:0;margin:0;padding:0;border:1px dashed #eee;background-color:#aaa;z-index:1000;opacity:0.6;display:none;';
    // 添加id
    selDiv.id = 'selectDiv';
    document.body.appendChild(selDiv);
    // 根据起始位置,添加定位
    selDiv.style.left = startX + 'px';
    selDiv.style.top = startY + 'px';
   }, 300);
  });

  document.body.addEventListener('mousemove', function (e) {
   if (!mouseOn) return;
   clearEventBubble(e);
   // 获取当前坐标
   var _x = e.clientX;
   var _y = e.clientY;
   // 根据坐标给选框修改样式
   var selDiv = document.getElementById('selectDiv');
   selDiv.style.display = 'block';
   selDiv.style.left = Math.min(_x, startX) + 'px';
   selDiv.style.top = Math.min(_y, startY) + 'px';
   selDiv.style.width = Math.abs(_x - startX) + 'px';
   selDiv.style.height = Math.abs(_y - startY) + 'px';
   // 如果需要更直观一点的话,我们还可以在这里进行对框选元素覆盖到的元素进行修改被框选样式的修改。
  });

  document.body.addEventListener('mouseup', function (e) {
   clearEventBubble(e);
  });

  function clearEventBubble (e) {
   if (e.stopPropagation) e.stopPropagation();
   else e.cancelBubble = true;

   if (e.preventDefault) e.preventDefault();
   else e.returnValue = false;
  }
 })();
</script>

添加鼠标松开事件监听

使用原生js编写一个简单的框选功能方法

元素是否被选中示意图

我们没有在鼠标移动的时候去实时统计被框选到的DOM元素,如果需要实时统计或者实时修改被选择的DOM元素的样式,以便更准确的让用户感知到被框选的内容的话,可以选择在mousemove事件里边去实现以下代码:

<script>
 (function () {
  var mouseStopId;
  var mouseOn = false;
  var startX = 0;
  var startY = 0;
  document.onmousedown = function (e) {
   clearEventBubble(e);
   if (e.buttons !== 1 || e.which !== 1) return;

   mouseStopId = setTimeout(function () {
    mouseOn = true;
    startX = e.clientX;
    startY = e.clientY;
    var selDiv = document.createElement('div');
    selDiv.style.cssText = 'position:absolute;width:0;height:0;margin:0;padding:0;border:1px dashed #eee;background-color:#aaa;z-index:1000;opacity:0.6;display:none;';
    selDiv.id = 'selectDiv';
    document.body.appendChild(selDiv);
    selDiv.style.left = startX + 'px';
    selDiv.style.top = startY + 'px';
   }, 300);
  }

  document.onmousemove = function (e) {
   if (!mouseOn) return;
   clearEventBubble(e);
   var _x = e.clientX;
   var _y = e.clientY;
   var selDiv = document.getElementById('selectDiv');
   selDiv.style.display = 'block';
   selDiv.style.left = Math.min(_x, startX) + 'px';
   selDiv.style.top = Math.min(_y, startY) + 'px';
   selDiv.style.width = Math.abs(_x - startX) + 'px';
   selDiv.style.height = Math.abs(_y - startY) + 'px';
  };

  // 添加鼠标松开事件监听
  document.onmouseup = function (e) {
   if (!mouseOn) return;
   clearEventBubble(e);
   var selDiv = document.getElementById('selectDiv');
   var fileDivs = document.getElementsByClassName('fileDiv');
   var selectedEls = [];
   // 获取参数
   var l = selDiv.offsetLeft;
   var t = selDiv.offsetTop;
   var w = selDiv.offsetWidth;
   var h = selDiv.offsetHeight;
   for (var i = 0; i < fileDivs.length; i++) {
    var sl = fileDivs[i].offsetWidth + fileDivs[i].offsetLeft;
    var st = fileDivs[i].offsetHeight + fileDivs[i].offsetTop;

    if (sl > l && st > t && fileDivs[i].offsetLeft < l + w && fileDivs[i].offsetTop < t + h) {
     // 该DOM元素被选中,进行处理
     selectedEls.push(fileDivs[i]);
    }
   }
   // 打印被选中DOM元素
   console.log(selectedEls);
   // 恢复参数
   selDiv.style.display = 'none';
   mouseOn = false;
  };

  function clearEventBubble (e) {
   if (e.stopPropagation) e.stopPropagation();
   else e.cancelBubble = true;

   if (e.preventDefault) e.preventDefault();
   else e.returnValue = false;
  }
 })();
</script>

这里判断一个元素是否被选中采用的判断条件是:

  • 该DOM元素的最右边(fileDiv[i].offsetLeft + fileDiv[i].offsetWidth)是否要比选框元素最左边(selDiv.offsetLeft)的位置要小;
  • 该DOM元素的最下边(fileDiv[i].offsetTop + fileDiv[i].offsetHeight)是否要比选框元素的最上边(selDiv.offsetTop)的位置要大;
  • 该DOM元素的最左边(fileDiv[i].offsetLeft)是否要比选框元素的最后边(selDiv.offsetLeft + selDiv.offsetWidth)的位置数值要小;
  • 该DOM元素的最上边(fileDiv[i].offsetTop)是否要比选框元素的最下边(selDiv.offsetTop + selDiv.offsetHeight)的位置数值要小;

满足了以上四个条件,即可判别为该DOM元素被选中了。

实际应用

上边的例子,举得有些过于简单了。实际的开发当中,框选的范围往往不可能是整个document.body,而是某一个具体的有特定宽度跟高度限制的元素。这个时候,就还需要考虑这个框选容器元素造成的定位偏差,以及容器内元素过多,出现滚动条的情况了。

乍一看,上边的情况需要考虑的因素多了不少,比较容易乱。我这里采用的方法是修改坐标系的方式来实现上边描述的功能。上文我们已经实现了在document.body整个页面左上角顶点作为坐标原点来实现框选功能,这时候我们需要修改坐标原点为框选容器的左上角顶点作为坐标原点即可。

换言之,就是修改mousedown跟mousemove事件时,初始位置由原来的e.clientX跟e.clientY修改为e.clientX - selectContaienr.offsetLeft + selectContainer.scrollLeft跟e.clientY - selectContainer.offsetTop + selectContainer.scrollTop。

使用原生js编写一个简单的框选功能方法

坐标更改shi'yi'tu

<html>
 <head>
  <title>region</title>
  <style>
   body {
    margin: 0;
    padding: 0;
   }
   #selectContainer {
    position: relative;
    width: 400px; /* 演示宽高与位置 */
    height: 400px;
    top: 200px;
    left: 200px;
    border: 1px solid #eee;
    overflow: hidden;
    overflow-y: auto;
   }
   .fileDiv {
    display: inline-block;
    width: 100px;
    height: 100px;
    margin: 24px;
    background-color: #0082CC;
   }
  </style>
 </head>
 <body>
  <div id="selectContainer">
   <div class="fileDiv"></div>
   <div class="fileDiv"></div>
   <div class="fileDiv"></div>
   <div class="fileDiv"></div>
   <div class="fileDiv"></div>
   <div class="fileDiv"></div>
   <div class="fileDiv"></div>
   <div class="fileDiv"></div>
   <div class="fileDiv"></div>
   <div class="fileDiv"></div>
   <div class="fileDiv"></div>
   <div class="fileDiv"></div>
   <div class="fileDiv"></div>
   <div class="fileDiv"></div>
  </div>
 </body>
</html>
<script>
 (function () {
  var mouseStopId;
  var mouseOn = false;
  var startX = 0;
  var startY = 0;
  document.onmousedown = function (e) {
   clearEventBubble(e);
   if (e.buttons !== 1 || e.which !== 1) return;

   mouseStopId = setTimeout(function () {
    mouseOn = true;
    // 获取容器元素
    var selectContainer = document.getElementById('selectContainer');
    // 调整坐标原点为容器左上角
    startX = e.clientX - selectContainer.offsetLeft + selectContainer.scrollLeft;
    startY = e.clientY - selectContainer.offsetTop + selectContainer.scrollTop;
    var selDiv = document.createElement('div');
    selDiv.style.cssText = 'position:absolute;width:0;height:0;margin:0;padding:0;border:1px dashed #eee;background-color:#aaa;z-index:1000;opacity:0.6;display:none;';
    selDiv.id = 'selectDiv';
    // 添加框选元素到容器内
    document.getElementById('selectContainer').appendChild(selDiv);
    selDiv.style.left = startX + 'px';
    selDiv.style.top = startY + 'px';
   }, 300);
  }

  document.onmousemove = function (e) {
   if (!mouseOn) return;
   clearEventBubble(e);
   var selectContainer = document.getElementById('selectContainer');
   var _x = e.clientX - selectContainer.offsetLeft + selectContainer.scrollLeft;
   var _y = e.clientY - selectContainer.offsetTop + selectContainer.scrollTop;
   var _H = selectContainer.clientHeight;
   // 鼠标移动超出容器内部,进行相应的处理
   // 向下拖拽
   if (_y >= _H && selectContainer.scrollTop <= _H) {
    selectContainer.scrollTop += _y - _H;
   }
   // 向上拖拽
   if (e.clientY <= selectContainer.offsetTop && selectContainer.scrollTop > 0) {
    selectContainer.scrollTop = Math.abs(e.clientY - selectContainer.offsetTop);
   }
   var selDiv = document.getElementById('selectDiv');
   selDiv.style.display = 'block';
   selDiv.style.left = Math.min(_x, startX) + 'px';
   selDiv.style.top = Math.min(_y, startY) + 'px';
   selDiv.style.width = Math.abs(_x - startX) + 'px';
   selDiv.style.height = Math.abs(_y - startY) + 'px';
  };

  document.onmouseup = function (e) {
   if (!mouseOn) return;
   clearEventBubble(e);
   var selDiv = document.getElementById('selectDiv');
   var fileDivs = document.getElementsByClassName('fileDiv');
   var selectedEls = [];
   var l = selDiv.offsetLeft;
   var t = selDiv.offsetTop;
   var w = selDiv.offsetWidth;
   var h = selDiv.offsetHeight;
   for (var i = 0; i < fileDivs.length; i++) {
    var sl = fileDivs[i].offsetWidth + fileDivs[i].offsetLeft;
    var st = fileDivs[i].offsetHeight + fileDivs[i].offsetTop;

    if (sl > l && st > t && fileDivs[i].offsetLeft < l + w && fileDivs[i].offsetTop < t + h) {
     selectedEls.push(fileDivs[i]);
    }
   }
   console.log(selectedEls);
   selDiv.style.display = 'none';
   mouseOn = false;
  };

  function clearEventBubble (e) {
   if (e.stopPropagation) e.stopPropagation();
   else e.cancelBubble = true;

   if (e.preventDefault) e.preventDefault();
   else e.returnValue = false;
  }
 })();
</script>

使用前端框架

上边的代码,我们只是在一个html文件里边实现了框选的功能。很多时候,我们会使用一些前端框架来编写框选的功能(例如vue.js,angular,react,polymer之类的前端框架)。这个时候,我们可以利用框架自身的生命周期的函数,添加对应的监听事件,然后在mouseup事件里移除掉上边这些事件监听,以减少不必要的资源消耗。而且,很多时候,组件化的使用,使得被框选的元素,往往也是一个可重复利用的小组件,也是需要根据相应的框架的对应的途径获取到对应的DOM元素来获取其属性。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
javascript中比较字符串是否相等的方法
Jul 23 Javascript
js 点击页面其他地方关闭弹出层(示例代码)
Dec 24 Javascript
使用GruntJS构建Web程序之Tasks(任务)篇
Jun 06 Javascript
extjs每个组件要设置唯一的ID否则会出错
Jun 15 Javascript
网页中表单按回车就自动提交的问题的解决方案
Nov 03 Javascript
Bootstrap学习笔记之css样式设计(2)
Jun 07 Javascript
jQuery实现图片滑动效果
Mar 08 Javascript
深入对Vue.js $watch方法的理解
Mar 20 Javascript
利用jquery如何从json中读取数据追加到html中
Dec 01 jQuery
用node-webkit把web应用打包成桌面应用(windows环境)
Feb 01 Javascript
JavaScript工具库MyTools详解
Jan 01 Javascript
深入解读VUE中的异步渲染的实现
Jun 19 Javascript
Node.js实现一个HTTP服务器的方法示例
May 13 #Javascript
jQuery事件绑定和解绑、事件冒泡与阻止事件冒泡及弹出应用示例
May 13 #jQuery
Vue表单绑定的实例代码(单选按钮,选择框(单选时,多选时,用 v-for 渲染的动态选项)
May 13 #Javascript
JavaScript封闭函数及常用内置对象示例
May 13 #Javascript
vue 表单之通过v-model绑定单选按钮radio
May 13 #Javascript
JS中的函数与对象的创建方式
May 12 #Javascript
详解无限滚动插件vue-infinite-scroll源码解析
May 12 #Javascript
You might like
关于PHP语言构造器介绍
2013/07/08 PHP
PHP+sqlite数据库操作示例(创建/打开/插入/检索)
2016/05/26 PHP
PHP实现模拟http请求的方法分析
2017/12/20 PHP
Laravel框架控制器的middleware中间件用法分析
2019/09/30 PHP
Prototype使用指南之hash.js
2007/01/10 Javascript
通过jQuery打造支持汉字,拼音,英文快速定位查询的超级select插件
2010/06/18 Javascript
jquery如何改变html标签的样式(两种实现方法)
2013/01/16 Javascript
jQuery .attr()和.removeAttr()方法操作元素属性示例
2013/07/16 Javascript
JavaScript获取当前cpu使用率的方法
2015/12/15 Javascript
js代码实现点击按钮出现60秒倒计时
2021/01/28 Javascript
JavaScript实战之菜单特效
2016/08/16 Javascript
node.js的exports、module.exports与ES6的export、export default深入详解
2017/10/26 Javascript
微信小程序 JS动态修改样式的实现方法
2018/12/16 Javascript
判断“命令按钮”是否被鼠标单击详解
2019/07/31 Javascript
Vue使用富文本编辑器Vue-Quill-Editor(含图片自定义上传服务、清除复制粘贴样式等)
2020/05/15 Javascript
[10:05]DOTA2-DPC中国联赛 正赛 iG vs PSG.LGD 选手采访
2021/03/11 DOTA
常用python数据类型转换函数总结
2014/03/11 Python
python中的__slots__使用示例
2015/02/26 Python
带你认识Django
2019/01/15 Python
Django处理多用户类型的方法介绍
2019/05/18 Python
python单例模式原理与创建方法实例分析
2019/10/26 Python
Win下PyInstaller 安装和使用教程
2019/12/25 Python
PyTorch中topk函数的用法详解
2020/01/02 Python
Python如何生成xml文件
2020/06/04 Python
python自动从arxiv下载paper的示例代码
2020/12/05 Python
纯DOM+CSS3实现简单的小风车动画
2016/09/27 HTML / CSS
英国性感内衣和睡衣品牌:Bluebella
2018/01/26 全球购物
如何拷贝一整个Java对象,包括它的状态
2013/12/27 面试题
与C++相比,Java中的数组有什么不同
2014/03/25 面试题
毕业生优秀推荐信
2013/11/26 职场文书
后勤服务中心总经理工作职责
2014/03/03 职场文书
电焊工岗位职责
2014/03/06 职场文书
毕业生面试求职信
2014/06/23 职场文书
新农村建设标语
2014/06/24 职场文书
SQL中的三种去重方法小结
2021/11/01 SQL Server
Go语言基础map用法及示例详解
2021/11/17 Golang