Годные js библиотеки (1): jsTree

Written by elwood

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

jstree

Начнём с jsTree. Библиотека представляет собой плагин к jQuery. С помощью jsTree я делал дерево категорий. Категорий много, все сразу загружать проблематично. Поэтому в первую очередь я разобрался с тем, как настраивать аяксовую подгрузку json-данных. После этого самым важным было научиться определять выбранные элементы. Потом занялся улучшением UI – задал отдельную иконку для представления категорий с заданными правилами (это была админка для задания правил к отдельным категориям, соответственно, категории делились на те, для которых правил нет, и те, для которых они уже заданы). Научился открывать нужные ноды при загрузке дерева автоматически (при этом ajax-запрос на получение дочерних элементов формируется автоматически). Ну и напоследок включил отображение выделенного элемента а-ля OS X, с выделением всей строки полностью. Рассмотрим эти шаги подробнее. Возможно, опытным джаваскриптерам приведённые рецепты и пояснения покажутся чересчур банальными, но лично мне бы это очень помогло.

1. Подключение исходников

В документации к плагину написано, что для работы необходим jQuery 1.4.2. По поводу того, будет ли он корректно работать на более свежих версиях – информации нет. Но в скачиваемом архиве запакована совсем другая версия, значительно более свежая (у меня это был jQuery 1.9.1), из чего можно предположить, что с последующими версиями ветки 1.9.x всё должно работать, а изначальная фраза про 1.4.2 на самом деле означает минимально необходимую версию. Итак, с jQuery мы разобрались, теперь нужно подключить исходники собственно jsTree:

<script src="/resources/js/jstree/jquery.cookie.js"></script>
<script src="/resources/js/jstree/jquery.hotkeys.js"></script>
<script src="/resources/js/jstree/jquery.jstree.js"></script>

2. Создание дерева

<div id="categoryTree" style="width: 20%; height: 800px; overflow: auto;">
</div>
 
<script type="text/javascript">
    $('#categoryTree').jstree({
        themes: {
            theme: 'default', // название темы, для смены темы поменять url недостаточно, нужно ещё и сменить название (т.к. названия стилей содержат в себе его)
            url: '/resources/css/jstree/themes/default/style.css' // URL к файлу стилей (рядом должны лежать картинки, как в архиве с плагином)
        },
        json_data: { // это всё конфигурация плагина 'json_data'
            data: [ // тестовые данные - массив из объектов, в каждом из которых есть набор атрибутов и массив дочерних элементов
                {
                    // заголовок элемента, может быть представлен не только строкой, но и объектом (см документацию по json_data)
                    // это может быть использовано для того, чтобы установить дополнительные атрибуты ссылке (тегу <a>), генерируемой при конвертации
                    // json-данных в код разметки
                    data: "First node",
                    attr: { // attr
                        id: 1
                    },
                    state: 'open', // по наличию одного из ключей 'state' или 'children' jsTree определяет, что этот узел содержит детей
                    children: [
                        {
                            data: "Child1",
                            attr: {
                                id: 3
                            }
                        },
                        {
                            data: "Child2",
                            attr: {
                                id: 4
                            }
                        }
                    ]
                },
                {
                    data: "Second node - leaf",
                    attr: {
                        id: 2
                    }
                }
            ]
        },
        // тут мы перечисляем все плагины, которые используем
        plugins: [ 'themes', 'json_data', 'ui' ]
    });
</script>

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

3. Конфигурация Ajax

Чтобы json_data получал данные с сервера, нужно не задавать ему атрибут data, а сконфигурировать атрибут ajax:

