Upgrade to ExtJS 4.0.2 - Released 06/09/2011
[extjs.git] / src / menu / Menu.js
1 /*
2
3 This file is part of Ext JS 4
4
5 Copyright (c) 2011 Sencha Inc
6
7 Contact:  http://www.sencha.com/contact
8
9 GNU General Public License Usage
10 This file may be used under the terms of the GNU General Public License version 3.0 as published by the Free Software Foundation and appearing in the file LICENSE included in the packaging of this file.  Please review the following information to ensure the GNU General Public License version 3.0 requirements will be met: http://www.gnu.org/copyleft/gpl.html.
11
12 If you are unsure which license is appropriate for your use, please contact the sales department at http://www.sencha.com/contact.
13
14 */
15 /**
16  * @class Ext.menu.Menu
17  * @extends Ext.panel.Panel
18  *
19  * A menu object. This is the container to which you may add {@link Ext.menu.Item menu items}.
20  *
21  * Menus may contain either {@link Ext.menu.Item menu items}, or general {@link Ext.Component Components}.
22  * Menus may also contain {@link Ext.panel.AbstractPanel#dockedItems docked items} because it extends {@link Ext.panel.Panel}.
23  *
24  * To make a contained general {@link Ext.Component Component} line up with other {@link Ext.menu.Item menu items},
25  * specify `{@link Ext.menu.Item#iconCls iconCls}: 'no-icon'` _or_ `{@link Ext.menu.Item#indent indent}: true`.
26  * This reserves a space for an icon, and indents the Component in line with the other menu items.
27  * See {@link Ext.form.field.ComboBox}.{@link Ext.form.field.ComboBox#getListParent getListParent} for an example.
28  *
29  * By default, Menus are absolutely positioned, floating Components. By configuring a Menu with `{@link #floating}:false`,
30  * a Menu may be used as a child of a {@link Ext.container.Container Container}.
31  *
32  * {@img Ext.menu.Item/Ext.menu.Item.png Ext.menu.Item component}
33  *
34  *__Example Usage__
35  *
36  *     Ext.create('Ext.menu.Menu', {
37  *         width: 100,
38  *         height: 100,
39  *         margin: '0 0 10 0',
40  *         floating: false,  // usually you want this set to True (default)
41  *         renderTo: Ext.getBody(),  // usually rendered by it's containing component
42  *         items: [{                        
43  *             text: 'regular item 1'        
44  *         },{
45  *             text: 'regular item 2'
46  *         },{
47  *             text: 'regular item 3'  
48  *         }]
49  *     }); 
50  *     
51  *     Ext.create('Ext.menu.Menu', {
52  *         width: 100,
53  *         height: 100,
54  *         plain: true,
55  *         floating: false,  // usually you want this set to True (default)
56  *         renderTo: Ext.getBody(),  // usually rendered by it's containing component
57  *         items: [{                        
58  *             text: 'plain item 1'    
59  *         },{
60  *             text: 'plain item 2'
61  *         },{
62  *             text: 'plain item 3'
63  *         }]
64  *     }); 
65  *
66  */
67 Ext.define('Ext.menu.Menu', {
68     extend: 'Ext.panel.Panel',
69     alias: 'widget.menu',
70     requires: [
71         'Ext.layout.container.Fit',
72         'Ext.layout.container.VBox',
73         'Ext.menu.CheckItem',
74         'Ext.menu.Item',
75         'Ext.menu.KeyNav',
76         'Ext.menu.Manager',
77         'Ext.menu.Separator'
78     ],
79
80     /**
81      * @cfg {Boolean} allowOtherMenus
82      * True to allow multiple menus to be displayed at the same time. Defaults to `false`.
83      * @markdown
84      */
85     allowOtherMenus: false,
86
87     /**
88      * @cfg {String} ariaRole @hide
89      */
90     ariaRole: 'menu',
91
92     /**
93      * @cfg {Boolean} autoRender @hide
94      * floating is true, so autoRender always happens
95      */
96
97     /**
98      * @cfg {String} defaultAlign
99      * The default {@link Ext.core.Element#getAlignToXY Ext.core.Element#getAlignToXY} anchor position value for this menu
100      * relative to its element of origin. Defaults to `'tl-bl?'`.
101      * @markdown
102      */
103     defaultAlign: 'tl-bl?',
104
105     /**
106      * @cfg {Boolean} floating
107      * A Menu configured as `floating: true` (the default) will be rendered as an absolutely positioned,
108      * {@link Ext.Component#floating floating} {@link Ext.Component Component}. If configured as `floating: false`, the Menu may be
109      * used as a child item of another {@link Ext.container.Container Container}.
110      * @markdown
111      */
112     floating: true,
113
114     /**
115      * @cfg {Boolean} @hide
116      * Menus are constrained to the document body by default
117      */
118     constrain: true,
119
120     /**
121      * @cfg {Boolean} hidden
122      * True to initially render the Menu as hidden, requiring to be shown manually.
123      * Defaults to `true` when `floating: true`, and defaults to `false` when `floating: false`.
124      * @markdown
125      */
126     hidden: true,
127
128     hideMode: 'visibility',
129
130     /**
131      * @cfg {Boolean} ignoreParentClicks
132      * True to ignore clicks on any item in this menu that is a parent item (displays a submenu)
133      * so that the submenu is not dismissed when clicking the parent item. Defaults to `false`.
134      * @markdown
135      */
136     ignoreParentClicks: false,
137
138     isMenu: true,
139
140     /**
141      * @cfg {String/Object} layout @hide
142      */
143
144     /**
145      * @cfg {Boolean} showSeparator True to show the icon separator. (defaults to true).
146      */
147     showSeparator : true,
148
149     /**
150      * @cfg {Number} minWidth
151      * The minimum width of the Menu. Defaults to `120`.
152      * @markdown
153      */
154     minWidth: 120,
155
156     /**
157      * @cfg {Boolean} plain
158      * True to remove the incised line down the left side of the menu and to not
159      * indent general Component items. Defaults to `false`.
160      * @markdown
161      */
162
163     initComponent: function() {
164         var me = this,
165             prefix = Ext.baseCSSPrefix,
166             cls = [prefix + 'menu'],
167             bodyCls = me.bodyCls ? [me.bodyCls] : [];
168
169         me.addEvents(
170             /**
171              * @event click
172              * Fires when this menu is clicked
173              * @param {Ext.menu.Menu} menu The menu which has been clicked
174              * @param {Ext.Component} item The menu item that was clicked. `undefined` if not applicable.
175              * @param {Ext.EventObject} e The underlying {@link Ext.EventObject}.
176              * @markdown
177              */
178             'click',
179
180             /**
181              * @event mouseenter
182              * Fires when the mouse enters this menu
183              * @param {Ext.menu.Menu} menu The menu
184              * @param {Ext.EventObject} e The underlying {@link Ext.EventObject}
185              * @markdown
186              */
187             'mouseenter',
188
189             /**
190              * @event mouseleave
191              * Fires when the mouse leaves this menu
192              * @param {Ext.menu.Menu} menu The menu
193              * @param {Ext.EventObject} e The underlying {@link Ext.EventObject}
194              * @markdown
195              */
196             'mouseleave',
197
198             /**
199              * @event mouseover
200              * Fires when the mouse is hovering over this menu
201              * @param {Ext.menu.Menu} menu The menu
202              * @param {Ext.Component} item The menu item that the mouse is over. `undefined` if not applicable.
203              * @param {Ext.EventObject} e The underlying {@link Ext.EventObject}
204              */
205             'mouseover'
206         );
207
208         Ext.menu.Manager.register(me);
209
210         // Menu classes
211         if (me.plain) {
212             cls.push(prefix + 'menu-plain');
213         }
214         me.cls = cls.join(' ');
215
216         // Menu body classes
217         bodyCls.unshift(prefix + 'menu-body');
218         me.bodyCls = bodyCls.join(' ');
219
220         // Internal vbox layout, with scrolling overflow
221         // Placed in initComponent (rather than prototype) in order to support dynamic layout/scroller
222         // options if we wish to allow for such configurations on the Menu.
223         // e.g., scrolling speed, vbox align stretch, etc.
224         me.layout = {
225             type: 'vbox',
226             align: 'stretchmax',
227             autoSize: true,
228             clearInnerCtOnLayout: true,
229             overflowHandler: 'Scroller'
230         };
231
232         // hidden defaults to false if floating is configured as false
233         if (me.floating === false && me.initialConfig.hidden !== true) {
234             me.hidden = false;
235         }
236
237         me.callParent(arguments);
238
239         me.on('beforeshow', function() {
240             var hasItems = !!me.items.length;
241             // FIXME: When a menu has its show cancelled because of no items, it
242             // gets a visibility: hidden applied to it (instead of the default display: none)
243             // Not sure why, but we remove this style when we want to show again.
244             if (hasItems && me.rendered) {
245                 me.el.setStyle('visibility', null);
246             }
247             return hasItems;
248         });
249     },
250
251     afterRender: function(ct) {
252         var me = this,
253             prefix = Ext.baseCSSPrefix,
254             space = ' ';
255
256         me.callParent(arguments);
257
258         // TODO: Move this to a subTemplate When we support them in the future
259         if (me.showSeparator) {
260             me.iconSepEl = me.layout.getRenderTarget().insertFirst({
261                 cls: prefix + 'menu-icon-separator',
262                 html: space
263             });
264         }
265
266         me.focusEl = me.el.createChild({
267             cls: prefix + 'menu-focus',
268             tabIndex: '-1',
269             html: space
270         });
271
272         me.mon(me.el, {
273             click: me.onClick,
274             mouseover: me.onMouseOver,
275             scope: me
276         });
277         me.mouseMonitor = me.el.monitorMouseLeave(100, me.onMouseLeave, me);
278
279         if (me.showSeparator && ((!Ext.isStrict && Ext.isIE) || Ext.isIE6)) {
280             me.iconSepEl.setHeight(me.el.getHeight());
281         }
282
283         me.keyNav = Ext.create('Ext.menu.KeyNav', me);
284     },
285
286     afterLayout: function() {
287         var me = this;
288         me.callParent(arguments);
289
290         // For IE6 & IE quirks, we have to resize the el and body since position: absolute
291         // floating elements inherit their parent's width, making them the width of
292         // document.body instead of the width of their contents.
293         // This includes left/right dock items.
294         if ((!Ext.iStrict && Ext.isIE) || Ext.isIE6) {
295             var innerCt = me.layout.getRenderTarget(),
296                 innerCtWidth = 0,
297                 dis = me.dockedItems,
298                 l = dis.length,
299                 i = 0,
300                 di, clone, newWidth;
301
302             innerCtWidth = innerCt.getWidth();
303
304             newWidth = innerCtWidth + me.body.getBorderWidth('lr') + me.body.getPadding('lr');
305
306             // First set the body to the new width
307             me.body.setWidth(newWidth);
308
309             // Now we calculate additional width (docked items) and set the el's width
310             for (; i < l, di = dis.getAt(i); i++) {
311                 if (di.dock == 'left' || di.dock == 'right') {
312                     newWidth += di.getWidth();
313                 }
314             }
315             me.el.setWidth(newWidth);
316         }
317     },
318
319     /**
320      * Returns whether a menu item can be activated or not.
321      * @return {Boolean}
322      */
323     canActivateItem: function(item) {
324         return item && !item.isDisabled() && item.isVisible() && (item.canActivate || item.getXTypes().indexOf('menuitem') < 0);
325     },
326
327     /**
328      * Deactivates the current active item on the menu, if one exists.
329      */
330     deactivateActiveItem: function() {
331         var me = this;
332
333         if (me.activeItem) {
334             me.activeItem.deactivate();
335             if (!me.activeItem.activated) {
336                 delete me.activeItem;
337             }
338         }
339         if (me.focusedItem) {
340             me.focusedItem.blur();
341             if (!me.focusedItem.$focused) {
342                 delete me.focusedItem;
343             }
344         }
345     },
346
347     clearStretch: function () {
348         // the vbox/stretchmax will set the el sizes and subsequent layouts will not
349         // reconsider them unless we clear the dimensions on the el's here:
350         if (this.rendered) {
351             this.items.each(function (item) {
352                 // each menuItem component needs to layout again, so clear its cache
353                 if (item.componentLayout) {
354                     delete item.componentLayout.lastComponentSize;
355                 }
356                 if (item.el) {
357                     item.el.setWidth(null);
358                 }
359             });
360         }
361     },
362
363     onAdd: function () {
364         var me = this;
365
366         me.clearStretch();
367         me.callParent(arguments);
368
369         if (Ext.isIE6 || Ext.isIE7) {
370             // TODO - why does this need to be done (and not ok to do now)?
371             Ext.Function.defer(me.doComponentLayout, 10, me);
372         }
373     },
374
375     onRemove: function () {
376         this.clearStretch();
377         this.callParent(arguments);
378
379     },
380
381     redoComponentLayout: function () {
382         if (this.rendered) {
383             this.clearStretch();
384             this.doComponentLayout();
385         }
386     },
387
388     // inherit docs
389     getFocusEl: function() {
390         return this.focusEl;
391     },
392
393     // inherit docs
394     hide: function() {
395         this.deactivateActiveItem();
396         this.callParent(arguments);
397     },
398
399     // private
400     getItemFromEvent: function(e) {
401         return this.getChildByElement(e.getTarget());
402     },
403
404     lookupComponent: function(cmp) {
405         var me = this;
406
407         if (Ext.isString(cmp)) {
408             cmp = me.lookupItemFromString(cmp);
409         } else if (Ext.isObject(cmp)) {
410             cmp = me.lookupItemFromObject(cmp);
411         }
412
413         // Apply our minWidth to all of our child components so it's accounted
414         // for in our VBox layout
415         cmp.minWidth = cmp.minWidth || me.minWidth;
416
417         return cmp;
418     },
419
420     // private
421     lookupItemFromObject: function(cmp) {
422         var me = this,
423             prefix = Ext.baseCSSPrefix,
424             cls,
425             intercept;
426
427         if (!cmp.isComponent) {
428             if (!cmp.xtype) {
429                 cmp = Ext.create('Ext.menu.' + (Ext.isBoolean(cmp.checked) ? 'Check': '') + 'Item', cmp);
430             } else {
431                 cmp = Ext.ComponentManager.create(cmp, cmp.xtype);
432             }
433         }
434
435         if (cmp.isMenuItem) {
436             cmp.parentMenu = me;
437         }
438
439         if (!cmp.isMenuItem && !cmp.dock) {
440             cls = [prefix + 'menu-item', prefix + 'menu-item-cmp'];
441             intercept = Ext.Function.createInterceptor;
442
443             // Wrap focus/blur to control component focus
444             cmp.focus = intercept(cmp.focus, function() {
445                 this.$focused = true;
446             }, cmp);
447             cmp.blur = intercept(cmp.blur, function() {
448                 this.$focused = false;
449             }, cmp);
450
451             if (!me.plain && (cmp.indent === true || cmp.iconCls === 'no-icon')) {
452                 cls.push(prefix + 'menu-item-indent');
453             }
454
455             if (cmp.rendered) {
456                 cmp.el.addCls(cls);
457             } else {
458                 cmp.cls = (cmp.cls ? cmp.cls : '') + ' ' + cls.join(' ');
459             }
460             cmp.isMenuItem = true;
461         }
462         return cmp;
463     },
464
465     // private
466     lookupItemFromString: function(cmp) {
467         return (cmp == 'separator' || cmp == '-') ?
468             Ext.createWidget('menuseparator')
469             : Ext.createWidget('menuitem', {
470                 canActivate: false,
471                 hideOnClick: false,
472                 plain: true,
473                 text: cmp
474             });
475     },
476
477     onClick: function(e) {
478         var me = this,
479             item;
480
481         if (me.disabled) {
482             e.stopEvent();
483             return;
484         }
485
486         if ((e.getTarget() == me.focusEl.dom) || e.within(me.layout.getRenderTarget())) {
487             item = me.getItemFromEvent(e) || me.activeItem;
488
489             if (item) {
490                 if (item.getXTypes().indexOf('menuitem') >= 0) {
491                     if (!item.menu || !me.ignoreParentClicks) {
492                         item.onClick(e);
493                     } else {
494                         e.stopEvent();
495                     }
496                 }
497             }
498             me.fireEvent('click', me, item, e);
499         }
500     },
501
502     onDestroy: function() {
503         var me = this;
504
505         Ext.menu.Manager.unregister(me);
506         if (me.rendered) {
507             me.el.un(me.mouseMonitor);
508             me.keyNav.destroy();
509             delete me.keyNav;
510         }
511         me.callParent(arguments);
512     },
513
514     onMouseLeave: function(e) {
515         var me = this;
516
517         me.deactivateActiveItem();
518
519         if (me.disabled) {
520             return;
521         }
522
523         me.fireEvent('mouseleave', me, e);
524     },
525
526     onMouseOver: function(e) {
527         var me = this,
528             fromEl = e.getRelatedTarget(),
529             mouseEnter = !me.el.contains(fromEl),
530             item = me.getItemFromEvent(e);
531
532         if (mouseEnter && me.parentMenu) {
533             me.parentMenu.setActiveItem(me.parentItem);
534             me.parentMenu.mouseMonitor.mouseenter();
535         }
536
537         if (me.disabled) {
538             return;
539         }
540
541         if (item) {
542             me.setActiveItem(item);
543             if (item.activated && item.expandMenu) {
544                 item.expandMenu();
545             }
546         }
547         if (mouseEnter) {
548             me.fireEvent('mouseenter', me, e);
549         }
550         me.fireEvent('mouseover', me, item, e);
551     },
552
553     setActiveItem: function(item) {
554         var me = this;
555
556         if (item && (item != me.activeItem && item != me.focusedItem)) {
557             me.deactivateActiveItem();
558             if (me.canActivateItem(item)) {
559                 if (item.activate) {
560                     item.activate();
561                     if (item.activated) {
562                         me.activeItem = item;
563                         me.focusedItem = item;
564                         me.focus();
565                     }
566                 } else {
567                     item.focus();
568                     me.focusedItem = item;
569                 }
570             }
571             item.el.scrollIntoView(me.layout.getRenderTarget());
572         }
573     },
574
575     /**
576      * Shows the floating menu by the specified {@link Ext.Component Component} or {@link Ext.core.Element Element}.
577      * @param {Mixed component} The {@link Ext.Component} or {@link Ext.core.Element} to show the menu by.
578      * @param {String} position (optional) Alignment position as used by {@link Ext.core.Element#getAlignToXY Ext.core.Element.getAlignToXY}. Defaults to `{@link #defaultAlign}`.
579      * @param {Array} offsets (optional) Alignment offsets as used by {@link Ext.core.Element#getAlignToXY Ext.core.Element.getAlignToXY}. Defaults to `undefined`.
580      * @return {Menu} This Menu.
581      * @markdown
582      */
583     showBy: function(cmp, pos, off) {
584         var me = this,
585             xy,
586             region;
587
588         if (me.floating && cmp) {
589             me.layout.autoSize = true;
590
591             // show off-screen first so that we can calc position without causing a visual jump
592             me.doAutoRender();
593
594             // Component or Element
595             cmp = cmp.el || cmp;
596
597             // Convert absolute to floatParent-relative coordinates if necessary.
598             xy = me.el.getAlignToXY(cmp, pos || me.defaultAlign, off);
599             if (me.floatParent) {
600                 region = me.floatParent.getTargetEl().getViewRegion();
601                 xy[0] -= region.x;
602                 xy[1] -= region.y;
603             }
604             me.showAt(xy);
605         }
606         return me;
607     },
608     
609     // inherit docs
610     showAt: function(){
611         this.callParent(arguments);
612         if (this.floating) {
613             this.doConstrain();
614         }    
615     },
616
617     doConstrain : function() {
618         var me = this,
619             y = me.el.getY(),
620             max, full,
621             vector,
622             returnY = y, normalY, parentEl, scrollTop, viewHeight;
623
624         delete me.height;
625         me.setSize();
626         full = me.getHeight();
627         if (me.floating) {
628             parentEl = Ext.fly(me.el.dom.parentNode);
629             scrollTop = parentEl.getScroll().top;
630             viewHeight = parentEl.getViewSize().height;
631             //Normalize y by the scroll position for the parent element.  Need to move it into the coordinate space
632             //of the view.
633             normalY = y - scrollTop;
634             max = me.maxHeight ? me.maxHeight : viewHeight - normalY;
635             if (full > viewHeight) {
636                 max = viewHeight;
637                 //Set returnY equal to (0,0) in view space by reducing y by the value of normalY
638                 returnY = y - normalY;
639             } else if (max < full) {
640                 returnY = y - (full - max);
641                 max = full;
642             }
643         }else{
644             max = me.getHeight();
645         }
646         // Always respect maxHeight
647         if (me.maxHeight){
648             max = Math.min(me.maxHeight, max);
649         }
650         if (full > max && max > 0){
651             me.layout.autoSize = false;
652             me.setHeight(max);
653             if (me.showSeparator){
654                 me.iconSepEl.setHeight(me.layout.getRenderTarget().dom.scrollHeight);
655             }
656         }
657         vector = me.getConstrainVector(me.el.dom.parentNode);
658         if (vector) {
659             me.setPosition(me.getPosition()[0] + vector[0]);
660         }
661         me.el.setY(returnY);
662     }
663 });