【step by step】100行代码撸一个矩形选框效果

241 阅读5分钟

规矩依旧,先上效果!!

demo.gif

step by step第三弹, 100行代码撸一个动态矩形选框效果(应该叫这个名字吧~)。

动态选框通常用来一次框选多个内容,我们在PC电脑上经常会看到这个功能,在一些和多文件相关的web系统中ye非常常见。我们先不关心怎么选文件,先看看这个跟随鼠标的框选效果怎么实现。

基本思路

首先,有几个基本点要明确:

  1. 选框是个矩形
  2. 它要脱离文档流,不能影响其他元素
  3. 它的位置和尺寸是跟随鼠标移动变化的

前两点都很好解决,矩形的基本元素我这里选的是使用svg渲染,svg后期很好扩展(比如加选框网格线,四角的拖动方块等,几乎不渲染css辅助,可移植性高),脱离文档流就绝对定位,层级设置高一些。
第三点, 位置和尺寸是重点了。要想动态绘制矩形,我们就要知道矩形的宽、高,和起始坐标(用于确定位置), 而这些,我们都可以通过鼠标的坐标信息获得,在鼠标按下,抬起,移动等过程中,我们都可以通过event.clientXevent.clientY来获取鼠标相对于屏幕的坐标。

Group 1.png

要计算矩形的尺寸,就要知道两组坐标,也就是起始坐标和终点坐标。

Group 2.png

起始坐标在鼠标按下时记录,鼠标移动得到终点坐标,通过两组坐标实时算出矩形的宽高,起始坐标作为矩形选框的绝对定位的top,left值, 宽高绘制矩形,这样就能实现跟随鼠标绘制选框的效果了。

代码实现

原理讲完,就是上手撸代码了,整体效果不带100行代码,使用纯javascript完成。 让我们一步一步实现它。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>selection</title>
    <link rel="stylesheet" href="style.css" />
  </head>
  <body>
  </body>
  <script src="select.js"></script>
</html>

新建一个html文件,引入select.js, 我们的代码都会放到这里,样式文件里我给body加了一个网格背景,通过渐变实现的,感兴趣的小伙伴可以看一下:

