简体   繁体   中英

How to observe DOM element position changes

I need to observe a DOM element position as I need to show a popup panel relative to it (but not in the same container) and the panel should follow the element. How I should implement such logic?

Here is a snippet where you can see the opening of outer and nested popup panels, but they do not follow the horizontal scroll. I want them both to follow it and keep showing near the corresponding icon (and it should be a generic approach that will work in any place). You may ignore that nested popup is not closed together with outer - it's just to make the snippet simpler. I expect no changes except the showPopup function. Markup is specially simplified for this example; do not try to change it - I need it as it is.

 ~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>

Now I have following ideas:

  • Listening to the scroll event on the capturing phase on body and getting the actual position of the element via getBoundingClientRect and the reposition panel according to the current location. I am currently using a similar solution, but there is an issue. When the element is moving by another script, it doesn't force panel repositioning. One of the cases - when the element itself is another panel - simple filtering of unrelated scroll events filters such scrolls out. Also I have some cases with debounce and they are difficult to handle too.

  • Create IntersectionObserver to track moves. The problem seems to be in the fact that it only works on intersection size changes, not on any moves. I have an idea to crop viewport by rootMargin to the same rectangle that the element covers, but as options are readonly. It means I would need to create new observer on each move. I'm not sure about the performance impact of such a solution. Also as it provides only an approximate position, so I think that I can't eliminate calls to getBoundingClientRect .

  • A hybrid solution as scrolls are usually taking some continuous time. Use the previous idea with IntersectionObserver , but when the first move is detected, just subscribe to requestAnimationFrame and check the element position there. While position differs, handle it and recursively use requestAnimationFrame . If the position is the same (I am not sure if one frame is enough, maybe in 5 frames?), stop subscribing requestAnimationFrame and create a new IntersectionObserver .

I'm afraid that such solutions will have issues with performance. Also they seem to me too complex. Maybe there is some known solution which I should use?

Implementation of the first approach. Just subscribe all scroll events across the document and update the position in the handler. You can't filter events by the parents of an src element as in case of a nested popup scrolling element is not presented in the events chain.

Also it doesn't work if the popup is moved programmatically - you may notice it when the outer popup is moved to the other icon and nested stays in the old place.

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)
  }
}

Full code:

 ~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>

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM