Jump to content

Recursive navigation


apeisa
 Share

Recommended Posts

Some folks here have probably developed few different ways for recursive navigation. I was just starting to write my own, but thought that I ask from here first.

Looking for something like this:

- Products

- Services

- People

-- Management

-- HR

---- Matti Meikäläinen

---- Jaana Jallakkala

---- Will Ferrell

-- Marketing

-- Pets

- Contact us

Link to comment
Share on other sites

Output could be something like this:

<ul>
 <li><a>Products</a></li>
 <li><a>Services</a></li>
 <li class="on-path"><a>People</a>
   <ul>
      <li class="here"><a>Someone</a></li>
   </ul>
 </li>
 <li><a>Contact Us</a></li>
</ul>
Link to comment
Share on other sites

I wanted to try coding it, and turned out pretty simple. Of course the code could be cleaner (and I could be a better programmer ;)), but it is a start. There is one TODO: that I couldn't get right. Our current cms puts level-n classes for css-styling and wanted to add that into markup too, but couldn't find a way to get last item on loop.

class simpleNav {

/**
 * Holds the current page
 *
 */
public $page;

/**
 * Root page, this won't be rendered, but pages below this will be
 *
 */
public $rootPage;

/**
 * Keeps count of the current depth
 *
 */
public $depth;

/**
 * Unique id for the most outer ul element
 *
 */
public $id;

/**
 * Holds page id:s of all the pages on path
 *
 */
private $pathIdArray;

public function __construct($page, $rootPage = null, $depth = 1, $id = null)
{
	$this->page = $page;
	$this->pathIdArray = $this->_getPathNav($page);
	$this->rootPage = $page->rootParent;
	$this->depth = $depth;
	$this->id = $id;
	if(!$rootPage) $this->rootPage = $page->rootParent;
}

public function render()
{
	if ($this->rootPage->numChildren > 0) {

		// Output the ul and id only if it's given
		echo "\n<ul class='navi level-$this->depth'";
		if($this->id) {
			echo " id='$this->id'";
			$this->id = null; // we want to give ID only for the first ul
		}
		echo ">";

		$loopedPages = $this->rootPage->children;

		foreach($loopedPages as $child) {
			$class = "level-$this->depth";

			// add "here" class if the current page is active
			if ($this->page === $child) $class .= " here";

			// check if looped page is one of the parents of current page (or is current page)
			if (in_array($child->id, $this->pathIdArray)) $class .= " on-path";

			echo "\n\t<li class='{$class}'>\n\t\t<a href='{$child->url}'>{$child->title}</a>";
			if (in_array($child->id, $this->pathIdArray)) {
				$this->depth++;
				$this->rootPage = $child;
				$this->render();
			}
			// TODO: If last element, $this->depth--
		}
		echo "\n</ul>";
	}
}

private function _getPathNav() {
foreach($this->page->parents as $parent) {
	$this->pathIdArray[] = $parent->id;
}
	$this->pathIdArray[] = $this->page->id;
	return $this->pathIdArray;
}

}

Usage is pretty simple:

$navi = new simpleNav($page);
$navi->render();
Link to comment
Share on other sites

I would solve it with something like this:

function menuLevel($menuPage, $maxLevel, $currentLevel = 0, $args=''){
 if (!$menuPages->numChildren()) return;
 
 $retStr = array();
 $retStr[] = '<ul '.($currentLevel===0?$args:'').'>';
 foreach($menuPage->children() as $p){
   $retStr[] = '<li><a href="'.$p->url.'">'.$p->title.'</a>';
   if ($maxLevel > $currentLevel)
     $retStr[] = menuLevel($p, $maxLevel, ++$currentLevel);
   $maxLevel = '</li>';
 }
 $retStr[] = '</ul>';
 
 return implode("\n",$retStr);
}

echo menuLevel($page->find('root'), 2, 0, 'id="navigation"');

But also, I'm weird.

untested. written in browser.

Edit:

Antti, last page in foreach (simplest)

 $count = count($pagesArray); //this depends: custom pages array or just children of one page?
 $count = $menuRootPage->numChildren();
 $i=0;
 foreach(...){
   if (++$i === $count) ... //last element
 }
Link to comment
Share on other sites

Looks like great solutions. Here's one more:

<?php

function treeMenu(Page $page = null, Page $rootPage = null) {

        if(is_null($page)) $page = wire('page');
        if(is_null($rootPage)) $rootPage = wire('pages')->get('/');

        $out = "\n<ul>";

        $parents = $page->parents;

        foreach($rootPage->children as $child) {
                $class = '';
                $s = '';

                if($child->numChildren && $parents->has($child)) {
                        $class = 'on_parent';
                        $s = str_replace("\n", "\n\t\t", treeMenu($page, $child));

                } else if($child === $page) {
                        $class = "on_page";
                        if($page->numChildren) $s = str_replace("\n", "\n\t\t", treeMenu($page, $page));
                }

                if($class) $class = " class='$class'";
                $out .= "\n\t<li>\n\t\t<a$class href='{$child->url}'>{$child->title}</a>$s\n\t</li>";
        }

        $out .= "\n</ul>";
        return $out;
}

