feat: add editor scaffolding — plugin manifest, Extension, SQL (3 tables), 3 helpers (Profile, Media, Template)
Universal: Auto Version Bump / Version Bump (push) Successful in 14s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 20s

This commit is contained in:
Jonathan Miller
2026-06-23 11:48:21 -05:00
parent 59b9626de7
commit 6a1687e00a
10 changed files with 722 additions and 0 deletions
@@ -0,0 +1,15 @@
PLG_EDITORS_MOKOSUITEEDITOR="Editors - MokoSuite Editor"
PLG_EDITORS_MOKOSUITEEDITOR_DESC="Advanced WYSIWYG editor built on TinyMCE 7 with CodeMirror 6 source editing, role-based profiles, and integrated media management."
PLG_EDITORS_MOKOSUITEEDITOR_PROFILES="Editor Profiles"
PLG_EDITORS_MOKOSUITEEDITOR_DEFAULT_PROFILE="Default Profile"
PLG_EDITORS_MOKOSUITEEDITOR_ADMIN_TOOLBAR="Admin Toolbar Layout"
PLG_EDITORS_MOKOSUITEEDITOR_AUTHOR_TOOLBAR="Author Toolbar Layout"
PLG_EDITORS_MOKOSUITEEDITOR_BASIC_TOOLBAR="Basic Toolbar Layout"
PLG_EDITORS_MOKOSUITEEDITOR_MEDIA="Media Settings"
PLG_EDITORS_MOKOSUITEEDITOR_MAX_UPLOAD_MB="Max Upload Size (MB)"
PLG_EDITORS_MOKOSUITEEDITOR_AUTO_RESIZE_WIDTH="Auto Resize Width (px)"
PLG_EDITORS_MOKOSUITEEDITOR_WEBP_CONVERSION="Auto Convert to WebP"
PLG_EDITORS_MOKOSUITEEDITOR_WEBP_QUALITY="WebP Quality"
PLG_EDITORS_MOKOSUITEEDITOR_SOURCE_EDITING="Source Editing"
PLG_EDITORS_MOKOSUITEEDITOR_ENABLE_SOURCE_EDITING="Enable Source Editing"
PLG_EDITORS_MOKOSUITEEDITOR_CODEMIRROR_THEME="CodeMirror Theme"
@@ -0,0 +1,2 @@
PLG_EDITORS_MOKOSUITEEDITOR="Editors - MokoSuite Editor"
PLG_EDITORS_MOKOSUITEEDITOR_DESC="Advanced WYSIWYG editor built on TinyMCE 7 with CodeMirror 6 source editing, role-based profiles, and integrated media management."
@@ -0,0 +1,70 @@
<?xml version="1.0" encoding="utf-8"?>
<extension type="plugin" group="editors" method="upgrade">
<name>Editors - MokoSuite Editor</name>
<element>mokosuiteeditor</element>
<author>Moko Consulting</author>
<creationDate>2026-06-23</creationDate>
<copyright>Copyright (C) 2026 Moko Consulting. All rights reserved.</copyright>
<license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>01.00.10</version>
<php_minimum>8.3</php_minimum>
<description>PLG_EDITORS_MOKOSUITEEDITOR_DESC</description>
<namespace path="src">Moko\Plugin\Editors\MokoSuiteEditor</namespace>
<files>
<folder>src</folder>
<folder>services</folder>
<folder>language</folder>
<folder>sql</folder>
</files>
<languages folder="language">
<language tag="en-GB">en-GB/plg_editors_mokosuiteeditor.ini</language>
<language tag="en-GB">en-GB/plg_editors_mokosuiteeditor.sys.ini</language>
</languages>
<install>
<sql>
<file driver="mysql" charset="utf8">sql/install.mysql.sql</file>
</sql>
</install>
<uninstall>
<sql>
<file driver="mysql" charset="utf8">sql/uninstall.mysql.sql</file>
</sql>
</uninstall>
<config>
<fields name="params">
<fieldset name="profiles" label="PLG_EDITORS_MOKOSUITEEDITOR_PROFILES">
<field name="default_profile" type="list" default="admin" label="PLG_EDITORS_MOKOSUITEEDITOR_DEFAULT_PROFILE">
<option value="admin">Admin (full toolbar)</option>
<option value="author">Author (standard toolbar)</option>
<option value="basic">Basic (minimal toolbar)</option>
</field>
<field name="admin_toolbar" type="textarea" rows="3" default="undo redo | blocks fontfamily fontsize | bold italic underline strikethrough | link image media table | align lineheight | numlist bullist indent outdent | emoticons charmap | removeformat | code fullscreen" label="PLG_EDITORS_MOKOSUITEEDITOR_ADMIN_TOOLBAR" />
<field name="author_toolbar" type="textarea" rows="3" default="undo redo | blocks | bold italic underline | link image | align | numlist bullist | removeformat | code" label="PLG_EDITORS_MOKOSUITEEDITOR_AUTHOR_TOOLBAR" />
<field name="basic_toolbar" type="textarea" rows="3" default="undo redo | bold italic underline | link | numlist bullist | removeformat" label="PLG_EDITORS_MOKOSUITEEDITOR_BASIC_TOOLBAR" />
</fieldset>
<fieldset name="media" label="PLG_EDITORS_MOKOSUITEEDITOR_MEDIA">
<field name="max_upload_mb" type="number" default="10" label="PLG_EDITORS_MOKOSUITEEDITOR_MAX_UPLOAD_MB" min="1" max="100" />
<field name="auto_resize_width" type="number" default="1920" label="PLG_EDITORS_MOKOSUITEEDITOR_AUTO_RESIZE_WIDTH" min="0" max="4096" />
<field name="webp_conversion" type="radio" default="1" label="PLG_EDITORS_MOKOSUITEEDITOR_WEBP_CONVERSION" class="btn-group btn-group-yesno">
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
<field name="webp_quality" type="number" default="85" label="PLG_EDITORS_MOKOSUITEEDITOR_WEBP_QUALITY" min="1" max="100" showon="webp_conversion:1" />
</fieldset>
<fieldset name="source_editing" label="PLG_EDITORS_MOKOSUITEEDITOR_SOURCE_EDITING">
<field name="enable_source_editing" type="radio" default="1" label="PLG_EDITORS_MOKOSUITEEDITOR_ENABLE_SOURCE_EDITING" class="btn-group btn-group-yesno">
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
<field name="codemirror_theme" type="list" default="one-dark" label="PLG_EDITORS_MOKOSUITEEDITOR_CODEMIRROR_THEME" showon="enable_source_editing:1">
<option value="one-dark">One Dark</option>
<option value="one-light">One Light</option>
<option value="basic-dark">Basic Dark</option>
<option value="basic-light">Basic Light</option>
</field>
</fieldset>
</fields>
</config>
</extension>
@@ -0,0 +1,34 @@
<?php
/**
* @package MokoSuite
* @subpackage plg_editors_mokosuiteeditor
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GPL-3.0-or-later
*/
defined('_JEXEC') or die;
use Joomla\CMS\Extension\PluginInterface;
use Joomla\CMS\Factory;
use Joomla\CMS\Plugin\PluginHelper;
use Joomla\DI\Container;
use Joomla\DI\ServiceProviderInterface;
use Joomla\Event\DispatcherInterface;
use Moko\Plugin\Editors\MokoSuiteEditor\Extension\MokoSuiteEditor;
return new class implements ServiceProviderInterface
{
public function register(Container $container): void
{
$container->set(
PluginInterface::class,
function (Container $container) {
$dispatcher = $container->get(DispatcherInterface::class);
$plugin = new MokoSuiteEditor($dispatcher, (array) PluginHelper::getPlugin('editors', 'mokosuiteeditor'));
$plugin->setApplication(Factory::getApplication());
return $plugin;
}
);
}
};
@@ -0,0 +1,56 @@
--
-- MokoSuite Editor Tables
--
CREATE TABLE IF NOT EXISTS `#__mokosuiteeditor_profiles` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`title` VARCHAR(255) NOT NULL,
`user_groups` JSON DEFAULT NULL,
`toolbar_config` JSON DEFAULT NULL,
`allowed_tags` TEXT DEFAULT NULL,
`source_editing` TINYINT NOT NULL DEFAULT 1,
`media_upload` TINYINT NOT NULL DEFAULT 1,
`max_upload_mb` INT UNSIGNED NOT NULL DEFAULT 10,
`auto_resize_width` INT UNSIGNED NOT NULL DEFAULT 1920,
`webp_conversion` TINYINT NOT NULL DEFAULT 1,
`published` TINYINT NOT NULL DEFAULT 1,
`ordering` INT NOT NULL DEFAULT 0,
`created` DATETIME NOT NULL,
PRIMARY KEY (`id`),
KEY `idx_published` (`published`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS `#__mokosuiteeditor_templates` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`title` VARCHAR(255) NOT NULL,
`content` TEXT NOT NULL,
`category` VARCHAR(100) NOT NULL DEFAULT '',
`thumbnail` VARCHAR(500) NOT NULL DEFAULT '',
`published` TINYINT NOT NULL DEFAULT 1,
`ordering` INT NOT NULL DEFAULT 0,
`created` DATETIME NOT NULL,
PRIMARY KEY (`id`),
KEY `idx_category` (`category`),
KEY `idx_published` (`published`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS `#__mokosuiteeditor_media_presets` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`title` VARCHAR(255) NOT NULL,
`max_width` INT UNSIGNED NOT NULL DEFAULT 1920,
`max_height` INT UNSIGNED NOT NULL DEFAULT 1080,
`quality` INT UNSIGNED NOT NULL DEFAULT 85,
`format` ENUM('webp','jpg','png') NOT NULL DEFAULT 'webp',
`created` DATETIME NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
--
-- Default profiles
--
INSERT INTO `#__mokosuiteeditor_profiles` (`title`, `user_groups`, `toolbar_config`, `allowed_tags`, `source_editing`, `media_upload`, `max_upload_mb`, `auto_resize_width`, `webp_conversion`, `published`, `ordering`, `created`)
VALUES
('Admin', '[8]', '{"toolbar": "undo redo | blocks fontfamily fontsize | bold italic underline strikethrough | link image media table | align lineheight | numlist bullist indent outdent | emoticons charmap | removeformat | code fullscreen"}', NULL, 1, 1, 50, 1920, 1, 1, 1, NOW()),
('Author', '[3,4]', '{"toolbar": "undo redo | blocks | bold italic underline | link image | align | numlist bullist | removeformat | code"}', '<p><a><strong><em><ul><ol><li><h2><h3><h4><img><table><tr><td><th><blockquote>', 1, 1, 10, 1920, 1, 1, 2, NOW()),
('Basic', '[2]', '{"toolbar": "undo redo | bold italic underline | link | numlist bullist | removeformat"}', '<p><a><strong><em><ul><ol><li>', 0, 0, 5, 1024, 1, 1, 3, NOW());
@@ -0,0 +1,7 @@
--
-- MokoSuite Editor — Uninstall
--
DROP TABLE IF EXISTS `#__mokosuiteeditor_media_presets`;
DROP TABLE IF EXISTS `#__mokosuiteeditor_templates`;
DROP TABLE IF EXISTS `#__mokosuiteeditor_profiles`;
@@ -0,0 +1,99 @@
<?php
/**
* @package MokoSuite
* @subpackage plg_editors_mokosuiteeditor
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GPL-3.0-or-later
*/
namespace Moko\Plugin\Editors\MokoSuiteEditor\Extension;
defined('_JEXEC') or die;
use Joomla\CMS\Editor\EditorProviderInterface;
use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\Event\SubscriberInterface;
/**
* MokoSuite Editor plugin.
*
* TinyMCE 7 based WYSIWYG editor with CodeMirror 6 source editing,
* role-based profiles, and integrated media management.
*
* @since 01.00.00
*/
class MokoSuiteEditor extends CMSPlugin implements SubscriberInterface, EditorProviderInterface
{
protected $autoloadLanguage = true;
/**
* Returns an array of events this subscriber will listen to.
*
* @return array
*/
public static function getSubscribedEvents(): array
{
return [
'onEditorSetup' => 'onEditorSetup',
];
}
/**
* Handle the editor setup event.
*
* @param \Joomla\Event\Event $event The event object.
*
* @return void
*
* @since 01.00.00
*/
public function onEditorSetup(\Joomla\Event\Event $event): void
{
// Register this editor with Joomla's editor manager
}
/**
* Get the content of the editor area.
*
* @param string $editorId The editor instance identifier.
*
* @return string JavaScript to retrieve editor content.
*
* @since 01.00.00
*/
public function getContent(string $editorId): string
{
return "tinymce.get('{$editorId}')?.getContent() ?? ''";
}
/**
* Set the content of the editor area.
*
* @param string $editorId The editor instance identifier.
* @param string $content The content to set.
*
* @return string JavaScript to set editor content.
*
* @since 01.00.00
*/
public function setContent(string $editorId, string $content): string
{
$escapedContent = addslashes($content);
return "tinymce.get('{$editorId}')?.setContent('{$escapedContent}')";
}
/**
* Get the editor extended buttons (XTD).
*
* @param string $editorId The editor instance identifier.
*
* @return array Array of button objects.
*
* @since 01.00.00
*/
public function getButtons(string $editorId): array
{
return [];
}
}
@@ -0,0 +1,251 @@
<?php
/**
* @package MokoSuite
* @subpackage plg_editors_mokosuiteeditor
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GPL-3.0-or-later
*/
namespace Moko\Plugin\Editors\MokoSuiteEditor\Helper;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\Database\DatabaseInterface;
/**
* Media upload, resize, and WebP conversion for the editor.
*/
class MediaHelper
{
/**
* Upload a file respecting the profile's media constraints.
*
* @param array $file The $_FILES entry (name, tmp_name, size, type, error).
* @param int $profileId The editor profile ID for size/format limits.
*
* @return array ['success' => bool, 'path' => string, 'error' => string]
*
* @since 01.00.00
*/
public static function upload(array $file, int $profileId): array
{
$db = Factory::getContainer()->get(DatabaseInterface::class);
$query = $db->getQuery(true)
->select('*')
->from($db->quoteName('#__mokosuiteeditor_profiles'))
->where($db->quoteName('id') . ' = ' . (int) $profileId);
$profile = $db->setQuery($query)->loadObject();
if (!$profile || !$profile->media_upload) {
return ['success' => false, 'path' => '', 'error' => 'Media upload not permitted for this profile.'];
}
$maxBytes = ($profile->max_upload_mb ?: 10) * 1024 * 1024;
if ($file['error'] !== UPLOAD_ERR_OK) {
return ['success' => false, 'path' => '', 'error' => 'Upload error code: ' . $file['error']];
}
if ($file['size'] > $maxBytes) {
return ['success' => false, 'path' => '', 'error' => 'File exceeds maximum upload size of ' . $profile->max_upload_mb . ' MB.'];
}
$uploadDir = JPATH_ROOT . '/images/mokosuiteeditor/' . date('Y/m');
$extension = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
$allowed = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg'];
if (!in_array($extension, $allowed, true)) {
return ['success' => false, 'path' => '', 'error' => 'File type not allowed.'];
}
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0755, true);
}
$filename = uniqid('mse_', true) . '.' . $extension;
$destPath = $uploadDir . '/' . $filename;
if (!move_uploaded_file($file['tmp_name'], $destPath)) {
return ['success' => false, 'path' => '', 'error' => 'Failed to move uploaded file.'];
}
// Auto-resize if configured
if ($profile->auto_resize_width > 0 && in_array($extension, ['jpg', 'jpeg', 'png', 'webp'], true)) {
self::resize($destPath, (int) $profile->auto_resize_width);
}
// WebP conversion if enabled
if ($profile->webp_conversion && in_array($extension, ['jpg', 'jpeg', 'png'], true)) {
$webpPath = self::convertToWebP($destPath);
if ($webpPath) {
$destPath = $webpPath;
}
}
$relativePath = str_replace(JPATH_ROOT . '/', '', $destPath);
return ['success' => true, 'path' => $relativePath, 'error' => ''];
}
/**
* Resize an image to a maximum width, preserving aspect ratio.
*
* @param string $path Absolute path to the image file.
* @param int $maxWidth Maximum width in pixels.
*
* @return bool True on success.
*
* @since 01.00.00
*/
public static function resize(string $path, int $maxWidth): bool
{
if (!file_exists($path) || $maxWidth <= 0) {
return false;
}
$info = getimagesize($path);
if ($info === false) {
return false;
}
[$width, $height] = $info;
if ($width <= $maxWidth) {
return true;
}
$ratio = $maxWidth / $width;
$newHeight = (int) round($height * $ratio);
$source = match ($info['mime']) {
'image/jpeg' => imagecreatefromjpeg($path),
'image/png' => imagecreatefrompng($path),
'image/webp' => imagecreatefromwebp($path),
default => null,
};
if (!$source) {
return false;
}
$resized = imagecreatetruecolor($maxWidth, $newHeight);
imagecopyresampled($resized, $source, 0, 0, 0, 0, $maxWidth, $newHeight, $width, $height);
$result = match ($info['mime']) {
'image/jpeg' => imagejpeg($resized, $path, 90),
'image/png' => imagepng($resized, $path, 6),
'image/webp' => imagewebp($resized, $path, 85),
default => false,
};
imagedestroy($source);
imagedestroy($resized);
return $result;
}
/**
* Convert an image to WebP format.
*
* @param string $path Absolute path to the source image.
* @param int $quality WebP quality (1-100).
*
* @return string|null Path to the WebP file, or null on failure.
*
* @since 01.00.00
*/
public static function convertToWebP(string $path, int $quality = 85): ?string
{
if (!file_exists($path) || !function_exists('imagewebp')) {
return null;
}
$info = getimagesize($path);
if ($info === false) {
return null;
}
$source = match ($info['mime']) {
'image/jpeg' => imagecreatefromjpeg($path),
'image/png' => imagecreatefrompng($path),
default => null,
};
if (!$source) {
return null;
}
$webpPath = preg_replace('/\.(jpe?g|png)$/i', '.webp', $path);
if (imagewebp($source, $webpPath, $quality)) {
imagedestroy($source);
// Remove original file
if ($webpPath !== $path) {
unlink($path);
}
return $webpPath;
}
imagedestroy($source);
return null;
}
/**
* Get a listing of media files for the browser.
*
* @param string $path Relative path under images/mokosuiteeditor/.
*
* @return array Array of ['name', 'path', 'size', 'type', 'modified'] entries.
*
* @since 01.00.00
*/
public static function getMediaBrowser(string $path = ''): array
{
$basePath = JPATH_ROOT . '/images/mokosuiteeditor';
$fullPath = $basePath . ($path ? '/' . ltrim($path, '/') : '');
// Prevent directory traversal
$realBase = realpath($basePath);
$realFull = realpath($fullPath);
if ($realFull === false || !str_starts_with($realFull, $realBase)) {
return [];
}
$items = [];
if (!is_dir($realFull)) {
return $items;
}
$entries = scandir($realFull);
foreach ($entries as $entry) {
if ($entry === '.' || $entry === '..') {
continue;
}
$entryPath = $realFull . '/' . $entry;
$relative = str_replace(JPATH_ROOT . '/', '', $entryPath);
$items[] = [
'name' => $entry,
'path' => $relative,
'size' => is_file($entryPath) ? filesize($entryPath) : 0,
'type' => is_dir($entryPath) ? 'folder' : mime_content_type($entryPath),
'modified' => date('Y-m-d H:i:s', filemtime($entryPath)),
];
}
return $items;
}
}
@@ -0,0 +1,102 @@
<?php
/**
* @package MokoSuite
* @subpackage plg_editors_mokosuiteeditor
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GPL-3.0-or-later
*/
namespace Moko\Plugin\Editors\MokoSuiteEditor\Helper;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\Database\DatabaseInterface;
/**
* Editor profile management — match user groups to editor profiles, retrieve toolbar configs.
*/
class ProfileHelper
{
/**
* Get the most specific editor profile for a given user.
*
* Matches the user's Joomla groups against the profile user_groups JSON column.
* Returns the first published match ordered by `ordering ASC`.
*
* @param int $userId The Joomla user ID.
*
* @return object|null Profile row or null if none matched.
*
* @since 01.00.00
*/
public static function getForUser(int $userId): ?object
{
$db = Factory::getContainer()->get(DatabaseInterface::class);
$user = Factory::getApplication()->getIdentity();
$groups = $user->getAuthorisedGroups();
$query = $db->getQuery(true)
->select('*')
->from($db->quoteName('#__mokosuiteeditor_profiles'))
->where($db->quoteName('published') . ' = 1')
->order('ordering ASC');
$profiles = $db->setQuery($query)->loadObjectList();
foreach ($profiles as $profile) {
$profileGroups = json_decode($profile->user_groups, true) ?: [];
if (array_intersect($groups, $profileGroups)) {
return $profile;
}
}
// Fallback: return first published profile
return $profiles[0] ?? null;
}
/**
* Get the toolbar configuration for a specific profile.
*
* @param int $profileId The profile ID.
*
* @return array Decoded toolbar_config or empty array.
*
* @since 01.00.00
*/
public static function getToolbarConfig(int $profileId): array
{
$db = Factory::getContainer()->get(DatabaseInterface::class);
$query = $db->getQuery(true)
->select($db->quoteName('toolbar_config'))
->from($db->quoteName('#__mokosuiteeditor_profiles'))
->where($db->quoteName('id') . ' = ' . (int) $profileId);
$result = $db->setQuery($query)->loadResult();
return $result ? (json_decode($result, true) ?: []) : [];
}
/**
* Get all published profiles.
*
* @return array Array of profile objects.
*
* @since 01.00.00
*/
public static function getAllProfiles(): array
{
$db = Factory::getContainer()->get(DatabaseInterface::class);
$query = $db->getQuery(true)
->select('*')
->from($db->quoteName('#__mokosuiteeditor_profiles'))
->where($db->quoteName('published') . ' = 1')
->order('ordering ASC');
return $db->setQuery($query)->loadObjectList();
}
}
@@ -0,0 +1,86 @@
<?php
/**
* @package MokoSuite
* @subpackage plg_editors_mokosuiteeditor
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GPL-3.0-or-later
*/
namespace Moko\Plugin\Editors\MokoSuiteEditor\Helper;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\Database\DatabaseInterface;
/**
* Content template management — pre-built HTML snippets for insertion into the editor.
*/
class TemplateHelper
{
/**
* Get all published templates, optionally filtered by category.
*
* @param string|null $category Category slug filter, or null for all.
*
* @return array Array of template objects.
*
* @since 01.00.00
*/
public static function getAll(?string $category = null): array
{
$db = Factory::getContainer()->get(DatabaseInterface::class);
$query = $db->getQuery(true)
->select('*')
->from($db->quoteName('#__mokosuiteeditor_templates'))
->where($db->quoteName('published') . ' = 1')
->order('ordering ASC');
if ($category !== null && $category !== '') {
$query->where($db->quoteName('category') . ' = ' . $db->quote($category));
}
return $db->setQuery($query)->loadObjectList();
}
/**
* Get a single template by ID.
*
* @param int $id The template ID.
*
* @return object|null Template row or null.
*
* @since 01.00.00
*/
public static function getById(int $id): ?object
{
$db = Factory::getContainer()->get(DatabaseInterface::class);
$query = $db->getQuery(true)
->select('*')
->from($db->quoteName('#__mokosuiteeditor_templates'))
->where($db->quoteName('id') . ' = ' . (int) $id)
->where($db->quoteName('published') . ' = 1');
$result = $db->setQuery($query)->loadObject();
return $result ?: null;
}
/**
* Get a template's HTML content ready for insertion into the editor.
*
* @param int $templateId The template ID.
*
* @return string The template HTML content, or empty string if not found.
*
* @since 01.00.00
*/
public static function insertTemplate(int $templateId): string
{
$template = self::getById($templateId);
return $template ? $template->content : '';
}
}