О сайтах и не о сайтах

Теперь и в tg!

tg

Переехал с твиттера на t.me/tulvit_blog, если что.

Drupal 8: размещение блоков в произвольных местах

Добавление мест ("регионов") где могут выводиться блоки происходит в файле page.html.twig. Собственно, внутри этого файла мы можем разместить столько регионов, сколько хотим. И там, где хотим.

Единственное ограничение (хотя не совсем ограничение, а больше логичный и предсказуемый функционал) - блоки нельзя размещать внутри блоков. Но ведь иногда хочется.

Наиболее распространенный пример. Вот есть системный предопределенный блок Main page content. Что туда входит для, скажем, страницы блога?

  • Дата публикации, автор материала.
  • Текст статьи.
  • Теги.
  • Комментарии.
  • Форма комментариев.

И все эти вроде как разные сущности - являются содержимым одного блока, Main page content. А что если хочется вставить другой блок между комментариями и формой комментариев? Или после первого абзаца в тексте статьи?

Но нет. Разместить один блок внутри другого блока - нельзя. Впрочем, если очень хочется - то можно.

Вариантов два. Перестройкой DOMa средствами ЯС или программно. Ну т. е. либо на клиенте, либо на сервере. Рассмотрим их оба.

Перестройка DOM-a

Тут все просто. Создаем наш блок, размещаем его где угодно, а потом на стороне клиента средствами JS уже переносим, куда нужно.

Когда имеет смысл использовать этот способ?

Когда не уверены, что и куда хотим поместить, а просто "тестим". Например, рекламные блоки. Создали несколько штук, потом ЯС-ом раскидали по нужным местам, через неделю по новым, через неделю еще куда-то, всякое там сплит-тестирование и прочее. Смысла тут каждый раз "ковырять" бэкэнд никакого, слишком трудозатратно.

Или когда хотим поместить блок уж в слишком специфичном месте. Не между "тегами и комментариями", а "в этом списке после третьего элемента" или "в тексте после второго абзаца". В целом, тут тоже можно в сторону темизации/написания модуля "копать" - но это уж излишнее усложнение, как мне кажется. ЯС-ом проще.

Собственно, создадим наш новый кастомный блок, My brand new custom block, с какой-нибудь цитаткой в качестве содержимого, и разместим его куда-нибудь. Без разницы куда. Пусть будет до контента.

Размещение пользовательского блока

Ну а теперь сделаем так, чтобы этот блок всегда показывался после первого абзаца текста статьи.

В DevTools браузера смотрим, какой айдишник имеет наш кастомный блок. В данном случае - это id="block-mybrandnewcustomblock".

Дальше смотрим, как мы может "достать" первый абзац текста статьи. Например, так:

.block-system-main-block .field--name-body p:first-child

Но так-то набор классов/айдишников может быть на свое усмотрение.

Теперь пишем строчечку на jQuery:

jQuery('#block-mybrandnewcustomblock').insertAfter(jQuery('.block-system-main-block .field--name-body p:first-child'));

Так, а почему "jQuery", а не всем привычный "$"? Ну, потому что $ - это не jQuery только, а еще те же mooTools с Prototype. Вот Друпал и не берется решать, к чему этот знак доллара будет привязан. Поэтому либо используем вызовы через jQuery, либо делаем оберточку:

(function ($) {
  $('#block-mybrandnewcustomblock').insertAfter($('.block-system-main-block .field--name-body p:first-child'));
}(jQuery));

А, еще небольшое отступление. Drupal 8 - это не Drupal 7. И не шестой, и не пятый... Если раньше jQuery и вообще все остальные скрипты грузились по умолчанию на каждой станице, то теперь только по требованию. Т. е., например, на чистом инсталле для главной страницы вообще ничего из скриптов не погружается, ни jQuery, ни вообще ничего.

В нашем случае (а мы размещаем блок на странице со статьей с комментариями) такой проблемы нет, т. к. модуль для комментирования comment тянет за собой модуль filter, который в свою очередь уже тянет jQuery как dependency.

Т. е. прежде чем использовать jQuery где либо, надо быть уверенным, что на конкретных страницах этот самый jQuery вообще используется. Впрочем, можно вообще в теме в MYTHEME.libraries.yml прописать подгрузку jQuery для каждой страницы. Или без оглядки на библиотечки писать все на ванильке. Но это уже все мелочи жизни, к теме поста имеющие не самое прямое отношение. В нашем конкретном случае - на целевых страницах jQuery грузится.

Идем значит проверять нашу написанную строчечку. Проверить хочется быстро, и желательно бесплатно и без СМС. Без проблем!

