Сортировка двух-уровневых списков, используем jQuery UI

Как то попалась мне вот такая задачка. Нужно было сделать двух-уровневые фильтры для акций, потратил я конечно целый денёк на это дело и вот хочу поделиться с вами, что у меня получилось.

Структура таблицы весьма простая: id, name, parent, position. На выходе у нас должен получиться HTML код, имея следующую структуру:

<ul>
    <li>...</li>
    ...
    <li>
        <ul>
            <li></li>
            ...
        </ul>
    </li>
    ...
</ul>

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

И так пристутим к самому интересному. Для того, чтобы перетаскивать списки, вверх и вниз, добавим в каждый тег li один элемент с классом sort. Для фильтров первого уровня добавим к этому элементу ещё один класс lvl1, а для второго уровня lvl2. Для возможности перетаскивания из одного уровня в другой, необходимо добавить ещё один элемент с классом drag. В итоге у нас должно получиться:

<ul class="tags-list">
    <li class="droppable draggable">
        <span class="sort lvl1"></span>
        <span class="drag"></span>
        <span class="name">Европа</span>
    </li>
    <li class="droppable draggable">
        <span class="sort lvl1"></span>
        <span class="drag disable"></span>
        <span class="name">Москва</span>
        <ul class="tags-list1">
            <li class="droppable draggable">
                <span class="sort lvl2"></span>
                <span class="drag left"></span>
                <span class="name">Подмосковье</span>
            </li>
        </ul>
    </li>
    <li class="droppable draggable">
        <span class="sort lvl1"></span>
        <span class="drag"></span>
        <span class="name">Италия</span>
    </li>
</ul>

Добавляем функцию инициализации сортировки для одного списка, исключая вложенные. Тем самым у нас получиться, что сортировка будет накладываться на каждый список.

function sort(ul, handle) {
    $(ul).sortable({
        items:'li',
        axis:'y',
        handle: handle,
        cursor: 'move',
        update: function(event,ui) {
            //Send data to server
        }
    });
}

 Инициализируем все доступные списки после загрузки страницы:

$(document).ready(function(){
    //Инициализируем сортировку внутри списка
    sort('.tags-list', '.sort.lvl1');
    sort('.tags-list1', '.sort.lvl2');
});

Начинаем работу с перетаскиванием элементов между списками. Создадим функцию, которая будет добавлять перетаскиваемый элемент из первого уровня списка, во второй:

//Доабвление во второй уровень
function appendToRight(obj, droppable) {
    if( $(obj).parents(".tags-list1").length ) {
        obj = $(obj).parents(".tags-list1").parent();
    }
    //Если внутри списка нет, создаём его
    if( $(obj).find("ul").length == 0 ) {
        $(obj).append("");
        $(obj).find(".drag").addClass("disable");
    }
    var parent = droppable.parent();
    droppable.find('.drag').addClass("left");
    $(obj).find("ul").append(droppable);
    if( parent.find('li').length == 0 ) {
        parent.parent().find(".drag").removeClass("disable");
        parent.remove();
    }
    $(obj).find("ul .sort").removeClass("lvl1").addClass('lvl2');
    $(obj).removeClass("ui-state-active");

    //Инициализируем сортировку внутри списка
    sort($(obj).find('ul'), '.sort.lvl2');
}

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

<ul class="tags-root">
    <li class="droppable root">..</li>
</ul>

И добавим функцию, которая будет обрабатывать дроп на корневой элемент. Что делает данная фукнция, переносит в конец списка элемент и проверяет остались ли ещё элементы в предыдущем списке, если нет, то удаляет список.

//Добавление в корень
function appendToRoot(droppable) {
    var parent = droppable.parent();
    droppable.find('.drag.left').removeClass("left");
    $(droppable).find(".sort").removeClass("lvl2").addClass('lvl1');
    $('.tags-list').append(droppable);
    if( parent.find('li').length == 0 ) {
        parent.parent().find(".drag").removeClass("disable");
        parent.remove();
    }
}

Для этого пропишем действия нажатия и отпускания кнопки мыши, в которых мы будем добавлять / удалять специальный класс drag-drop, по которому мы будем определять действие, что юзер хочет перетащить элемент в другой список. Тут же инициализируем фукнции draggable и droppable. Вот, что у нас получилось:

$(".drag").mousedown(function(){
    $(this).parents("li").addClass("drag-drop");
});
$(".drag").mouseup(function(){
    var self = this;
    setTimeout(function(){
        $(self).parents("li").removeClass("drag-drop");
    }, 1)
});
$(".tags-list li.draggable").draggable({
    handle: ".drag",
    revert: true,
    start: function() {
        if( $(this).find("ul").length ) {
            return false;
        }
    }
});
$('.droppable').droppable({
    activeClass: "ui-state-hover",
    hoverClass: "ui-state-active",
    accept: ".drag-drop",
    drop: function( event, ui ) {
        setTimeout(function(){
            $('ul, li').removeClass("drag-drop ui-state-hover ui-state-active");
        },1);
        if( $(this).hasClass("root") ) {
            appendToRoot(ui.draggable);
        } else {
            appendToRight(this, ui.draggable);
        }
    }
});

 Вот такой способ. Возможно не совсем идеальный, и возможно кто-то напишет лучше, но почему то я не нашёл в просторах инета...

P.Nixx, 16.10.2012, 16:59