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

September 25, 2008Comments Off on Сложные интерфейсы на javascript вместе Yahoo UI. Часть 3

Сегодня я продолжаю рассказывать об одной из лучших javascript-библиотек для построения сложных и “богатых” пользовательских интерфейсов - yui (yahoo user interface). В прошлых двух статьях мы изучили модули dom, selector, events, animation, logger и тем самым заложили хороший фундамент для перехода к изучению, нет, не более сложной, но более интересной теме – работе с компонентами пользовательского интерфейса.

Вот перечень модулей yui содержащих различные визуальные компоненты: Layout Manager (позиционирование других визуальных компонентов), Button (кнопки, простые и не очень), Calendar (календарь), Charts (построение графиков), ColorPicker (окно выбора цвета), Container (основа для создания многих сложносоставных элементов управления), DataTable (сетка или таблица с данными), Resize (этот компонент добавляет возможность динамически менять размеры некоторого контейнера), Rich Text Editor (html-редактор), Slider (ползунки), TabView (набор закладок), TreeView (старое-доброе дерево), Menu (меню для сайта с несколькими уровнями вложенности) и последний компонент Uploader добавляет улучшенную поддержку загрузки файлов на сервер. Вот вкратце и план дальнейшего изучения yui. Откровенно говоря, в составе yui есть и еще несколько модулей, например: поддержка Drag&Drop, возможности работы с cookie и поддержка ajax. Но все эти темы будут рассмотрены вместе со своими “большими братьями”, так “особый шик” будет иметь шанс попробовать создать в yui на странице таблицу для данных, а сами же данные будут загружены с сервера с помощью ajax.

Первым на рассмотрении будет создание меню. Для этого мы используем возможности модуля Menu, который нужно подключить в секции head вашего html-файла. Для этого и всех последующих примеров я использую загрузку модулей с помощью yahoo Loader (о котором я подробно рассказал в прошлой статье). Все действия по инициализации компонентов выполняются в обработчике события “и DOM загружен и модули тоже”:
  1. loader = new YAHOO.util.YUILoader(); 
  2. loader.require(["menu"]); 
  3. loader.loadOptional = true; 
  4. loader.base =  'js/';
  5.  
  6. loader.insert({ onSuccess: loadCompleted}); 
  7.  
  8. // после загрузки всех необходимых модулей
  9. function loadCompleted (){
  10.  YAHOO.util.Event.onDOMReady (onDOMReadyHandler); 
  11. }
  12.  
  13. function onDOMReadyHandler (){
  14.    startApp();   
  15. }
  16.  
  17.  
  18. function startApp (){ 
  19.  /* делай это */
  20. }
Чтобы избежать проблем я первым шагом делаю загрузку необходимых для работы приложения модулей (помимо, menu это такие модули как dom и event). В обработчике события “модули были загружены” я создаю новый обработчик события DOMReady и, который уже зная, что все необходимые ресурсы html готовы к работе, вызывает функцию startApp. Внутрь именно этой функции я буду помещать код для всех последующих примеров. Особенность работы onDOMReady такова, что даже, если назначу обработчика события “документ html готов к работе” гораздо позже того, как это случилось, то все равно событие не будет “потеряно”. Для того чтобы, меню выглядело пристойно, а его верстка не разъезжалась, я назначил тегу body значение css-класса class="yui-skin-sam".

Сразу же нужно выделить важный момент. Создавать и настраивать поведение yui компонентов возможно двумя способами: с помощью специальной html-разметки и программно с помощью javascript. Большой разницы между этими двумя подходами нет, и хотя в начале я покажу два способа, но в последующем буду стараться избегать html-кода как слишком не гибкого и довольно громоздкого. Итак, поехали. Самое простое меню должно быть создано внутри некоторого div блока и иметь структуру, заданную с помощью элемента UL (сам список) и LI (элементы меню). Тег div с классом bd создает для меню эффект тени (см. рис. 1).



Для начала я написал js-код, который создает yahoo объект меню, указав ему в качестве параметра имя элемента содержащего внутри себя содержимое меню:
  1. menu = new YAHOO.widget.Menu("menu"); 
  2. menu.render ();
