CSS Menu Helper

This article is also available in the following languages:
By reevesj
This is a helper and CSS file for displaying a dropdown menu on your site.
This can be used to display a dropdown menu with sub menus. There are currently 3 kinds of menu: menus where elements are listed down, and sub menus pop right, menus where submenus pop left, and menus where the top level elements are listed horizontally and sub menus pop right (except for the first, which pops down).

Demo site: http://vectorjohn.com/cake/
The way the CSS works now, you can only have a maximum of 5 levels deep in your menu structure. If you have more, they are all displayed when you hover over the 5th level menu item. The limit can be increased by adding 2 lines to the CSS file.

To use it, just put the file css_menu.php in your helpers directory, and put the file css_menu.css in your webroot/css directory. You also need the 3 arrow image files, which are referenced by the CSS file. You could make your own, but here they are. They also belong in webroot/css:
http://vectorjohn.com/cake/files/arrow-left.png http://vectorjohn.com/cake/files/arrow-right.png http://vectorjohn.com/cake/files/arrow-down.png
Then in a view, you can do:

echo $cssMenu->menu($menu,'down');
To make a horizontal menu, for example. The other options are 'left' and 'right'. The $menu argument is explained in the comments of the code.

If you want to use it in any view, you can add 'CssMenu' to your $helpers array.

It would be easy to make a menu model to go along with this, and store a menu structure in the database. I might just do that.

Here is the code for the helper, css_menu.php:

Helper Class:

<?php 
/*
 * CSS menu helper.  
 * Author: John Reeves.
 */
class CssMenuHelper extends Helper{

    var 
$helpers = array('Html');
    
    
/*
     * display a menu list.
     * @arg $data: a nested associative array.  The keys are the text that
     *     is displayed for that menu item.  If the value is an array, it is
     *    treated as a sub menu, with the same format.  Otherwise it is 
     *    interpreted as a URL to be used for a link.
     * @arg $type: the type of array.  Can be right, left, or down.
     */
    
function menu($data=array(), $type='right'){
        global 
$cm_css_inc;
        
$out ='';
        if(
$cm_css_inc != true){
            
$cm_css_inc true;
            
$out .= $this->Html->css('css_menu');
        }
        return 
$this->output($out $this->_cm_render($data$type));
    }

    
/* render a menu. 
     * This is a helper for recursion.  The arguments are the 
     * same as for $this->menu().
     */
    
function _cm_render($data=array(), $type='right'){
        
$out='';
        if(
$data == array()) return;
        if(
is_array($data)){
            
$out .= "<ul class=\"css_menu cm_$type\">\n";
            foreach(
$data as $key => $item){
                if(
is_array($item)){
                    
$out .= '<li class="parent">'$key"\n";
                    
$out .= $this->_cm_render($item$type);
                    
$out .= "</li>\n";
                }else{
                    
$out .= '<li><a href="'$item'">'$key'</a></li>'"\n";
                }
            }
            
$out .=  "</ul>\n";
        }
        return 
$out;
    }
}
?>

Here is the CSS file, css_menu.css, to go in your webroot/css folder:

/*
 * CSS for css menu helper.
 * Author: John Reeves
 * Credit given to Jake Gordon, author of Nice Menus module
 * for Drupal, for much of the idea.
 */
ul.css_menu,
ul.css_menu ul{
    list-style: none;
    margin: 0;
    padding: 1px;
}

ul.css_menu ul{
    display: none;
    position: absolute;
    margin-right: 0;
    z-index: 5;
}

ul.css_menu li{
    margin: 0;
    padding: .1em;
}

ul.css_menu li{
    float: left;
    border: 1px solid black;
    background-color: #99fefd;
    width: 6em;
    position: relative;
    left: 2px;
    top: 0;
    
}

ul.css_menu ul li{
    display: block;
}

ul.css_menu:after{
    clear: both;
    display: block;
    height: 0;
    visibility: hidden;
}

ul.css_menu li:hover{
    background-color: #66cbca;
}

/*
 * Hide sub menus that are not hovered over.
 * It only works for 5 levels deep.  If for some reason you need
 * more, it should be easy to see how to copy the last selector and
 * add one more li:hover.  Same goes for the display: block; part below.
 */
