按实际z-index比较HTML元素。

12
给定同一文档中的两个任意HTML元素A和B,如何找出哪一个更接近用户(即如果它们重叠,哪一个遮挡了另一个)? W3C CSS规范描述了堆叠上下文,符合规范的渲染引擎应该实现。然而,我无法找到一种方法来在JavaScript程序中访问这些信息,无论是跨浏览器还是不跨浏览器。我只能读取css z-index属性,这本身并没有太多意义,因为大多数情况下它被设置为auto,甚至当表示为数字值时,也不能可靠地指示它实际上是如何显示的(如果它们属于不同的堆叠上下文,则比较z-index是无关紧要的)。
请注意,我对任意元素感兴趣:如果两个元素都在鼠标指针下方,只有一个将被视为“悬停”,因此在这种情况下可以轻松找到最接近的元素。但是,我正在寻找一种更通用的解决方案,最好不涉及重新实现渲染引擎已执行的堆叠算法。

更新:让我稍微澄清一下这个问题背后的原因:最近我遇到了一个问题,暴露出jQuery的拖放机制存在限制——当拖动时它不会考虑z-index(层叠顺序),所以如果一个元素挡住了另一个元素,它仍然可以在"后面"的元素上执行拖放操作。虽然链接中的问题已经为OP的特定情况得到了解答,但是普遍的问题仍然存在,而且我不知道有什么简单的解决方案。

alex's answer以下是有用的,但对于手头的情况不够:在拖动时,被拖动的元素本身(或更准确地说是其帮助程序)是鼠标光标下面最上面的元素,因此elementFromPoint将返回而不是我们真正需要的下一个最上面的元素(解决方法:style the cursor使其位于帮助程序之外)。 jQuery使用的其他交集策略还考虑了不止一个点,从而使确定与帮助程序相交的最上面的元素变得复杂。能够按实际z-index比较(或排序)元素将使“z-index感知”交集模式适用于一般情况。

3个回答

5

您可以获取元素的尺寸和偏移量,然后使用document.elementFromPoint()来确定哪个元素是最上层渲染的元素。


这非常有用!我希望有一种不需要给定点的方法,但对于许多实际目的来说,这确实提供了一个很好的解决方案。 - mgibsonbr

4

注意:经过一年多没有答案的情况,这个问题也在葡萄牙的Stack Overflow上发布,虽然仍然没有一个确定的解决方案,但一些用户和我能够在JavaScript中复制堆叠机制(重新发明轮子,但仍然……)

引用CSS2规范中的堆叠上下文算法(强调是我的):

根元素形成根堆叠上下文。其他堆叠上下文由任何定位元素(包括相对定位元素)具有计算值为“z-index”的值而不是“auto”。堆叠上下文不一定与包含块相关。在CSS的未来级别中,其他属性可能会引入堆叠上下文,例如'opacity'

根据这个描述,以下是一个函数的返回值: 如果一个元素生成了新的堆叠上下文,则返回该元素的 z-index;如果没有生成,则返回 undefined

function zIndex(ctx) {
    if ( !ctx || ctx === document.body ) return;

    var positioned = css(ctx, 'position') !== 'static';
    var hasComputedZIndex = css(ctx, 'z-index') !== 'auto';
    var notOpaque = +css(ctx, 'opacity') < 1;

    if(positioned && hasComputedZIndex) // Ignoring CSS3 for now
        return +css(ctx, 'z-index');
}

function css(el, prop) {
     return window.getComputedStyle(el).getPropertyValue(prop);
}

这应该能够区分形成不同堆叠上下文的元素。对于其余的元素(以及具有相等 z-index 的元素),附录 E 表示它们应该遵循“树顺序”:

在考虑移动框的属性后,按逻辑(而非视觉)顺序对呈现树进行先序深度优先遍历,对于双向内容。

除了那些“移动框的属性”之外,此函数应正确实现遍历:

/* a and b are the two elements we want to compare.
 * ctxA and ctxB are the first noncommon ancestor they have (if any)
 */
function relativePosition(ctxA, ctxB, a, b) {
    // If one is descendant from the other, the parent is behind (preorder)
    if ( $.inArray(b, $(a).parents()) >= 0 )
        return a;
    if ( $.inArray(a, $(b).parents()) >= 0 )
        return b;
    // If two contexts are siblings, the one declared first - and all its
    // descendants (depth first) - is behind
    return ($(ctxA).index() - $(ctxB).index() > 0 ? a : b);
}