Теперь пора создать html код с разметкой меню (обратите внимание на идентификатор первого блока div):
  1. <div id="menu" class="yuimenu"> 
  2.  <div class="bd"> 
  3.   <ul class="first-of-type"> 
  4.     <li class="yuimenuitem">
  5.         <a class="yuimenuitemlabel" href="http://black-zorro.com">Apple</a> 
  6.     </li> 
  7.     <li class="yuimenuitem">
  8.         <a class="yuimenuitemlabel" href="http://black-zorro.com"> Orange</a>
  9.      </li> 
  10.   </ul>
  11.  </div> 
  12. </div>
Надо сказать, что названия классов элементов предопределены, и заменять их на что-то другое не стоит – меню работать не будет. Есть еще один “фокус”. Открыв страницу в первый раз, вы ничего не увидите: меню изначально скрыто и для того чтобы его показать, нужно явно вызвать на объекте меню функцию show. Например, так: menu.show(). Соответственно, есть функция hide, которую вы вызываете, когда необходимо меню спрятать. Также меню прячется при любом “клике” на странице за пределами меню (как изменить такое поведение я покажу позже). Теперь попробуем второй способ, когда содержимое меню, их надписи и адреса ссылок задаются программно. Для этого я удалил все содержимое из элемента < div id="menu" > . Затем в методе startApp сразу же после создания объекта menu, я добавляю к нему парочку элементов, вот так:
  1. menu.addItems([ 
  2.   { text: "Apple", url: "http://..." }, 
  3.   { text: "Orange", url: "http://..." } 
  4. ]);
Если внимательно посмотреть на описание метода addItems в документации yahoo, то увидите, что есть и второй, необязательный параметр функции - номер группы. Дело в том, что менюшки yahoo умеют представлять не только плоский набор данных, но и группы связанных элементов с заголовками. Например, чтобы создать меню, показанное на рис. 2 я сделал так:


  1. menu.addItems(
  2.  [ 
  3.      { text: "Apple", url: "http://black-zorro.com" }, 
  4.      { text: "Orange", url: "http://black-zorro.com" } 
  5.  ], 0); 
  6.  
  7. menu.addItems(
  8.  [ 
  9.      { text: "Red", url: "http://black-zorro.com" }, 
  10.      { text: "Green", url: "http://black-zorro.com" } 
  11.  ], 1); 
  12.  
  13. menu.setItemGroupTitle("Fruits", 0); 
  14. menu.setItemGroupTitle("Colors", 1);
Здесь вызвав метод addItems, я указал массив с описанием элементов массива, а также порядковый номер этой группы элементов. Затем этот номер я использовал при вызове функции setItemGroupTitle, указав для какого номера группы будет назначено какое название. Но это снова не все: теперь попробуем создать меню из нескольких вложенных уровней (см. рис. 3).



Для этого при перечислении содержимого меню, всего лишь нужно указать в качестве параметров не только характеристики text и url, но и параметр submenu. Его значением будет еще один вложенный объект, содержащий внутри элемента itemdata перечень дочерних элементов меню. Если же хочется уже не двух уровневого меню, а трехуровневого, то нужно всего лишь повторить описанный выше трюк еще раз:
  1. menu.addItems(
  2.  [ 
  3.     {  text: "Animals" , url:"http://" , 
  4.        submenu : {
  5.                id: "animals",
  6.                itemdata: 
  7.                  [
  8.                     {text:"Bird", url: "http://"},
  9.                     {text:"Fish", url: "http://"} 
  10.                  ]
  11.         }
  12.    } 
  13.  ] 
  14. );
Возможно, вы захотите, чтобы содержимое подменю было бы разбито на подгруппы как показано на рис. 5.



Добиться такого эффекта очень легко: все, что нужно так это в качестве значения атрибута itemdata передать не массив, состоящий из объектов (отдельных элементов меню), а именно из массивов элементов. Т.е. каждый подмассив должен содержать уже конкретные объекты, соответствующие пунктам меню. Каждый раз, когда мы создаем новый пункт меню, то можем управлять не только его текстовой надписью и адресом перехода при активации данного пункта меню. Еще можно имитировать (увы, но не слишком удачно) “настоящее меню в windows”. Прежде всего, определенные пункты меню можно заблокировать (свойство “disabled: true”). Еще можно настроить меню так, чтобы некоторый пункт меню был бы уже выбран (выделен) сразу после появления меню на экране, для этого я устанавливаю свойство элемента меню “selected: true”. Параметр “checked: true” служит для того, чтобы задать для некоторого элемента меню отметку-checkbox “пункт меню выбран” (см. рис. 4).



