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

September 15, 2008

Я продолжаю начатый в предыдущей статье рассказ об известной javascript-библиотеке yui (yahoo user interface). В прошлый раз я поверхностно прошелся по модулям, из которых состоит yui, рассказал об ее истории, лицензировании. Завершив рассмотрение модуля Selector, отвечавшего за решение задач поиска в исходном дереве html-страницы некоторых узлов по условию. Научившись выполнять простейшие операции над DOM структурой html-документа, назначать обработчики событий для кнопок и других элементов интерфейса. Сейчас самое время пойти дальше, и начать рассматривать то, что и составило славу yui – развитую библиотеку визуальных компонентов.

Начну же я по уже сложившейся традиции, с одной полезной мелочи, рассмотреть которую стоит обязательно перед переходом к созданию богатых пользовательских интерфейсов. Я говорю про журналирование или logging. При написании больших программ одним из ключевых моментов является допускать как можно меньше ошибок. Вы скажете, очевидно, что ошибок должно быть как можно меньше не только в больших, но и самых маленьких программах. Очевидно, то оно очевидно, но при создании больших программ достаточную долю времени приходится тратить не написание самого кода, а создание средств узнать, что же происходит “внутри этого монстра”. И не всегда можно обойтись javascript-отладчиком (слава разработчикам firebug): ведь многие эффекты и баги проявляются только при естественном течении времени выполнения программы. По этой же причине уходят в прошлое и множественные вызовы функции alert, расставленные в ключевых местах приложения. Нам нужен инструмент, который позволит не только выдавать пользователю сообщения, но и делать это максимально удобно. Например, так чтобы можно было мгновенно отключить вывод отладочных сообщений, можно было бы переключать уровень подробности журналирования, например, разделить сообщения на категории: ошибка, предупреждение, просто сообщение. Естественно, что почти всегда мы должны получить только сообщения о произошедших в ходе работы нашего javascript-кода ошибках. Но как только произошел сбой, то мы переключаемся в режим максимально подробного журналирования. Итак, давайте познакомимся: модуль Logger. Прежде всего, я в начале html-файла подключу новую библиотеку:
  1. <script type="text/javascript" src="js/logger/logger.js"> </script>
Затем я в “теле” документа создаю блок div с идентификатором “logzone” и кнопку “button” вызывающую javascript-функцию “doSomething”.
  1. <div id="logzone"> </div>
  2.  
  3. <button onclick="doSomething()">click me</button>
Теперь нужно использовать уже привычную нам методику, и назначать на событие DOMReady функцию, выполняющую инициализацию модуля logger:
  1. YAHOO.util.Event.onDOMReady (initYUI);
  2.  
  3. function initYUI (){
  4.  new YAHOO.widget.LogReader('logzone');  
  5. }
Как видите, здесь все тривиально, я вызываю конструктор объекта LogReader, передавая ему в качестве параметра идентификатор созданного на предыдущем шаге блока div. Результат работы скрипта показан на рис. 1.



Теперь нужно создать функцию doSomething, которая и будет выводить в окно сообщения:
  1. function doSomething(){
  2.  YAHOO.log("info message "+ Math.random(), 'info');
  3.  YAHOO.log("warn message "+ Math.random(), 'warn');
  4.  YAHOO.log("error message "+ Math.random(), 'error', 'Apple'); 
  5. }
Для того чтобы отправить сообщение в окошко логгера я использовал функцию log. В качестве первого параметра ей передается текст сообщения. Второй параметр задает категорию сообщения, так помимо показанных в примере вариантов по степени опасности ошибки: error, warn, info, можно указать режим time, window. Более того, вы можете создать собственные степени важности сообщений (просто передайте новое имя режима как второй параметр при вызове функции log). Третий параметр служит для указания источника сообщения. Так если у вас в приложении есть, грубо говоря, две кнопки, то вы можете каждой из них поставить в соответствии отдельную группу сообщений (опять таки просто укажите название группы как третий параметр функции log). Визуально новые категории и источники сообщений добавляются в самый низ окна логгера (см. рис. 1. где подчеркнуто). Для каждого из названий категории сообщения и источника есть checkbox, включая и отключая который вы можете фильтровать показываемые на экране сообщения. Еще можно с помощью css задать цветовое оформление для подписей каждой категории, например, так (здесь Marker это название категории):
  1. .yui-log .Marker { font-weight: bold; font-size: 14px;}
По аналогии можно настроить внешний вид и других компонент окна logger-а. Если вы повторили один к одному мой пример, то хотя все заработало, но внешний вид окна логгера совершенно другой: убогое оформление, где все расползлось, нет цветового выделения, ничего нет. Дело в том, что yui это не только javascript код, но и набор стилевых файлов оформления. Даже для такого простого примера как окно логгера нужно подключить дополнительный css файл, например, так:
  1. <link rel="stylesheet" type="text/css" href="js/logger/assets/logger.css" />
Надо сказать, что разработчики yui предусмотрели множество способов программно настроить параметры внешнего вида окошка logger-а, создали набор функций, вызывая которые можно приостанавливать и продолжать работу логгера, программно отключать и включать отображение сообщений относящихся к определенной категории. Также можно изменить надпись заголовка окошка logger-а, спрятать “подвал” окна. Большой пользы от всех этих функций немного, разве что иногда пригодится инвертировать порядок вывода сообщений, так чтобы последние по дате записи были бы не вверху, а внизу (как видите второй, необязательный, параметр конструктора LogReader принимает список с параметрами, меняющими поведение logger-а):
  1. new YAHOO.widget.LogReader("logzone ", {newestOnTop:false});
А с помощью параметра entryFormat можно настроить внешний вид строки сообщения, этот параметр просто шаблон, в теле которого встречаются ключевые слова. Например, “{message}” задает текст сообщения. В исходниках модуля logger.js можно найти и значения других кодов. Завершая рассмотрение Logger-а, осталось упомянуть, что в современных браузерах уже есть стандартная возможность для журналирования. Например, firebug для firefox позволяют отправлять сообщения в консоль с помощью вызова:
  1. console.log ('message',' from', ' apple');
Так мы увидим, что в консоль firebug добавилось запись “message from apple”. Для того, чтобы сообщения, отправляемые yui Logger-ом, также попадали в консольку браузера, необходимо включить этот режим, с помощью вызова функции enableBrowserConsole:
  1. // так я включаю режим консоли
  2. YAHOO.widget.Logger.enableBrowserConsole ();
  3. // а так выключаю
  4. YAHOO.widget.Logger.disableBrowserConsole ();
На этом про Logger все и мы переходим к рассмотрению следующего вспомогательного модуля - Loader-а. Давайте еще раз взглянем на показанный шагом выше пример подключения javascript кода и css стилей. Не то чтобы способ с явно заданными тегами script и link был бы ужасен, но хотелось бы получить унифицированный механизм загрузки всего содержимого модуля. И такой способ есть, более того при загрузке модулей есть возможность загрузить все зависимости (например, если мы хотим загрузить модуль “A”, для работы которого нужен модуль “B”, то этот модуль будет загружен yui loader-ом автоматически). Также корректно обрабатывается ситуация с загрузкой css-стилей. В секции head страницы html я теперь подключаю только один модуль loader-а:
  1. <script type="text/javascript" src="js/yuiloader/yuiloader-beta.js"></script>
Затем в коде страницы я создаю объект YAHOO.util.YUILoader, настраиваю ему параметры, что загрузить и как:
  1. loader = new YAHOO.util.YUILoader(); 
  2. loader.require(["logger"]); 
  3. loader.loadOptional = true; 
  4. loader.base =  'js/';
Основной параметр управляющий работой loader-а, это что загружать (какие модули). Обратите внимание, что я указываю имя модуля без расширения. Запись с квадратными скобками говорит, что значением параметра для метода require является массив со строками – именами модулей. Затем я включаю режим загрузки всех дополнительных модулей нужных для работы logger-а, и последний шаг – указать путь к каталогу, куда я распаковал загруженную с сайта yahoo саму библиотеку yui. Последний шаг – запустить загрузку. Для этого используется метод loader.insert:
  1. loader.insert(
  2.   { onSuccess: 
  3.      function() { 
  4.         new YAHOO.widget.LogReader("logzone "); 
  5.      }
  6.   }
  7. );
Параметров управляющих работой загрузки модулей достаточно много. Но самый главный из них onSuccess – так мы можем назначить функцию, которая будет вызвана по завершению загрузки модулей. Если же мы хотим обработать ситуацию ошибки загрузки данных, то используем функцию-параметр onFailure:
  1. loader.insert(
  2.   { onFailure: 
  3.      function (err){
  4.          alert ('unable load module' + YAHOO.lang.dump(err) ); 
  5.      }
  6.   }
  7. );
