2010-06-13

Element.pageX and Element.pageY provisions

Do you know how to compute a DOM node's page coordinates, counted in pixels from the document body's top left corner? It sounds like it would be easy, but it isn't. I think it should be. Here is a utility function getViewOffset I cleaned up and lifted out of Firebug (thus MIT licensed, so you may do pretty much whatever you like with it) in early 2008 that computes it, taking into consideration static and non-static positioning, scroll positions of every parent node, special cases for table and body nodes and (optionally not) the current document's position in its parent windows, if it lives deep down in some iframe of the top document:
function getViewOffset(node, singleFrame) {
function addOffset(node, coords, view) {
var p = node.offsetParent;
coords.x += node.offsetLeft - (p ? p.scrollLeft : 0);
coords.y += node.offsetTop - (p ? p.scrollTop : 0);

if (p) {
if (p.nodeType == 1) {
var parentStyle = view.getComputedStyle(p, '');
if (parentStyle.position != 'static') {
coords.x += parseInt(parentStyle.borderLeftWidth);
coords.y += parseInt(parentStyle.borderTopWidth);

if (p.localName == 'TABLE') {
coords.x += parseInt(parentStyle.paddingLeft);
coords.y += parseInt(parentStyle.paddingTop);
}
else if (p.localName == 'BODY') {
var style = view.getComputedStyle(node, '');
coords.x += parseInt(style.marginLeft);
coords.y += parseInt(style.marginTop);
}
}
else if (p.localName == 'BODY') {
coords.x += parseInt(parentStyle.borderLeftWidth);
coords.y += parseInt(parentStyle.borderTopWidth);
}

var parent = node.parentNode;
while (p != parent) {
coords.x -= parent.scrollLeft;
coords.y -= parent.scrollTop;
parent = parent.parentNode;
}
addOffset(p, coords, view);
}
}
else {
if (node.localName == 'BODY') {
var style = view.getComputedStyle(node, '');
coords.x += parseInt(style.borderLeftWidth);
coords.y += parseInt(style.borderTopWidth);

var htmlStyle = view.getComputedStyle(node.parentNode, '');
coords.x -= parseInt(htmlStyle.paddingLeft);
coords.y -= parseInt(htmlStyle.paddingTop);
}

if (node.scrollLeft)
coords.x += node.scrollLeft;
if (node.scrollTop)
coords.y += node.scrollTop;

var win = node.ownerDocument.defaultView;
if (win && (!singleFrame && win.frameElement))
addOffset(win.frameElement, coords, win);
}
}

var coords = { x: 0, y: 0 };
if (node)
addOffset(node, coords, node.ownerDocument.defaultView);

return coords;
}
(The optional second argument turns off recursing in parent frames, when set, so you get document-relative coordinates .)

As I recently had a use for half of it -- computing Y positions -- I hacked out separate smaller getYOffset and getXOffset versions, and when I had those, it occurred to me that these ought to be properties in the Element DOM interface and implemented behind the curtains, so we could simply write img.documentX, img.documentY, et cetera, or img.pageX and img.pageY, if we wanted the coordinates of the image, counting from the outer(most) surrounding parent window. Hacking up a mini-library for that was a breeze from these primitives:
function documentX() { return getXOffset(this, 1); }
function documentY() { return getYOffset(this, 1); }
function pageX() { return getXOffset(this); }
function pageY() { return getYOffset(this); }
Node.prototype.__defineGetter__('documentX', documentX);
Node.prototype.__defineGetter__('documentY', documentY);
Node.prototype.__defineGetter__('pageX', pageX);
Node.prototype.__defineGetter__('pageY', pageY);
So now you could write code looking like this to inspect coordinates of things hovered by the mouse:
Hovered element: <input type="text" id="node-coords" /> &lt;=
<input type="text" id="mouse-coords" />
<script src="http://ecmanaut.googlecode.com/svn/trunk/lib/getXOffset.js"></script>
<script src="http://ecmanaut.googlecode.com/svn/trunk/lib/getYOffset.js"></script>
<script>
var mouse = document.getElementById('mouse-coords');
var output = document.getElementById('node-coords');
var hovered = document.body, saved = hovered.style.outline || '';
hovered.addEventListener('mousemove', hovering, false);

function hovering(e) {
mouse.value = 'mouse @ '+ e.pageX + ', '+ e.pageY;
var node = e.target;
if (node === hovered) return;

var what = node.tagName +' @ ';
var where = node.pageX +', '+ node.pageY
output.value = what + where;

hovered.style.outline = saved;
saved = (hovered = node).style.outline;
hovered.style.outline = '1px dashed lightBlue';
}
</script>
Hovered element: <=

Until this kind of ease gets into DOM 3 or 4 (we can hope, at least), your code is better off using getViewOffset instead, though, when you wanted both properties anyway:
// ...
var coords = getViewOffset(node);
var where = coords.x + ', '+ coords.y;
// ...
blog comments powered by Disqus