原文链接:https://javascript.info/coordinates,translate with ❤️ by zhangbao.

如果要移动元素,我们必须要熟悉坐标系统。

多数 JavaScript 方法都是在处理两类坐标系统:

  • 相对于窗口(或称“视口”)的上边沿/左边沿。

  • 相对于文档的上边沿/左边沿。

理解两者之间的区别非常重要。

窗口坐标:getBoundingClientRect

窗口坐标的参照点是窗口左上角。

elem.getBoundingClientRect 返回元素 elem 相对于窗口的坐标信息,是一个对象,包含下列属性:

  • top:元素上边沿的 Y 轴坐标,

  • bottom:元素下边沿的 Y 轴坐标,

  • left:元素左边沿的 X 轴坐标,

  • right:元素右边沿的 X 轴坐标。

图示如下:

坐标系统 - 图1

再一次说明,窗口坐标是相对于窗口左上角计算的。

也就是说,当我们滚动页面的时候,元素上升或下降,它的对应窗口坐标是一直改变的,理解这点很重要。

还需说明的是:

  • 坐标值可能包含小数,但这丝毫不会影响将它们用在 style.positoin.left/top 上。

  • 坐标值可能是负值。例如,当我们向下滚动页面,elem 就会往上去,当元素顶部超出窗口范围外时,得到的 top 值就是负值。

  • 一些浏览器(像 Chrome)会在 getBoundingClientRect 返回的对象里,添加 widthheight 属性。当然,计算它们也容易:height=bottom-topwidth=right-left

坐标系统 - 图2坐标 **right/bottom** 跟同名的 CSS 定位属性名含义不同

如果比较窗口坐标和 CSS 定位属性的话就会发现,与 position: fixed 定位元素使用的属性名一样,都是相对于窗口定位的,但含义是不同的。

CSS 定位属性里的 right 是指元素距离窗口右边缘的距离,bottom 是指元素距离窗口下边缘的距离。但窗口坐标含义就不同了——坐标值都是相对窗口左上角位置计算的。

elementFromPoint(x, y)

docuemnt.elementFromPoint(x, y) 返回距离窗口坐标点 (x, y) 最近的那个元素。

语法是:

  1. let elem = document.elementFromPoint(x, y);

例如,下面代码高亮输出距离窗口中心点最近的那个元素:

  1. let centerX = docuement.documentElement.clientWidth / 2;
  2. let centerY = docuement.documentElement.clientHeight / 2;
  3. let elem = document.elementFromPoint(centerX, centerY);
  4. elem.style.background = 'red';
  5. alert(elem.tagName);

因为我们使用的是窗口坐标,所以高亮的元素会根据我们实时的滚动发生改变。

坐标系统 - 图3针对窗口外坐标点,elementFromPoint 返回 null

elem.elementFromPoint(x, y) 仅对可视区范围内的坐标位置是有效的。

如果坐标点之一的坐标值为负值,或者坐标点超出了窗口的宽高范围,那么 elementFromPoint 返回 null

多数情况下这种行为不会造成问题,但我们还是要记住这个特性。

如果我们不对 elementFromPoint 返回值做检查的话,代码中可能会发生错误:

  1. let elem = document.elementFromPoint(x, y); // 如果坐标点在窗口之外的话,得到的 elem 是 null
  2. elem.style.background = ''; // 出错了!

使用 positinon: fixed

多数时候,我们都需要用坐标来定位元素。在 CSS 中,相对于视口定义元素我们使用 position: fixed 搭配 left/top(或者 right/bottom)。

我们可以用 getBoundingClientRect 获得元素坐标,然后在它附近显示一些东西。

例如,下面的函数 createMessageUnder(elem, html) 用来在 elem 下面显示信息:

  1. let elem = document.getElementById('coords-show-mask');
  2. function createMessageUnder(elem, html) {
  3. // 创建消息元素
  4. let message = document.creatElement('div');
  5. // 当然,最好是用类名来赋予样式
  6. message.style.cssText = 'position: fixed; color: red;';
  7. let coords = elem.getBoudingClientRect();
  8. // 赋值坐标,不要忘记加 px!
  9. message.style.left = coords.left + 'px';
  10. message.style.right = coords.bottom + 'px';
  11. message.innerHTML = html;
  12. return message;
  13. }
  14. // 使用:
  15. // 创建消息框后,5 秒后消失
  16. let message = createMessageUnder(elem, '你好,世界!');
  17. document.body.append(message);
  18. setTimeout(() => message.remove(), 5000);

