Drupal 8: Modal API или как работать с модальными/диалоговыми окнами

В Drupal 7 для модальных окон я использовал Ctools Modal, в 8-ке подобный функционал появился в ядре, и он достаточно сильно отличается от того как это сделано в 7-ки и ctools, возможно ctools потом завезет свой Modal API.

Modal API теперь в ядре, он обзавелся новыми возможностями, стал проще к пониманию и использованию. Модальные окна в ядре основаны на Jquery UI Dialog. Лично для меня, это как плюс, так и минус. Плюс в том что это уже известный элемент, отточен, без багов, куча опций из коробки. Ctools Modal очень хорошенько в этом плане уступает. Он даже нормально позиционировать окно без фикса не может. Минус который я заметил, это то, что не получится переопределить форму под свои задачи как это можно было сделать в Ctools. С другой стороны, если честно, у меня все формы как dialog из jquery ui, и в 99.999% стандартного форматирования мне хватит просто с головой. Так что это лишь мои придирки.

Для того чтобы это всё завелось, нам необходимо подключать библиотеку core/drupal.dialog.ajax, не зависимо от того способа который будет использоваться. Подключив эту библиотеку, у вас автоматически также подключатся: core/jquery, core/drupal, core/drupalSettings, core/drupal.ajax, core/drupal.dialog.

Вызывать модальные окна можно откуда угодно, и я опишу все варианты которые я смог найти в ядре.

Для открытия модального окна достаточно указать HTML ссылку с путем до страницы, содержимое которой будет загружено в модальное окно, классом use-ajax, парочкой data атрибутов и подключенной библиотекой на странице. Звучит сложно? На самом деле нет!

Итак ссылку вы можете создать как угодно, руками, render array, не важно, чтобы она работала как вызов модального окна мы обязательно должны указать:

  • href ссылка до страницы, содержимое которой будет отображено;
  • class как минимум должен содержать use-ajax, далее на ваше усмотрение;
  • data-dialog-type должен принимать modal.

Всё, это набор минимум. Вы также можете передавать параметры для модального окна при помощи атрибута data-dialog-options, который принимает настройки jQuery Dialog API в формате encoded json. Например: data-dialog-options="{"width": 700}".

Ну и не забыть подключить библиотеку! В связи с тем, что в Drupal 8 библиотеки грузятся только на тех страницах, где они используются, то цеплять библиотеку на все страницы сайта, если в этом нет необходимости - моветон. Поэтому изучите как подключаются библиотеки в Drupal 8 и выберите подходящий способ чтобы не грузить core/drupal.dialog.ajax и все его зависимости в холостую.

Пример №1 - просто ссылка в блоке

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

<a href="/admin/help" class="use-ajax" data-dialog-type="modal">Помощь в
    модальном окне</a>

При попытке нажать на ссылку, вы просто перейдете по ней, это означает что мы не подключили библиотеку. Разумеется, мы будем следовать best practice и не будем подключать библиотеку глобально на всем сайте, а подключим её только к этому блоку. Это означает что core/drupal.dialog.ajax со всеми зависимостями будет грузиться только на тех страницах, где присутствует данный блок, и если блок отсутствует на странице и больше ни один модуль не вызывает данную библиотеку, то все эти JS файлы просто на просто не будут даже отдаваться пользователю. Самый очевидный вариант решения - использовать hook_block_view_alter() для нашего блока и подключать библиотеку в его render array. Это мы и сделаем.

Далее по тексту, подразумевается что весь код пишется в модуле с названием dummy

hook_block_view_alter() в dummy.module
/**
 * Implements hook_block_view_alter().
 */
function dummy_block_view_alter(array &$build, \Drupal\Core\Block\BlockPluginInterface $block) {
  switch ($build['#id']) {
    # Машинное имя блока.
    case 'linktohelp':
      $build['#attached']['library'][] = 'core/drupal.dialog.ajax';
  }
}
При нажатии на ссылку должно открыться модальное окно.

Пример №2 - вывод ссылки при помощи render array

Всё достаточно просто, не так ли? Давайте немного усложним задачу кодом и сделаем вызов модального окна немного "динамическим". Для этого мы опишем свой блок, который будет при помощи render array генерировать ссылку на модальное окно, а также добавим простенькую настройку.

Листинг /src/Plugin/Block/BlockWithModalHtmlLink.php
<?php

/**
 * @file
 * Contains \Drupal\dummy\Plugin\Block\BlockWithModalHtmlLink.
 */

namespace Drupal\dummy\Plugin\Block;

use Drupal\Component\Render\FormattableMarkup;
use Drupal\Component\Serialization\Json;
use Drupal\Core\Block\BlockBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;

/**
 * @Block(
 *   id = "block_with_modal_html_link",
 *   admin_label = @Translation("Modal API example: HTML link"),
 * )
 */
class BlockWithModalHtmlLink extends BlockBase {

  public function defaultConfiguration() {
    return [
      'nid' => '1',
    ];
  }

  /**
   * {@inheritdoc}
   */
  public function blockForm($form, FormStateInterface $form_state) {
    $form = parent::blockForm($form, $form_state);
    $config = $this->getConfiguration();

    $form['nid'] = [
      '#type' => 'textfield',
      '#title' => 'NID to display in modal',
      '#default_value' => $config['nid'],
    ];

    return $form;
  }

  /**
   * {@inheritdoc}
   */
  public function blockSubmit($form, FormStateInterface $form_state) {
    $this->configuration['nid'] = $form_state->getValue('nid');
  }

  /**
   * {@inheritdoc}
   */
  public function build() {
    $config = $this->getConfiguration();
    return [
      '#type' => 'link',
      '#title' => new FormattableMarkup('Open node @nid in modal!', ['@nid' => $config['nid']]),
      '#url' => Url::fromRoute('entity.node.canonical', ['node' => $config['nid']]),
      '#options' => [
        'attributes' => [
          'class' => ['use-ajax'],
          'data-dialog-type' => 'modal',
          'data-dialog-options' => Json::encode([
            'width' => 700,
          ]),
        ]
      ],
      '#attached' => ['library' => ['core/drupal.dialog.ajax']],
    ];
  }

}
Блок с ссылкой на модальное окно. Модальное окно с нашим содержимым.

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

Обратите внимание, подгружается именно содержимое, а не вся страница. Т.е. Drupal ищет route по данному пути, и вызывает его контроллер, который отдает содержимое без рендера всей страницы целиком.

Пример №3 - ещё раз render array

Давайте ещё раз проделаем то же самое. В этот раз в качестве содержимого мы загрузим контактную форму модуля contact. Для этого нам потребуется объявить ещё раз наш блок. Можете даже устроить себе challange и попробовать самостоятельно написать такой блок, а затем сравнить с тем что написал я, он не очень сильно отличается от того как грузится нода, зато покопаться все же придется поверхностно.

Листинг /src/Plugin/Block/BlockWithModalContactForm.php
<?php

/**
 * @file
 * Contains \Drupal\dummy\Plugin\Block\BlockWithModalContactForm.
 */

namespace Drupal\dummy\Plugin\Block;

use Drupal\Component\Serialization\Json;
use Drupal\Core\Block\BlockBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;

/**
 * @Block(
 *   id = "block_with_modal_contact_form",
 *   admin_label = @Translation("Modal API example: Contact form"),
 * )
 */
class BlockWithModalContactForm extends BlockBase {

  /**
   * {@inheritdoc}
   */
  public function getContactForms() {
    $bundle_info = \Drupal::service('entity_type.bundle.info')
      ->getBundleInfo('contact_message');
    $forms = [];
    foreach ($bundle_info as $k => $v) {
      $forms[$k] = $v['label'];
    }
    unset($forms['personal']);
    return $forms;
  }

  /**
   * {@inheritdoc}
   */
  public function defaultConfiguration() {
    return [
      'form' => NULL,
    ];
  }

