Развиваем дерево категорий jstree, реализуем перемещение на клиенте и сервере

июнь 12 , 2016
Метки:
Предыдущая статья Демо Исходники

Дерево категорий на javascriptНедавно в одной из статей я рассмотрел интересный плагин для построения дерева категорий - jstree. В посте мельком была затронута тема перемещения категорий в дереве мышкой, методом drag-and-drop. На клиентской стороне это делается очень просто - добавлением одного поля в параметрах инициализации плагина. Но чтобы структура дерева не потерялась после обновления страницы, нужно зафиксировать эти изменения в базе данных. Для этого придется написать немного php-скриптов и составить парочку sql-запросов. Как это сделать, рассказывается в статье ниже.


Что нам нужно сделать?

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

Ссылка на демо приложения

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


Идея, как будем реализовывать

Нам нужно включить drag-and-drop на клиенте, понять, какие данные нужно собрать и как их отправить на сервер, а также, какие запросы написать на php/mysql, чтобы все заработало.

Если хорошенько подумать, для того, чтобы перенести одну категорию в любое место дерева, нам нужно всего лишь 5 штук данных: id категории, 2 id родительских категорий (из какой перемещаем и в какую) и порядковые номера категории в старом и новом месте. Если звучит пока запутанно, не переживайте, дальше по коду будет яснее. Приступаем


Пишем клиентскую часть

Сначала самое простое - включаем drag-and-drop. Для этого в настройках плагина добавляем один объект - plugins: ['dnd']. Все! В браузере мы можем перемещать категории по всему дереву как нам вздумается. То есть, в итоге имеем такой код (подробно разбирается в первой части статьи)

    // Инициализация дерева категорий с помощью jstree
    function _initTree(data) {
        var category;
        ui.$categories.jstree({
            core: {
                check_callback: true,
                multiple: false,
                data: data
            },
            plugins: ['dnd']
        }).bind('changed.jstree', function(e, data) {
            category = data.node.text;
            ui.$goods.html('Товары из категории ' + category);
            console.log('changed node: ', data);
        });
    }

Теперь напишем код для перемещения категорий, для этого добавим еще один bind - move_node.jstree

    ui.$categories.jstree({
        // инициализация дерева
    }).bind('changed.jstree', function(e, data) {
        // код для выбора элемента дерева
    }).bind('move_node.jstree', function(e, data) {
        var params = {
            id: +data.node.id,
            old_parent: +data.old_parent,
            new_parent: +data.parent,
            old_position: +data.old_position,
            new_position: +data.position
        };
        _moveCategory(params);
        console.log('move_node params', params);
    });

Как видим, здесь собраны в один объект params 5 вышеозначенных параметров. Их названия почти совпадают с полями из объекта node библиотеки jstree, дабы не сильно путаться. Посмотрите на структуру дерева в таблице mysql, подвигайте мышкой категории в нашем дереве и наблюдайте, какие параметры выкидываются в консоль. Единственное отличие в нумерации состоит в том, что в базе мы используем нумерацию от 1, так по-человечески удобнее, а в плагине jstree от нуля, так удобнее по-программистски. В серверном коде мы будем учитывать эту разницу.

А теперь напишем несложный код функции _moveCategory(), который отправит данные на сервер и примет с него ответ.

    // Перемещение категории
    function _moveCategory(params) {
        var data = $.extend(params, {
            action: 'move_category'
        });

        $.ajax({
            url: ajaxUrl,
            data: data,
            dataType: 'json',
            success: function(resp) {
                if (resp.code === 'success') {
                    console.log('category moved');
                } else {
                    console.error('Ошибка получения данных с сервера: ', resp.message);
                }
            },
            error: function(error) {
                console.error('Ошибка: ', error);
            }
        });
    }

Здесь мы расширяем объект с данными полем action. Далее задаем стандартные параметры $.ajax и спокойно отправляем все собранное добро на сервер. Для обработки успешного и ошибочного ответа по уже старой привычке юзаем console.log/error(). А теперь идем на серверную сторону.


