Hackery, Math & Design

Steven Wittens i

Mouse Handling and Absolute Positions in JavaScript

As I was working on the new recolorable Garland Drupal theme, I noticed that suddenly my Farbtastic color picker wasn't working right anymore in IE. A lot of headscratching later, I found the cause and discovered a useful trick for dealing with mouse coordinates in JavaScript. Essentially, when you click on Farbtastic, the mouse position is compared to the position of the color picker, so we can determine which color you clicked. Sounds simple? Well, no. There is no direct DOM API to get an elements's absolute position on the page. The most common technique to find it is to iterate through an element's offsetParents until you reach the root, and add together all the offsets:
  function getAbsolutePosition(element) {
    var r = { x: element.offsetLeft, y: element.offsetTop };
    if (element.offsetParent) {
      var tmp = getAbsolutePosition(element.offsetParent);
      r.x += tmp.x;
      r.y += tmp.y;
    }
    return r;
  };

Unfortunately, even this does not work well. Various browsers have various quirks, but (no surprise) IE wins the contest hands down. When you try to resolve absolute positions in any sort of advanced CSS-based layout, the return coordinates are often completely wrong. This is exactly what was happening in the new (completely tableless) Garland theme.

After trying various ways to correct the absolute values, I decided I didn't want to waste hours of my life cleaning up after somebody elses mess. And of course, hardcoding in the correction is mostly useless in a dynamic CMS like Drupal.

I did come up with an alternative which works well enough, and is perfectly suited for making self-contained HTML widgets: that's the most common use case after all.

You see, aside from the absolute mouse position (event.pageX/Y) we often also get the mouse position relative to the clicked element (event.offsetX/Y). Now, if we try to resolve these coordinates back to the root of the page, we end up with the same problem. The trick is to realize that we often don't need completely absolute coordinates: all we need is coordinates relative to a common reference frame. So, we need to find the closest, common offsetParent for the clicked element and the reference element, and then compare the coordinates in that frame.

The snippet below achieves this. As most of the bad offsetParent numbers are located very high up in the page hierarchy, they are practically never used with this approach. Typically you only go up one or two offsetParents and there is no error.

Some browsers don't provide the offsetX/Y information (e.g. Firefox) or tend to screw it up (e.g. Opera), but luckily they are the ones that provide (mostly) accurate pageX/Y coordinates, even in exotic layouts. So using that as a fallback, we end up with the following function, which works in every browser I've tried:


  /**
   * Retrieve the coordinates of the given event relative to the center
   * of the widget.
   *
   * @param event
   *   A mouse-related DOM event.
   * @param reference
   *   A DOM element whose position we want to transform the mouse coordinates to.
   * @return
   *    A hash containing keys 'x' and 'y'.
   */
  function = getRelativeCoordinates(event, reference) {
    var x, y;
    event = event || window.event;
    var el = event.target || event.srcElement;

    if (!window.opera && typeof event.offsetX != 'undefined') {
      // Use offset coordinates and find common offsetParent
      var pos = { x: event.offsetX, y: event.offsetY };

      // Send the coordinates upwards through the offsetParent chain.
      var e = el;
      while (e) {
        e.mouseX = pos.x;
        e.mouseY = pos.y;
        pos.x += e.offsetLeft;
        pos.y += e.offsetTop;
        e = e.offsetParent;
      }

      // Look for the coordinates starting from the reference element.
      var e = reference;
      var offset = { x: 0, y: 0 }
      while (e) {
        if (typeof e.mouseX != 'undefined') {
          x = e.mouseX - offset.x;
          y = e.mouseY - offset.y;
          break;
        }
        offset.x += e.offsetLeft;
        offset.y += e.offsetTop;
        e = e.offsetParent;
      }

      // Reset stored coordinates
      e = el;
      while (e) {
        e.mouseX = undefined;
        e.mouseY = undefined;
        e = e.offsetParent;
      }
    }
    else {
      // Use absolute coordinates
      var pos = getAbsolutePosition(reference);
      x = event.pageX  - pos.x;
      y = event.pageY - pos.y;
    }
    // Subtract distance to middle
    return { x: x, y: y };
  }

Obviously if the elements you apply it to have an exotic positioning, it'll still go sour, but the above code at least improves the situation massively in Internet Explorer. Here's a little example.

Drupal  JavaScript  jQuery
This article contains graphics made with WebGL, which your browser does not seem to support.
Try Google Chrome or Mozilla Firefox. ×