Getting nested set model into a <ul> but hiding "closed" subtrees
Asked Answered
C

8

9

Based on Getting a modified preorder tree traversal model (nested set) into a <ul>

One of answers gave right code to display full tree. What i need is to always show first level (depth=0) and siblings+childrens for active list item. Goal is to expand visible part of tree when user selects list item which is parent for more list items.

So, if i got this list:

1. item
2. item
  2.1. item
  2.2. item
    2.2.1. item
    2.2.2. item
    2.2.3. item
  2.3. item
  2.4. item
    2.4.1. item
    2.4.2. item
3. item
4. item
  4.1. item
  4.2. item
    4.2.1. item
    4.2.2. item
5. item

and if current list item is "2.", list should look like that:

1. item
2. item // this needs class .selected
  2.1. item
  2.2. item
  2.3. item
  2.4. item
3. item
4. item
5. item

and if current list item is "2.2.", list should look like that:

1. item
2. item // this needs class .selected
  2.1. item
  2.2. item // this needs class .selected
    2.2.1. item
    2.2.2. item
    2.2.3. item
  2.3. item
  2.4. item
3. item
4. item
5. item

Below there is an example code which works well for me to display full tree. I also added lft/rgt/current which will be needed to solve my issue.

<?php
function MyRenderTree ( $tree = array(array('name'=>'','depth'=>'', 'lft'=>'','rgt'=>'')) , $current=false){

   $current_depth = 0;
   $counter = 0;

   $result = '<ul>';

   foreach($tree as $node){
       $node_depth = $node['depth'];
       $node_name = $node['name'];
       $node_id = $node['category_id'];

       if($node_depth == $current_depth){
           if($counter > 0) $result .= '</li>';
       }
       elseif($node_depth > $current_depth){
           $result .= '<ul>';
           $current_depth = $current_depth + ($node_depth - $current_depth);
       }
       elseif($node_depth < $current_depth){
           $result .= str_repeat('</li></ul>',$current_depth - $node_depth).'</li>';
           $current_depth = $current_depth - ($current_depth - $node_depth);
       }
       $result .= '<li id="c'.$node_id.'"';
       $result .= $node_depth < 2 ?' class="open"':'';
       $result .= '><a href="#">'.$node_name.'</a>';
       ++$counter;
   }
   $result .= str_repeat('</li></ul>',$node_depth).'</li>';

   $result .= '</ul>';

   return $result;
}

// "$current" may contain category_id, lft, rgt for active list item
print MyRenderTree($categories,$current);
?>
Cavanaugh answered 11/10, 2011 at 16:13 Comment(2)
What you mean by "$current" may contain category_id, lft, rgt for active list item? is it an array that have the 3 data?Lum
@Lum it's an array with values of "selected" list item.Despoliation
E
8

As you already managed to sort the sequence, why not just output as needed?

As some leafs need to appear closed, so the iterator should be able to skip children of non-selected nodes.

Doing so lead me to an idea to solve the problem of terminating the output tree (output = parsing). What to do if the last valid node in the sequence is at a higher depth than 0? I appended a NULL terminator for that. So still open levels can be closed before the loop finishes.

Additionally the iterator overloads nodes to offer common methods on them, like comparing against the currently selected element.

The MyRenderTree function (Demo/Full code)

Edit: The Demo Codepad has problems, here is the source-code: Gist
Getting nested set model into a but hiding “closed” subtrees

