legrass
legrass

Reputation: 417

Absolute positioning for nested elements

My problem relates to the following demos http://jsfiddle.net/legrass/ca22L/ and http://jsfiddle.net/legrass/eER66/2/.

In short, I need highlight some elements on a page (e.g., by a red border) but without touching the actual elements (to avoid affecting the original page's layout). For that, in JS I create 'overlay' elements positioned absolutely upon the element to highlight using top, left coordinates. These are appended to body and styled via CSS (see first JSFiddle). For instance:

<body>
<div class='overlay'>
<span class="a" style="top: 14px; left: 321px; width: 200px; height: 140px;"> </span>
<span class="b" style="top: 52px; left: 351px; width: 140px; height: 63px;"> </span>
<span class="c" style="top: 72px; left: 400px; width: 8px; height: 16px;"></span>
<span class="c" style="top: 74px; left: 420px; width: 8px; height: 16px;"></span>
</div>
....

That works fine. However, to allow more flexible css selectors (e.g., child), I need to build the above overlays by nesting them like:

<body>
<div class='overlay'>
<span class="a" style="top: 14px; left: 321px; width: 200px; height: 140px;">
   <span class="b" style="top: 52px; left: 351px; width: 140px; height: 63px;">
      <span class="c" style="top: 72px; left: 400px; width: 8px; height: 16px;"></span>
      <span class="c" style="top: 74px; left: 420px; width: 8px; height: 16px;"></span>
   <span/>
<span/>
</div>
....

The nesting can be at any depth. Unfortunately now the positioning it's incorrect, (see second JSFiddle).

I understand that it is because each span is now positioned absolutely but with respect to its parent, and not anymore to the single container div. I have tried relative positioning but doesn't work of course.

QUESTION: Is there a way to obtain the layout as in the first JSfiddle but maintaining span the hierarchy??

UPDATE From the answer below, adjusting the coordinates by using offsetLeft and offsetTop really helps. I compute the total offset going up the DOM and adding offsetLeft (offsetTop) until offsetParent exists. However, the result is still few pixels shifted, it seems there is something else to be taken into account. Any idea?

SOLVED As mentioned below in the comments, it is necessary to consider also border width.

Upvotes: 3

Views: 4391

Answers (3)

Marat Tanalin
Marat Tanalin

Reputation: 14123

Currently, CSS does not have a way to position element other than relative to its closest ancestor positioned relatively or absolutely. But since you use JS, you can change offset (left, top) for each element according to offsets of its ancestor elements and achieve needed results.

Improved version (taking borders into account) of Quirks mode function:

function getCssPropertyValue(elem, prop) {
    return window.getComputedStyle
         // Modern browsers.
         ? window.getComputedStyle(elem, null).getPropertyValue(prop)
         // IE8 and older.
         : elem.currentStyle.getAttribute(prop);
}

function findPos(obj) {
    var curleft = curtop = 0;

    if (!obj.offsetParent) {
        return;
    }

    do {
        curleft += obj.offsetLeft;
        curtop  += obj.offsetTop;

        // If not Opera and not IE8 (see http://tanalin.com/en/articles/ie-version-js/ )
        // Opera and IE8 return incorrect values otherwise.
        if (!window.opera && (!document.all || document.addEventListener || !document.querySelector)) {
            var blw = parseInt(getCssPropertyValue(obj, 'border-left-width'), 10),
                btw = parseInt(getCssPropertyValue(obj, 'border-top-width'), 10);

            if (blw) {
                curleft += blw;
            }

            if (btw) {
                curtop += btw;
            }
        }
    }
    while (obj = obj.offsetParent);

    return [curleft, curtop];
}

Update: A more clear, compact, fast, precise, future-proof and bullet-proof solution is to use element.getBoundingClientRect():

function getElementCoords(elem) {
    var root  = document.documentElement,
        body  = document.body,
        sTop  = window.pageYOffset || root.scrollTop  || body.scrollTop,
        sLeft = window.pageXOffset || root.scrollLeft || body.scrollLeft,
        cTop  = root.clientTop  || body.clientTop  || 0,
        cLeft = root.clientLeft || body.clientLeft || 0,
        rect  = elem.getBoundingClientRect(),
        top   = Math.round(rect.top  + sTop  - cTop),
        left  = Math.round(rect.left + sLeft - cLeft);

    return {
        top  : top,
        left : left
    };
}

By the way, consider using jQuery's offset() where crossbrowser inconsistencies are already worked around out-of-the-box.

Upvotes: 2

allyourcode
allyourcode

Reputation: 22603

position: absolute makes an element's positioning relative to the first non-statically positioned ancestor (in other words, "absolute" is actually relative!). Since your containing spans are not statically positioned (they are absolutely positioned), the contained spans are positioned relative to their parents, not .overlay. The only way to get the same effect is to account for the cumulative offsets of all ancestors, and set top, left, etc. accordingly.

PS: It's a little odd to use spans for your rectangles, because they are in-line elements, rather than block elements. Why don't you use div instead? Shouldn't change the result, but divs seem like the right thing for you to use.

Upvotes: 0

chrisbulmer
chrisbulmer

Reputation: 1245

You can use outline to add a border without affecting the box model.

Upvotes: 2

Related Questions