/** * 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.Panel#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.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, hideMode: 'visibility', /** * @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, defaultMinWidth: 120, /** * @cfg {String} [defaultAlign="tl-bl?"] * The default {@link Ext.util.Positionable#getAlignToXY Ext.dom.Element#getAlignToXY} anchor position value for this menu * relative to its owner. Used in conjunction with {@link #showBy}. */ defaultAlign: 'tl-bl?', /** * @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. */ focusable: true, // private baseCls: Ext.baseCSSPrefix + 'menu', _iconSeparatorCls: Ext.baseCSSPrefix + 'menu-icon-separator', _itemCmpCls: Ext.baseCSSPrefix + 'menu-item-cmp', /** * @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.event.Event} e The underlying {@link Ext.event.Event}. */ /** * @event mouseenter * Fires when the mouse enters this menu * @param {Ext.menu.Menu} menu The menu * @param {Ext.event.Event} e The underlying {@link Ext.event.Event} */ /** * @event mouseleave * Fires when the mouse leaves this menu * @param {Ext.menu.Menu} menu The menu * @param {Ext.event.Event} e The underlying {@link Ext.event.Event} */ /** * @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.event.Event} e The underlying {@link Ext.event.Event} */ layout: { type: 'vbox', align: 'stretchmax', overflowHandler: 'Scroller' }, initComponent: function() { var me = this, cls = [Ext.baseCSSPrefix + 'menu'], bodyCls = me.bodyCls ? [me.bodyCls] : [], isFloating = me.floating !== false; Ext.menu.Manager.register(me); // Menu classes if (me.plain) { cls.push(Ext.baseCSSPrefix + 'menu-plain'); } me.cls = cls.join(' '); // Menu body classes bodyCls.push(Ext.baseCSSPrefix + 'menu-body', Ext.dom.Element.unselectableCls); me.bodyCls = bodyCls.join(' '); 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); // Configure items prior to render with special classes to align // non MenuItem child components with their MenuItem siblings. Ext.override(me.getLayout(), { configureItem: me.configureItem }); }, // Private implementation for Menus. They are a special case, in that in the vast majority // (nearly all?) of use cases they shouldn't be constrained to anything other than the viewport. // See EXTJS-13596. initFloatConstrain: Ext.emptyFn, // Menus do not have owning containers on which they depend for visibility. They stand outside // any container hierarchy. initHierarchyEvents: Ext.emptyFn, // As menus are never contained, a Menu's visibility only ever depends upon its own hidden state. // Ignore hiddenness from the ancestor hierarchy, override it with local hidden state. getInherited: function() { var result = this.callParent(); result.hidden = this.hidden; return result; }, 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, listeners = { click: me.onClick, mouseover: me.onMouseOver, scope: me }, iconSeparatorCls = me._iconSeparatorCls; if (Ext.supports.Touch) { listeners.pointerdown = me.onMouseOver; me.mon(Ext.GlobalEvents, 'mousedown', me.onDocMouseDown, me); } me.callParent(arguments); // TODO: Move this to a subTemplate When we support them in the future if (me.showSeparator) { me.iconSepEl = me.body.insertFirst({ role: 'presentation', cls: iconSeparatorCls + ' ' + iconSeparatorCls + '-' + me.ui, html: ' ' }); } me.mon(me.el, listeners); 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() }); } }, /** * Returns whether a menu item can be activated or not. * @return {Boolean} */ canActivateItem: function(item) { return item && !item.isDisabled() && item.isVisible() && (item.canActivate || !item.isMenuItem); }, /** * 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; } }, // @inheritdoc hide: function() { this.deactivateActiveItem(true); this.callParent(arguments); }, // @private getItemFromEvent: function(e) { var me = this, renderTarget = me.layout.getRenderTarget().dom, toEl = e.getTarget(); // See which top level element the event is in and find its owning Component. while (toEl.parentNode !== renderTarget) { toEl = toEl.parentNode; if (!toEl) { return; } } return Ext.getCmp(toEl.id); }, 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; 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; } 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 }); }, // Override applied to the Menu's layout. Runs in the context of the layout. // Add special classes to allow non MenuItem components to coexist with MenuItems. // If there is only *one* child, then this Menu is just a vehicle for floating // and aligning the component, so do not do this. configureItem: function(cmp) { var me = this.owner, prefix = Ext.baseCSSPrefix, ui = me.ui, cls, cmpCls; if (cmp.isMenuItem) { cmp.setUI(ui); } else if (me.items.getCount() > 1 && !cmp.rendered && !cmp.dock) { cmpCls = me._itemCmpCls; cls = [cmpCls + ' ' + cmpCls + '-' + ui]; // 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-' + ui); } if (cmp.rendered) { cmp.el.addCls(cls); } else { cmp.cls = (cmp.cls || '') + ' ' + cls.join(' '); } } // @noOptimize.callParent this.callParent(arguments); }, onClick: function(e) { var me = this, type = e.type, item; if (me.disabled) { e.stopEvent(); return; } // if e.type !== 'keydown', then we're dealing with a click or tap event item = (type !== 'keydown') ? 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); me.parentMenu = me.ownerCmp = null; if (me.rendered) { me.el.un(me.mouseMonitor); Ext.destroy(me.keyNav); me.keyNav = null; } me.callParent(arguments); }, onDocMouseDown: function(e) { if (!this.owns(e.target)) { this.deactivateActiveItem(); } }, 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, ownerCmp = me.ownerCmp; if (mouseEnter && parentMenu) { parentMenu.setActiveItem(ownerCmp); ownerCmp.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) { // Activate passing skipCheck flag. We checked using me.canActivate() item.activate(true); if (item.activated) { me.activeItem = item; me.focusedItem = item; } } else { item.focus(); me.focusedItem = item; } } // Activating will focus, focusing will scroll the item into view. } }, beforeShow: function() { 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); }, afterShow: function() { var me = this; me.callParent(arguments); // Restore configured maxHeight if (me.floating) { me.maxHeight = me.savedMaxHeight; } }, privates: { getFocusEl: function() { return this.el; } }});