0

I am trying to display a node next to another (on top of the content), with Javascript. I cannot change the document structure for this node, and I need to insert the new node under the same parent. I would like to use position:absolute to position the new node, but it seems hard to find the reference node position. Using getBoundingClientRect() on the reference node does not work, because the node might be inside an ancestor with position:relative or position:absolute, and getBoundingClientRect() returns a position relative to the document, which cannot be used as coordinates with CSS position:absolute.

Do I really have to loop through the ancestors to find the first one with position:absolute or position:relative in order to find the CSS absolute position, or is there an easier way to get this information ?

Note: I am looking for an answer in pure Javascript.

Update: In other words, I am looking for the top/left pixel position an element should have to be displayed at the same place when its CSS position is changed to 'absolute'. offsetTop/offsetLeft is not the answer because the element might be inside a table, and in that case offsetParent is the table cell.

Damien
  • 3,060
  • 1
  • 25
  • 29
  • possible duplicate of [Javascript: get position/offset of element relative to a parent container?](http://stackoverflow.com/questions/11634770/javascript-get-position-offset-of-element-relative-to-a-parent-container) - accepted answer details the pure javascript properties for this – Josh Harrison Feb 04 '15 at 15:23
  • I do not see how these questions are related: I am not looking for the position relative to the parent. – Damien Feb 04 '15 at 15:56
  • I've added an example to demonstrate how to use `offsetLeft` and `offsetTop`. Perhaps the title of that question was misleading, but the solution is the same - you need the position relative to the `offsetParent` which may not actually be the direct parent - it refers to the nearest ancestor with absolute or relative positioning. – Josh Harrison Feb 04 '15 at 16:14
  • Here is a fiddle to experiment: http://jsfiddle.net/ujzppwwd/ – Damien Feb 06 '15 at 16:12

3 Answers3

1

Here's a recursive solution:

function getCSSAbsolutePosition(el) {
  var pos= ('absolute relative').indexOf(getComputedStyle(el).position) == -1,
      rect1= {top: el.offsetTop * pos, left: el.offsetLeft * pos},
      rect2= el.offsetParent ? getCSSAbsolutePosition(el.offsetParent) : {top:0,left:0};

  return {top: rect1.top + rect2.top,
          left: rect1.left + rect2.left,
          width: el.offsetWidth,
          height: el.offsetHeight
         };
}

In this Fiddle, a red sentence needs to be highlighted. Click the button, and getCSSAbsolutePosition will be called to get its coordinates, width, and height. A new div is placed underneath it (with a negative z-Index), which has a background and border.

Notice that the "goal" div is within an absolute-positioned table within a relative-positioned div, with various padding and margins applied.

Rick Hitchcock
  • 35,202
  • 5
  • 48
  • 79
  • In your fiddle you append the new element to document.body. One of my requirements is that the new element is added inside the parent of the target ("I need to insert the new node under the same parent"; in your fiddle, that would be td). – Damien Feb 05 '15 at 15:11
  • I think I understand. See http://jsfiddle.net/mqb9ua12/ Is this what you're looking for? If so, I'll update my response. – Rick Hitchcock Feb 05 '15 at 15:24
  • This is more like it, but your new fiddle does not work if you have a div in-between the td and the target: http://jsfiddle.net/0ohj6muy/ – Damien Feb 05 '15 at 16:18
  • I was confused as to why your second fiddle worked at all, and I found why: a syntax error in "div.style.left= (rect1.left - rect2.left) + 'px;'" (the left property was not set). – Damien Feb 05 '15 at 16:29
  • Ha, good catch. And good point about the div in-between. This is more complicated than I thought. – Rick Hitchcock Feb 05 '15 at 16:34
  • This new answer does not account for possible ancestor scrolling: in the fiddle, if the parent div has "height:200px; overflow:auto" and is scrolled before you click "highlight", the new element is not positioned correctly. – Damien Feb 05 '15 at 21:32
  • Ahh, you can't put an absolute positioned element *within* a non-positioned element that has overflow. See http://jsfiddle.net/c7armdnr/. It works if you add `position: relative` to the element that has the overflow. – Rick Hitchcock Feb 05 '15 at 21:48
  • Now see http://jsfiddle.net/5nfjfw14/. I've added `position: relative` to the element with overflow, and my solution works. – Rick Hitchcock Feb 05 '15 at 21:49
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/70344/discussion-between-damien-and-rick-hitchcock). – Damien Feb 05 '15 at 22:15
0

