Вывод материалов по Ajax в собственном модуле

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