function MyRenderTree($tree = array(array('name'=>'','depth'=>'', 'lft'=>'','rgt'=>'')) , $current=false)
{
    $sequence = new SequenceTreeIterator($tree);

    echo '<ul>';
    $hasChildren = FALSE;
    foreach($sequence as $node)
    {
        if ($close = $sequence->getCloseLevels())
        {
            echo str_repeat('</ul></li>', $close);
            $hasChildren = FALSE;
        }
        if (!$node && $hasChildren)
        {
            echo '</li>', "\n";
        }
        if (!$node) break; # terminator

        $hasChildren = $node->hasChildren();
        $isSelected = $node->isSupersetOf($current);

        $classes = array();
        $isSelected && ($classes[] = 'selected') && $hasChildren && $classes[] = 'open';
        $node->isSame($current) && $classes[] = 'current';

        printf('<li class="%s">%s', implode(' ', $classes), $node['name']);

        if ($hasChildren)
            if ($isSelected)
                echo '<ul>';
            else
                $sequence->skipChildren()
            ;
        else
            echo '</li>'
        ;
    }
    echo '</ul>';
}

This can be solved as well in a single foreach and some variables, however I think for re-useablilty, the implementation based on the SPL Iterators is better.

Eriha answered 16/10, 2011 at 19:31 Comment(3)
Thank you for great piece of code and best answer. Do you have any ideas on how to improve performance for this one? It takes ~0.3-0.4 sec on my box with 250 list items with max depth of 4.Despoliation
Can you put your array as var_export output on some pastebin? I'd like to take a look what takes the time and I've got some unrolled code as well from the session I made this example. It sounds indeed a bit slow as it's just iterating over the array.Eriha
Just tested it, just runs quite quickly through here: 0.0054750442504883 - Do you have a specific current value? Is this probably your DB access?Eriha
W
2

Instead of using PHP script for handling tree navigation, Jquery can be used. Once the tree is generated rest of the things will handled on client itself, it will also save server requests.

See Sample 2 and 3

http://jquery.bassistance.de/treeview/demo/

http://docs.jquery.com/Plugins/Treeview

It may help as per your requirement.

Wintry answered 19/10, 2011 at 14:9 Comment(0)
L
1

The function expect the $tree is order by the 'left'.

I have modified your function to selected items based on the 'left' and 'right' value. Hope it's what you are after.

The modified function:

function MyRenderTree($tree = array(array('name' => '', 'depth' => '', 'lft' => '', 'rgt' => '')), $current=false)
    {
        $current_depth = 0;
        $counter = 0;
        $found = false;
        $nextSibling = false;
        $result = '<ul>';
        foreach ($tree as $node) {
            $node_depth = $node['depth'];
            $node_name = $node['name'];
            $node_id = 1;//$node['category_id'];

            if ($current !== false) {

                if ($node_depth ==0) {

                    if ($node['lft'] <= $current['lft'] && $node['rgt'] >= $current['rgt']) {
                        // selected root item
                        $root = $node;
                    }
                } else if (!isset($root)) {
                    // skip all items that are not under the selected root
                    continue;
                } else {
                    // when selected root is found

                    $isInRange = ($root['lft'] <= $node['lft'] && $root['rgt'] >= $node['rgt']);
                    if (!$isInRange) {
                        // skip all of the items that are not in range of the selected root
                        continue;
                    } else if (isset($current['lft']) && $node['lft'] == $current['lft']) {
                        // selected item reached
                        $found  = true;
                        $current = $node;
                    } else if ($nextSibling !== false && $nextSibling['depth'] < $node['depth']) {

                        // if we have siblings after the selected item
                        // skip any other childerns in the same range or the selected root item
                        continue;
                    } else if ($found && $node_depth == $node['depth']) {
                        // siblings after the selected item
                        $nextSibling = $node;
                    }
                }
            } else if ($node_depth > 0) {
                // show root items only if no childern is selected
                continue;
            }

            if ($node_depth == $current_depth) {
                if ($counter > 0)
                    $result .= '</li>';
            }
            elseif ($node_depth > $current_depth) {

                $result .= '<ul>';
                $current_depth = $current_depth + ($node_depth - $current_depth);
            } elseif ($node_depth < $current_depth) {

                $result .= str_repeat('</li></ul>', $current_depth - $node_depth) . '</li>';
                $current_depth = $current_depth - ($current_depth - $node_depth);
            }
            $result .= '<li id="c' . $node_id . '" ';
            $result .= $node_depth < 2 ?' class="open"':'';
            $result .= '><a href="#">' . $node_name .'(' . $node['lft'] . '-' . $node['rgt'] . ')' . '</a>';
            ++$counter;
        }
        unset($found);
        unset($nextSibling);

        $result .= str_repeat('</li></ul>', $node_depth) . '</li>';

        $result .= '</ul>';

        return $result;
    }

