javascript - 如何观察 DOM 元素位置变化

标签 javascript performance scroll requestanimationframe intersection-observer

我需要观察一个 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/

    相关文章:

    css - 布局,其中许多容器需要占用剩余空间,但需要占用一个内部滚动条。 CSS

    javascript - 如何在 p5.js 中访问 PGraphics 像素

    python - 有效地获得 Pandas 指数的联合

    c# - 在 C# 中同步多行文本框位置

    CSS/页面加载速度

    javascript - 将文档和窗口引用存储在对象中以获得更好的性能

    javascript - JQuery 一页自动滚动菜单

    javascript - 无法从 Laravel 5 中的 PUBLIC 文件夹加载 AngularJs 文件

    javascript - jQuery .attr ('type' , 'submit' ) 在一个按钮元素上给我一个奇怪的 IE7 错误

    javascript - 在我的应用程序中为 HTML/CSS 编辑哪些文件 (ruby)