...
...

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

Я продолжаю серию материалов, рассказывающих про javascript-библиотеку Yahoo UI. Прошлая статья была посвящена такому элементу управления, как календарь (группа календарей). Мы научились внедрять его в html-страницу, управлять параметрами внешнего вида, выполнять интернализацию интерфейса. Сегодня я завершу рассказ о календаре и перейду к рассмотрению других элементов управления.

В прошлый раз мы остановились на том, что я показал, как можно выполнить настройку внешнего вида календаря с помощью css-стилей. Когда мы внедряем в веб-страницу календарь, то сгенерированный html-код представляет собой таблицу со строками и ячейками. У каждой строки (недели) есть свой css-стиль — например, "w30" (30-я неделя года). В свою очередь, у каждой ячейки (конкретного дня недели) есть свой набор стилей — например, "wd5 d7" (день недели 5 и номер дня месяца 7). Так что вы можете задать индивидуальное css-оформление для любого дня. В практике бывают более сложные ситуации, когда ячейки календаря должны отличаться не только стилевым оформлением, но и своим содержимым. И, более того, содержимое этих ячеек может изменяться динамически. Например, если вы создаете приложение ежедневник, то захотите расширить календарь так, чтобы он показывал не только дату, но и краткую аннотацию дел (может быть, стилизованные картинки-иконки), запланированных на этот день. Естественно, наш календарь будет меняться динамически: так, по клику на ячейке мы должны показать диалоговое окно для выбора времени, когда должна быть выполнена в этот день некая работа, а также описание самой работы. В YUI (равно как и в тысяче и одной javascript-библиотеке) активно используется концепция renderer'ов, т.е. специализированных функций, которые вызываются всякий раз, когда нужно сформировать графическое представление некоторого элемента (ячейки календаря). Так, когда мы будем изучать элемент управления DataTable (таблицу с данными), то будем сталкиваться с понятием render'ов, formatter'ов буквально на каждом шагу. Пока же сделаем несложный пример: на странице размещается календарь, два текстовых поля и кнопка. В текстовые поля вводятся даты, а по нажатию на кнопку запускается процесс перерисовки календаря. Предварительно я назначаю календарю функцию render так, чтобы внутри ее проверить, попадает ли каждая из рисуемых ячеек в выбранный пользователем интервал. И если это так, то ячейки примут специальное цветовое выделение:
min date: <input type="text" id="mindate" />
max date: <input type="text" id="maxdate" />
<button onclick="doRenderCalendar()">render</button>
<!-- и теперь укажем то, куда вставлять сам календарь -->
<div id="put_it_here"></div>

Внутри функции обработчика события нажатия на кнопку я назначаю renderer на диапазон дат. Первый параметр функции addRenderer — это строка следующего вида: "1/13/2008-1/16/2008". Как видите, две даты (в формате месяц/день/год) разделяются знаком дефиса:
var c = null;
function startApp() {
с = new YAHOO.widget.Calendar("put_it_here",{});
c.render (); };
// теперь обработка нажатия на кнопку:
function doRenderCalendar (){
c.addRenderer(YAHOO.util.Dom.get('mindate').value+"-"+YAHOO.util.Dom.get('maxdate').value, c.renderCellStyleHighlight1);
c.render (); }

Важно, что после вызова addRenderer внешний вид календаря не изменится до тех пор, пока вы не прикажете его перерисовать (render). Запись "c.renderCellStyleHighlight1" означает то, что я хочу возложить ответственность за формирование внешнего вида ячейки на одну из встроенных в YUI функций. Так, существуют четыре функции (renderCellStyleHighlight1, renderCellStyleHighlight2, renderCellStyleHighlight3,
renderCellStyleHighlight4), изначально используемые YUI для отрисовки ячеек таблицы в состоянии "на ячейку наведена мышь, и ее нужно подсветить". Есть еще стиль renderBodyCellRestricted (ячейка запрещена для выделения), renderCellStyleSelected (ячейка выделена), найдется и еще парочка стилей, вот только ясно, что стандартными стилями обойтись нельзя (их мало, и они используются для своих целей). Так что попробуем создать свою функцию renderer. Сначала я определил css-стиль, которым буду оформлять ячейку таблицы:
.my_class {color: red;}

Затем так же, как и в прошлый раз, я назначаю календарю функцию renderer:
c.addRenderer("1/13/2008-1/16/2008", onMyRenderer);
// не забываем выполнить перерисовку календаря
c.render ();

