Статья о том, как удобно расширить пользовательские возможности при редактировании или создании контента в Joomla. Для начала небольшое отступление, которое пригодится начинающим разработчикам или разработчикам, незнакомым с нутром Joomla. Если Вы опытный пользователь или разработчик - можете перейти сразу к разделу статьи Как добавить кнопку редактора в Joomla со своим функционалом?

Что такое плагин в Joomla?

В терминологии Joomla "плагин" - это расширение, которое предоставляет функции, связанные с событиями (Event Dispatching). В Joomla довольно много типов плагинов, используемых для решения самых разных задач. Возможности и функционал плагина зависит от времени и места вызываемого события, по которому он срабатывает: можно добавить некий кусочек HTML к статье, а можно отправить данные заказа по API в CRM систему, "прочесать" весь готовый HTML-код страницы регуляркой или добавить новые команды в Joomla CLI интерфейс.

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

Плагины группы Editors-Xtd или плагины кнопок редактора

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

Для удобства и скорости работы все плагины в Joomla делятся на группы, каждая из которых срабатывает только при определенных условиях и в определенных местах сайта. Исключением является группа плагинов system. Плагины этой группы вызываются всегда ПЕРЕД плагинами групп. То есть, если у Вас будет 2 плагина, один из которых системный (группа system), а другой, например, контент-плагин (группа content) с одним и тем же событием (триггер), то сначала сработает системный, а потом уже контент-плагин.

Плагины группы editors-xtd вызываются только в том месте сайта, где есть поле для редактирования текста с помощью редактора: редактирование материала, описания товара, содержимого модуля и т.д.

Кнопки редактора в Joomla 4. Штатный редактор Tiny MCE.
Кнопки редактора в Joomla 4. Штатный редактор Tiny MCE.

Зачем они нужны?

Плагины этой группы добавляют дополнительные кнопки к окну редактирования текста, которые не зависят от собственно редактора текста. Вы можете пользоваться стандартным TinyMCE, Code Mirror, скачать из Joomla Extensions Directory JCE или любой другой редактор, или вообще работать без редактора и писать HTML код руками. Но эти вспомогательные кнопки будут всегда. Обычно они находятся под окном редактирования текста, но это уже регулируется настройками конкретного редактора.

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

Это может быть:

  • вставка шорткода для вставки модуля по позиции ({loadposition position_name}) или по id модуля ({loadmoduleid 42}).

  • вставка шорткода для вывода данных контакта, материала, пункта меню и т.д.

  • вставка картинки в текст с помощью штатного медиа менеджера (многие редакторы предоставляют свои альтернативы).

  • вставка линий "подробнее" и/или "разрыв страницы".

  • вставка шорткода для замены его на изображения из компонента фотогалереи

  • вообще любые манипуляции с HTML-кодом поля редактирования: исправления опечаток, типографирование текста, отправка или получение данных к стороннему API и так далее. Всё, что Ваша душа пожелает.

Обычно плагины кнопки редактора работают в 2-х режимах:

  • Вариант 1: напрямую модифицируют HTML-код в поле редактирования

  • Вариант 2: вызывают модальное окно для сортировки и выбора данных, затем модифицируют HTML-код в поле редактирования

Пример первого варианта - плагин для вставки линии "подробнее". В материалах Joomla эта линия используется для разделения текста статьи на вступительный и полный текст.

Пример 2-го варианта - кнопка для вставки SEF ссылки на материал. Сначала открывается модальное окно для поиска и выбора нужного материала, затем в текст статьи вставляется non-SEF ссылка на нужный материал. При рендере страницы системный плагин SEF обрабатывает все вхождения таких ссылок и заменяет их на актуальные SEF ссылки. Если материал останется в той же категории, но будет, например, переименован - url ссылок на него обновятся автоматически.

Как добавить кнопку редактора в Joomla со своим функционалом?

Ответ простой: написать свой плагин группы editors-xtd. Также Вам может пригодится статья Создание плагинов с учётом новой структуры Joomla 4.

В данном разделе я постараюсь описать какие инструменты даёт Вам Joomla для реализации своего функционала. В качестве примера будет плагин WT Typograph, служащий для исправления типографики текста и приведения его к единому стилю.

Плагин:

  • обрабатывает текст поля редактора с помощью стороннего сервиса типографики typograf.ru. Данный сервис имеет бесплатное API.

  • отправляет в API выделенный текст. Если выделения нет - весь текст поля редактора.

  • имеет 2 режима: обычный (по умолчанию) и экспресс.

    • обычный режим при нажатии на кнопку редактора открывает модальное окно с предпросмотром изменений типографа и кнопкой "вставить". Замена текста на измененный происходит по нажатию этой кнопки.

    • В экспресс-режиме при нажатии на кнопку редактора текст отправляется в API в фоновом режиме и заменяет содержимое или выделение сразу, без промежуточного окна и предпросмотра.

  • Режим работы плагина переключается в настройках плагина.

Таким образом, данный плагин сможет продемонстрировать оба распространённых режима работы с контентом в Joomla.

