1
0
mirror of https://github.com/processwire/processwire.git synced 2025-08-09 08:17:12 +02:00

Updates to FieldtypeRepeater and InputfieldRepeater to support single mode, as used by FieldtypeFieldsetPage

This commit is contained in:
Ryan Cramer
2017-08-30 09:37:19 -04:00
parent 718baae573
commit d9fb9cd026
3 changed files with 208 additions and 141 deletions

View File

@@ -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);
}
}

View File

@@ -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 .= "<script>ProcessWire.config['$idAttr'] = " . json_encode($jsValue) . ';</script>';
}
}
} 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
*

View File

@@ -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;