1
0
mirror of https://github.com/processwire/processwire.git synced 2025-08-23 23:02:58 +02:00

Add new Page::numDescendants() method and property, plus descendants() and descendant() alias methods. Add Page::findOne() method. Update ProcessPageList with the ability to customize what is shown in the numChildren/count shown for each Page, along with the ability to display the the newly added Page descendants numbers instead of or in addition to the Page numChildren.

This commit is contained in:
Ryan Cramer
2018-10-05 08:11:16 -04:00
parent faa0d4f4df
commit 5fddd95b43
7 changed files with 277 additions and 43 deletions

View File

@@ -46,6 +46,7 @@
* @property int $numChildren The number of children (subpages) this page has, with no exclusions (fast). #pw-group-traversal
* @property int $hasChildren The number of visible children this page has. Excludes unpublished, no-access, hidden, etc. #pw-group-traversal
* @property int $numVisibleChildren Verbose alias of $hasChildren #pw-internal
* @property int $numDescendants Number of descendants (quantity of children, and their children, and so on). #pw-group-traversal
* @property PageArray $children All the children of this page. Returns a PageArray. See also $page->children($selector). #pw-group-traversal
* @property Page|NullPage $child The first child of this page. Returns a Page. See also $page->child($selector). #pw-group-traversal
* @property PageArray $siblings All the sibling pages of this page. Returns a PageArray. See also $page->siblings($selector). #pw-group-traversal
@@ -142,6 +143,11 @@
* @method PageArray references($selector = '', $field = '') Return pages that are pointing to this one by way of Page reference fields. #pw-group-traversal
* @method PageArray links($selector = '', $field = '') Return pages that link to this one contextually in Textarea/HTML fields. #pw-group-traversal
*
* Alias/alternate methods
* -----------------------
* @method PageArray descendants($selector = '', array $options = array()) Find descendant pages, alias of `Page::find()`, see that method for details. #pw-group-traversal
* @method Page|NullPage descendant($selector = '', array $options = array()) Find one descendant page, alias of `Page::findOne()`, see that method for details. #pw-group-traversal
*
*/
class Page extends WireData implements \Countable, WireMatchable {
@@ -597,6 +603,7 @@ class Page extends WireData implements \Countable, WireMatchable {
'namePrevious' => 'p',
'next' => 'm',
'numChildren' => 's',
'numDescendants' => 'm',
'numLinks' => 't',
'numReferences' => 't',
'output' => 'm',
@@ -661,6 +668,17 @@ class Page extends WireData implements \Countable, WireMatchable {
'templatesID' => 'templates_id',
);
/**
* Method alternates/aliases (alias => actual)
*
* @var array
*
*/
static $baseMethodAlternates = array(
'descendants' => 'find',
'descendant' => 'findOne',
);
/**
* Create a new page in memory.
*
@@ -1063,7 +1081,7 @@ class Page extends WireData implements \Countable, WireMatchable {
$value = $this->template ? $this->template->id : 0;
break;
case 'fieldgroup':
$value = $this->template->fieldgroup;
$value = $this->template ? $this->template->fieldgroup : null;
break;
case 'modifiedUser':
case 'createdUser':
@@ -1175,6 +1193,7 @@ class Page extends WireData implements \Countable, WireMatchable {
public function getFields() {
if(!$this->template) return new FieldsArray();
$fields = new FieldsArray();
/** @var Fieldgroup $fieldgroup */
$fieldgroup = $this->template->fieldgroup;
foreach($fieldgroup as $field) {
if($fieldgroup->hasFieldContext($field)) {
@@ -1663,6 +1682,8 @@ class Page extends WireData implements \Countable, WireMatchable {
} else {
return $this->get($method);
}
} else if(isset(self::$baseMethodAlternates[$method])) {
return call_user_func_array(array($this, self::$baseMethodAlternates[$method]), $arguments);
} else {
return parent::___callUnknown($method, $arguments);
}
@@ -1925,7 +1946,7 @@ class Page extends WireData implements \Countable, WireMatchable {
}
/**
* Find pages matching given selector in the descendent hierarchy
* Find descendant pages matching given selector
*
* This is the same as `Pages::find()` except that the results are limited to descendents of this Page.
*
@@ -1953,6 +1974,35 @@ class Page extends WireData implements \Countable, WireMatchable {
return $this->_pages('find', $selector, $options);
}
/**
* Find one descendant page matching given selector
*
* This is the same as `Pages::findOne()` except that the match is always a descendant of page it is called on.
*
* ~~~~~
* // Find the most recently modified descendant page
* $item = $page->findOne("sort=-modified");
* ~~~~~
*
* #pw-group-common
* #pw-group-traversal
*
* @param string|array $selector Selector string or array
* @param array $options Optional options to modify default bheavior, see options for `Pages::find()`.
* @return Page|NullPage Returns Page when found, or NullPage when nothing found.
* @see Pages::findOne(), Page::child()
*
*/
public function findOne($selector = '', $options = array()) {
if(!$this->numChildren) return $this->wire('pages')->newNullPage();
if(is_string($selector)) {
$selector = trim("has_parent={$this->id}, $selector", ", ");
} else if(is_array($selector)) {
$selector["has_parent"] = $this->id;
}
return $this->_pages('findOne', $selector, $options);
}
/**
* Return this pages children, optionally filtered by a selector
*
@@ -2233,6 +2283,35 @@ class Page extends WireData implements \Countable, WireMatchable {
return $this->traversal()->siblings($this, $selector);
}
/**
* Return number of descendants (children, grandchildren, great-grandchildren, …), optionally with conditions
*
* Use this over the `$page->numDescendants` property when you want to specify a selector or apply
* some other filter to the result (see options for `$selector` argument). If you want to include only
* visible descendants specify a selector (string or array) or boolean true for the `$selector` argument,
* if you dont need a selector.
*
* If you want to find descendant pages (rather than count), use the `Page::find()` method.
*
* ~~~~~
* // Find how many descendants were modified in the last week
* $qty = $page->numDescendants("modified>='-1 WEEK'");
* ~~~~~
*
* #pw-group-traversal
*
* @param bool|string|array $selector
* - When not specified, result includes all descendants without conditions, same as $page->numDescendants property.
* - When a string or array, a selector is assumed and quantity will be counted based on selector.
* - When boolean true, number includes only visible descendants (excludes unpublished, hidden, no-access, etc.)
* @return int Number of descendants
* @see Page::numChildren(), Page::find()
*
*/
public function numDescendants($selector = null) {
return $this->traversal()->numDescendants($this, $selector);
}
/**
* Return the next sibling page
*

View File

@@ -26,39 +26,83 @@ class PageTraversal {
* When boolean true, number includes only visible children (excludes unpublished, hidden, no-access, etc.)
* When boolean false, number includes all children without conditions, including unpublished, hidden, no-access, etc.
* When integer 1 number includes viewable children (as opposed to visible, viewable includes hidden pages + it also includes unpublished pages if user has page-edit permission).
* @param array $options
* - `descendants` (bool): Use descendants rather than direct children
* @return int Number of children
*
*/
public function numChildren(Page $page, $selector = null) {
public function numChildren(Page $page, $selector = null, array $options = array()) {
$descendants = empty($options['descendants']) ? false : true;
$parentType = $descendants ? 'has_parent' : 'parent_id';
if(is_bool($selector)) {
// onlyVisible takes the place of selector
$onlyVisible = $selector;
if(!$onlyVisible) return $page->get('numChildren');
return $page->_pages('count', "parent_id=$page->id");
$numChildren = $page->get('numChildren');
if(!$numChildren) {
return 0;
} else if($onlyVisible) {
return $page->_pages('count', "$parentType=$page->id");
} else if($descendants) {
return $this->numDescendants($page);
} else {
return $numChildren;
}
} else if($selector === 1) {
// viewable pages only
$numChildren = $page->get('numChildren');
if(!$numChildren) return 0;
if($page->wire('user')->isSuperuser()) return $numChildren;
if($page->wire('user')->hasPermission('page-edit')) {
return $page->_pages('count', "parent_id=$page->id, include=unpublished");
if($page->wire('user')->isSuperuser()) {
if($descendants) return $this->numDescendants($page);
return $numChildren;
} else if($page->wire('user')->hasPermission('page-edit')) {
return $page->_pages('count', "$parentType=$page->id, include=unpublished");
} else {
return $page->_pages('count', "$parentType=$page->id, include=hidden");
}
return $page->_pages('count', "parent_id=$page->id, include=hidden");
} else if(empty($selector) || (!is_string($selector) && !is_array($selector))) {
// no selector provided
if($descendants) return $this->numDescendants($page);
return $page->get('numChildren');
} else {
// selector string or array provided
if(is_string($selector)) {
$selector = "parent_id=$page->id, $selector";
$selector = "$parentType=$page->id, $selector";
} else if(is_array($selector)) {
$selector["parent_id"] = $page->id;
$selector[$parentType] = $page->id;
}
return $page->_pages('count', $selector);
}
}
/**
* Return number of descendants, optionally with conditions
*
* Use this over $page->numDescendants property when you want to specify a selector or when you want the result to
* include only visible descendants. See the options for the $selector argument.
*
* @param Page $page
* @param bool|string|int|array $selector
* When not specified, result includes all descendants without conditions, same as $page->numDescendants property.
* When a string or array, a selector is assumed and quantity will be counted based on selector.
* When boolean true, number includes only visible descendants (excludes unpublished, hidden, no-access, etc.)
* When boolean false, number includes all descendants without conditions, including unpublished, hidden, no-access, etc.
* When integer 1 number includes viewable descendants (as opposed to visible, viewable includes hidden pages + it also includes unpublished pages if user has page-edit permission).
* @return int Number of descendants
*
*/
public function numDescendants(Page $page, $selector = null) {
if($selector === null) {
return $page->_pages('count', "has_parent=$page->id, include=all");
} else {
return $this->numChildren($page, $selector, array('descendants' => true));
}
}
/**
* Return this page's children pages, optionally filtered by a selector
*

View File

@@ -131,6 +131,10 @@ $(document).ready(function() {
// session field name that holds page label format, when used
labelName: '',
// what to show in the PageListNumChildren quantity: 'children', 'total', 'children/total', 'total/children', or 'id'
// default is blank, which implies 'children'
qtyType: '',
};
// array of "123.0" (page_id.start) that are currently open (used in non-select mode only)
@@ -511,7 +515,9 @@ $(document).ready(function() {
var nextStart = data.start + data.limit;
//var openPageKey = id + '-' + start;
if($target.hasClass('PageListItem')) setNumChildren($target, data.page.numChildren);
if($target.hasClass('PageListItem')) {
setNumChildren($target, data.page.numChildren, data.page.numTotal);
}
if(data.page.numChildren > nextStart) {
var $a = $("<a></a>").attr('href', nextStart).data('pageId', id).text(options.moreLabel).click(clickMore);
@@ -717,8 +723,7 @@ $(document).ready(function() {
});
$li.append($a);
var $numChildren = $("<span>" + (child.numChildren ? child.numChildren : '') + "</span>").addClass('PageListNumChildren detail');
$li.append($numChildren);
setNumChildren($li, child.numChildren, child.numTotal, true);
if(child.note && child.note.length) $li.append($("<span>" + child.note + "</span>").addClass('PageListNote detail'));
@@ -794,30 +799,101 @@ $(document).ready(function() {
* Get number of children for given .PageListItem
*
*/
function getNumChildren($item) {
var $numChildren = $item.children('.PageListNumChildren');
if(!$numChildren.length) return 0;
var numChildren = $numChildren.text();
return numChildren.length ? parseInt(numChildren) : 0;
function getNumChildren($item, getTotal) {
if(typeof getTotal == "undefined") var getTotal = false;
if(getTotal) {
var n = $item.attr('data-numTotal');
} else {
var n = $item.attr('data-numChild');
}
return n && n.length > 0 ? parseInt(n) : 0;
}
function getNumTotal($item) {
return getNumChildren($item, true);
}
/**
* Set number of children for given PageListItem
*
*/
function setNumChildren($item, numChildren) {
var $numChildren = $item.children('.PageListNumChildren');
if(!$numChildren.length) {
$numChildren = $('<span>0</span>').addClass('PageListNumChildren detail');
$item.append($numChildren);
function setNumChildren($item, numChildren, numTotal, addNew) {
if(typeof numTotal == "undefined") var numTotal = numChildren;
if(typeof addNew == "undefined") var addNew = false;
var $numChildren = addNew ? '' : $item.children('.PageListNumChildren');
var n = numChildren === false ? numTotal : numChildren;
if(addNew || !$numChildren.length) {
$numChildren = $('<span></span>').addClass('PageListNumChildren detail');
addNew = true;
}
if(numChildren < 1) {
if(n < 1) {
$item.removeClass('PageListHasChildren').addClass('PageListNoChildren');
numChildren = '';
if(numTotal !== false) numTotal = 0;
} else {
$item.removeClass('PageListNoChildren').addClass('PageListHasChildren');
}
$numChildren.text(numChildren);
if(numTotal === false) {
numTotal = getNumTotal($item);
} else {
$item.attr('data-numTotal', numTotal);
}
if(numChildren === false) {
numChildren = getNumChildren($item);
} else {
if(numChildren < 0) numChildren = 0;
$item.attr('data-numChild', numChildren);
}
var numLabel = '';
switch(options.qtyType) {
case 'total':
numLabel = numTotal;
break;
case 'total/children':
var slash = "<span class='ui-priority-secondary'>/</span>";
numLabel = numTotal > 0 && numTotal != numChildren ? numTotal + slash + numChildren : numTotal;
break;
case 'children/total':
var slash = "<span class='ui-priority-secondary'>/</span>";
numLabel = numTotal > 0 && numTotal != numChildren ? numChildren + slash + numTotal : numTotal;
break;
case 'id':
numLabel = $item.data('pageId');
break;
default:
numLabel = numChildren;
}
if(!numLabel) numLabel = '';
$numChildren.html(numLabel);
if(addNew) $item.append($numChildren);
}
/**
* Set total/descendants
*
*/
function setNumTotal($item, numTotal) {
setNumChildren($item, false, numTotal);
}
/**
* Recursively adjust total/descendants number up the tree by given amount (n)
*
*/
function adjustNumTotal($item, n) {
var numTotal = getNumTotal($item);
numTotal += n;
if(numTotal < 0) numTotal = 0;
setNumTotal($item, numTotal);
var $parentItem = $item.closest('.PageList').prev('.PageListItem');
if($parentItem.length) adjustNumTotal($parentItem, n);
}
/**
@@ -935,12 +1011,15 @@ $(document).ready(function() {
$msg.fadeOut('normal', function () {
var $parentItem = $liNew.closest('.PageList').prev('.PageListItem');
var numChildren = getNumChildren($parentItem);
var numTotal = getNumTotal($parentItem);
if(removeItem) {
numChildren--;
adjustNumTotal($parentItem, -1);
} else if(addNew) {
numChildren++;
adjustNumTotal($parentItem, 1);
}
setNumChildren($parentItem, numChildren);
setNumChildren($parentItem, numChildren, false);
setForceReload($parentItem);
if(removeItem) {
$liNew.next('.PageList').fadeOut('fast');
@@ -1031,7 +1110,7 @@ $(document).ready(function() {
}
} else {
$li.addClass('PageListItemOpen');
var numChildren = parseInt($li.children('.PageListNumChildren').text());
var numChildren = getNumChildren($li);
if(numChildren > 0 || $li.hasClass('PageListForceReload')) {
ignoreClicks = true;
var start = getOpenPageStart(id);
@@ -1169,7 +1248,7 @@ $(document).ready(function() {
// make an invisible PageList placeholder that allows 'move' action to create a child below this
$root.find('.PageListItemOpen').each(function() {
var numChildren = $(this).children('.PageListNumChildren').text();
var numChildren = getNumChildren($(this));
// if there are children and the next sibling doesn't contain a visible .PageList, then don't add a placeholder
if(parseInt(numChildren) > 1 && $(this).next().find(".PageList:visible").length == 0) {
return;
@@ -1333,20 +1412,22 @@ $(document).ready(function() {
if(!$ul.is("#PageListMoveFrom")) {
// update count where item came from
var $fromItem = $from.prev(".PageListItem");
var $numChildren = $fromItem.children(".PageListNumChildren");
var n = $numChildren.text().length > 0 ? parseInt($numChildren.text()) - 1 : 0;
if(n == 0) {
n = '';
var numChildren = getNumChildren($fromItem);
var numTotal = getNumTotal($fromItem);
if(numChildren > 0) {
numChildren--;
adjustNumTotal($fromItem, -1);
} else {
$from.remove(); // empty list, no longer needed
}
$numChildren.text(n);
setNumChildren($fromItem, numChildren, false);
setForceReload($fromItem);
// update count where item went to
var $toItem = $ul.prev(".PageListItem");
$numChildren = $toItem.children(".PageListNumChildren");
n = $numChildren.text().length > 0 ? parseInt($numChildren.text()) + 1 : 1;
$numChildren.text(n);
numChildren = getNumChildren($toItem) + 1;
adjustNumTotal($toItem, 1);
setNumChildren($toItem, numChildren, false);
setForceReload($toItem);
}
$from.attr('id', ''); // remove tempoary #PageListMoveFrom

File diff suppressed because one or more lines are too long

View File

@@ -20,6 +20,7 @@
* @property int $hoverActionFade Milliseconds to spend fading in or out actions.
* @property bool|int $useBookmarks Allow use of PageList bookmarks?
* @property bool|int $useTrash Allow non-superusers to use Trash?
* @property string $qtyType What to show in children quantity label? 'children', 'total', 'children/total', 'total/children', or 'id'
*
* @method string ajaxAction($action)
* @method PageArray find($selectorString, Page $page)
@@ -116,6 +117,7 @@ class ProcessPageList extends Process implements ConfigurableModule {
$this->set('useBookmarks', false);
$this->set('useTrash', false);
$this->set('bookmarks', array());
$this->set('qtyType', '');
parent::__construct();
}
@@ -302,6 +304,7 @@ class ProcessPageList extends Process implements ConfigurableModule {
'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,
@@ -347,12 +350,14 @@ class ProcessPageList extends Process implements ConfigurableModule {
$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 || $this->wire('user')->isSuperuser());
$renderer->setQtyType($this->qtyType);
return $renderer;
}
@@ -701,6 +706,20 @@ class ProcessPageList extends Process implements ConfigurableModule {
$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;
}

View File

@@ -20,6 +20,7 @@ abstract class ProcessPageListRender extends Wire {
protected $actions = null;
protected $options = array();
protected $useTrash = false;
protected $qtyType = '';
public function __construct(Page $page, PageArray $children) {
$this->page = $page;
@@ -77,6 +78,10 @@ abstract class ProcessPageListRender extends Wire {
$this->pageLabelField = $pageLabelField;
}
public function setQtyType($qtyType) {
$this->qtyType = $qtyType;
}
public function actions() {
return $this->actions;
}

View File

@@ -72,13 +72,18 @@ class ProcessPageListRenderJSON extends ProcessPageListRender {
if($child->listable()) $numChildren++;
}
}
if(strpos($this->qtyType, 'total') !== false) {
$numTotal = $this->wire('pages')->trasher()->getTrashTotal();
} else {
$numTotal = $numChildren;
}
} else {
if($page->hasStatus(Page::statusTemp)) $icons[] = 'bolt';
if($page->hasStatus(Page::statusLocked)) $icons[] = 'lock';
if($page->hasStatus(Page::statusDraft)) $icons[] = 'paperclip';
$numChildren = $page->numChildren(1);
$numTotal = strpos($this->qtyType, 'total') !== false ? $page->numDescendants : $numChildren;
}
if(!$label) $label = $this->getPageLabel($page);
if(count($icons)) foreach($icons as $n => $icon) {
@@ -90,6 +95,7 @@ class ProcessPageListRenderJSON extends ProcessPageListRender {
'label' => $label,
'status' => $page->status,
'numChildren' => $numChildren,
'numTotal' => $numTotal,
'path' => $page->template->slashUrls || $page->id == 1 ? $page->path() : rtrim($page->path(), '/'),
'template' => $page->template->name,
//'rm' => $this->superuser && $page->trashable(),