如何获取重叠的 div 区域内所有兄弟 div?

3

我有一个大的div,里面还有像这样位置排列的小兄弟div:

.large{
  height:20rem;
  width:20rem;
  background-color:red;
  position:absolute;
}

.item1{
  height:5rem;
  width:5rem;
  background-color:blue;
  top:1rem;
  position:absolute;
}

.item2{
  height:5rem;
  width:5rem;
  background-color:green;
  top:3rem;
  left:2rem;
  position:absolute;
}

.item3{
  height:5rem;
  width:5rem;
  background-color:yellow;
  top:1rem;
  left:6rem;
  position:absolute;
}
<div class="large"></div>
<div class="item1"></div>
<div class="item2"></div>
<div class="item3"></div>

如何获取大 div 尺寸内的所有小 div?
是否有类似于 elementsFromPoint 的东西?也许像 elementsFromArea 这样的东西

编辑:

假设 .large 为 320 像素 x 320 像素
我屏幕上有多个较小的 div,它们可以重叠 .large 或在其外面

如何找到与 .large 重叠的 div?

也许我们可以获取 .large 的位置和其高度和宽度,然后将其添加到这样一个函数中:

elementsFromArea(large_x,large_y,large_height,large_width);

这应该返回给定范围内所有div的数组

(.large仅为参考,我只想通过任何给定的正方形区域并找到其中所有的div)

Bounty Edit:

@A Haworth提供的解决方案可以工作,但我正在寻找一种不涉及循环和检查每个元素的解决方案

这个演示解释了我最终想要实现的目标

任何巧妙的解决方法也将被接受!


你所说的“get”是什么意思?你是想通过JavaScript引用它们以允许操作吗?此外,所有的div都是兄弟节点,较小的div只是出现在大div的顶部(而不是内部),因为它们都是绝对定位在body中。需要进一步澄清,不清楚你想做什么。 - Dave Pritlove
@DavePritlove,我已经编辑了这个问题,请您告诉我现在是否更加清晰明了? - cak3_lover
我无法想象如何做到这一点(查找所有元素,无论它们是否是大型的子元素重叠),而不使用循环,但也许如果您能更详细地描述您想要使用该信息做什么,可能会有解决方法。例如,如果您想要更改重叠部分的颜色。 - A Haworth
@AHaworth 我正在尝试获取给定fiddle示例中框架内的所有元素,当我说“不使用循环”时,我指的是不必每次框架发生更改时都检查每个单独的元素。 - cak3_lover
1
我认为@AHaworth更想知道你正在尝试构建的功能的更广泛背景。一旦你的应用程序拥有这些元素,它会对它们进行什么操作?更改是否在页面加载时发生一次?还是小元素的位置不断变化? - inwerpsel
显示剩余3条评论
4个回答

4

您可以使用getBoundingClientRect来查找每个元素的左、右、上和下边界。

然后,通过检查左侧是否在大元素的右侧以及右侧是否在大元素的左侧等方式测试是否与大元素重叠:

if ( ((l <= Right) && (r >= Left)) && ( (t <= Bottom) && (b >= Top)) )

为了进行更彻底的测试,在这个片段中,蓝色元素已被向下推,因此它只部分重叠大元素,黄色元素则完全不重叠。

const large = document.querySelector('.large');
const largeRect = large.getBoundingClientRect();
const Left = largeRect.left;
const Right = largeRect.right;
const Top = largeRect.top;
const Bottom = largeRect.bottom;
const items = document.querySelectorAll('.large ~ *');
let overlappers = [];
items.forEach(item => {
  const itemRect = item.getBoundingClientRect();
  const l = itemRect.left;
  const r = itemRect.right;
  const t = itemRect.top;
  const b = itemRect.bottom;
  if (((l <= Right) && (r >= Left)) && ((t <= Bottom) && (b >= Top))) {
    overlappers.push(item);
  }
});
console.log('The items with these background colors overlap the large element:');
overlappers.forEach(item => {
  console.log(window.getComputedStyle(item).backgroundColor);
});
.large {
  height: 20rem;
  width: 20rem;
  background-color: red;
  position: absolute;
}

.item1 {
  height: 5rem;
  width: 5rem;
  background-color: blue;
  top: 19rem;
  position: absolute;
}

.item2 {
  height: 5rem;
  width: 5rem;
  background-color: green;
  top: 3rem;
  left: 2rem;
  position: absolute;
}

.item3 {
  height: 5rem;
  width: 5rem;
  background-color: yellow;
  top: 1rem;
  left: 26rem;
  position: absolute;
}
<div>
  <div class="large"></div>
  <div class="item1"></div>
  <div class="item2"></div>
  <div class="item3"></div>
</div>

