使用原生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 相关文章推荐
解决使用attachEvent函数时,this指向被绑定的元素的问题的方法
Aug 13 Javascript
javascript 新浪背投广告实现代码
Jul 07 Javascript
基于jquery的多彩百分比 动态进度条 投票效果显示效果实现代码
Aug 28 Javascript
现如今最流行的JavaScript代码规范
Mar 08 Javascript
node.js中的fs.openSync方法使用说明
Dec 17 Javascript
JavaScript动态添加style节点的方法
Jun 09 Javascript
jQuery之动画效果大全
Nov 09 Javascript
JS倒计时实例_天时分秒
Aug 22 Javascript
vue项目实现记住密码到cookie功能示例(附源码)
Jan 31 Javascript
JsChart组件使用详解
Mar 04 Javascript
微信小程序云开发使用方法新手初体验
May 16 Javascript
vscode 配置vue+vetur+eslint+prettier自动格式化功能
Mar 23 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中的phpinfo()函数
2013/06/06 PHP
php使用递归计算文件夹大小
2014/12/24 PHP
PHP之浮点数计算比较以及取整数不准确的解决办法
2015/07/29 PHP
PHP使用strtotime获取上个月、下个月、本月的日期
2015/12/30 PHP
php把字符串指定字符分割成数组的方法
2018/03/12 PHP
PHP排序算法之堆排序(Heap Sort)实例详解
2018/04/21 PHP
PHP中上传文件打印错误错误类型分析
2019/04/14 PHP
PHP面向对象程序设计中的self、static、parent关键字用法分析
2019/08/14 PHP
PHP dirname(__FILE__)原理及用法解析
2020/10/28 PHP
使用Json比用string返回数据更友好,也更面向对象一些
2011/09/13 Javascript
jquery实现的一个简单进度条效果实例
2014/05/12 Javascript
JavaScript实现算术平方根算法-代码超简单
2015/09/11 Javascript
jquery动态遍历Json对象的属性和值的方法
2016/07/27 Javascript
jQuery实用小技巧_输入框文字获取和失去焦点的简单实例
2016/08/25 Javascript
ExtJS 4.2 Grid组件单元格合并的方法
2016/10/12 Javascript
vuejs使用FormData实现ajax上传图片文件
2017/08/08 Javascript
Layui 动态禁止select下拉的例子
2019/09/03 Javascript
swiper4实现移动端导航栏tab滑动切换
2020/10/16 Javascript
python使用opencv进行人脸识别
2017/04/07 Python
Python中使用haystack实现django全文检索搜索引擎功能
2017/08/26 Python
pandas 像SQL一样使用WHERE IN查询条件说明
2020/06/05 Python
无惧面试,带你搞懂python 装饰器
2020/08/17 Python
python 用struct模块解决黏包问题
2020/11/07 Python
eBay荷兰购物网站:eBay.nl
2020/06/26 全球购物
党员的自我评价范文
2014/01/02 职场文书
党员学习中共十八大报告思想汇报
2014/09/15 职场文书
学习教师敬业奉献模范事迹材料思想汇报
2014/09/19 职场文书
2014统计局民主生活会对照检查材料思想汇报
2014/10/02 职场文书
2015公务员试用期工作总结
2014/12/12 职场文书
师德标兵事迹材料
2014/12/19 职场文书
村干部任职承诺书
2015/01/21 职场文书
党支部对转正的意见
2015/06/02 职场文书
美甲店的创业计划书模板
2019/08/23 职场文书
2019年教师节活动策划方案
2019/09/09 职场文书
mysql部分操作
2021/04/05 MySQL
CocosCreator入门教程之网络通信
2021/04/16 Javascript