Перейти к основному содержимому

Сетки (LayoutApi)

Подсистема сеток позволяет управлять расположением виджетов на странице или в любом другом контейнере.

В качестве фреймворка для реализации данной плагинной системы использован PluginApi. Поэтому технически LayoutApi реализована в виде PluginApi-плагинов типа layout и работает в соответствии с особенностями PluginApi.

Layout-плагин - это именованная сетка, которая имеет набор именованных регионов, которые могут быть использованы для группировки виджетов, добавленных в сетку.

Сетки могут располагаться как в модулях, так в темах.
На уровне файловой системы каждая сетка представляет собой папку вида

  {module_or_theme}/src/Plugins/layout/{layout-name}

Регионы

Регион - это именованная и неделимая область (переменная), имеющая своё расположение в шаблоне сетки. Регион используется для группировки блоков переданных в сетку извне.

Каждая сетка имеет строго заданный набор регионов. Каждая сетка имеет по крайней мере один регион main.

Регионы могут иметь доп. настройки, с помощью которых можно указать стиль группировки блоков, кэширование, дефолтное содержание.

Для включения определённого стиля региона необходимо передать соответствующие настройки региона.

Примеры:

  layout_info.regions.main.style.type='xtabs'
layout_info.regions.main.style.type='xslider'

Стили отображения регионов

В настоящий момент доступны следующие стили отображения регионов:

  • default
  • xtabs
  • xslider

Рассмотрим подробнее каждый из этих стилей.

Стиль 'default'

Простое вертикальное склеивание блоков региона в порядке их добавления. Каждый блок оборачивается в следующий html-код:

<div class='b-xl__block $class'>
<div class='b-xl__block-title h2'><span>$title</span></div>
<div class='b-xl__block-content'>$block_html</div>
</div>

Стиль 'xtabs'

Отображение блоков региона в виде табов с помощью модуля xtabs.

Отображаются только блоки, у которых есть title. Возможна передача параметров в widget('xtabs') с помощью настроек стиля региона.

Например:

  layout_info.regions.main.style=['type'=>'xtabs','params'=>['id'=>'xxx','format'=>'accordion','js.history'=>1]]

Стиль 'xslider'

Отображение блоков региона в виде слайдов с помощью клиентского плагина xslider.

Задание настроек для региона производится следующим образом:

  layout_info.regions.<region_name>.<option_name>=<value>;
или
$data['layout_info']['regions']['<region_name>']['<option_name>']=<value>;

В настоящий момент поддерживаются следующие опции:

  • style - Настройки стиля отображения региона
  • default_block - Здесь можно указать дефолтный блок, который будет добавляться в том случае, если содержимое региона по какой-то причине оказалось пустым

Блоки

Блок - это HTML-код, который, в зависимости от настроек при передаче в layout, встраивается в один из регионов layout.

