Как создать кастомный pane для панелайзера

Создание кастомного пейна для панелайзера и панели

На стандартных пейнах из коробки далеко не уедешь, писать кастомный придется в 90% случаев. Как написать кастомный пейн, какие функции использовать и какая должна быть структура плагина - сегодня в этой статье.
Необходимо определиться за что будет отвечать плагин, какие в нем будут настройки и что он будет выводить. Чтобы пример не был совсем тривиальным, пусть плагин выводит кастомные табы (по сути простые ссылки, сверстанные в виде табов). В настройках пейна можно будет добавлять/удалять таб по аяксу, определять его заголовок, путь, видимость и порядок (вес).

Содержание

Структура плагина

Для объявления плагина можно использовать уже готовый тип content_types или же объявить собственный. Воспользуемся первым вариантом. Пейн для панелайзера (как и для панелей) будет создан на основе ctools plugin API. Структура как и большинства плагинов, схожая - в модуле располагаем папку plugins, в ней тип плагина (в данном случае content_types), далее непосредственно сам файл, содержащий практически все необходимое для работы кастомного pane.
Структура модуля

Объявление плагина

Объявление плагина в файле MYMODULE_tabs.inc

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
 * Объявление плагина.
 */
$plugin = array(
  'title' => t('MYMODULE tabs'),
  'description' => t('Output custom tabs'),
  'render callback' => 'MYMODULE_tabs_render',
  'admin title' => 'MYMODULE_tabs_admin_title',
  'admin info' => 'MYMODULE_tabs_admin_info',
  // категория расположения пейна.
  'category' => t('MYMODULE'),
  'edit form' => 'MYMODULE_tabs_edit_form',
);

В объявлении плагина приведены колбеки для формы настроек, рендера, для формирования заголовка и описания на странице редактирования. Далее распишем каждый из них.
Начнем с рендер функции MYMODULE_tabs_render(). Размещаем в этом же файле. В данной функции получаем все добавленные табы, пробегаем по каждому из них для формирования массива ссылок и отдаем теме theme_item_list() для построения обычного HTML списка.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
 * Render callback for MYMODULE tabs.
 */
function MYMODULE_tabs_render($subtype, $conf, $args, $context) {
  $block = new stdClass();
 
  $tabs  = $conf['tabs'];
  $items = array();
  foreach ($tabs as $tab) {
    if ($tab['enabled']) {
      $items[] = l($tab['name'], $tab['path']);
    }
  }
 
  $content['#markup'] = theme('item_list', array(
    'items' => $items,
    'title' => '',
    'type'  => 'ul',
    'attributes' => array('class' => array('clearfix')),
  ));
 
  $path = drupal_get_path('module', 'MYMODULE');
  $content['#attached']['css'][] = $path . '/css/MYMODULE_tabs.css';
  $block->content['content'] = $content;
 
  return $block;
}

MYMODULE_tabs.css содержит немного кода для придания ссылкам стиля табов.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
.pane-mymodule-tabs ul.clearfix {
  margin: 0;
  padding: 0;
}
 
.pane-mymodule-tabs ul.clearfix li {
  list-style: none;
  float: left;
  border: 1px solid #73909e;
  padding: 0.1em 0.5em;
  margin: 0.2em;
  border-radius: 5px;
  background: #A0C4D2;
}
 
.pane-mymodule-tabs ul.clearfix li:hover {
  cursor: pointer;
}
 
.pane-mymodule-tabs ul.clearfix li a {
  text-decoration: none;
  color: #fff;
  font-family: Arial, Helvetica, sans-serif;
}

Следующие два - MYMODULE_tabs_admin_title() и MYMODULE_tabs_admin_info() отвечают за отображение административного заголовка и краткой информации о добавленных табах соответственно. Функция MYMODULE_tabs_get_names() является вспомогательной, которая агрегирует и возвращает массив заголовков табов.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/**
 * Returns the administrative title for a module.
 */
function MYMODULE_tabs_admin_title($subtype, $conf) {
  $title = 'MYMODULE tabs';
  $items = MYMODULE_tabs_get_names($conf);
  $title .= !empty($items) ? ': ' . implode(', ', $items) : '';
 
  return $title;
}
 
