13

When the mouse is moved over an element, I want to get the mouse coordinates of the cursor relative to the top-left of the element's content area (this is the area excluding padding, border and outline). Sounds simple, right? What I have so far is a very popular function:

function element_position(e) {
    var x = 0, y = 0;
    do {
        x += e.offsetLeft;
        y += e.offsetTop;
    } while (e = e.offsetParent);
    return { x: x, y: y };
}

And I'd get the mouse position relative to an element element with:

p = element_position(element);
x = mouseEvent.pageX - p.x;
y = mouseEvent.pageY - p.y;

That isn't quite correct. Because the offsetLeft and offsetTop are the differences between the 'outer' top left of an element and the 'inner' top left of its offset parent, the sum position will skip over all borders and paddings in the hierarchy.

Here's a comparison that should (hopefully) clarify what I mean.

  • If I get the sum of the distances in position between the 'outer' top left of the elements and the 'inner' top left of their offset parents (outers minus inners; what I am doing right now), I get the element's content area's position, minus all the borders and paddings in the offset hierarchy.
  • If I get the sum of the distances in position between the 'outer' top left of the elements and the 'outer' top left of their offset parents (outers minus outers), I get the element's content area's position, minus the border and padding of the desired element (close, but not quite there).
  • If I get the sum of the distances in position between the 'inner' top left of the elements and the 'inner' top left of their offset parents (inners minus inners), I get the element's content area's position. This is what I want.
Delan Azabani
  • 79,602
  • 28
  • 170
  • 210
  • I personally prefer not to apply CSS on a canvas itself but wrap the canvas inside an element that decorates it with CSS. Takes away lots of PITA. ;) – Caspar Kleijne Apr 22 '11 at 12:49
  • That is true, and I wouldn't apply padding/border on my canvas elements. Just a theoretical question, really. – Delan Azabani Apr 22 '11 at 12:52
  • 1
    It seems that the `offsetLeft/Top`-type method of getting an element's position is not only incorrect by the size of your target element's border and padding, but all of the borders and paddings of the offset parent elements. Please check out my rewritten question and see if you can weigh in on my situation. – Delan Azabani Apr 22 '11 at 13:30

4 Answers4

10

Here's a live example that uses an element_position() function that is aware of padding and borders. I've added some extra padding and margins to your original example.

http://jsfiddle.net/Skz8g/4/

To use it, move the cursor over the brown area. The resulting white area is the actual canvas content. The brown is padding, the red is a border, and so on. In both this example and the one later on, the canvas x and canvas y readouts indicate the cursor position relative to canvas content.

Here's the code for element_position():

function getNumericStyleProperty(style, prop){
    return parseInt(style.getPropertyValue(prop),10) ;
}

function element_position(e) {
    var x = 0, y = 0;
    var inner = true ;
    do {
        x += e.offsetLeft;
        y += e.offsetTop;
        var style = getComputedStyle(e,null) ;
        var borderTop = getNumericStyleProperty(style,"border-top-width") ;
        var borderLeft = getNumericStyleProperty(style,"border-left-width") ;
        y += borderTop ;
        x += borderLeft ;
        if (inner){
          var paddingTop = getNumericStyleProperty(style,"padding-top") ;
          var paddingLeft = getNumericStyleProperty(style,"padding-left") ;
          y += paddingTop ;
          x += paddingLeft ;
        }
        inner = false ;
    } while (e = e.offsetParent);
    return { x: x, y: y };
}

The code should work properly in IE9, FF and Chrome, although I notice it is not quite right in Opera.

My original inclination was to use something like the e.offsetX/Y properties because they were closer to what you want, and do not involve looping over nested elements. However, their behaviour varies wildly across browsers, so a bit of cross-browser finagling is necessary. The live example is here:

http://jsfiddle.net/xUZAa/6/

It should work across all modern browsers - Opera, FF, Chrome, IE9. I personally prefer it, but thought that although your original question was just about "getting mouse position relative to content area of an element", you were really asking about how to make the element_position() function work correctly.