这里有一个细节需要注意,当我们滚页面的时候,消息框不会跟随 #coords-show-mask 移动。

原因很简单,因为消息元素是 position: fixed 的,相对窗口定位,所以页面滚动时,相对与窗口坐标值定位的消息框始终是固定在同一个位置的。

为了达到理想效果,我们需要基于文档坐标系统定位元素:position: absolute

文档坐标

文档坐标系统的定位参考点(或称坐标原点),是文档的左上角。

在 CSS 中,position: fixed 元素相对于窗口坐标定位;而 position: absolute 元素则是相对于文档坐标定位。

我们用 position: absolute 搭配 top/left 将元素定位在文档中的某处。这样在页面滚动时,定位元素也会跟随页面一起滚动。当然,前提是获取到了正确坐标值。

为了方便区分,我们把窗口坐标写作 (clientX, clientY),文档坐标写作 (pageX, pageY)

当页面没有滚动时,窗口坐标和文档坐标的坐标原点重合在一处:

坐标系统 - 图4

当滚动页面时,(clientX, clientY) 发生改变,因为此坐标是相对窗口左上角计算的,但 (pageX, pageY) 是不变的,因为元素在文档中的位置未发生改变。

还是相同的页面,这是滚动之后的样子:

坐标系统 - 图5

  • 标题“From today’s featured article”的 clientY 坐标值变为 0,因为元素现在位于窗口顶部。

  • clientX 没有变,因为我们没有水平滚动。

  • 元素的 pageXpageY 没有变,因为反映的是元素文档坐标,而元素在文档中的位置未发生改变。

获取文档坐标

规范中并未提供获取元素文档坐标的接口,但很好实现:

两个坐标值可以通过下列公式求得:

  • pageX = clientX + 文档垂直滚动距离

  • pageY = clientY + 文档水平滚动距离

下面实现了一个函数 getCoords(elem),提供获取元素文档坐标的功能。用到了 elem.getBoundingClientRect() 接口和页面当前滚动偏移:

  1. function getCoords(elem) {
  2. let box = elem.getBoundingClientRect();
  3. return {
  4. top: box.top + window.pageYOffset,
  5. left: box.left + window.pageXOffset
  6. };
  7. }

总结

网页中,一个点能对应两个坐标系统下的坐标:

  • 窗口坐标:elem.getBoundingClientRect()

  • 文档坐标:elem.getBoundingClientRect() + 当前页面滚动偏移。

position: fixed 元素相对窗口坐标定位;positon: absolute 元素相对文档坐标定位。

两种坐标都有它们各自使用场景,就像 CSS positionfixedabsolute 属性值一样。

练习题

问题

一、显示球场上点击点的窗口坐标

你可以看到下面有一个绿色“球场”。

使用 JavaScript 查找角落处用箭头标识出的窗口坐标。

为方便起见,文档中实现了一个小功能。 任何地方的点击都会显示那里的坐标。

坐标系统 - 图6
你的代码应该使用 DOM 获取窗口坐标:

  1. 左上角外部拐角(简单)。

  2. 右下角外部拐角(也简单)。

  3. 左上角内部拐角(有点难)。

  4. 右下角的内部拐角(有几种方式,任选其一)

计算出来的坐标值应该跟鼠标点击返回的坐标值一致。

PS. 如果元素具有其他尺寸和边框宽度,代码同样能够正确计算出来,而不是始终返回固定值。

二、在元素附近显示注释

创建一个函数 positionAt(anchor, positino, elem),定位注释元素 elem,相对于锚点元素 anchor 进行定位,定位方向使用 position 参数指定,可选值为 "top"(顶部)、"right"(右部)、"bottom"(底部)。

再写一个函数 showNote(anchor, position, html) 使用指定的 html 和类名 "note" 来指定位置上显示咱们创建的注释元素。

像这样显示注释:

坐标系统 - 图7

PS. 我们使用 position: fixed 来定位注释元素。

三、在元素附近显示注释(绝对定位版本)

修改上一个任务,注释元素使用 position: absolute 而不是 position: fixed 定位。

这样的话,就不会发生滚动页面时,注释元素不跟随的情况发生了。

为了能让页面滚动,我们先为 body 设置样式 <body style="height: 2000px;">

四、在内部定位注释

扩展上一个任务的功能:让函数 positionAt(anchor, position, elem) 在 anchor 内部插入 elem。

position 支持的新值包括:

  • top-out,right-out,bottom-out:分别对应之前的 top,out 和 bottom。

  • top-in,right-in,bottom-in:与上面的 *-out 对应,只不过位置是在 anchor 元素内侧。

例如:

  1. // 在 blockquote 元素内部顶端显示注释
  2. positionAt(blockquote, "top-in", note);
  3. // 在 blockquote 上面显示注释
  4. positionAt(blockquote, "top-out", note);
  5. // 下面同理,在相应位置显示
  6. showNote(blockquote, "right-in", "note right-in");
  7. showNote(blockquote, "right-out", "note right-out");
  8. showNote(blockquote, "bottom-in", "note bottom-in");
  9. showNote(blockquote, "bottom-out", "note bottom-out");

坐标系统 - 图8

答案

一、显示球场上点击点的窗口坐标

外部拐角

外部拐角坐标我们可以直接通过 elem.getBoundingClientRect() 获取到。

我们假设左上角的点是 answer1,右下角的那个点是 answer2

  1. let coords = elem.getBoundingClientRect();
  2. let answer1 = [coords.left, coords.top];
  3. let answer2 = [coords.right, coords.bottom];

左上角内部拐点

左上角内部拐点比左上角外部拐点多了一个边框的距离。可靠的方式是使用 clientLeft/Top:

  1. let answer3 = [coords.left + field.clientLeft, coords.top + field.clientTop];

右下角内部拐点

在当前场景里,拿外部坐标减去边框尺寸得到的就是内部拐点坐标了。

使用 CSS 方式:

  1. let answer4 = [
  2. coords.right - parseInt(getComputedStyle(field).borderRightWidth),
  3. coords.bottom - parseInt(getComputedStyle(field).borderBottomWidth)
  4. ];

一个可选的方式是为外部左上角拐点坐标 ➕ clientWidth/clientHeight

  1. let answer4 = [
  2. coords.left + elem.clientLeft + elem.clientWidth,
  3. coords.top + elem.clientTop + elem.clientHeight
  4. ];

