[ Team LiB ] Previous Section Next Section

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

    [ Team LiB ] Previous Section Next Section