使用原生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 OOP面向对象介绍
Dec 02 Javascript
原生js实现淘宝首页点击按钮缓慢回到顶部效果
Apr 06 Javascript
JavaScript 变量、作用域及内存
Apr 08 Javascript
JavaScript数据类型学习笔记
Jan 25 Javascript
jQuery ajaxSubmit 实现ajax提交表单局部刷新
Jul 04 Javascript
移动端脚本框架Hammer.js
Dec 15 Javascript
Vue声明式渲染详解
May 17 Javascript
详解使用Vue Router导航钩子与Vuex来实现后退状态保存
Sep 11 Javascript
React-intl 实现多语言的示例代码
Nov 03 Javascript
vue文件运行的方法教学
Feb 12 Javascript
详解mpvue中使用vant时需要注意的onChange事件的坑
May 16 Javascript
ES6 Promise对象概念及用法实例详解
Oct 15 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
织梦sitemap地图实时推送给百度的教程
2015/08/03 PHP
PHP数组函数array_multisort()用法实例分析
2016/04/02 PHP
微信红包随机生成算法php版
2016/07/21 PHP
PHP使用Redis实现防止大并发下二次写入的方法
2017/10/09 PHP
基于jQuery图片平滑连续滚动插件
2009/04/27 Javascript
JS控件ASP.NET的treeview控件全选或者取消(示例代码)
2013/12/16 Javascript
Jquery 监视按键,按下回车键触发某方法的实现代码
2014/05/11 Javascript
Python脚本后台运行的几种方式
2015/03/09 Javascript
js实现的星星评分功能函数
2015/12/09 Javascript
jQuery与JS加载事件用法分析
2016/09/04 Javascript
详解XMLHttpRequest(二)响应属性、二进制数据、监测上传下载进度
2016/09/14 Javascript
javascript贪吃蛇游戏设计与实现
2020/09/17 Javascript
Python群发邮件实例代码
2014/01/03 Python
用Python登录Gmail并发送Gmail邮件的教程
2015/04/17 Python
举例讲解Python中字典的合并值相加与异或对比
2016/06/04 Python
关于python pyqt5安装失败问题的解决方法
2017/08/08 Python
Python如何发布程序的详细教程
2018/10/09 Python
Python实现分段线性插值
2018/12/17 Python
Python3.5 Json与pickle实现数据序列化与反序列化操作示例
2019/04/29 Python
python实现随机漫步方法和原理
2019/06/10 Python
使用OpenCV-python3实现滑动条更新图像的Canny边缘检测功能
2019/12/12 Python
Python2和Python3中@abstractmethod使用方法
2020/02/04 Python
python matlab库简单用法讲解
2020/12/31 Python
光荣入党自我鉴定
2014/01/22 职场文书
先进班级集体事迹材料
2014/01/30 职场文书
个人自我鉴定总结
2014/03/25 职场文书
房屋租赁协议书
2014/04/10 职场文书
微笑面对生活演讲稿
2014/05/13 职场文书
闪闪的红星观后感
2015/06/08 职场文书
婚庆公司开业主持词
2015/06/30 职场文书
你会写报告?产品体验报告到底该怎么写?
2019/08/14 职场文书
解决Jupyter-notebook不弹出默认浏览器的问题
2021/03/30 Python
Java spring单点登录系统
2021/09/04 Java/Android
Python 中的 copy()和deepcopy()
2021/11/07 Python
javascript Number 与 Math对象的介绍
2021/11/17 Javascript
HTTP中的Content-type详解
2022/01/18 HTML / CSS