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

Материал из DOM

Перейти к: навигация, поиск


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

В прошлый раз я остановился на том, что показал, как DataTable интегрируется с компонентом Paginator для отображения большого объема информации постранично. В самом простом случае DataSource загружает сразу все содержимое таблицы базы данных на сервере в память браузера. И затем Paginator обслуживает (уже мгновенный) переход по страничкам. Во втором случае данные с сервера подтягиваются постепенно, по мере перемещения по страничкам. Это означает, что мы должны на сервере реализовать не только постраничный отбор данных, но и их сортировку. Действительно, при изменении порядка сортировки, например, с “ФИО” на “зарплата” записи, которые отображались на первой странице, могут быть разбросаны по другим страницам. И, наоборот, записи с других страниц (еще не загруженных), должны попасть на первую страницу. Это довольно скользкий момент т.к. резко увеличивается количество обращений к серверу, хотя грамотная система кэширования и может помочь сохранить производительность. Поддержка динамической загрузки данных и сортировка на сервере в YUI DataTable реализуется очень просто: нужно при создании DataTable указать значение “true” для конфигурационной переменной “dynamicData” и все. Теперь DataTable будет формировать запросы к серверному php-скрипту передавая ряд переменных. Во-первых: sort – поле, по которому нужно выполнить сортировку записей. Затем идет направление сортировки (по возрастанию или по убыванию); оно задается переменной dir. Переменные startIndex и results задают, соответственно, позицию записи, с которой идет выборка данных из таблицы БД и еще количество записей, которые нужно отобрать. К счастью, вам вовсе не обязательно завязывать наш php-код на именно эти имена переменных. Более того, YUI может передать нам полную ответственность за формирование http-запроса к серверу. Для этого я переопределяю функцию generateRequest:

function customGenerateRequest (state, table){
  state = state || {pagination:null, sortedBy:null};
  var sortBy = (state.sortedBy) ? state.sortedBy.key : table.getColumnSet().keys[0].getKey();
  descCssName = YAHOO.widget.DataTable.CLASS_DESC;
  var sortDir = (state.sortedBy && state.sortedBy.dir === descCssName) ? "desc" : "asc";
  var from = (state.pagination) ? state.pagination.recordOffset : 0;
  var size = (state.pagination) ? state.pagination.rowsPerPage : 10;
  return  "sortBy=" + sortBy + "&sortDir=" + sortDir + "&from=" + from + "&size=" + size ;
}

Входным параметром функции является объект state (состояние таблицы). Этот объект содержит внутри себя набор характеристик примененных к DataTable. В частности есть поля sortedBy.key – имя поля таблицы, к которому была применена сортировка. В случае если сортировка пока ни к чему не применена, то я обращаюсь к таблице (входной аргумент функции “table”) и прошу table дать мне список всех колонок. Затем извлекаю из них самую первую и полагаю, что сортировку следует применить по полю таблицы связанной с этой первой колонкой. Следующая переменная sortedBy.dir задает направление сортировки. Может немного смутить сравнение “state.sortedBy.dir === descCssName”. Дело в том, что внутри объекта state хранится (не самый лучший выбор, я предпочел бы кодовое обозначение направления сортировки) имя css-класса, который сейчас применен к заголовку. Еще объект state хранит сведения о номере текущей записи и количестве строк на странице (переменные pagination.recordOffset и pagination.rowsPerPage). Мне осталось только взять все эти величины и сформировать как результат работы функции строку с теми именами переменных, которые и ожидает php-скрипт. В прошлой статье, показывая как загрузить с сервера данные, я для простоты обошелся имитацией. Теперь придется создать полноценную базу данных mysql, в ней таблицу employees и наполнить ее парой тысяч сгенерированных случайным образом записей:

CREATE TABLE `employees` (
  `user_id` int(11) NOT NULL AUTO_INCREMENT,  `fio` varchar(100) DEFAULT NULL,
  `birthday` date DEFAULT NULL,  `salary` double DEFAULT NULL,  `sex` enum('male','female') DEFAULT NULL,  PRIMARY KEY (`user_id`)
) ENGINE=InnoDB

