<?php /** * @package Regular Labs Library * @version 25.7.12430 * * @author Peter van Westen <[email protected]> * @link https://regularlabs.com * @copyright Copyright © 2025 Regular Labs All Rights Reserved * @license GNU General Public License version 2 or later */ namespace RegularLabs\Library\Form; defined('_JEXEC') or die; use DateTimeZone; use Joomla\CMS\Application\CMSApplicationInterface as JCMSApplicationInterface; use Joomla\CMS\Factory as JFactory; use Joomla\CMS\Form\Form as JForm; use Joomla\CMS\Form\FormField as JFormField; use Joomla\CMS\Form\FormHelper as JFormHelper; use Joomla\CMS\HTML\HTMLHelper as JHtml; use Joomla\CMS\Language\Text as JText; use Joomla\Database\DatabaseDriver as JDatabaseDriver; use Joomla\Registry\Registry; use Joomla\Registry\Registry as JRegistry; use ReflectionClass; use RegularLabs\Library\Document as RL_Document; use RegularLabs\Library\Parameters as RL_Parameters; use RegularLabs\Library\RegEx as RL_RegEx; use RegularLabs\Library\StringHelper as RL_String; use SimpleXMLElement; use function explode; use function is_array; class FormField extends JFormField { public JCMSApplicationInterface $app; /* @var object|JRegistry $attributes */ public $attributes; public bool $collapse_children = \false; public JDatabaseDriver $db; public bool $is_select_list = \false; public null|int $max_list_count = null; public $params; public array $parent_request = []; public bool $use_ajax = \false; public bool $use_tree_select = \false; /** * @param JForm $form */ public function __construct($form = null) { $this->type ??= $this->getShortFieldName(); parent::__construct($form); $this->db = JFactory::getDbo(); $this->app = JFactory::getApplication(); $params = RL_Parameters::getPlugin('regularlabs'); $this->max_list_count = $this->max_list_count ?? $params->max_list_count; RL_Document::style('regularlabs.admin-form'); } /** * Get a value from the field params */ public function get(string $key, mixed $default = ''): mixed { $value = $default; if (isset($this->params[$key]) && (string) $this->params[$key] != '') { $value = (string) $this->params[$key]; } return $this->sanitizeValue($value); } public function getAjaxRaw(JRegistry|\RegularLabs\Library\Form\FormField $attributes): string { return $this->selectListForAjax($attributes); } public function getControlGroupEnd(): string { return '</div></div>'; } public function getControlGroupStart(): string { return '<div class="control-group"><div class="control-label">'; } /** * Return a list option using the custom prepare methods */ public function getOptionByListItem(object $item, array $extras = [], int $levelOffset = 0, string $key = 'id'): object { $name = \RegularLabs\Library\Form\Form::getNameWithExtras($item, $extras); $option = JHtml::_('select.option', $item->{$key}, $name, 'value', 'text', 0); if (isset($item->level)) { $option->level = $item->level + $levelOffset; } return $option; } /** * Return an array of options using the custom prepare methods */ public function getOptionsByList(array $list, array $extras = [], int $levelOffset = 0, string $key = 'id'): array { $options = []; foreach ($list as $id => $item) { $options[$id] = $this->getOptionByListItem($item, $extras, $levelOffset, $key); } return $options; } /** * Method to post-process a field value. * * @param mixed $value The optional value to use as the default for the field. * @param string $group The optional dot-separated form group path on which to find the field. * @param ?Registry $input An optional Registry object with the entire data set to filter * against the entire form. * * @return mixed The processed value. */ public function postProcess($value, $group = null, ?Registry $input = null) { if (!$this->multiple) { return $value; } if (!is_array($value)) { $value = [$value]; } if (count($value) == 1) { $value = explode(',', $value[0]); } return $value; } /** * Prepare the option string, handling language strings */ public function prepareText(?string $string = ''): string { $string = trim((string) $string); if ($string == '') { return ''; } $string = JText::_($string); $string = $this->replaceDateTags($string); $string = $this->fixLanguageStringSyntax($string); return $string; } public function replaceDateTags(string $string): string { if (!RL_RegEx::matchAll('\[date:(?<format>.*?)\]', $string, $matches)) { return $string; } $date = JFactory::getDate(); $tz = new DateTimeZone(JFactory::getApplication()->getCfg('offset')); $date->setTimeZone($tz); foreach ($matches as $match) { $replace = $date->format($match['format'], \true); $string = str_replace($match[0], $replace, $string); } return $string; } public function sanitizeValue(mixed $value): mixed { if (is_bool($value) || is_array($value) || is_object($value)) { return $value; } if ($value === 'true') { return \true; } if ($value === 'false') { return \false; } return (string) $value; } public function selectList(): string { return $this->selectListFromData($this); } public function selectListAjax(): string { $class = $this->get('class', ''); $multiple = $this->get('multiple', \false); $attributes = compact('class', 'multiple'); if (!empty($this->attributes)) { foreach ($this->attributes as $key => $default) { $attributes[$key] = $this->get($key, $default); } } if (!empty($this->params)) { foreach ($this->params as $key => $value) { $attributes[$key] = (string) $value; } } $tree_select = $this->use_tree_select && $multiple; return \RegularLabs\Library\Form\Form::selectListAjax($this::class, $this->name, $this->value, $this->id, $attributes, $tree_select, $tree_select && $this->collapse_children); } public function selectListForAjax(JRegistry|\RegularLabs\Library\Form\FormField $data): string { return $this->selectListFromData($data); } public function selectListFromData(JRegistry|\RegularLabs\Library\Form\FormField $data): string { $data_attributes = $data->get('attributes', []); $this->parent_request = (array) $data->get('parent_request', []); $name = $this->name ?: $data->get('name', $this->type); $id = $this->id ?: $data->get('id', strtolower($name)); $value = $this->value ?: $data->get('value', []); $class = $data->get('class', $data_attributes->class ?? ''); $multiple = $data->get('multiple', $data_attributes->multiple ?? 0); $tree_select = $data->get('treeselect', $this->use_tree_select); $collapse_children = $data->get('collapse_children', $this->collapse_children); if (!is_array($value)) { $value = explode(',', $value); } $this->value = $value; $attributes = compact('class', 'multiple'); if (!empty($this->attributes)) { foreach ($this->attributes as $key => $default) { $attributes[$key] = $data->get($key, $this->get($key, $default)); } } foreach ($data_attributes as $key => $val) { $this->params[$key] = $this->sanitizeValue($val); $attributes[$key] = $this->sanitizeValue($val); } $attributes = array_diff_key($attributes, ['name' => '', 'type' => '']); $options = $this->getListOptions($attributes); if ($this->get('text_as_value')) { $this->setTextAsValue($options); } return \RegularLabs\Library\Form\Form::selectList($options, $name, $value, $id, $attributes, $tree_select && $multiple, $tree_select && $multiple && $collapse_children, $this->max_list_count); } /** * Method declaration must be compatible with JFormField::setup() */ public function setup(SimpleXMLElement $element, $value, $group = null) { $this->params = $element->attributes(); return parent::setup($element, $value, $group); } /** * Method declaration must be compatible with JFormField::getInput() */ protected function getInput() { if (!$this->is_select_list) { return ''; } if (!$this->use_ajax && !$this->use_tree_select) { return $this->selectList(); } return $this->selectListAjax(); } /** * Method declaration must be compatible with JFormField::getLabel() */ protected function getLabel() { $this->element['label'] = $this->prepareText($this->element['label']); return $this->element['label'] == '---' ? ' ' : parent::getLabel(); } protected function getListOptions(array $attributes): array|int { return $this->getOptions(); } /** * Return the field options (array) * Overrules the Joomla core functionality * Method declaration must be compatible with JFormField::getOptions() */ protected function getOptions() { if (empty($this->element->option)) { return []; } $fieldname = RL_RegEx::replace('[^a-z0-9_\-]', '_', $this->fieldname); $options = []; foreach ($this->element->option as $option) { $value = (string) $option['value']; $text = trim((string) $option) != '' ? trim((string) $option) : $value; $disabled = (string) $option['disabled']; $disabled = $disabled === 'true' || $disabled === 'disabled' || $disabled === '1'; $disabled = $disabled || $this->readonly && $value != $this->value; $checked = (string) $option['checked']; $checked = $checked === 'true' || $checked === 'checked' || $checked === '1'; $selected = (string) $option['selected']; $selected = $selected === 'true' || $selected === 'selected' || $selected === '1'; $attributes = ''; if ((string) $option['showon']) { $encodedConditions = json_encode(JFormHelper::parseShowOnConditions((string) $option['showon'], $this->formControl, $this->group)); $attributes .= ' data-showon="' . $encodedConditions . '"'; } // Add the option object to the result set. $options[] = ['value' => $value, 'text' => '- ' . JText::alt($text, $fieldname) . ' -', 'disable' => $disabled, 'class' => (string) $option['class'], 'selected' => $checked || $selected, 'checked' => $checked || $selected, 'onclick' => (string) $option['onclick'], 'onchange' => (string) $option['onchange'], 'optionattr' => $attributes]; } return $options; } /** * Fix some syntax/encoding issues in option text strings */ private function fixLanguageStringSyntax(string $string = ''): string { $string = str_replace('[:COMMA:]', ',', $string); $string = trim(RL_String::html_entity_decoder($string)); $string = str_replace('"', '"', $string); $string = str_replace('span style="font-family:monospace;"', 'span class="rl_code"', $string); return $string; } /** * Get the short name of the field class * FoobarField => Foobar */ private function getShortFieldName(): string { return substr((new ReflectionClass($this))->getShortName(), 0, -strlen('Field')); } private function setTextAsValue(array &$options): void { if (empty($options)) { return; } foreach ($options as &$option) { $option->value = $option->text; } } }