Сложные интерфейсы на javascript вместе Yahoo UI. Часть 10
Новые версии для рассматриваемой в этой серии статей javascript-библиотека Yahoo UI появляются не слишком быстро. Так первые строки, рассказывающие про YUI, были написаны еще в июле и на примере версии 2.5.2. Буквально пару дней назад вышла следующая версия 2.6. Количество нововведений не слишком велико: два новых компонента, с десяток улучшений в работе существующих и почти три сотни исправлений ошибок. Тем не менее, на вопрос: "переходить или нет на новую версию?" следует дать четкий ответ: "да, переходить". Обратная совместимость не была потеряна, а новые возможности - они всегда возможности.
Прошлая статья была посвящена такому компоненту YUI как AutoComplete. Мы разместили на html-странице текстовое поле, которое по мере набора в нем текста предлагало "живые" подсказки, варианты продолжения. Любая библиотека (не только javascript), претендующая на успех, должна быть гибкой в плане встраивания в существующую инфраструктуру. К примеру, при отправке запроса на сервер с целью получения списка строк подсказок, мы должны передать введенное пользователем слово. В YUI версии 2.5, эта строка выглядела следующим образом:
http://site.site/script.php?query=СЛОВО&ДОПОЛНИТЕЛЬНЫЕ_ПАРАМЕТРЫ
Мы могли только настроить отличное от "query" имя для переменной "слова", да передать список дополнительных переменных. В случае, если адреса страниц сайта строились по правилу ЧПУ, когда нет явно выделенных имен страниц (имен файлов скриптов), а передаваемые параметры кодируются, например, так:
catalog/furniture/armchair
То для интеграции YUI и сайта приходилось создавать специальный скрипт, перехватывающий и перекодирующий адреса в приемлемый стиль (например .htaccess файл с правилами для mod_rewrite). В версии 2.6 yui можно сделать так:
ds = new YAHOO.widget.DS_XHR('/site/pages', schema);
ds.responseType = YAHOO.widget.DS_XHR.TYPE_XML;
aNames = new YAHOO.widget.AutoComplete('fio','fiosuggest', ds);
aNames.generateRequest = function (query){
if ( ... )
return "/suggest/" + query ;
else
return "/conference/ask.php?word="+query;
}
При создании объекта DS_XHR в качестве адреса php-скрипта, генерирующего слова-подсказки, я указываю "пустышку". Затем назначается функция, формирующая действительный адрес запрашиваемого документа (generateRequest). Внутри функции (единственный ее параметр - строка с введенным пользователем фрагментом слова) проверяется некоторое условие и возвращается один из двух вариантов: '/site/pages/suggest/apple' или '/site/pages/conference/ask.php?word=apple'.
Замечание: для загрузки модулей я, как и ранее, использую YUI компонент yui-loader. При переходе версии yui с 2.5 на 2.6 статус модуля loader сменился с beta на "стабильный". А, следовательно, нужно указать другой адрес загрузки js-кода:
<script type="text/javascript" src="ПУТЬ/yuiloader/yuiloader.js"></script>
Также при переходе версий функциональность объекта источника данных для AutoComplete была перенесена из модуля autocomplete в модуль datasource и теперь рекомендуется писать так:
var ds = new YAHOO.util.XHRDataSource("jsonsearch.php");
ds.responseType = YAHOO.util.XHRDataSource.TYPE_JSON;
ds.responseSchema = { resultsList : "Department.usersList", fields : ["fio", "age", "sex"] };
var aNames = new YAHOO.widget.AutoComplete("fio", "fiosuggest", ds);
Помимо изменения имени используемого класса с YAHOO.widget.DS_XHR на YAHOO.util.XHRDataSource, изменился и стиль указания "схемы". Раньше при создании массива, описывающего как интерпретировать возвращенный серверным скриптом результат, использовалось правило, что первый элемент списка задает путь к объекту, а последующие элементы - имена атрибутов объекта. Теперь же путь к объекту задается как значение resultsList, а атрибуты объекта - хранятся в массиве fields.
Хотя использование AutoComplete может значительно облегчить клиенту пользование вашим сайтом, однако стоит помнить, что ничего не дается бесплатно и на выполнение запроса к серверу требуется какое-то время. Следовательно, нам нужен механизм кэширования, чтобы уменьшить количество обращений к серверу при вводе повторяющихся слов. Нужен и способ отслеживания событий: "начало отправки запроса" и "данные пришли с сервера". Так при длительном выполнении запроса можно будет выводить на страницу подсказку "мол, ждите, идет выполнение запроса". Есть три базовых события:
aNames.dataRequestEvent.subscribe(onDataRequest);
aNames.dataReturnEvent.subscribe(onDataReturn);
aNames.dataErrorEvent.subscribe(onDataError);
Функция onDataRequest будет выполнена при отправке запроса на сервер, а когда данные придут, будет вызвана функция onDataReturn. Если же произошел сбой при отправке запроса, то будет вызвана функция onDataError. Механизм кэширования не столь прост. Как только вы ввели несколько букв в текстовое поле, то элемент AutoComplete делегирует саму работу классу YAHOO.util.XHRDataSource (или любому другому источнику данных). Он, прежде всего, проверяет, вдруг ответ на наш запрос уже был получен ранее и сохранен в КЭШе. Размер КЭШа задается атрибутом maxCacheEntries (по-умолчанию он равен нулю), так что AutoComplete (равно как и другие компоненты, использующие услуги util.DataSourceBase) КЭШ не использует. В ходе работы КЭШ перемещает используемые элементы поближе к началу списка содержимого кэша. Соответственно, те элементы, которые не использовались какое-то время, из КЭШа будут удалены. К сожалению, разработчики yui остановились на полпути и не предоставили возможность гибко выбирать ситуацию для каких запросов нужно делать всегда прямые запросы на сервер, для каких запросов можно брать данные из КЭШа. Нет возможности очищать кэш основываясь на более сложных стратегиях, чем превышение количества maxCacheEntries, например, время или относительная частота запросов. В любом случае если такая потребность у вас возникает, то "подписывайтесь" на рассылаемые DataSource события cacheResponseEvent и responseCacheEvent. Первое из них "выбрасывается" когда значение берется из КЭШа, а второе - когда значение помещается в КЭШ. Если выполняется много "мелких" запросов, каждый из которых несет накладные расходы в виде обязательно передаваемых http-заголовков, то с целью уменьшения количества hit-ов сервера можно использовать следующий прием. Например, пользователь вводит слово "Ji", мы же отправляем на сервер запрос на поиск продолжений для буквы "J". Естественно, что пришедший результат сразу показать посетителю сайта не возможно: в вариантах подсказки будет и Jim, и Janet, и Jacob. Я назначаю для AutoComplete специальную функцию локальной фильтрации (filterResults), которая отберет из широкого списка лишь те значения, которые нужны (начинаются именно на "Ji"). За счет кэширования мы сможем уменьшить время отклика приложения.
// включаем локальную фильтрацию
aNames.applyLocalFilter = true;
// назначаем функцию, которая делает это
aNames.filterResults = function (query, original, parsed){
parsed.results.unshift ( {fio: "Liusa", age: 12, sex: "female"} );
return parsed;
}
Хотя в примере выше, я не фильтровал, а наоборот добавил к списку полученных от сервера строк подсказок новое значение, но идея должна быть очевидна. Третий параметр функции filterResults представляет собой подготовленные для отображения записи (на основании имен полей указанных в схеме). Схожая функциональность предоставляется yui "из коробки", когда идет несколько последовательных запросов с уточняющими параметрами. Так если первый запрос был к "Ji", а второй к "Jim", то очевидно, что нет необходимости обращаться к серверу еще раз, а можно взять результаты выполнения первого запроса и отбросить из них ненужное (установите в "true" параметр "queryMatchSubset" либо в объекте AutoComplete, либо внутри объекта DataSource).
Последнее о чем стоит упомянуть перед тем, как рассказ об AutoComplete будет завершен, так это о "тонкой" настройке внешнего вида всплывающего окна с подсказками. В прошлой статье я рассказал о функции formatResult. Мы использовали ее для пользовательского форматирования элементов списка с подсказками (в примере строки имели либо красный, либо зеленый цвет шрифта для разных имен сотрудников). Если вы хотите полностью изменить внешний вид выводимой подсказки, то придется постараться. Когда я создавал объект AutoComplete, то передал как параметр его конструктору имена двух html-элементов. Первый из них - текстовое поле, второе - "контейнер" для подсказки. YUI поместил в этот контейнер блок div с классом yui-ac-content, который в свою очередь содержит три подблока: yui-ac-hd, yui-ac-bd, yui-ac-ft. Во втором из них выводится список с элементами посказками. Для изменения содержимого блоков yui-ac-hd (заголовок) и yui-ac-ft (футер) используются методы (см. рис. 1):
aNames.setHeader ('<b>Список сотрудников нашего отдела</b>');
aNames.setFooter ('<i>Всего в отделе 10 человек</i>');
Более сложные модификации внешнего вида компонента требуют уже создания собственного класса производного от AutoComplete и реализующего собственную логику оформления результатов выполнения запроса за подсказками.
Следующая тема сегодняшней статьи - элемент управления TabView (набор закладок). Когда я только начинал рассказ об yui и описывал компонент меню, то упомянул, что для многих визуальных элементов управления есть два стиля создания. В первом случае в html-коде страницы нужно разместить строго предопределенную разметку, играющую роль скелета для компонента. Или, во-втором случае, создание компонента ведется с помощью javascript. Не исключение из этого правила и TabView. В следующем примере я создаю набор из трех закладок (имена классов и вложенность тегов обязательно должна быть такой):
<div id="pages" class="yui-navset">
<ul class="yui-nav">
<li><a href="#apple"><em>Apple</em></a></li>
<li class="selected"><a href="#orange"><em>Orange</em></a></li>
<li><a href="#grapes"><em>Grapes</em></a></li>
</ul>
<div class="yui-content">
<div id="apple">Content for Apple</div>
<div id="orange">Content for Orange</div>
<div id="grapes">Content for Grapes</div>
</div>
</div>
Набор закладок состоит из перечня их имен (список ul) и перечня html-блоков div с соответствующим содержимым. Та закладка, которая будет изначально выделена после открытия страницы, помечается классом
. Каждая ссылка имеет значение атрибута href равным "#x", где "x" совпадает с идентификатором блока div с соответствующим этой вкладке содержимым. Откровенно говоря, это правило не играет большой практической роли (так идентификаторы блоков могут отличаться от значений ссылок), т.е. главное - совпадение порядка, когда первой по счету ссылке соответствует первый по счету блок div. Теперь же нужно создать компонент TabView (результат показан на рис. 2):
// привычная загрузка кода модуля tabview
loader = new YAHOO.util.YUILoader();
loader.require(["tabview"]);
loader.loadOptional = true;
loader.base = 'js/';
loader.insert({ onSuccess: startApp });
// и теперь внутри функции обработчика создаем сам компонент
var p = null;
function startApp() {
p = new YAHOO.widget.TabView("pages");
}
Как видите, единственный параметр для конструктора TabView - идентификатор html-блока с информацией о содержимом набора закладок. Во втором случае мы создаем набор закладок программно. Для этого я предварительно разместил внутри html-кода страницы специальный тег заглушку (именно внутрь него и будет помещен html-код сгенерированный TabView):
Теперь после завершения загрузки страницы я создаю компонент TabView (никаких параметров конструктору не передается) и начинаю наполнять его вкладками (метод addTab):
p = new YAHOO.widget.TabView();
var tab = new YAHOO.widget.Tab ({label : 'Mathematic',
content : '<i>Information about mathematic</i>',
active : true} );
p.addTab (tab);
p.appendTo ('target');
По-умолчанию ориентация ярлычков вкладок горизонтальная, вверху страницы. Передав параметр orientation (других параметров управляющих внешним видом набора закладок нет), расположение списка ярлычков можно заменить на любое из следующих: top, left, right, bottom (см. рис. 3):
p = new YAHOO.widget.TabView({orientation: "right"});
При создании объекта YAHOO.widget.Tab необходимо указать значения атрибутов label (надпись на ярлычке закладки) и content (само содержимое закладки). Необязательный атрибут active позволяет указать, что данная закладка будет выделена изначально. В случае, если вкладка временно должна быть не доступна (ее нельзя открыть), то следует указать конфигурационный параметр disabled: true. Очевидно, что указывать содержимое вкладки в виде большого фрагмента html внедренного в javascript не красиво. Поэтому я рекомендовал бы в том случае если содержимое вкладки формируется динамически использовать какой-либо из движков шаблонов на javascript. Еще один способ указать содержимое вкладки - это обратиться к php-скрипту, который сформирует фрагмент html, который и будет размещен внутри закладки. Для этого я вместо атрибута content, при создании Tab укажу параметр dataSrc:
var tab = new YAHOO.widget.Tab ({label : 'Mathematic', dataSrc : 'tabcontent.php', active : true} );
Естественно, что при переключениях между закладками мы вовсе не хотим постоянно делать новые и новые обращения к серверу за ее содержимым (а может быть как раз этого и хотим). В любом случае, параметр "cacheData" предназначен для управления кэшированием содержимого вкладки. Еще, учитывая, что загрузка вкладки может занять длительное время, мы можем задать ограничение времени на загрузку (параметр dataTimeout). К сожалению YUI не предоставляет способа узнать, когда содержимое для какой-то из закладок было загружено и было ли успешно загружено оно для всех закладок. Некоторое подспорье разве что представляет событие contentChange, оно вызывается, когда содержимое вкладки было изменено (т.е. после того как данные были успешно загружены). Свойства newValue и oldValue хранят старое и новое значение содержимого вкладки:
tab.addListener ("contentChange", onContentChange);
function onContentChange (e){
alert ('new content = '+ e.newValue);
alert ('old content = '+ e.oldValue);
}
Созданный набор вкладок не является статическим образованием и его можно изменять по ходу выполнения программы. Добавлять закладки с помощью метода addTab мы научились, теперь попробуем удалить вкладку:
Как видите, в качестве параметра методу removeTab я передаю ссылку на объект закладки (Tab). В случае, если я знаю только номер закладки, которую хочу удалить, то сперва нужно выполнить преобразование этого номера в объект закладки и только потом вызывать removeTab. Функция getTab вернет вам объект вкладки по указанному номеру (отсчет начинается с нуля), а обратное преобразование выполнит функция getTabIndex. Если же вы хотите узнать, то какая из закладок сейчас выбрана, то используйте такой вызов:
После того как закладки были определены, вы можете захотеть изменить их содержимое или надпись ярлычка. Специальных методов, делающих такую работу нет, однако можно обратиться к объекту вкладки, а затем с помощью "общих" методов get и set изменять параметры вкладки:
p.getTab (1).set('content', 'new content for tab');
p.getTab (1).set('label', 'new label');
p.getTab (1).set('disabled', false);
В последнем случае я отменил свойство вкладки "недоступна" (в одном из ранних примеров, создавая вкладку, я указал значение disabled равное true и тем самым запретил ее выбирать). Научившись менять содержимое вкладки, самое время подумать о том, как можно реагировать на ситуацию "был выполнен переход на новую вкладку". Для этого мы подписываемся на событие activeTabChange. Когда это событие произойдет, то нам передадут ссылку на старую активную закладку и ту, на которую только что был выполнен переход:
p.addListener ("activeTabChange", onActiveChange);
// и пример обработки событий
function onActiveChange (e){
alert ('old = '+ e.prevValue.get('label'));
alert ('new = '+ e.newValue.get('label'));
}
Теперь попробуем выполнить настройку того, как будет выглядеть переход от одной закладке к другой. Хотя никаких стандартных атрибутов управляющих этим процессом нет, но у нас в распоряжении есть более гибкий механизм, основанный на переопределении функции вызывающейся TabView когда требуется выполнить переключение закладок:
p.contentTransition = function(newTab, oldTab) {
newTab.set('contentVisible', true);
oldTab.set('contentVisible', false);
};
Здесь я скопировал код функции contentTransition из исходников самого компонента TabView. Как видите, у каждой вкладки (и новой и старой) есть свойство contentVisible, изменяя которое мы прячем или показываем вкладку. А метод set наталкивает на мысль что можно получить ссылку на html-элемент представляющий вкладку и применить к нему средства анимации. Так следующий пример выполняет анимацию сворачивания старой вкладки и разворачивания новой:
p.contentTransition = function(newTab, oldTab) {
a1 = new YAHOO.util.Anim(oldTab.get('contentEl'), { height: {to: 0} });
a2 = new YAHOO.util.Anim(newTab.get('contentEl'), { height: {to: 400} });
// и подписываемся на событие первая анимация завершена
a1.onComplete.subscribe(
function (){
oldTab.set('contentVisible', false);
newTab.set('contentVisible', true);
a2.animate ();
}
);
a1.animate ();
};
Здесь я создал два объекта анимации (a1 и a2). Первый из них меняет высоту "старой" вкладки до нуля, а второй - наоборот увеличивает высоту "новой" вкладки до 400px. Самое главное, внутри обработчика события "первая анимация завершена" запустить вторую анимацию. Так чтобы они выполнялись последовательно, а не параллельно. За деталями работы модуля Anim прощу обращаться ко второй статье серии.