Menu icon Foundation
[plugin] mtree menu

Screen shot 2014 07 01 at 14.04.26

MTREE JS
mtree is a jquery plugin that creates a smooth vertical expandable menu from a basic html list element. Furthermore, it adds various dynamic classes to the list, so you have full control of styling- and design. It works well as a sidebar menu, or inside the off-canvas nav!

TRY IT
http://codepen.io/mjau-mjau/full/yDKBh/

INFO
Although the mtree jquery plugin does not *require* Foundation, it is made on top of Foundation framework and combines nicely. It is strongly recommended to use SCSS for styling the list menu, and obviously the menu can inherit Foundation styles.

Mtree uses velocity.js for improved animation performance and additional features. Although it has fallback for plain jQuery animation, I strongly recommend adding the velocity plugin.
http://velocityjs.org

See the codepen for proper examples of styling in CSS (SCSS).

// simply add a list at any depth with the mtree class

<ul class=mtree>
  <li><a href="#">Africa</a>
    <ul>
      <li><a href="#">Algeria</a></li>
      <li><a href="#">Marocco</a></li>
      <li><a href="#">Libya</a></li>
      <li><a href="#">Somalia</a></li>
      <li><a href="#">Kenya</a></li>
      <li><a href="#">Mauritania</a></li>
      <li><a href="#">South Africa</a></li>
    </ul>
  </li>
  <li><a href="#">Arctica</a></li>
  <li><a href="#">Antarctica</a></li>
</ul>         

// SCSS Specific classes that can be styled!

// ul.mtree : root <ul> list element.
// li.mtree-node : All <li> list items with children.
// li.mtree-open : <li> list items that are open
// li.mtree-closed : <li> list items that are closed
// ul.mtree-level-1 : Style <ul> list elements at different levels (depth)
// li.mtree-active : Class for last active <li> list item (experimental)

