/** * 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 #cfg-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.Manager', 'Ext.menu.Separator' ], mixins: [ 'Ext.util.FocusableContainer' ], /** * @property {Ext.menu.Menu} parentMenu * The parent Menu of this Menu. */ /** * @cfg {Boolean} [enableKeyNav=true] * @deprecated 5.1.0 Intra-menu key navigation is always enabled. */ 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#cfg-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] * 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 #cfg-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. */ focusOnToFront: false, bringParentToFront: false, defaultFocus: ':focusable', /** * @private */ menuClickBuffer: 0, 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, listeners = { element: 'el', click: me.onClick, mouseover: me.onMouseOver, scope: me }; if (Ext.supports.Touch) { listeners.pointerdown = me.onMouseOver; } me.on(listeners); me.on({ beforeshow: me.onBeforeShow, scope: 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. /** * @private */ initFloatConstrain: Ext.emptyFn, getInherited: function() { // 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. var result = this.callParent(); result.hidden = this.hidden; return result; }, beforeRender: function() { var me = this; me.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 (!me.getSizeModel().width.shrinkWrap) { me.layout.align = 'stretch'; } if (me.floating) { me.ariaRenderAttributes = me.ariaRenderAttributes || {}; me.ariaRenderAttributes['aria-expanded'] = !!me.autoShow; } }, onBoxReady: function() { var me = this, iconSeparatorCls = me._iconSeparatorCls, keyNav = me.focusableKeyNav; // Keyboard handling can be disabled, e.g. by the DatePicker menu // or the Date filter menu constructed by the Grid if (keyNav) { keyNav.map.processEventScope = me; keyNav.map.processEvent = function(e) { // ESC may be from input fields, and FocusableContainers ignore keys from // input fields. We do not want to ignore ESC. ESC hide menus. if (e.keyCode === e.ESC) { e.target = this.el.dom; } return e; }; // Handle ESC key keyNav.map.addBinding([{ key: Ext.event.Event.ESC, handler: me.onEscapeKey, scope: me }, // Handle character shortcuts { key: /[\w]/, handler: me.onShortcutKey, scope: me, shift: false, ctrl: false, alt: false }]); } else { // Even when FocusableContainer key event processing is disabled, // we still need to handle the Escape key! me.escapeKeyNav = new Ext.util.KeyNav(me.el, { eventName: 'keydown', scope: me, esc: me.onEscapeKey }); } 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: ' ' }); } // Modern IE browsers have click events translated to PointerEvents, and b/c of this the // event isn't being canceled like it needs to be. So, we need to add an extra listener. if (Ext.supports.MSPointerEvents || Ext.supports.PointerEvents) { me.el.on({ scope: me, click: me.preventClick, translate: false }); } me.mouseMonitor = me.el.monitorMouseLeave(100, me.onMouseLeave, me); }, onFocusLeave: function(e) { var me = this; me.callParent([e]); me.mixins.focusablecontainer.onFocusLeave.call(me, e); if (me.floating) { me.hide(); } }, /** * @param {Ext.Component} item The child item to test for focusability. * Returns whether a menu item can be activated or not. * @return {Boolean} `true` if the passed item is focusable. */ canActivateItem: function(item) { return item && item.isFocusable(); }, /** * Deactivates the current active item on the menu, if one exists. */ deactivateActiveItem: function() { var me = this, activeItem = me.lastFocusedChild; if (activeItem) { activeItem.blur(); } }, /** * @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 non-docked child components (Menu extends Panel) // so it's accounted for in our VBox layout if (!cmp.dock) { 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(' '); } // So we can clean the item if it gets removed. cmp.$extraMenuCls = cls; } // @noOptimize.callParent this.callParent(arguments); }, onRemove: function(cmp) { this.callParent([cmp]); // Remove any extra classes we added to non-MenuItem child items if (!cmp.destroyed && cmp.$extraMenuCls) { cmp.el.removeCls(cmp.$extraMenuCls); } }, onClick: function(e) { var me = this, type = e.type, item, clickResult, iskeyEvent = type === 'keydown'; if (me.disabled) { e.stopEvent(); return; } item = me.getItemFromEvent(e); if (item && item.isMenuItem) { if (!item.menu || !me.ignoreParentClicks) { clickResult = item.onClick(e); } else { e.stopEvent(); } // SPACE and ENTER invokes the menu if (item.menu && clickResult !== false && iskeyEvent) { item.expandMenu(e, 0); } } // 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; if (me.escapeKeyNav) { me.escapeKeyNav.destroy(); } me.parentMenu = me.ownerCmp = me.escapeKeyNav = null; if (me.rendered) { me.el.un(me.mouseMonitor); Ext.destroy(me.iconSepEl); } // Menu can be destroyed while shown; // we should notify the Manager Ext.menu.Manager.onHide(me); me.callParent(arguments); }, onMouseLeave: function(e) { if (this.disabled) { return; } this.fireEvent('mouseleave', this, 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) { if (!item.containsFocus) { item.focus(); } if (item.expandMenu) { item.expandMenu(e); } } if (mouseEnter) { me.fireEvent('mouseenter', me, e); } me.fireEvent('mouseover', me, item, e); }, setActiveItem: function(item) { var me = this; if (item && (item !== me.lastFocusedChild)) { me.focusChild(item, 1); // Focusing will scroll the item into view. } }, onEscapeKey: function() { if (this.floating) { this.hide(); } }, onShortcutKey: function(keyCode, e) { var shortcutChar = String.fromCharCode(e.getCharCode()), items = this.query('>[text]'), len = items.length, item = this.lastFocusedChild, focusIndex = Ext.Array.indexOf(items, item), i = focusIndex; // Loop through all items which have a text property starting at the one after the current focus. for (;;) { if (++i === len) { i = 0; } item = items[i]; // Looped back to start - no matches if (i === focusIndex) { return; } // Found a text match if (item.text && item.text[0].toUpperCase() === shortcutChar) { item.focus(); return; } } }, // Tabbing in a floating menu must hide, but not move focus. // onHide takes care of moving focus back to an owner Component. onFocusableContainerTabKey: function(e) { if (this.floating) { this.hide(); } }, onFocusableContainerEnterKey: function(e) { this.onClick(e); }, onFocusableContainerSpaceKey: function(e) { this.onClick(e); }, onFocusableContainerLeftKey: function(e) { // The default action is to scroll the nearest horizontally scrollable container e.preventDefault(); // If we are a submenu, then left arrow focuses the owning MenuItem if (this.parentMenu) { this.ownerCmp.focus(); this.hide(); } }, onFocusableContainerRightKey: function(e) { var me = this, focusItem = me.lastFocusedChild; // See above e.preventDefault(); if (focusItem && focusItem.expandMenu) { focusItem.expandMenu(e, 0); } }, onBeforeShow: function() { // Do not allow show immediately after a hide if (Ext.Date.getElapsed(this.lastHide) < this.menuClickBuffer) { return false; } }, beforeShow: function() { var me = this, parent, activeEl, viewHeight; // Constrain the height to the containing element's viewable area if (me.floating) { parent = me.hasFloatMenuParent(); if (!parent && !me.allowOtherMenus) { Ext.menu.Manager.hideAll(); } // Only register a focusAnchor to return to on hide if the active element // is not the document; if we have a floating parent menu, use its focusAnchor. // If there's no focusAnchor, we return to the ownerCmp, or first focusable ancestor. if (parent) { me.focusAnchor = parent.focusAnchor; } else { activeEl = Ext.Element.getActiveElement(); // IE8 sometimes allow <html> node to focus if (activeEl === document.body || activeEl === document.documentElement) { me.focusAnchor = null; } else { me.focusAnchor = activeEl; } } 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, ariaDom = me.ariaEl.dom; me.callParent(arguments); Ext.menu.Manager.onShow(me); if (me.floating && ariaDom) { ariaDom.setAttribute('aria-expanded', true); } // Restore configured maxHeight if (me.floating && me.autoFocus) { me.maxHeight = me.savedMaxHeight; me.focus(); } }, onHide: function(animateTarget, cb, scope) { var me = this, ariaDom = me.ariaEl.dom, focusTarget; // If we contain focus just before element hide, move it elsewhere before hiding if (me.el.contains(Ext.Element.getActiveElement())) { // focusAnchor was the active element before this menu was shown. focusTarget = me.focusAnchor || me.ownerCmp || me.up(':focusable'); // Component hide processing will focus the "previousFocus" element. if (focusTarget) { me.previousFocus = focusTarget; } } me.callParent([animateTarget, cb, scope]); me.lastHide = Ext.Date.now(); Ext.menu.Manager.onHide(me); if (me.floating && ariaDom) { ariaDom.setAttribute('aria-expanded', false); } }, preventClick: function (e) { var item = this.getItemFromEvent(e); if (item && item.isMenuItem && !item.href) { e.preventDefault(); } }, privates: { hasFloatMenuParent: function() { return this.parentMenu || this.up('menu[floating=true]'); }, setOwnerCmp: function(comp, instanced) { var me = this; me.parentMenu = comp.isMenuItem ? comp : null; me.ownerCmp = comp; me.registerWithOwnerCt(); delete me.hierarchicallyHidden; me.onInheritedAdd(comp, instanced); me.containerOnAdded(comp, instanced); } }});