Usage:

<?php

// no params: print out tree menu from current page to root parent
echo treeMenu(); 

// or specify what you want it to display
echo treeMenu($page, $page->rootParent);
Link to comment
Share on other sites

Thanks guys.

I modified few lines on Ryan's snippet (to add level-n to class):

<?php

function treeMenu(Page $page = null, Page $rootPage = null) {

        if(is_null($page)) $page = wire('page');
        if(is_null($rootPage)) $rootPage = wire('pages')->get('/');

        $out = "\n<ul>";

        $parents = $page->parents;

        foreach($rootPage->children as $child) {
                $class = "level-" . count($child->parents);
                $s = '';

                if($child->numChildren && $parents->has($child)) {
                        $class .= " on_parent";
                        $s = str_replace("\n", "\n\t\t", treeMenu($page, $child));

                } else if($child === $page) {
                        $class .= " on_page";
                        if($page->numChildren) $s = str_replace("\n", "\n\t\t", treeMenu($page, $page));
                }

                $class = " class='$class'";
                $out .= "\n\t<li>\n\t\t<a$class href='{$child->url}'>{$child->title}</a>$s\n\t</li>";
        }

        $out .= "\n</ul>";
        return $out;
}

Works nicely! Actually learned few new tricks from this also.

Reason for this script is that I want to build generic "default site" that can be used pretty much "out of the box". Of course good only for simple and generic sites, but I need something that sales guys can demo and let clients to play with. Of course this can be useful in other cases too.

  • Like 1
Link to comment
Share on other sites

  • 1 year later...

This is so common - I do things like this all the time, not just for menus.

Does anyone else think this should be part of the core?

For now, I wrote a simple flat function:

/**
 * Recursive traverse and visit every child in a sub-tree of Pages.
 *
 * @param Page $parent root Page from which to traverse
 * @param callable $enter function to call upon visiting a child Page
 * @param callable|null $exit function to call after visiting a child Page (and all of it's children)
 */
function visit(Page $parent, $enter, $exit=null)
{
    foreach ($parent->children() as $child) {
        call_user_func($enter, $child);
        
        if ($child->numChildren > 0) {
            visit($child, $enter, $exit);
        }
        
        if ($exit) {
            call_user_func($exit, $child);
        }
    }
}

With PHP 5.3 you can generate a menu (or whatever else) recursively as simple as this:

visit(
    $pages->get('/menus/main')
    ,
    function(Page $page) {
        echo '<li><a href="' . $page->url . '">' . $page->title . '</a>';
        if ($page->numChildren > 0) {
            echo '<ul>';
        }
    }
    ,
    function(Page $page) {
        echo '</li>';
        if ($page->numChildren > 0) {
            echo '</ul>';
        }
    }
);

Dead simple.

I've seen the options for modules etc. that generate menus - they seem to grow out of control with a million options, and my own helpers seem to evolve the same way, and it doesn't jive with the beautiful, self-contained, simple templates you normally get away with in PW.

Would it make sense to have a standard visit() method in Page in the core?

  • Like 7
Link to comment
Share on other sites

Admittedly I don't use a treeMenu type function all that often myself. I think it just depends on the sites you are building. But I really like the solutions you've mentioned here--very elegant. Perhaps we should have some kind of visit() function like this in the core. Though unless it would be something most people would use, it may belong as a module or library. I'd be curious what other people think. 

Btw, I thought you might like the planned roadmap for ProcessWire 2.4. I'm hoping to finally get started on the full transition to PHP 5.3 here in the next couple of weeks, after PW 2.3 is final. I'm also going to try and get that alternate module configuration method in there for you, if you don't beat me to it. 

  • Like 3
Link to comment
Share on other sites

Admittedly I don't use a treeMenu type function all that often myself. I think it just depends on the sites you are building. But I really like the solutions you've mentioned here--very elegant. Perhaps we should have some kind of visit() function like this in the core. Though unless it would be something most people would use, it may belong as a module or library. I'd be curious what other people think. 

In my experience, 2 out of 3 sites need recursive navigation somewhere.

What do you think about adding a $selector argument to the function? So as to allow recursively visiting pages of a particular type, etc.

Edit: I also wonder if it's possible to preload the entire sub-tree to prevent high number of recursive queries?

Link to comment
Share on other sites

What do you think about adding a $selector argument to the function? So as to allow recursively visiting pages of a particular type, etc.

Seems like a good idea!