brainjam
  • 18,863
  • 8
  • 57
  • 82
  • 1
    Thanks for your answer, and to everyone else's as well. I'm especially accepting yours because it doesn't use an external library, and chose yours over Aleadam's because you went to the trouble of creating code and a demo (though Aleadam, you are completely correct, so thanks for the answer!). This task seems so simple and yet it must be implemented manually; I really think, ideally, there should at least be native properties in the spec, like `offsetLeft` and `offsetTop`, but for content area positions. – Delan Azabani Apr 25 '11 at 13:45
  • I notice if I place the cursor on the top left corner of the canvas it registers (2,1) not (0,0). I've tried a separate approach and running to the same problem ([here](http://stackoverflow.com/questions/29607924/click-in-canvas-is-three-pixels-off)) -- does anyone have a fix? – P i Apr 13 '15 at 15:16
  • 1
    @Pi, just a wild guess, but it may depend on what the "origin" of the cursor icon is. It's possible that the origin is not where you would expect, i.e. at the tip of the arrow. You might want to try using a cross-hair cursor and see what results you get. – brainjam Apr 13 '15 at 16:26
  • I think you're right. It might be the tip of the black, the tip of the white edge, or something else. I very much like the idea of a crosshair cursor for OSX. Alas I can't find any implementation of such a thing. – P i Apr 13 '15 at 16:55
  • 1
    @Pi, this may help: http://www.w3schools.com/cssref/tryit.asp?filename=trycss_cursor – brainjam Apr 13 '15 at 17:21
  • That's super! I've edited it into the [answer](http://stackoverflow.com/a/29609180/435129) snippet. – P i Apr 13 '15 at 17:51
  • When I tried this it seemed to not take into account the page's scrollbar. I was using firefox 45.6.0. – Mutant Bob Mar 04 '17 at 18:47
4

using jQuery:

function posRelativeToElement(elem, ev){
    var $elem = $(elem),
         ePos = $elem.offset(),
     mousePos = {x: ev.pageX, y: ev.pageY};

    mousePos.x -= ePos.left + parseInt($elem.css('paddingLeft')) + parseInt($elem.css('borderLeftWidth'));
    mousePos.y -= ePos.top + parseInt($elem.css('paddingTop')) + parseInt($elem.css('borderTopWidth'));

    return mousePos;
};

live example: http://jsfiddle.net/vGKM3/

The root of this is simple: compute the element's position relative to the document. I then drop the top & left padding & border (margin is included with basic positioning calculations). The internal jQuery code for doing this is based on getComputedStyle and element.currentStyle. Unfortunately I don't think there is another way...

The core of jQuery's .offset() function, which gets an element's position relative to document:

if ( "getBoundingClientRect" in document.documentElement ) {
    ...
    try {
        box = elem.getBoundingClientRect();
    } catch(e) {}

    var body = doc.body,
        win = getWindow(doc),
        clientTop  = docElem.clientTop  || body.clientTop  || 0,
        clientLeft = docElem.clientLeft || body.clientLeft || 0,
        scrollTop  = (win.pageYOffset || jQuery.support.boxModel && docElem.scrollTop  || body.scrollTop ),
        scrollLeft = (win.pageXOffset || jQuery.support.boxModel && docElem.scrollLeft || body.scrollLeft),
        top  = box.top  + scrollTop  - clientTop,
        left = box.left + scrollLeft - clientLeft;

    return { top: top, left: left };
}else{
    // calculate recursively based on .parentNode and computed styles
}

Theoretically, another way to do this would be, using the above positioning code:

  • make sure your element has position: relative (or absolute) set
  • append a new element with position: absolute; top:0px; left:0px;
  • get the position of the new element relative to the document. It will be the same as the content position of the parent
  • delete the new element
2

In your element_position(e) function, iterate through the hierarchy using parentNode, get the padding, offsets and border using getComputedStyle(e, null).getPropertyValue(each_css), and sum them to the value of your x and y values before return.

There's a post proposing a cross-browser reading of styles here:

http://bytes.com/topic/javascript/answers/796275-get-div-padding

Aleadam
  • 40,203
  • 9
  • 86
  • 108
1

I am not sure if this is the best way, or most resource efficient...

But I would suggest getting X/Y for the canvas tag, width of the border, and padding and using them all together as the offset.

Edit:

Use offsetLeft and offsetTop

Reference: How to Use the Canvas and Draw Elements in HTML5

var x;
var y;
if (e.pageX || e.pageY) { 
  x = e.pageX;
  y = e.pageY;
}
else { 
  x = e.clientX + document.body.scrollLeft + document.documentElement.scrollLeft; 
  y = e.clientY + document.body.scrollTop + document.documentElement.scrollTop; 
} 
x -= gCanvasElement.offsetLeft;
y -= gCanvasElement.offsetTop;
David Houde
  • 4,835
  • 1
  • 20
  • 29
  • Updated answer with a piece of code I found over at http://stackoverflow.com/questions/55677/how-do-i-get-the-coordinates-of-a-mouse-click-on-a-canvas-element by N4ppeL -- Using offsetLeft and offsetTop – David Houde Apr 22 '11 at 12:44
  • Please check out my completely rewritten question and let me know if you can weigh in on the situation. ;) – Delan Azabani Apr 22 '11 at 13:28
  • Sorry brother, I guess I am in over my head here. I would hate to have to get all the styles by hand, i don't think theres a simple cross browser solution unless you're using a framework. http://www.quirksmode.org/dom/getstyles.html – David Houde Apr 22 '11 at 13:34