Распределение функционала

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

Получение текста для типографирования, отправка текста в API и получение его, возврат результата в javascript ложится на плечи PHP.

Фронтенд часть (javascript код) мы разнесём по двум разным js-файлам, которые будем подключать в зависимости от выбранного режима работы.

Файловая структура

В Joomla 3 и Joomla 4 различается файловая структура расширений. Однако, Joomla 4 поддерживает старую файловую структуру, поэтому в нашем примере будет использоваться именно она. В рамках данной статьи файловая структура не имеет большого значения. Это, скорее, чисто техническая деталь. Но, обратите внимание на то, что до версии Joomla 4.3 плагины группы редактора имели старую файловую структуру, не использовали namespaces, так как их поддержка ещё не была реализована. Начиная с версии 4.3 можно писать плагин кнопки редактора по новой файловой структуре. Это предпочтительнее по многим причинам. Подробнее в разделе "Новая система событий для плагинов в Joomla 4" статьи Создание плагинов с учётом новой структуры Joomla 4.

Серверная часть плагина. PHP

Для создания и установки плагина, возможности сделать раздел настроек плагина нам нужно создать минимум 2 файла (в рамках старой файловой структуры плагинов Joomla 3):

  • XML-манифест плагина

  • файл класса плагина

XML-манифест плагина

В нём указывается описание плагина для установщика расширений Joomla (системное имя, дата, версия, сайт разработчика и т.д.), параметры конфигурации плагина, сервер обновлений и т.д.

<?xml version="1.0" encoding="utf-8"?>
<extension type="plugin" group="editors-xtd" method="upgrade">
	<name>Кнопка - Типограф</name>
	<author>Sergey Tolkachyov</author>
	<creationDate>12/03/2023</creationDate>
	<copyright>(C) 2023 Sergey Tolkachyov</copyright>
	<license>GNU General Public License version 2 or later; see LICENSE.txt</license>
	<authorEmail>info@web-tolk.ru</authorEmail>
	<authorUrl>https://web-tolk.ru</authorUrl>
	<version>1.0.0</version>
	<description>Типографирует текст с помощью типографов</description>
	<scriptfile>script.php</scriptfile>
	<files>
		<filename plugin="wttipograph">wttipograph.php</filename>
	</files>
	<scriptfile>script.php</scriptfile>
	<media folder="media" destination="plg_editors-xtd_wttipograph">
		<folder>js</folder>
	</media>
	<config>
		<fields name="params">
			<fieldset name="basic">
				<field type="radio"
					   name="express_mode"
					   label="Экспресс режим?"
					   description="По умолчанию будет открываться промежуточное модальное (всплывающее) окно. Экспресс режим позволяет изменять текст сразу в редакторе, без модального окна."
					   class="btn-group btn-group-yesno"
					   default="0">
					<option value="1">JYES</option>
					<option value="0">JNO</option>
				</field>
			</fieldset>
		</fields>
	</config>
</extension>

Секция <files> говорит Joomla в каком именно файле находится класс плагина.

Секция <media> говорит Joomla, что содержимое папки js при установке нужно скопировать в media/plg_editors-xtd_wttipograph/js. Находящиеся в этой папке скрипты (и стили) удобно и безопасно потом подключать с помощью HTMLHelper (или Web Assets Manager в Joomla 4).

Секция <config> реализует возможность настройки плагина из админки. Если Вам это не нужно - эту секцию можно удалить полностью. В нашем случае настройка использования экспресс-режима находится именно там. В данном примере в атрибутах label и description я написал сразу по-русски. Joomla так позволяет делать. Но правильно создать файлы локализации для английского и Вашего языка, указать в них языковые константы и в значениях этих атрибутов указывать именно языковые константы.

PHP файл класса плагина. Создание и вывод кнопки

Класс плагина расширяет класс CMSPlugin и в Joomla 3 (и legacy Joomla 4) должен включать в своё имя префикс Plg, словоButton и имя класса. Имя класса совпадает с именем файла и папки, где плагин лежит.

То есть в файле plugins/editors-xtd/wttipograph/wttipograph.php будет находится класс PlgButtonWttipograph. В Joomla 4 с этим, конечно, проще ????. Таким образом class PlgButtonWttipograph extends CMSPlugin. в нём должен быть как минимум один метод onDisplay(). Именно этот метод возвращает объект самой кнопки редактора со всеми параметрами.

<?php
use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\CMS\Factory;

\defined('_JEXEC') or die;

class PlgButtonWttipograph extends CMSPlugin
{

   /**
	 * Display the button
	 *
	 * @param   string  $name  The name of the button to add
	 *
	 * @return  CMSObject|void  The button options as CMSObject, void if ACL check fails.
	 *
	 * @since   1.5
	 */
	public function onDisplay($name)
	{
		// Здесь код для формирования кнопки.
	}
}

