first commit

This commit is contained in:
2025-06-17 11:53:18 +02:00
commit 9f0f7ba12b
8804 changed files with 1369176 additions and 0 deletions

View File

@ -0,0 +1,82 @@
<?xml version="1.0" encoding="UTF-8"?>
<form>
<field
name="default_value"
type="hidden"
default=""
/>
<fields name="fieldparams">
<fieldset name="fieldparams">
<field
name="repeat"
type="radio"
label="PLG_FIELDS_SUBFORM_PARAMS_REPEAT_LABEL"
layout="joomla.form.field.radio.switcher"
default="1"
>
<option value="0">JNO</option>
<option value="1">JYES</option>
</field>
<field
name="max_rows"
type="number"
label="PLG_FIELDS_SUBFORM_PARAMS_MAX_ROWS_LABEL"
default=""
filter="integer"
showon="repeat:1"
/>
<field
name="options"
type="subform"
label="PLG_FIELDS_SUBFORM_PARAMS_OPTIONS_LABEL"
icon="list"
layout="joomla.form.field.subform.repeatable-table"
min="1"
multiple="true"
>
<form hidden="true" name="options_modal" repeat="true">
<field
context=""
name="customfield"
type="subfields"
label="PLG_FIELDS_SUBFORM_PARAMS_CUSTOMFIELD_LABEL"
default=""
required="true"
/>
<field
name="render_values"
type="radio"
label="PLG_FIELDS_SUBFORM_PARAMS_RENDER_VALUES_LABEL"
layout="joomla.form.field.radio.switcher"
default="1"
>
<option value="0">JNO</option>
<option value="1">JYES</option>
</field>
</form>
</field>
</fieldset>
</fields>
<fields name="params">
<fieldset name="basic">
<fieldset name="formoptions">
<field
name="form_layout"
type="list"
label="COM_FIELDS_FIELD_FORM_LAYOUT_LABEL"
default=""
class="form-select"
showon="repeat:1"
>
<option value="">JDEFAULT</option>
<option value="joomla.form.field.subform.repeatable-table">PLG_FIELDS_SUBFORM_PARAMS_EDIT_LAYOUT_OPTION_REPEATABLE_TABLE_LABEL</option>
<option value="joomla.form.field.subform.repeatable">PLG_FIELDS_SUBFORM_PARAMS_EDIT_LAYOUT_OPTION_REPEATABLE_FORM_LABEL</option>
</field>
</fieldset>
</fieldset>
</fields>
</form>

View File

@ -0,0 +1,46 @@
<?php
/**
* @package Joomla.Plugin
* @subpackage Fields.subform
*
* @copyright (C) 2023 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
\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 Joomla\Plugin\Fields\Subform\Extension\Subform;
return new class () implements ServiceProviderInterface {
/**
* Registers the service provider with a DI container.
*
* @param Container $container The DI container.
*
* @return void
*
* @since 4.3.0
*/
public function register(Container $container)
{
$container->set(
PluginInterface::class,
function (Container $container) {
$plugin = new Subform(
$container->get(DispatcherInterface::class),
(array) PluginHelper::getPlugin('fields', 'subform')
);
$plugin->setApplication(Factory::getApplication());
return $plugin;
}
);
}
};

View File

