Сложные интерфейсы на javascript вместе Yahoo UI. Часть 19

February 28, 2009

Эта статья начнет рассказ о последнем из больших и сложных компонентов YUI – TreeView. Разрабатывая с помощью YUI “богатые” пользовательские интерфейсов для веб-приложений мы нуждаемся не только в средствах удобного отображения табличной информации (DataTable), но еще и информации иерархической (например, структура организации или категории товаров). Тема сегодняшней статьи – компонент TreeView – как раз и предназначен для того, чтобы показывать информацию в форме дерева.

Исходные данные для TreeView могут быть размещены как на самой странице, так и загружаться динамически с сервера. В первом случае я говорю не столько о программном наполнении TreeView (создание узлов дерева) информацией с помощью javascript, сколько о технике progressive enhancement. Об этой методике я уже упоминал в статьях посвященных созданию выпадающего меню. Тогда перед нами стояла задача не только разместить на странице “красивое и движущееся” меню, но при этом позаботиться о той, пусть и редкой, категории посетителей нашего сайта, что отключает в своем браузере поддержку javascript. Напомню, что идея progressive enhancement в том, чтобы разместить на веб-странице в виде обычной статической (видимой всем без ограничения) разметки меню, таблицу или дерево. Однако, если у клиента включена поддержка javascript, то эта разметка поступает на вход какому-либо из “продвинутых” компонентов YUI, который замещает информацию-разметку собой. В следующем примере я создам на веб-страничке несколько вложенных друг в друга списков (“UL” и “OL”):
  1. <div id="treeplaceholder">
  2. <ol> 
  3.  <li>Animals
  4.   <ol> 
  5.    <li>Birds
  6.      <ul> 
  7.        <li>Sparrow</li> 
  8.        <li>Parrot</li> 
  9.      </ul> 
  10.    </li> 
  11. ... и много других элементов списка …
  12. </ol>
  13. </div>
Как видите, я использовал в этом примере одновременно два вида списка (упорядоченный “ol” и неупорядоченный “ul”) - здесь ограничений нет. Также нет ограничений (кроме здравого смысла, конечно) на количество уровней вложенности списков, главное, что список высшего уровня следует поместить внутрь блока “div”. Запрещено размещать внутри div-а одновременно два или более высокоуровневых списка, т.е. “корень” дерева должен быть только один. Затем ссылку на блок div, хранящий внутри себя описание дерева, нужно подать на вход конструктора класса TreeView. Естественно, чтобы браузер знал о существовании класса TreeView, нужно предварительно подгрузить внутрь html-страницы исходный код модуля treeview с помощью хорошо знакомого нам по прошлым статьям класса YAHOO.util.YUILoader (название модуля совпадает с именем хранящегося в нем класса - treeview). Завершающим штрихом будет вызов метода render, который удалит из div-а все вложенные в него списка “ol” и “ul”, и вместо них нарисует дерево, так как это показано на рис. 1:


  1. tree =  new YAHOO.widget.TreeView("treeplaceholder"); 
  2. tree.render ();
Перед тем как я покажу то, как можно наполнить TreeView информацией, загруженной с сервера, давайте лучше попробуем “поиграть” с узлами дерева: попробуем их программно сворачивать и закрывать, добавлять новые узлы, изменять их надпись и удалять узлы дерева целиком. К примеру, если у вас есть на html-странице дерево с перечнем категорий товаров, и вы хотите при клике по узлу дерева переходить на страницу со списком товаров в выбранной клиентом категории. То, согласитесь, было бы удобным не терять информацию о том, какие узлы были развернуты. Т.е. после нажатия на ссылку с названием категории товара, нужно сначала сохранить (да хотя бы в cookie) сведения о том, какие узлы дерева были развернуты клиентом. А затем, после завершения загрузки новой страницы, восстановить эти сведения из cookie и развернуть дерево точь-в-точь как оно выглядело на предыдущей странице. Следующий пример показывает как “клонировать” дерево. Первое дерево будет создано на основании данных, содержащихся во множестве вложенных друг в друга списков ul и ol (предыдущий пример). Второй же конструктор класса TreeView получит первым параметром ссылку на пустой блок div, а необходимую для построения дерева информацию об узлах и их вложенностях, извлечь из первого дерева (функция getTreeDefinition):
  1. var def = tree.getTreeDefinition();
  2. var tree_clone =  new YAHOO.widget.TreeView("treeplaceholder_clone", def); 
  3. tree_clone.render ();