Как переместить категории на php/mysql

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

    // Получаем данные из массива GET
    $action = $_GET['action'];
    switch ($action) {
        // Получаем дерево категорий
        case 'get_categories':
            $result = getCategories($conn);
            break;

        // Перемещаем категорию
        case 'move_category':
            $result = moveCategory($conn, $_GET);
            break;

        // Действие по умолчанию, ничего не делаем
        default:
            $result = 'unknown action';
            break;
    }

Весь код взят из первой статьи, мы лишь расширили его вызовом функции moveCategory(). Вот ее код:

    // Перемещение категории
    function moveCategory($db, $params) {
        $categoryId = (int)$params['id'];
        $oldParentId = (int)$params['old_parent'];
        $newParentId = (int)$params['new_parent'];
        $oldPosition = (int)$params['old_position'] + 1;
        $newPosition = (int)$params['new_position'] + 1;
    
        excludePosition($db, $oldParentId, $oldPosition);
        includePosition($db, $categoryId, $newParentId, $newPosition);
    
        return json_encode(array(
            'code' => 'success'
        ));
    }

Выглядит все довольно просто: мы вытаскиваем 5 переменных из входящих параметров (данные из массива $_GET), выполняем 2 функции excludePosition и includePosition, которые и совершают все манипуляции с базой, и возвращаем успешный ответ клиенту.

Еще раз подробнее про 5 переменных:

  • 1. $categoryId - это id той самой категории, которую мы перемещаем по дереву, назовем ее основной
  • 2. $oldParentId - id категории, из которой мы нашу основную категорию вытаскиваем - родительская категория
  • 3. $newParentId - новое место жительства основной категории, новый родитель
  • 4. $oldPosition - какой порядковый номер был у основной категории на старом месте
  • 5. $newPosition - какой порядковый номер будет у основной категории на новом месте

Если мы не вытаскиваем основную категорию в новое место, а просто меняем ее местами с соседом, то $oldParentId и $newParentId будут совпадать. Когда берем $oldPosition и $newPosition, не забываем, что в базе нумерация идет с единички, поэтому ставим +1

Теперь самое интересное - функции excludePosition и includePosition.

excludePosition должна удалить основную категорию из ее старого родителя. Для это не требуется физически удалять запись из таблицы, нужно всего лишь пересчитать поля number для оставшихся категорий, чтобы не возникло "пробелов". Например, есть категория Смартфоны, в которой находятся iPhone, Samsung, LG и Vertu. Их порядковые номера в базе соответственно 1, 2, 3 и 4. Мы переносим Samsung куда-то в другое место. После этого в категории Смартфоны должны 3 подкатегории: iPhone, LG и Vertu - с номерами 1, 2 и 3. То есть у всех последующих за основной категорией мы должны уменьшить поле number на 1. В коде это реализуется так:

    // Исключение категории по ее родителю и позиции number
    function excludePosition($db, $parentId, $position) {
        $query = "update categories set number = number - 1 where parent_id = $parentId and number > $position";
        $db->query($query);
    }

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

    // Вставка категории по ее id, родителю и позиции number
    function includePosition($db, $categoryId, $parentId, $position) {
        $query = "update categories set number = number + 1 where parent_id = $parentId and number >= $position";
        $db->query($query);
        $query = "update categories set parent_id = $parentId, number = $position where id = $categoryId";
        $db->query($query);
    }

Здесь уже 2 sql запроса, но тоже ничего сложного. Первый запрос делает противоположное предыдущей функции - увеличивает на 1 позиции категорий в новом месте. Он, скажем так, освобождает место для новой категории, предоставляет число, number. Которое успешно и занимает основная категория во втором запросе. И одновременно устанавливает новое значение parent_id.

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


Ссылки из статьи

Предыдующая статья - Строим дерево категорий на клиенте и сервере
Демо-страница приложения
Архив с исходниками

Предыдущая статья Демо Исходники
Метки:
Заходите в группу в контакте - https://vk.com/webdevkin
Анонсы статей, обсуждения интернет-магазинов, vue, фронтенда, php, гита.
Истории из жизни айти и обсуждение кода.
Как Вам статья? Оцените!