getMarkup() as alternative to $labelFieldName * @property string $findPagesCode * @property string $findPagesSelector * @property string $findPagesSelect Same as findPageSelector, but configured interactively with InputfieldSelector . * @property int|bool $addable * @property int|bool $allowUnpub * @property int $derefAsPage * @property-read string $inputfieldClass Public property alias of protected getInputfieldClass() method * @property array $inputfieldClasses * * @method string renderAddable() * @method void processInputAddPages(WireInputData $input) * * @todo make findPagesCode disabled by default * */ class InputfieldPage extends Inputfield implements ConfigurableModule { public static function getModuleInfo() { return array( 'title' => 'Page', 'version' => 107, 'summary' => 'Select one or more pages', 'permanent' => true, ); } /** * @var Inputfield|null * */ protected $inputfieldWidget = null; /** * Default options for Inputfield classes * * @var array * */ protected static $defaultInputfieldClasses = array( 'InputfieldSelect', 'InputfieldSelectMultiple', 'InputfieldCheckboxes', 'InputfieldRadios', 'InputfieldAsmSelect', 'InputfieldPageListSelect', 'InputfieldPageAutocomplete', 'InputfieldTextTags', ); /** * Default configuration values * * @var array * */ protected static $defaultConfig = array( 'parent_id' => 0, 'template_id' => 0, 'template_ids' => array(), 'inputfield' => '', 'labelFieldName' => '', 'labelFieldFormat' => '', 'findPagesCode' => '', 'findPagesSelect' => '', 'findPagesSelector' => '', 'derefAsPage' => 0, 'addable' => 0, 'allowUnpub' => 0, // This option configured by FieldtypePage:Advanced ); /** * Contains true when this module is in configuration state (via it's getConfigInputfields function) * */ protected $configMode = false; /** * True when processInput is currently processing * */ protected $processInputMode = false; /** * True when in renderValue mode * * @var bool * */ protected $renderValueMode = false; /** * PageArray of pages that were added in the request * * @var PageArray|null * */ protected $pagesAdded; /** * CSS class names added to the Inputfield (will be applied to delegate Inputfield) * * @var array * */ protected $classesAdded = array(); /** * Construct * */ public function __construct() { $this->set('inputfieldClasses', self::$defaultInputfieldClasses); parent::__construct(); } /** * Init (populate default values) * */ public function init() { foreach(self::$defaultConfig as $key => $value) { $this->set($key, $value); } $this->attr('value', $this->wire('pages')->newPageArray()); parent::init(); } /** * Add a CSS class name (extends Inputfield::addClass) * * @param array|string $class * @param string $property * @return InputfieldPage|Inputfield * */ public function addClass($class, $property = 'class') { if($property == 'class') { $this->classesAdded[] = $class; } return parent::addClass($class, $property); } /** * Set an input attribute * * Overrides Inputfield::setAttribute() to capture 'value' attribute * * @param array|string $key * @param array|int|string $value * @return InputfieldPage|Inputfield * */ public function setAttribute($key, $value) { if($key == 'value') { $pages = $this->wire()->pages; if(is_string($value) || is_int($value)) { // setting the value attr from a string, whether 1234 or 123|446|789 if(ctype_digit("$value")) { // i.e. "1234" $a = $pages->newPageArray(); $page = $pages->get((int) $value); if($page->id) $a->add($page); $value = $a; } else if(strpos($value, '|') !== false) { // i.e. 123|456|789 $a = $pages->newPageArray(); foreach(explode('|', $value) as $id) { if(!ctype_digit("$id")) continue; $page = $pages->get((int) $id); if($page->id) $a->add($page); } $value = $a; } else { // unrecognized format } } } return parent::setAttribute($key, $value); } /** * Is the given $page valid for the given $field? * * Note that this validates all but findPagesCode (eval) based page selections. * This is primarily for use by FieldtypePage, but kept here since the config options * it uses to check are part of this module's config. * * If false is returned and given an $editPage, a reason for the false will be populated * to the $editPage->_isValidPage property. * * @param Page $page * @param Field|InputfieldPage|string|int $field Field instance of field name (string) or ID * @param Page $editPage Page being edited * @return bool * @throws WireException * */ public static function isValidPage(Page $page, $field, Page $editPage = null) { $pages = $page->wire()->pages; $user = $page->wire()->user; if(!$field instanceof Field && !$field instanceof InputfieldPage) { $field = $page->wire()->fields->get($field); if(!$field instanceof Field) throw new WireException('isValidPage requires a valid Field or field name'); } if($editPage && $editPage->id && $page->id == $editPage->id) { $editPage->setQuietly('_isValidPage', "Page is referencing itself and circular page reference not allowed"); return false; // prevent circular reference } if($pages->cloning) { return true; // bypass check when cloning is active } $valid = true; $findPagesSelector = $field->get('findPagesSelector'); if(empty($findPagesSelector)) $findPagesSelector = $field->get('findPagesSelect'); if($findPagesSelector) { $selector = $findPagesSelector; if($editPage && $editPage->id) $selector = self::populateFindPagesSelector($editPage, $selector); if(!$page->matches($selector)) { // failed in-memory check, attempt $page->count() check... $selector .= ", id=$page->id"; if($pages->count($selector)) { // looks like its okay } else { // also fails $pages->cont() check, so definitely not valid if($editPage) { $editPage->setQuietly('_isValidPage', "Page $page does not match " . ($user->isSuperuser() ? "findPagesSelector: $selector" : "required selector") ); } $valid = false; } } } // if($field->findPagesCode) { } // we don't currently validate these $parent_id = $field->get('parent_id'); if($parent_id && $parent_id != $page->parent_id) { $inputfieldClass = ltrim($field->get('inputfield'), '_'); if(empty($inputfieldClass)) $inputfieldClass = 'InputfieldSelect'; if(version_compare(PHP_VERSION, '5.3.8') >= 0) { $interfaces = wireClassImplements($inputfieldClass); if(in_array('InputfieldPageListSelection', $interfaces)) { // parent_id represents a root parent $rootParent = $pages->get($parent_id); if(!$page->parents()->has($rootParent)) $valid = false; } else { // parent_id represents a direct parent $valid = false; } if(!$valid && $editPage) { $editPage->setQuietly('_isValidPage', "Page $page does not have required parent $parent_id"); } } else { // PHP version prior to 5.3.8 // @deprecated $reflector = new \ReflectionClass($inputfieldClass); $valid = $reflector->implementsInterface('InputfieldPageListSelection'); } } $hasRequiredTemplate = true; $template_ids = FieldtypePage::getTemplateIDs($field); if(!empty($template_ids)) { $hasRequiredTemplate = in_array($page->template->id, $template_ids); } if(!$hasRequiredTemplate) { $valid = false; if($editPage) { $editPage->setQuietly('_isValidPage', "Page $page does not have required template(s): " . implode(',', $template_ids)); } } return $valid; } /** * Execute the findPagesCode * * @param Page $page The page being edited * @return PageArray (hopefully) * @deprecated Use hook to InputfieldPage::getSelectablePages() instead * */ protected function ___findPagesCode(Page $page) { if($page) {} $pages = $this->wire('pages'); // so that it is locally scoped to the eval if(empty($this->findPagesCode)) return $pages->newPageArray(); return eval($this->findPagesCode); } public function has($key) { // ensures it accepts any config value (like those for delegate inputfields) return true; } public function getSetting($key) { if($key === 'inputfieldClass') return $this->getInputfieldClass(); if($key === 'template_ids') return $this->getTemplateIDs(); $value = parent::getSetting($key); if($key === 'template_id' && empty($value)) { $templateIDs = $this->getTemplateIDs(); if(!empty($templateIDs)) $value = reset($templateIDs); } return $value; } /** * Return PageArray of selectable pages for this input * * @param Page $page The Page being edited * @return PageArray|null * */ public function ___getSelectablePages(Page $page) { $pages = $this->wire()->pages; $lockedModes = array(Inputfield::collapsedNoLocked, Inputfield::collapsedYesLocked); $statusUnder = $this->allowUnpub ? Page::statusTrash : Page::statusUnpublished; $children = null; $templateIDs = $this->getTemplateIDs(true); $findPagesSelector = $this->getSetting('findPagesSelector'); if(empty($findPagesSelector)) $findPagesSelector = $this->getSetting('findPagesSelect'); if($this->configMode) { $children = $pages->newPageArray(); } else if($this->renderValueMode || in_array($this->getSetting('collapsed'), $lockedModes)) { $children = $this->attr('value'); // convert to PageArray if not already if($children instanceof Page) { $children = $children->and(); } else if(!$children instanceof PageArray) { $children = $pages->newPageArray(); } } else if($findPagesSelector) { // a find() selector $instance = $this->processInputMode ? $this : null; $selector = self::populateFindPagesSelector($page, $findPagesSelector, $instance); $children = $pages->find($selector); } else if($this->findPagesCode) { // php statement that returns a PageArray or a Page (to represent a parent) $children = $this->findPagesCode($page); if($children instanceof Page) $children = $children->children(); // @teppokoivula } else if($this->parent_id) { $parent = $pages->get($this->parent_id); if($parent) { if($templateIDs) { $children = $parent->children("templates_id=$templateIDs, check_access=0, status<$statusUnder"); } else { $children = $parent->children("check_access=0, status<$statusUnder"); } } } else if($templateIDs) { $children = $pages->find("templates_id=$templateIDs, check_access=0, status<$statusUnder"); } else { $children = $pages->newPageArray(); } if($children && $children->has($page)) { $children->remove($page); // don't allow page being edited to be selected } return $children; } /** * Return array or string of configured template IDs * * @param bool $getString Specify true to return a 1|2|3 style string rather than an array * @return array|string * */ public function getTemplateIDs($getString = false) { $templateIDs = parent::getSetting('template_ids'); $templateID = parent::getSetting('template_id'); return FieldtypePage::getTemplateIDs(array($templateIDs, $templateID), $getString); } /** * Populate any variables in findPagesSelector * * @param Page $page * @param string $selector * @param Inputfield $inputfield * @return string * */ protected static function populateFindPagesSelector(Page $page, $selector, $inputfield = null) { // if an $inputfield is passed in, then we want to retrieve dependent values directly // from the form, rather than from the $page /** @var InputfieldWrapper $form */ if($inputfield) { // locate the $form $n = 0; $form = $inputfield; do { $form = $form->getParent(); if(++$n > 10) break; } while($form && $form->className() != 'InputfieldForm'); } else $form = null; // find variables identified by: page.field or page.field.subfield if(strpos($selector, '=page.') !== false) { preg_match_all('/=page\.([_.a-zA-Z0-9]+)/', $selector, $matches); foreach($matches[0] as $key => $tag) { $field = $matches[1][$key]; $subfield = ''; if(strpos($field, '.')) list($field, $subfield) = explode('.', $field); $value = null; if($form && (!$subfield || $subfield == 'id')) { // attempt to get value from the form, to account for ajax changes that would not yet be reflected on the page $in = $form->getChildByName($field); if($in) $value = $in->attr('value'); } if(is_null($value)) $value = $page->get($field); if(is_object($value) && $subfield) $value = $value->$subfield; if(is_array($value)) $value = implode('|', $value); if(!strlen("$value") && (!$subfield || $subfield == 'id')) $value = '-1'; // force fail $selector = str_replace($tag, "=$value", $selector); } } return $selector; } /** * Create a page finding selector from all configured properties * * @param array $options * @return string|array * */ public function createFindPagesSelector(array $options = array()) { $defaults = array( 'page' => null, // optional $page for context (for selectors where it might matter) 'findRaw' => false, // include properties only recognized by $pages->findRaw()? 'getArray' => false, // return array rather than string? ); $options = array_merge($defaults, $options); $id = $this->getSetting('parent_id'); if($id) { if(is_array($id)) $id = implode('|', $id); $selector['parent_id'] = $id; } $ids = $this->getTemplateIDs(); if(count($ids)) { $selector['templates_id'] = implode('|', $ids); } else { $id = $this->getSetting('template_id'); if(is_array($id)) $id = implode('|', $id); if($id) $selector['templates_id'] = $id; } $findPagesCode = $this->getSetting('findPagesCode'); if(strlen($findPagesCode) && $options['page'] && empty($selector['parent_id'])) { // via teppokoivula: use findPagesCode to return single parent page $parent = $this->findPagesCode($options['page']); if($parent instanceof Page) $selector['parent_id'] = $parent->id; } $s = $this->getSetting('findPagesSelector'); if(!strlen($s)) $s = $this->getSetting('findPagesSelect'); if(strlen($s)) { if($options['page']) $s = self::populateFindPagesSelector($options['page'], $s, $this); // @todo getstring vs getarray $selector['selector'] = $s; } if($this->getSetting('allowUnpub')) { $selector['include'] = 'unpublished'; } else { $selector['include'] = 'hidden'; } if($options['findRaw']) { $labelFieldName = $this->getSetting('labelFieldName'); $labelFieldFormat = $this->getSetting('labelFieldFormat'); if(strlen($labelFieldFormat) && $labelFieldName === '.') { // @todo find raw does not support labelFieldFormat $selector['field'] = "$labelFieldFormat"; } else if($labelFieldName) { $selector['field'] = $labelFieldName == '.' ? "name" : "$labelFieldName"; } else { $selector['field'] = 'title'; } } if($options['getArray']) return $selector; $a = array(); $operatorChars = Selectors::getOperatorChars(); foreach($selector as $key => $value) { $v = substr($value, 0, 1); $operator = strlen($v) && isset($operatorChars[$v]) ? '' : '='; // omit operator if already in $value $a[] = "$key$operator$value"; } return implode(', ', $a); } /** * Get a label for the given page * * @param Page $page * @param bool $allowMarkup Whether or not to allow markup in the label (default=false) * @return string * */ public function getPageLabel(Page $page, $allowMarkup = false) { $label = ''; if(strlen($this->labelFieldFormat) && $this->labelFieldName == '.') { $label = $page->getMarkup($this->labelFieldFormat); } else if($this->labelFieldName === '.') { // skip } else if($this->labelFieldName) { $label = $page->get($this->labelFieldName); } if(!strlen($label)) $label = $page->name; if($page->hasStatus(Page::statusUnpublished)) $label .= ' ' . $this->_('(unpublished)'); if(!$allowMarkup) $label = $this->wire('sanitizer')->markupToLine($label); return $label; } /** * Get the selected Inputfield class for input (adjuted version of $this->inputfield) * * @return string * */ protected function getInputfieldClass() { return ltrim($this->getSetting('inputfield'), '_'); } /** * Get delegate Inputfield for page selection * * @return Inputfield|null * @throws WireException * */ public function getInputfield() { if($this->inputfieldWidget && ((string) $this->inputfieldWidget) == $this->getInputfieldClass()) { return $this->inputfieldWidget; } /** @var Inputfield $inputfield */ $inputfield = $this->wire()->modules->get($this->getInputfieldClass()); if(!$inputfield) return null; $inputfield->set('hasField', $this->hasField); $inputfield->set('hasInputfield', $this); $page = $this->page; $input = $this->wire()->input; $process = $this->wire()->process; if($this->derefAsPage) $inputfield->set('maxSelectedItems', 1); if($process && $process instanceof WirePageEditor) $page = $process->getPage(); $inputfield->attr('name', $this->attr('name')); $inputfield->attr('id', $this->attr('id')); $keys = array('label', 'description', 'notes', 'detail'); foreach($keys as $key) { $value = $this->getSetting($key); if(strlen($value)) $inputfield->set($key, $value); } $collapsed = $this->getSetting('collapsed'); if($collapsed == Inputfield::collapsedYesAjax || ($collapsed == Inputfield::collapsedBlankAjax && $this->isEmpty())) { // quick exit when possible due to ajax field, and not being time to render or process it if($this->getParent()) { // limit only to inputfields that have a parent, to keep out of other form contexts like Lister $renderInputfieldAjax = $input->get('renderInputfieldAjax'); $processInputfieldAjax = $input->post('processInputfieldAjax'); if(!is_array($processInputfieldAjax)) $processInputfieldAjax = array(); if($renderInputfieldAjax != $this->attr('id') && !in_array($this->attr('id'), $processInputfieldAjax)) { $this->inputfieldWidget = $inputfield; return $inputfield; } } } $value = $this->attr('value'); $valueArray = array(); if($value instanceof Page) { $valueArray[$value->id] = $value; } else if($value instanceof PageArray) { foreach($value as $v) { $valueArray[$v->id] = $v; } } if($inputfield instanceof InputfieldSupportsPageSelector && $inputfield->setPageSelector('') !== false) { // Inputfield has ability to find pages with a selector $selector = $this->createFindPagesSelector(array('page' => $page)); $inputfield->setPageSelector($selector); if($inputfield instanceof InputfieldHasSelectableOptions) { foreach($valueArray as $p) { $inputfield->addOption($p->id, $this->getPageLabel($p)); } } } else if(method_exists($inputfield, 'addOption') || $inputfield instanceof InputfieldHasSelectableOptions) { // All selectable options types $children = $this->getSelectablePages($page); if($children) { foreach($children as $child) { $label = $this->getPageLabel($child); $inputfield->addOption($child->id, $label); } } } else { // InputfieldPageAutocomplete or similar (older style InputfieldSupportsPageSelector) $parent_id = $this->getSetting('parent_id'); $template_id = $this->getSetting('template_id'); $template_ids = $this->getTemplateIDs(); $findPagesCode = $this->getSetting('findPagesCode'); $findPagesSelector = $this->getSetting('findPagesSelector'); $labelFieldName = $this->getSetting('labelFieldName'); $labelFieldFormat = $this->getSetting('labelFieldFormat'); if(empty($findPagesSelector)) $findPagesSelector = $this->getSetting('findPagesSelect'); if($parent_id) { $inputfield->set('parent_id', $parent_id); } else if($findPagesCode) { // @teppokoivula: use findPagesCode to return single parent page $child = $this->findPagesCode($page); if($child instanceof Page) $inputfield->set('parent_id', $child->id); } if($template_id) $inputfield->set('template_id', $template_id); if(!empty($template_ids)) $inputfield->set('template_ids', $template_ids); if($findPagesSelector) { $inputfield->set('findPagesSelector', self::populateFindPagesSelector($page, $findPagesSelector)); } if(strlen($labelFieldFormat) && $labelFieldName === '.') { $inputfield->set('labelFieldName', $labelFieldFormat); $inputfield->set('labelFieldFormat', $labelFieldFormat); } else { $inputfield->set('labelFieldName', $labelFieldName == '.' ? 'name' : $labelFieldName); $inputfield->set('labelFieldFormat', ''); } } if($value instanceof Page) { $inputfield->attr('value', $value->id); // derefAsPage } else if($inputfield instanceof InputfieldPageListSelect) { if($value instanceof PageArray) $value = $value->first(); $inputfield->attr('value', $value); } else if($inputfield instanceof InputfieldHasArrayValue || $inputfield instanceof InputfieldSupportsArrayValue) { $inputfield->attr('value', array_keys($valueArray)); } else { $inputfield->attr('value', $value); } // pass along any relevant configuration items foreach($this->data as $key => $value) { if(in_array($key, array('value', 'collapsed')) || array_key_exists($key, self::$defaultConfig)) continue; if($key == 'required' && empty($this->data['defaultValue'])) continue; // for default value support with InputfieldSelect $inputfield->set($key, $value); } $inputfield->set('allowUnpub', $this->getSetting('allowUnpub')); $this->inputfieldWidget = $inputfield; return $inputfield; } /** * Called before render() * * @param Inputfield $parent * @param bool $renderValueMode * @return bool * */ public function renderReady(Inputfield $parent = null, $renderValueMode = false) { $this->renderValueMode = $renderValueMode; parent::renderReady($parent, $renderValueMode); $inputfield = $this->getInputfield(); if(!$inputfield) { $this->error($this->_('This field needs to be configured before it can be used.')); return false; } $this->addClass('InputfieldNoFocus', 'wrapClass'); return $inputfield->renderReady($this, $renderValueMode); } /** * Render * * @return string * @throws WireException * */ public function ___render() { $inputfield = $this->getInputfield(); if(!$inputfield) return $this->attr('name'); $classes = InputfieldWrapper::getClasses(); $class = $inputfield->className(); if(isset($classes[$class]['item_content'])) $class .= " " . $classes[$class]['item_content']; foreach($this->classesAdded as $addClass) { $inputfield->addClass($addClass); } $out = "
"; $out .= $inputfield->render(); $out .= $this->renderAddable(); $findPagesSelector = $this->getSetting('findPagesSelector'); if(empty($findPagesSelector)) $findPagesSelector = $this->getSetting('findPagesSelect'); $labelFieldFormat = $this->getSetting('labelFieldFormat'); $labelFieldName = $this->getSetting('labelFieldName'); if($findPagesSelector) { $selector = $this->wire()->sanitizer->entities($findPagesSelector); $formatName = ''; if($this->wire()->user->hasPermission('page-edit') && strlen($labelFieldFormat) && $labelFieldName === '.') { /** @var ProcessPageSearch $pps */ $formatName = "page_" . $this->attr('name'); try { /** @var ProcessPageSearch $pps */ $pps = $this->wire()->modules->get('ProcessPageSearch'); $pps->setDisplayFormat($formatName, $labelFieldFormat); } catch(\Exception $e) { // most likely user does not have access to ProcessPageSearch } } $labelFieldName = $labelFieldName == '.' ? 'name' : $labelFieldName; $out .= ""; } $out .= "
"; return $out; } /** * Render the add page(s) section * * @return string * @throws WireException * */ protected function ___renderAddable() { $parent_id = $this->getSetting('parent_id'); $template_id = $this->getSetting('template_id'); $labelFieldName = $this->getSetting('labelFieldName'); if(!$this->getSetting('addable') || !$parent_id || !$template_id) return ''; if($labelFieldName && $labelFieldName != 'title') return ''; $parent = $this->wire('pages')->get($parent_id); $test = $this->wire('pages')->newPage($template_id); $test->parent = $parent; $test->id = -1; // prevents permissions check from failing if(!$parent->addable($test)) return ''; if(!$test->publishable()) return ''; $inputfield = $this->wire('modules')->get($this->getInputfieldClass()); if(!$inputfield) return ''; $key = "_{$this->name}_add_items"; if($inputfield instanceof InputfieldHasArrayValue || $inputfield instanceof InputfieldSupportsArrayValue) { // multi value $description = $this->_('Enter the titles of the items you want to add, one per line. They will be created and added to your selection when you save the page.'); $input = ""; } else { // single value $description = $this->_('Enter the title of the item you want to add. It will become selected when you save the page.'); $input = ""; } $notes = sprintf($this->_('New pages will be added to %s'), $parent->path); $label = wireIconMarkup('plus-circle', 'fw') . $this->_('Create New'); $out = "
" . "

$label

" . "

" . "" . "$input" . "$notes" . "

" . "
"; return $out; } /** * Render non-editable value * * @return string * */ public function ___renderValue() { if($this->labelFieldName == '.') { $labelFieldFormat = $this->labelFieldFormat; $labelFieldName = 'title|name'; } else { $labelFieldFormat = ''; $labelFieldName = $this->labelFieldName ? $this->labelFieldName : 'title'; $labelFieldName .= "|name"; } $value = $this->attr('value'); if(is_array($value) || $value instanceof PageArray) { $out = '"; } else if($value instanceof Page) { $of = $value->of(); $value->of(true); $out = $labelFieldFormat ? $value->getText($labelFieldFormat, true, true) : $value->get($labelFieldName); if(!strlen($out)) $out = $value->get('name'); $value->of($of); } else { $out = $value; } return $out; } /** * Process input * * @param WireInputData $input * @return $this|Inputfield * @throws WireException * */ public function ___processInput(WireInputData $input) { $pages = $this->wire()->pages; $user = $this->wire()->user; $this->processInputMode = true; $inputfield = $this->getInputfield(); if(!$inputfield) return $this; $inputfield->processInput($input); $value = $this->attr('value'); $existingValueStr = $value ? "$value" : ''; $newValue = null; if($inputfield instanceof InputfieldSupportsArrayValue) { $value = $inputfield->getArrayValue(); } else { $value = $inputfield->attr('value'); } if(is_array($value)) { $newValue = $pages->newPageArray(); $mockPage = new Page(); foreach($value as $v) { $id = (int) $v; if(!$id) continue; if($id > 0) { // existing page $page = $pages->get($id); if(!$this->hasFieldtype && !self::isValidPage($page, $this, $mockPage)) { // extra validation for usage without FieldtypePage $error = $mockPage->get('_isValidPage'); if($error) $this->error($error); continue; } else if($page->hasStatus(Page::statusUnpublished) && !$this->getSetting('allowUnpub')) { // disallow unpublished $warning = sprintf($this->_('Unpublished page %1$s is not allowed in field "%2$s"'), "$page->id", $this->label); if($user->isSuperuser()) { $warning .= ' ' . sprintf($this->_('To allow unpublished pages, edit the “%s” field and see the setting on the “Details” tab.'), $this->name); } $this->warning($warning); continue; } } else { // placeholder for new page, to be sorted later $page = $pages->newNullPage(); } $newValue->add($page); } } else if($value) { $newValue = $pages->get((int) $value); if($newValue->hasStatus(Page::statusUnpublished) && !$this->getSetting('allowUnpub')) { $newValue = null; // disallow unpublished } else if($newValue && $newValue->id && !$this->hasFieldtype) { $mockPage = new Page(); if(!self::isValidPage($newValue, $this, $mockPage)) { $error = $mockPage->get('_isValidPage'); if($error) $this->error($error); $newValue = null; } } } if($this->derefAsPage < 1 && $newValue instanceof Page) { // i.e. value from a PageListSelect (single) when using PageArray value $newValuePage = $newValue; $newValue = $pages->newPageArray(); $newValue->add($newValuePage); } $this->setAttribute('value', $newValue); $this->processInputAddPages($input); // if pages were added, re-sort them in case they were dragged to a different order // an example of this would be when used with the InputfieldPageAutocomplete if(count($this->pagesAdded) && is_array($value)) { $sortedValue = $pages->newPageArray(); foreach($newValue as $page) { if($page->id < 1) $page = $this->pagesAdded->shift(); if($page->id && !$sortedValue->has($page)) $sortedValue->add($page); } $newValue = $sortedValue; $this->setAttribute('value', $newValue); } if("$newValue" != "$existingValueStr") { $this->trackChange('value'); } $this->processInputMode = false; return $this; } /** * Check for the addable pages option and process if applicable * * @param WireInputData $input * */ protected function ___processInputAddPages($input) { $this->pagesAdded = $this->wire('pages')->newPageArray(); $parent_id = $this->getSetting('parent_id'); $template_id = $this->getSetting('template_id'); if(!$this->getSetting('addable') || !$parent_id || !$template_id) return; $user = $this->wire('user'); $key = "_{$this->name}_add_items"; $value = trim($input->$key); if(empty($value)) return; $parent = $this->pages->get($parent_id); $sort = $parent->numChildren; $titles = explode("\n", $value); $n = 0; foreach($titles as $title) { // check if there is an existing page using this title $selector = "include=all, templates_id=$template_id, title=" . $this->wire('sanitizer')->selectorValue($title); $existingPage = $parent->child($selector); if($existingPage->id) { // use existing page $this->pagesAdded->add($existingPage); if($this->value instanceof PageArray) { $this->value->add($existingPage); continue; } else { $this->value = $existingPage; break; } } // create a new page $page = $this->wire('pages')->newPage(array( 'template' => $template_id, 'parent' => $parent, 'title' => trim($title), 'sort' => $sort++, 'id' => -1, // prevents the permissions check from failing )); // on first iteration perform a page-context access check if(!$n && (!$parent->addable($page) || !$page->publishable())) { $this->error("No access to add {$page->template} pages to {$parent->path}"); break; } $page->id = 0; try { $page->save(); $this->message(sprintf($this->_('Added page %s'), $page->path)); if($this->value instanceof PageArray) $this->value->add($page); else $this->value = $page; $this->pagesAdded->add($page); $this->trackChange('value'); $n++; } catch(\Exception $e) { $error = sprintf($this->_('Error adding page "%s"'), $page->title); if($user->isSuperuser()) $error .= " - " . $e->getMessage(); $this->error($error); break; } if($this->value instanceof Page) break; } } /** * Does this Inputfield have an empty value? * * @return bool * */ public function isEmpty() { $value = $this->attr('value'); if($value instanceof Page) { // derefAsPage return $value->id < 1; } else if($value instanceof PageArray) { // derefAsPageArray /** @var PageArray $value */ if(!count($value)) return true; } else { // null return true; } return false; } /** * Get field configuration Inputfields * * @return InputfieldWrapper * @throws WireException * */ public function ___getConfigInputfields() { // let the module know it's being used for configuration purposes $this->configMode = true; $exampleLabel = $this->_('Example:') . ' '; $defaultLabel = ' ' . $this->_('(default)'); $inputfields = new InputfieldWrapper(); $this->wire($inputfields); $fieldset = $this->wire('modules')->get('InputfieldFieldset'); $fieldset->label = $this->_('Selectable pages'); $fieldset->attr('name', '_selectable_pages'); $fieldset->description = $this->_('Use at least one of the options below to determine which pages will be selectable with this field.'); $fieldset->icon = 'files-o'; $selectablePagesFieldset = $fieldset; /** @var InputfieldPageListSelect $field */ $field = $this->modules->get('InputfieldPageListSelect'); $field->setAttribute('name', 'parent_id'); $field->label = $this->_('Parent'); $field->attr('value', (int) $this->parent_id); $field->description = $this->_('Select the parent of the pages that are selectable.'); $field->required = false; $field->icon = 'folder-open-o'; $field->collapsed = Inputfield::collapsedBlank; $fieldset->append($field); /** @var InputfieldSelect $field */ $field = $this->modules->get('InputfieldSelect'); $field->setAttribute('name', 'template_id'); $field->label = $this->_('Template'); $field->description = $this->_('Select the template of the pages that are selectable. May be used instead of, or in addition to, the parent above.'); // Description for Template of selectable pages foreach($this->templates as $template) { $field->addOption($template->id, $template->name); } $template_id = $this->getSetting('template_id'); $field->attr('value', $template_id); $field->collapsed = Inputfield::collapsedBlank; $field->icon = 'cube'; $fieldset->append($field); $templateIDs = $this->getTemplateIDs(); $key = array_search($template_id, $templateIDs); if(is_int($key)) unset($templateIDs[$key]); /** @var InputfieldAsmSelect $field */ $field = $this->modules->get('InputfieldAsmSelect'); $field->attr('name', 'template_ids'); $field->label = $this->_('Additional templates'); $field->description = $this->_('If you need additional templates for selectable pages, select them here.'); // $field->description .= ' ' . $this->_('This may not be supported by all input types.'); $field->icon = 'cubes'; foreach($this->templates as $template) { $field->addOption($template->id, $template->name); } $field->attr('value', $templateIDs); $field->collapsed = Inputfield::collapsedBlank; $field->showIf = 'template_id!=0'; if(count($templateIDs) == 1 && reset($templateIDs) == $this->getSetting('template_id')) { $field->collapsed = Inputfield::collapsedYes; } $fieldset->append($field); $extra = $this->_('While this overrides parent and template selections above, those selections (if present) are still used for validation.'); // Additional notes /** @var InputfieldSelector $field */ $field = $this->modules->get('InputfieldSelector'); $field->description = $this->_('Add one or more fields below to create a query that finds the pages you want to make selectable.'); $field->description .= ' ' . $this->_('This creates a selector that finds pages at runtime. If you prefer to enter this manually, use the “Selector string” option below instead.'); $field->description .= ' ' . $extra; $field->attr('name', 'findPagesSelect'); $field->label = $this->_('Custom find'); $field->attr('value', $this->get('findPagesSelect')); //$field->collapsed = Inputfield::collapsedBlank; $field->icon = 'search-plus'; $field->addLabel = $this->_('Add field to query'); $field->allowSystemCustomFields = true; $field->allowSystemTemplates = true; $field->showFieldLabels = 1; $field->collapsed = Inputfield::collapsedBlank; $fieldset->append($field); /** @var InputfieldText $field */ $field = $this->modules->get('InputfieldText'); $field->attr('name', 'findPagesSelector'); $field->label = $this->_('Selector string'); $field->attr('value', $this->findPagesSelector); $field->description = $this->_('If you want to find selectable pages using a ProcessWire selector, enter the selector string to find the selectable pages. This selector will be passed to a `$pages->find("your selector");` call.'); // Description for Custom selector to find selectable pages $field->description .= ' ' . $extra; $field->notes = $exampleLabel . $this->_('parent=/products/, template=product, sort=name'); // Example of Custom selector to find selectable pages $field->collapsed = Inputfield::collapsedBlank; $field->icon = 'search'; $fieldset->append($field); if($this->findPagesCode) { // allow only if already present, as this option is deprecated /** @var InputfieldTextarea $field */ $field = $this->modules->get('InputfieldTextarea'); $field->attr('name', 'findPagesCode'); $field->attr('value', $this->findPagesCode); $field->attr('rows', 4); $field->description = $this->_('If you want to find selectable pages using a PHP code snippet rather than selecting a parent page or template (above) then enter the code to find the selectable pages. This statement has access to the $page and $pages API variables, where $page refers to the page being edited.'); // Description for Custom PHP to find selectable pages 1 $field->description .= ' ' . $this->_('The snippet should return either a PageArray, Page or NULL. If it returns a Page, children of that Page are used as selectable pages. Using this is optional, and if used, it overrides the parent/template/selector fields above.'); // Description for Custom PHP to find selectable pages 2 $field->description .= ' ' . $extra; $field->description .= ' ' . $this->_('NOTE: Not compatible with PageListSelect or Autocomplete input field types.'); // Description for Custom PHP to find selectable pages 3 $field->notes = $exampleLabel . $this->_('return $page->parent->parent->children("name=locations")->first()->children();'); // Example of Custom PHP code to find selectable pages $field->collapsed = Inputfield::collapsedBlank; } else { $field = $this->modules->get('InputfieldMarkup'); $field->attr('name', '_findPagesCode'); $field->collapsed = Inputfield::collapsedYes; $if = "\$event->object->" . ($this->name ? "hasField == '$this->name'" : "name == 'your_field_name'"); $field->value = '

' . sprintf($this->_('Add the following hook to a %s file and modify per your needs. The hook should find and return selectable pages in a PageArray.'), '/site/ready.php') . "

" . 
				"\$wire->addHookAfter('InputfieldPage::getSelectablePages', function(\$event) {" .
				"\n  if($if) {" . 
				"\n    \$event->return = \$event->pages->find('your selector here');" . 
				"\n  }" . 
				"\n});" . 
				"
" . "

" . sprintf($this->_('If you need to know the page being edited, it is accessible from: %s'), "\$event->arguments('page');") . "

"; } $field->label = $this->_('Custom PHP code'); $field->icon = 'code'; $field->showIf = 'inputfield!=InputfieldPageAutocomplete|InputfieldPageListSelect|InputfieldPageListSelectMultiple'; $fieldset->append($field); $inputfields->append($fieldset); /** @var InputfieldSelect $field */ $field = $this->modules->get('InputfieldSelect'); $field->attr('name', 'labelFieldName'); $field->label = $this->_('Label field'); $field->required = true; $field->icon = 'thumb-tack'; $field->description = $this->_('Select the page field that you want to be used in generating the labels for each selectable page.'); // Description for Label Field $field->notes = $this->_('Select "Custom format" if you want to specify multiple fields, or other fields you do not see above.'); $field->addOption('.', $this->_('Custom format (multiple fields)' . ' ...')); $field->columnWidth = 50; if($this->wire('fields')->get('title')) { $field->addOption('title', 'title' . $defaultLabel); $field->addOption('name', 'name'); $titleIsDefault = true; } else { $field->addOption('name', 'name' . $defaultLabel); $titleIsDefault = false; } $field->addOption('path', 'path'); foreach($this->wire('fields') as $f) { if(!$f->type instanceof FieldtypeText) continue; if($f->type instanceof FieldtypeTextarea) continue; if($titleIsDefault && $f->name == 'title') continue; $field->addOption($f->name); } if(!$this->labelFieldFormat) { if($this->labelFieldName === '.') { // they want a custom format, but they didn't provide one $this->labelFieldName = $titleIsDefault ? 'title' : 'name'; } } if(!$this->labelFieldName) { // no label field name means we fall back to default $this->labelFieldName = $titleIsDefault ? 'title' : 'name'; } $field->attr('value', $this->labelFieldName); $inputfields->append($field); /** @var InputfieldText $field */ $field = $this->modules->get('InputfieldText'); $field->attr('name', 'labelFieldFormat'); $field->attr('value', $this->labelFieldFormat); $field->label = $this->_('Custom page label format'); $field->description = $this->_('Specify one or more field names surrounded by curly {brackets} along with any additional characters, spacing or punctuation.'); // Description for custom page label format $field->notes = $this->_('Example: {parent.title} - {title}, {date}'); $field->columnWidth = 50; $field->showIf = 'labelFieldName=.'; $field->required = true; $field->requiredIf = 'labelFieldName=.'; $inputfields->add($field); if(!$this->inputfield) $this->inputfield = 'InputfieldSelect'; /** @var InputfieldSelect $field */ $field = $this->modules->get('InputfieldSelect'); $field->setAttribute('name', 'inputfield'); $field->setAttribute('value', $this->inputfield); $field->label = $this->_('Input field type'); $field->description = $this->_('The type of input field (Inputfield module) that will be used to select page(s) for this field.'); $field->description .= ' ' . $this->_('Select one that is consistent with your “Value type” selection on the “Details” tab for single or multiple-page selection.'); $field->notes = $this->_('After selecting an input field type and saving changes, please note that additional configuration options specific to your selection may appear directly below this.'); $field->required = true; $field->icon = 'plug'; $inputfieldSelection = $field; $singles = array(); $multiples = array(); $sortables = array(); $pageListTypes = array(); $inputfieldClasses = $this->inputfieldClasses; if($this->hasFieldtype) $inputfieldClasses = array_merge(self::$defaultInputfieldClasses, $inputfieldClasses); foreach($inputfieldClasses as $class) { $module = $this->modules->getModule($class, array('noInit' => true)); $info = $this->modules->getModuleInfo($module); $label = ucfirst($info['title']); if($module instanceof InputfieldPageListSelection) { $pageListTypes[] = $class; } if($module instanceof InputfieldHasSortableValue) { $sortables[$class] = $label; } else if($module instanceof InputfieldHasArrayValue || $module instanceof InputfieldSupportsArrayValue) { $multiples[$class] = $label; } else { $singles[$class] = $label; } if($class == 'InputfieldPageAutocomplete') $singles["_$class"] = $label; } $multiLabel = $this->_('Multiple page selection'); $field->addOption($this->_('Single page selection'), $singles); $field->addOption($multiLabel, $multiples); $field->addOption($multiLabel . ' (' . $this->_('sortable') . ')', $sortables); $inputfields->insertBefore($field, $selectablePagesFieldset); if($this->hasFieldtype !== false) { /** @var InputfieldMarkup $f */ $f = $this->modules->get('InputfieldMarkup'); $f->label = $this->_('Regarding “Page List” input types'); $f->icon = 'warning'; $f->showIf = 'inputfield=' . implode('|', $pageListTypes); $f->value = '

' . $this->_('You have selected an input type that has specific requirements.') . ' ' . $this->_('Specify only the “Parent” option below when configuring “Selectable pages”.') . ' ' . $this->_('Note that the parent you specify implies the root of the tree of selectable pages.') . ' ' . $this->_('If you want to make everything selectable, then specify nothing.') . '

'; $inputfields->insertAfter($f, $field); $field = $this->modules->get('InputfieldCheckbox'); $field->attr('name', 'addable'); $field->attr('value', 1); $field->icon = 'lightbulb-o'; $field->label = $this->_('Allow new pages to be created from field?'); $field->description = $this->_('If checked, an option to add new page(s) will also be present if the indicated requirements are met.'); $field->notes = $this->_('1. Both a parent and template must be specified in the “Selectable pages” section above.') . "\n" . $this->_('2. The editing user must have access to create/publish these pages.') . "\n" . $this->_('3. The “label field” must be set to “title (default)”.'); if($this->addable) { $field->attr('checked', 'checked'); } else { $field->collapsed = Inputfield::collapsedYes; } $inputfields->append($field); } foreach(parent::___getConfigInputfields() as $inputfield) { $inputfields->add($inputfield); } $inputfield = $this->getInputfield(); if($inputfield) { // tell it it's under control of a parent, regardless of whether this one is hasFieldtype true or not. $info = $this->modules->getModuleInfo($inputfield); $inputfield->hasFieldtype = $this->hasFieldtype ? $this->hasFieldtype : true; $inputfield->hasInputfield = $this; if($inputfield instanceof InputfieldSupportsPageSelector) { $exampleSelector = $this->createFindPagesSelector(); $inputfield->setPageSelector($exampleSelector); } /** @var InputfieldFieldset $fieldset */ $fieldset = $this->modules->get('InputfieldFieldset'); $n = 0; foreach($inputfield->___getConfigInputfields() as $f) { if(in_array($f->name, array('required', 'requiredIf', 'showIf', 'collapsed', 'columnWidth'))) continue; if(array_key_exists($f->name, self::$defaultConfig)) continue; // if we already have the given field, skip over it to avoid duplication if($f->name && $inputfields->getChildByName($f->name)) continue; $fieldset->add($f); $n++; } if($n) { $fieldset->label = sprintf($this->_('Settings specific to “%s”'), $info['title']); $fieldset->icon = 'gear'; $fieldset->collapsed = Inputfield::collapsedYes; $inClass = $inputfield->className(); $fieldset->showIf = 'inputfield=' . $inClass; if($inClass == 'InputfieldPageAutocomplete') $fieldset->showIf .= "|_$inClass"; $inputfields->insertAfter($fieldset, $inputfieldSelection); } } $this->configMode = false; // reverse what was set at the top of this function return $inputfields; } /** * Get module configuration Inputfields * * @param array $data * @return InputfieldWrapper * */ public function getModuleConfigInputfields(array $data) { $name = 'inputfieldClasses'; if(!isset($data[$name]) || !is_array($data[$name])) $data[$name] = self::$defaultInputfieldClasses; $fields = $this->wire(new InputfieldWrapper()); $modules = $this->wire('modules'); /** @var InputfieldAsmSelect $field */ $field = $modules->get("InputfieldAsmSelect"); $field->attr('name', $name); foreach($modules->findByPrefix('Inputfield') as $className) { $field->addOption($className, str_replace('Inputfield', '', $className)); } $field->attr('value', $data[$name]); $field->label = $this->_('Inputfield modules available for page selection'); $field->description = $this->_('Select the Inputfield modules that may be used for page selection. These should generally be Inputfields that allow you to select one or more options.'); // Description $fields->append($field); return $fields; } }