@ -0,0 +1,418 @@
<?php
/**
* @package Joomla.Plugin
* @subpackage Fields.subform
*
* @copyright (C) 2019 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\Plugin\Fields\Subform\Extension;
use DOMDocument;
use DOMElement;
use DOMXPath;
use Joomla\CMS\Form\Form;
use Joomla\Component\Fields\Administrator\Helper\FieldsHelper;
use Joomla\Component\Fields\Administrator\Plugin\FieldsPlugin;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Fields Subform Plugin
*
* @since 3.7.0
*/
final class Subform extends FieldsPlugin
{
/**
* Two-dimensional array to hold to do a fast in-memory caching of rendered
* subfield values.
*
* @var array
*
* @since 4.0.0
*/
protected $renderCache = [];
/**
* Array to do a fast in-memory caching of all custom field items.
*
* @var array
*
* @since 4.0.0
*/
protected static $customFieldsCache = null;
/**
* Handles the onContentPrepareForm event. Adds form definitions to relevant forms.
*
* @param Form $form The form to manipulate
* @param array|object $data The data of the form
*
* @return void
*
* @since 4.0.0
*/
public function onContentPrepareForm(Form $form, $data)
{
// Get the path to our own form definition (basically ./params/subform.xml)
$path = $this->getFormPath($form, $data);
if ($path === null) {
return;
}
// Ensure it is an object
$formData = (object) $data;
// Now load our own form definition into a DOMDocument, because we want to manipulate it
$xml = new \DOMDocument();
$xml->load($path);
// Prepare a DOMXPath object
$xmlxpath = new \DOMXPath($xml);
/**
* Get all fields of type "subfields" in our own XML
*
* @var $valuefields \DOMNodeList
*/
$valuefields = $xmlxpath->evaluate('//field[@type="subfields"]');
// If we haven't found it, something is wrong
if (!$valuefields || $valuefields->length != 1) {
return;
}
// Now iterate over those fields and manipulate them, set its parameter `context` to our context
foreach ($valuefields as $valuefield) {
$valuefield->setAttribute('context', $formData->context);
}
// When this is not a new instance (editing an existing instance)
if (isset($formData->id) && $formData->id > 0) {
// Don't allow the 'repeat' attribute to be edited
foreach ($xmlxpath->evaluate('//field[@name="repeat"]') as $field) {
$field->setAttribute('readonly', '1');
}
}
// And now load our manipulated form definition into the JForm
$form->load($xml->saveXML(), true, '/form/*');
}
/**
* Manipulates the $field->value before the field is being passed to
* onCustomFieldsPrepareField.
*
* @param string $context The context
* @param object $item The item
* @param \stdClass $field The field
*
* @return void
*
* @since 4.0.0
*/
public function onCustomFieldsBeforePrepareField($context, $item, $field)
{
if (!$this->isTypeSupported($field->type)) {
return;
}
if (\is_array($field->value)) {
return;
}
$decoded_value = json_decode($field->value, true);
if (!$decoded_value || !\is_array($decoded_value)) {
return;
}
$field->value = $decoded_value;
}
/**
* Renders this fields value by rendering all sub fields and joining all those rendered sub fields together.
*
* @param string $context The context
* @param object $item The item
* @param \stdClass $field The field
*
* @return string
*
* @since 4.0.0
*/
public function onCustomFieldsPrepareField($context, $item, $field)
{
// Check if the field should be processed by us
if (!$this->isTypeSupported($field->type)) {
return;
}
// If we don't have any subfields (or values for them), nothing to do.
if (!\is_array($field->value) || \count($field->value) < 1) {
return;
}
// Get the field params
$field_params = $this->getParamsFromField($field);
/**
* Placeholder to hold all rows (if this field is repeatable).
* Each array entry is another array representing a row, containing all of the sub fields that
* are valid for this row and their raw and rendered values.
*/
$subform_rows = [];
// Create an array with entries being subfields forms, and if not repeatable, containing only one element.
$rows = $field->value;
if ($field_params->get('repeat', '1') == '0') {
$rows = [$field->value];
}
// Iterate over each row of the data
foreach ($rows as $row) {
// Holds all sub fields of this row, incl. their raw and rendered value
$row_subfields = [];
// For each row, iterate over all the subfields
foreach ($this->getSubfieldsFromField($field) as $subfield) {
// Fill value (and rawvalue) if we have data for this subfield in the current row, otherwise set them to empty
$subfield->rawvalue = $subfield->value = $row[$subfield->name] ?? '';
// Do we want to render the value of this field, and is the value non-empty?
if ($subfield->value !== '' && $subfield->render_values == '1') {
/**
* Construct the cache-key for our renderCache. It is important that the cache key
* is as unique as possible to avoid false duplicates (e.g. type and rawvalue is not
* enough for the cache key, because type 'list' and value '1' can have different
* rendered values, depending on the list items), but it also must be as general as possible
* to not cause too many unneeded rendering processes (e.g. the type 'text' will always be
* rendered the same when it has the same rawvalue).
*/
$renderCache_key = serialize(
[
$subfield->type,
$subfield->id,
$subfield->rawvalue,
]
);
// Let's see if we have a fast in-memory result for this
if (isset($this->renderCache[$renderCache_key])) {
$subfield->value = $this->renderCache[$renderCache_key];
} else {
// Render this virtual subfield
$subfield->value = $this->getApplication()->triggerEvent(
'onCustomFieldsPrepareField',
[$context, $item, $subfield]
);
$this->renderCache[$renderCache_key] = $subfield->value;
}
}
// Flatten the value if it is an array (list, checkboxes, etc.) [independent of render_values]
if (\is_array($subfield->value)) {
$subfield->value = implode(' ', $subfield->value);
}
// Store the subfield (incl. its raw and rendered value) into this rows sub fields
$row_subfields[$subfield->fieldname] = $subfield;
}
// Store all the sub fields of this row
$subform_rows[] = $row_subfields;
}
// Store all the rows and their corresponding sub fields in $field->subform_rows
$field->subform_rows = $subform_rows;
// Call our parent to combine all those together for the final $field->value
return parent::onCustomFieldsPrepareField($context, $item, $field);
}
/**
* Returns a DOMElement which is the child of $parent and represents
* the form XML definition for this field.
*
* @param \stdClass $field The field
* @param \DOMElement $parent The original parent element
* @param Form $form The form
*
* @return \DOMElement
*
* @since 4.0.0
*/
public function onCustomFieldsPrepareDom($field, \DOMElement $parent, Form $form)
{
// Call the onCustomFieldsPrepareDom method on FieldsPlugin
$parent_field = parent::onCustomFieldsPrepareDom($field, $parent, $form);
if (!$parent_field) {
return $parent_field;
}
// Override the fieldname attribute of the subform - this is being used to index the rows
$parent_field->setAttribute('fieldname', 'row');
// If the user configured this subform instance as required
if ($field->required) {
// Then we need to have at least one row
$parent_field->setAttribute('min', '1');
}
// Get the configured parameters for this field
$field_params = $this->getParamsFromField($field);
// If this fields should be repeatable, set some attributes on the subform element
if ($field_params->get('repeat', '1') == '1') {
$parent_field->setAttribute('multiple', 'true');
$parent_field->setAttribute('layout', 'joomla.form.field.subform.repeatable-table');
}
// Create a child 'form' DOMElement under the field[type=subform] element.
$parent_fieldset = $parent_field->appendChild(new \DOMElement('form'));
$parent_fieldset->setAttribute('hidden', 'true');
$parent_fieldset->setAttribute('name', ($field->name . '_modal'));
if ($field_params->get('max_rows')) {
$parent_field->setAttribute('max', $field_params->get('max_rows'));
}
// If this field should be repeatable, set some attributes on the modal
if ($field_params->get('repeat', '1') == '1') {
$parent_fieldset->setAttribute('repeat', 'true');
}
// Get the configured sub fields for this field
$subfields = $this->getSubfieldsFromField($field);
// If we have 5 or more of them, use the `repeatable` layout instead of the `repeatable-table`
if (\count($subfields) >= 5) {
$parent_field->setAttribute('layout', 'joomla.form.field.subform.repeatable');
}
// Iterate over the sub fields to call prepareDom on each of those sub-fields
foreach ($subfields as $subfield) {
// Let the relevant plugins do their work and insert the correct
// DOMElement's into our $parent_fieldset.
$this->getApplication()->triggerEvent(
'onCustomFieldsPrepareDom',
[$subfield, $parent_fieldset, $form]
);
}
// If the edit layout is set we override any automation
$editLayout = $field->params->get('form_layout');
if ($editLayout) {
$parent_field->setAttribute('layout', $editLayout);
}
return $parent_field;
}
/**
* Returns an array of all options configured for this field.
*
* @param \stdClass $field The field
*
* @return \stdClass[]
*
* @since 4.0.0
*/
protected function getOptionsFromField(\stdClass $field)
{
$result = [];
// Fetch the options from the plugin
$params = $this->getParamsFromField($field);
foreach ($params->get('options', []) as $option) {
$result[] = (object) $option;
}
return $result;
}
/**
* Returns the configured params for a given field.
*
* @param \stdClass $field The field
*
* @return \Joomla\Registry\Registry
*
* @since 4.0.0
*/
protected function getParamsFromField(\stdClass $field)
{
$params = (clone $this->params);
if (isset($field->fieldparams) && \is_object($field->fieldparams)) {
$params->merge($field->fieldparams);
}
return $params;
}
/**
* Returns an array of all subfields for a given field. This will always return a bare clone
* of a sub field, so manipulating it is safe.
*
* @param \stdClass $field The field
*
* @return \stdClass[]
*
* @since 4.0.0
*/
protected function getSubfieldsFromField(\stdClass $field)
{
if (static::$customFieldsCache === null) {
// Prepare our cache
static::$customFieldsCache = [];
// Get all custom field instances
$customFields = FieldsHelper::getFields('', null, false, null, true);
foreach ($customFields as $customField) {
// Store each custom field instance in our cache with its id as key
static::$customFieldsCache[$customField->id] = $customField;
}
}
$result = [];
// Iterate over all configured options for this field
foreach ($this->getOptionsFromField($field) as $option) {
// Check whether the wanted sub field really is an existing custom field
if (!isset(static::$customFieldsCache[$option->customfield])) {
continue;
}
// Get a clone of the sub field, so we and the caller can do some manipulation with it.
$cur_field = (clone static::$customFieldsCache[$option->customfield]);
// Manipulate it and add our custom configuration to it
$cur_field->render_values = $option->render_values;
/**
* Set the name of the sub field to its id so that the values in the database are being saved
* based on the id of the sub fields, not on their name. Actually we do not need the name of
* the sub fields to render them, but just to make sure we have the name when we need it, we
* store it as `fieldname`.
*/
$cur_field->fieldname = $cur_field->name;
$cur_field->name = 'field' . $cur_field->id;
// And add it to our result
$result[] = $cur_field;
}
return $result;
}
}

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<extension type="plugin" group="fields" method="upgrade">
<name>plg_fields_subform</name>
<author>Joomla! Project</author>
<creationDate>2017-06</creationDate>
<copyright>(C) 2019 Open Source Matters, Inc.</copyright>
<license>GNU General Public License version 2 or later; see LICENSE.txt</license>
<authorEmail>admin@joomla.org</authorEmail>
<authorUrl>www.joomla.org</authorUrl>
<version>4.0.0</version>
<description>PLG_FIELDS_SUBFORM_XML_DESCRIPTION</description>
<namespace path="src">Joomla\Plugin\Fields\Subform</namespace>
<files>
<folder>params</folder>
<folder plugin="subform">services</folder>
<folder>src</folder>
<folder>tmpl</folder>
</files>
<languages>
<language tag="en-GB">language/en-GB/plg_fields_subform.ini</language>
<language tag="en-GB">language/en-GB/plg_fields_subform.sys.ini</language>
</languages>
</extension>

