GreyWyvern.com

Updated "find position" script

Jan 3, 2011

I've been a long-time user of @PPK's findPos script which works like a veritable charm. Because it's also easy to drop in, it's found its way into dozens of scripts I've written, many to do with mouse interaction and animation.

One script of mine with which the findPos function has been intimately related is the Virtual Keyboard. Because the script needs to position a keyboard element relative to a form input which could be anywhere on the page, the findPos function is an extremely easy and resilient way to find the page position of that input.

A couple years back, TarquinWJ and I discovered a minor issue with the original findPos script relating to CSS fixed position containers. A fixed position element had no offsetParent, even though it did possess offsetTop and offsetLeft values. The solution was to add the offset values first, and check for the presence of the offsetParent value afterwards, a simple rearrangement of the do ... while loop. For your own examination, here is the original findPos code:

function findPos(obj) {
  var curleft = curtop = 0;
  if (obj.offsetParent) {
    while (obj.offsetParent) {
      curleft += obj.offsetLeft;
      curtop += obj.offsetTop;
      obj = obj.offsetParent;
    }
  }
  return [curleft,curtop];
}

The current findPos function at PPK's QuirksMode.org has been updated to reflect the fix for this case. This code works except in the case where you are trying to find the position of the fixed container itself rather than one of its children. The if statement blocks this edge case (will return [0,0] instead of the correct position), and I recommend removing it. I've had no problems with browsers not supporting offsetParent due to this change. PPK's current version is below:

function findPos(obj) {
  var curleft = curtop = 0;
  if (obj.offsetParent) {
    do {
      curleft += obj.offsetLeft;
      curtop += obj.offsetTop;
    } while (obj = obj.offsetParent);
  }
  return [curleft, curtop];
}

... and the version I use is:

function findPos(obj) {
  var curleft = curtop = 0;
  do {
    curleft += obj.offsetLeft;
    curtop += obj.offsetTop;
  } while (obj = obj.offsetParent);
  return [curleft, curtop];
}

As mentioned, I've used this latter function for years without any further problems... until recently :P

Recently a user of the keyboard script contacted me because of problems he was having using the keyboard on his spreadsheet web application. The application consisted of a grid of input elements within a scrollable container. He found that whenever the container was scrolled (down or right), the keyboard would appear in the wrong position relative to the input element which was clicked. Rather, it would appear where the input would have been had the container not been scrolled at all.

I considered a lot of possibilities, but it soon became clear that the findPos function was not accounting for the scroll position of parent containers within the chain of position. For instance, if you had a 300 pixel square element which contained scrollable access to 1000 pixels square of area, and there was an input element in the bottom right hand corner of that area, then if you opened a keyboard for that element, it would appear at a page position approximately 1300 pixels from the left and top of the entire document (probably off-screen). It should appear at about 300 pixels from the left and top of the document (overlapping the scrollable container), with the scrolled distance of the parent container subtracted from the final output values.

To account for this, the function needs to travel up the tree and compile the scroll values of all parent containers (except for the BODY element which is the document scroll value; we want the coordinates relative to this element) so they can be subtracted from the returned values. You'd think this would be easy, since the function is already traveling up the tree, so we just need to add the scroll values at each step. Unfortunately, the current function is traveling up the offsetParent hierarchy rather than the DOM tree. A parent element that scrolls does not have to be an offsetParent, so the current loop is not usable for what we need.

Instead, we need to add a preliminary loop that travels to every parentNode, stopping when we get to the document.body. Once we have added up all of the scroll values as negative numbers, we add to them the offset values in the second loop. Now the function looks like so:

function findPos(obj) {
  var curleft = curtop = 0, scr = obj;
    while ((scr = scr.parentNode) && scr != document.body) {
      curleft -= scr.scrollLeft || 0;
      curtop -= scr.scrollTop || 0;
    }
    do {
      curleft += obj.offsetLeft;
      curtop += obj.offsetTop;
    } while (obj = obj.offsetParent);
  return [curleft, curtop];
}

We've copied the reference to the incoming object to a new variable ( scr) for the added loop to act upon. Because whether the current element can scroll or not is irrelevant to its position in the document, we can start the loop right away at the first parentNode and go from there.