// mtree.js
// Requires jquery.js and velocity.js (optional but recommended).
// Copy the below function, add to your JS, and simply add a list <ul class=mtree> ... </ul>
;(function ($, window, document, undefined) {
  
  // Only apply if mtree list exists
  if($('ul.mtree').length) { 
  
    
  // Settings
  var collapsed = true; // Start with collapsed menu (only level 1 items visible)
  var close_same_level = false; // Close elements on same level when opening new node.
  var duration = 400; // Animation duration should be tweaked according to easing.
  var listAnim = true; // Animate separate list items on open/close element (velocity.js only).
  var easing = 'easeOutQuart'; // Velocity.js only, defaults to 'swing' with jquery animation.
    
  
  // Set initial styles 
  $('.mtree ul').css({'overflow':'hidden', 'height': (collapsed) ? 0 : 'auto', 'display': (collapsed) ? 'none' : 'block' });
  
  // Get node elements, and add classes for styling
  var node = $('.mtree li:has(ul)');  
  node.each(function(index, val) {
    $(this).children(':first-child').css('cursor', 'pointer')
    $(this).addClass('mtree-node mtree-' + ((collapsed) ? 'closed' : 'open'));
    $(this).children('ul').addClass('mtree-level-' + ($(this).parentsUntil($('ul.mtree'), 'ul').length + 1));
  });
  
  // Set mtree-active class on list items for last opened element
  $('.mtree li > *:first-child').on('click.mtree-active', function(e){
    if($(this).parent().hasClass('mtree-closed')) {
      $('.mtree-active').not($(this).parent()).removeClass('mtree-active');
      $(this).parent().addClass('mtree-active');
    } else if($(this).parent().hasClass('mtree-open')){
      $(this).parent().removeClass('mtree-active'); 
    } else {
      $('.mtree-active').not($(this).parent()).removeClass('mtree-active');
      $(this).parent().toggleClass('mtree-active'); 
    }
  });

  // Set node click elements, preferably <a> but node links can be <span> also
  node.children(':first-child').on('click.mtree', function(e){
    
    // element vars
    var el = $(this).parent().children('ul').first();
    var isOpen = $(this).parent().hasClass('mtree-open');
    
    // close other elements on same level if opening 
    if((close_same_level || $('.csl').hasClass('active')) && !isOpen) {
      var close_items = $(this).closest('ul').children('.mtree-open').not($(this).parent()).children('ul');
      
      // Velocity.js
      if($.Velocity) {
        close_items.velocity({
          height: 0
        }, {
          duration: duration,
          easing: easing,
          display: 'none',
          delay: 100,
          complete: function(){
            setNodeClass($(this).parent(), true)
          }
        });
        
      // jQuery fallback
      } else {
        close_items.delay(100).slideToggle(duration, function(){
          setNodeClass($(this).parent(), true);
        });
      }
    }
    
    // force auto height of element so actual height can be extracted
    el.css({'height': 'auto'}); 
    
    // listAnim: animate child elements when opening
    if(!isOpen && $.Velocity && listAnim) el.find(' > li, li.mtree-open > ul > li').css({'opacity':0}).velocity('stop').velocity('list');
    
    // Velocity.js animate element
    if($.Velocity) {
      el.velocity('stop').velocity({
        //translateZ: 0, // optional hardware-acceleration is automatic on mobile
        height: isOpen ? [0, el.outerHeight()] : [el.outerHeight(), 0]
      },{
        queue: false,
        duration: duration,
        easing: easing,
        display: isOpen ? 'none' : 'block',
        begin: setNodeClass($(this).parent(), isOpen),
        complete: function(){
          if(!isOpen) $(this).css('height', 'auto');
        }
      });
    
    // jQuery fallback animate element
    } else {
      setNodeClass($(this).parent(), isOpen);
      el.slideToggle(duration);
    }
    
    // We can't have nodes as links unfortunately
    e.preventDefault();
  });
  
  // Function for updating node class
  function setNodeClass(el, isOpen) {
    if(isOpen) {
      el.removeClass('mtree-open').addClass('mtree-closed');
    } else {
      el.removeClass('mtree-closed').addClass('mtree-open');
    }
  }
  
  // List animation sequence
  if($.Velocity && listAnim) {
    $.Velocity.Sequences.list = function (element, options, index, size) {
      $.Velocity.animate(element, { 
        opacity: [1,0],
        translateY: [0, -(index+1)]
      }, {
        delay: index*(duration/size/2),
        duration: duration,
        easing: easing
      });
    };
  }
    
    // Fade in mtree after classes are added.
    // Useful if you have set collapsed = true or applied styles that change the structure so the menu doesn't jump between states after the function executes.
    if($('.mtree').css('opacity') == 0) {
      if($.Velocity) {
        $('.mtree').css('opacity', 1).children().css('opacity', 0).velocity('list');
      } else {
        $('.mtree').show(200);
      }
    }
  }
}(jQuery, this, this.document));

pluginjsjquerymtreemenusidebaroffcanvas

Screen shot 2014 07 01 at 14.04.26

MTREE JS
mtree is a jquery plugin that creates a smooth vertical expandable menu from a basic html list element. Furthermore, it adds various dynamic classes to the list, so you have full control of styling- and design. It works well as a sidebar menu, or inside the off-canvas nav!

TRY IT
http://codepen.io/mjau-mjau/full/yDKBh/

INFO
Although the mtree jquery plugin does not *require* Foundation, it is made on top of Foundation framework and combines nicely. It is strongly recommended to use SCSS for styling the list menu, and obviously the menu can inherit Foundation styles.

Mtree uses velocity.js for improved animation performance and additional features. Although it has fallback for plain jQuery animation, I strongly recommend adding the velocity plugin.
http://velocityjs.org

See the codepen for proper examples of styling in CSS (SCSS).

// simply add a list at any depth with the mtree class

