В большинстве случаев для множественного вывода материалов на сайте, построенном на 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 и запоминает выбранные значения.