Для более “тонких” манипуляций с деревом нужно познакомиться с классами, представляющими информационную модель дерева, т.е. с узлами. Есть базовый класс узла YAHOO.widget.Node и производные от него разновидности: YAHOO.widget.TextNode (узел с простенькой текстовой надписью), YAHOO.widget.HTMLNode (здесь надпись для узла может быть с картинкой, или содержать другой элемент управления, например, падающее меню, checkbox, даже таблицу). Есть и третья разновидность узла - YAHOO.widget.RootNode - это “невидимый” узел представляющий собой корень дерева. Почему я назвал узел невидимым? Дело в том, что этот узел не имеет графического представления на экране. В примере ранее, когда я создал список на основании внедренной в html-код страницы разметки, то на верхнем уровне списка “ol” находились два элемента “Animals” и “Eatables”. Так вот, если получить из дерева с помощью метода getRoot ссылку на корневой элемент, то окажется, что он содержит внутри себя два дочерних узла: “Animals” и “Eatables”. В следующем примере я хочу вывести на экран надписи для всех узлов дочерних по отношению к корневому, т.е. все те же “Animals” и “Eatables” (узлы хранят список своих дочерних узлов в свойстве children):
  1. var root = tree.getRoot ();
  2. for (i = 0; i < root.children.length; i++)
  3.   alert (root.children[i].data.label);
