将嵌套集模型转换为 <ul>,但隐藏“关闭”的子树

9
基于将修改的先序树遍历模型(嵌套集)转换为<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
@satrun77 这是一个包含“已选择”列表项值的数组。 - Māris Kiseļovs
8个回答

8

既然您已经成功地对序列进行了排序,为什么不按需输出呢?

由于某些叶子需要显示为关闭状态,因此迭代器应该能够跳过非选定节点的子节点。

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

此外,迭代器重载了节点,以提供常用的方法,例如与当前选择元素进行比较。

MyRenderTree函数(演示/完整代码

编辑:演示Codepad存在问题,这里是源代码:Gist
将嵌套集模型放入<ul>中,但隐藏“关闭”的子树

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迭代器的实现更具可重用性。


感谢您提供如此出色的代码和最佳答案。您有任何关于如何提高此代码性能的想法吗?在我的计算机上,使用250个列表项和最大深度为4时,它需要约0.3-0.4秒的时间。 - Māris Kiseļovs
你能否将数组作为var_export的输出放在某个pastebin上吗?我想看一下哪些地方耗费了时间,而且我也从此次会话中得到了一些展开的代码。这听起来确实有点慢,因为它只是在遍历数组。 - hakre
刚刚测试了一下,运行速度非常快:0.0054750442504883 - 你有特定的当前值吗?这可能是你的数据库访问吧? - hakre

2

1

该函数期望$tree按'left'排序。

我已经修改了您的函数,以基于'left'和'right'值选择项目。希望这正是您想要的。

修改后的函数:

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
@Māris Kiseļovs,我已经更新了答案以隐藏这些项目。 - satrun77

1

基于satrun77的答案,我为 + + (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;
}

额外标签:,


1

0

该方法检查节点是否是所选节点的父节点、所选节点或深度=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" 可能包含类别 ID、左值和右值,用于活动列表项 print MyRenderTree($categories,$current); ?>


0

只是想提供一个面向对象、更干净的版本,这样可以更容易地添加除所选逻辑以外的任何逻辑。

它与@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();
}

-1

这不是最好的解决方案吗?为什么会有那么多类、对象之类的东西呢? 这个简单的函数在各个方面都是完美而灵活的。 演示

$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>';
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接