Координация нескольких Popper

Popper.js — это библиотека для управления позиционированием элементов, таких как всплывающие подсказки, тултипы и dropdown-меню, с учётом ограничений окна браузера. Когда речь идёт о нескольких Popper-элементах на одной странице, возникает задача синхронизации их поведения, предотвращения перекрытия и оптимизации производительности.


Создание нескольких Popper-экземпляров

Каждый Popper создаётся через конструктор createPopper(reference, popper, options), где:

  • reference — элемент, к которому привязан Popper.
  • popper — сам всплывающий элемент.
  • options — конфигурация позиции, стратегия рендеринга и модификаторы.
const button1 = document.querySelector('#button1');
const tooltip1 = document.querySelector('#tooltip1');

const popper1 = Popper.createPopper(button1, tooltip1, {
  placement: 'top',
  modifiers: [{ name: 'offset', options: { offset: [0, 8] } }],
});

const button2 = document.querySelector('#button2');
const tooltip2 = document.querySelector('#tooltip2');

const popper2 = Popper.createPopper(button2, tooltip2, {
  placement: 'bottom',
  modifiers: [{ name: 'offset', options: { offset: [0, 8] } }],
});

Каждый Popper работает независимо, но при наличии нескольких элементов требуется учитывать потенциальные конфликты в позиции и перекрытие контента.


Общие модификаторы для координации

Для управления несколькими Popper-элементами стоит использовать следующие модификаторы:

  1. offset — смещает Popper относительно reference.
  2. preventOverflow — предотвращает выход за границы контейнера или окна.
  3. flip — автоматически изменяет позицию, если Popper выходит за границы.
  4. computeStyles — управляет CSS-трансформациями для точного позиционирования.
  5. eventListeners — отслеживает скролл и ресайз, чтобы обновлять позиции.

Для нескольких Popper важно объединять их модификаторы так, чтобы они не противоречили друг другу.


Обновление и синхронизация нескольких Popper

Если на странице несколько Popper, обновление их можно выполнять централизованно:

const poppers = [popper1, popper2];

function updateAllPoppers() {
  poppers.forEach(p => p.update());
}

window.addEventListener('resize', updateAllPoppers);
window.addEventListener('scroll', updateAllPoppers);

Метод update() пересчитывает позиции с учётом модификаторов. При динамическом изменении DOM это помогает избежать накладывающихся подсказок.


Стратегии предотвращения перекрытия

  1. Раздельные контейнеры Каждый Popper помещается в отдельный контейнер div, чтобы избежать пересечения с другими Popper.

  2. Использование модификатора flip с границами

const popper = Popper.createPopper(reference, tooltip, {
  placement: 'top',
  modifiers: [
    {
      name: 'flip',
      options: {
        fallbackPlacements: ['bottom', 'right', 'left'],
        boundary: document.body
      }
    }
  ]
});
  1. Динамическое смещение При появлении нового Popper можно смещать уже существующие:
const dynamicOffset = (index) => [0, 8 + index * 40];

poppers.forEach((p, i) => {
  p.setOptions({
    modifiers: [{ name: 'offset', options: { offset: dynamicOffset(i) } }]
  });
});

Группировка Popper-элементов

Иногда необходимо, чтобы Popper-элементы двигались согласованно, например, список всплывающих подсказок. Для этого используют подход «группы»:

  1. Создаётся объект-группа:
const popperGroup = [];

popperGroup.push(popper1, popper2, popper3);
  1. Централизованно обновляется положение всех Popper:
function updateGroup() {
  popperGroup.forEach(p => p.update());
}
  1. Можно добавлять приоритетное смещение, чтобы верхние Popper не перекрывались нижними:
popperGroup.forEach((p, index) => {
  p.setOptions({
    modifiers: [{ name: 'offset', options: { offset: [0, index * 10] } }]
  });
});

Анимация и трансформации при множественных Popper

Использование computeStyles с gpuAcceleration: true позволяет плавно анимировать несколько Popper без падений производительности:

modifiers: [
  { name: 'computeStyles', options: { gpuAcceleration: true } }
]

Важно учитывать, что каждый Popper генерирует отдельные transform и top/left, поэтому при наложении анимации нужно синхронизировать длительность и задержки, чтобы не было дерганий.


Оптимизация производительности

При множественных Popper:

  • Объединять обработчики событий scroll и resize для всех Popper.
  • Использовать passive: true для событий прокрутки.
  • Динамически удалять Popper из DOM, если он скрыт, и заново создавать при необходимости.
  • Минимизировать количество модификаторов для каждого Popper, чтобы уменьшить вычисления.

Пример комплексного подхода

const buttons = document.querySelectorAll('.button');
const tooltips = document.querySelectorAll('.tooltip');
const poppers = [];

buttons.forEach((btn, index) => {
  const popper = Popper.createPopper(btn, tooltips[index], {
    placement: 'top',
    modifiers: [
      { name: 'offset', options: { offset: [0, index * 10] } },
      { name: 'flip', options: { fallbackPlacements: ['bottom'] } },
      { name: 'preventOverflow', options: { padding: 8 } },
    ]
  });
  poppers.push(popper);
});

function updateAll() {
  poppers.forEach(p => p.update());
}

window.addEventListener('resize', updateAll);
window.addEventListener('scroll', updateAll);

Такой подход обеспечивает координированное позиционирование и предотвращает перекрытия, одновременно поддерживая плавность анимаций и корректное поведение при скролле и изменении размеров окна.