  /**
   * {@inheritdoc}
   */
  public function blockForm($form, FormStateInterface $form_state) {
    $form = parent::blockForm($form, $form_state);
    $config = $this->getConfiguration();

    $form['form'] = [
      '#type' => 'select',
      '#title' => 'Select form to open in modal',
      '#options' => $this->getContactForms(),
      '#empty_option' => '- Select -',
      '#default_value' => $config['form'] ? $config['form'] : FALSE,
      '#required' => TRUE,
    ];

    return $form;
  }

  /**
   * {@inheritdoc}
   */
  public function blockSubmit($form, FormStateInterface $form_state) {
    $this->configuration['form'] = $form_state->getValue('form');
  }

  /**
   * {@inheritdoc}
   */
  public function build() {
    $config = $this->getConfiguration();
    return [
      '#type' => 'link',
      '#title' => 'Contact with me!',
      '#url' => Url::fromRoute('entity.contact_form.canonical', ['contact_form' => $config['form']]),
      '#options' => [
        'attributes' => [
          'class' => ['use-ajax', 'button', 'button--small'],
          'data-dialog-type' => 'modal',
          'data-dialog-options' => Json::encode([
            'width' => 700,
          ]),
        ]
      ],
      '#attached' => ['library' => ['core/drupal.dialog.ajax']],
    ];
  }

}

Как я и говорил, блок по сути идентичен тому что был ранее с нодой. Я добавил лишь один метод, для получения списка всех доступных контактных форм на сайте за исключением персональной контактной офрмы и вывел их в качестве настройки блока, чтобы можно было выбирать какую форму подгружать, заменил роут на соответствующий и всё! Ах да, я ещё добавил пару классов, чтобы ссылка смотрелась как кнопка ;)

Настройки блока с выбором нужной формы. А это блок с ссылкой! Форма в модальном окне.

Заметили как кнопки формы автоматически добавились в нижний враппер для красоты и удобства? И всё это автоматически. Единственный мелкий недостаток - форма субитится без аякса. С другой стороны, зачем? Все обязательные поля проходят проверку на стороне клиента. Но все же, аякс формы несколько круче и гибче, и при кастомной валидации не будет редиректа, и вообще редиректа после субмита. О том как делать формы аяксовыми я расскажу в следующем материале.

Пример № 4 - как ajax callback

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

Листинг /src/Form/FormWithModalButton.php
<?php
/**
 * @file
 * Contains \Drupal\dummy\Form\FormWithModalButton.
 */

namespace Drupal\dummy\Form;

use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\OpenModalDialogCommand;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;

/**
 * Form with modal window.
 */
class FormWithModalButton extends FormBase {

  /**
   * {@inheritdoc}.
   */
  public function getFormId() {
    return 'form_with_modal_button';
  }

  /**
   * {@inheritdoc}.
   */
  public function buildForm(array $form, FormStateInterface $form_state) {
    $form['text'] = [
      '#type' => 'textarea',
      '#title' => 'Text to show in modal',
      '#default_value' => 'Lorem ipsum',
    ];

    $form['show_im_modal'] = [
      '#type' => 'button',
      '#name' => 'show_im_modal',
      '#value' => 'Show in modal',
      '#ajax' => [
        # Вы также можете указать просто callback функцию а не метод.
        # Если собираетесь использовать метод другого класса, то нужно
        # указывать метод включая пространство имен, Drupal\module\Class::method
        'callback' => '::ajaxModal',
      ],
    ];

    return $form;
  }

  /**
   * {@inheritdoc}
   */
  public function submitForm(array &$form, FormStateInterface $form_state) {
    drupal_set_message('Form submitted.');
  }

  /**
   * {@inheritdoc}
   */
  public function ajaxModal(array &$form, FormStateInterface $form_state) {
    $content['#markup'] = $form_state->getValue('text');
    $content['#attached']['library'][] = 'core/drupal.dialog.ajax';
    $title = 'Here is your content in modal';
    $response = new AjaxResponse();
    $response->addCommand(new OpenModalDialogCommand($title, $content, ['width' => '400', 'height' => '400']));
    return $response;
  }

}