Usage:

$categories = array(
    array('name' => '1. item',
        'depth' => '0',
        'lft' => '1',
        'rgt' => '2'),
    array('name' => '2. item',
        'depth' => '0',
        'lft' => '3',
        'rgt' => '22'),
    array('name' => '2.1 item',
        'depth' => '1',
        'lft' => '4',
        'rgt' => '5'),
    array('name' => '2.2 item',
        'depth' => '1',
        'lft' => '6',
        'rgt' => '13'),
    array('name' => '2.2.1 item',
        'depth' => '2',
        'lft' => '7',
        'rgt' => '8'),
    array('name' => '2.2.2 item',
        'depth' => '2',
        'lft' => '9',
        'rgt' => '10'),
    array('name' => '2.2.3 item',
        'depth' => '2',
        'lft' => '11',
        'rgt' => '12'),
    array('name' => '2.3 item',
        'depth' => '1',
        'lft' => '14',
        'rgt' => '15'),
    array('name' => '2.4 item',
        'depth' => '1',
        'lft' => '16',
        'rgt' => '21'),
    array('name' => '2.4.1 item',
        'depth' => '2',
        'lft' => '17',
        'rgt' => '18'),
    array('name' => '2.4.2 item',
        'depth' => '2',
        'lft' => '19',
        'rgt' => '20'),
    array('name' => '3. item',
        'depth' => '0',
        'lft' => '23',
        'rgt' => '24'),
    array('name' => '4. item',
        'depth' => '0',
        'lft' => '25',
        'rgt' => '34'),
     array('name' => '4.1 item',
        'depth' => '1',
        'lft' => '26',
        'rgt' => '27'),
     array('name' => '4.2 item',
        'depth' => '1',
        'lft' => '28',
        'rgt' => '33'),
     array('name' => '4.2.1 item',
        'depth' => '2',
        'lft' => '29',
        'rgt' => '30'),
     array('name' => '4.2.2 item',
        'depth' => '2',
        'lft' => '31',
        'rgt' => '32',
         'category_id' => 5),
    array('name' => '5. item',
        'depth' => '0',
        'lft' => '35',
        'rgt' => '36'),
);
$current = array('lft' => '9', 'rgt' => '10');
print MyRenderTree($categories, $current);
Lum answered 14/10, 2011 at 0:30 Comment(2)
I need to actually skip "hidden" items, not just hide them with CSS.Despoliation
@Māris Kiseļovs I have updated the answer to hidden the items.Lum
A
1

http://www.jstree.com/ is a jQuery plugin which will handle this for you far more elegantly and quickly than trying to do a PHP based solution.

Check out http://www.jstree.com/demo for a live demo and instruction on how tom implement.

Acaulescent answered 19/10, 2011 at 15:23 Comment(0)
C
1

