Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
tree: 77249efc81
Fetching contributors…

Cannot retrieve contributors at this time

361 lines (294 sloc) 10.488 kb
<?php
/**
* ProcessWire Repeater Inputfield
*
* Maintains a collection of fields that are repeated for any number of times.
*
* For documentation about how Fieldtypes work, see:
* /wire/core/Fieldtype.php
* /wire/core/FieldtypeMulti.php
*
* ProcessWire 2.x
* Copyright (C) 2012 by Ryan Cramer
* Licensed under GNU/GPL v2, see LICENSE.TXT
*
* http://www.processwire.com
* http://www.ryancramer.com
*
*/
class InputfieldRepeater extends Inputfield {
public static function getModuleInfo() {
return array(
'title' => __('Repeater', __FILE__), // Module Title
'summary' => __('Repeats fields from another template. Provides the input for FieldtypeRepeater.', __FILE__), // Module Summary
'version' => 100,
'requires' => 'FieldtypeRepeater',
);
}
/**
* Array of InputfieldWrapper objects indexed by repeater page ID
*
*/
protected $wrappers = array();
/**
* Array of text labels indexed by repeater page ID
*
*/
protected $labels = array();
/**
* The page that the repeaters field lives on, set by FieldtypeRepeater::getInputfield
*
*/
protected $page = null;
/**
* The field this InputfieldRepeater is serving, set by FieldtypeRepeater::getInputfield
*
*/
protected $field = null;
/**
* Cached form containing the repeaters
*
*/
protected $form = null;
/**
* Set config defaults
*
*/
public function __construct() {
parent::__construct();
// these are part of the Fieldtype's config, and automatically set from it
$this->set('repeaterMaxItems', 0);
$this->set('repeaterReadyItems', 0);
}
/**
* Initialize the repeaters inputfield
*
*/
public function init() {
parent::init();
if(is_null($this->page)) $this->page = new NullPage();
$this->attr('value', new PageArray());
}
/**
* Build and cache the form containing the repeaters
*
* @return InputfieldWrapper
*
*/
protected function buildForm() {
// if it's already been built, then return the cached version
if(!is_null($this->form)) return $this->form;
// if required fields don't exist then exit
if(!$this->field || !$this->field->type instanceof FieldtypeRepeater) throw new WireException("You must set a 'field' (type Field) property to {$this->className} and the Fieldtype must be FieldtypeRepeater");
if(!$this->page || !$this->page->id) throw new WireException("You must set a 'page' (type Page) property to {$this->className} that has a repeater field assigned to its template");
$out = '';
$form = new InputfieldWrapper();
$value = $this->attr('value');
$language = wire('user')->language;
$languages = wire('languages');
// locate and remove any unpublished+hidden items so we can ensure they are at the end
$appendItems = array();
foreach($value as $key => $page) {
if($page->is(Page::statusUnpublished) && $page->is(Page::statusHidden)) {
$value->remove($page);
$appendItems[] = $page;
}
}
// if we found any unpublished/hidden items, then append them back at the end
if(count($appendItems)) {
$value->import($appendItems);
$value->resetTrackChanges(true);
}
$blankPage = $this->field->type->getBlankRepeaterPage($this->page, $this->field);
$template = $blankPage->template;
// get field label in user's language if available
$key = ($languages && $language && $language->id ? "label{$language->id}" : "label");
$label = $this->field->get($key);
if(!$label) $label = $this->field->label;
if(!$label) $label = ucfirst($this->field->name);
$cnt = 0;
// create field for each repeater iteration
foreach($value as $key => $page) {
// get the inputfields for the repeater page
$inputfields = $page->template->fieldgroup->getPageInputfields($page, "_repeater{$page->id}");
$this->wrappers[$page->id] = $inputfields;
// also add a delete checkbox to the repeater page fields
$delete = wire('modules')->get('InputfieldCheckbox');
$delete->attr('id+name', "delete_repeater{$page->id}");
$delete->class = 'InputfieldRepeaterDelete';
$delete->label = $this->_('Delete');
$delete->attr('value', $page->id);
$sort = wire('modules')->get('InputfieldHidden');
$sort->attr('id+name', "sort_repeater{$page->id}");
$sort->class = 'InputfieldRepeaterSort';
$sort->label = $this->_('Sort');
$sort->attr('value', $cnt);
$wrap = wire('modules')->get('InputfieldFieldset');
$wrap->label = "$label #" . (++$cnt);
// 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 = wire('modules')->get('InputfieldHidden');
$f->attr('name', "publish_repeater{$page->id}");
$f->attr('class', 'InputfieldRepeaterPublish');
$f->attr('value', $page->is(Page::statusHidden) ? 0 : 1);
$wrap->add($f);
if($page->is(Page::statusHidden)) {
$wrap->class = 'InputfieldRepeaterReady';
$wrap->label .= ' - ' . $this->_('New');
// add a hidden field that will be removed upon revealing it with JS
// this is so fields can have default values (like Datetime today's date)
$f = wire('modules')->get('InputfieldHidden');
$f->attr('name', "_disable_repeater{$page->id}");
$f->attr('class', 'InputfieldRepeaterDisabled');
$f->attr('value', $page->id);
$wrap->add($f);
} else if($page->is(Page::statusUnpublished)) {
$wrap->label .= ' - ' . $this->_('Unpublished');
}
$wrap->add($inputfields);
$wrap->prepend($delete);
$wrap->prepend($sort);
$form->add($wrap);
$this->labels[$page->id] = $wrap->label;
}
// create a new/blank item to be used as a template for any new items added
$wrap = wire('modules')->get('InputfieldFieldset');
$wrap->label = "$label #" . (++$cnt);
$wrap->description = $this->_('This item will become editable after you save.');
$wrap->class = 'InputfieldRepeaterNewItem';
$wrap->collapsed = Inputfield::collapsedNo;
$form->add($wrap);
// cache
$this->form = $form;
return $form;
}
/**
* Render the repeater items
*
*/
public function ___render() {
// a hidden checkbox with link that we use to identify when items have been added
$out = "\n<p class='InputfieldRepeaterAddItem'>" .
"\n\t<input type='text' name='_{$this->name}_add_items' value='0' />" .
"\n\t<span class='ui-icon ui-icon-plus'></span>" .
"\n\t<a href='#' class='InputfieldRepeaterAddLink'>" . $this->_('Add Item') . "</a>" .
"\n</p>";
return $this->buildForm()->render() . $out;
}
/**
* Process the input from a submitted repeaters field
*
*/
public function ___processInput(WireInputData $input) {
$this->buildForm();
$value = $this->attr('value'); // PageArray
$numChanges = 0;
$sortChanged = false;
// existing items
foreach($value as $key => $page) {
$deleteName = "delete_repeater{$page->id}";
$sortName = "sort_repeater{$page->id}";
$disabledName = "_disable_repeater{$page->id}";
$publishName = "publish_repeater{$page->id}";
if($input->$deleteName == $page->id) {
$value->remove($page);
continue;
}
// skip items disabled/hidden (ready items)
if($input->$disabledName == $page->id) {
$page->resetTrackChanges(false);
continue;
}
if($input->$publishName > 0 && $page->is(Page::statusHidden)) {
$page->removeStatus(Page::statusHidden | Page::statusUnpublished);
}
$page->sort = (int) $input->$sortName;
if($page->isChanged('sort')) {
$this->message("Sort changed for field {$this->field} page {$page->id}", Notice::debug);
$sortChanged = true;
}
$wrapper = $this->wrappers[$page->id];
$wrapper->resetTrackChanges(true);
$wrapper->processInput($input);
$this->formToPage($wrapper, $page);
if($page->isChanged() && $this->page->id) $numChanges++;
}
// if the sort changed, then tell the PageArray to sort by _repeater_sort
if($sortChanged) {
$this->value->sort('sort');
$numChanges++;
}
// if changes occurred, then tell $this->page and the PageArray $value
if($numChanges) {
$this->page->trackChange($this->attr('name'));
$this->trackChange('value');
}
// if no value present, then no new items were added so we can exit now
$numNewItems = (int) $input["_{$this->name}_add_items"];
if(!$numNewItems) return $this;
// new items were added
$newItems = array();
$name = $this->attr('name');
// iterate through each new item
for($n = 0; $n < $numNewItems; $n++) {
$page = $this->field->type->getBlankRepeaterPage($this->page, $this->field);
$page->removeStatus(Page::statusHidden);
$page->sort = count($value);
$value->add($page);
}
return $this;
}
/**
* Take a form (InputfieldWrapper) and map the data to a Page that has the same fields
*
* @TODO convert this to it's own FormToPage class to avoid duplication between this as ProcessPageEdit
*
*/
protected function formToPage(InputfieldWrapper $wrapper, Page $page, $level = 0) {
$languages = wire('languages');
foreach($wrapper as $inputfield) {
$name = $inputfield->attr('name');
$name = preg_replace('/_repeater\d+$/', '', $name);
if($name && $inputfield->isChanged()) {
if($languages && $inputfield->useLanguages) {
$value = $page->get($name);
if(is_object($value)) {
$value->setFromInputfield($inputfield);
$page->set($name, $value);
}
} else {
$value = $inputfield->attr('value');
$page->set($name, $value);
}
if($page->isChanged($name)) {
// if a 'ready' page was changed, then we may now consider it a regular repeater page
if($page->is(Page::statusHidden)) $page->removeStatus(Page::statusHidden);
}
}
if($inputfield instanceof InputfieldWrapper && count($inputfield->getChildren())) {
$this->formToPage($inputfield, $page, $level + 1);
}
}
}
/**
* Returns whether any values are present
*
*/
public function isEmpty() {
return count($this->attr('value')) == 0;
}
/**
* Override the default set() to capture the required $page variable that the repeaters field lives on.
*
*/
public function set($key, $value) {
if($key == 'page') $this->page = $value;
else if($key == 'field') $this->field = $value;
else return parent::set($key, $value);
return $this;
}
public function ___getConfigInputfields() {
$inputfields = parent::___getConfigInputfields();
return $inputfields;
}
}
Jump to Line
Something went wrong with that request. Please try again.