'Page List', 'summary' => 'List pages in a hierarchical tree structure', 'version' => 124, 'permanent' => true, 'permission' => 'page-edit', 'icon' => 'sitemap', 'useNavJSON' => true, ); } /** * @var Page|null * */ protected $page; /** * @var int * */ protected $id; /** * @var Page|null * */ protected $openPage; /** * @var int * */ protected $start; /** * @var string * */ protected $trashLabel; /** * @var string|null i.e. "JSON" * */ protected $render; /** * @var array * */ protected $allowRenderTypes = array( 'JSON' => 'ProcessPageListRenderJSON' ); /** * Default max pages to show before pagination (configurable in the module editor) * */ const defaultLimit = 50; /** * Default animation speed (in ms) for the PageList * */ const defaultSpeed = 200; /** * Construct and establish default config values * */ public function __construct() { $this->set('showRootPage', true); $this->set('pageLabelField', 'title'); $this->set('limit', self::defaultLimit); $this->set('useHoverActions', false); $this->set('useBookmarks', false); $this->set('useTrash', false); $this->set('bookmarks', array()); $this->set('qtyType', ''); parent::set('hidePages', array(404)); parent::set('hidePagesNot', array()); parent::__construct(); } /** * Initialize the Page List * */ public function init() { parent::init(); $config = $this->wire()->config; $input = $this->wire()->input; $isAjax = $config->ajax; $limit = (int) $input->get->int('limit'); $render = $input->get('render'); $settings = $config->pageList; $this->start = (int) $input->get->int('start'); $this->limit = $limit > 0 && $limit < $this->limit ? $limit : $this->limit; $this->render = $render ? strtoupper($this->wire()->sanitizer->name($render)) : ''; if($isAjax && !$this->render && !$input->get('renderInputfieldAjax')) $this->render = 'JSON'; if(strlen($this->render) && !isset($this->allowRenderTypes[$this->render])) $this->render = null; if(is_array($settings)) { if(!empty($settings['useHoverActions'])) $this->set('useHoverActions', true); $this->set('hoverActionDelay', isset($settings['hoverActionDelay']) ? (int) $settings['hoverActionDelay'] : 100); $this->set('hoverActionFade', isset($settings['hoverActionFade']) ? (int) $settings['hoverActionFade'] : 100); if($this->speed == self::defaultSpeed) $this->set('speed', isset($settings['speed']) ? (int) $settings['speed'] : self::defaultSpeed); if($this->limit == self::defaultLimit) $this->set('limit', isset($settings['limit']) ? (int) $settings['limit'] : self::defaultLimit); } if(!$isAjax) { $modules = $this->wire()->modules; $jQuery = $modules->get('JqueryCore'); /** @var JqueryCore $jQuery */ $jQueryUI = $modules->get('JqueryUI'); /** @var JqueryUI $jQueryUI */ $jQuery->use('cookie'); $jQuery->use('longclick'); $jQueryUI->use('modal'); $jQueryUI->use('vex'); } } /** * Execute the Page List * * @return string * @throws WireException|Wire404Exception|WirePermissionException * */ public function ___execute() { $pages = $this->wire()->pages; $input = $this->wire()->input; $config = $this->wire()->config; $session = $this->wire()->session; $ajax = $config->ajax; $langID = (int) $input->get('lang'); if($langID) { $languages = $this->wire()->languages; if($languages) $this->wire()->user->language = $languages->get($langID); } $id = $input->get('id'); if($id === 'bookmark') $session->location('./bookmarks/'); $id = (int) $id; if(!$this->id && $id > 0) $this->id = $id; $this->trashLabel = $this->_('Trash'); // Label for 'Trash' page in PageList // Overrides page title if used $openID = (int) $input->get('open'); $this->openPage = $openID ? $pages->get($openID) : $pages->newNullPage(); if($this->openPage->id && $this->speed > 50) $this->speed = floor($this->speed / 2); $this->page = $pages->get("id=" . ($this->id > 0 ? $this->id : 1) . ", status<" . Page::statusMax); if(!$this->page) { throw new Wire404Exception("Unable to load page $this->id", Wire404Exception::codeSecondary); } if(!$this->page->listable()) { throw new WirePermissionException("You don't have access to list page {$this->page->url}"); } $this->page->setOutputFormatting(false); $action = $input->post('action'); if($ajax && $action) { return $this->ajaxAction($this->wire()->sanitizer->name($action)); } $p = $this->wire()->page; if($p->name === 'list' && "$p->process" === "$this") { // ensure that we use the page's title is always consistent in the admin (i.e. 'Pages' not 'Page List') $p->title = $p->parent->title; } if($ajax && $this->id > 1 && "$p->process" === "$this" && $input->get('mode') != 'select') { // remember last requested id $session->setFor($this, 'lastID', $this->id); } return $this->render(); } /** * Render the Page List * * @return string * */ protected function render() { $this->setupBreadcrumbs(); if($this->render) { return $this->getPageListRender($this->page)->render(); } $session = $this->wire()->session; $input = $this->wire()->input; $isAjax = $this->wire()->config->ajax; $tokenName = $session->CSRF->getTokenName(); $tokenValue = $session->CSRF->getTokenValue(); $class = $this->id ? "PageListContainerPage" : "PageListContainerRoot"; $this->renderReady(); if($isAjax && $input->get('renderInputfieldAjax')) { $script = 'script'; $script = "<$script>ProcessPageListInit();"; } else { $script = ''; } return "\n" . "
" . "
$script"; } /** * Setup for render * */ public function renderReady() { $input = $this->wire()->input; $config = $this->wire()->config; $urls = $config->urls; $isAjax = $config->ajax; $openPageIDs = array(); $openPageData = array(); if($this->openPage) { $page = $this->wire()->page; if($this->openPage->id > 1) { $openPageIDs[] = $this->openPage->id; foreach($this->openPage->parents() as $parent) { if($parent->id > 1 && $parent->id != $this->id) $openPageIDs[] = $parent->id; } } else if(!$isAjax && ((string) $page->process) == "$this") { if($this->id) { // leave openPageIDs as empty array } else { $openPageIDs = $input->cookie->array('pagelist_open'); } } if(!$isAjax && count($openPageIDs)) { $pages = $this->wire()->pages; $render = $this->render; $this->render = 'JSON'; foreach($openPageIDs as $key => $openPageID) { if(strpos($openPageID, '-')) { list($openPageID, $openPageStart) = explode('-', $openPageID); $openPageStart = (int) $openPageStart; } else { $openPageStart = 0; } $openPageID = (int) $openPageID; $openPageIDs[$key] = "$openPageID-$openPageStart"; $p = $pages->get($openPageID); if(!$p->id || !$p->listable()) continue; $renderer = $this->getPageListRender($p, $this->limit, $openPageStart); $openPageData["$openPageID-$openPageStart"] = $renderer->setOption('getArray', true)->render(); } $this->render = $render; } } $defaults = array( 'containerID' => 'PageListContainer', 'ajaxURL' => $urls->admin . "page/list/", 'ajaxMoveURL' => $urls->admin . "page/sort/", 'rootPageID' => $this->id, 'openPageIDs' => $openPageIDs, 'openPageData' => $openPageData, 'openPagination' => (int) $input->get('n'), 'paginationClass' => 'PageListPagination', 'showRootPage' => $this->showRootPage ? true : false, 'limit' => $this->limit, 'start' => $this->start, 'speed' => ($this->speed !== null ? (int) $this->speed : self::defaultSpeed), 'qtyType' => $this->qtyType, 'useHoverActions' => $this->useHoverActions ? true : false, 'hoverActionDelay' => (int) $this->hoverActionDelay, 'hoverActionFade' => (int) $this->hoverActionFade, 'selectStartLabel' => $this->_('Change'), // Change a page selection 'selectCancelLabel' => $this->_('Cancel'), // Cancel a page selection 'selectSelectLabel' => $this->_('Select'), // Select a page 'selectUnselectLabel' => $this->_('Unselect'), // Unselect a page 'moreLabel' => $this->_('More'), // Show more pages 'moveInstructionLabel' => $this->_('Click and drag to move'), // Instruction on how to move a page 'trashLabel' => $this->trashLabel, 'ajaxNetworkError' => $this->_('Network error, please try again later'), // Network error during AJAX request 'ajaxUnknownError' => $this->_('Unknown error, please try again later'), // Unknown error during AJAX request ); $settings = $config->ProcessPageList; $settings = is_array($settings) ? array_merge($defaults, $settings) : $defaults; $config->js('ProcessPageList', $settings); } /** * Get the appropriate PageListRender class * * @param Page $page * @param null|int $limit * @param null|int $start * @return ProcessPageListRender * */ protected function getPageListRender(Page $page, $limit = null, $start = null) { require_once(dirname(__FILE__) . '/ProcessPageListRender.php'); if(!$this->render || !isset($this->allowRenderTypes[$this->render])) $this->render = 'JSON'; $class = $this->allowRenderTypes[$this->render]; $className = wireClassName($class, true); $user = $this->wire()->user; $superuser = $user->isSuperuser(); if(!class_exists($className, false)) require_once(dirname(__FILE__) . "/$class.php"); if(is_null($limit)) $limit = $this->limit; if(is_null($start)) $start = $this->start; if($limit) { $selector = "start=$start, limit=$limit, status<" . Page::statusMax; if($this->useTrash && !$superuser) { $trashID = $this->wire()->config->trashPageID; if($page->id == $trashID && $user->hasPermission('page-edit') && $page->listable()) { $selector .= ", check_access=0"; } } $children = $this->find($selector, $page); } else { $children = $this->wire()->pages->newPageArray(); } /** @var ProcessPageListRender $renderer */ $renderer = $this->wire(new $className($page, $children)); $renderer->setStart($start); $renderer->setLimit($limit); $renderer->setPageLabelField($this->getPageLabelField()); $renderer->setLabel('trash', $this->trashLabel); $renderer->setUseTrash($this->useTrash || $superuser); $renderer->setQtyType($this->qtyType); $renderer->setHidePages($this->hidePages, $this->hidePagesNot); return $renderer; } /** * Set the page label field * * @param $name * @param $pageLabelField * */ public function setPageLabelField($name, $pageLabelField) { $this->wire()->session->setFor($this, $name, $pageLabelField); } /** * Get the page label field * * @return string * */ protected function getPageLabelField() { $pageLabelField = ''; $name = $this->wire()->input->get('labelName'); if($name) { $name = $this->wire()->sanitizer->fieldName($name); if($name) $pageLabelField = $this->wire()->session->getFor($this, $name); if($pageLabelField) $pageLabelField = '!' . $pageLabelField; // "!" means it may not be overridden by template } if(empty($pageLabelField)) { $pageLabelField = $this->pageLabelField; } return $pageLabelField; } /** * Process an AJAX action and return JSON string * * @param string $action * @return string * @throws WireException * */ public function ___ajaxAction($action) { $input = $this->wire()->input; $session = $this->wire()->session; if(!$this->page->editable()) throw new WireException("Page not editable"); if($this->page->id != $input->post('id')) throw new WireException("GET id does not match POST id"); $tokenName = $session->CSRF->getTokenName(); $tokenValue = $session->CSRF->getTokenValue(); $postTokenValue = $input->post($tokenName); if($postTokenValue === null || $postTokenValue !== $tokenValue) throw new WireException("CSRF token does not match"); $renderer = $this->getPageListRender($this->page, 0); $result = $renderer->actions()->processAction($this->page, $action); if(!empty($result['updateItem'])) { $result['child'] = $renderer->renderChild($this->page); unset($result['updateItem']); } if(!empty($result['appendItem'])) { $newChild = $this->wire()->pages->get((int) $result['appendItem']); $result['newChild'] = $renderer->renderChild($newChild); unset($result['appendItem']); } header("Content-type: application/json"); return json_encode($result); } /** * @param string $selectorString * @param Page $page * @return PageArray * */ public function ___find($selectorString, Page $page) { if($page->id === $this->wire()->config->trashPageID && !preg_match('/\bsort=/', $selectorString)) { $sortfield = $page->sortfield(); if(!$sortfield || $sortfield === 'sort') { $selectorString = trim("$selectorString,sort=-modified", ','); } } return $page->children($selectorString); } /** * Set a value to this Page List (see WireData) * * @param string $key * @param mixed $value * @return Process|ProcessPageList * */ public function set($key, $value) { if($key === 'id') { // allow setting by other modules, overrides $_GET value of ID $this->id = (int) $value; return $this; } return parent::set($key, $value); } /** * Setup the Breadcrumbs for the UI * */ public function setupBreadcrumbs() { $process = $this->wire()->process; if("$process" !== "$this" || !$this->wire()->breadcrumbs) return; if($this->wire()->input->urlSegment1) return; $url = $this->wire()->config->urls->admin . 'page/list/?id='; foreach($this->page->parents() as $p) { $this->breadcrumb($url . $p->id, $p->get('title|name')); } } /** * Get an instance of PageBookmarks (to be phased out) * * @return PageBookmarks * */ protected function getPageBookmarks() { static $bookmarks = null; if(is_null($bookmarks)) { require_once($this->wire()->config->paths('ProcessPageEdit') . 'PageBookmarks.php'); $bookmarks = $this->wire(new PageBookmarks($this)); } return $bookmarks; } /** * Output JSON list of navigation items for this module's bookmarks * * @param array $options * @return string|array * */ public function ___executeNavJSON(array $options = array()) { $config = $this->wire()->config; $urls = $this->wire()->urls; if($this->useBookmarks) { $bookmarks = $this->getPageBookmarks(); $options['edit'] = $urls->admin . 'page/?id={id}'; $options = $bookmarks->initNavJSON($options); return parent::___executeNavJSON($options); } $parentID = (int) $this->wire()->input->get('parent_id'); if(!$parentID) $parentID = 1; $parent = $this->wire()->pages->get($parentID); $parentViewable = $parent->viewable(false); $renderer = $this->getPageListRender($parent); $items = $parentViewable ? $renderer->getChildren() : new PageArray(); if($parentID === 1 && $parentViewable) $items->prepend($parent); $skipPageIDs = array($config->trashPageID, $config->adminRootPageID); $maxLabelLength = 40; $data = array( 'url' => $urls->admin . 'page/list/navJSON/', 'label' => '', 'icon' => 'sitemap', 'list' => array(), ); $data = array_merge($options, $data); foreach($items as $page) { $id = $page->id; if(in_array($id, $skipPageIDs)) continue; $url = ''; $editable = false; if(!$page->listable()) { continue; } else if($page->editable()) { $url = $page->editUrl(); $editable = true; } else if($page->viewable()) { // do not show view URLs per #818 // $url = $page->url(); } $numChildren = $id > 1 ? $renderer->numChildren($page) : 0; $label = $renderer->getPageLabel($page, array('noTags' => true, 'noIcon' => true)); if(strlen($label) > $maxLabelLength) { $label = substr($label, 0, $maxLabelLength); $pos = strrpos($label, ' '); if($pos !== false) $label = substr($label, 0, $pos); $label .= ' …'; } $labelClasses = array(); if($page->isUnpublished()) $labelClasses[] = 'PageListStatusUnpublished'; if($page->isHidden()) $labelClasses[] = 'PageListStatusHidden'; if($page->hasStatus(Page::statusLocked)) $labelClasses[] = 'PageListStatusLocked'; if($page->hasStatus(Page::statusDraft)) $labelClasses[] = 'PageListStatusDraft'; if(!$editable) $labelClasses[] = 'PageListStatusNotEditable'; if(count($labelClasses)) { $label = "$label"; } if($numChildren) $label .= " $numChildren"; $label .= '   '; $a = array( 'url' => $url, 'id' => $id, 'label' => $label, 'icon' => $page->getIcon(), 'edit' => $editable ); if($numChildren) { $a['navJSON'] = $data['url'] . "?parent_id=$page->id"; } $data['list'][] = $a; } if($items->getTotal() > $items->count()) { $data['list'][] = array( 'url' => $urls->admin . "page/?open=$parentID", 'label' => $this->_('Show All') . ' ' . '' . sprintf($this->_('(%d pages)'), $items->getTotal()) . '', 'icon' => 'arrow-circle-right', 'className' => 'separator pw-pagelist-show-all', ); } if($parent->addable()) { $data['list'][] = array( 'url' => $urls->admin . "page/add/?parent_id=$parentID", 'label' => __('Add New', '/wire/templates-admin/default.php'), 'icon' => 'plus-circle', 'className' => 'separator pw-nav-add', ); } if($config->ajax) header("Content-Type: application/json"); return json_encode($data); } public function ___executeOpen() { $input = $this->wire()->input; $id = (int) $input->urlSegment2; $input->get->set('open', $id); $this->wire()->breadcrumbs->removeAll(); return $this->execute(); } public function ___executeId() { $input = $this->wire()->input; $id = (int) $input->urlSegment2; $input->get->set('id', $id); return $this->execute(); } /** * Execute the Page Bookmarks (to be phased out) * * @return string * @throws WireException * @throws WirePermissionException * */ public function ___executeBookmarks() { $bookmarks = $this->getPageBookmarks(); return $bookmarks->editBookmarks(); } /** * URL to redirect to after non-authenticated user is logged-in, or false if module does not support * * @param Page $page * @return string * @sine 3.0.167 * */ public static function getAfterLoginUrl(Page $page) { $url = $page->url(); $input = $page->wire()->input; list($s1, $s2) = array($input->urlSegment1, $input->urlSegment2); if(ctype_digit($s2) && ($s1 === 'id' || $s1 === 'open')) { return $url . "$s1/" . (int) $s2; // i.e. /id/123 or /open/456 } else { $intVars = array('limit', 'start', 'lang', 'open', 'id', 'n'); $data = array(); foreach($intVars as $name) { $value = (int) $input->get($name); if($value > 0) $data[$name] = $value; } $render = $input->get->name('render'); if($render) $data['render'] = strtoupper($render); if(count($data)) $url .= "?" . implode('&', $data); } return $url; } /** * Build a form allowing configuration of this Module * * @param array $data * @return InputfieldWrapper * */ public function getModuleConfigInputfields(array $data) { /** @var InputfieldWrapper $fields */ $fields = $this->wire(new InputfieldWrapper()); $modules = $this->wire()->modules; /** @var InputfieldPageListSelectMultiple $field */ $field = $modules->get('InputfieldPageListSelectMultiple'); $field->attr('name', 'hidePages'); $field->label = $this->_('Hide these pages in page list(s)'); $field->description = $this->_('Select one or more pages that you do not want to appear in page list(s).'); $field->val($this->hidePages); $field->columnWidth = 60; $field->icon = 'eye-slash'; $fields->add($field); /** @var InputfieldCheckboxes $field */ $field = $modules->get('InputfieldCheckboxes'); $field->attr('name', 'hidePagesNot'); $field->label = $this->_('Except when (AND condition)'); $field->addOption('debug', $this->_('System in debug mode')); $field->addOption('advanced', $this->_('System in advanced mode')); $field->addOption('superuser', $this->_('Current user is superuser')); $field->showIf = 'hidePages.count>0'; $field->val($this->hidePagesNot); $field->icon = 'eye-slash'; $field->columnWidth = 40; $fields->add($field); /** @var InputfieldCheckbox $field */ $field = $modules->get('InputfieldCheckbox'); $field->attr('name', 'useTrash'); $field->label = $this->_('Allow non-superuser editors to use Trash?'); $field->icon = 'trash-o'; $field->description = $this->_('When checked, users will be able to see pages in the trash (only pages they have access to).') . ' ' . $this->_('This will also enable the “Trash” and “Restore” actions, where access control allows.'); if(!empty($data['useTrash'])) $field->attr('checked', 'checked'); $fields->append($field); /** @var InputfieldText $field */ $field = $modules->get("InputfieldText"); $field->attr('name', 'pageLabelField'); $field->attr('value', !empty($data['pageLabelField']) ? $data['pageLabelField'] : 'title'); $field->label = $this->_("Name of page field to display"); $field->description = $this->_('Every page in a PageList is identified by a label, typically a title or headline field. You may specify which field it should use here. To specify multiple fields, separate each field name with a space, or use your own format string with field names surrounded in {brackets}. If the field resolves to an object (like another page), then specify the property with a dot, i.e. {anotherpage.title}. Note that if the format you specify resolves to a blank value then ProcessWire will use the page "name" field.'); // pageLabelField description $field->notes = $this->_('You may optionally override this setting on a per-template basis in each template "advanced" settings.'); // pageLabelField notes $fields->append($field); if(!empty($data['useBookmarks'])) { // support bookmarks only if already in use as bookmarks for ProcessPageList to be phased out $bookmarks = $this->getPageBookmarks(); $bookmarks->addConfigInputfields($fields); $pages = $this->wire()->pages; $admin = $pages->get($this->wire()->config->adminRootPageID); $page = $pages->get($admin->path . 'page/list/'); $bookmarks->checkProcessPage($page); } /* $settings = $this->wire('config')->pageList; if(empty($settings['useHoverActions'])) { $field = $modules->get('InputfieldCheckbox'); $field->attr('name', 'useHoverActions'); $field->label = __('Show page actions on hover?'); $field->description = __('By default, actions for a page appear after a click (at least in the default admin theme). To make them appear on hover instead, check this box.'); // useHoverActions description $field->notes = __('For more options here, see the $config->pageList setting in /wire/config.php. You may copy those settings to /site/config.php and override them.'); // useHoverActions notes if(!empty($data['useHoverActions'])) $field->attr('checked', 'checked'); $fields->append($field); } */ $defaultNote1 = $this->_('Default value is %d.'); $defaultNote2 = $this->_('If left at the default value, this setting can also be specified in the $config->pageList array.'); /** @var InputfieldInteger $field */ $field = $modules->get("InputfieldInteger"); $field->attr('name', 'limit'); $field->attr('value', !empty($data['limit']) ? (int) $data['limit'] : self::defaultLimit); $field->label = $this->_('Number of pages to display before pagination'); $field->notes = sprintf($defaultNote1, self::defaultLimit) . ' ' . $defaultNote2; $fields->append($field); /** @var InputfieldInteger $field */ $field = $modules->get("InputfieldInteger"); $field->attr('name', 'speed'); $field->attr('value', array_key_exists('speed', $data) ? (int) $data['speed'] : self::defaultSpeed); $field->label = $this->_('Animation Speed (in ms)'); $field->description = $this->_('This is the speed at which each branch in the page tree animates up or down. Lower numbers are faster but less visible. For no animation specify 0.'); // Animation speed description $field->notes = sprintf($defaultNote1, self::defaultSpeed) . ' ' . $defaultNote2; $fields->append($field); /** @var InputfieldRadios $field */ $field = $modules->get('InputfieldRadios'); $field->attr('name', 'qtyType'); $field->label = $this->_('Children quantity type'); $field->description = $this->_('In the page list, a quantity of children is shown next to each page when applicable. What type of quantity should it show?'); $field->notes = $this->_('When showing descendants, the quantity includes all descendants, whether listable to the user or not.'); $field->addOption('', $this->_('Immediate children (default)')); $field->addOption('total', $this->_('Descendants: children, grandchildren, great-grandchildren, and on…')); $field->addOption('children/total', $this->_('Both: children/descendants')); $field->addOption('total/children', $this->_('Both: descendants/children')); $field->addOption('id', $this->_('Show Page ID number instead')); $field->attr('value', empty($data['qtyType']) ? '' : $data['qtyType']); $fields->append($field); return $fields; } }