Как можно увидеть из кода, модальное окно вызвать при аякс ответе не так то и сложно. Подключаем библиотеку (это достаточно сделать при самом аяксе), и вернуть команду на открытие модального окна OpenModalDialogCommand с необходимыми параметрами и содержимым. В нашем случае содержимое бы берем из текущего состояния формы $form_state.

Форма на странице. Модальное окно при нажатии на кнопку с нашим содержимым.

Пример №5 - вызов модального окна из JS

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

Листинг dummy.libraries.yml - объявляем наш JS
modal.from.js:
  version: 1.x
  js:
    js/modal_form.js: { }
  dependencies:
    - core/drupal.dialog.ajax
Листинг dummy.module
/**
 * Implements hook_preprocess_HOOK().
 */
function dummy_preprocess_page(&$variables) {
  # Подключаем только на главной странице.
  if (\Drupal::service('path.matcher')->isFrontPage()) {
    $variables['#attached']['library'][] = 'dummy/modal.from.js';
  }
}
Листинг js/modal_form.js
(function ($, Drupal, drupalSettings) {

  'use strict';

  Drupal.behaviors.dummy_modal_form_js = {
    attach: function (context, settings) {
      var frontpageModal = Drupal.dialog('<div>Modal content</div>', {
        title: 'Modal on frontpage',
        dialogClass: 'front-modal',
        width: 400,
        height: 400,
        autoResize: true,
        close: function (event) {
          // Удаляем элемент который использовался для содержимого.
          $(event.target).remove();
        }
      });
      // Отображает модальное окно с overlay.
      frontpageModal.showModal();
      // Вы также можете использовать
      // frontpageModal.show();
      // чтобы отобразить модальное окно без overlay, и все элементы за
      // модальным окном останутся активными.
    }
  }

}(jQuery, Drupal, drupalSettings));
В результате, при заходе на главную страницу вы должны увидеть модальное окно.

В JS вызове также можно добавлять различные настройки в соответствии с jQuery Dialog UI. Например мы можем добавлять кнопки со своим поведением.

JS с добавленными кнопками
(function ($, Drupal, drupalSettings) {

  'use strict';

  Drupal.behaviors.dummy_modal_form_js = {
    attach: function (context, settings) {
      var frontpageModal = Drupal.dialog('<div>Modal content</div>', {
        title: 'Modal on frontpage',
        dialogClass: 'front-modal',
        width: 500,
        height: 400,
        autoResize: true,
        close: function (event) {
          $(event.target).remove();
        },
        buttons: [
          {
            text: 'Make some Love',
            class: 'love-class',
            icons: {
              primary: 'ui-icon-heart'
            },
            click: function () {
              $(this).html('From Russia with <3');
            }
          },
          {
            text: 'Close the window',
            icons: {
              primary: 'ui-icon-close'
            },
            click: function () {
              $(this).dialog('close');
            }
          }
        ]
      });
      frontpageModal.showModal();
    }
  }

}(jQuery, Drupal, drupalSettings));
Теперь у нас есть две кнопки со своим поведением.

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

Ссылки

Комментарии

pr0g
пн, 03/10/2016 - 20:31

Привет! На втором примере столкнулся с проблемой вывод заголовка модального окна. Вместо заголовка ноды выводится: <span data-quickedit-field-id="node/5/title/ru/full" class="field field--name-title field--ty

Нашел в инете что это из-за RDF модуля, отключения модуля ситуацию не исправило. После отключения кеш очищал.

Скриншот http://www.awesomescreenshot.com/image/1664745/fef1b8ded6bb564844821caf3a1170b3

pr0g
пн, 03/10/2016 - 22:31

В пятом примере модальное окно открывается почему-то 3 раза. Найти причину почему так происходит не нашел. Можете подсказать в чем может быть проблема?

P.S. Заметка по комментариям на сайте. Не видна сразу капча "Я не робот". Появляется только если отправить форму и только после этого можно оставить комментарий. Так и было задумано?