Остальные параметры меню относятся к вопросу визуализации и не все типовые задачи имеют легкие решения. Например, если необходимо сопоставить каждому пункту меню некоторую иконку, то стандартной функции для этого вы не найдете, и приходится манипулировать построенным из javascript фрагментом дерева DOM. При программном построении меню в страницу встраивается html-код один-к-одному с показанным мною в самом начале статьи набором тегов ul & li. Это несложно, но неприятно. Например, в следующем примере я добавлю новый пункт меню. Затем значение, возвращенное функцией addItem (если вы использовали для добавления функцию addItems, то вам будет возвращен массив) я использую для “тонкой” настройки внешнего вида меню. Так поле element хранит ссылку на html-элемент представления пункта меню (“li” – элемент списка, а внутри “li” находится “a”). Затем, используя старый-добрый css я добавляю к пункту меню иконку:
  1. var mi = menu.addItem({text: "Green", url:"http://color.com"});
  2. mi.element.style.backgroundImage = 'url(diskget_sm.gif)';
  3. mi.element.style.backgroundPosition = 'right bottom';
  4. mi.element.style.backgroundRepeat = 'no-repeat';
Общий совет: многие функции работы с визуальными элементами возвращают ссылку на созданный html-элемент, к которому можно применять css-стили, чтобы выполнить тонкую настройку внешнего вида создаваемой страницы.

Что касается остальных свойств настройки внешнего вида меню, то начнем мы с рассмотрения модификатора position. Есть два варианта того, как меню может быть расположено на странице: статически (static) или динамически (dynamic).
  1. new YAHOO.widget.Menu("menu", {position: 'dynamic'});
Статически позиционированное меню занимает постоянное место на html-странице и никуда не исчезает при клике за границами меню (фактически тег div, внутрь которого я внедряю меню, получает значение css атрибута position равное static). Режим расположения меню dynamic (на самом деле здесь div-у контейнеру меню назначается стиль позиционирования absolute) предполагает, что меню автоматически прячется после клика за границами меню. Если мы создали сложное меню с подменю, то можно настроить параметры анимации развертывания подменю, например, задать длительность интервала, через который подменю будет показано или скрыто:
  1. new YAHOO.widget.Menu("menu", { position: "static",  hidedelay: 2000, showdelay: 1000});
Подменю может не просто появляться при наведении на него мыши, но и сопровождать этот процесс эффектом изменения прозрачности:
  1. new YAHOO.widget.Menu("menu", 
  2.  {
  3.     effect: {
  4.          effect: YAHOO.widget.ContainerEffect.FADE,
  5.          duration: 1
  6.    }
  7.  });
Еще одним занимательным свойством при конфигировании меню является “ленивость”. Идея в том, что если меню очень-очень большое и при этом не всегда используется, то имеет смысл отложить создание меню до момента первого обращения к нему. Для подобной “экономии на спичках” используется запись “lazyload: true”. Последнее о чем я упомяну, перед тем как начну рассматривать особые подвиды меню, так это параметры метода render. Вспомните, когда я создавал меню на основании явно заданной мною html-разметки, то вызов метода render был без параметров. Однако это правило не абсолютно: когда меню генерируется на основании данных из javascript возникает вопрос, где разместить меню? И если я пишу что-то вроде menu.render (‘target’). То меню будет добавлено к элементу с идентификатором target.

Когда говорят об меню, то почти всегда вспоминают об еще нескольких элементах управления решающих сходные задачи. Прежде всего, это контекстное меню, появляющееся при правом клике мыши по любой части html-странички. Никаких изменений в работе с контекстным меню по сравнению с “обычным” нет. Действительно, контекстное меню представляется классом YAHOO.widget.ContextMenu, наследником от знакомого нам класса Menu. Для демонстрации работы контекстного меню я создал блок div (с идентификатором равным “block”), который будет реагировать на нажатие правой кнопки мыши. Затем я пишу следующий javascript-код:
  1. var items = [
  2.  { text: "Apple", url:"http://"  },
  3.  { text: "Orange", url:"http://" }
  4. ];
  5. new YAHOO.widget.ContextMenu("generateto",
  6.   {trigger: "block", itemdata: items,  lazyload: true}
  7. );
