From d9fb9cd0261a0f81a5a263dbebb0bf73f2811417 Mon Sep 17 00:00:00 2001 From: Ryan Cramer Date: Wed, 30 Aug 2017 09:37:19 -0400 Subject: [PATCH] Updates to FieldtypeRepeater and InputfieldRepeater to support single mode, as used by FieldtypeFieldsetPage --- .../FieldtypeRepeater.module | 110 +++++---- .../InputfieldRepeater.module | 214 ++++++++++-------- .../FieldtypeRepeater/RepeaterPage.php | 25 +- 3 files changed, 208 insertions(+), 141 deletions(-) diff --git a/wire/modules/Fieldtype/FieldtypeRepeater/FieldtypeRepeater.module b/wire/modules/Fieldtype/FieldtypeRepeater/FieldtypeRepeater.module index 984d2775..7fdc2ae9 100644 --- a/wire/modules/Fieldtype/FieldtypeRepeater/FieldtypeRepeater.module +++ b/wire/modules/Fieldtype/FieldtypeRepeater/FieldtypeRepeater.module @@ -531,27 +531,30 @@ class FieldtypeRepeater extends Fieldtype implements ConfigurableModule { * * @param Page $page * @param Field $field - * @param PageArray $value + * @param PageArray|Page $value * @param array $notIDs Optional Page IDs that should be excluded from the next ready page * @return Page * */ - public function getNextReadyPage(Page $page, Field $field, PageArray $value, array $notIDs = array()) { + public function getNextReadyPage(Page $page, Field $field, $value = null, array $notIDs = array()) { $readyPage = null; - foreach($value as $item) { - if($item->hasStatus(Page::statusUnpublished) - && $item->hasStatus(Page::statusHidden) - && $item->id - && substr($item->name, -1) !== 'c' // cloned item - && !in_array($item->id, $notIDs)) { - // existing/unused ready item that we will reuse - $readyPage = $item; - // touch the modified date for existing page to identify it as still current - $query = $this->wire('database')->prepare('UPDATE pages SET modified=NOW(), modified_users_id=:user_id WHERE id=:id'); - $query->bindValue(':id', $readyPage->id, \PDO::PARAM_INT); - $query->bindValue(':user_id', $this->wire('user')->id, \PDO::PARAM_INT); - $query->execute(); - break; + if($value) { + if($value instanceof Page) $value = array($value); + foreach($value as $item) { + if($item->hasStatus(Page::statusUnpublished) + && $item->hasStatus(Page::statusHidden) + && $item->id + && substr($item->name, -1) !== 'c' // cloned item + && !in_array($item->id, $notIDs)) { + // existing/unused ready item that we will reuse + $readyPage = $item; + // touch the modified date for existing page to identify it as still current + $query = $this->wire('database')->prepare('UPDATE pages SET modified=NOW(), modified_users_id=:user_id WHERE id=:id'); + $query->bindValue(':id', $readyPage->id, \PDO::PARAM_INT); + $query->bindValue(':user_id', $this->wire('user')->id, \PDO::PARAM_INT); + $query->execute(); + break; + } } } if(!$readyPage) { @@ -931,11 +934,11 @@ class FieldtypeRepeater extends Fieldtype implements ConfigurableModule { * @return Page * */ - protected function getRepeaterPageParent(Page $page, Field $field) { + protected function getRepeaterPageParent(Page $page, Field $field) { $repeaterParent = $this->getRepeaterParent($field); - $parent = $repeaterParent->child('name=' . self::repeaterPageNamePrefix . $page->id . ', include=all'); - if($parent->id) return $parent; + $parent = $repeaterParent->child('name=' . self::repeaterPageNamePrefix . $page->id . ', include=all'); + if($parent->id) return $parent; $parent = $this->wire('pages')->newPage(array('template' => $repeaterParent->template)); $parent->parent = $repeaterParent; @@ -1040,10 +1043,7 @@ class FieldtypeRepeater extends Fieldtype implements ConfigurableModule { $template = $this->wire(new Template()); $template->name = $templateName; $template->fieldgroup = $fieldgroup; - $template->flags = Template::flagSystem; - $template->noChildren = 1; - $template->noParents = 1; // prevents users from creating pages with this template, but not us - $template->noGlobal = 1; + $this->populateRepeaterTemplateSettings($template); $template->save(); if(!$template->id) throw new WireException("Unable to create repeater template: $templateName"); @@ -1057,6 +1057,19 @@ class FieldtypeRepeater extends Fieldtype implements ConfigurableModule { return $template; } + /** + * Populate the settings for a newly created repeater template + * + * @param Template $template + * + */ + protected function populateRepeaterTemplateSettings(Template $template) { + $template->flags = Template::flagSystem; + $template->noChildren = 1; + $template->noParents = 1; // prevents users from creating pages with this template, but not us + $template->noGlobal = 1; + } + /** * Handles the sanitization and convertion to PageArray value * @@ -1146,8 +1159,6 @@ class FieldtypeRepeater extends Fieldtype implements ConfigurableModule { /** * Perform output formatting on the value delivered to the API * - * If the repeaterMaxItems setting is 1, then we format the value to dereference as single Page rather than a PageArray. - * * This method is only used when $page->outputFormatting is true. * * @param Page $page @@ -1162,13 +1173,6 @@ class FieldtypeRepeater extends Fieldtype implements ConfigurableModule { if(!$value instanceof PageArray) $value = $this->getBlankValue($page, $field); - /* TBA - if($field->repeaterMaxItems == 1) { - if(count($value)) $value = $value->first(); - else $value = new NullPage(); - } - */ - // used as a clone if a formatted version of $value is different from non-formatted $formatted = null; $cnt = 0; @@ -1330,7 +1334,9 @@ class FieldtypeRepeater extends Fieldtype implements ConfigurableModule { if($value instanceof RepeaterPage) { // for FieldsetPage compatibility - $pageArray = $this->getBlankValue($page, $field); + $pageArrayClass = $this->getPageArrayClass(); + /** @var RepeaterPageArray $pageArray */ + $pageArray = $this->wire(new $pageArrayClass($page, $field)); $pageArray->add($value); $pageArray->resetTrackChanges(); $value = $pageArray; @@ -1565,7 +1571,7 @@ class FieldtypeRepeater extends Fieldtype implements ConfigurableModule { * */ public function ___cloneField(Field $field) { - throw new WireException("Sorry, repeater fields are not currently cloneable."); + throw new WireException($this->className() . " does not currently support field cloning."); /* $field = parent::___cloneField($field); $field->parent_id = null; @@ -1644,38 +1650,44 @@ class FieldtypeRepeater extends Fieldtype implements ConfigurableModule { * */ public function ___install() { + + /** @var Pages $pages */ + $pages = $this->wire('pages'); - $adminRoot = $this->wire('pages')->get($this->wire('config')->adminRootPageID); - - $page = $this->wire('pages')->newPage(array('template' => 'admin')); - $page->parent = $adminRoot; - $page->status = Page::statusHidden | Page::statusLocked | Page::statusSystemID; - $page->name = 'repeaters'; - $page->title = 'Repeaters'; - $page->sort = $adminRoot->numChildren; - $page->save(); + $adminRoot = $pages->get($this->wire('config')->adminRootPageID); + $page = $adminRoot->child("name=repeaters, template=admin, include=all"); + + if(!$page->id) { + $page = $pages->newPage(array('template' => 'admin')); + $page->parent = $adminRoot; + $page->status = Page::statusHidden | Page::statusLocked | Page::statusSystemID; + $page->name = 'repeaters'; + $page->title = 'Repeaters'; + $page->sort = $adminRoot->numChildren; + $page->save(); + $this->message("Added page {$page->path}", Notice::debug); + } $configData = array('repeatersRootPageID' => $page->id); $this->wire('modules')->saveModuleConfigData($this, $configData); - - $this->message("Added page {$page->path}", Notice::debug); } /** - * Uninstall the module + * Uninstall the module (delete the repeaters page) * */ public function ___uninstall() { - // delete the repeaters page + // don't delete repeaters page unless actually for FieldtypeRepeater + if($this->className() != 'FieldtypeRepeater') return; $page = $this->wire('pages')->get($this->repeatersRootPageID); - if($page->id) { + if($page->id && $page->name == 'repeaters' && $page->template == 'admin') { $page->addStatus(Page::statusSystemOverride); $page->removeStatus(Page::statusSystem); $page->removeStatus(Page::statusSystemID); $page->removeStatus(Page::statusSystemOverride); $page->removeStatus(Page::statusLocked); - if($page->id) $this->wire('pages')->delete($page); + $this->wire('pages')->delete($page); $this->message("Removed page {$page->path}", Notice::debug); } } diff --git a/wire/modules/Fieldtype/FieldtypeRepeater/InputfieldRepeater.module b/wire/modules/Fieldtype/FieldtypeRepeater/InputfieldRepeater.module index 39f7041c..edfc4a0a 100644 --- a/wire/modules/Fieldtype/FieldtypeRepeater/InputfieldRepeater.module +++ b/wire/modules/Fieldtype/FieldtypeRepeater/InputfieldRepeater.module @@ -283,11 +283,12 @@ class InputfieldRepeater extends Inputfield implements InputfieldItemList { } $minItems = $this->repeaterMinItems; + $maxItems = $this->repeaterMaxItems; // if there are a minimum required number of items, set them up now if(!$itemID && $minItems > 0) { $notIDs = $value->explode('id'); - while($value->count() < $this->repeaterMinItems) { + while($value->count() < $minItems) { $item = $this->getNextReadyPage($notIDs); $value->add($item); $notIDs[] = $item->id; @@ -299,6 +300,7 @@ class InputfieldRepeater extends Inputfield implements InputfieldItemList { $numVisible = 0; $numOpen = 0; $isPost = $this->wire('input')->requestMethod('POST'); + $isSingle = $minItems == 1 && $maxItems == 1; // create field for each repeater iteration foreach($value as $key => $page) { @@ -310,11 +312,11 @@ class InputfieldRepeater extends Inputfield implements InputfieldItemList { $isOn = $page->hasStatus(Page::statusOn); $isReadyItem = $isHidden && $isUnpublished; $isClone = $page->get('_repeater_clone'); - $isOpen = in_array($page->id, $openIDs) || $isClone; + $isOpen = in_array($page->id, $openIDs) || $isClone || $isSingle; $isMinItem = $isReadyItem && $minItems && $cnt < $minItems; if($isOpen && $numOpen > 0 && $this->accordionMode) $isOpen = false; - + // get the inputfields for the repeater page if(is_null($loadInputsForIDs) || in_array($page->id, $loadInputsForIDs) || $isOpen) { $inputfields = $this->getRepeaterItemInputfields($page); @@ -325,37 +327,44 @@ class InputfieldRepeater extends Inputfield implements InputfieldItemList { } $inputfields->set('useDependencies', false); $this->wrappers[$page->id] = $inputfields; - - // also add a delete checkbox to the repeater page fields - $delete = $this->wire('modules')->get('InputfieldCheckbox'); - $delete->attr('id+name', "delete_repeater{$page->id}"); - $delete->addClass('InputfieldRepeaterDelete', 'wrapClass'); - $delete->label = $this->_('Delete'); - $delete->attr('value', $page->id); - - $sort = $this->wire('modules')->get('InputfieldHidden'); - $sort->attr('id+name', "sort_repeater{$page->id}"); - $sort->class = 'InputfieldRepeaterSort'; - $sort->label = $this->_('Sort'); - $sort->attr('value', $cnt); - - if($this->repeaterDepth > 0) { - $depth = $this->wire('modules')->get('InputfieldHidden'); - $depth->attr('id+name', "depth_repeater{$page->id}"); - $depth->class = 'InputfieldRepeaterDepth'; - $depth->label = $this->_('Depth'); - $depthValue = $page->getDepth(); - $depth->attr('value', $depthValue); - $depth->set('renderValueAsInput', true); - } else { + + if($isSingle) { + $delete = null; + $sort = null; $depth = null; - } + $loaded = null; + } else { + // also add a delete checkbox to the repeater page fields + $delete = $this->wire('modules')->get('InputfieldCheckbox'); + $delete->attr('id+name', "delete_repeater{$page->id}"); + $delete->addClass('InputfieldRepeaterDelete', 'wrapClass'); + $delete->label = $this->_('Delete'); + $delete->attr('value', $page->id); - $loaded = $this->wire('modules')->get('InputfieldHidden'); - $loaded->attr('id+name', "loaded_repeater{$page->id}"); - $loaded->attr('value', $isLoaded ? 1 : 0); - $loaded->set('renderValueAsInput', true); - $loaded->class = 'InputfieldRepeaterLoaded'; + $sort = $this->wire('modules')->get('InputfieldHidden'); + $sort->attr('id+name', "sort_repeater{$page->id}"); + $sort->class = 'InputfieldRepeaterSort'; + $sort->label = $this->_('Sort'); + $sort->attr('value', $cnt); + + if($this->repeaterDepth > 0) { + $depth = $this->wire('modules')->get('InputfieldHidden'); + $depth->attr('id+name', "depth_repeater{$page->id}"); + $depth->class = 'InputfieldRepeaterDepth'; + $depth->label = $this->_('Depth'); + $depthValue = $page->getDepth(); + $depth->attr('value', $depthValue); + $depth->set('renderValueAsInput', true); + } else { + $depth = null; + } + + $loaded = $this->wire('modules')->get('InputfieldHidden'); + $loaded->attr('id+name', "loaded_repeater{$page->id}"); + $loaded->attr('value', $isLoaded ? 1 : 0); + $loaded->set('renderValueAsInput', true); + $loaded->class = 'InputfieldRepeaterLoaded'; + } $wrap = $this->wire('modules')->get('InputfieldFieldset'); $wrap->addClass('InputfieldRepeaterItem InputfieldNoFocus'); @@ -379,7 +388,7 @@ class InputfieldRepeater extends Inputfield implements InputfieldItemList { if($isClone) $wrap->addClass('InputfieldRepeaterItemClone'); if($itemID) $wrap->addClass('InputfieldRepeaterItemRequested'); - if($page->get('_repeater_delete')) { + if($delete && $page->get('_repeater_delete')) { // something indicates it should already show delete state in editor $delete->attr('checked', 'checked'); $wrap->addClass('InputfieldRepeaterDeletePending'); @@ -400,36 +409,39 @@ class InputfieldRepeater extends Inputfield implements InputfieldItemList { $hasErrors = count($inputfields->getErrors()) > 0; if($hasErrors) $wrap->icon = 'warning'; - // add a hidden field that will be populated with a positive value for all visible repeater items - // this is so that processInput can see this item should be a published item - $f = $this->wire('modules')->get('InputfieldHidden'); - $f->attr('name', "publish_repeater{$page->id}"); - $f->attr('class', 'InputfieldRepeaterPublish'); - - if($isReadyItem) { - // ready item - $f->attr('value', 0); - } else if($isUnpublished && !$isOn) { - // unpublished item - $f->attr('value', -1); - } else { - // published item - $f->attr('value', 1); - } - - $wrap->add($f); + if(!$isSingle) { + // add a hidden field that will be populated with a positive value for all visible repeater items + // this is so that processInput can see this item should be a published item + $f = $this->wire('modules')->get('InputfieldHidden'); + $f->attr('name', "publish_repeater{$page->id}"); + $f->attr('class', 'InputfieldRepeaterPublish'); - if($isUnpublished) { - $wrap->addClass('InputfieldRepeaterUnpublished'); - if(!$isOn) $wrap->addClass('InputfieldRepeaterOff'); + if($isReadyItem) { + // ready item + $f->attr('value', 0); + } else if($isUnpublished && !$isOn) { + // unpublished item + $f->attr('value', -1); + } else { + // published item + $f->attr('value', 1); + } + + $wrap->add($f); + + if($isUnpublished) { + $wrap->addClass('InputfieldRepeaterUnpublished'); + if(!$isOn) $wrap->addClass('InputfieldRepeaterOff'); + } + + $wrap->prepend($delete); + $wrap->prepend($sort); + if($depth) $wrap->prepend($depth); + $wrap->prepend($loaded); + } else { + $wrap->add($inputfields); } - - $wrap->add($inputfields); - $wrap->prepend($delete); - $wrap->prepend($sort); - if($depth) $wrap->prepend($depth); - $wrap->prepend($loaded); - + if($isMinItem) { // allow this ready item to be added so that minimum is met $wrap->addClass('InputfieldRepeaterMinItem'); @@ -455,7 +467,7 @@ class InputfieldRepeater extends Inputfield implements InputfieldItemList { $inputfield->appendMarkup .= "'; } } - } else { + } else if(!$isSingle) { // create a new/blank item to be used as a template for any new items added /** @var InputfieldWrapper $wrap */ $wrap = $this->wire('modules')->get('InputfieldFieldset'); @@ -715,6 +727,8 @@ class InputfieldRepeater extends Inputfield implements InputfieldItemList { */ public function ___processInput(WireInputData $input) { + $isSingle = $this->repeaterMinItems == 1 && $this->repeaterMaxItems == 1; + /** @var PageArray $value */ $value = $this->attr('value'); $loadedIDs = array(); @@ -722,7 +736,7 @@ class InputfieldRepeater extends Inputfield implements InputfieldItemList { // determine which repeater pages have data posted in this request foreach($value as $key => $page) { $loadedName = "loaded_repeater$page->id"; - if(((int) $input->$loadedName) > 0) $loadedIDs[$page->id] = $page->id; + if($isSingle || ((int) $input->$loadedName) > 0) $loadedIDs[$page->id] = $page->id; } $this->buildForm(0, $loadedIDs); @@ -741,33 +755,37 @@ class InputfieldRepeater extends Inputfield implements InputfieldItemList { $isHidden = $page->isHidden(); $isUnpublished = $page->isUnpublished(); $isOn = $page->hasStatus(Page::statusOn); - - $deleteName = "delete_repeater{$page->id}"; - $sortName = "sort_repeater{$page->id}"; - $publishName = "publish_repeater{$page->id}"; - $depthName = "depth_repeater{$page->id}"; - if($input->$deleteName == $page->id) { - $value->remove($page); - $numChanges++; - continue; - } + if($isSingle) { + $publishName = ''; + } else { + $deleteName = "delete_repeater{$page->id}"; + $sortName = "sort_repeater{$page->id}"; + $publishName = "publish_repeater{$page->id}"; + $depthName = "depth_repeater{$page->id}"; - $sort = $input->$sortName; - // skip pages that don't appear in the POST data (most likely ready pages) - if(is_null($sort)) continue; - - $page->sort = (int) $sort; - if($page->isChanged('sort')) { - // $this->message("Sort changed for field {$this->field} page {$page->id}", Notice::debug); - $sortChanged = true; - } - - if($this->repeaterDepth > 0) { - $depth = (int) $input->$depthName; - if($page->getDepth() != $depth) { - $page->setDepth($depth); + if($input->$deleteName == $page->id) { + $value->remove($page); $numChanges++; + continue; + } + + $sort = $input->$sortName; + // skip pages that don't appear in the POST data (most likely ready pages) + if(is_null($sort)) continue; + + $page->sort = (int) $sort; + if($page->isChanged('sort')) { + // $this->message("Sort changed for field {$this->field} page {$page->id}", Notice::debug); + $sortChanged = true; + } + + if($this->repeaterDepth > 0) { + $depth = (int) $input->$depthName; + if($page->getDepth() != $depth) { + $page->setDepth($depth); + $numChanges++; + } } } @@ -780,7 +798,7 @@ class InputfieldRepeater extends Inputfield implements InputfieldItemList { $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); - $publish = (int) $input->$publishName; + $publish = $isSingle ? 0 : (int) $input->$publishName; if($publish > 0 && ($isHidden || $isUnpublished)) { // publish requested (publish=1) @@ -808,7 +826,7 @@ class InputfieldRepeater extends Inputfield implements InputfieldItemList { $numChanges++; } - if($this->field->get('repeaterLoading') == FieldtypeRepeater::loadingOff) { + if(!$isSingle && $this->field->get('repeaterLoading') == FieldtypeRepeater::loadingOff) { $numNewItems = (int) $input["_{$this->name}_add_items"]; if($numNewItems) { // iterate through each new item added for non-ajax mode @@ -907,6 +925,24 @@ class InputfieldRepeater extends Inputfield implements InputfieldItemList { return $this; } + /** + * Set attribute + * + * @param array|string $key + * @param array|int|string $value + * @return InputfieldRepeater|Inputfield + * + */ + public function setAttribute($key, $value) { + if($key === 'value' && $value instanceof Page) { + if($this->field && method_exists($this->field->type, 'getRepeaterPageArray')) { + if(!$value->id) $value = null; + $value = $this->field->type->getRepeaterPageArray($this->page, $this->field, $value); + } + } + return parent::setAttribute($key, $value); + } + /** * Get the repeater wrappers (InputfieldWrappers) indexed by repeater page ID * diff --git a/wire/modules/Fieldtype/FieldtypeRepeater/RepeaterPage.php b/wire/modules/Fieldtype/FieldtypeRepeater/RepeaterPage.php index 30277ade..45aca55f 100644 --- a/wire/modules/Fieldtype/FieldtypeRepeater/RepeaterPage.php +++ b/wire/modules/Fieldtype/FieldtypeRepeater/RepeaterPage.php @@ -114,7 +114,14 @@ class RepeaterPage extends Page { return $this->forField; } - + + /** + * Get property + * + * @param string $key + * @return int|mixed|null + * + */ public function get($key) { $value = parent::get($key); if($key === 'depth' && is_null($value)) { @@ -122,7 +129,13 @@ class RepeaterPage extends Page { } return $value; } - + + /** + * Get depth + * + * @return int + * + */ public function getDepth() { if(is_null($this->depth)) { $this->depth = 0; @@ -131,7 +144,13 @@ class RepeaterPage extends Page { } return $this->depth; } - + + /** + * Set depth + * + * @param int $depth + * + */ public function setDepth($depth) { $name = $this->name; $_name = $name;