注意,此代码段仅测试那些在 CSS 中与 large 元素为同级的元素,即跟随 large 元素的元素。如果您想要所有的同级元素,无论它们是在 large 元素之前还是之后,请返回到 large 的父级并获取其所有子元素(这当然也包括 large 元素)。

哎呀,我以为会有内置方法来做这件事,但还是谢谢你! - cak3_lover

2
IntersectionObserver API提供了正好符合您需求的功能。由于这是一个相对较新的API,因此其他答案未提及它并不奇怪。
在我的个人经验中,我将其用于在不一次渲染9001行的情况下显示大型表格中的懒加载。在我的情况下,我会使用IntersectionObserver来确定用户视野内是否有最后一行表格,并加载更多行。它非常高效,因为它不需要任何轮询DOM元素位置的循环,浏览器可以自由优化它。
从MDN网站上,这里有一种简单的方法创建IntersectionObserver。我已经注释掉了一些我认为您不需要的选项。


    let options = {
        root: document.querySelector('.large'),
        // rootMargin: '0px',
        // threshold: 1.0
    }
    
    let observer = new IntersectionObserver(callback, options);


回调函数是一种在元素与 .large 的交叉区域发生特定阈值更改时触发的函数。如果 threshold = 0(默认值,我认为您在这种情况下想要这个值),则即使只有1像素重叠,它也会触发。
创建了一个具有 .large 根的 IntersectionObserver 后,您将希望 .observe() 较小的 div,以便 IntersectionObserver 可以报告它们何时与 .large 相交。
再次从 MDN 中获取,回调的格式如下所示。请注意,回调在 交叉变化 时触发,这意味着以前与 .large 相交的较小 div 如果不再相交,则会出现在条目列表中。要获取与 .large 相交的元素,您将需要过滤 entries,以便仅存在 entry.isInterecting === true 的条目。从筛选后的 entries 列表中,您可以获取每个条目的 entry.target


    let callback = (entries, observer) => {
        entries.forEach(entry => {
            // Each entry describes an intersection change for one observed
            // target element:
            //   entry.boundingClientRect
            //   entry.intersectionRatio
            //   entry.intersectionRect
            //   entry.isIntersecting
            //   entry.rootBounds
            //   entry.target
            //   entry.time
        });
    };


嗨,是的,我已经经常使用IntersectionObserver了,它非常有用,但我真的不明白它如何避免需要循环。您将不得不循环遍历所有元素并在它们上面放置观察器。这意味着如果事物动态变化,您将得到通知,这是一个优点,但它如何避免需要循环呢? - A Haworth
这似乎是我正在寻找的,但我不知道它是如何工作的,你能否使用我提供的示例fiddle来说明一下? - cak3_lover
我尝试了这个,但是我不太理解根选项。 - cak3_lover
1
由于需要观察的元素是 .frame同级元素而不是后代元素,因此这种方法行不通。它还解释了根选项的作用。 - inwerpsel
正如@inwerpsel所指出的那样,这个答案是不正确的。IntersectionObserver报告与祖先的交叉,而不是任何随机元素的交叉。有关此内容的更多详细信息,请参阅https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API。 - A Haworth

1

注意:一个轻松且可能没用的回答

然而,这个问题本身也似乎相当无聊。还没有提供任何真实的用例,我也想不到任何一个。同样地,在理论上,我的答案可能是有用的,但你更有可能被小行星击中,而不是发现自己需要它。

发布的重点在于提供了一些关于其他解决方案性能的视角。你可以看到,至少需要数百个元素才会开始关注性能。

我的“答案”仅在以下情况下有效:

  • 项目为矩形
  • 项目不能重叠

潜在的“性能问题”

也许“不是循环”要求是指具有不需要您在 JS 中循环可能大量其他项目的解决方案?如果项目数量可能变得非常大,则这可能是一个有效的问题。

假设要测试的区域相对于项目较小,并且有成千上万个可能在内部或外部的项目,循环所有这些项目可能会比较昂贵。特别是如果你需要为每个事件侦听器。

如已经指出,如果存在类似于 document.getElementFromPoint 的本机 API,则最好,因为这无疑比在 JS 中实现要更有效。

但是这种 API 不存在。可能因为在真实世界的用例中找不到需要它的人。

采样帧的点

现在你可以在框架的每个点上使用document.ElementFromPoint API。然而,与帧的大小成比例地扩展。

但是,我们需要检查每个点来保证检测到所有元素吗?如果元素不能重叠:由于最小元素可能仍具有许多像素高和宽,因此我们可以创建一个具有这些最小值的点网格。只要元素没有变化尺寸(或者只能增大),我们就只需要循环它们一次(以确定最小值),而不是更新时。 请注意,我每次都会循环它们,以考虑设置更改。如果您确定元素具有固定尺寸,则只需在脚本开始时执行一次即可。