有了这两个函数的定义,我们终于可以创建我们的元素比较函数了:
function inFront(a, b) {
    // Skip all common ancestors, since no matter its stacking context,
    // it affects a and b likewise
    var pa = $(a).parents(), ia = pa.length;
    var pb = $(b).parents(), ib = pb.length;
    while ( ia >= 0 && ib >= 0 && pa[--ia] == pb[--ib] ) { }

    // Here we have the first noncommon ancestor of a and b  
    var ctxA = (ia >= 0 ? pa[ia] : a), za = zIndex(ctxA);
    var ctxB = (ib >= 0 ? pb[ib] : b), zb = zIndex(ctxB);

    // Finds the relative position between them    
    // (this value will only be used if neither has an explicit
    // and different z-index)
    var relative = relativePosition(ctxA, ctxB, a, b);

    // Finds the first ancestor with defined z-index, if any
    // The "shallowest" one is what matters, since it defined the most general
    // stacking context (affects all the descendants)
    while ( ctxA && za === undefined ) {
        ctxA = ia < 0 ? null : --ia < 0 ? a : pa[ia];
        za = zIndex(ctxA);
    }
    while ( ctxB && zb === undefined ) {
        ctxB = ib < 0 ? null : --ib < 0 ? b : pb[ib];
        zb = zIndex(ctxB);
    }

    // Compare the z-indices, if applicable; otherwise use the relative method
    if ( za !== undefined ) {
        if ( zb !== undefined )
            return za > zb ? a : za < zb ? b : relative;
        return za > 0 ? a : za < 0 ? b : relative;
    }
    else if ( zb !== undefined )
        return zb < 0 ? a : zb > 0 ? b : relative;
    else
        return relative;
}

以下是三个实践此方法的示例:示例1, 示例2, 示例3(抱歉,没有将所有内容翻译成英语...它们是完全相同的代码,只是不同的函数和变量名称)。
这个解决方案很可能不完整,在边缘情况下可能会失败(尽管我自己找不到任何问题)。如果有人对改进有任何建议,那将非常感激。

谢谢分享,听起来有点疯狂。 - godblessstrawberry

1
经过几天的研究,我认为我已经成功地根据2016年的规则重新实现了堆叠机制。我基本上更新了OP发布的2013年方法。结果是一个比较两个DOM节点并返回视觉上在顶部的节点的函数。
front = $.fn.visuallyInFront(document.documentElement, document.body);
// front == <body>...</body> because the BODY node is 'on top' of the HTML node

推理

确定哪个元素在另一个元素之上还有其他方法。例如,document.elementFromPoint()或者document.elementsFromPoint()可能会想到。然而,有许多(未记录的)因素会影响这些方法的可靠性。例如,不透明度、可见性、指针事件、背面可见性和一些变换可能会使document.elementFromPoint()无法对特定元素进行命中测试。此外,document.elementFromPoint()只能查询最上层的元素(而非底层元素)。这应该可以通过document.elementsFromPoint()来解决,但目前只在Chrome中实现了这一点。除此之外,我向Chrome开发人员提交了一个关于document.elementsFromPoint()的错误报告。当命中测试锚标记时,所有底层元素都被忽略。

所有这些问题的综合使我决定尝试重新实现堆叠机制。这种方法的好处是堆叠机制已经有相当广泛的文档,并且可以进行测试和理解。

工作原理

我的方法重新实现了HTML层叠机制。它旨在正确遵循影响HTML元素层叠顺序的所有规则,包括定位规则、浮动、DOM顺序,还包括CSS3属性,如不透明度、变换以及更多实验性质,如滤镜和蒙版。这些规则似乎已经在2016年3月正确实现,但在规范和浏览器支持发生变化时,需要进行更新。
我将所有内容放在GitHub存储库中。希望这种方法能够继续可靠地工作。这是示例JSFiddle的代码演示。在示例中,所有元素都按实际“z-index”排序,这就是OP想要的。
对此方法的测试和反馈将非常欢迎!

我会在有时间的时候更仔细地测试你的代码,但乍一看它似乎没问题。目前我接受了你的答案,因为它是我们目前为止最完整的答案。 - mgibsonbr

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接