Небольшая подсказка: в состав yui входит полезная функция dump, которая получает в качестве входного параметра произвольный javascript-объект и возвращает его удобочитаемое строковое представление, что довольно приятно при отладке кода. Теперь давайте запустим приведенный выше пример кода и посмотрим, что получится. Получилось, вообще-то, некое промежуточное состояние: javascript-часть модуля logger была успешно загружена и работает, сообщения добавляются в окошко журнала. Вот только внешний вид окошка не радует: снова куда-то пропали css-стили оформления. А ведь я только что говорил, что yui loader умеет корректно загружать стилевые файлы. Действительно умеет, просто мы столкнулись еще с одним из ключевых понятий в yui – скинами. Что такое скины в мире компьютеров знают, уверен, все. Настройка внешнего вида компонентов в yui реализована за счет css-стилей организованных в пакеты скинов. Например, откройте каталог build/calendar. В нем находятся js-файлы нужные для работы визуального компонента календаря. Затем зайдите в подпапку assets, там вы увидите набор css-файлов и еще один подкаталог skins, внутри которого находится в свою очередь подкаталог sam, содержимым которого являются еще несколько css-файлов. Как видите скин это нескольких стилевых файлов, помещенных в подкаталог с именем sam (собственным именем скина). Откроем в firebug исходный код веб-странички и увидим, что модуль loader динамически сгенерировал и разместил внутри секции head подключения еще нескольких файлов: js-модули yui необходимые для работы logger-а, а также был подключен файл js/logger/assets/skins/sam/logger.css. Так почему же если файл со стилями подключен, они не применились? Взгляните на правила записанные в упомянутом выше css-файле: все они предваряются записью yui-skin-sam. Это значит, что в одном html-файле мы вполне можем сочетать несколько компонентов оформленных разными скинами, и все что нам нужно для этого сделать – это окружить компоненты любым блочным элементом, имя css-класса которого равно yui-skin-имя-того-самого-скина. Например:
  1. <body class="yui-skin-sam">
  2.   <div id="logzone"> </div>
  3. </body>
Теперь перезагрузим страничку с компонентом logger-а и видим, что стили применились, и окошко “журнала сообщений” выглядит на отлично. Еще одной полезной возможностью loader-а является указание ему того, какую версию компонента нужно загружать. Под версией я здесь понимаю различия в упаковке js-файла для компонента. Если вы посмотрите в любой из подкаталогов модулей, то увидите, что присутствуют три файла, например, для календаря это будут: calendar.js, calendar-debug.js, calendar-min.js. Первый файл задает “обычную” версию календаря, во втором случае внутренние методы компонента активно используют описанный в этой статье инструмент logger, третий же файл хранит ужатую для быстрой загрузке в internet версию библиотеки. Итак, я хочу, чтобы мой loader загрузил, именно, debug версию компонента (приятно, что если для какого-то компонента debug-версии просто физически нет, то автоматически будет загружена обычная или “-min” версия):
  1. loader.filter =  'DEBUG';
Я завершу рассмотрение loader-а тем, что покажу, как при загрузке компонента указать какой именно стиль вы хотите ему назначить. Для этого обращаемся к свойству skin объекта loader-а, например, так:
  1. loader.skin.defaultSkin = ‘название скина’;
Следующая часть этой статьи будет посвящена анимации. Методики создания анимации одинаковы почти во всех javascript-библиотеках и основаны на возможности создать специальный объект “аниматор”, назначением которого является изменять некоторые стилевые (css) атрибуты целевого html-элемента по определенному закону. Для демонстрации анимационных средств yui я создал на странице синий квадратик, который через секунду начнет двигаться:
  1. <div id="box" style="background-color: blue; width: 100px; height: 100px;"> </div>
Для работы с анимацией я подключаю модуль animate (использую при этом рассмотренный шагом назад loader):
  1. <script type="text/javascript" src="js/yuiloader/yuiloader-beta.js"></script>
  2. <script>
  3.  loader = new YAHOO.util.YUILoader(); 
  4.  loader.require(["logger", "animation"]); 
  5.  loader.loadOptional = true; 
  6.  loader.base =  'js/';
  7.  loader.insert({ 
  8.   onSuccess: function() { 
  9.      var attributes = { width: { to: 0 } , height: {to: 0} }; 
  10.      a = new YAHOO.util.Anim('box', attributes);
  11.   }});
