协慌网

登录 贡献 社区

如何判断 DOM 元素在当前视口中是否可见?

是否有一种有效的方法来判断 DOM 元素(在 HTML 文档中)当前是否可见(出现在视口中 )?

(问题是指 Firefox)

答案

现在大多数浏览器都支持getBoundingClientRect方法,这已成为最佳实践。使用旧的答案非常慢不准确,有几个错误

选择正确的解决方案几乎从不精确 。您可以阅读有关其错误的更多信息。


该解决方案在 IE7 +,iOS5 + Safari,Android2 +,Blackberry,Opera Mobile 和 IE Mobile 10 上进行了测试


function isElementInViewport (el) {

    //special bonus for those using jQuery
    if (typeof jQuery === "function" && el instanceof jQuery) {
        el = el[0];
    }

    var rect = el.getBoundingClientRect();

    return (
        rect.top >= 0 &&
        rect.left >= 0 &&
        rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && /*or $(window).height() */
        rect.right <= (window.innerWidth || document.documentElement.clientWidth) /*or $(window).width() */
    );
}

如何使用:

您可以确定上面给出的函数在调用时返回正确的答案,但是跟踪元素作为事件的可见性呢?

将以下代码放在<body>标记的底部:

function onVisibilityChange(el, callback) {
    var old_visible;
    return function () {
        var visible = isElementInViewport(el);
        if (visible != old_visible) {
            old_visible = visible;
            if (typeof callback == 'function') {
                callback();
            }
        }
    }
}

var handler = onVisibilityChange(el, function() {
    /* your code go here */
});


//jQuery
$(window).on('DOMContentLoaded load resize scroll', handler); 

/* //non-jQuery
if (window.addEventListener) {
    addEventListener('DOMContentLoaded', handler, false); 
    addEventListener('load', handler, false); 
    addEventListener('scroll', handler, false); 
    addEventListener('resize', handler, false); 
} else if (window.attachEvent)  {
    attachEvent('onDOMContentLoaded', handler); // IE9+ :(
    attachEvent('onload', handler);
    attachEvent('onscroll', handler);
    attachEvent('onresize', handler);
}
*/

如果您进行任何 DOM 修改,它们可以改变元素的可见性。

准则和常见陷阱:

也许你需要跟踪页面缩放 / 移动设备捏? jQuery 应该处理缩放 / 捏交叉浏览器,否则第一个第二个链接应该可以帮助你。

如果修改 DOM ,则会影响元素的可见性。您应该控制它并手动调用handler() 。不幸的是,我们没有跨浏览器onrepaint事件。另一方面,它允许我们进行优化并仅对可以改变元素可见性的 DOM 修改执行重新检查。

从来没有在 jQuery $(document).ready()中使用它,因为此刻没有保证 CSS 。您的代码可以在硬盘驱动器上与您的 CSS 本地工作,但一旦放在远程服务器上它将失败。

触发DOMContentLoaded后, 将应用样式 ,但尚未加载图像 。所以,我们应该添加window.onload事件监听器。

我们还无法捕捉缩放 / 捏合事件。

最后的手段可能是以下代码:

/* TODO: this looks like a very bad code */
setInterval(handler, 600);

如果您关心网页的标签是否有效且可见,您可以使用精彩的功能页面 Visibiliy HTML5 API。

TODO:此方法不处理两种情况:

