Home

Mouse Handling and Absolute Positions in JavaScript

Oct 27, 2006

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.

In the example when you

Oct 27, 2006 Anonymous

In the example when you place the white box in the red square it doesn't show. Probably a z-index issue.

Drawing order

Oct 27, 2006 Steven

It works fine in browsers that respect the proper drawing order. I really can't be bothered fixing up such a silly example.

Geez Steve!

Jun 15, 2007 Anonymous

tell us how you really feel!!? LOL

The getAbsolutePosition()

Jul 12, 2007 James

The getAbsolutePosition() function works fine if your element is an Image ( <img> ) in a css layout. It is certainly suprising that the other elements do not give a correct position. Must be to do with the flow rendering.

colour information

Oct 15, 2007 sarat

Is it possible to obtain the colour information at the point we click the mouse?

Weee

Apr 16, 2008 falldeaf

Haha, I made a purely css and javascript scrubber for the flash chromeless video player using your function... sweet. Thanks man :)

Thank's!

Aug 07, 2008 The Ape!!

Thank's mate!!!
It was exactly what I needed.
An admin tool to mark points on a map.

Thank's a lot!!!

Unfortunately, even this

Aug 17, 2008 Anonymous

Unfortunately, even this does not work well

Your getAbsolutePosition is not dealing with margins, borders, paddings... so - it is supposed to not to work well! Add border, margin or padding to your main DIV and all go wrong!

apply fix to farbtastic

Oct 29, 2008 Bill Burcham

Has this fix been applied to farbtastic? Reason I ask is because in IE7, when I bring up a farbtastic color wheel and click on it, it looks like the handler is calculating the position incorrectly (causing the crosshairs to not land where the pointer is clicked). Also I don't see the getRelativeCoordinates function in farbtastic.js 1.2.

re: apply fix to farbtastic

Oct 29, 2008 Bill Burcham

’pon further inspection I now see that fb.widgetCoords in farbtastic.js is essentially getRelativeCoordinates. Hum. Still have the IE7 problem though.

Try this

Oct 30, 2008 Matt Farina

The fix listed here worked with an older version of jQuery. Try replacing fb.widgetCoords with the following (for jQuery 1.2.6):

fb.widgetCoords = function (event) {
    var offset = $(fb.wheel).offset();
    return { x: (event.pageX - offset.left) - fb.width / 2, y: (event.pageY - offset.top) - fb.width / 2 };
};

cool style!!

Dec 27, 2008 Tim

I found your site googling for javascript mouse position and my eyes popped at your style -- it's fantastic!! and I don't think I've seen anything quite like it anywhere. Well done!!

Tim

Well.. It still sux big ass

Jan 19, 2009 Anonymous

Well.. It still sux big ass big time, that you cant get the position when using margin.. It totally ruins every chance of making a nice dropdownmenu on a scalable site without hardcoding it :( I just hat IE so much !

Thanx :)

Jan 06, 2010 Asif

Thanx, I am new in javascript , needed it desperately u made my day,
If possible suggest me some books or some tuts or to be good as u in javascript
.Thanx

Awesome plugin (Farbtastic)

Jan 14, 2010 Nathan

Thanks -- it was very easy to use. However, had to make two changes for my implementation.....