Отобрать данные из mysql базы просто, даже очень просто. Гораздо сложнее правильно это сделать с учетом того, что мне нужны не только записи в указанном диапазоне (например, первая десятка), но нужен способ узнать сколько этих записей во всей таблице. Нужно не только вычислить это число (желательно без лишних запросов, нагружающих СУБД), но и передать эти сведения в DataTable. Без этого DataTable просто не сможет правильно сформировать внешний вид таблицы: не будет знать, как должен выглядеть блок paging-а (навигации по страницам). На шаг раз я подключаюсь к СУБД и перехожу в базу данных ‘kadry’:

mysql_connect ('localhost', 'root', '') or die ('unable to connect to mysql server');
mysql_select_db (‘kadry') or die ('unable to select db');

На шаг два я “принимаю” входные переменные. Естественно, что мне нужно корректно обработать ситуацию, когда направление сортировки не задано:

$from = $_REQUEST['from'];
$size = $_REQUEST['size'];
$sortBy = isset($_REQUEST['sortBy'])?$_REQUEST['sortBy']:"user_id";
$sortDir = isset($_REQUEST['sortDir'])?$_REQUEST['sortDir']:"";

На шаг три я формирую строку SQL-запроса, использующего полученные php-скриптом переменные. Обратите внимание на то, что перед символом “*” я поместил ключевое слово “SQL_CALC_FOUND_ROWS”. Он нужно, для того чтобы mysql помимо отбора записей из таблицы employees подсчитал сколько записей в этой таблице всего.

$sql = "select SQL_CALC_FOUND_ROWS * from employees order by $sortBy $sortDir limit $from, $size";

Завершающий этап – выполнить запрос и сформатировать JSON-строку, отправляемую назад в браузер. Кроме основного запроса, я выполнил еще и дополнительный “select FOUND_ROWS()”, который вернет количество записей отобранных первым запросом.

$rez = mysql_query ($sql) or die ('unable to select data' . $sql);
 $count = mysql_query ('select FOUND_ROWS()') or die ('unable to calculate total records count');
 $count = mysql_fetch_array ($count);
 $count = $count[0];
 $rows = array ();
 while ($row = mysql_fetch_assoc($rez)) 
   $rows [] = $row;
 
 $final = array ('users' => array ('user' => $rows), 'total_records' => $count );
 
 print json_encode ($final);

Количество записей возвращается как значение переменной “total_records”. Теперь нужно подсказать YUI как добраться до этого значения. Когда я создаю объект DataSource и указываю для него свойство responseSchema, то помимо ссылки на массив записей, можно попросить YUI извлечь из входного JSON-объекта другие поля, играющие роль метаинформации, в частности, поля total_records с количество всех записей в БД:

ds.responseSchema = { 
    resultsList: "users.user", 
    fields: [
         "fio",
         {key:"birthday", parser: strToDate},
         "sex",
         {key:"salary", parser:"number"}
    ] ,
    metaFields: { total_records: "total_records"}  
 };

Последняя загвоздка в том, что YUI все еще не ассоциирует количество записей в таблице (то, что нужно для работы paginator-а) и метапеременную total_records. Давайте подскажем ему это:

function handlePayload (oRequest, oResponse, oPayload) { 
 oPayload.totalRecords = parseInt(oResponse.meta.total_records); 
 return oPayload; 
}

Последний шаг, это привязать функцию handlePayload к DataTable (сама переменная table создается точь-в-точь как ранее вызовом конструктора DataTable):

table.handleDataReturnPayload = handlePayload;

На рис. 1 я поймал момент, когда после нажатия на кнопку сортировки отправляется запрос на сервер, и пока ответ не пришел, то выводится надпись “Loading …”. Как изменить внешний вид этого сообщения я рассказывал в прошлой статье.

Изображение:yui_17_1.png

Теперь займемся дальнейшими улучшениями DataTable. И начнем с того, что попробуем реализовать функцию “спрятать столбец”. Это очень полезная функция, если в таблице очень много колонок и не все из них имеет смысл одновременно показывать. В левом верхнем углу таблицы можно сделать специальную кнопку, по нажатию на которую появляется диалоговое окно. В этом окне вы с помощью checkbox-ов отмечаете то, какие колонки в DataTable нужно спрятать или отобразить. К сожалению, хотя YUI содержит ряд методов для управления видимостью колонок, но “готового из коробки” решения нет (снова пинок в пользу ExtJs). Так что тряхнем стариной (точнее вспомним материал из четвертой статьи серии) и попробуем реализовать диалоговое окно выбора отображаемых колонок сами. Пример мы реализуем по шагам: первым делом я хочу добавить к таблице еще одну фиктивную колонку. Она будет расположена крайней слева и не содержит никакой информации: мне нужен пустой заголовок таблицы, в котором разместится кнопка вызова диалогового окна (описания остальных колонок остались без изменения).

var columns = [ 
{key:"fake_hide_show", sortable:false, label:'<div id="columnHideShowBtn">&nbsp;</div>'}, 
{key:"fio", sortable:true}, 
… ];

Фиктивное поле “fake_hide_show” естественно не должно быть упомянуто в описании источника данных (responseSchema). Теперь внимание на свойство “label” в описании фиктивной колонки: вместо надписи с названием колонки я решил вывести пустой блок div с идентификатором “columnHideShowBtn”. Благодаря этому уникальному имени я могу в последующем изменить внешний вид или стилевое оформление заголовка колонки. Например, так я превращаю блок div в кнопку (при наведении мыши курсор также меняет вид как будто для кнопки) с картинкой:

<style>
#columnHideShowBtn {
 background:#D8D8DA url(pic_props.png) no-repeat 50% 50%;
 cursor: pointer;
 width: 24px;
 height: 24px; 
}
</style>

