Откат изменений при ошибках

Dragula — это лёгкая библиотека для реализации drag-and-drop функционала на стороне клиента с минимальными усилиями по настройке. Основная цель библиотеки — упрощение перемещения элементов между контейнерами, при этом сохраняя возможность полного контроля над поведением перетаскивания.

Dragula работает по принципу drag source → drag target, где исходный контейнер и целевой контейнер определяются при инициализации. Библиотека автоматически обрабатывает визуальные эффекты перемещения и создаёт «призрачный» элемент для наглядного отображения перетаскивания.

Инициализация Dragula

Инициализация осуществляется через вызов функции dragula с массивом контейнеров:

const containers = [document.getElementById('left'), document.getElementById('right')];
const drake = dragula(containers);

Параметры Dragula позволяют гибко настраивать поведение:

  • moves — функция, определяющая, какие элементы могут быть перетаскиваемыми.
  • accepts — функция, определяющая допустимые контейнеры для перемещения конкретного элемента.
  • copy — булевый флаг или функция, определяющая, будет ли элемент копироваться вместо перемещения.

Пример настройки с фильтрацией элементов:

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 и другими фреймворками, если использовать рефы контейнеров и управлять состоянием элементов через внутренние структуры данных. Основной принцип отката и контроля остается идентичным: хранение исходного состояния, проверка условий и восстановление при ошибках.