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

IT技术 javascript html firefox dom browser
2020-12-22 15:44:30

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

(问题是指 Firefox。)

6个回答

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

选择为正确的解决方案几乎从不精确


此解决方案已在 Internet Explorer 7(及更高版本)、iOS 5(及更高版本)Safari、Android 2.0 (Eclair) 及更高版本、BlackBerry、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

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

计算假设元素小于屏幕。如果您有高或宽的元素,使用它可能更准确return (rect.bottom >= 0 && rect.right >= 0 && rect.top <= (window.innerHeight || document.documentElement.clientHeight) && rect.left <= (window.innerWidth || document.documentElement.clientWidth));
2021-02-07 15:44:30
我正在使用此解决方案(不过要注意“底部”拼写错误)。还有一些事情需要注意,当我们考虑的元素会包含图像时。Chrome(至少)必须等待图像加载以具有 boundingRectangle 的确切值。Firefox 似乎没有这个“问题”
2021-02-10 15:44:30
el is not defined
2021-02-16 15:44:30
当您在 body 内的容器中启用滚动时,它是否有效。例如,它在这里不起作用 - agaase.github.io/webpages/demo/isonscreen2.html isElementInViewport(document.getElementById("innerele"))。innerele 存在于启用了滚动的容器内。
2021-02-20 15:44:30
提示:对于那些试图用 jQuery 实现这一点的人,只是一个友好的提醒,传递 HTML DOM 对象(例如,isElementInViewport(document.getElementById('elem')))而不是 jQuery 对象(例如,isElementInViewport($("#elem)))。jQuery 等效项是这样添加的[0]isElementInViewport($("#elem)[0]).
2021-02-22 15:44:30

更新:时间在前进,我们的浏览器也在前进。不再推荐使用此技术,如果您不需要支持 Internet Explorer 7 之前的版本,则应使用Dan 的解决方案

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

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

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
  );
}
也很好奇@amartynov 的问题。任何人都知道如何简单地判断一个元素是否由于祖先元素的溢出而被隐藏?如果无论子级嵌套多深都可以检测到这一点,则奖励。
2021-02-14 15:44:30
如果元素位于可滚动的 div 中并从视图中滚动出来怎么办?
2021-02-27 15:44:30
原来的函数贴有错误。在重新分配 el 之前需要保存宽度/高度...
2021-03-01 15:44:30
请查看以下脚本的较新版本
2021-03-01 15:44:30
@deadManN 通过 DOM 递归是出了名的慢。这就是理由,但浏览器供应商也getBoundingClientRect专门为查找元素坐标而创建......我们为什么不使用它?
2021-03-09 15:44:30

更新

在现代浏览器中,您可能需要查看Intersection Observer API,它具有以下优点:

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

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


上一个答案

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

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

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

测试失败,使用 isElementInViewport

解决方案: 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/

结果:

通过测试,使用 isElementVisible

补充说明

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

两者element.getBoundingClientRect()document.elementFromPoint()是CSSOM工作草案规范的一部分,并且在至少IE 6和更高的支持和长一段时间的桌面浏览器(尽管,不完全)。有关更多信息,请参阅有关这些功能的 Quirksmode

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

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

你的意思是使用 doc.documentElement.clientWidth 吗?那应该是'document.documentElement'吗?另一方面,这是唯一一种也适用于使用 CSS 'clip' 属性隐藏元素内容以实现可访问性的用例的方法:snook.ca/archives/html_and_css/hiding-content-for-accessibility
2021-02-16 15:44:30
没有为我处理输入(chrome canary 50)。不知道为什么,也许是原生圆角?我不得不稍微减少坐标以使其工作 el.contains(efp(rect.left+1, rect.top+1)) || el.contains(efp(rect.right-1, rect.top+1)) || el.contains(efp(rect.right-1, rect.bottom-1)) || el.contains(efp(rect.left+1, rect.bottom-1))
2021-02-22 15:44:30
如果您有圆角或应用了变换,检查元素的中心是否可见也可能是有益的,因为边界角可能不会返回预期的元素: element.contains(efp(rect.right - (rect.width / 2), rect.bottom - (rect.height / 2)))
2021-02-24 15:44:30
@klamping:很好的收获,谢谢。我已经将它从我doc用作document. 是的,我喜欢将其视为边缘情况的一个不错的解决方案。
2021-02-27 15:44:30
对我来说这是行不通的。但是之前答案中的 inViewport() 正在 FF 中工作。
2021-03-02 15:44:30

我尝试了丹的回答,但是,用于确定边界的代数意味着元素必须≤视口大小并且完全在视口内才能得到true,这很容易导致误报。如果你想确定一个元素是否在视口中,ryanve 的答案很接近,但被测试的元素应该与视口重叠,所以试试这个:

function isElementInViewport(el) {
    var rect = el.getBoundingClientRect();

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

作为一项公共服务:
Dan 的答案具有正确的计算(元素可以是 > 窗口,尤其是在手机屏幕上)和正确的 jQuery 测试,以及添加 isElementPartiallyInViewport:

顺便说一下,window.innerWidth 和 document.documentElement.clientWidth的区别在于 clientWidth/clientHeight 不包含滚动条,而 window.innerWidth/Height 包含。

function isElementPartiallyInViewport(el)
{
    // Special bonus for those using jQuery
    if (typeof jQuery !== 'undefined' && el instanceof jQuery) 
        el = el[0];

    var rect = el.getBoundingClientRect();
    // DOMRect { x: 8, y: 8, width: 100, height: 100, top: 8, right: 108, bottom: 108, left: 8 }
    var windowHeight = (window.innerHeight || document.documentElement.clientHeight);
    var windowWidth = (window.innerWidth || document.documentElement.clientWidth);

    // http://stackoverflow.com/questions/325933/determine-whether-two-date-ranges-overlap
    var vertInView = (rect.top <= windowHeight) && ((rect.top + rect.height) >= 0);
    var horInView = (rect.left <= windowWidth) && ((rect.left + rect.width) >= 0);

    return (vertInView && horInView);
}


// http://stackoverflow.com/questions/123999/how-to-tell-if-a-dom-element-is-visible-in-the-current-viewport
function isElementInViewport (el)
{
    // Special bonus for those using jQuery
    if (typeof jQuery !== 'undefined' && el instanceof jQuery) 
        el = el[0];

    var rect = el.getBoundingClientRect();
    var windowHeight = (window.innerHeight || document.documentElement.clientHeight);
    var windowWidth = (window.innerWidth || document.documentElement.clientWidth);

    return (
           (rect.left >= 0)
        && (rect.top >= 0)
        && ((rect.left + rect.width) <= windowWidth)
        && ((rect.top + rect.height) <= windowHeight)
    );
}


function fnIsVis(ele)
{
    var inVpFull = isElementInViewport(ele);
    var inVpPartial = isElementPartiallyInViewport(ele);
    console.clear();
    console.log("Fully in viewport: " + inVpFull);
    console.log("Partially in viewport: " + inVpPartial);
}

测试用例

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="description" content="">
    <meta name="author" content="">
    <title>Test</title>
    <!--
    <script src="http://cdnjs.cloudflare.com/ajax/libs/jquery/1.8.3/jquery.min.js"></script>
    <script src="scrollMonitor.js"></script>
    -->

    <script type="text/javascript">

        function isElementPartiallyInViewport(el)
        {
            // Special bonus for those using jQuery
            if (typeof jQuery !== 'undefined' && el instanceof jQuery) 
                el = el[0];

            var rect = el.getBoundingClientRect();
            // DOMRect { x: 8, y: 8, width: 100, height: 100, top: 8, right: 108, bottom: 108, left: 8 }
            var windowHeight = (window.innerHeight || document.documentElement.clientHeight);
            var windowWidth = (window.innerWidth || document.documentElement.clientWidth);

            // http://stackoverflow.com/questions/325933/determine-whether-two-date-ranges-overlap
            var vertInView = (rect.top <= windowHeight) && ((rect.top + rect.height) >= 0);
            var horInView = (rect.left <= windowWidth) && ((rect.left + rect.width) >= 0);

            return (vertInView && horInView);
        }


        // http://stackoverflow.com/questions/123999/how-to-tell-if-a-dom-element-is-visible-in-the-current-viewport
        function isElementInViewport (el)
        {
            // Special bonus for those using jQuery
            if (typeof jQuery !== 'undefined' && el instanceof jQuery) 
                el = el[0];

            var rect = el.getBoundingClientRect();
            var windowHeight = (window.innerHeight || document.documentElement.clientHeight);
            var windowWidth = (window.innerWidth || document.documentElement.clientWidth);

            return (
                   (rect.left >= 0)
                && (rect.top >= 0)
                && ((rect.left + rect.width) <= windowWidth)
                && ((rect.top + rect.height) <= windowHeight)
            );
        }


        function fnIsVis(ele)
        {
            var inVpFull = isElementInViewport(ele);
            var inVpPartial = isElementPartiallyInViewport(ele);
            console.clear();
            console.log("Fully in viewport: " + inVpFull);
            console.log("Partially in viewport: " + inVpPartial);
        }


        // var scrollLeft = (window.pageXOffset !== undefined) ? window.pageXOffset : (document.documentElement || document.body.parentNode || document.body).scrollLeft,
        // var scrollTop = (window.pageYOffset !== undefined) ? window.pageYOffset : (document.documentElement || document.body.parentNode || document.body).scrollTop;
    </script>
</head>

<body>
    <div style="display: block; width: 2000px; height: 10000px; background-color: green;">

        <br /><br /><br /><br /><br /><br />
        <br /><br /><br /><br /><br /><br />
        <br /><br /><br /><br /><br /><br />

        <input type="button" onclick="fnIsVis(document.getElementById('myele'));" value="det" />

        <br /><br /><br /><br /><br /><br />
        <br /><br /><br /><br /><br /><br />
        <br /><br /><br /><br /><br /><br />

        <div style="background-color: crimson; display: inline-block; width: 800px; height: 500px;" ></div>
        <div id="myele" onclick="fnIsVis(this);" style="display: inline-block; width: 100px; height: 100px; background-color: hotpink;">
        t
        </div>

        <br /><br /><br /><br /><br /><br />
        <br /><br /><br /><br /><br /><br />
        <br /><br /><br /><br /><br /><br />

        <input type="button" onclick="fnIsVis(document.getElementById('myele'));" value="det" />
    </div>

    <!--
    <script type="text/javascript">

        var element = document.getElementById("myele");
        var watcher = scrollMonitor.create(element);

        watcher.lock();

        watcher.stateChange(function() {
            console.log("state changed");
            // $(element).toggleClass('fixed', this.isAboveViewport)
        });
    </script>
    -->
</body>
</html>
isElementPartiallyInViewport也非常有用。好一个。
2021-02-11 15:44:30
@MCCCS:哈哈,不错!很高兴它对某人有用;)
2021-02-20 15:44:30
@StefanSteiger 根据 MDN,它从 IE9 开始就得到支持,因此直接使用 window.innerHeight 实际上是安全的(至少在我的情况下)。谢谢!
2021-02-24 15:44:30
@Arun chauhan:我的代码都没有加载图像,所以为什么要加载,而且公式是正确的。
2021-02-27 15:44:30
@targumon:原因是支持旧浏览器。
2021-03-06 15:44:30