找到鼠标相对于元素的位置

389

我想使用画布(canvas)制作一个小的绘画应用程序。因此,我需要在画布上找到鼠标的位置。


3
完整解决方案:http://acko.net/blog/mouse-handling-and-absolute-positions-in-javascript/您好,我会尽力为您提供准确的翻译。以上链接是有关JavaScript中鼠标操作和绝对位置的博客文章。 - edA-qa mort-ora-y
30个回答

645

因为我找不到一个没有 jQuery 的答案可以复制/粘贴,所以这是我使用的解决方案:

document.getElementById('clickme').onclick = function(e) {
      // e = Mouse click event.
      var rect = e.target.getBoundingClientRect();
      var x = e.clientX - rect.left; //x position within the element.
      var y = e.clientY - rect.top;  //y position within the element.
      console.log("Left? : " + x + " ; Top? : " + y + ".");
    }
#clickme {
  margin-top: 20px;
  margin-left: 100px;
  border: 1px solid black;
  cursor: pointer;
}
<div id="clickme">Click Me -<br>
(this box has margin-left: 100px; margin-top: 20px;)</div>

完整示例的JSFiddle链接


6
使用该方法需要将边界矩形的值舍入为整数。 - Jake Cobb
4
新的修改应该可以解决滚动问题(使用clientX/Y)。 - Erik Koopmans
6
这个答案应该被投票支持。感谢anytimecoder和@ErikKoopmans。 - Ivan
3
提示:@mpen的评论已经过时;即使在有滚动条的情况下,这种方法仍然有效。请查看他们Fiddle的更新版本:https://jsfiddle.net/6wcsovp2/ - Xharlie
20
谢谢!需要注意的是,你需要使用 e.currentTarget 而不是 e.target。否则,子元素会影响它。 - Maxim Georgievskiy
显示剩余8条评论

205

对于使用JQuery的人:

有时候,当你有嵌套元素,并且其中一个元素具有事件附加到它时,了解你的浏览器将哪个元素视为父级可能会很困惑。在这种情况下,你可以指定父级元素。

你可以获取鼠标位置,然后从父元素的偏移位置中减去它。

var x = evt.pageX - $('#element').offset().left;
var y = evt.pageY - $('#element').offset().top;

如果您尝试获取滚动窗格内页面上的鼠标位置:

var x = (evt.pageX - $('#element').offset().left) + self.frame.scrollLeft();
var y = (evt.pageY - $('#element').offset().top) + self.frame.scrollTop();

或相对于页面的位置:

var x = (evt.pageX - $('#element').offset().left) + $(window).scrollLeft();
var y = (evt.pageY - $('#element').offset().top) + $(window).scrollTop();

请注意以下性能优化:

var offset = $('#element').offset();
// Then refer to 
var x = evt.pageX - offset.left;

这种方式,JQuery无需为每行代码查找#element

更新

有一种新的、仅基于JavaScript的版本在@anytimecoder的答案中,另请参见getBoundingClientRect()的浏览器支持情况


7
@ben,它对你有用吗?我会感激你给出“正确答案”或评论说明仍然存在的问题。也许我可以提供更进一步的帮助。 - sparkyspider
9
除非你想让代码变得非常缓慢,否则应避免在事件处理程序中使用 $('selector')。预先选择元素,并在处理程序中使用预先选定的引用。同样适用于 $(window),只需要执行一次并将其缓存即可。 - Austin France
2
如果您复制粘贴上面的代码,请记得将 var y = (evt.pageY - $('#element').offset().left) + $(window).scrollTop(); 更正为 var y = (evt.pageY - $('#element').offset().top) + $(window).scrollTop(); - Raj Nathani
126
问题与jQuery无关,那怎么可能是“答案”呢? - FlorianB
4
有些人会在JavaScript中提问,而不是用jQuery,因为在他们的环境中可能不支持jQuery。 - dbrree
显示剩余2条评论

86
以下代码计算鼠标位置相对于画布元素的位置:
const example = document.getElementById('example');

example.onmousemove = function(e) { 
    const x = e.pageX - e.currentTarget.offsetLeft; 
    const y = e.pageY - e.currentTarget.offsetTop; 
}