Следующий шаг – сделать так, чтобы по нажатию на “кнопку” вызывалось диалоговое окно с набором checkbox-ов соответствующих колонкам таблицы. Привязать к любому элементу html обработчик события проще простого с помощью YUI модуля events:

YAHOO.util.Event.addListener("columnHideShowBtn", "click", doHideShowDialog);

Функция doHideShowDialog должна не просто создать диалоговое окно (класс SimpleDialog), но настроить его содержимое в соответствии со списком колонок таблицы. Мне нужно организовать цикл по всем колонкам (кроме, конечно, первой фиктивной) и для каждой из колонок разместить на диалоговом окне тег checkbox-а. Более того, нужно проверить в каком сейчас состоянии находится колонка, т.е. спрятана она или отображается и это повлияет на внешний вид checkbox-а:

function doHideShowDialog (){
 d = new YAHOO.widget.SimpleDialog("placeholderforDialog", 
   { 
     width : "400px", 
     icon: YAHOO.widget.SimpleDialog.ICON_INFO,
     fixedcenter : true, 
     visible : false, 
     constraintoviewport : true, 
     buttons : [ 
         { text:"Accept", handler:onAccept}, 
         { text:"Discard", handler:onDiscard, isDefault:true} 
     ] 
  } 
 ); 
 
 // настройка внешнего вида диалогового окошка
 d.setHeader("Выберите колонки таблицы для отображения");    	
 rez = '<br />';
 var defs = table.getColumnSet().getDefinitions();
 for (i = 1; i < defs.length; i++){
   def = defs[i];
   column = table.getColumnSet().getColumn (i);
   label = def.label || def.key;
   if_checked = column.hidden?'':'checked';
   rez+= '<input type="checkbox" id="chk_'+i+'" '+if_checked+'/>'  + label + "<br />";
 } 
 
 d.setBody(rez); 
 
 d.render (document.body);
 d.show (); 
}

Начало функции не требует комментариев: я просто обратился к четвертой статье серии и скопировал оттуда кусочек кода, создающий диалоговое окно. Там же вы найдете и подробное описание параметров конструктора класса SimpleDialog. Создав диалоговое окно, я формирую большую-большую строку (rez), задающую будущий внешний вид диалогового окошка. Для этого я в цикле перебираю все колонки (пропуская нулевую фиктивную), для каждой из них извлекаю из table текстовую надпись заголовка столбца (или название поля таблицы, если специальный заголовок отсутствует). Эту строку я вывожу рядом с элементом checkbox. Для каждого из checkbox-ов может быть установлена отметка “checked” в том, случае если соответствующая ему колонка видима (hidden). Важно, что у каждого из checkbox-ов есть уникальный идентификатор, формируемый по правилу “chk_” плюс номер колонки. Это мне нужно для того, чтобы в последующем (когда пользователь закроет диалоговое окно нажатием кнопки “Accept” или “Принять”) можно было выполнить обратную операцию и, основываясь на том какие checkbox-ы пользователь “поставил” или “снял”, показать и спрятать, соответственно, колонки таблицы.

