В этом материале я расскажу вам, как с помощью встроенного в Drupal 8 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
/**
* 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 генерировать ссылку на модальное окно, а также добавим простенькую настройку.
<?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 и попробовать самостоятельно написать такой блок, а затем сравнить с тем что написал я, он не очень сильно отличается от того как грузится нода, зато покопаться все же придется поверхностно.
<?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 колбэком и текстовым полем, по нажатию на кнопку будем показывать содержимое текстового поля в модальном окне.
<?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 мы будем только на главной. Дальнейший код. я думаю, в комментариях не нуждается.
modal.from.js:
version: 1.x
js:
js/modal_form.js: { }
dependencies:
- core/drupal.dialog.ajax
/**
* Implements hook_preprocess_HOOK().
*/
function dummy_preprocess_page(&$variables) {
# Подключаем только на главной странице.
if (\Drupal::service('path.matcher')->isFrontPage()) {
$variables['#attached']['library'][] = 'dummy/modal.from.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. Например мы можем добавлять кнопки со своим поведением.
(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));
На этом всё, спасибо за прочтение. Модуль с кодом прилагается.
Ссылки
Комментарии
В пятом примере модальное окно открывается почему-то 3 раза. Найти причину почему так происходит не нашел. Можете подсказать в чем может быть проблема?
P.S. Заметка по комментариям на сайте. Не видна сразу капча "Я не робот". Появляется только если отправить форму и только после этого можно оставить комментарий. Так и было задумано?
Попробуйте
$('body').once(function () {
frontpageModal.showModal();
});
Капча недавно добавлена была, возможно страница была закеширована ещё ранее и поэтому не было видно, а так да, поведение кривое.
С данной логикой модальное окно вообще не выводится.
Три раза окно выводится, только если авторизован. Видимо штатное административное меню так влияет, так как под анонимом все работает нормально, т.е. модальное окно выводится один раз.
Да это 99% аякс бехейворы реаттачит а он вызывает трижды. Типичная проблема в Drupal JS, надо это учитывать. Будет под рукой 8-ка и возможность там проверить, я проверю код и перепишу. На днях постараюсь посмотреть.
Странно, но модал не работает для анонимов! ((
Если явно подключить 'core/drupal.dialog.ajax', то у меня рабоатет.
Возникла проблема с маской телефона - http://joxi.ru/12Mp8eDSMG0Llm Как я понял, скрипты грузятся раньше модального окна и не не находят нужный селектор.
Как можно сделать загрузку окна в коде раньше системных скриптов? Или как лучше?
Добавить подключение скрипта к форме через libraries API.
Спасибо за труд. Вопрос. Допустим я передаю в модальный диалог заголовок: data-dialog-options='{"title":"Коментарi"}' Как мне сделать мультиязычность? То есть чтобы когда пользователь выбирает русский язык - вместо "Коментарi" было "Комментарии"?
Смотря откуда всё вызывается. Если этот аттрибут устанавливается программно, то: t('Comments')
, либо $this->t('Comments')
, смотря откуда вызывается. Только исходный текст должен быть обязательно на английском.
В JS можно через Drupal.t()
, но их нужно добавить для начала туда.
Спасибо. Я вызываю из вьюхи. Вызываю ссылкой <a href="/node/{{ nid }}" class="use-ajax" data-dialog-type="modal" data-dialog-options='{"title":"Коментарi"}' >Комментарии</a>
ссылкой с атрибутом data-dialog-type="modal" data-dialog-options='{"title":"Коментарi"}'
Во вьюсах не знаю. Если там поля можно переводить, то тогда так. Просто так строкой не перевести.
Большая благодарность за статью. Использовал первый вариант - работает. Однако вопрос: как сделать чтобы окно закрывалось не только по нажатию на крестик, но и если клацнуть мышкой рядом с модальным окном?
Я делаю через небольшой скрипт: (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);
Для ядра 8.5 пора дописывать про setting tray https://dri.es/how-to-use-drupal-8-off-canvas-dialog-in-your-modules
В Примере №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 должно быть аналогично.
Drupal 8. Имеем контакт форму на аяксе. Как вывести в попапе confirmation_message?
А как создать диалоговое окно без заголовка и места для заголовка?
Я про последний 5 пример, какое свойство нужно указать у showDialog?
Судя по API jQuery Dialog - никак. Проще добавить dialogClass определенный и для него заголовку делать display: none. У jquery ui кастомизация очень скудная. Если есть возможность и желание, лучше вообще не бодаться с ним и взять что-то другое и не париться вообще, если решение с css не подойдет.
Да, так и пришлось, заголовок прятать через стили
Добрый день, Никита. Скажите пожалуйста, а каким образом можно с помощью Modal API обработать самбит вызываемом диалога на вызывающей странице? Например, у меня на странице список заголовков нод. Также на странице есть ссылка "Добавить ноду", по которой таким вот образом вызывается стандартная форма добавления ноды опрееленного типа в виде диалога. И при самбите этой формы и закрытия диалога нужно чтобы заголовок этой ноды также отобразился на странице, причем всё должно работать через AJAX. В drupal 7 мне этот функционал удалось решить с помощью модулей asaf и autodialog путем использовния hook_asaf_form_ajax_commands_alter из модуля asaf, в котором в список команд добавлял команду со своим функционалом, которую описывал в js-файле через Drupal.ajax.prototype.commands.myCommand. Таким образом можно взять данные из $form_state формы добавления ноды и уже передать их в свою js-функцию, которая будет вызываться сабмите формы добавления ноды.
Это нужно обрабатывать средствами Form API, а не Modal API. Добавить свой субмит, в зависимости от реализации передавать заголовок и заменять. А вообще странно, в 8-ке для этого есть quickedit.
Как в модальном окне вывести все блоки страницы, к которой эти блоки прикреплены?
Если нужно применить разное оформление к 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
пример 1 - небольшое дополнение: размер настроился только при таком написании: data-dialog-options="{"width":1200,"height":600}"
Вопрос по выводу страницы по аджак-ссылке. Все открывается нормально, но все системные сообщения по отработке функционала на странице (в модальном окне), выводятся за окном, на базовой странице. Не подскажете как сделать так, чтобы они тоже выводились в модальном окне.
День добрый, Спасибо за гайд. Никто не знает, как можно в примере 3 передать в модальное окно кастомный aria-describedby? По умолчанию выводится aria-describedby ="drupal-modal", что не совсем то, что надо. В крайнем случаем дайте направление, куда "рыть"...
Разве так не работает?:
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']],
];
спасибо за инфу, только я бы добавил уточнение, что для роута на который идёт ссылка как на окно, следует добавить в файл маршрутов разрешение на метод POST, я над этим малость потупил
Хотел картинку в попапе открыть подставляю абсол урл картинки
но нее открывается, неужели картинку не открыть?
Привет! На втором примере столкнулся с проблемой вывод заголовка модального окна. Вместо заголовка ноды выводится: <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