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

(这个问题指的是Firefox。)


当前回答

查看verge的源代码,它使用getBoundingClientRect。这就像:

function inViewport (element) {
  if (!element) return false;
  if (1 !== element.nodeType) return false;

  var html = document.documentElement;
  var rect = element.getBoundingClientRect();

  return !!rect &&
    rect.bottom >= 0 &&
    rect.right >= 0 && 
    rect.left <= html.clientWidth &&
    rect.top <= html.clientHeight;
}

如果元素的任何部分在视口中,则返回true。

其他回答

查看verge的源代码,它使用getBoundingClientRect。这就像:

function inViewport (element) {
  if (!element) return false;
  if (1 !== element.nodeType) return false;

  var html = document.documentElement;
  var rect = element.getBoundingClientRect();

  return !!rect &&
    rect.bottom >= 0 &&
    rect.right >= 0 && 
    rect.left <= html.clientWidth &&
    rect.top <= html.clientHeight;
}

如果元素的任何部分在视口中,则返回true。

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

被选为正确的解几乎从来不是精确的。


该解决方案在Internet Explorer 7(及更高版本)、iOS 5(及更高版本)Safari、Android 2.0 (Eclair)及更高版本、黑莓、Opera Mobile和Internet Explorer Mobile 9上进行了测试。


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); // Internet Explorer 9+ :(
    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);

你可以使用HTML5 API的pageVisibiliy功能,如果你关心你的网页标签是否活跃和可见。

此方法不能处理以下两种情况:

使用z-index进行重叠。 在元素的容器中使用溢出滚动。 尝试一些新的东西-路口观察者API解释。

在Android上放大谷歌Chrome浏览器时,最被接受的答案是不工作的。结合Dan的回答,要考虑Android上的Chrome,必须使用visualViewport。下面的例子只考虑了垂直检查,并使用jQuery来计算窗口高度:

var Rect = YOUR_ELEMENT.getBoundingClientRect();
var ElTop = Rect.top, ElBottom = Rect.bottom;
var WindowHeight = $(window).height();
if(window.visualViewport) {
    ElTop -= window.visualViewport.offsetTop;
    ElBottom -= window.visualViewport.offsetTop;
    WindowHeight = window.visualViewport.height;
}
var WithinScreen = (ElTop >= 0 && ElBottom <= WindowHeight);

新的交集观察者API非常直接地解决了这个问题。

这个解决方案将需要一个polyfill,因为Safari, Opera和Internet Explorer还不支持这个(polyfill包含在解决方案中)。

在这个解决方案中,在视图外有一个框,即目标(观察到的)。当它进入视图时,头部顶部的按钮是隐藏的。一旦框离开视图,就会显示它。

const buttonToHide = document.querySelector('button'); const hideWhenBoxInView = new IntersectionObserver((entries) => { if (entries[0].intersectionRatio <= 0) { // If not in view buttonToHide.style.display = "inherit"; } else { buttonToHide.style.display = "none"; } }); hideWhenBoxInView.observe(document.getElementById('box')); header { position: fixed; top: 0; width: 100vw; height: 30px; background-color: lightgreen; } .wrapper { position: relative; margin-top: 600px; } #box { position: relative; left: 175px; width: 150px; height: 135px; background-color: lightblue; border: 2px solid; } <script src="https://polyfill.io/v2/polyfill.min.js?features=IntersectionObserver"></script> <header> <button>NAVIGATION BUTTON TO HIDE</button> </header> <div class="wrapper"> <div id="box"> </div> </div>

更新

在现代浏览器中,你可能想看看交集观察者API,它提供了以下好处:

比监听滚动事件更好的性能 工作在跨域iframes 可以判断一个元素是否阻碍/交叉另一个元素

交集观察者正在成为一个成熟的标准,已经在Chrome 51+, Edge 15+和Firefox 55+中得到支持,并且正在为Safari开发中。还有一种可用的填充材料。


以前的回答

Dan提供的答案有一些问题,可能使它不适用于某些情况。他在接近底部的回答中指出了其中一些问题,他的代码会对以下元素给出假阳性:

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

以下简单测试的结果证明了这些限制:

解决方案:isElementVisible()

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

function isElementVisible(el) {
    var rect     = el.getBoundingClientRect(),
        vWidth   = window.innerWidth || document.documentElement.clientWidth,
        vHeight  = window.innerHeight || document.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/

结果是:

额外的笔记

然而,这种方法也有其局限性。例如,一个被测试的元素在相同位置的z-index比另一个元素低,即使前面的元素实际上没有隐藏它的任何部分,它也会被标识为隐藏。不过,这种方法在丹的解决方案没有涵盖的某些情况下也有它的用途。

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

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

如果您想测试元素周围的更多点以获得可见性—例如,确保元素没有被超过50%的部分覆盖—那么调整答案的最后一部分并不需要太多。然而,请注意,如果你检查每个像素,以确保它是100%可见的,它可能会非常慢。