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

February 6, 2009Comments Off on Сложные интерфейсы на javascript вместе Yahoo UI. Часть 17

Эта статья продолжает рассказ об одном из самых часто используемых и сложных компонентов YUI – DataTable. Сегодня я расскажу о том, как загружать данные для DataTable с сервера с поддержкой paging-а, как сделать табличку более дружественной к пользователю и как работать с моделями выделения строк.

В прошлый раз я остановился на том, что показал, как DataTable интегрируется с компонентом Paginator для отображения большого объема информации постранично. В самом простом случае DataSource загружает сразу все содержимое таблицы базы данных на сервере в память браузера. И затем Paginator обслуживает (уже мгновенный) переход по страничкам. Во втором случае данные с сервера подтягиваются постепенно, по мере перемещения по страничкам. Это означает, что мы должны на сервере реализовать не только постраничный отбор данных, но и их сортировку. Действительно, при изменении порядка сортировки, например, с “ФИО” на “зарплата” записи, которые отображались на первой странице, могут быть разбросаны по другим страницам. И, наоборот, записи с других страниц (еще не загруженных), должны попасть на первую страницу. Это довольно скользкий момент т.к. резко увеличивается количество обращений к серверу, хотя грамотная система кэширования и может помочь сохранить производительность. Поддержка динамической загрузки данных и сортировка на сервере в YUI DataTable реализуется очень просто: нужно при создании DataTable указать значение “true” для конфигурационной переменной “dynamicData” и все. Теперь DataTable будет формировать запросы к серверному php-скрипту передавая ряд переменных. Во-первых: sort – поле, по которому нужно выполнить сортировку записей. Затем идет направление сортировки (по возрастанию или по убыванию); оно задается переменной dir. Переменные startIndex и results задают, соответственно, позицию записи, с которой идет выборка данных из таблицы БД и еще количество записей, которые нужно отобрать. К счастью, вам вовсе не обязательно завязывать наш php-код на именно эти имена переменных. Более того, YUI может передать нам полную ответственность за формирование http-запроса к серверу. Для этого я переопределяю функцию generateRequest:
  1. function customGenerateRequest (state, table){
  2.   state = state || {pagination:null, sortedBy:null};
  3.   var sortBy = (state.sortedBy) ? state.sortedBy.key : table.getColumnSet().keys[0].getKey();
  4.   descCssName = YAHOO.widget.DataTable.CLASS_DESC;
  5.   var sortDir = (state.sortedBy && state.sortedBy.dir === descCssName) ? "desc" : "asc";
  6.   var from = (state.pagination) ? state.pagination.recordOffset : 0;
  7.   var size = (state.pagination) ? state.pagination.rowsPerPage : 10;
  8.   return  "sortBy=" + sortBy + "&sortDir=" + sortDir + "&from=" + from + "&size=" + size ;
  9. }
Входным параметром функции является объект state (состояние таблицы). Этот объект содержит внутри себя набор характеристик примененных к DataTable. В частности есть поля sortedBy.key – имя поля таблицы, к которому была применена сортировка. В случае если сортировка пока ни к чему не применена, то я обращаюсь к таблице (входной аргумент функции “table”) и прошу table дать мне список всех колонок. Затем извлекаю из них самую первую и полагаю, что сортировку следует применить по полю таблицы связанной с этой первой колонкой. Следующая переменная sortedBy.dir задает направление сортировки. Может немного смутить сравнение “state.sortedBy.dir === descCssName”. Дело в том, что внутри объекта state хранится (не самый лучший выбор, я предпочел бы кодовое обозначение направления сортировки) имя css-класса, который сейчас применен к заголовку. Еще объект state хранит сведения о номере текущей записи и количестве строк на странице (переменные pagination.recordOffset и pagination.rowsPerPage). Мне осталось только взять все эти величины и сформировать как результат работы функции строку с теми именами переменных, которые и ожидает php-скрипт. В прошлой статье, показывая как загрузить с сервера данные, я для простоты обошелся имитацией. Теперь придется создать полноценную базу данных mysql, в ней таблицу employees и наполнить ее парой тысяч сгенерированных случайным образом записей:
  1. CREATE TABLE `employees` (
  2.   `user_id` int(11) NOT NULL AUTO_INCREMENT,  `fio` varchar(100) DEFAULT NULL,
  3.   `birthday` date DEFAULT NULL,  `salary` double DEFAULT NULL,  `sex` enum('male','female') DEFAULT NULL,
  4.   PRIMARY KEY (`user_id`)
  5. ) ENGINE=InnoDB
