0, 'name' => '', ); /** * Any fields that were removed from this instance are noted so that Fieldgroups::save() can delete unused data * * @var FieldsArray|null * */ protected $removedFields = null; /** * Array indexed by field_id containing an array of variables specific to the context of that field in this fieldgroup * * This context overrides the values set in the field when it doesn't have context. * */ protected $fieldContexts = array(); /** * Per WireArray interface, items added must be instances of Field * * #pw-internal * * @param $item * @return bool * */ public function isValidItem($item) { return is_object($item) && $item instanceof Field; } /** * Per WireArray interface, keys must be numeric * * #pw-internal * * @param int|string $key * @return bool * */ public function isValidKey($key) { return is_int($key) || ctype_digit("$key"); } /** * Per WireArray interface, the item key is it's ID * * #pw-internal * * @param $item * @return int|string * */ public function getItemKey($item) { return $item->id; } /** * Per WireArray interface, return a blank item * * #pw-internal * * @return Wire|Field * */ public function makeBlankItem() { return $this->wire(new Field()); } /** * Add a field to this Fieldgroup * * ~~~~~ * $field = $fields->get('body'); * $fieldgroup->add($field); * ~~~~~ * * #pw-group-manipulation * * @param Field|string $field Field object, field name or id. * @return $this * @throws WireException * */ public function add($field) { if(!is_object($field)) $field = $this->wire()->fields->get($field); if($field && $field instanceof Field) { if(!$field->id) { throw new WireException("You must save field '$field' before adding to Fieldgroup '$this->name'"); } parent::add($field); } else { // throw new WireException("Unable to add field '$field' to Fieldgroup '{$this->name}'"); } return $this; } /** * Remove a field from this fieldgroup * * Note that this must be followed up with a `$fieldgroup->save()` before it does anything destructive. * This method does nothing more than queue the removal. * * _Technical Details_ * Performs a deletion by finding all templates using this fieldgroup, then finding all pages using the template, then * calling upon the Fieldtype to delete them one at a time. This is a potentially expensive/time consuming method, and * may need further consideration. * * #pw-group-manipulation * * @param Field|string $field Field object or field name, or id. * @return bool True on success, false on failure. * */ public function remove($field) { if(!is_object($field)) $field = $this->wire('fields')->get($field); if(!$this->getField($field->id)) return false; if(!$field) return true; // Make note of any fields that were removed so that Fieldgroups::save() // can delete data for those fields if(is_null($this->removedFields)) $this->removedFields = $this->wire(new FieldsArray()); $this->removedFields->add($field); $this->trackChange("remove:$field", $field, null); // parent::remove($field->id); replaced with finishRemove() method below return true; } /** * Intended to be called by Fieldgroups::save() to complete the field removal * * This completes the removal process. The remove() method above only queues the removal but doesn't execute it. * Instead, Fieldgroups::save() calls this method to finish the removal. This is necessary because if remove() * removes the data from memory, then save() won't still have access to determine what related assets should * be removed. * * This method is for use by Fieldgroups::save() and not intended for API usage. * * #pw-internal * * @param Field $field * @return Fieldgroup|WireArray $this * */ public function finishRemove(Field $field) { return parent::remove($field->id); } /** * Remove a field without queueing it to be removed from database * * Removes a field from the fieldgroup without deleting any associated field data when fieldgroup * is saved to the database. This is useful in the API when you want to move a field around within * a fieldgroup, like when moving a field to a Fieldset within the Fieldgroup. * * #pw-group-manipulation * * @param Field|string|int $field Field object, name or id. * @return bool|Fieldgroup|WireArray * */ public function softRemove($field) { if(!is_object($field)) $field = $this->wire('fields')->get($field); if(!$this->getField($field->id)) return false; if(!$field) return true; return parent::remove($field->id); } /** * Clear all removed fields, for use by Fieldgroups::save * * #pw-internal * */ public function resetRemovedFields() { $this->removedFields = null; } /** * Get a field that is part of this fieldgroup * * Same as `Fieldgroup::get()` except that it only checks fields, not other properties of a fieldgroup. * Meaning, this is the preferred way to retrieve a Field from a Fieldgroup. * * #pw-group-retrieval * * @param string|int|Field $key Field object, name or id. * @param bool|string $useFieldgroupContext Optionally specify one of the following (default=false): * - `true` (boolean) Returned Field will be a clone of the original with context data set. * - Specify a namespace (string) to retrieve context within that namespace. * @return Field|null Field object when present in this Fieldgroup, or null if not. * */ public function getField($key, $useFieldgroupContext = false) { if(is_object($key) && $key instanceof Field) $key = $key->id; if(is_string($key) && ctype_digit("$key")) $key = (int) $key; if($this->isValidKey($key)) { $value = parent::get($key); } else { $value = null; foreach($this as $field) { if($field->name == $key) { $value = $field; break; } } } if($value && $useFieldgroupContext) { $value = clone $value; if(isset($this->fieldContexts[$value->id])) { $context = $this->fieldContexts[$value->id]; $namespace = is_string($useFieldgroupContext) ? self::contextNamespacePrefix . $useFieldgroupContext : ""; if($namespace && isset($context[$namespace]) && is_array($context[$namespace])) $context = $context[$namespace]; foreach($context as $k => $v) { // if(strpos($k, self::contextNamespacePrefix) === 0) continue; $value->set($k, $v); } } } if($useFieldgroupContext && $value) { $value->flags = $value->flags | Field::flagFieldgroupContext; $value->setQuietly('_contextFieldgroup', $this); } return $value; } /** * Does the given Field have context data available in this fieldgroup? * * A Field with context data is one that overrides one or more settings present with the Field * when it is outside the context of this Fieldgroup. For example, perhaps a Field has a * columnWidth setting of 100% in its global settings, but only 50% when used in this Fieldgroup. * * #pw-group-retrieval * * @param int|string|Field $field Field object, name or id * @param string $namespace Optional namespace string for context * @return bool True if additional context information is available, false if not. * */ public function hasFieldContext($field, $namespace = '') { if(is_object($field) && $field instanceof Field) $field = $field->id; if(is_string($field) && !ctype_digit($field)) { $field = $this->wire('fields')->get($field); $field = $field && $field->id ? $field->id : 0; } if(isset($this->fieldContexts[(int) $field])) { if($namespace) return isset($this->fieldContexts[(int) $field][self::contextNamespacePrefix . $namespace]); return true; } return false; } /** * Get a Field that is part of this Fieldgroup, in the context of this Fieldgroup. * * Returned Field will be a clone of the original with additional context data * already populated to it. * * #pw-group-retrieval * * @param string|int|Field $key Field object, name or id. * @param string $namespace Optional namespace string for context * @return Field|null * */ public function getFieldContext($key, $namespace = '') { return $this->getField($key, $namespace ? $namespace : true); } /** * Does this fieldgroup have the given field? * * #pw-group-retrieval * * @param string|int|Field $key Field object, name or id. * @return bool True if this Fieldgroup has the field, false if not. * */ public function hasField($key) { return $this->getField($key) !== null; } /** * Get a Fieldgroup property or a Field. * * It is preferable to use `Fieldgroup::getField()` to retrieve fields from the Fieldgroup because * the scope of this `get()` method means it can return more than just Field object. * * #pw-group-retrieval * * @param string|int $key Property name to retrieve, or Field name * @return Field|string|int|null|array * */ public function get($key) { if($key == 'fields') return $this; if($key == 'fields_id') { $values = array(); foreach($this as $field) $values[] = $field->id; return $values; } if($key == 'removedFields') return $this->removedFields; if(isset($this->settings[$key])) return $this->settings[$key]; $value = parent::get($key); if($value !== null) return $value; return $this->getField($key); } /** * Per HasLookupItems interface, add a Field to this Fieldgroup * * #pw-internal * * @param Saveable|Field $item * @param array $row * @return $this * */ public function addLookupItem($item, array &$row) { if($item) $this->add($item); if(!empty($row['data'])) { // set field context for this fieldgroup $data = $row['data']; if(is_string($data)) $data = wireDecodeJSON($data); if(!is_array($data)) $row['data'] = array(); $this->fieldContexts[(int) "$item"] = $data; } return $this; } /** * Set a fieldgroup property * * #pw-group-manipulation * * @param string $key Name of property to set * @param string|int|object $value Value of property * @return Fieldgroup|WireArray $this * @throws WireException if passed invalid data * */ public function set($key, $value) { if($key == 'data') return $this; // we don't have a data field here if($key === 'id') { $value = (int) $value; } else if($key === 'name') { $value = $this->wire()->sanitizer->templateName($value); } if(isset($this->settings[$key])) { if($this->trackChanges && $this->settings[$key] !== $value) { $this->trackChange($key, $this->settings[$key], $value); } $this->settings[$key] = $value; } else { return parent::set($key, $value); } return $this; } /** * Save this Fieldgroup to the database * * To hook into this, hook to `Fieldgroups::save()` instead. * * #pw-group-manipulation * * @return $this * */ public function save() { $this->wire('fieldgroups')->save($this); return $this; } /** * Fieldgroups always return their name when dereferenced as a string * */ public function __toString() { return $this->name; } /** * Per Saveable interface, get an array of data associated with the database table * * #pw-internal * * @return array * */ public function getTableData() { return $this->settings; } /** * Per Saveable interface: return data for external storage * * #pw-internal * */ public function getExportData() { /** @var Fieldgroups $fieldgroups */ $fieldgroups = $this->wire('fieldgroups'); return $fieldgroups->getExportData($this); } /** * Given an export data array, import it back to the class and return what happened * * Changes are not committed until the item is saved * * #pw-internal * * @param array $data * @return array Returns array( * [property_name] => array( * 'old' => 'old value', // old value, always a string * 'new' => 'new value', // new value, always a string * 'error' => 'error message or blank if no error' * ) * @throws WireException if given invalid data * */ public function setImportData(array $data) { /** @var Fieldgroups $fieldgroups */ $fieldgroups = $this->wire('fieldgroups'); return $fieldgroups->setImportData($this, $data); } /** * Per HasLookupItems interface, get a WireArray of Field instances associated with this Fieldgroup * * #pw-internal * */ public function getLookupItems() { return $this; } /** * Get all of the Inputfields for this Fieldgroup associated with the provided Page and populate them. * * #pw-group-retrieval * * @param Page $page Page that the Inputfields will be for. * @param string|array $contextStr Optional context string to append to all the Inputfield names, OR array of options. * - Optional context string is helpful for things like repeaters. * - You may instead specify associative array of any method arguments if preferred. * @param string|array $fieldName Limit to a particular fieldName(s) or field IDs (optional). * - If specifying a single field (name or ID) and it refers to a fieldset, then all fields in that fieldset will be included. * - If specifying an array of field names/IDs the returned InputfieldWrapper will maintain the requested order. * @param string $namespace Additional namespace for the Inputfield context (optional). * @param bool $flat Returns all Inputfields in a flattened InputfieldWrapper (default=true). * @return InputfieldWrapper Returns an InputfieldWrapper that acts as a container for multiple Inputfields. * */ public function getPageInputfields(Page $page, $contextStr = '', $fieldName = '', $namespace = '', $flat = true) { if(is_array($contextStr)) { // 2nd argument is instead an array of options $defaults = array( 'contextStr' => '', 'fieldName' => $fieldName, 'namespace' => $namespace, 'flat' => $flat, ); $options = $contextStr; $options = array_merge($defaults, $options); $contextStr = $options['contextStr']; $fieldName = $options['fieldName']; $namespace = $options['namespace']; $flat = $options['flat']; } $container = $this->wire(new InputfieldWrapper()); $containers = array(); $inFieldset = false; $inHiddenFieldset = false; $inModalGroup = ''; // for multiple named fields $multiMode = false; $fieldInputfields = array(); if(is_array($fieldName)) { // an array was specified for $fieldName if(count($fieldName) == 1) { // single field requested, revert to single field $fieldName = reset($fieldName); } else if(count($fieldName) == 0) { // blank array, no field name requested $fieldName = ''; } else { // multiple field names asked for, setup for retaining requested order $multiMode = true; foreach($fieldName as $name) { $field = $this->getField($name); if($field) $fieldInputfields[$field->id] = false; // placeholder } $fieldName = ''; } } foreach($this as $field) { // for named multi-field retrieval if($multiMode && !isset($fieldInputfields[$field->id])) continue; // get a clone in the context of this fieldgroup, if it has contextual settings if(isset($this->fieldContexts[$field->id])) $field = $this->getFieldContext($field->id, $namespace); if($inModalGroup) { // we are in a modal group that should be skipped since all the inputs require the modal if($field->name == $inModalGroup . "_END") { // exit modal group $inModalGroup = false; } else { // skip field continue; } } if($inHiddenFieldset) { // we are in a modal group that should be skipped since all the inputs require the modal if($field->name == $inHiddenFieldset . "_END") { $inHiddenFieldset = false; } else { continue; } } if($fieldName) { // limit to specific field name if($inFieldset) { // allow the field if($field->type instanceof FieldtypeFieldsetClose && $field->name == $fieldName . "_END") { // stop, as we've got all the fields we need break; } // allow } else if($field->name == $fieldName || (ctype_digit("$fieldName") && $field->id == $fieldName)) { // start allow fields if($field->type instanceof FieldtypeFieldsetOpen) { $container = $field->getInputfield($page, $contextStr); $inFieldset = true; continue; } else { // allow 1 field } } else { // disallow continue; } } else if($field->modal && $field->type instanceof FieldtypeFieldsetOpen) { // field requires modal $inModalGroup = $field->name; } else if($field->type instanceof FieldtypeFieldsetOpen && $field->collapsed == Inputfield::collapsedHidden) { $inHiddenFieldset = $field->name; continue; } else if(!$flat && $field->type instanceof FieldtypeFieldsetOpen) { // new fieldset in non-flat mode if($field->type instanceof FieldtypeFieldsetClose) { // restore back to previous container if(count($containers)) $container = array_pop($containers); } else { // start a new container $inputfield = $field->getInputfield($page, $contextStr); if(!$inputfield) $inputfield = $this->wire(new InputfieldWrapper()); if($inputfield->collapsed == Inputfield::collapsedHidden) continue; $container->add($inputfield); $containers[] = $container; $container = $inputfield; // container is now the child InputfieldWrapper } continue; } $inputfield = $field->getInputfield($page, $contextStr); if(!$inputfield) continue; if($inputfield->collapsed == Inputfield::collapsedHidden) continue; if(!$page instanceof NullPage) { $value = $page->get($field->name); $inputfield->setAttribute('value', $value); } if($multiMode) { $fieldInputfields[$field->id] = $inputfield; } else { $container->add($inputfield); } } if($multiMode) { // add to container in requested order foreach($fieldInputfields as $fieldID => $inputfield) { if($inputfield) $container->add($inputfield); } } return $container; } /** * Get a list of all templates using this Fieldgroup * * #pw-group-retrieval * * @return TemplatesArray * */ public function getTemplates() { /** @var Fieldgroups $fieldgroups */ $fieldgroups = $this->wire('fieldgroups'); return $fieldgroups->getTemplates($this); } /** * Get the number of templates using this Fieldgroup * * #pw-group-retrieval * * @return int * */ public function getNumTemplates() { return $this->wire('fieldgroups')->getNumTemplates($this); } /** * Alias of getNumTemplates() * * #pw-internal * * @return int * */ public function numTemplates() { return $this->getNumTemplates(); } /** * Return an array of context data for the given field ID * * #pw-internal * * @param int|null $field_id Field ID or omit to return all field contexts * @param string $namespace Optional namespace * @return array * */ public function getFieldContextArray($field_id = null, $namespace = '') { if(is_null($field_id)) return $this->fieldContexts; if(isset($this->fieldContexts[$field_id])) { if($namespace) { $namespace = self::contextNamespacePrefix . $namespace; if(isset($this->fieldContexts[$field_id][$namespace])) { return $this->fieldContexts[$field_id][$namespace]; } return array(); } else if(isset($this->fieldContexts[$field_id])) { return $this->fieldContexts[$field_id]; } } return array(); } /** * Set an array of context data for the given field ID * * #pw-internal * * @param int $field_id Field ID * @param array $data * @param string $namespace Optional namespace * */ public function setFieldContextArray($field_id, $data, $namespace = '') { if($namespace) { if(!isset($this->fieldContexts[$field_id])) $this->fieldContexts[$field_id] = array(); $namespace = self::contextNamespacePrefix . $namespace; $this->fieldContexts[$field_id][$namespace] = $data; } else { $this->fieldContexts[$field_id] = $data; } } /** * Save field contexts for this fieldgroup * * #pw-group-manipulation * * @return int Number of contexts saved * */ public function saveContext() { return $this->wire('fieldgroups')->saveContext($this); } }