Edit: I also wonder if it's possible to preload the entire sub-tree to prevent high number of recursive queries?

ProcessWire caches a lot of this stuff behind the scenes. Once a page is loaded, it doesn't reload it unless the memory cache gets cleared. Likewise, results of selector queries are cached as well. So the navigation tree would have to be pretty large before preloading would help much. But it is feasible to do. It would mean SQL querying the pages_parents table and pulling out all IDs, then preloading those pages with $pages->getById(array(ids...)). But I'm not sure it would amount to a measurable difference or not. 

Link to comment
Share on other sites

  • 1 year later...
  • 2 months later...

Dead simple.

I've seen the options for modules etc. that generate menus - they seem to grow out of control with a million options, and my own helpers seem to evolve the same way, and it doesn't jive with the beautiful, self-contained, simple templates you normally get away with in PW.

Would it make sense to have a standard visit() method in Page in the core?

Hi,

this works fine for me - but how can I get the "home" in this menu?

thanks for helping!

Link to comment
Share on other sites

Something like this should work:

$menu_items->prepend($home);

Of course this all depends on the array of pages that you are iterating through to generate your menu. And of course you need to define $home, like:

$home = $pages->get("/");

Lots of options really.

Link to comment
Share on other sites

Something like this should work:

$menu_items->prepend($home);

Thank you! I tried this in the code above from mindplay.dk but I don´t get it work in the function call "visit".

I tried this:

     $home = $pages->get('/');
     $menu_items->prepend($home);
    
        visit(
            $menu_items
            ,
            function(Page $page) { 
            ...

But I got this error:

Error: Call to a member function prepend() on a non-object
Link to comment
Share on other sites

Thank you! I tried this in the code above from mindplay.dk but I don´t get it work in the function call "visit".

I tried this:

     $home = $pages->get('/');
     $menu_items->prepend($home);
    
        visit(
            $menu_items
            ,
            function(Page $page) { 
            ...

But I got this error:

Error: Call to a member function prepend() on a non-object

Guenter have you set $menu_items?

Link to comment
Share on other sites

Sorry I didn't actually look at mindplay's code. It might be easiest to just:

echo '<li><a href="/">Home</a></li>';

before the spot where you return the output of the function.

Perhaps if you are still confused so us your complete code so far.

  • Like 2
Link to comment
Share on other sites

It might be easiest to just:

echo '<li><a href="/">Home</a></li>';

before the spot where you return the output of the function.

That was my first simple solution, but I think it is better in the function or foreach loop.

Here my full code:

<?php
// function für Menü
function visit(Page $parent, $enter, $exit=null)
{
    foreach ($parent->children() as $child) {
        call_user_func($enter, $child);
        // wir müssen jene Kinder ausklammern, die nicht wirklich Untermenüs sind!
        // no childs that are not really childs (submenus) or such I dont want!
        if ($child->numChildren > 0 && $child->title !== 'Zimmer'  && $child->title !== 'Ferienwohnungen') {
            visit($child, $enter, $exit);
        }
        
        if ($exit) {
            call_user_func($exit, $child);
        }
    }
}	
?>
        <nav id="navigation" role="navigation">
          <div id="main-menu">
            <!-- <ul><li><a href="/">Home</a></li> --> <!-- dont want it here -->
             <?php 
                visit(
                    $pages->get('/')
                    ,
                    function(Page $page) {
                        //echo '<li><a href="' . $page->url . '">' . $page->title . '</a>';
                        // make the current page and only its first level parent have an active class
                        if($page === wire("page")){
                            $class .= ' active';
                        } 
                        /*if($page === wire("page")->rootParent || wire("page")->parents->has($child)){
                            $class .= ' active';
                        }*/
						$class = strlen($class) ? " class='".trim($class)."'" : '';
        
						echo "<li><a$class href='$page->url'>$page->title</a>";
                        if ($page->numChildren > 0) {
                            echo '<ul>';
                        }
                    }
                    ,
                    function(Page $page) {
                        echo '</li>';
                        if ($page->numChildren > 0) {
                            echo '</ul>';
                        }
                    }
                ); 
			 ?>
            </ul>
          </div>
        </nav>
Link to comment
Share on other sites

I don't really understand why you want it in the foreach loop. Unless you are rendering out this menu in multiple places, there is no need to have the code in the function either. Maybe that is just my take though? Curious to hear what others think.

Link to comment
Share on other sites

Unless you are rendering out this menu in multiple places, there is no need to have the code in the function either.

Thanks for answer! You are right! But "why simply, even if you can have it with difficulty..."  (I hope it is in english what I mean in german...) :)

Now, just for interest. I think it would be a cleaner codedesign.

As it is a code from mindplay.dk I still hope he maybe give a solution to that.

Link to comment
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
 Share

  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...