This is my current solution. It works, but I am still looking for a simpler one.

var getCSSAbsolutePosition = function getCSSAbsolutePosition(el) {
    var x = 0;
    var y = 0;
    while (el && !isNaN(el.offsetLeft) && !isNaN(el.offsetTop)) {
        x += el.offsetLeft - el.scrollLeft;
        y += el.offsetTop - el.scrollTop;
        el = el.offsetParent;
        if (el) {
            var style = window.getComputedStyle(el);
            if (style.position == 'absolute' || style.position == 'relative')
                break;
        }
    }
    return {top: y, left: x};
}
Damien
  • 3,060
  • 1
  • 25
  • 29
0

You can use the offsetLeft and offsetTop properties to get the position of an element relative to its offsetParent.

Here's a quick example, taking into account the fact that an element's offsetParent may be a table cell without positioning as you so rightly point out:

function positionOver(targetNode) {
    var el = document.createElement("div"),
        left = targetNode.offsetLeft,
        top = targetNode.offsetTop,
        testParent = targetNode.offsetParent;

    while (testParent) {
        if (
            testParent.tagName === "TD" ||
            testParent.tagName === "TH" ||
            testParent.tagName === "TABLE"
        ) {
            left += testParent.offsetLeft;
            top += testParent.offsetTop;
        }
        testParent = testParent.offsetParent;
    }
    el.style.left = left + "px";
    el.style.top = top + "px";
    el.classList.add("created");
    targetNode.parentNode.appendChild(el);
}

// Create and position a new element over #target
positionOver(document.querySelector("#target"));

CSS

.created {
    position: absolute;
    background: red;
    z-index: 10;
    width: 10px;
    height: 10px;
}

Fiddle here.

Note that the above function won't work as expected if the targetNode is inside an absolutely positioned table.

Josh Harrison
  • 5,927
  • 1
  • 30
  • 44
  • offsetLeft and offsetTop are not the answer, because the parent might not be absolutely positioned. Another ancestor might be (or not: I am not assuming anything about the ancestors). Also, the target node might not be absolutely positioned either. – Damien Feb 04 '15 at 16:20
  • You mean like this? http://jsfiddle.net/ju3uyva3/3/ - uses the exact same technique, no positioning at all on the target or parent nodes. – Josh Harrison Feb 04 '15 at 16:26
  • ...and [this](http://jsfiddle.net/ju3uyva3/4/) with an ancestor who has some positioning. – Josh Harrison Feb 04 '15 at 16:27
  • You are right that it works when an ancestor is absolutely positioned (I misunderstood what offsetLeft/offsetTop were), but it does not work if the element is inside a table: http://jsfiddle.net/j1jpydu7/ (because then offsetLeft is relative to the first table cell ancestor). My solution still works inside a table. – Damien Feb 04 '15 at 18:36
  • Hmm, you're right! I missed that note in the docs. Fun problem :) My updated answer hopefully deals with this, just by checking if any of the element's `offsetParent`s are table cells and adding on their offsets. I suppose we're kind of almost back to where you were with your original method in terms of a loop. – Josh Harrison Feb 05 '15 at 09:10
  • You know what, my updated example breaks if the table in the fiddle has `absolute` positioning. Your original code works fine with that. Given that the `offsetParent` loop seems unavoidable, I wave my white flag, but it was fun! I'll delete my answer soon as it's not an improvement. – Josh Harrison Feb 05 '15 at 09:18
  • I think you should keep your answer visible, it could help other people. You should just update it to explain why it is not working. Actually, it would have been good to preserve your inital answer too, so if you have it somewhere, I think you should re-insert it (without it, the comments don't make sense). We all learn by making mistakes :-) – Damien Feb 05 '15 at 13:35
  • Also, maybe you could add a comment to the question, to better explain what this question is about, and why it is not a duplicate ? I tried to be clear, but it was not easy and I was not surprised that the question was misunderstood. Thanks ! – Damien Feb 05 '15 at 13:40