检查DOM元素是否可交互

9
在我的html5应用中,我需要大量创建/操作动态dom元素。在某些情况下,我需要验证用户是否可以“点击”一个元素(例如div)。 “可点击”意味着满足以下两个条件:
  • 它的计算CSS样式意味着它实际上被显示出来(即元素及其所有父级的"display" 和 "visibility" 属性)
  • 它没有被任何其他具有更高z-index或后来创建的绝对定位元素遮挡-在DOM的任何级别上,而不仅仅是在它的同级元素。
我可以使用纯JS或jQuery。使用jQuery很容易检查第一部分(即使用.is(':visible'))。但是,如果我有一个被另一个元素遮挡的元素,则仍然返回true。
如何检查元素是否真正可点击?

我很好奇,当元素被其他东西遮盖时,你希望你的代码执行什么操作? - Sidney
2
你可以查看elementFromPoint来完成第二部分。虽然你需要测试元素的多个点(x,y),至少要测试4个角落。 - KevBot
1
投票将此问题关闭为“过于宽泛”的人,我很想了解原因。 - Aleks G
1
只需找到此 GitHub https://github.com/UseAllFive/true-visibility - Alexis Vandepitte
@Rob 这个问题提出了一个具体的问题:如何检测元素是否被另一个元素遮挡 - 我正在寻找解决这个问题的想法。如果你能帮忙 - 谢谢 - 如果不能,那也没关系。 - Aleks G
显示剩余7条评论
2个回答

2

这里使用了标准的视频游戏风格碰撞测试来确定一个物品是否占据了另一个物品的全部空间。我不会在这里解释这一部分,你可以看看其他答案。

对我来说难点在于尝试获取每个元素的z-index来确定一个元素实际上是在另一个元素的上面还是下面。首先我们检查是否定义了z-index,如果没有设置,我们会检查父元素,直到找到文档为止。如果我们一直到达文档而没有找到定义的z-index,我们就知道哪个项目是先渲染的(标记在文档中更高),将位于下方。

我已经将其实现为jQuery插件... $("#myElement").isClickable()

$.fn.isClickable = function() {
  if (!this.length) return false;

  const getZIndex = e => {
    if (e === window || e === document) return 0;
    var z = document.defaultView.getComputedStyle(e).getPropertyValue('z-index');
    if (isNaN(z)) return getZIndex(e.parentNode);
    else return z;
  };

  var width = this.width(),
    height = this.height(),
    offset = this.offset(),
    zIndex = getZIndex(this[0]),
    clickable = true,
    target = this[0],
    targetIsBefore = false;

  $("body *").each(function() {
    if (this === target) targetIsBefore = true;
    if (!$(this).is(":visible") || this === target) return;

    var e_width = $(this).width(),
      e_height = $(this).height(),
      e_offset = $(this).offset(),
      e_zIndex = getZIndex(this),

      leftOfTarget = offset.left >= e_offset.left,
      rightOfTarget = width + offset.left <= e_width + e_offset.left,
      belowTarget = offset.top >= e_offset.top,
      aboveTarget = height + offset.top <= e_height + e_offset.top,
      behindTarget = e_zIndex === zIndex ? targetIsBefore : e_zIndex > zIndex;

    if (leftOfTarget && rightOfTarget && belowTarget && aboveTarget && behindTarget) clickable = false;
  });

  return clickable;
};

$(".clickme").click(function() {
  alert("u clicked " + this.id)
});

$(".clickme").each(function() {
  console.log("#"+this.id, $(this).isClickable() ? "is clickable" : "is NOT clickable");
})
#item1 {
  background: rgba(230, 30, 43, 0.3);
  position: absolute;
  top: 3px;
  left: 4px;
  width: 205px;
  height: 250px;
}

#item2 {
  background: rgba(30, 250, 43, 0.3);
  position: absolute;
  top: 100px;
  left: 50px;
  width: 148px;
  height: 50px;
}

#item3 {
  background: rgba(30, 25, 110, 0.3);
  position: absolute;
  top: 23px;
  left: 101px;
  width: 32px;
  height: 100px;
}

#item4 {
  background: rgba(159, 25, 110, 0.3);
  position: absolute;
  top: 10px;
  left: 45px;
  width: 23px;
  height: 45px;
  z-index: -111
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id="item1" class='clickme'></div>
<div id="item2" class='clickme'></div>
<div id="item3" class='clickme'></div>
<div id="item4" class='clickme'></div>