The first was that I needed to use your Utility methods outside of the function. Let me explain: I initialize farbtastic on a form input onclick. However, when the page loads, the value is already filled in (#123456, for example). When the user clicks on the field, the background color and the foreground color change appropriately. I wanted to implement that same look before farbtastic was initialized, so I moved those utility functions out:

//end of file
jQuery._farbtastic.prototype = {
  pack : function (rgb) {
    var r = Math.round(rgb[0] * 255);
    var g = Math.round(rgb[1] * 255);
    var b = Math.round(rgb[2] * 255);
    return '#' + (r < 16 ? '0' : '') + r.toString(16) +
           (g < 16 ? '0' : '') + g.toString(16) +
           (b < 16 ? '0' : '') + b.toString(16);
  },

  unpack : function (color) {
    if (color.length == 7) {
      return [parseInt('0x' + color.substring(1, 3)) / 255,
        parseInt('0x' + color.substring(3, 5)) / 255,
        parseInt('0x' + color.substring(5, 7)) / 255];
    }
    else if (color.length == 4) {
      return [parseInt('0x' + color.substring(1, 2)) / 15,
        parseInt('0x' + color.substring(2, 3)) / 15,
        parseInt('0x' + color.substring(3, 4)) / 15];
    }
  },

  HSLToRGB : function (hsl) {
    var m1, m2, r, g, b;
    var h = hsl[0], s = hsl[1], l = hsl[2];
    m2 = (l <= 0.5) ? l * (s + 1) : l + s - l*s;
    m1 = l * 2 - m2;
    return [this.hueToRGB(m1, m2, h+0.33333),
        this.hueToRGB(m1, m2, h),
        this.hueToRGB(m1, m2, h-0.33333)];
  },

  hueToRGB : function (m1, m2, h) {
    h = (h < 0) ? h + 1 : ((h > 1) ? h - 1 : h);
    if (h * 6 < 1) return m1 + (m2 - m1) * h * 6;
    if (h * 2 < 1) return m2;
    if (h * 3 < 2) return m1 + (m2 - m1) * (0.66666 - h) * 6;
    return m1;
  },

  RGBToHSL : function (rgb) {
    var min, max, delta, h, s, l;
    var r = rgb[0], g = rgb[1], b = rgb[2];
    min = Math.min(r, Math.min(g, b));
    max = Math.max(r, Math.max(g, b));
    delta = max - min;
    l = (min + max) / 2;
    s = 0;
    if (l > 0 && l < 1) {
      s = delta / (l < 0.5 ? (2 * l) : (2 - 2 * l));
    }
    h = 0;
    if (delta > 0) {
      if (max == r && max != g) h += (g - b) / delta;
      if (max == g && max != b) h += (2 + (b - r) / delta);
      if (max == b && max != r) h += (4 + (r - g) / delta);
      h /= 6;
    }

    return [h, s, l];
  }
}

jQuery.farbtastic.Utils = jQuery._farbtastic.prototype;

I can then call the methods using $.farbtastic.Utils.fName:

var unpack = $.farbtastic.Utils.unpack(fullHex);
var hsl = $.farbtastic.Utils.RGBToHSL(unpack);
input.css({
'color' : (hsl[2] > 0.5 ? '#000' : '#fff'),
'background-color' : str
});

The second change I made was more of a bugfix. When farbtastic is initialized on an empty input, $("#farb-div").farbtastic(input), I was not getting the value to populate in the input (however, the background color was changing successfully). I looked in the code and saw this line:

$(fb.callback).each(function() {
if (this.value && this.value != fb.color) {
this.value = fb.color;
}
});

This basically means that if this.value is "true-y" (not null, not undefined, not blank, yada yada), then continue to the rest of the 'if-statement'. I think a better way of handling it is to use ('value' in this) -- just to check if 'value' is a property of the element 'this' (which it will be for all 'input' elements).

$(fb.callback).each(function() {
if (('value' in this) && this.value != fb.color) {
this.value = fb.color;
}
});

Thanks for making this plugin public. Keep up the good work.
Nathan

Post new comment

Note: all posts containing spam will be removed.
The content of this field is kept private and will not be shown publicly.
  • Web page addresses and e-mail addresses turn into links automatically.
  • Allowed HTML tags: <a> <b> <dd> <dl> <dt> <i> <li> <ol> <u> <ul> <img> <em> <p> <br> <span> <div> <h2> <h3> <abbr> <small> <table> <tr> <td> <strong> <acronym> <th> <blockquote>
  • Lines and paragraphs break automatically.
  • You may post code using <code>...</code> (generic) or <?php ... ?> (highlighted PHP) tags.

More information about formatting options

Recent comments

Images