Javascript 坐标
要移动页面的元素,我们应该先熟悉坐标。
大多数 JavaScript 方法处理的是以下两种坐标系中的一个:
- 相对于窗口 —— 类似于
position:fixed
,从窗口的顶部/左侧边缘计算得出。 - 我们将这些坐标表示为
clientX/clientY
,当我们研究事件属性时,就会明白为什么使用这种名称来表示坐标。 - 相对于文档 —— 与文档根(document root)中的
position:absolute
类似,从文档的顶部/左侧边缘计算得出。 - 我们将它们表示为
pageX/pageY
。
当页面滚动到最开始时,此时窗口的左上角恰好是文档的左上角,它们的坐标彼此相等。但是,在文档移动之后,元素的窗口相对坐标会发生变化,因为元素在窗口中移动,而元素在文档中的相对坐标保持不变。
在下图中,我们在文档中取一点,并演示了它滚动之前(左)和之后(右)的坐标:
当文档滚动了:
-
pageY
—— 元素在文档中的相对坐标保持不变,从文档顶部(现在已滚动出去)开始计算。 -
clientY
—— 窗口相对坐标确实发生了变化(箭头变短了),因为同一个点越来越靠近窗口顶部。
元素坐标:getBoundingClientRect
方法 elem.getBoundingClientRect()
返回最小矩形的窗口坐标,该矩形将 elem
作为内建 DOMRect 类的对象。
主要的 DOMRect
属性:
-
x/y
—— 矩形原点相对于窗口的 X/Y 坐标, -
width/height
—— 矩形的 width/height(可以为负)。
此外,还有派生(derived)属性:
-
top/bottom
—— 顶部/底部矩形边缘的 Y 坐标, -
left/right
—— 左/右矩形边缘的 X 坐标。
如果你滚动此页面并重复点击上面那个按钮,你会发现随着窗口相对按钮位置的改变,其窗口坐标(如果你垂直滚动页面,则为 y/top/bottom
)也随之改变。
下面这张是 elem.getBoundingClientRect()
的输出的示意图:
正如你所看到的,x/y
和 width/height
对矩形进行了完整的描述。可以很容易地从它们计算出派生(derived)属性:
-
left = x
-
top = y
-
right = x + width
-
bottom = y + height
请注意:
- 坐标可能是小数,例如
10.5
。这是正常的,浏览器内部使用小数进行计算。在设置 style.left/top
时,我们不是必须对它们进行舍入。 - 坐标可能是负数。例如滚动页面,使
elem
现在位于窗口的上方,则 elem.getBoundingClientRect().top
为负数。
为什么需要派生(derived)属性?如果有了
x/y
,为什么还要还会存在 top/left
?从数学上讲,一个矩形是使用其起点
(x,y)
和方向向量(width,height)
唯一定义的。因此,其它派生属性是为了方便起见。
从技术上讲,
width/height
可能为负数,从而允许“定向(directed)”矩形,例如代表带有正确标记的开始和结束的鼠标选择。
负的
width/height
值表示矩形从其右下角开始,然后向左上方“增长”。
这是一个矩形,其
width
和height
均为负数(例如width=-200
,height=-100
):
正如你所看到的,在这个例子中,
left/top
与x/y
不相等。
但是实际上,
elem.getBoundingClientRect()
总是返回正数的 width/height,这里我们提及负的width/height
只是为了帮助你理解,为什么这些看起来重复的属性,实际上并不是重复的。
IE 浏览器不支持
x/y
由于历史原因,IE 浏览器不支持
x/y
属性。
因此,我们可以写一个 polyfill(在
DomRect.prototype
中添加一个 getter),或者仅使用top/left
,因为对于正值的width/height
来说,它们和x/y
一直是一样的,尤其是对于elem.getBoundingClientRect()
的结果。
坐标的 right/bottom 与 CSS position 属性不同
相对于窗口(window)的坐标和 CSS
position:fixed
之间有明显的相似之处。
但是在 CSS 定位中,
right
属性表示距右边缘的距离,而bottom
属性表示距下边缘的距离。
如果我们再看一下上面的图片,我们可以看到在 JavaScript 中并非如此。窗口的所有坐标都从左上角开始计数,包括这些坐标。
elementFromPoint(x, y)
对 document.elementFromPoint(x, y)
的调用会返回在窗口坐标 (x, y)
处嵌套最多(the most nested)的元素。
语法如下:
let elem = document.elementFromPoint(x, y);
例如,下面的代码会高亮显示并输出现在位于窗口中间的元素的标签:
let centerX = document.documentElement.clientWidth / 2;
let centerY = document.documentElement.clientHeight / 2;
let elem = document.elementFromPoint(centerX, centerY);
elem.style.background = "red";
alert(elem.tagName);
因为它使用的是窗口坐标,所以元素可能会因当前滚动位置而有所不同。
对于在窗口之外的坐标,
elementFromPoint
返回 null
方法
document.elementFromPoint(x,y)
只对在可见区域内的坐标(x,y)
起作用。
如果任何坐标为负或者超过了窗口的 width/height,那么该方法就会返回
null
。
在大多数情况下,这种行为并不是一个问题,但是我们应该记住这一点。
如果我们没有对其进行检查,可能就会发生下面这个典型的错误:
let elem = document.elementFromPoint(x, y); // 如果坐标恰好在窗口外,则 elem = null elem.style.background = ''; // Error!
用于 “fixed” 定位
大多数时候,我们需要使用坐标来确定某些内容的位置。
想要在某元素附近展示内容,我们可以使用 getBoundingClientRect
来获取这个元素的坐标,然后使用 CSS position
以及 left/top
(或 right/bottom
)。
例如,下面的函数 createMessageUnder(elem, html)
在 elem
下显示了消息:
let elem = document.getElementById("coords-show-mark");
function createMessageUnder(elem, html) {
// 创建 message 元素
let message = document.createElement('div');
// 在这里最好使用 CSS class 来定义样式
message.style.cssText = "position:fixed; color: red";
// 分配坐标,不要忘记 "px"!
let coords = elem.getBoundingClientRect();
message.style.left = coords.left + "px";
message.style.top = coords.bottom + "px";
message.innerHTML = html;
return message;
}
// 用法:
// 在文档中添加 message 保持 5 秒
let message = createMessageUnder(elem, 'Hello, world!');
document.body.append(message);
setTimeout(() => message.remove(), 5000);
我们可以修改代码以在元素左侧,右侧或下面显示消息,也可以应用 CSS 动画来营造“淡入淡出”效果等。这很简单,因为我们有该元素所有坐标和大小。
但是请注意一个重要的细节:滚动页面时,消息就会从按钮流出。
原因很显然:message 元素依赖于 position:fixed
,因此当页面滚动时,它仍位于窗口的同一位置。
要改变这一点,我们需要使用基于文档(document)的坐标和 position:absolute
样式。
文档坐标
文档相对坐标从文档的左上角开始计算,而不是窗口。
在 CSS 中,窗口坐标对应于 position:fixed
,而文档坐标与顶部的 position:absolute
类似。
我们可以使用 position:absolute
和 top/left
来把某些内容放到文档中的某个位置,以便在页面滚动时,元素仍能保留在该位置。但是我们首先需要正确的坐标。
这里没有标准方法来获取元素的文档坐标。但是写起来很容易。
这两个坐标系统通过以下公式相连接:
-
pageY
= clientY
+ 文档的垂直滚动出的部分的高度。 -
pageX
= clientX
+ 文档的水平滚动出的部分的宽度。
函数 getCoords(elem)
将从 elem.getBoundingClientRect()
获取窗口坐标,并向其中添加当前滚动:
// 获取元素的文档坐标
function getCoords(elem) {
let box = elem.getBoundingClientRect();
return {
top: box.top + window.pageYOffset,
right: box.right + window.pageXOffset,
bottom: box.bottom + window.pageYOffset,
left: box.left + window.pageXOffset
};
}
如果在上面的示例中,我们将其与 position:absolute
一起使用,则在页面滚动时,消息仍停留在元素附近。
修改后的 createMessageUnder
函数:
function createMessageUnder(elem, html) {
let message = document.createElement('div');
message.style.cssText = "position:absolute; color: red";
let coords = getCoords(elem);
message.style.left = coords.left + "px";
message.style.top = coords.bottom + "px";
message.innerHTML = html;
return message;
}
总结
页面上的任何点都有坐标:
- 相对于窗口的坐标 ——
elem.getBoundingClientRect()
。 - 相对于文档的坐标 ——
elem.getBoundingClientRect()
加上当前页面滚动。
窗口坐标非常适合和 position:fixed
一起使用,文档坐标非常适合和 position:absolute
一起使用。
这两个坐标系统各有利弊。有时我们需要其中一个或另一个,就像 CSS position
的 absolute
和 fixed
一样。
任务
查找区域的窗口坐标
在下面的 iframe 中,你可以看到一个带有绿色区域(field)的文档。
使用 JavaScript 查找带箭头指向的角的窗口坐标。
你的代码应该使用 DOM 来获取以下窗口坐标:
- 左上的外角(这很简单)。
- 右下的外角(也挺简单)。
- 左上的内角(这有点难)。
- 右下的内角(有几种方式,选择其中一种)。
你计算得到的坐标,应该与点击鼠标返回的坐标相同。
P.S. 如果元素具有其他大小(size)和边框(border),且未绑定任何固定的值,你写的代码也应该起作用。
解决方案
- 外角
外角就是我们从 elem.getBoundingClientRect() 获取的。
answer1
为左上角的坐标,answer2
为右下角的坐标:
let coords = elem.getBoundingClientRect();
let answer1 = [coords.left, coords.top];
let answer2 = [coords.right, coords.bottom];
内角与外角主要的不同在于边框的宽度。一种获取距离的可靠的方法是 clientLeft/clientTop
:
let answer3 = [coords.left + field.clientLeft, coords.top + field.clientTop];
在我们的例子中,我们需要把外部坐标减去边框(border)大小。
我们可以使用 CSS 的方式:
let answer4 = [
coords.right - parseInt(getComputedStyle(field).borderRightWidth),
coords.bottom - parseInt(getComputedStyle(field).borderBottomWidth)
];
另一种方式是把 clientWidth/clientHeight
和左上角的坐标相加。这个方式相较于上一个或许更好:
let answer4 = [
coords.left + elem.clientLeft + elem.clientWidth,
coords.top + elem.clientTop + elem.clientHeight
];
在元素旁显示一个 note
创建一个函数 positionAt(anchor, position, elem)
来定位 elem
,具体取决于 anchor
元素附近的 position
。
position
必须具有下列三个字符串中的一个:
-
"top"
— 将 elem
定位在 anchor
上方 -
"right"
— 将 elem
定位在 anchor
右侧 -
"bottom"
— 将 elem
定位在 anchor
下方
position
被用在函数 showNote(anchor, position, html)
内,该函数使用给定的 html
创建一个 “note” 元素,并将其显示在 anchor
附近的 position
处。
这是一个演示示例:
解决方案
在这个任务中,我们只需要准确地计算坐标即可。具体细节,请参见代码。
请注意:元素必须在文档中才能读取 offsetHeight
和其它属性。 隐藏的(display:none
)或者不在文档中的元素没有大小。
在元素旁(absolute)显示一个 note
修改 上一个任务 的解决方案,让 note 元素使用 position:absolute
来替代 position:fixed
。
这可以防止页面滚动时元素的“失控”。
以上一个任务的解决方案为基础。为了测试页面滚动,请添加样式 <body style="height: 2000px">
。
解决方案
解决方案实际上很简单:
- 在
.note
的 CSS 中,使用 position:absolute
代替 position:fixed
。 - 使用在 坐标 一章中所讲的函数 getCoords() 来获取相对于文档的坐标。
把 note 放在元素内部(absolute)
扩展上一个任务 在元素旁(absolute)显示一个 note:教函数 positionAt(anchor, position, elem)
把 elem
插入到 anchor
内部。
position
的新值:
-
top-out
,right-out
,bottom-out
— 和之前一样工作,它们把 elem
插入到 anchor
的上方/右侧/下方。 -
top-in
,right-in
,bottom-in
— 把 elem
插入到 anchor
内部:将其粘贴到上/右/下边缘。
例如:
// 在 blockquote 上方显示 note
positionAt(blockquote, "top-out", note);
// 在 blockquote 内部的上边缘显示 note
positionAt(blockquote, "top-in", note);
结果:
可以以上一个任务 在元素旁(absolute)显示一个 note 的解决方案为基础。
更多建议: