1
0
mirror of https://github.com/processwire/processwire.git synced 2025-08-15 03:05:26 +02:00

Add FieldtypeFielsetPage module to core as part of the repeaters package

This commit is contained in:
Ryan Cramer
2017-09-01 10:58:52 -04:00
parent 3cb9c46e7d
commit c0e8485a77
3 changed files with 615 additions and 0 deletions

View File

@@ -0,0 +1,104 @@
<?php namespace ProcessWire;
/**
* FieldsetPage represents Page objects used by the FieldtypeFieldsetPage module
*
* ProcessWire 3.x, Copyright 2017 by Ryan Cramer
* https://processwire.com
*
*/
class FieldsetPage extends RepeaterPage {
/**
* Track a change to a property in this object
*
* The change will only be recorded if change tracking is enabled for this object instance.
*
* #pw-group-changes
*
* @param string $what Name of property that changed
* @param mixed $old Previous value before change
* @param mixed $new New value
* @return $this
*
*/
public function trackChange($what, $old = null, $new = null) {
if($this->trackChanges()) {
$forPage = $this->getForPage();
$forField = $this->getForField();
if($forPage && $forField) $forPage->trackChange($forField->name);
}
return parent::trackChange($what, $old, $new);
}
/**
* Get a property
*
* @param string $key
* @return mixed
*
*/
public function get($key) {
// mirror the output formatting state of the owning page
if($this->forPage) {
$of = $this->forPage->of();
if($of != $this->of()) $this->of($of);
}
if(strpos($key, 'for_page_') === 0) {
list(,$property) = explode('for_page_', $key);
if($property) return $this->getForPage()->get($property);
}
return parent::get($key);
}
/**
* Return the page that this repeater item is for
*
* @return Page
*
*/
public function getForPage() {
if(!is_null($this->forPage)) return $this->forPage;
$prefix = FieldtypeRepeater::repeaterPageNamePrefix; // for-page-
$name = $this->name;
if(strpos($name, $prefix) === 0) {
// determine owner page from name in format: for-page-1234
$forID = (int) substr($name, strlen($prefix));
$this->forPage = $this->wire('pages')->get($forID);
} else {
$this->forPage = $this->wire('pages')->newNullPage();
}
return $this->forPage;
}
/**
* Return the field that this repeater item belongs to
*
* @return Field
*
*/
public function getForField() {
if(!is_null($this->forField)) return $this->forField;
$parentName = $this->parent()->name;
$prefix = FieldtypeRepeater::fieldPageNamePrefix; // for-field-
if(strpos($parentName, $prefix) === 0) {
// determine field from grandparent name in format: for-field-1234
$forID = (int) substr($parentName, strlen($prefix));
$this->forField = $this->wire('fields')->get($forID);
}
return $this->forField;
}
}

View File

@@ -0,0 +1,75 @@
<?php namespace ProcessWire;
function FieldsetPageInstructions(Field $field) {
$exampleName = 'title';
$exampleText = __('This is some example text');
$repeaterFields = $field->get('repeaterFields');
if(is_array($repeaterFields)) foreach($repeaterFields as $fid) {
$f = $field->wire('fields')->get((int) $fid);
if($f && $f->type instanceof FieldtypeText) {
$exampleName = $f->name;
break;
}
}
$f = $field->wire('modules')->get('InputfieldMarkup');
$f->attr('name', '_instructions');
$f->label = __('Instructions on how to use this field');
$f->collapsed = Inputfield::collapsedYes;
$f->icon = 'life-ring';
$f->value =
"<p class='description'>" .
__('This type of fieldset uses a separate page (behind the scenes) to store values for the fields you select above.') . ' ' .
__('A benefit is that you can re-use fields that might already be present on your page, outside of the fieldset.') . ' ' .
__('For example, you could have a “title” field on your page, and another in your fieldset. Likewise for any other fields.') . ' ' .
__('This is possible because fields in the fieldset are in their own namespace—another page—separate from the main page.') . ' ' .
__('Below are several examples on how to use this field in your template files and from the API.') .
"</p>" .
"<p>" .
"<span class='notes'>" . __('Getting a value:') . "</span><br />" .
"<code>\$$exampleName = \$page->{$field->name}->$exampleName;</code>" .
"</p>" .
"<p>" .
"<span class='notes'>" . __('Outputting a value:') . "</span><br />" .
"<code>echo \$page->{$field->name}->$exampleName;</code>" .
"</p>" .
"<p>" .
"<span class='notes'>" . __('Outputting a value when in markup:') . "</span><br />" .
"<code>&lt;div class='example'&gt;<code><br />" .
"<code>&nbsp; &lt;?=\$page->{$field->name}->$exampleName?&gt;</code><br />" .
"<code>&lt;/div&gt;</code>" .
"</p>" .
"<p>" .
"<span class='notes'>" . __('Setting a value:') . "</span><br />" .
"<code>\$page->{$field->name}->$exampleName = '$exampleText';</code>" .
"</p>" .
"<p>" .
"<span class='notes'>" . __('Setting and saving a value:') . "</span><br />" .
"<code>\$page->of(false); <span class='detail'>// " . __('this turns off output formatting, when necessary') .
"</span></code><br />" .
"<code>\$page->{$field->name}->$exampleName = '$exampleText';</code><br />" .
"<code>\$page->save();</code>" .
"</p>" .
"<p>" .
"<span class='notes'>" . __('Assigning fieldset to another (shorter) variable and outputting a value:') . "</span><br />" .
"<code>\$p = \$page->{$field->name};</code><br />" .
"<code>echo \$p->$exampleName;</code>" .
"</p>" .
"<p>" .
"<span class='notes'>" .
sprintf(__('Finding pages having fieldset with “%s” field containing text “example”:'), $exampleName) .
"</span><br />" .
"<code>\$items = \$pages->find('$field->name.$exampleName%=example');</code>" .
"</p>";
return $f;
}

View File

@@ -0,0 +1,436 @@
<?php namespace ProcessWire;
require_once(dirname(__FILE__) . '/FieldtypeRepeater.module');
/**
* ProcessWire Fieldset (Page)
*
* Maintains a collection of fields as a fieldset via an independent Page.
*
* ProcessWire 3.x, Copyright 2017 by Ryan Cramer
* https://processwire.com
*
* @property int $repeatersRootPageID
*
*/
class FieldtypeFieldsetPage extends FieldtypeRepeater implements ConfigurableModule {
public static function getModuleInfo() {
return array(
'title' => __('Fieldset (Page)', __FILE__), // Module Title
'summary' => __('Fieldset with fields isolated to separate namespace (page), enabling re-use of fields.', __FILE__), // Module Summary
'version' => 1,
'autoload' => true,
'requires' => 'FieldtypeRepeater'
);
}
/**
* Construct
*
*/
public function __construct() {
parent::__construct();
require_once(dirname(__FILE__) . '/FieldsetPage.php');
}
/**
* Get the FieldsetPage object for the given $page and $field, or NullPage if field is not on page
*
* @param Page $page
* @param Field $field
* @param bool $createIfNotExists
* @return FieldsetPage|NullPage
*
*/
public function getFieldsetPage(Page $page, Field $field, $createIfNotExists = true) {
/** @var FieldsetPage|NullPage $readyPage */
if(!$page->hasField($field)) return new NullPage();
$parent = $this->getRepeaterPageParent($page, $field);
if($page->id) {
$name = self::repeaterPageNamePrefix . $page->id;
$readyPage = $parent->child("name=$name, include=all");
} else {
$name = '';
$readyPage = new NullPage();
}
if(!$readyPage->id) {
$class = $this->getPageClass();
$readyPage = $this->wire(new $class());
$readyPage->template = $this->getRepeaterTemplate($field);
if($parent->id) $readyPage->parent = $parent;
$readyPage->addStatus(Page::statusOn);
if($name) $readyPage->name = $name;
if($name && $createIfNotExists) {
$readyPage->save();
$readyPage->setQuietly('_repeater_new', 1);
$this->readyPageSaved($readyPage, $page, $field);
} else {
$readyPage->setQuietly('_repeater_new', 1);
}
}
if($readyPage instanceof FieldsetPage) {
$readyPage->setForPage($page);
$readyPage->setForField($field);
$readyPage->setTrackChanges(true);
}
return $readyPage;
}
/**
* Convert a repeater Page to a PageArray for use with methods/classes expecting RepeaterPageArray
*
* @param Page $page
* @param Field $field
* @param RepeaterPage|FieldsetPage $value Optionally add this item to it
* @return RepeaterPageArray|PageArray
*
*/
public function getRepeaterPageArray(Page $page, Field $field, $value = null) {
if($value && $value instanceof PageArray) return $value;
$pageArray = parent::getBlankValue($page, $field);
if($value && $value instanceof Page) $pageArray->add($value);
$pageArray->resetTrackChanges();
return $pageArray;
}
/**
* Get the class used for repeater Page objects
*
* @return string
*
*/
public function getPageClass() {
return __NAMESPACE__ . "\\FieldsetPage";
}
/**
* Get a blank value of this type, i.e. return a blank PageArray
*
* @param Page $page
* @param Field $field
* @return FieldsetPage|PageArray
*
*/
public function getBlankValue(Page $page, Field $field) {
return $this->getFieldsetPage($page, $field, false);
}
/**
* Return an InputfieldRepeater, ready to be used
*
* @param Page $page Page being edited
* @param Field $field Field that needs an Inputfield
* @return Inputfield
*
*/
public function getInputfield(Page $page, Field $field) {
$hasField = $page->hasField($field->name);
/** @var InputfieldRepeater $inputfield */
$inputfield = $this->wire('modules')->get($this->getInputfieldClass());
$inputfield->set('page', $page);
$inputfield->set('field', $field);
$inputfield->set('repeaterDepth', 0);
$inputfield->set('repeaterReadyItems', 0); // ready items deprecated
if($hasField) {
$inputfield->set('repeaterMaxItems', 1);
$inputfield->set('repeaterMinItems', 1);
$inputfield->set('singleMode', true);
}
if($page->id && $hasField) {
$item = $page->get($field->name);
if(!$item->id) $item = null;
} else {
$item = null;
}
$value = $this->getRepeaterPageArray($page, $field, $item);
$inputfield->val($value);
return $inputfield;
}
/**
* Returns a page ready for use as a repeater
*
* @param Field $field
* @param Page $page The page that the repeater field lives on
* @return Page
*
*/
public function getBlankRepeaterPage(Page $page, Field $field) {
return $this->getFieldsetPage($page, $field, true);
}
/**
* Get next page ready to be used as a new repeater item, creating it if it doesn't already exist
*
* @param Page $page
* @param Field $field
* @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, $value = null, array $notIDs = array()) {
return $this->getFieldsetPage($page, $field, true);
}
/**
* Given a raw value (value as stored in DB), return the value as it would appear in a Page object
*
* Something to note is that this wakeup function is different than most in that the $value it is given
* is just an array like array('data' => 123, 'parent_id' => 456) -- it doesn't actually contain any of the
* repeater page data other than saying how many there are and the parent where they are stored. So this
* wakeup function can technically do its job without even having the $value, unlike most other fieldtypes.
*
* @param Page $page
* @param Field $field
* @param array $value
* @return FieldsetPage|Page $value
*
*/
public function ___wakeupValue(Page $page, Field $field, $value) {
if($value instanceof Page) return $value;
return $this->getFieldsetPage($page, $field, true);
}
/**
* Return the database schema in predefined format
*
* @param Field $field
* @return array
*
*/
public function getDatabaseSchema(Field $field) {
$schema = parent::getDatabaseSchema($field);
unset(
$schema['parent_id'],
$schema['count'],
$schema['keys']['parent_id'],
$schema['keys']['count'],
$schema['keys']['data_exact']
);
$schema['data'] = 'int UNSIGNED NOT NULL';
$schema['keys']['data'] = 'KEY `data` (`data`)';
return $schema;
}
/**
* Given an 'awake' value, as set by wakeupValue, convert the value back to a basic type for storage in DB.
*
* In this case, the sleepValue doesn't represent the actual value as they are stored in pages.
*
* @param Page $page
* @param Field $field
* @param string|int|array|object $value
* @return int
*
*/
public function ___sleepValue(Page $page, Field $field, $value) {
if($value instanceof PageArray) $value = $value->first();
$sleepValue = $value instanceof Page ? (int) $value->id : 0;
/*
$sleepValue = array(
'data' => "$id",
'count' => $id > 0 ? 1 : 0,
'parent_id' => $id
);
*/
return $sleepValue;
}
/**
* Export repeater value
*
* @param Page $page
* @param Field $field
* @param RepeaterPageArray$value
* @param array $options
* @return array
*
*/
public function ___exportValue(Page $page, Field $field, $value, array $options = array()) {
$value = $this->getRepeaterPageArray($page, $field, $value); // export as PageArray
return parent::___exportValue($page, $field, $value, $options);
}
/**
* Return the parent used by the repeater pages for the given Page and Field
*
* Unlike regular repeaters, all page items for a given field share the same parent, regardless of owning page.
*
* i.e. /processwire/repeaters/for-field-123/
*
* @param Page $page
* @param Field $field
* @return Page
*
*/
protected function getRepeaterPageParent(Page $page, Field $field) {
return $this->getRepeaterParent($field);
}
/**
* Handles the sanitization and convertion to RepeaterPage value
*
* @param Page $page
* @param Field $field
* @param mixed $value
* @return FieldsetPage|Page
*
*/
public function sanitizeValue(Page $page, Field $field, $value) {
if(is_string($value) || is_array($value)) {
$value = parent::sanitizeValue($page, $field, $value);
}
if($value instanceof PageArray) {
if($value->count()) {
$value = $value->first();
} else {
$value = $this->getFieldsetPage($page, $field);
}
}
if(!$value instanceof Page) {
$value = $this->getFieldsetPage($page, $field);
}
return $value;
}
/**
* Perform output formatting on the value delivered to the API
*
* This method is only used when $page->outputFormatting is true.
*
* @param Page $page
* @param Field $field
* @param Page|PageArray $value
* @return Page
*
*/
public function ___formatValue(Page $page, Field $field, $value) {
return $value;
}
/**
* Load the given page field from the database table and return the value.
*
* - Return NULL if the value is not available.
* - Return the value as it exists in the database, without further processing.
* - This is intended only to be called by Page objects on an as-needed basis.
* - Typically this is only called for fields that don't have 'autojoin' turned on.
* - Any actual conversion of the value should be handled by the `Fieldtype::wakeupValue()` method.
*
* #pw-group-loading
*
* @param Page $page Page object to save.
* @param Field $field Field to retrieve from the page.
* @return mixed|null
*
*/
public function ___loadPageField(Page $page, Field $field) {
return $this->getFieldsetPage($page, $field, true);
}
/**
* Save the given field from page
*
* @param Page $page Page object to save.
* @param Field $field Field to retrieve from the page.
* @return bool True on success, false on DB save failure.
*
*/
public function ___savePageField(Page $page, Field $field) {
if(!$page->id || !$field->id) return false;
$value = $page->get($field->name);
if(!$value instanceof Page) return false;
// make the FieldsetPage mirror unpublished/hidden state from its owner
if($page->isUnpublished()) {
if(!$value->isUnpublished()) $value->addStatus(Page::statusUnpublished);
} else {
if($value->isUnpublished()) $value->removeStatus(Page::statusUnpublished);
}
if($page->isHidden()) {
if(!$value->isHidden()) $value->addStatus(Page::statusHidden);
} else {
if($value->isHidden()) $value->removeStatus(Page::statusHidden);
}
$this->wire('pages')->save($value);
$table = $this->wire('database')->escapeTable($field->table);
$sql =
"INSERT INTO `$table` (pages_id, data) VALUES(:pages_id, :data) " .
"ON DUPLICATE KEY UPDATE `data`=VALUES(`data`)";
$query = $this->wire('database')->prepare($sql);
$query->bindValue(':pages_id', $page->id, \PDO::PARAM_INT);
$query->bindValue(':data', $value->id, \PDO::PARAM_INT);
$result = $query->execute();
return $result;
}
/**
* Return configuration fields definable for each FieldtypePage
*
* @param Field $field
* @return InputfieldWrapper
*
*/
public function ___getConfigInputfields(Field $field) {
$inputfields = parent::___getConfigInputfields($field);
$f = $inputfields->getChildByName('repeaterFields');
$f->label = $this->_('Fields in fieldset');
$f->description = $this->_('Define the fields that are used by this fieldset. You may also drag and drop fields to the desired order.');
$f->notes = '';
$field->repeaterLoading = FieldtypeRepeater::loadingOff;
$field->repeaterMaxItems = 1;
$field->repeaterMinItems = 1;
include(dirname(__FILE__) . '/FieldsetPageInstructions.php');
$inputfields->add(FieldsetPageInstructions($field));
return $inputfields;
}
/**
* Populate the settings for a newly created repeater template
*
* @param Template $template
*
*/
protected function populateRepeaterTemplateSettings(Template $template) {
parent::populateRepeaterTemplateSettings($template);
$template->pageLabelField = 'for_page_path';
}
}