更新:时间推进,我们的浏览器也是如此。如果您不需要支持 IE <7, 则不再推荐使用此技术 ,您应该使用 @ Dan 的解决方案( https://stackoverflow.com/a/7557433/5628 )。

原始解决方案(现已过时):

这将检查元素在当前视口中是否完全可见:

function elementInViewport(el) {
  var top = el.offsetTop;
  var left = el.offsetLeft;
  var width = el.offsetWidth;
  var height = el.offsetHeight;

  while(el.offsetParent) {
    el = el.offsetParent;
    top += el.offsetTop;
    left += el.offsetLeft;
  }

  return (
    top >= window.pageYOffset &&
    left >= window.pageXOffset &&
    (top + height) <= (window.pageYOffset + window.innerHeight) &&
    (left + width) <= (window.pageXOffset + window.innerWidth)
  );
}

您可以简单地修改它以确定元素的任何部分是否在视口中可见:

function elementInViewport2(el) {
  var top = el.offsetTop;
  var left = el.offsetLeft;
  var width = el.offsetWidth;
  var height = el.offsetHeight;

  while(el.offsetParent) {
    el = el.offsetParent;
    top += el.offsetTop;
    left += el.offsetLeft;
  }

  return (
    top < (window.pageYOffset + window.innerHeight) &&
    left < (window.pageXOffset + window.innerWidth) &&
    (top + height) > window.pageYOffset &&
    (left + width) > window.pageXOffset
  );
}

更新

在现代浏览器中,您可能需要查看Intersection Observer API ,它提供以下好处:

  • 比收听滚动事件更好的性能
  • 适用于跨域 iframe
  • 可以判断一个元素是否阻碍 / 交叉另一个元素

Intersection Observer 正在成为一个成熟的标准,并已在 Chrome 51 +,Edge 15 + 和 Firefox 55 + 中得到支持,目前正在开发 Safari。还有一个polyfill可用。


以前的答案

Dan 提供答案存在一些问题,可能会使其在某些情况下成为不合适的方法。其中一些问题在他的答案附近指出,他的代码将给出以下元素的误报:

  • 被测试者面前的另一个元素隐藏
  • 在父元素或祖先元素的可见区域之外
  • 使用 CSS clip属性隐藏的元素或其子元素

这些限制在以下简单测试结果中得到证明:

使用isElementInViewport进行测试失败

解决方案: isElementVisible()

以下是这些问题的解决方案,下面是测试结果,并解释了代码的某些部分。

function isElementVisible(el) {
    var rect     = el.getBoundingClientRect(),
        vWidth   = window.innerWidth || doc.documentElement.clientWidth,
        vHeight  = window.innerHeight || doc.documentElement.clientHeight,
        efp      = function (x, y) { return document.elementFromPoint(x, y) };     

    // Return false if it's not in the viewport
    if (rect.right < 0 || rect.bottom < 0 
            || rect.left > vWidth || rect.top > vHeight)
        return false;

    // Return true if any of its four corners are visible
    return (
          el.contains(efp(rect.left,  rect.top))
      ||  el.contains(efp(rect.right, rect.top))
      ||  el.contains(efp(rect.right, rect.bottom))
      ||  el.contains(efp(rect.left,  rect.bottom))
    );
}

通过测试: http //jsfiddle.net/AndyE/cAY8c/

结果如下:

通过测试,使用isElementVisible

补充说明

然而,这种方法并非没有自身的局限性。例如,即使前面的元素实际上没有隐藏它的任何部分,使用比同一位置的另一个元素更低的 z-index 测试的元素也将被识别为隐藏。尽管如此,这种方法在某些情况下仍然使用 Dan 的解决方案。

element.getBoundingClientRect()document.elementFromPoint()都是 CSSOM 工作草案规范的一部分,并且至少在 IE 6 及更高版本和大多数桌面浏览器中都支持很长时间(尽管不是很完美)。有关更多信息,请参阅这些函数的 Quirksmode

contains()用于查看document.elementFromPoint()返回的元素是否是我们正在测试可见性的元素的子节点。如果返回的元素是相同的元素,它也返回 true。这只会使检查更加健壮。所有主流浏览器都支持它,Firefox 9.0 是最后一个添加它的人。对于较早的 Firefox 支持,请查看此答案的历史记录。

如果你想在元素周围测试更多的点以获得可见性 - 也就是说,为了确保元素不被覆盖超过 50%,那么调整答案的最后部分并不需要太多。但是,请注意,如果检查每个像素以确保它是 100%可见,则可能会非常慢。