Note that the || 0 bits are in place because the scroll values may return null or the empty string, which when subtracted from the curleft and curtop values would result in NaN. You could also use the Number() constructor here to convert such values to 0. In this case I've gone with the less verbose option.

The function above will find the position of an element relative to the document except when the element itself is of fixed position (or one of its parentNode's is fixed position), and the document has been scrolled away from the top left corner. For some reason, only Opera will report the correct positioning with respect to the document container here, while Chrome, Firefox, IE and Safari all report the offset with respect to the viewport. This means for those four browsers if you want to position an element near the fixed position element, you will have to define that element as fixed position also or else it won't display in the correct location.

This is precisely what the Virtual Keyboard does, because the expected behaviour for the keyboard is to scroll along with the fixed position input as the viewport is scrolled. However in doing so we are sort of bypassing the intended purpose of the script which is to return the coordinates of the element relative to the document 100% of the time. In order to do that, significant modifications are required. I recommend that you examine your intended use and if fixed positioning is not involved at all, please please stick with the simpler version above.

The version below, on the other hand, covers all the bases, but requires two additional functions: one to get the computed CSS style of an element (to detect position:fixed;), and the other to get the cross-browser viewport scroll distance values.

function findPos(obj) {
  var curleft = curtop = 0, scr = obj, fixed = false;
  while ((scr = scr.parentNode) && scr != document.body) {
    curleft -= scr.scrollLeft || 0;
    curtop -= scr.scrollTop || 0;
    if (getStyle(scr, "position") == "fixed") fixed = true;
  }
  if (fixed && !window.opera) {
    var scrDist = scrollDist();
    curleft += scrDist[0];
    curtop += scrDist[1];
  }
  do {
    curleft += obj.offsetLeft;
    curtop += obj.offsetTop;
  } while (obj = obj.offsetParent);
  return [curleft, curtop];
}

function scrollDist() {
  var html = document.getElementsByTagName('html')[0];
  if (html.scrollTop && document.documentElement.scrollTop) {
    return [html.scrollLeft, html.scrollTop];
  } else if (html.scrollTop || document.documentElement.scrollTop) {
    return [
      html.scrollLeft + document.documentElement.scrollLeft,
      html.scrollTop + document.documentElement.scrollTop
    ];
  } else if (document.body.scrollTop)
    return [document.body.scrollLeft, document.body.scrollTop];
  return [0, 0];
}

function getStyle(obj, styleProp) {
  if (obj.currentStyle) {
    var y = obj.currentStyle[styleProp];
  } else if (window.getComputedStyle)
    var y = window.getComputedStyle(obj, null)[styleProp];
  return y;
}

Despite the extensive addition, all the new code is doing is searching for any parentNode with style position:fixed;. If it finds such an element as it travels up the DOM tree, it adds the viewport scroll values for all browsers except for Opera. Done and done!

You can see all four versions of this function in action and examine the shortcomings of each on this test page. Finding the positioning of an element within the document is actually pretty easy until you throw scrolling elements and fixed positioning into the mix. Arguably the number of sites using either of those things is small, but still significant and shouldn't be ignored. When using the function on your site, you'll need to examine the cases in which it will be used and try using the simplest one you can. For the majority of cases, that will be the current Quirksmode.org function.

But if you need more, use the above. ;) Big thanks to Peter-Paul Koch (PPK) for providing the original resources to the development community.

My top 10 in 2010 Consistent, cross-browser legend positioning within fieldsets

Comments closed

Recent posts

  1. Cyprus, and what capitalists want Mar 2013
  2. Let interest rates on housing rise Sep 2012
  3. Low carb mashed cauliflower with avocado Jan 2012
  4. The Zalman Odyssey Sep 2011
  5. New pants, new perspective Sep 2011
  6. Archive

Items of Interest

Webcomics Reading List

Good Eats

Twitter Identi.ca Google+ RSS 2.0 Valid XHTML 1.0! Copyright © 2014 Brian Huisman AKA GreyWyvern
ContactSite mapSearch