获取页面矩形区域内的DOM元素

18

在网页上给定两个点和一组DOM元素,如何找到那些位于由这两个点定义的矩形区域内的DOM元素子集?

我正在开发一个基于Web的画廊,在该画廊中,每张照片都包含在一个 li 标签中。当用户使用鼠标拖出一个矩形区域时,所有位于该矩形内的 li 元素将被标记为已选中。

最好使用jQuery解决方案,以实现简洁高效。

2个回答

14

尝试使用类似以下的代码:

// x1, y1 would be mouse coordinates onmousedown
// x2, y2 would be mouse coordinates onmouseup
// all coordinates are considered relative to the document
function rectangleSelect(selector, x1, y1, x2, y2) {
    var elements = [];
    jQuery(selector).each(function() {
        var $this = jQuery(this);
        var offset = $this.offset();
        var x = offset.left;
        var y = offset.top;
        var w = $this.width();
        var h = $this.height();

        if (x >= x1 
            && y >= y1 
            && x + w <= x2 
            && y + h <= y2) {
            // this element fits inside the selection rectangle
            elements.push($this.get(0));
        }
    });
    return elements;
}

// Simple test
// Mark all li elements red if they are children of ul#list
// and if they fall inside the rectangle with coordinates: 
// x1=0, y1=0, x2=200, y2=200
var elements = rectangleSelect("ul#list li", 0, 0, 200, 200);
var itm = elements.length;
while(itm--) {
    elements[itm].style.color = 'red';
    console.log(elements[itm]);
}

如果需要纯JS的解决方案,可以查看这个代码pen:https://codepen.io/ArtBIT/pen/KOdvjM


2
谢谢ArtBIT。我在Google上搜索了一会儿,似乎没有方便的方法来做这件事,除了循环所有DOM元素并对它们进行基本数学运算外,没有更好的解决方案。 - powerboy
不用担心,@powerboy。是的,这就是我添加selector支持的原因,以减少您需要处理的元素数量。 - ArtBIT
你可以使用 getBoundingClientRect() 一次性获取一个包含 toprightbottomleftwidthheight 属性的对象。 - Yuval A.
@ArtBIT:如果我没有选择器可以传递到上面提到的'rectangleSelect'方法,我该如何在给定的矩形坐标内查找元素?不想将顶级文档作为选择器,因为这会对每个元素进行大量性能损耗。 - Pankaj Kapare
@PankajKapare 性能正是您需要传递选择器的原因,因为它将限制遍历仅在该元素的子级中进行。在上面的示例中,jQuery会为您进行优化,它首先会获取对ul#list元素的引用,然后选择任何作为其子级的li元素,并检查它们是否落在选择矩形内。尝试传递后代选择器,这将帮助jQuery限制搜索范围。 - ArtBIT

0
嘿 @powerboy,我几年前遇到过类似的用例,让我列举一下你的选择:

使用一个库

这将非常简单:

const ds = new DragSelect({
  selectables: document.querySelectorAll('.item')
});

例如:

const ds = new DragSelect({
  selectables: document.querySelectorAll('.item')
});
* { user-select: none; }

.item {
  width: 50px;
  height: 50px;
  position: absolute;
  color: white;
  border: 0;
  background: hotpink;
  top: 10%;
  left: 10%;
}

.ds-selected {
  outline: 3px solid black;
  outline-offset: 3px;
  color: black;
  font-weight: bold;
}

.item:nth-child(2),
.item:nth-child(4) { top: 50% }
.item:nth-child(3),
.item:nth-child(4) { left: 50% }
<script src="https://unpkg.com/dragselect@latest/dist/ds.min.js"></script>
<button type="button" class="item one">1</button>
<button type="button" class="item two">2</button>
<button type="button" class="item three">3</button>
<button type="button" class="item four">4</button>

图书馆自带拖放功能,但如果你只想要选择功能,而不需要拖放功能,你可以通过设置draggability: false来关闭它(文档 | 示例)。在文档中,你还会找到如何与自定义选择库一起使用的部分。

i.e.:

const ds = new DragSelect({
  selectables: document.querySelectorAll('.item'),
  draggability: false
});

背景:十多年前,当我面临这个挑战时,我编写了这个方便的选择库。现在它已经非常成熟,并且能够解决您的使用情况。我强烈建议您使用它,而不是自己编写,因为这是一个非常非常困难的挑战,很容易出错。

在编写DragSelect时,我尝试了各种方法:

在指定点下的元素

理论上我们有document.elementFromPoint(x, y); (MDN) 所以我们可以做以下操作(简化伪代码):
const div = document.getElementById('myDiv');
const rect = div.getBoundingClientRect();
const elements = [];

for (let x = rect.left; x <= rect.right; x++) {
  for (let y = rect.top; y <= rect.bottom; y++) {
    const el = document.elementFromPoint(x, y);
    if (el && !elements.includes(el)) {
      elements.push(el);
    }
  }
}

