Конфликты и их разрешение

Интерактивные интерфейсы с перетаскиванием содержат большое количество событий, состояний и взаимодействий с DOM. Библиотека Shopify Draggable реализует сложную систему обработки событий и внутренних модулей, что делает возможным расширение поведения через плагины, сенсоры и пользовательскую логику. Однако подобная архитектура неизбежно приводит к появлению конфликтов.

Конфликтом в контексте Draggable считается ситуация, при которой:

  • два обработчика событий пытаются изменить один и тот же элемент;
  • пользовательская логика вмешивается в стандартный жизненный цикл перетаскивания;
  • несколько сенсоров одновременно реагируют на ввод;
  • сторонние библиотеки изменяют DOM во время операции drag;
  • разные плагины пытаются управлять одинаковыми состояниями.

Корректное разрешение подобных конфликтов требует понимания архитектуры библиотеки, механизма событий и порядка их выполнения.


Конфликты сенсоров ввода

Одновременная работа нескольких сенсоров

В Draggable ввод пользователя обрабатывается через сенсоры (Sensors). Наиболее распространённые:

  • MouseSensor
  • TouchSensor
  • ForceTouchSensor
  • DragSensor

В некоторых случаях несколько сенсоров могут реагировать на одно и то же действие. Например, на устройствах с сенсорным экраном и мышью.

Пример инициализации нескольких сенсоров:

import {Draggable, Sensors, MouseSensor, TouchSensor} from '@shopify/draggable';

const draggable = new Draggable(document.querySelectorAll('.container'), {
  draggable: '.item',
  sensors: [MouseSensor, TouchSensor]
});

На гибридных устройствах возможно одновременное срабатывание событий мыши и касания. Это может привести к:

  • двойному запуску dragStart
  • конфликту координат
  • рассинхронизации состояний

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

1. Ограничение сенсоров

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

sensors: [TouchSensor]

2. Использование условий

Создание сенсоров динамически:

const sensors = 'ontouchstart' in window
  ? [TouchSensor]
  : [MouseSensor];

3. Отмена событий

В некоторых случаях можно отменять событие начала перетаскивания:

draggable.on('drag:start', (event) => {
  if (event.sensor.constructor.name === 'MouseSensor' && isTouchDevice()) {
    event.cancel();
  }
});

Конфликты DOM-изменений

Изменение структуры во время перетаскивания

Во время операции drag библиотека активно изменяет DOM:

  • создаёт зеркальный элемент (mirror)
  • перемещает элементы
  • обновляет контейнеры

Если сторонний код также изменяет структуру DOM в этот момент, возникают проблемы:

  • потеря ссылок на элементы
  • некорректные координаты
  • разрушение состояния draggable

Пример конфликтного сценария:

setInterval(() => {
  document.querySelector('.container').appendChild(createItem());
}, 1000);

Если новые элементы добавляются во время drag, индексы могут перестать соответствовать текущему состоянию.

Безопасная синхронизация

Изменения DOM должны выполняться вне активной операции перетаскивания.

Отслеживание состояния:

let isDragging = false;

draggable.on('drag:start', () => {
  isDragging = true;
});

draggable.on('drag:stop', () => {
  isDragging = false;
});

Контроль изменений:

function safeAppend(element) {
  if (!isDragging) {
    container.appendChild(element);
  }
}

Конфликты пользовательских обработчиков событий

Draggable построен на собственной системе событий. Основные:

  • drag:start
  • drag:move
  • drag:stop
  • sortable:start
  • sortable:sorted
  • sortable:stop

Каждое событие может быть перехвачено и отменено.

Прерывание стандартного поведения

Если обработчик вызывает event.cancel(), стандартная логика библиотеки не выполняется.

Пример:

draggable.on('drag:start', (event) => {
  if (!event.source.classList.contains('allowed')) {
    event.cancel();
  }
});

Если несколько обработчиков пытаются отменить или изменить событие, возникает конфликт.

Последовательность обработчиков

Draggable выполняет обработчики в порядке их регистрации.

draggable.on('drag:start', handlerA);
draggable.on('drag:start', handlerB);

Сначала выполняется handlerA, затем handlerB.

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

Изоляция логики

Разделение ответственности между обработчиками уменьшает вероятность конфликта.

Плохая практика:

draggable.on('drag:start', (event) => {
  event.source.style.opacity = 0.3;
  updateDatabase(event.source.id);
});

Лучше разделять:

draggable.on('drag:start', setDraggingStyle);
draggable.on('drag:stop', syncDatabase);

Конфликты между плагинами

Shopify Draggable использует систему плагинов. Некоторые из встроенных:

  • Mirror
  • Scrollable
  • SortAnimation
  • Collidable
  • Snappable

Каждый плагин вмешивается в жизненный цикл drag.

Конкурирующие изменения позиции