Обратите внимание на параметр “trigger”: именно он задает идентификатор того элемента html-страницы, при правом клике на котором и будет показано контекстное меню. Также внимание на параметр lazyload: он обязательно должен быть установлен в true (иначе меню не работает). Первый параметр конструктора ContextMenu по аналогии с рассмотренным шагом ранее обычным меню задает идентификатор того html-элемента страницы, который будет заменен сгенерированным html-представлением меню. Теперь запустим пример и посмотрим, что происходит в трех самых популярных браузерах. В firefox и internet explorer меню замечательно работает, а вот для opera правый клик недоступен и для отображения контекстного меню приходится пользоваться комбинацией клавиш “ctrl + левый клик”. Создание контекстного меню подняло одну интересную проблему: посмотрите внимательно на приведенный выше код массива items (элементы меню). Каждый элемент меню задается парой: “текстовая надпись названия пункта меню” и “адрес страницы, куда перейти после клика на этом пункте меню”. Дело в том, что не всегда при активации какого-то пункта меню нам нужно загружать новую страницу: может быть, я просто хочу вызывать некоторую функцию javascript. В таком случае нам потребуется альтернативный синтаксис:
  1. { text: "buy Apple for 13$", onclick: 
  2.            {fn: doFruit, obj: ['apple', 13]}  
  3.   }
Здесь я вместо атрибута url использую параметр onclick, значением которого является объект со следующими характеристиками. Fn – имя функции, которую нужно вызвать при активации данного пункта меню. Параметр obj – может быть чем угодно: строка, число, массив, сложный объект. Т.к. мы можем создать множество пунктов меню вызывающих одну и туже функцию, то нам нужен способ отличать один вызов от другого и сделать это можно наилучшим образом, если передать в функцию-обработчик события “клик” некоторый набор информации, говорящий что же функция должна делать.
  1. function doFruit (eventType, eventObj , args){
  2.   fruit = args [0];
  3.   price = args [1];
  4.   alert ('eventType='+eventType+ "; eventObj= "+ eventObj+"; fruit="+fruit+"; price="+price); 
  5. }
Информация, указанная при создании пункта меню в атрибуте obj, будет передана в функцию doFruit как третий параметр - args. Первый и же второй параметры содержат информацию о произошедшем событии с точки зрения библиотеки yahoo: первый параметр – строка с названием события (просто слово ‘click’), второй атрибут задает html объект события MouseEvent. Последним о чем я хочу упомянуть, перед тем как перейду к рассмотрению других компонентов yui – это забота о посетителе вашего сайта. Существует вероятность (хоть и очень маленькая), что в браузере клиента будет отключен javascript и нам нужно предложить ему альтернативный контент. Если сделать это для сложных компонентов, например, дерева или таблички практически невозможно (максимум, обойтись надписью “для корректной работы сайта нужно включить javascript”), то для меню можно предложить альтернативный статический контент. Я советую обратиться к примерам работы с меню от yahoo, там вы найдете демонстрацию приема Progressive Enhancement. Это когда на странице присутствует статическая версия меню (в виде того же списка ul и это меню видно всем пользователям и, особенно важно, поисковым машинам). А если у клиента включен javascript, то меню заменяется на версию со “спец-эффектами”.

Еще одной разновидностью меню является menubar. В этом случае меню “прилипает” к верхней части окна браузера, пункты меню расположены горизонтально. Одним словом, меню ведет себя точь-в-точь как главное меню ms word, в котором я набираю эти строки (см. рис. 6).


  1. var items = [
  2.  { text: "Apple", submenu : 
  3.        {
  4.          id:"apple", 
  5.          itemdata:[
  6.               {text: "Buy"}, {text: "Sell"}, {text: "Wait a little"}
  7.          ]
  8.        } 
  9.  },
  10.  { text: "Orange", url:"http://"  } 
  11. ];
  12.  
  13. var bar = new YAHOO.widget.MenuBar("barplaceholder", 
  14.    {
  15.      lazyload: true,
  16.      itemdata: items 
  17.    }
  18. );
  19.  
  20. bar.render(document.body);
Никаких особенностей кроме как замена имени создаваемого класса с Menu на MenuBar не произошло: параметры внешнего вида и перечень элементов меню задается одинаково во всех случаях. Не забудьте только вызвать метод render, передав ему в качестве параметра body. В этом случае для представления меню будет сгенерирован новый блок div с id равным “barplaceholder” и будет приклеен к верхней границе всего содержимого html-странички.

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