我需要观察一个 DOM 元素的位置,因为我需要显示一个相对于它的弹出面板(但不在同一个容器中),并且面板应该跟随元素。我应该如何实现这样的逻辑?
这是一个片段,您可以在其中看到外部和嵌套弹出面板的打开,但它们不跟随水平滚动。我希望他们都关注它并继续显示在相应的图标附近(它应该是一种适用于任何地方的通用方法)。您可能会忽略嵌套弹出窗口没有与外部一起关闭 - 这只是为了使片段更简单。我预计除了 showPopup
之外没有任何变化功能。对于这个例子,标记被特别简化了;不要试图改变它——我需要它。
~function handlePopups() {
function showPopup(src, popup, popupContainer) {
var bounds = popupContainer.getBoundingClientRect()
var bb = src.getBoundingClientRect()
popup.style.left = bb.right - bounds.left - 1 + 'px'
popup.style.top = bb.bottom - bounds.top - 1 + 'px'
return () => {
// fucntion to cleanup handlers when closed
}
}
var opened = new Map()
document.addEventListener('click', e => {
if (e.target.tagName === 'I') {
var wasActive = e.target.classList.contains('active')
var popup = document.querySelector(`.popup[data-popup="${e.target.dataset.popup}"]`)
var old = opened.get(popup)
if (old) {
old.src.classList.remove('active')
popup.hidden = true
old.close()
opened.delete(old)
}
if (!wasActive) {
e.target.classList.add('active')
popup.hidden = false
opened.set(popup, {
src: e.target,
close: showPopup(e.target, popup, document.querySelector('.popup-dest')),
})
}
}
})
}()
~function syncParts() {
var scrollLeft = 0
document.querySelector('main').addEventListener('scroll', e => {
if (e.target.classList.contains('inner') && e.target.scrollLeft !== scrollLeft) {
scrollLeft = e.target.scrollLeft
void [...document.querySelectorAll('.middle .inner')]
.filter(x => x.scrollLeft !== scrollLeft)
.forEach(x => x.scrollLeft = scrollLeft)
}
}, true)
}()
* {
box-sizing: border-box;
}
[hidden] {
display: none !important;
}
html, body, main {
height: 100%;
margin: 0;
}
main {
display: grid;
grid-template: auto 1fr 17px / auto 1fr auto;
}
section {
overflow: hidden;
display: flex;
flex-direction: column;
outline: 1px dotted red;
outline-offset: -1px;
position: relative;
}
.inner {
overflow: scroll;
padding: 0 1px 1px 0;
margin: 0 -18px -18px 0;
flex: 1 1 0px;
display: flex;
flex-direction: column;
}
.top {
grid-row: 1;
}
.bottom {
grid-row: 2;
}
.left {
grid-column: 1;
}
.middle {
grid-column: 2;
}
.right {
grid-column: 3;
}
.wide, .scroller {
width: 2000px;
flex: 1 0 1px;
}
.wide {
background: repeating-linear-gradient(to right, rgba(0,255,0,.5), rgba(0,0,255,.5) 16em);
}
.visible-scroll .inner {
margin-top: -1px;
margin-bottom: 0;
}
.scroller {
height: 1px;
}
.popup-dest {
pointer-events: none;
grid-row: 1 / 3;
position: relative;
}
.popup {
position: absolute;
border: 1px solid;
pointer-events: all;
}
.popup-outer {
width: 8em;
height: 8em;
background: silver;
}
.popup-nested {
width: 5em;
height: 5em;
background: antiquewhite;
}
i {
display: inline-block;
border-radius: 50% 50% 0 50%;
border: 1px solid;
width: 1.5em;
height: 1.5em;
line-height: 1.5em;
text-align: center;
cursor: pointer;
}
i::after {
content: "i";
}
i.active {
background: rgba(255,255,255,.5);
}
<main>
<section class="top left">
<div><div class="inner">
<div>Smth<br>here</div>
</div></div>
</section>
<section class="top middle">
<div class="inner">
<div class="wide">
<i data-popup="outer" style="margin-left:10em"></i>
<i data-popup="outer" style="margin-left:10em"></i>
<i data-popup="outer" style="margin-left:10em"></i>
<i data-popup="outer" style="margin-left:10em"></i>
<i data-popup="outer" style="margin-left:10em"></i>
<i data-popup="outer" style="margin-left:10em"></i>
<i data-popup="outer" style="margin-left:10em"></i>
</div>
</div>
</section>
<section class="top right">
<div class="inner">Smth here</div></section>
<section class="bottom left">
<div class="inner">Smth here</div>
</section>
<section class="bottom middle">
<div class="inner">
<div class="wide"><script>document.write("Smth is here too... ".repeat(1000))</script></div>
</div>
</section>
<section class="bottom right">
<div class="inner">Smth here</div>
</section>
<section class="middle visible-scroll">
<div class="inner">
<div class="scroller"></div>
</div>
</section>
<section class="middle popup-dest">
<div class="popup popup-outer" data-popup="outer" hidden>
<i data-popup="nested" style="margin-left:5em;margin-top:5em;"></i>
</div>
<div class="popup popup-nested" data-popup="nested" hidden>
</div>
</section>
</main>
现在我有以下想法:
scroll
capturing
上的事件通过 getBoundingClientRect
获取元素的实际位置并根据当前位置重新定位面板。我目前正在使用类似的解决方案,但是有一个问题。当元素被另一个脚本移动时,它不会强制面板重新定位。其中一种情况——当元素本身是另一个面板时——对不相关的滚动事件的简单过滤会过滤掉这样的滚动。我也有一些反跳的情况,它们也很难处理。 IntersectionObserver
跟踪 Action 。问题似乎在于它仅适用于交叉口大小的变化,而不适用于任何移动。我有一个想法,通过 rootMargin
裁剪视口(viewport)到元素覆盖的同一个矩形,但选项是只读的。这意味着我需要在每次移动时创建新的观察者。我不确定这种解决方案对性能的影响。另外因为它只提供了一个大概的位置,所以我认为我无法消除对 getBoundingClientRect
的调用. IntersectionObserver
一起使用,但是当检测到第一个 Action 时,只需订阅requestAnimationFrame
并检查那里的元素位置。虽然位置不同,但处理它并递归使用 requestAnimationFrame
.如果位置相同(我不确定一帧是否足够,可能是 5 帧?),停止订阅 requestAnimationFrame
并创建一个新的IntersectionObserver
. 恐怕这样的解决方案会出现性能问题。在我看来,它们也太复杂了。也许有一些我应该使用的已知解决方案?
最佳答案
实现第一种方法。订阅所有scroll
整个文档中的事件并更新处理程序中的位置。您不能按 src
的父级过滤事件事件链中不显示嵌套弹出滚动元素的元素。
如果以编程方式移动弹出窗口,它也不起作用 - 您可能会在 outer
时注意到它。弹出窗口移动到另一个图标和nested
留在老地方。
function showPopup(src, popup, popupContainer) {
function position() {
var bounds = popupContainer.getBoundingClientRect()
var bb = src.getBoundingClientRect()
popup.style.left = bb.right - bounds.left - 1 + 'px'
popup.style.top = bb.bottom - bounds.top - 1 + 'px'
}
position()
document.addEventListener('scroll', position, true)
return () => { // cleanup
document.removeEventListener('scroll', position, true)
}
}
完整代码:
~function syncParts() {
var sl = 0
document.querySelector('main').addEventListener('scroll', e => {
if (e.target.classList.contains('inner') && e.target.scrollLeft !== sl) {
sl = e.target.scrollLeft
void [...document.querySelectorAll('.middle .inner')]
.filter(x => x.scrollLeft !== sl)
.forEach(x => x.scrollLeft = sl)
}
}, true)
}()
~function handlePopups() {
function showPopup(src, popup, popupContainer) {
function position() {
var bounds = popupContainer.getBoundingClientRect()
var bb = src.getBoundingClientRect()
popup.style.left = bb.right - bounds.left - 1 + 'px'
popup.style.top = bb.bottom - bounds.top - 1 + 'px'
}
position()
document.addEventListener('scroll', position, true)
return () => { // cleanup
document.removeEventListener('scroll', position, true)
}
}
var opened = new Map()
document.addEventListener('click', e => {
if (e.target.tagName === 'I') {
var wasActive = e.target.classList.contains('active')
var popup = document.querySelector(`.popup[data-popup="${e.target.dataset.popup}"]`)
var old = opened.get(popup)
if (old) {
old.src.classList.remove('active')
popup.hidden = true
old.close()
opened.delete(old)
}
if (!wasActive) {
e.target.classList.add('active')
popup.hidden = false
opened.set(popup, {
src: e.target,
close: showPopup(e.target, popup, document.querySelector('.popup-dest')),
})
}
}
})
}()
* {
box-sizing: border-box;
}
[hidden] {
display: none !important;
}
html, body, main {
height: 100%;
margin: 0;
}
main {
display: grid;
grid-template: auto 1fr 17px / auto 1fr auto;
}
section {
overflow: hidden;
display: flex;
flex-direction: column;
outline: 1px dotted red;
outline-offset: -1px;
position: relative;
}
.inner {
overflow: scroll;
padding: 0 1px 1px 0;
margin: 0 -18px -18px 0;
flex: 1 1 0px;
display: flex;
flex-direction: column;
}
.top {
grid-row: 1;
}
.bottom {
grid-row: 2;
}
.left {
grid-column: 1;
}
.middle {
grid-column: 2;
}
.right {
grid-column: 3;
}
.wide, .scroller {
width: 2000px;
flex: 1 0 1px;
}
.wide {
background: repeating-linear-gradient(to right, rgba(0,255,0,.5), rgba(0,0,255,.5) 16em);
}
.visible-scroll .inner {
margin-top: -1px;
margin-bottom: 0;
}
.scroller {
height: 1px;
}
.popup-dest {
pointer-events: none;
grid-row: 1 / 3;
position: relative;
}
.popup {
position: absolute;
border: 1px solid;
pointer-events: all;
}
.popup-outer {
width: 8em;
height: 8em;
background: silver;
}
.popup-nested {
width: 5em;
height: 5em;
background: antiquewhite;
}
i {
display: inline-block;
border-radius: 50% 50% 0 50%;
border: 1px solid;
width: 1.5em;
height: 1.5em;
line-height: 1.5em;
text-align: center;
cursor: pointer;
}
i::after {
content: "i";
}
i.active {
background: rgba(255,255,255,.5);
}
<main>
<section class="top left">
<div><div class="inner">
<div>Smth<br>here</div>
</div></div>
</section>
<section class="top middle">
<div class="inner">
<div class="wide">
<i data-popup="outer" style="margin-left:10em"></i>
<i data-popup="outer" style="margin-left:10em"></i>
<i data-popup="outer" style="margin-left:10em"></i>
<i data-popup="outer" style="margin-left:10em"></i>
<i data-popup="outer" style="margin-left:10em"></i>
<i data-popup="outer" style="margin-left:10em"></i>
<i data-popup="outer" style="margin-left:10em"></i>
</div>
</div>
</section>
<section class="top right">
<div class="inner">Smth here</div></section>
<section class="bottom left">
<div class="inner">Smth here</div>
</section>
<section class="bottom middle">
<div class="inner">
<div class="wide"></div>
</div>
</section>
<section class="bottom right">
<div class="inner">Smth here</div>
</section>
<section class="middle visible-scroll">
<div class="inner">
<div class="scroller"></div>
</div>
</section>
<section class="middle popup-dest">
<div class="popup popup-outer" data-popup="outer" hidden>
<i data-popup="nested" style="margin-left:5em;margin-top:5em;"></i>
</div>
<div class="popup popup-nested" data-popup="nested" hidden>
</div>
</section>
</main>
关于javascript - 如何观察 DOM 元素位置变化,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/59792071/