В панели администратора Joomla встречаются поля, которые нужно заполнить данными из других компонентов: указать материал, пункт меню, контакт, товар и т.д. Обычно такие поля оформляются в виде выпадающего списка select option, могут оформляться в виде input type="text" с datalist, но есть и удобные поля, показывающие список искомых сущностей, с фильтрацией, поиском, пагинацией и т.д.

Все мы видели эти поля в действии при выборе материала в пункте меню типа "Материалы - Материал", "Контакты - Контакт", или при создании алиаса (псевдонима) пункта меню - "Системные - Псевдоним". Однако напомним себе как они выглядят.

Модальное окно выбора материала.

На видео - утро, компьютер ещё не проснулся и локальный сервер чуть тупит ))

А это модальное окно выбора контакта.

Возможности поля выбора в модальном окне в 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, работающий по-новому.

Сейчас Joomla 5 фронтенд переходит на использование JavaScript postMessages. Поскольку не все старые расширения ещё готовы перейти на новые рельсы внедрён флаг JoomlaExpectingPostMessage, который позволяет отличать устаревшие методы вызова событий. К описываемому способу работы он имеет косвенное отношение, но может быть кому-то полезен. Данный флаг будет удалён после полного перехода на postMessages.

Итак, нам теперь не нужны лишние атрибуты ссылок с именем вызываемых функций. Вместо этого мы используем механизм 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(). И столкнулся с тем, что модальное окно ни в какую не хотело закрываться, хотя данные в поля вставлялись корректно. Дальнейшие поиски привели сначала к правильному типу события, а потом и к удалению ненужных скриптов и упрощению кода в целом.

Вместо заключения

Конечно, такое модальное окно и выбор сущностей в нём - это довольно частный случай, но, надеюсь, он будет кому-то полезным.

Если уважаемые читатели обнаружат где-то неточности, опечатки, неверные тезисы - пишите, подправим.

Полезные ресурсы

Ресурсы сообщества:

Telegram:

Комментарии (3)


  1. MaxPro33
    28.11.2023 13:39

    Вы упоминаете о параметре urlCheckin и его роли в отправке выбранного значения на указанный в поле URL. Могли бы вы предоставить пример использования этого параметра и объяснить, как это может быть полезно в практических сценариях разработки?


    1. sergeytolkachyov Автор
      28.11.2023 13:39

      Я при беглом поиске пока не нашёл примеров использования в ядре. Возможно, это основа будущего функционала. Тоже он обратил на себя внимание)


    1. sergeytolkachyov Автор
      28.11.2023 13:39

      В joomla бывают довольно сложные формы и поле может оказаться в разных условиях. Могу предположить, что возможно это будет использоваться для зависимых полей, когда значения поля зависят от значения другого. Но это пока умозрительно.