Метод onDisplay() принимает аргумент $name, который (вопреки комментариям в коде) означает имя экземпляра редактора на странице, а не "название кнопки для добавления". Например, у Вас сайт на двух языках и Вы редактируете статью одновременно на 2-х языках, на странице у Вас будет 2 экземпляра редактора для каждого языка свой. У каждого экземпляра редактора будут показываться кнопки редактора, в том числе и Ваша.

Одновременное редактирование текста на двух языках в Joomla.
Одновременное редактирование текста на двух языках в Joomla.

Поскольку кнопки редактора можно показывать лишь определенным группам пользователей, в метод onDisplay() обычно добавляют проверку прав текущего пользователя на создание и редактирование.

<?php
use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\CMS\Factory;

\defined('_JEXEC') or die;

class PlgButtonWttipograph extends CMSPlugin
{
    public function onDisplay($name)
	{
        // Получаем объект пользователя 
		$user = Factory::getUser();

		// Может ли пользователь создавать статьи
		$canCreateRecords = $user->authorise('core.create', 'com_content')
			|| count($user->getAuthorisedCategories('com_content', 'core.create')) > 0;

		// Может ли пользователь редактировать статьи.
		$values           = (array) Factory::getApplication()->getUserState('com_content.edit.article.id');
		$isEditingRecords = count($values);

		// Если не может - кнопку не показываем.
        // This ACL check is probably a double-check (form view already performed checks)
		$hasAccess = $canCreateRecords || $isEditingRecords;
		if (!$hasAccess)
		{
			return;
		}
    }
}

Проверки прав доступа в разных плагинах реализованы несколько по-разному, поэтому стоит посмотреть их реализацию в плагинах ядра Joomla и выбрать самый подходящий.

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

<?php
use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\CMS\Factory;

\defined('_JEXEC') or die;

class PlgButtonWttipograph extends CMSPlugin
{
    public function onDisplay($name)
	{
        // Для краткости мы опускаем здесь код проверки прав пользователя
        
        /**
         * Кнопка - это экземпляр класса CMSObject().
         */
        $button          = new \Joomla\CMS\Object\CMSObject();
    }
}

Класс CMSObject помечен как deprecated в Joomla 4 и будет удалён в Joomla 6 (предполагаемая дата выхода - 2025 год). Вместо него рекомендуют использовать \stdClass или \Joomla\Registry\Registry. Например: new \Joomla\Registry\Registry(). Однако, на момент написания плагина (в Joomla 4.2) это ещё не работало стабильно, поэтому кнопка использует класс CMSObject .

<?php
use Joomla\CMS\Factory;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\CMS\Session\Session;

\defined('_JEXEC') or die;

class PlgButtonWttipograph extends CMSPlugin
{
    public function onDisplay($name)
	{
        // Для краткости мы опускаем здесь код проверки прав пользователя
        
        /**
         * Кнопка - это экземпляр класса CMSObject().
         */
        $button          = new \Joomla\CMS\Object\CMSObject();

        // Получаем параметр плагина express_mode,
        // который мы указали в XML-манифесте
        if($this->params->get('express_mode',0) == 1){

            // Это экспресс-режим, без модального окна.
          
			$app = Factory::getApplication();
			$doc = $app->getDocument();
			$doc->getWebAssetManager()
				->registerAndUseScript('admin-wttipograph', 'plg_editors-xtd_wttipograph/admin-wttipograph.js', [], ['defer' => true], ['core']);

			$button->modal   = false;
			$button->onclick = 'wtTipograf(\'' . $name . '\');return false;';
			$button->link    = '#';
		} else {

          // Это обычный режим, с модальным окном.
          
			$button->modal   = true;
			$link = 'index.php?option=com_ajax&plugin=wttipograph&group=editors-xtd&format=html&tmpl=component&action=showform&'
				. Session::getFormToken() . '=1&amp;editor=' . $name;
			$button->link    = $link;
		}

        // Это общие для обоих режимов работы плагина параметры 
        $button->text    = Text::_('Типограф');
		$button->name    = $this->_type . '_' . $this->_name;
		$button->icon    = 'file-add';
		$button->iconSVG = '<svg viewBox="0 0 32 32" width="24" height="24"><path d="M28 24v-4h-4v4h-4v4h4v4h4v-4h4v-4zM2 2h18v6h6v10h2v-10l-8-'
			. '8h-20v32h18v-2h-16z"></path></svg>';
		$button->options = [
			'height'     => '300px',
			'width'      => '800px',
			'bodyHeight' => '70',
			'modalWidth' => '80',
		];

		return $button;
      
    }
}

Как уже писал выше, кнопка может вызывать модальное окно, а может сразу вызывать нужную нам функцию javascript. Поэтому в здесь мы проверяем режим работы плагина: $this->params->get('express_mode',0) - это наш параметр-переключатель из XML-манифеста. Второй аргумент в get('express_mode',0) означает значение по умолчанию, если в результате get() из params ничего не приходит. Такое может быть, если Вы плагин только что установили, но в настройки даже не заходили и сохранённых параметров плагина по сути не существует.

