坐标与滚动定位
窗口与文档的宽高该量哪些属性?
- 可见区域 (内容区):
document.documentElement.clientWidth/clientHeight, 已扣掉滚动条占用的宽度; - 快捷键:
window.innerWidth/innerHeight包含滚动条, 有滚动条时两者数值不同, 定位与绘制要用扣掉滚动条的那个;
window.innerWidth; // 完整窗口宽
document.documentElement.clientWidth; // 窗口宽减去滚动条
- 无
<!DOCTYPE html>时顶层几何属性行为会异常, 现代 HTML 必须写 DOCTYPE; - 整份文档高度 (含滚出部分):
documentElement.scrollHeight不可靠, Chrome/Safari/Opera 在无滚动时甚至小于clientHeight, 要取多个属性的最大值;
const scrollHeight = Math.max(
document.body.scrollHeight,
document.documentElement.scrollHeight,
document.body.offsetHeight,
document.documentElement.offsetHeight,
document.body.clientHeight,
document.documentElement.clientHeight,
);
- 元素自身的偏移/客户端/滚动尺寸: 见 样式与尺寸, 本节只补整页口径;
文档坐标与视口坐标如何换算?
- 两套坐标系: 视口坐标从窗口左上角算起 (对应 CSS
position:fixed); 文档坐标从文档左上角算起 (对应锚定在文档上的position:absolute); - 换算公式:
pageY = clientY + 页面纵向已滚出高度,pageX同理; 即文档坐标 = 视口坐标 + 当前滚动; - 元素视口坐标:
elem.getBoundingClientRect(), 细节见 样式与尺寸; 注意right/bottom也是从左上角量起, 与 CSS 的right/bottom语义不同; - 派生属性:
left = x、top = y、right = x + width、bottom = y + height; 之所以两组看似重复, 是因为width/height可为负 (表示从右下角往左上生长的有向矩形), 那时left/top不等于x/y; 实践中getBoundingClientRect()的宽高恒为正; - 兼容: 老 IE 不支持
x/y, 直接用恒等的left/top即可; - 元素文档坐标: 没有标准方法, 自己用滚动量补齐;
// 文档坐标 = 视口坐标 + 当前滚动
function getCoords(elem) {
const 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:fixed+ 视口坐标; 想跟着文档滚动用position:absolute+ 文档坐标;
// 提示框贴在元素下方
const message = document.createElement("div");
message.style.cssText = "position:absolute; color:red";
const coords = getCoords(elem);
message.style.left = coords.left + "px";
message.style.top = coords.bottom + "px";
- 边界:
getBoundingClientRect可能返回小数与负数 (元素被滚到窗口上方时top为负), 不必取整; offsetLeft/offsetTop: 相对offsetParent(最近定位祖先), 不随页面滚动改变; 要视口坐标就用getBoundingClientRect, 要文档坐标就再加滚动量;
如何读取与修改页面滚动位置?
- 读取:
window.pageYOffset/pageXOffset, 只读, 是window.scrollY/scrollX的别名; - 元素级滚动状态: 元素的
scrollTop/scrollLeft, 页面对应documentElement.scrollTop/scrollLeft(老 WebKit/Safari 要用document.body), 用pageYOffset就不必记这些差异; - 修改:
scrollBy相对当前位置,scrollTo到绝对坐标;
window.scrollBy(0, 10); // 从当前位置下移 10px
window.scrollTo(0, 0); // 回到页面开头
document.documentElement.scrollTop = 0; // 等价写法
- 让元素可见:
elem.scrollIntoView(top),top=true(默认) 与窗口顶部对齐,top=false与窗口底部对齐; - 前提: 滚动前 DOM 必须已构建完, 放在
<head>里的脚本滚不动页面;
如何禁止页面滚动, 滚动条消失怎么办?
- 做法: 设置
document.body.style.overflow = "hidden", 页面冻结在当前滚动位置; 同样可用于冻结任意元素; - 副作用: 滚动条消失, 原本被它占的宽度空出来, 内容会横向"跳"一下;
- 补偿: 比较冻结前后的
clientWidth, 若变大说明滚动条消失了, 就给document.body补上等宽padding, 保持内容宽度不变;
const before = document.documentElement.clientWidth;
document.body.style.overflow = "hidden";
const scrollbar = document.documentElement.clientWidth - before;
document.body.style.paddingRight = `${scrollbar}px`;
如何取某个视口坐标上的元素?
document.elementFromPoint(x, y): 返回该视口坐标下最内层的元素, 坐标是视口坐标, 所以结果随滚动变化;
const elem = document.elementFromPoint(
document.documentElement.clientWidth / 2,
document.documentElement.clientHeight / 2,
);
elem.style.background = "red"; // elem 可能为 null!
- 边界: 坐标越出可视区域 (为负或超过窗口宽高) 时返回
null, 直接访问属性会报错, 必须先判空; - 复数形式:
document.elementsFromPoint(x, y)返回该点的所有元素数组, 从最内层到最外层;