3 This file is part of Ext JS 4
5 Copyright (c) 2011 Sencha Inc
7 Contact: http://www.sencha.com/contact
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.
12 If you are unsure which license is appropriate for your use, please contact the sales department at http://www.sencha.com/contact.
16 * A menu object. This is the container to which you may add {@link Ext.menu.Item menu items}.
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}.
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.
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}.
29 * Ext.create('Ext.menu.Menu', {
33 * floating: false, // usually you want this set to True (default)
34 * renderTo: Ext.getBody(), // usually rendered by it's containing component
36 * text: 'regular item 1'
38 * text: 'regular item 2'
40 * text: 'regular item 3'
44 * Ext.create('Ext.menu.Menu', {
48 * floating: false, // usually you want this set to True (default)
49 * renderTo: Ext.getBody(), // usually rendered by it's containing component
51 * text: 'plain item 1'
53 * text: 'plain item 2'
55 * text: 'plain item 3'
59 Ext.define('Ext.menu.Menu', {
60 extend: 'Ext.panel.Panel',
63 'Ext.layout.container.Fit',
64 'Ext.layout.container.VBox',
73 * @property {Ext.menu.Menu} parentMenu
74 * The parent Menu of this Menu.
78 * @cfg {Boolean} allowOtherMenus
79 * True to allow multiple menus to be displayed at the same time.
81 allowOtherMenus: false,
84 * @cfg {String} ariaRole @hide
89 * @cfg {Boolean} autoRender @hide
90 * floating is true, so autoRender always happens
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.
98 defaultAlign: 'tl-bl?',
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}.
109 * @cfg {Boolean} @hide
110 * Menus are constrained to the document body by default
115 * @cfg {Boolean} [hidden=undefined]
116 * True to initially render the Menu as hidden, requiring to be shown manually.
118 * Defaults to `true` when `floating: true`, and defaults to `false` when `floating: false`.
122 hideMode: 'visibility',
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.
129 ignoreParentClicks: false,
134 * @cfg {String/Object} layout @hide
138 * @cfg {Boolean} showSeparator
139 * True to show the icon separator.
141 showSeparator : true,
144 * @cfg {Number} minWidth
145 * The minimum width of the Menu.
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.
154 initComponent: function() {
156 prefix = Ext.baseCSSPrefix,
157 cls = [prefix + 'menu'],
158 bodyCls = me.bodyCls ? [me.bodyCls] : [];
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}.
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}
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}
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}
196 Ext.menu.Manager.register(me);
200 cls.push(prefix + 'menu-plain');
202 me.cls = cls.join(' ');
205 bodyCls.unshift(prefix + 'menu-body');
206 me.bodyCls = bodyCls.join(' ');
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.
216 clearInnerCtOnLayout: true,
217 overflowHandler: 'Scroller'
220 // hidden defaults to false if floating is configured as false
221 if (me.floating === false && me.initialConfig.hidden !== true) {
225 me.callParent(arguments);
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);
239 afterRender: function(ct) {
241 prefix = Ext.baseCSSPrefix,
244 me.callParent(arguments);
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',
254 me.focusEl = me.el.createChild({
255 cls: prefix + 'menu-focus',
262 mouseover: me.onMouseOver,
265 me.mouseMonitor = me.el.monitorMouseLeave(100, me.onMouseLeave, me);
267 if (me.showSeparator && ((!Ext.isStrict && Ext.isIE) || Ext.isIE6)) {
268 me.iconSepEl.setHeight(me.el.getHeight());
271 me.keyNav = Ext.create('Ext.menu.KeyNav', me);
274 afterLayout: function() {
276 me.callParent(arguments);
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(),
285 dis = me.dockedItems,
290 innerCtWidth = innerCt.getWidth();
292 newWidth = innerCtWidth + me.body.getBorderWidth('lr') + me.body.getPadding('lr');
294 // First set the body to the new width
295 me.body.setWidth(newWidth);
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();
303 me.el.setWidth(newWidth);
307 getBubbleTarget: function(){
308 return this.parentMenu || this.callParent();
312 * Returns whether a menu item can be activated or not.
315 canActivateItem: function(item) {
316 return item && !item.isDisabled() && item.isVisible() && (item.canActivate || item.getXTypes().indexOf('menuitem') < 0);
320 * Deactivates the current active item on the menu, if one exists.
322 deactivateActiveItem: function() {
326 me.activeItem.deactivate();
327 if (!me.activeItem.activated) {
328 delete me.activeItem;
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;
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:
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;
351 item.el.setWidth(null);
361 me.callParent(arguments);
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);
369 onRemove: function () {
371 this.callParent(arguments);
375 redoComponentLayout: function () {
378 this.doComponentLayout();
383 getFocusEl: function() {
389 this.deactivateActiveItem();
390 this.callParent(arguments);
394 getItemFromEvent: function(e) {
395 return this.getChildByElement(e.getTarget());
398 lookupComponent: function(cmp) {
401 if (Ext.isString(cmp)) {
402 cmp = me.lookupItemFromString(cmp);
403 } else if (Ext.isObject(cmp)) {
404 cmp = me.lookupItemFromObject(cmp);
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;
415 lookupItemFromObject: function(cmp) {
417 prefix = Ext.baseCSSPrefix,
421 if (!cmp.isComponent) {
423 cmp = Ext.create('Ext.menu.' + (Ext.isBoolean(cmp.checked) ? 'Check': '') + 'Item', cmp);
425 cmp = Ext.ComponentManager.create(cmp, cmp.xtype);
429 if (cmp.isMenuItem) {
433 if (!cmp.isMenuItem && !cmp.dock) {
434 cls = [prefix + 'menu-item', prefix + 'menu-item-cmp'];
435 intercept = Ext.Function.createInterceptor;
437 // Wrap focus/blur to control component focus
438 cmp.focus = intercept(cmp.focus, function() {
439 this.$focused = true;
441 cmp.blur = intercept(cmp.blur, function() {
442 this.$focused = false;
445 if (!me.plain && (cmp.indent === true || cmp.iconCls === 'no-icon')) {
446 cls.push(prefix + 'menu-item-indent');
452 cmp.cls = (cmp.cls ? cmp.cls : '') + ' ' + cls.join(' ');
454 cmp.isMenuItem = true;
460 lookupItemFromString: function(cmp) {
461 return (cmp == 'separator' || cmp == '-') ?
462 Ext.createWidget('menuseparator')
463 : Ext.createWidget('menuitem', {
471 onClick: function(e) {
480 if ((e.getTarget() == me.focusEl.dom) || e.within(me.layout.getRenderTarget())) {
481 item = me.getItemFromEvent(e) || me.activeItem;
484 if (item.getXTypes().indexOf('menuitem') >= 0) {
485 if (!item.menu || !me.ignoreParentClicks) {
492 me.fireEvent('click', me, item, e);
496 onDestroy: function() {
499 Ext.menu.Manager.unregister(me);
501 me.el.un(me.mouseMonitor);
505 me.callParent(arguments);
508 onMouseLeave: function(e) {
511 me.deactivateActiveItem();
517 me.fireEvent('mouseleave', me, e);
520 onMouseOver: function(e) {
522 fromEl = e.getRelatedTarget(),
523 mouseEnter = !me.el.contains(fromEl),
524 item = me.getItemFromEvent(e);
526 if (mouseEnter && me.parentMenu) {
527 me.parentMenu.setActiveItem(me.parentItem);
528 me.parentMenu.mouseMonitor.mouseenter();
536 me.setActiveItem(item);
537 if (item.activated && item.expandMenu) {
542 me.fireEvent('mouseenter', me, e);
544 me.fireEvent('mouseover', me, item, e);
547 setActiveItem: function(item) {
550 if (item && (item != me.activeItem && item != me.focusedItem)) {
551 me.deactivateActiveItem();
552 if (me.canActivateItem(item)) {
555 if (item.activated) {
556 me.activeItem = item;
557 me.focusedItem = item;
562 me.focusedItem = item;
565 item.el.scrollIntoView(me.layout.getRenderTarget());
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.
577 showBy: function(cmp, pos, off) {
582 if (me.floating && cmp) {
583 me.layout.autoSize = true;
585 // show off-screen first so that we can calc position without causing a visual jump
587 delete me.needsLayout;
589 // Component or Element
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();
604 doConstrain : function() {
609 returnY = y, normalY, parentEl, scrollTop, viewHeight;
613 full = me.getHeight();
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
621 normalY = y - scrollTop;
622 max = me.maxHeight ? me.maxHeight : viewHeight - normalY;
623 if (full > 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);
632 max = me.getHeight();
634 // Always respect maxHeight
636 max = Math.min(me.maxHeight, max);
638 if (full > max && max > 0){
639 me.layout.autoSize = false;
641 if (me.showSeparator){
642 me.iconSepEl.setHeight(me.layout.getRenderTarget().dom.scrollHeight);
645 vector = me.getConstrainVector(me.el.getScopeParent());
647 me.setPosition(me.getPosition()[0] + vector[0]);