当然,现在你必须循环点。然而...

在最理想的情况下,其中最小元素的宽度和高度相等(或更大),您只需要检查4个点。实际上,我在一个函数中使用了这个功能来生成随机立方体,以避免与早期立方体重叠。

它不能用于重叠的元素,因为document.ElementFromPoint 仅知道最顶部的元素。您可以通过临时设置 z-index 来解决这个问题,但我必须停止。

它表现得更好吗?

我不确定是否有必要这样做,但我目前没有看到其他处理大量项目的方法。在最好的情况下,只需要检查4个点(小区域重叠),很难想象另一种方法会更快,如果另一种方法需要在JS中遍历成千上万个元素。即使有几十个点,无论页面上有多少元素,它也可能仍然很“快”。

let allItems = [...document.querySelectorAll('.item')];
const frame = document.getElementById('frame')

function measureLoop() {
  const start = performance.now();
  const large = document.querySelector('#frame');
  const largeRect = large.getBoundingClientRect();
  const Left = largeRect.left;
  const Right = largeRect.right;
  const Top = largeRect.top;
  const Bottom = largeRect.bottom;
  const items = document.querySelectorAll('#frame ~ *');
  let overlappers = [];
  items.forEach(item => {
    const itemRect = item.getBoundingClientRect();
    const l = itemRect.left;
    const r = itemRect.right;
    const t = itemRect.top;
    const b = itemRect.bottom;
    if (((l <= Right) && (r >= Left)) && ((t <= Bottom) && (b >= Top))) {
      overlappers.push(item);
    }
  });

  document.getElementById('result-loop').innerHTML = overlappers.length;
  document.getElementById('time-loop').innerHTML = performance.now() - start;
}

function randomColor() {
  var letters = '0123456789ABCDEF';
  var color = '#';
  for (var i = 0; i < 6; i++) {
    color += letters[Math.floor(Math.random() * 16)];
  }
  return color;
}

function within_frame(frame, items) {
  const rect = frame.getBoundingClientRect();
  const frameX = rect.left;
  const frameY = rect.top;
  const frameWidth = frame.clientWidth;
  const frameHeight = frame.clientHeight;
  const smallestWidth = Math.min(...(items.map(i => i.clientWidth)));
  const smallestHeight = Math.min(...(items.map(i => i.clientHeight)));

  const set = new Set();
  let points = 0;
  const lastY = frameHeight + smallestHeight;
  const lastX = frameWidth + smallestWidth;
  for (let y = 0; y < lastY; y += smallestHeight) {
    for (let x = 0; x < lastX; x += smallestWidth) {
      points++;
      const checkX = Math.min(frameX + x, rect.right)
      const checkY = Math.min(frameY + y, rect.bottom)
      // Note there is always a result, but sometimes it's not the elements we're looking for.
      // Set takes care of only storing unique, so we can loop a small amount of elements at the end and filter.
      set.add(document.elementFromPoint(checkX, checkY));
    }
  }
  set.forEach(el => (el === frame || el === document.documentElement || !items.includes(el)) && set.delete(el))

  document.getElementById('points').innerHTML = points;

  return set;
}

function measure() {
  // Frame needs to be on top for resizing, put it below while calculating.
  frame.style.zIndex = 1;
  const start = performance.now();
  const result = within_frame(frame, allItems)
  const duration = performance.now() - start
  document.getElementById('result').innerHTML = [...result.entries()].length;
  document.getElementById('time').innerHTML = duration;
  // Restore.
  frame.style.zIndex = 3;
}

document.getElementById('measure').addEventListener('click', () => {measure(); measureLoop();})


const overlapsExisting = (el) => {
  return within_frame(el, allItems);
}

let failedGenerated = 0;

function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

async function spawnCubes() {
  frame.style.zIndex = 1;

  allItems.forEach(item => item.parentNode.removeChild(item));
  const nPoints = document.getElementById('nCubes').value;
  const cubeSize = document.getElementById('size').value;

  let newItems = [];
  let failedGenerated = 0;
  for (let i = 0; i < nPoints && failedGenerated < 1000; i++) {
    // Sleep so that stuff is drawn.
    if ((i + failedGenerated) % 100 === 0) {
      document.getElementById('nCubes').value = newItems.length;
      await sleep(0);
    }
    const el = document.createElement('div');
    el.className = 'item';
    //el.innerHTML = i;
    el.style.backgroundColor = randomColor();
    el.style.top = `${Math.round(Math.random() * 90)}%`;
    el.style.left = `${Math.round(Math.random() * 60)}%`;
    el.style.width = `${cubeSize}px`;
    el.style.height = `${cubeSize}px`;

    frame.after(el);

    const existingOverlapping = within_frame(el, newItems);

    if (existingOverlapping.size > 0) {
      i--;
      failedGenerated++;
      el.parentNode.removeChild(el);
      continue;
    }
    newItems.push(el);
  }
  console.log('failedAttempts', failedGenerated);
  allItems = newItems;
  frame.style.zIndex = 3;
  document.getElementById('nCubes').value = newItems.length;
}
frame.addEventListener('mouseup', () => {measure(); measureLoop()});


