В панели администратора Joomla встречаются поля, которые нужно заполнить данными из других компонентов: указать материал, пункт меню, контакт, товар и т.д. Обычно такие поля оформляются в виде выпадающего списка select option
, могут оформляться в виде input type="text"
с datalist
, но есть и удобные поля, показывающие список искомых сущностей, с фильтрацией, поиском, пагинацией и т.д.
Все мы видели эти поля в действии при выборе материала в пункте меню типа "Материалы - Материал", "Контакты - Контакт", или при создании алиаса (псевдонима) пункта меню - "Системные - Псевдоним". Как создать точно такое же, но своё (например, для выбора товаров) расскажет эта статья.
Эта же статья на английском языке, опубликованная в официальном журнале международного Joomla-сообщества Joomla Community Magazine - Creating a custom Form field type in Joomla 5 using the Modal Select example.
Upd. 30.08.2024: ссылка на официальную документацию Joomla по этому типу поля ModalSelect - Joomla Dialog (popup) script.
Напомним себе как они выглядят. Модальное окно выбора материала:
На видео - утро, компьютер ещё не проснулся и локальный сервер чуть тупит ))
А это модальное окно выбора контакта.
Возможности поля выбора в модальном окне в Joomla
Давайте пристально посмотрим на эти поля - что именно они позволяют сделать. Мы где-то глубоко внутри понимаем, что основная работа этого поля - получить id выбираемой сущности и поместить это id в текстовое поле. Но на экране мы видим нечто другое - вместо цифры мы видим название материала или контакта. Это приятно и удобно. Не нужно вспоминать название материала с id 1452704. Также, на видео отчётливо видно, что если в поле уже есть значение - появляется кнопка "очистить". Она сбрасывает значение поля и позволяет снова нажать на кнопку "выбрать".
В некоторых случаях у нас есть возможность создать выбранный тип сущности - материал, контакт и т.д. прямо в процессе создания пункта меню. Эта кнопка работает с учётом ACL - разделения прав доступа в Joomla.
Представьте, Вы собираете сайт и создаёте страницу "Контакты". Если у Вас не куча филиалов с разветвлённой структурой, то это как правило обычный материал Joomla в категории "Без категории". И в нём уже все контакты. В бородатые времена нужно было сначала создать материал, а потом уже идти в пункты меню и делать на него ссылку. Сейчас так можно не делать.
И если в поле уже есть значение, то в некоторых же случаях есть возможность редактировать выбранную сущность (материал, пункт меню и т.д.) прямо в процессе создания пункта меню.
Итак, с помощью поля выбора в модальном окне мы можем:
-
выбрать
-
создать
-
редактировать
-
очистить
Это то, что перед глазами. Но в недрах Joomla есть ещё любопытный параметр urlCheckin
, который позволяет отправлять выбранное значение на указанный в поле url. Стоит заметить, что этот функционал в Joomla постепенно развивался с довольно давних пор. Однако, отдельный универсальный тип поля, который можно использовать для своих нужд появился только в Joomla 5. Его нет даже в Joomla 4.
Как устроены поля конструктора Form интерфейса панели администратора Joomla?
Раньше этот конструктор назывался JForm
. Я буду предполагать, что не все мои читатели имеют в руках такой инструмент разработки как IDE - среду разработки - а-ля PHP Storm или VS Code, поэтому постараюсь давать дополнительные ориентиры для навигации по кодовой базе.
В Joomla логика разделена от представления (собственно вывода HTML), поэтому исследовать мы будем одновременно в нескольких местах.
Логика - класс Form
Логика - это класс Form. В Joomla 5 файлы классов Form находятся в libraries/src/Form. Эти файлы мы исследуем для того, чтобы понять саму логику, что и как происходит с данными и как с этим работать.
Если кратко, то конструктор Form
получает XML с описанием полей. считывает данные (тип поля, кастомный класс поля из атрибута addfieldprefix
, если есть и т.д.), загружает с помощью FormHelper
нужный класс поля. Если у поля есть некие правила фильтрации выводимых данных - используется класс FormRule
- вспомните поля Joomla типа filelist
, где можно указать параметры фильтрации и выбрать, например, только php или только css файлы.
Честно скажу, что на данный момент сам до конца не знаю всех тонкостей работы, статья выросла сугубо из практического опыта. Поэтому не буду погружаться здесь слишком глубоко, дабы случайно не наврать 😎.
Файлы классов полей Joomla Form находятся в libraries/src/Form/Field. Их, мягко говоря, много. Это строительный материал админки, а порой и фронтенда.
В файлах классов описаны свойства класса, такие как $type
, $layout
и другие необходимые для работы. У большинства полей есть методы getInput()
- собственно вызов представления (HTML-вывода) поля, getLayoutData()
- предобработка данных для поля перед отдачей их на рендер, getLabel()
- работа с заголовком поля и т.д.
Мы помним, что классы полей наследуют родительский класс FormField
. В файле класса libraries/src/Form/FormField.php описаны возможные атрибуты поля, которые можно использовать при описании в XML с кратким описанием того что это и зачем.
У классов-детей (наследников) есть возможность работать с методами родительского класса и при необходимости переопределять его.
Представление (HTML вывод, layout) поля в Joomla 5
У каждого класса поля есть представление. В классическом MVC представление работает с выводом данных сразу, однако в Joomla есть прослойка - Layout, которая позволяет делать переопределения макетов - одна из важнейших фич этого движка. Ожидаемо лейауты ядра находятся в папке layouts
. В них передаётся массив $displayData
со всеми данными, полученными из метода getLayoutData()
. Какой именно макет вывода использовать указываем в свойстве класса $layout.
<?php
/**
* Name of the layout being used to render the field
*
* @var string
* @since 3.7
*/
protected $layout = 'joomla.form.field.email';
Такой тип записи встречается довольно часто. В Joomla layout - это разделенный точками путь к файлу макета от папки layouts в корне сайта. То есть запись $layout = 'joomla.form.field.email'
означает, что при рендере поля будет использоваться макет layouts/joomla/form/field/email.php.
<?php
use Joomla\CMS\Layout\LayoutHelper;
$displayData = [
'src' => $this->item->image,
'alt' => $this->item->name,
];
echo LayoutHelper::render(
'joomla.html.image',
$displayData
);
Аналогично, в этом примере будет использоваться макет layouts/joomla/html/image.php
. Некоторые макеты можно переопределять в папке html
шаблонов сайта и админки. Подробнее об этом статья Создание шаблонов сайта в Joomla 4+.
Соответственно, если мы хотим посмотреть какие именно данные приходят в конце-концов в макет и как они отображаются - идём в файл макета и смотрим.
Создание поля выбора данных в модальном окне Modal Select в Joomla 5
Теперь вернемся к основной задаче статьи. Упомяну здесь статью Создание плагина кнопки редактора в Joomla 4, которая в целом описывает ту же задачу, но в контексте плагина кнопки редактора и вставки данных в редактируемую область.
Эта статья описывает много деталей, поэтому далее я буду предполагать, что с ней Вы уже познакомились. В этой статье я буду больше говорить об отличиях и деталях, характерных именно для поля Form.
Нам важны примеры для изучения:
-
основной класс поля -
libraries/src/Form/Field/ModalSelectField.php
-
выбор материала Joomla -
admin istrator/components/com_content/src/Field/Modal/ArticleField.php
-
выбор типа меню -
administrator/components/com_menus/src/Field/MenutypeField.php
-
выбор пункта меню -
administrator/components/com_menus/src/Field/MenutypeField.php
-
макет вывода -
layouts/joomla/form/field/modal-select.php
Поле с выбором контакта из com_contacts
на момент написания статьи ещё не переделано на универсальное и просто лежит в administrator/components/com_contact/src/Field/Modal/ContactField.php
. Оно наследует напрямую FormField
, а не ModalSelectField
.
Алгоритм действий для добавления своего поля следующий:
-
создаём в xml-файле или программным методом с помощью
\SimpleXMLElement
XML форму с нашим полем. -
Если мы работаем "на лету", то плагином на событие
onContentPrepareForm
добавляем XML формы в нужную форму (проверяем$form->getName()
перед этим) -
Создаем класс поля.
-
Если нужно - создаём собственный вывод (layout) поля. Это мы оставим за пределами данной статьи.
И оно работает.
XML поля
Самым важным в этом коде является атрибут addfieldprefix
, который означает namespace Вашего класса поля. Имя класса образуется из addfieldprefix + "\" + type + "Field". В данном случае класс поля будет Joomla\Plugin\Wtproductbuilder\Providerjoomshopping\Field\ProductlistField
.
<field
type="productlist"
name="product_id"
addfieldprefix="Joomla\Plugin\Wtproductbuilder\Providerjoomshopping\Field"
label="Название поля, например Товар JoomShopping"
hint="Подсказка, если ничего ещё не выбрано, например Выберите товар."
/>
HTML вывод (layout) поля
Для того, чтобы всё происходящее в PHP было понятно - нужно для начала посмотреть макет вывода поля. Он находится в файле layouts/joomla/form/field/modal-select.php. На самом деле выводится 2 поля input
- одно видимое, другое невидимое. В видимое поле в виде placeholder
заносится название выбранного материала, контакта или товара - параметр $valueTitle
. А во второе - его id - $value
. Если у нас еще ничего не выбрано - в поле должна быт фраза в духе "выберите материал" или "выберите товар". Это языковая константа, которую мы помещаем в атрибут hint
в XML поля или же в методе setup
класса поля.
Все доступные для макета вывода параметры (а значит те, что можно использовать программно или в XML-файле):
<?php
extract($displayData);
/**
* Layout variables
* -----------------
* @var string $autocomplete Autocomplete attribute for the field.
* @var boolean $autofocus Is autofocus enabled?
* @var string $class Classes for the input.
* @var string $description Description of the field.
* @var boolean $disabled Is this field disabled?
* @var string $group Group the field belongs to. <fields> section in form XML.
* @var boolean $hidden Is this field hidden in the form?
* @var string $hint Placeholder for the field.
* @var string $id DOM id of the field.
* @var string $label Label of the field.
* @var string $labelclass Classes to apply to the label.
* @var boolean $multiple Does this field support multiple values?
* @var string $name Name of the input field.
* @var string $onchange Onchange attribute for the field.
* @var string $onclick Onclick attribute for the field.
* @var string $pattern Pattern (Reg Ex) of value of the form field.
* @var boolean $readonly Is this field read only?
* @var boolean $repeat Allows extensions to duplicate elements.
* @var boolean $required Is this field required?
* @var integer $size Size attribute of the input.
* @var boolean $spellcheck Spellcheck state for the form field.
* @var string $validate Validation rules to apply.
* @var string $value Value attribute of the field.
* @var string $dataAttribute Miscellaneous data attributes preprocessed for HTML output
* @var array $dataAttributes Miscellaneous data attribute for eg, data-*
* @var string $valueTitle
* @var array $canDo
* @var string[] $urls
* @var string[] $modalTitles
* @var string[] $buttonIcons
*/
PHP класс поля
Класс поля, как Вы уже догадались, находится у меня в плагине. Путь к нему plugins/wtproductbuilder/providerjoomshopping/src/Field/ProductlistField.php
. Я взял за основу поле выбора материалов и переделал его под свои нужды - выбор товара из интернет-магазина JoomShopping. Мы расширяем родительский класс ModalSelectField
своим классом.
В мои задачи входит только выбор товара, редактирование и создание - нет, поэтому в тексте статьи речь идёт только о выборе товара. Класс небольшой, приведу его целиком и прокомментирую.
<?php
namespace Joomla\Plugin\Wtproductbuilder\Providerjoomshopping\Field;
use Joomla\CMS\Factory;
use Joomla\CMS\Form\Field\ModalSelectField;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Layout\FileLayout;
use Joomla\CMS\Session\Session;
use Joomla\CMS\Uri\Uri;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Supports a modal article picker.
*
* @since 1.6
*/
class ProductlistField extends ModalSelectField
{
/**
* The form field type.
*
* @var string
* @since 1.6
*/
protected $type = 'Productlist';
/**
* Method to attach a Form object to the field.
*
* @param \SimpleXMLElement $element The SimpleXMLElement object representing the `<field>` tag for the form field object.
* @param mixed $value The form field value to validate.
* @param string $group The field name group control value.
*
* @return boolean True on success.
*
* @see FormField::setup()
* @since 5.0.0
*/
public function setup(\SimpleXMLElement $element, $value, $group = null)
{
// Получаем само поле
$result = parent::setup($element, $value, $group);
if (!$result)
{
return $result;
}
$app = Factory::getApplication();
// Нам нужны Url для получения списка товаров,
// получения формы редактирования, формы создания
// сущности. Указываем их тут.
// Результат обращения по этим URL должен отдавать HTML,
// в котором в том числе будет небольшой javascript,
// передающий выбранные значения - id товара и название товара.
// В целом здесь следуем статье о создании плагина кнопки редактора.
$urlSelect = (new Uri())->setPath(Uri::base(true) . '/index.php');
$urlSelect->setQuery([
'option' => 'com_ajax',
'plugin' => 'providerjoomshopping',
'group' => 'wtproductbuilder',
'format' => 'html',
'tmpl' => 'component',
Session::getFormToken() => 1,
]);
$modalTitle = Text::_('PLG_WTPRODUCTBUILDER_PROVIDERJOOMSHOPPING_MODAL_SELECT_CHOOSE_PRODUCT');
$this->urls['select'] = (string) $urlSelect;
// Комментируем эти строки, они не нужны. В разделе статьи о JavaScript
// рассказываем почему.
// $wa = $app->getDocument()->getWebAssetManager();
// $wa->useScript('field.modal-fields')->useScript('core');
// Заголовок модального окна выбора сущности
// Для создания и редактирования соответственно тоже нужны
// отдельные заголовки
$this->modalTitles['select'] = $modalTitle;
// hint - подсказка placeholder в HTML поля.
$this->hint = $this->hint ?: Text::_('PLG_WTPRODUCTBUILDER_PROVIDERJOOMSHOPPING_MODAL_SELECT_CHOOSE_PRODUCT');
return $result;
}
}
Отдельно вынес метод getValueTitle()
, который показывает название выбранной сущности (название товара, заголовок материала и т.д.) в тех случаях, когда они уже выбраны и сохранены. То есть, мы зашли отредактировать пункт меню, поле не трогаем, но хотим видеть заголовок материала / название товара человекопонятное, а не просто id. Этот метод показывает нужный заголовок.
<?php
/**
* Метод показывает название выбранного товара в поле-плейсхолдере.
*
* @return string
*
* @since 5.0.0
*/
protected function getValueTitle()
{
$value = (int) $this->value ?: ''; // Это id материала или товара или...
$title = '';
if ($value)
{
try
{
// Для получения нужных данных лучше всего использовать
// методы API ядра Joomla и/или компонентов,
// а не прямые запросы в базу.
$lang = \JSFactory::getLang();
$name = $lang->get('name');
$jshop_product = \JSFactory::getTable('product', 'jshop');
$jshop_product->load($value);
$title = $jshop_product->$name;
}
catch (\Throwable $e)
{
Factory::getApplication()->enqueueMessage($e->getMessage(), 'error');
}
}
return $title ?: $value;
}
В некоторых полях, где требуется более сложный функционал - мультиязычные ассоциации и прочее - в классе поля встречаются и другие методы, которые переопределяют базовые методы класса FormField
:
-
getLayoutData()
- метод предобработки данных перед собственно рендером поля -
getRenderer()
- дополнительные параметры для рендера
и так далее.
В нашем случае такой необходимости нет, поэтому их не используем.
HTML вывод содержимого модального окна
При нажатии на кнопку "выбрать" открывается модальное Bootstrap окно, в котором в <iframe>
открывается список товаров. По клику на название товара или картинку Javascript получит id и название товара для нашего поля. Но, чтобы получить сам HTML этого списка - мы должны его где-то реализовать. То есть такой вывод и функционал должен либо поддерживать компонент и тогда мы будем обращаться по URL компонента. Либо через com_ajax
мы обращаемся к плагину и получаем HTML оттуда. Так и поступим.
В моём плагине метод onAjaxProviderjoomshopping()
возвращает вёрстку списка товаров. Там мы циклом проходим массив с ними, берем картинку, название и выводим. Код в целом объёмный, поэтому опубликую самые важные фрагменты.
Первое - добавляем в <iframe>
наш javascript, который будет слушать клики по картинкам и названиям товаров. Оформляем его в виде медиа-ассета к плагину и подключаем через Web Asset Manager.
<?php
$app = $this->getApplication();
$doc = $app->getDocument();
$doc->getWebAssetManager()
->useScript('core')
->registerAndUseScript(
'wtproductbuilder.providerjoomshopping.modal', 'plg_wtproductbuilder_providerjoomshopping/providerjoomshopping.modal.js'
);
Для любопытных статьи:
Второе. Код тега ссылки должен содержать data-атрибуты с нужными нам данными.
<?php
use Joomla\CMS\HTML\HTMLHelper;
// этот код выполняется внутри foreach($products as $row)
$link_attribs = [
'class' => 'select-link',
'data-product-id' => $row->product_id,
'data-product-title' => htmlspecialchars($row->name),
];
echo HTMLHelper::link(
'#', // url
$row->name, // текст ссылки, анкор
$link_attribs // атрибуты ссылки
);
JavaScript обработка. Отправляем данные из <iframe> в поле в родительском окне
Теперь приступим к работе с JavaScript. В процессе написания статьи выяснились нюансы, которые позволяют говорить о старом и новом способе работы.
Мы помним, что в процессе работы мы подключили следующие js-скрипты
-
media/system/js/fields/modal-fields.min.js
- этот файл подключался в классе поля выбора материала. Однако, сейчас можно говорить. что архаизм и устаревший метод работы. Этот файл уже не нужен. Мы его закомментировали в нашем PHP классе. -
media/plg_wtproductbuilder_providerjoomshopping/js/providerjoomshopping.modal.js
- наш собственный js-файл.
Начнём с собственного javascript. Здесь мы по классу select-link
получаем все селекторы и вешаем на них слушателя события клика.
(() => {
document.addEventListener('DOMContentLoaded', () => {
// Get the elements
const product_links = document.querySelectorAll('.select-link');
// Listen for click event
product_links.forEach((element) => {
element.addEventListener('click', event => {
event.preventDefault();
const {
target
} = event;
let data = {
'messageType' : 'joomla:content-select',
'id' : target.getAttribute('data-product-id'),
'title' : target.getAttribute('data-product-title')
};
window.parent.postMessage(data);
});
});
});
})();
Если с id
и title
всё интуитивно понятно, то с объектом data
и postMessage
тем, кто привык работать с Joomla, может быть не всё очевидно.
Раньше в Joomla 2.5, 3.x и даже в 4.x использовался следующий подход: в макете вывода поля мы инлайн скриптом вешали на window
функцию-обработчик, а из <iframe>
вызывали её как window.parent[functionName]
. Посмотрите на этот код
element.addEventListener('click', event => {
event.preventDefault();
const functionName = event.target.getAttribute('data-function');
if (functionName === 'jSelectMenuItem' && window[functionName]) {
// Used in xtd_contacts
window[functionName](event.target.getAttribute('data-id'), event.target.getAttribute('data-title'), event.target.getAttribute('data-uri'), null, null, event.target.getAttribute('data-language'));
} else if (window.parent[functionName]) {
// Used in com_menus
window.parent[functionName](event.target.getAttribute('data-id'), event.target.getAttribute('data-title'), null, null, event.target.getAttribute('data-uri'), event.target.getAttribute('data-language'), null);
}
В таком виде имя функции было указано в атрибуте data-function
каждой ссылки списка материалов / контактов / пунктов меню. А саму функцию помещали инлайн, иногда уникализируя её имя дополнительным id. Например, "jSelectArticle_".$this->id
.
Функция jSelectArticle()
или ей подобные (у нас была бы jSelectProduct()
) являются обёрткой для штатной функции processModalSelect()
из файла modal-fields.min.js
. Она в свою очередь вызывает функцию processModalParent()
и после выполнения закрывает модальное окно.
Этой функции для работы требовалось указать кучу параметров: тип сущности (материал. контакт и т.д.), префикс поля (который на практике оказался id
HTML-селектора поля), собственно id
и title
- нужные нам параметры и т.д.
В одной функции было собрано всё на все случаи жизни. Именно там данные помещались в наше поле. Однако, сейчас, в Joomla 5 этот файл уже не нужен. Если мы используем стандартный макет вывода поля, то в нём подключается ассет modal-content-select-field
, работающий по-новому.
Итак, нам теперь не нужны лишние атрибуты ссылок с именем вызываемых функций. Вместо этого мы используем механизм postMessage
. Для этого в объекте data
нам нужно указать параметр messageType
равным joomla:content-select
. Почему? С точки зрения JavaScript работа в Joomla происходит следующим образом:
-
клик по ссылке и получение атрибутов ссылки
-
отправка сообщения в родительское окно
window.parent.postMessage(data)
-
в родительском окне подключён файл
media/system/js/fields/modal-content-select-field.js
, в котором есть слушатель событияmessage
. -
Он проверяет тип сообщения и если это
joomla:content-select
, то значения помещаются в нужные поля и модальное окно закрывается
Я, в процессе изучения кода ядра и поиске решения естественно набрёл на функции jSelectArticle()
и подобные. Потом столкнулся с postMessage
и решил сделать свой messageType
, дав ему длинное уникальное имя. Чтобы оно заработало - к нему написал свою обработку, вызвав (как оказалось устаревшую) функцию processModalSelect()
. И столкнулся с тем, что модальное окно ни в какую не хотело закрываться, хотя данные в поля вставлялись корректно. Дальнейшие поиски привели сначала к правильному типу события, а потом и к удалению ненужных скриптов и упрощению кода в целом.
Вместо заключения
Конечно, такое модальное окно и выбор сущностей в нём - это довольно частный случай, но, надеюсь, он будет кому-то полезным.
Если уважаемые читатели обнаружат где-то неточности, опечатки, неверные тезисы - пишите, подправим.