В качестве параметров функции renderer'у передается, во-первых, дата, которая должна быть отрисована в ячейке, а во-вторых, ссылка на тег "td". Именно к нему и будет применено стилевое оформление.
function onMyRenderer (workingDate, cell){
YAHOO.util.Dom.addClass(cell, 'my_class');
cell.innerHTML = workingDate.getDate();
return YAHOO.widget.Calendar.STOP_RENDER; }

Для того, чтобы динамически добавить к произвольному тегу (ячейке "td" в нашем случае) css-класс "my_class", я использовал рассмотренные еще в первой статье серии функции объекта YAHOO.util.Dom (Dom умеет добавлять стили к элементу, удалять стилевое оформление, проверять наличие примененного к элементу css-стиля). Основное внимание обратите на последнюю строку функции onMyRenderer. Чему равна константа YAHOO.widget.Calendar.STOP_RENDER, и зачем я вернул ее из функции renderer'а? Все дело в том, что, когда я назначал функцию renderer, то сделал это с помощью вызова фунции addRenderer, и из названия можно догадаться, что добавление очередной функции renderer'а не приводит к потере всех предыдущих функций render'ов. Calendar сохраняет у себя внутри список всех назначенных на определенную дату render'ов и, когда нужно отрисовать некоторую ячейку, перебирает и вызывает эти функции-renderer'ы в порядке, обратном порядку их добавления (дисциплина LIFO). Каждая из функций render'ов должна не только назначить ячейке какое-то оформление, но и решить: следует ли дальше разрешить вызывать оставшиеся render'ы. Если вернуть, как показано в примере, константу YAHOO.widget.Calendar.STOP_RENDER, то функция будет последней в цепочке вызовов. Так, запустив пример, я увидел, что, хотя для диапазона дат 1/13/2008-1/16/2008 цвет шрифта в ячейках стал красным, но выделить эти ячейки невозможно (в ячейках нет ссылок, по которым можно кликать), также при наведении мыши у ячеек не меняется подсветка фона. Все это поведение добавляют стандартные функции-render'ы, от вызова которых я отказался. Если же поменять код примера так, чтобы функция onMyRenderer ничего не возвращала, то, хотя ячейки снова будут реагировать на клик или наведение мыши, но красное оформление шрифта будет утеряно. Это произойдет потому, что следующая в стеке вызовов функция-render затрет мои изменения. Как вывод: render'ы можно и нужно использовать, но, чтобы не попасть впросак, предварительно подсмотрите в исходном коде YUI то, какую работу делает функция renderCellDefault, как назначаются и обрабатываются события клика по ячейкам таблицы.
Следующий элемент управления, о котором мы поговорим — ColorPicker, — представляет собой диалоговое окно выбора цвета. Использование ColorPicker очень похоже на работу с Calendar'ом: мы так же загружаем с помощью универсальной утилиты YUI loader'а модуль colorpicker. Затем создаем внутри html-страницы блок div, который будет играть роль контейнера, внутрь которого будет помещен сгенерированный html-код для colorpicker'а, и так же, как в прошлый раз, идентификатор этого блока будет передан как первый параметр конструктору ColorPicker:
<div id="put_it_here"></div>

Второй же параметр конструктора представляет объект с настройками, управляющими поведением и деталями внешнего вида ColorPicker'а:
var c = null;
function startApp() {
var config = {};
c = new YAHOO.widget.ColorPicker("put_it_here",config); }

Внешний вид получившейся страницы показан на рис. 1. По сравнению с тем, как мы создавали calendar, есть небольшое отличие — нам не нужно вызывать метод render для завершения конструирования окошка выбора цвета: метода render вообще нет, и сразу после вызова конструктора на экране должен появиться colorpicker. Также обратите внимание на то, что внешний вид ползунков выбора цвета несколько необычен — вместо них выводится "битая" картинка, сигнализируя о том, что ColorPicker не может загрузить и отрисовать картинки, на которых и изображен этот ползунок. В исходном коде модуля colorpicker путь к предполагаемому месту расположения картинок задан жестко и не всегда верен. В следующем примере я не только укажу правильный путь к картинкам ползунков, но и включу режим, когда в окне colorpicker'а, кроме шкалы RGB (текстовых полей, в которых отображается разложение цвета на red-, green-, blue-компоненты), также отображены шкала HSV и текстовое поле для шестнадцатеричного представления цвета (т.е. формат #RRGGBB).
var config = { images: {
PICKER_THUMB: "js/colorpicker/assets/picker_thumb.png",
HUE_THUMB: "js/colorpicker/assets/hue_thumb.png" },
showhsvcontrols: true,
showhexcontrols: true };

