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

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

Структура вложенного списка

Вложенный список обычно строится с использованием стандартной HTML-разметки <ul> и <li>:

<ul id="parent-list">
    <li>Элемент 1</li>
    <li>Элемент 2
        <ul>
            <li>Подэлемент 2.1</li>
            <li>Подэлемент 2.2</li>
        </ul>
    </li>
    <li>Элемент 3</li>
</ul>

Каждый <ul> может быть инициализирован как отдельный контейнер SortableJS, что позволяет создавать независимые зоны перетаскивания для подэлементов.

Инициализация SortableJS для вложенных списков

Для реализации вложенного списка необходимо создать основной Sortable для верхнего уровня и дополнительные экземпляры для каждого вложенного <ul>:

const parentList = document.getElementById('parent-list');

Sortable.create(parentList, {
    group: 'nested',
    animation: 150,
    fallbackOnBody: true,
    swapThreshold: 0.65
});

parentList.querySelectorAll('ul').forEach(subList => {
    Sortable.create(subList, {
        group: 'nested',
        animation: 150,
        fallbackOnBody: true,
        swapThreshold: 0.65
    });
});

Ключевые параметры:

  • group — позволяет объединять списки в одну группу для поддержки перемещения элементов между уровнями.
  • animation — задаёт плавность анимации при перемещении.
  • fallbackOnBody — предотвращает проблемы с позиционированием в сложных контейнерах.
  • swapThreshold — управляет чувствительностью перетаскивания при смене позиции элемента.

Динамическая работа с вложенными списками

Часто требуется создавать или удалять вложенные списки динамически. В таких случаях важно повторно инициализировать SortableJS на вновь добавленных <ul>:

function addSubListItem(parentLi, text) {
    let subList = parentLi.querySelector('ul');
    if (!subList) {
        subList = document.createElement('ul');
        parentLi.appendChild(subList);
        Sortable.create(subList, {
            group: 'nested',
            animation: 150
        });
    }
    const newItem = document.createElement('li');
    newItem.textContent = text;
    subList.appendChild(newItem);
}

Это гарантирует, что каждый новый подсписок получает те же возможности перетаскивания, что и статично определённые списки.

Ограничение уровней вложенности

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

Sortable.create(parentList, {
    group: 'nested',
    animation: 150,
    onMove: function (evt) {
        let depth = 0;
        let current = evt.to;
        while (current && current !== parentList) {
            if (current.tagName === 'UL') depth++;
            current = current.parentNode;
        }
        return depth <= 2; // ограничение глубины вложенности до 2 уровней
    }
});

События и управление данными

SortableJS предоставляет множество событий, полезных для вложенных списков:

  • onEnd — позволяет обновить порядок элементов после завершения перемещения.
  • onAdd и onRemove — помогают синхронизировать данные при переносе элементов между списками.
  • onSort — срабатывает при каждом изменении порядка внутри контейнера.

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

parentList.addEventListener('sortupdate', function(evt) {
    const movedItem = evt.item;
    const newParent = movedItem.parentNode.closest('li');
    const parentId = newParent ? newParent.dataset.id : null;
    console.log('Элемент перемещён. Новый родитель ID:', parentId);
});

Поддержка drag-and-drop между уровнями

С помощью свойства group можно организовать перемещение элементов между уровнями вложенности. Значение group может быть объектом:

Sortable.create(parentList, {
    group: {
        name: 'nested',
        pull: true,
        put: true
    },
    animation: 150
});
  • pull: true — элемент можно перемещать из этого списка.
  • put: true — элемент можно помещать в этот список.

Такой подход позволяет создавать гибкие интерфейсы с перемещением элементов из верхнего уровня в подсписки и наоборот.

Визуальные подсказки при перемещении

Для улучшения восприятия можно использовать класс ghostClass, который применяется к перетаскиваемому элементу:

Sortable.create(parentList, {
    group: 'nested',
    animation: 150,
    ghostClass: 'sortable-ghost'
});

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

Сортировка с вложенными данными

Для сохранения состояния вложенных списков удобно использовать рекурсивное преобразование DOM в JSON:

function serializeList(ul) {
    return Array.from(ul.children).map(li => {
        const childUl = li.querySelector('ul');
        return {
            text: li.firstChild.textContent.trim(),
            children: childUl ? serializeList(childUl) : []
        };
    });
}

const data = serializeList(parentList);
console.log(JSON.stringify(data, null, 2));

Такой подход позволяет хранить структуру для последующей обработки на сервере или в приложении.


Вложенные списки в SortableJS дают возможность создавать сложные иерархические интерфейсы с контролируемым уровнем вложенности, поддержкой динамических изменений и сохранением структуры данных. Использование событий и параметров контейнера позволяет гибко настраивать логику перемещения элементов и обеспечивать удобный пользовательский опыт.