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 = "
" . "" . "$input" . "$notes" . "
" . "' . 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');
") .
"
' . $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; } }