. * * #pw-summary A type of Inputfield that contains other Inputfield objects as children. Commonly a form or a fieldset. * * InputfieldWrapper is not designed to render an Inputfield specifically, but you can set a value attribute * containing content that will be rendered before the wrapper. * * @property bool $renderValueMode True when only rendering values, i.e. no inputs (default=false). #pw-internal * @property bool $quietMode True to suppress label, description and notes, often combined with renderValueMode (default=false). #pw-internal * @property int $columnWidthSpacing Percentage spacing between columns or 0 for none. Default pulled from `$config->inputfieldColumnWidthSpacing`. #pw-internal * @property bool $useDependencies Whether or not to consider `showIf` and `requiredIf` dependencies during processing (default=true). #pw-internal * @property bool|null $InputfieldWrapper_isPreRendered Whether or not children have been pre-rendered (internal use only) #pw-internal * @property InputfieldsArray|null $children Inputfield instances that are direct children of this InputfieldWrapper. #pw-group-properties * * @method string renderInputfield(Inputfield $inputfield, $renderValueMode = false) #pw-group-output * @method Inputfield new($typeName, $name = '', $label = '', array $settings = []) #pw-group-manipulation * @method bool allowProcessInput(Inputfield $inputfield) Allow Inputfield to have input processed? (3.0.207+) #pw-internal * * @property InputfieldAsmSelect $InputfieldAsmSelect * @property InputfieldButton $InputfieldButton * @property InputfieldCheckbox $InputfieldCheckbox * @property InputfieldCheckboxes $InputfieldCheckboxes * @property InputfieldCKEditor $InputfieldCkeditor * @property InputfieldCommentsAdmin $InputfieldCommentsAdmin * @property InputfieldDatetime $InputfieldDatetime * @property InputfieldEmail $InputfieldEmail * @property InputfieldFieldset $InputfieldFieldset * @property InputfieldFieldsetClose $InputfieldlFieldsetClose * @property InputfieldFieldsetOpen $InputfieldFieldsetOpen * @property InputfieldFieldsetTabOpen $InputfieldFieldsetTabOpen * @property InputfieldFile $InputfieldFile * @property InputfieldFloat $InputfieldFloat * @property InputfieldForm $InputfieldForm * @property InputfieldHidden $InputfieldHidden * @property InputfieldIcon $InputfieldIcon * @property InputfieldImage $InputfieldImage * @property InputfieldInteger $InputfieldInteger * @property InputfieldMarkup $InputfieldMarkup * @property InputfieldName $InputfieldName * @property InputfieldPage $InputfieldPage * @property InputfieldPageAutocomplete $InputfieldPageAutocomplete * @property InputfieldPageListSelect $InputfieldPageListSelect * @property InputfieldPageListSelectMultiple $InputfieldPageListSelectMultiple * @property InputfieldPageName $InputfieldPageName * @property InputfieldPageTable $InputfieldPageTable * @property InputfieldPageTitle $InputfieldPageTitle * @property InputfieldPassword $InputfieldPassword * @property InputfieldRadios $InputfieldRadios * @property InputfieldRepeater $InputfieldRepeater * @property InputfieldSelect $InputfieldSelect * @property InputfieldSelectMultiple $InputfieldSelectMultiple * @property InputfieldSelector $InputfieldSelector * @property InputfieldSubmit $InputfieldSubmit * @property InputfieldText $InputfieldText * @property InputfieldTextarea $InputfieldTextarea * @property InputfieldTextTags $InputfieldTextTags * @property InputfieldToggle $InputfieldToggle * @property InputfieldURL $InputfieldURL * @property InputfieldWrapper $InputfieldWrapper * */ class InputfieldWrapper extends Inputfield implements \Countable, \IteratorAggregate { /** * Set to true for debugging optimization of property accesses * * #pw-internal * */ const debugPropertyAccess = false; /** * Markup used during the render() method - customize with InputfieldWrapper::setMarkup($array) * */ static protected $defaultMarkup = array( 'list' => "", 'item' => "
  • {out}
  • ", 'item_label' => "", 'item_label_hidden' => "", 'item_content' => "
    {out}
    ", 'item_error' => "

    {out}

    ", 'item_description' => "

    {out}

    ", 'item_head' => "

    {out}

    ", 'item_notes' => "

    {out}

    ", 'item_detail' => "

    {out}

    ", 'item_icon' => " ", 'item_toggle' => "", // ALSO: // InputfieldAnything => array(any of the properties above to override on a per-Inputfield basis) ); static protected $markup = array(); /** * Classes used during the render() method - customize with InputfieldWrapper::setClasses($array) * */ static protected $defaultClasses = array( 'form' => '', // additional clases for InputfieldForm (optional) 'list' => 'Inputfields', 'list_clearfix' => 'ui-helper-clearfix', 'item' => 'Inputfield {class} Inputfield_{name} ui-widget', 'item_label' => '', // additional classes for InputfieldHeader (optional) 'item_content' => '', // additional classes for InputfieldContent (optional) 'item_required' => 'InputfieldStateRequired', // class is for Inputfield 'item_error' => 'ui-state-error InputfieldStateError', // note: not the same as markup[item_error], class is for Inputfield 'item_collapsed' => 'InputfieldStateCollapsed', 'item_column_width' => 'InputfieldColumnWidth', 'item_column_width_first' => 'InputfieldColumnWidthFirst', 'item_show_if' => 'InputfieldStateShowIf', 'item_required_if' => 'InputfieldStateRequiredIf' // ALSO: // InputfieldAnything => array(any of the properties above to override on a per-Inputfield basis) ); static protected $classes = array(); /** * Instance of InputfieldsArray, if this Inputfield contains child Inputfields * * @var InputfieldsArray * */ protected $children; /** * Array of Inputfields that had their processing delayed by dependencies. * */ protected $delayedChildren = array(); /** * Label displayed when a value is required but missing * */ protected $requiredLabel = 'Missing required value'; /** * Whether or not column width is handled internally * * @var bool * */ protected $useColumnWidth = true; /** * Construct the Inputfield, setting defaults for all properties * */ public function __construct() { parent::__construct(); $this->children = new InputfieldsArray(); $this->set('skipLabel', Inputfield::skipLabelFor); $this->set('useDependencies', true); // whether or not to use consider field dependencies during processing $this->set('renderValueMode', false); $this->set('quietMode', false); // suppress label, description and notes $this->set('columnWidthSpacing', 0); } /** * Wired to API * */ public function wired() { $config = $this->wire()->config; $this->wire($this->children); $this->requiredLabel = $this->_('Missing required value'); $columnWidthSpacing = $config->inputfieldColumnWidthSpacing; $columnWidthSpacing = is_null($columnWidthSpacing) ? 1 : (int) $columnWidthSpacing; if($columnWidthSpacing > 0) $this->set('columnWidthSpacing', $columnWidthSpacing); $settings = $config->InputfieldWrapper; if(is_array($settings)) { foreach($settings as $key => $value) { if($key == 'requiredLabel') { $this->requiredLabel = $value; } else if($key == 'useColumnWidth') { $this->useColumnWidth = $value; } else { $this->set($key, $value); } } } parent::wired(); } /** * Get a child Inputfield having a name attribute matching the given $key. * * This method can also get settings, attributes or API variables, so long as they don't * collide with an Inputfield name. For that reason, you may prefer to use the `Inputfield::getSetting()`, * `Inputfield::attr()` or `Wire::wire()` methods for those other purposes. * * If you want a method that can only return a matching Inputfield object, use the * `InputfieldWrapper::getChildByName()` method . * * #pw-group-retrieval-and-traversal * * @param string $key Name of Inputfield or setting/property to retrieve. * @return Inputfield|mixed * @see InputfieldWrapper::getChildByName() * @throws WireException Only in core development/debugging, otherwise does not throw exceptions. * */ public function get($key) { $inputfield = $this->getChildByName($key); if($inputfield) return $inputfield; if(self::debugPropertyAccess) throw new WireException("Access of attribute or setting: $key"); $value = $this->wire($key); if($value) return $value; if($key === 'children') return $this->children(); if(($value = parent::get($key)) !== null) return $value; return null; } /** * Provides direct reference to attributes and settings, and falls back to Inputfield children * * This is different behavior from the get() method. * * @param string $key * @return mixed|null * */ public function __get($key) { if($key === 'children') return $this->children(); if(strpos($key, 'Inputfield') === 0 && strlen($key) > 10) { if($key === 'InputfieldWrapper') return $this->wire(new InputfieldWrapper()); $value = $this->wire()->modules->get($key); if($value instanceof Inputfield) return $value; if(wireClassExists($key)) return $this->wire(new $key()); $value = null; } $value = parent::get($key); if(is_null($value)) $value = $this->getChildByName($key); return $value; } /** * Add an Inputfield item as a child (also accepts array definition) * * Since 3.0.110: If given a string value, it is assumed to be an Inputfield type that you * want to add. In that case, it will create the Inputfield and return it instead of $this. * * #pw-group-manipulation * * @param Inputfield|array|string $item * @return Inputfield|InputfieldWrapper|$this * @see InputfieldWrapper::import() * */ public function add($item) { if(is_string($item)) { return $this->___new($item); } else if(is_array($item)) { $this->importArray($item); } else { $this->children()->add($item); $item->setParent($this); } return $this; } /** * Create a new Inputfield, add it to this InputfieldWrapper, and return the new Inputfield * * - Only the $typeName argument is required. * - You may optionally substitute the $settings argument for the $name or $label arguments. * - You may optionally substitute Inputfield “description” property for $settings argument. * * #pw-group-manipulation * * @param string $typeName Inputfield type, i.e. “InputfieldCheckbox” or just “checkbox” for short. * @param string|array $name Name of input (or substitute $settings here). * @param string|array $label Label for input (or substitute $settings here). * @param array|string $settings Settings to add to Inputfield (optional). Or if string, assumed to be “description”. * @return Inputfield|InputfieldSelect|InputfieldWrapper An Inputfield instance ready to populate with additional properties/attributes. * @throws WireException If you request an unknown Inputfield type * @since 3.0.110 * */ public function ___new($typeName, $name = '', $label = '', $settings = array()) { if(is_array($name)) { $settings = $name; $name = ''; } else if(is_array($label)) { $settings = $label; $label = ''; } if(strpos($typeName, 'Inputfield') !== 0) { $typeName = "Inputfield" . ucfirst($typeName); } /** @var Inputfield|InputfieldSelect|InputfieldWrapper $inputfield */ $inputfield = $this->wire('modules')->getModule($typeName); if(!$inputfield && wireClassExists($typeName)) { $inputfield = $this->wire(new $typeName()); } if(!$inputfield instanceof Inputfield) { throw new WireException("Unknown Inputfield type: $typeName"); } if(strlen($name)) $inputfield->attr('name', $name); if(strlen($label)) $inputfield->label = $label; if(is_array($settings)) { foreach($settings as $key => $value) { $inputfield->set($key, $value); } } else if(is_string($settings)) { $inputfield->description = $settings; } $this->add($inputfield); return $inputfield; } /** * Import the given Inputfield items as children * * If given an `InputfieldWrapper`, it will import the children of it and * exclude the wrapper itself. This is different from `InputfieldWrapper::add()` * in that add() would add the wrapper, not just the children. See also * the `InputfieldWrapper::importArray()` method. * * #pw-group-manipulation * * @param InputfieldWrapper|array|InputfieldsArray $items Wrapper containing items to add * @return $this * @throws WireException * @see InputfieldWrapper::add(), InputfieldWrapper::importArray() * */ public function import($items) { if($items instanceof InputfieldWrapper || $items instanceof InputfieldsArray) { foreach($items as $item) { $this->add($item); } } else if(is_array($items)) { $this->importArray($items); } else if($items instanceof Inputfield) { $this->add($items); } else { throw new WireException("InputfieldWrapper::import() requires InputfieldWrapper, InputfieldsArray, array, or Inputfield"); } return $this; } /** * Prepend an Inputfield to this instance’s children. * * #pw-group-manipulation * * @param Inputfield $item Item to prepend * @return $this * */ public function prepend(Inputfield $item) { $item->setParent($this); $this->children()->prepend($item); return $this; } /** * Append an Inputfield to this instance’s children. * * #pw-group-manipulation * * @param Inputfield $item Item to append * @return $this * */ public function append(Inputfield $item) { $item->setParent($this); $this->children()->append($item); return $this; } /** * Insert new or existing Inputfield before or after another * * @param Inputfield|array|string $item New or existing item Inputfield, name, or new item array to insert. * @param Inputfield|string $existingItem Existing item or item name you want to insert before. * @param bool $before True to insert before, false to insert after (default=false). * @return $this * @throws WireException * @since 3.0.196 * */ public function insert($item, $existingItem, $before = false) { $children = $this->children(); if($existingItem instanceof Inputfield) { // ok } else if(is_string($existingItem)) { $name = $existingItem; $existingItem = $this->getByName($name); if(!$existingItem) throw new WireException("Cannot find Inputfield[name=$name] to insert"); } else { throw new WireException('Invalid value for $existingItem argument'); } if(is_array($item)) { // new item definition if(isset($item['name'])) { // first check if there's an existing item with this name $f = $this->getByName($item['name']); if($f) return $this->insert($f, $existingItem, $before); } $nBefore = $children->count(); $this->add($item); $nAfter = $children->count(); if($nAfter > $nBefore) { // new item was added by the above $this->add() call $item = $children->last(); $children->remove($item); } else { throw new WireException('Unable to insert new item: ' . print_r($item, true)); } } else if(!$item instanceof Inputfield) { // get item to insert by name $name = (string) $item; $item = $this->getByName($name); if(!$item) { // if named item isn't found, create one $item = $this->___new('text', $name, $name); } } if($children->has($existingItem)) { // existing item is a direct child of this InputfieldWrapper $item->setParent($this); $method = $before ? 'insertBefore' : 'insertAfter'; $children->$method($item, $existingItem); } else { // find existing item recursively $f = $this->getByName($existingItem->attr('name')); if($f && $f->parent) { // existing item was found $existingItem = $f; } else { // existing item not found, add it as direct child $this->add($existingItem); } $existingItem->parent->insert($item, $existingItem, $before); } return $this; } /** * Insert one Inputfield before one that’s already there. * * Note: string or array values for either argument require 3.0.196+. * * ~~~~~ * // example 1: Get existing Inputfields and insert first_name before last_name * $firstName = $form->getByName('first_name'); * $lastName = $form->getByName('last_name'); * $form->insertBefore($firstName, $lastName); * * // example 2: Same as above but use Inputfield names (3.0.196+) * $form->insertBefore('first_name', 'last_name'); * * // example 3: Create new Inputfield and insert before last_name (3.0.196+) * $form->insertBefore([ 'type' => 'text', 'name' => 'first_name' ], 'last_name'); * ~~~~~ * * #pw-group-manipulation * * @param Inputfield|array|string $item Item to insert * @param Inputfield|string $existingItem Existing item you want to insert before. * @return $this * */ public function insertBefore($item, $existingItem) { return $this->insert($item, $existingItem, true); } /** * Insert one Inputfield after one that’s already there. * * Note: string or array values for either argument require 3.0.196+. * * ~~~~~ * // example 1: Get existing Inputfields, insert last_name after first_name * $lastName = $form->getByName('last_name'); * $firstName = $form->getByName('first_name'); * $form->insertAfter($lastName, $firstName); * * // example 2: Same as above but use Inputfield names (3.0.196+) * $form->insertBefore('last_name', 'first_name'); * * // example 3: Create new Inputfield and insert after first_name (3.0.196+) * $form->insertAfter([ 'type' => 'text', 'name' => 'last_name' ], 'first_name'); * ~~~~~ * * #pw-group-manipulation * * @param Inputfield|array|string $item Item to insert * @param Inputfield|string $existingItem Existing item you want to insert after. * @return $this * */ public function insertAfter($item, $existingItem) { return $this->insert($item, $existingItem, false); } /** * Remove an Inputfield from this instance’s children. * * #pw-group-manipulation * * @param Inputfield|string $key Inputfield object or name * @return $this * */ public function remove($key) { $item = $key; if(!$item) return $this; if(!$item instanceof Inputfield) { if(!is_string($item)) return $this; $item = $this->getChildByName($item); if(!$item) return $this; } if($this->children()->has($item)) { $this->children()->remove($item); } else if($this->getChildByName($item->attr('name')) && $item->parent) { $item->parent->remove($item); } return $this; } /** * Prepare children for rendering by creating any fieldset groups * */ protected function preRenderChildren() { if($this->getSetting('InputfieldWrapper_isPreRendered')) return $this->children(); $children = $this->wire(new InputfieldWrapper()); $wrappers = array($children); $prepend = array(); $append = array(); $numMove = 0; foreach($this->children() as $inputfield) { $wrapper = end($wrappers); if($inputfield instanceof InputfieldFieldsetClose) { if(count($wrappers) > 1) array_pop($wrappers); continue; } else if($inputfield instanceof InputfieldFieldsetOpen) { $inputfield->set('InputfieldWrapper_isPreRendered', true); $wrappers[] = $inputfield; } $inputfield->unsetParent(); $wrapper->add($inputfield); $flags = $inputfield->renderFlags; if($flags & Inputfield::renderFirst) { $prepend[] = $inputfield; $numMove++; } else if($flags & Inputfield::renderLast) { $append[] = $inputfield; $numMove++; } } if($numMove) { foreach($prepend as $f) { /** @var Inputfield $f */ $f->getParent()->prepend($f); } foreach($append as $f) { /** @var Inputfield $f */ $f->getParent()->append($f); } } return $children; } /** * Cached class parents indexed by Inputfield class name * * @var array * */ static protected $classParents = array(); /** * Get array of parent Inputfield classes for given Inputfield (excluding the base Inputfield class) * * @param Inputfield|string $inputfield * @return array * */ protected function classParents($inputfield) { $p = &self::$classParents; $c = is_object($inputfield) ? $inputfield->className() : $inputfield; if(!isset($p[$c])) { $p[$c] = array(); foreach(wireClassParents($inputfield) as $parentClass) { if(strpos($parentClass, 'Inputfield') !== 0 || $parentClass === 'Inputfield') break; $p[$c][] = $parentClass; } } return $p[$c]; } /** * Prepare Inputfield for attributes used during rendering * * #pw-internal * * @param Inputfield $inputfield * @param array $markup * @param array $classes * @since 3.0.144 * */ private function attributeInputfield(Inputfield $inputfield, &$markup, &$classes) { $inputfieldClass = $inputfield->className(); $markupTemplate = array('attr' => array(), 'wrapAttr' => array(), 'set' => array()); $markupKeys = array($inputfieldClass, "name=$inputfield->name", "id=$inputfield->id"); $classKeys = array('class', 'wrapClass', 'headerClass', 'contentClass'); $addClasses = array(); $attr = array(); $wrapAttr = array(); $sets = array(); foreach($markupKeys as $key) { if(isset($markup[$key])) $markup = array_merge($markup, $markup[$key]); if(isset($classes[$key])) $classes = array_merge($classes, $classes[$key]); } foreach(array_merge($this->classParents($inputfield), $markupKeys) as $key) { if(!isset($markup[$key])) continue; $markupParent = array_merge($markupTemplate, $markup[$key]); foreach($classKeys as $classKey) { if(!empty($markupParent[$classKey])) { $addClasses[$classKey] = $markupParent[$classKey]; } } foreach($markupParent['attr'] as $k => $v) { $attr[$k] = $v; } foreach($markupParent['wrapAttr'] as $k => $v) { $wrapAttr[$k] = $v; } foreach($markupParent['set'] as $k => $v) { $sets[$k] = $v; } } foreach($attr as $attrName => $attrVal) { $inputfield->attr($attrName, $attrVal); } foreach($wrapAttr as $attrName => $attrVal) { $inputfield->wrapAttr($attrName, $attrVal); } foreach($addClasses as $classKey => $class) { $inputfield->addClass($class, $classKey); } foreach($sets as $setName => $setVal) { $inputfield->set($setName, $setVal); } } /** * Render this Inputfield and the output of its children. * * #pw-group-output * * @todo this method has become too long/complex, move to its own pluggable class and split it up a lot * @return string * */ public function ___render() { $sanitizer = $this->wire()->sanitizer; $out = ''; $children = $this->preRenderChildren(); $columnWidthTotal = 0; $columnWidthSpacing = $this->getSetting('columnWidthSpacing'); $quietMode = $this->getSetting('quietMode'); $lastInputfield = null; $_markup = array_merge(self::$defaultMarkup, self::$markup); $_classes = array_merge(self::$defaultClasses, self::$classes); $markup = array(); $classes = array(); $useColumnWidth = $this->useColumnWidth; $renderAjaxInputfield = $this->wire()->config->ajax ? $this->wire()->input->get('renderInputfieldAjax') : null; $lockedStates = array( Inputfield::collapsedNoLocked, Inputfield::collapsedYesLocked, Inputfield::collapsedBlankLocked, Inputfield::collapsedTabLocked ); if($useColumnWidth === true && isset($_classes['form']) && strpos($_classes['form'], 'InputfieldFormNoWidths') !== false) { $useColumnWidth = false; } // show description for tabs $description = $quietMode ? '' : $this->getSetting('description'); if($description && wireClassExists("InputfieldFieldsetTabOpen") && $this instanceof InputfieldFieldsetTabOpen) { $out .= str_replace('{out}', nl2br($this->entityEncode($description, true)), $_markup['item_head']); } foreach($children as $inputfield) { /** @var Inputfield $inputfield */ if($renderAjaxInputfield && $inputfield->attr('id') !== $renderAjaxInputfield && !$inputfield instanceof InputfieldWrapper) { $skip = true; foreach($inputfield->getParents() as $parent) { /** @var InputfieldWrapper $parent */ if($parent->attr('id') === $renderAjaxInputfield) $skip = false; } if($skip && !empty($parents)) continue; } list($markup, $classes) = array($_markup, $_classes); $this->attributeInputfield($inputfield, $markup, $classes); $renderValueMode = $this->getSetting('renderValueMode'); $collapsed = (int) $inputfield->getSetting('collapsed'); $required = $inputfield->getSetting('required'); $requiredIf = $required ? $inputfield->getSetting('requiredIf') : false; $showIf = $inputfield->getSetting('showIf'); if($collapsed == Inputfield::collapsedHidden) continue; if(in_array($collapsed, $lockedStates)) $renderValueMode = true; $ffOut = $this->renderInputfield($inputfield, $renderValueMode); if(!strlen($ffOut)) continue; $collapsed = (int) $inputfield->getSetting('collapsed'); // retrieve again after render $entityEncodeText = $inputfield->getSetting('entityEncodeText') === false ? false : true; $errorsOut = ''; if(!$inputfield instanceof InputfieldWrapper) { $errors = $inputfield->getErrors(true); if(count($errors)) { $collapsed = $renderValueMode ? Inputfield::collapsedNoLocked : Inputfield::collapsedNo; $comma = $this->_(','); // Comma or other character to separate multiple error messages $errorsOut = implode("$comma ", $errors); } } else $errors = array(); foreach(array('error', 'description', 'head', 'notes', 'detail') as $property) { $text = $property == 'error' ? $errorsOut : $inputfield->getSetting($property); if($property === 'detail' && !is_string($text)) continue; // may not be necessary if(!empty($text) && !$quietMode) { if($entityEncodeText) { $text = $inputfield->entityEncode($text, true); } if($inputfield->textFormat != Inputfield::textFormatMarkdown) { $text = str_replace('{out}', nl2br($text), $markup["item_$property"]); } } else { $text = ''; } $_property = '{' . $property . '}'; if(strpos($markup['item_content'], $_property) !== false) { $markup['item_content'] = str_replace($_property, $text, $markup['item_content']); } else if(strpos($markup['item_label'], $_property) !== false) { $markup['item_label'] = str_replace($_property, $text, $markup['item_label']); } else if($text && ($property == 'notes' || $property == 'detail')) { $ffOut .= $text; } else if($text) { $ffOut = $text . $ffOut; } } if(!$quietMode) { $prependMarkup = $inputfield->getSetting('prependMarkup'); if($prependMarkup) $ffOut = $prependMarkup . $ffOut; $appendMarkup = $inputfield->getSetting('appendMarkup'); if($appendMarkup) $ffOut .= $appendMarkup; } // The inputfield classname is always used in its wrapping element $ffAttrs = array( 'class' => str_replace( array('{class}', '{name}'), array($inputfield->className(), $inputfield->attr('name') ), $classes['item']) ); if($inputfield instanceof InputfieldItemList) $ffAttrs['class'] .= " InputfieldItemList"; if($collapsed) $ffAttrs['class'] .= " collapsed$collapsed"; if(count($errors)) $ffAttrs['class'] .= ' ' . $classes['item_error']; if($required) $ffAttrs['class'] .= ' ' . $classes['item_required']; if(strlen($showIf) && !$this->getSetting('renderValueMode')) { // note: $this->renderValueMode (rather than $renderValueMode) is intentional $ffAttrs['data-show-if'] = $showIf; $ffAttrs['class'] .= ' ' . $classes['item_show_if']; } if(strlen($requiredIf)) { $ffAttrs['data-required-if'] = $requiredIf; $ffAttrs['class'] .= ' ' . $classes['item_required_if']; } if($collapsed && $collapsed !== Inputfield::collapsedNever) { $isEmpty = $inputfield->isEmpty(); if(($isEmpty && $inputfield instanceof InputfieldWrapper && $collapsed !== Inputfield::collapsedPopulated) || $collapsed === Inputfield::collapsedYes || $collapsed === Inputfield::collapsedYesLocked || $collapsed === true || $collapsed === Inputfield::collapsedYesAjax || ($isEmpty && $collapsed === Inputfield::collapsedBlank) || ($isEmpty && $collapsed === Inputfield::collapsedBlankAjax) || ($isEmpty && $collapsed === Inputfield::collapsedBlankLocked) || (!$isEmpty && $collapsed === Inputfield::collapsedPopulated)) { $ffAttrs['class'] .= ' ' . $classes['item_collapsed']; } } if($inputfield instanceof InputfieldWrapper) { // if the child is a wrapper, then id, title and class attributes become part of the LI wrapper foreach($inputfield->getAttributes() as $k => $v) { if(in_array($k, array('id', 'title', 'class'))) { $ffAttrs[$k] = isset($ffAttrs[$k]) ? $ffAttrs[$k] . " $v" : $v; } } } // if inputfield produced no output, then move to next if(!strlen($ffOut)) continue; // wrap the inputfield output $attrs = ''; $label = (string) $inputfield->getSetting('label'); $skipLabel = $inputfield->getSetting('skipLabel'); $skipLabel = is_bool($skipLabel) || empty($skipLabel) ? (bool) $skipLabel : (int) $skipLabel; // force as bool or int if(!strlen($label) && $skipLabel !== Inputfield::skipLabelBlank && $inputfield->className() != 'InputfieldWrapper') { $label = $inputfield->attr('name'); } if(($label || $quietMode) && $skipLabel !== Inputfield::skipLabelMarkup) { $for = $skipLabel || $quietMode ? '' : $inputfield->attr('id'); // if $inputfield has a property of entityEncodeLabel with a value of boolean FALSE, we don't entity encode $entityEncodeLabel = $inputfield->getSetting('entityEncodeLabel'); if(is_int($entityEncodeLabel) && $entityEncodeLabel >= Inputfield::textFormatBasic) { // uses an Inputfield::textFormat constant $label = $inputfield->entityEncode($label, $entityEncodeLabel); } else if($entityEncodeLabel !== false) { $label = $inputfield->entityEncode($label); } $icon = $inputfield->getSetting('icon'); $icon = $icon ? str_replace('{name}', $sanitizer->name(str_replace(array('icon-', 'fa-'), '', $icon)), $markup['item_icon']) : ''; $toggle = $collapsed == Inputfield::collapsedNever ? '' : $markup['item_toggle']; if($toggle && strpos($toggle, 'title=') === false) { $toggle = str_replace("class=", "title='" . $this->_('Toggle open/close') . "' class=", $toggle); } if($skipLabel === Inputfield::skipLabelHeader || $quietMode) { // label only shows when field is collapsed $label = str_replace('{out}', $icon . $label . $toggle, $markup['item_label_hidden']); } else { // label always visible $label = str_replace(array('{for}', '{out}'), array($for, $icon . $label . $toggle), $markup['item_label']); } $headerClass = trim($inputfield->getSetting('headerClass') . " $classes[item_label]"); if($headerClass) { if(strpos($label, '{class}') !== false) { $label = str_replace('{class}', ' ' . $headerClass, $label); } else { $label = preg_replace('/( class=[\'"][^\'"]+)/', '$1 ' . $headerClass, $label, 1); } } else if(strpos($label, '{class}') !== false) { $label = str_replace('{class}', '', $label); } } else if($skipLabel === Inputfield::skipLabelMarkup) { // no header and no markup for header $label = ''; } else { // no header // $inputfield->addClass('InputfieldNoHeader', 'wrapClass'); } $columnWidth = (int) $inputfield->getSetting('columnWidth'); $columnWidthAdjusted = $columnWidth; if($columnWidthSpacing) { $columnWidthAdjusted = $columnWidth + ($columnWidthTotal ? -1 * $columnWidthSpacing : 0); } if($columnWidth >= 9 && $columnWidth <= 100) { $ffAttrs['class'] .= ' ' . $classes['item_column_width']; if(!$columnWidthTotal) { $ffAttrs['class'] .= ' ' . $classes['item_column_width_first']; } $columnWidthTotal += $columnWidth; if(!$useColumnWidth || $useColumnWidth > 1) { if($columnWidthTotal >= 95 && $columnWidthTotal < 100) { $columnWidthAdjusted += (100 - $columnWidthTotal); $columnWidthTotal = 100; } $ffAttrs['data-colwidth'] = "$columnWidthAdjusted%"; } if($useColumnWidth) { $ffAttrs['style'] = "width: $columnWidthAdjusted%;"; } //if($columnWidthTotal >= 100 && !$requiredIf) $columnWidthTotal = 0; // requiredIf meant to be a showIf? if($columnWidthTotal >= 100) $columnWidthTotal = 0; } else { $columnWidthTotal = 0; } if(!isset($ffAttrs['id'])) $ffAttrs['id'] = 'wrap_' . $inputfield->attr('id'); $ffAttrs['class'] = str_replace('Inputfield_ ', '', $ffAttrs['class']); $wrapClass = $inputfield->getSetting('wrapClass'); $fieldName = $inputfield->attr('data-field-name'); if($fieldName && $fieldName != $inputfield->attr('name')) { // ensures that Inputfields renamed by context retain the original field-name based class $wrapClass = "Inputfield_$fieldName $wrapClass"; if(!isset($ffAttrs['data-id'])) $ffAttrs['data-id'] = "wrap_Inputfield_$fieldName"; } if($wrapClass) $ffAttrs['class'] = trim("$ffAttrs[class] $wrapClass"); foreach($inputfield->wrapAttr() as $k => $v) { if($k === 'class' && !empty($ffAttrs[$k])) { $ffAttrs[$k] .= " $v"; } else { $ffAttrs[$k] = $v; } } foreach($ffAttrs as $k => $v) { $k = $this->entityEncode($k); $v = $this->entityEncode(trim($v)); $attrs .= " $k='$v'"; } $markupItemContent = $markup['item_content']; $contentClass = trim($inputfield->getSetting('contentClass') . " $classes[item_content]"); if($contentClass) { if(strpos($markupItemContent, '{class}') !== false) { $markupItemContent = str_replace('{class}', ' ' . $contentClass, $markupItemContent); } else { $markupItemContent = preg_replace('/( class=[\'"][^\'"]+)/', '$1 ' . $contentClass, $markupItemContent, 1); } } else if(strpos($markupItemContent, '{class}') !== false) { $markupItemContent = str_replace('{class}', '', $markupItemContent); } if($inputfield->className() != 'InputfieldWrapper') $ffOut = str_replace('{out}', $ffOut, $markupItemContent); $out .= str_replace(array('{attrs}', '{out}'), array(trim($attrs), $label . $ffOut), $markup['item']); $lastInputfield = $inputfield; } // foreach($children as $inputfield) if($out) { $ulClass = $classes['list']; $lastColumnWidth = $lastInputfield ? $lastInputfield->getSetting('columnWidth') : 0; if($columnWidthTotal || ($lastInputfield && $lastColumnWidth >= 10 && $lastColumnWidth < 100)) { $ulClass .= ' ' . $classes['list_clearfix']; } $attrs = "class='$ulClass'"; // . ($this->attr('class') ? ' ' . $this->attr('class') : '') . "'"; if(!($this instanceof InputfieldForm)) { foreach($this->getAttributes() as $attr => $value) { if(strpos($attr, 'data-') === 0) $attrs .= " $attr='" . $this->entityEncode($value) . "'"; } } $out = $this->attr('value') . str_replace(array('{attrs}', '{out}'), array($attrs, $out), $markup['list']); } return $out; } /** * Render the output of this Inputfield and its children, showing values only (no inputs) * * #pw-group-output * * @return string * */ public function ___renderValue() { if(!count($this->children())) return ''; $this->addClass('InputfieldRenderValueMode'); $this->set('renderValueMode', true); $out = $this->render(); $this->set('renderValueMode', false); return $out; } /** * Render output for an individual Inputfield * * This method takes care of all the pre-and-post requisites needed for rendering an Inputfield * among a group of Inputfields. It is used by the `InputfieldWrapper::render()` method for each * Inputfield present in the children. * * #pw-group-output * * @param Inputfield $inputfield The Inputfield to render. * @param bool $renderValueMode Specify true if we are only rendering values (default=false). * @return string Rendered output * */ public function ___renderInputfield(Inputfield $inputfield, $renderValueMode = false) { $inputfieldID = $inputfield->attr('id'); $collapsed = (int) $inputfield->getSetting('collapsed'); $ajaxInputfield = $collapsed == Inputfield::collapsedYesAjax || $collapsed === Inputfield::collapsedTabAjax || ($collapsed == Inputfield::collapsedBlankAjax && $inputfield->isEmpty()); $ajaxHiddenInput = ""; $ajaxID = $this->wire()->config->ajax ? $this->wire()->input->get('renderInputfieldAjax') : ''; $required = $inputfield->getSetting('required'); if($ajaxInputfield && (($required && $inputfield->isEmpty()) || !$this->wire()->user->isLoggedin())) { // if an ajax field is empty, and is required, then we don't use ajax render mode // plus, we only allow ajax inputfields for logged-in users $ajaxInputfield = false; if($collapsed == Inputfield::collapsedYesAjax) $inputfield->collapsed = Inputfield::collapsedYes; if($collapsed == Inputfield::collapsedBlankAjax) $inputfield->collapsed = Inputfield::collapsedBlank; if($collapsed == Inputfield::collapsedTabAjax) $inputfield->collapsed = Inputfield::collapsedTab; // indicate to next processInput that this field can be processed $inputfield->appendMarkup .= $ajaxHiddenInput; } $restoreValue = null; // value to restore, if we happen to modify it before render (renderValueMode only) if($renderValueMode) { $flags = $inputfield->getSetting('renderValueFlags'); $inputfield->addClass('InputfieldRenderValueMode', 'wrapClass'); if($flags & Inputfield::renderValueMinimal) { $inputfield->addClass('InputfieldRenderValueMinimal', 'wrapClass'); } if($flags & Inputfield::renderValueFirst) { // render only first item value $inputfield->addClass('InputfieldRenderValueFirst', 'wrapClass'); $value = $inputfield->attr('value'); if(WireArray::iterable($value) && count($value) > 1) { $restoreValue = $value; if(is_array($value)) { $inputfield->attr('value', array_slice($value, 0, 1)); } else if($value instanceof WireArray) { $inputfield->attr('value', $value->slice(0, 1)); } } } } $inputfield->renderReady($this, $renderValueMode); if($ajaxInputfield) { if($ajaxID && $ajaxID === $inputfieldID) { // render ajax inputfield $editable = $inputfield->editable(); if($renderValueMode || !$editable) { echo $inputfield->renderValue(); } else { echo $inputfield->render(); echo $ajaxHiddenInput; } exit; } else if($ajaxID && $ajaxID != $inputfieldID && $inputfield instanceof InputfieldWrapper && $inputfield->getChildByName(str_replace('Inputfield_', '', $ajaxID))) { // nested ajax inputfield, within another ajax inputfield $in = $inputfield->getChildByName(str_replace('Inputfield_', '', $ajaxID)); return $this->renderInputfield($in, $renderValueMode); } else { // do not render ajax inputfield now, instead render placeholder return $this->renderInputfieldAjaxPlaceholder($inputfield, $renderValueMode); } } if(!$renderValueMode && $inputfield->editable()) return $inputfield->render(); // renderValueMode $out = $inputfield->renderValue(); if(!is_null($restoreValue)) { $inputfield->attr('value', $restoreValue); $inputfield->resetTrackChanges(); } if(is_null($out)) return ''; if(!strlen($out) && !$inputfield instanceof InputfieldWrapper) $out = ' '; // prevent output from being skipped over return $out; } /** * Render a placeholder for an ajax-loaded Inputfield * * @param Inputfield $inputfield * @param bool $renderValueMode * @return string * */ protected function renderInputfieldAjaxPlaceholder(Inputfield $inputfield, $renderValueMode) { $input = $this->wire()->input; $sanitizer = $this->wire()->sanitizer; $inputfieldID = $inputfield->attr('id'); $url = $input->url(); $queryString = $input->queryString(); if(strpos($queryString, 'renderInputfieldAjax=') !== false) { // in case nested ajax request $queryString = preg_replace('/&?renderInputfieldAjax=[^&]+/', '', $queryString); } $url .= $queryString ? "?$queryString&" : "?"; $url .= "renderInputfieldAjax=$inputfieldID"; $url = $sanitizer->entities($url); $out = "
    "; if($inputfield instanceof InputfieldWrapper) { // load assets they will need foreach($inputfield->getAll() as $in) { /** @var Inputfield $in */ $in->renderReady($inputfield, $renderValueMode); } } // ensure that Inputfield::render() hooks are still called if($inputfield->hasHook('render()')) { $inputfield->runHooks('render', array(), 'before'); } return $out; } /** * Process input for all children * * #pw-group-input * * @param WireInputData $input * @return $this * */ public function ___processInput(WireInputData $input) { if(!$this->children) return $this; $hasHook = $this->isHooked('InputfieldWrapper::allowProcessInput()'); foreach($this->children() as $child) { /** @var Inputfield $child */ // skip over the inputfield if hook tells us so if($hasHook && !$this->allowProcessInput($child)) continue; // skip over the inputfield if it is not processable if(!$this->isProcessable($child)) continue; // pass along the dependencies value to child wrappers if($child instanceof InputfieldWrapper && $this->getSetting('useDependencies') === false) { $child->set('useDependencies', false); } // call the inputfield's processInput method $child->processInput($input); // check if a value is required and field is empty, trigger an error if so if($child->attr('name') && $child->getSetting('required') && $child->isEmpty()) { $requiredLabel = $child->getSetting('requiredLabel'); if(empty($requiredLabel)) $requiredLabel = $this->requiredLabel; $child->error($requiredLabel); } } return $this; } /** * Is the given Inputfield processable for input? * * Returns whether or not the given Inputfield should be processed by processInput() * * When an `Inputfield` has a `showIf` property, then this returns false, but it queues * the field in the delayedChildren array for later processing. The root container should * temporarily remove the 'showIf' property of inputfields they want processed. * * #pw-internal * * @param Inputfield $inputfield * @return bool * */ public function isProcessable(Inputfield $inputfield) { if(!$inputfield->editable()) return false; // visibility settings that aren't saveable static $skipTypes = array( Inputfield::collapsedHidden, Inputfield::collapsedLocked, Inputfield::collapsedNoLocked, Inputfield::collapsedBlankLocked, Inputfield::collapsedYesLocked, Inputfield::collapsedTabLocked, ); $ajaxTypes = array( Inputfield::collapsedYesAjax, Inputfield::collapsedBlankAjax, Inputfield::collapsedTabAjax, ); $collapsed = (int) $inputfield->getSetting('collapsed'); if(in_array($collapsed, $skipTypes)) return false; if(in_array($collapsed, $ajaxTypes)) { $processAjax = $this->wire()->input->post('processInputfieldAjax'); if(is_array($processAjax) && in_array($inputfield->attr('id'), $processAjax)) { // field can be processed (convention used by InputfieldWrapper) } else if($collapsed == Inputfield::collapsedBlankAjax && !$inputfield->isEmpty()) { // field can be processed because it is only collapsed if blank } else if(isset($_SERVER['HTTP_X_FIELDNAME']) && $_SERVER['HTTP_X_FIELDNAME'] === $inputfield->attr('name')) { // field can be processed (convention used by ajax uploaded file and other ajax types) } else { // field was not rendered via ajax and thus can't be processed return false; } unset($processAjax); } // if dependencies aren't in use, we can skip the rest if($this->getSetting('useDependencies') === false) return true; if(strlen($inputfield->getSetting('showIf')) || ($inputfield->getSetting('required') && strlen($inputfield->getSetting('requiredIf')))) { $name = $inputfield->attr('name'); if(!$name) { $name = $inputfield->attr('id'); if(!$name) $name = $this->wire()->sanitizer->fieldName($inputfield->getSetting('label')); $inputfield->attr('name', $name); } $this->delayedChildren[$name] = $inputfield; return false; } return true; } /** * Allow input to be processed for given Inputfield? (for hooks) * * IMPORTANT: This method is not called unless it is hooked! Descending classes * should instead implement the isProcessable() method (when needed) and be sure to * call the parent isProcessable() method too. * * #pw-hooker * #pw-internal * * @param Inputfield $inputfield * @return bool * @since 3.0.207 * */ public function ___allowProcessInput(Inputfield $inputfield) { return true; } /** * Returns true if all children are empty, or false if one or more is populated * * #pw-group-retrieval-and-traversal * * @return bool * */ public function isEmpty() { $empty = true; foreach($this->children() as $child) { /** @var Inputfield $child */ if(!$child->isEmpty()) { $empty = false; break; } } return $empty; } /** * Return Inputfields in this wrapper that are required and have empty values * * This method includes all children up through the tree, not just direct children. * * #pw-internal * * @param bool $required Only include empty Inputfields that are required? (default=true) * @return array of Inputfield instances indexed by name attributes * */ public function getEmpty($required = true) { $a = array(); static $n = 0; foreach($this->children() as $child) { /** @var Inputfield $child */ if($child instanceof InputfieldWrapper) { $a = array_merge($a, $child->getEmpty($required)); } else { if($required && !$child->getSetting('required')) continue; if(!$child->isEmpty()) continue; $name = $child->attr('name'); if(empty($name)) $name = "_unknown" . (++$n); $a[$name] = $child; } } return $a; } /** * Return an array of errors that occurred on any of the children during input processing. * * Should only be called after `InputfieldWrapper::processInput()`. * * #pw-group-errors * * @param bool $clear Specify true to clear out the errors (default=false). * @return array Array of error strings * */ public function getErrors($clear = false) { $errors = parent::getErrors($clear); foreach($this->children() as $child) { /** @var Inputfield $child */ foreach($child->getErrors($clear) as $e) { $label = $child->getSetting('label'); $msg = $label ? $label : $child->attr('name'); $errors[] = $msg . " - $e"; } } return $errors; } /** * Get Inputfield objects that have errors * * #pw-group-errors * * @return array|Inputfield[] Array of Inputfield objects indexed by Inputfield name attribute * @since 3.0.205 * */ public function getErrorInputfields() { $a = array(); if(count(parent::getErrors())) { $name = $this->attr('name'); $a[$name] = $this; } foreach($this->children() as $child) { /** @var Inputfield $child */ if($child instanceof InputfieldWrapper) { $a = array_merge($a, $child->getErrorInputfields()); } else if(count($child->getErrors())) { $name = $child->attr('name'); $a[$name] = $child; } } return $a; } /** * Return all children Inputfield objects * * #pw-group-retrieval-and-traversal * * @param string $selector Optional selector string to filter the children by * @return InputfieldsArray * */ public function children($selector = '') { if($selector) { return $this->children->find($selector); } else { return $this->children; } } /** * Find an Inputfield below this one that has the given name * * This is an alternative to the `getChildByName()` method, with more options for when you need it. * For instance, it can also accept a selector string or numeric index for the $name argument, and you * can optionally disable the $recursive behavior. * * #pw-group-retrieval-and-traversal * * @param string|int $name Name or selector string of child to find, omit for first child, or specify zero-based index of child to return. * @param bool $recursive Find child recursively? Looks for child in this wrapper, and all other wrappers below it. (default=true) * @return Inputfield|null Returns Inputfield instance if found, or null if not. * @since 3.0.110 * */ public function child($name = '', $recursive = true) { $child = null; $children = $this->children(); if(!$children->count()) { // no child possible } else if(empty($name)) { // first child $child = $children->first(); } else if(is_int($name)) { // number index $child = $children->eq($name); } else if($this->wire()->sanitizer->name($name) === $name) { // child by name $wrappers = array(); foreach($children as $f) { /** @var Inputfield $f */ if($f->getAttribute('name') === $name) { $child = $f; break; } else if($recursive && $f instanceof InputfieldWrapper) { $wrappers[] = $f; } } if(!$child && $recursive && count($wrappers)) { foreach($wrappers as $wrapper) { $child = $wrapper->child($name, $recursive); if($child) break; } } } else if(Selectors::stringHasSelector($name)) { // first child matching selector string $child = $children->find("$name, limit=1")->first(); } return $child; } /** * Return all children Inputfields (alias of children method) * * #pw-internal * * @param string $selector Optional selector string to filter the children by * @return InputfieldsArray * */ public function getChildren($selector = '') { return $this->children($selector); } /** * Return array of inputfields (indexed by name) of fields that had dependencies and were not processed * * The results are to be handled by the root containing element (i.e. InputfieldForm). * * #pw-internal * * @param bool $clear Set to true in order to clear the delayed children list. * @return array|Inputfield[] * */ public function _getDelayedChildren($clear = false) { $a = $this->delayedChildren; foreach($this->children() as $child) { if(!$child instanceof InputfieldWrapper) continue; $a = array_merge($a, $child->_getDelayedChildren($clear)); } if($clear) $this->delayedChildren = array(); return $a; } /** * Find all children Inputfields matching a selector string * * #pw-group-retrieval-and-traversal * * @param string $selector Required selector string to filter the children by * @return InputfieldsArray * */ public function find($selector) { return $this->children()->find($selector); } /** * Given an Inputfield name, return the child Inputfield or NULL if not found. * * This traverses all children recursively to find the requested Inputfield. * * This is the same as the `InputfieldWrapper::get()` method except that it can * only return Inputfield or null, and has no crossover with other settings, * properties or API variables. * * #pw-group-retrieval-and-traversal * * @param string $name Name of Inputfield * @return Inputfield|InputfieldWrapper|null * @see InputfieldWrapper::get(), InputfieldWrapper::children() * */ public function getChildByName($name) { return strlen($name) ? $this->getByAttr('name', $name) : null; } /** * Shorter alias of getChildByName() * * #pw-group-retrieval-and-traversal * * @param string $name * @return Inputfield|InputfieldWrapper|null * @since 3.0.172 * */ public function getByName($name) { return strlen($name) ? $this->getByAttr('name', $name) : null; } /** * Given an attribute name and value, return the first matching Inputfield or null if not found * * This traverses all children recursively to find the requested Inputfield. * * #pw-group-retrieval-and-traversal * * @param string $attrName Attribute to match, such as 'id', 'name', 'value', etc. * @param string $attrValue Attribute value to match * @return Inputfield|InputfieldWrapper|null * @since 3.0.196 * */ public function getByAttr($attrName, $attrValue) { $inputfield = null; foreach($this->children() as $child) { /** @var Inputfield $child */ if($child->getAttribute($attrName) === $attrValue) { $inputfield = $child; } else if($child instanceof InputfieldWrapper) { $inputfield = $child->getByAttr($attrName, $attrValue); } if($inputfield) break; } return $inputfield; } /** * Get value of Inputfield by name * * This traverses all children recursively to find the requested Inputfield, * and get the value attribute from it. A value of null is returned if the * Inputfield cannot be found. * * @param string $name * @return array|float|int|object|Wire|WireArray|WireData|string|null * @since 3.0.172 * */ public function getValueByName($name) { $inputfield = $this->getByName($name); return $inputfield ? $inputfield->val() : null; } /** * Enables foreach() of the children of this class * * Per the InteratorAggregate interface, make the Inputfield children iterable. * * #pw-group-retrieval-and-traversal * * @return InputfieldsArray * */ #[\ReturnTypeWillChange] public function getIterator() { return $this->children(); } /** * Return the quantity of children present * * #pw-group-retrieval-and-traversal * * @return int * */ #[\ReturnTypeWillChange] public function count() { return count($this->children()); } /** * Get all Inputfields below this recursively in a flat InputfieldWrapper (children, and their children, etc.) * * Note that all InputfieldWrapper instances are removed as a result (except for the containing InputfieldWrapper). * * #pw-group-retrieval-and-traversal * * @param array $options Options to modify behavior (3.0.169+) * - `withWrappers` (bool): Also include InputfieldWrapper objects? (default=false) 3.0.169+ * @return InputfieldsArray * */ public function getAll(array $options = array()) { /** @var InputfieldsArray $all */ $all = $this->wire(new InputfieldsArray()); foreach($this->children() as $child) { if($child instanceof InputfieldWrapper) { if(!empty($options['withWrappers'])) $all->add($child); foreach($child->getAll($options) as $c) { $all->add($c); } } else { $all->add($child); } } return $all; } /** * Start or stop tracking changes, applying the same to any children * * #pw-internal * * @param bool $trackChanges * @return Inputfield|InputfieldWrapper * */ public function setTrackChanges($trackChanges = true) { $children = $this->children(); if(count($children)) foreach($children as $child) $child->setTrackChanges($trackChanges); return parent::setTrackChanges($trackChanges); } /** * Start or stop tracking changes after clearing out any existing tracked changes, applying the same to any children * * #pw-internal * * @param bool $trackChanges * @return Inputfield|InputfieldWrapper * */ public function resetTrackChanges($trackChanges = true) { $children = $this->children(); if(count($children)) foreach($children as $child) $child->resetTrackChanges($trackChanges); return parent::resetTrackChanges($trackChanges); } /** * Get configuration Inputfields for this InputfieldWrapper * * #pw-group-module * * @return InputfieldWrapper * */ public function ___getConfigInputfields() { $inputfields = parent::___getConfigInputfields(); /** @var InputfieldSelect $f */ $f = $inputfields->getChildByName('collapsed'); if($f) { // remove all options for 'collapsed' except for a few $allow = array( Inputfield::collapsedNo, Inputfield::collapsedYes, Inputfield::collapsedYesAjax, Inputfield::collapsedNever, ); foreach($f->getOptions() as $value => $label) { if(!in_array($value, $allow)) $f->removeOption($value); } } return $inputfields; } /** * Set custom markup for render, see self::$markup at top for reference. * * #pw-internal * * @param array $markup * */ public static function setMarkup(array $markup) { self::$markup = array_merge(self::$markup, $markup); } /** * Get custom markup for render, see self::$markup at top for reference. * * #pw-internal * * @return array * */ public static function getMarkup() { return array_merge(self::$defaultMarkup, self::$markup); } /** * Set custom classes for render, see self::$classes at top for reference. * * #pw-internal * * @param array $classes * */ public static function setClasses(array $classes) { self::$classes = array_merge(self::$classes, $classes); } /** * Get custom classes for render, see self::$classes at top for reference. * * #pw-internal * * @return array * */ public static function getClasses() { return array_merge(self::$defaultClasses, self::$classes); } /** * Import an array of Inputfield definitions to to this InputfieldWrapper instance * * Your array should be an array of associative arrays, with each element describing an Inputfield. * The following properties are required for each Inputfield definition: * * - `type` Which Inputfield module to use (may optionally exclude the "Inputfield" prefix). * - `name` Name attribute to use for the Inputfield. * - `label` Text label that appears above the Inputfield. * * ~~~~~ * // Example array for Inputfield definitions * array( * array( * 'name' => 'fullname', * 'type' => 'text', * 'label' => 'Field label' * 'columnWidth' => 50, * 'required' => true, * ), * array( * 'name' => 'color', * 'type' => 'select', * 'label' => 'Your favorite color', * 'description' => 'Select your favorite color or leave blank if you do not have one.', * 'columnWidth' => 50, * 'options' => array( * 'red' => 'Brilliant Red', * 'orange' => 'Citrus Orange', * 'blue' => 'Sky Blue' * ) * ), * // alternative usage: associative array where name attribute is specified as key * 'my_fieldset' => array( * 'type' => 'fieldset', * 'label' => 'My Fieldset', * 'children' => array( * 'some_field' => array( * 'type' => 'text', * 'label' => 'Some Field', * ) * ) * ); * // Note: you may alternatively use associative arrays where the keys are assumed to * // be the 'name' attribute.See the last item 'my_fieldset' above for an example. * ~~~~~ * * #pw-group-manipulation * * @param array $a Array of Inputfield definitions * @param InputfieldWrapper $inputfields Specify the wrapper you want them added to, or omit to use current. * @return $this * */ public function importArray(array $a, InputfieldWrapper $inputfields = null) { $modules = $this->wire()->modules; if(is_null($inputfields)) $inputfields = $this; if(!count($a)) return $inputfields; // if just a single field definition rather than an array of them, normalize to array of array $first = reset($a); if(!is_array($first)) $a = array($a); foreach($a as $name => $info) { if(isset($info['name'])) { $name = $info['name']; unset($info['name']); } if(!isset($info['type'])) { $this->error("Skipped field '$name' because no 'type' is set"); continue; } $type = $info['type']; unset($info['type']); if(strpos($type, 'Inputfield') !== 0) $type = "Inputfield" . ucfirst($type); /** @var Inputfield $f */ $f = $modules->get($type); if(!$f) { $this->error("Skipped field '$name' because module '$type' does not exist"); continue; } $f->attr('name', $name); if($type === 'InputfieldCheckbox') { // checkbox behaves a little differently, just like in HTML /** @var InputfieldCheckbox $f */ if(!empty($info['attr']['value'])) { $f->attr('value', $info['attr']['value']); } else if(!empty($info['value'])) { $f->attr('value', $info['value']); } unset($info['attr']['value'], $info['value']); $f->autocheck = 1; // future value attr set triggers checked state } if(isset($info['attr']) && is_array($info['attr'])) { foreach($info['attr'] as $key => $value) { $f->attr($key, $value); } unset($a['attr']); } foreach($info as $key => $value) { if($key == 'children') continue; $f->$key = $value; } if($f instanceof InputfieldWrapper && !empty($info['children'])) { $this->importArray($info['children'], $f); } $inputfields->add($f); } return $inputfields; } /** * Populate values for all Inputfields in this wrapper from the given $data object or array. * * This iterates through every field in this InputfieldWrapper and looks for field names * that are also present in the given object or array. If present, it uses them to populate * the associated Inputfield. * * If given an array, it should be an associative with the field 'name' as the keys and * the field 'value' as the array value, i.e. `['field_name' => 'field_value']`. * * #pw-group-manipulation * * @param WireData|Wire|ConfigurableModule|array $data * @return array Returns array of field names that were populated * */ public function populateValues($data) { $populated = array(); foreach($this->getAll() as $inputfield) { /** @var Inputfield $inputfield */ if($inputfield instanceof InputfieldWrapper) continue; $name = $inputfield->attr('name'); if(!$name) continue; $value = null; if(is_array($data)) { // array $value = isset($data[$name]) ? $data[$name] : null; } else if($data instanceof WireData) { // WireData object $value = $data->data($name); } else if(is_object($data)) { // Wire or other object with __get() implemented $value = $data->$name; } if($value === null) continue; if($inputfield instanceof InputfieldCheckbox) $inputfield->autocheck = 1; $inputfield->attr('value', $value); $populated[$name] = $name; } return $populated; } /** * Get an array of all family below this (recursively) for debugging purposes * * #pw-internal * * @return array * */ public function debugMap() { $a = array(); foreach($this as $in) { /** @var Inputfield $in */ $info = array( 'id' => $in->id, 'name' => $in->name, 'type' => $in->className(), ); if($in instanceof InputfieldWrapper) { $info['children'] = $in->debugMap(); } $a[] = $info; } return $a; } /** * Debug info * * #pw-internal * * @return array * */ public function __debugInfo() { $info = parent::__debugInfo(); $info['children'] = $this->debugMap(); return $info; } }