2011-05-06 00:51:02 +02:00
|
|
|
<?php
|
2016-06-15 06:03:16 +02:00
|
|
|
|
2016-08-19 00:51:35 +02:00
|
|
|
namespace SilverStripe\Forms;
|
|
|
|
|
2016-10-14 03:30:05 +02:00
|
|
|
use SilverStripe\Dev\Deprecation;
|
2016-06-15 06:03:16 +02:00
|
|
|
use SilverStripe\ORM\ArrayList;
|
2016-08-19 00:51:35 +02:00
|
|
|
|
2011-05-06 00:51:02 +02:00
|
|
|
/**
|
|
|
|
* A list designed to hold form field instances.
|
|
|
|
*
|
2016-08-19 00:51:35 +02:00
|
|
|
* @method FormField[] getIterator()
|
2011-05-06 00:51:02 +02:00
|
|
|
*/
|
2016-11-29 00:31:16 +01:00
|
|
|
class FieldList extends ArrayList
|
|
|
|
{
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Cached flat representation of all fields in this set,
|
|
|
|
* including fields nested in {@link CompositeFields}.
|
|
|
|
*
|
|
|
|
* @uses self::collateDataFields()
|
|
|
|
* @var FormField[]
|
|
|
|
*/
|
|
|
|
protected $sequentialSet;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @var FormField[]
|
|
|
|
*/
|
|
|
|
protected $sequentialSaveableSet;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* If this fieldlist is owned by a parent field (e.g. CompositeField)
|
|
|
|
* this is the parent field.
|
|
|
|
*
|
2018-10-05 05:56:30 +02:00
|
|
|
* @var CompositeField
|
2016-11-29 00:31:16 +01:00
|
|
|
*/
|
|
|
|
protected $containerField;
|
|
|
|
|
2020-04-20 19:58:09 +02:00
|
|
|
public function __construct($items = [])
|
2016-11-29 00:31:16 +01:00
|
|
|
{
|
|
|
|
if (!is_array($items) || func_num_args() > 1) {
|
|
|
|
$items = func_get_args();
|
|
|
|
}
|
|
|
|
|
|
|
|
parent::__construct($items);
|
|
|
|
|
|
|
|
foreach ($items as $item) {
|
|
|
|
if ($item instanceof FormField) {
|
|
|
|
$item->setContainerFieldList($this);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public function __clone()
|
|
|
|
{
|
|
|
|
// Clone all fields in this list
|
|
|
|
foreach ($this->items as $key => $field) {
|
|
|
|
$this->items[$key] = clone $field;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-01-19 11:00:25 +01:00
|
|
|
/**
|
|
|
|
* Iterate over each field in the current list recursively
|
|
|
|
*
|
|
|
|
* @param callable $callback
|
|
|
|
*/
|
|
|
|
public function recursiveWalk(callable $callback)
|
|
|
|
{
|
|
|
|
$stack = $this->toArray();
|
|
|
|
while (!empty($stack)) {
|
|
|
|
/** @var FormField $field */
|
|
|
|
$field = array_shift($stack);
|
|
|
|
$callback($field);
|
|
|
|
if ($field instanceof CompositeField) {
|
|
|
|
$stack = array_merge($field->getChildren()->toArray(), $stack);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Return a flattened list of all fields
|
|
|
|
*
|
|
|
|
* @return static
|
|
|
|
*/
|
|
|
|
public function flattenFields()
|
|
|
|
{
|
|
|
|
$fields = [];
|
|
|
|
$this->recursiveWalk(function (FormField $field) use (&$fields) {
|
|
|
|
$fields[] = $field;
|
|
|
|
});
|
|
|
|
return static::create($fields);
|
|
|
|
}
|
|
|
|
|
2016-11-29 00:31:16 +01:00
|
|
|
/**
|
|
|
|
* Return a sequential set of all fields that have data. This excludes wrapper composite fields
|
|
|
|
* as well as heading / help text fields.
|
|
|
|
*
|
|
|
|
* @return FormField[]
|
|
|
|
*/
|
|
|
|
public function dataFields()
|
|
|
|
{
|
2017-01-19 11:00:25 +01:00
|
|
|
if (empty($this->sequentialSet)) {
|
|
|
|
$fields = [];
|
|
|
|
$this->recursiveWalk(function (FormField $field) use (&$fields) {
|
|
|
|
if (!$field->hasData()) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
$name = $field->getName();
|
|
|
|
if (isset($fields[$name])) {
|
|
|
|
$this->fieldNameError($field, __FUNCTION__);
|
|
|
|
}
|
|
|
|
$fields[$name] = $field;
|
|
|
|
});
|
|
|
|
$this->sequentialSet = $fields;
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
|
|
|
return $this->sequentialSet;
|
|
|
|
}
|
|
|
|
|
2016-11-29 13:45:41 +01:00
|
|
|
/**
|
|
|
|
* @return FormField[]
|
|
|
|
*/
|
2016-11-29 00:31:16 +01:00
|
|
|
public function saveableFields()
|
|
|
|
{
|
2017-01-19 11:00:25 +01:00
|
|
|
if (empty($this->sequentialSaveableSet)) {
|
|
|
|
$fields = [];
|
|
|
|
$this->recursiveWalk(function (FormField $field) use (&$fields) {
|
|
|
|
if (!$field->canSubmitValue()) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
$name = $field->getName();
|
|
|
|
if (isset($fields[$name])) {
|
|
|
|
$this->fieldNameError($field, __FUNCTION__);
|
|
|
|
}
|
|
|
|
$fields[$name] = $field;
|
|
|
|
});
|
|
|
|
$this->sequentialSaveableSet = $fields;
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
|
|
|
return $this->sequentialSaveableSet;
|
|
|
|
}
|
|
|
|
|
2017-01-19 11:00:25 +01:00
|
|
|
/**
|
|
|
|
* Return array of all field names
|
|
|
|
*
|
|
|
|
* @return array
|
|
|
|
*/
|
|
|
|
public function dataFieldNames()
|
|
|
|
{
|
2022-04-14 03:12:59 +02:00
|
|
|
return array_keys($this->dataFields() ?? []);
|
2017-01-19 11:00:25 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Trigger an error for duplicate field names
|
|
|
|
*
|
|
|
|
* @param FormField $field
|
|
|
|
* @param $functionName
|
|
|
|
*/
|
|
|
|
protected function fieldNameError(FormField $field, $functionName)
|
|
|
|
{
|
2021-03-31 15:55:50 +02:00
|
|
|
if ($field->getForm()) {
|
2017-01-19 11:00:25 +01:00
|
|
|
$errorSuffix = sprintf(
|
|
|
|
" in your '%s' form called '%s'",
|
2021-03-31 15:55:50 +02:00
|
|
|
get_class($field->getForm()),
|
|
|
|
$field->getForm()->getName()
|
2017-01-19 11:00:25 +01:00
|
|
|
);
|
|
|
|
} else {
|
|
|
|
$errorSuffix = '';
|
|
|
|
}
|
|
|
|
|
2020-09-25 02:09:37 +02:00
|
|
|
throw new \RuntimeException(sprintf(
|
|
|
|
"%s() I noticed that a field called '%s' appears twice%s",
|
|
|
|
$functionName,
|
|
|
|
$field->getName(),
|
|
|
|
$errorSuffix
|
|
|
|
));
|
2017-01-19 11:00:25 +01:00
|
|
|
}
|
|
|
|
|
2016-11-29 00:31:16 +01:00
|
|
|
protected function flushFieldsCache()
|
|
|
|
{
|
|
|
|
$this->sequentialSet = null;
|
|
|
|
$this->sequentialSaveableSet = null;
|
|
|
|
}
|
|
|
|
|
2017-01-19 11:00:25 +01:00
|
|
|
/**
|
2022-10-13 03:49:15 +02:00
|
|
|
* @deprecated 4.1.0 Please use dataFields or saveableFields instead
|
2017-01-19 11:00:25 +01:00
|
|
|
* @param $list
|
|
|
|
* @param bool $saveableOnly
|
|
|
|
*/
|
2016-11-29 00:31:16 +01:00
|
|
|
protected function collateDataFields(&$list, $saveableOnly = false)
|
|
|
|
{
|
2022-10-13 03:49:15 +02:00
|
|
|
Deprecation::notice('4.1.0', 'Please use dataFields or saveableFields instead');
|
2016-11-29 13:45:41 +01:00
|
|
|
if (!isset($list)) {
|
2020-04-20 19:58:09 +02:00
|
|
|
$list = [];
|
2016-11-29 13:45:41 +01:00
|
|
|
}
|
|
|
|
/** @var FormField $field */
|
2016-11-29 00:31:16 +01:00
|
|
|
foreach ($this as $field) {
|
|
|
|
if ($field instanceof CompositeField) {
|
|
|
|
$field->collateDataFields($list, $saveableOnly);
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($saveableOnly) {
|
2016-11-29 13:45:41 +01:00
|
|
|
$isIncluded = $field->canSubmitValue();
|
2016-11-29 00:31:16 +01:00
|
|
|
} else {
|
2016-11-29 13:45:41 +01:00
|
|
|
$isIncluded = $field->hasData();
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
|
|
|
if ($isIncluded) {
|
|
|
|
$name = $field->getName();
|
|
|
|
if (isset($list[$name])) {
|
|
|
|
if ($this->form) {
|
2017-05-25 01:55:12 +02:00
|
|
|
$formClass = get_class($this->form);
|
|
|
|
$errSuffix = " in your '{$formClass}' form called '" . $this->form->Name() . "'";
|
2016-11-29 00:31:16 +01:00
|
|
|
} else {
|
|
|
|
$errSuffix = '';
|
|
|
|
}
|
2020-09-25 02:09:37 +02:00
|
|
|
throw new \RuntimeException(
|
|
|
|
"collateDataFields() I noticed that a field called '$name' appears twice$errSuffix."
|
2016-11-29 00:31:16 +01:00
|
|
|
);
|
|
|
|
}
|
|
|
|
$list[$name] = $field;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Add an extra field to a tab within this FieldList.
|
|
|
|
* This is most commonly used when overloading getCMSFields()
|
|
|
|
*
|
|
|
|
* @param string $tabName The name of the tab or tabset. Subtabs can be referred to as TabSet.Tab
|
|
|
|
* or TabSet.Tab.Subtab. This function will create any missing tabs.
|
|
|
|
* @param FormField $field The {@link FormField} object to add to the end of that tab.
|
|
|
|
* @param string $insertBefore The name of the field to insert before. Optional.
|
2017-11-01 02:24:21 +01:00
|
|
|
*
|
|
|
|
* @return $this
|
2016-11-29 00:31:16 +01:00
|
|
|
*/
|
|
|
|
public function addFieldToTab($tabName, $field, $insertBefore = null)
|
|
|
|
{
|
|
|
|
// This is a cache that must be flushed
|
|
|
|
$this->flushFieldsCache();
|
|
|
|
|
|
|
|
// Find the tab
|
|
|
|
$tab = $this->findOrMakeTab($tabName);
|
|
|
|
|
|
|
|
// Add the field to the end of this set
|
|
|
|
if ($insertBefore) {
|
|
|
|
$tab->insertBefore($insertBefore, $field);
|
|
|
|
} else {
|
|
|
|
$tab->push($field);
|
|
|
|
}
|
2018-07-13 04:18:04 +02:00
|
|
|
|
2017-11-01 02:24:21 +01:00
|
|
|
return $this;
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Add a number of extra fields to a tab within this FieldList.
|
|
|
|
* This is most commonly used when overloading getCMSFields()
|
|
|
|
*
|
|
|
|
* @param string $tabName The name of the tab or tabset. Subtabs can be referred to as TabSet.Tab
|
2018-05-01 22:47:17 +02:00
|
|
|
* or TabSet.Tab.Subtab. This function will create any missing tabs.
|
2016-11-29 00:31:16 +01:00
|
|
|
* @param array $fields An array of {@link FormField} objects.
|
|
|
|
* @param string $insertBefore Name of field to insert before
|
2017-11-01 02:24:21 +01:00
|
|
|
*
|
|
|
|
* @return $this
|
2016-11-29 00:31:16 +01:00
|
|
|
*/
|
|
|
|
public function addFieldsToTab($tabName, $fields, $insertBefore = null)
|
|
|
|
{
|
|
|
|
$this->flushFieldsCache();
|
|
|
|
|
|
|
|
// Find the tab
|
|
|
|
$tab = $this->findOrMakeTab($tabName);
|
|
|
|
|
|
|
|
// Add the fields to the end of this set
|
|
|
|
foreach ($fields as $field) {
|
|
|
|
// Check if a field by the same name exists in this tab
|
|
|
|
if ($insertBefore) {
|
|
|
|
$tab->insertBefore($insertBefore, $field);
|
|
|
|
} elseif (($name = $field->getName()) && $tab->fieldByName($name)) {
|
|
|
|
// It exists, so we need to replace the old one
|
|
|
|
$this->replaceField($field->getName(), $field);
|
|
|
|
} else {
|
|
|
|
$tab->push($field);
|
|
|
|
}
|
|
|
|
}
|
2018-07-13 04:18:04 +02:00
|
|
|
|
2017-11-01 02:24:21 +01:00
|
|
|
return $this;
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Remove the given field from the given tab in the field.
|
|
|
|
*
|
|
|
|
* @param string $tabName The name of the tab
|
|
|
|
* @param string $fieldName The name of the field
|
2017-11-01 02:24:21 +01:00
|
|
|
*
|
|
|
|
* @return $this
|
2016-11-29 00:31:16 +01:00
|
|
|
*/
|
|
|
|
public function removeFieldFromTab($tabName, $fieldName)
|
|
|
|
{
|
|
|
|
$this->flushFieldsCache();
|
|
|
|
|
|
|
|
// Find the tab
|
2018-07-13 04:18:04 +02:00
|
|
|
$tab = $this->findTab($tabName);
|
|
|
|
if ($tab) {
|
|
|
|
$tab->removeByName($fieldName);
|
|
|
|
}
|
|
|
|
|
2017-11-01 02:24:21 +01:00
|
|
|
return $this;
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Removes a number of fields from a Tab/TabSet within this FieldList.
|
|
|
|
*
|
|
|
|
* @param string $tabName The name of the Tab or TabSet field
|
|
|
|
* @param array $fields A list of fields, e.g. array('Name', 'Email')
|
2017-11-01 02:24:21 +01:00
|
|
|
*
|
|
|
|
* @return $this
|
2016-11-29 00:31:16 +01:00
|
|
|
*/
|
|
|
|
public function removeFieldsFromTab($tabName, $fields)
|
|
|
|
{
|
|
|
|
$this->flushFieldsCache();
|
|
|
|
|
|
|
|
// Find the tab
|
2020-05-10 04:16:10 +02:00
|
|
|
if ($tab = $this->findTab($tabName)) {
|
|
|
|
// Remove the fields from this set
|
2018-07-13 04:18:04 +02:00
|
|
|
foreach ($fields as $field) {
|
|
|
|
$tab->removeByName($field);
|
|
|
|
}
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
2018-07-13 04:18:04 +02:00
|
|
|
|
2017-11-01 02:24:21 +01:00
|
|
|
return $this;
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Remove a field or fields from this FieldList by Name.
|
|
|
|
* The field could also be inside a CompositeField.
|
|
|
|
*
|
|
|
|
* @param string|array $fieldName The name of, or an array with the field(s) or tab(s)
|
|
|
|
* @param boolean $dataFieldOnly If this is true, then a field will only
|
|
|
|
* be removed if it's a data field. Dataless fields, such as tabs, will
|
|
|
|
* be left as-is.
|
2017-11-01 02:24:21 +01:00
|
|
|
*
|
|
|
|
* @return $this
|
2016-11-29 00:31:16 +01:00
|
|
|
*/
|
|
|
|
public function removeByName($fieldName, $dataFieldOnly = false)
|
|
|
|
{
|
|
|
|
if (!$fieldName) {
|
|
|
|
user_error('FieldList::removeByName() was called with a blank field name.', E_USER_WARNING);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Handle array syntax
|
|
|
|
if (is_array($fieldName)) {
|
|
|
|
foreach ($fieldName as $field) {
|
|
|
|
$this->removeByName($field, $dataFieldOnly);
|
|
|
|
}
|
2018-03-14 07:40:17 +01:00
|
|
|
return $this;
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
$this->flushFieldsCache();
|
|
|
|
foreach ($this as $i => $child) {
|
|
|
|
$childName = $child->getName();
|
|
|
|
if (!$childName) {
|
|
|
|
$childName = $child->Title();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (($childName == $fieldName) && (!$dataFieldOnly || $child->hasData())) {
|
2022-04-14 03:12:59 +02:00
|
|
|
array_splice($this->items, $i ?? 0, 1);
|
2016-11-29 00:31:16 +01:00
|
|
|
break;
|
|
|
|
} elseif ($child instanceof CompositeField) {
|
|
|
|
$child->removeByName($fieldName, $dataFieldOnly);
|
|
|
|
}
|
|
|
|
}
|
2018-07-13 04:18:04 +02:00
|
|
|
|
2017-11-01 02:24:21 +01:00
|
|
|
return $this;
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Replace a single field with another. Ignores dataless fields such as Tabs and TabSets
|
|
|
|
*
|
|
|
|
* @param string $fieldName The name of the field to replace
|
|
|
|
* @param FormField $newField The field object to replace with
|
2019-04-15 06:22:54 +02:00
|
|
|
* @param boolean $dataFieldOnly If this is true, then a field will only be replaced if it's a data field. Dataless
|
|
|
|
* fields, such as tabs, will be not be considered for replacement.
|
2020-07-23 12:40:14 +02:00
|
|
|
* @return bool TRUE field was successfully replaced
|
2016-11-29 00:31:16 +01:00
|
|
|
* FALSE field wasn't found, nothing changed
|
|
|
|
*/
|
2019-04-15 06:22:54 +02:00
|
|
|
public function replaceField($fieldName, $newField, $dataFieldOnly = true)
|
2016-11-29 00:31:16 +01:00
|
|
|
{
|
|
|
|
$this->flushFieldsCache();
|
|
|
|
foreach ($this as $i => $field) {
|
2019-04-15 06:22:54 +02:00
|
|
|
if ($field->getName() == $fieldName && (!$dataFieldOnly || $field->hasData())) {
|
2016-11-29 00:31:16 +01:00
|
|
|
$this->items[$i] = $newField;
|
|
|
|
return true;
|
|
|
|
} elseif ($field instanceof CompositeField) {
|
|
|
|
if ($field->replaceField($fieldName, $newField)) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Rename the title of a particular field name in this set.
|
|
|
|
*
|
|
|
|
* @param string $fieldName Name of field to rename title of
|
|
|
|
* @param string $newFieldTitle New title of field
|
2020-07-23 12:40:14 +02:00
|
|
|
* @return bool
|
2016-11-29 00:31:16 +01:00
|
|
|
*/
|
|
|
|
public function renameField($fieldName, $newFieldTitle)
|
|
|
|
{
|
|
|
|
$field = $this->dataFieldByName($fieldName);
|
|
|
|
if (!$field) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
$field->setTitle($newFieldTitle);
|
|
|
|
|
|
|
|
return $field->Title() == $newFieldTitle;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2020-07-23 12:40:14 +02:00
|
|
|
* @return bool
|
2016-11-29 00:31:16 +01:00
|
|
|
*/
|
|
|
|
public function hasTabSet()
|
|
|
|
{
|
|
|
|
foreach ($this->items as $i => $field) {
|
|
|
|
if (is_object($field) && $field instanceof TabSet) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2018-07-13 04:18:04 +02:00
|
|
|
/**
|
|
|
|
* Returns the specified tab object, if it exists
|
|
|
|
*
|
|
|
|
* @param string $tabName The tab to return, in the form "Tab.Subtab.Subsubtab".
|
|
|
|
* @return Tab|null The found or null
|
|
|
|
*/
|
|
|
|
public function findTab($tabName)
|
|
|
|
{
|
2022-04-14 03:12:59 +02:00
|
|
|
$parts = explode('.', $tabName ?? '');
|
|
|
|
$last_idx = count($parts ?? []) - 1;
|
2018-07-13 04:18:04 +02:00
|
|
|
|
|
|
|
$currentPointer = $this;
|
|
|
|
|
|
|
|
foreach ($parts as $k => $part) {
|
|
|
|
$parentPointer = $currentPointer;
|
|
|
|
/** @var FormField $currentPointer */
|
|
|
|
$currentPointer = $currentPointer->fieldByName($part);
|
|
|
|
}
|
|
|
|
|
|
|
|
return $currentPointer;
|
|
|
|
}
|
|
|
|
|
2016-11-29 00:31:16 +01:00
|
|
|
/**
|
|
|
|
* Returns the specified tab object, creating it if necessary.
|
|
|
|
*
|
|
|
|
* @todo Support recursive creation of TabSets
|
|
|
|
*
|
|
|
|
* @param string $tabName The tab to return, in the form "Tab.Subtab.Subsubtab".
|
|
|
|
* Caution: Does not recursively create TabSet instances, you need to make sure everything
|
|
|
|
* up until the last tab in the chain exists.
|
|
|
|
* @param string $title Natural language title of the tab. If {@link $tabName} is passed in dot notation,
|
|
|
|
* the title parameter will only apply to the innermost referenced tab.
|
|
|
|
* The title is only changed if the tab doesn't exist already.
|
|
|
|
* @return Tab The found or newly created Tab instance
|
|
|
|
*/
|
|
|
|
public function findOrMakeTab($tabName, $title = null)
|
|
|
|
{
|
2022-04-14 03:12:59 +02:00
|
|
|
$parts = explode('.', $tabName ?? '');
|
|
|
|
$last_idx = count($parts ?? []) - 1;
|
2016-11-29 00:31:16 +01:00
|
|
|
// We could have made this recursive, but I've chosen to keep all the logic code within FieldList rather than
|
|
|
|
// add it to TabSet and Tab too.
|
|
|
|
$currentPointer = $this;
|
|
|
|
foreach ($parts as $k => $part) {
|
|
|
|
$parentPointer = $currentPointer;
|
|
|
|
/** @var FormField $currentPointer */
|
|
|
|
$currentPointer = $currentPointer->fieldByName($part);
|
|
|
|
// Create any missing tabs
|
|
|
|
if (!$currentPointer) {
|
|
|
|
if ($parentPointer instanceof TabSet) {
|
|
|
|
// use $title on the innermost tab only
|
|
|
|
if ($k == $last_idx) {
|
|
|
|
$currentPointer = isset($title) ? new Tab($part, $title) : new Tab($part);
|
|
|
|
} else {
|
|
|
|
$currentPointer = new TabSet($part);
|
|
|
|
}
|
|
|
|
$parentPointer->push($currentPointer);
|
|
|
|
} else {
|
|
|
|
$withName = $parentPointer instanceof FormField
|
|
|
|
? " named '{$parentPointer->getName()}'"
|
|
|
|
: null;
|
2017-05-17 07:40:13 +02:00
|
|
|
$parentPointerClass = get_class($parentPointer);
|
2020-09-25 02:09:37 +02:00
|
|
|
throw new \InvalidArgumentException(
|
2017-05-17 07:40:13 +02:00
|
|
|
"FieldList::addFieldToTab() Tried to add a tab to object"
|
2020-09-25 02:09:37 +02:00
|
|
|
. " '{$parentPointerClass}'{$withName} - '{$part}' didn't exist."
|
2017-05-17 07:40:13 +02:00
|
|
|
);
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return $currentPointer;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns a named field.
|
|
|
|
* You can use dot syntax to get fields from child composite fields
|
|
|
|
*
|
|
|
|
* @todo Implement similarly to dataFieldByName() to support nested sets - or merge with dataFields()
|
|
|
|
*
|
|
|
|
* @param string $name
|
2020-07-23 12:40:14 +02:00
|
|
|
* @return FormField|null
|
2016-11-29 00:31:16 +01:00
|
|
|
*/
|
|
|
|
public function fieldByName($name)
|
|
|
|
{
|
2021-05-20 10:07:44 +02:00
|
|
|
$fullName = $name;
|
2022-04-14 03:12:59 +02:00
|
|
|
if (strpos($name ?? '', '.') !== false) {
|
|
|
|
list($name, $remainder) = explode('.', $name ?? '', 2);
|
2016-11-29 00:31:16 +01:00
|
|
|
} else {
|
|
|
|
$remainder = null;
|
|
|
|
}
|
|
|
|
|
|
|
|
foreach ($this as $child) {
|
2022-04-14 03:12:59 +02:00
|
|
|
if (trim($fullName ?? '') == trim($child->getName() ?? '') || $fullName == $child->id) {
|
2021-05-20 10:07:44 +02:00
|
|
|
return $child;
|
2022-04-14 03:12:59 +02:00
|
|
|
} elseif (trim($name ?? '') == trim($child->getName() ?? '') || $name == $child->id) {
|
2016-11-29 00:31:16 +01:00
|
|
|
if ($remainder) {
|
|
|
|
if ($child instanceof CompositeField) {
|
|
|
|
return $child->fieldByName($remainder);
|
|
|
|
} else {
|
2017-05-17 07:40:13 +02:00
|
|
|
$childClass = get_class($child);
|
2016-11-29 00:31:16 +01:00
|
|
|
user_error(
|
2017-05-17 07:40:13 +02:00
|
|
|
"Trying to get field '{$remainder}' from non-composite field {$childClass}.{$name}",
|
2016-11-29 00:31:16 +01:00
|
|
|
E_USER_WARNING
|
|
|
|
);
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
return $child;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns a named field in a sequential set.
|
|
|
|
* Use this if you're using nested FormFields.
|
|
|
|
*
|
|
|
|
* @param string $name The name of the field to return
|
2020-07-23 12:40:14 +02:00
|
|
|
* @return FormField|null
|
2016-11-29 00:31:16 +01:00
|
|
|
*/
|
|
|
|
public function dataFieldByName($name)
|
|
|
|
{
|
|
|
|
if ($dataFields = $this->dataFields()) {
|
|
|
|
foreach ($dataFields as $child) {
|
2022-04-14 03:12:59 +02:00
|
|
|
if (trim($name ?? '') == trim($child->getName() ?? '') || $name == $child->id) {
|
2016-11-29 00:31:16 +01:00
|
|
|
return $child;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Inserts a field before a particular field in a FieldList.
|
2021-12-13 09:05:33 +01:00
|
|
|
* Will traverse CompositeFields depth-first to find the matching $name, and insert before the first match
|
2016-11-29 00:31:16 +01:00
|
|
|
*
|
|
|
|
* @param string $name Name of the field to insert before
|
|
|
|
* @param FormField $item The form field to insert
|
2018-10-01 07:20:04 +02:00
|
|
|
* @param bool $appendIfMissing Append to the end of the list if $name isn't found
|
|
|
|
* @return FormField|false Field if it was successfully inserted, false if not inserted
|
2016-11-29 00:31:16 +01:00
|
|
|
*/
|
2018-10-01 07:20:04 +02:00
|
|
|
public function insertBefore($name, $item, $appendIfMissing = true)
|
2016-11-29 00:31:16 +01:00
|
|
|
{
|
|
|
|
// Backwards compatibility for order of arguments
|
|
|
|
if ($name instanceof FormField) {
|
|
|
|
Deprecation::notice('5.0', 'Incorrect order of arguments for insertBefore');
|
2020-04-20 19:58:09 +02:00
|
|
|
list($item, $name) = [$name, $item];
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
|
|
|
$this->onBeforeInsert($item);
|
|
|
|
$item->setContainerFieldList($this);
|
|
|
|
|
|
|
|
$i = 0;
|
|
|
|
foreach ($this as $child) {
|
|
|
|
if ($name == $child->getName() || $name == $child->id) {
|
2022-04-14 03:12:59 +02:00
|
|
|
array_splice($this->items, $i ?? 0, 0, [$item]);
|
2016-11-29 00:31:16 +01:00
|
|
|
return $item;
|
|
|
|
} elseif ($child instanceof CompositeField) {
|
2018-10-01 07:20:04 +02:00
|
|
|
$ret = $child->insertBefore($name, $item, false);
|
2016-11-29 00:31:16 +01:00
|
|
|
if ($ret) {
|
|
|
|
return $ret;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
$i++;
|
|
|
|
}
|
|
|
|
|
2018-10-01 07:20:04 +02:00
|
|
|
// $name not found, append if needed
|
|
|
|
if ($appendIfMissing) {
|
|
|
|
$this->push($item);
|
|
|
|
return $item;
|
|
|
|
}
|
|
|
|
|
2016-11-29 00:31:16 +01:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Inserts a field after a particular field in a FieldList.
|
2021-12-13 09:05:33 +01:00
|
|
|
* Will traverse CompositeFields depth-first to find the matching $name, and insert after the first match
|
2016-11-29 00:31:16 +01:00
|
|
|
*
|
|
|
|
* @param string $name Name of the field to insert after
|
|
|
|
* @param FormField $item The form field to insert
|
2018-10-01 07:20:04 +02:00
|
|
|
* @param bool $appendIfMissing Append to the end of the list if $name isn't found
|
|
|
|
* @return FormField|false Field if it was successfully inserted, false if not inserted
|
2016-11-29 00:31:16 +01:00
|
|
|
*/
|
2018-10-01 07:20:04 +02:00
|
|
|
public function insertAfter($name, $item, $appendIfMissing = true)
|
2016-11-29 00:31:16 +01:00
|
|
|
{
|
|
|
|
// Backwards compatibility for order of arguments
|
|
|
|
if ($name instanceof FormField) {
|
|
|
|
Deprecation::notice('5.0', 'Incorrect order of arguments for insertAfter');
|
2020-04-20 19:58:09 +02:00
|
|
|
list($item, $name) = [$name, $item];
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
|
|
|
$this->onBeforeInsert($item);
|
|
|
|
$item->setContainerFieldList($this);
|
|
|
|
|
|
|
|
$i = 0;
|
|
|
|
foreach ($this as $child) {
|
|
|
|
if ($name == $child->getName() || $name == $child->id) {
|
2020-04-20 19:58:09 +02:00
|
|
|
array_splice($this->items, $i+1, 0, [$item]);
|
2016-11-29 00:31:16 +01:00
|
|
|
return $item;
|
|
|
|
} elseif ($child instanceof CompositeField) {
|
2018-10-01 07:20:04 +02:00
|
|
|
$ret = $child->insertAfter($name, $item, false);
|
2016-11-29 00:31:16 +01:00
|
|
|
if ($ret) {
|
|
|
|
return $ret;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
$i++;
|
|
|
|
}
|
|
|
|
|
2018-10-01 07:20:04 +02:00
|
|
|
// $name not found, append if needed
|
|
|
|
if ($appendIfMissing) {
|
|
|
|
$this->push($item);
|
|
|
|
return $item;
|
|
|
|
}
|
|
|
|
|
2016-11-29 00:31:16 +01:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Push a single field onto the end of this FieldList instance.
|
|
|
|
*
|
|
|
|
* @param FormField $item The FormField to add
|
|
|
|
*/
|
|
|
|
public function push($item)
|
|
|
|
{
|
|
|
|
$this->onBeforeInsert($item);
|
|
|
|
$item->setContainerFieldList($this);
|
|
|
|
|
|
|
|
return parent::push($item);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Push a single field onto the beginning of this FieldList instance.
|
|
|
|
*
|
|
|
|
* @param FormField $item The FormField to add
|
|
|
|
*/
|
|
|
|
public function unshift($item)
|
|
|
|
{
|
|
|
|
$this->onBeforeInsert($item);
|
|
|
|
$item->setContainerFieldList($this);
|
|
|
|
|
|
|
|
return parent::unshift($item);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handler method called before the FieldList is going to be manipulated.
|
|
|
|
*
|
|
|
|
* @param FormField $item
|
|
|
|
*/
|
|
|
|
protected function onBeforeInsert($item)
|
|
|
|
{
|
|
|
|
$this->flushFieldsCache();
|
|
|
|
|
|
|
|
if ($item->getName()) {
|
|
|
|
$this->rootFieldList()->removeByName($item->getName(), true);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Set the Form instance for this FieldList.
|
|
|
|
*
|
|
|
|
* @param Form $form The form to set this FieldList to
|
|
|
|
* @return $this
|
|
|
|
*/
|
|
|
|
public function setForm($form)
|
|
|
|
{
|
|
|
|
foreach ($this as $field) {
|
|
|
|
$field->setForm($form);
|
|
|
|
}
|
|
|
|
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Load the given data into this form.
|
|
|
|
*
|
|
|
|
* @param array $data An map of data to load into the FieldList
|
|
|
|
* @return $this
|
|
|
|
*/
|
|
|
|
public function setValues($data)
|
|
|
|
{
|
|
|
|
foreach ($this->dataFields() as $field) {
|
|
|
|
$fieldName = $field->getName();
|
|
|
|
if (isset($data[$fieldName])) {
|
|
|
|
$field->setValue($data[$fieldName]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Return all <input type="hidden"> fields
|
|
|
|
* in a form - including fields nested in {@link CompositeFields}.
|
|
|
|
* Useful when doing custom field layouts.
|
|
|
|
*
|
|
|
|
* @return FieldList
|
|
|
|
*/
|
|
|
|
public function HiddenFields()
|
|
|
|
{
|
|
|
|
$hiddenFields = new FieldList();
|
|
|
|
$dataFields = $this->dataFields();
|
|
|
|
|
|
|
|
if ($dataFields) {
|
|
|
|
foreach ($dataFields as $field) {
|
|
|
|
if ($field instanceof HiddenField) {
|
|
|
|
$hiddenFields->push($field);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return $hiddenFields;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Return all fields except for the hidden fields.
|
|
|
|
* Useful when making your own simplified form layouts.
|
|
|
|
*/
|
|
|
|
public function VisibleFields()
|
|
|
|
{
|
|
|
|
$visibleFields = new FieldList();
|
|
|
|
|
|
|
|
foreach ($this as $field) {
|
|
|
|
if (!($field instanceof HiddenField)) {
|
|
|
|
$visibleFields->push($field);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return $visibleFields;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2021-12-13 09:05:33 +01:00
|
|
|
* Transform this FieldList with a given transform method,
|
2016-11-29 00:31:16 +01:00
|
|
|
* e.g. $this->transform(new ReadonlyTransformation())
|
|
|
|
*
|
|
|
|
* @param FormTransformation $trans
|
|
|
|
* @return FieldList
|
|
|
|
*/
|
|
|
|
public function transform($trans)
|
|
|
|
{
|
|
|
|
$this->flushFieldsCache();
|
|
|
|
$newFields = new FieldList();
|
|
|
|
foreach ($this as $field) {
|
|
|
|
$newFields->push($field->transform($trans));
|
|
|
|
}
|
|
|
|
return $newFields;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the root field set that this belongs to
|
|
|
|
*
|
|
|
|
* @return FieldList|FormField
|
|
|
|
*/
|
|
|
|
public function rootFieldList()
|
|
|
|
{
|
|
|
|
if ($this->containerField) {
|
|
|
|
return $this->containerField->rootFieldList();
|
|
|
|
}
|
|
|
|
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2018-10-05 05:56:30 +02:00
|
|
|
* @return CompositeField|null
|
|
|
|
*/
|
|
|
|
public function getContainerField()
|
|
|
|
{
|
|
|
|
return $this->containerField;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param CompositeField|null $field
|
2016-11-29 00:31:16 +01:00
|
|
|
* @return $this
|
|
|
|
*/
|
|
|
|
public function setContainerField($field)
|
|
|
|
{
|
|
|
|
$this->containerField = $field;
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Transforms this FieldList instance to readonly.
|
|
|
|
*
|
|
|
|
* @return FieldList
|
|
|
|
*/
|
|
|
|
public function makeReadonly()
|
|
|
|
{
|
|
|
|
return $this->transform(new ReadonlyTransformation());
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2017-11-07 03:51:13 +01:00
|
|
|
* Transform the named field into a readonly field.
|
2016-11-29 00:31:16 +01:00
|
|
|
*
|
2017-11-07 03:51:13 +01:00
|
|
|
* @param string|array|FormField $field
|
2016-11-29 00:31:16 +01:00
|
|
|
*/
|
|
|
|
public function makeFieldReadonly($field)
|
|
|
|
{
|
2017-11-07 03:51:13 +01:00
|
|
|
if (!is_array($field)) {
|
|
|
|
$field = [$field];
|
|
|
|
}
|
|
|
|
|
|
|
|
foreach ($field as $item) {
|
|
|
|
$fieldName = ($item instanceof FormField) ? $item->getName() : $item;
|
|
|
|
$srcField = $this->dataFieldByName($fieldName);
|
|
|
|
if ($srcField) {
|
|
|
|
$this->replaceField($fieldName, $srcField->performReadonlyTransformation());
|
|
|
|
} else {
|
|
|
|
user_error("Trying to make field '$fieldName' readonly, but it does not exist in the list", E_USER_WARNING);
|
|
|
|
}
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Change the order of fields in this FieldList by specifying an ordered list of field names.
|
|
|
|
* This works well in conjunction with SilverStripe's scaffolding functions: take the scaffold, and
|
|
|
|
* shuffle the fields around to the order that you want.
|
|
|
|
*
|
|
|
|
* Please note that any tabs or other dataless fields will be clobbered by this operation.
|
|
|
|
*
|
|
|
|
* @param array $fieldNames Field names can be given as an array, or just as a list of arguments.
|
|
|
|
*/
|
|
|
|
public function changeFieldOrder($fieldNames)
|
|
|
|
{
|
|
|
|
// Field names can be given as an array, or just as a list of arguments.
|
|
|
|
if (!is_array($fieldNames)) {
|
|
|
|
$fieldNames = func_get_args();
|
|
|
|
}
|
|
|
|
|
|
|
|
// Build a map of fields indexed by their name. This will make the 2nd step much easier.
|
2020-04-20 19:58:09 +02:00
|
|
|
$fieldMap = [];
|
2016-11-29 00:31:16 +01:00
|
|
|
foreach ($this->dataFields() as $field) {
|
|
|
|
$fieldMap[$field->getName()] = $field;
|
|
|
|
}
|
|
|
|
|
2017-12-14 01:50:52 +01:00
|
|
|
// Iterate through the ordered list of names, building a new array to be put into $this->items.
|
2016-11-29 00:31:16 +01:00
|
|
|
// While we're doing this, empty out $fieldMap so that we can keep track of leftovers.
|
|
|
|
// Unrecognised field names are okay; just ignore them
|
2020-04-20 19:58:09 +02:00
|
|
|
$fields = [];
|
2016-11-29 00:31:16 +01:00
|
|
|
foreach ($fieldNames as $fieldName) {
|
|
|
|
if (isset($fieldMap[$fieldName])) {
|
|
|
|
$fields[] = $fieldMap[$fieldName];
|
|
|
|
unset($fieldMap[$fieldName]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Add the leftover fields to the end of the list.
|
|
|
|
$fields = array_values($fields + $fieldMap);
|
|
|
|
|
|
|
|
// Update our internal $this->items parameter.
|
|
|
|
$this->items = $fields;
|
|
|
|
|
|
|
|
$this->flushFieldsCache();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Find the numerical position of a field within
|
|
|
|
* the children collection. Doesn't work recursively.
|
|
|
|
*
|
2020-12-21 22:23:23 +01:00
|
|
|
* @param string|FormField $field
|
2016-11-29 00:31:16 +01:00
|
|
|
* @return int Position in children collection (first position starts with 0).
|
|
|
|
* Returns FALSE if the field can't be found.
|
|
|
|
*/
|
|
|
|
public function fieldPosition($field)
|
|
|
|
{
|
|
|
|
if ($field instanceof FormField) {
|
|
|
|
$field = $field->getName();
|
|
|
|
}
|
|
|
|
|
|
|
|
$i = 0;
|
|
|
|
foreach ($this->dataFields() as $child) {
|
|
|
|
if ($child->getName() == $field) {
|
|
|
|
return $i;
|
|
|
|
}
|
|
|
|
$i++;
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Default template rendering of a FieldList will concatenate all FieldHolder values.
|
|
|
|
*/
|
|
|
|
public function forTemplate()
|
|
|
|
{
|
|
|
|
$output = "";
|
|
|
|
foreach ($this as $field) {
|
|
|
|
$output .= $field->FieldHolder();
|
|
|
|
}
|
|
|
|
return $output;
|
|
|
|
}
|
2012-03-24 04:04:52 +01:00
|
|
|
}
|