Upgrade to ExtJS 3.1.0 - Released 12/16/2009
[extjs.git] / src / widgets / menu / Menu.js
1 /*!
2  * Ext JS Library 3.1.0
3  * Copyright(c) 2006-2009 Ext JS, LLC
4  * licensing@extjs.com
5  * http://www.extjs.com/license
6  */
7 /**
8  * @class Ext.layout.MenuLayout
9  * @extends Ext.layout.ContainerLayout
10  * <p>Layout manager used by {@link Ext.menu.Menu}. Generally this class should not need to be used directly.</p>
11  */
12  Ext.layout.MenuLayout = Ext.extend(Ext.layout.ContainerLayout, {
13     monitorResize : true,
14
15     setContainer : function(ct){
16         this.monitorResize = !ct.floating;
17         // This event is only fired by the menu in IE, used so we don't couple
18         // the menu with the layout.
19         ct.on('autosize', this.doAutoSize, this);
20         Ext.layout.MenuLayout.superclass.setContainer.call(this, ct);
21     },
22
23     renderItem : function(c, position, target){
24         if (!this.itemTpl) {
25             this.itemTpl = Ext.layout.MenuLayout.prototype.itemTpl = new Ext.XTemplate(
26                 '<li id="{itemId}" class="{itemCls}">',
27                     '<tpl if="needsIcon">',
28                         '<img src="{icon}" class="{iconCls}"/>',
29                     '</tpl>',
30                 '</li>'
31             );
32         }
33
34         if(c && !c.rendered){
35             if(Ext.isNumber(position)){
36                 position = target.dom.childNodes[position];
37             }
38             var a = this.getItemArgs(c);
39
40 //          The Component's positionEl is the <li> it is rendered into
41             c.render(c.positionEl = position ?
42                 this.itemTpl.insertBefore(position, a, true) :
43                 this.itemTpl.append(target, a, true));
44
45 //          Link the containing <li> to the item.
46             c.positionEl.menuItemId = c.getItemId();
47
48 //          If rendering a regular Component, and it needs an icon,
49 //          move the Component rightwards.
50             if (!a.isMenuItem && a.needsIcon) {
51                 c.positionEl.addClass('x-menu-list-item-indent');
52             }
53             this.configureItem(c, position);
54         }else if(c && !this.isValidParent(c, target)){
55             if(Ext.isNumber(position)){
56                 position = target.dom.childNodes[position];
57             }
58             target.dom.insertBefore(c.getActionEl().dom, position || null);
59         }
60     },
61
62     getItemArgs : function(c) {
63         var isMenuItem = c instanceof Ext.menu.Item;
64         return {
65             isMenuItem: isMenuItem,
66             needsIcon: !isMenuItem && (c.icon || c.iconCls),
67             icon: c.icon || Ext.BLANK_IMAGE_URL,
68             iconCls: 'x-menu-item-icon ' + (c.iconCls || ''),
69             itemId: 'x-menu-el-' + c.id,
70             itemCls: 'x-menu-list-item '
71         };
72     },
73
74 //  Valid if the Component is in a <li> which is part of our target <ul>
75     isValidParent : function(c, target) {
76         return c.el.up('li.x-menu-list-item', 5).dom.parentNode === (target.dom || target);
77     },
78
79     onLayout : function(ct, target){
80         this.renderAll(ct, target);
81         this.doAutoSize();
82     },
83
84     doAutoSize : function(){
85         var ct = this.container, w = ct.width;
86         if(ct.floating){
87             if(w){
88                 ct.setWidth(w);
89             }else if(Ext.isIE){
90                 ct.setWidth(Ext.isStrict && (Ext.isIE7 || Ext.isIE8) ? 'auto' : ct.minWidth);
91                 var el = ct.getEl(), t = el.dom.offsetWidth; // force recalc
92                 ct.setWidth(ct.getLayoutTarget().getWidth() + el.getFrameWidth('lr'));
93             }
94         }
95     }
96 });
97 Ext.Container.LAYOUTS['menu'] = Ext.layout.MenuLayout;
98
99 /**
100  * @class Ext.menu.Menu
101  * @extends Ext.Container
102  * <p>A menu object.  This is the container to which you may add menu items.  Menu can also serve as a base class
103  * when you want a specialized menu based off of another component (like {@link Ext.menu.DateMenu} for example).</p>
104  * <p>Menus may contain either {@link Ext.menu.Item menu items}, or general {@link Ext.Component Component}s.</p>
105  * <p>To make a contained general {@link Ext.Component Component} line up with other {@link Ext.menu.Item menu items}
106  * specify <tt>iconCls: 'no-icon'</tt>.  This reserves a space for an icon, and indents the Component in line
107  * with the other menu items.  See {@link Ext.form.ComboBox}.{@link Ext.form.ComboBox#getListParent getListParent}
108  * for an example.</p>
109  * <p>By default, Menus are absolutely positioned, floating Components. By configuring a Menu with
110  * <b><tt>{@link #floating}:false</tt></b>, a Menu may be used as child of a Container.</p>
111  *
112  * @xtype menu
113  */
114 Ext.menu.Menu = Ext.extend(Ext.Container, {
115     /**
116      * @cfg {Object} defaults
117      * A config object that will be applied to all items added to this container either via the {@link #items}
118      * config or via the {@link #add} method.  The defaults config can contain any number of
119      * name/value property pairs to be added to each item, and should be valid for the types of items
120      * being added to the menu.
121      */
122     /**
123      * @cfg {Mixed} items
124      * An array of items to be added to this menu. Menus may contain either {@link Ext.menu.Item menu items},
125      * or general {@link Ext.Component Component}s.
126      */
127     /**
128      * @cfg {Number} minWidth The minimum width of the menu in pixels (defaults to 120)
129      */
130     minWidth : 120,
131     /**
132      * @cfg {Boolean/String} shadow True or 'sides' for the default effect, 'frame' for 4-way shadow, and 'drop'
133      * for bottom-right shadow (defaults to 'sides')
134      */
135     shadow : 'sides',
136     /**
137      * @cfg {String} subMenuAlign The {@link Ext.Element#alignTo} anchor position value to use for submenus of
138      * this menu (defaults to 'tl-tr?')
139      */
140     subMenuAlign : 'tl-tr?',
141     /**
142      * @cfg {String} defaultAlign The default {@link Ext.Element#alignTo} anchor position value for this menu
143      * relative to its element of origin (defaults to 'tl-bl?')
144      */
145     defaultAlign : 'tl-bl?',
146     /**
147      * @cfg {Boolean} allowOtherMenus True to allow multiple menus to be displayed at the same time (defaults to false)
148      */
149     allowOtherMenus : false,
150     /**
151      * @cfg {Boolean} ignoreParentClicks True to ignore clicks on any item in this menu that is a parent item (displays
152      * a submenu) so that the submenu is not dismissed when clicking the parent item (defaults to false).
153      */
154     ignoreParentClicks : false,
155     /**
156      * @cfg {Boolean} enableScrolling True to allow the menu container to have scroller controls if the menu is too long (defaults to true).
157      */
158     enableScrolling : true,
159     /**
160      * @cfg {Number} maxHeight The maximum height of the menu. Only applies when enableScrolling is set to True (defaults to null).
161      */
162     maxHeight : null,
163     /**
164      * @cfg {Number} scrollIncrement The amount to scroll the menu. Only applies when enableScrolling is set to True (defaults to 24).
165      */
166     scrollIncrement : 24,
167     /**
168      * @cfg {Boolean} showSeparator True to show the icon separator. (defaults to true).
169      */
170     showSeparator : true,
171     /**
172      * @cfg {Array} defaultOffsets An array specifying the [x, y] offset in pixels by which to
173      * change the default Menu popup position after aligning according to the {@link #defaultAlign}
174      * configuration. Defaults to <tt>[0, 0]</tt>.
175      */
176     defaultOffsets : [0, 0],
177
178     /**
179      * @cfg {Boolean} plain
180      * True to remove the incised line down the left side of the menu. Defaults to <tt>false</tt>.
181      */
182     plain : false,
183
184     /**
185      * @cfg {Boolean} floating
186      * <p>By default, a Menu configured as <b><code>floating:true</code></b>
187      * will be rendered as an {@link Ext.Layer} (an absolutely positioned,
188      * floating Component with zindex=15000).
189      * If configured as <b><code>floating:false</code></b>, the Menu may be
190      * used as child item of another Container instead of a free-floating
191      * {@link Ext.Layer Layer}.
192      */
193     floating : true,
194
195     // private
196     hidden : true,
197
198     /**
199      * @cfg {String/Object} layout
200      * This class assigns a default layout (<code>layout:'<b>menu</b>'</code>).
201      * Developers <i>may</i> override this configuration option if another layout is required.
202      * See {@link Ext.Container#layout} for additional information.
203      */
204     layout : 'menu',
205     hideMode : 'offsets',    // Important for laying out Components
206     scrollerHeight : 8,
207     autoLayout : true,       // Provided for backwards compat
208     defaultType : 'menuitem',
209     bufferResize : false,
210
211     initComponent : function(){
212         if(Ext.isArray(this.initialConfig)){
213             Ext.apply(this, {items:this.initialConfig});
214         }
215         this.addEvents(
216             /**
217              * @event click
218              * Fires when this menu is clicked (or when the enter key is pressed while it is active)
219              * @param {Ext.menu.Menu} this
220             * @param {Ext.menu.Item} menuItem The menu item that was clicked
221              * @param {Ext.EventObject} e
222              */
223             'click',
224             /**
225              * @event mouseover
226              * Fires when the mouse is hovering over this menu
227              * @param {Ext.menu.Menu} this
228              * @param {Ext.EventObject} e
229              * @param {Ext.menu.Item} menuItem The menu item that was clicked
230              */
231             'mouseover',
232             /**
233              * @event mouseout
234              * Fires when the mouse exits this menu
235              * @param {Ext.menu.Menu} this
236              * @param {Ext.EventObject} e
237              * @param {Ext.menu.Item} menuItem The menu item that was clicked
238              */
239             'mouseout',
240             /**
241              * @event itemclick
242              * Fires when a menu item contained in this menu is clicked
243              * @param {Ext.menu.BaseItem} baseItem The BaseItem that was clicked
244              * @param {Ext.EventObject} e
245              */
246             'itemclick'
247         );
248         Ext.menu.MenuMgr.register(this);
249         if(this.floating){
250             Ext.EventManager.onWindowResize(this.hide, this);
251         }else{
252             if(this.initialConfig.hidden !== false){
253                 this.hidden = false;
254             }
255             this.internalDefaults = {hideOnClick: false};
256         }
257         Ext.menu.Menu.superclass.initComponent.call(this);
258         if(this.autoLayout){
259             this.on({
260                 add: this.doLayout,
261                 remove: this.doLayout,
262                 scope: this
263             });
264         }
265     },
266
267     //private
268     getLayoutTarget : function() {
269         return this.ul;
270     },
271
272     // private
273     onRender : function(ct, position){
274         if(!ct){
275             ct = Ext.getBody();
276         }
277
278         var dh = {
279             id: this.getId(),
280             cls: 'x-menu ' + ((this.floating) ? 'x-menu-floating x-layer ' : '') + (this.cls || '') + (this.plain ? ' x-menu-plain' : '') + (this.showSeparator ? '' : ' x-menu-nosep'),
281             style: this.style,
282             cn: [
283                 {tag: 'a', cls: 'x-menu-focus', href: '#', onclick: 'return false;', tabIndex: '-1'},
284                 {tag: 'ul', cls: 'x-menu-list'}
285             ]
286         };
287         if(this.floating){
288             this.el = new Ext.Layer({
289                 shadow: this.shadow,
290                 dh: dh,
291                 constrain: false,
292                 parentEl: ct,
293                 zindex:15000
294             });
295         }else{
296             this.el = ct.createChild(dh);
297         }
298         Ext.menu.Menu.superclass.onRender.call(this, ct, position);
299
300         if(!this.keyNav){
301             this.keyNav = new Ext.menu.MenuNav(this);
302         }
303         // generic focus element
304         this.focusEl = this.el.child('a.x-menu-focus');
305         this.ul = this.el.child('ul.x-menu-list');
306         this.mon(this.ul, {
307             scope: this,
308             click: this.onClick,
309             mouseover: this.onMouseOver,
310             mouseout: this.onMouseOut
311         });
312         if(this.enableScrolling){
313             this.mon(this.el, {
314                 scope: this,
315                 delegate: '.x-menu-scroller',
316                 click: this.onScroll,
317                 mouseover: this.deactivateActive
318             });
319         }
320     },
321
322     // private
323     findTargetItem : function(e){
324         var t = e.getTarget('.x-menu-list-item', this.ul, true);
325         if(t && t.menuItemId){
326             return this.items.get(t.menuItemId);
327         }
328     },
329
330     // private
331     onClick : function(e){
332         var t = this.findTargetItem(e);
333         if(t){
334             if(t.isFormField){
335                 this.setActiveItem(t);
336             }else if(t instanceof Ext.menu.BaseItem){
337                 if(t.menu && this.ignoreParentClicks){
338                     t.expandMenu();
339                     e.preventDefault();
340                 }else if(t.onClick){
341                     t.onClick(e);
342                     this.fireEvent('click', this, t, e);
343                 }
344             }
345         }
346     },
347
348     // private
349     setActiveItem : function(item, autoExpand){
350         if(item != this.activeItem){
351             this.deactivateActive();
352             if((this.activeItem = item).isFormField){
353                 item.focus();
354             }else{
355                 item.activate(autoExpand);
356             }
357         }else if(autoExpand){
358             item.expandMenu();
359         }
360     },
361
362     deactivateActive : function(){
363         var a = this.activeItem;
364         if(a){
365             if(a.isFormField){
366                 //Fields cannot deactivate, but Combos must collapse
367                 if(a.collapse){
368                     a.collapse();
369                 }
370             }else{
371                 a.deactivate();
372             }
373             delete this.activeItem;
374         }
375     },
376
377     // private
378     tryActivate : function(start, step){
379         var items = this.items;
380         for(var i = start, len = items.length; i >= 0 && i < len; i+= step){
381             var item = items.get(i);
382             if(!item.disabled && (item.canActivate || item.isFormField)){
383                 this.setActiveItem(item, false);
384                 return item;
385             }
386         }
387         return false;
388     },
389
390     // private
391     onMouseOver : function(e){
392         var t = this.findTargetItem(e);
393         if(t){
394             if(t.canActivate && !t.disabled){
395                 this.setActiveItem(t, true);
396             }
397         }
398         this.over = true;
399         this.fireEvent('mouseover', this, e, t);
400     },
401
402     // private
403     onMouseOut : function(e){
404         var t = this.findTargetItem(e);
405         if(t){
406             if(t == this.activeItem && t.shouldDeactivate && t.shouldDeactivate(e)){
407                 this.activeItem.deactivate();
408                 delete this.activeItem;
409             }
410         }
411         this.over = false;
412         this.fireEvent('mouseout', this, e, t);
413     },
414
415     // private
416     onScroll : function(e, t){
417         if(e){
418             e.stopEvent();
419         }
420         var ul = this.ul.dom, top = Ext.fly(t).is('.x-menu-scroller-top');
421         ul.scrollTop += this.scrollIncrement * (top ? -1 : 1);
422         if(top ? ul.scrollTop <= 0 : ul.scrollTop + this.activeMax >= ul.scrollHeight){
423            this.onScrollerOut(null, t);
424         }
425     },
426
427     // private
428     onScrollerIn : function(e, t){
429         var ul = this.ul.dom, top = Ext.fly(t).is('.x-menu-scroller-top');
430         if(top ? ul.scrollTop > 0 : ul.scrollTop + this.activeMax < ul.scrollHeight){
431             Ext.fly(t).addClass(['x-menu-item-active', 'x-menu-scroller-active']);
432         }
433     },
434
435     // private
436     onScrollerOut : function(e, t){
437         Ext.fly(t).removeClass(['x-menu-item-active', 'x-menu-scroller-active']);
438     },
439
440     /**
441      * If <code>{@link #floating}=true</code>, shows this menu relative to
442      * another element using {@link #showat}, otherwise uses {@link Ext.Component#show}.
443      * @param {Mixed} element The element to align to
444      * @param {String} position (optional) The {@link Ext.Element#alignTo} anchor position to use in aligning to
445      * the element (defaults to this.defaultAlign)
446      * @param {Ext.menu.Menu} parentMenu (optional) This menu's parent menu, if applicable (defaults to undefined)
447      */
448     show : function(el, pos, parentMenu){
449         if(this.floating){
450             this.parentMenu = parentMenu;
451             if(!this.el){
452                 this.render();
453                 this.doLayout(false, true);
454             }
455             this.showAt(this.el.getAlignToXY(el, pos || this.defaultAlign, this.defaultOffsets), parentMenu);
456         }else{
457             Ext.menu.Menu.superclass.show.call(this);
458         }
459     },
460
461     /**
462      * Displays this menu at a specific xy position and fires the 'show' event if a
463      * handler for the 'beforeshow' event does not return false cancelling the operation.
464      * @param {Array} xyPosition Contains X & Y [x, y] values for the position at which to show the menu (coordinates are page-based)
465      * @param {Ext.menu.Menu} parentMenu (optional) This menu's parent menu, if applicable (defaults to undefined)
466      */
467     showAt : function(xy, parentMenu){
468         if(this.fireEvent('beforeshow', this) !== false){
469             this.parentMenu = parentMenu;
470             if(!this.el){
471                 this.render();
472             }
473             if(this.enableScrolling){
474                 // set the position so we can figure out the constrain value.
475                 this.el.setXY(xy);
476                 //constrain the value, keep the y coordinate the same
477                 this.constrainScroll(xy[1]);
478                 xy = [this.el.adjustForConstraints(xy)[0], xy[1]];
479             }else{
480                 //constrain to the viewport.
481                 xy = this.el.adjustForConstraints(xy);
482             }
483             this.el.setXY(xy);
484             this.el.show();
485             Ext.menu.Menu.superclass.onShow.call(this);
486             if(Ext.isIE){
487                 // internal event, used so we don't couple the layout to the menu
488                 this.fireEvent('autosize', this);
489                 if(!Ext.isIE8){
490                     this.el.repaint();
491                 }
492             }
493             this.hidden = false;
494             this.focus();
495             this.fireEvent('show', this);
496         }
497     },
498
499     constrainScroll : function(y){
500         var max, full = this.ul.setHeight('auto').getHeight();
501         if(this.floating){
502             max = this.maxHeight ? this.maxHeight : Ext.fly(this.el.dom.parentNode).getViewSize(false).height - y;
503         }else{
504             max = this.getHeight();
505         }
506         if(full > max && max > 0){
507             this.activeMax = max - this.scrollerHeight * 2 - this.el.getFrameWidth('tb') - Ext.num(this.el.shadowOffset, 0);
508             this.ul.setHeight(this.activeMax);
509             this.createScrollers();
510             this.el.select('.x-menu-scroller').setDisplayed('');
511         }else{
512             this.ul.setHeight(full);
513             this.el.select('.x-menu-scroller').setDisplayed('none');
514         }
515         this.ul.dom.scrollTop = 0;
516     },
517
518     createScrollers : function(){
519         if(!this.scroller){
520             this.scroller = {
521                 pos: 0,
522                 top: this.el.insertFirst({
523                     tag: 'div',
524                     cls: 'x-menu-scroller x-menu-scroller-top',
525                     html: '&#160;'
526                 }),
527                 bottom: this.el.createChild({
528                     tag: 'div',
529                     cls: 'x-menu-scroller x-menu-scroller-bottom',
530                     html: '&#160;'
531                 })
532             };
533             this.scroller.top.hover(this.onScrollerIn, this.onScrollerOut, this);
534             this.scroller.topRepeater = new Ext.util.ClickRepeater(this.scroller.top, {
535                 listeners: {
536                     click: this.onScroll.createDelegate(this, [null, this.scroller.top], false)
537                 }
538             });
539             this.scroller.bottom.hover(this.onScrollerIn, this.onScrollerOut, this);
540             this.scroller.bottomRepeater = new Ext.util.ClickRepeater(this.scroller.bottom, {
541                 listeners: {
542                     click: this.onScroll.createDelegate(this, [null, this.scroller.bottom], false)
543                 }
544             });
545         }
546     },
547
548     onLayout : function(){
549         if(this.isVisible()){
550             if(this.enableScrolling){
551                 this.constrainScroll(this.el.getTop());
552             }
553             if(this.floating){
554                 this.el.sync();
555             }
556         }
557     },
558
559     focus : function(){
560         if(!this.hidden){
561             this.doFocus.defer(50, this);
562         }
563     },
564
565     doFocus : function(){
566         if(!this.hidden){
567             this.focusEl.focus();
568         }
569     },
570
571     /**
572      * Hides this menu and optionally all parent menus
573      * @param {Boolean} deep (optional) True to hide all parent menus recursively, if any (defaults to false)
574      */
575     hide : function(deep){
576         this.deepHide = deep;
577         Ext.menu.Menu.superclass.hide.call(this);
578         delete this.deepHide;
579     },
580
581     // private
582     onHide : function(){
583         Ext.menu.Menu.superclass.onHide.call(this);
584         this.deactivateActive();
585         if(this.el && this.floating){
586             this.el.hide();
587         }
588         var pm = this.parentMenu;
589         if(this.deepHide === true && pm){
590             if(pm.floating){
591                 pm.hide(true);
592             }else{
593                 pm.deactivateActive();
594             }
595         }
596     },
597
598     // private
599     lookupComponent : function(c){
600          if(Ext.isString(c)){
601             c = (c == 'separator' || c == '-') ? new Ext.menu.Separator() : new Ext.menu.TextItem(c);
602              this.applyDefaults(c);
603          }else{
604             if(Ext.isObject(c)){
605                 c = this.getMenuItem(c);
606             }else if(c.tagName || c.el){ // element. Wrap it.
607                 c = new Ext.BoxComponent({
608                     el: c
609                 });
610             }
611          }
612          return c;
613     },
614
615     applyDefaults : function(c){
616         if(!Ext.isString(c)){
617             c = Ext.menu.Menu.superclass.applyDefaults.call(this, c);
618             var d = this.internalDefaults;
619             if(d){
620                 if(c.events){
621                     Ext.applyIf(c.initialConfig, d);
622                     Ext.apply(c, d);
623                 }else{
624                     Ext.applyIf(c, d);
625                 }
626             }
627         }
628         return c;
629     },
630
631     // private
632     getMenuItem : function(config){
633        if(!config.isXType){
634             if(!config.xtype && Ext.isBoolean(config.checked)){
635                 return new Ext.menu.CheckItem(config)
636             }
637             return Ext.create(config, this.defaultType);
638         }
639         return config;
640     },
641
642     /**
643      * Adds a separator bar to the menu
644      * @return {Ext.menu.Item} The menu item that was added
645      */
646     addSeparator : function(){
647         return this.add(new Ext.menu.Separator());
648     },
649
650     /**
651      * Adds an {@link Ext.Element} object to the menu
652      * @param {Mixed} el The element or DOM node to add, or its id
653      * @return {Ext.menu.Item} The menu item that was added
654      */
655     addElement : function(el){
656         return this.add(new Ext.menu.BaseItem(el));
657     },
658
659     /**
660      * Adds an existing object based on {@link Ext.menu.BaseItem} to the menu
661      * @param {Ext.menu.Item} item The menu item to add
662      * @return {Ext.menu.Item} The menu item that was added
663      */
664     addItem : function(item){
665         return this.add(item);
666     },
667
668     /**
669      * Creates a new {@link Ext.menu.Item} based an the supplied config object and adds it to the menu
670      * @param {Object} config A MenuItem config object
671      * @return {Ext.menu.Item} The menu item that was added
672      */
673     addMenuItem : function(config){
674         return this.add(this.getMenuItem(config));
675     },
676
677     /**
678      * Creates a new {@link Ext.menu.TextItem} with the supplied text and adds it to the menu
679      * @param {String} text The text to display in the menu item
680      * @return {Ext.menu.Item} The menu item that was added
681      */
682     addText : function(text){
683         return this.add(new Ext.menu.TextItem(text));
684     },
685
686     //private
687     onDestroy : function(){
688         var pm = this.parentMenu;
689         if(pm && pm.activeChild == this){
690             delete pm.activeChild;
691         }
692         delete this.parentMenu;
693         Ext.menu.Menu.superclass.onDestroy.call(this);
694         Ext.menu.MenuMgr.unregister(this);
695         Ext.EventManager.removeResizeListener(this.hide, this);
696         if(this.keyNav) {
697             this.keyNav.disable();
698         }
699         var s = this.scroller;
700         if(s){
701             Ext.destroy(s.topRepeater, s.bottomRepeater, s.top, s.bottom);
702         }
703         Ext.destroy(
704             this.el,
705             this.focusEl,
706             this.ul
707         );
708     }
709 });
710
711 Ext.reg('menu', Ext.menu.Menu);
712
713 // MenuNav is a private utility class used internally by the Menu
714 Ext.menu.MenuNav = Ext.extend(Ext.KeyNav, function(){
715     function up(e, m){
716         if(!m.tryActivate(m.items.indexOf(m.activeItem)-1, -1)){
717             m.tryActivate(m.items.length-1, -1);
718         }
719     }
720     function down(e, m){
721         if(!m.tryActivate(m.items.indexOf(m.activeItem)+1, 1)){
722             m.tryActivate(0, 1);
723         }
724     }
725     return {
726         constructor : function(menu){
727             Ext.menu.MenuNav.superclass.constructor.call(this, menu.el);
728             this.scope = this.menu = menu;
729         },
730
731         doRelay : function(e, h){
732             var k = e.getKey();
733 //          Keystrokes within a form Field (e.g.: down in a Combo) do not navigate. Allow only TAB
734             if (this.menu.activeItem && this.menu.activeItem.isFormField && k != e.TAB) {
735                 return false;
736             }
737             if(!this.menu.activeItem && e.isNavKeyPress() && k != e.SPACE && k != e.RETURN){
738                 this.menu.tryActivate(0, 1);
739                 return false;
740             }
741             return h.call(this.scope || this, e, this.menu);
742         },
743
744         tab: function(e, m) {
745             e.stopEvent();
746             if (e.shiftKey) {
747                 up(e, m);
748             } else {
749                 down(e, m);
750             }
751         },
752
753         up : up,
754
755         down : down,
756
757         right : function(e, m){
758             if(m.activeItem){
759                 m.activeItem.expandMenu(true);
760             }
761         },
762
763         left : function(e, m){
764             m.hide();
765             if(m.parentMenu && m.parentMenu.activeItem){
766                 m.parentMenu.activeItem.activate();
767             }
768         },
769
770         enter : function(e, m){
771             if(m.activeItem){
772                 e.stopPropagation();
773                 m.activeItem.onClick(e);
774                 m.fireEvent('click', this, m.activeItem);
775                 return true;
776             }
777         }
778     };
779 }());