Jump to content

Volunteer needed to help sort menu


Joss
 Share

Recommended Posts

Oh, gawd.... its one of my Bootstrap moments again!

I have been pretty determined to do a better version of my boostrap menu markup - and, well, THAT was a steep learning curve!

And for the most part I have got there.

The bootstrap menu has an issue - it tends to change class names as you go down the list!

So, the UL has the class dropdown-menu (and all the lower ones do too)

<li> that has no child has no class name but

<li> that have children have the class dropdown. 

<a> That trigger the drop down have a ton of stuff

And 

<li> that are children of the original <li> and have children get the class - dropdown-submen.

Here is a rendered example from one of my demos (you can tell its mine because the words Food and Bar appear in it)


<ul class="nav">
  <li class="active"><a href="/">Home</a></li>
  <li><a href="/food/">Food</a></li>
  <li><a href="/the-bars/">The Bars</a></li>
  <li><a href="/news/">News & Events</a></li>
  <li class="dropdown open"><a class="dropdown-toggle" data-toggle="dropdown" href="/about/">About<b class="caret"></b></a>
 <ul class="dropdown-menu">
  <li class="dropdown-submenu"><a href="/about/about-us/">About Us</a>
     <ul class="dropdown-menu">
       <li><a href="/about/about-us/about-us-1-2/">about us 1 - 2</a></li>
       <li><a href="/about/about-us/about-1-1/">about 1 - 1</a></li>
     </ul>
   </li>
    <li class=""><a href="/about/about-us-2/">about us 2</a></li>
 </ul>
  </li>
</ul>
Now, I am very please to say that I have come up with a nice new clean version of the code that does the above. 
 
But it only goes as far as the first set of sub-menus. 
 
In theory you can keep adding sub-menus to sub-menus till you break the browser, but I don't know how to write it so that from where the sub-menu starts, it keeps rolling out children and grandchildren and greatgrandchildren and .....
 
Here is the code so far - I have commented it heavily:
 
<?php

$homepage = $pages->get("/");
$children = $homepage->children;
$children->prepend($homepage);

echo "<ul class='nav'>";

foreach($children as $child) {

// Some variables
$aclass = count($child->children) ? "dropdown-toggle' data-toggle='dropdown" : '';
$dropclass = count($child->children) ? "dropdown" : '';
$dropcaret = count($child->children) ? "<b class='caret'></b>" : '';
$class = $child === $page->rootParent ? " active" : '';

// first level <li>

// Start by printing off those that actually have children and are not the homepage.

if($child->numChildren > 0 && $child<>$homepage){

echo "<li class='$class $dropclass'><a class='$aclass' href='{$child->url}'>{$child->title}$dropcaret</a>";

// Drop down of second level
if(count($child->children)){
echo "<ul class='dropdown-menu'>";
foreach($child->children as $subchild){
$subdropclass = count($subchild->children) ? "dropdown-submenu" : '';
echo "<li class='$subdropclass'><a href='{$subchild->url}'>" . $subchild->title . "</a>"; // Second level list

// HELP WITH THIS SECTION 
// This next bit is the sub menu and needs to go on for infinity! 
// I dont know how to do that.

if(count($subchild->children)){
echo "<ul class='dropdown-menu'>";
foreach($subchild->children as $babychild){
echo "<li ><a href='{$babychild->url}'>" . $babychild->title . "</a>"; // 3rd level list
echo "</li>";
}
echo "</ul>";
}

// End of the sub menu bit that I need help with.



echo "</li>"; // end second level list

}
echo "</ul>";
} // end second level

echo "</li>"; // End first level

// And finally, do the top level menu items that dont have children and chuck in the homepage.

 }else{
 $class = $child === $page->rootParent ? " class='active'" : '';
 echo "<li$class><a href='{$child->url}'>{$child->title}</a></li>";
 }

// end the loop
}

echo "</ul>";
So:
 
1. Can some nice person show me how to keep the bit I have marked with "Help with this section" going ad-infinitum?
 
2. Is there a better way of doing this?
 
I will make the person a cup of really good espresso next time they pop round! Honest!
 
 
Joss
Link to comment
Share on other sites

Hey Joss,

