apeisa Posted February 25, 2011 Share Posted February 25, 2011 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 More sharing options...
ryan Posted February 25, 2011 Share Posted February 25, 2011 Are you looking for something that generates a nested list from current page down to homepage? That's what it looks like, but I just wanted to double check. Link to comment Share on other sites More sharing options...
apeisa Posted February 25, 2011 Author Share Posted February 25, 2011 Yes. And nice bonus would be that it adds some classes on path. Link to comment Share on other sites More sharing options...
apeisa Posted February 25, 2011 Author Share Posted February 25, 2011 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 More sharing options...
ryan Posted February 25, 2011 Share Posted February 25, 2011 Sounds good. I will put together a code sample. I'm leaving to pick up my daughter from school now, so may not be back online for awhile. But I will post as soon as I can. Link to comment Share on other sites More sharing options...
apeisa Posted February 25, 2011 Author Share Posted February 25, 2011 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 More sharing options...
Adam Kiss Posted February 26, 2011 Share Posted February 26, 2011 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 More sharing options...
ryan Posted February 26, 2011 Share Posted February 26, 2011 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 More sharing options...
apeisa Posted February 27, 2011 Author Share Posted February 27, 2011 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. 1 Link to comment Share on other sites More sharing options...
ryan Posted February 28, 2011 Share Posted February 28, 2011 Looks good! I didn't do a lot of testing with the function I posted previously, so if you find any bugs that aren't easy to resolve, just let me know and I'll fix them. Link to comment Share on other sites More sharing options...
mindplay.dk Posted February 22, 2013 Share Posted February 22, 2013 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? 7 Link to comment Share on other sites More sharing options...
ryan Posted February 23, 2013 Share Posted February 23, 2013 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. 3 Link to comment Share on other sites More sharing options...
mindplay.dk Posted February 26, 2013 Share Posted February 26, 2013 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 More sharing options...
apeisa Posted February 26, 2013 Author Share Posted February 26, 2013 9 out of 10 of our sites need recursive navigation and Soma's module solves 98% of those needs. Visit function on the core: I like the idea - though really hard to say how much use it would get. Link to comment Share on other sites More sharing options...
ryan Posted February 28, 2013 Share Posted February 28, 2013 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 More sharing options...
pine3ree Posted March 1, 2014 Share Posted March 1, 2014 what about implementing the closure table pattern for specific templates and only for branches/leaves sharing the same template, as in trees of categories? Link to comment Share on other sites More sharing options...
guenter55 Posted May 15, 2014 Share Posted May 15, 2014 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 More sharing options...
adrian Posted May 16, 2014 Share Posted May 16, 2014 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 More sharing options...
guenter55 Posted May 16, 2014 Share Posted May 16, 2014 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 More sharing options...
onjegolders Posted May 16, 2014 Share Posted May 16, 2014 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 More sharing options...
guenter55 Posted May 16, 2014 Share Posted May 16, 2014 Guenter have you set $menu_items? hm - what exactly do you mean? Please see the code above in Post #11 and my changes made above, I made nothing more. Link to comment Share on other sites More sharing options...
adrian Posted May 16, 2014 Share Posted May 16, 2014 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. 2 Link to comment Share on other sites More sharing options...
guenter55 Posted May 16, 2014 Share Posted May 16, 2014 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 More sharing options...
adrian Posted May 16, 2014 Share Posted May 16, 2014 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 More sharing options...
guenter55 Posted May 17, 2014 Share Posted May 17, 2014 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 More sharing options...
Recommended Posts
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 accountSign in
Already have an account? Sign in here.
Sign In Now