<ul class=mtree>
  <li><a href="#">Africa</a>
    <ul>
      <li><a href="#">Algeria</a></li>
      <li><a href="#">Marocco</a></li>
      <li><a href="#">Libya</a></li>
      <li><a href="#">Somalia</a></li>
      <li><a href="#">Kenya</a></li>
      <li><a href="#">Mauritania</a></li>
      <li><a href="#">South Africa</a></li>
    </ul>
  </li>
  <li><a href="#">Arctica</a></li>
  <li><a href="#">Antarctica</a></li>
</ul>         

// SCSS Specific classes that can be styled!

// ul.mtree : root <ul> list element.
// li.mtree-node : All <li> list items with children.
// li.mtree-open : <li> list items that are open
// li.mtree-closed : <li> list items that are closed
// ul.mtree-level-1 : Style <ul> list elements at different levels (depth)
// li.mtree-active : Class for last active <li> list item (experimental)

// mtree.js
// Requires jquery.js and velocity.js (optional but recommended).
// Copy the below function, add to your JS, and simply add a list <ul class=mtree> ... </ul>
;(function ($, window, document, undefined) {
  
  // Only apply if mtree list exists
  if($('ul.mtree').length) { 
  
    
  // Settings
  var collapsed = true; // Start with collapsed menu (only level 1 items visible)
  var close_same_level = false; // Close elements on same level when opening new node.
  var duration = 400; // Animation duration should be tweaked according to easing.
  var listAnim = true; // Animate separate list items on open/close element (velocity.js only).
  var easing = 'easeOutQuart'; // Velocity.js only, defaults to 'swing' with jquery animation.
    
  
  // Set initial styles 
  $('.mtree ul').css({'overflow':'hidden', 'height': (collapsed) ? 0 : 'auto', 'display': (collapsed) ? 'none' : 'block' });
  
  // Get node elements, and add classes for styling
  var node = $('.mtree li:has(ul)');  
  node.each(function(index, val) {
    $(this).children(':first-child').css('cursor', 'pointer')
    $(this).addClass('mtree-node mtree-' + ((collapsed) ? 'closed' : 'open'));
    $(this).children('ul').addClass('mtree-level-' + ($(this).parentsUntil($('ul.mtree'), 'ul').length + 1));
  });
  
  // Set mtree-active class on list items for last opened element
  $('.mtree li > *:first-child').on('click.mtree-active', function(e){
    if($(this).parent().hasClass('mtree-closed')) {
      $('.mtree-active').not($(this).parent()).removeClass('mtree-active');
      $(this).parent().addClass('mtree-active');
    } else if($(this).parent().hasClass('mtree-open')){
      $(this).parent().removeClass('mtree-active'); 
    } else {
      $('.mtree-active').not($(this).parent()).removeClass('mtree-active');
      $(this).parent().toggleClass('mtree-active'); 
    }
  });

  // Set node click elements, preferably <a> but node links can be <span> also
  node.children(':first-child').on('click.mtree', function(e){
    
    // element vars
    var el = $(this).parent().children('ul').first();
    var isOpen = $(this).parent().hasClass('mtree-open');
    
    // close other elements on same level if opening 
    if((close_same_level || $('.csl').hasClass('active')) && !isOpen) {
      var close_items = $(this).closest('ul').children('.mtree-open').not($(this).parent()).children('ul');
      
      // Velocity.js
      if($.Velocity) {
        close_items.velocity({
          height: 0
        }, {
          duration: duration,
          easing: easing,
          display: 'none',
          delay: 100,
          complete: function(){
            setNodeClass($(this).parent(), true)
          }
        });
        
      // jQuery fallback
      } else {
        close_items.delay(100).slideToggle(duration, function(){
          setNodeClass($(this).parent(), true);
        });
      }
    }
    
    // force auto height of element so actual height can be extracted
    el.css({'height': 'auto'}); 
    
    // listAnim: animate child elements when opening
    if(!isOpen && $.Velocity && listAnim) el.find(' > li, li.mtree-open > ul > li').css({'opacity':0}).velocity('stop').velocity('list');
    
    // Velocity.js animate element
    if($.Velocity) {
      el.velocity('stop').velocity({
        //translateZ: 0, // optional hardware-acceleration is automatic on mobile
        height: isOpen ? [0, el.outerHeight()] : [el.outerHeight(), 0]
      },{
        queue: false,
        duration: duration,
        easing: easing,
        display: isOpen ? 'none' : 'block',
        begin: setNodeClass($(this).parent(), isOpen),
        complete: function(){
          if(!isOpen) $(this).css('height', 'auto');
        }
      });
    
    // jQuery fallback animate element
    } else {
      setNodeClass($(this).parent(), isOpen);
      el.slideToggle(duration);
    }
    
    // We can't have nodes as links unfortunately
    e.preventDefault();
  });
  
  // Function for updating node class
  function setNodeClass(el, isOpen) {
    if(isOpen) {
      el.removeClass('mtree-open').addClass('mtree-closed');
    } else {
      el.removeClass('mtree-closed').addClass('mtree-open');
    }
  }
  
  // List animation sequence
  if($.Velocity && listAnim) {
    $.Velocity.Sequences.list = function (element, options, index, size) {
      $.Velocity.animate(element, { 
        opacity: [1,0],
        translateY: [0, -(index+1)]
      }, {
        delay: index*(duration/size/2),
        duration: duration,
        easing: easing
      });
    };
  }
    
    // Fade in mtree after classes are added.
    // Useful if you have set collapsed = true or applied styles that change the structure so the menu doesn't jump between states after the function executes.
    if($('.mtree').css('opacity') == 0) {
      if($.Velocity) {
        $('.mtree').css('opacity', 1).children().css('opacity', 0).velocity('list');
      } else {
        $('.mtree').show(200);
      }
    }
  }
}(jQuery, this, this.document));
Rafi Benkual over 5 years ago