Not a total solution, more of an idea to get you started; when you hear phrases like "ad-infinitum" in the context of computation you probably need to be thinking of using recursion rather than iteration. In most programming languages this is handled by having a function that calls itself as many times as needed to get the job done.

Something like this perhaps...

function renderChildrenOf($parent)
{
    $output = '';
    $children = $parent->children;

    foreach ($children as $child) {
        // Render this child. NB use of .= which is a PHP shortcut to 
        // concatenate stuff at the end of existing $output.
        // Adjust this to suit your needs
        $output .= '<li>' . $child->title . '</li>';

        // If this child is itself a parent, then render it's children in their own menu too...
        if (count($child->children)) {
            $output .= '<ul class="dropdown-menu">' . renderChildrenOf($child) . '</ul>';
        }
    }

    return $output;
}

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

// Setup the start of the top-level menu...
$menu = '<ul class="nav">';

// Append something for the homepage (as menus usually squish the root node in with its children anyway)...
$menu .= '<li><a href="/">' . $homepage->title . '</a></li>';

// Set the ball rolling...
$menu .= renderChildrenOf($homepage);

// Close the top-level menu...
$menu .= '</ul>';

// Show the result to the world...
echo $menu;

Ok, that's off the top of my head and is meant to give you a starting point to address your ad-infinitum issue. You'll need to experiment some and adjust the markup as needed for bootstrap.

Hope that helps.

  • Like 1
Link to comment
Share on other sites

Thanks Mr Carver

I did play with recursion and processwire chucked me a huge error - but then, I think I was doing it wrong.

I will have a play once I have sorted out the little bit of work I have just got from my leaflet run

Joss

Link to comment
Share on other sites

Hi Netcarver

Okay, I am a bit of the way there.

What I need to do is say something like:
 

If the child <li> is a child of the homepage (or belongs to the toplevel <ul>), then $dropclass = ' dropdown';

else, $dropclass = ' dropdown-submenu';

I will need to do similar to the <a> tag that needs to go within the li

I

Basically, the first level of <li> that have children need the class "dropdown" but lower levels of <li> that have chidren need the class "dropdown-submenu"

Likewise, the <a> of top level <li> that have children need a class, where as lowever level ones need something else

Sorry I have been so slow on this!
 

Joss

Link to comment
Share on other sites

One last thing!
 

I can't get this to work:

$class = $child === $page->rootParent ? " active" : '';

I need to put the class active into the first level (only) <li> tags when the page is, umm, active.

But this doesn't seem to want to work in the code by Netcarver above. It isn't returning anything.

Link to comment
Share on other sites

$class = ($child === wire("page") && count($child->parents) == 1) ? " active" : '';

if it's inside a function the scope doesn't have $page. Also I think the logik was wrong too. Maybe I'm wrong.

Link to comment
Share on other sites

Okay, it now works and the top level <li> get the active class when their link is active.

However, they are not yet getting the class if a child is active.

This is using my old menu: http://pwdemo2.stonywebsites.co.uk/about/about-us/

When the sub item is clicked the parent gets an active class, this is not happening with the new menu ..... yet!

This is the complete code so far, thanks guys!

EDIT: Of course, the home page doesn't get active either because it is outside of the loop....

<?php
/*
* Thanks to Netcarver and Soma for sorting this out for me
*/
$homepage = $pages->get("/");

function renderChildrenOf($parent)
{
    $output = '';
    $children = $parent->children;
  
    foreach ($children as $child) {

    if(count($child->parents) == 1){
        $atoggle = 'class="dropdown-toggle" data-toggle="dropdown" '; // The toggle for the top level <a> tags when there are children
        $dropclass = ' dropdown'; // The class for the top level <li> tabs when they have children
        } else {
        $dropclass = ' dropdown-submenu'; // The class for lower level <li> that also have children
        } 
        $class = ($child === wire("page")) ? " active" : ''; // Makes the top level <li> acive when on that page

        // If this child is itself a parent, then render it's children in their own menu too...
        if (count($child->children)) {

            $output .= '<li class=" ' . $dropclass . '' . $class . ' "><a href="' . $child->url .'"  '. $atoggle .'>' . $child->title . '</a>';
            $output .= '<ul class="dropdown-menu">' . renderChildrenOf($child) . '</ul>';
            $output .= '</li>';
        } else {
            $output .= '<li class=" '. $class .' "><a href="' . $child->url .'" >' . $child->title . '</a></li>';
        }
    }

    return $output;
}

