* A menu object. This is the container to which you may add {@link Ext.menu.Item menu items}. * * Menus may contain either {@link Ext.menu.Item menu items}, or general {@link Ext.Component Components}. * Menus may also contain {@link Ext.panel.AbstractPanel#dockedItems docked items} because it extends {@link Ext.panel.Panel}. * * To make a contained general {@link Ext.Component Component} line up with other {@link Ext.menu.Item menu items}, * specify `{@link Ext.menu.Item#plain plain}: true`. This reserves a space for an icon, and indents the Component * in line with the other menu items. * * By default, Menus are absolutely positioned, floating Components. By configuring a Menu with `{@link #floating}: false`, * a Menu may be used as a child of a {@link Ext.container.Container Container}. * * @example * Ext.create('Ext.menu.Menu', { * width: 100, * margin: '0 0 10 0', * floating: false, // usually you want this set to True (default) * renderTo: Ext.getBody(), // usually rendered by it's containing component * items: [{ * text: 'regular item 1' * },{ * text: 'regular item 2' * },{ * text: 'regular item 3' * }] * }); * * Ext.create('Ext.menu.Menu', { * width: 100, * plain: true, * floating: false, // usually you want this set to True (default) * renderTo: Ext.getBody(), // usually rendered by it's containing component * items: [{ * text: 'plain item 1' * },{ * text: 'plain item 2' * },{ * text: 'plain item 3' * }] * }); */ Ext.define('Ext.menu.Menu', { extend: 'Ext.panel.Panel', alias: 'widget.menu', requires: [ 'Ext.layout.container.Fit', 'Ext.layout.container.VBox', 'Ext.menu.CheckItem', 'Ext.menu.Item', 'Ext.menu.KeyNav', 'Ext.menu.Manager', 'Ext.menu.Separator' ], * @property {Ext.menu.Menu} parentMenu * The parent Menu of this Menu. */ * @cfg {Boolean} allowOtherMenus * True to allow multiple menus to be displayed at the same time. */ allowOtherMenus: false, * @cfg {String} ariaRole * @private */ ariaRole: 'menu', * @cfg {Boolean} autoRender * Floating is true, so autoRender always happens. * @private */ * @cfg {String} defaultAlign * The default {@link Ext.Element#getAlignToXY Ext.Element#getAlignToXY} anchor position value for this menu * relative to its element of origin. */ defaultAlign: 'tl-bl?', * @cfg {Boolean} floating * A Menu configured as `floating: true` (the default) will be rendered as an absolutely positioned, * {@link Ext.Component#floating floating} {@link Ext.Component Component}. If configured as `floating: false`, the Menu may be * used as a child item of another {@link Ext.container.Container Container}. */ floating: true, * @cfg {Boolean} constrain * Menus are constrained to the document body by default. * @private */ constrain: true, * @cfg {Boolean} [hidden=undefined] * True to initially render the Menu as hidden, requiring to be shown manually. * * Defaults to `true` when `floating: true`, and defaults to `false` when `floating: false`. */ hidden: true, hideMode: 'visibility', * @cfg {Boolean} ignoreParentClicks * True to ignore clicks on any item in this menu that is a parent item (displays a submenu) * so that the submenu is not dismissed when clicking the parent item. */ ignoreParentClicks: false, * @property {Boolean} isAction * `true` in this class to identify an object as an instantiated Menu, or subclass thereof. */ isMenu: true, * @cfg {String/Object} layout * @private */ * @cfg {Boolean} showSeparator * True to show the icon separator. */ showSeparator : true, * @cfg {Number} [minWidth=120] * The minimum width of the Menu. The default minWidth only applies when the {@link #floating} config is true. */ minWidth: undefined, defaultMinWidth: 120, * @cfg {Boolean} [plain=false] * True to remove the incised line down the left side of the menu and to not indent general Component items. */ initComponent: function() { var me = this, prefix = Ext.baseCSSPrefix, cls = [prefix + 'menu'], bodyCls = me.bodyCls ? [me.bodyCls] : [], isFloating = me.floating !== false; me.addEvents( * @event click * Fires when this menu is clicked * @param {Ext.menu.Menu} menu The menu which has been clicked * @param {Ext.Component} item The menu item that was clicked. `undefined` if not applicable. * @param {Ext.EventObject} e The underlying {@link Ext.EventObject}. */ 'click', * @event mouseenter * Fires when the mouse enters this menu * @param {Ext.menu.Menu} menu The menu * @param {Ext.EventObject} e The underlying {@link Ext.EventObject} */ 'mouseenter', * @event mouseleave * Fires when the mouse leaves this menu * @param {Ext.menu.Menu} menu The menu * @param {Ext.EventObject} e The underlying {@link Ext.EventObject} */ 'mouseleave', * @event mouseover * Fires when the mouse is hovering over this menu * @param {Ext.menu.Menu} menu The menu * @param {Ext.Component} item The menu item that the mouse is over. `undefined` if not applicable. * @param {Ext.EventObject} e The underlying {@link Ext.EventObject} */ 'mouseover' ); Ext.menu.Manager.register(me); // Menu classes if (me.plain) { cls.push(prefix + 'menu-plain'); } me.cls = cls.join(' '); // Menu body classes bodyCls.unshift(prefix + 'menu-body'); me.bodyCls = bodyCls.join(' '); // Internal vbox layout, with scrolling overflow // Placed in initComponent (rather than prototype) in order to support dynamic layout/scroller // options if we wish to allow for such configurations on the Menu. // e.g., scrolling speed, vbox align stretch, etc. if (!me.layout) { me.layout = { type: 'vbox', align: 'stretchmax', overflowHandler: 'Scroller' }; } // only apply the minWidth when we're floating & one hasn't already been set if (isFloating && me.minWidth === undefined) { me.minWidth = me.defaultMinWidth; } // hidden defaults to false if floating is configured as false if (!isFloating && me.initialConfig.hidden !== true) { me.hidden = false; } me.callParent(arguments); me.on('beforeshow', function() { var hasItems = !!me.items.length; // FIXME: When a menu has its show cancelled because of no items, it // gets a visibility: hidden applied to it (instead of the default display: none) // Not sure why, but we remove this style when we want to show again. if (hasItems && me.rendered) { me.el.setStyle('visibility', null); } return hasItems; }); }, beforeRender: function() { this.callParent(arguments); // Menus are usually floating: true, which means they shrink wrap their items. // However, when they are contained, and not auto sized, we must stretch the items. if (!this.getSizeModel().width.shrinkWrap) { this.layout.align = 'stretch'; } }, onBoxReady: function() { var me = this, separatorSpec; me.callParent(arguments); // TODO: Move this to a subTemplate When we support them in the future if (me.showSeparator) { separatorSpec = { cls: Ext.baseCSSPrefix + 'menu-icon-separator', html: ' ' }; if ((!Ext.isStrict && Ext.isIE) || Ext.isIE6) { separatorSpec.style = 'height:' + me.el.getHeight() + 'px'; } me.iconSepEl = me.layout.getElementTarget().insertFirst(separatorSpec); } me.mon(me.el, { click: me.onClick, mouseover: me.onMouseOver, scope: me }); me.mouseMonitor = me.el.monitorMouseLeave(100, me.onMouseLeave, me); me.keyNav = new Ext.menu.KeyNav(me); }, getBubbleTarget: function() { // If a submenu, this will have a parentMenu property // If a menu of a Button, it will have an ownerButton property // Else use the default method. return this.parentMenu || this.ownerButton || this.callParent(arguments); }, * Returns whether a menu item can be activated or not. * @return {Boolean} */ canActivateItem: function(item) { return item && !item.isDisabled() && item.isVisible() && (item.canActivate || item.getXTypes().indexOf('menuitem') < 0); }, * Deactivates the current active item on the menu, if one exists. */ deactivateActiveItem: function(andBlurFocusedItem) { var me = this, activeItem = me.activeItem, focusedItem = me.focusedItem; if (activeItem) { activeItem.deactivate(); if (!activeItem.activated) { delete me.activeItem; } } // Blur the focused item if we are being asked to do that too // Only needed if we are being hidden - mouseout does not blur. if (focusedItem && andBlurFocusedItem) { focusedItem.blur(); delete me.focusedItem; } }, // inherit docs getFocusEl: function() { return this.focusedItem || this.el; }, // inherit docs hide: function() { this.deactivateActiveItem(true); this.callParent(arguments); }, // private getItemFromEvent: function(e) { return this.getChildByElement(e.getTarget()); }, lookupComponent: function(cmp) { var me = this; if (typeof cmp == 'string') { cmp = me.lookupItemFromString(cmp); } else if (Ext.isObject(cmp)) { cmp = me.lookupItemFromObject(cmp); } // Apply our minWidth to all of our child components so it's accounted // for in our VBox layout cmp.minWidth = cmp.minWidth || me.minWidth; return cmp; }, // private lookupItemFromObject: function(cmp) { var me = this, prefix = Ext.baseCSSPrefix, cls; if (!cmp.isComponent) { if (!cmp.xtype) { cmp = Ext.create('Ext.menu.' + (Ext.isBoolean(cmp.checked) ? 'Check': '') + 'Item', cmp); } else { cmp = Ext.ComponentManager.create(cmp, cmp.xtype); } } if (cmp.isMenuItem) { cmp.parentMenu = me; } if (!cmp.isMenuItem && !cmp.dock) { cls = [prefix + 'menu-item', prefix + 'menu-item-cmp']; if (!me.plain && (cmp.indent === true || cmp.iconCls === 'no-icon')) { cls.push(prefix + 'menu-item-indent'); } if (cmp.rendered) { cmp.el.addCls(cls); } else { cmp.cls = (cmp.cls ? cmp.cls : '') + ' ' + cls.join(' '); } } return cmp; }, // private lookupItemFromString: function(cmp) { return (cmp == 'separator' || cmp == '-') ? new Ext.menu.Separator() : new Ext.menu.Item({ canActivate: false, hideOnClick: false, plain: true, text: cmp }); }, onClick: function(e) { var me = this, item; if (me.disabled) { e.stopEvent(); return; } item = (e.type === 'click') ? me.getItemFromEvent(e) : me.activeItem; if (item && item.isMenuItem) { if (!item.menu || !me.ignoreParentClicks) { item.onClick(e); } else { e.stopEvent(); } } // Click event may be fired without an item, so we need a second check if (!item || item.disabled) { item = undefined; } me.fireEvent('click', me, item, e); }, onDestroy: function() { var me = this; Ext.menu.Manager.unregister(me); delete me.parentMenu; delete me.ownerButton; if (me.rendered) { me.el.un(me.mouseMonitor); me.keyNav.destroy(); delete me.keyNav; } me.callParent(arguments); }, onMouseLeave: function(e) { var me = this; me.deactivateActiveItem(); if (me.disabled) { return; } me.fireEvent('mouseleave', me, e); }, onMouseOver: function(e) { var me = this, fromEl = e.getRelatedTarget(), mouseEnter = !me.el.contains(fromEl), item = me.getItemFromEvent(e), parentMenu = me.parentMenu, parentItem = me.parentItem; if (mouseEnter && parentMenu) { parentMenu.setActiveItem(parentItem); parentItem.cancelDeferHide(); parentMenu.mouseMonitor.mouseenter(); } if (me.disabled) { return; } // Do not activate the item if the mouseover was within the item, and it's already active if (item && !item.activated) { me.setActiveItem(item); if (item.activated && item.expandMenu) { item.expandMenu(); } } if (mouseEnter) { me.fireEvent('mouseenter', me, e); } me.fireEvent('mouseover', me, item, e); }, setActiveItem: function(item) { var me = this; if (item && (item != me.activeItem)) { me.deactivateActiveItem(); if (me.canActivateItem(item)) { if (item.activate) { item.activate(); if (item.activated) { me.activeItem = item; me.focusedItem = item; me.focus(); } } else { item.focus(); me.focusedItem = item; } } item.el.scrollIntoView(me.layout.getRenderTarget()); } }, * Shows the floating menu by the specified {@link Ext.Component Component} or {@link Ext.Element Element}. * @param {Ext.Component/Ext.Element} component The {@link Ext.Component} or {@link Ext.Element} to show the menu by. * @param {String} [position] Alignment position as used by {@link Ext.Element#getAlignToXY}. * Defaults to `{@link #defaultAlign}`. * @param {Number[]} [offsets] Alignment offsets as used by {@link Ext.Element#getAlignToXY}. * @return {Ext.menu.Menu} This Menu. */ showBy: function(cmp, pos, off) { var me = this; if (me.floating && cmp) { me.show(); // Align to Component or Element using setPagePosition because normal show // methods are container-relative, and we must align to the requested element // or Component: me.setPagePosition(me.el.getAlignToXY(cmp.el || cmp, pos || me.defaultAlign, off)); me.setVerticalPosition(); } return me; }, show: function() { var me = this, parentEl, viewHeight, result, maxWas = me.maxHeight; // we need to get scope parent for height constraint if (!me.rendered){ me.doAutoRender(); } // constrain the height to the curren viewable area if (me.floating) { //if our reset css is scoped, there will be a x-reset wrapper on this menu which we need to skip parentEl = Ext.fly(me.el.getScopeParent()); viewHeight = parentEl.getViewSize().height; me.maxHeight = Math.min(maxWas || viewHeight, viewHeight); } result = me.callParent(arguments); me.maxHeight = maxWas; return result; }, afterComponentLayout: function(width, height, oldWidth, oldHeight){ var me = this; me.callParent(arguments); // fixup the separator if (me.showSeparator){ me.iconSepEl.setHeight(me.componentLayout.lastComponentSize.contentHeight); } }, // private // adjust the vertical position of the menu if the height of the // menu is equal (or greater than) the viewport size setVerticalPosition: function(){ var me = this, max, y = me.el.getY(), returnY = y, height = me.getHeight(), viewportHeight = Ext.Element.getViewportHeight().height, parentEl = Ext.fly(me.el.getScopeParent()), viewHeight = parentEl.getViewSize().height, normalY = y - parentEl.getScroll().top; // factor in scrollTop of parent parentEl = null; if (me.floating) { max = me.maxHeight ? me.maxHeight : viewHeight - normalY; if (height > viewHeight) { returnY = y - normalY; } else if (max < height) { returnY = y - (height - max); } else if((y + height) > viewportHeight){ // keep the document from scrolling returnY = viewportHeight - height; } } me.el.setY(returnY); } });