Что такое data и label мы разберем на последующих примерах. Возвращаясь к созданию узлов, я хочу сказать что, вы не ограничены только теми типами узлов, которые я перечислил, и вы можете создавать свои типы узлов. Особый смысл это имеет при создании сложных визуальных компонентов, например tabletree, т.е. смеси дерева с таблицей, в которой строки таблицы можно сворачивать в группы. Еще можно создать свой вид узла, если есть желание переопределить внешний вид inline-редактора. Советую в качестве основы для создания собственных типов узлов, попробовать разобраться с тем, как реализован, идущий составе YUI, пример узла YAHOO.widget.DateNode. Этот узел при переходе в режим редактирования показывает на экране компонент календарь YUI Calendar (с ним мы знакомились в статье № 11). Еще один полезный пример – это класс YAHOO.widget.MenuNode. Построен этот узел на базе “простого” текстового узла, но накладывает такое ограничение, что если рядом расположить несколько узлов типа “menu”, то в один момент времени может быть раскрыт только один из них: так выбирая новый узел, мы автоматически сворачиваем все соседние узлы. Следующий пример будет посвящен тому, как создать TreeView, а информацию об узлах передать не в виде разметки html, и наполнить его узлами самостоятельно на javascript. Т.е. мы будем создавать узлы различных типов, назначать им текстовую надпись, вкладывать узлы друг у друга, строить иерархию. В следующем примере я, создавая объект Menu, передам ему первым параметром (точь-в-точь как в предыдущем примере) ссылку на тот элемент html, внутрь которого и будет сгенерировано визуальное представление дерева. А информация об устройстве дерева задается с помощью JSON-массива data. Каждый элемент этого массива – объект, с набором характеристик. Большей частью назначение характеристики можно угадать из ее названия. Так самой “важным” свойством является тип узла, возможные варианты это “text”, “html” (в примере содержимым узла является картинка) и “menu”. Если вам потребуется другой тип узла (в том числе и созданный вами), то можно смело использовать не псевдоним, а имя класса узла, например, MenuNode. Вторая важнейшая характеристика узла – это его надпись. Точнее, не так, не надпись, лучше заменить это слово на более абстрактное “данные”. В прошлых статьях я многократно акцентировал внимание на том, что YUI построен в соответствии с идеей MVC, а значит, существует четкое разделение между данными и их визуальным представлением. Данные узла - это все что находится внутри фигурных скобок, и type и label и другие предопределенные для TreeNode свойства-характеристики. Также вы можете добавить к узлу и свои собственные свойства, например, узел описывающий автомобиль, может содержать свойство “car” содержимым которого (“car” – это полноценный JSON объект) будут сведения о марке авто, объеме двигателя … В самом простом случае, вся информация об узле может быть ужата до одной строки (названия узла), в примере это узлы дочерние к узлу “Menu”. Еще я каждому из узлов назначил параметр “title”, т.е. текст надписи, которая всплывает, когда клиент наводит мышь на узел дерева (вот только в случае, если надпись должна содержать кавычки, то их следует заменить на html-последовательность "). Результат выполнения следующего примера показан на рис. 2:


  1. var data = [ 
  2.  {type:'text', label:'People', title:'the tooltip for "people" node'}, 
  3.  {type:'text', label:'Travel facilities', title:'the tooltip for "travel facilities" node',  children:
  4.     [ 
  5.        {type:'text',label:'Cars',title:'the toolktip for "cars" node', href:'http://cars.site', target:'_blank'}, 
  6.        {type:'html',html:'<img src="img/car1.png" />'}, 
  7.        {type:'menu',label:'Menu',title:'this is a menu', expanded:true, children:
  8.           [ 
  9.             'First Menu Item', 
  10.             'Second Menu Item' 
  11.           ]
  12.        }
  13.     ]
  14.  }  
  15. ];
  16.  
  17. // а теперь на основании массива определений узлов дерева создадим его самого
  18. tree =  new YAHOO.widget.TreeView("treeplaceholder", data);
  19. tree.render ();
Теперь еще один пример, который покажет, как динамически наполнить дерево узлами. Вызывая конструктор класса TextNode, я передаю первым параметром текст надписи (label) для узла, второй параметр – это ссылка на родительский узел, к которому будет добавлен узел. Третий параметр – необязательный, играет роль признака состояния, в котором изначально будет находиться узел (свернут или развернут). Вместо надписи для узла (первого параметра) я могу передать внутрь конструктора класса TextNode полноценный JSON объект, все свойства которого будут учитываться при создании узла дерева и формирования его внешнего вида. К примеру, параметр contentStyle позволяет задать имя css класса, управляющего внешним видом узла. А параметры “href” и “target” позволят при клике на узел открывать всплывающее окно браузера и загружать в нем другую веб-страницу:
  1. // создаем само дерево
  2. tree =  new YAHOO.widget.TreeView("treeplaceholder"); 
  3. // теперь создадим для него узлы
  4. var fruits = new YAHOO.widget.TextNode("Fruits", tree.getRoot(), false); 
  5. var animals = new YAHOO.widget.TextNode("Animals", tree.getRoot(), false); 
  6. var props = 
  7.    {
  8.       label: "Apples", target : "_blank", href: "http://site.site", contentStyle  : "customStyleClass", 
  9.       data : {fio: "Vasya", age: 12} 
  10.    }; 
  11. var apples = new YAHOO.widget.TextNode(props , fruits, false); 
  12. // а теперь “нарисуем” дерево
  13. tree.render ();
Вызов метода render должен идти после того, как все узлы были определены. Если же добавление узлов дерева выполнятся постепенно, например, после нажатия на кнопку или выбора элемента списка, то следует всегда вызывать метод “render” для того чтобы он обновил визуальное представление дерева. Особое внимание обратите на свойство “data” с его помощью я могу к каждому узлу привязать некоторый кусочек информации (вспомните о разделении информации и ее представления в модели MVC). А раз уж я “положил” в узел немножко сведений, то давайте попробуем их использовать. К примеру, по клику на узел я хотел бы показать диалоговое окно, в котором выведется значение “data”. Конечно, для адресной привязки функции обрабатывающей событие “клик по узлу” можно было бы применить “старый варварский” прием с указанием значения атрибута “href” для узла-ссылки как “javascript:функция_которую_нужно_вызвать()”. А лучше сделать так:
  1. tree.subscribe("expand", function(node) { 
  2.   alert(node.index + " was expanded"); 
  3.   return true; 
  4. }); 
  5.  
  6. tree.subscribe("collapse", function(node) { 
  7.   alert(node.index + " was collapsed"); 
  8.   return false;
  9. }); 
  10.  
  11. tree.subscribe("labelClick", function(node) { 
  12.   alert (node.data.fio);
  13. });
В примере я создал три функции обработчика: первая из них вызывается до того, как любой узел в дереве будет развернут, вторая – до сворачивания узла дерева. А третья функция срабатывает при клике по текстовой надписью рядышком с узлом. Особое внимание прошу обратить на слова “до того как”. Функции не просто извещаются о действиях пользователя с деревом, но и могут влиять на его работу. Так значение, которое возвращают функции, является признаком того, будет ли действие, запрошенное пользователем, выполняться дальше или будет отменено. В примере, я разрешил развернуть любой из узлов, но при этом запретил их обратно сворачивать. Надо сказать, что кроме функций “до” есть и функции “после”, например, collapseComplete, expandComplete. Для того, чтобы “услышать” событие двойного клика мышью по надписи узла дерева – подписывайтесь на событие “dblClickEvent”. Какая бы функция не была вызвана вы можете легко узнать по какому из множества узлов был выполнен клик или свернут/развернут узел. Для этого нужно анализировать переданный функциям обработки события параметр “node” (ссылка на узел). В простейшем случае можно воспользоваться свойством “index” (каждому из создаваемых узлов дерева, YUI сам назначит уникальный порядковый номер). Например, такая система номеров будет полезной для решения задачки упомянутой в самом начале статьи (когда нужно было при переходе с одной страницы на другую сохранять и восстанавливать состояние дерева, т.е. то какие из его узлов были свернуты или развернуты). Хотя задачка проста, но давайте ее все же разберем (на самом деле я больше хочу показать вам еще один модуль в составе YUI - cookie). Подключается модуль cookie к странице все с помощью того же YUI Loader-а, а после загрузки в наше распоряжение попадет класс YAHOO.util.Cookie. Его основные методы это set – сохраняет значение переменной в cookie. Функция get извлекает из cookie значение переменной, а функция remove служит для того, чтобы удалить из cookie информацию:
  1. YAHOO.util.Cookie.set("fio",  “Vasya Tapkin” ); 
  2. alert (YAHOO.util.Cookie.get("fio") ); 
  3. YAHOO.util.Cookie.remove("fio");
К сожалению, cookie не слишком приспособлены для хранения больших объемов информации или информации имеющей сложную структуру массивы, объекты с вложенными подобъектами. Если у вас есть подобная необходимость, то советую обратиться к альтернативным подходам: flash, google gears, HTML 5 LocalStorage, “window.name”. На рынке есть множество маленьких и не очень библиотек, которые предоставляют унифицированный интерфейс для работы с перечисленными выше разнородными технологиями хранения данных, например, PersistJS. В крайнем случае, можно воспользоваться механизмом subcookie. Например, если я хотел бы сохранить информацию в cookie о человеке и информация эта включает в себя сведения об его имени, возрасте и поле, то можно было бы сделать так:
  1. var c = YAHOO.util.Cookie;
  2. c.setSub ("user", "fio", "Petrov");
  3. c.setSub ("user", "age", 12);
  4. c.setSub ("user", "sex", "male");
Для того, чтобы извлечь информацию используем метод getSub (основное имя, дополнительное имя). Теперь я приведу пример функции, которая выполняет сканирование дерева и сохраняет сведения о состоянии его узлов в cookie:
  1. function doSave (node){
  2.   if (! node){
  3.     node = tree.getRoot ();
  4.     YAHOO.util.Cookie.remove ('tree');  } 
  5.   else
  6.     YAHOO.util.Cookie.setSub ('tree', ""+node.index, node.expanded)
  7.   for (var i =0; i < node.children.length; i++)
  8.     doSave (node.children[i])
  9. }
  10. // И назначаем эту функцию как обрабатывающую клик по кнопке
  11. <button onclick="doSave()"> Save </button>
Когда функция doSave вызывается, то она первым делом получает ссылку на корневой элемент дерева, затем удаляет из cookie устаревшие сведения о состоянии узлов. Теперь нужно сохранить в cookie состояние узла (expanded). Информация о каждом таком узле хранится под индексом (порядковым номером узла). Несколько неудобно то, что функция setCookie требует, чтобы тип данных для второго параметра был строкой. Т.к. у каждого из узлов может быть несколько дочерних узлов, то нужно организовать по ним цикл и для каждого из них рекурсивно вызвать функцию сохранения состояния дерева в cookie (doSave). Восстановление состояния узлов тривиально: сначала мы получаем список всех “sub-cookie” (как индексов узлов, так и их состояний). Затем внутри цикла, перебирающего индексы всех узлов, я получаю ссылку на сам узел дерева с помощью функции getNodeByIndex и вызываю одну из двух функций: либо expand либо collapse:
  1. function doRestore (){
  2.  var state = YAHOO.util.Cookie.getSubs('tree') || {};
  3.  for (index in state){
  4.    var node = tree.getNodeByIndex(parseInt(index));
  5.    if (state[index] == "true")
  6.        node.expand ();
  7.    else
  8.        node.collapse ();
  9.  } 
  10. }
Помимо “точных” функций сворачивающих или разворачивающих каждый узел индивидуально есть и функции, действующие сразу на все узлы дерева, например, collapseAll и expandAll.

В следующий раз я продолжу рассказ об TreeView. Нас ждет знакомство с методиками наполнения TreeView информацией загружаемой с сервера с помощью ajax. Также мы попробуем “поиграть” с визуальным представлением узла (создать собственный тип Node), настроить редактор содержимого узла и контекстное меню для него.