/**
 * Returns admin info about Module pane.
 */
function MYMODULE_tabs_admin_info($subtype, $conf, $context) {
  $block = new stdClass();
  $items = MYMODULE_tabs_get_names($conf);
  $block->title = 'Expand for view the added tabs';
  $block->content = !empty($items) ? implode(', ', $items) : t('No available info.');
 
  return $block;
}
 
/**
 * Returns the array of tab names.
 */
function MYMODULE_tabs_get_names($conf) {
  $tabs = $conf['tabs'];
  $items = array();
  foreach ($tabs as $tab) {
    if ($tab['enabled']) {
      $items[] = $tab['name'];
    }
  }
 
  return $items;
}

И, наконец, остается функция формы настроек, с помощью которой и будет осуществляться добавление табов и их конфигурация. Т.к. данная функция будет иметь аякс операции (добавление/удаление), то необходимо чтобы она всегда была в области видимости. Если расположить ее в тот же файл, где находится описание плагина - форма не будет видна после аякс событий. Поэтому расположим ее, вместе с аякс колбеком в файле MYMODULE.module.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
/**
 * MYMODULE tabs form.
 */
function MYMODULE_tabs_edit_form($form, &$form_state) {
  $conf = $form_state['conf'];
 
  $default = array(
    'enabled' => 0,
    'name' => '',
    'path' => '',
    'weight' => 0,
  );
  $tabs = isset($conf['tabs']) ? $conf['tabs'] : array($default);
 
  if (isset($form_state['values']['tabs'])) {
    $tabs = $form_state['values']['tabs'];
  }
  
  // Если пользователь нажимает на кнопку "Добавить" - добавляем массив с дефолтными значениями.
  $is_trigger = isset($form_state['triggering_element']['#value']);
  if ($is_trigger && isset($form_state['triggering_element']['#add_more_id'])) {
    $tabs[] = $default;
  }
 
  // Если пользователь нажимает на кнопку "Удалить" - убираем соответствующий таб из массива.
  if ($is_trigger && isset($form_state['triggering_element']['#remove_id'])) {
    list(, $id) = explode('|', $form_state['triggering_element']['#remove_id']);
    unset($tabs[$id]);
  }
 
  $form['tabs'] = array(
    '#type'  => 'container',
    // Будем рендерить через свою темирующую функцию.
    '#theme' => 'MYMODULE_tabs',
    '#tree'  => TRUE,
  );
 
  // Кнопка "добавить".
  $form['add_more'] = array(
    '#type' => 'button',
    '#add_more_id' => 'mymodule-button-add-more',
    // Важное свойство, не позволяющее аякс кнопке сабмитать форму.
    '#executes_submit_callback' => FALSE,
    '#value' => t('Add more'),
    '#ajax' => array(
      'callback' => 'MYMODULE_tabs_ajax_callback',
      'wrapper'  => 'mymodule-tabs-id',
      'method'   => 'replace',
      'effect'   => 'fade',
    ),
  );
 
  foreach ($tabs as $key => $tab) {
    $form['tabs'][$key]['enabled'] = array(
      '#type' => 'checkbox',
      '#default_value' => $tab['enabled'],
    );
 
    // Заголовок таба.
    $form['tabs'][$key]['name'] = array(
      '#type' => 'textfield',
      '#default_value' => $tab['name'],
    );
 
    // Урл таба.
    $form['tabs'][$key]['path'] = array(
      '#type' => 'textfield',
      '#default_value' => $tab['path'],
    );
 
    // Кнопка "удалить".
    $form['tabs'][$key]['del'] = array(
      '#type' => 'button',
      '#executes_submit_callback' => FALSE,
      '#value' => t('remove'),
      '#remove_id' => 'mymodule-remove-button-id|' . $key,
      '#name' => 'op-' . $key,
      '#disabled' => count($tabs) < 2,
      '#ajax' => array(
        'callback' => 'MYMODULE_tabs_ajax_callback',
        'wrapper'  => 'mymodule-tabs-id',
      ),
    );
 
    // Вес.
    $form['tabs'][$key]['weight'] = array(
      '#type'  => 'weight',
      '#delta' => 10,
      '#title' => t('Weight'),
      '#title_display' => 'invisible',
      '#default_value' => $tab['weight'],
      '#attributes' => array('name' => 'weight[' . $key . ']'),
    );
  }
 
  return $form;
}

