Сложные интерфейсы на javascript вместе Yahoo UI. Часть 13
В прошлой статье серии я закончил рассказ о таком визуальном элементе в библиотеке Yahoo UI, как colorpicker (окошко для выбора цвета). Сегодня мы продолжим изучать другие визуальные компоненты: нас ждет знакомство с carousel и slider.
Что такое carousel? С английского это слово переводится как “карусель”. И в полном соответствии со своим названием этот элемент управления представляет собой набор закладок (т.е. чем-то похож на знакомый уже нам TabView). Только вместо ярлычков с названиями закладок есть две кнопки перемещения по закладкам вперед и назад (см. на рис. 1 отмеченное буквой “B”).
Для перемещения по кадрам carousel вы может использовать не только эти кнопки, но и клавиши “вперед”, “назад”, “вверх”, “вниз”, “page up” и “page down”. Рисунок 1 соответствует состоянию carousel, когда на ней размещено 6 картинок, размер кадра равен 3 (три одновременно отображаемые картинки). Также вы видите наверху, рядом с кнопками навигации по carousel, две точки (на рисунке обозначены буквой “A”). Это условные обозначения для страниц с кадрами, нажимая по этим точкам, вы будете быстро переходить на выбранную страницу. Также, carousel содержит средства анимации смены отображаемых страниц, создавая тем самым иллюзию того, что содержимое carousel и действительно крутится на какой-то невероятной карусели. Появление carousel в недавно вышедшей версии yui 2.6, очень удачно совпало для меня с задачей, где этот элемент управления пригодился. Для одного сайта мне нужно было сделать страничку, где отображались бы картинки комикса. Момент в том, что для того чтобы разместить на такой страничке более-менее сложный сюжет требуется много места, которого всегда не хватает. Также комикс может содержать интригу, которую легко испортить, если показать все сцены сюжета одновременно. Как вывод: желательно показывать картинки комикса по очереди, так чтобы пользователь видел единовременно только одну картинку, и мог с помощью кнопок “вперед” и “назад” перемещаться по остальным картинкам сюжета. Сразу предупреждаю, что использовать carousel в реальных проектах без изрядной доработки исходных кодов “напильником” не получится – качество кода просто ужасное.
Создать carousel просто: начнем с того, что загрузим с помощью yui loader-а модуль carousel. Затем в теле веб-страницы размещается блок div, он будет играть роль контейнера для carousel. Внутрь этого контейнера yui вставит элементы управления для навигации. А что касается, собственно, содержимого, то у нас есть два варианта. Либо эти данные изначально присутствуют в странице, и после загрузки страницы помещаются внутрь carousel. Это так называемая техника progressive enhancement, позволяющая просмотреть страницу тем редким пользователям, у которых отключен javascript. И второй вариант – данные для carousel загружаются динамически с помощью javascript|ajax. Итак, вот пример html-разметки, где я внутрь блока div, контейнера для carousel, помещаю тег ol (неупорядоченный список ul использовать нельзя) и каждый из элементов этого списка (картинка) – отдельный кадр для carousel:
<div id="put_it_here">
<ol>
<li><img src="img/1.png" /></li>
<li><img src="img/2.png" /></li>
<li><img src="img/3.png" /></li>
</ol>
</div>
Теперь привожу пример javascript-кода, который создает и настраивает некоторые из параметров управляющие внешним видом carousel. И как всегда первый параметр конструктора carousel – идентификатор тега с данными, а второй – объект с настройками:
var c = null;
function startApp() {
c = new YAHOO.widget.Carousel("put_it_here",
{ isCircular: true, isVertical: false, animation:
{ speed: 2, effect: YAHOO.util.Easing.easeIn }
}
);
c.render();
c.show();
}
Настроек для carousel немного и почти все они приведены в примере выше: переменная isCircular включает режим, когда кнопки перемещения вперед и назад по кадрам при достижении последнего и первого кадров выполняют автоматический переход на начало или конец списка. Параметр animation состоит из двух свойств: время анимации задается (в секундах) свойством speed. А свойство effect управляет используемым эффектом перехода между кадрами. Кроме показанного в примере YAHOO.util.Easing.easeIn, есть еще и easeOut, easeBoth и многие другие правила анимации. Я рекомендую обратиться ко второй статье серии, чтобы освежить воспоминания о том какие эффекты есть в YUI и как можно управлять их параметрами. Последняя показанная в примере переменная isVertical служит для того, чтобы управлять ориентацией выводимых на экран элементов: в режиме vertical кнопки навигации по carousel размещаются вверху, а элементы … а элементов просто нет. Вот таким замечательным багом (причем стабильно проявляющимся в различных версиях браузеров) порадовали нас разработчики YUI. Кроме того, формально заявлено свойство revealAmount (задается в процентах). Оно должно было бы отобразить на одной странице carousel не только “целые кадры”, но и частичку (в соответствии со значением revealAmount) двух кадров расположенных до первого и после последнего из кадров текущего фрейма. Работает данное свойство крайне не корректно и его лучше не использовать. Еще одна конфигурационная переменная – numVisible, служит для того, чтобы управлять количеством кадров появляющихся на одной странице. Если же создать carousel без указания этого числа, то одновременно будут отображены три кадра. Если вы хотите, чтобы сразу же после создания carousel-и был открыт не первый кадр, а, например, 3, то укажите это число как значение конфигурационной переменной firstVisible.
Когда мы отображаем информацию с помощью carousel, то грех не воспользоваться методикой отложенной загрузки ресурсов. Так в моем примере с комиксом, я мог изначально загрузить только первые пару кадров, а остальные загружал бы по требованию. Т.е. по мере просмотра будут загружаться остальные картинки (если делать загрузку с некоторым опережением на пару кадров, то просмотр будет плавным, без задержек). Следующий пример показывает то, как наполнить carousel данными с помощью javascript, пока без ajax:
c.addItem ('<img src="img/8.pnh" />');
Теперь я покажу то, как можно узнать какой кадр выбран в carousel и изменить его на другой:
alert(c.get('firstVisible'));
c.set('firstVisible', 3);
Возвращаясь к примеру с динамической подзагрузкой кадров для carousel, нам нужно научиться делать две вещи. Во-первых: создать carousel c нужным количеством кадров (естественно, что сами кадры будут пустыми). А во-вторых: назначать функции, срабатывающие тогда, когда пользователь хочет перейти на определенный кадр и внутри этой функции загрузить с помощью ajax содержимое кадра. Перед тем как создать carousel я изменяю значение свойства ITEM_LOADING:
YAHOO.widget.Carousel.prototype.CONFIG.ITEM_LOADING = '<b>Please wait, content is loading</b>';
Дело в том, что когда я создаю carousel, предназначенную для отображения еще не загруженных элементов, то вначале на экране появляется надпись “мол, подождите, сейчас содержимое кадра будет загружено”, также появляется картинка, обозначающая прогресс загрузки. Если же вы хотите изменить внешний вид кадра на что-то другое, то нужно указать этот фрагмент html как значение свойства ITEM_LOADING. Теперь можно и создать carousel, обратите внимание на значение свойства numItems (количество элементов в carousel):
c = new YAHOO.widget.Carousel("put_it_here", {numVisible: 2, numItems : 10} );
Необходимо выполнить еще небольшие правки в html-шаблоне страницы: внутрь блока div помещается пустой список “ol”. А для того, чтобы carousel не “расползался” до тех пор, пока мы не загрузим наполнение кадров, нужно задать стилевые параметры для элемента списка: указать ширину и высоту кадра:
.yui-carousel-element li {height: 200px; width: 200px; }
<div id="put_it_here" >
<ol> </ol>
</div>
Теперь я назначаю для carousel функцию обработчик события “нужны данные”:
c.addListener ('loadItems', onLoadItems);
Неприятно то, что carousel не обратится к этой функции в самый первый раз, сразу после своего создания, а только в ходе навигации по кадрам. Поэтому я сам явно вызвал функцию onLoadItems, передав как параметры сведения о том, какие элементы выбраны: first и last указывают на индексы этих элементов (отсчет начинается с нуля):
onLoadItems ({first: 0, last: 1});
Теперь осталось только привести код функции onLoadItems. И здесь нас ждет еще несколько багов. Рассмотренная чуть выше функция добавления элементов addItem помимо первого параметра (текстового представления элемента, который добавляется) может еще принять и второй параметр – позицию, куда нужно вставить новый элемент. И этот метод отлично работает: так я могу вставить новый элемент в carousel, перед первым из существующих:
c.addItem ('<img src="img/8.png" />', 0);
Однако внутри функции loadItems использовать прием с указанием позиции, куда вставляются данные не возможно. Т.е. показанный далее код отлично работает с ситуацией, когда данные уже присутствуют в странице или могут быть быстро и последовательно вычислены:
var lastLoaded = -1;
function onLoadItems (e){
if (e.last < lastLoaded) return;
for (var i = e.first; i <= e.last; i++)
if (i >= lastLoaded){
c.addItem ('item # '+ i);
lastLoaded = i;
}
}
Переменная lastLoaded хранит номер последнего загруженного элемента. Внутри функции onLoadItems я “пробегаюсь” в цикле по номерам элементов, которые должны быть отображены в carousel и добавляю (addItem) содержимое элементу, только в том случае, если это не было сделано ранее. У этого приема есть забавный побочный эффект: рядом с кнопками “вперед” и “назад” находится индикатор количества страниц. Так вот, несмотря на то, что при создании carousel я указал значению свойства numItems, то индикатор показывается только для тех страниц, которые уже наполнены содержимым. В случае моей задачи с комиксом это было даже полезно, т.к. не давало возможности выполнить переход на произвольную страницу впереди. Есть и другой прием динамического наполнения carousel содержимым. Представьте, что содержимое кадров загружаются с помощью ajax. Ajax - это значит асинхронный. Т.е. когда мы внутри функции onLoadItems просто делаем запрос на то, чтобы содержимое было загружено с сервера, но ведь происходит это не сразу. И более того, если содержимое каждого из кадров грузится по отдельности (в случае если страница состоит из двух или более кадров), то вовсе не факт, что ответы с сервера придут в нужном порядке. А значит при добавлении содержимого кадру, нам нужно указать номер позиции куда его вставлять. Увы и ах, но carousel “умирает” когда комбинируется отложенная загрузка содержимого и вставка элементов с указанием их позиций. Кардинальным решением будет создание carousel сразу со всеми заполненными кадрами, например, так:
c = new YAHOO.widget.Carousel("put_it_here", {numItems : 10});
//и сразу после этого заполним кадры фиктивным содержимым:
for (var i = 0; i < 10; i++)
c.addItem ('Loading ...');
Как видите, я отказываюсь от встроенной в carousel функции подмены содержимого кадра на ITEM_LOADING. Осталось только, создать функцию, обрабатывающую ситуацию перехода на новый кадр. А внутри этой функции проверить, было ли ранее загружено содержимое кадра, и если это не так, то отправить ajax-запрос на сервер. После того как наполнение кадра будет получено браузером, я перед вызовом addItem (уже можно без боязни использовать номер позиции, куда вставляется содержимое) удаляю кадр “пустышку”. Давайте начнем: сперва я назначаю обработчик события, затем вызываю метод _syncPagerUI (это внутренний метод yui), предназначенный для того, чтобы перерисовать carousel, и последним шагом явно вызываю функцию onAfterScroll, передав ей как параметры позицию первого и последнего выбранных элементов:
c.addListener("afterScroll", onAfterScroll);
c._syncPagerUI (0);
onAfterScroll ({first: 0, last: 1});
Для функции onAfterScroll мне нужно создать массив кэш, в котором будет храниться информация о том, какие кадры для carousel уже были загружены. В цикле я перебираю все кадры, находящиеся между first и last. Я беру значение last+2, так чтобы еще загрузить парочку элементов расположенных сразу после тех, которые были выбраны клиентом.
var cache = {};
function onAfterScroll (e) {
for (var i = e.first; i <= e.last + 2; i++)
if (!cache [i] && i < c.get('numItems') ){
var callback = {success: onLoadOk, argument: {frame_id: i}};
r = YAHOO.util.Connect.asyncRequest('GET', '/ydoc1/loadCarousel.php?frame_id='+ i, callback);
}
}
Для отправки ajax-запроса я использую рассмотренный в пятой части серии статей про YUI метод YAHOO.util.Connect.asyncRequest. Важно, что я передаю номер кадра не только как параметр к php-скрипту, но и помещаю эту цифру внутрь массива argument. Дело в том, что когда ответ от сервера придет назад мне нужно будет узнать то, для какого из кадров пришло наполнение:
function onLoadOk (e){
var idx = e.argument.frame_id;
c.removeItem (idx);
if (idx >= c.get('numItems') )
c.addItem (e.responseText);
else
c.addItem (e.responseText, idx);
cache [idx] = 2;
}
Здесь есть два момента, на которые нужно обратить внимание. Во-первых: несколько странный код добавления нового кадра: зачем мне нужна проверка того, что idx >= c.get('numItems') и два способа добавления элемента с указанием его позиции и без этого. Вся беда в том, что метод addItem не умеет добавлять элемент, если его индекс указывает “за последний существующий”. Второй момент: обязательно нужно сохранить в массив cache признак того, что содержимое кадра номер idx было загружено. Внимательный читатель скажет, что вместо того, что загружать кадр после того, как был выполнен переход на новую страницу не правильно. Ведь в этом случае в промежутке времени между завершением перехода и загрузкой данных для выбранного кадра с сервера carousel находится в “разобранном состоянии”. И лучше было бы начать загрузку содержимого кадра до того как переход (точнее анимация перехода) был бы выполнен. Так то оно так, но разработчикам yui лень было протестировать свой продукт до конца и события onBeforeLoad вызывается с некорректными параметрами: значение last равно не пойми чему, в случае если вы перемещаетесь по кадрам не с помощью кнопки “вперед” и “назад”, а кнопок быстрого перехода. Я для своего проекта подправил код carousel (и вам рекомендую не бояться и править исходники yui под свои потребности), так чтобы и событие beforeScroll вызывалось бы с правильными значениями first и last. Также я научил yui передавать в функцию обработки события и старые координаты видимых кадров, и новые. Еще одна из возможных доработок приведенного выше кода заключается в более умной загрузке содержимого для кадра с сервера. Например, следующий сценарий: содержимое каждого из фреймов достаточно “тяжелое” и время его загрузки велико. В случае, если пользователь делает переход по кадрам “вперед” и сразу “назад”, то мы отправляем на сервер два запроса для одного и того же кадра. Решение очевидно: значение массива cache должно принимать уже не два значения, а три. Undefined, когда загрузка кадра не начиналась, 1 - загрузка уже начата, но ответ от сервера еще не пришел, и, наконец, 2 - когда ответ пришел. Соответственно, внутри функции onAfterScroll запрос к серверу выполняется только тогда, когда код для кадра равен undefined.
На этом про Carousel на сегодня все и я перехожу к следующему визуальному компоненту – это Slider или ползунок (см. рис. 2).
Этот компонент предназначен для того, чтобы выбрать некоторое число из диапазона допустимых значений (выгодно отличаясь этим от обычного текстового поля, как с визуальной стороны, так и средствами контроля “чтобы не ввели что-то неправильное”). Slider может быть ориентирован как горизонтально (рис. 2a), так и вертикально (рис. 2b), кроме того мы можем выбирать число на XY-плоскости (рис. 2c). Еще мы можем создать “dual” slider, состоящий из двух ползунков (рис. 2d), с его помощью можно выбрать не отдельное число на диапазоне допустимых чисел, а выбрать фрагмент этого диапазона, ограничив его с двух краев. Как пользоваться slider-ом? После загрузки модуля slider с помощью yui-loader-а, мне нужно разместить в коде html-страницы “заготовку” slider-а, состоящую из блока div для фона и еще один div для thumb (ползунка):
<div id="sliderAreaOX" class="yui-h-slider">
<div id="sliderThumbOX" class="yui-slider-thumb">
<img src="img/thumb.gif">
</div>
</div>
Есть определенные требования к стилевому оформлению и блока фона и блока thumb. Именно, для этого я назначил тегам div классы yui-h-slider и yui-slider-thumb. Эти css классы также назначают определенный внешний вид slider-а (фоновые картинки). Если вас эти картинки не устраивают, то можно создать собственный стиль. Теперь можно и вызвать метод getHorizSlider:
var ox = YAHOO.widget.Slider.getHorizSlider ('sliderAreaOX', 'sliderThumbOX', 0, 500, 10);
Первый и второй параметр метода getHorizSlider (для вертикально ориентированного slider-а есть метод getVertSlider) – это идентификаторы двух блоков div: фона и ползунка. Третий и четвертый параметры задают границы диапазона, в котором может перемещаться ползунок. Последний параметр – служит для включения режима “ticks”, в котором ползунок перемещается по шкале не с произвольным шагом, а минимальными шажками по 10 … интересно, десять чего? Slider абстрагирован от конкретных единиц измерения, это могут быть метры, километры, аршины, можно перемещаться по шкале цветовых оттенков. Только помните, что для созданного шагом ранее Slider-а требуется место в 500px, плюс расстояние равное ширине ползунка. Slider не подгоняет размер фона ползунка – это должны сделать вы сами, например, так:
#sliderAreaOX {
background: url('img/bgSlider.gif') repeat-x;
width: 520px;
}