Weird behavior with jQuery .hover() and $(window).resize()

Viewed 60

What I want to do:

I have a navigation bar on my website. Some navigation links have sub-menus associated with them. I want to be able to hover over the navigation links that have sub-menus and have the sub-menu appear.

But, I want the sub-menu to appear differently based on the window size:

  • If the window is greater than or equal to 960px wide, my navigation bar is horizontal. I want the sub-menu to appear as a dropdown above the other content on the page.

  • If the window is less than 960px wide, my navigation bar is vertical. I want the sub-menu to appear as an accordion, pushing the other navigation links down.

I also want my script to execute when the page is done loading and when I resize the window.

My script:

function navMenu() {
  if ($(window).width() >= 960) {
    $('.menu-item-has-children').hover(function() {
      $(this).children(".sub-menu").css('display', 'block');
    }, function() {
      $(this).children(".sub-menu").css('display', 'none');
    });
  } else {
    $('.menu-item-has-children').hover(function() {
        $(this).children(".sub-menu").slideToggle(500);
      }, function() {
        $(this).children(".sub-menu").slideUp(500);
    });
  }
}
$(document).ready(function() {
   navMenu();
});
$(window).resize(function() {
   navMenu();
});

What happens:

The above script works fine when $(document).ready() fires.

The problem lies here: If I resize the window after it has loaded, the script creates multiple mouseover and mouseout event listeners. One hover over a navigation link causes the sub-menu to bounce up and down repeatedly.

You can see what I mean here: https://codepen.io/ben393/pen/qLrMmX.

Some things I've tried (and have not worked so far):

  • making .hover() a function of ($(window).width() >= 960)

  • making $(document).ready() a function of $(window).resize()

Any ideas? Thanks!

2 Answers

As you mentioned, the issue is that you keep listening to hover events on every single resize. An easy way to solve this is to unbind the corresponding DOM events every time, with:

$('.menu-item-has-children').off('mouseenter mouseleave');

Note that hover() binds both these events in the background. Also note that this will remove all custom handlers for these events. You can also store the handler functions into variables and unbind only them if you want to be more specific.

Demo (click "Expand Snippet" to test it full-screen):

function navMenu() {
  $('.menu-item-has-children').off('mouseenter mouseleave');
  if ($(window).width() >= 960) {
    $('.menu-item-has-children').hover(function() {
      $(this).children(".sub-menu").css('display', 'block');
    }, function() {
      $(this).children(".sub-menu").css('display', 'none');
    });
  } else {
    $('.menu-item-has-children').hover(function() {
      $(this).children(".sub-menu").slideToggle(500);
    }, function() {
      $(this).children(".sub-menu").slideUp(500);
    });
  }
}
$(document).ready(function() {
  navMenu();
});
$(window).resize(function() {
  navMenu();
});
ul {
  list-style-type: none;
  margin: 0;
  padding: 0;
  font-weight: bold;
}

ul li {
  display: inline-block;
  padding: 10px;
  background-color: grey;
}

ul.sub-menu {
  display: none;
  font-weight: normal;
  position: absolute;
}

ul.sub-menu li {
  display: block;
}

@media only screen and (max-width: 959px) {
  ul li {
    display: block;
    width: 100px;
  }
  ul.sub-menu {
    position: relative;
  }
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<ul id="main_header_menu">
  <li class="menu-item-has-children">Main item 1
    <ul class="sub-menu">
      <li>sub item 1</li>
      <li>sub item 2</li>
      <li>sub item 3</li>
    </ul>
  </li>
  <li>Main item 2</li>
  <li class="menu-item-has-children">Main item 3
    <ul class="sub-menu">
      <li>sub item 1</li>
      <li>sub item 2</li>
      <li>sub item 3</li>
    </ul>
  </li>
</ul>

If you change your resize call to this then it works:

$(document).resize(function() { navMenu(); });

Also, you should wrap all your JS inside the $(document).ready()

Related