/** * @description prototype.js based hover menu * @author Peter Slagter; peter [at] procurios [dot] nl; http://twitter.com/pesla or http://techblog.procurios.nl/k/618/news/view/34556/14863/ProtoFish-advanced-hover-menu-based-on-Prototype.html * @license ProtoFish is based on the MIT license (http://protofish.procurios.nl/protofish-license). * If you want to remove this copyright notice, contact me for a crate of beer, and we'll see whats possible ;) * @parameters id: menu id , timeout: amount of milliseconds delay on mouseout , cssClass: hover class * remActive: whether or not remove active class when user enters menu , ARIA: choose to use ARIA roles and states * useShortKey: whether or not to use a shortkey to focus menu * */ var ProtoFish = Class.create({ 'initialize': function(id, timeout, cssClass, remActive, ARIA, useShortKey) { // Store function parameters this.id = id; this.timeout = timeout || '400'; this.cssClass = cssClass || 'hover'; this.remActive = remActive || false; this.ARIA = ARIA || false; this.useShortKey = useShortKey || false; // Initialize timeout queue & activeTimeout variable this.queue = []; this.activeTimeout = ''; this.menuFocus = false; this.menuCount = 0; this.isParent = false; // Store keys this.shiftDown = false; this.mDown = false; this.ctrlDown = false; this.altDown = false; // Get relevant DOM elements and store them if ($(id) && $(id).down()) { if (this.ARIA != false) { $(id).writeAttribute('role', 'menubar'); this.menuContainers = $(id).select('ul'); this.menuContainers.each( function(elem, i) { elem.writeAttribute('role', 'menu'); }); } this.listItems = $(id).select('li'); this.activeItems = $(id).select('li.active'); // Set tabindex of first menuitem this.listItems[0].down('a').setAttribute('tabindex','0'); // Start observing my menu! this.initObservers(); } }, 'initObservers': function() { this.listItems.each( function(elem) { // Mouseover and mouseout handlers for regular mouse based navigation elem.observe('mouseover', function(event, element){ this.enterMenu(element); element.addClassName(this.cssClass); }.bindAsEventListener(this, elem)); elem.observe('mouseout', function(event, element) { this.queue.push([this.leaveMenu.delay(this.timeout/1000, this), element]); }.bindAsEventListener(this, elem)); if (this.ARIA != false) { elem.down('a').writeAttribute('role', 'menuitem'); if (elem.down('ul')) { elem.down('a').writeAttribute('aria-haspopup', 'true'); } } }.bind(this)); Event.observe(document, 'keydown', function(event) { var code = event.keyCode; var allowedCodes = [9,13,27,32,37,38,39,40]; if (allowedCodes.indexOf(code) != -1) { this.keyBoardNav(event, code, allowedCodes); } if (event.keyCode == 16) { this.shiftDown = true; } else if (this.useShortKey != false) { if (event.keyCode == 77) { this.mDown = true; } if (event.keyCode == 17) { this.ctrlDown = true; } if (event.keyCode == 18) { this.altDown = true; } if (this.mDown == true && this.ctrlDown == true && this.altDown == true) { this.listItems[0].down('a').focus(); } } }.bind(this)); Event.observe(document, 'keyup', function(event) { if (event.keyCode == 16) { this.shiftDown = false; } else if (this.useShortKey != false) { if (event.keyCode == 77) { this.mDown = false; } if (event.keyCode == 17) { this.ctrlDown = false; } if (event.keyCode == 18) { this.altDown = false; } } }.bind(this)); Event.observe(document, 'click', function(event) { var element = Event.element(event); if (element != $(this.id) && !element.descendantOf(this.id) && this.menuFocus == true) { this.listItems.invoke('removeClassName', this.cssClass); this.menuFocus = false; } }.bind(this)); $$('body')[0].observe('focusin', this.handleMenuFocus.bind(this)); if (window.addEventListener) { $$('body')[0].addEventListener('focus', this.handleMenuFocus.bind(this), true); } }, 'handleMenuFocus': function(event) { var element = Event.element(event); if (element.up('#'+this.id)) { this.menuFocus = true; this.menuCount = this.listItems.indexOf(element.up('li')); this.isParent = (element.next()) ? true : false; if (this.isParent == false) { element.up().addClassName(this.cssClass); while (element.up('li')) { element.up('li').addClassName(this.cssClass); element = element.up('li'); } } else if (this.isParent == true) { element.up().removeClassName('hover'); } } else { this.listItems.invoke('removeClassName', this.cssClass); this.menuFocus = false; } }, 'keyBoardNav': function(event, code, allowedCodes) { if (this.menuFocus == true) { if (allowedCodes.indexOf(code) != 0) { event.preventDefault(); } var element = this.listItems[this.menuCount]; switch (true) { case code == Event.KEY_DOWN: if (!element.up('li')) { var nextElement = element.down('li'); } else { var nextElement = (element.next('li')) || element.up('ul').childElements().first(); if (nextElement) { element.removeClassName(this.cssClass); } } if (nextElement) { this.menuCount = this.listItems.indexOf(nextElement); nextElement.addClassName(this.cssClass); nextElement.down('a').focus(); } break; case code == Event.KEY_UP: if (!element.up('li')) { var prevElement = false; } else { var prevElement = element.previous('li') || element.up('ul').childElements().last(); element.removeClassName(this.cssClass); } if (prevElement) { this.menuCount = this.listItems.indexOf(prevElement); prevElement.addClassName(this.cssClass); prevElement.down('a').focus(); } break; case code == Event.KEY_RIGHT: if (!element.up('li')) { var rightElement = element.next('li'); if (rightElement) { element.removeClassName(this.cssClass); } } else { var rightElement = element.down('li') || false; } if (rightElement) { this.menuCount = this.listItems.indexOf(rightElement); rightElement.addClassName(this.cssClass); rightElement.down('a').focus(); } break; case code == Event.KEY_LEFT: if (!element.up('li')) { var leftElement = element.previous('li'); if (leftElement) { element.removeClassName(this.cssClass); } } else { var leftElement = element.up('li') || false; if (leftElement) { element.removeClassName(this.cssClass); } } if (leftElement) { this.menuCount = this.listItems.indexOf(leftElement); leftElement.addClassName(this.cssClass); leftElement.down('a').focus(); } break; case code == Event.KEY_TAB: if (this.shiftDown == false) { this.menuCount++; var prevElement = this.listItems[this.menuCount-1]; if (!prevElement.down('li')) { prevElement.removeClassName(this.cssClass); while (prevElement.up('li') && !prevElement.next('li')) { prevElement.up('li').removeClassName(this.cssClass); prevElement = prevElement.up('li'); } } } else if (this.shiftDown == true) { this.menuCount--; var element = this.listItems[this.menuCount]; var nextElement = this.listItems[this.menuCount+1]; nextElement.removeClassName(this.cssClass); if (element) { while (element.up('li') && element.up('li').hasClassName(this.cssClass) == false) { element.up('li').addClassName(this.cssClass); element = element.up('li'); } } } break; case code == Event.KEY_ESC: while (element.up('li')) { element.removeClassName(this.cssClass); var parentElement = element.up('li'); element = element.up('li'); } if (parentElement) { parentElement.down('a').focus(); this.menuCount = this.listItems.indexOf(element); } break; case code == 32: if (this.isParent == true) { this.parentBehavior(element); } else { var href = element.down('a').href; window.location.href = href; } break; case code == Event.KEY_RETURN: if (this.isParent == true) { this.parentBehavior(element); } break; } } }, 'parentBehavior': function(element) { var nextElement = element.down('li'); if (nextElement) { this.menuCount = this.listItems.indexOf(nextElement); nextElement.addClassName(this.cssClass); nextElement.down('a').focus(); } }, 'enterMenu': function() { while (this.queue.length) { clearTimeout(this.queue[0][0]); this.leaveMenu(this); } // If removal of .active class is set to true, do it if (this.remActive == true) { if (typeof this.activeTimeout == "number") { clearTimeout(this.activeTimeout); delete this.activeTimeout; } this.activeItems.invoke('removeClassName', 'active'); } }, 'leaveMenu': function(parent) { if (parent.queue.length) { var el = parent.queue.shift()[1]; el.removeClassName(parent.cssClass); } // If removal of .active class is set to true, restore the active class if (parent.remActive == true) { parent.activeItems.invoke('addClassName', 'active'); } } });