Годные js библиотеки (1): jsTree
За последние месяцы я частенько пользовался различными джаваскриптовыми контролами и плагинами к jQuery. Думаю, имеет смысл поделиться инфой о самых полезных из них. Пока список таков:
- jsTree
- Ace editor
- X-editable & Poshytip
Начнём с 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 и рецептами наподобие тех, которые изложены выше. Надеюсь, что в будущем всё это появится.
4