SitePoint Sponsor

User Tag List

Results 1 to 4 of 4
  1. #1
    SitePoint Evangelist kyllle's Avatar
    Join Date
    Jun 2008
    Posts
    469
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)

    Keeping drop down menu sub navigation transition consistent

    I've created a drop down menu where each main link has a mouseenter and mouseleave event attached that first of all checks if there is a sub nav list associated with the link and then slides the menu into visibility. This works fine if you enter link 1 and leave the dropdown slides in then slides out but if you enter link 1 and then hover over link 2 the slideUp() consistency has gone, ideally i would like to make sure the active menu has closed before the new mouseenter code is run again. Can anyone suggest how I can achieve this?

    Link to fiddle: http://jsfiddle.net/f3ZpN/1/

    JS
    Code:
    var mainNav = $('.main-nav'),
        topLevelLinks = mainNav.children('li'),
        subNav = $('.sub-nav-panel'),
        subNavPanel,
        isActive = false,
        inner,
        innerLink;
    
    topLevelLinks.on({
        mouseenter: function () {
            var el = $(this);
    
            if (el.children('.sub-nav').length > 0) {
                inner = el.children('.sub-nav');
    
                subNav.stop(true, true).slideDown(function () {
                    inner.fadeIn();
                });
            }
        },
        mouseleave: function () {
            //make sure this finishes before mouseenter is run again?
            inner.fadeOut(function() {
                subNav.slideUp();
            });
        }
    });

  2. #2
    Gre aus'm Pott gold trophysilver trophybronze trophy
    Pullo's Avatar
    Join Date
    Jun 2007
    Location
    Germany
    Posts
    5,941
    Mentioned
    215 Post(s)
    Tagged
    12 Thread(s)
    Hi there,

    I've been puzzling over this for a couple of days now.
    Basically the problem you are facing is that on mouseenter and mouseleave, you need to have some kind of idea about what your expandable bar is doing.
    It can have four different states, namely: fully expanded, fully contracted, expanding out or contacting.
    And depending on the state, you need to do different things. For example, if the bar is already contracting when you mouse over it (your mouse having just left a different expandable menu point), you need to catch that, stop the animation, then re-expand the bar.

    Also, you could do with a short delay, before contracting the bar, to make sure you haven't left one expandable point, only to hover over another (this would otherwise result in the bar contracting, then expanding again when you move your mouse from point one to point two).

    Then there's the matter of the second tier of animation, i.e. fading in the sub-menu points.
    This starts getting tricky, as you have different animations which need to wait for each other to finish, before running themselves.

    I did manage to get all of this working.

    Demo 1
    Demo 2

    Demo 1 uses fadeIn() to animate the sub-menu points, demo 2 just uses show()
    The disadvantage of demo 1, is that if the user moves their mouse very quickly over the top-level menu points, it is possible to "confuse" the bar and end up hovering over a top-level expandable menu point, without seeing the assosciated sub navigation.
    I don't know how many users would do this in the real world, but it bugged me.
    In demo 2 I was not able to reproduce this.

    To be complete, here's the code.
    It has lots of duplication and I hate it. I thought I'd leave a possible refactoring to you.

    Code:
    <!DOCTYPE HTML>
    <html>
      <head>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
        <title>Expandable menu bar</title>
        <script src="http://code.jquery.com/jquery-latest.min.js" type="text/javascript"></script>
        <style>
          * {
            @include box-sizing(border-box);
          }
          body {
            background: silver;
            color: white;
            font-family: Arial;
            padding: 20px 0;
          }
          a {
            text-decoration: none;
            color: white;
            height: 100%;
            display: block;
          }
          .header {
            cursor: pointer;
          }
          .nav-ctn {
            background: black;
            position: relative;
          }
          .main-nav {
            position: relative;
            width: 960px;
            margin: auto;
          }
          .main-nav li {
            display: inline-block;
            border-right: 1px solid silver;
            height: 50px;
            line-height: 50px;
            margin-right: -4px;
            //position: relative;
          }
          .main-nav li a {
            display: block;
            padding: 0 30px;
          }
          .main-nav li ul {
            position: absolute;
            top: 100%;
            left: 0;
            width: 960px;
            z-index: 10;
          }
          .inner > li {
            //border-bottom: 1px solid silver;
            //text-indent: 30px;
          }
          .level2.sub-nav {
            //background: darkGrey;
            display: none;
          }
          .sub-nav-panel {
            height: 50px;
            background: #454545;
            position: absolute;
            top: 100%;
            left: 0;
            width: 100%;
            z-index: 9;
            display: none;
          }
        </style>
      </head>
      
      <body>
        <div class="nav-ctn">
          <ul class="main-nav">
            <li class="expandable">
              <a href="">Link 1</a>
              <ul class="level2 sub-nav">
                <li><a href="">Link 1</a></li>
                <li><a href="">Link 2</a></li>
                <li><a href="">Link 3</a></li>
                <li><a href="">Link 4</a></li>
                <li><a href="">Link 5</a></li>
              </ul>
            </li>
            <li class="expandable">
              <a href="">Link 2</a>
              <ul class="level2 sub-nav">
                <li><a href="">This is the new link 1</a></li>
                <li><a href="">This is the new link 2</a></li>
                <li><a href="">This is the new link 3</a></li>
                <li><a href="">This is the new link 4</a></li>
              </ul>
            </li>
            <li><a href="">Link 3</a></li>
            <li><a href="">Link 4</a></li>
          </ul>
          <div class="sub-nav-panel"></div>
        </div>
    
        <script>
          var mainNav = $('.main-nav'),
              topLevelLinks = mainNav.children('li'),
              subNav = $('.sub-nav-panel'),
              isAnimating = false,
              isAnimatingUp = false,
              isAnimatingDown = false,
              t;
          
          $(".expandable").on("mouseenter", function(){
            inner = $(this).children('.sub-nav');
            clearTimeout(t);
    
            if (isAnimating == false){
              isAnimating = true;
              isAnimatingDown = true;
              subNav.slideDown(function(){
                isAnimating = false;
                isAnimatingDown = false;
                inner.show();
              });
            } else {
              if (isAnimatingUp){
                subNav.stop(true, true);
                isAnimatingUp = false;
                isAnimatingDown = true;
                subNav.slideDown(function(){
                  isAnimating = false;
                  isAnimatingDown = false;
                  inner.show();
                });
              } 
            }
          });
          
          $(".expandable").on("mouseleave", function(){
            inner = $(this).children('.sub-nav');
            
            if (isAnimating == false){
              t = setTimeout(function(){
                isAnimating = true;
                isAnimatingUp = true;
                subNav.slideUp(function(){
                  isAnimating = false;
                  isAnimatingUp = false;
                });
              }, 250);
            } else {
              if (isAnimatingDown){
                subNav.stop(true, true);
                isAnimatingDown = false;
                isAnimatingUp = true;
                subNav.slideUp(function(){
                  isAnimating = false;
                  isAnimatingUp = false;
                });
              }
            }
            inner.hide();
          });
        </script>
      </body>
    </html>
    If anyone has an idea for a different approach to tackle this, I'd sure be glad to hear it.

  3. #3
    Gre aus'm Pott gold trophysilver trophybronze trophy
    Pullo's Avatar
    Join Date
    Jun 2007
    Location
    Germany
    Posts
    5,941
    Mentioned
    215 Post(s)
    Tagged
    12 Thread(s)
    Hi again,

    The code I posted above was bugging me, so I went ahead and refactored it for you.
    Here you go, no more nasty globals and considerably less duplication:

    Code:
    <!DOCTYPE HTML>
    <html>
      <head>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
        <title>Expandable menu bar</title>
        <script src="http://code.jquery.com/jquery-latest.min.js" type="text/javascript"></script>
        <style>
          * {
            @include box-sizing(border-box);
          }
          body {
            background: silver;
            color: white;
            font-family: Arial;
            padding: 20px 0;
          }
          a {
            text-decoration: none;
            color: white;
            height: 100%;
            display: block;
          }
          .header {
            cursor: pointer;
          }
          .nav-ctn {
            background: black;
            position: relative;
          }
          .main-nav {
            position: relative;
            width: 960px;
            margin: auto;
          }
          .main-nav li {
            display: inline-block;
            border-right: 1px solid silver;
            height: 50px;
            line-height: 50px;
            margin-right: -4px;
            //position: relative;
          }
          .main-nav li a {
            display: block;
            padding: 0 30px;
          }
          .main-nav li ul {
            position: absolute;
            top: 100%;
            left: 0;
            width: 960px;
            z-index: 10;
          }
          .inner > li {
            //border-bottom: 1px solid silver;
            //text-indent: 30px;
          }
          .level2.sub-nav {
            //background: darkGrey;
            display: none;
          }
          .sub-nav-panel {
            height: 50px;
            background: #454545;
            position: absolute;
            top: 100%;
            left: 0;
            width: 100%;
            z-index: 9;
            display: none;
          }
        </style>
      </head>
      
      <body>
        <div class="nav-ctn">
          <ul class="main-nav">
            <li class="expandable">
              <a href="">Link 1</a>
              <ul class="level2 sub-nav">
                <li><a href="">Link 1</a></li>
                <li><a href="">Link 2</a></li>
                <li><a href="">Link 3</a></li>
                <li><a href="">Link 4</a></li>
                <li><a href="">Link 5</a></li>
              </ul>
            </li>
            <li class="expandable">
              <a href="">Link 2</a>
              <ul class="level2 sub-nav">
                <li><a href="">This is the new link 1</a></li>
                <li><a href="">This is the new link 2</a></li>
                <li><a href="">This is the new link 3</a></li>
                <li><a href="">This is the new link 4</a></li>
              </ul>
            </li>
            <li><a href="">Link 3</a></li>
            <li><a href="">Link 4</a></li>
          </ul>
          <div class="sub-nav-panel"></div>
        </div>
    
        <script>
          var subNav = $('.sub-nav-panel'),
              timeouts = [];
              
          function clearTimeouts(){
            for (var i = 0; i < timeouts.length; i++) {
              clearTimeout(timeouts[i]);
            }
            timeouts = [];
          }
          
          function appendTimeout(func, t){
            timeouts.push(setTimeout(func, t));
          }
          
          var Navbar =
          {
            init: function(){
              $(".expandable").on("mouseenter mouseleave", function(e){
                Navbar.inner = $(this).children('.sub-nav');
                Navbar.inner.hide();
                Navbar.animate(e);
              });
            },
            
            animate: function(e){
              clearTimeouts();
              if(e.type == "mouseenter"){
                  if (subNav.is(':animated')){
                    if(Navbar.direction == "up"){
                      appendTimeout(function(){subNav.slideDown();}, 150);
                      appendTimeout(function(){Navbar.inner.fadeIn();}, 600);
                    } else {
                      appendTimeout(function(){subNav.slideDown();}, 300);
                      appendTimeout(function(){Navbar.inner.fadeIn();}, 400);
                    }
                  } else {
                    subNav.slideDown();
                    appendTimeout(function(){Navbar.inner.fadeIn();}, 150);
                  }
                  Navbar.direction = 'down';
              } else {
                appendTimeout(function(){
                  Navbar.direction = 'up';
                  subNav.slideUp();
                }, 250);
              }
            }
          };
          
          Navbar.init();
        </script>
      </body>
    </html>
    It also uses the fade effect you wanted and regardless of what the user does with their mouse, the bar no longer gets "confused" (at least I wasn't able to make it show anything unexpected).
    Updated demo.

  4. #4
    Gre aus'm Pott gold trophysilver trophybronze trophy
    Pullo's Avatar
    Join Date
    Jun 2007
    Location
    Germany
    Posts
    5,941
    Mentioned
    215 Post(s)
    Tagged
    12 Thread(s)
    Still testing.
    Just noticed that in the above code, all of the instances of:

    Code JavaScript:
    Navbar.inner.fadeIn();

    should be replaced with:

    Code JavaScript:
    Navbar.inner.filter(':not(:animated)').fadeIn();

    I updated my last demo.


Bookmarks

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
  •