Monday, May 16, 2011

Simplifying a javascript jump menu



The jump menu is a reasonably common UI design pattern on the web. It is used by big guns such as Facebook and Twitter, and has been dissected and reproduced many times over.

When I set about implementing my own jump menu I found opening and closing the menu when it is clicked on was rather trivial.

$('.jump-menu').click(function(e){
toggleMenu();
}

The difficulty came in closing the menu when there is a click outside the jump menu. Googling for answers, I found the scenario of closing a menu on clicking outside it to be a well trodden path. Stackoverflow had plenty of examples and possible solutions such as here and here. I even found a plugin created by Ben Alman that extended Jquery to provide a custom clickoutside event. Many of the proposed solutions were fragile, and I was slow to add a new plugin to my web application - as good and all as the implementation appeared. I really wanted to understand the problem before opting to take a proposed solution off the shelf.



The state diagram above illustrates the four possible actions for the two menu states of open and closed. We can see that one of the four actions results in no change of state, while another two result in the same change of state. Clearly this diagram could be simplified, and along with it my perception of the problem.

"Make things as simple as possible, but not simpler" - Albert Einstein




Given this simplified state transition diagram, there was no need for an event register, a global array, comparing with event.target, a handler that would be called for each and every document click, or the jquery plugin. All that I needed was something like the following:


isMenuOpen = function(menu) {
return menu.hasClass('ui-active-state');
};
openMenu = function(event) {
var documentClicks, menu, self;
menu = $(event.target).parents('.jump-menu');
if (isMenuOpen(menu)) {
menu.removeClass('ui-default-state');
menu.addClass('ui-active-state');
self = this;
documentClicks = 0;
$(document).bind("click", __bind(function(e) {
documentClicks++;
if (documentClicks === 2) {
$(this).unbind(e);
this.closeMenu(menu);
}
return this;
}, this));
}
return this;
};
closeMenu = function(menu) {
menu = $(this.el).find('.jump-menu');
menu.removeClass('ui-active-state');
menu.addClass('ui-default-state');
};


There is always another way to look at a problem.
Happy Coding!

Credits:
http://davidwalsh.name/twitter-dropdown-jquery
http://stackoverflow.com/questions/152975/how-to-detect-a-click-outside-an-element
http://stackoverflow.com/questions/2868582/click-outside-menu-to-close-in-jquery
http://www.filamentgroup.com/lab/developer_your_own_jquery_themeroller_ready_components/

2 comments:

  1. Your code doesn't seem to work? Why do you set self to = this, when you don't ever use the self variable again? What am I missing?

    ReplyDelete
    Replies
    1. Oh, it has been a long time since I wrote this so I'm not sure... Looking over it, it might be the case that self is not required as this is passed into the .bind method as the last argument, so I'm assuming that is a means of supplying the context the method is called in. You could try replace: $(this).unbind(e); with $(document).unbind(e);

      By the looks of the "return this" the above code is some kind of pseudo code, I had originally written in coffeescript. Sorry if it is causing you a headache, and hope the change I suggested works for you. If you have understood the article, you should be able to get it working.

      Delete