Откат изменений

Библиотека Shopify Draggable предоставляет мощный и гибкий инструмент для реализации перетаскивания элементов в браузере с помощью JavaScript. Одной из ключевых особенностей является возможность откатывать изменения, которые были произведены пользователем в процессе перетаскивания или сортировки элементов. Это особенно полезно в интерфейсах, где требуется поддержка undo/redo, корректное управление состояниями списка или сетки элементов.

Создание Draggable и настройка отката

Для начала создаётся экземпляр Draggable, который связывает контейнер и его элементы:

import { Draggable } from '@shopify/draggable';

const container = document.querySelector('.list');
const draggable = new Draggable(container, {
  draggable: '.list-item',
  mirror: {
    constrainDimensions: true
  }
});

Ключевые моменты:

  • draggable — CSS-селектор элементов, которые можно перетаскивать.
  • mirror — объект, отвечающий за отображение зеркального элемента при перетаскивании. Опция constrainDimensions фиксирует размер зеркала по отношению к оригиналу.

Чтобы реализовать откат изменений, важно хранить исходное состояние элементов. Обычно это массив, содержащий идентификаторы элементов в порядке их отображения:

const originalOrder = Array.from(container.children).map(el => el.dataset.id);

Отслеживание событий Draggable

Draggable генерирует несколько событий, которые позволяют реализовать откат:

  • drag:start — начинается перетаскивание.
  • drag:move — элемент перемещается.
  • drag:stop — завершение перетаскивания.
  • sortable:stop — завершение сортировки, момент, когда можно сохранить новое состояние или откатить изменения.

Для отката интерес представляет событие sortable:stop, которое позволяет сравнить новое состояние с исходным и при необходимости вернуть элементы на прежние позиции:

draggable.on('sortable:stop', (evt) => {
  const currentOrder = Array.from(container.children).map(el => el.dataset.id);
  if (!arraysEqual(originalOrder, currentOrder)) {
    rollbackChanges();
  }
});

function arraysEqual(a, b) {
  return a.length === b.length && a.every((val, index) => val === b[index]);
}

function rollbackChanges() {
  originalOrder.forEach((id, index) => {
    const element = container.querySelector(`[data-id="${id}"]`);
    container.appendChild(element);
  });
}

Пояснения:

  • Функция arraysEqual проверяет, изменился ли порядок элементов.
  • rollbackChanges возвращает каждый элемент на исходную позицию в DOM, тем самым реализуя откат изменений.

Работа с несколькими контейнерами

Draggable поддерживает drag-and-drop между несколькими контейнерами. В этом случае откат должен учитывать не только порядок внутри одного контейнера, но и перемещения между ними:

const containers = document.querySelectorAll('.list-container');
const draggable = new Draggable(containers, {
  draggable: '.list-item',
  swapAnimation: true
});

const originalStates = Array.from(containers).map(container => {
  return Array.from(container.children).map(el => el.dataset.id);
});

draggable.on('sortable:stop', (evt) => {
  const currentStates = Array.from(containers).map(container => {
    return Array.from(container.children).map(el => el.dataset.id);
  });

  if (!statesEqual(originalStates, currentStates)) {
    rollbackMultiContainer();
  }
});

function statesEqual(original, current) {
  return original.every((arr, i) => arraysEqual(arr, current[i]));
}

function rollbackMultiContainer() {
  originalStates.forEach((state, containerIndex) => {
    const container = containers[containerIndex];
    state.forEach(id => {
      const element = document.querySelector(`[data-id="${id}"]`);
      container.appendChild(element);
    });
  });
}

Поддержка анимаций при откате

Draggable позволяет включить анимацию при перемещении элементов с помощью опции swapAnimation:

const draggable = new Draggable(container, {
  draggable: '.list-item',
  swapAnimation: {
    duration: 300,
    easingFunction: 'ease-in-out'
  }
});

Это делает процесс отката более плавным, так как элементы визуально «скользят» на свои позиции, вместо резкого перескакивания.

Сохранение истории изменений

Для сложных интерфейсов часто используется стек изменений (history stack), чтобы реализовать несколько уровней отката:

const history = [];

draggable.on('sortable:stop', () => {
  const state = Array.from(container.children).map(el => el.dataset.id);
  history.push(state);
});

function undo() {
  if (history.length > 1) {
    history.pop();
    const previousState = history[history.length - 1];
    previousState.forEach(id => {
      const element = container.querySelector(`[data-id="${id}"]`);
      container.appendChild(element);
    });
  }
}

Особенности:

  • Первый элемент в стеке хранит исходное состояние.
  • При каждом изменении добавляется новый элемент в стек.
  • Функция undo возвращает предыдущую конфигурацию.

Итоговая структура для отката

  • Хранение исходного состояния — массив идентификаторов или объектов с данными элементов.
  • Отслеживание событий Draggable — ключевые события sortable:stop или drag:stop.
  • Сравнение текущего и исходного состояния — проверка на изменения.
  • Выполнение отката — возвращение элементов на прежние позиции с возможной анимацией.
  • Поддержка нескольких контейнеров — управление состоянием каждого контейнера отдельно.
  • История изменений — стек для нескольких уровней undo/redo.

Эти методы позволяют использовать Shopify Draggable не только для перетаскивания, но и для надежного контроля за состоянием интерфейса, с возможностью полностью откатить пользовательские изменения.