Вложенные списки

В Shopify Draggable вложенные списки позволяют реализовать сложные интерфейсы перетаскивания с иерархической структурой элементов. Каждый элемент списка может содержать дочерние элементы, которые сами являются draggable-элементами. Такая структура особенно полезна для управления категориями, меню и деревьями объектов в приложениях e-commerce.

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

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

const sortable = new Sortable(document.querySelectorAll('.nested-list'), {
  draggable: '.draggable-item',
  swapAnimation: {
    duration: 200,
    easingFunction: 'ease-in-out',
  },
  handle: '.drag-handle',
});

Здесь:

  • .nested-list — контейнер верхнего уровня, который может содержать другие списки;
  • .draggable-item — элемент, который можно перемещать;
  • handle — элемент, за который можно “хватать” объект; без handle весь элемент перетаскивается при клике на любую его часть;
  • swapAnimation отвечает за плавное перемещение элементов.

Организация структуры DOM для вложенных списков

Правильная структура DOM — ключ к корректной работе вложенных списков. Обычно используется вложение

    и
  • :

    • Пункт 1
      • Подпункт 1.1
      • Подпункт 1.2
    • Пункт 2

    Важно, чтобы все вложенные элементы имели одинаковый класс для draggable (.draggable-item) и были частью родительского контейнера (.nested-list). Draggable автоматически определяет дочерние элементы и позволяет им быть sortable внутри своих родительских списков.

    Конфигурация вложенной сортировки

    Draggable поддерживает сортировку внутри контейнера и между контейнерами. Для вложенных списков можно задать несколько уровней, используя containers:

    const nestedSortable = new Sortable(document.querySelectorAll('.nested-list'), {
      draggable: '.draggable-item',
      handle: '.drag-handle',
      swapAnimation: {
        duration: 200,
        easingFunction: 'ease-in-out',
      },
      mirror: {
        constrainDimensions: true,
      },
      containers: Array.from(document.querySelectorAll('.nested-list')),
    });

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

    Mirror и визуализация перетаскивания

    Элемент mirror — это копия перетаскиваемого объекта, которая следует за курсором. Для вложенных списков важно правильно настроить mirror, чтобы избежать визуальных сбоев при перемещении дочерних элементов:

    mirror: {
      constrainDimensions: true, // сохраняет размер оригинала
      appendTo: 'body',          // добавляет mirror в body, чтобы не ломать структуру DOM
    },

    Дополнительно можно использовать CSS для mirror:

    .draggable-mirror {
      box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
      border-radius: 4px;
      background-color: #fff;
    }

    Обработка событий

    Draggable предоставляет события для управления логикой при перетаскивании элементов. Для вложенных списков наиболее полезны:

    • sortable:stop — срабатывает после завершения перемещения элемента;
    • sortable:sort — срабатывает при каждом изменении позиции;
    • sortable:swap — когда элементы меняются местами.

    Пример привязки событий:

    nestedSortable.on('sortable:stop', (event) => {
      const item = event.data.dragEvent.source;
      const parent = item.parentElement;
      console.log('Элемент перемещен:', item.textContent, 'в родителя:', parent);
    });

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

    Ограничения и особенности

    1. Необходимость уникальных контейнеров: Каждый уровень вложенности должен быть отдельным контейнером для корректного отслеживания перемещений.
    2. Производительность: Большие и сильно вложенные списки могут снижать производительность. Рекомендуется оптимизировать DOM и ограничить количество одновременно draggable элементов.
    3. Анимации: Swap-анимация облегчает восприятие перемещения, особенно в сложных деревьях.
    4. Ручки перетаскивания: Использование handle предотвращает случайное перемещение родительских элементов при попытке перемещать дочерние.

    Пример полного вложенного списка с drag handle

    • Пункт 1
      • Подпункт 1.1
      • Подпункт 1.2
    • Пункт 2
    const nestedSortable = new Sortable(document.querySelectorAll('.nested-list'), {
      draggable: '.draggable-item',
      handle: '.drag-handle',
      swapAnimation: {duration: 200, easingFunction: 'ease-in-out'},
      mirror: {constrainDimensions: true, appendTo: 'body'},
      containers: Array.from(document.querySelectorAll('.nested-list')),
    });

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