diff --git a/administrator/language/en-GB/plg_media-action_crop.ini b/administrator/language/en-GB/plg_media-action_crop.ini index 57c9846ad67a0..8dcff92fcd22a 100644 --- a/administrator/language/en-GB/plg_media-action_crop.ini +++ b/administrator/language/en-GB/plg_media-action_crop.ini @@ -4,6 +4,8 @@ ; Note : All ini files need to be saved as UTF-8 PLG_MEDIA-ACTION_CROP="Media Action - Crop" +PLG_MEDIA-ACTION_CROP_ASPECT_RATIOS_DESC="Configure custom aspect ratios for the crop tool. Each ratio needs a label (e.g. '16:9'), a value (e.g. '1.7777777777777777'), and optionally a group (landscape/portrait)." +PLG_MEDIA-ACTION_CROP_ASPECT_RATIOS_LABEL="Aspect Ratios" PLG_MEDIA-ACTION_CROP_LABEL="Crop" PLG_MEDIA-ACTION_CROP_PARAM_ASPECT="Aspect Ratio" PLG_MEDIA-ACTION_CROP_PARAM_DEFAULT_RATIO="Default aspect ratio" @@ -15,4 +17,19 @@ PLG_MEDIA-ACTION_CROP_PARAM_WIDTH="Width" PLG_MEDIA-ACTION_CROP_PARAM_X="X-Axis" PLG_MEDIA-ACTION_CROP_PARAM_Y="Y-Axis" PLG_MEDIA-ACTION_CROP_QUALITY="Quality" +PLG_MEDIA-ACTION_CROP_RATIO_CALCULATOR_COPY_DONE_LABEL="Copied" +PLG_MEDIA-ACTION_CROP_RATIO_CALCULATOR_COPY_LABEL="Copy" +PLG_MEDIA-ACTION_CROP_RATIO_CALCULATOR_DESC="Use this calculator to determine the decimal value for your aspect ratios. Enter width and height, then copy the calculated value." +PLG_MEDIA-ACTION_CROP_RATIO_CALCULATOR_HEIGHT_LABEL="Height" +PLG_MEDIA-ACTION_CROP_RATIO_CALCULATOR_LABEL="Aspect Ratio Calculator" +PLG_MEDIA-ACTION_CROP_RATIO_CALCULATOR_OUTPUT_LABEL="Calculated value:" +PLG_MEDIA-ACTION_CROP_RATIO_CALCULATOR_OUTPUT_TITLE_LABEL="Click to copy" +PLG_MEDIA-ACTION_CROP_RATIO_CALCULATOR_WIDTH_LABEL="Width" +PLG_MEDIA-ACTION_CROP_RATIO_GROUP="Group" +PLG_MEDIA-ACTION_CROP_RATIO_GROUP_DESC="Optionally group this ratio under landscape or portrait" +PLG_MEDIA-ACTION_CROP_RATIO_GROUP_NONE="No group" +PLG_MEDIA-ACTION_CROP_RATIO_LABEL="Label" +PLG_MEDIA-ACTION_CROP_RATIO_LABEL_DESC="The label to display for this ratio (e.g. '16:9', '4:3', etc.)" +PLG_MEDIA-ACTION_CROP_RATIO_VALUE="Value" +PLG_MEDIA-ACTION_CROP_RATIO_VALUE_DESC="The calculated aspect ratio value (width/height). For example, 16:9 = 1.7777777777777777, 4:3 = 1.3333333333333333, 1:1 = 1" PLG_MEDIA-ACTION_CROP_XML_DESCRIPTION="Adds crop functionality for images." diff --git a/build/media_source/plg_media-action_crop/joomla.asset.json b/build/media_source/plg_media-action_crop/joomla.asset.json new file mode 100644 index 0000000000000..1ce229b3c33b7 --- /dev/null +++ b/build/media_source/plg_media-action_crop/joomla.asset.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://developer.joomla.org/schemas/json-schema/web_assets.json", + "name": "plg_media-action_crop", + "version": "6.1.0", + "description": "Web Assets for Media Action Crop Plugin", + "license": "GPL-2.0-or-later", + "assets": [ + { + "name": "plg_media-action_crop.calculator", + "type": "script", + "uri": "plg_media-action_crop/calculator.js", + "attributes": { + "defer": true + } + } + ] +} diff --git a/build/media_source/plg_media-action_crop/js/calculator.es6.js b/build/media_source/plg_media-action_crop/js/calculator.es6.js new file mode 100644 index 0000000000000..040588ed2afe8 --- /dev/null +++ b/build/media_source/plg_media-action_crop/js/calculator.es6.js @@ -0,0 +1,84 @@ +/** + * @copyright (C) 2025 Open Source Matters, Inc. + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +/** + * Aspect Ratio Calculator Module + */ +(() => { + const initCalculator = () => { + const widthInput = document.getElementById('calc-width'); + const heightInput = document.getElementById('calc-height'); + const output = document.getElementById('calc-output'); + const copyBtn = document.getElementById('copy-calc-value'); + + if (!widthInput || !heightInput || !output || !copyBtn) { + return; + } + + /** + * Calculate aspect ratio from width and height + */ + const calculateRatio = () => { + const width = parseFloat(widthInput.value); + const height = parseFloat(heightInput.value); + + if (width > 0 && height > 0) { + const ratio = width / height; + output.textContent = ratio.toString(); + } else { + output.textContent = '0'; + } + }; + + /** + * Show copy feedback on button + */ + const showCopyFeedback = () => { + const originalHTML = copyBtn.innerHTML; + const copiedIcon = copyBtn.getAttribute('data-copied-icon'); + const copiedText = copyBtn.getAttribute('data-copied-text'); + + copyBtn.innerHTML = Joomla.sanitizeHtml(copiedIcon + copiedText); + copyBtn.disabled = true; + + setTimeout(() => { + copyBtn.innerHTML = originalHTML; + copyBtn.disabled = false; + }, 2000); + }; + + /** + * Copy the calculated value to clipboard + */ + const copyToClipboard = () => { + const value = output.textContent; + + navigator.clipboard.writeText(value) + .then(() => { + showCopyFeedback(); + }) + .catch((err) => { + console.error('Clipboard copy failed:', err); + }); + }; + + // Attach event listeners + widthInput.addEventListener('input', calculateRatio); + heightInput.addEventListener('input', calculateRatio); + copyBtn.addEventListener('click', copyToClipboard); + output.addEventListener('click', copyToClipboard); + + // Calculate initial ratio + calculateRatio(); + }; + + // Initialize when DOM is ready + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initCalculator); + } else { + // DOM already loaded, run with slight delay for form rendering + setTimeout(initCalculator, 100); + } +})(); diff --git a/plugins/media-action/crop/crop.xml b/plugins/media-action/crop/crop.xml index 4b914e679a0aa..5826636bc6e84 100644 --- a/plugins/media-action/crop/crop.xml +++ b/plugins/media-action/crop/crop.xml @@ -12,6 +12,7 @@ Joomla\Plugin\MediaAction\Crop form + layouts services src @@ -19,4 +20,57 @@ language/en-GB/plg_media-action_crop.ini language/en-GB/plg_media-action_crop.sys.ini + + +
+ + +
+ + + + + + + + + +
+
+
diff --git a/plugins/media-action/crop/form/crop.xml b/plugins/media-action/crop/form/crop.xml index 696bde1336c20..b4219935c5bf6 100644 --- a/plugins/media-action/crop/form/crop.xml +++ b/plugins/media-action/crop/form/crop.xml @@ -78,7 +78,7 @@ - + diff --git a/plugins/media-action/crop/layouts/field/calculator.php b/plugins/media-action/crop/layouts/field/calculator.php new file mode 100644 index 0000000000000..10df36b7ebaca --- /dev/null +++ b/plugins/media-action/crop/layouts/field/calculator.php @@ -0,0 +1,74 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +\defined('_JEXEC') or die; + +use Joomla\CMS\Factory; +use Joomla\CMS\Language\Text; +use Joomla\CMS\Layout\LayoutHelper; + +extract($displayData); + +/** + * Layout variables + * ----------------- + * @var string $id The field ID + */ + +// Load the calculator JavaScript +$wa = Factory::getApplication()->getDocument()->getWebAssetManager(); +$wa->getRegistry()->addExtensionRegistryFile('plg_media-action_crop'); +$wa->useScript('plg_media-action_crop.calculator'); + +// Prepare data for JavaScript +$copiedIcon = LayoutHelper::render('joomla.icon.iconclass', ['icon' => 'icon-check']); +$copiedText = Text::_('PLG_MEDIA-ACTION_CROP_RATIO_CALCULATOR_COPY_DONE_LABEL'); +?> +
+
+