Based on answer by satrun77. I created a helper for + + (http://www.doctrine-project.org/projects/orm/1.2/docs/manual/hierarchical-data/en):

function render_tree_html_list($nodes, Doctrine_Record $current_node, $render = true) {
    $html = '';
    $current_node_level = $current_node->getLevel();
    $counter = 0;
    $found = false;
    $nextSibling = false;

    foreach ($nodes as $i => $node):
        $node_level = $node->getLevel();
        $node_name = $node->getTitulo();
        $node_id = $node->getId();

        if ($current_node !== false) {
            if ($node_level == 0) {

                if ($node->getLft() <= $current_node->getLft() && $node->getRgt() >= $current_node->getRgt()) {
                    // selected root item
                    $root = $node;
                }
            } else if (!isset($root)) {
                // skip all items that are not under the selected root
                continue;
            } else {
                // when selected root is found

                $isInRange = ($root->getLft() <= $node->getLft() && $root->getRgt() >= $node->getRgt());
                if (!$isInRange) {
                    // skip all of the items that are not in range of the selected root
                    continue;
                } else if ($current_node->getLft() && $node->getLft() == $current_node->getLft()) {
                    // selected item reached
                    $found = true;
                    $current_node = $node;
                } else if ($nextSibling !== false && $nextSibling->getLevel() < $node->getLevel()) {

                    // if we have siblings after the selected item
                    // skip any other childerns in the same range or the selected root item
                    continue;
                } else if ($found && $node_level == $node->getLevel()) {
                    // siblings after the selected item
                    $nextSibling = $node;
                }
            }
        } else if ($node_level > 0) {
            // show root items only if no childern is selected
            continue;
        }

        if ($node_level == $current_node_level) {
            if ($counter > 0)
                $html .= '</li>';
        }
        elseif ($node_level > $current_node_level) {
            $html .= '<ol>';
            $current_node_level = $current_node_level + ($node_level - $current_node_level);
        } elseif ($node_level < $current_node_level) {
            $html .= str_repeat('</li></ol>', $current_node_level - $node_level) . '</li>';
            $current_node_level = $current_node_level - ($current_node_level - $node_level);
        }

        $html .= sprintf('<li node="%d" class="%s"><div>%s</div>',
                $node_id,
                (isset($nodes[$i + 1]) && $nodes[$i + 1]->getLevel() > $node_level) ? "node" : "leaf",
                $node->getLevel() > 0 ? link_to($node->getTitulo(), 'cms_categoria_edit', $node) : $node->getTitulo()
        );

        ++$counter;
    endforeach;

    $html .= str_repeat('</li></ol>', $node_level) . '</li>';
    $html = '<ol class="sortable">'. $html .'</ol>';


    return $render ? print($html) : $html;
}

Extra tags: ,

Colver answered 20/1, 2012 at 23:35 Comment(0)
E
0

This method checks to see if the node is a parent of the selected node, the selected node, or depth=0. Only iterations for nodes which meet one of these conditions add list items to the result string. All of the nodes get either the selected class, open class or both. Otherwise, it is your code.

$current_depth = 0;
$counter = 0;

$result = '<ul>';

foreach($tree as $node){
   $node_depth = $node['depth'];
   $node_name = $node['name'];
   $node_id = $node['category_id'];
   $selected = false; 

   if( $node['lft'] <= current['lft'] && $node['rgt'] >= $current['rgt'] ) $selected=true

   if ($node_depth == 0 || $selected == true)
   {
     if($node_depth == $current_depth)
     {
       if($counter > 0) $result .= '</li>';
     }
     elseif($node_depth > $current_depth)
     {
       $result .= '<ul>';
       $current_depth = $current_depth + ($node_depth - $current_depth);
     }
     elseif($node_depth < $current_depth)
     {
       $result .= str_repeat('</li></ul>',$current_depth - $node_depth).'</li>';
       $current_depth = $current_depth - ($current_depth - $node_depth);
     }

     $result .= '<li id="c'.$node_id.'"';
     $result .= ' class="';
     $result .= $node_depth < 2 ?' open':' ';
     $result .= $select == true  ?' selected':' ';
     $result .= '"';
     $result .= '><a href="#">'.$node_name.'</a>';
     ++$counter;
   }
}


$result .= str_repeat('</li></ul>',$node_depth).'</li>';

  $result .= '</ul>';

  return $result;
}

// "$current" may contain category_id, lft, rgt for active list item print MyRenderTree($categories,$current); ?>

Eschatology answered 19/10, 2011 at 2:13 Comment(0)
O
0

Just wanted to provide a OOP, cleaner version, which should make it easier to add any sort of logic apart from the selected one.

It works properly with the array structure posted by @satrun77.

