Интерактивные интерфейсы с перетаскиванием содержат большое количество событий, состояний и взаимодействий с DOM. Библиотека Shopify Draggable реализует сложную систему обработки событий и внутренних модулей, что делает возможным расширение поведения через плагины, сенсоры и пользовательскую логику. Однако подобная архитектура неизбежно приводит к появлению конфликтов.
Конфликтом в контексте Draggable считается ситуация, при которой:
Корректное разрешение подобных конфликтов требует понимания архитектуры библиотеки, механизма событий и порядка их выполнения.
В Draggable ввод пользователя обрабатывается через сенсоры (Sensors). Наиболее распространённые:
В некоторых случаях несколько сенсоров могут реагировать на одно и то же действие. Например, на устройствах с сенсорным экраном и мышью.
Пример инициализации нескольких сенсоров:
import {Draggable, Sensors, MouseSensor, TouchSensor} from '@shopify/draggable';
const draggable = new Draggable(document.querySelectorAll('.container'), {
draggable: '.item',
sensors: [MouseSensor, TouchSensor]
});
На гибридных устройствах возможно одновременное срабатывание событий мыши и касания. Это может привести к:
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();
}
});
Во время операции drag библиотека активно изменяет DOM:
Если сторонний код также изменяет структуру DOM в этот момент, возникают проблемы:
Пример конфликтного сценария:
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 построен на собственной системе событий. Основные:
Каждое событие может быть перехвачено и отменено.
Если обработчик вызывает 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 использует систему плагинов. Некоторые из встроенных:
Каждый плагин вмешивается в жизненный цикл drag.
Например:
Если несколько плагинов одновременно пытаются управлять координатами, возникает нестабильность интерфейса.
При создании 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]
});
Минимизация количества активных плагинов снижает вероятность конфликтов.
Стили могут вмешиваться в механику перетаскивания.
position: relative или absoluteoverflow: hiddentransformz-indexОсобенно опасен transform, потому что он меняет систему
координат.
Пример проблемного стиля:
.container {
transform: scale(0.9);
}
После этого координаты мыши перестают совпадать с координатами элемента.
transform на контейнерах draggableoverflow родительских элементовВо время перетаскивания 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';
});
Фреймворки вроде React, Vue или Svelte используют виртуальный DOM. Они могут перерисовывать элементы во время drag.
Это приводит к:
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();
});
Событие вызывается десятки раз в секунду.
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 полезно проверять:
draggable.on('drag:move', (event) => {
console.log(event.sensorEvent.clientX);
});
Можно временно отключать плагины, чтобы определить источник проблемы.
Наиболее устойчивые системы перетаскивания строятся на следующих принципах:
1. Минимизация глобального состояния
Все состояния drag должны храниться внутри одного модуля.
2. Разделение ответственности
3. Изоляция DOM-операций
DOM должен изменяться только в заранее определённых точках жизненного цикла.
4. Предсказуемая последовательность событий
Каждый обработчик должен быть независимым и не полагаться на побочные эффекты других обработчиков.
Такая архитектура позволяет значительно уменьшить вероятность конфликтов и делает систему перетаскивания стабильной даже при сложных пользовательских сценариях.