Библиотека Interact.js предоставляет удобный способ реализации интерактивных элементов на веб-странице, включая возможность создавать сортируемые списки. Основная идея заключается в том, чтобы сделать элементы списка перетаскиваемыми, а затем отслеживать их позицию для обновления порядка.
Для начала необходимо подключить Interact.js к проекту:
После подключения можно инициализировать элементы списка как draggable и настроить контейнер для их сортировки.
interact('.sortable-item')
.draggable({
inertia: true,
autoScroll: true,
onmove: dragMoveListener,
onend: dragEndListener
});
function dragMoveListener(event) {
const target = event.target;
const x = (parseFloat(target.getAttribute('data-x')) || 0) + event.dx;
const y = (parseFloat(target.getAttribute('data-y')) || 0) + event.dy;
target.style.transform = `translate(${x}px, ${y}px)`;
target.setAttribute('data-x', x);
target.setAttribute('data-y', y);
}
function dragEndListener(event) {
// Логика перестановки элементов после завершения перетаскивания
}
Контейнер, в котором находятся элементы списка, должен отслеживать события dropzone. Это позволяет корректно размещать элементы в новых позициях при перетаскивании.
interact('.sortable-container').dropzone({
accept: '.sortable-item',
overlap: 0.5,
ondrop: function (event) {
const dragged = event.relatedTarget;
const container = event.target;
container.insertBefore(dragged, container.children[event.dragEvent.targetIndex]);
}
});
Ключевой момент: overlap: 0.5
гарантирует, что элемент будет считаться “брошенным” в новую позицию
только после пересечения половины его площади с другой целью.
После каждого перемещения важно обновлять порядок элементов в DOM и,
при необходимости, синхронизировать его с данными на сервере. Например,
с использованием атрибута data-index:
function updateIndexes(container) {
Array.from(container.children).forEach((child, index) => {
child.setAttribute('data-index', index);
});
}
interact('.sortable-item').on('dragend', function (event) {
updateIndexes(event.target.parentNode);
});
Это обеспечивает корректное хранение последовательности элементов, независимо от того, сколько раз их перетаскивали.
Interact.js позволяет ограничивать движение элементов по оси X, Y или полностью внутри родительского контейнера:
interact('.sortable-item').draggable({
modifiers: [
interact.modifiers.restrictRect({
restriction: 'parent',
endOnly: true
}),
interact.modifiers.snap({
targets: [interact.snappers.grid({ x: 50, y: 50 })],
range: Infinity,
relativePoints: [{ x: 0, y: 0 }]
})
]
});
restrictRect предотвращает выход элементов за пределы
контейнера.snap позволяет реализовать сетки или
фиксированные позиции для элементов, что особенно удобно для
упорядоченных списков.Для визуального удобства рекомендуется использовать
CSS-переходы вместе с transform.
Например:
.sortable-item {
transition: transform 0.2s ease;
cursor: grab;
}
.sortable-item:active {
cursor: grabbing;
}
Это делает перемещение элементов плавным и естественным, улучшая пользовательский опыт.
Для реализации вложенных сортируемых списков Interact.js поддерживает nested dropzones. Каждому уровню списка можно назначить отдельный dropzone:
interact('.nested-container').dropzone({
accept: '.sortable-item',
overlap: 0.5,
ondropactivate: function (event) {
event.target.classList.add('active-dropzone');
},
ondropdeactivate: function (event) {
event.target.classList.remove('active-dropzone');
}
});
Такой подход позволяет реализовать перетаскивание между уровнями, при этом сохраняя точный порядок элементов в каждой ветке.
requestAnimationFrame внутри
onmove для плавного обновления позиции:function dragMoveListener(event) {
window.requestAnimationFrame(() => {
const target = event.target;
const x = (parseFloat(target.getAttribute('data-x')) || 0) + event.dx;
const y = (parseFloat(target.getAttribute('data-y')) || 0) + event.dy;
target.style.transform = `translate(${x}px, ${y}px)`;
target.setAttribute('data-x', x);
target.setAttribute('data-y', y);
});
}
dragmove для
избежания лагов при больших списках.Для React, Vue или Svelte сортируемые списки можно интегрировать с
управлением состояния. После окончания перетаскивания достаточно
обновлять массив элементов в состоянии, используя
data-index или уникальные идентификаторы:
onDragEnd(event) {
const newIndex = parseInt(event.target.getAttribute('data-index'));
const oldIndex = this.state.items.findIndex(i => i.id === event.target.id);
const items = [...this.state.items];
const [movedItem] = items.splice(oldIndex, 1);
items.splice(newIndex, 0, movedItem);
this.setState({ items });
}
Это позволяет синхронизировать визуальный порядок с внутренними данными приложения.