Если плагин работает в экспресс режиме - мы подключаем javascript-файл для экспресс режима - admin-wttipograph.js. Этот файл физически находится в media/plg_editors-xtd_wttipograph/js/admin-wttipograph.js. Далее, устанавливаем флаг $button->modal = false; , $button->link становится простым шарпом, и для onclick мы пишем вызов нужной нам javascript-функции, куда передаётся id окна редактора.

Если плагин работает в обычном режиме, мы устанавливаем флаг $button->modal = true; , это отрисует бутстраповскую модалку для нашей кнопки. Внутри модального окна будет <iframe>, src которого мы передаём в $button->link.

В общих для обоих режимов работы параметрах кнопки нам будут интересны:

  • $button->text - это текст на кнопке

  • $button->icon - css-класс иконки для кнопки. Joomla использует чуть модифицированный Font Awesome 5.1. В Joomla 5 ожидается FontAwesome 6.

  • $button->iconSVG - SVG-код иконки кнопки.

  • $button->options - массив с параметрами <iframe> и самого модального окна.

    • height - 400px - высота <iframe> в модалке

    • width - 800px - ширина <iframe> в модалке

    • bodyHeight - высота modal body во viewport units. Указывается целым числом. Будет преобразовано в css-класс jviewport-height + Ваше число. В css шаблона Atum админки Joomla есть классы от jviewport-height10 до jviewport-height100 с шагом = 10.

    • modalWidth - ширина modal dialogво viewport units, целое число. Будет преобразовано в css-класс jviewport-width + Ваше число. В css шаблона Atum админки Joomla есть классы от jviewport-width10 до jviewport-width100 с шагом = 10.

Также ожидается поддержка параметра modalCss (Pull Request на GitHub) для возможности указать свой css класс (для поддержки Bootstrap 5 Optional sizes или  Fullscreen modal).

Результатом нашей работы станет вывод кнопки среди прочих под окном редактора или в выпадающем списке редактора (у кого как сделано).

Кнопка редактора Joomla, выведенная плагином
Кнопка редактора Joomla, выведенная плагином

Далее для разнообразия предлагаю временно переключиться во фронтенд и посмотреть как происходит работа в браузере.

Фронтенд. Javascript плагина для работы с содержимым поля редактора в Joomla.

Для начала рассмотрим более простой режим - экспресс-режим.

Экспресс режим работы плагина. Шаблон панели администратора Joomla, а также и фронтенда подключает файл media/system/js/core.js. О некоторых полезных его функциях я уже писал в статье Ajax-запросы нативными средствами Joomla. Применять эти функции мы будем и здесь - для отправки обрабатываемого содержимого в API через метод плагина. Однако, нам для этого нужно получить это содержимое.

Признаюсь, опыта работы с javascript на данный момент у меня меньше, чем с php и не всему в нём происходящему я могу дать объяснение :) Но оно работает :) Javascript-код родился из анализа кода плагинов ядра Joomla. Вы также можете посмотреть js-код плагинов кнопки "подробнее" media/com_content/js/admin-article-readmore.js и "разрыв страницы" media/com_content/js/admin-article-pagebreak.js.

Методы работы с содержимым окна редактора

В файле core.js (который очень любят вырезать во фронтенде СЕО-специалисты, так как не знают зачем он нужен) есть объявление объекта window.Joomla.editors.instances.

В комментариях к этому объекту указаны методы, которые должны реализовать у себя редакторы для Joomla:

  • getValue - функция, возвращающая всё содержимое окна редактора.

  • setValue - функция, заменяющая всё содержимое окна редактора новым содержимым.

  • getSelection - функция возвращает выделенный фрагмент текста из окна редактора

  • disable - функция переключает редактор в неактивное состояние.

  • replaceSelection - функция заменяет выделенный ранее фрагмент содержимого окна редактора новым содержимым.

И так же приводятся примеры использования (в Joomla в принципе сам код является документацией. Комментарии как правило довольно информативны).

Примеры использования методов в комментариях в коде Joomla
Примеры использования методов в комментариях в коде Joomla

В javascript коде нашего типографа нам нужно реализовать следующую логику:

  1. Проверить, а есть ли текст вообще? Если нет - сообщить об этом и ничего не делать.

  2. Если текст есть - проверить есть ли выделенный фрагмент текста. Если есть - работаем с ним. Если нет - работаем со всем содержимым.

  3. Аналогично пункту 2, после обработки содержимого мы должны заменить весь текст в редакторе, если не был выделен фрагмент текста. И заменить только выделенный фрагмент, если он был выделен. Для этого нам понадобится переменная логический флаг, который мы будем проверять.

Далее смотрим код и комментарии к нему:

