如何使用原生JavaScript选取指定坐标矩形范围内的特定类名元素?
原生JavaScript完全可以实现该需求,最高效的实现不需要逐点调用elementsFromPoint()扫描区域,那种方案不仅存在浮点坐标无法全覆盖的漏洞,性能也会随矩形面积增大快速下降。最优思路是先通过原生选择器收窄候选范围,再通过几何相交判断做精确筛选,全程线性时间复杂度,哪怕页面元素过万也能在毫秒级完成计算。
具体实现逻辑
1. 归一化矩形边界
首先把传入的4组坐标做统一处理,算出选择矩形的四个边界值,避免坐标传入顺序错乱导致判断失效:
// 入参points为4组坐标的数组,格式为[{x: 12.3, y: 45.6}, ...],坐标为视口相对坐标 const xs = points.map(p => p.x) const ys = points.map(p => p.y) const selectArea = { left: Math.min(...xs), top: Math.min(...ys), right: Math.max(...xs), bottom: Math.max(...ys) }
2. 初筛候选元素
不要上来就遍历全量DOM,利用浏览器原生选择器直接拉取最小候选集:
- 如果需要筛选带指定类名(比如
selectable)的元素,直接调用document.querySelectorAll('.selectable')获取候选,这个接口是浏览器底层实现的,筛选速度远快于JS层遍历 - 如果需要获取矩形内所有元素,再用
document.body.querySelectorAll('*')拉取全量元素,遍历前提前排除offsetParent === null的不可见元素(比如设置了display: none的元素),进一步减少计算量
3. 几何相交校验
对每个候选元素,调用getBoundingClientRect()获取元素自身的视口位置矩形,和你计算出的选择矩形做相交判断:只要两个矩形存在重叠区域,就判定元素在选择范围内。
矩形相交的判断逻辑非常简单,排除「元素完全在选择框上下左右四个方向外侧」的情况,剩下的就是相交的元素:
function checkIntersect(rectA, rectB) { return !( rectA.right < rectB.left || rectA.left > rectB.right || rectA.bottom < rectB.top || rectA.top > rectB.bottom ) }
如果你的业务要求「元素必须完全被矩形包裹才算选中」,只需要把判断条件改成元素的left >= selectArea.left、right <= selectArea.right、top >= selectArea.top、bottom <= selectArea.bottom即可。
4. (可选)遮挡校验
如果你的场景需要排除被其他元素遮挡、实际无法交互的元素,不需要扫描整个矩形,只需要对已经通过相交校验的元素,取元素的中心点坐标调用一次document.elementsFromPoint(),判断当前元素是否在返回的元素栈中即可,每个元素仅需调用1次接口,性能损耗极低。
完整可复用代码
/** * 获取指定矩形范围内的元素 * @param {Array<{x: number, y: number}>} points 定义矩形的4组视口相对坐标 * @param {string} [targetClassName] 要筛选的目标类名,不传则返回矩形内所有元素 * @param {boolean} [checkOcclusion=false] 是否校验元素是否被遮挡 * @returns {HTMLElement[]} 符合条件的元素数组 */ function getElementsInRect(points, targetClassName, checkOcclusion = false) { // 归一化选择矩形边界 const xs = points.map(p => p.x) const ys = points.map(p => p.y) const selectArea = { left: Math.min(...xs), top: Math.min(...ys), right: Math.max(...xs), bottom: Math.max(...ys) } // 拉取候选元素 const selector = targetClassName ? `.${targetClassName}` : '*' const candidates = document.querySelectorAll(selector) const matched = [] for (const el of candidates) { // 跳过非body的不可见元素 if (el !== document.body && el.offsetParent === null) continue const elRect = el.getBoundingClientRect() if (!checkIntersect(elRect, selectArea)) continue // 可选遮挡校验 if (checkOcclusion) { const centerX = elRect.left + elRect.width / 2 const centerY = elRect.top + elRect.height / 2 const stack = document.elementsFromPoint(centerX, centerY) if (!stack.includes(el)) continue } matched.push(el) } return matched } function checkIntersect(rectA, rectB) { return !( rectA.right < rectB.left || rectA.left > rectB.right || rectA.bottom < rectB.top || rectA.top > rectB.bottom ) }
注意事项
- 代码中使用的是视口相对坐标体系,如果你手里的坐标是页面绝对坐标(相对于页面左上角,包含滚动距离),要么把传入的坐标减去
window.scrollX/window.scrollY转成视口坐标,要么给元素的getBoundingClientRect()返回值加上滚动偏移转成页面坐标,保证两个矩形的坐标系一致即可。 - 这个方案不存在浮点坐标精度问题,不管传入的坐标是整数还是小数,几何计算都是精确的,不会出现漏选、错选。
内容的提问来源于stack exchange,提问作者user11092881

