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

November 25, 2008

Я продолжаю рассказ о библиотеке Yahoo UI. В прошлой статье серии я рассказывал о компоненте TabView (наборе закладок). Сегодня я продолжу рассмотрение возможностей YUI именно в области построения пользовательских интерфейсов и начну рассказ про один из наиболее часто используемых элементов интерфейса - календарь.

Элемент управления calendar нам пригодится в тех случаях, когда пользователь должен указать дату в одном из полей формы на сайте. Вводить значение даты в обычное текстовое поле - не самая лучшая идея, как с точки зрения удобства, так и ограничения потенциальных ошибок (пользователь может ввести дату в неправильном формате). В YUI компонент calendar имеет ряд конфигурационных переменных, позволяющих тонко настроить параметры внешнего вида календаря, внедрить календарь в другие элементы управления (вызов календаря при редактировании ячейки таблицы), можно задать ограничение диапазона, в котором можно выбрать дату (только в определенные месяца или дни недели). Пользоваться календарем легко: прежде всего, нужно загрузить модуль calendar, используя для этого yui-loader. Затем я помещу на html-страницу блок div без какого-либо содержимого:
  1. <div id="put_it_here" ></div>
Когда я вызову конструктор класса calendar, то должен передать ему два параметра: первый из них обязателен и равен идентификатору того тега страницы, внутрь которого и будет помещен сгенерированный html-код календаря. Второй же параметр представляет собой хранилище множества настроек, управляющих, как особенностями внешнего вида календаря, так и его поведения.
  1. var c  = null;
  2. function startApp() {
  3.  c = new YAHOO.widget.Calendar("put_it_here",{ properties: "values" }); 
  4.  c.render();  
  5. };
Что получится в результате выполнения этого фрагмента кода показано на рис. 1.



Сразу после создания календаря, в нем выбрана текущая дата, точнее отображается текущий месяц, подписи дней недели сделаны на английском. Текущий день выделен в рамку, а после щелчка по любой ячейке таблицы, она выделяется синей подсветкой. Выделить можно только одну ячейку, а при повторном же клике она теряет выделение. Вверху календаря размещены кнопки перехода на месяц вперед и месяц назад. Давайте попробуем поменять что-нибудь из внешнего вида календаря. Начнем с того, какой месяц будет изначально выделен:
  1. var config = {PAGEDATE : "08/2008"};
  2. c = new YAHOO.widget.Calendar("put_it_here",config);
В примере выше я при создании календаря указал вторым параметром конструктора объект config, одним из элементов которого является свойство "PAGEDATE" (формат ). В случае, если вы работаете с датами в отличном формате от "день/месяц/год", то лучшим выходом будет передавать как значение параметра PAGEDATE, либо непосредственно объект Date, либо строку с датой форматированной по вашим правилам, но тогда и правила следует указать (какой символ играет роль разделителя и в каком порядке расположены месяц и год):
  1. // отсчет номера месяца задается от 0
  2. var config = {PAGEDATE : new Date (2008,0,1)};
  3. // а можно и так
  4. var config = {PAGEDATE : "2008-08", DATE_FIELD_DELIMITER: "-", MY_MONTH_POSITION: 2, MY_YEAR_POSITION: 1};
Следующим шагом настройки внешнего вида календаря будет его интернализация (заменим английские подписи дней недели и месяцев на русские). С одной стороны, сделать это очень просто (указав эти надписи в объекте config), например, так:
  1. var config = {
  2.    MONTHS_LONG: [
  3.           "Январь", "Февраль", "Март", "Апрель", "Май", "Июнь", 
  4.           "Июль", "Август", "Сентябрь", "Октябрь", "Ноябрь", "Декабрь" 
  5.    ],
  6.    WEEKDAYS_SHORT : ["Вс", "Пн", "Вт", "Ср", "Чт", "Пт", "Сб"] 
  7. };
В том случае, если вас не устраивает то, что неделя начинается с воскресенья, а хочется с понедельника, то измените значение свойства START_WEEKDAY на 1. Полный перечень подобных ключей конфигурации можно посмотреть в javascript-файле с исходным кодом модуля calendar. Просто найдите место, где создается переменная YAHOO.widget.Calendar._DEFAULT_CONFIG (в ней хранятся настройки календаря по-умолчанию) и меняйте их сколько душе угодно. К сожалению, задача локализации и интернализации приложений гораздо сложнее чем кажется с первого взгляда. Хорошо, если ваша аудитория - только русскоговорящая или только говорящая на английском языке. В этом случае вы можете непосредственно в html-коде страницы перечислить (как было показано мною выше) текстовые надписи для месяцев и дней недель. Все хуже, если ваша аудитория из разных стран. В этом случае вам нужно определить то какая локаль (совокупность информации о языке, национальных настройках, формате чисел, дат и денежных единиц) используется клиентом (точнее выбрана в его браузере) и присвоить соответствующие локали значения для свойств MONTHS_LONG и WEEKDAYS_SHORT. Для того, чтобы в

