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