This menu is really sweet! Thanks for sharing!

Alex Plaza over 5 years ago

this plugin is awesome! thanks for this :)

versedi almost 5 years ago

Really nice solution. I'm wondering just about one thing...how to adapt it into php that's adding class name .i.e. "XXX" when something in that menu is active.

<li class="XXX mtree-node mtree-closed">
<a href="#" class="expander" style="cursor: pointer;"></a>
<a href="woodenboxes.html">
Wooden Boxes      
</a>

<ul class="mtree-level-1" style="overflow: hidden; height: 0px; display: none;">
  <li><a href="Elegant">Elegant</a></li>
  <li><a href="Luxury">Luxury</a></li>

</ul>

</li>

So now I'd have to edit something in Javascript so it should check – if parent LI element has class "XXX" then replace 'mtree-closed' with 'mtree-open'. Any thoughts?

I've managed to check for that class 'XXX' but instead of expanding/opening that item, this specific item is the only one that's having classes from JS script. So item with 'XXX'- only one expandable.

Paul Adams over 2 years ago

Hi I am trying to alter the mtree plugin so that if the parent menu item has a child menu item selected the menu stays open while browsing through the sub menu pages, but closes when same level item is selected

I'm currently hacking the code using the following which I am very ashamed of!

 

Any advice on how to do this would be great as I have only just started JavaScript / jQuery

 

Many Thanks

$('.current-menu-parent').removeClass('mtree-closed').addClass('mtree-open mtree-active block');
  
$('.current-menu-parent').click(function(){
    $(this).next('.sub-menu').slideUp();
    $(this).toggleClass('block');
});

 

.block .sub-menu {
    display:block!important;
    overflow:visible!important;
    height:auto!important;
}

Rick Chasin over 5 years ago

Fantastic! Thank you!!

Darek Wędrychowski over 5 years ago

That's a great plugin, thanks a lot! I especially like the close_same_level switch. Good job!

I think a good addition to that would be a similar option allowing to open and close nodes on mouseover.