Rev 22 | Blame | Compare with Previous | Last modification | View Log | Download
<?php namespace ProcessWire;/*** ProcessWire Page Type Process** Manage, edit add pages of a specific type in ProcessWire** For more details about how Process modules work, please see:* /wire/core/Process.php** ProcessWire 3.x, Copyright 2023 by Ryan Cramer* https://processwire.com** @property array $showFields Names of fields to show in the main list table (default=['name'])* @property string $addLabel Translated "Add New" label* @property string $jsonListLabel What to use for 'label' property in JSON nav data (default='name')** @method string executeList()**/class ProcessPageType extends Process implements ConfigurableModule, WirePageEditor {static public function getModuleInfo() {return array('title' => __('Page Type', __FILE__), // getModuleInfo title'version' => 101,'summary' => __('List, Edit and Add pages of a specific type', __FILE__), // getModuleInfo summary'permanent' => true,'useNavJSON' => true,'addFlag' => Modules::flagsNoUserConfig);}/*** @var PagesType**/protected $pages;/*** Predefined template for page type represented by this Process** @var null|Template**/protected $template = null;/*** ProcessPageEdit or ProcessPageAdd** @var WirePageEditor|ProcessPageEdit|null**/protected $editor = null;/*** Instance of ProcessPageLister** @var null|ProcessPageLister or ProcessPageListerPro**/protected $lister = null;/*** Requested Lister bookmark ID (when applicable)** @var string|null|bool**/protected $listerBookmarkID = '';/*** Construct**/public function __construct() {$this->set('showFields', array('name'));$this->set('addLabel', $this->_('Add New'));$this->set('jsonListLabel', 'name'); // what to use for 'label' property in JSON nav dataparent::__construct();}/*** Init**/public function init() {$config = $this->wire()->config;$config->scripts->add($config->urls('ProcessPageType') . 'ProcessPageType.js');$config->styles->add($config->urls('ProcessPageType') . 'ProcessPageType.css');$this->pages = $this->wire($this->page->name);if(!$this->pages instanceof PagesType) {$this->pages = $this->wire()->pages;}if($this->pages instanceof PagesType) {$this->template = $this->pages->getTemplate();}$this->initLister();parent::init();}/*** Return true or false as to whether use of Lister should be attempted (template method)** @return bool**/protected function useLister() {return false;}/*** Initialize the $this->lister variable, if Lister is in use**/protected function initLister() {if(!$this->useLister() || $this->lister) return;// init lister, but only if executing an action that will use it$modules = $this->wire()->modules;$segment = $this->wire()->input->urlSegment1;$user = $this->wire()->user;$listerSegments = array('list','config','viewport','reset','actions','save','edit-bookmark',);if(strpos($segment, 'bm') === 0 && preg_match('/^bm[0-9O]+$/', $segment)) {// bookmark ID, i.e. users/bm42O1604139292$bookmarkID = $segment;} else {$bookmarkID = '';}if(empty($segment) || $bookmarkID || in_array($segment, $listerSegments)) {if(!$user->hasPermission('page-lister')) return;if($modules->isInstalled('ProcessPageListerPro')) {$this->lister = $modules->get('ProcessPageListerPro');if($this->lister && method_exists($this->lister, 'isValid') && !$this->lister->isValid()) {$this->lister = null;}}if(!$this->lister && $modules->isInstalled('ProcessPageLister')) {// for regular Lister we init() in the getLister() method instead$this->lister = $modules->getModule('ProcessPageLister', array('noInit' => true));}}if($this->lister && $bookmarkID) {if($this->lister->className() === 'ProcessPageLister') {$this->lister->set('_' . $this->className(), true)->init();}$bookmarks = $this->lister->getBookmarksInstance();$bookmarkID = $bookmarks->_bookmarkID(ltrim($bookmarkID, 'bm'));$this->listerBookmarkID = $this->lister->checkBookmark($bookmarkID);}}// Lister-specific methods, all mapped directly to Lister or ListerPropublic function ___executeConfig() { return $this->getLister()->executeConfig(); }public function ___executeViewport() { return $this->getLister()->executeViewport(); }public function ___executeReset() { return $this->getLister()->executeReset(); }public function ___executeActions() { return $this->getLister()->executeActions(); }public function ___executeSave() { return $this->getLister()->executeSave(); }public function ___executeEditBookmark() { return $this->getLister()->executeEditBookmark(); }/*** Catch-all for bookmarks** @return string* @throws Wire404Exception* @throws WireException**/public function ___executeUnknown() {if($this->useLister()) {$this->initLister();$lister = $this->getLister();if($lister && $this->listerBookmarkID) return $lister->executeUnknown();}throw new Wire404Exception("Unknown action", Wire404Exception::codeNonexist);}/*** Main execution method, delegated to listing items in this page type** @return string**/public function ___execute() {return $this->executeList();}/*** List items in this page type** @return string**/public function ___executeList() {$templateID = (int) $this->wire()->input->get('templates_id');if(!$templateID) $templateID = (int) $this->wire()->session->get($this->className() . 'TemplatesID');$selector = $templateID ? "templates_id=$templateID, " : "";return $this->renderList($selector . "limit=100, status<" . Page::statusMax);}/*** Output JSON list of navigation items for this (intended to for ajax use)** @param array $options* @return string|array**/public function ___executeNavJSON(array $options = array()) {if(!isset($options['items'])) {$input = $this->wire()->input;$limit = (int) $input->get('limit');if(!$limit || $limit > 100) $limit = 100;$start = (int) $input->get('start');$pages = $this->pages->find("start=$start, limit=$limit");foreach($pages as $page) {if(!$page->editable()) $pages->remove($page);$status = ucwords($page->statusStr);if($status) $page->setQuietly('_labelClass', trim(str_replace(' ', ' PageListStatus', " $status")));$icon = $page->getIcon();if($icon) $page->setQuietly('_labelIcon', $icon);}$options['items'] = $pages;$parent = $this->pages instanceof PagesType ? $this->pages->getParent() : null;if($parent && $parent->id && $parent->addable()) {$options['add'] = 'add/';} else {$options['add'] = false;}$options['edit'] = "edit/?id={id}";}if(!isset($options['itemLabel'])) $options['itemLabel'] = $this->jsonListLabel;if(!isset($options['iconKey'])) $options['iconKey'] = '_labelIcon';return parent::___executeNavJSON($options);}/*public function x___executeNavJSON(array $options = array()) {if(!isset($options['items'])) {$sanitizer = $this->wire()->sanitizer;$input = $this->wire()->input;$limit = (int) $input->get('limit');if(!$limit || $limit > 100) $limit = 100;$start = (int) $input->get('start');$options['items'] = $this->pages->find("start=$start, limit=$limit");if(empty($options['itemLabel'])) $options['itemLabel'] = $this->jsonListLabel;foreach($options['items'] as $page) {if(!$page->editable()) $options['items']->remove($page);$label = $page->get($options['itemLabel']);$a = array();foreach(explode(' ', $page->statusStr) as $status) {if($status) $a[] = 'PageListStatus' . ucfirst($status);}$label = $sanitizer->entities1($label);if(count($a)) $label = "<span class='" . implode(' ', $a) . "'>$label</span>";$page->setQuietly('_itemLabel', $label);$options['entityEncode'] = false;}$parent = $this->pages->getParent();if($parent && $parent->id && $parent->addable()) {$options['add'] = 'add/';} else {$options['add'] = false;}$options['edit'] = "edit/?id={id}";}$options['itemLabel'] = '_itemLabel';return parent::___executeNavJSON($options);}*//*** Get an instanceof ProcessPageLister or null if not applicable** @param string $selector* @return ProcessPageLister|null**/public function getLister($selector = '') {$lister = $this->lister;if(!$lister) return null;$settings = $this->getListerSettings($lister, $selector);foreach($settings as $name => $value) {$lister->$name = $value;}if($lister->className() == 'ProcessPageListerPro') {// ProcessPageListerPro only$data = $this->wire()->modules->getConfig('ProcessPageListerPro');if(isset($data['settings'][$this->page->name])) {foreach($data['settings'][$this->page->name] as $key => $value) {$lister->$key = $value;}}} else if(!$lister->get('_' . $this->className())) {// ProcessPageLister only$lister->set('_'. $this->className(), true)->init(); // because it used noInit option on get()}return $lister;}/*** Return an array of Lister settings, ready to be populated to Lister** @param ProcessPageLister $lister* @param string $selector* @return array**/protected function getListerSettings(ProcessPageLister $lister, $selector) {$templates = $this->pages->getTemplates();$parents = $this->pages->getParents();$_selector = "template=" . implode('|', array_keys($templates)) . ", ";if(count($parents)) $_selector .= "parent=$parents, ";$_selector .= "include=all, $selector";$selector = rtrim($_selector, ", ");$settings = array('initSelector' => $selector,'columns' => $this->showFields,'defaultSelector' => "name%=",'defaultSort' => 'name','parent' => $this->page,'editURL' => './edit/','addURL' => './add/','delimiters' => array(),'allowSystem' => true,'allowIncludeAll' => true,'allowBookmarks' => true,'showIncludeWarnings' => false,'toggles' => array('collapseFilters'),);if($lister->className() == 'ProcessPageLister') $settings['editMode'] = ProcessPageLister::windowModeDirect;if(count($templates) == 1) $settings['template'] = reset($templates);return $settings;}/*** Get the page editor** @param string $moduleName One of 'ProcessPageEdit' or 'ProcessPageAdd' (or other that extends)* @return ProcessPageEdit|ProcessPageAdd|WirePageEditor* @throws WireException If requested editor moduleName not found**/protected function getEditor($moduleName) {if($this->editor && $this->editor->className() == $moduleName) {return $this->editor;}$this->editor = $this->modules->get($moduleName);if(!$this->editor) {throw new WireException("Unable to load editor: $moduleName");}if(wireInstanceOf($this->editor, array('ProcessPageEdit', 'ProcessPageAdd'))) {$this->editor->setEditor($this); // set us as the parent editorif($this->pages instanceof PagesType) {$templates = $this->pages->getTemplates();$parents = $this->pages->getParentIDs();$this->editor->setPredefinedTemplates($templates);$this->editor->setPredefinedParents($this->wire()->pages->getById($parents));}}return $this->editor;}/*** Edit item of this page type** @return string**/public function ___executeEdit() {$pageTitle = $this->page->get('title|name');$this->breadcrumb('../', $pageTitle);$editor = $this->getEditor('ProcessPageEdit');$urlSegment = ucfirst($this->wire()->input->urlSegment2);$editPage = $this->getPage();$this->browserTitle("$pageTitle > " . $editPage->name);if($urlSegment && (method_exists($editor, "___execute$urlSegment") || method_exists($editor, "execute$urlSegment"))) {// i.e. executeTemplate() and executeSaveTemplate()return $editor->{"execute$urlSegment"}();} else {// regular editreturn $editor->execute();}}/*** Add item of this page type** @return string**/public function ___executeAdd() {$pageTitle = $this->page->get('title|name');$this->breadcrumb('../', $pageTitle);/** @var ProcessPageAdd $editor */$editor = $this->getEditor("ProcessPageAdd");$editor->template = $this->template;try {$out = $editor->execute();} catch(\Exception $e) {$out = '';$this->error($e->getMessage());}$this->browserTitle("$pageTitle > $this->addLabel");return $out;}/*** Execute the "change template" action delegated to ProcessPageEdit** @return string**/public function ___executeTemplate() {$editor = $this->getEditor('ProcessPageEdit');return $editor->executeTemplate();}/*** Execute saving changes of the "change template" action delegated to ProcessPageEdit** @return string**/public function ___executeSaveTemplate() {$editor = $this->getEditor('ProcessPageEdit');return $editor->executeSaveTemplate();}/*** Render Lister output of pages** This is used as an alternative to the built-in item list when Lister/ListerPro is available.** @param string $selector Selector string for pages* @param array $pagerOptions Not currently used by Lister* @return string* @throws WireException**/protected function renderLister($selector = '', $pagerOptions = array()) {if($pagerOptions) {} // ignore$lister = $this->getLister($selector);if(!$lister) throw new WireException("Lister not available");return $lister->execute();}/*** Render page list** When Lister/ListerPro is available, this will delegate to the renderLister() method instead.* When not available, it will render the list itself.** @param string $selector Selector string for pages* @param array $pagerOptions** @return string**/protected function renderList($selector = '', $pagerOptions = array()) {if($this->lister && $this->useLister()) {// delegate to Lister/ListerPro when availablereturn $this->renderLister($selector, $pagerOptions);}$out = '';if(!$this->pages instanceof PagesType || count($this->pages->getTemplates()) != 1) {$form = $this->getTemplateFilterForm();$out = $form->render();}/** @var MarkupAdminDataTable $table */$table = $this->wire()->modules->get("MarkupAdminDataTable");$table->setEncodeEntities(false);$fieldNames = $this->showFields;$fieldLabels = $fieldNames;foreach($fieldLabels as $key => $name) {if($name == 'name') {$fieldLabels[$key] = $this->_('Name'); // Label for 'name' fieldcontinue;}$field = $this->wire()->fields->get($name);if($field) {$label = $field->getLabel();$fieldLabels[$key] = htmlentities($label, ENT_QUOTES, "UTF-8");}}$table->headerRow($fieldLabels);$pages = $this->pages->find($selector);$numRows = 0;foreach($pages as $page) {if(!$page->editable()) continue;$n = 0;$row = array();foreach($fieldNames as $name) {if(!$n) {$value = htmlentities($page->getUnformatted($name), ENT_QUOTES, 'UTF-8') . ' ';$status = '';if($page->hasStatus(Page::statusUnpublished)) $status .= 'PageListStatusUnpublished ';if($page->hasStatus(Page::statusHidden)) $status .= 'PageListStatusHidden ';if($status) $value = "<span class='" . trim($status) . "'>$value</span>";$row[$value] = "edit/?id={$page->id}";} else {$row[] = $this->renderListFieldValue($name, $page->getUnformatted($name));}$n++;}$table->row($row);$numRows++;}if($this->wire()->page->addable()) $table->action(array($this->addLabel => 'add/'));if($pages->getTotal() > count($pages)) {/** @var MarkupPagerNav $pager */$pager = $this->modules->get("MarkupPagerNav");$out .= $pager->render($pages, $pagerOptions);}if(!$numRows) $out .= $this->renderEmptyList($pages);$out .= $table->render();return $out;}/*** Render an empty page list** @param PageArray $pages* @return string**/protected function renderEmptyList(PageArray $pages) {$out = "<p>" . $this->_('No items to display yet.') . "</p>";return $out;}/*** Return a value for output in list table** Only used if Lister/ListerPro is not available.** @param string $name Name of property* @param mixed $value Value of property* @return string**/protected function renderListFieldValue($name, $value) {if($name) {} // ignoreif(is_string($value) || is_int($value)) return htmlspecialchars($value, ENT_QUOTES, 'UTF-8');if(is_array($value)) return htmlspecialchars(print_r($value, true), ENT_QUOTES, 'UTF-8');if(is_object($value)) {if($value instanceof PageArray) {$item = $value->first();if($item && $item->title) {$out = $value->implode("\n", '{title} (~{name}~)');$out = nl2br($this->wire('sanitizer')->entities1($out));$out = str_replace(array('(~', '~)'), array('<span class="detail">(', ')</span>'), $out);} else {$out = $value->implode("\n", 'name');}return $out;} else if($value instanceof WireArray) {$out = '';foreach($value as $k => $v) {$out .= $v->name . ", ";}return nl2br(rtrim($out, ", "));} else if($value instanceof Wire) {if($value->name) return $value->name;return (string) $value;}}return '';}/*** Get the filter-by-template form** Only used if Lister/ListerPro is not available.** @return InputfieldForm**/protected function getTemplateFilterForm() {/** @var InputfieldForm $form */$form = $this->modules->get("InputfieldForm");$form->attr('id', 'template_filter_form');$form->attr('method', 'get');$form->attr('action', './list');/** @var InputfieldSelect $field */$field = $this->modules->get("InputfieldSelect");$field->attr('id+name', 'templates_id');$field->label = $this->_('Filter by Template');$field->addOption('', $this->_('Show All'));$field->icon = 'filter';$field->collapsed = Inputfield::collapsedBlank;$templates = $this->pages instanceof PagesType ? $this->pages->getTemplates() : array();if(!count($templates)) $templates = $this->wire('templates');foreach($templates as $template) {$field->addOption($template->id, $template->name);}$filterName = $this->className . 'TemplatesID';$templatesId = (int) $this->wire()->input->get('templates_id');if($templatesId) {$this->wire()->session->set($filterName, (int) $templatesId);}$filterValue = (int) $this->wire()->session->get($filterName);if($filterValue) $this->template = $this->wire()->templates->get($filterValue);$field->attr('value', $filterValue);$form->append($field);return $form;}/*** Module config** @param array $data* @return InputfieldWrapper**/public function getModuleConfigInputfields(array $data) {$showFields = isset($data['showFields']) ? $data['showFields'] : array();$fields = array('name');foreach($this->wire()->fields as $field) {$fields[] = $field->name;}/** @var InputfieldWrapper $inputfields */$inputfields = $this->wire(new InputfieldWrapper());/** @var InputfieldAsmSelect $f */$f = $this->wire()->modules->get('InputfieldAsmSelect');$f->label = $this->_("What fields should be displayed in the page listing?");$f->attr('id+name', 'showFields');foreach($fields as $name) $f->addOption($name);$f->attr('value', $showFields);$inputfields->add($f);return $inputfields;}/*** Get page being edited (for WirePageEditor interface)** @return NullPage|Page**/public function getPage() {if($this->editor) return $this->editor->getPage();return $this->wire()->pages->newNullPage();}/*** Search for items containing $text and return an array representation of them** Implementation for SearchableModule interface** @param string $text Text to search for* @param array $options Options to modify behavior:* - `edit` (bool): True if any 'url' returned should be to edit items rather than view them* - `multilang` (bool): If true, search all languages rather than just current (default=true).* - `start` (int): Start index (0-based), if pagination active (default=0).* - `limit` (int): Limit to this many items, if pagination active (default=0, disabled).* @return array**/public function search($text, array $options = array()) {$page = $this->getProcessPage();$this->pages = $this->wire($page->name);$templates = $this->pages->getTemplates();/** @var Languages $languages */$page = $this->getProcessPage();$result = array('title' => $page->id ? $page->title : $this->className(),'items' => array(),);if(!empty($options['help'])) {$result['properties'] = array('name');foreach($templates as $template) {foreach($template->fieldgroup as $field) {if($field->type instanceof FieldtypePassword) continue;if($field->type instanceof FieldtypeFieldsetOpen) continue;$result['properties'][] = $field->name;}}return $result;}$text = $this->wire()->sanitizer->selectorValue($text);$property = empty($options['property']) ? 'name' : $options['property'];$operator = isset($options['operator']) ? $options['operator'] : '%=';$selector = "$property$operator$text, ";if(isset($options['start'])) $selector .= "start=$options[start], ";if(!empty($options['limit'])) $selector .= "limit=$options[limit], ";$items = $this->pages->find(trim($selector, ", "));foreach($items as $item) {$result['items'][] = $this->getSearchItemInfo($item, $options);}return $result;}protected function getSearchItemInfo(Page $item, array $options) {return array('id' => $item->id,'name' => $item->name,'title' => $item->get('title|name'),'subtitle' => $item->template->name,'summary' => '','icon' => $item->getIcon(),'url' => empty($options['edit']) ? $item->url() : $item->editUrl());}}