我不得不寻找一个可能的解决方案很长时间,我想我找到了一个使用Intersection Observer API 来检测父文档中 iframe 的滚动位置而无需访问父文档 DOM .
我在 iframe 中创建了一堆隐藏的 100 像素高的元素。它们绝对位于彼此下方,以便它们一起填充整个 iframe 文档的高度。交集观察者然后观察(顶级文档)视口和每个隐藏元素之间的交集,并根据它返回的值计算 iframe 的滚动位置。如果主体高度增加,ResizeObserver 会创建额外的隐藏元素。
此方法假定您的 iframe 始终至少为 100 像素高。如果您期望更小的高度,则需要调整隐藏容器的高度。原因是一旦隐藏的容器 100% 可见,交叉点观察者在父文档滚动时不会发出回调(因为交叉点比率保持在 1)。这也是为什么我需要很多小容器而不是观察与 iframe body 本身的交集的原因。
const CONTAINER_HEIGHT = 100;
const threshold = [...Array(CONTAINER_HEIGHT + 1).keys()].map((i) => i / CONTAINER_HEIGHT);
/**
* Registers an intersection handler that detects the scrolled position of the current
* iframe within the browser viewport and calls a handler when it is first invoked and
* whenever the scrolled position changes. This allows to position elements within the
* iframe in a way that their position stays sticky in relation to the browser window.
* @param handler Is invoked when the function is first called and whenever the scroll
* position changes (for example due to the user scrolling the parent document). The
* "top" parameter is the number of pixels from the top of the browser viewport to the
* top of the iframe (if the top of the iframe is above the top of the browser viewport)
* or 0 (if the top of the iframe is below the top of the browser viewport). Positioning
* an element absolutely at this top position inside the iframe will simulate a sticky
* positioning at the top edge of the browser viewport.
* @returns Returns a callback that unregisters the handler.
*/
function registerScrollPositionHandler(handler: (top: number) => void): () => void {
const elementContainer = document.createElement('div');
Object.assign(elementContainer.style, {
position: 'absolute',
top: '0',
bottom: '0',
width: '1px',
pointerEvents: 'none',
overflow: 'hidden'
});
document.body.appendChild(elementContainer);
const elements: HTMLDivElement[] = [];
let intersectionObserver: IntersectionObserver | undefined = undefined;
const resizeObserver = new ResizeObserver(() => {
intersectionObserver = new IntersectionObserver((entries) => {
for (const entry of entries) {
if (entry.intersectionRatio > 0 && (entry.intersectionRect.top > entry.boundingClientRect.top || entry.target === elements[0])) {
handler(entry.intersectionRect.top);
}
}
}, { threshold });
const count = Math.ceil(document.documentElement.offsetHeight / CONTAINER_HEIGHT);
for (let i = 0; i < count; i++) {
if (!elements[i]) {
elements[i] = document.createElement('div');
Object.assign(elements[i].style, {
position: 'absolute',
top: `${i * CONTAINER_HEIGHT}px`,
height: `${CONTAINER_HEIGHT}px`,
width: '100%'
});
elementContainer.appendChild(elements[i]);
intersectionObserver.observe(elements[i]);
}
}
});
resizeObserver.observe(document.documentElement);
return () => {
resizeObserver.disconnect();
intersectionObserver?.disconnect();
elementContainer.remove();
};
}
此示例代码应在浏览器视口顶部创建一个粘性工具栏:
<div style="position: absolute; top: 0; left: 0; bottom: 0; right: 0; overflow: hidden; pointer-events: none; z-index: 90">
<div id="toolbar" style="position: absolute; top: 0; left: 0; right: 0; pointer-events: auto; transition: top 0.3s">
Line 1<br/>Line 2<br/>Line 3<br/>Line 4<br/>Line 5<br/>Line 6<br/>Line 7<br/>Line 8<br/>Line 9<br/>Line 10
</div>
</div>
<script>
registerScrollPositionHandler((top) => {
document.querySelector('#toolbar').style.top = `${top}px`;
});
</script>
Note that other than what you asked for, this will position the toolbar at the top of the viewport rather than at the bottom. Positioning at the bottom should also be possible, but is slightly more complex. If anyone requires a solution for this, please let me know in the comments and I will invest the time to adjust my answer.