javascript узнать выбранную локаль, нужно обратиться к объекту navigator:
  1. // узнаем текущую локаль
  2. var locale = '?';
  3. if ( navigator.language )
  4.   locale = navigator.language;
  5. else if ( navigator.browserLanguage ) 
  6.   locale = navigator.browserLanguage;
  7. else if ( navigator.systemLanguage )
  8.   locale = navigator.systemLanguage;
  9. else if ( navigator.userLanguage )
  10.   locale = navigator.userLanguage;
  11. // и выводим ее на экран
  12. alert ('locale = ' + locale);
Например, для моего браузера значение переменной locale будет равно слову "ru", так что остается только написать множество условных проверок, и присвоить свойствам MONTHS_LONG и WEEKDAYS_SHORT значения соответствующие переменной locale. И делать так не стоит. Дело вовсе не в том, что размер javascript-файла резко увеличивается (если, конечно, вы не решили перечислить названия месяцев и дней недели для всех пяти с чем-то тысяч существующих на Земле языков). В практике при разработке сайтов с мультиязычной аудиторией все равно приходится делать значительные правки шаблона страниц для каждого языка: свои картинки, разные надписи и стили т.к. для различных языков разметка текста может меняться, и даже заголовок сайта может банально не уместиться в отведенном ему месте. А эти правки вы можете сделать только в php-коде. Например, вызвав функцию getallheaders, вы получите ассоциативный массив http-заголовков, которые были отправлены браузером на сервер. Наибольший интерес представляет элемент этого массива Accept-Language (значением может быть таким: "ru,en-us;q=0.7,en;q=0.3") и может быть Accept-Charset (а вот пример его значения "windows-1251,utf-8;q=0.7,*;q=0.7"). Из этой информации вполне можно определить язык клиента и сформировать для него html-страницу (в том числе подставить правильные надписи и для элемента calendar).

Следующая опция, управляющая поведением календаря, - ограничение на диапазон дат, которые можно выбирать. Например, делая систему резервации билетов на следующий сезон в театре глупо позволять клиенту выбрать дату, находящуюся в прошлом или далеком будущем. При вызове конструктора календаря я должен в объект config поместить два свойства: MINDATE и MAXDATE.
  1. var config = {MINDATE: "03/10/2008",MAXDATE: "04/10/2008"};
  2. c = new YAHOO.widget.Calendar("put_it_here",config);
Теперь ячейки таблицы календаря не попадающие в отрезок от десятого марта по десятое апреля 2008 г. будут не доступны для выделения (см. рис. 2).



Календарь может быть использован для выбора не одной даты, а любого их количества. Включает этот режим свойство MULTI_SELECT, а свойство SELECTED указывает то, какие ячейки календаря должны быть выбраны:
  1. var config = {MULTI_SELECT : true, SELECTED: "10/12/2008,10/16/2008-10/20/2008"};
Обратите внимание на то, как задается перечисление выбранных дат: к использованию по-умолчанию в качестве разделителя компонент даты символа "/" и порядку "месяц/день/год" мы уже привыкли. Так что запоминайте: свойство SELECTED представляет собой строку, в которой перечислены несколько дат через запятую, либо диапазон дат (в этом случае даты разделаются знаком дефиса).

В практике при вводе большого количества документов средства быстрой навигации по календарю становятся критически важными. Оператор не будет сто раз кликать на кнопку перехода на месяц назад, когда нужно выбрать дату, находящуюся далеко в прошлом. Первым шагом в создании удобного интерфейса будет вывод на экран элемента управления календарь показывающего сразу не один месяц, а несколько месяцев (календарей расположенных рядом друг с другом и действующих как единое целое)
  1. c= new YAHOO.widget.CalendarGroup("put_to_here", {PAGES:3}); 
  2. с.render();