这段代码循环遍历myDiv元素范围内的每个点,并使用document.elementFromPoint()获取该点上的元素。然后,如果该元素尚未添加到数组中,将其添加到数组中。这将给您一个包含在正方形div范围内的所有元素的数组。
这似乎是最直接的方法,但它也有一些缺点:
- 仅适用于页面上可见的元素。 - 仅获取该点上的最顶层元素,使用elementsFromPoint可以返回所有元素。 - 在较大的正方形上性能相当差,因为复杂度基于选择区域的大小是O(n^2)。在我的测试中,这是性能最差的方法。
然而,如果您只需要某个点下面的元素,这种方法非常棒。
了解了这一点,我们可以通过检查可选择元素的中心点是否存在参考矩形来改进算法。

const boxes = document.querySelectorAll(".box");
const ref = document.querySelector("#ref");
const answer = document.querySelector("#answer");

document.addEventListener("mousemove", (e) => {
  ref.style.left = `${e.clientX}px`;
  ref.style.top = `${e.clientY}px`;
  
  let isColliding = false;
  boxes.forEach((box, index) => {
    const rect = box.getBoundingClientRect();
    const el = document.elementFromPoint((rect.left + rect.right) / 2, (rect.top + rect.bottom) / 2);
    if (el === ref) {
      isColliding = true;
      box.style.background = "blue";
    } else {
      box.style.background = "grey";
    }
  });
  answer.innerText = isColliding ? "Collision with box" : "No collision";
});
.box {
  position: absolute;
  top: 20%;
  left: 20%;
  width: 15%;
  height: 15%;
  background: grey;
}
.box:nth-child(2),
.box:nth-child(4){ top: 60% }
.box:nth-child(3),
.box:nth-child(4){ left: 60% }

#ref {
  width: 15%;
  height: 15%;
  background: hotpink;
  position: fixed;
}
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
<div id="ref"></div>
<div id="answer"></div>

  • 现在的复杂度是 O(n)
  • 这很好玩,但比使用碰撞算法要灵活得多:

使用碰撞算法

轴对齐最小包围盒

DragSelect 在底层使用的是这个算法的增强版。我在这里详细解释它, 这里还有一个关于2D碰撞的MDN文章

const boxes = document.querySelectorAll(".box");
const ref = document.querySelector("#ref");
const answer = document.querySelector("#answer");

document.addEventListener("mousemove", (e) => {
  ref.style.left = `${e.clientX}px`;
  ref.style.top = `${e.clientY}px`;
  const refRect = ref.getBoundingClientRect();

  let isColliding = false;
  boxes.forEach((box, index) => {
    const boxRect = box.getBoundingClientRect();
    if (AABBCollision(refRect, boxRect)) {
      isColliding = true;
      box.style.background = "blue";
    } else {
      box.style.background = "grey";
    }
  });
  answer.innerText = isColliding ? "Collision with box" : "No collision";
});

const AABBCollision = (a, b) => {
  if (
    a.left < b.right && // 1.
    a.right > b.left && // 2.
    a.top < b.bottom && // 3.
    a.bottom > b.top // 4.
  ) {
    return true;
  } else {
    return false;
  }
};
.box {
  position: absolute;
  top: 20%;
  left: 20%;
  width: 15%;
  height: 15%;
  background: grey;
}

.box:nth-child(2),
.box:nth-child(4) {
  top: 60%
}

.box:nth-child(3),
.box:nth-child(4) {
  left: 60%
}

#ref {
  width: 15%;
  height: 15%;
  background: hotpink;
  position: fixed;
}
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
<div id="ref"></div>
<div id="answer"></div>

为了简单起见,一个方块会跟随鼠标移动,当发生碰撞时,方块会从灰色变为蓝色,反之亦然。
让我解释一下:
轴对齐边界框碰撞检测。 想象以下示例:
        b01
     a01[1]a02
        b02      b11
              a11[2]a12
                 b12

检查这两个框是否碰撞,我们进行以下AABB计算:
  1. a01 < a12(框1左边界位置小于框2右边界位置)
  2. a02 > a11(框1右边界位置大于框2左边界位置)
  3. b01 < b12(框1顶部边界位置小于框2底部边界位置)
  4. b02 > b11(框1底部边界位置大于框2顶部边界位置)
由于您必须对每个可能发生碰撞的元素进行此检查,因此(假设只有1个参考元素),理论上的复杂度为O(n),这非常好,因为它与输入量成线性比例。
这在DragSelect中已被证明对多达30,000个元素有效,然后开始变慢,因为在JavaScript中获取边界框是昂贵的,我正在寻找更好的算法,幸运的是,在99.99%的用例中,30,000个元素已足够 :)
但会随时向您更新未来的发现(例如其他算法或广义和狭义阶段可能会很有趣)。
希望我能帮到您,祝您好运!

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接