json_data: {
    // часть опций соответствует опциям, которые доступны для установки в jQuery.ajax() вызове - например url, type
    // с другой стороны, success и data - семантически отличаются от их аналогов в jQuery ajax options -
    // success не только является callback'ом успешного ajax-запроса, но и должна возвращать данные для jsTree
    // а на функцию data как и в jquery ajax options, возлагается задача подготовить параметры для ajax запроса
    // но здесь в эту функцию будет передана нода, для которой будет выполняться ajax-запрос, то есть смысл функции несколько изменяется
    ajax: {
        url: '/child_categories',
        type: 'post',
        // вызывается для ноды перед тем как jsTree будет получать выполнять ajax-запрос
        // задача функции - "сконвертировать" ноду в набор параметров для ajax-запроса
        data: function(node) {
            if (node.attr) {
                return {
                    categoryId: node.attr('categoryId')
                }
            } else return {};
        },
        // а задача этой функции - обратная, "сконвертировать" данные, полученные в результате
        // выполнения ajax-запроса, в данные, нужные для jsTree
        success: function(data) {
            // for each item in array
            var arr = [];
            var i = 0;
            data.map(function(item) {
                if (!item.leaf) {
                    arr[i] = {
                        data: item.name,
                        // даём понять jstree, что эта нода имеет детей
                        state: 'closed'
                    };
                } else {
                    arr[i] = {
                        data: item.name
                    }
                }
                // добавляем атрибуты к ноде, их потом можно будет получить
                arr[i].attr = {
                    categoryId : item.id,
                    hasChildren: !item.leaf
                };
                i++;
            });
            return arr;
        }
    }
}

Соответственно, на стороне сервера нужно написать нечто, возвращающее по переданному параметру categoryId кусок json, представляющий собой массив элементов с полями id, name, leaf.

4. Получение текущих выбранных элементов

Чтобы получить список выбранных нод, ищем функцию в API. Находим её в плагине UI: get_selected ( context ). Чтобы выполнить эту функцию, нам нужен объект jsTree, который привязан к нашему контейнеру. Находим нужную функцию в описании Core API: jQuery.jstree._reference ( needle ). В качестве needle можно передавать элемент DOM, объект jQuery или селектор, ссылающийся на контейнер, или на элемент внутри дерева. Очень элегантный подход. Итак,

    // получаем экземпляр jsTree по нашему контейнеру
    var jsTree = $.jstree._reference($('#categoryTree'));
    // вызываем функцию из API плагина 'ui'
    var selectedItems = jsTree.get_selected();
    // это массив
    if (selectedItems.length == 0) {
        alert('Выберите категорию');
        return false;
    }
    // получать данные можно только те, которые сохранены в атрибутах
    // поэтому и нужно их создавать при построении дерева
    var firstNodeHasChildren = selectedItems[0].attributes['hasChildren'].value;

5. Добавление контекстного меню.

Тоже совершенно замечательная вещь. Позволяет нормально (НОРРМААЛЬНО!) работать с элементами дерева, без необходимости выводить кнопки управления на отдельную панель. Конфигурирование описано в документации, однако у меня возникла пара вопросов при использовании. Первый состоит в том, чтобы убрать из меню элементы по умолчанию (Create, Edit, Remove итд). Почему-то разработчики плагина сделали эти элементы присутствующими везде по умолчанию. Однако, решение очень простое:

contextmenu: {
    select_node: true,
    // items можно определить как функцию от ноды, и для каждой ноды таким образом определить свой набор
    // элементов меню. Другого способа сделать различные наборы (разные disabled/enabled к примеру) элементов,
    // судя по всему, нет
    items: function(node) {
        return {
            // убираем элементы по умолчанию
            create: false,
            rename: false,
            remove: false,
            ccp: false,
            // добавляем свои
            import: {
                label: 'Вывести в CSV',
                _disabled: node.attr('hasChildren') === 'true',
                action: function() {
                    // в эту функцию также передаётся нода, но нам она ни к чему, поскольку
                    // уже имеется замыкание на переменную node, и мы можем использовать её
                    if (node.attr('hasChildren') !== 'true') {
                        var categoryId = node.attr('categoryId');
                        window.open('/import?categoryId=' + categoryId, '_blank');
                    }
                }
            }
        }
    }
}

6. Тюнингуем юзабилити

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

// to expand nodes by clicking on it
$("#categoryTree").bind("select_node.jstree", function (event, data) {
    // data.inst is the tree object, and data.rslt.obj is the node
    return data.inst.toggle_node(data.rslt.obj);
});

А во-вторых, я хотел, чтобы текущий элемент выделялся во всю длину строки. Для этого оказалось достаточно подключить плагин ‘wholerow’.

Заключение

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