Отобрать данные из mysql базы просто, даже очень просто. Гораздо сложнее правильно это сделать с учетом того, что мне нужны не только записи в указанном диапазоне (например, первая десятка), но нужен способ узнать сколько этих записей во всей таблице. Нужно не только вычислить это число (желательно без лишних запросов, нагружающих СУБД), но и передать эти сведения в DataTable. Без этого DataTable просто не сможет правильно сформировать внешний вид таблицы: не будет знать, как должен выглядеть блок paging-а (навигации по страницам). На шаг раз я подключаюсь к СУБД и перехожу в базу данных ‘kadry’:
  1. mysql_connect ('localhost', 'root', '') or die ('unable to connect to mysql server');
  2. mysql_select_db (‘kadry') or die ('unable to select db');
На шаг два я “принимаю” входные переменные. Естественно, что мне нужно корректно обработать ситуацию, когда направление сортировки не задано:
  1. $from = $_REQUEST['from'];
  2. $size = $_REQUEST['size'];
  3. $sortBy = isset($_REQUEST['sortBy'])?$_REQUEST['sortBy']:"user_id";
  4. $sortDir = isset($_REQUEST['sortDir'])?$_REQUEST['sortDir']:"";
На шаг три я формирую строку SQL-запроса, использующего полученные php-скриптом переменные. Обратите внимание на то, что перед символом “*” я поместил ключевое слово “SQL_CALC_FOUND_ROWS”. Он нужно, для того чтобы mysql помимо отбора записей из таблицы employees подсчитал сколько записей в этой таблице всего.
  1. $sql = "select SQL_CALC_FOUND_ROWS * from employees order by $sortBy $sortDir limit $from, $size";
Завершающий этап – выполнить запрос и сформатировать JSON-строку, отправляемую назад в браузер. Кроме основного запроса, я выполнил еще и дополнительный “select FOUND_ROWS()”, который вернет количество записей отобранных первым запросом.
  1. $rez = mysql_query ($sql) or die ('unable to select data' . $sql);
  2.  $count = mysql_query ('select FOUND_ROWS()') or die ('unable to calculate total records count');
  3.  
  4.  $count = mysql_fetch_array ($count);
  5.  $count = $count[0];
  6.  $rows = array ();
  7.  
  8.  while ($row = mysql_fetch_assoc($rez)) 
  9.    $rows [] = $row;
  10.  
  11.  $final = array ('users' => array ('user' => $rows), 'total_records' => $count );
  12.  
  13.  print json_encode ($final);
Количество записей возвращается как значение переменной “total_records”. Теперь нужно подсказать YUI как добраться до этого значения. Когда я создаю объект DataSource и указываю для него свойство responseSchema, то помимо ссылки на массив записей, можно попросить YUI извлечь из входного JSON-объекта другие поля, играющие роль метаинформации, в частности, поля total_records с количество всех записей в БД:
  1. ds.responseSchema = { 
  2.     resultsList: "users.user", 
  3.     fields: [
  4.          "fio",
  5.          {key:"birthday", parser: strToDate},
  6.          "sex",
  7.          {key:"salary", parser:"number"}
  8.     ] ,
  9.     metaFields: { total_records: "total_records"}  
  10.  };
Последняя загвоздка в том, что YUI все еще не ассоциирует количество записей в таблице (то, что нужно для работы paginator-а) и метапеременную total_records. Давайте подскажем ему это:
  1. function handlePayload (oRequest, oResponse, oPayload) { 
  2.  oPayload.totalRecords = parseInt(oResponse.meta.total_records); 
  3.  return oPayload; 
  4. }
Последний шаг, это привязать функцию handlePayload к DataTable (сама переменная table создается точь-в-точь как ранее вызовом конструктора DataTable):
  1. table.handleDataReturnPayload = handlePayload;
На рис. 1 я поймал момент, когда после нажатия на кнопку сортировки отправляется запрос на сервер, и пока ответ не пришел, то выводится надпись “Loading …”. Как изменить внешний вид этого сообщения я рассказывал в прошлой статье.



Теперь займемся дальнейшими улучшениями DataTable. И начнем с того, что попробуем реализовать функцию “спрятать столбец”. Это очень полезная функция, если в таблице очень много колонок и не все из них имеет смысл одновременно показывать. В левом верхнем углу таблицы можно сделать специальную кнопку, по нажатию на которую появляется диалоговое окно. В этом окне вы с помощью checkbox-ов отмечаете то, какие колонки в DataTable нужно спрятать или отобразить. К сожалению, хотя YUI содержит ряд методов для управления видимостью колонок, но “готового из коробки” решения нет (снова пинок в пользу ExtJs). Так что тряхнем стариной (точнее вспомним материал из четвертой статьи серии) и попробуем реализовать диалоговое окно выбора отображаемых колонок сами. Пример мы реализуем по шагам: первым делом я хочу добавить к таблице еще одну фиктивную колонку. Она будет расположена крайней слева и не содержит никакой информации: мне нужен пустой заголовок таблицы, в котором разместится кнопка вызова диалогового окна (описания остальных колонок остались без изменения).
  1. var columns = [ 
  2. {key:"fake_hide_show", sortable:false, label:'<div id="columnHideShowBtn">�</div>'}, 
  3. {key:"fio", sortable:true}, 
  4. ];
Фиктивное поле “fake_hide_show” естественно не должно быть упомянуто в описании источника данных (responseSchema). Теперь внимание на свойство “label” в описании фиктивной колонки: вместо надписи с названием колонки я решил вывести пустой блок div с идентификатором “columnHideShowBtn”. Благодаря этому уникальному имени я могу в последующем изменить внешний вид или стилевое оформление заголовка колонки. Например, так я превращаю блок div в кнопку (при наведении мыши курсор также меняет вид как будто для кнопки) с картинкой:
  1. <style>
  2. #columnHideShowBtn {
  3.  background:#D8D8DA url(pic_props.png) no-repeat 50% 50%;
  4.  cursor: pointer;
  5.  width: 24px;
  6.  height: 24px; 
  7. }
  8. </style>
