Порядок выполнения кастомных модификаторов

Popper.js использует концепцию модификаторов для управления позиционированием элементов. Модификаторы представляют собой объекты с определёнными свойствами и функциями, которые изменяют поведение Popper на различных этапах жизненного цикла. Понимание порядка их выполнения критично для создания сложных кастомных сценариев позиционирования.


Жизненный цикл модификаторов

Каждый модификатор в Popper.js выполняется в рамках заранее определённых фаз:

  1. beforeRead – фаза подготовки. Модификаторы на этом этапе могут изменять начальные параметры, проверять условия, загружать данные о DOM.
  2. read – фаза чтения данных. На этом этапе модификаторы получают актуальные размеры, позиции и стили целевых элементов.
  3. afterRead – завершающий этап чтения. Используется для обработки и корректировки данных перед записью.
  4. beforeMain – подготовка к основной логике модификаторов. Здесь можно подготавливать вспомогательные структуры или вычислять дополнительные параметры.
  5. main – ключевая фаза изменения положения и расчётов. Большинство кастомных модификаторов реализуют именно функцию fn на этом этапе.
  6. afterMain – завершающий этап основной логики. Используется для корректировок, зависящих от изменений, внесённых в main.
  7. beforeWrite – подготовка к применению изменений в DOM. Модификаторы могут здесь изменить стили, атрибуты или классы.
  8. write – непосредственное внесение изменений в DOM. Основная фаза отрисовки.
  9. afterWrite – финальные корректировки после применения изменений.

Каждый модификатор указывает свою фазу через свойство phase. Попытка изменить позицию в фазе read не сработает, так как модификатор в read ещё не применяет вычисления к DOM.


Структура кастомного модификатора

Кастомный модификатор определяется как объект со следующими ключевыми свойствами:

const customModifier = {
  name: 'customModifier',   // уникальное имя модификатора
  enabled: true,            // включение/отключение
  phase: 'main',            // фаза выполнения
  requires: ['offset'],     // зависимости от других модификаторов
  fn({ state, options, name }) {
    // основная функция модификатора
  },
  effect({ state, options, name }) {
    // побочные эффекты (например, обработка событий)
    return () => {
      // функция очистки эффектов
    };
  }
};
  • name – используется для идентификации и связывания с другими модификаторами.
  • enabled – контролирует выполнение. Если false, модификатор пропускается.
  • phase – определяет момент выполнения.
  • requires и requiresIfExists – гарантируют, что необходимые модификаторы будут выполнены до текущего.
  • fn – основная функция, выполняющая вычисления и изменения состояния.
  • effect – используется для установки побочных эффектов и очистки ресурсов.

Влияние порядка модификаторов

Popper.js выполняет модификаторы в следующем порядке:

  1. Сортировка по phase согласно жизненному циклу (beforeRead, read, afterRead, beforeMain, main, afterMain, beforeWrite, write, afterWrite).

  2. Внутри каждой фазы модификаторы сортируются по dependencies:

    • Модификаторы, перечисленные в requires, выполняются до текущего.
    • Если зависимость не найдена, выбрасывается предупреждение.
  3. После сортировки модификаторы последовательно вызываются функцией fn или effect.

Пример зависимости: если модификатор flip зависит от preventOverflow, порядок гарантирует, что сначала корректировка границ произойдёт через preventOverflow, а затем flip может корректировать позицию.


Создание корректного кастомного модификатора

Для стабильной работы кастомного модификатора нужно соблюдать три правила:

  1. Указывать фазу, соответствующую выполняемой логике

    • Расчёт позиции → main
    • Изменение DOM → write
    • Сбор данных → read
  2. Явно указывать зависимости

    • Это предотвращает ошибки, когда ваш модификатор полагается на данные других модификаторов.
  3. Использовать state вместо прямого обращения к DOM

    • Все вычисления должны основываться на state.rects, state.modifiersData и state.elements.
    • Фактическое применение стилей или классов выполняется в фазе write.

Пример: модификатор смещения с зависимостью

const offsetModifier = {
  name: 'customOffset',
  enabled: true,
  phase: 'main',
  requires: ['computeStyles'],
  fn({ state, options }) {
    const offset = options.offset || 10;
    const popper = state.elements.popper;
    const reference = state.elements.reference;

    state.modifiersData.customOffset = {
      top: reference.offsetTop + offset,
      left: reference.offsetLeft + offset
    };

    // Popper сам применит координаты в фазе write
  }
};

В этом примере:

  • requires: ['computeStyles'] гарантирует, что стили уже рассчитаны.
  • Данные смещения сохраняются в state.modifiersData для последующей фазы write.
  • Не происходит прямого изменения DOM в fn, что соответствует архитектуре Popper.js.

Итоговая схема работы

  1. Сбор данных (read)
  2. Основные вычисления (main)
  3. Корректировка данных после вычислений (afterMain)
  4. Подготовка к записи в DOM (beforeWrite)
  5. Применение изменений (write)
  6. Финальная корректировка (afterWrite)

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