Dragula — это лёгкая библиотека для реализации drag-and-drop функционала на стороне клиента с минимальными усилиями по настройке. Основная цель библиотеки — упрощение перемещения элементов между контейнерами, при этом сохраняя возможность полного контроля над поведением перетаскивания.
Dragula работает по принципу drag source → drag target, где исходный контейнер и целевой контейнер определяются при инициализации. Библиотека автоматически обрабатывает визуальные эффекты перемещения и создаёт «призрачный» элемент для наглядного отображения перетаскивания.
Инициализация осуществляется через вызов функции dragula
с массивом контейнеров:
const containers = [document.getElementById('left'), document.getElementById('right')];
const drake = dragula(containers);
Параметры Dragula позволяют гибко настраивать поведение:
Пример настройки с фильтрацией элементов:
const drake = dragula(containers, {
moves: (el, source, handle, sibling) => !el.classList.contains('non-draggable'),
accepts: (el, target, source, sibling) => target.id !== 'restricted'
});
Dragula предоставляет ряд событий, позволяющих контролировать процесс перетаскивания:
drag — событие начала
перетаскивания.drop — срабатывает при успешном
завершении перетаскивания.cancel — вызывается при отмене
перетаскивания.remove — при удалении элемента из
контейнера.Пример подписки на события:
drake.on('drag', (el) => {
el.classList.add('is-dragging');
});
drake.on('drop', (el, target, source, sibling) => {
el.classList.remove('is-dragging');
console.log(`Элемент перемещён из ${source.id} в ${target.id}`);
});
При работе с динамическими интерфейсами часто возникает необходимость откатить перемещение элемента, если произошла ошибка на стороне сервера или нарушены бизнес-правила. Dragula не предоставляет встроенного механизма отката, поэтому его реализуют через сохранение исходного состояния элемента.
Перед началом перемещения важно сохранить исходное положение элемента:
let originalPositions = new Map();
drake.on('drag', (el, source) => {
originalPositions.set(el, {
parent: source,
nextSibling: el.nextElementSibling
});
});
Здесь используется объект originalPositions, который
хранит родительский контейнер и следующий
соседний элемент, что позволяет точно восстановить элемент на
прежнее место.
При срабатывании события drop можно выполнить проверку и
при необходимости вернуть элемент:
drake.on('drop', (el, target, source, sibling) => {
const isValid = validateDrop(el, target);
if (!isValid) {
const original = originalPositions.get(el);
if (original.nextSibling) {
original.parent.insertBefore(el, original.nextSibling);
} else {
original.parent.appendChild(el);
}
}
});
function validateDrop(el, target) {
// Проверка бизнес-правил
return target.id !== 'restricted';
}
Этот подход гарантирует, что элемент вернётся на прежнее место без визуальных сбоев даже при ошибке.
Если проверка выполняется на сервере, откат необходимо производить после получения ответа:
drake.on('drop', (el, target, source) => {
sendToServer(el, target)
.then(response => {
if (!response.ok) {
rollbackElement(el);
}
})
.catch(() => {
rollbackElement(el);
});
});
function rollbackElement(el) {
const original = originalPositions.get(el);
if (original.nextSibling) {
original.parent.insertBefore(el, original.nextSibling);
} else {
original.parent.appendChild(el);
}
}
Использование отдельной функции rollbackElement упрощает
повторное применение отката к нескольким элементам.
Для лучшего UX можно добавить анимацию возврата:
.drag-revert {
transition: transform 0.3s ease;
}
function rollbackElement(el) {
el.classList.add('drag-revert');
const original = originalPositions.get(el);
if (original.nextSibling) {
original.parent.insertBefore(el, original.nextSibling);
} else {
original.parent.appendChild(el);
}
el.addEventListener('transitionend', () => {
el.classList.remove('drag-revert');
}, { once: true });
}
Таким образом, визуальная анимация возврата делает откат плавным и предсказуемым, улучшая взаимодействие с интерфейсом.
Для сложных сценариев полезно хранить стек перемещений, что позволяет реализовать множественные откаты и повторные действия:
let moveHistory = [];
drake.on('drop', (el, target, source, sibling) => {
moveHistory.push({
element: el,
from: source,
to: target,
nextSibling: sibling
});
});
function undoLastMove() {
const lastMove = moveHistory.pop();
if (!lastMove) return;
if (lastMove.nextSibling) {
lastMove.from.insertBefore(lastMove.element, lastMove.nextSibling);
} else {
lastMove.from.appendChild(lastMove.element);
}
}
Такой подход позволяет создавать сложные интерфейсы с историей действий, аналогичные undo/redo в профессиональных приложениях.
Dragula хорошо сочетается с React, Vue и другими фреймворками, если использовать рефы контейнеров и управлять состоянием элементов через внутренние структуры данных. Основной принцип отката и контроля остается идентичным: хранение исходного состояния, проверка условий и восстановление при ошибках.