I've set off to the task of finding an element's position.
getOffsetCoords( el, container, coords );
container (optional) is any ancestor of el.
coords (optional) an object that has x and y properties - { x: 0, y :
0 }
I'm including scroll widths and borders of parentNodes.
This is useful for widgets like tooltip, dragdrop, context menu.
You shouldn't need it for a context menu.
I'm throwing this up here for people to pick apart.
Areas that need improving:
* find cases where it fails
* find inefficiencies
* find things that can be improved
* formatting, or any other annoyances
Source:
http://dhtmlkitchen.com/ape/src/dom/position-f.js
testcase (I have not tested in IE6 (only 7))
http://dhtmlkitchen.com/ape/test/tests/dom/position-f-test.html
I took a little time to survey this.
// Load-time constants.
var IS_BACK_COMPAT = document.compatMode === "BackCompat";
You can't rely on this flag in IE < 6.
// IE, Safari, and Opera support clientTop. FF 2 doesn't
var IS_CLIENT_TOP_SUPPORTED = 'clientTop'in document.documentElement;
I use a wrapper that uses border styles in lieu of clientLeft/Top. I
think it makes the code easier to follow.
// XXX Opera <= 9.2 - parent border widths are included in offsetTop.
var IS_PARENT_BORDER_INCLUDED_IN_OFFSET;
Can I assume from the comment that this flag will be false in the new
Opera? It doesn't really matter, but it would be good if they changed
their scheme to match other browsers.
// XXX Opera <= 9.2 - body offsetTop is inherited to children's
offsetTop
// when body position is not static.
var IS_BODY_OFFSET_INHERITED;
A positioned body element is a case I didn't consider. I am not
surprised that such a style causes issues.
// XXX Mozilla includes a table border in the TD's offsetLeft.
// There is 1 exception:
// When the TR has position: relative and the TD has block level
content.
// In that case, the TD does not include the TABLE's border in it's
offsetLeft.
// We do not account for this peculiar bug.
var IS_TABLE_BORDER_INCLUDED_IN_TD_OFFSET;
So is this variable a placeholder?
var
IS_STATIC_BODY_OFFSET_PARENT_BUT_ABSOLUTE_CHILD_SUBTRACTS_BODY_BORDER_WIDTH
= false;
Borders/margins on the HTML element (in standards mode) cause
additional aggravations.
var getComputedStyle = window.getComputedStyle;
var bcs;
var positionedExp = /^(?:r|a|f)/,
absoluteExp = /^(?:a|f)/;
There are several issues that are unique to fixed positioning. For
one, fixed elements in Opera 9 have a null offsetParent. In this
case, I resorted to getComputedStyle.
/**
* @param {HTMLElement} el you want coords of.
* @param {HTMLElement} container to look up to.
* @param {x:{Number}, y:{Number}} coords object to pass in.
* @param {boolean} forceRecalc if true, forces recalculation of body
scroll offsets.
This is an interesting idea. Is it to make things less painful for
applications that don't scroll?
* @return {x:{Number}, y:{Number}} coords of el from container.
*
* Passing in a container will improve performance in other browsers,
* but will punish IE with a recursive call. Test accordingly.
Same for Firefox if you use getBoxObjectFor. For some reason, I
didn't do this in the getBoundingClientRect branch, but resorted to
the offsetParent loop. IIRC, IE6/7 had the fewest issues with that
method, though I should change it to work like the gBOF branch for
performance reasons.
* <p>
* Container is sometimes irrelevant. Container is irrelevant when
comparing two objects'
* positions against one another, to see if they intersect. In this
case, pass in document.
It can be relevant if the two elements share a common positioned
parent. And why not default to document?
* </p>
* Passing in re-used coords will greatly improve performance in all
browsers.
Can you elaborate on re-used coords?
* There is a side effect to passing in coords:
* For animation or drag drop operations, reuse coords.
*/
I don't follow that. And I can't conceive of an animation that would/
should be concerned with offset positions.
function getOffsetCoords(el, container, coords) {
var doc = document, body = doc.body, documentElement =
doc.documentElement;
Apparently this function is good for one document. I think it is a
good idea to allow for multiple documents in functions like these
(e.g. for iframes, objects, etc.)
if(!container)
container = doc;
if(!coords)
coords = {x:0, y:0};
if(el === container) {
coords.x = coords.y = 0;
return coords;
}
if("getBoundingClientRect"in el) {
I would avoid the in operator for compatibility reasons.
// In BackCompat mode, body's border goes to the window. BODY is
ICB.
var rootBorderEl = (IS_BACK_COMPAT ? body : documentElement);
But this flag isn't correct in IE < 6 and those versions do not
display the documentElement. IIRC, this is okay in this case as I
think IE5.x considers the viewport border to be part of the (otherwise
invisible) HTML element.
var box = el.getBoundingClientRect();
var x, y;
x = box.left - rootBorderEl.clientLeft
+ Math.max( documentElement.scrollLeft, body.scrollLeft );
y = box.top - rootBorderEl.clientTop
+ Math.max( documentElement.scrollTop, body.scrollTop );
if(container !== doc) {
box = getOffsetCoords(container, null);
x -= box.x;
y -= box.y;
}
if(IS_BACK_COMPAT) {
var curSty = body.currentStyle;
Object inference based on getBoundingClientRect (will break in the new
Opera.)
x += parseInt(curSty.marginLeft);
y += parseInt(curSty.marginTop);
Use parseFloat. Oddly enough, my gBCR branch does not consider
margins at all and I am pretty sure I tested quirks mode w/ body
margins in IE6/7. I'll have to re-test that. Of course, I need to re-
test everything now that I have transplanted the code into a new
library (I'm really looking forward to *that*.)
}
coords.x = x;
coords.y = y;
return coords;
}
// Crawling up the tree.
else {
var offsetLeft = el.offsetLeft,
offsetTop = el.offsetTop,
isBodyStatic = !positionedExp.test(bcs.position);
What if bcs does not exist. You should allow applications that need
to run in ancient browsers to compensate by setting inline styles
(i.e. check inline styles as a fallback.)
[snip]
----
// Loop up, gathering scroll offsets on parentNodes.
// when we get to a parent that's an offsetParent, update
// the current offsetParent marker.
I prefer to loop through offsetParents and then deal with scrolling
parents as an optional afterthought. This makes it easy for
applications that do not involve scrolling containers to prevent the
extra work. Also, the adjustments for scrolling containers are needed
for the gBOF branch.
for( var parent = el.parentNode; parent && parent !== container;
parent = parent.parentNode) {
if(parent !== body && parent !== documentElement) {
lastOffsetParent = parent;
offsetLeft -= parent.scrollLeft;
offsetTop -= parent.scrollTop;
}
if(parent === offsetParent) {
// If we get to BODY and have static position, skip it.
if(parent === body && isBodyStatic);
else {
// XXX Mozilla; Exclude static body; if static, it's offsetTop
will be wrong.
Negative in some cases, IIRC.
// Include parent border widths. This matches behavior of
clientRect approach.
// XXX Opera <= 9.2 includes parent border widths.
// See IS_PARENT_BORDER_INCLUDED_IN_OFFSET below.
if( !IS_PARENT_BORDER_INCLUDED_IN_OFFSET &&
! (parent.tagName === "TABLE" &&
IS_TABLE_BORDER_INCLUDED_IN_TD_OFFSET)) {
You don't need a strict comparison there.
if( IS_CLIENT_TOP_SUPPORTED ) {
offsetLeft += parent.clientLeft;
offsetTop += parent.clientTop;
}
else {
var pcs = getComputedStyle(parent, "");
// Mozilla doesn't support clientTop. Add borderWidth to the
sum.
offsetLeft += parseInt(pcs.borderLeftWidth)||0;
offsetTop += parseInt(pcs.borderTopWidth)||0;
As mentioned, allow for inline styles.
}
}
if(parent !== body) {
offsetLeft += offsetParent.offsetLeft;
offsetTop += offsetParent.offsetTop;
offsetParent = parent.offsetParent; // next marker to check
for offsetParent.
}
}
}
}
[snip]
var bodyOffsetLeft = parseInt(bcs.marginLeft)||0;
var bodyOffsetTop = parseInt(bcs.marginTop)||0;
}
Use parseFloat or em-based layout (for example) will have rounding
errors.
if(isBodyStatic) {
// XXX: Safari will use HTML for containing block (CSS),
// but will subtract the body's border from the body's absolutely
positioned
// child.offsetTop. Safari reports the child's offsetParent is
BODY, but
// doesn't treat it that way (Safari bug).
if(!isLastElementAbsolute) {
if(false == IS_PARENT_BORDER_INCLUDED_IN_OFFSET
Shouldn't this be !IS_PARENT_BORDER_INCLUDED_IN_OFFSET?
&& (container === document || container === documentElement)){
offsetTop += parseInt(bcs.borderTopWidth);
offsetLeft += parseInt(bcs.borderLeftWidth);
Use parseFloat.
}
}
else {
// XXX Safari subtracts border width of body from element's
offsetTop (opera does it, too)
I definitely remember this one.
if(IS_STATIC_BODY_OFFSET_PARENT_BUT_ABSOLUTE_CHILD_SUBTRACTS_BODY_BORDER_WIDTH)
{
offsetTop += parseInt(bcs.borderTopWidth);
offsetLeft += parseInt(bcs.borderLeftWidth);
Same here (parseFloat.)
}
}
}
else if(container === doc || container === documentElement) {
// If the body is positioned, add its left and top value.
// Safari will sometimes return "auto" for computedStyle, which
results NaN.
So will IE for statically positioned elements. Opera will return
incorrect results for those with borders.
bodyOffsetLeft += parseInt(bcs.left)||0;
bodyOffsetTop += parseInt(bcs.top)||0;
Use parseFloat.
// XXX: Opera normally include the parentBorder in offsetTop.
// We have a preventative measure in the loop above.
if(isLastElementAbsolute) {
if(IS_CLIENT_TOP_SUPPORTED &&
IS_PARENT_BORDER_INCLUDED_IN_OFFSET) {
offsetTop += body.clientTop;
offsetLeft += body.clientLeft;
}
}
}
}
coords.x = offsetLeft + bodyOffsetLeft;
coords.y = offsetTop + bodyOffsetTop;
return coords;
}
}
// A closure for initializing load time constants.
if(!("getBoundingClientRect"in document.documentElement))
As mentioned, I would avoid the in operator.
(function(){
var waitForBodyTimer = setInterval(function
domInitLoadTimeConstants() {
What is this about? A DOM ready simulation?
if(!document.body) return;
This excludes XHTML documents in Windows Safari and (reportedly) some
older Gecko-based browsers.
clearInterval(waitForBodyTimer);
var body = document.body;
var s = body.style, padding = s.padding, border = s.border,
position = s.position, marginTop = s.marginTop;
s.padding = 0;
s.top = 0;
s.border = '1px solid transparent';
var x = document.createElement('div');
x.id='asdf';
Why do you need to assign an ID?
var xs = x.style;
xs.margin = 0;
xs.position = "static";
x = body.appendChild(x);
This will cause a twitch during page load. I would make the height
and width 0 if you can get away with it in this test.
IS_PARENT_BORDER_INCLUDED_IN_OFFSET = (x.offsetTop === 1);
s.border = 0;
s.padding = 0;
var table = document.createElement('table');
try {
table.innerHTML = "<tbody><tr><td>bla</td></tr></tbody>";
Why not use DOM methods and lose the try-catch?
table.style.border = "17px solid red";
Why set border colors on these dummy elements?
table.cellSpacing = table.cellPadding = 0;
body.appendChild(table);
IS_TABLE_BORDER_INCLUDED_IN_TD_OFFSET =
table.getElementsByTagName("td")[0].offsetLeft === 17;
body.removeChild(table);
} catch(ex){/*IE, we don't care*/}
if(getComputedStyle) {
bcs = getComputedStyle(document.body,'');
}
// Now add margin to determine if body offsetTop is inherited.
s.marginTop = "1px";
s.position = "relative";
IS_BODY_OFFSET_INHERITED = (x.offsetTop === 1);
s.marginTop = "0";
xs.position = "absolute";
s.position = "static";
if(x.offsetParent === body) {
s.border = "1px solid #f3f3f3";
xs.top = "2px";
// XXX Safari gets offsetParent wrong (says 'body' when body is
static,
// but then positions element from ICB and then subtracts body's
clientWidth.
// Safari is half wrong.
//
// XXX Mozilla says body is offsetParent but does NOT subtract
BODY's offsetWidth.
Subtracts BODY's clientWidth?
// Mozilla is completely wrong.
IS_STATIC_BODY_OFFSET_PARENT_BUT_ABSOLUTE_CHILD_SUBTRACTS_BODY_BORDER_WIDTH
= x.offsetTop === 1;
}
s.position = position;
s.marginTop = marginTop;
// Put back border and padding the way they were.
s.border = border;
s.padding = padding;
This is going to be twitchy.
// Release memory (IE).
body = s = x = xs = table = null;
}, 60);
})();
/**
* @return {boolean} true if a is vertically within b's content area
(and does not overlap, top nor bottom).
*/
function isInsideElement(a, b) {
var aTop = getOffsetCoords(a).y;
var bTop = getOffsetCoords(b).y;
return aTop + a.offsetHeight <= bTop + b.offsetHeight && aTop >=
bTop;
}
/**
* @return {boolean} true if a overlaps the top of b's content area.
*/
function isAboveElement(a, b) {
return (getOffsetCoords(a).y <= getOffsetCoords(b).y);
}
/**
* @return {boolean} true if a overlaps the bottom of b's content
area.
*/
function isBelowElement(a, b) {
return (getOffsetCoords(a).y + a.offsetHeight >=
getOffsetCoords(b).y + b.offsetHeight);
}
[snip]
The rest appears unrelated.
Sorry for the inevitable wrapping. I didn't have time to make this
newsreader-friendly.
BTW, the email I sent to you bounced (and I was replying to one of
your messages.) Perhaps GMail is on the fritz tonight?