diff --git a/wire/core/InputfieldWrapper.php b/wire/core/InputfieldWrapper.php index 0b4f0fb9..cab2269c 100644 --- a/wire/core/InputfieldWrapper.php +++ b/wire/core/InputfieldWrapper.php @@ -966,6 +966,34 @@ class InputfieldWrapper extends Inputfield implements \Countable, \IteratorAggre 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) { + 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. * diff --git a/wire/modules/Fieldtype/FieldtypeRepeater/InputfieldRepeater.module b/wire/modules/Fieldtype/FieldtypeRepeater/InputfieldRepeater.module index ffe18a12..022c718f 100644 --- a/wire/modules/Fieldtype/FieldtypeRepeater/InputfieldRepeater.module +++ b/wire/modules/Fieldtype/FieldtypeRepeater/InputfieldRepeater.module @@ -82,6 +82,14 @@ class InputfieldRepeater extends Inputfield implements InputfieldItemList { */ protected $renderValueMode = false; + /** + * Number of required empty Inputfields after processing + * + * @var int + * + */ + protected $numRequiredEmpty = 0; + /** * Set config defaults * @@ -271,14 +279,18 @@ class InputfieldRepeater extends Inputfield implements InputfieldItemList { $label = $this->field->getLabel(); if(!$label) $label = ucfirst($this->field->name); - // remember which repeater items are open, when enabled + // remember which repeater items are open (as stored in cookie), when enabled + $openIDs = array(); if((int) $this->field->get('rememberOpen')) { $this->addClass('InputfieldRepeaterRememberOpen', 'wrapClass'); - $openIDs = $this->wire('input')->cookie('repeaters_open'); + $openIDs = $this->wire('input')->cookie('repeaters_open'); if($openIDs) $openIDs = explode('|', trim($openIDs, '|')); if(!is_array($openIDs)) $openIDs = array(); - } else { - $openIDs = array(); + } + // merge with any open IDs in session + $_openIDs = $this->wire('session')->getFor($this, 'openIDs'); + if(is_array($_openIDs) && !empty($_openIDs)) { + $openIDs = array_merge($openIDs, array_values($_openIDs)); } $minItems = $this->repeaterMinItems; @@ -754,6 +766,11 @@ class InputfieldRepeater extends Inputfield implements InputfieldItemList { $sortChanged = false; $value->setTrackChanges(true); $pageIDs = array(); + $_openIDs = $this->wire('session')->getFor($this, 'openIDs'); + if(!is_array($_openIDs)) $_openIDs = array(); + $openIDs = $_openIDs; // these two are compared with each other at the end + $this->numRequiredEmpty = 0; + $this->getErrors(true); // existing items foreach($value as $key => $page) { @@ -802,9 +819,11 @@ class InputfieldRepeater extends Inputfield implements InputfieldItemList { /** @var InputfieldWrapper $wrapper */ $wrapper = $this->wrappers[$page->id]; $wrapper->resetTrackChanges(true); + $wrapper->getErrors(true); // clear out any errors $wrapper->processInput($input); $numErrors = count($wrapper->getErrors()); + $numRequiredEmpty = count($wrapper->getEmpty(true)); $page->setQuietly('_repeater_errors', $numErrors); // signal to FieldtypeRepeater::savePageField() that page has errors $page->setQuietly('_repeater_processed', true); // signal to FieldtypeRepeater::savePageField() that page had input processed $this->formToPage($wrapper, $page); @@ -829,6 +848,14 @@ class InputfieldRepeater extends Inputfield implements InputfieldItemList { $page->addStatus(Page::statusOn); } } + + if($numErrors || $numRequiredEmpty) { + $this->error(sprintf($this->_('Errors in “%s” item %d'), $this->label, $key + 1)); + if(!$page->hasStatus(Page::statusUnpublished)) $this->numRequiredEmpty += $numRequiredEmpty; + $openIDs[$page->id] = $page->id; // force item with error to be open on next request + } else if(isset($openIDs[$page->id])) { + unset($openIDs[$page->id]); + } if($page->isChanged() && $this->page->id) $numChanges++; } @@ -858,6 +885,9 @@ class InputfieldRepeater extends Inputfield implements InputfieldItemList { $this->page->trackChange($this->attr('name')); $this->trackChange('value'); } + + // if openIDs value changed, update the session variable + if($_openIDs !== $openIDs) $this->wire('session')->setFor($this, 'openIDs', $openIDs); return $this; } @@ -923,6 +953,33 @@ class InputfieldRepeater extends Inputfield implements InputfieldItemList { return $cnt === 0; } + /** + * Return quantity of published items + * + * @return int + * + */ + public function numPublished() { + /** @var PageArray $value */ + $value = $this->attr('value'); + if(empty($value) || !count($value)) return 0; + $num = 0; + foreach($value as $item) { + if(!$item->hasStatus(Page::statusUnpublished)) $num++; + } + return $num; + } + + /** + * Get number of required but empty Inputfields (across all repeater items) + * + * @return int + * + */ + public function numRequiredEmpty() { + return $this->numRequiredEmpty; + } + /** * Override the default set() to capture the required $page variable that the repeaters field lives on. * diff --git a/wire/modules/Process/ProcessPageEdit/ProcessPageEdit.module b/wire/modules/Process/ProcessPageEdit/ProcessPageEdit.module index 0a56bc36..ed4dce06 100644 --- a/wire/modules/Process/ProcessPageEdit/ProcessPageEdit.module +++ b/wire/modules/Process/ProcessPageEdit/ProcessPageEdit.module @@ -1825,39 +1825,8 @@ class ProcessPageEdit extends Process implements WirePageEditor, ConfigurableMod if($name == 'status' && $this->processInputStatus($inputfield)) continue; } - - if($name && $errorAction - && $inputfield->getSetting('required') && $inputfield->isEmpty() - && !$inputfield->getSetting('requiredSkipped') - && !$this->page->isUnpublished()) { - - if($errorAction === 1) { - // restore existing value by skipping processing of empty when required - $value = $inputfield->attr('value'); - if($value instanceof Wire) $value->resetTrackChanges(); - if($this->page->getField($name)) $this->page->remove($name); // force fresh copy to reload - $previousValue = $this->page->get($name); - $this->page->untrackChange($name); - if($previousValue) { - // we should have a previous value to restore - if(WireArray::iterable($previousValue) && !count($previousValue)) { - // previous value still empty - } else { - // previous value restored by simply not setting new value to $page - $inputfield->error($this->_('Restored previous value')); - continue; - } - } - - } else if($errorAction === 2 && $this->page->publishable() && $this->page->id > 1) { - // unpublish page missing required value - $this->page->setQuietly('_forceAddStatus', Page::statusUnpublished); - $label = $inputfield->getSetting('label'); - if(empty($label)) $label = $inputfield->attr('name'); - $this->error(sprintf($this->_('Page unpublished because field "%s" is required'), $label)); - continue; - } - } + + if($this->processInputErrorAction($this->page, $inputfield, $name, $errorAction)) continue; if($name && $inputfield->isChanged()) { if($languages && $inputfield->getSetting('useLanguages')) { @@ -1887,6 +1856,72 @@ class ProcessPageEdit extends Process implements WirePageEditor, ConfigurableMod } } + /** + * Process required error actions as configured with page’s template + * + * @param Page $page + * @param Inputfield|InputfieldRepeater $inputfield Inputfield that has already had its processInput() method called. + * @param string $name Name of field that we are checking. + * @param null|int $errorAction Error action from $page->template->errorAction, or omit to auto-detect. + * @return bool Returns true if field $name should be skipped over during processing, or false if not + * + */ + public function processInputErrorAction(Page $page, Inputfield $inputfield, $name, $errorAction = null) { + + if(empty($name)) return false; + if($errorAction === null) $errorAction = (int) $page->template->get('errorAction'); + if(!$errorAction) return false; + if($page->isUnpublished()) return false; + + $isRequired = $inputfield->getSetting('required'); + $isRepeater = strpos($inputfield->className(), 'Repeater') > 0 && wireInstanceOf($inputfield, 'InputfieldRepeater', false); + + if(!$isRepeater && !$isRequired) return false; + if($inputfield->getSetting('requiredSkipped')) return false; + + if($isRepeater) { + if($inputfield->numRequiredEmpty() > 0) { + // repeater has required fields that are empty + } else if($isRequired && $inputfield->numPublished() < 1) { + // repeater is required and has no published items + } else { + // repeater is okay for now + return false; + } + } else if(!$inputfield->isEmpty()) { + return false; + } + + if($errorAction === 1) { + // restore existing value by skipping processing of empty when required + $value = $inputfield->attr('value'); + if($value instanceof Wire) $value->resetTrackChanges(); + if($page->getField($name)) $page->remove($name); // force fresh copy to reload + $previousValue = $page->get($name); + $page->untrackChange($name); + if($previousValue) { + // we should have a previous value to restore + if(WireArray::iterable($previousValue) && !count($previousValue)) { + // previous value still empty + } else { + // previous value restored by simply not setting new value to $page + $inputfield->error($this->_('Restored previous value')); + return true; + } + } + + } else if($errorAction === 2 && $page->publishable() && $page->id > 1) { + // unpublish page missing required value + $page->setQuietly('_forceAddStatus', Page::statusUnpublished); + $label = $inputfield->getSetting('label'); + if(empty($label)) $label = $inputfield->attr('name'); + $inputfield->error(sprintf($this->_('Page unpublished because field "%s" is required'), $label)); + return false; + } + + return false; + } + /** * Check to see if the page's created user has changed and make sure it's valid *