diff --git a/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-04-15.sql b/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-04-15.sql
new file mode 100644
index 0000000000000..baa717f01993c
--- /dev/null
+++ b/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-04-15.sql
@@ -0,0 +1,2 @@
+INSERT INTO `#__extensions` (`package_id`, `name`, `type`, `element`, `folder`, `client_id`, `enabled`, `access`, `protected`, `manifest_cache`, `params`, `checked_out`, `checked_out_time`, `ordering`, `state`) VALUES
+(0, 'plg_fields_subfields', 'plugin', 'subfields', 'fields', 0, 1, 1, 0, '', '', 0, '0000-00-00 00:00:00', 0, 0);
diff --git a/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-04-15.sql b/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-04-15.sql
new file mode 100644
index 0000000000000..0fd58a83142b3
--- /dev/null
+++ b/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-04-15.sql
@@ -0,0 +1,2 @@
+INSERT INTO "#__extensions" ("package_id", "name", "type", "element", "folder", "client_id", "enabled", "access", "protected", "manifest_cache", "params", "checked_out", "checked_out_time", "ordering", "state") VALUES
+(0, 'plg_fields_subfields', 'plugin', 'subfields', 'fields', 0, 1, 1, 0, '', '', 0, '1970-01-01 00:00:00', 0, 0);
diff --git a/administrator/components/com_fields/Field/SubfieldstypeField.php b/administrator/components/com_fields/Field/SubfieldstypeField.php
new file mode 100644
index 0000000000000..ffef94fa44942
--- /dev/null
+++ b/administrator/components/com_fields/Field/SubfieldstypeField.php
@@ -0,0 +1,133 @@
+context]))
+ {
+ static::$customFieldsCache[$this->context] = FieldsHelper::getFields($this->context);
+ }
+
+ // Iterate over the custom fields for this context
+ foreach (static::$customFieldsCache[$this->context] as $customField)
+ {
+ // Skip our own subfields type. We won't have subfields in subfields.
+ if ($customField->type == 'subfields')
+ {
+ continue;
+ }
+
+ /**
+ * Skip the repeatable custom field type too. It is currently still part of the Joomla! core, but it
+ * shall be removed soon. See issue #23659
+ */
+ if ($customField->type == 'repeatable')
+ {
+ continue;
+ }
+
+ $options[] = HTMLHelper::_(
+ 'select.option',
+ $customField->id,
+ ($customField->title . ' (' . $customField->type . ')')
+ );
+ }
+
+ // Sorting the fields based on the text which is displayed
+ usort(
+ $options,
+ function ($a, $b)
+ {
+ return strcmp($a->text, $b->text);
+ }
+ );
+
+ return $options;
+ }
+
+ /**
+ * Method to attach a JForm object to the field.
+ *
+ * @param \SimpleXMLElement $element The SimpleXMLElement object representing the `` tag for the form field object.
+ * @param mixed $value The form field value to validate.
+ * @param string $group The field name group control value. This acts as an array container for the field.
+ * For example if the field has name="foo" and the group value is set to "bar" then the
+ * full field name would end up being "bar[foo]".
+ *
+ * @return boolean True on success.
+ *
+ * @since __DEPLOY_VERSION__
+ */
+ public function setup(\SimpleXMLElement $element, $value, $group = null)
+ {
+ $return = parent::setup($element, $value, $group);
+
+ if ($return)
+ {
+ $this->context = (string) $this->element['context'];
+ }
+
+ return $return;
+ }
+}
diff --git a/administrator/components/com_fields/Helper/FieldsHelper.php b/administrator/components/com_fields/Helper/FieldsHelper.php
index 4978bccc03ca8..60df37ba75a9b 100644
--- a/administrator/components/com_fields/Helper/FieldsHelper.php
+++ b/administrator/components/com_fields/Helper/FieldsHelper.php
@@ -587,6 +587,36 @@ public static function displayFieldOnForm($field)
return true;
}
+ /**
+ * Gets assigned categories ids for a field
+ *
+ * @param stdClass[] $fieldId The field ID
+ *
+ * @return array Array with the assigned category ids
+ *
+ * @since __DEPLOY_VERSION__
+ */
+ public static function getAssignedCategoriesIds($fieldId)
+ {
+ $fieldId = (int) $fieldId;
+
+ if (!$fieldId)
+ {
+ return array();
+ }
+
+ $db = Factory::getDbo();
+ $query = $db->getQuery(true);
+
+ $query->select($db->quoteName('a.category_id'))
+ ->from($db->quoteName('#__fields_categories', 'a'))
+ ->where('a.field_id = ' . $fieldId);
+
+ $db->setQuery($query);
+
+ return $db->loadColumn();
+ }
+
/**
* Gets assigned categories titles for a field
*
@@ -602,7 +632,7 @@ public static function getAssignedCategoriesTitles($fieldId)
if (!$fieldId)
{
- return array();
+ return [];
}
$db = Factory::getDbo();
diff --git a/administrator/components/com_fields/Model/FieldModel.php b/administrator/components/com_fields/Model/FieldModel.php
index a36b8f67bc560..a6b2e112fad0a 100644
--- a/administrator/components/com_fields/Model/FieldModel.php
+++ b/administrator/components/com_fields/Model/FieldModel.php
@@ -169,6 +169,13 @@ public function save($data)
foreach ($cats as $cat)
{
+ // If we have found the 'JNONE' category, remove all other from the result and break.
+ if ($cat == '-1')
+ {
+ $assignedCatIds = array('-1');
+ break;
+ }
+
if ($cat)
{
$assignedCatIds[] = $cat;
@@ -192,8 +199,16 @@ public function save($data)
$db->insertObject('#__fields_categories', $tupel);
}
- // If the options have changed delete the values
- if ($field && isset($data['fieldparams']['options']) && isset($field->fieldparams['options']))
+ /**
+ * If the options have changed, delete the values. This should only apply for list, checkboxes and radio
+ * custom field types, because when their options are being changed, their values might get invalid, because
+ * e.g. there is a value selected from a list, which is not part of the list anymore. Hence we need to delete
+ * all values that are not part of the options anymore. Note: The only field types with fieldparams+options
+ * are those above listed plus the subfields type. And we do explicitly not want the values to be deleted
+ * when the options of a subfields field are getting changed.
+ */
+ if ($field && in_array($field->type, array('list', 'checkboxes', 'radio'), true)
+ && isset($data['fieldparams']['options']) && isset($field->fieldparams['options']))
{
$oldParams = $this->getParams($field->fieldparams['options']);
$newParams = $this->getParams($data['fieldparams']['options']);
diff --git a/administrator/components/com_fields/Plugin/FieldsPlugin.php b/administrator/components/com_fields/Plugin/FieldsPlugin.php
index e9061d1f9a1fc..a5f874d876b3e 100644
--- a/administrator/components/com_fields/Plugin/FieldsPlugin.php
+++ b/administrator/components/com_fields/Plugin/FieldsPlugin.php
@@ -244,11 +244,34 @@ public function onCustomFieldsPrepareDom($field, \DOMElement $parent, Form $form
* @since 3.7.0
*/
public function onContentPrepareForm(Form $form, $data)
+ {
+ $path = $this->getFormPath($form, $data);
+
+ if ($path === null)
+ {
+ return;
+ }
+
+ // Load the specific plugin parameters
+ $form->load(file_get_contents($path), true, '/form/*');
+ }
+
+ /**
+ * Returns the path of the XML definition file for the field parameters
+ *
+ * @param Form $form The form
+ * @param stdClass $data The data
+ *
+ * @return string
+ *
+ * @since __DEPLOY_VERSION__
+ */
+ protected function getFormPath(Form $form, $data)
{
// Check if the field form is calling us
if (strpos($form->getName(), 'com_fields.field') !== 0)
{
- return;
+ return null;
}
// Ensure it is an object
@@ -265,7 +288,7 @@ public function onContentPrepareForm(Form $form, $data)
// Not us
if (!$this->isTypeSupported($type))
{
- return;
+ return null;
}
$path = JPATH_PLUGINS . '/' . $this->_type . '/' . $this->_name . '/params/' . $type . '.xml';
@@ -273,11 +296,10 @@ public function onContentPrepareForm(Form $form, $data)
// Check if params file exists
if (!file_exists($path))
{
- return;
+ return null;
}
- // Load the specific plugin parameters
- $form->load(file_get_contents($path), true, '/form/*');
+ return $path;
}
/**
diff --git a/administrator/components/com_fields/Table/FieldTable.php b/administrator/components/com_fields/Table/FieldTable.php
index 7710df4db045f..953e5030d1347 100644
--- a/administrator/components/com_fields/Table/FieldTable.php
+++ b/administrator/components/com_fields/Table/FieldTable.php
@@ -65,6 +65,34 @@ public function bind($src, $ignore = '')
if (isset($src['fieldparams']) && is_array($src['fieldparams']))
{
+ // Make sure $registry->options contains no duplicates when the field type is subfields
+ if (isset($src['type']) && $src['type'] == 'subfields' && isset($src['fieldparams']['options']))
+ {
+ // Fast lookup map to check which custom field ids we have already seen
+ $seen_customfields = array();
+
+ // Container for the new $src['fieldparams']['options']
+ $options = array();
+
+ // Iterate through the old options
+ $i = 0;
+
+ foreach ($src['fieldparams']['options'] as $option)
+ {
+ // Check whether we have not yet seen this custom field id
+ if (!isset($seen_customfields[$option['customfield']]))
+ {
+ // We haven't, so add it to the final options
+ $seen_customfields[$option['customfield']] = true;
+ $options['option' . $i] = $option;
+ $i++;
+ }
+ }
+
+ // And replace the options with the deduplicated ones.
+ $src['fieldparams']['options'] = $options;
+ }
+
$registry = new Registry;
$registry->loadArray($src['fieldparams']);
$src['fieldparams'] = (string) $registry;
diff --git a/administrator/components/com_fields/forms/field.xml b/administrator/components/com_fields/forms/field.xml
index 5f8318d1d7077..8e35169e79f6b 100644
--- a/administrator/components/com_fields/forms/field.xml
+++ b/administrator/components/com_fields/forms/field.xml
@@ -38,6 +38,7 @@
addfieldprefix="Joomla\Component\Categories\Administrator\Field"
>
+
-
+ id); ?>
+
diff --git a/administrator/language/en-GB/en-GB.plg_fields_subfields.ini b/administrator/language/en-GB/en-GB.plg_fields_subfields.ini
new file mode 100644
index 0000000000000..61226fd351508
--- /dev/null
+++ b/administrator/language/en-GB/en-GB.plg_fields_subfields.ini
@@ -0,0 +1,13 @@
+; Joomla! Project
+; Copyright (C) 2005 - 2019 Open Source Matters. All rights reserved.
+; License GNU General Public License version 2 or later; see LICENSE.txt, see LICENSE.php
+; Note : All ini files need to be saved as UTF-8
+
+PLG_FIELDS_SUBFIELDS="Fields - Subfields"
+PLG_FIELDS_SUBFIELDS_LABEL="Subfields (%s)"
+PLG_FIELDS_SUBFIELDS_PARAMS_CUSTOMFIELD_LABEL="Sub field"
+PLG_FIELDS_SUBFIELDS_PARAMS_OPTIONS_LABEL="Sub fields"
+PLG_FIELDS_SUBFIELDS_PARAMS_RENDER_VALUES_DESC="Whether you want the plugin to pre-render the values of this sub field. Pre-rendering comes at a cost of performance and it might not be necessary, especially if you have a custom layout override where you take care of rendering the raw value."
+PLG_FIELDS_SUBFIELDS_PARAMS_RENDER_VALUES_LABEL="Render values"
+PLG_FIELDS_SUBFIELDS_PARAMS_REPEAT_LABEL="Repeatable"
+PLG_FIELDS_SUBFIELDS_XML_DESCRIPTION="This plugin lets you create new fields of type 'subfields' in any extension where custom fields are supported."
diff --git a/administrator/language/en-GB/en-GB.plg_fields_subfields.sys.ini b/administrator/language/en-GB/en-GB.plg_fields_subfields.sys.ini
new file mode 100644
index 0000000000000..ed10cec599113
--- /dev/null
+++ b/administrator/language/en-GB/en-GB.plg_fields_subfields.sys.ini
@@ -0,0 +1,7 @@
+; Joomla! Project
+; Copyright (C) 2005 - 2019 Open Source Matters. All rights reserved.
+; License GNU General Public License version 2 or later; see LICENSE.txt, see LICENSE.php
+; Note : All ini files need to be saved as UTF-8
+
+PLG_FIELDS_SUBFIELDS="Fields - Subfields"
+PLG_FIELDS_SUBFIELDS_XML_DESCRIPTION="This plugin lets you create new fields of type 'subfields' in any extension where custom fields are supported."
diff --git a/build/media_source/system/js/fields/calendar.es5.js b/build/media_source/system/js/fields/calendar.es5.js
index cf29d845621b5..8ee40130446d3 100644
--- a/build/media_source/system/js/fields/calendar.es5.js
+++ b/build/media_source/system/js/fields/calendar.es5.js
@@ -1045,11 +1045,20 @@
return false;
};
- /** Method to change input values with the data-alt-value values. **/
+ /**
+ * Method to change input values with the data-alt-value values. This method is e.g. being called
+ * by the onSubmit handler of the calendar fields form.
+ */
JoomlaCalendar.prototype.setAltValue = function() {
var input = this.inputField;
if (input.getAttribute('disabled')) return;
- input.value = input.getAttribute('data-alt-value') ? input.getAttribute('data-alt-value') : '';
+
+ // Set the value to the data-alt-value attribute, but only if it really has a value.
+ input.value = (
+ input.getAttribute('data-alt-value') && input.getAttribute('data-alt-value') !== '0000-00-00 00:00:00'
+ ? input.getAttribute('data-alt-value')
+ : ''
+ );
};
/** Method to change the inputs before submit. **/
diff --git a/installation/sql/mysql/joomla.sql b/installation/sql/mysql/joomla.sql
index 9d069aca8e8fb..5aef689e75ce6 100644
--- a/installation/sql/mysql/joomla.sql
+++ b/installation/sql/mysql/joomla.sql
@@ -719,6 +719,7 @@ INSERT INTO `#__extensions` (`package_id`, `name`, `type`, `element`, `folder`,
(0, 'plg_media-action_rotate', 'plugin', 'rotate', 'media-action', 0, 1, 1, 0, '', '{}', 0, '0000-00-00 00:00:00', 0, 0),
(0, 'atum', 'template', 'atum', '', 1, 1, 1, 0, '', '', 0, '0000-00-00 00:00:00', 0, 0),
(0, 'cassiopeia', 'template', 'cassiopeia', '', 0, 1, 1, 0, '', '{"logoFile":"","fluidContainer":"0","sidebarLeftWidth":"3","sidebarRightWidth":"3"}', 0, '0000-00-00 00:00:00', 0, 0),
+(0, 'plg_fields_subfields', 'plugin', 'subfields', 'fields', 0, 1, 1, 0, '', '', 0, '0000-00-00 00:00:00', 0, 0),
(0, 'files_joomla', 'file', 'joomla', '', 0, 1, 1, 1, '', '', 0, '0000-00-00 00:00:00', 0, 0),
(0, 'English (en-GB) Language Pack', 'package', 'pkg_en-GB', '', 0, 1, 1, 1, '', '', 0, '0000-00-00 00:00:00', 0, 0);
diff --git a/installation/sql/postgresql/joomla.sql b/installation/sql/postgresql/joomla.sql
index bac630c0aae34..6e4c4c680e87b 100644
--- a/installation/sql/postgresql/joomla.sql
+++ b/installation/sql/postgresql/joomla.sql
@@ -730,6 +730,7 @@ INSERT INTO "#__extensions" ("package_id", "name", "type", "element", "folder",
(0, 'plg_media-action_rotate', 'plugin', 'rotate', 'media-action', 0, 1, 1, 0, '', '{}', 0, '1970-01-01 00:00:00', 0, 0),
(0, 'atum', 'template', 'atum', '', 1, 1, 1, 0, '', '', 0, '1970-01-01 00:00:00', 0, 0),
(0, 'cassiopeia', 'template', 'cassiopeia', '', 0, 1, 1, 0, '', '{"logoFile":"","fluidContainer":"0","sidebarLeftWidth":"3","sidebarRightWidth":"3"}', 0, '1970-01-01 00:00:00', 0, 0),
+(0, 'plg_fields_subfields', 'plugin', 'subfields', 'fields', 0, 1, 1, 0, '', '', 0, '1970-01-01 00:00:00', 0, 0),
(0, 'files_joomla', 'file', 'joomla', '', 0, 1, 1, 1, '', '', 0, '1970-01-01 00:00:00', 0, 0),
(0, 'English (en-GB) Language Pack', 'package', 'pkg_en-GB', '', 0, 1, 1, 1, '', '', 0, '1970-01-01 00:00:00', 0, 0);
diff --git a/libraries/src/Extension/ExtensionHelper.php b/libraries/src/Extension/ExtensionHelper.php
index 3c242886114f9..d815def440b1c 100644
--- a/libraries/src/Extension/ExtensionHelper.php
+++ b/libraries/src/Extension/ExtensionHelper.php
@@ -213,6 +213,7 @@ class ExtensionHelper
array('plugin', 'radio', 'fields', 0),
array('plugin', 'repeatable', 'fields', 0),
array('plugin', 'sql', 'fields', 0),
+ array('plugin', 'subfields', 'fields', 0),
array('plugin', 'text', 'fields', 0),
array('plugin', 'textarea', 'fields', 0),
array('plugin', 'url', 'fields', 0),
diff --git a/libraries/src/Form/Field/SubformField.php b/libraries/src/Form/Field/SubformField.php
index 17c5f05103a11..d7da410eb0ed6 100644
--- a/libraries/src/Form/Field/SubformField.php
+++ b/libraries/src/Form/Field/SubformField.php
@@ -195,7 +195,7 @@ public function setup(\SimpleXMLElement $element, $value, $group = null)
return false;
}
- foreach (array('formsource', 'min', 'max', 'layout', 'groupByFieldset', 'buttons') as $attributeName)
+ foreach (array('fieldname', 'formsource', 'min', 'max', 'layout', 'groupByFieldset', 'buttons') as $attributeName)
{
$this->__set($attributeName, $element[$attributeName]);
}
diff --git a/plugins/fields/subfields/params/subfields.xml b/plugins/fields/subfields/params/subfields.xml
new file mode 100644
index 0000000000000..902af3e12f165
--- /dev/null
+++ b/plugins/fields/subfields/params/subfields.xml
@@ -0,0 +1,57 @@
+
+
diff --git a/plugins/fields/subfields/subfields.php b/plugins/fields/subfields/subfields.php
new file mode 100644
index 0000000000000..fac1d2cb39111
--- /dev/null
+++ b/plugins/fields/subfields/subfields.php
@@ -0,0 +1,439 @@
+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 "subfieldstype" in our own XML
+ *
+ * @var $valuefields \DOMNodeList
+ */
+ $valuefields = $xmlxpath->evaluate('//field[@type="subfieldstype"]');
+
+ // 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 __DEPLOY_VERSION__
+ */
+ public function onCustomFieldsBeforePrepareField($context, $item, $field)
+ {
+ // Check if the field should be processed by us
+ if (!$this->isTypeSupported($field->type))
+ {
+ 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 __DEPLOY_VERSION__
+ */
+ 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.
+ */
+ $subfields_rows = array();
+
+ // 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 = array($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 = array();
+
+ // For each row, iterate over all the subfields
+ foreach ($this->getSubfieldsFromField($field) as $subfield)
+ {
+ // Just to be sure, unset this subfields value (and rawvalue)
+ $subfield->rawvalue = $subfield->value = '';
+
+ // If we have data for this field in the current row
+ if (isset($row[$subfield->name]) && $row[$subfield->name])
+ {
+ // Take over the data into our virtual subfield
+ $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(
+ array(
+ $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 = Factory::getApplication()->triggerEvent(
+ 'onCustomFieldsPrepareField',
+ array($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
+ $subfields_rows[] = $row_subfields;
+ }
+
+ // Store all the rows and their corresponding sub fields in $field->subfields_rows
+ $field->subfields_rows = $subfields_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 $orig_parent and represents
+ * the form XML definition for this field.
+ *
+ * @param \stdClass $field The field
+ * @param DOMElement $orig_parent The original parent element
+ * @param JForm $form The form
+ *
+ * @return \DOMElement
+ *
+ * @since __DEPLOY_VERSION__
+ */
+ public function onCustomFieldsPrepareDom($field, DOMElement $orig_parent, Form $form)
+ {
+ // Call the onCustomFieldsPrepareDom method on FieldsPlugin
+ $parent_field = parent::onCustomFieldsPrepareDom($field, $orig_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');
+
+ // Make sure this `field` DOMElement has an attribute type=subform - our parent set this to
+ // subfields, because that is our name. But we want the XML to be a subform.
+ $parent_field->setAttribute('type', 'subform');
+
+ // If the user configured this subfields 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 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.
+ Factory::getApplication()->triggerEvent(
+ 'onCustomFieldsPrepareDom',
+ array($subfield, $parent_fieldset, $form)
+ );
+ }
+
+ return $parent_field;
+ }
+
+ /**
+ * Returns an array of all options configured for this field.
+ *
+ * @param \stdClass $field The field
+ *
+ * @return \stdClass[]
+ *
+ * @since __DEPLOY_VERSION__
+ */
+ protected function getOptionsFromField(\stdClass $field)
+ {
+ $result = array();
+
+ // Fetch the options from the plugin
+ $params = $this->getParamsFromField($field);
+
+ foreach ($params->get('options', array()) 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 __DEPLOY_VERSION__
+ */
+ 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 __DEPLOY_VERSION__
+ */
+ protected function getSubfieldsFromField(\stdClass $field)
+ {
+ if (static::$customFieldsCache === null)
+ {
+ // Prepare our cache
+ static::$customFieldsCache = array();
+
+ // Get all custom field instances
+ $customFields = FieldsHelper::getFields('');
+
+ foreach ($customFields as $customField)
+ {
+ // Store each custom field instance in our cache with its id as key
+ static::$customFieldsCache[$customField->id] = $customField;
+ }
+ }
+
+ $result = array();
+
+ // 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;
+ }
+}
diff --git a/plugins/fields/subfields/subfields.xml b/plugins/fields/subfields/subfields.xml
new file mode 100644
index 0000000000000..b7d0bc82c83e6
--- /dev/null
+++ b/plugins/fields/subfields/subfields.xml
@@ -0,0 +1,19 @@
+
+
+ plg_fields_subfields
+ Joomla! Project
+ June 2017
+ Copyright (C) 2005 - 2019 Open Source Matters. All rights reserved.
+ GNU General Public License version 2 or later; see LICENSE.txt
+ admin@joomla.org
+ www.joomla.org
+ __DEPLOY_VERSION__
+ PLG_FIELDS_SUBFIELDS_XML_DESCRIPTION
+
+ subfields.php
+
+
+ en-GB.plg_fields_subfields.ini
+ en-GB.plg_fields_subfields.sys.ini
+
+
diff --git a/plugins/fields/subfields/tmpl/subfields.php b/plugins/fields/subfields/tmpl/subfields.php
new file mode 100644
index 0000000000000..9a459f4045a28
--- /dev/null
+++ b/plugins/fields/subfields/tmpl/subfields.php
@@ -0,0 +1,64 @@
+subfields_rows))
+{
+ return;
+}
+
+$result = '';
+
+// Iterate over each row that we have
+foreach ($field->subfields_rows as $subfields_row)
+{
+ // Placeholder array to generate this rows output
+ $row_output = array();
+
+ // Iterate over each sub field inside of that row
+ foreach ($subfields_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'
+ array('field' => $subfield)
+ )
+ );
+
+ // Skip empty output
+ if ($content === '')
+ {
+ continue 1;
+ }
+
+ // Generate the output for this sub field and row
+ $row_output[] = '' . $content . '';
+ }
+
+ // Skip empty rows
+ if (count($row_output) == 0)
+ {
+ continue 1;
+ }
+
+ $result .= '