(() => {
    // Это наша функция, которая вызывается по onclick кнопки
    // Аргумент editor - это id поля редактора.
	window.wtTipograf = editor => {

        // Получаем ВСЁ содержимое окна редактора методом getValue()
		const content = window.Joomla.editors.instances[editor].getValue();
        
      // Если содержимое пусто - сообщеаем об этом и ничего дальше не делаем
		if (!content) {
			Joomla.renderMessages({
				error: ['There is no text for Tipograf']
			});
		} else if (content) {
            // Всё-таки в окне редактора какие-то мысли есть.
            // Их нужно привести в порядок типографом.
            // Получаем выделенный фрагмент текста
            // методом getSelection()
			let selection = window.Joomla.editors.instances[editor].getSelection();
            
            // Логический флаг ставим в положение false
			window.useFullTextForTipograph = false;
            // Если размер выделенного фрагмента текста равен 0 - он пустой. 
            // Значит выделения нет. Будем работать со всем содержимым.
            // Логический флаг переводим в положение true
            // И в переменную selection берём уже всё содержимое поля редактора
			if (selection.length == 0) {
				selection = window.Joomla.editors.instances[editor].getValue();
				window.useFullTextForTipograph = true;
			}

            // Выполняем ajax-запрос в PHP, к методу нашего плагина, 
            // который уже будет выполнять запрос к стороннему серверу
            // или же может делать обработку с помощью установленной 
            // локально библиотеки.
			Joomla.request({
				url: 'index.php?option=com_ajax&plugin=wttipograph&group=editors-xtd&format=json&action=doTipograph',
				method: 'POST',
				data: JSON.stringify({
					'text': selection,
				}),
				onSuccess: function (response, xhr) {
					// Выполняется только в случае успешного ответа
                    // Сервер не упал с 500-й, пришёл валидный ответ и т.д.

					//Проверяем пришли ли ответы
					if (response !== '') {
                        // Получили результат - обработанный текст.
						let tipograph_result = JSON.parse(response);
                        // Если логический флаг говорит нам заменить весь текст
                        // заменяем его методом setValue()
						if (window.useFullTextForTipograph === true) {
							window.Joomla.editors.instances[editor].setValue(tipograph_result.data[0]);
						} else {
                        // Если же логический флаг сообщает нам, что
                        // мы работаем только с определенным фрагментом текста,
                        // то заменяем лишь только его методом replaceSelection()
							window.Joomla.editors.instances[editor].replaceSelection(tipograph_result.data[0]);
						}
					}
				},
			});
		}

		return true;
	};
})();

Режим работы плагина с промежуточным модальным окном

В экспресс-режиме вся работа с содержимым редактора происходила в основном окне браузера. В режиме с промежуточным окном в модалке мы увидим <iframe>, в котором происходит работа по отправке, получению, отображению и вставке текста и результатов.

Скриншот модального окна плагина типографа для Joomla 4
Скриншот модального окна плагина типографа для Joomla 4

<iframe> является дочерним, но самостоятельным окном (window) по отношению к родительскому. Чтобы было легче работать с содержимым <iframe>, js-файл мы подключаем в HTML-коде, который мы отдаём в <iframe> при обращении к url в его src. В этом случае содержимое модального окна <iframe> загружается в браузер каждый раз и постоянно на странице не присутствует.

В целом, логика для реализации остаётся та же, с некоторыми добавлениями:

  1. При клике на кнопку редактора открывается модальное окно (это делает сама Joomla)

  2. В <iframe> загружается HTML, который мы формируем в плагине. В нём подключен наш js-файл для работы.

  3. Проверяем есть ли текст, есть ли выделение фрагмента, ставим логический флаг в нужное положение.

  4. [NEW] Показываем исходный HTML-код текста в <textarea id="wttipograph-textarea-1">.

  5. Отправляем текст для обработки в плагин по ajax, а тот отправляет по API и возвращает нам.

  6. [NEW] Показываем обработанный типографом HTML-код текста в <textarea id="wttipograph-textarea-2">.

  7. [NEW] Показываем обработанный типографом текст в виде предпросмотра в <div id="wttipograph-render">.

  8. Вставляем результат по нажатию кнопки "Вставить".

  9. Закрываем модальное окно. А то ж мешается оно.

В таком виде у нас появляется возможность сравнить как было и как стало. А также мы можем в <textarea> с результатом обработки руками что-то подправить. Правда, там HTML-код и не думаю. что это будет прям киллер-фичей данного плагина, но почему бы и нет? ????

Далее читаем код и комментарии к нему.