В случае, если вы не укажете значение для свойства PAGES, то оно будет полагаться равным двум. Важный момент в том, что хотя набор свойств, которые можно указать при создании CalendarGroup совпадает с набором свойств допустимых для одиночного календаря, но указывать эти свойства нужно в правильном порядке (строго до свойства PAGES), иначе набор календарей работает с ошибками (только не спрашивайте, почему разработчики Yahoo UI сделали так). Вторым способом улучшить навигацию по календарю является включение режима navigator. В этом режиме расположенная вверху календаря надпись с указанием текущего года и месяца превращается в кнопку, по нажатию на которой всплывает диалоговое окно, в котором можно ввести в текстовое поле номер года и выбрать месяц в падающем списке (см. рис. 3).



Следующий пример не только покажет то, как перевести календарь в navigator-режим, но также покажет, как можно настроить внешний вид всплывающего окна (заменить текст надписей расположенных рядом с полями для выбора года и месяца):
  1. var navConfig = { 
  2.   strings : { 
  3.     month: "Выберите месяц", 
  4.     year: "Укажите год", 
  5.     submit: "Принять выбранную дату", 
  6.     cancel: "Отказаться", 
  7.     invalidYear: "Значение года не верное"  
  8.   }, 
  9.   monthFormat: YAHOO.widget.Calendar.LONG, 
  10.   initialFocus: "month"  
  11. }; 
  12.  
  13. var config = { PAGES: 2, navigator: navConfig, MULTI_SELECT : true, SELECTED: "10/12/2008" };
  14. c = new YAHOO.widget.CalendarGroup("put_it_here",config); 
  15. c.render();
Первым шагом я создал объект navConfig, в котором задал значения текстовых надписей (за что отвечает каждая из конфигурационных переменных очевидно из рис. 3). Свойство monthFormat должно быть равно одной из предопределенных констант в классе YAHOO.widget.Calendar (кроме показанного в примере LONG, есть еще MEDIUM и SHORT). В случае, если пользователь введет в текстовое поле для года некорректное значение (например, отрицательное), то на экране появится сообщение об ошибке указанное в свойстве invalidYear (вот правда, если выбрать дату, находящуюся вне допустимого диапазона MINDATE и MAXDATE, то YUI на это, увы, никак не прореагирует). Следующим шагом сделать календарь более удобным будет добавление такого режима работы календаря, когда помимо кнопок перемещения вперед-назад на один месяц, есть кнопки для перемещения сразу не на один месяц, а на полгода вперед и назад, затем на год, пять и десять лет. "Волшебной" конфигурационной переменной, так меняющей внешний вид календаря нет, но когда это нас останавливало? Предположим, что вы создали чуть выше календаря набор кнопок, нажатия на которые и должно приводить к изменению текущей даты:
  1. <button onclick="doClickSub5Years()">-5 years</button>
Внутри функции doClickSub5Years я сначала заставлю календарь перейти на пять лет назад, а затем выведу на экран значение переменной "pagedate" (ее значение должно быть равно тому месяцу, который сейчас активен в календаре):
  1. function doClickSub5Years (){
  2.   c.subtractYears(5);
  3.   var pagedate = c.cfg.getProperty("pagedate");
  4.   alert (pagedate); 
  5. }
По аналогии, кроме функции subtructYears есть методы subtractMonths (перейти на заданное количеством месяцев в прошлое), а методы addMonths и addYears позволяют двигаться в "будущее". Если вы будете внедрять календарь на страницу, то, скорее всего это будет сделано вместе с формой отправляющей данные на сервер, т.к. календарь не является ни текстовым полем, ни списком значений или радио-кнопкой, то выбранные в календаре значения не будут отправлены на сервер - нам нужно самим об этом позаботиться. Можно либо привязать к форме событие onsubmit, внутри которого обратиться к объекту календаря, спросить его какие ячейки (даты) были выделены клиентом, а затем эти значения поместить внутрь скрытых (hidden) полей формы. Можно сделать и по-другому. Не дожидаясь отправки формы, подписаться на получение извещений от календаря "была выбрана дата", внутри же функции обработчика этого события присвоить значение, выбранное в календаре какому-нибудь текстовому полю формы. Можно даже сделать обратную связь, когда пользователь может вводить в это текстовое поле дату, и эта дата автоматически будет назначаться календарю. Для примера я создал в html-коде страницы текстовое поле, в которой должна появляться выбранная в календаре дата, а также кнопку, по нажатию на которую календарь должен появиться:
  1. <input type="text" id="dateof" onchange="updateCalendar()" />
  2. <button onclick="showCalendar()">show calendar</button>