// Setup the start of the top-level menu...
$menu = '<ul class="nav">';

// Append something for the homepage (as menus usually squish the root node in with its children anyway)...
$menu .= '<li><a href="/">' . $homepage->title . '</a></li>';

// Set the ball rolling...
$menu .= renderChildrenOf($homepage);

// Close the top-level menu...
$menu .= '</ul>';

// Show the result to the world...
echo $menu;
Link to comment
Share on other sites

Joss, I rewrote your script to actually work and did some refactoring. I think you'll live it. Let me know if it works.

Here's the code: https://gist.github.com/somatonic/4743011

Which gives you this:

<ul class='nav'>
    <li><a href='/'>Home</a></li>
    <li class='dropdown active'><a href='/about/' class="dropdown-toggle" data-toggle="dropdown">About 2</a>
        <ul class='dropdown-menu'>
            <li><a href='/about/what/'>Child page example 1</a></li>
            <li class='dropdown-submenu'><a href='/about/background/'>Child page example 2</a>
                <ul class='dropdown-menu'>
                    <li><a href='/about/background/another-child-page/'>Another Child Page</a></li>
                    <li><a href='/about/background/another-child-page-2/'>Another Child Page 2</a></li>
                    <li><a href='/about/background/another-page/'>Another Page</a></li>
                </ul>
            </li>
        </ul>
    </li>
    <li class='dropdown'><a href='/templates/' class="dropdown-toggle" data-toggle="dropdown">Templates</a>
        <ul class='dropdown-menu'>
            <li><a href='/templates/asdasd/'>asdasd</a></li>
        </ul>
    </li>
    <li><a href='/site-map/'>Site Map</a></li>
</ul> 
  • Like 2
Link to comment
Share on other sites

Absolutely fantastic!

Not sure what else to say really  :)

Right, well that make the Bootstrap basic profile complete, since that was the last element to get properly right.

I will also post this on the wiki to replace the current, out of date version that is on there.

Next thing is I need to study it and understand how it works - and so I will learn a little bit more.....

Thanks mate

Joss

Link to comment
Share on other sites

Your welcome.

Ok, I updated the script above a little, so it also would works for pages not on top level but further down on a branch. 

The second parameter can be used to hand over the prepended root page, this is to avoid it being used for it's children again, since it will result in endless recursion.

So you can for example now do this:

// bundle up the first level pages and prepend the root page
$root = $pages->get("/about/");
$pa = $root->children;
$pa = $pa->prepend($root);

// Set the ball rolling...
echo renderChildrenOf($pa,$root);
Link to comment
Share on other sites

Oh rats, not quite there.

If I create a child page to a top level page and then make it unpublished and/or hidden, then its parent gets a UL and the classes, even though there are no <li> generated.

Link to comment
Share on other sites

Nice catch joss! Yeah that was missing.

The problem was that $child->numChildren isn't access aware so I use now count($child->children). numChildren also count unpublished and hidden pages. It's now fixed. I also rewrote some if else in there to keep thing short.

Since I'm sick updating it here I created a snippelt. Also for Sublime2 Fetchers and myself in case I need it once ;)


https://gist.github.com/somatonic/4743011

Link to comment
Share on other sites

The problem was that $child->numChildren isn't access aware so I use now count($child->children). numChildren also count unpublished and hidden pages. It's now fixed. I also rewrote some if else in there to keep thing short.

If dealing with potential large quantities of pages, a fairly efficient way to check if the page has visible children:

if($page->child->id)  {
  // page has children
} else {
  // page has no children
  // or you are paginating and none are left
}

If you are going to be using $page->children either way, then it's better just to use it the way you are though. If pagination is involved (whether with  the children or something else), then it's better to $page->children->getTotal(); to eliminate the possibility of count() returning 0 due to being at the end of a pagination set. 

  • Like 1
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...