Как наблюдать изменения положения элемента DOM - PullRequest
2 голосов
/ 17 января 2020

Мне нужно наблюдать за положением элемента DOM, так как мне нужно показать всплывающую панель относительно него (но не в том же контейнере), и панель должна следовать за элементом. Как мне реализовать такую ​​логику c?

Вот фрагмент, где вы можете видеть открытие внешних и вложенных всплывающих панелей, но они не следуют горизонтальной прокрутке. Я хочу, чтобы они оба следовали ему и продолжали показываться рядом с соответствующим значком (и это должен быть обобщенный c подход, который будет работать в любом месте). Вы можете игнорировать то, что вложенное всплывающее окно не закрывается вместе с внешним - это просто для того, чтобы сделать фрагмент проще. Я не ожидаю никаких изменений, кроме функции 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 и панель репозиции в соответствии с текущим местоположением. В настоящее время я использую подобное решение, но есть проблема. Когда элемент перемещается другим скриптом, он не вызывает перестановку панели. Один из случаев - когда сам элемент является другой панелью - простая фильтрация несвязанных событий прокрутки отфильтровывает такие прокрутки. Также у меня есть некоторые случаи с debounce, и с ними тоже сложно работать.

  • Создать IntersectionObserver для отслеживания ходов. Кажется, проблема в том, что он работает только на изменениях размера пересечения, а не на любых движениях. У меня есть идея обрезать область просмотра по rootMargin до того же прямоугольника, который охватывает элемент, но в качестве опций доступны только для чтения. Это означает, что мне нужно будет создавать нового наблюдателя на каждом шаге. Я не уверен насчет производительности такого решения. Кроме того, поскольку он предоставляет только приблизительную позицию, я думаю, что не могу исключить вызовы на getBoundingClientRect.

  • Гибридное решение, поскольку прокрутки обычно занимают некоторое непрерывное время. Используйте предыдущую идею с IntersectionObserver, но когда обнаружен первый ход, просто подпишитесь на requestAnimationFrame и проверьте положение элемента там. В то время как положение отличается, обработайте его и рекурсивно используйте requestAnimationFrame. Если позиция одинакова (я не уверен, достаточно ли одного кадра, может быть, в 5 кадрах?), Прекратите подписку requestAnimationFrame и создайте новый IntersectionObserver.

I Боюсь, что такие решения будут иметь проблемы с производительностью. Также они кажутся мне слишком сложными. Может быть, есть какое-то известное решение, которое мне следует использовать?

1 Ответ

1 голос
/ 17 января 2020

Реализация первого подхода. Просто подпишите все 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>
...