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 .= '
  • ' . implode(', ', $row_output) . '
  • '; +} +?> + + +
      + +
    +