(() => {
    // Этот файл загружается вместе с содержимым <iframe>
    // Функция вызывается также после загрузки содержимого <iframe>
    // по событию DOMContentLoaded
	window.wtTipograph = () => {
        // Id экземпляра редактора мы получаем из Joomla options storage,
        // который мы будем передавать из PHP в JS.
        // Если отсутствует - мы в печали.
		if (!Joomla.getOptions('xtd-wttipograph')) {

			return false;
		}
        // Здесь мы всё-таки получили имя экземпляра редактора. 
        // Можно работать дальше.
		const {
			editor
		} = Joomla.getOptions('xtd-wttipograph');

        // Этот код в целом Вам уже знаком.
        // Отличие только в том, что мы находимся 
        // в дочернем окне - в <iframe>, поэтому
        // вызов методов идёт у родительского window.parent,
        // вместо window.Joomla.editors...
      
		let selection = window.parent.Joomla.editors.instances[editor].getSelection();
		window.useFullTextForTipograph = false;
        // Если длина выделеннго фрагмента текста больше 0 - работаем с выделением.
		if (selection.length == 0) {
			selection = window.parent.Joomla.editors.instances[editor].getValue();
			window.useFullTextForTipograph = true;
		}
        // Здесь получаем id 2-х <textarea> для сравнения текстов ДО и ПОСЛЕ
        // А таже id <div> для предпросмотра
		let textarea1 = document.getElementById('wttipograph-textarea-1');
		let textarea2 = document.getElementById('wttipograph-textarea-2');
		let wttipographRenderDiv = document.getElementById('wttipograph-render');
        // Устанавливаем исходный текст для БЫЛО.
        textarea1.innerText = selection;
        // Делаем ajax-запрос к плагину для обработки текста.
		Joomla.request({
			url: 'index.php?option=com_ajax&plugin=wttipograph&group=editors-xtd&format=json&action=doTipograph',
			method: 'POST',
			data: JSON.stringify({
				'text': selection,
			}),
			onSuccess: function (response, xhr) {
				// Выполняется только в случае успешного ответа
                // Сервер не упал с 500-й, пришёл валидный ответ и т.д.
				//Проверяем пришли ли ответы
				if (response !== '') {
					let tipograph_result = JSON.parse(response);
                    // Устанавливаем результат для СТАЛО
					textarea2.innerHTML = tipograph_result.data[0];
                    // Устанавливаем предпросмотр в <div>
					wttipographRenderDiv.innerHTML = tipograph_result.data[0];
				}
			},
		});

		return true;
	};

	document.addEventListener('DOMContentLoaded', () => {
        // Вызываем функцию для обработки.
		window.wtTipograph();

		// Получаем кнопку "Вставить"
		let pasteBtn = document.getElementById('pasteTipographedTextBtn');
		pasteBtn.addEventListener('click', () => {
            // Проверям наличие имени экземпляра редактора
			if (!Joomla.getOptions('xtd-wttipograph')) {

				return false;
			}
            
            // Получили имя редактора
			const {
				editor
			} = Joomla.getOptions('xtd-wttipograph');
          
            // Берём готовый обработанный результат для вставки в редактор.
			let wttipographRenderDiv = document.getElementById('wttipograph-render');
          
            // Проверяем логический флаг. От него зависит какой метод редактора
            // мы будем использовать setValue() или replaceSelection()
			if (window.useFullTextForTipograph === true) {
				window.parent.Joomla.editors.instances[editor].setValue(wttipographRenderDiv.innerHTML);
			} else {
				window.parent.Joomla.editors.instances[editor].replaceSelection(wttipographRenderDiv.innerHTML);
			}
            
          // После всего произошедшего остаётся только закрыть модальное окно.
			if (window.parent.Joomla.Modal) {
				window.parent.Joomla.Modal.getCurrent().close();
			}
		});
	});
})();

Наш плагин почти готов. Осталось приоткрыть завесу тайны над тем, как происходит обработка полученного текста. Мы возвращаемся в PHP.

Получение данных для обработки в Joomla PHP по ajax и работа со сторонним API

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