Переходим в браузере на страницу нашего сайта где блок перемещать собираемся, вызываем DevTools, выбираем вкладку Console, вставляем туда нашу строчку:

Выполнение ЯС кода в консоле ДевТулз

Жмем на Enter. И любуемся результатами:

Блок после первого параграфа.

Блок переместился внутрь текста аккурат после первого параграфа. Как и планировалось.

Ну, потестить потестили, теперь надо куда-то этот код вставить внутрь Друпала. Вообще, можно много куда, прямиком в тему, например. Но почему бы и не внутрь самого блока, впрочем. Так и быстрее, и путаницы меньше (или больше - это с какой стороны смотреть). Ну, в блок так в блок.

Т. е. вставляем наш JS код сразу внутрь текстового содержимого созданного блока. Если выбран текстовый формат FULL HTML, то <script> тэги должны парсером пропускаться. Вставляем, понятное дело, не через WYSIWYG, а тыкнув на вкладку Source Code:

Редактирование исходного кода блока.

Эм, а зачем мы нашу строчку кода "повесили" на событие DOMContentLoaded?

Тут дело в том, что в Drupal 7/6/5 скрипты вставлялись всегда в <head>. А Drupal 8 - он не такой. Он новый, модный, молодежный. Поэтому скрипты вставляются последним ребенком тега <body>.

А наш код в блоке выводится где-то в серединке странички. А все остальные скрипты - в самом конце. А мы используем jQuery. А jQuery еще не "подтянулся", потому что он тоже в самом конце странички, да.

Ну и тут одно из двух. Либо отказаться от использования jQuery и переписать эту строчечку опять же на ванильке. Либо вот ждать события, когда DOM простроится и все скрипты подтянутся, и тогда уже выполнять наш код. Тут уж на любителя, кому как. Сам я раньше на ванильке обычно писал в таких случаях, ну а сейчас вот что-то решил вот так. Ну да ладно.

Вот, собственно, и все. Блок выводится где надо ("технически" он выводится где и выводился, просто мы его "переставляем" - но кого это волнует?).

Темизация

А это для случая, если мы точно знаем, где и как должен выводиться блок, что это будет "на века", не хотим никаких манипуляций с DOM-ом и вообще.

Тут будет быстрее и проще. Потому что backend он вообще быстрее и проще, чем frontend. Внезапно.

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

Идем в файлик MYTHEME.theme (или создаем его, если его нет), добавляем следующую функцию:

function MYTHEME_preprocess_field__comment(&$variables) {
  $block_name = 'mybrandnewcustomblock';
  $my_block = \Drupal\block\Entity\Block::load($block_name);
  if ($my_block) {
    $my_block = \Drupal::entityTypeManager()->getViewBuilder('block')->view($my_block);
    $variables['my_brand_new_custom_block'] = $my_block;
  }
}

Где mybrandnewcustomblock - машинное имя нашего блока. Посмотреть его можно внутри админки в конфигурациях блока:

Конфигурация блока в Друпал 8

Теперь мы можем спокойно вставить наш блок в файл шаблона Twig-а. Осталось только определиться, в какой.

Включаем для Твига development mode, для этого редактируем файлик sites/default/services.yml (если он отсутствует - создаем копию файла default.services.yml и переименовываем ее в просто services.yml) где выставляем параметр debug в значение true:

twig.config:
  # Twig debugging:
  # @default false
  debug: true

А дальше инспектируем страничку нашего сайта с помощью DevTools, смотрим, куда хотим вставить блок, и какой шаблон за это местоположение ответственен (после включения дебага Твига в исходном коде HTML будут выводиться соответствующие комментарии):

Инспектирование исходного кода ДевТулзами

Собственно, нас интересует файлик field--comment.html.twig, лежащий в папке с шаблонами в теме Classy. Если эта тема не наша (а Classy - не наша) или путь идет не до какой-то темы, а до модуля - то просто берем и копируем этот файл в папку с нашей темой. Ну и открываем его для редактирования, куда вставляем наш ранее определенный в MYTHEME.theme файле блок:

{% if my_brand_new_custom_block %}
{{ my_brand_new_custom_block }}
{% endif %}

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

Отключенный блок в Друпале

Только в этом случае способ через темизацию будет работать.

Ну, вроде все сделали правильно. Обновляем кэш и идем проверять - наш кастомный блок должен отображаться после контента и до комментариев.

Блок после контента но до комментариев.

Работает!

