规矩依旧,先上效果!!
step by step第三弹, 100行代码撸一个动态矩形选框效果(应该叫这个名字吧~)。
动态选框通常用来一次框选多个内容,我们在PC电脑上经常会看到这个功能,在一些和多文件相关的web系统中ye非常常见。我们先不关心怎么选文件,先看看这个跟随鼠标的框选效果怎么实现。
基本思路
首先,有几个基本点要明确:
- 选框是个矩形
- 它要脱离文档流,不能影响其他元素
- 它的位置和尺寸是跟随鼠标移动变化的
前两点都很好解决,矩形的基本元素我这里选的是使用svg渲染,svg后期很好扩展(比如加选框网格线,四角的拖动方块等,几乎不渲染css辅助,可移植性高),脱离文档流就绝对定位,层级设置高一些。
第三点, 位置和尺寸是重点了。要想动态绘制矩形,我们就要知道矩形的宽、高,和起始坐标(用于确定位置), 而这些,我们都可以通过鼠标的坐标信息获得,在鼠标按下,抬起,移动等过程中,我们都可以通过event.clientX
,event.clientY
来获取鼠标相对于屏幕的坐标。
要计算矩形的尺寸,就要知道两组坐标,也就是起始坐标和终点坐标。
起始坐标在鼠标按下时记录,鼠标移动得到终点坐标,通过两组坐标实时算出矩形的宽高,起始坐标作为矩形选框的绝对定位的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。