Внедрение ColorPicker'а в html-форму имеет ряд хитростей. Немного пугает то, что окошко выбора цвета состоит из семи текстовых полей (input type="text"), и все они будут отправлены на сервер вместе с остальным содержимым формы. Как управлять тем, какие будут имена текстовых полей, и можно ли запретить их отправку:
// идентификаторы текстовых полей
var cids = YAHOO.lang.merge(YAHOO.widget.ColorPicker.prototype.ID, {
R: "txtR", G: "txtG", B: "txtB" });
// надписи рядом с текстовыми полями
var txts = YAHOO.lang.merge(YAHOO.widget.ColorPicker.prototype.TXT, {
R: "red", G: "green", B: "blue" });
// конструируем настройки
var config = {ids: cids, txt: txts};
c = new YAHOO.widget.ColorPicker("put_it_here",config);

Объект config с настройками для ColorPicker'а содержит два специальных свойства: ids и txt. В первом случае значением для ids является массив с расшифровкой того, какие имена будут назначены для текстовых полей. Кроме показанных в примере тегов R, G, B, есть теги H, S, V, HEX. Кроме того, можно изменять значения id для html-элементов, которые играют роль ползунков выбора цвета, но практической пользы это не имеет, т.к. эти данные на сервер все равно отправлены не будут. Объект txts хранит значения для подписей, которые выводятся рядом с соответствующими текстовыми полями. Теперь главное — что означает YAHOO.lang.merge? Просматривая исходный код YUI, иногда остается только удивляться, что же "курили" разработчики yahoo, когда писали свой код. Предположим, что вы хотите вывести в ColorPicker'е текстовые поля для R, G, B, H, S, V компонент, а идентификаторы или текстовые надписи меняются только для R, G, B полей. Все равно вы обязаны явно предоставить значения для всех используемых полей. Если для какого-то из полей вы не предоставили данных, то YUI не использует ни значений по умолчанию, ни сообщает об ошибке или блокирует создание элемента colorpicker'а — вместо этого YUI просто не работает. Для того, чтобы не перечислять все значения надписей и идентификаторов элементов, я воспользовался стандартной для YUI функцией merge, которая получает в качестве входных параметров два массива (объекта) и выполняет их объединение (здесь YAHOO.widget.ColorPicker.prototype.ID и YAHOO.widget.ColorPicker.prototype.TXT — это массивы со значениями по умолчанию). Создав colorpicker, давайте попробуем программно узнать, какой цвет сейчас выбран, а затем поменять его на другой. И тут нас ждет "засада", пусть небольшая, но неприятная. Разработчики Yahoo создали отлично работающий элемент colorpicker, но сами им, похоже, не пользуются, т.к. простого способа узнать то, какой сейчас выбран цвет в разрезе RGB, или HSV, или HEX, нет. Нет функций вроде getRed, getBlue, getHue. Единственная доступная (public) функция в составе colorpicker'а — setValue, предназначенная для того, чтобы указать, какой цвет следует выбрать:
c.setValue ([100, 255, 0], true);

Первый параметр для setValue — это массив RGB-компонент. Второй же параметр — булева переменная. Если она равна true, то colorpicker работает в "тихом" режиме, т.е. после того, как цвет был изменен, событие, извещающее заинтересованные функции об изменении цвета, не выбрасывается. В следующем примере я подпишусь на получение извещений об изменении цвета:
c = new YAHOO.widget.ColorPicker("put_it_here",config);
c.on("rgbChange", onColorChange);

Функция onColorChange получает в качестве единственного параметра объект, содержащий два свойства: newValue и prevValue. Это массивы из трех чисел: компонент red, green, blue. В случае, если вас интересует разложение цвета по HSV или его шестнадцатеричное представление, следует воспользоваться услугами еще одного YUI-класса YAHOO.util.Color, например, так:
function onColorChange (e){
var co = YAHOO.util.Color;
alert (co.rgb2hsv (e.newValue) );
alert (co.rgb2hsv (e.newValue[0], e.newValue[1], e.newValue[2]) ); }