Следующий шаг – сделать так, чтобы по нажатию на “кнопку” вызывалось диалоговое окно с набором checkbox-ов соответствующих колонкам таблицы. Привязать к любому элементу html обработчик события проще простого с помощью YUI модуля events:
  1. YAHOO.util.Event.addListener("columnHideShowBtn", "click", doHideShowDialog);
Функция doHideShowDialog должна не просто создать диалоговое окно (класс SimpleDialog), но настроить его содержимое в соответствии со списком колонок таблицы. Мне нужно организовать цикл по всем колонкам (кроме, конечно, первой фиктивной) и для каждой из колонок разместить на диалоговом окне тег checkbox-а. Более того, нужно проверить в каком сейчас состоянии находится колонка, т.е. спрятана она или отображается и это повлияет на внешний вид checkbox-а:
  1. function doHideShowDialog (){
  2.  d = new YAHOO.widget.SimpleDialog("placeholderforDialog", 
  3.    { 
  4.      width : "400px", 
  5.      icon: YAHOO.widget.SimpleDialog.ICON_INFO,
  6.      fixedcenter : true, 
  7.      visible : false, 
  8.      constraintoviewport : true, 
  9.      buttons : [ 
  10.          { text:"Accept", handler:onAccept}, 
  11.          { text:"Discard", handler:onDiscard, isDefault:true} 
  12.      ] 
  13.   } 
  14.  ); 
  15.  
  16.  // настройка внешнего вида диалогового окошка
  17.  d.setHeader("Выберите колонки таблицы для отображения");    	
  18.  rez = '<br />';
  19.  var defs = table.getColumnSet().getDefinitions();
  20.  for (i = 1; i < defs.length; i++){
  21.    def = defs[i];
  22.    column = table.getColumnSet().getColumn (i);
  23.    label = def.label || def.key;
  24.    if_checked = column.hidden?'':'checked';
  25.    rez+= '<input type="checkbox" id="chk_'+i+'" '+if_checked+'/>'  + label + "<br />";
  26.  } 
  27.  
  28.  d.setBody(rez); 
  29.  
  30.  d.render (document.body);
  31.  d.show (); 
  32. }
Начало функции не требует комментариев: я просто обратился к четвертой статье серии и скопировал оттуда кусочек кода, создающий диалоговое окно. Там же вы найдете и подробное описание параметров конструктора класса SimpleDialog. Создав диалоговое окно, я формирую большую-большую строку (rez), задающую будущий внешний вид диалогового окошка. Для этого я в цикле перебираю все колонки (пропуская нулевую фиктивную), для каждой из них извлекаю из table текстовую надпись заголовка столбца (или название поля таблицы, если специальный заголовок отсутствует). Эту строку я вывожу рядом с элементом checkbox. Для каждого из checkbox-ов может быть установлена отметка “checked” в том, случае если соответствующая ему колонка видима (hidden). Важно, что у каждого из checkbox-ов есть уникальный идентификатор, формируемый по правилу “chk_” плюс номер колонки. Это мне нужно для того, чтобы в последующем (когда пользователь закроет диалоговое окно нажатием кнопки “Accept” или “Принять”) можно было выполнить обратную операцию и, основываясь на том какие checkbox-ы пользователь “поставил” или “снял”, показать и спрятать, соответственно, колонки таблицы.
  1. function onAccept (e){
  2.  var defs = table.getColumnSet().getDefinitions();
  3.  for (i = 1; i < defs.length; i++)
  4.    if (YAHOO.util.Dom.get('chk_' + i).checked)
  5.      table.showColumn (i);
  6.    else
  7.      table.hideColumn (i);
  8.  this.hide (); 
  9. }