Niklan
вт, 04/10/2016 - 12:51

Попробуйте

$('body').once(function () {
  frontpageModal.showModal();
});

Капча недавно добавлена была, возможно страница была закеширована ещё ранее и поэтому не было видно, а так да, поведение кривое.

pr0g
вт, 04/10/2016 - 21:41

С данной логикой модальное окно вообще не выводится.

Три раза окно выводится, только если авторизован. Видимо штатное административное меню так влияет, так как под анонимом все работает нормально, т.е. модальное окно выводится один раз.

Niklan
ср, 05/10/2016 - 11:02

Да это 99% аякс бехейворы реаттачит а он вызывает трижды. Типичная проблема в Drupal JS, надо это учитывать. Будет под рукой 8-ка и возможность там проверить, я проверю код и перепишу. На днях постараюсь посмотреть.

Дмитрий
Домашняя страница пн, 17/04/2017 - 10:46

Возникла проблема с маской телефона - http://joxi.ru/12Mp8eDSMG0Llm Как я понял, скрипты грузятся раньше модального окна и не не находят нужный селектор.

Как можно сделать загрузку окна в коде раньше системных скриптов? Или как лучше?

YARIK
ср, 12/07/2017 - 14:56

Спасибо за труд. Вопрос. Допустим я передаю в модальный диалог заголовок: data-dialog-options='{"title":"Коментарi"}' Как мне сделать мультиязычность? То есть чтобы когда пользователь выбирает русский язык - вместо "Коментарi" было "Комментарии"?

Niklan
ср, 12/07/2017 - 17:05

Смотря откуда всё вызывается. Если этот аттрибут устанавливается программно, то: t('Comments'), либо $this->t('Comments'), смотря откуда вызывается. Только исходный текст должен быть обязательно на английском.

В JS можно через Drupal.t(), но их нужно добавить для начала туда.

YARIK
ср, 12/07/2017 - 21:26

Спасибо. Я вызываю из вьюхи. Вызываю ссылкой <a href="/node/{{ nid }}" class="use-ajax" data-dialog-type="modal" data-dialog-options='{"title":"Коментарi"}' >Комментарии</a>

Niklan
чт, 13/07/2017 - 06:15

Во вьюсах не знаю. Если там поля можно переводить, то тогда так. Просто так строкой не перевести.

Олег
ср, 28/02/2018 - 01:51

Большая благодарность за статью. Использовал первый вариант - работает. Однако вопрос: как сделать чтобы окно закрывалось не только по нажатию на крестик, но и если клацнуть мышкой рядом с модальным окном?

Savage
вс, 05/04/2020 - 13:25

Я делаю через небольшой скрипт: (function($) { $('.commerce-product--catalog__hover').click(function(event) { $('.ui-dialog.ui-corner-all.ui-front').toggle(); }); $(document).click(function (event) { if ($(event.target).closest('.ui-dialog.ui-corner-all.ui-front').length == 0 && $(event.target).attr('id') != 'toggle-link') { setTimeout(function(){ $('.ui-dialog-titlebar-close').click(); }, 150); } }); })(jQuery);

Андрей
чт, 19/04/2018 - 12:50

В Примере №2 в методе "public function build()" из-за возвращения рендера ссылки "return [ '#type' => 'link' ..." тайтл блока перезаписывается на "Open node/@nid in modal!", а не отображается его указанный заголовок "Modal API example: HTML link". И он не поддается изменению через настройки блока.

Метод переписал и работает смена заголовка (так же вынес подключение либы с линка в return):

public function build() { $config = $this->getConfiguration(); $items[] = [ '#type' => 'link', ... (начинка та же самая) ];

return [
  '#theme' => 'item_list',
  '#items' => $items,
  '#attached' => ['library' => ['core/drupal.dialog.ajax']],
];

}

Для Примера №3 должно быть аналогично.

Niklan
пн, 29/10/2018 - 08:18