ul.css_menu ul,
ul.css_menu li:hover ul ul,
ul.css_menu li:hover li:hover ul ul,
ul.css_menu li:hover li:hover li:hover ul ul{
  display: none; 


/* show hovered submenus */
ul.css_menu li:hover ul,
ul.css_menu li:hover li:hover ul,
ul.css_menu li:hover li:hover li:hover ul,
ul.css_menu li:hover li:hover li:hover li:hover ul{
    display: block;
}

/* RIGHT type menus */

ul.cm_right li{
    float: none;
}

ul.cm_right li.parent:hover,
ul.cm_right li li.parent:hover{ 
    background: #66cbca url(arrow-right.png) right center no-repeat;
}

ul.cm_right li.parent,
ul.cm_right li li.parent{ 
    background-image: url(arrow-right.png);
    background-position: right center;
    background-repeat: no-repeat;
}

ul.cm_right li ul,
ul.cm_right li ul li.parent ul{
    left: 5.9em;
    top: -2px;
}


/* LEFT type menus */

ul.cm_left li{
    float: none;
    padding-left: 15px;
}

ul.cm_left li.parent:hover,
ul.cm_left li li.parent:hover{ 
    background: #66cbca url(arrow-left.png) left center no-repeat;
}

ul.cm_left li.parent,
ul.cm_left li li.parent{ 
    background-image: url(arrow-left.png);
    background-position: left center;
    background-repeat: no-repeat;
}

ul.cm_left li ul,
ul.cm_left li ul li.parent ul{
    left: -7.8em;
    top: -2px;
}



/* DOWN type menus */
ul.cm_down li ul{
    top: 1.5em;
    left: -4px;
}

ul.cm_down li ul li.parent ul{
    left: 5.9em;
    top: -0.1em;
}

ul.cm_down li.parent:hover{
    background: #66cbca url(arrow-down.png) right center no-repeat;
}

ul.cm_down li.parent{
    background-image: url(arrow-down.png);
    background-position: right center;
    background-repeat: no-repeat;
}

ul.cm_down li li.parent:hover{ 
    background: #66cbca url(arrow-right.png) right center no-repeat;
}

ul.cm_down li li.parent{ 
    background-image: url(arrow-right.png);
    background-position: right center;
    background-repeat: no-repeat;
}


Comments

  • Posted 01/19/10 09:39:46 AM
    Sorry for the double post. Apparently I can't delete it.
    =====
    Hi,
    Thanks for your comment. No, this is a separate incident. I added it here because it looks exactly like the problem I am having.

    Let me know if you have any ideas on it, or if you want me to post the entire CSS associated with the menus I am using.


    Thanks
  • Posted 01/19/10 09:39:22 AM
    Hi,
    Thanks for your comment. No, this is a separate incident. I added it here because it looks exactly like the problem I am having.

    Let me know if you have any ideas on it, or if you want me to post the entire CSS associated with the menus I am using.

    Thanks
  • Posted 01/17/10 09:27:54 AM
    I think someone can help you around with your CSS problem. Just a clarification is this related to the CakePHP helper above?
  • Posted 01/17/10 09:05:34 AM
    I inherited a site where the CSS menus don't work in IE6 only and I'm looking to fix it. Can someone point me to a fix that will work in IE6 as well as the other versions and browsers?
    Your help is greatly appreciated.

    To minimize the code here are the hover sections. I can add all of the menu CSS if you'd like:

    #mainlevel li:hover ul ul, #mainlevel li:hover ul ul ul,
    #mainlevel li.sfhover ul ul, #mainlevel li.sfhover ul ul ul {
    left: -999em;
    }
    /* This "unhides" the sub-menus (left: -999em is what hides them) */
    #mainlevel li:hover ul, #mainlevel li li:hover ul, #mainlevel li li li:hover ul,
    #mainlevel li.sfhover ul, #mainlevel li li.sfhover ul, #mainlevel li li li.sfhover ul {
    left: auto;
    }

    #mainlevel li:hover,
    #mainlevel li.sfhover {
    /*background: url(img/nav-hover.png) no-repeat right #959793;*/
    /*background-color: #959793;*/
    position: static;
    }
    #mainlevel ul li:hover, #mainlevel ul ul li:hover,
    #mainlevel ul li.sfhover, #mainlevel ul ul li.sfhover {
    width:220px;
    }
  • Posted 12/11/09 09:08:54 AM
    @Joseph Mosam: Try http://vectorjohn.com/. A cursory inspection of the menu there suggests that it was generated with this helper.

    The arrow images can be found at:

    http://johnreeves.info/dev/css/arrow-down.png
    http://johnreeves.info/dev/css/arrow-left.png
    http://johnreeves.info/dev/css/arrow-right.png
  • Posted 10/01/09 07:12:44 PM
    demo page is saying "page not found"
  • Posted 10/24/08 04:07:24 PM
    In IE and Opera, there is a small but visible gap between the parent li and the child li that breaks the menu functionality. For some reason, Firefox renders the parent and child li elements without the gap, so you can move the mouse easily from one to the other without the child li elements disappearing. Obviously, the gap translates to a mouseout event which removes the child li elements. With some effort (in IE and Opera), I can move the mouse quickly enough but not too far, so that I am able to mouseover the child element before the mouseout on the parent is handled. That's all beside the point. The real question is what is causing the gap and how to get rid of it. I am using the css_menu.css as is. Fwiw, here is my helper function call
    echo $cssMenu->menu(array('Activities'=> array('Add'=> $this->base.'/activities/add','List'=> $this->base.'/activities/index')),'down');
    • Posted 09/04/09 08:07:25 PM
      Eliminate the gap changing the following in the CCS:

      (original values commented)

      ul.cm_down li ul{
      top: 1.2em; /* 1.5em; */
      left: 0; /* -4px; */
      }
  • Posted 08/06/08 02:40:30 AM
    why does it display the array's index number, menu children then the field names?

    am i passing wrong data?
    • Posted 02/03/09 06:45:43 AM
      why does it display the array's index number, menu children then the field names?

      am i passing wrong data?

      ian vill, I had the same problem, so I've made a couple of changes in the code. My array of Links looks like:

      Array
      (
          [0] => Array
              (
                  [Link] => Array
                      (
                          [id] => 6
                          url => http://www.google.com
                          [category_id] => 1
                          [display] => Y
                          [parent_id] => 0
                          [title] => Home
                          [description] => Back to main
                      )
                 )
      )

      In case of child links, it will show the the parent_id (which is not null, of non-zero).

      So, every time, doing(in controller):

       // $type stands for category name and related links in it
       function get_menu($type){
                 $navbar_links = $this->Category->Link->findAll(
                                        array('Category.name_eng'=>$type,
                                              'Link.display'     =>'Y'
                                    ));
                $navbar = $this->Link->set_menu($navbar_links);
                $this->set('navbar',$navbar);
            }   

      Set menu method in the Link model grabs the results and create a nested associative array fitting the helper of John above:
      (in models/link.php)

       function set_menu($data){
              $parents = array();
              $children = array();
              $menu = array();
                  
              foreach($data as $ln){
                  if(empty($ln['Link']['parent_id'])){
                      $parents[$ln['Link']['id']] = $ln['Link'];
                  } else {
                      $children[$ln['Link']['id']] = $ln['Link'];
                  }   
              }   

              foreach($data as $k=>$ln){
                  if(empty($ln['Link']['parent_id'])){
                          $chld = $this->get_children($ln['Link'],$children);
                      if(!empty($chld)){
                          $menu[$ln['Link']['title']] = $chld;
                      } else {
                          $menu[$ln['Link']['title']] = $ln['Link']['url'];    
                      }   
                  }   
              }   
              unset($parents);
              unset($children);

              return $menu;
          }   

          /*  
          *   @array $ln - array of Link table entity
          */
          function get_children($parent,$children){
              $arr = array();
              $parent_id = $parent['id'];
              foreach($children as $k=>$ch){
                  if($parent_id == $ch['parent_id']){
                      $arr[$ch['title']] = $ch['url'];
                  }   
              }   
              if(!empty($arr)){
                  return $arr;
              }   
                  
              return; 
          }   

      The code is a bit dirty, but working
  • Posted 07/30/07 09:47:16 PM
    The down menu doesn't seem to render (or work at all) in Internet Explorer 6.0... any fixes for it?

Comments are closed for articles over a year old