body {
  margin: 0;
  padding: 0;
  height: 100vh;
  overflow: hidden;
  background-size: 20px 20px;
  background-image: linear-gradient(to right, #cecece 1px, transparent 1px),
    linear-gradient(to bottom, #cecece 1px, transparent 1px);
}

首先定义几个变量用于存储一些信息:

// 初始坐标
const startPos = {
  x: null,
  y: null,
};
// 选框元素的id, 后面用于确定选框是否存在,来判断是update还是create
const selectionId = 'selection-box';
// 标记当前是否完成本次绘制
let end = false;
// svg矩形的线宽,如果你使用其他方式或没有线宽的矩形,就不需要这变量,它用来作为阈值,防止svgd的react宽高出现负值
const strokeWith = 2;

为文档对象添加指针事件, 这里pointerEvent可以兼容PC端和移动端的事件, 这样在手机上就不用判断是否为手机而切换touchEvent了。鼠标按下时记录初始坐标,并标记end=false, 告诉程序,我要开始画啦!

document.addEventListener('pointerdown', function (e) {
  // 记录起始点坐标
  startPos.x = e.clientX;
  startPos.y = e.clientY;
  // 标记
  end = false;
});

在鼠标抬起时,通常需要清除之前记录的信息,为下次绘制做准备。

document.addEventListener('pointerup', function () {
  end = true;
  startPos.x = null;
  startPos.y = null;
});

鼠标移动时,也就是pointermove事件中,要执行绘制选框的函数:

document.addEventListener('pointermove', function (e) {
  const { clientX, clientY } = e;
  if (startPos.x !== null && startPos.y !== null && !end) {
    drawBoxSelection(startPos.x, startPos.y, clientX, clientY);
  }
});
// 实时渲染(创建or更新)svg信息
function drawBoxSelection(startX, startY, endX, endY) {
  const { width, height } = calculateSize(startX, startY, endX, endY);
  const selectEl = document.getElementById(selectionId);
  // 如果已经有selection, 改变属性
  if (selectEl) {
    // 更新选框信息
  } else {
    // 如果没有selection, 创建新的selection
    createSelectionSvg(top, left, absW, absH);
  }
}

这里有两个函数,一个是根据坐标计算宽高,一个是创建svg选框。 逻辑是根据id找页面上有没有选框元素,如果有,就更新选框的信息,包括坐标,尺寸信息,如果没有,就绘制一个。
注意,这里并没有在结束绘制时移除选框元素,一个是绘制选框是个高频率的dom操作,频繁移除添加dom会增加开销,其次移除在绘制可能会出现闪烁的现象,干脆就把他留在页面中,之后操作做更新即可。

// 根据坐标计算尺寸
function calculateSize(startX, startY, endX, endY) {
  return { width: endX - startX, height: endY - startY };
}
// 创建svg
function createSelectionSvg(top, left, width, height) {
  const svg = `<svg id="${selectionId}"  width="100%" height="100%" viewBox="0 0 ${width} ${height}" fill="none" xmlns="http://www.w3.org/2000/svg">
  <rect x="1" y="1" width="${width}" height="${height}" fill="#A4CDF4" fill-opacity="0.1" stroke="#4AB3FF" stroke-width="2" />
</svg>`;

  const div = document.createElement('div');
  div.style = `position:absolute;top:${Math.round(top)}px;left:${Math.round(left)}px;z-index:9999`;
  div.innerHTML = svg;

  document.body.appendChild(div);
}

因为svg的一些属性需要动态计算,所以它是一个模板字符串,不能直接append到body中,因此用div包一层。定位信息也设置到div上即可。

接下来就是更新,更新操发生在鼠标移动过程中,以及第二次以后的所有绘制。

// 实时渲染(创建or更新)svg信息
function drawBoxSelection(startX, startY, endX, endY) {
  const { width, height } = calculateSize(startX, startY, endX, endY);
  const absW = Math.abs(width);
  const absH = Math.abs(height);
  const left = Math.min(startX, endX);
  const top = Math.min(startY, endY);
  const selectEl = document.getElementById(selectionId);
  // 如果已经有selection, 改变属性
  if (selectEl) {
    const rectEl = selectEl.querySelector(`#${selectionId} rect`);
    const parent = selectEl.parentNode;
    parent.style = `position:absolute;top:${Math.round(top)}px;left:${Math.round(left)}px`;

    selectEl.setAttribute('width', absW);
    selectEl.setAttribute('height', absH);
    selectEl.setAttribute('viewBox', `0 0 ${absW} ${absH}`);
    absW > strokeWith && rectEl.setAttribute('width', absW - strokeWith);
    absH > strokeWith && rectEl.setAttribute('height', absH - strokeWith);
  } else {
    // 如果没有selection, 创建新的selection
    createSelectionSvg(top, left, absW, absH);
  }
}

这里有几点需要注意:
要判断start和end坐标的大小来决定起始位置,当鼠标由右向左移动时,矩形应该向左侧扩展,如果一直使用start作为起始坐标,则无论鼠如何移动,都会向右扩展;
因为我这里使用svg矩形,矩形是有宽度的,当移动距离小于宽度时,绘制的矩形会有类似裁剪的现象,所以要保证宽高大于矩形的线宽,这也是之前为什么定义strokeWith的原因。

最后,我们初始化一个空的svg矩形到屏幕上,防止首次绘制时有闪烁的现象。

// 初始化一个空的svg, 防止第一次绘制时有闪烁现象
createSelectionSvg(0, 0, 0, 0);

到此,一个纯手工打造的矩形选框效果就实现了,总共不到100行代码。快动手实践一下吧!
最后附上完整代码,欢迎CV。