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;
// ...

2010-06-12

Google BOM feature: ms since pageload

I expect this feature has been around for quite a while already, but this is the first time I have seen it: stealthy browser object model improvements letting a web page figure out how many milliseconds ago it was loaded. It presumably works in any web browser that is Chrome or that runs the Google Toolbar:

function msSincePageLoad() {
try {
var t = null;
if (window.chrome && chrome.csi)
t = chrome.csi().pageT;
if (t === null && window.gtbExternal)
t = window.gtbExternal.pageT();
if (t === null && window.external)
t = window.external.pageT;
} catch (e) {};
return t;
}


In Chrome it (chrome.csi().pageT, that is) even reports the time with decimals, for sub-millisecond precision.

Google, this kind of browser improvements should be blogged! Maybe even documented. All I caught in a brief googling for it were two now-garbage-collected tweets by Paul Irish, leading to where it was committed to Chromium, and a screenshot of the feature in action, along with all the other related features not brought up now:



The rest of this post, about how I happened upon it myself, is probably only interesting to the insatiably curious:

Upon having grown weary of all the Chinese automated porn/malware comment spam that passes through Blogger's sub-par spam filtering to my moderation inbox, I decided to replace it with one that is maintained by a service specializing in (and presumably committed to!) blog comments: Disqus. In the process, being lazy, I decided to let their template wizard install itself in my blog template, which required dropping my old blogger template, upgrading it a few versions, and then (only required by my own discerning taste) attempting to manually weed out the worst crud from the new template (none of which was added by Disqus, I might add).

In the apparently uneditable <b:include data='blog' name='all-head-content'/> section, sat a minified version of approximately this code, which seems to look up the vertical position of some latency-testing DOM node passed to it, the first time the visitor scrolls the page, if it's above the fold (which in Blogger's world is apparently a constant 750 pixels into the page :-). And maybe other things.

(function() {
function Ticker(x) {
this.t = {};
this.tick = function tick(name, data, time) {
time = time ? time : (new Date).getTime();
this.t[name] = [time, data];
};
this.tick("start", null, x);
}

window.jstiming = {
Timer: Ticker,
load: new Ticker
};

try {
var pt = null;
if (window.chrome && window.chrome.csi)
pt = Math.floor(window.chrome.csi().pageT);
if (pt == null && window.gtbExternal)
pt = window.gtbExternal.pageT();
if (pt == null && window.external)
pt = window.external.pageT;
if (pt) window.jstiming.pt = pt;
} catch (e) {};

window.tickAboveFold = function tickAboveFold(node) {
var y = 0;
if (node.offsetParent) {
do y += node.offsetTop;
while ((node = node.offsetParent))
}
if (y <= 750) window.jstiming.load.tick("aft");
};

var alreadyLoggedFirstScroll = false;

function onScroll() {
if (!alreadyLoggedFirstScroll) {
alreadyLoggedFirstScroll = true;
window.jstiming.load.tick("firstScrollTime");
}
}

if (window.addEventListener)
window.addEventListener("scroll", onScroll, false);
else
window.attachEvent("onscroll", onScroll);
})();

Safari Reader Underwhelm

I was somewhat underwhelmed by Safari Reader, mainly on account of the enforced extra friction their designers added to its UI, presumably to make it "look nice":

  • On top (and bottom, but I personally don't mind that part), it adds a drop shadow that makes text harder to read there, as is my personal habit.

  • Even worse, on navigating with the keyboard (arrow keys, Page Down or Up, and worst of all, Home / End), it painstakingly slowly SCROLLS you there -- instead of just snapping into place as Google Chrome, for instance, would. On at least a really page such as this great current SSD disks review, it takes over a second getting from top to bottom or back, which is just massively annoying.

    This actually applies to all of Safari to a lesser degree, I just never noticed it before, as I don't usually use Safari when reading long pages. In normal browsing, it seems to do it in about nine frames (and with ugly visual blits), over the course of somewhat too long a fraction of a second (this even on my massively over-powered state-of-the-art mac pro extra-everything).

    Update: This Safari "feature" can be disabled in System Preferences / Appearance on the mac; uncheck the box "Use smooth scrolling". (Thanks, Fredrik; I was unaware of this.)

  • A slight missed opportunity: every sub-page in the page gets its own top-right discreet "Page n of m" header. That much is great. It just isn't also a permalink to that sub-page, so if you want to toss someone a link to the relevant part (to your own discussion) of the known-to-be-huge article, well, you're out of luck and have to dig it up in non-Reader mode. Unwebby!

As a statement about what we should demand of our digital readership experience, I very much appreciate the idea (yes, in the greater business reality, it's a hypocritical move to strip ads from the web with one hand, while enforcing ads on devices you stepmother with the other -- but I care more about the web). They have just been encumbered by a bit too much Apple designerism. I hope that copycats will borrow the good parts and throw away the bad. Please don't copy Apple's bugs.