Например:

  • SortAnimation пытается анимировать элементы
  • Snappable изменяет позицию объекта
  • Collidable блокирует движение

Если несколько плагинов одновременно пытаются управлять координатами, возникает нестабильность интерфейса.

Управление плагинами

При создании Draggable можно отключить плагины:

const draggable = new Draggable(containers, {
  draggable: '.item',
  plugins: []
});

И подключать их вручную:

import {Mirror, Scrollable} from '@shopify/draggable';

const draggable = new Draggable(containers, {
  draggable: '.item',
  plugins: [Mirror, Scrollable]
});

Минимизация количества активных плагинов снижает вероятность конфликтов.


Конфликты CSS

Стили могут вмешиваться в механику перетаскивания.

Частые причины

  1. position: relative или absolute
  2. overflow: hidden
  3. transform
  4. z-index

Особенно опасен transform, потому что он меняет систему координат.

Пример проблемного стиля:

.container {
  transform: scale(0.9);
}

После этого координаты мыши перестают совпадать с координатами элемента.

Рекомендации

  • избегать transform на контейнерах draggable
  • использовать отдельный слой для анимаций
  • проверять overflow родительских элементов

Конфликты зеркального элемента (Mirror)

Во время перетаскивания Draggable создаёт зеркальный элемент.

Он:

  • копирует внешний вид оригинала
  • перемещается вместе с курсором
  • размещается в отдельном контейнере

Если пользовательский код также клонирует элементы, возможны конфликты.

Пример:

const clone = element.cloneNode(true);
document.body.appendChild(clone);

В итоге появляются два визуально одинаковых элемента.

Контроль зеркала

Настройка через события:

draggable.on('mirror:create', (event) => {
  event.mirror.classList.add('drag-mirror');
});

Удаление конфликтных эффектов:

draggable.on('mirror:created', (event) => {
  event.mirror.style.pointerEvents = 'none';
});

Конфликты с виртуальными DOM библиотеками

Фреймворки вроде React, Vue или Svelte используют виртуальный DOM. Они могут перерисовывать элементы во время drag.

Это приводит к:

  • уничтожению mirror
  • потере состояния draggable
  • скачкам интерфейса

Типичная проблема React

React может изменить DOM, если состояние компонента обновилось.

Пример:

setItems(newItems);

Если это происходит во время drag, Draggable теряет ссылку на элемент.

Стратегия решения

Заморозка обновлений во время drag.

let dragging = false;

draggable.on('drag:start', () => {
  dragging = true;
});

draggable.on('drag:stop', () => {
  dragging = false;
});

Контроль обновлений:

if (!dragging) {
  setItems(data);
}

Конфликты контейнеров

Draggable может работать с несколькими контейнерами.

const draggable = new Draggable(document.querySelectorAll('.column'), {
  draggable: '.card'
});

Проблемы возникают когда:

  • контейнеры динамически удаляются
  • контейнеры пересекаются
  • элементы имеют одинаковые селекторы

Проверка контейнеров

Контейнеры должны быть стабильными во время drag.

Плохой пример:

container.remove();

Во время операции drag это приведёт к ошибкам.

Безопасное удаление

Удалять контейнеры после завершения операции:

draggable.on('drag:stop', () => {
  container.remove();
});

Конфликты производительности

Если обработчик drag:move содержит тяжёлую логику, интерфейс начинает тормозить.

Пример плохой практики:

draggable.on('drag:move', () => {
  heavyCalculation();
});

Событие вызывается десятки раз в секунду.

Использование throttling

function throttle(fn, delay) {
  let last = 0;

  return function(...args) {
    const now = Date.now();

    if (now - last > delay) {
      last = now;
      fn(...args);
    }
  };
}

Применение:

draggable.on('drag:move', throttle(handleMove, 50));

Отладка конфликтов

Логирование событий

Draggable позволяет отслеживать практически весь жизненный цикл.

draggable.on('drag:start', console.log);
draggable.on('drag:move', console.log);
draggable.on('drag:stop', console.log);

Это помогает понять порядок выполнения.

Инспекция элементов

Во время drag полезно проверять:

  • mirror
  • source
  • container
  • координаты
draggable.on('drag:move', (event) => {
  console.log(event.sensorEvent.clientX);
});

Проверка плагинов

Можно временно отключать плагины, чтобы определить источник проблемы.


Архитектурные подходы предотвращения конфликтов

Наиболее устойчивые системы перетаскивания строятся на следующих принципах:

1. Минимизация глобального состояния

Все состояния drag должны храниться внутри одного модуля.

2. Разделение ответственности

  • обработка UI
  • бизнес-логика
  • синхронизация данных

3. Изоляция DOM-операций

DOM должен изменяться только в заранее определённых точках жизненного цикла.

4. Предсказуемая последовательность событий

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

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