spawnCubes().then(() => {measure(); measureLoop();});
document.getElementById('randomize').addEventListener('click', e => {
  spawnCubes().then(measure);
})
#frame {
  height: 3rem;
  width: 3rem;
  display: inline-block;
  resize: both;
  border: solid black 0.1rem;
  overflow: auto;
  position: absolute;
  z-index: 3;
}

.item {
  height: 1rem;
  width: 1rem;
  position: absolute;
  z-index: 2;
}

.controls {
  position: fixed;
  bottom: 4px;
  right: 4px;
  text-align: right;
}
<div id="frame"></div>



<div class="controls">
  <button id="measure">
    measure
  </button>
  <button id="randomize">
    spawn cubes
  </button>

  <div>
    N cubes:
    <input id="nCubes" type="number" value="40">
  </div>
  <div>
    Cube size:
    <input id="size" type="number" value="16">
  </div>

  <div>
    N inside large:
    <span id="result">

    </span>
  </div>
  <div>
    Time (ms):
    <span id="time">

    </span>
  </div>
  <div>
    Points:
    <span id="points">

    </span>
  </div>
  <div>
    N inside large (loop):
    <span id="result-loop">

    </span>
  </div>
  <div>
    Time (ms) (loop):
    <span id="time-loop">

    </span>
  </div>
</div>


1
@A Haworth提供的解决方案有效,但我正在寻找一种不需要循环检查每个元素的解决方案。如果我们处理一个元素数组,我不知道如何在没有循环的情况下实现这一点,但您可以使用resizeObserverloops测试此解决方案。

// Init elements
const items = [...document.querySelectorAll('.item')];
const frame = document.getElementById('frame');

const resultElement = document.getElementById('for-result');

// Creating an array of properties
// Math.trunc() removing any fractional digits
const itemsProperties = items.map(item => {
  return {
    width: item.getBoundingClientRect().width,
    height: item.getBoundingClientRect().height,
    x: Math.trunc(item.getBoundingClientRect().x),
    y: Math.trunc(item.getBoundingClientRect().y),
  };
});

function within_frame(frameSize) {
  const inside = [];
  for (const i in itemsProperties) {
    // Determine current height and width of the square
    // Because X, Y is TOP, LEFT, and we need RIGHT, BOTTOM values.
    const positionY = itemsProperties[i].height + itemsProperties[i].y;
    const positionX = itemsProperties[i].width + itemsProperties[i].x;

    // If the position square less than or equal to the size of the inner frame,
    // then we will add values to the array.
    if (
      positionY <= frameSize.blockSize &&
      positionX <= frameSize.inlineSize
    ) {
      inside.push(itemsProperties[i]);
    }
  }
  //returns all the elements within the frame bounds
  return inside;
}

// Initialize observer
const resizeObserver = new ResizeObserver(entries => {
  // Determine height and width of the 'frame'
  const frameSize = entries[0].borderBoxSize[0];
  // Return an array of values inside  'frame'
  const result = within_frame(frameSize);
  //console.log(result);
  
  // for result
  resultElement.innerHTML = result.map(
    (el, idx) => `<code>square${idx + 1} position: ${el.x}px x ${el.y}px</code>`
  );
});
// Call an observer to watch the frame
resizeObserver.observe(frame);
#frame {
  height: 10rem;
  width: 10rem;
  display: inline-block;
  resize: both;
  border: solid black 0.5rem;
  overflow: auto;
  position: absolute;
  z-index: 1;
}

.item {
  height: 2rem;
  width: 2rem;
  position: absolute;
}


/* for result */
pre {
  position: fixed;
  right: 0.5rem;
  top: 0.5rem;
  border: 2px solid black;
  padding: 0.5rem 1rem;
  display: flex;
  flex-flow: column;
}
#for-result {
  font-weight: bold;
  font-size: 1.5em;
}
<div id="frame"></div>

<div class="item" style="background-color: red"></div>
<div class="item" style="background-color: green; top: 50%"></div>
<div class="item" style="background-color: blue; top: 20%; left: 30%"></div>
<div class="item" style="background-color: pink; top: 60%; left: 20%"></div>
<div class="item" style="background-color: yellow; top: 25%; left: 10%"></div>

<pre id="for-result"></pre>


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