View File

@ -0,0 +1,60 @@
<?php
/**
* @package Joomla.Plugin
* @subpackage Fields.Subform
*
* @copyright (C) 2019 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
use Joomla\Component\Fields\Administrator\Helper\FieldsHelper;
defined('_JEXEC') or die;
if (!$context || empty($field->subform_rows)) {
return;
}
$result = '';
// Iterate over each row that we have
foreach ($field->subform_rows as $subform_row) {
// Placeholder array to generate this rows output
$row_output = [];
// Iterate over each sub field inside of that row
foreach ($subform_row as $subfield) {
$class = trim($subfield->params->get('render_class', ''));
$layout = trim($subfield->params->get('layout', 'render'));
$content = trim(
FieldsHelper::render(
$context,
'field.' . $layout, // normally just 'field.render'
['field' => $subfield]
)
);
// Skip empty output
if ($content === '') {
continue;
}
// Generate the output for this sub field and row
$row_output[] = '<span class="field-entry' . ($class ? (' ' . $class) : '') . '">' . $content . '</span>';
}
// Skip empty rows
if (count($row_output) == 0) {
continue;
}
$result .= '<li>' . implode(', ', $row_output) . '</li>';
}
?>
<?php if (trim($result) != '') : ?>
<ul class="fields-container">
<?php echo $result; ?>
</ul>
<?php endif; ?>