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; } } /** * Return a sequential set of all fields that have data. This excludes wrapper composite fields * as well as heading / help text fields. */ public function dataFields() { if(!$this->sequentialSet) $this->collateDataFields($this->sequentialSet); return $this->sequentialSet; } public function saveableFields() { if(!$this->sequentialSaveableSet) $this->collateDataFields($this->sequentialSaveableSet, true); return $this->sequentialSaveableSet; } protected function flushFieldsCache() { $this->sequentialSet = null; $this->sequentialSaveableSet = null; } protected function collateDataFields(&$list, $saveableOnly = false) { foreach($this as $field) { if($field->isComposite()) $field->collateDataFields($list, $saveableOnly); if($saveableOnly) { $isIncluded = ($field->hasData() && !$field->isReadonly() && !$field->isDisabled()); } else { $isIncluded = ($field->hasData()); } if($isIncluded) { $name = $field->getName(); if(isset($list[$name])) { $errSuffix = ""; if($this->form) { $errSuffix = " in your '{$this->form->class}' form called '" . $this->form->Name() . "'"; } else { $errSuffix = ''; } user_error("collateDataFields() I noticed that a field called '$name' appears twice$errSuffix.", E_USER_ERROR); } $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. */ 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); } /** * 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 * or TabSet.Tab.Subtab. * This function will create any missing tabs. * @param array $fields An array of {@link FormField} objects. */ 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); } } } /** * 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 */ public function removeFieldFromTab($tabName, $fieldName) { $this->flushFieldsCache(); // Find the tab $tab = $this->findOrMakeTab($tabName); $tab->removeByName($fieldName); } /** * 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') */ public function removeFieldsFromTab($tabName, $fields) { $this->flushFieldsCache(); // Find the tab $tab = $this->findOrMakeTab($tabName); // Add the fields to the end of this set foreach($fields as $field) $tab->removeByName($field); } /** * 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. */ 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); } return; } $this->flushFieldsCache(); foreach($this->items as $i => $child) { if(is_object($child)){ $childName = $child->getName(); if(!$childName) $childName = $child->Title(); if(($childName == $fieldName) && (!$dataFieldOnly || $child->hasData())) { array_splice( $this->items, $i, 1 ); break; } else if($child->isComposite()) { $child->removeByName($fieldName, $dataFieldOnly); } } } } /** * 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 * @return boolean TRUE field was successfully replaced * FALSE field wasn't found, nothing changed */ public function replaceField($fieldName, $newField) { $this->flushFieldsCache(); foreach($this->items as $i => $field) { if(is_object($field)) { if($field->getName() == $fieldName && $field->hasData()) { $this->items[$i] = $newField; return true; } else if($field->isComposite()) { 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 * @return boolean */ public function renameField($fieldName, $newFieldTitle) { $field = $this->dataFieldByName($fieldName); if(!$field) return false; $field->setTitle($newFieldTitle); return $field->Title() == $newFieldTitle; } /** * @return boolean */ public function hasTabSet() { foreach($this->items as $i => $field) { if(is_object($field) && $field instanceof TabSet) { return true; } } return false; } /** * 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) { // Backwards compatibility measure: Allow rewriting of outdated tab paths $tabName = $this->rewriteTabPath($tabName); $parts = explode('.',$tabName); $last_idx = count($parts) - 1; // 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; $currentPointer = $currentPointer->fieldByName($part); // Create any missing tabs if(!$currentPointer) { if(is_a($parentPointer, '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->hasMethod('Name')) ? " named '{$parentPointer->getName()}'" : null; user_error("FieldList::addFieldToTab() Tried to add a tab to object" . " '{$parentPointer->class}'{$withName} - '$part' didn't exist.", E_USER_ERROR); } } } 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 * @return FormField */ public function fieldByName($name) { $name = $this->rewriteTabPath($name); if(strpos($name,'.') !== false) list($name, $remainder) = explode('.',$name,2); else $remainder = null; foreach($this->items as $child) { if(trim($name) == trim($child->getName()) || $name == $child->id) { if($remainder) { if($child->isComposite()) { return $child->fieldByName($remainder); } else { user_error("Trying to get field '$remainder' from non-composite field $child->class.$name", E_USER_WARNING); return null; } } else { return $child; } } } } /** * 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 * @return FormField instance */ public function dataFieldByName($name) { if($dataFields = $this->dataFields()) { foreach($dataFields as $child) { if(trim($name) == trim($child->getName()) || $name == $child->id) return $child; } } } /** * Inserts a field before a particular field in a FieldList. * * @param string $name Name of the field to insert before * @param FormField $item The form field to insert * @return FormField|false */ public function insertBefore($name, $item) { // Backwards compatibility for order of arguments if($name instanceof FormField) { list($item, $name) = array($name, $item); } $this->onBeforeInsert($item); $item->setContainerFieldList($this); $i = 0; foreach($this->items as $child) { if($name == $child->getName() || $name == $child->id) { array_splice($this->items, $i, 0, array($item)); return $item; } elseif($child->isComposite()) { $ret = $child->insertBefore($name, $item); if($ret) return $ret; } $i++; } return false; } /** * Inserts a field after a particular field in a FieldList. * * @param string $name Name of the field to insert after * @param FormField $item The form field to insert * @return FormField|false */ public function insertAfter($name, $item) { // Backwards compatibility for order of arguments if($name instanceof FormField) { list($item, $name) = array($name, $item); } $this->onBeforeInsert($item); $item->setContainerFieldList($this); $i = 0; foreach($this->items as $child) { if($name == $child->getName() || $name == $child->id) { array_splice($this->items, $i+1, 0, array($item)); return $item; } elseif($child->isComposite()) { $ret = $child->insertAfter($name, $item); if($ret) return $ret; } $i++; } 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. */ 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 */ public function setForm($form) { foreach($this as $field) { $field->setForm($form); } return $this; } /** * Load the given data into this form. * * @param data An map of data to load into the FieldList */ public function setValues($data) { foreach($this->dataFields() as $field) { $fieldName = $field->getName(); if(isset($data[$fieldName])) $field->setValue($data[$fieldName]); } return $this; } /** * Return all 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; } /** * Transform this FieldList with a given tranform method, * e.g. $this->transform(new ReadonlyTransformation()) * * @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 */ public function rootFieldList() { if($this->containerField) return $this->containerField->rootFieldList(); else 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()); } /** * Transform the named field into a readonly feld. * * @param string|FormField */ public function makeFieldReadonly($field) { $fieldName = ($field instanceof FormField) ? $field->getName() : $field; $srcField = $this->dataFieldByName($fieldName); $this->replaceField($fieldName, $srcField->performReadonlyTransformation()); } /** * 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. $fieldMap = array(); foreach($this->dataFields() as $field) $fieldMap[$field->getName()] = $field; // Iterate through the ordered list of names, building a new array to be put into $this->items. // While we're doing this, empty out $fieldMap so that we can keep track of leftovers. // Unrecognised field names are okay; just ignore them $fields = array(); 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. * * @param string|FormField * @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(is_object($field)) $field = $field->getName(); $i = 0; foreach($this->dataFields() as $child) { if($child->getName() == $field) return $i; $i++; } return false; } /** * Ordered list of regular expressions * matching a tab path, to their rewrite rule (through preg_replace()). * Mainly used for backwards compatibility. * Ensure that more specific rules are placed earlier in the list, * and that tabs with children are accounted for in the rule sets. * * Example: * $fields->setTabPathRewriting(array( * // Rewrite specific innermost tab * '/^Root\.Content\.Main$/' => 'Root.Content', * // Rewrite all innermost tabs * '/^Root\.Content\.([^.]+)$/' => 'Root.\\1', * )); * * @param array $rewrites */ public function setTabPathRewrites($rewrites) { $this->tabPathRewrites = $rewrites; } /** * @return array */ public function getTabPathRewrites() { return $this->tabPathRewrites; } /** * Support function for backwards compatibility purposes. * Caution: Volatile API, might be removed in 3.1 or later. * * @param String $tabname Path to a tab, e.g. "Root.Content.Main" * @return String Rewritten path, based on {@link tabPathRewrites} */ protected function rewriteTabPath($name) { foreach($this->getTabPathRewrites() as $regex => $replace) { if(preg_match($regex, $name)) { $newName = preg_replace($regex, $replace, $name); Deprecation::notice('3.0.0', sprintf( 'Using outdated tab path "%s", please use the new location "%s" instead', $name, $newName ), Deprecation::SCOPE_GLOBAL ); return $newName; } } // No match found, return original path return $name; } /** * Default template rendering of a FieldList will concatenate all FieldHolder values. */ public function forTemplate() { $output = ""; foreach($this as $field) { $output .= $field->FieldHolder(); } return $output; } }