将嵌套集模型放入 <ul> 但隐藏“封闭”子树

Posted

技术标签:

【中文标题】将嵌套集模型放入 <ul> 但隐藏“封闭”子树【英文标题】:Getting nested set model into a <ul> but hiding "closed" subtrees 【发布时间】:2011-12-05 11:06:30 【问题描述】:

基于Getting a modified preorder tree traversal model (nested set) into a <ul>

其中一个答案给出了正确的代码来显示完整的树。我需要的是始终显示活动列表项的第一级(深度=0)和兄弟姐妹+孩子。目标是当用户选择更多列表项的父列表项时扩展树的可见部分。

所以,如果我得到这个列表:

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

如果当前列表项为“2.”,则列表应如下所示:

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

如果当前列表项是“2.2.”,则列表应如下所示:

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

下面有一个示例代码,可以很好地显示完整的树。我还添加了 lft/rgt/current 来解决我的问题。

<?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);
?>

【问题讨论】:

你的意思是“$current”可能包含活动列表项的category_id、lft、rgt?它是一个包含 3 个数据的数组吗? @satrun77 它是一个包含“选定”列表项值的数组。 【参考方案1】:

既然您已经设法对序列进行排序,为什么不按需要输出?

由于一些叶子需要看起来是关闭的,所以迭代器应该能够跳过未选择节点的子节点。

这样做让我想到了解决终止输出树(输出=解析)问题的想法。如果序列中最后一个有效节点的深度大于 0 怎么办?我为此附加了一个 NULL 终止符。所以在循环结束之前仍然可以关闭打开的关卡。

此外,迭代器重载节点以在它们上提供通用方法,例如与当前选定的元素进行比较。

MyRenderTree 函数 (Demo/Full 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>';

这也可以在单个 foreach 和一些变量中解决,但是我认为对于可重用性,基于 SPL Iterators 的实现更好。

【讨论】:

感谢您提供出色的代码和最佳答案。您对如何提高这一性能有任何想法吗?在我的盒子上有 250 个列表项,最大深度为 4 需要大约 0.3-0.4 秒。 你能把你的数组作为var_export 输出到某个pastebin 上吗?我想看看什么需要花费时间,并且我还从我制作这个示例的会话中获得了一些展开的代码。听起来确实有点慢,因为它只是在遍历数组。 刚刚测试过,在这里运行得很快:0.0054750442504883 - 你有具体的当前值吗?这可能是您的数据库访问权限吗?【参考方案2】:

基于 satrun77 的回答。我为symfony + doctrine + nestedset 创建了一个助手(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;

额外标签:tree、node

【讨论】:

【参考方案3】:

这不是最好的解决方案。为什么有这么多类,对象bla bla ..? 这个简单的功能在各个方面都是完美和灵活的。 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>';

【讨论】:

【参考方案4】:

只是想提供一个 OOP 更简洁的版本,它应该可以更轻松地添加除所选逻辑之外的任何类型的逻辑。

它适用于@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();

【讨论】:

【参考方案5】:

http://www.jstree.com/ 是一个 jQuery 插件,它比尝试基于 PHP 的解决方案更优雅、更快速地为您处理这个问题。

查看http://www.jstree.com/demo 以获取有关如何实现 tom 的现场演示和说明。

【讨论】:

【参考方案6】:

可以使用 Jquery,而不是使用 PHP 脚本来处理树导航。 生成树后,其余的事情将由客户端自己处理,它还将保存服务器请求。

参见示例 2 和 3

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

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

它可能会根据您的要求有所帮助。

【讨论】:

【参考方案7】:

此方法检查节点是否为选定节点的父节点、选定节点或深度=0。只有满足这些条件之一的节点的迭代才会将列表项添加到结果字符串中。所有节点都获得选定的类、公开类或两者。否则,它就是你的代码。

$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" 可能包含活动列表项的 category_id, lft, rgt 打印 MyRenderTree($categories,$current); ?>

【讨论】:

【参考方案8】:

函数期望 $tree 是按“左”排序的。

我已根据“左”和“右”值将您的功能修改为选定项目。希望这就是你所追求的。

修改后的功能:

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;
    

用法:

$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);

【讨论】:

我实际上需要跳过“隐藏”项目,而不仅仅是用 CSS 隐藏它们。 @Māris Kiseļovs 我已经更新了隐藏项目的答案。

以上是关于将嵌套集模型放入 <ul> 但隐藏“封闭”子树的主要内容,如果未能解决你的问题,请参考以下文章

将 html 嵌套列表解析为 perl 数组

Bootstrap 隐藏纵向但显示横向

将 HTML 放入 JSON

关于 ul 嵌套 li 并且再嵌套 a 的 BUG

使用 jQuery(this).next() 在菜单中显示/隐藏下一个嵌套的 UL

BeautifulSoup:如何从包含一些嵌套 <ul> 的 <ul> 列表中提取所有 <li>?