Существует несколько способов передачи блоков в layout:

  • В виде готового html-кода в формате ['src'=>"<html_code>"] или даже проще <html_code>
  • В виде виджета в формате ['src'=>'widget:<widget_name>','args'=>[...]]
  • В виде "ленивого" виджета в формате ['src'=>'widget_lazzy:<widget_name>','args'=>[...]]
  • В виде формы в формате ['src'=>'form:<form_id>','args'=>[...]]
  • В виде массива с настройками для вложенного layout в формате ['src'=>['layout'=>'<some_layout>','blocks'=>[[...]]]
  • В виде командных блоков в формате ['src'=>'cmd:<cmd_id>', ...]

В общем случае в настройки блока можно передавать следующие параметры:

  • src - Источник контента для блока
  • args - (опционально и только для источника в виде виджета или формы) Набор аргументов в виде индексированного массива для передачи в theme-функцию
  • region - (по умолчанию используется регион main) Название региона, в который следует встроить блок
  • title - (опционально) Заголовок, который будет ассоциирован с блоком. Стиль отображения заголовка может зависеть от текущего стиля региона. Например, если для региона выбран стиль tabs или accordion, то заголовок блок будет использован в качастве названия вкладки. Чтобы запретить отображение title для такого блока, достаточно установить title в <none>.
  • class - (опционально) CSS-классы, которые добавятся в обёртку блока
  • attrs - (опционально, пока не поддерживается) HTML-аттрибуты, которые добавятся в обёртку блока
  • id - (опционально) id блока

Примеры добавления блоков в сетку

// Блоки на странице хаба.
$blocks = [];

//Добавим форму фильтрации в правую колонку
$blocks[] = [
'src' => 'form:form_hashhub_filters',
'region' => 'sidebar2',
'args' => [$hub, $hashes],
'weight' => 0,
];

//Добавим карточку хаба в правую колонку
$blocks[] = [
'src' => 'widget:block-hashhub-card',
'region' => 'sidebar2',
'args' => [$hub],
'weight' => 100,
];

$advSidebarKey = Config::getValue('xtagsHashHubPageAdvSidebar', '');

if (!empty($advSidebarKey)){
//Добавим ленивый виджет рекламного баннера в правую колонку в самый низ
$blocks[] = [
'src' => 'widget_lazzy:block-xad',
'args' => [$advSidebarKey],
'region' => 'sidebar2',
'title' => '<none>',
'weight' => 900,
];
}

//В главную колонку в самый верх добавим список хэшей хаба
$blocks[] = [
'src' => 'widget:block-hashhub-hashes',
'region' => 'sidebar2',
'weight' => -1,
];

//Выключим регион content_header, поскольку не требуется отображение заголовка страницы
$blocks[] = [
'src' => 'cmd:disable_region',
'region' => 'content_header',
];

//Добавим блоки в сетку страницы
$layoutApi->addPageBlocks($blocks);

Добавление коммандных блоков в регион

Рассмотрим отдельно вариант передачи командных блоков. Коммандный блок заставляет LayoutApi выполнить определённую команду над набором блоков и регионов текущей сетки.

На данный момент существуют следующие команды:

  • disable_region - Выключает регион. Выключенный регион полностью игнорируется LayoutApi при формировании сетки
    Пример:
      $blocks[] = [
    'src' => 'cmd:disable_region',
    'region' => 'content_header',
    ];
    $layoutApi->addPageBlocks($blocks);
  • enable_region - Включает регион, если он был выключен ранее.
    Пример:
      $blocks[] = [
    'src' => 'cmd:enable_region',
    'region' => 'content_header',
    ];
    $layoutApi->addPageBlocks($blocks);
  • clear_region - Очищает регион (т.е. удаляет все блоки, содержащиеся в нём), указанный в параметре region.
    Пример:
      $blocks[] = [
    'src' => 'cmd:clear_region',
    'region' => 'content_header',
    ];
    $layoutApi->addPageBlocks($blocks);

Описание файлов сетки

В папке виджета могут быть следующие файлы:

  • {layout-name}.info.inc файл с настройками
  • {layout-name}.tpl.php файл, представляющий собой php-шаблон сетки
  • {layout-name}.inc файл, представляющий собой контроллер сетки
  • {layout-name}.helpers.inc
  • {layout-name}.css
  • {layout-name}.js
  • {layout-name}.icon.png или {layout-name}.icon.svg - изображение-схема сетки
  • папки с ресурсами (images, fonts, ...)

Что даёт LayoutApi

  1. Чёткое разделение сетки и блоков, что даёт возможность сосредоточиться на разработке блоков как самостоятельных строительных элементов интерфейса, не беспокоясь о настройке отступов, а также размещении блоков внутри сетки в зависимости от типа и размера устройства (компьютер, планшет, смартфон).
  2. Спецификация единых подходов при построении пользовательского интерфейса страниц и отдельных элементов
  3. Выделение типовых сеток при построении интерфейсов. Это формирует определённость и стабильность.
  4. Наделение сеток именем, что добавляет семантику коду интерфейсов
  5. Единый стандарт к разработке интерфейсов
  6. Повторное использование сеток
  7. Выработка чёткого и лаконичного "словаря" приложения
  8. Дополнительные возможности по оптимизации, мониторингу и отладке кода

Создание новых сеток

Поскольку сетка это всегда PluginApi-плагин типа layout, то добавление новой сетки сводится к следующим шагам:

  1. В модуле или теме создаём папку src/Plugins/layout/{layout-name} (например, src/Plugins/layout/page)

  2. Создаём .info.inc файл с определением сетки.

    $plugin_info['name']='Шаблон страницы';
    $plugin_info['regions']=[
    'content_header'=>[
    'name'=>'Шапка контента',
    'visibility' => [
    'response_statuses'=> ['!403'], //Регион не будет отображаться в случае ошибки 403
    // 'perms' => [],
    // 'pathes' => ['!<front>'],
    ],
    ],
    'content_toolbar'=>[
    'name'=>'Тулбар контента',
    'visibility' => [
    'response_statuses'=> ['!403'],
    ],
    ],
    'messages'=>[
    'name'=>'Сообщения и нотификации системы',
    ],
    'tabs'=>[
    'name'=>'Табы',
    ],
    'main_top'=>[
    'name'=>'Верх содержимого',
    'blocks' => [ //Блоки будут добавляться в регион по умолчанию
    [
    'src'=>'widget:content-header_default',
    'default_block'=>true, //Блок будет добавлен в регион только, если в этот регион не было добавлено других блоков
    ]
    ],
    ],
    'main'=>[
    'name'=>'Содержимое'
    ],
    'main_bottom'=>[
    'name'=>'Низ содержимого',
    'visibility' => [
    'response_statuses'=> ['!403'], //Блок будет отображаться только, если это не страница ошибки 403
    ],
    ],
    'sidebar1'=>[
    'name'=>'Левая колонка',
    'visibility' => [
    'response_statuses'=> ['!403'],
    ],
    'blocks' => [ [ 'src'=>'widget:xmenu--vert'] ],
    ],
    'sidebar2'=>[
    'name'=>'Правая колонка',
    'visibility' => [
    'response_statuses'=> ['!403'],
    ],
    ],
    'page_header'=>[
    'name'=>'Шапка страницы',
    'blocks' => [ ['src'=>'widget:page-header'] ],
    ],
    'page_footer'=>[
    'name'=>'Подвал страницы',
    'blocks' => [ ['src'=>'widget:page-footer'] ],
    ],
    ];
    $plugin_info['scope'] = 'page'; // Для сеток, используемых для отрисовки страниц, необходимо указать этот параметр
  3. Создаём файл шаблона src/Plugins/layout/page/page.tpl.php
    В файле шаблона создаём необходимую разметку и отображаем в нужных местах регионы.

    В файле layout-шаблона всегда доступна переменная $data, имеющая следующую структуру:

    $data=[
    'blocks' => [], // Исходный массив блоков
    'regions' => [], // Массив полностью обработанных и готовых для отображения регионов
    ];

    Пример шаблона page.tpl.php:

    <div class='b-page'>
    <?php print $data['regions']['page_header']; ?>

    <div class="b-page__content" >
    <?php if($data['regions']['content_header']): ?>
    <div class="b-page__content-top">
    <?php print $data['regions']['content_header']; ?>
    <?php print $data['regions']['content_header__toolbar']; ?>
    </div>
    <?php endif; ?>
    <div class="<?php print $main_class; ?> b-page__content-main">
    <?php print $data['regions']['main']; ?>
    </div>

    <?php if($data['regions']['content_footer']): ?>
    <div class="b-page__content-bottom">
    <?php print $data['regions']['content_footer']; ?>
    </div>
    <?php endif; ?>
    </div>

    <?php print $data['regions']['page_footer']; ?>

    </div>

Отрисовка сетки

Для того чтобы отрисовать любую сетку, достаточно сделать вызов:

$html = print widget('xlayout',$layoutData)`; //, где `$layoutData` - это массив с настройками сетки и массив блоков.

Отрисовка страницы

Несмотря на то, что LayoutApi можно использовать для отрисовки любых сеток внутри любых виджетов, основное назначение LayoutApi - это управление отрисовкой запрашиваемой страницы.

Это удобно и позволяет получить полный контроль на структурой любой страницы сайта.

Вам становятся доступны следующие методы манипулирования структурой страницы:

  • Использование возможности автоматического выбора подходящяей сетки исходя из адреса текущей страницы
  • Использования хука hook_page_layout_variants_alter() для управления выбором подходящей сетки для текущей страницы
  • Использования хука hook_page_layout_alter() для управления содержимым сетки текущей страницы
  • Использования LayoutApi-методов:
    • Для добавления блоков в сетку текущей страницы
    • Для удаления блоков из сетки текущей страницы
    • Для отключения регионов из сетки текущей страницы

Далее будут подробно рассмотрены все эти возможности.

Пример добавления блоков на страницу

Чаще всего использование LayoutApi сводится к простому добавлению блоков в сетку страницы.

Пример:

Файл master-home.inc
$blocks = [
['src' => 'widget:block-xad', 'args' => ['main_banner'], 'region' => 'main_top'],
['src' => 'widget:block-homepage-banner','region'=>'main_top'],
['src' => 'widget:block-xad', 'args' => ['main_banner2'], 'region' => 'main_top'],
['src' => 'widget:block-events','region'=>'main_top'],
['src' => 'widget:block-forum-updates','region'=>'main_top'],
['src' => 'widget:block-wish-and-poll','region'=>'main_top'],
['src' => 'widget:block-reading','region'=>'main_top'],
['src' => 'widget:block-clubs','region'=>'main_top'],
['src' => 'widget:block-users-online','region'=>'main_top'],
['src' => 'widget:block-photo-video','region'=>'main_top'],
['src' => 'widget:block-xad', 'args' => ['main_banner5'], 'region' => 'main_top'],
];

$layoutApi->addPageBlocks($blocks);

Использование хука hook_page_layout_alter()

С помощью данного хука любой модуль сможет полностью переопределить настройки для передачи в widget('xlayout', $data) текущей страницы, т.е. выбрать подходящий для тек. страницы layout, а также сформировать настройки отображения блоков, добавить доп. блоки.

Данный хук будет срабатывать только для Server::isFrontContext().

Для Server::isAdminContext() используйте хук hook_admin_page_layout_alter() можно изменить массив вариантов сеток для текущего пути и таким образом повлиять на выбор сетки.

Пример, как может выглядеть реализация hook_page_layout_alter():

//Implements hook_page_layout_alter().

function app_page_layout_alter(array &$data): void
{
$context_api = xlib(ContextPluginRegistry::class);
$ctx_main=$context_api->get_main_context();
$ctx_node=$context_api->get_context('node');
$ctx_user=$context_api->get_context('user');

switch($ctx_main['type']){
case 'user':
$data['layout'] = 'user_page',
if($ctx_main['data']['type']=='root'){
$data['blocks'][]=['src'=>'widget:content-header_user-root','region'=>'content_header'];
}
break;
case 'blogs':
case 'adverts':
if($ctx_node){
if($ctx_node['data']['mode']=='view'){
$data['blocks'][]=['src'=>'widget:content-header_author-node','region'=>'content_header'];
}
}
else if($ctx_user){
$data['blocks'][]=['src'=>'widget:content-header_user-section','region'=>'content_header'];
}
break;
case 'marks':
if($ctx_node && $ctx_node['data']['mode']=='view'){
$data['blocks'][]=['src'=>'widget:content-header_marks-node','region'=>'content_header'];
$node = $ctx_node['data']['node'];
$data['blocks'][]=['src'=>'widget:node_cars','args'=>[$node],'region'=>'content_footer'];
}
break;
case 'reviews':
if($ctx_node && $ctx_node['data']['mode']=='view'){
$data['blocks'][]=['src'=>'widget:content-header_reviews-node','region'=>'content_header'];
}
else if($ctx_user){
$data['blocks'][]=['src'=>'widget:content-header_user-section','region'=>'content_header'];
}
break;
}

$data['#regions']['page_header']['default_block']=['src'=>'widget:page__header'];
$data['#regions']['page_footer']['default_block']=['src'=>'widget:page__footer'];

if(!Url::isPath('<front> user/%d/garage')){
$data['#regions']['content_header']['default_block']=['src'=>'widget:content-header_default'];
}
}

Использование хука hook_page_layout_variants_alter()

При отрисовке страницы LayoutApi выбирает наиболее подходящую сетку. По умолчанию выбор сетки для текущей страницы делается автоматически на основе текущего пути: рассчитывается наиболее подходящая по названию под текущий url path сетка.

LayoutApi формирует массив вариантов сеток для текущего пути. Варианты в конце массива считаются более специфичным (т.е. более подходящими) и поэтому имеют больший приоритет при определении сетки для страницы.

Принцип формирования массива вариантов покажем на примерах ниже.

Пример 1 - Для пути node/%node/delete будет сформирован следующий массив:

Для пути $path = 'node/%node/delete'  будет сформирован следующий массив:
$layoutVariants = [
"page",
"page-x",
"page-x-x",
"page-x-x-x",
"page-node",
"page-node-x",
"page-node-x-x",
"page-node-add",
"page-node-add-x",
"page-node-add-page",
];

Пример 2 - Для пути user/%/settings/profile будет сформирован следующий массив:

   $layoutVariants = [
"page",
"page-x",
"page-x-x",
"page-x-x-x",
"page-x-x-x-x",
"page-user",
"page-user-x",
"page-user-x-x",
"page-user-x-x-x",
"page-user-x-x",
"page-user-x-x-x",
"page-user-x-settings",
"page-user-x-settings-x",
"page-user-x-settings-profile",
];

Для Server::isFrontContext() с помощью хука hook_page_layout_variants_alter() можно изменить массив вариантов сеток для текущего пути и таким образом повлиять на выбор сетки.

Для Server::isAdminContext() с помощью хука hook_admin_page_layout_variants_alter() можно изменить массив вариантов сеток для текущего пути и таким образом повлиять на выбор сетки.

/**
* Implementation of hook_page_layout_variants_alter().
*/
function app_page_layout_variants_alter(array &$pageLayoutVariants): void
{
$layoutApi = xlib(LayoutPluginRegistry::class);
$contextApi = xlib(ContextPluginRegistry::class);

$ctxMain = $contextApi->get_main_context();
$ctxNode = $contextApi->get_context('node');

if (empty($ctxNode) && in_array($ctxMain['type'], ['articles', 'contests', 'events', 'parties', 'reviews'])) {
if (!Url::isPath('calendar')) {
$layoutApi->appendPageLayoutVariant('page-nodes');
$layoutApi->appendPageLayoutVariant("page-nodes-{$ctxMain['type']}");
}
}

if ($ctxMain['type'] == 'clubs' && $ctxMain['data']['type'] == 'page') {
if (empty($ctxNode) || $ctxNode['data']['type'] == 'view') {
$layoutApi->appendPageLayoutVariant('page-node-club-x');
$clubSection = arg(2);

if ($clubSection) {
$layoutApi->appendPageLayoutVariant("page-node-club-{$clubSection}");
}
}
}

if (Url::isPath('(photo|video|audio)*')) {
$mediaType = arg(0);
$layoutApi->appendPageLayoutVariant('page-media');
$layoutApi->appendPageLayoutVariant("page-media-{$mediaType}");
}

if (xlib(NodeUtils::class)->isNodePage('forum')) {
$layoutApi->appendPageLayoutVariant('page-node-forum');
}


// Страница всех отправок формы.
if (Url::isPath('user/%d/xpay/transactions*,login,signup,pass')) {
$layoutApi->appendPageLayoutVariant('page-x');
}
}

LayoutApi

В рамках данного раздела рассмотрим наиболее важные программные средства, которые доступны прикладным программистам при работе с LayoutApi.

  • API-методы класса LayoutPluginRegistry

Класс LayoutPluginRegistry

Логика работы подсистемы сеток реализована в классе LayoutPluginRegistry, который представляет собой реализацию репозитория плагинов типа layout в рамках PluginApi.

Как и любая другая подсистема, построенная на базе PluginApi, подсистема сеток предоставляет доступ к своему API с помощью xlib(LayoutPluginRegistry::class):

$layoutApi = xlib(LayoutPluginRegistry::class);

В контроллере виджета (.inc-файлы) и в шаблоне виджета (.tpl.php-файлы) уже есть объект $layoutApi, который рекомендуется использовать вместо вызова xlib(LayoutPluginRegistry::class).

Класс LayoutPluginRegistry предоставляет следующие собственные публичные методы:

  • renderLayout(array $data): string
  • addPageBlocks(array $blocks): void
  • setPageRegionStyle(string $region, string $style): void
  • appendPageLayoutVariant(string $variant): void
  • removePageLayoutVariant(string $variant): void
  • delegate(array &$data, string $layoutId = null, bool $overrideSettings = true): void

Метод renderLayout(array $data): string

Данный метод формирует html для layout-сетки, определяемой настройками $data.

Пример вызова addPluginAssets():

xlib(LayoutPluginRegistry::class)->renderLayout($data);

Метод addPageBlocks(array $blocks): void

Позволяет добавить блоки в сетку, отвечающую за отрисовку страницы.

Пример:

$blocks=[
['src'=>'form:form_events_filter','region'=>'sidebar2','weight'=>-1],
['src'=>'widget:section-menu','region'=>'sidebar'],
];
$layoutApi->addPageBlocks($blocks);

Метод setPageRegionStyle(string $region, string $style): void

Позволяет установить стиль региона $region

Пример:

$layoutApi->setPageRegionStyle('main', 'list');

Метод appendPageLayoutVariant(string $variant): void

Добавляет вариант сетки для отрисовки страницы в конец массива вариантов.

Перед добавлением варианта сначала производится его удаление. Это гарантирует однократное присутствие сетки в массиве вариантов.

Добавленный вариант автоматически получает приоритет перед уже добавленными, поскольку варианты из конца массива имеют приоритет перед вариантами из начала.

Примеры использования:

  $layoutId = $layoutApi->appendPageLayoutVariant('page-node-article');

Метод removePageLayoutVariant(string $variant): void

Удаляет вариант сетки для отрисовки страницы из массива вариантов.

Примеры использования:

  $layoutId = $layoutApi->removePageLayoutVariant('page-node-article');

Метод delegate(array &$data, string $layoutId = null, bool $overrideSettings = true): void

Передаёт (делегирует) управление, а также все переменные от текущей сетки к сетке $layoutId.

Предназначена для вызова в inc-файлах сеток.

Пример:

  // Допустим есть сетка page-node, а такжа сетка page-node-blog. 
// При попадании в обработчик сетки page-node-blog мы хотим использовать в качестве основы (шаблон, настройки, стили)сетку page-node, но при этом добавить некоторые доп. блоки. Эту задачу можнорешить так:

$data['blocks'][] = ['src' => 'widget:block-specific-for-blog','region' => 'sidebar'];
$res = $layoutApi->delegate($data, 'page-node');
if($res) { //делегирование успешно настроено, сетка заменена
//Здесь мы также можем поменять настройки $data['layout_info']
$data['layout_info']['params']['regions']['main_top'] = [
'name'=>'Верх содержимого',
'blocks' => [ ['src'=>'widget:content-header_default','default_block'=>true] ] ,
];
}