Отмена операций

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


Основные подходы к отмене операций

Отмена перемещения элементов в SortableJS не реализована «из коробки» через встроенные методы. Существуют три основных подхода к реализации отката:

  1. Хранение предыдущего состояния списка

    Перед началом перетаскивания необходимо сохранить текущее состояние элементов. Обычно это массив объектов или идентификаторов элементов в порядке их расположения.

    let previousOrder = [];
    
    const sortable = new Sortable(listElement, {
        onStart: () => {
            previousOrder = Array.from(listElement.children).map(el => el.id);
        }
    });

    Этот подход позволяет при необходимости восстановить порядок элементов:

    function undo() {
        previousOrder.forEach((id, index) => {
            const element = document.getElementById(id);
            listElement.appendChild(element);
        });
    }

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

  2. Использование событий onEnd и onAdd

    SortableJS предоставляет события onEnd (перетаскивание завершено) и onAdd (элемент добавлен в новый список). Для поддержки отмены в этих обработчиках можно сохранять информацию о старой позиции элемента и о том, из какого списка он был перемещен:

    const sortable = new Sortable(listElement, {
        group: "shared",
        onEnd: (evt) => {
            const { oldIndex, newIndex, item } = evt;
            previousOrder = { item, oldIndex, newIndex };
        }
    });
    
    function undoLastMove() {
        const { item, oldIndex, newIndex } = previousOrder;
        if (oldIndex !== undefined) {
            listElement.insertBefore(item, listElement.children[oldIndex]);
        }
    }

    Этот метод подходит для пошаговой отмены последних перемещений.

  3. Интеграция с системами хранения истории

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

    const history = [];
    
    const sortable = new Sortable(listElement, {
        onEnd: (evt) => {
            history.push({
                itemId: evt.item.id,
                from: evt.oldIndex,
                to: evt.newIndex
            });
        }
    });
    
    function undo() {
        const lastAction = history.pop();
        if (!lastAction) return;
        const item = document.getElementById(lastAction.itemId);
        listElement.insertBefore(item, listElement.children[lastAction.from]);
    }

    Этот подход облегчает реализацию нескольких уровней отмены операций.


Тонкости работы с группами и мультисписками

SortableJS позволяет объединять списки в группы (group), что увеличивает сложность при реализации отмены. Основные аспекты:

  • Перемещение между списками: нужно сохранять как исходный список, так и позицию элемента в нем.
  • Совместимость с pull и put: если элемент может быть перемещен в другой список, undo должен учитывать ограничения группы.
  • Удаление элементов: при перемещении между списками элемент физически удаляется из исходного, поэтому откат должен вставлять его обратно.

Пример реализации undo для мультисписочного сценария:

const history = [];

const lists = document.querySelectorAll('.sortable-list');
lists.forEach(list => {
    new Sortable(list, {
        group: 'shared',
        onEnd: evt => {
            history.push({
                itemId: evt.item.id,
                fromList: evt.from,
                toList: evt.to,
                oldIndex: evt.oldIndex,
                newIndex: evt.newIndex
            });
        }
    });
});

function undo() {
    const last = history.pop();
    if (!last) return;
    const item = document.getElementById(last.itemId);
    last.fromList.insertBefore(item, last.fromList.children[last.oldIndex]);
}

Сохранение состояния и синхронизация с сервером

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

  • Локальные копии состояния до отправки изменений.
  • Отмена серверных операций через API (например, повторная отправка старого состояния).
  • Комбинированный подход: сначала восстановление DOM, затем синхронизация с сервером.

Важно учитывать, что если список синхронизируется с сервером, простое изменение DOM без уведомления сервера может вызвать рассинхронизацию данных.


Важные рекомендации

  • Всегда сохранять уникальные идентификаторы элементов для точного восстановления.
  • Не полагаться только на индексы элементов, особенно если DOM может изменяться динамически.
  • Использовать стек истории для многоуровневой отмены.
  • При работе с группами всегда фиксировать исходный список и целевой список.
  • Тестировать undo в разных сценариях: одиночное перемещение, мультисписки, удаление и добавление элементов.

Эти методы позволяют создать надёжную и гибкую систему отмены операций, полностью совместимую с функционалом SortableJS и динамическими веб-интерфейсами.