/* * jQuery Menu plugin * Version: 0.0.9 * * Copyright (c) 2007 Roman Weich * http://p.sohei.org * * Dual licensed under the MIT and GPL licenses * (This means that you can choose the license that best suits your project, and use it accordingly): * http://www.opensource.org/licenses/mit-license.php * http://www.gnu.org/licenses/gpl.html * * Changelog: * v 0.0.9 - 2008-01-19 */ (function($) { var menus = [], //list of all menus visibleMenus = [], //list of all visible menus activeMenu = activeItem = null, menuDIVElement = $('')[0], menuULElement = $('')[0], menuItemElement = $('
  • ')[0], arrowElement = $('')[0], $rootDiv = $('
    '), //create main menu div defaults = { // $.Menu options showDelay : 200, hideDelay : 200, hoverOpenDelay: 0, offsetTop : 0, offsetLeft : 0, minWidth: 0, onOpen: null, onClose: null, // $.MenuItem options onClick: null, arrowSrc: null, addExpando: false, // $.fn.menuFromElement options copyClassAttr: false }; $(function(){ $rootDiv.appendTo('body'); }); $.extend({ MenuCollection : function(items) { this.menus = []; this.init(items); } }); $.extend($.MenuCollection, { prototype : { init : function(items) { if ( items && items.length ) { for ( var i = 0; i < items.length; i++ ) { this.addMenu(items[i]); items[i].menuCollection = this; } } }, addMenu : function(menu) { if ( menu instanceof $.Menu ) this.menus.push(menu); menu.menuCollection = this; var self = this; $(menu.target).hover(function(){ if ( menu.visible ) return; //when there is an open menu in this collection, hide it and show the new one for ( var i = 0; i < self.menus.length; i++ ) { if ( self.menus[i].visible ) { self.menus[i].hide(); menu.show(); return; } } }, function(){}); } } }); $.extend({ Menu : function(target, items, options) { this.menuItems = []; //all direct child $.MenuItem objects this.subMenus = []; //all subMenus from this.menuItems this.visible = false; this.active = false; //this menu has hover or one of its submenus is open this.parentMenuItem = null; this.settings = $.extend({}, defaults, options); this.target = target; this.$eDIV = null; this.$eUL = null; this.timer = null; this.menuCollection = null; this.openTimer = null; this.init(); if ( items && items.constructor == Array ) this.addItems(items); } }); $.extend($.Menu, { checkMouse : function(e) { var t = e.target; //the user clicked on the target of the currenty open menu if ( visibleMenus.length && t == visibleMenus[0].target ) return; //get the last node before the #root-menu-div while ( t.parentNode && t.parentNode != $rootDiv[0] ) t = t.parentNode; //is the found node one of the visible menu elements? if ( !$(visibleMenus).filter(function(){ return this.$eDIV[0] == t }).length ) { $.Menu.closeAll(); } }, checkKey : function(e) { switch ( e.keyCode ) { case 13: //return if ( activeItem ) activeItem.click(e, activeItem.$eLI[0]); break; case 27: //ESC $.Menu.closeAll(); break; case 37: //left if ( !activeMenu ) activeMenu = visibleMenus[0]; var a = activeMenu; if ( a && a.parentMenuItem ) //select the parent menu and close the submenu { //unbind the events temporary, as we dont want the hoverout event to fire var pmi = a.parentMenuItem; pmi.$eLI.unbind('mouseout').unbind('mouseover'); a.hide(); pmi.hoverIn(true); setTimeout(function(){ //bind again..but delay it pmi.bindHover(); }); } else if ( a && a.menuCollection ) //select the previous menu in the collection { var pos, mcm = a.menuCollection.menus; if ( (pos = $.inArray(a, mcm)) > -1 ) { if ( --pos < 0 ) pos = mcm.length - 1; $.Menu.closeAll(); mcm[pos].show(); mcm[pos].setActive(); if ( mcm[pos].menuItems.length ) //select the first item mcm[pos].menuItems[0].hoverIn(true); } } break; case 38: //up if ( activeMenu ) activeMenu.selectNextItem(-1); break; case 39: //right if ( !activeMenu ) activeMenu = visibleMenus[0]; var m, a = activeMenu, asm = activeItem ? activeItem.subMenu : null; if ( a ) { if ( asm && asm.menuItems.length ) //select the submenu { asm.show(); asm.menuItems[0].hoverIn(); } else if ( (a = a.inMenuCollection()) ) //select the next menu in the collection { var pos, mcm = a.menuCollection.menus; if ( (pos = $.inArray(a, mcm)) > -1 ) { if ( ++pos >= mcm.length ) pos = 0; $.Menu.closeAll(); mcm[pos].show(); mcm[pos].setActive(); if ( mcm[pos].menuItems.length ) //select the first item mcm[pos].menuItems[0].hoverIn(true); } } } break; case 40: //down if ( !activeMenu ) { if ( visibleMenus.length && visibleMenus[0].menuItems.length ) visibleMenus[0].menuItems[0].hoverIn(); } else activeMenu.selectNextItem(); break; } if ( e.keyCode > 36 && e.keyCode < 41 ) return false; //this will prevent scrolling }, closeAll : function() { while ( visibleMenus.length ) visibleMenus[0].hide(); }, setDefaults : function(d) { $.extend(defaults, d); }, prototype : { /** * create / initialize new menu */ init : function() { var self = this; if ( !this.target ) return; else if ( this.target instanceof $.MenuItem ) { this.parentMenuItem = this.target; this.target.addSubMenu(this); this.target = this.target.$eLI; } menus.push(this); //use the dom methods instead the ones from jquery (faster) this.$eDIV = $(menuDIVElement.cloneNode(1)); this.$eUL = $(menuULElement.cloneNode(1)); this.$eDIV[0].appendChild(this.$eUL[0]); $rootDiv[0].appendChild(this.$eDIV[0]); //bind events if ( !this.parentMenuItem ) { $(this.target).click(function(e){ self.onClick(e); }).hover(function(e){ self.setActive(); if ( self.settings.hoverOpenDelay ) { self.openTimer = setTimeout(function(){ if ( !self.visible ) self.onClick(e); }, self.settings.hoverOpenDelay); } }, function(){ if ( !self.visible ) $(this).removeClass('activetarget'); if ( self.openTimer ) clearTimeout(self.openTimer); }); } else { this.$eDIV.hover(function(){ self.setActive(); }, function(){}); } }, setActive : function() { if ( !this.parentMenuItem ) $(this.target).addClass('activetarget'); else this.active = true; }, addItem : function(item) { if ( item instanceof $.MenuItem ) { if ( $.inArray(item, this.menuItems) == -1 ) { this.$eUL.append(item.$eLI); this.menuItems.push(item); item.parentMenu = this; if ( item.subMenu ) this.subMenus.push(item.subMenu); } } else { this.addItem(new $.MenuItem(item, this.settings)); } }, addItems : function(items) { for ( var i = 0; i < items.length; i++ ) { this.addItem(items[i]); } }, removeItem : function(item) { var pos = $.inArray(item, this.menuItems); if ( pos > -1 ) this.menuItems.splice(pos, 1); item.parentMenu = null; }, hide : function() { if ( !this.visible ) return; var i, pos = $.inArray(this, visibleMenus); this.$eDIV.hide(); if ( pos >= 0 ) visibleMenus.splice(pos, 1); this.visible = this.active = false; $(this.target).removeClass('activetarget'); //hide all submenus for ( i = 0; i < this.subMenus.length; i++ ) { this.subMenus[i].hide(); } //set all items inactive (e.g. remove hover class..) for ( i = 0; i < this.menuItems.length; i++ ) { if ( this.menuItems[i].active ) this.menuItems[i].setInactive(); } if ( !visibleMenus.length ) //unbind events when the last menu was closed $(document).unbind('mousedown', $.Menu.checkMouse).unbind('keydown', $.Menu.checkKey); if ( activeMenu == this ) activeMenu = null; if ( this.settings.onClose ) this.settings.onClose.call(this); }, show : function(e) { if ( this.visible ) return; var zi, pmi = this.parentMenuItem; if ( this.menuItems.length ) //show only when it has items { if ( pmi ) //set z-index { zi = parseInt(pmi.parentMenu.$eDIV.css('z-index')); this.$eDIV.css('z-index', (isNaN(zi) ? 1 : zi + 1)); } this.$eDIV.css({visibility: 'hidden', display:'block'}); //set min-width if ( this.settings.minWidth ) { if ( this.$eDIV.width() < this.settings.minWidth ) this.$eDIV.css('width', this.settings.minWidth); } this.setPosition(); this.$eDIV.css({display:'none', visibility: ''}).show(); if ( this.settings.onOpen ) this.settings.onOpen.call(this); } if ( visibleMenus.length == 0 ) $(document).bind('mousedown', $.Menu.checkMouse).bind('keydown', $.Menu.checkKey); this.visible = true; visibleMenus.push(this); }, setPosition : function() { var $t, o, posX, posY, pmo, //parent menu offset wst, //window scroll top wsl, //window scroll left ww = $(window).width(), wh = $(window).height(), pmi = this.parentMenuItem, height = this.$eDIV[0].clientHeight, width = this.$eDIV[0].clientWidth, pheight; //parent height if ( pmi ) { //position on the right side of the parent menu item o = pmi.$eLI.offset(); posX = o.left + pmi.$eLI.width(); posY = o.top; } else { //position right below the target $t = $(this.target); o = $t.offset(); posX = o.left + this.settings.offsetLeft; posY = o.top + $t.height() + this.settings.offsetTop; } //y-pos if ( $.fn.scrollTop ) { wst = $(window).scrollTop(); if ( wh < height ) //menu is bigger than the window { //position the menu at the top of the visible area posY = wst; } else if ( wh + wst < posY + height ) //outside on the bottom? { if ( pmi ) { pmo = pmi.parentMenu.$eDIV.offset(); pheight = pmi.parentMenu.$eDIV[0].clientHeight; if ( height <= pheight ) { //bottom position = parentmenu-bottom position posY = pmo.top + pheight - height; } else { //top position = parentmenu-top position posY = pmo.top; } //still outside on the bottom? if ( wh + wst < posY + height ) { //shift the menu upwards till the bottom is visible posY -= posY + height - (wh + wst); } } else { //shift the menu upwards till the bottom is visible posY -= posY + height - (wh + wst); } } } //x-pos if ( $.fn.scrollLeft ) { wsl = $(window).scrollLeft(); if ( ww + wsl < posX + width ) { if ( pmi ) { //display the menu not on the right side but on the left side posX -= pmi.$eLI.width() + width; //outside on the left now? if ( posX < wsl ) posX = wsl; } else { //shift the menu to the left until it fits posX -= posX + width - (ww + wsl); } } } //set position this.$eDIV.css({left: posX, top: posY}); }, onClick : function(e) { if ( this.visible ) { this.hide(); this.setActive(); //the class is removed in the hide() method..add it again } else { //close all open menus $.Menu.closeAll(); this.show(e); } }, addTimer : function(callback, delay) { var self = this; this.timer = setTimeout(function(){ callback.call(self); self.timer = null; }, delay); }, removeTimer : function() { if ( this.timer ) { clearTimeout(this.timer); this.timer = null; } }, selectNextItem : function(offset) { var i, pos = 0, mil = this.menuItems.length, o = offset || 1; //get current pos for ( i = 0; i < mil; i++ ) { if ( this.menuItems[i].active ) { pos = i; break; } } this.menuItems[pos].hoverOut(); do //jump over the separators { pos += o; if ( pos >= mil ) pos = 0; else if ( pos < 0 ) pos = mil - 1; } while ( this.menuItems[pos].separator ); this.menuItems[pos].hoverIn(true); }, inMenuCollection : function() { var m = this; while ( m.parentMenuItem ) m = m.parentMenuItem.parentMenu; return m.menuCollection ? m : null; }, destroy : function() //delete menu { var pos, item; this.hide(); //unbind events if ( !this.parentMenuItem ) $(this.target).unbind('click').unbind('mouseover').unbind('mouseout'); else this.$eDIV.unbind('mouseover').unbind('mouseout'); //destroy all items while ( this.menuItems.length ) { item = this.menuItems[0]; item.destroy(); delete item; } if ( (pos = $.inArray(this, menus)) > -1 ) menus.splice(pos, 1); if ( this.menuCollection ) { if ( (pos = $.inArray(this, this.menuCollection.menus)) > -1 ) this.menuCollection.menus.splice(pos, 1); } this.$eDIV.remove(); } } }); $.extend({ MenuItem : function(obj, options) { if ( typeof obj == 'string' ) obj = {src: obj}; this.src = obj.src || ''; this.url = obj.url || null; this.urlTarget = obj.target || null; this.addClass = obj.addClass || null; this.data = obj.data || null; this.$eLI = null; this.parentMenu = null; this.subMenu = null; this.settings = $.extend({}, defaults, options); this.active = false; this.enabled = true; this.separator = false; this.init(); if ( obj.subMenu ) new $.Menu(this, obj.subMenu, options); } }); $.extend($.MenuItem, { prototype : { init : function() { var i, isStr, src = this.src, self = this; this.$eLI = $(menuItemElement.cloneNode(1)); if ( this.addClass ) this.$eLI[0].setAttribute('class', this.addClass); if ( this.settings.addExpando && this.data ) this.$eLI[0].menuData = this.data; if ( src == '' ) { this.$eLI.addClass('menu-separator'); this.separator = true; } else { isStr = typeof src == 'string'; if ( isStr && this.url ) //create a link node, when we have an url src = $('' + src + ''); else if ( isStr || !src.length ) src = [src]; //go through the passed DOM-Elements (or jquery objects or text nodes.) and append them to the menus list item //this.$eLI.append(this.src) is really slow when having a lot(!!!) of items for ( i = 0; i < src.length; i++ ) { if ( typeof src[i] == 'string' ) { //we cant use createTextNode, as html entities won't be displayed correctly (eg. ©) elem = document.createElement('span'); elem.innerHTML = src[i]; this.$eLI[0].firstChild.appendChild(elem); } else this.$eLI[0].firstChild.appendChild(src[i].cloneNode(1)); } } this.$eLI.click(function(e){ self.click(e, this); }); this.bindHover(); }, click : function(e, scope) { if ( this.enabled && this.settings.onClick ) this.settings.onClick.call(scope, e, this); }, bindHover : function() { var self = this; this.$eLI.hover(function(){ self.hoverIn(); }, function(){ self.hoverOut(); }); }, hoverIn : function(noSubMenu) { this.removeTimer(); var i, pms = this.parentMenu.subMenus, pmi = this.parentMenu.menuItems, self = this; //remove the timer from the parent item, when there is one (e.g. to close the menu) if ( this.parentMenu.timer ) this.parentMenu.removeTimer(); if ( !this.enabled ) return; //deactivate all menuItems on the same level for ( i = 0; i < pmi.length; i++ ) { if ( pmi[i].active ) pmi[i].setInactive(); } this.setActive(); activeMenu = this.parentMenu; //are there open submenus on the same level? close them! for ( i = 0; i < pms.length; i++ ) { if ( pms[i].visible && pms[i] != this.subMenu && !pms[i].timer ) //close if there is no closetimer running already pms[i].addTimer(function(){ this.hide(); }, pms[i].settings.hideDelay); } if ( this.subMenu && !noSubMenu ) { //set timeout to show menu this.subMenu.addTimer(function(){ this.show(); }, this.subMenu.settings.showDelay); } }, hoverOut : function() { this.removeTimer(); if ( !this.enabled ) return; if ( !this.subMenu || !this.subMenu.visible ) this.setInactive(); }, removeTimer : function() { if ( this.subMenu ) { this.subMenu.removeTimer(); } }, setActive : function() { this.active = true; this.$eLI.addClass('active'); //set the parent menu item active too if necessary var pmi = this.parentMenu.parentMenuItem; if ( pmi && !pmi.active ) pmi.setActive(); activeItem = this; }, setInactive : function() { this.active = false; this.$eLI.removeClass('active'); if ( this == activeItem ) activeItem = null; }, enable : function() { this.$eLI.removeClass('disabled'); this.enabled = true; }, disable : function() { this.$eLI.addClass('disabled'); this.enabled = false; }, destroy : function() { this.removeTimer(); this.$eLI.remove(); //unbind events this.$eLI.unbind('mouseover').unbind('mouseout').unbind('click'); //delete submenu if ( this.subMenu ) { this.subMenu.destroy(); delete this.subMenu; } this.parentMenu.removeItem(this); }, addSubMenu : function(menu) { if ( this.subMenu ) return; this.subMenu = menu; if ( this.parentMenu && $.inArray(menu, this.parentMenu.subMenus) == -1 ) this.parentMenu.subMenus.push(menu); if ( this.settings.arrowSrc ) { var a = arrowElement.cloneNode(0); a.setAttribute('src', this.settings.arrowSrc); this.$eLI[0].firstChild.appendChild(a); } } } }); $.extend($.fn, { menuFromElement : function(options, list, bar) { var createItems = function(ul) { var menuItems = [], subItems, menuItem, lis, $li, i, subUL, submenu, target, classNames = null; lis = getAllChilds(ul, 'LI'); for ( i = 0; i < lis.length; i++ ) { subItems = []; if ( !lis[i].childNodes.length ) //empty item? add separator { menuItems.push(new $.MenuItem('', options)); continue; } if ( (subUL = getOneChild(lis[i], 'UL')) ) { subItems = createItems(subUL); //remove subUL from DOM $(subUL).remove(); } //select the target...get the elements inside the li $li = $(lis[i]); if ( $li[0].childNodes.length == 1 && $li[0].childNodes[0].nodeType == 3 ) target = $li[0].childNodes[0].nodeValue; else target = $li[0].childNodes; if ( options && options.copyClassAttr ) classNames = $li.attr('class'); //create item menuItem = new $.MenuItem({src: target, addClass: classNames}, options); menuItems.push(menuItem); //add submenu if ( subItems.length ) new $.Menu(menuItem, subItems, options); } return menuItems; }; return this.each(function() { var ul, m; //get the list element if ( list || (ul = getOneChild(this, 'UL')) ) { //if a specific list element is used, clone it, as we probably need it more than once ul = list ? $(list).clone(true)[0] : ul; menuItems = createItems(ul); if ( menuItems.length ) { m = new $.Menu(this, menuItems, options); if ( bar ) bar.addMenu(m); } $(ul).hide(); } }); }, menuBarFromUL : function(options) { return this.each(function() { var i, lis = getAllChilds(this, 'LI'); if ( lis.length ) { bar = new $.MenuCollection(); for ( i = 0; i < lis.length; i++ ) $(lis[i]).menuFromElement(options, null, bar); } }); }, menuBar : function(options, items) { return this.each(function() { if ( items && items.constructor == Array ) new $.Menu(this, items, options); else { if ( this.nodeName.toUpperCase() == 'UL' ) $(this).menuBarFromUL(options); else $(this).menuFromElement(options, items); } }); } }); //faster than using jquery var getOneChild = function(elem, name) { if ( !elem ) return null; var n = elem.firstChild; for ( ; n; n = n.nextSibling ) { if ( n.nodeType == 1 && n.nodeName.toUpperCase() == name ) return n; } return null; }; //faster than using jquery var getAllChilds = function(elem, name) { if ( !elem ) return []; var r = [], n = elem.firstChild; for ( ; n; n = n.nextSibling ) { if ( n.nodeType == 1 && n.nodeName.toUpperCase() == name ) r[r.length] = n; } return r; }; })(jQuery);