Сортируемые списки

Библиотека 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);
  });
}
  • Минимизировать DOM-операции внутри 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 });
}

Это позволяет синхронизировать визуальный порядок с внутренними данными приложения.