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

(这个问题指的是Firefox。)


当前回答

新的交集观察者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>

其他回答

对于类似的挑战,我非常喜欢这个要点,它为scrollIntoViewIfNeeded()暴露了一个填充。

所有必要的功夫都需要回答这个问题:

var parent = this.parentNode,
    parentComputedStyle = window.getComputedStyle(parent, null),
    parentBorderTopWidth = parseInt(parentComputedStyle.getPropertyValue('border-top-width')),
    parentBorderLeftWidth = parseInt(parentComputedStyle.getPropertyValue('border-left-width')),
    overTop = this.offsetTop - parent.offsetTop < parent.scrollTop,
    overBottom = (this.offsetTop - parent.offsetTop + this.clientHeight - parentBorderTopWidth) > (parent.scrollTop + parent.clientHeight),
    overLeft = this.offsetLeft - parent.offsetLeft < parent.scrollLeft,
    overRight = (this.offsetLeft - parent.offsetLeft + this.clientWidth - parentBorderLeftWidth) > (parent.scrollLeft + parent.clientWidth),
    alignWithTop = overTop && !overBottom;

这指的是你想知道的元素,例如,overTop或overBottom -你只需要得到漂移…

我发现这里公认的答案对于大多数用例来说过于复杂。这段代码很好地完成了工作(使用jQuery),并区分了完全可见和部分可见的元素:

var element         = $("#element");
var topOfElement    = element.offset().top;
var bottomOfElement = element.offset().top + element.outerHeight(true);
var $window         = $(window);

$window.bind('scroll', function() {

    var scrollTopPosition   = $window.scrollTop()+$window.height();
    var windowScrollTop     = $window.scrollTop()

    if (windowScrollTop > topOfElement && windowScrollTop < bottomOfElement) {
        // Element is partially visible (above viewable area)
        console.log("Element is partially visible (above viewable area)");

    } else if (windowScrollTop > bottomOfElement && windowScrollTop > topOfElement) {
        // Element is hidden (above viewable area)
        console.log("Element is hidden (above viewable area)");

    } else if (scrollTopPosition < topOfElement && scrollTopPosition < bottomOfElement) {
        // Element is hidden (below viewable area)
        console.log("Element is hidden (below viewable area)");

    } else if (scrollTopPosition < bottomOfElement && scrollTopPosition > topOfElement) {
        // Element is partially visible (below viewable area)
        console.log("Element is partially visible (below viewable area)");

    } else {
        // Element is completely visible
        console.log("Element is completely visible");
    }
});

更新

在现代浏览器中,你可能想看看交集观察者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%可见的,它可能会非常慢。

现在大多数浏览器都支持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解释。

作为Element.getBoundingClientRect()的支持,最简单的解决方案已经变得完美:

function isInView(el) {
  const box = el.getBoundingClientRect();
  return box.top < window.innerHeight && box.bottom >= 0;
}