只是一个想法:完全披露,我更喜欢不使用否定条件,因为我的大脑可以更快地处理积极的条件(这里没有“错误”,但是 $(selector).filter(':hidden') 与你的 !$(this).is(":visible")。但请注意,“visible”使用“:visible”元素选择器。如果它们有布局框,则将被视为可见元素。完整的讨论:https://dev59.com/-GQm5IYBdhLWcg3wrAiY#17426800(请注意,“这包括那些具有零宽度和/或高度的元素。”)有可能涉及到什么是“可点击”的问题。 - Mark Schultheiss
我对 $("body *").each(...) 这种方式表示怀疑。我的 DOM 元素大约有五十万个。测试一个元素是否被遮挡大约需要 2 秒钟。 - Aleks G
你也可以考虑在选择器 $("body").find("*").filter(':visible').each( 上使用过滤器。然而,我不是这个插件的作者,并没有完全测试这个快速假设是否有效。 - Mark Schultheiss
请注意,$("body").find("*")是一种微小的优化,但在这里似乎定义上下文是相关的。参考:https://dev59.com/s2Qn5IYBdhLWcg3w_LJG#16423239 - Mark Schultheiss
关于我之前的评论,$(this).not(':visible').length 看起来和 !$(this).is(':visible') 是一样的,但我没有检查速度差异。 - Mark Schultheiss

1
以下是一个非常粗糙的实现 - 它使用 document.elementFromPoint(x, y) 方法 并广泛扫描每个元素的位置,以查看该元素是否可点击。
为了保持简单和更高效,它在50像素网格中调查每个元素的位置。例如,如果一个元素是100x100像素,则会进行9次检查(0 0、50 0、100 0、0 50、50 50、100 50、0 100、50 100 和 100 100)。这个值可以调整以进行更详细的扫描。
另一个因素是您可能需要考虑的,即元素的多少是可点击的。例如,如果一个元素的1像素线可见,它真的是可点击的吗?需要添加一些额外的检查来解决这些情况。
在以下演示中,有5个正方形 - 红色、绿色、蓝色、黄色、青色、黑色和灰色。青色元素被隐藏在黄色元素下面。黑色元素在灰色元素下面,但使用z-index将其显示在上面。因此,除了青色和灰色之外的每个元素都会显示为可点击。
注意:绿色显示为不可点击,因为它被隐藏在控制台日志后面(我相信)。
这是演示:

// Create an array of the 5 blocks
const blocks = Array.from(document.querySelectorAll(".el"));

// Loop through the blocks
blocks.forEach(block => {
  // Get the block position
  const blockPos = block.getBoundingClientRect();
  let clickable = false;
  
  // Cycle through every 50-pixels in the X and Y directions
  // testing if the element is clickable
  for (var x = blockPos.left; x <= blockPos.right; x+=50) {
    for (var y = blockPos.top; y <= blockPos.bottom; y+=50) {
      // If clickable, log it
      if (block == document.elementFromPoint(x, y)) {
        console.log('clickable - ', block.classList[1])
        clickable = true;
        break;
      }
    }
    
    if (clickable) {
      break;
    }
  }
  
  if (!clickable) {
    console.log('not clickable - ', block.classList[1]);
  }
});
.el {
  position: absolute;
  width: 100px;
  height: 100px;
}

.red {
  top: 25px;
  left: 25px;
  background-color: red;
}

.green {
  top: 150px;
  left: 25px;
  background-color: green;
}

.blue {
  top: 75px;
  left: 75px;
  background-color: blue;
}

.yellow {
  top: 50px;
  left: 200px;
  background-color: yellow;
}

.cyan {
  top: 50px;
  left: 200px;
  background-color: cyan;
}

.black {
  top: 25px;
  left: 325px;
  z-index: 10;
  background-color: black;
}

.gray {
  top: 25px;
  left: 325px;
  z-index: 1;
  background-color: gray;
}
<div class="el red"></div>
<div class="el green"></div>
<div class="el blue"></div>
<div class="el cyan"></div>
<div class="el yellow"></div>
<div class="el black"></div>
<div class="el gray"></div>


1
这个颜色 background-color: ycyanellow; 和草原犬便便的颜色一样吗? - Mark Schultheiss
@Rob - 你是在说这个答案有问题吗? - Brett DeWoody
谢谢。我也考虑使用 elementFromPoint。你的代码可能需要一些调整,因为 getBoundingClientRect 返回视口内的坐标,而 elementFromPoint 使用文档根相对坐标,但总体上想法很好。我想知道在实际场景中,当我需要在循环中检查多个元素时速度会变慢多少。 - Aleks G
2
@Iwrestledabearonce。document.getElementFromPoint()返回在x,y位置上z-index更高的元素,因此它考虑了z-index。如果您想获取较低z-index的元素,则应使用document.getElementsFromPoint()(注意复数),它将返回x-y pos上所有元素的数组,按z-index排序。 - Kaiido
使用getElementFromPoint是个好主意,但在某些情况下会失败:定位的内部元素或伪类可能是可点击的,并将其单击事件冒泡到父级,但此脚本不会检测到它们(我猜是因为gBCR)。对于内部元素,可能会有递归调用,但我认为这对于伪元素来说是不可能的。但它非常好地处理了计算样式,如指针事件,并且正如我在先前的评论中所说的z-index,所以你已经得到了我的+1。 - Kaiido
显示剩余3条评论

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