Внутри функции я снова организую цикл по списку колонок таблицы (переменная table) и для каждой из них получаю ссылку на соответствующий html-элемент checkbox. После проверки в каком состоянии он находится нужно прятать/показывать колонку таблицы. Завершив цикл нужно спрятать само диалоговое окно настроек (здесь this – ссылка на объект SimpleDialog). Последнее о чем стоит упомянуть так это функция onDiscard. Она вызывается при нажатии на кнопку “Discard” на диалоговом окошке. Устройство функции тривиально и я его не привожу: нужно всего лишь вызывать метод this.hide() для того, чтобы спрятать диалоговое окно настройки таблицы, не изменяя внешний вид самой таблицы. Внешний вид примера показан на рис. 2.



Таблицы редко используются только для отображения информации – гораздо чаще для ее редактирования. В простейшем случае сама таблица находится в режиме “только для чтения”. Зато по выбору какой-либо строки (неплохо бы это еще и визуально отметить) внизу страницы можно отобразить панель с множеством полей редактирования для колонок таблицы. Этот вариант особенно удобен, если количество отображаемых колонок в таблице гораздо меньше всех, которые есть у записи (и которые может захотеть отредактировать пользователь). На специальной панели должно быть достаточно места для того, чтобы разместить сложные (главное удобные) элементы редактирования. Если количество полей таблицы не велико, и все они умещаются на экране без громоздких горизонтальных прокруток, то можно использовать и inline-редактирование, как если бы мы захотели отредактировать ячейку обычной таблицы ms excel. Давайте попробуем реализовать оба эти варианта и начнем с редактирования содержимого таблицы на специальной панели. Первым шагом я должен настроить режим выделения строк таблицы: т.е. можно ли выделить с нажатой клавишей “ctrl” несколько строк или только одну строку. Создавая DataTable, я передам ее конструктору параметр selectionMode, равный “single”. Это значит, что выделить можно только одну строку. Кроме варианта “single” есть еще “standard” (режим по умолчанию), когда работают клавиши ctrl и shift для выделения нескольких строк. Режим “singlecell” разрешает выделить только одну ячейку таблицы, а режим “cellblock” позволяет вам выделить любые ячейки таблицы, так чтобы они образовывали прямоугольный блок. А режим “cellrange” позволяет выделить несколько последовательно расположенных ячеек таблицы (как будто выделяете диапазон дат на календаре).
  1. table = new YAHOO.widget.DataTable("tableplaceholder", columns, ds, { selectionMode: "single"} );
Я указал режим выделения и запустил пример, но он не заработал. Разработчики YUI по странной причине решили, что мало назначить режим выделения, нужно еще и назначить для DataTable функции, которые обрабатывают события: “выделена строка”, “курсор мыши над строкой”, “курсор мыши покидает строку”. К счастью, в классе DataTable есть приемлемые реализации этих функций по-умолчанию:
  1. function onSelectRow (oArgs){
  2.  table.onEventSelectRow (oArgs); 
  3. }
  4.  
  5. // и назначаем функции
  6. table.subscribe("rowMouseoverEvent", table.onEventHighlightRow); 
  7. table.subscribe("rowMouseoutEvent", table.onEventUnhighlightRow); 
  8. table.subscribe("rowClickEvent", onSelectRow); 
  9.  
  10. // выделяем первую строку
  11. table.selectRow(table.getTrEl(0));
Первые два присвоения функции обработчика событий заурядны: я использую встроенные в YUI функции, которые подсвечивают строку пока над ней находится курсор. Для обработки события "rowClickEvent" пришлось создать собственную функцию onSelectRow, которая просто перевызывает опять-таки стандартную функцию onEventSelectRow из YUI, подсвечивающую выделенную строку таблицы (см. рис. 3). В следующей статье останется только показать то, как внутри функции “onSelectRow” узнать то, какая строка таблицы была выделена. Затем получить содержимое всех полей строки и наполнить этими данными панель редактирования.