Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
629 views
in Technique[技术] by (71.8m points)

css - Compare HTML elements by actual z-index

Given two abitrary HTML elements A and B in the same document, how can I find out which one is "closer" to the user (i.e. if they overlap, which one is obscuring the other)?

The W3C CSS Specification describes stacking contexts, which compliant rendering engines should implement. However, I couldn't find a way to access this information in a JavaScript program, cross-browsers or not. All I can read is the css z-index property, that per se doesn't say much, since most of the time is set to auto or, even when expressed as a numeric value, is not a reliable indicator of how it's actually displayed (if they belong to different statcking contexts, comparing z-indexes is irrelevant).

Please note that I'm interested in arbitrary elements: if both elements are below the mouse pointer, only one will be considered "hovered", so I can easily find the closest one in this case. However, I'm looking for a more general solution, preferably one that does not involve re-implementing the stacking algorithm that the rendering engine is already performing.

Update: let me clarify a bit the reason behind this question: I recently tackled a question that exposed a limitation in jQuery's drag and drop mechanism - it doesn't take z-indexes into account when dropping, so if an element is obscuring another, it can still perform the drop operation in the element that is "behind". While the linked question was answered for the OP particular case, the general problem persists, and there's no easy solution that I know of.

alex's answer below is useful, but not enough for the case at hand: when dragging, the dragged element itself (or more precisely its helper) is the topmost element under the mouse cursor, so elementFromPoint will return it instead of the next topmost element, that we really need (workaround: style the cursor so it's placed outside the helper). The other intersection strategies that jQuery employ also take into account more than just one point, complicating the task of determining the topmost element that intersects the helper somehow. Being able to compare (or sort) elements by actual z-index would make a "z-index aware" intersection mode viable for the general case.

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

Note: after over one year without an answer, this question was also posted at Stack Overflow in Portuguese and - while still without a conclusive solution - some users and me were able to replicate the stacking mechanism in JavaScript (reinventing the wheel, but still...)

Quoting the stacking context algorithm at the CSS2 specification (emphasis mine):

The root element forms the root stacking context. Other stacking contexts are generated by any positioned element (including relatively positioned elements) having a computed value of 'z-index' other than 'auto'. Stacking contexts are not necessarily related to containing blocks. In future levels of CSS, other properties may introduce stacking contexts, for example 'opacity'

From that description, here's a function to return: a) the z-index of an element, if it generates a new stacking contex; or b) undefined if it doesn't>

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

This should be able to set apart elements that form different stacking contexts. For the rest of the elements (and for elements with an equal z-index) the Appendix E says they should respect "tree order":

Preorder depth-first traversal of the rendering tree, in logical (not visual) order for bidirectional content, after taking into account properties that move boxes around.

Except for those "properties that move boxes around", this function shoud correctly implements the traversal:

/* 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);
}

With these two functions defined, we can finally create our element comparison function:

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;
}

Here are three examples showing this method in practice: Example 1, Example 2, Example 3 (sorry, didn't bother translating everything to english... it's the exact same code, just different function and variable names).

This solution is most likely incomplete, and should fail in edge cases (though I couldn't find any myself). If anyone has any suggestions for improvements, it'd be really appreciated.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...