Метод rgb2hsv достаточно умный и умеет работать как с отдельно переданными компонентами RGB-цвета, так и с его представлением в виде массива. В составе класса YAHOO.util.Color найдется и еще парочка полезных функций: hsv2rgb выполняет обратное преобразование из HSV-палитры в RGB. Функция rgb2hex формирует представление цвета в виде шестнадцатеричного числа. Еще есть hex2rgb, dec2hex, hex2dec — назначения этих функций вполне определенно говорят о том, какие преобразования они выполняют. В тех случаях, когда выбранный пользователем RGB-цвет должен быть преобразован в один из 216 websafe-цветов, используйте метод websafe. Теперь вернемся назад — к задаче, как узнать в произвольный момент времени то, какой цвет сейчас выбран в colopicker'е. Так неужели нет способа узнать цвет, кроме подписки на получение события "цвет был изменен"? Несмотря на то, что в документации yui про это не говорят, можно сделать так:
alert ("hex = "+ c.get(c.OPT.HEX));
// или даже так, только это очень-очень громоздко
alert ("red = " +YAHOO.util.Dom.get(YAHOO.widget.ColorPicker.prototype.ID.R).value )

Во втором случае я получаю ссылку на html-элемент, используемый colorpicker'ом для отображения компоненты красного цвета. Получать таким образом доступ к значению цвета глупо, а вот если вам нужно выполнить тонкую настройку внешнего вида текстового поля — самое то. Последнее, о чем стоит рассказать, говоря про colorpicker, — это то, как можно встроить вызов этих элементов (отображение) в ответ на какое-то действие пользователя. Например, окошко colorpicker'а занимает достаточно много места на экране, так что при заполнении формы удобно создать текстовое поле для ввода цвета и рядом расположить кнопку, по нажатию на которую будет показываться colorpicker, а после выбора цвета — исчезать. К счастью, в calendar подобная функциональность введена изначально: в прошлой статье я показал, как с помощью методов show и hide можно показывать и прятать окошко выбора календаря. А вот для colorpicker все гораздо хуже: методов show и hide нет. Решением будет создать окошко colorpicker'а не на самом body страницы, а на диалоговом окне (как работать с классом Dialog и SimpleDialog, можно прочитать в четвертой статье серии). Сначала я создал в теле html-странички блок div, который играет роль контейнера для SimpleDialog:
<div id="dialogcontainer">
<div class="hd">Выберите цвет</div>
<div class="bd" id="container_for_color_picker" style="position: relative;"> </div>
</div>

А вот в это текстовое поле будет помещен выбранный пользователем цвет:
<input type="text" id="colorfield" />

Содержимое блока "hd" тривиально: это значение заголовка для диалогового окна. А вот содержимое "тела" (bd) хитрее: я должен обязательно дать этому блоку идентификатор, иначе не смогу сослаться на этот блок при создании colorpicker'а. Стиль же style="position: relative;" необходим для того, чтобы colorpicker не залезал из "тела" диалогового окна на его заголовок (все дело в том, что компоненты colorpicker'а позиционируются абсолютно, что и приводит к некрасивому эффекту). Теперь пример кода, создающего сначала диалоговое окно, а после — и colorpicker'а: d = new YAHOO.widget.SimpleDialog("dialogcontainer", {
width: "400px", height: "300px", effect:{effect:YAHOO.widget.ContainerEffect.FADE, duration: 1}, modal:true, fixedcenter:true,
visible:false, draggable:false,
buttons: [
{text: "Принять", handler:onAcceptColor},
{text: "Отказаться", handler:onDiscardColor, isDefault:true}
] } );
d.render ();
// теперь создаем colorpicker
var config = { };
c = new YAHOO.widget.ColorPicker("container_for_color_picker",config);

Последний шаг — написать функции, которые будут вызываться диалоговым окном, при нажатии на кнопки Принять и Закрыть. Обе они должны прятать диалоговое окошко, а функция для кнопки Принять еще должна поместить выбранное цветовое значение в текстовое поле на форме:
// по нажатию на кнопку отмены просто прячем диалог выбора цвета
function onDiscardColor (){
d.hide (); }
// а здесь значение цвета нужно сохранить
function onAcceptColor (){
var color = c.get(c.OPT.RED) + ", "+ c.get(c.OPT.GREEN)+", " + c.get(c.OPT.BLUE);
YAHOO.util.Dom.get('colorfield').value = color;
d.hide (); }

Вам осталось сделать заключительный шаг: отобразить диалоговое окно (например, по нажатию на кнопку, расположенную рядом с текстовым полем). Сделайте это, вызвав метод "d.show()", и вы получите картинку, изображенную на рис. 2. На этом рассказ про colorpicker завершен, а в следующий раз я расскажу про парочку новых компонентов, которые появились в YUI в версии 2.6.

black-zorro@tut.by, black-zorro.com

© Компьютерная газета

полезные ссылки
Охранные видеокамеры