В код создания календаря нужно внести небольшие правки: назначить обработчик события "выбрана дата", а также сразу после того как календарь будет создан (render), его нужно спрятать: ведь показываться он должен только после нажатия на кнопку:
  1. var config = {DATE_FIELD_DELIMITER : '.',
  2. MDY_YEAR_POSITION: 1, MDY_MONTH_POSITION: 2, MDY_DAY_POSITION: 3};
  3. c = new YAHOO.widget.Calendar("put_it_here",config); 
  4. c.selectEvent.subscribe(onSelectDate); 
  5. c.render(); 		
  6. c.hide();
Создавая календарь я выполнил настройку того в каком формате будет представлена дата: так стиль "месяц/день/год" я предпочел такому "день.месяц.год". Код функции onSelectDate тривиален: в качестве параметров ей передается массив хранящий сведения о всех выбранных календарем датах. Каждая из дат в свою очередь представлена в виде массива компонент даты ([[yyyy,mm,dd],[yyyy,mm,dd]]). Разобрав массив компонент даты на части, я должен их снова собрать, но уже в виде строки (с соответствующими разделителями компонент даты) и поместить внутрь текстового поля "dateof".
  1. function onSelectDate (type, args) { 
  2.   // первый аргумент функции type всегда равен "select" и нас не интересует
  3.   if (! args) return; // извлекаем компоненты выбранной даты
  4.   var date = args[0][0];
  5.   var year  =date[0];
  6.   var month  =date[1];
  7.   var day  =date[2];
  8.  
  9.   YAHOO.util.Dom.get("dateof").value = year+"."+month+"."+day; 
  10.   // после выбора даты, прячем календарь
  11.   c.hide(); 
  12. }
Мне осталось только привести пример кода для функций showCalendar и updateCalendar. Там тоже нет ничего сложного: в первом случае я должен показать окошко календаря (метод show):
  1. function showCalendar (){
  2.   c.show(); 
  3. }
Во втором же случае я получаю значение, введенное в текстовое поле "dateof " и вызвал метод "select" у объекта календаря, так чтобы он перешел на новую дату. Значение, которое вернет метод select - это массив выбранных дат. В действительности, при вызове select я могу передать строку, кодирующую несколько дат, которые нужно выделить и даже их диапазоны, так что использование массива оправдано. Т.к. выбор даты на календаре, вовсе не означает то, что при следующем его открытии будет активен именно тот месяц, которому принадлежит эта дата, то я должен изменить свойство "pagedate" и перерисовать (render) календарь заново.
  1. function updateCalendar (){
  2.  var text = YAHOO.util.Dom.get("dateof").value;
  3.  if (!text)
  4.      return;
  5.  var selectedDate = c.select(text) ; 
  6.  c.cfg.setProperty ("pagedate", selectedDate[0]);
  7.  c.render (); 
  8. }
Теперь рассмотрим то, как можно управлять внешним видом календаря, как назначить стили оформления даже для индивидуальных ячеек (дат). Календарь - на самом деле это таблица, в ячейках которой хранятся значения дат. Пусть наш календарь отображает содержимое месяца октябрь, 2008 г. В таком случае тег таблицы будет иметь дополнительный css-класс y2008, тег tbody (тело таблицы) получит css-класс "m10", каждая из строк (недель) будет иметь css-класс кодирующий номер недели от начала года (например, w40). И наконец, каждая ячейка имеет css-классы кодирующие номер дня недели (wd0 - воскресение) и конкретное число, отображаемое в этой ячейке (d2). Таким образом, вы можете легко найти нужную ячейку в календаре и назначить ей стиль, просто перечислив цепочку идентификаторов, через которые нужно пройти:
  1. tr.w43 td { background-color:blue !important; }  	
  2. td.d22 { background-color:yellow !important; }
В первом случае я выделил синим фоновым цветом всю строку соответствующую 43 недели года, во втором - конкретный день 22 числа. Не смотря на кажущуюся простоту такой стилизации календаря, здесь полно подводных камней и главный из них использования css-правила "! important". Дело в том, что на каждый элемент календаря уже назначено множество yui стилей по-умолчанию, и эти стили имеют высокий уровень приоритета. Так что без указания "!important" мой стиль просто не применится. С другой стороны, если нужно выделить всю строку синим цветом, а затем только одну конкретную ячейку желтым (как в примере), то правило "! important" начинает мешать и один из стилей не сработает. Как вывод лучше всего цепочку css-стилей использовать для вычисления ссылки (идентификатора того html-элемента, который соответствует определенной дате), а затем с помощью javascript адресно назначать ему стили.