Внутри функции обрабатывающей событие “модуль анимации успешно загружен” я создаю объект YAHOO.util.Anim, передавая ему два параметра: идентификатор созданного шагом ранее синего квадрата (box) и ассоциативный массив с параметрами анимации. Читать сценарий анимации очень легко: ключом массива является название css-свойства (width и height, т.е. я хочу изменять размеры синего квадрата). Значением же свойства является еще один ассоциативный массив с параметрами “как менять”. Здесь я только задал конечные значения параметров (to: 0), таким образом, синий квадрат должен уменьшиться в размерах до нуля. Последним шагом будет запустить сценарий анимации, для этого я создаю кнопку, обработчик события нажатия на которой выполняет только одну команду (анимация, старт):
  1. a.animate ();
Теперь откроем созданную html-страницу и убедимся, что все работает как надо. Осталось только разобраться с параметрами анимации и тогда, комбинируя эффекты, можно сделать практически все что угодно. Прежде всего, когда я создавал объект YAHOO.util.Anim, то можно указать еще несколько параметров. Так третий параметр задает длительность анимации (сколько секунд уменьшался бы наш синий квадрат). Четвертый параметр управляет стратегией изменения параметра. В нашем простейшем случае изменение размера квадрата выполнялось линейно, с одинаковой скоростью. Однако, в общем случае это не так и есть алгоритмы с нарастающей, убывающей скоростью изменения параметра, есть алгоритмы, которые имитируют более сложные законы:
  1. a = new YAHOO.util.Anim('box', attributes, 1, YAHOO.util.Easing.elasticBoth);
Обо всем этом я уже говорил и приводил примеры в статье посвященной другой javascript библиотеке mootols, так что повторять математические формулы лежащие в основе движения тел я не буду и перейду к следующему шагу управления анимацией – рассмотрим внимательнее второй параметр вызова конструктора Anim. В общем свойстве он выглядит так:
  1. animatedAttributes = { 
  2.  animatedPropertyA: { 
  3.   by: 10,   to: 100, 
  4.   from: 0,  unit: 'em' 
  5.   } 
  6. }
Параметр by говорит на какую величину шага должно измениться текущее значение параметра каждую единицу времени. Параметр to говорит, что анимируемый параметр начинает изменяться от текущего значения до заданного с помощью to. Параметр from позволяет выполнить в самом начале анимации быстрое перемещение к заданному значению и затем изменяться согласно правилам заданным by и to. Последний параметр unit задает единицы измерения. Есть специализированные версии класса Anim предназначенные для анимации, во-первых, цвета:
  1. var colorAnim = new YAHOO.util.ColorAnim(‘box’, {backgroundColor: { to: '#eaeaea' } });
Следует упомянуть, что в yui есть небольшой баг с анимацией цвета, в случае если исходное значение либо конечное задано не шестнадцатеричным кодом, а названием цвета, то анимация работать не будет.

Затем можно проанимировать движение тела:
  1. var attributes = { 
  2.  points: {
  3.    to: [500, 500], control: [[100, 100], [200, 100], [800, 800]]
  4.  }
  5. };
  6.  
  7. moveAnim= new YAHOO.util.Motion('box', attributes, 1, YAHOO.util.Easing.easeIn);
Здесь перемещение должно быть выполнено в точку 500, 500, однако, в ходе этого синий квадратик должен “обежать” по плавной кривой все заданные контрольные точки.

Последней специализированной версией контроллера анимации является класс YAHOO.util.Scroll. Он позволяет выполнить плавную прокрутку содержимого некоторого окна (обычного div с настроенным стилем overflow). В практике для создания более-менее красивого анимационного эффекта приходится “собирать” его из маленьких кусочков (отдельных анимаций). Так что нам нужен механизм, позволяющий точно узнать, когда какой-то из шагов завершился и перейти к следующему (создать цепочку шагов преобразования). Мы можем подписаться на получение событий “анимация начата”, “анимация все еще продолжается”, “анимация была завершена”. И по событию завершения первой анимации запустить вторую.
  1. // первая анимация изменения цвета синего квадратика
  2. a1 = new YAHOO.util.ColorAnim('box', {backgroundColor: { to: '#eaeaea' } });
  3. // вторая изменяет размер уже серого квадратика до размеров точки
  4. a2 = new YAHOO.util.Anim('box', { width: { to: 0 } , height: {to: 0} });
  5. // и подписываемся на событие первая анимация завершена
  6. a1.onComplete.subscribe(function (){ a2.animate (); });
Теперь если по нажатию на кнопку запустить первую анимацию (a1), то квадратик сначала изменит свой цвет с синего на серый, а затем начнет уменьшаться в размерах.

На этом я завершаю рассказ об еще нескольких модулях библиотеки yui. В следующий раз нас ждет знакомство с визуальными элементами управления (кнопки, меню, таблицы с данными).