二、在元素附近显示注释

  1. <!DOCTYPE HTML>
  2. <html>
  3. <head>
  4. <meta charset="utf-8">
  5. <style>
  6. .note {
  7. position: fixed; /* 注释元素使用固定定位 */
  8. z-index: 1000;
  9. padding: 5px;
  10. border: 1px solid black;
  11. background: white;
  12. text-align: center;
  13. font: italic 14px Georgia;
  14. }
  15. blockquote {
  16. background: #f9f9f9;
  17. border-left: 10px solid #ccc;
  18. margin: 0 0 0 100px;
  19. padding: .5em 10px;
  20. quotes: "\201C""\201D""\2018""\2019";
  21. display: inline-block;
  22. white-space: pre;
  23. }
  24. blockquote:before {
  25. color: #ccc;
  26. content: open-quote;
  27. font-size: 4em;
  28. line-height: .1em;
  29. margin-right: .25em;
  30. vertical-align: -.4em;
  31. }
  32. </style>
  33. </head>
  34. <body>
  35. <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Reprehenderit sint atque dolorum fuga ad incidunt voluptatum error fugiat animi amet! Odio temporibus nulla id unde quaerat dignissimos enim nisi rem provident molestias sit tempore omnis recusandae
  36. esse sequi officia sapiente.</p>
  37. <blockquote>
  38. Teacher: Why are you late?
  39. Student: There was a man who lost a hundred dollar bill.
  40. Teacher: That's nice. Were you helping him look for it?
  41. Student: No. I was standing on it.
  42. </blockquote>
  43. <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Reprehenderit sint atque dolorum fuga ad incidunt voluptatum error fugiat animi amet! Odio temporibus nulla id unde quaerat dignissimos enim nisi rem provident molestias sit tempore omnis recusandae
  44. esse sequi officia sapiente.</p>
  45. <script>
  46. /**
  47. * Positions elem relative to anchor as said in position.
  48. *
  49. * @param {Node} anchor Anchor element for positioning
  50. * @param {string} position One of: top/right/bottom
  51. * @param {Node} elem Element to position
  52. *
  53. * Both elements: elem and anchor must be in the document
  54. */
  55. function positionAt(anchor, position, elem) {
  56. let anchorCoords = anchor.getBoundingClientRect();
  57. switch (position) {
  58. case "top":
  59. elem.style.left = anchorCoords.left + "px";
  60. elem.style.top = anchorCoords.top - elem.offsetHeight + "px";
  61. break;
  62. case "right":
  63. elem.style.left = anchorCoords.left + anchor.offsetWidth + "px";
  64. elem.style.top = anchorCoords.top + "px";
  65. break;
  66. case "bottom":
  67. elem.style.left = anchorCoords.left + "px";
  68. elem.style.top = anchorCoords.top + anchor.offsetHeight + "px";
  69. break;
  70. }
  71. }
  72. /**
  73. * Shows a note with the given html at the given position
  74. * relative to the anchor element.
  75. */
  76. function showNote(anchor, position, html) {
  77. let note = document.createElement('div');
  78. note.className = "note";
  79. note.innerHTML = html;
  80. document.body.append(note);
  81. positionAt(anchor, position, note);
  82. }
  83. // test it
  84. let blockquote = document.querySelector('blockquote');
  85. showNote(blockquote, "top", "note above");
  86. showNote(blockquote, "right", "note at the right");
  87. showNote(blockquote, "bottom", "note below");
  88. </script>
  89. </body>
  90. </html>

在线实例

三、在元素附近显示注释(绝对定位版本)

这里只要稍微修改下任务二的代码就可以了:

  1. CSS:.note 类使用 position: absolute 而非 position: fixed 定位。

  2. 使用本章中定义的 getCoords 函数来获得元素相对文档的坐标位置。

  1. .note {
  2. position: absolute;
  3. ...
  4. }
  1. function getCoords(elem) {
  2. let box = elem.getBoundingClientRect();
  3. return {
  4. top: box.top + pageYOffset,
  5. left: box.left + pageXOffset
  6. };
  7. }
  8. function positionAt(anchor, position, elem) {
  9. // let anchorCoords = anchor.getBoundingClientRect(); 改为
  10. let anchorCoords = getCoords(anchor);
  11. ...
  12. }

在线实例

四、在内部定位注释

其实我们只要把上一个任务中的函数 positionAt 稍微修改下就好了。完整的修改后的函数代码如下:

  1. function positionAt(anchor, position, elem) {
  2. let anchorCoords = getCoords(anchor);
  3. switch (position) {
  4. case "top-out":
  5. elem.style.left = anchorCoords.left + "px";
  6. elem.style.top = anchorCoords.top - elem.offsetHeight + "px";
  7. break;
  8. case "right-out":
  9. elem.style.left = anchorCoords.left + anchor.offsetWidth + "px";
  10. elem.style.top = anchorCoords.top + "px";
  11. break;
  12. case "bottom-out":
  13. elem.style.left = anchorCoords.left + "px";
  14. elem.style.top = anchorCoords.top + anchor.offsetHeight + "px";
  15. break;
  16. case "top-in":
  17. elem.style.left = anchorCoords.left + "px";
  18. elem.style.top = anchorCoords.top + "px";
  19. break;
  20. case "right-in":
  21. elem.style.width = '150px';
  22. elem.style.left = anchorCoords.left + anchor.offsetWidth - elem.offsetWidth + "px";
  23. elem.style.top = anchorCoords.top + "px";
  24. break;
  25. case "bottom-in":
  26. elem.style.left = anchorCoords.left + "px";
  27. elem.style.top = anchorCoords.top + anchor.offsetHeight - elem.offsetHeight + "px";
  28. break;
  29. }
  30. }

在线实例

(完)