function onAccept (e){
 var defs = table.getColumnSet().getDefinitions();
 for (i = 1; i < defs.length; i++)
   if (YAHOO.util.Dom.get('chk_' + i).checked)
     table.showColumn (i);
   else
     table.hideColumn (i);
 this.hide (); 
}

Внутри функции я снова организую цикл по списку колонок таблицы (переменная table) и для каждой из них получаю ссылку на соответствующий html-элемент checkbox. После проверки в каком состоянии он находится нужно прятать/показывать колонку таблицы. Завершив цикл нужно спрятать само диалоговое окно настроек (здесь this – ссылка на объект SimpleDialog). Последнее о чем стоит упомянуть так это функция onDiscard. Она вызывается при нажатии на кнопку “Discard” на диалоговом окошке. Устройство функции тривиально и я его не привожу: нужно всего лишь вызывать метод this.hide() для того, чтобы спрятать диалоговое окно настройки таблицы, не изменяя внешний вид самой таблицы. Внешний вид примера показан на рис. 2.

Изображение:yui_17_2.png

Таблицы редко используются только для отображения информации – гораздо чаще для ее редактирования. В простейшем случае сама таблица находится в режиме “только для чтения”. Зато по выбору какой-либо строки (неплохо бы это еще и визуально отметить) внизу страницы можно отобразить панель с множеством полей редактирования для колонок таблицы. Этот вариант особенно удобен, если количество отображаемых колонок в таблице гораздо меньше всех, которые есть у записи (и которые может захотеть отредактировать пользователь). На специальной панели должно быть достаточно места для того, чтобы разместить сложные (главное удобные) элементы редактирования. Если количество полей таблицы не велико, и все они умещаются на экране без громоздких горизонтальных прокруток, то можно использовать и inline-редактирование, как если бы мы захотели отредактировать ячейку обычной таблицы ms excel. Давайте попробуем реализовать оба эти варианта и начнем с редактирования содержимого таблицы на специальной панели. Первым шагом я должен настроить режим выделения строк таблицы: т.е. можно ли выделить с нажатой клавишей “ctrl” несколько строк или только одну строку. Создавая DataTable, я передам ее конструктору параметр selectionMode, равный “single”. Это значит, что выделить можно только одну строку. Кроме варианта “single” есть еще “standard” (режим по умолчанию), когда работают клавиши ctrl и shift для выделения нескольких строк. Режим “singlecell” разрешает выделить только одну ячейку таблицы, а режим “cellblock” позволяет вам выделить любые ячейки таблицы, так чтобы они образовывали прямоугольный блок. А режим “cellrange” позволяет выделить несколько последовательно расположенных ячеек таблицы (как будто выделяете диапазон дат на календаре). table = new YAHOO.widget.DataTable("tableplaceholder", columns, ds, { selectionMode: "single"} ); Я указал режим выделения и запустил пример, но он не заработал. Разработчики YUI по странной причине решили, что мало назначить режим выделения, нужно еще и назначить для DataTable функции, которые обрабатывают события: “выделена строка”, “курсор мыши над строкой”, “курсор мыши покидает строку”. К счастью, в классе DataTable есть приемлемые реализации этих функций по-умолчанию:

function onSelectRow (oArgs){
 table.onEventSelectRow (oArgs); 
}
 
// и назначаем функции
table.subscribe("rowMouseoverEvent", table.onEventHighlightRow); 
table.subscribe("rowMouseoutEvent", table.onEventUnhighlightRow); 
table.subscribe("rowClickEvent", onSelectRow); 
 
// выделяем первую строку
table.selectRow(table.getTrEl(0));

Первые два присвоения функции обработчика событий заурядны: я использую встроенные в YUI функции, которые подсвечивают строку пока над ней находится курсор. Для обработки события "rowClickEvent" пришлось создать собственную функцию onSelectRow, которая просто перевызывает опять-таки стандартную функцию onEventSelectRow из YUI, подсвечивающую выделенную строку таблицы (см. рис. 3). В следующей статье останется только показать то, как внутри функции “onSelectRow” узнать то, какая строка таблицы была выделена. Затем получить содержимое всех полей строки и наполнить этими данными панель редактирования.

Изображение:yui_17_3.png

Subscribe Now!

 

ObMachine projects & articles (java, flash, flex, php, ...)  -- black-zorro.com