9.3 Determining the Coordinates of a Click Event
NN 6, IE 5
9.3.1 Problem
You want to read the
x,y coordinates of a click (or other) event with respect to the
coordinate plane of the entire page or just the element being
clicked.
9.3.2 Solution
This recipe presents solutions for two situations because each has
its own idiosyncrasies when trying to merge event coordinates with
page coordinates typically used for positioning elements. The same
scenario is assumed: a user clicks somewhere on the page to point to
a location where a positioned element is to be placed. Imagine the
user clicking on a map to position an arrow graphic. Differences
accrue as to whether the positioning is relative to the page or to
the rectangle occupied by a positioned element. Use one of two
functions described in the Discussion, getPageEventCoords(
) or getPositionedEventCoords(
), to obtain coordinates that coincide
with the event's coordinates. Both functions return
an object with left and top
properties whose values represent position coordinates.
The basis for this example's user interface is one
of two versions of the moveToClick(
) function, which relies on the
shiftTo( ) function of the DHTML API (Recipe
13.3). When the user clicks anywhere within the scope of the event
binding with the Shift key down, the top-left corner of a positioned
element is brought to the click spot.
The first case we'll cover obtains coordinates
relative to the space occupied by the entire page, so you can
position the top-left corner of a first-level (i.e., nonnested)
positioned element at the spot of a user click. The event binding can
be assigned to the document object:
document.onmousedown = moveToClick;
For this version, the moveToClick( ) function
calls upon getPageEventCoords( ). Returned values
are applied as arguments to the shiftTo( )
function:
function moveToClick(evt) {
evt = (evt) ? evt : event;
if (evt.shiftKey) {
var coords = getPageEventCoords(evt);
shiftTo("mapArrow", coords.left, coords.top);
}
}
For the second click-positioning case, the task is to locate a
nested-positioned element inside its parent-positioned element. In
other words, the goal is to get the coordinates of the click within
the outer-positioned element because the outer element defines its
own rectangle as the coordinate plane for its children.
It's best in this situation to bind the event
handler to the outer-positioned element, although
it's not a requirement. It just makes it easier to
confine processing to clicks on that element rather than the entire
document. In an initialization routine triggered by
onload, bind the event accordingly:
document.getElementById("myMap").onmousedown = moveToClick;
moveToClick( ) calls upon
getPositionedEventCoords( ) to read the nested
coordinates:
function moveToClick(evt) {
evt = (evt) ? evt : event;
if (evt.shiftKey) {
var coords = getPositionedEventCoords(evt);
shiftTo("mapArrow", coords.left, coords.top);
}
}
9.3.3 Discussion
To determine the mouse event location in the coordinate plane of the
entire document, the getPageEventCoords( )
function shown in the following example has two main branches. The
first gets the simpler pageX and
pageY properties of the Netscape event object. For
IE, many more calculations need to be carried out to derive the
coordinates to accurately position an element at the specified
location. The clientX and
clientY properties need additional factors for any
scrolling of the body content and some small padding that IE
automatically adds to the body (normally two pixels along both axes).
In the case of IE 6 running in CSS-compatibility mode, the
html element's small padding must
also be factored out of the equation.
function getPageEventCoords(evt) {
var coords = {left:0, top:0};
if (evt.pageX) {
coords.left = evt.pageX;
coords.top = evt.pageY;
} else if (evt.clientX) {
coords.left =
evt.clientX + document.body.scrollLeft - document.body.clientLeft;
coords.top =
evt.clientY + document.body.scrollTop - document.body.clientTop;
// include html element space, if applicable
if (document.body.parentElement && document.body.parentElement.clientLeft) {
var bodParent = document.body.parentElement;
coords.left += bodParent.scrollLeft - bodParent.clientLeft;
coords.top += bodParent.scrollTop - bodParent.clientTop;
}
}
return coords;
}
Deriving the event coordinates inside a positioned element is the job
of the getPositionedEventCoords( ) function, shown
in the following code listing. The IE branch, which supports the
offsetX and offsetY properties
of the event object, is the easy one here. Those values are relative
to the coordinate plane of the positioned element target. On the
Netscape side, the layerX and
layerY properties need only an adjustment for the
target element's borders. To prevent the event from
propagating any further (and possibly conflicting with other
onmousedown event targets), the
event's cancelBubble property is
set to true:
function getPositionedEventCoords(evt) {
var elem = (evt.target) ? evt.target : evt.srcElement;
var coords = {left:0, top:0};
if (evt.layerX) {
var borders = {left:parseInt(getElementStyle("progressBar",
"borderLeftWidth", "border-left-width")),
top:parseInt(getElementStyle("progressBar",
"borderTopWidth", "border-top-width"))};
coords.left = evt.layerX - borders.left;
coords.top = evt.layerY - borders.top;
} else if (evt.offsetX) {
coords.left = evt.offsetX;
coords.top = evt.offsetY;
}
evt.cancelBubble = true;
return coords;
}
A compatibility complication must be accounted for, however. If the
outer element has a CSS border assigned to it, Netscape and IE (in
any mode) disagree whether the coordinate plane begins where the
border starts or where the content rectangle starts. Netscape
includes the border; IE does not. Therefore, along the way, the
situation is equalized by factoring out the border in the Netscape
calculations. This is done with the help of the
getElementStyle(
) function from Recipe
11.12:
function getElementStyle(elemID, IEStyleAttr, CSSStyleAttr) {
var elem = document.getElementById(elemID);
if (elem.currentStyle) {
return elem.currentStyle[IEStyleAttr];
} else if (window.getComputedStyle) {
var compStyle = window.getComputedStyle(elem, "");
return compStyle.getPropertyValue(CSSStyleAttr);
}
return "";
}
It may seem odd that deriving these kinds of event coordinates should
be so laborious in one circumstance or the other. There is little
justification for this, except perhaps that those who designed the
event object and content-coordinate systems didn't
envision how DHTML designers might utilize these features. The W3C
DOM Level 2 event model is only partially helpful by defining two
pairs of coordinate-related properties of the
event object:
clientX/clientY and
screenX/screenY. But even
then, the formal descriptions of the clientX and
clientY properties—a coordinate at which the
event occurred relative to the DOM implementation's
client area—leave a lot to interpretation. Is the
"client area" the page or just the
visible portion of the page? Netscape interprets it as being the
entire page, but IE's clientX and
clientY properties (admittedly not based on the
W3C DOM event model) are measures within the visible space of the
document, thus requiring adjustments for document scrolling.
The W3C DOM Level 2 is mum on event coordinates within a positioned
element. Of course, with some more arithmetic and element inspection,
you can figure out those values from the style properties of the
element and the event's clientX
and clientY properties. The proprietary properties
for offsetX/offsetY in IE and
layerX/layerY in Netscape (a
convenience holdover from Navigator 4) partially pick up the slack,
but as you've seen, they're not
universally perfect.
Even with the adjustments shown in the examples for this recipe, you
may still encounter combinations of CSS borders, margins, and padding
that throw off these careful calculations. If these CSS-style touches
are part of the body element or the element
you're positioning, you will probably have to
experiment with adjustment values that work for the particular design
situation of the page. In particular, inspect the
offsetLeft, offsetTop,
clientLeft, and clientTop
properties of not only the direct elements you're
working with, but also those within the containers that impact
elements' offset measures (usually reachable through
the offsetParent property, and further
offsetParent chains outward to the
html element). Also, don't
overlook CSS border, margin, and padding thicknesses that might
impact coordinate measures of the elements. Look for values that
represent the number of pixels that your calculations miss.
It's a tedious process, so be prepared to spend some
time figuring it out. One size does not fit all.
9.3.4 See Also
Recipe 9.4 for canceling event bubbling; Recipe 11.12 for a utility
function that reveals values from imported style sheets; Recipe 13.8
for determining the pixel position of an element within the normal
flow of a document
|