Sunday, February 17, 2008

Animated Horizontal MenuBar

The animated horizontal menubar isn't the most practical or useful menubar around, but it does allow us to explore events with complex workflows. We'll also explore how we fire events programatically.

Here's our horizontal menubar --



The menubar is based on the YUI MenuBar ( in fact, I've kept some of the original comments from the YUI examples ). There are many examples and it's well documented. So, it's a good choice to add to it's behavior. The workflow that we want is simple --

Mouseover a menu item and the "cap" moves toward the item. Once the cap arrives, the menu item changes background and the submenu appears. Mouse to another menu item and the current submenu hides. The new menu item behaves as before.

Having users explore by mouseover is easier than having users click on each item. Animation allows us to "control the pace" at which our users explore.

Our workflow involves a sequence of events --
  1. The container containing the menubar listens for the mouseover event
  2. When a mouseover event is fired, the "cap" moves towards the menu item
  3. With the animation, we subscribe and handle the onStart and onComplete events for the animated object
  4. When onComplete occurs, we fire a custom event to the menubar which then displays the submenu
Note that when we use the term "listen" we mean that we listen for non-custom or "normal" events. An example of these are mouseover, mouseout, click, etc. They're fired when the user interacts with our application. Essentially, these are the basic events that you deal with in an web environment.

When we use the term "subscribe" we mean custom events or events that are fired programmatically. We're using three of these --
  • Both onStart and onComplete are found in YAHOO.util.Motion, the object that moves
  • A "showmenu" custom event attached to the MenuBar, fired once the animation completes
Here's the mouseover event handler, moveThing. It's invoked when you mouseover the container containing the menubar --
 var moveThing = function(e) {
clearID>-1?clearInterval(clearID):"";
var target = YAHOO.util.Event.getTarget(e);
var id = target.id;
var menuBar = (id == "Com" || id =="Shop" || id == "Ent" || id == "Inf");
if (menuBar) {
(doingMenu && doingMenu !== target)?hideMenu(doingMenu):"";
doingMenu = target;
oMenuBar.myCustomEvent.unsubscribe(handleCustomEvent, oMenuBar);
oMenuBar.cfg.setProperty("autosubmenudisplay", false);
var x = YAHOO.util.Dom.getX(target);
var y = YAHOO.util.Dom.getY(target)-4;
!w?w = parseInt(YAHOO.util.Dom.getStyle(target, "width")) + parseInt(YAHOO.util.Dom.getStyle(target, "padding-right")) + parseInt(YAHOO.util.Dom.getStyle(target, "padding-left")) + "px":"";
YAHOO.util.Dom.setStyle(thingToMove, "width", w);

var attributes = { points: { to: [x, y] }};
var anim = new YAHOO.util.Motion(thingToMove.id, attributes, 1, YAHOO.util.Easing.easeOut);

var handleOnStart = function(e) {
var el = anim.getEl();
YAHOO.util.Dom.setStyle(el, "display", "block");
}

// BEGIN :: Create/subscribe custom event
var handleCustomEvent = function(type, args, me) {
if (args.length>0) {
var current = parseInt(args[0].getAttribute("index"));
var item = me.getItem(current);
item.cfg.getProperty("submenu").show();
}
}
oMenuBar.myCustomEvent.subscribe(handleCustomEvent, oMenuBar);
// END :: Create/subscribe custom event

var handleOnComplete = function(e) {
anim.onStart.unsubscribe(handleOnStart);
anim.onComplete.unsubscribe(handleOnComplete);
anim=null;
if (doingMenu === target) {
var parentNode = doingMenu.parentNode;
parentNode.className.indexOf(" showing-menu")==-1?parentNode.className += " showing-menu":"";
oMenuBar.myCustomEvent.fire(target);
}
}

anim.onStart.subscribe(handleOnStart);
anim.onComplete.subscribe(handleOnComplete);
anim.animate();
} else if (id == "Container") {
doingMenu?hideMenu(doingMenu, 1000):"";
}
}

Everytime you mouseover the container, we create a new animation object. We do this so that the animation doesn't stop when we consecutively mouseover other menubar items. So, we intentionally don't reuse the YAHOO.util.Motion object.

To prevent memory leaks and to "clean up" our event handling we unsubscribe our events in the onComplete handler --
var handleOnComplete = function(e) {
anim.onStart.unsubscribe(handleOnStart);
anim.onComplete.unsubscribe(handleOnComplete);
anim=null;
if (doingMenu === target) {
var parentNode = doingMenu.parentNode;
parentNode.className.indexOf(" showing-menu")==-1?parentNode.className += " showing-menu":"";
oMenuBar.myCustomEvent.fire(target);
}
}

The onComplete handler changes the background image of the MenuItem ( by appending the class "showing-menu" ) and fires the "showmenu" custom event. This only occurs if the menu item is the same one that was the target of the mouseover in which case the menubar object subscribes and handles the event by displaying the submenu --
 var handleCustomEvent = function(type, args, me) {
if (args.length>0) {
var current = parseInt(args[0].getAttribute("index"));
var item = me.getItem(current);
item.cfg.getProperty("submenu").show();
}
}

The attribute "index" is a custom attribute added to each link in the HTML. It's used to get the menu item. Every YUI MenuItem ( or for that matter every Menu ) has a configuration property ( cfg ). For menu items, we've set the "submenu" property and so, in the event handler, we'll call "show()" to explicitly display that.

There's also an equivalent hide() that we call when we need to hide the submenu. Once again, we see symmetry of operations --
 var hideMenu = function(menu,t) {
// t is the time that it waits to hide it
var current = parseInt(menu.getAttribute("index"));
var item = oMenuBar.getItem(current);
var parentNode = menu.parentNode;

var hideIt = function() {
parentNode.className = parentNode.className.indexOf(" showing-menu")>-1?parentNode.className.replace(" showing-menu",""):parentNode.className;
item.cfg?item.cfg.getProperty("submenu").hide():"";
}
clearID=(arguments.length==2)?setTimeout(hideIt,t):hideIt();
}

You can find the example here. Feel free to view, use and improve it.
Have fun!

1 comment:

Rahul Bairathi said...

Can you please tell me how to use you scripts.....