* 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}. * * By default, non {@link Ext.menu.Item menu items} are indented so that they line up with the text of menu items. clearing * the icon column. To make a contained general {@link Ext.Component Component} left aligned configure the child * Component with `indent: false. * * 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} [enableKeyNav=true] * True to enable keyboard navigation for controlling the menu. * This option should generally be disabled when form fields are * being used inside the menu. */ enableKeyNav: true, * @cfg {Boolean} [allowOtherMenus=false] * 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 {Boolean} [floating=true] * 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, * @cfg {Boolean} [ignoreParentClicks=false] * 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} isMenu * `true` in this class to identify an object as an instantiated Menu, or subclass thereof. */ isMenu: true, * @cfg {Ext.enums.Layout/Object} layout * @private */ * @cfg {Boolean} [showSeparator=true] * 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, * @cfg {Boolean} [plain=false] * True to remove the incised line down the left side of the menu and to not indent general Component items. * * {@link Ext.menu.Item MenuItem}s will *always* have space at their start for an icon. With the `plain` setting, * non {@link Ext.menu.Item MenuItem} child components will not be indented to line up. * * Basically, `plain:true` makes a Menu behave more like a regular {@link Ext.layout.container.HBox HBox layout} * {@link Ext.panel.Panel Panel} which just has the same background as a Menu. * * See also the {@link #showSeparator} config. */ 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.push(prefix + 'menu-body', Ext.dom.Element.unselectableCls); 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' }; } if (isFloating) { // only apply the minWidth when we're floating & one hasn't already been set if (me.minWidth === undefined) { me.minWidth = me.defaultMinWidth; } } else { // hidden defaults to false if floating is configured as false me.hidden = !!me.initialConfig.hidden; me.constrain = false; } me.callParent(arguments); }, // They are always global floaters, never contained. registerWithOwnerCt: function() { if (this.floating) { this.ownerCt = null; Ext.WindowManager.register(this); } }, // any container hierarchy. initHierarchyEvents: Ext.emptyFn, isVisible: function() { return this.callParent(); }, // Ignore hiddenness from the ancestor hierarchy, override it with local hidden state. getHierarchyState: function() { var result = this.callParent(); result.hidden = this.hidden; return result; }, 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'; } }, var me = this; me.callParent(arguments); // TODO: Move this to a subTemplate When we support them in the future if (me.showSeparator) { me.iconSepEl = me.layout.getElementTarget().insertFirst({ cls: Ext.baseCSSPrefix + 'menu-icon-separator', html: ' ' }); } me.mon(me.el, { click: me.onClick, mouseover: me.onMouseOver, scope: me }); me.mouseMonitor = me.el.monitorMouseLeave(100, me.onMouseLeave, me); // A Menu is a Panel. The KeyNav can use the Panel's KeyMap if (me.enableKeyNav) { me.keyNav = new Ext.menu.KeyNav({ target: me, keyMap: me.getKeyMap() }); } }, // 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; } }, getFocusEl: function() { return this.focusedItem || this.el; }, hide: function() { this.deactivateActiveItem(true); this.callParent(arguments); }, getItemFromEvent: function(e) { return this.getChildByElement(e.getTarget()); }, 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; }, 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-cmp']; // The "plain" setting means that the menu does not look so much like a menu. It's more like a grey Panel. // So it has no vertical separator. // Plain menus also will not indent non MenuItem components; there is nothing to indent them to the right of. if (!me.plain && (cmp.indent !== false || cmp.iconCls === 'no-icon')) { cls.push(prefix + 'menu-item-indent'); } if (cmp.rendered) { cmp.el.addCls(cls); } else { cmp.cls = (cmp.cls || '') + ' ' + cls.join(' '); } } return cmp; }, lookupItemFromString: function(cmp) { return (cmp == 'separator' || cmp == '-') ? new Ext.menu.Separator() : new Ext.menu.Item({ canActivate: false, hideOnClick: false, plain: true, text: cmp }); }, 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); }, var me = this; Ext.menu.Manager.unregister(me); me.parentMenu = me.ownerButton = null; if (me.rendered) { me.el.un(me.mouseMonitor); Ext.destroy(me.keyNav); me.keyNav = null; } me.callParent(arguments); }, var me = this; me.deactivateActiveItem(); if (me.disabled) { return; } me.fireEvent('mouseleave', me, 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); }, 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()); } }, var me = this; me.callParent(arguments); if (!me.hidden) { // show may have been vetoed me.setVerticalPosition(); } return me; }, var me = this, viewHeight; // Constrain the height to the containing element's viewable area if (me.floating) { me.savedMaxHeight = me.maxHeight; viewHeight = me.container.getViewSize().height; me.maxHeight = Math.min(me.maxHeight || viewHeight, viewHeight); } me.callParent(arguments); }, var me = this; me.callParent(arguments); // Restore configured maxHeight if (me.floating) { me.maxHeight = me.savedMaxHeight; } }, // 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.getY(), returnY = y, height = me.getHeight(), viewportHeight = Ext.Element.getViewportHeight().height, parentEl = me.el.parent(), 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.setY(returnY); } });