Судя по API jQuery Dialog - никак. Проще добавить dialogClass определенный и для него заголовку делать display: none. У jquery ui кастомизация очень скудная. Если есть возможность и желание, лучше вообще не бодаться с ним и взять что-то другое и не париться вообще, если решение с css не подойдет.

Роман Якимкин
ср, 14/08/2019 - 10:04

Добрый день, Никита. Скажите пожалуйста, а каким образом можно с помощью Modal API обработать самбит вызываемом диалога на вызывающей странице? Например, у меня на странице список заголовков нод. Также на странице есть ссылка "Добавить ноду", по которой таким вот образом вызывается стандартная форма добавления ноды опрееленного типа в виде диалога. И при самбите этой формы и закрытия диалога нужно чтобы заголовок этой ноды также отобразился на странице, причем всё должно работать через AJAX. В drupal 7 мне этот функционал удалось решить с помощью модулей asaf и autodialog путем использовния hook_asaf_form_ajax_commands_alter из модуля asaf, в котором в список команд добавлял команду со своим функционалом, которую описывал в js-файле через Drupal.ajax.prototype.commands.myCommand. Таким образом можно взять данные из $form_state формы добавления ноды и уже передать их в свою js-функцию, которая будет вызываться сабмите формы добавления ноды.

Niklan
чт, 15/08/2019 - 11:19

Это нужно обрабатывать средствами Form API, а не Modal API. Добавить свой субмит, в зависимости от реализации передавать заголовок и заменять. А вообще странно, в 8-ке для этого есть quickedit.

Happy pottery
ср, 11/09/2019 - 22:28

Как в модальном окне вывести все блоки страницы, к которой эти блоки прикреплены?

_Skerth
Домашняя страница ср, 25/09/2019 - 13:15

Если нужно применить разное оформление к WEBFORM'ам:

THEME_theme_suggestions_alter(array &$suggestions, array $vars, $hook) { if (in_array($hook, array('webform'))) {

$wrapper_format = \Drupal::request()->query->get('_wrapper_format');

if ($wrapper_format == 'drupal_modal') {
  $suggestions[] = $hook . '' . 'modal';

  return;
}

$params = \Drupal::routeMatch()->getParameters();

if ($params->get('webform')) {
  $suggestions[] = $hook . '' . 'full';
}

} }

Для модалок будет webform—modal.html.twig Для страниц просмотра формы webform—full.html.twig Для всех остальных (блоки и тд) webform.html.twig

Виталий
вс, 24/11/2019 - 05:58

пример 1 - небольшое дополнение: размер настроился только при таком написании: data-dialog-options="{"width":1200,"height":600}"

Виталий
сб, 10/10/2020 - 12:50

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

Savage
пн, 18/10/2021 - 10:22

День добрый, Спасибо за гайд. Никто не знает, как можно в примере 3 передать в модальное окно кастомный aria-describedby? По умолчанию выводится aria-describedby ="drupal-modal", что не совсем то, что надо. В крайнем случаем дайте направление, куда "рыть"...

Niklan
пн, 18/10/2021 - 14:34

Разве так не работает?:

    return [
      '#type' => 'link',
      '#title' => 'Contact with me!',
      '#url' => Url::fromRoute('entity.contact_form.canonical', ['contact_form' => $config['form']]),
      '#options' => [
        'attributes' => [
          'class' => ['use-ajax', 'button', 'button--small'],
          'data-dialog-type' => 'modal',
          'data-dialog-options' => Json::encode([
            'width' => 700,
          ]),
         'aria-describedby' => 'foo-bar',
        ]
      ],
      '#attached' => ['library' => ['core/drupal.dialog.ajax']],
    ];
dimitriy
ср, 01/06/2022 - 14:18

спасибо за инфу, только я бы добавил уточнение, что для роута на который идёт ссылка как на окно, следует добавить в файл маршрутов разрешение на метод POST, я над этим малость потупил

cosmos@inbox.ru
вт, 26/12/2023 - 08:32

Хотел картинку в попапе открыть подставляю абсол урл картинки
но нее открывается, неужели картинку не открыть?

Добавить комментарий

Поддерживается Markdown
Поделиться