87
也许是我自己的问题,但是使用关键词"this"的两行代码缺乏上下文。 - Deratrius
10
缺乏上下文。'e'是事件,而'this'是与事件相关联的元素。var example = document.getElementById('example'); example.onmousemove = function(e) { var X = e.pageX - this.offsetLeft; var Y = e.pageY - this.offsetTop; } - Nick Bisby
27
如果某个后代元素使用相对或绝对定位,则此方法无效。 - edA-qa mort-ora-y
12
你可以将“this”替换为“e.currentTarget”,这样可以获取与添加事件侦听器的元素相关的位置。 - Linus Unnebäck
4
这似乎是相对于元素的可见部分给出位置,而不是整个元素。如果元素的某些部分无法在视口中看到并且存在滚动条等情况,有没有一种方法可以适应这种情况? - frabjous
显示剩余3条评论

47

在纯JavaScript中,当参考元素嵌套在其他具有绝对定位的元素中时,没有返回相对坐标的答案。以下是解决此情况的方法:

function getRelativeCoordinates (event, referenceElement) {

  const position = {
    x: event.pageX,
    y: event.pageY
  };

  const offset = {
    left: referenceElement.offsetLeft,
    top: referenceElement.offsetTop
  };

  let reference = referenceElement.offsetParent;

  while(reference){
    offset.left += reference.offsetLeft;
    offset.top += reference.offsetTop;
    reference = reference.offsetParent;
  }

  return { 
    x: position.x - offset.left,
    y: position.y - offset.top,
  }; 

}

3
我认为它没有考虑到html元素的偏移量。 - apieceofbart
4
在具有滚动容器的情况下,只需进行一点更改即可实现良好的效果:将“while(reference !== undefined)”替换为“while(reference)”。至少在Chrome中,offsetParent不会变成“undefined”,而是变成“null”。仅检查“reference”是否真实可以确保它适用于“undefined”和“null”两种情况。 - blex
通常来说,检查未定义的值通常是一个不好的想法。 - Ruan Mendes
我认为你需要减去 scrollLeftscrollTop 来补偿已滚动的后代元素。 - Robert
还没有考虑填充,偏移值不包括填充。例如:offset.left += reference.offsetLeft - reference.scrollLeft + parseInt(getComputedStyle(reference).paddingLeft);,同样适用于顶部。 - Andrew

40

我尝试了所有这些解决方案,但由于我的特殊设置与矩阵变换容器(panzoom库)不兼容,所以没有一个可行。即使是缩放和平移,这个解决方案也可以返回正确的值:

mouseevent(e) {
 const x = e.offsetX,
       y = e.offsetY
}

但仅在没有子元素阻碍的情况下才能这样做。可以通过使用CSS将它们对事件“隐藏”来规避此问题:

.child {
   pointer-events: none;
}

4
这绝对是2019年应该采取的方式。其他答案都充满了陈旧的过时观念。 - Colin D
1
@ColinD 谢谢!我忘记说了,这在IE11中也适用。 - Fabian von Ellerts
1
@MarkE,这没有什么可疑的。这是StackOverflow的许多缺陷之一。你在顶部看到的那些答案是很多年前发布的,因此它们获得了巨大的赞同动力。一旦一个答案获得了这种动力,要超越它就非常困难,也非常罕见。 - Jack G
2
@MarkE 答案很简单:它是实验性的。有些人可能在意,有些人可能不在意,但这是个事实。 - cubuspl42
另一个可能的原因是它是_target_节点的偏移量,这不一定是事件监听器所附加到的节点。这可能是期望的行为,但也可能不是。 - cubuspl42
显示剩余3条评论

22
可以在这里找到关于该问题困难的良好描述:http://www.quirksmode.org/js/events_properties.html#position 使用上述描述的技术,您可以在文档中找到鼠标的位置。然后,您只需检查它是否在元素的边界框内,可以调用element.getBoundingClientRect()来找到元素的边界框,它将返回一个具有以下属性的对象:{ bottom, height, left, right, top, width }。从那里开始,很容易确定事件是否发生在您的元素内部。

1
遗憾的是,如果发生任何旋转,这将失败。 - Eric

11

我遇到了这个问题,但为了使它适用于我的情况(在DOM元素上使用dragover(在我的情况下不是canvas)),我发现你只需要在dragover-mouse事件中使用offsetXoffsetY

onDragOver(event){
 var x = event.offsetX;
 var y = event.offsetY;
}

14
event.offsetX是相对于鼠标指针当前所在的元素而非拥有dragover监听器的元素。如果你没有嵌套元素结构,那么这没问题,但如果有的话,这就行不通了。 - opsb

11

如果您想获取与一个元素相关的layerX和layerY,也许可以尝试:

let bbox_rect = document.getElementById("dom-ID").getBoundingClientRect()
let layerX = e.clientX-bbox_rect.left
let layerY = e.clientY-bbox_rect.top