+ 'icon-wand']); ?> + +

+
+
+

+ +

+
+
+ + +
+
+ + +
+
+ +
+
diff --git a/plugins/media-action/crop/src/Extension/Crop.php b/plugins/media-action/crop/src/Extension/Crop.php index 8f56d62876889..cc437042281ca 100644 --- a/plugins/media-action/crop/src/Extension/Crop.php +++ b/plugins/media-action/crop/src/Extension/Crop.php @@ -11,6 +11,7 @@ namespace Joomla\Plugin\MediaAction\Crop\Extension; use Joomla\CMS\Application\CMSWebApplicationInterface; +use Joomla\CMS\Form\Form; use Joomla\Component\Media\Administrator\Plugin\MediaActionPlugin; use Joomla\Event\SubscriberInterface; @@ -25,6 +26,161 @@ */ final class Crop extends MediaActionPlugin implements SubscriberInterface { + /** + * The form event. Load additional parameters when available into the field form. + * Override to dynamically inject aspect ratios from plugin settings. + * + * @param Form $form The form + * @param \stdClass $data The data + * + * @return void + * + * @since __DEPLOY_VERSION__ + */ + public function onContentPrepareForm(Form $form, $data): void + { + // Check if it is the right form + if ($form->getName() !== 'com_media.file') { + return; + } + + $this->loadCss(); + $this->loadJs(); + + // The file with the params for the edit view + $paramsFile = JPATH_PLUGINS . '/media-action/' . $this->_name . '/form/' . $this->_name . '.xml'; + + // When the file exists, load it into the form + if (file_exists($paramsFile)) { + $form->loadFile($paramsFile); + + // Get the aspect ratios from plugin parameters + $aspectRatios = $this->params->get('aspect_ratios'); + + // Convert stdClass to array if needed + if (\is_object($aspectRatios)) { + $aspectRatios = json_decode(json_encode($aspectRatios), true); + } + + // If we have custom aspect ratios, modify the form field + if (!empty($aspectRatios) && \is_array($aspectRatios)) { + $this->injectAspectRatios($form, $aspectRatios); + } + // If no custom ratios configured, the form will use the hard-coded values from form/crop.xml + // This is intentional - allows fallback to defaults + } + } + + /** + * Inject custom aspect ratios into the crop form + * + * @param Form $form The form object + * @param array $aspectRatios The aspect ratios from plugin settings + * + * @return void + * + * @since __DEPLOY_VERSION__ + */ + protected function injectAspectRatios(Form $form, array $aspectRatios): void + { + // Get the aspectRatio field (try without group first, then with 'crop' group) + $field = $form->getField('aspectRatio'); + + if (!$field) { + $field = $form->getField('aspectRatio', 'crop'); + } + + if (!$field) { + return; + } + + // Build new XML for the field with custom options + $xml = new \SimpleXMLElement(''); + $xml->addAttribute('name', 'aspectRatio'); + $xml->addAttribute('type', 'groupedlist'); + $xml->addAttribute('label', 'PLG_MEDIA-ACTION_CROP_PARAM_ASPECT'); + $xml->addAttribute('hiddenLabel', 'true'); + $xml->addAttribute('class', 'crop-aspect-ratio-options'); + $xml->addAttribute('default', '1.111'); + + // Add default options + $option = $xml->addChild('option', 'PLG_MEDIA-ACTION_CROP_PARAM_DEFAULT_RATIO'); + $option->addAttribute('class', 'crop-aspect-ratio-option'); + $option->addAttribute('value', '1.111'); + + $option = $xml->addChild('option', 'PLG_MEDIA-ACTION_CROP_PARAM_NO_RATIO'); + $option->addAttribute('class', 'crop-aspect-ratio-option'); + $option->addAttribute('value', ''); + + // Group ratios by landscape/portrait + $grouped = [ + '' => [], + 'landscape' => [], + 'portrait' => [], + ]; + + foreach ($aspectRatios as $ratio) { + // Handle both array and object formats + $label = \is_array($ratio) ? ($ratio['label'] ?? '') : ($ratio->label ?? ''); + $value = \is_array($ratio) ? ($ratio['value'] ?? '') : ($ratio->value ?? ''); + $group = \is_array($ratio) ? ($ratio['group'] ?? '') : ($ratio->group ?? ''); + + // Normalize group to lowercase + $group = strtolower(trim($group)); + + if (empty($label) || $value === '' || $value === null) { + continue; + } + + if (!isset($grouped[$group])) { + $grouped[$group] = []; + } + + $grouped[$group][] = [ + 'label' => $label, + 'value' => $value, + 'group' => $group, + ]; + } + + // Add ungrouped ratios + foreach ($grouped[''] as $ratio) { + $option = $xml->addChild('option', htmlspecialchars($ratio['label'], ENT_XML1, 'UTF-8')); + $option->addAttribute('class', 'crop-aspect-ratio-option'); + $option->addAttribute('value', $ratio['value']); + } + + // Add landscape group + if (!empty($grouped['landscape'])) { + $group = $xml->addChild('group'); + $group->addAttribute('label', 'PLG_MEDIA-ACTION_CROP_PARAM_LANDSCAPE'); + + foreach ($grouped['landscape'] as $ratio) { + $option = $group->addChild('option', htmlspecialchars($ratio['label'], ENT_XML1, 'UTF-8')); + $option->addAttribute('class', 'crop-aspect-ratio-option'); + $option->addAttribute('value', $ratio['value']); + } + } + + // Add portrait group + if (!empty($grouped['portrait'])) { + $group = $xml->addChild('group'); + $group->addAttribute('label', 'PLG_MEDIA-ACTION_CROP_PARAM_PORTRAIT'); + + foreach ($grouped['portrait'] as $ratio) { + $option = $group->addChild('option', htmlspecialchars($ratio['label'], ENT_XML1, 'UTF-8')); + $option->addAttribute('class', 'crop-aspect-ratio-option'); + $option->addAttribute('value', $ratio['value']); + } + } + + // Replace the field in the form + // Try setting in default group first, then 'crop' group + if (!$form->setField($xml, null, true)) { + $form->setField($xml, 'crop', true); + } + } + /** * Load the javascript files of the plugin. * @@ -32,7 +188,7 @@ final class Crop extends MediaActionPlugin implements SubscriberInterface * * @since 4.0.0 */ - protected function loadJs() + protected function loadJs(): void { parent::loadJs(); @@ -50,7 +206,7 @@ protected function loadJs() * * @since 4.0.0 */ - protected function loadCss() + protected function loadCss(): void { parent::loadCss(); diff --git a/plugins/media-action/crop/src/Field/CalculatorField.php b/plugins/media-action/crop/src/Field/CalculatorField.php new file mode 100644 index 0000000000000..d755705c3a577 --- /dev/null +++ b/plugins/media-action/crop/src/Field/CalculatorField.php @@ -0,0 +1,102 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\Plugin\MediaAction\Crop\Field; + +use Joomla\CMS\Factory; +use Joomla\CMS\Form\FormField; + +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects + +/** + * Aspect Ratio Calculator Field + * + * @since __DEPLOY_VERSION__ + */ +class CalculatorField extends FormField +{ + /** + * The form field type. + * + * @var string + * @since __DEPLOY_VERSION__ + */ + protected $type = 'Calculator'; + + /** + * The layout path to use for rendering the field. + * + * @var string + * @since __DEPLOY_VERSION__ + */ + protected $layout = 'field.calculator'; + + /** + * Method to get the field label markup. + * + * @return string The field label markup. + * + * @since __DEPLOY_VERSION__ + */ + protected function getLabel(): string + { + return ''; + } + + /** + * Method to get the field input markup. + * + * @return string The field input markup. + * + * @since __DEPLOY_VERSION__ + */ + protected function getInput(): string + { + return $this->getRenderer($this->layout)->render($this->getLayoutData()); + } + + /** + * Get the layouts paths + * + * @return array An array of layout paths + * + * @since __DEPLOY_VERSION__ + */ + protected function getLayoutPaths(): array + { + $template = Factory::getApplication()->getTemplate(); + + return [ + JPATH_ADMINISTRATOR . '/templates/' . $template . '/html/layouts/plugins/media-action/crop', + JPATH_PLUGINS . '/media-action/crop/layouts', + JPATH_SITE . '/layouts', + ]; + } + + /** + * Method to get the data to be passed to the layout for rendering. + * + * @return array The layout data. + * + * @since __DEPLOY_VERSION__ + */ + protected function getLayoutData(): array + { + $data = parent::getLayoutData(); + + $extraData = [ + 'id' => $this->id, + ]; + + return array_merge($data, $extraData); + } +}