Хотя это, конечно, немного изобретение колеса. Ибо для вставки блоков напрямую в шаблоны Твига есть кастомный модуль, Twig tweak. Но изобретать колеса иногда полезно. А иногда - просто весело. Хотя тут явно не тот случай. Но пусть будет.

Программный вывод

Ну и раз пошла такая пьянка - то и модуль напишем. С минимальным функционалом. Пусть вставляет определенный блок в тело статьи, после даты, но до основного текста.

Создаем в папочке /modules/custom папочку для нашего модуля, my_custom_block. А внутри папочки - два файлика, my_custom_block.info.yml и my_custom_block.module.

Содержание my_custom_block.info.yml проще некуда:

name: Placing Custom Module
description: Placing custom module inside of node's content.
package: Custom

type: module
core: 8.x

Теперь открываем файлик my_custom_block.module, где и будем кодить код. А точнее -препроцессить вывод нод. Поможет нам в этом следующий хук: hook_ENTITY_TYPE_view_alter.

Итого, код в my_custom_block.module выходит таким:

<?php

function my_custom_block_node_view_alter(array &$build, Drupal\Core\Entity\EntityInterface $entity, \Drupal\Core\Entity\Display\EntityViewDisplayInterface $display) {
  if ($build['#view_mode'] == 'full') {
   // Getting block's content.
    $block_name = 'mybrandnewcustomblock';
    $block = \Drupal\block\Entity\Block::load($block_name);
    $block_content = \Drupal::entityTypeManager()->getViewBuilder('block')->view($block);

   // Adding additional field to node.
    $build['custom_field'] = array(
      '#weight' => -999,
      '#markup' => drupal_render($block_content),
    );
  }
}

По машинному имени блока получаем его содержимое. Потом добавляем к ноде дополнительное поле с минимальным весом (-999, чтобы в самом вверху выводился) и контентом нашего блока.

Теперь надо не забыть проверить, что наш блок как и в случае с темизацией был назначен в админке любому региону и отключен (disabled). Без этого - не заработает, будем лишь любоваться WSOD-ом с ошибкой "The website encountered an unexpected error. Please try again later." Ну, проверили - теперь самое время включить наш кастомный модуль.

Включение кастомного блока

Чистим кэш - и смотрим, работает ли.

Программный вывод блока.

Работает, как и хотели. Аккурат в начале содержимого ноды.

За сим, стало быть, все. Разве что еще можно упомянуть про контрибный модуль Insert Block, судя по описанию - позволяет вставлять блоки внутрь контента через токены [block:name], тоже может пригодиться в некоторых случаях. Но сам я этот модуль не тестировал (он находится еще в разработке, хотя статистика "9,536 sites report using this module" вполне себе приятная, значит решение более чем рабочее).

Комментарии

Insert Block - та еще лажа, пол дня с ним бьюсь, не пашет. Теоретически в форматах ввода текста должна появится галочка обработки данным модулем текста и поиска там [block:2], [block:osnovnayanavigatsiya]  и тд, но у меня галочка не появляется. Уже и пропатчил модуль 46 патчем, но нифига не пашет, взял в разделе issues на друпал.орг. Люди пишут, что патч помог, но у меня нихрена, друпал последний 8-й.

Insert Block - та еще лажа

Охотно верю. Но оно и не удивительно, судя по тому, что его забросили уже почти пять лет как (Development version: 8.x-1.x-dev updated 6 May 2014).

Написано отлично, не часто такое встретишь. А у меня такой случай, не могу сообразить. Модуль PrintFriendly & PDF размещает не блок, а одну только ссылку на свой сервис. Ставит ее в самое начало контента. Как мне по уму переместить эту ссылку в конец статьи?

Спасибо!

Ну тут только поковырять исходники модуля, где что препроцессится, и от этого уже "плясать". Либо "хакнуть" модуль, держа в голове, что это все слетит со следующим апдейтом. Либо установить модуль не как контрибный, а как кастомный, и уже переписать его под себя (но тут уже никаких обновлений не будет). Либо "в лоб" просто добавить строчку на JS для перемещения этого блока/ссылки уже на стороне клиента за счет манипуляции с DOM.

Все три варианта - костыли. Поэтому, собственно, от Друпала я уже отошел в самопис и низкоуровневые фреймворки по типу Ларавел. Ну и от ВП никуда не деться, он везде.

А вообще, Друпал очень сдал за последнее время, вообще не востребованным стал, работы по нему фактически не найти, сейчас глянул, на АпВорке всего с сотню тематичных тасок, что считай ноль. Последний клиент на Друпал у меня был аж два года назад, например. Хотя таски мониторю.