5 * Copyright (c) 2007 Roman Weich
8 * Dual licensed under the MIT and GPL licenses
9 * (This means that you can choose the license that best suits your project, and use it accordingly):
10 * http://www.opensource.org/licenses/mit-license.php
11 * http://www.gnu.org/licenses/gpl.html
14 * v 0.0.9 - 2008-01-19
19 var menus
= [], //list of all menus
20 visibleMenus
= [], //list of all visible menus
21 activeMenu
= activeItem
= null,
22 menuDIVElement
= $('<div class="menu-div outerbox" style="position:absolute;top:0;left:0;display:none;"><div class="shadowbox1"></div><div class="shadowbox2"></div><div class="shadowbox3"></div></div>')[0],
23 menuULElement
= $('<ul class="menu-ul innerbox"></ul>')[0],
24 menuItemElement
= $('<li style="position:relative;"><div class="menu-item"></div></li>')[0],
25 arrowElement
= $('<img class="menu-item-arrow" />')[0],
26 $rootDiv
= $('<div id="root-menu-div" style="position:absolute;top:0;left:0;"></div>'), //create main menu div
43 // $.fn.menuFromElement options
48 $rootDiv
.appendTo('body');
52 MenuCollection : function(items
) {
59 $.extend($.MenuCollection
, {
61 init : function(items
)
63 if ( items
&& items
.length
)
65 for ( var i
= 0; i
< items
.length
; i
++ )
67 this.addMenu(items
[i
]);
68 items
[i
].menuCollection
= this;
72 addMenu : function(menu
)
74 if ( menu
instanceof $.Menu
)
75 this.menus
.push(menu
);
77 menu
.menuCollection
= this;
80 $(menu
.target
).hover(function(){
84 //when there is an open menu in this collection, hide it and show the new one
85 for ( var i
= 0; i
< self
.menus
.length
; i
++ )
87 if ( self
.menus
[i
].visible
)
101 Menu : function(target
, items
, options
) {
102 this.menuItems
= []; //all direct child $.MenuItem objects
103 this.subMenus
= []; //all subMenus from this.menuItems
104 this.visible
= false;
105 this.active
= false; //this menu has hover or one of its submenus is open
106 this.parentMenuItem
= null;
107 this.settings
= $.extend({}, defaults
, options
);
108 this.target
= target
;
112 this.menuCollection
= null;
113 this.openTimer
= null;
116 if ( items
&& items
.constructor == Array
)
117 this.addItems(items
);
122 checkMouse : function(e
)
126 //the user clicked on the target of the currenty open menu
127 if ( visibleMenus
.length
&& t
== visibleMenus
[0].target
)
130 //get the last node before the #root-menu-div
131 while ( t
.parentNode
&& t
.parentNode
!= $rootDiv
[0] )
134 //is the found node one of the visible menu elements?
135 if ( !$(visibleMenus
).filter(function(){ return this.$eDIV
[0] == t
}).length
)
140 checkKey : function(e
)
146 activeItem
.click(e
, activeItem
.$eLI
[0]);
153 activeMenu
= visibleMenus
[0];
155 if ( a
&& a
.parentMenuItem
) //select the parent menu and close the submenu
157 //unbind the events temporary, as we dont want the hoverout event to fire
158 var pmi
= a
.parentMenuItem
;
159 pmi
.$eLI
.unbind('mouseout').unbind('mouseover');
162 setTimeout(function(){ //bind again..but delay it
166 else if ( a
&& a
.menuCollection
) //select the previous menu in the collection
169 mcm
= a
.menuCollection
.menus
;
170 if ( (pos
= $.inArray(a
, mcm
)) > -1 )
173 pos
= mcm
.length
- 1;
176 mcm
[pos
].setActive();
177 if ( mcm
[pos
].menuItems
.length
) //select the first item
178 mcm
[pos
].menuItems
[0].hoverIn(true);
184 activeMenu
.selectNextItem(-1);
188 activeMenu
= visibleMenus
[0];
191 asm
= activeItem
? activeItem
.subMenu
: null;
194 if ( asm
&& asm
.menuItems
.length
) //select the submenu
197 asm
.menuItems
[0].hoverIn();
199 else if ( (a
= a
.inMenuCollection()) ) //select the next menu in the collection
202 mcm
= a
.menuCollection
.menus
;
203 if ( (pos
= $.inArray(a
, mcm
)) > -1 )
205 if ( ++pos
>= mcm
.length
)
209 mcm
[pos
].setActive();
210 if ( mcm
[pos
].menuItems
.length
) //select the first item
211 mcm
[pos
].menuItems
[0].hoverIn(true);
219 if ( visibleMenus
.length
&& visibleMenus
[0].menuItems
.length
)
220 visibleMenus
[0].menuItems
[0].hoverIn();
223 activeMenu
.selectNextItem();
226 if ( e
.keyCode
> 36 && e
.keyCode
< 41 )
227 return false; //this will prevent scrolling
229 closeAll : function()
231 while ( visibleMenus
.length
)
232 visibleMenus
[0].hide();
234 setDefaults : function(d
)
236 $.extend(defaults
, d
);
240 * create / initialize new menu
247 else if ( this.target
instanceof $.MenuItem
)
249 this.parentMenuItem
= this.target
;
250 this.target
.addSubMenu(this);
251 this.target
= this.target
.$eLI
;
256 //use the dom methods instead the ones from jquery (faster)
257 this.$eDIV
= $(menuDIVElement
.cloneNode(1));
258 this.$eUL
= $(menuULElement
.cloneNode(1));
259 this.$eDIV
[0].appendChild(this.$eUL
[0]);
260 $rootDiv
[0].appendChild(this.$eDIV
[0]);
263 if ( !this.parentMenuItem
)
265 $(this.target
).click(function(e
){
267 }).hover(function(e
){
270 if ( self
.settings
.hoverOpenDelay
)
272 self
.openTimer
= setTimeout(function(){
275 }, self
.settings
.hoverOpenDelay
);
279 $(this).removeClass('activetarget');
281 if ( self
.openTimer
)
282 clearTimeout(self
.openTimer
);
287 this.$eDIV
.hover(function(){
292 setActive : function()
294 if ( !this.parentMenuItem
)
295 $(this.target
).addClass('activetarget');
299 addItem : function(item
)
301 if ( item
instanceof $.MenuItem
)
303 if ( $.inArray(item
, this.menuItems
) == -1 )
305 this.$eUL
.append(item
.$eLI
);
306 this.menuItems
.push(item
);
307 item
.parentMenu
= this;
309 this.subMenus
.push(item
.subMenu
);
314 this.addItem(new $.MenuItem(item
, this.settings
));
317 addItems : function(items
)
319 for ( var i
= 0; i
< items
.length
; i
++ )
321 this.addItem(items
[i
]);
324 removeItem : function(item
)
326 var pos
= $.inArray(item
, this.menuItems
);
328 this.menuItems
.splice(pos
, 1);
329 item
.parentMenu
= null;
337 pos
= $.inArray(this, visibleMenus
);
342 visibleMenus
.splice(pos
, 1);
343 this.visible
= this.active
= false;
345 $(this.target
).removeClass('activetarget');
348 for ( i
= 0; i
< this.subMenus
.length
; i
++ )
350 this.subMenus
[i
].hide();
353 //set all items inactive (e.g. remove hover class..)
354 for ( i
= 0; i
< this.menuItems
.length
; i
++ )
356 if ( this.menuItems
[i
].active
)
357 this.menuItems
[i
].setInactive();
360 if ( !visibleMenus
.length
) //unbind events when the last menu was closed
361 $(document
).unbind('mousedown', $.Menu
.checkMouse
).unbind('keydown', $.Menu
.checkKey
);
363 if ( activeMenu
== this )
366 if ( this.settings
.onClose
)
367 this.settings
.onClose
.call(this);
375 pmi
= this.parentMenuItem
;
377 if ( this.menuItems
.length
) //show only when it has items
379 if ( pmi
) //set z-index
381 zi
= parseInt(pmi
.parentMenu
.$eDIV
.css('z-index'));
382 this.$eDIV
.css('z-index', (isNaN(zi
) ? 1 : zi
+ 1));
384 this.$eDIV
.css({visibility
: 'hidden', display
:'block'});
387 if ( this.settings
.minWidth
)
389 if ( this.$eDIV
.width() < this.settings
.minWidth
)
390 this.$eDIV
.css('width', this.settings
.minWidth
);
394 this.$eDIV
.css({display
:'none', visibility
: ''}).show();
396 if ( this.settings
.onOpen
)
397 this.settings
.onOpen
.call(this);
399 if ( visibleMenus
.length
== 0 )
400 $(document
).bind('mousedown', $.Menu
.checkMouse
).bind('keydown', $.Menu
.checkKey
);
403 visibleMenus
.push(this);
405 setPosition : function()
407 var $t
, o
, posX
, posY
,
408 pmo
, //parent menu offset
409 wst
, //window scroll top
410 wsl
, //window scroll left
411 ww
= $(window
).width(),
412 wh
= $(window
).height(),
413 pmi
= this.parentMenuItem
,
414 height
= this.$eDIV
[0].clientHeight
,
415 width
= this.$eDIV
[0].clientWidth
,
416 pheight
; //parent height
420 //position on the right side of the parent menu item
421 o
= pmi
.$eLI
.offset();
422 posX
= o
.left
+ pmi
.$eLI
.width();
427 //position right below the target
430 posX
= o
.left
+ this.settings
.offsetLeft
;
431 posY
= o
.top
+ $t
.height() + this.settings
.offsetTop
;
435 if ( $.fn
.scrollTop
)
437 wst
= $(window
).scrollTop();
438 if ( wh
< height
) //menu is bigger than the window
440 //position the menu at the top of the visible area
443 else if ( wh
+ wst
< posY
+ height
) //outside on the bottom?
447 pmo
= pmi
.parentMenu
.$eDIV
.offset();
448 pheight
= pmi
.parentMenu
.$eDIV
[0].clientHeight
;
449 if ( height
<= pheight
)
451 //bottom position = parentmenu-bottom position
452 posY
= pmo
.top
+ pheight
- height
;
456 //top position = parentmenu-top position
459 //still outside on the bottom?
460 if ( wh
+ wst
< posY
+ height
)
462 //shift the menu upwards till the bottom is visible
463 posY
-= posY
+ height
- (wh
+ wst
);
468 //shift the menu upwards till the bottom is visible
469 posY
-= posY
+ height
- (wh
+ wst
);
474 if ( $.fn
.scrollLeft
)
476 wsl
= $(window
).scrollLeft();
477 if ( ww
+ wsl
< posX
+ width
)
481 //display the menu not on the right side but on the left side
482 posX
-= pmi
.$eLI
.width() + width
;
483 //outside on the left now?
489 //shift the menu to the left until it fits
490 posX
-= posX
+ width
- (ww
+ wsl
);
496 this.$eDIV
.css({left
: posX
, top
: posY
});
498 onClick : function(e
)
503 this.setActive(); //the class is removed in the hide() method..add it again
507 //close all open menus
512 addTimer : function(callback
, delay
)
515 this.timer
= setTimeout(function(){
520 removeTimer : function()
524 clearTimeout(this.timer
);
528 selectNextItem : function(offset
)
531 mil
= this.menuItems
.length
,
535 for ( i
= 0; i
< mil
; i
++ )
537 if ( this.menuItems
[i
].active
)
543 this.menuItems
[pos
].hoverOut();
545 do //jump over the separators
552 } while ( this.menuItems
[pos
].separator
);
553 this.menuItems
[pos
].hoverIn(true);
555 inMenuCollection : function()
558 while ( m
.parentMenuItem
)
559 m
= m
.parentMenuItem
.parentMenu
;
560 return m
.menuCollection
? m
: null;
562 destroy : function() //delete menu
569 if ( !this.parentMenuItem
)
570 $(this.target
).unbind('click').unbind('mouseover').unbind('mouseout');
572 this.$eDIV
.unbind('mouseover').unbind('mouseout');
575 while ( this.menuItems
.length
)
577 item
= this.menuItems
[0];
582 if ( (pos
= $.inArray(this, menus
)) > -1 )
583 menus
.splice(pos
, 1);
585 if ( this.menuCollection
)
587 if ( (pos
= $.inArray(this, this.menuCollection
.menus
)) > -1 )
588 this.menuCollection
.menus
.splice(pos
, 1);
597 MenuItem : function(obj
, options
)
599 if ( typeof obj
== 'string' )
602 this.src
= obj
.src
|| '';
603 this.url
= obj
.url
|| null;
604 this.urlTarget
= obj
.target
|| null;
605 this.addClass
= obj
.addClass
|| null;
606 this.data
= obj
.data
|| null;
609 this.parentMenu
= null;
611 this.settings
= $.extend({}, defaults
, options
);
614 this.separator
= false;
619 new $.Menu(this, obj
.subMenu
, options
);
623 $.extend($.MenuItem
, {
631 this.$eLI
= $(menuItemElement
.cloneNode(1));
634 this.$eLI
[0].setAttribute('class', this.addClass
);
636 if ( this.settings
.addExpando
&& this.data
)
637 this.$eLI
[0].menuData
= this.data
;
641 this.$eLI
.addClass('menu-separator');
642 this.separator
= true;
646 isStr
= typeof src
== 'string';
647 if ( isStr
&& this.url
) //create a link node, when we have an url
648 src
= $('<a href="' + this.url
+ '"' + (this.urlTarget
? 'target="' + this.urlTarget
+ '"' : '') + '>' + src
+ '</a>');
649 else if ( isStr
|| !src
.length
)
651 //go through the passed DOM-Elements (or jquery objects or text nodes.) and append them to the menus list item
652 //this.$eLI.append(this.src) is really slow when having a lot(!!!) of items
653 for ( i
= 0; i
< src
.length
; i
++ )
655 if ( typeof src
[i
] == 'string' )
657 //we cant use createTextNode, as html entities won't be displayed correctly (eg. ©)
658 elem
= document
.createElement('span');
659 elem
.innerHTML
= src
[i
];
660 this.$eLI
[0].firstChild
.appendChild(elem
);
663 this.$eLI
[0].firstChild
.appendChild(src
[i
].cloneNode(1));
667 this.$eLI
.click(function(e
){
672 click : function(e
, scope
)
674 if ( this.enabled
&& this.settings
.onClick
)
675 this.settings
.onClick
.call(scope
, e
, this);
677 bindHover : function()
680 this.$eLI
.hover(function(){
686 hoverIn : function(noSubMenu
)
691 pms
= this.parentMenu
.subMenus
,
692 pmi
= this.parentMenu
.menuItems
,
695 //remove the timer from the parent item, when there is one (e.g. to close the menu)
696 if ( this.parentMenu
.timer
)
697 this.parentMenu
.removeTimer();
702 //deactivate all menuItems on the same level
703 for ( i
= 0; i
< pmi
.length
; i
++ )
706 pmi
[i
].setInactive();
710 activeMenu
= this.parentMenu
;
712 //are there open submenus on the same level? close them!
713 for ( i
= 0; i
< pms
.length
; i
++ )
715 if ( pms
[i
].visible
&& pms
[i
] != this.subMenu
&& !pms
[i
].timer
) //close if there is no closetimer running already
716 pms
[i
].addTimer(function(){
718 }, pms
[i
].settings
.hideDelay
);
721 if ( this.subMenu
&& !noSubMenu
)
723 //set timeout to show menu
724 this.subMenu
.addTimer(function(){
726 }, this.subMenu
.settings
.showDelay
);
729 hoverOut : function()
736 if ( !this.subMenu
|| !this.subMenu
.visible
)
739 removeTimer : function()
743 this.subMenu
.removeTimer();
746 setActive : function()
749 this.$eLI
.addClass('active');
751 //set the parent menu item active too if necessary
752 var pmi
= this.parentMenu
.parentMenuItem
;
753 if ( pmi
&& !pmi
.active
)
758 setInactive : function()
761 this.$eLI
.removeClass('active');
762 if ( this == activeItem
)
767 this.$eLI
.removeClass('disabled');
772 this.$eLI
.addClass('disabled');
773 this.enabled
= false;
782 this.$eLI
.unbind('mouseover').unbind('mouseout').unbind('click');
786 this.subMenu
.destroy();
789 this.parentMenu
.removeItem(this);
791 addSubMenu : function(menu
)
796 if ( this.parentMenu
&& $.inArray(menu
, this.parentMenu
.subMenus
) == -1 )
797 this.parentMenu
.subMenus
.push(menu
);
798 if ( this.settings
.arrowSrc
)
800 var a
= arrowElement
.cloneNode(0);
801 a
.setAttribute('src', this.settings
.arrowSrc
);
802 this.$eLI
[0].firstChild
.appendChild(a
);
810 menuFromElement : function(options
, list
, bar
)
812 var createItems = function(ul
)
817 lis
, $li
, i
, subUL
, submenu
, target
,
820 lis
= getAllChilds(ul
, 'LI');
821 for ( i
= 0; i
< lis
.length
; i
++ )
825 if ( !lis
[i
].childNodes
.length
) //empty item? add separator
827 menuItems
.push(new $.MenuItem('', options
));
831 if ( (subUL
= getOneChild(lis
[i
], 'UL')) )
833 subItems
= createItems(subUL
);
834 //remove subUL from DOM
838 //select the target...get the elements inside the li
840 if ( $li
[0].childNodes
.length
== 1 && $li
[0].childNodes
[0].nodeType
== 3 )
841 target
= $li
[0].childNodes
[0].nodeValue
;
843 target
= $li
[0].childNodes
;
845 if ( options
&& options
.copyClassAttr
)
846 classNames
= $li
.attr('class');
849 menuItem
= new $.MenuItem({src
: target
, addClass
: classNames
}, options
);
850 menuItems
.push(menuItem
);
852 if ( subItems
.length
)
853 new $.Menu(menuItem
, subItems
, options
);
858 return this.each(function()
861 //get the list element
862 if ( list
|| (ul
= getOneChild(this, 'UL')) )
864 //if a specific list element is used, clone it, as we probably need it more than once
865 ul
= list
? $(list
).clone(true)[0] : ul
;
866 menuItems
= createItems(ul
);
867 if ( menuItems
.length
)
869 m
= new $.Menu(this, menuItems
, options
);
877 menuBarFromUL : function(options
)
879 return this.each(function()
882 lis
= getAllChilds(this, 'LI');
886 bar
= new $.MenuCollection();
887 for ( i
= 0; i
< lis
.length
; i
++ )
888 $(lis
[i
]).menuFromElement(options
, null, bar
);
892 menuBar : function(options
, items
)
894 return this.each(function()
896 if ( items
&& items
.constructor == Array
)
897 new $.Menu(this, items
, options
);
900 if ( this.nodeName
.toUpperCase() == 'UL' )
901 $(this).menuBarFromUL(options
);
903 $(this).menuFromElement(options
, items
);
909 //faster than using jquery
910 var getOneChild = function(elem
, name
)
915 var n
= elem
.firstChild
;
916 for ( ; n
; n
= n
.nextSibling
)
918 if ( n
.nodeType
== 1 && n
.nodeName
.toUpperCase() == name
)
923 //faster than using jquery
924 var getAllChilds = function(elem
, name
)
931 for ( ; n
; n
= n
.nextSibling
)
933 if ( n
.nodeType
== 1 && n
.nodeName
.toUpperCase() == name
)