В большинстве случаев для множественного вывода материалов на сайте, построенном на Drupal, достаточно прибегнуть к услугам модуля Views. Данный модуль имеет массу возможностей и доп. модулей. Однако иногда встаёт задача нестандартного вывода нод на странице, когда "ковыряние" во Views окажется более сложным и нудным делом, нежели написание своего модуля. Рассмотрим упорядоченный вывод нод в таблице с постраничной навигацией, сортировкой по любой колонке, фильтрами и всё это через Ajax!
1. Создаём папку модуля, info-файл и hook-menu()
Сперва пишем свой модуль (назовём его my_module). Создаём стандартный info файл по пути '/sites/all/modules/my_module/my_module.info':
name = My CRM module description = "Section CRM in Discopack CRM" core = 7.x package = Other
Далее создаём основной файл модуля по пути '/sites/all/modules/my_module/my_module.module'. В нём сперва прописываем hook_menu():
/**
* Реализация hook_menu().
*/
function my_module_menu() {
$items = array();
/** Листинг заказов **/
$items['orders'] = array(
'title' => t('Orders'),
'page callback' => 'my_module_orders_table_data',
'access arguments' => array("access content"),
'type' => MENU_CALLBACK
);
$items['orders/callback'] = array(
'title' => 'My module Callback',
'type' => MENU_CALLBACK,
'page callback' => '_my_module_orders_table_data_callback',
'access arguments' => array('access content')
);
return $items;
}
Почему мы прописываем 2 страницы? Страница /orders - на неё мы будем непосредственно ссылаться и переходить в браузере. А на javascript, находясь на данной странице, мы будем отправлять Ajax-запрос на странице /orders/callback откуда будем получать нужные нам материалы. В данном примере будем работать с заказами (orders).
2. Создаём основную страницу, в которую будут помещаться ноды по Ajax
Теперь нам нужно добавить функции "my_module_orders_table_data" и "_my_module_orders_table_data_callback", в которых и будем выполнять всю работу. Давать названия этим функциям можно абсолютно любые. Однако для удобства я рекомендую в начале названия любой вашей функции внутри модуля писать название этого модуля - это общепринятое правило. Также для себя я взял за правило добавлять нижнее подчеркивание к "callback-функциям", на страницы которых мы переходить напрямую не будем. Итак, привожу пример кода "my_module_orders_table_data":
function my_module_orders_table_data () {
drupal_add_js(path_to_theme() . '/js/jquery.url.js');
drupal_add_js(path_to_theme() . '/js/jquery.cookie.js');
drupal_add_js(drupal_get_path('module', 'my_module') . '/my_module.js');
drupal_add_js('initializeTable();', 'inline');
return theme('my_module_orders_view', false );
}
function my_module_theme() {
return array(
'my_module_orders_view' => array(
'arguments' => array('variable' => NULL),
'template' => 'my_module_orders_view'
)
);
}
Рассмотрим код по порядку.
Подключаем jQuery библиотеки для удобного парсинга урла (jquery.url.js) и для работы с куками (jquery.cookie.js), заранее закинув эти файлы в папку темы.
Далее подключаем наш основной javascript файл, который и будет выполнять Ajax запрос и подставлять данные на страницу - my_module.js. Его мы закидываем в папку модуля (можно и в папку тему - кому как больше нравится).
Далее мы вызываем javascript функцию 'initializeTable();', которая у нас будет определена в файле my_module.js. Ниже будет расписано.
После всего этого мы возвращаем шаблон 'my_module_orders_view', который у нас определен в hook_theme() и ссылается на одноименный файл в папке темы (my_module_orders_view.tpl.php). В данном файле у нас будет лежать шаблон, например:
<div id="filters">
<div id="limit" class="filter_block">Выводить по:
<select id="limit_items">
<option selected>10</option>
<option>50</option>
<option>100</option>
</select>
</div>
<div id="order_id" class="filter_block">Номер заказа:
<select class="update_select" id="nid_order">
<option value="" selected>Все</option>
<?php
$nodes = node_load_multiple(array(), array('type' => 'order'));
foreach($nodes as $node_item):
echo '<option value="'.$node_item->nid.'">'.$node_item->nid .'</option>';
endforeach;
?>
</select>
</div>
<div id="status" class="filter_block">Статус:
<select class="update_select" id="status_order">
<option value="" selected>Все</option>
<option value="request">Запрос</option>
<option value="canceled">Отменен</option>
<option value="confirmed">Подтвержден</option>
<option value="launched">Запущен</option>
<option value="production">Производство</option>
<option value="ready">Готов</option>
<option value="shipped">Отгружен</option>
<option value="checked">Проверен</option>
<option value="closed">Закрыт</option>
<option value="archive">Архив</option>
</select>
</div>
</div>
<div id="table-container"></div>
В данном файле у нас сперва идёт "div#filters", в котором у нас лежат селекты для сортировки и фильтрации. А далее находится основной 'div#table-container' куда мы и будем помещать нашу таблицу по Ajax.
3. Прописываем SQL запрос в callback-функции
Содержимое функции "_my_module_orders_table_data_callback()" у нас будет следующим:
function _my_module_orders_table_data_callback() {
header("Content-type: text/html");
header("Expires: Wed, 29 Jan 1975 04:15:00 GMT");
header("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT");
header("Cache-Control: no-cache, must-revalidate");
header("Pragma: no-cache");
$output = '';
/* Сортировка нод */
if(isset($_GET['sort']) && isset($_GET['order'])) {
if($_GET['sort'] == 'asc')
$sort = 'ASC';
else
$sort = 'DESC';
switch($_GET['order']) {
case 'ID':
$order = 'nid';
$col_number = 1;
break;
case 'Дата заказа':
$order = 'created';
$col_number = 2;
break;
/* ...и т.п. */
case 'Менеджер':
$order = 'field_user_surname_value';
$col_number = 10;
break;
default:
$order = 'nid';
$col_number = 1;
}
} else {
$sort = 'ASC';
$order = 'nid';
}
/* Ограничение числа нод (по-умолчанию "10"") */
(isset($_GET['limit'])) ? $limit = intval($_GET['limit']) : $limit=10;
// Выполняем SELECT запрос
$query = db_select("node", "c");
// Ограничиваем выборку по типам "Заказы"
$query->condition('c.type', 'order');
/*** Добавляем дополнительные таблицы с необходимыми полями *************************************/
/** Статус заказа **********************************************************/
$query->leftJoin('field_data_field_status', 'status', 'status.entity_id = c.nid AND status.bundle = :bundle',array(':bundle'=>'order'));
$query->leftJoin('field_config', 'status_val', 'status_val.field_name = :field_status', array(':field_status' => 'field_status'));
/** Контакт ***************************************************************/
$query->leftJoin('field_data_field_fio_order', 'contact', 'contact.entity_id = c.nid AND contact.bundle = :bundle');
/** Сумма заказа ***************************************************************/
$query->leftJoin('field_data_field_summ', 'summ', 'summ.entity_id = c.nid AND summ.bundle = :bundle');
// Фильтрация по номеру заказа
if (isset($_COOKIE['nid_order'])) $_GET['nid_order']=$_COOKIE['nid_order'];
if ((isset($_GET['nid_order']))and($_GET['nid_order'])) {
$nid_order = $_GET['nid_order'];
$query->condition('c.nid', $nid_order);
}
// Фильтрация по статусу заказа
if (isset($_COOKIE['status_order'])) $_GET['status_order']=$_COOKIE['status_order'];
if ((isset($_GET['status_order']))and($_GET['status_order'])) {
$status = $_GET['status_order'];
$query->condition('status.field_status_value',$status);
}
/** Добавляем необходимые поля *********************************************/
$query->fields('c',array('nid','title','created'));
$query->fields('status',array('field_status_value'));
$query->fields('status_val',array('data'));
$query->fields('contact',array('field_fio_order_value'));
$query->fields('summ',array('field_summ_value'));
$query->groupBy('c.nid'); // Группируем по Node ID
$query->orderBy($order, $sort); // Сортируем выборку
$total_count = $query->countQuery()->execute()->fetchField(); // Считаем общее кол-во
$query = $query->extend('TableSort')->extend('PagerDefault')->limit($limit); // Постраничная навигация
$result = $query->execute(); // Выполняем запрос
/** Цикл для заполнения строк таблицы *****************************************/
while($data = $result->fetchObject()) {
$status = unserialize($data->data); // Получаем значение статуса
$status=$status['settings']['allowed_values'][$data->field_status_value];
$rows[] = array(
$data->nid,
'<a target="_blank" href="/'.url("node/".$data->nid).'/">'.$data->title.'</a>',
$data->field_summ_value,
$status,
$data->field_fio_order_value
);
}
/** Заголовки столбцов таблицы *****************************************/
$header = array(
array('data' => 'ID', 'field'=>'nid', 'sort' => 'asc', 'class'=>array('id_label')),
array('data' => 'Название заказа','field'=>'title', 'class'=>array('title_label')),
array('data' => 'Сумма', 'field'=>'field_summ_value', 'class'=>array('summ_label')),
array('data' => 'Статус', 'field'=>'field_status_value', 'class'=>array('status_label')),
array('data' => 'Контакт', 'field'=>'contact_surname_field_user_surname_value', 'class'=>array('contact_label'))
);
$header[$col_number]['class'][0] .= " ".strtolower($sort);
// Настройки вывода таблицы
$output = theme_table(
array(
'header' => $header,
'rows' => $rows,
'attributes' => array('id'=>'orders','class'=>'nice-table'),
'sticky' => false,
'caption' => '',
'colgroups' => array(),
'empty' => t("Ничего не найдено. Попробуйте расширить критерии поиска.")
)
).theme('pager');
$output .= "<div class='stat'><b>Отображено:</b> ".count($rows)." из ".$total_count."</div>";
die ($output);
}
Код изобилует комментариями, поэтому всё должно быть понятно. Отмечу лишь то, что в конце мы делаем die($output), так как результат мы будем получать по Ajax И помещать внутрь контейнера(<div>), поэтому нам нужно, чтобы на данной странице не было никаких "<head>","<body>", а только сами ноды.
4. Создаём Ajax запрос в my_module.js
Теперь перейдём к нашему основному JavaScript файлу. В нём код будет следующий:
function refreshTable(page, sort, order, limit, params) {
(function($) {
if(!page) page = 0;
if(!sort) sort = '';
if(!order) order = '';
if(typeof params == "undefined") params = { };
var this_url = window.location.pathname.substring(1);
if (this_url.slice(-1)!='/') { this_url = this_url+'/'; }
if((!params.status_order)&&($("#status_order").length)) {
params.status_order = $("#status_order").multipleSelect('getSelects');
}
if(!params.nid_order) {
params.nid_order = $('#nid_order').val();
($.cookie("nid_order")) ? $('#nid_order').val($.cookie("nid_order")) : '';
}
if (!limit) {
limit = $('#limit select').val();
if ($.cookie("limit")) {
limit = $.cookie("limit"); $('#limit select').val(limit);
}
}
$.ajax({
cache: false,
url: Drupal.settings.basePath + '?q='+this_url+'callback',
data: {
page: page,
sort: sort,
order: order,
status_order: params.status_order,
nid_order: params.nid_order,
limit: limit
},
dataType: 'text',
error: function(request, status, error) {
alert('Возникла непредвиденная ошибка!');
},
success: function(data, status, request) {
var html = data;
$('#table-container').html(html);
$('#table-container th a').
add('#table-container .pager-item a')
.add('#table-container .pager-first a')
.add('#table-container .pager-previous a')
.add('#table-container .pager-next a')
.add('#table-container .pager-last a')
.click(function(el, a, b, c) {
var url = $.url(el.currentTarget.getAttribute('href'));
refreshTable(url.param('page'), url.param('sort'), url.param('order'));
return (false);
});
}
});
})(jQuery);
}
function initializeTable() {
jQuery(document).ready(function() {
refreshTable();
(function($) {
/** Фильтрация по параметрам в листинге **/
$('select.update_select, select#limit_items').on('change',function(){
var this_url = window.location.pathname.substring(1);
var limit = $("#limit_items").val();
$.cookie("limit",limit,{expires: 7,path: '/' });
var page = 0;
var sort = '';
var order = '';
var status_order = $("#status_order").val();
$.cookie("status_order",status_order,{expires: 7,path: '/' });
var nid_order = $("#nid_order").val();
$.cookie("nid_order",nid_order,{expires: 7,path: '/' });
refreshTable(page, sort, order, limit, {
status_order:status_order,
nid_order:nid_order
});
return (false);
});
})(jQuery);
});
}
На этом всё. Помимо обычной фильтрации - она у нас еще сохранятся в cookies и запоминает выбранные значения.