Hey I have question about Dynamic menu which is created in php.
Code is from stackoverflow, what I want is to get my parent styled with red color if children of those parent is selected, here is code:
$menu = Array(
Array(
'title' => 'Home',
'link' => 'a'
),
Array(
'title' => 'Parent',
'link' => 'b',
'children' => Array(
Array(
'title' => 'Sub 1',
'link' => 'c'
),
Array(
'title' => 'Sub 2',
'link' => 'd'
),
)
)
);
function buildMenu($menuArray)
{
foreach ($menuArray as $node)
{
$selected = ($node['link']== $_GET['menu']) ? $selected = 'style="color: red;"' : null;
echo "<li ".$selected."><a href='?menu=".$node['link']."'/>" . $node['title'] . "</a>";
if ( ! empty($node['children'])) {
echo "<ul>";
buildMenu($node['children']);
echo "</ul>";
}
echo "</li>";
}
}
buildMenu($menu);
So how it needs to go:
Home
Parent - selected
Sub 1 - selected
Sub 2
or
Home
Parent - selected
Sub 1
Sub 2 - selected
Hope someone understand what i want? If my children under parent is selected also parent needs to be selected.
I have added one function to check element in children array. May be something better solution are there. But at this its quick solution for you :)
$menu = array(
array(
'title' => 'Home',
'link' => 'a'
),
array(
'title' => 'Parent',
'link' => 'b',
'children' => array(
array(
'title' => 'Sub 1',
'link' => 'c'
),
array(
'title' => 'Sub 2',
'link' => 'd'
),
)
)
);
function buildMenu($menuArray) {
foreach ($menuArray as $node) {
$getMenu = isset($_GET['menu']) ? $_GET['menu'] : '';
$checkParent = (isset($node['children']) && !empty($node['children'])) ? checkInChildArray($getMenu, $node['children']) : '';
$parentSelected = ($checkParent) ? $selected = 'style="color: red;"' : null;
echo "<li " . $parentSelected . "><a href='?menu=" . $node['link'] . "'>" . $node['title'] . "</a></li>";
if (isset($node['children']) && !empty($node['children'])) {
echo "<ul>";
foreach ($node['children'] as $subMenu) {
$childSelected = ($subMenu['link'] == $getMenu) ? $selected = 'style="color: red;"' : null;
echo "<li " . $childSelected . "><a href='?menu=" . $subMenu['link'] . "'>" . $subMenu['title'] . "</a></li>";
}
echo "</ul>";
}
echo "</li>";
}
}
// Checking if selected menu inside children array.
function checkInChildArray($needle, $haystack, $strict = false) {
foreach ($haystack as $item) {
if (($strict ? $item['link'] === $needle : $item == $needle) || (is_array($item) && checkInChildArray($needle, $item, $strict))) {
return true;
}
}
return false;
}
echo buildMenu($menu);
Working Demo
Use jQuery to add parent li a background color
$('li.selected').parent().closest('li').css("color","red");
Pass every menu level to the url. You can add the "selected" class for every menu level. So:
$current_menu_level_1 = (isset($_GET['menu_level_1'])) ? $_GET['menu_level_1'] : false;
$current_menu_level_2 = (isset($_GET['menu_level_2'])) ? $_GET['menu_level_2'] : false;
When building your menu, compare the 'to be build item' to the $current_menu_level1/2 variable and add echo the class when they are the same.
please, consider create a css class selected for <li> elements and <ul> elements.
With PHP insert this styles when needed, Like this:
$selected = ($node['link']== $_GET['menu']) ? $selected = 'selected' : '';
echo "<li class='".$selected."'>";
echo "<ul class='".$selected."'>";
buildMenu($node['children']);
echo "</ul>";
Related
Hello I have a menu made in codeigniter. but I also want this to have submenu's
Therefore I get an array and go through it with a foreach loop.
<ul>
<?php foreach ($menu_item as $menu =>& $key): ?>
<li><?php echo anchor($menu, $key, $this->uri->slash_segment(1, 'leading') == $menu ? 'class="active"' : '') ?></li>
<?php endforeach ?>
</ul>
Now the problem is that this works great if its just one menu without submenu's but when I get an array like this
$menu_item = array(
'/' => 'Home',
'/about' => 'About',
'/foo' => 'boo',
'/contact' => 'contact',
'test' => array(
'foo' => 'boo'
),
'test2' => 'foo2'
);
Than it doesn't work anymore. How can I loop through everything and output it as a good menu?
You can use recursion to do that job. It takes a bit of getting your head around if you're not familiar with it, but it's very well suited to this kind of problem.
I haven't run this code in PHP, but it will give you an idea.
Basically what happens is that the main menu function checks each item to see if it's an array, and then calls the function again using the sub menu. This will work infinitely deep if required.
<?php
$menu = array(
'/' => 'Home',
'/about' => 'About',
'/foo' => 'boo',
'/contact' => 'contact',
'test' => array(
'foo' => 'boo'
),
'test2' => 'foo2'
);
?>
<ul>
<?php showMenu($menu); ?>
</ul>
<?php
function showMenu($menu)
{
<?php foreach ($menu_item as $menu =>& $key): ?>
<li><?php echo anchor($menu, $key, $this->uri->slash_segment(1, 'leading') == $menu ? 'class="active"' : '') ?></li>
if(is_array($menu_item))
{
echo "<ul>";
showMenu($menu_item);
echo "</ul>";
}
<?php endforeach ?>
}
?>
Hope this helps.
The concept of the other answers is true, but they generate invalid DOM structure, so I decided to fix it.
You can make a helper file and put the drawMenu() function inside. So, you'll be able to call the function as much as you need.
$menu = array(
'/' => 'Home',
'/about' => 'About',
'/foo' => 'boo',
'/contact' => 'contact',
'test' => array(
'foo' => 'bar',
'baz' => 'qux'
),
'test2' => 'foo2'
);
function drawMenu($menu)
{
$CI =& get_instance();
$output = '';
foreach ($menu as $key => $value) {
$output .= "<li>";
if (is_array($value)) {
$output .= anchor('#', $key);
$output .= PHP_EOL."<ul>".PHP_EOL;
$output .= drawMenu($value);
$output .= "</ul>".PHP_EOL."</li>".PHP_EOL;
} else {
$output .= anchor($key, $value, $CI->uri->slash_segment(1, 'leading') == $key ? 'class="active"' : '');
$output .= "</li>".PHP_EOL;
}
}
return $output;
}
$html = drawMenu($menu);
echo '<ul>'. $html .'</ul>';
Side-note: Usage PHP_EOL constant is arbitrary, it just makes generated DOM more readable.
Update:
I improved the drawMenu() functionality, now you can add a URL address for the headers of sub-menus:
$menu = array(
'/' => 'Home',
'/about' => 'About',
'/foo' => 'boo',
'/contact' => 'contact',
'test' => array(
'foo' => 'bar'
),
'This is Test2|/url/to/test2' => array(
'baz' => 'qux'
)
);
You can add the URL after | separator.
function drawMenu($menu)
{
$CI =& get_instance();
$output = '';
foreach ($menu as $key => $value) {
$output .= "<li>";
if (is_array($value)) {
if (strpos($key, '|') !== false) {
$param = explode('|', $key);
$output .= anchor($param[1], $param[0]);
} else {
$output .= anchor('#', $key);
}
$output .= PHP_EOL."<ul>".PHP_EOL;
$output .= drawMenu($value);
$output .= "</ul>".PHP_EOL."</li>".PHP_EOL;
} else {
$output .= anchor($key, $value, $CI->uri->slash_segment(1, 'leading') == $key ? 'class="active"' : '');
$output .= "</li>".PHP_EOL;
}
}
return $output;
}
You can check if the $key is an array: is_array
Then you can use another foreach to loop through the submenus.
try this
<ul>
<?php function buildmenu($menu_item){ ?>
<?php foreach($menu_item as $item){ ?>
<li><?php echo anchor($menu, $key, $this->uri->slash_segment(1, 'leading') == $menu ? 'class="active"' : '') ?></li>
<?php if(is_array($item)){
buildmenu($item);
} ?>
<?php } ?>
<php} ?>
<?php buildmenu($menu_item) ?>
</ul>
$menu = "<ul>\n";
foreach ($menu_item as $key => $value){
if (is_array($value)){
$menu.= "\t<li>".$key."\n\t\t<ul>\n";
foreach ($value as $key2 => $value2){
$menu .= "\t\t\t<li>".$value2."</li>\n";
}
$menu.= "\t\t</u>\n\t</li>\n";
} else {
$menu .= "\t<li>".$value."</li>\n";
}
}
$menu .= "</ul>";
echo $menu;
Output:
<ul>
<li>Home</li>
<li>About</li>
<li>boo</li>
<li>contact</li>
<li>test
<ul>
<li>boo</li>
</u>
</li>
<li>foo2</li>
</ul>
I'm currently looking for help with linking the main category links in the header menu directly to their first subcategory.
In other words:
when clicking on the category link I would like to add the ID of the first subcategory at the end, i.e.
route=product/category&path=18_59
instead of
route=product/category&path=18
Does anyone has a suggestion how to do that?
<div id="menu">
<ul>
<?php foreach ($categories as $category) { ?>
<li><?php echo $category['name']; ?>
<?php if ($category['children']) { ?>
<div>
<?php for ($i = 0; $i < count($category['children']);) { ?>
<ul>
<?php $j = $i + ceil(count($category['children']) / $category['column']); ?>
<?php for (; $i < $j; $i++) { ?>
<?php if (isset($category['children'][$i])) { ?>
<li><?php echo $category['children'][$i]['name']; ?></li>
<?php } ?>
<?php } ?>
</ul>
<?php } ?>
</div>
<?php } ?>
</li>
<?php } ?>
</ul>
</div>
Welcome to StackOverflow!
This could be achieved by getting the first child of the main category and we have two options, one cleaner and one easier.
The cleaner one. This would require the changes in the catalog/controller/common/header.php and model catalog/model/catalog/category.php, let's go on step by step. First, add the new necessary function into the category model:
public function getCategoryFirstChildId($category_id) {
$query = $this->db->query('SELECT category_id FROM '. DB_PREFIX .'category WHERE parent_id = '. (int)$category_id .' ORDER BY category_id ASC LIMIT 1');
return $query->row['category_id'];
}
This function will get all the categories where parent_id is the $category_id given as a parameter, sort them ascending by category_id (You can change that to whatever sorting You want) and return only the first one.
Now let's go to the controller - we will edit the part where categories are loaded:
foreach ($categories as $category) {
if ($category['top']) {
// Level 2
$children_data = array();
$children = $this->model_catalog_category->getCategories($category['category_id']);
foreach ($children as $child) {
$data = array(
'filter_category_id' => $child['category_id'],
'filter_sub_category' => true
);
$product_total = $this->model_catalog_product->getTotalProducts($data);
$children_data[] = array(
'name' => $child['name'] . ($this->config->get('config_product_count') ? ' (' . $product_total . ')' : ''),
'href' => $this->url->link('product/category', 'path=' . $category['category_id'] . '_' . $child['category_id'])
);
}
/*NEW =>*/ $first_child_id = $this->model_catalog_category->getCategoryFirstChildId($category['category_id']);
// Level 1
$this->data['categories'][] = array(
'name' => $category['name'],
'children' => $children_data,
'column' => $category['column'] ? $category['column'] : 1,
'href' => $this->url->link('product/category', 'path=' . $category['category_id'] . '_' . $first_child_id)
// >>> >>> >>> >>> >>> >>> >>> ^^^^^^^^^^^^^^^^^^^^^^^^
);
}
}
This should be it. Be wise that this though cleaner way requires one more DB query per category thus may slow down (though a tiny bit) the site when large number of categories is being processed.
The easier way - need changes to be done only within header controller (no new function is added to the model):
foreach ($categories as $category) {
if ($category['top']) {
// Level 2
$children_data = array();
/*NEW =>*/ $first_child_id = 0;
/*NEW =>*/ $first_child = true;
$children = $this->model_catalog_category->getCategories($category['category_id']);
foreach ($children as $child) {
/*NEW =>*/ if($first_child) {
/*NEW =>*/ $first_child = false;
/*NEW =>*/ $first_child_id = $child['category_id'];
/*NEW =>*/ }
$data = array(
'filter_category_id' => $child['category_id'],
'filter_sub_category' => true
);
$product_total = $this->model_catalog_product->getTotalProducts($data);
$children_data[] = array(
'name' => $child['name'] . ($this->config->get('config_product_count') ? ' (' . $product_total . ')' : ''),
'href' => $this->url->link('product/category', 'path=' . $category['category_id'] . '_' . $child['category_id'])
);
}
// Level 1
$this->data['categories'][] = array(
'name' => $category['name'],
'children' => $children_data,
'column' => $category['column'] ? $category['column'] : 1,
'href' => $this->url->link('product/category', 'path=' . $category['category_id'] . '_' . $first_child_id)
// >>> >>> >>> >>> >>> >>> >>> ^^^^^^^^^^^^^^^^^^^^^^^^
);
}
}
The easier way will also work very well and would be a little quicker but the first category it will point to will always be the one that comes as first when sorting ascending by sort_order - whenever it may change, the main category will point to a different child category...
I didn't test neither of the ways but I believe they will work on 100%.
I did research on this, and wasn't able to find an exact answer. Most of the questions/answers on here pertaining to this seem to be unfinished. If anyone knows of a finished solution similar to my question, please point me in that direction!
Here is my array:
Array
(
['home'] => Array
(
[0] => sub-home1
[1] => sub-home2
)
['about'] => Array
(
[0] => sub-about
['about2'] => Array
(
[0] => sub-sub-about
)
)
['staff'] => Array
(
[0] => sub-staff1
[1] => sub-staff2
)
['contact'] => contact
)
And here is what I would like to turn it into:
<ul>
<li><a href="">home<a/>
<ul>
<li>sub-home1</li>
<li>sub-home2</li>
</ul>
</li>
<li><a href="">about<a/>
<ul>
<li>sub-about</li>
<li>about2
<ul>
<li><a href="">sub-sub-about<a/></li>
</ul>
</li>
</ul>
</li>
<li><a href="">staff<a/>
<ul>
<li>sub-staff1</li>
<li>sub-staff2</li>
</ul>
</li>
<li><a href="">contact<a/></li>
</ul>
The array will be dynamically generated, but will have a limit of 3 levels ex: about->about2->sub-sub-about. I tried going off of this question: PHP/MySQL Navigation Menu but they didn't really seem to come to a conclusion? I am familiar with foreach's whiles and for loops but I just can't seem to wrap my head around this one.
EDIT: Enzino, your code works!
Here is my solution:
<?php
function MakeMenu($items, $level = 0) {
$ret = "";
$indent = str_repeat(" ", $level * 2);
$ret .= sprintf("%s<ul>\n", $indent);
$indent = str_repeat(" ", ++$level * 2);
foreach ($items as $item => $subitems) {
if (!is_numeric($item)) {
$ret .= sprintf("%s<li><a href=''>%s</a>", $indent, $item);
}
if (is_array($subitems)) {
$ret .= "\n";
$ret .= MakeMenu($subitems, $level + 1);
$ret .= $indent;
} else if (strcmp($item, $subitems)){
$ret .= sprintf("%s<li><a href=''>%s</a>", $indent, $subitems);
}
$ret .= sprintf("</li>\n", $indent);
}
$indent = str_repeat(" ", --$level * 2);
$ret .= sprintf("%s</ul>\n", $indent);
return($ret);
}
$menu = Array(
'home' => Array("sub-home1", "sub-home2"),
'about' => Array("sub-about", "about2" => Array("sub-sub-about")),
'staff' => Array("sub-staff1", "sub-staff2"),
'contact' => "contact"
);
print_r($menu);
echo MakeMenu($menu);
?>
Calvin's solution worked for me. Here's the edited version. We can use more nested loops to get sub - sub menu items.
echo '<ul>';
foreach ($menu as $parent) {
echo '<li>' . $parent . '';
if (is_array($parent)) {
echo '<ul>';
foreach ($parent as $children) {
echo '<li>' . $children . '';
}
echo '</ul>';
}
echo '</li>';
}
echo '</ul>';
I think you can use recursion? Here is some pseudocode, not very familiar with php.
function toNavMenu(array A){
for each element in A{
echo "<li>" + element.name + ""
if (element is an array){
echo "<ul>"
toNavMenu(element)
echo "</ul>"
}
echo "</li>"
}
}
I would probably slightly adapt the array to be something like the following:
Array(
0 => Array(
'title' => 'Home',
'children' => Array()
),
1 => Array(
'title' => 'Parent',
'children' => Array(
0 => Array(
'title' => 'Sub 1',
'children' => Array(),
),
1 => Array(
'title' => 'Sub 2',
'children' => Array(
0 => Array(
'title' => 'Sub sub 2-1',
'children' => Array(),
),
),
),
)
)
)
With a structure like this you could use recursion to build your menu HTML:
function buildMenu($menuArray)
{
foreach ($menuArray as $node)
{
echo "<li><a href='#'/>" . $node['title'] . "</a>";
if ( ! empty($node['children'])) {
echo "<ul>";
buildMenu($node['children']);
echo "</ul>";
}
echo "</li>";
}
}
I'm trying to produce a multi-level HTML list from a source array that is formatted like this:
/**
* id = unique id
* parent_id = "id" that this item is directly nested under
* text = the output string
*/
$list = array(
array(
'id' => 1,
'parent_id' => 0,
'text' => 'Level 1',
), array(
'id' => 2,
'parent_id' => 0,
'text' => 'Level 2',
), array(
'id' => 3,
'parent_id' => 2,
'text' => 'Level 2.1',
), array(
'id' => 4,
'parent_id' => 2,
'text' => 'Level 2.2',
), array(
'id' => 5,
'parent_id' => 4,
'text' => 'Level 2.2.1',
), array(
'id' => 6,
'parent_id' => 0,
'text' => 'Level 3',
)
);
The goal is a nested <ul> with an infinite depth. The expected output of the array above is this:
Level 1Level 2Level 2.1Level 2.2Level 2.2.1Level 3
If only the array items had a key called child or something that contained the actual sub-array, it would be easy to recurse though these and get the desired output with a function like this:
function makeList($list)
{
echo '<ul>';
foreach ($list as $item)
{
echo '<li>'.$item['text'];
if (isset($item['child']))
{
makeList($item['child']);
}
echo '</li>';
}
echo '</ul>';
}
Unfortunately that's not the case for me - the format of the source arrays can't be changed. So, long ago I wrote this very nasty function to make it happen, and it only works up to three levels (code is pasted verbatim with original comments). I know it's a long boring read, please bear with me:
function makeArray($links)
{
// Output
$nav = array();
foreach ($links as $k => $v)
{
// If no parent_id is present, we can assume it is a top-level link
if (empty($v['parent_id']))
{
$id = isset($v['id']) ? $v['id'] : $k;
$nav[$id] = $v;
// Remove from original array
unset($links[$k]);
}
}
// Loop through the remaining links again,
// we can assume they all have a parent_id
foreach ($links as $k => $v)
{
// Link's parent_id is in the top level array, so this is a level-2 link
// We already looped through every item so we know they are all accounted for
if (isset($nav[$v['parent_id']]))
{
$id = isset($v['id']) ? $v['id'] : $k;
// Add it to the top level links as a child
$nav[$v['parent_id']]['child'][$id] = $v;
// Set a marker so we know which ones to loop through to add the third level
$nav2[$id] = $v;
// Remove it from the array
unset($links[$k]);
}
}
// Last iteration for the third level
// All other links have been removed from the original array at this point
foreach ($links as $k => $v)
{
$id = isset($v['id']) ? $v['id'] : $k;
// Link's parent_id is in the second level array, so this is a level-3 link
// Orphans will be ignored
if (isset($nav2[$v['parent_id']]))
{
// This part is crazy, just go with it
$nav3 = $nav2[$v['parent_id']]['parent_id'];
$nav[$nav3]['child'][$v['parent_id']]['child'][] = $v;
}
}
return $nav;
}
This makes an array like:
array(
'text' => 'Level 1'
'child' => array(
array(
'text' => 'Level 1.2'
'child' => array(
array(
'text' => 'Level 1.2.1'
'child' => array(
// etc.
),
array(
'text' => 'Level 1.2.2'
'child' => array(
// etc.
),
)
)
)
)
);
Usage:
$nav = makeArray($links);
makeList($nav);
I've spent many spare hours trying to work this out, and the original code which I have given here is still the best solution I've been able to produce.
How can I make this happen without that awful function (which is limited to a depth of 3), and have an infinite number of levels? Is there a more elegant solution to this?
Print:
function printListRecursive(&$list,$parent=0){
$foundSome = false;
for( $i=0,$c=count($list);$i<$c;$i++ ){
if( $list[$i]['parent_id']==$parent ){
if( $foundSome==false ){
echo '<ul>';
$foundSome = true;
}
echo '<li>'.$list[$i]['text'].'</li>';
printListRecursive($list,$list[$i]['id']);
}
}
if( $foundSome ){
echo '</ul>';
}
}
printListRecursive($list);
Create multidimensional array:
function makeListRecursive(&$list,$parent=0){
$result = array();
for( $i=0,$c=count($list);$i<$c;$i++ ){
if( $list[$i]['parent_id']==$parent ){
$list[$i]['childs'] = makeListRecursive($list,$list[$i]['id']);
$result[] = $list[$i];
}
}
return $result;
}
$result = array();
$result = makeListRecursive($list);
echo '<pre>';
var_dump($result);
echo '</pre>';
Tested and working :)
$list = array(...);
$nested = array();
foreach ($list as $item)
{
if ($item['parent_id'] == 0)
{
// Woot, easy - top level
$nested[$item['id']] = $item;
}
else
{
// Not top level, find it's place
process($item, $nested);
}
}
// Recursive function
function process($item, &$arr)
{
if (is_array($arr))
{
foreach ($arr as $key => $parent_item)
{
// Match?
if (isset($parent_item['id']) && $parent_item['id'] == $item['parent_id'])
{
$arr[$key]['children'][$item['id']] = $item;
}
else
{
// Keep looking, recursively
process($item, $arr[$key]);
}
}
}
}
Some methods I recently wrote, maybe some will help, sorry I'm short on time and cannot rewite them to match your needs.
This code is actually a part of Kohana Framework Model, method ->as_array() is used to flat an Database_Result object.
function array_tree($all_nodes){
$tree = array();
foreach($all_nodes as $node){
$tree[$node->id]['fields'] = $node->as_array();
$tree[$node->id]['children'] = array();
if($node->parent_id){
$tree[$node->parent_id]['children'][$node->id] =& $tree[$node->id];
}
}
$return_tree = array();
foreach($tree as $node){
if($node['fields']['depth'] == 0){
$return_tree[$node['fields']['id']] = $node;
}
}
return $return_tree;
}
array_tree() is used to make a tree out of a flat array. The key feature is the =& part ;)
function html_tree($tree_array = null){
if( ! $tree_array){
$tree_array = $this -> array_tree();
}
$html_tree = '<ul>'."\n";
foreach($tree_array as $node){
$html_tree .= $this->html_tree_crawl($node);
}
$html_tree .= '</ul>'."\n";
return $html_tree;
}
function html_tree_crawl($node){
$children = null;
if(count($node['children']) > 0){
$children = '<ul>'."\n";
foreach($node['children'] as $chnode){
$children .= $this->html_tree_crawl($chnode);
}
$children .= '</ul>'."\n";
}
return $this->html_tree_node($node, $children);
}
html_tree_node() is a simple method to display current node and children in HTML.
Example below:
<li id="node-<?= $node['id'] ?>">
<?= $node['title'] ?>
<?= (isset($children) && $children != null) ? $children : '' ?>
</li>
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 .= '>'.$node_name.'';
++$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);
?>
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.
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.
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 .= '>' . $node_name .'(' . $node['lft'] . '-' . $node['rgt'] . ')' . '';
++$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);
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.
Based on answer by satrun77. I created a helper for 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;
}
Extra tags: tree, node
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 .= '>'.$node_name.'';
++$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);
?>
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">%s%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();
}
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>';
}