class Node
{
    var $name;
    var $category;
    var $depth;
    var $lft;
    var $rgt;
    var $selected;
    var $nodes = array();

    public function __construct( $name, $category, $depth, $lft, $rgt, $selected = false )
    {
        $this->name = $name;
        $this->category = $category;
        $this->depth = $depth;
        $this->lft = $lft;
        $this->rgt = $rgt;
        $this->selected = $selected;
    }

    public function addNode( Node $node )
    {
        array_push( $this->nodes, $node );
    }

    public function render()
    {
        $renderedNodes = '';
        if ( $this->isSelected() ) {
            $renderedNodes = $this->renderNodes();
        }
        return sprintf( '<li id="c%s"><a href="">%s</a>%s</li>', $this->category, $this->name, $renderedNodes );
    }

    protected function renderNodes()
    {
        $renderedNodes = '';
        foreach ( $this->nodes as $node )
        {
            $renderedNodes .= $node->render();
        }
        return sprintf( '<ul>%s</ul>', $renderedNodes );
    }

    /** Return TRUE if this node or any subnode is selected */
    protected function isSelected()
    {
        return ( $this->selected || $this->hasSelectedNode() );
    }

    /** Return TRUE if a subnode is selected */
    protected function hasSelectedNode()
    {
        foreach ( $this->nodes as $node )
        {
            if ( $node->isSelected() )
            {
                return TRUE;
            }
        }
        return FALSE;
    }
}

class RootNode extends Node
{
    public function __construct() {}

    public function render()
    {
        return $this->renderNodes();
    }
}

function MyRenderTree( $tree, $current )
{
    /** Convert the $tree array to a real tree structure based on the Node class */
    $nodeStack = array();
    $rootNode = new RootNode();
    $nodeStack[-1] = $rootNode;

    foreach ( $tree as $category => $rawNode )
    {
        $node = new Node( $rawNode['name'], $category, $rawNode['depth'], $rawNode['lft'], $rawNode['rgt'], $rawNode['lft'] == $current['lft'] );
        $nodeStack[($node->depth -1)]->addNode( $node );
        $nodeStack[$node->depth] = $node;
        end( $nodeStack );
    }

    /** Render the tree and return the output */
    return $rootNode->render();
}
Oversee answered 20/10, 2011 at 12:34 Comment(0)
R
-1

isn't it the best solution. why there are so many classes, objects bla bla.. ? this simple function is perfect and flexible in everyways. DEMO

$categories = array(
array('id'=>1,'name'=>'test1','parent'=>0),
array('id'=>2,'name'=>'test2','parent'=>0),
array('id'=>3,'name'=>'test3','parent'=>1),
array('id'=>4,'name'=>'test4','parent'=>2),
array('id'=>5,'name'=>'test5','parent'=>1),
array('id'=>6,'name'=>'test6','parent'=>4),
array('id'=>7,'name'=>'test7','parent'=>6),
array('id'=>8,'name'=>'test7','parent'=>3)
); 
$cats = array();
foreach($categories as &$category)
    $cats[$category['parent']][] = $category;
unset($categories);

$selected = 6; // selected id;
echo standartCategory($cats,$selected);
function standartCategory(&$categories,$selected = '',$parent = 0 /*MAIN CATEGORY*/)
{
    if (!isset($categories[$parent])) return array('',0);
    $html = '';
    $haveSelected = 0;
    foreach($categories[$parent] as $category) {

        list($childHtml,$isVisible)   = standartCategory($categories,$selected,$category["id"]);

        $isSelected = $category['id']===$selected;
        if (! ($isVisible | $isSelected)) { // this if to prevent output
            $html .= '<li>'.$category['name'].'</li>';
            continue;
        }

        $haveSelected |= $isVisible | $isSelected;

        $html  .= '<li>'.$category['name'].$childHtml.'</li>';
    }

    return  $parent ? array('<ul>'.$html.'</ul>',$haveSelected) : '<ul>'.$html.'</ul>';
}
Raven answered 20/10, 2011 at 11:26 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.