Полазив по интернету я нашёл несколько сервисов типографики (типограф Артемия Лебедева, https://typograf.ru/), а также несколько различных PHP библиотек, которые можно установить в Joomla и работать с ними локально. Предположив, что со временем можно добавить в плагин настройку по выбору провайдера типографики я решил, что стоит обращаться к стороннему API не напрямую из javascript, а из PHP. Тем более, что упомянутые сервисы предлагают готовый PHP код для работы с ними.

Таким образом из js мы делаем ajax запрос к методу плагина onAjaxWttipograph(). Как это делать описано в официальной документации, упоминавшейся статье Ajax-запросы нативными средствами Joomla. В плагине должен быть реализован метод с названием [onAjax + имя класса плагина]. В нашем случае это onAjaxWttipograph().

Мы помним о том, что у нас обращение к этому методу идёт в обоих режимах работы плагина. В одном случае этот метод должен сразу отдать текст на обработку, а во втором - показать HTML-код содержимого <iframe> для модального окна. Поэтому ajax-метод мы делаем своеобразной точкой входа, где по ключу в URL будем вызывать нужные нам методы плагина.

<?php 
use Joomla\CMS\Factory;
class PlgButtonWttipograph extends CMSPlugin
{
  // Метод для отдачи данных в ответ на ajax запрос.
  public function onAjaxWttipograph()
	{
        // Получаем объект приложения
		$app = Factory::getApplication();
        // Если запрос идёт при редактировании с фронтенда, проверяем токен.
        // Строго говоря, его проверять нужно всегда, но в админке
        // вряд ли окажется лишний человек. А если оказался, то 
        // в данном случае проверка токена уже не поможет.
		if ($app->isClient('site'))
		{
			Session::checkToken('get') or die(Text::_('JINVALID_TOKEN'));
		}

        // Получаем параметр action из запроса, по которому 
        // мы определяем ЧТО делать - показть HTML-форму
        // или получить текст для обработки.
		$action = $app->input->getCmd('action', 'wtTipograph');
        // значение для дальнейшего типографирования
		if ($action == 'doTipograph')
		{
          // Получаем текст из запроса.
          // ОБРАТИТЕ ВНИМАНИЕ НА ПАРАМЕТР ФИЛЬТРАЦИИ - RAW!
          // Если будет указан какой-либо другой, Вы получите 
          // отфильтрованный безопасности ради текст.
          // По умолчанию Joomla использует фильтр Cmd
          // Читаем в официальной документации
          // https://docs.joomla.org/Retrieving_request_data_using_JInput#Available_Filters
          
			$text = Factory::getApplication()->input->json->get('text', '', 'raw');
			if (isset($text) && !empty($text))
			{
				return $this->doTipograph($text);
			}

		} // А тут показываем HTML-форму
		elseif ($action == 'showform')
		{
			$this->showTipographForm();
		}
	}
}

Полезная ссылка на официальную документацию по фильтрам в объекте Input Joomla.

Вывод HTML содержимого <iframe> по ajax запросу к плагину в Joomla

Тип вывода информации может быть абсолютно любой. Вы можете отдавать, например, json, а в админке использовать фронтенд-технологии в виде, например, <template>. Здесь представлен простой вариант с выводом HTML формы.

<?php 
use Joomla\CMS\Factory;

class PlgButtonWttipograph extends CMSPlugin
{
    /**
	 * Показывает форму для модального окна, вызываемого кнопкой редактора.
	 *
	 * @throws Exception
	 * @since 1.0.0
	 */
	public function showTipographForm()
	{
        // Получаем объект приложения
		$app    = Factory::getApplication();
        // Получаем параметр запроса editor, который нам нужно потом передать
        // в javascript. 
		$editor = $app->input->getCmd('editor', '');
		if (!empty($editor))
		{
          // Передаём данные из PHP в Javascript с помощью метода addScriptOptions()
          // Помните, мы в js обращались к методу Joomla.getOptions('xtd-wttipograph')?
          // Ещё почитать о методе можно здесь https://web-tolk.ru/blog/razrabotka-form-obratnoj-svyazi-dlya-magazinov-na-joomla-3.html
			$app->getDocument()->addScriptOptions('xtd-wttipograph', ['editor' => $editor]);
		}

		/** @var Joomla\CMS\WebAsset\WebAssetManager $wa */
		$wa = $app->getDocument()->getWebAssetManager();
        // Добавляем наш js-скрипт с помощью Web Asset Manager
		$wa->registerAndUseScript('admin-wttipograph-modal', 'plg_editors-xtd_wttipograph/admin-wttipograph-modal.js');

        // Собираем строку с HTML.
        // Для простоты все подписи сделаны сразу по-русски. Но мы помним, 
        // что правильный подход - использование языковых констант.
		$html = <<<HTML

		<div class="row">
			<div class="col h-100">
				<label for="wttipograph-textarea-1" class="form-label">Исходный текст</label>
				<textarea class="form-control" rows="10" disabled="disabled" id="wttipograph-textarea-1"></textarea>
			
			</div>
			<div class="col h-100">
				<label for="wttipograph-textarea-2" class="form-label">Типографированный текст</label>
				<textarea class="form-control" rows="10" id="wttipograph-textarea-2"></textarea>
			</div>
		</div>
		<div class="btn-group" role="group" aria-label="Вставить" id="pasteTipographedTextBtn">
			<button type="button" class="btn btn-lg btn-large btn-primary mt-3">Вставить</button>
		</div>
		<details class="shadow-sm border border-1 w-100">
			<summary class="btn btn-outline-ligh text-center w-100">Результат</summary>
				<div class="p-3" id="wttipograph-render"></div>
		</details>

		HTML;

		echo $html;
	}
}

В этом методе мы подключаем js-файл для работы в <iframe> и собираем строку с HTML кодом. Полезная ссылка на статью Разработка форм обратной связи для магазинов на Joomla 3, в середине статьи приводится пример работы с методом addScriptOptions() Joomla. Метод работает и на Joomla 4. Ещё полезная ссылка Как правильно подключать JavaScript и CSS в Joomla 4. И ещё одна Использование WebAssetsManager Joomla 4 и добавление собственных пресетов с помощью плагина. Для простоты все подписи сделаны сразу по-русски. Но мы помним, что правильный подход - использование языковых констант и метода Joomla\CMS\Language\Text::_('YOUR_CONST').

И остался последний метод плагина для внешнего запроса к стороннему API - doTipograph().

Внешний запрос к стороннему API из Joomla

Если сервис, который Вы хотите использовать, не предоставляет готового кода - в Joomla нужно использовать для создания внешних запросов из PHP класс HttpFactory. Если сторонний сервис предоставляет готовый код и он напичкан кучей библиотек - возможно лучше будет упростить его и сделать свой запрос с помощью HttpFactory.

Статья об этом была на Хабре Создание внешних запросов с использованием HttpFactory (Joomla), а также копия статьи на моём сайте с добавлением описания изменений для Joomla 4.

Почему лучше использовать средства ядра Joomla? Потому что они всегда актуальные и обновляются вместе с ядром. В случае использования зависимостей работа по обновлению их ложится на Ваши плечи. Рано или поздно сайту всё равно нужно поднимать версию PHP как минимум, обновлять Joomla, а устаревшие и/или брошенные библиотеки могут тормозить этот процесс.

В целом реализация этого метода очень проста: мы получаем строку, формируем массив с настройками сервиса по умолчанию, делаем запрос и возвращаем ответ. Если Вы познакомитесь со статьями по ссылкам выше - никаких вопросов у Вас, скорее всего, не возникнет.

<?php 

class PlgButtonWttipograph extends CMSPlugin
{
    /**
	 * Выполняет типографирование входящего текста с помощью локальной библиотеки или стороннего сервиса.
	 *
	 * @param   string  $text
	 *
	 * @return string
	 * @since 1.0.0
	 */
	public function doTipograph(string $text)
	{
        // Получаем объект HttpFactory
		$http       = (new \Joomla\Http\HttpFactory())->getHttp();
        // Параметры запроса для стороннего сервиса.
		$xml_params = '<?xml version="1.0" encoding="windows-1251" ?>
				<preferences>
					<!-- Теги -->
					<tags delete="0">1</tags>
					<!-- Абзацы -->
					<paragraph insert="1">
						<start><![CDATA[<p>]]></start>
						<end><![CDATA[</p>]]></end>
					</paragraph>
					<!-- Переводы строк -->
					<newline insert="1"><![CDATA[<br />]]></newline>
					<!-- Переводы строк <p>&nbsp;</p> -->
					<cmsNewLine valid="0" />
					<!-- DOS текст -->
					<dos-text delete="0" />
					<!-- Неразрывные конструкции -->
					<nowraped insert="1" nonbsp="0" length="0">
						<start><![CDATA[<nobr>]]></start>
						<end><![CDATA[</nobr>]]></end>
					</nowraped>
					<!-- Висячая пунктуация -->
					<hanging-punct insert="0" />
					<!-- Удалять висячие слова -->
					<hanging-line delete="0" />
					<!-- Символ минус -->
					<minus-sign><![CDATA[&ndash;]]></minus-sign>
					<!-- Переносы -->
					<hyphen insert="0" length="0" />
					<!-- Акронимы -->
					<acronym insert="1"></acronym>
					<!-- Вывод символов 0 - буквами 1 - числами -->
					<symbols type="0" />
					<!-- Параметры ссылок -->
					<link target="" class="" />
				</preferences>';

        // Готовый массив с параметрами для запроса
		$options = [
			'text' => $text,
			'chr'  => 'UTF-8',
			'xml'  => $xml_params
		];

		$url      = 'https://typograf.ru/webservice/';
		$response = $http->post($url, $options);
        // Возвращаем ответ
		return $response->body;
	}
}

Чего не хватает в этом методе?

  • В нём можно добавить проверку на код ответа стороннего сервиса$response->code.

  • Также, памятуя о том, что провайдеров типографики может быть несколько, в том числе локальные библиотеки - можно добавить получение из настроек плагина выбранного провайдера и обращаться к его методам: по URL или локально.

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

Заключение

Данный плагин - не самый простой, но и далеко не самый сложный. Его можно рассматривать как модельный организм, изучать и решать свои задачи. Контент-менеджеры, СЕО-специалисты в Joomla имеют довольно много возможностей, которые можно сделать ещё больше.

Например, в виде плагина кнопки редактора можно сделать помощник для подсчета статистики текста: длина, количество слов, тошнота, проверка на уникальность с помощью стороннего сервиса и т.д. Тогда результат работы с текстом из редактора может не возвращаться обратно в редактор, а выводится в некий модуль панели администратора. Или же можно внедрить сервис ИИ прямо в интерфейс Joomla. К слову сказать, для ChatGPT решения в Joomla Extensions Directory уже есть. И даже не одно.

Статья получилась гораздо больше, чем я сам ожидал. Надеюсь она будет полезна широкому кругу читателей, в том числе и тем, кто переходит на Joomla с других движков. С удовольствием приму все замечания и пожелания по улучшению статьи.

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

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

Telegram:

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


  1. Sulpher
    19.06.2023 10:52
    +1

    Большая статья получилась, подробная. И что особенно приятно - не переводная, а авторская. Отличная работа!


    1. sergeytolkachyov Автор
      19.06.2023 10:52

      Спасибо????


  1. b2z
    19.06.2023 10:52

    Спасибо, очень информативно!


  1. andreyuvikov_tver
    19.06.2023 10:52

    очень нужный материал, огонь


  1. gresserg
    19.06.2023 10:52

    Как всегда, очень полезная информация. Спасибо.