mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 14:05:37 +02:00
813 lines
23 KiB
PHP
813 lines
23 KiB
PHP
|
<?php
|
||
|
|
||
|
namespace SilverStripe\ORM\Hierarchy;
|
||
|
|
||
|
use InvalidArgumentException;
|
||
|
use LogicException;
|
||
|
use SilverStripe\Core\Injector\Injectable;
|
||
|
use SilverStripe\ORM\ArrayList;
|
||
|
use SilverStripe\ORM\DataObject;
|
||
|
use SilverStripe\ORM\FieldType\DBField;
|
||
|
use SilverStripe\ORM\SS_List;
|
||
|
use SilverStripe\View\ArrayData;
|
||
|
|
||
|
/**
|
||
|
* Contains a set of hierarchical objects generated from a marking compilation run.
|
||
|
*
|
||
|
* A set of nodes can be "marked" for later export, in order to prevent having to
|
||
|
* export the entire contents of a potentially huge tree.
|
||
|
*/
|
||
|
class MarkedSet
|
||
|
{
|
||
|
use Injectable;
|
||
|
|
||
|
/**
|
||
|
* Marked nodes for a given subtree. The first item in this list
|
||
|
* is the root object of the subtree.
|
||
|
*
|
||
|
* A marked item is an item in a tree which will be included in
|
||
|
* a resulting tree.
|
||
|
*
|
||
|
* @var array Map of [itemID => itemInstance]
|
||
|
*/
|
||
|
protected $markedNodes;
|
||
|
|
||
|
/**
|
||
|
* Optional filter callback for filtering nodes to mark
|
||
|
*
|
||
|
* Array with keys:
|
||
|
* - parameter
|
||
|
* - value
|
||
|
* - func
|
||
|
*
|
||
|
* @var array
|
||
|
* @temp made public
|
||
|
*/
|
||
|
public $markingFilter;
|
||
|
|
||
|
/**
|
||
|
* @var DataObject
|
||
|
*/
|
||
|
protected $rootNode = null;
|
||
|
|
||
|
/**
|
||
|
* Method to use for getting children. Defaults to 'AllChildrenIncludingDeleted'
|
||
|
*
|
||
|
* @var string
|
||
|
*/
|
||
|
protected $childrenMethod = null;
|
||
|
|
||
|
/**
|
||
|
* Method to use for counting children. Defaults to `numChildren`
|
||
|
*
|
||
|
* @var string
|
||
|
*/
|
||
|
protected $numChildrenMethod = null;
|
||
|
|
||
|
/**
|
||
|
* Minimum number of nodes to iterate over before stopping recursion
|
||
|
*
|
||
|
* @var int
|
||
|
*/
|
||
|
protected $nodeCountThreshold = null;
|
||
|
|
||
|
/**
|
||
|
* Max number of nodes to return from a single children collection
|
||
|
*
|
||
|
* @var int
|
||
|
*/
|
||
|
protected $maxChildNodes;
|
||
|
|
||
|
/**
|
||
|
* Enable limiting
|
||
|
*
|
||
|
* @var bool
|
||
|
*/
|
||
|
protected $enableLimiting = true;
|
||
|
|
||
|
/**
|
||
|
* Create an empty set with the given class
|
||
|
*
|
||
|
* @param DataObject $rootNode Root node for this set. To collect the entire tree,
|
||
|
* pass in a singelton object.
|
||
|
* @param string $childrenMethod Override children method
|
||
|
* @param string $numChildrenMethod Override children counting method
|
||
|
* @param int $nodeCountThreshold Minimum threshold for number nodes to mark
|
||
|
* @param int $maxChildNodes Maximum threshold for number of child nodes to include
|
||
|
*/
|
||
|
public function __construct(
|
||
|
DataObject $rootNode,
|
||
|
$childrenMethod = null,
|
||
|
$numChildrenMethod = null,
|
||
|
$nodeCountThreshold = null,
|
||
|
$maxChildNodes = null
|
||
|
) {
|
||
|
if (! $rootNode::has_extension(Hierarchy::class)) {
|
||
|
throw new InvalidArgumentException(
|
||
|
get_class($rootNode) . " does not have the Hierarchy extension"
|
||
|
);
|
||
|
}
|
||
|
$this->rootNode = $rootNode;
|
||
|
if ($childrenMethod) {
|
||
|
$this->setChildrenMethod($childrenMethod);
|
||
|
}
|
||
|
if ($numChildrenMethod) {
|
||
|
$this->setNumChildrenMethod($numChildrenMethod);
|
||
|
}
|
||
|
if ($nodeCountThreshold) {
|
||
|
$this->setNodeCountThreshold($nodeCountThreshold);
|
||
|
}
|
||
|
if ($maxChildNodes) {
|
||
|
$this->setMaxChildNodes($maxChildNodes);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get total number of nodes to get. This acts as a soft lower-bounds for
|
||
|
* number of nodes to search until found.
|
||
|
* Defaults to value of node_threshold_total of hierarchy class.
|
||
|
*
|
||
|
* @return int
|
||
|
*/
|
||
|
public function getNodeCountThreshold()
|
||
|
{
|
||
|
return $this->nodeCountThreshold
|
||
|
?: $this->rootNode->config()->get('node_threshold_total');
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Max number of nodes that can be physically rendered at any level.
|
||
|
* Acts as a hard upper bound, after which nodes will be trimmed for
|
||
|
* performance reasons.
|
||
|
*
|
||
|
* @return int
|
||
|
*/
|
||
|
public function getMaxChildNodes()
|
||
|
{
|
||
|
return $this->maxChildNodes
|
||
|
?: $this->rootNode->config()->get('node_threshold_leaf');
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Set hard limit of number of nodes to get for this level
|
||
|
*
|
||
|
* @param int $count
|
||
|
* @return $this
|
||
|
*/
|
||
|
public function setMaxChildNodes($count)
|
||
|
{
|
||
|
$this->maxChildNodes = $count;
|
||
|
return $this;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Set max node count
|
||
|
*
|
||
|
* @param int $total
|
||
|
* @return $this
|
||
|
*/
|
||
|
public function setNodeCountThreshold($total)
|
||
|
{
|
||
|
$this->nodeCountThreshold = $total;
|
||
|
return $this;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get method to use for getting children
|
||
|
*
|
||
|
* @return string
|
||
|
*/
|
||
|
public function getChildrenMethod()
|
||
|
{
|
||
|
return $this->childrenMethod ?: 'AllChildrenIncludingDeleted';
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get children from this node
|
||
|
*
|
||
|
* @param DataObject $node
|
||
|
* @return SS_List
|
||
|
*/
|
||
|
protected function getChildren(DataObject $node)
|
||
|
{
|
||
|
$method = $this->getChildrenMethod();
|
||
|
return $node->$method() ?: ArrayList::create();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Set method to use for getting children
|
||
|
*
|
||
|
* @param string $method
|
||
|
* @throws InvalidArgumentException
|
||
|
* @return $this
|
||
|
*/
|
||
|
public function setChildrenMethod($method)
|
||
|
{
|
||
|
// Check method is valid
|
||
|
if (!$this->rootNode->hasMethod($method)) {
|
||
|
throw new InvalidArgumentException(sprintf(
|
||
|
"Can't find the method '%s' on class '%s' for getting tree children",
|
||
|
$method,
|
||
|
get_class($this->rootNode)
|
||
|
));
|
||
|
}
|
||
|
$this->childrenMethod = $method;
|
||
|
return $this;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get method name for num children
|
||
|
*
|
||
|
* @return string
|
||
|
*/
|
||
|
public function getNumChildrenMethod()
|
||
|
{
|
||
|
return $this->numChildrenMethod ?: 'numChildren';
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Count children
|
||
|
*
|
||
|
* @param DataObject $node
|
||
|
* @return int
|
||
|
*/
|
||
|
protected function getNumChildren(DataObject $node)
|
||
|
{
|
||
|
$method = $this->getNumChildrenMethod();
|
||
|
return (int)$node->$method();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Set method name to get num children
|
||
|
*
|
||
|
* @param string $method
|
||
|
* @return $this
|
||
|
*/
|
||
|
public function setNumChildrenMethod($method)
|
||
|
{
|
||
|
// Check method is valid
|
||
|
if (!$this->rootNode->hasMethod($method)) {
|
||
|
throw new InvalidArgumentException(sprintf(
|
||
|
"Can't find the method '%s' on class '%s' for counting tree children",
|
||
|
$method,
|
||
|
get_class($this->rootNode)
|
||
|
));
|
||
|
}
|
||
|
$this->numChildrenMethod = $method;
|
||
|
return $this;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns the children of this DataObject as an XHTML UL. This will be called recursively on each child, so if they
|
||
|
* have children they will be displayed as a UL inside a LI.
|
||
|
*
|
||
|
* @param string $template Template for items in the list
|
||
|
* @param array|callable $context Additional arguments to add to template when rendering
|
||
|
* due to excessive line length. If callable, this will be executed with the current node dataobject
|
||
|
* @return string
|
||
|
*/
|
||
|
public function renderChildren(
|
||
|
$template = null,
|
||
|
$context = []
|
||
|
) {
|
||
|
// Default to HTML template
|
||
|
if (!$template) {
|
||
|
$template = [
|
||
|
'type' => 'Includes',
|
||
|
self::class . '_HTML'
|
||
|
];
|
||
|
}
|
||
|
$tree = $this->getSubtree($this->rootNode, 0);
|
||
|
$node = $this->renderSubtree($tree, $template, $context);
|
||
|
return (string)$node->getField('SubTree');
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get child data formatted as JSON
|
||
|
*
|
||
|
* @param callable $serialiseEval A callback that takes a DataObject as a single parameter,
|
||
|
* and should return an array containing a simple array representation. This result will
|
||
|
* replace the 'node' property at each point in the tree.
|
||
|
* @return array
|
||
|
*/
|
||
|
public function getChildrenAsArray($serialiseEval = null)
|
||
|
{
|
||
|
if (!$serialiseEval) {
|
||
|
$serialiseEval = function ($data) {
|
||
|
/** @var DataObject $node */
|
||
|
$node = $data['node'];
|
||
|
return [
|
||
|
'id' => $node->ID,
|
||
|
'title' => $node->getTitle()
|
||
|
];
|
||
|
};
|
||
|
}
|
||
|
|
||
|
$tree = $this->getSubtree($this->rootNode, 0);
|
||
|
|
||
|
return $this->getSubtreeAsArray($tree, $serialiseEval);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Render a node in the tree with the given template
|
||
|
*
|
||
|
* @param array $data array data for current node
|
||
|
* @param string|array $template Template to use
|
||
|
* @param array|callable $context Additional arguments to add to template when rendering
|
||
|
* due to excessive line length. If callable, this will be executed with the current node dataobject
|
||
|
* @return ArrayData Viewable object representing the root node. use getField('SubTree') to get HTML
|
||
|
*/
|
||
|
protected function renderSubtree($data, $template, $context = [])
|
||
|
{
|
||
|
// Render children
|
||
|
$childNodes = new ArrayList();
|
||
|
foreach ($data['children'] as $child) {
|
||
|
$childData = $this->renderSubtree($child, $template, $context);
|
||
|
$childNodes->push($childData);
|
||
|
}
|
||
|
|
||
|
// Build parent node
|
||
|
$parentNode = new ArrayData($data);
|
||
|
$parentNode->setField('children', $childNodes); // Replace raw array with template-friendly list
|
||
|
$parentNode->setField('markingClasses', $this->markingClasses($data['node']));
|
||
|
|
||
|
// Evaluate custom context
|
||
|
if (is_callable($context)) {
|
||
|
$context = call_user_func($context, $data['node']);
|
||
|
}
|
||
|
if ($context) {
|
||
|
foreach ($context as $key => $value) {
|
||
|
$parentNode->setField($key, $value);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Render
|
||
|
$subtree = $parentNode->renderWith($template);
|
||
|
$parentNode->setField('SubTree', $subtree);
|
||
|
return $parentNode;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Return sub-tree as json array
|
||
|
*
|
||
|
* @param array $data
|
||
|
* @param callable $serialiseEval A callback that takes a DataObject as a single parameter,
|
||
|
* and should return an array containing a simple array representation. This result will
|
||
|
* replace the 'node' property at each point in the tree.
|
||
|
* @return mixed|string
|
||
|
*/
|
||
|
protected function getSubtreeAsArray($data, $serialiseEval)
|
||
|
{
|
||
|
$output = $data;
|
||
|
|
||
|
// Serialise node
|
||
|
$output['node'] = $serialiseEval($data['node']);
|
||
|
|
||
|
// Force serialisation of DBField instances
|
||
|
if (is_array($output['node'])) {
|
||
|
foreach ($output['node'] as $key => $value) {
|
||
|
if ($value instanceof DBField) {
|
||
|
$output['node'][$key] = $value->getSchemaValue();
|
||
|
}
|
||
|
}
|
||
|
} elseif ($output['node'] instanceof DBField) {
|
||
|
$output['node'] = $output['node']->getSchemaValue();
|
||
|
}
|
||
|
|
||
|
// Replace children with serialised elements
|
||
|
$output['children'] = [];
|
||
|
foreach ($data['children'] as $child) {
|
||
|
$output['children'][] = $this->getSubtreeAsArray($child, $serialiseEval);
|
||
|
}
|
||
|
return $output;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get tree data for node
|
||
|
*
|
||
|
* @param DataObject $node
|
||
|
* @param int $depth
|
||
|
* @return array|string
|
||
|
*/
|
||
|
protected function getSubtree($node, $depth = 0)
|
||
|
{
|
||
|
// Check if this node is limited due to child count
|
||
|
$numChildren = $this->getNumChildren($node);
|
||
|
$limited = $this->isNodeLimited($node, $numChildren);
|
||
|
|
||
|
// Build root rode
|
||
|
$expanded = $this->isExpanded($node);
|
||
|
$opened = $this->isTreeOpened($node);
|
||
|
$output = [
|
||
|
'node' => $node,
|
||
|
'marked' => $this->isMarked($node),
|
||
|
'expanded' => $expanded,
|
||
|
'opened' => $opened,
|
||
|
'depth' => $depth,
|
||
|
'count' => $numChildren, // Count of DB children
|
||
|
'limited' => $limited, // Flag whether 'items' has been limited
|
||
|
'children' => [], // Children to return in this request
|
||
|
];
|
||
|
|
||
|
// Don't iterate children if past limit
|
||
|
// or not expanded (requires subsequent request to get)
|
||
|
if ($limited || !$expanded) {
|
||
|
return $output;
|
||
|
}
|
||
|
|
||
|
// Get children
|
||
|
$children = $this->getChildren($node);
|
||
|
foreach ($children as $child) {
|
||
|
// Recurse
|
||
|
if ($this->isMarked($child)) {
|
||
|
$output['children'][] = $this->getSubtree($child, $depth + 1);
|
||
|
}
|
||
|
}
|
||
|
return $output;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Mark a segment of the tree, by calling mark().
|
||
|
*
|
||
|
* The method performs a breadth-first traversal until the number of nodes is more than minCount. This is used to
|
||
|
* get a limited number of tree nodes to show in the CMS initially.
|
||
|
*
|
||
|
* This method returns the number of nodes marked. After this method is called other methods can check
|
||
|
* {@link isExpanded()} and {@link isMarked()} on individual nodes.
|
||
|
*
|
||
|
* @return $this
|
||
|
*/
|
||
|
public function markPartialTree()
|
||
|
{
|
||
|
$nodeCountThreshold = $this->getNodeCountThreshold();
|
||
|
|
||
|
// Add root node, not-expanded by default
|
||
|
/** @var DataObject|Hierarchy $rootNode */
|
||
|
$rootNode = $this->rootNode;
|
||
|
$this->clearMarks();
|
||
|
$this->markUnexpanded($rootNode);
|
||
|
|
||
|
// Build markedNodes for this subtree until we reach the threshold
|
||
|
// foreach can't handle an ever-growing $nodes list
|
||
|
while (list(, $node) = each($this->markedNodes)) {
|
||
|
$children = $this->markChildren($node);
|
||
|
if ($nodeCountThreshold && sizeof($this->markedNodes) > $nodeCountThreshold) {
|
||
|
// Undo marking children as opened since they're lazy loaded
|
||
|
/** @var DataObject|Hierarchy $child */
|
||
|
foreach ($children as $child) {
|
||
|
$this->markClosed($child);
|
||
|
}
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
return $this;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Filter the marking to only those object with $node->$parameterName == $parameterValue
|
||
|
*
|
||
|
* @param string $parameterName The parameter on each node to check when marking.
|
||
|
* @param mixed $parameterValue The value the parameter must be to be marked.
|
||
|
* @return $this
|
||
|
*/
|
||
|
public function setMarkingFilter($parameterName, $parameterValue)
|
||
|
{
|
||
|
$this->markingFilter = array(
|
||
|
"parameter" => $parameterName,
|
||
|
"value" => $parameterValue
|
||
|
);
|
||
|
return $this;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Filter the marking to only those where the function returns true. The node in question will be passed to the
|
||
|
* function.
|
||
|
*
|
||
|
* @param callable $callback Callback to filter
|
||
|
* @return $this
|
||
|
*/
|
||
|
public function setMarkingFilterFunction($callback)
|
||
|
{
|
||
|
$this->markingFilter = array(
|
||
|
"func" => $callback,
|
||
|
);
|
||
|
return $this;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns true if the marking filter matches on the given node.
|
||
|
*
|
||
|
* @param DataObject $node Node to check
|
||
|
* @return bool
|
||
|
*/
|
||
|
protected function markingFilterMatches(DataObject $node)
|
||
|
{
|
||
|
if (!$this->markingFilter) {
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
// Func callback filter
|
||
|
if (isset($this->markingFilter['func'])) {
|
||
|
$func = $this->markingFilter['func'];
|
||
|
return call_user_func($func, $node);
|
||
|
}
|
||
|
|
||
|
// Check object property filter
|
||
|
if (isset($this->markingFilter['parameter'])) {
|
||
|
$parameterName = $this->markingFilter['parameter'];
|
||
|
$value = $this->markingFilter['value'];
|
||
|
|
||
|
if (is_array($value)) {
|
||
|
return in_array($node->$parameterName, $value);
|
||
|
} else {
|
||
|
return $node->$parameterName == $value;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
throw new LogicException("Invalid marking filter");
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Mark all children of the given node that match the marking filter.
|
||
|
*
|
||
|
* @param DataObject $node Parent node
|
||
|
* @return array List of children marked by this operation
|
||
|
*/
|
||
|
protected function markChildren(DataObject $node)
|
||
|
{
|
||
|
$this->markExpanded($node);
|
||
|
|
||
|
// If too many children leave closed
|
||
|
if ($this->isNodeLimited($node)) {
|
||
|
// Limited nodes are always expanded
|
||
|
$this->markClosed($node);
|
||
|
return [];
|
||
|
}
|
||
|
|
||
|
// Iterate children if not limited
|
||
|
$children = $this->getChildren($node);
|
||
|
if (!$children) {
|
||
|
return [];
|
||
|
}
|
||
|
|
||
|
// Mark all children
|
||
|
$markedChildren = [];
|
||
|
foreach ($children as $child) {
|
||
|
$markingMatches = $this->markingFilterMatches($child);
|
||
|
if (!$markingMatches) {
|
||
|
continue;
|
||
|
}
|
||
|
// Mark a child node as unexpanded if it has children and has not already been expanded
|
||
|
if ($this->getNumChildren($child) > 0 && !$this->isExpanded($child)) {
|
||
|
$this->markUnexpanded($child);
|
||
|
} else {
|
||
|
$this->markExpanded($child);
|
||
|
}
|
||
|
|
||
|
$markedChildren[] = $child;
|
||
|
}
|
||
|
return $markedChildren;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Return CSS classes of 'unexpanded', 'closed', both, or neither, as well as a 'jstree-*' state depending on the
|
||
|
* marking of this DataObject.
|
||
|
*
|
||
|
* @param DataObject $node
|
||
|
* @return string
|
||
|
*/
|
||
|
protected function markingClasses($node)
|
||
|
{
|
||
|
$classes = [];
|
||
|
if (!$this->isExpanded($node)) {
|
||
|
$classes[] = 'unexpanded';
|
||
|
}
|
||
|
|
||
|
// Set jstree open state, or mark it as a leaf (closed) if there are no children
|
||
|
if (!$this->getNumChildren($node)) {
|
||
|
// No children
|
||
|
$classes[] = "jstree-leaf closed";
|
||
|
} elseif ($this->isTreeOpened($node)) {
|
||
|
// Open with children
|
||
|
$classes[] = "jstree-open";
|
||
|
} else {
|
||
|
// Closed with children
|
||
|
$classes[] = "jstree-closed closed";
|
||
|
}
|
||
|
return implode(' ', $classes);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Mark the children of the DataObject with the given ID.
|
||
|
*
|
||
|
* @param int $id ID of parent node
|
||
|
* @param bool $open If this is true, mark the parent node as opened
|
||
|
* @return bool
|
||
|
*/
|
||
|
public function markById($id, $open = false)
|
||
|
{
|
||
|
if (isset($this->markedNodes[$id])) {
|
||
|
$this->markChildren($this->markedNodes[$id]);
|
||
|
if ($open) {
|
||
|
$this->markOpened($this->markedNodes[$id]);
|
||
|
}
|
||
|
return true;
|
||
|
} else {
|
||
|
return false;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Expose the given object in the tree, by marking this page and all it ancestors.
|
||
|
*
|
||
|
* @param DataObject|Hierarchy $childObj
|
||
|
* @return $this
|
||
|
*/
|
||
|
public function markToExpose(DataObject $childObj)
|
||
|
{
|
||
|
if (!$childObj) {
|
||
|
return $this;
|
||
|
}
|
||
|
$stack = $childObj->getAncestors(true)->reverse();
|
||
|
foreach ($stack as $stackItem) {
|
||
|
$this->markById($stackItem->ID, true);
|
||
|
}
|
||
|
return $this;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Return the IDs of all the marked nodes.
|
||
|
*
|
||
|
* @refactor called from CMSMain
|
||
|
* @return array
|
||
|
*/
|
||
|
public function markedNodeIDs()
|
||
|
{
|
||
|
return array_keys($this->markedNodes);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Cache of DataObjects' expanded statuses: [ClassName][ID] = bool
|
||
|
* @var array
|
||
|
*/
|
||
|
protected $expanded = [];
|
||
|
|
||
|
/**
|
||
|
* Cache of DataObjects' opened statuses: [ID] = bool
|
||
|
* @var array
|
||
|
*/
|
||
|
protected $treeOpened = [];
|
||
|
|
||
|
/**
|
||
|
* Reset marked nodes
|
||
|
*/
|
||
|
public function clearMarks()
|
||
|
{
|
||
|
$this->markedNodes = [];
|
||
|
$this->expanded = [];
|
||
|
$this->treeOpened = [];
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Mark this DataObject as expanded.
|
||
|
*
|
||
|
* @param DataObject $node
|
||
|
* @return $this
|
||
|
*/
|
||
|
public function markExpanded(DataObject $node)
|
||
|
{
|
||
|
$id = $node->ID ?: 0;
|
||
|
$this->markedNodes[$id] = $node;
|
||
|
$this->expanded[$id] = true;
|
||
|
return $this;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Mark this DataObject as unexpanded.
|
||
|
*
|
||
|
* @param DataObject $node
|
||
|
* @return $this
|
||
|
*/
|
||
|
public function markUnexpanded(DataObject $node)
|
||
|
{
|
||
|
$id = $node->ID ?: 0;
|
||
|
$this->markedNodes[$id] = $node;
|
||
|
unset($this->expanded[$id]);
|
||
|
return $this;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Mark this DataObject's tree as opened.
|
||
|
*
|
||
|
* @param DataObject $node
|
||
|
* @return $this
|
||
|
*/
|
||
|
public function markOpened(DataObject $node)
|
||
|
{
|
||
|
$id = $node->ID ?: 0;
|
||
|
$this->markedNodes[$id] = $node;
|
||
|
$this->treeOpened[$id] = true;
|
||
|
return $this;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Mark this DataObject's tree as closed.
|
||
|
*
|
||
|
* @param DataObject $node
|
||
|
* @return $this
|
||
|
*/
|
||
|
public function markClosed(DataObject $node)
|
||
|
{
|
||
|
$id = $node->ID ?: 0;
|
||
|
$this->markedNodes[$id] = $node;
|
||
|
unset($this->treeOpened[$id]);
|
||
|
return $this;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Check if this DataObject is marked.
|
||
|
*
|
||
|
* @param DataObject $node
|
||
|
* @return bool
|
||
|
*/
|
||
|
public function isMarked(DataObject $node)
|
||
|
{
|
||
|
$id = $node->ID ?: 0;
|
||
|
return !empty($this->markedNodes[$id]);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Check if this DataObject is expanded.
|
||
|
* An expanded object has had it's children iterated through.
|
||
|
*
|
||
|
* @param DataObject $node
|
||
|
* @return bool
|
||
|
*/
|
||
|
public function isExpanded(DataObject $node)
|
||
|
{
|
||
|
$id = $node->ID ?: 0;
|
||
|
return !empty($this->expanded[$id]);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Check if this DataObject's tree is opened.
|
||
|
* This is an expanded node which also should have children visually shown.
|
||
|
*
|
||
|
* @param DataObject $node
|
||
|
* @return bool
|
||
|
*/
|
||
|
public function isTreeOpened(DataObject $node)
|
||
|
{
|
||
|
$id = $node->ID ?: 0;
|
||
|
return !empty($this->treeOpened[$id]);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Check if this node has too many children
|
||
|
*
|
||
|
* @param DataObject|Hierarchy $node
|
||
|
* @param int $count Children count (if already calculated)
|
||
|
* @return bool
|
||
|
*/
|
||
|
protected function isNodeLimited(DataObject $node, $count = null)
|
||
|
{
|
||
|
// Singleton root node isn't limited
|
||
|
if (!$node->ID) {
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
// Check if limiting is enabled first
|
||
|
if (!$this->getLimitingEnabled()) {
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
// Count children for this node and compare to max
|
||
|
if (!isset($count)) {
|
||
|
$count = $this->getNumChildren($node);
|
||
|
}
|
||
|
return $count > $this->getMaxChildNodes();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Toggle limiting on or off
|
||
|
*
|
||
|
* @param bool $enabled
|
||
|
* @return $this
|
||
|
*/
|
||
|
public function setLimitingEnabled($enabled)
|
||
|
{
|
||
|
$this->enableLimiting = $enabled;
|
||
|
return $this;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Check if limiting is enabled
|
||
|
*
|
||
|
* @return bool
|
||
|
*/
|
||
|
public function getLimitingEnabled()
|
||
|
{
|
||
|
return $this->enableLimiting;
|
||
|
}
|
||
|
}
|