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

(这个问题指的是Firefox。)


当前回答

 const isHTMLElementInView = (element: HTMLElement) => {
  const rect = element?.getBoundingClientRect()

  if (!rect) return
  return rect.top <= window.innerHeight && rect.bottom >= 0
 }

这个函数检查元素是否在垂直水平的视口中。

其他回答

更新

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

在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);

/**
 * Returns Element placement information in Viewport
 * @link https://stackoverflow.com/a/70476497/2453148
 *
 * @typedef {object} ViewportInfo - Whether the element is…
 * @property {boolean} isInViewport - fully or partially in the viewport
 * @property {boolean} isPartiallyInViewport - partially in the viewport
 * @property {boolean} isInsideViewport - fully inside viewport
 * @property {boolean} isAroundViewport - completely covers the viewport
 * @property {boolean} isOnEdge - intersects the edge of viewport
 * @property {boolean} isOnTopEdge - intersects the top edge
 * @property {boolean} isOnRightEdge - intersects the right edge
 * @property {boolean} isOnBottomEdge - is intersects the bottom edge
 * @property {boolean} isOnLeftEdge - is intersects the left edge
 *
 * @param el Element
 * @return {Object} ViewportInfo
 */
function getElementViewportInfo(el) {

    let result = {};

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

    let insideX = rect.left >= 0 && rect.left + rect.width <= windowWidth;
    let insideY = rect.top >= 0 && rect.top + rect.height <= windowHeight;

    result.isInsideViewport = insideX && insideY;

    let aroundX = rect.left < 0 && rect.left + rect.width > windowWidth;
    let aroundY = rect.top < 0 && rect.top + rect.height > windowHeight;

    result.isAroundViewport = aroundX && aroundY;

    let onTop    = rect.top < 0 && rect.top + rect.height > 0;
    let onRight  = rect.left < windowWidth && rect.left + rect.width > windowWidth;
    let onLeft   = rect.left < 0 && rect.left + rect.width > 0;
    let onBottom = rect.top < windowHeight && rect.top + rect.height > windowHeight;

    let onY = insideY || aroundY || onTop || onBottom;
    let onX = insideX || aroundX || onLeft || onRight;

    result.isOnTopEdge    = onTop && onX;
    result.isOnRightEdge  = onRight && onY;
    result.isOnBottomEdge = onBottom && onX;
    result.isOnLeftEdge   = onLeft && onY;

    result.isOnEdge = result.isOnLeftEdge || result.isOnRightEdge ||
        result.isOnTopEdge || result.isOnBottomEdge;

    let isInX =
        insideX || aroundX || result.isOnLeftEdge || result.isOnRightEdge;
    let isInY =
        insideY || aroundY || result.isOnTopEdge || result.isOnBottomEdge;

    result.isInViewport = isInX && isInY;

    result.isPartiallyInViewport =
        result.isInViewport && result.isOnEdge;

    return result;
}
 const isHTMLElementInView = (element: HTMLElement) => {
  const rect = element?.getBoundingClientRect()

  if (!rect) return
  return rect.top <= window.innerHeight && rect.bottom >= 0
 }

这个函数检查元素是否在垂直水平的视口中。

我认为这是一种更实用的方法。 Dan的答案在递归上下文中不起作用。

此函数通过递归测试HTML标记之前的任何级别,并在第一个false处停止,解决了当您的元素位于其他可滚动div中的问题。

/**
 * fullVisible=true only returns true if the all object rect is visible
 */
function isReallyVisible(el, fullVisible) {
    if ( el.tagName == "HTML" )
            return true;
    var parentRect=el.parentNode.getBoundingClientRect();
    var rect = arguments[2] || el.getBoundingClientRect();
    return (
            ( fullVisible ? rect.top    >= parentRect.top    : rect.bottom > parentRect.top ) &&
            ( fullVisible ? rect.left   >= parentRect.left   : rect.right  > parentRect.left ) &&
            ( fullVisible ? rect.bottom <= parentRect.bottom : rect.top    < parentRect.bottom ) &&
            ( fullVisible ? rect.right  <= parentRect.right  : rect.left   < parentRect.right ) &&
            isReallyVisible(el.parentNode, fullVisible, rect)
    );
};