9

我将Mark van Wyk的答案评为有帮助,因为它让我朝正确的方向前进,但是对于我来说并没有完全解决问题。我仍然需要在包含另一个元素的元素中绘制元素时进行偏移。

以下内容对我有帮助:

        x = e.pageX - this.offsetLeft - $(elem).offset().left;
        y = e.pageY - this.offsetTop - $(elem).offset().top;

换句话说,我只是把所有嵌套元素的偏移量堆叠在一起。

+1 给你!这就是我一直在寻找的答案。我有两个叠放在一起的画布,由于其他 div 的存在,坐标都是错误的。我知道这是问题所在,但不知道如何逻辑/方程来补偿它。你刚刚也解决了我的问题!=D 谢谢! - Partack

7

如果您正在为移动设备和/或带有触摸屏的笔记本电脑/监视器开发常规网站或PWA(渐进式Web应用程序),那么您来到这里可能是因为您习惯于鼠标事件,但对触摸事件的体验可能会有些痛苦…太好了!

只有三条规则:

  1. mousemovetouchmove事件中尽量少做。
  2. mousedowntouchstart事件中尽可能多地完成操作。
  3. 取消传播并防止默认的触摸事件,以防止鼠标事件同时在混合设备上触发。

不用说,由于touch事件可能存在多个且比鼠标事件更加灵活(复杂),因此事情变得更加复杂。我只会在这里介绍单点触控。是的,我有点懒,但它是最常见的触摸类型。

var posTop;
var posLeft;
function handleMouseDown(evt) {
  var e = evt || window.event; // Because Firefox, etc.
  posTop = e.target.offsetTop;
  posLeft = e.target.offsetLeft;
  e.target.style.background = "red";
  // The statement above would be better handled by CSS
  // but it's just an example of a generic visible indicator.
}
function handleMouseMove(evt) {
  var e = evt || window.event;
  var x = e.offsetX; // Wonderfully
  var y = e.offsetY; // Simple!
  e.target.innerHTML = "Mouse: " + x + ", " + y;
  if (posTop)
    e.target.innerHTML += "<br>" + (x + posLeft) + ", " + (y + posTop);
}
function handleMouseOut(evt) {
  var e = evt || window.event;
  e.target.innerHTML = "";
}
function handleMouseUp(evt) {
  var e = evt || window.event;
  e.target.style.background = "yellow";
}
function handleTouchStart(evt) {
  var e = evt || window.event;
  var rect = e.target.getBoundingClientRect();
  posTop = rect.top;
  posLeft = rect.left;
  e.target.style.background = "green";
  e.preventDefault(); // Unnecessary if using Vue.js
  e.stopPropagation(); // Same deal here
}
function handleTouchMove(evt) {
  var e = evt || window.event;
  var pageX = e.touches[0].clientX; // Touches are page-relative
  var pageY = e.touches[0].clientY; // not target-relative
  var x = pageX - posLeft;
  var y = pageY - posTop;
  e.target.innerHTML = "Touch: " + x + ", " + y;
  e.target.innerHTML += "<br>" + pageX + ", " + pageY;
  e.preventDefault();
  e.stopPropagation();
}
function handleTouchEnd(evt) {
  var e = evt || window.event;
  e.target.style.background = "yellow";
  // Yes, I'm being lazy and doing the same as mouseout here
  // but obviously you could do something different if needed.
  e.preventDefault();
  e.stopPropagation();
}
div {
  background: yellow;
  height: 100px;
  left: 50px;
  position: absolute;
  top: 80px;
  user-select: none; /* Disable text selection */
  -ms-user-select: none;
  width: 100px;
}
<div 
  onmousedown="handleMouseDown()" 
  onmousemove="handleMouseMove()"
  onmouseout="handleMouseOut()"
  onmouseup="handleMouseUp()" 
  ontouchstart="handleTouchStart()" 
  ontouchmove="handleTouchMove()" 
  ontouchend="handleTouchEnd()">
</div>
Move over box for coordinates relative to top left of box.<br>
Hold mouse down or touch to change color.<br>
Drag to turn on coordinates relative to top left of page.

喜欢使用Vue.js吗?我也是!那么你的HTML代码应该是这样的:

<div @mousedown="handleMouseDown"
     @mousemove="handleMouseMove"
     @mouseup="handleMouseUp"
     @touchstart.stop.prevent="handleTouchStart"
     @touchmove.stop.prevent="handleTouchMove"
     @touchend.stop.prevent="handleTouchEnd">

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接