В сабмит колбеке добавляем созданные/измененные табы в массив conf.

1
2
3
4
5
6
/**
 * Submit callback for MYMODULE_tabs_edit_form().
 */
function MYMODULE_tabs_edit_form_submit($form, &$form_state) {
  $form_state['conf']['tabs'] = $form_state['input']['tabs'];
}

Ajax колбек, как и вся ajax логика взята из статьи Разные способы создания Ajax запросов

1
2
3
4
5
6
/**
 * Ajax callback for adding/removing tab.
 */
function MYMODULE_tabs_ajax_callback($form, &$form_state) {
  return $form['tabs'];
}

Так как список добавленных табов рендерится в виде таблицы с помощью темы MYMODULE_tabs, необходимо объявить ее в хук тем.

1
2
3
4
5
6
7
8
9
10
/**
 * Implements hook_theme().
 */
function MYMODULE_theme() {
  return array(
    'MYMODULE_tabs' => array(
      'render element' => 'form',
    ),
  );
}

И непосредственно сам код темы возвращает таблицу draggable, описанную по аналогии с третьим примером из статьи Создаем разные виды Drupal HTML таблиц (часть 1).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
/**
 * Возвращает HTML для таблицы настроек табов.
 */
function theme_MYMODULE_tabs($variables) {
  $tabs = $variables['form'];
  $rows = array();
  foreach (element_children($tabs) as $key) {
    $tabs[$key]['weight']['#attributes']['class'] = array('tabs-order-weight');
    $rows[] = array(
      'data' => array(
        drupal_render($tabs[$key]['enabled']),
        drupal_render($tabs[$key]['name']),
        drupal_render($tabs[$key]['path']),
        drupal_render($tabs[$key]['del']),
        drupal_render($tabs[$key]['weight']),
      ),
      'class' => array('draggable'),
    );
  }
 
  // Заголовок таблицы.
  $header = array(
    t('Enabled'),
    t('Tab name'),
    t('Path'),
    t('Action'),
    t('Weight'),
  );
 
  $output = theme('table', array(
    'header' => $header,
    'rows'   => $rows,
    'empty'  => t('Table is empty'),
    'attributes' => array('id' => 'tabs-order'),
    'sticky' => FALSE,
  ));
 
  $output = '<div id="mymodule-tabs-id">' . $output . '</div>';
  $output .= drupal_render_children($form);
  drupal_add_tabledrag('tabs-order', 'order', 'sibling', 'tabs-order-weight');
 
  return $output;
}

Также не забываем объявить хук hook_ctools_plugin_directory()[1], чтобы кастомный плагин был виден панелям и сбросить кеш.

1
2
3
4
5
6
7
8
/**
 * Implements hook_ctools_plugin_directory().
 */
function MYMODULE_ctools_plugin_directory($module, $plugin) {
  if (in_array($module, array('panelizer', 'ctools', 'page_manager', 'panels'))) {
    return 'plugins/' . $plugin;
  }
}

Демонстрация работы

Для демо перейдем в настройки контента любой сущности.
добавление нового контента в панелайзер
Добавляем кастомный пейн.
добавление кастомного пейна
Добавляем несколько табов (заполняем имя, путь, ставим галочку что включен и меняем вес по желанию).
список новых табов
Сохраняем изменения. Предварительно сохраненный пейн имеет теперь свой административный заголовок и описание.
еще не сохраненный кастомный pane
Жмем кнопку Save и переходим на превью обновленной сущности.
просмотр обновленный сущности.png
Создание кастомных пейнов для контрибного модуля panels аналогично.

Дополнительная информация по статье

  1. http://www.drupalcontrib.org/api/drupal/contributions!ctools!ctools.api.php/function/hook_ctools_plugin_directory/7 - описание хука hook_ctools_plugin_directory.