Библиотека Shopify Draggable предоставляет мощный и гибкий инструмент для реализации перетаскивания элементов в браузере с помощью JavaScript. Одной из ключевых особенностей является возможность откатывать изменения, которые были произведены пользователем в процессе перетаскивания или сортировки элементов. Это особенно полезно в интерфейсах, где требуется поддержка undo/redo, корректное управление состояниями списка или сетки элементов.
Для начала создаётся экземпляр 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 генерирует несколько событий, которые позволяют реализовать откат:
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 возвращает предыдущую конфигурацию.sortable:stop или drag:stop.Эти методы позволяют использовать Shopify Draggable не только для перетаскивания, но и для надежного контроля за состоянием интерфейса, с возможностью полностью откатить пользовательские изменения.