/**
* 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'
],
defaultType: 'menuitem',
/**
* @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,
/**
* @cfg {Number} [mouseLeaveDelay]
* The delay in ms as to how long the framework should wait before firing a mouseleave event.
* This allows submenus not to be collapsed while hovering other menu items.
*
* Defaults to 50
*/
mouseLeaveDelay: 50,
/**
* @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.
*/
/**
* @cfg focusOnToFront
* @inheritdoc
*/
focusOnToFront: false,
bringParentToFront: false,
alignOnScroll: false,
// Menus are focusable
focusable: true,
tabIndex: -1,
focusableContainer: true,
// When a Menu is used as a carrier to float some focusable Component such as a DatePicker
// or ColorPicker
// This will be used to delegate focus to its focusable child.
// In normal usage, a Menu is a FocusableContainer, and this will not be consulted.
defaultFocus: ':focusable',
// We need to focus disabled menu items when arrowing as per WAI-ARIA:
// http://www.w3.org/TR/wai-aria-practices/#menu
allowFocusingDisabledChildren: true,
/**
* @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();
// 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
});
me.itemOverTask = new Ext.util.DelayedTask(me.handleItemOver, me);
},
// 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.
/**
* @method
* @private
*/
initFloatConstrain: Ext.emptyFn,
getInherited: function() {
// As floating menus are never contained, a floating 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();
if (this.floating) {
result.hidden = this.hidden;
}
return result;
},
beforeRender: function() {
var me = this;
me.callParent();
// 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(width, height) {
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) {
// 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({
target: me.el,
eventName: 'keydown',
scope: me,
esc: me.onEscapeKey
});
}
me.callParent([width, height]);
// 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.
// For devices that have touch support, the default click event may be a gesture that
// runs asynchronously, so by the time we try and prevent it, it's already happened
if (Ext.supports.Touch || Ext.supports.MSPointerEvents || Ext.supports.PointerEvents) {
me.el.on({
scope: me,
click: me.preventClick,
translate: false
});
}
me.mouseMonitor = me.el.monitorMouseLeave(me.mouseLeaveDelay, me.onMouseLeave, me);
},
onFocusEnter: function(e) {
var me = this,
hierarchyState;
me.callParent([e]);
me.mixins.focusablecontainer.onFocusEnter.call(me, e);
if (me.floating) {
hierarchyState = me.getInherited();
// The topmost focusEnter event upon entry into a floating menu stack
// is recorded in the hierarchy state.
//
// Focusing upwards from descendant menus in a stack will NOT trigger onFocusEnter
// on the parent menu because focus is already in its component tree.
// For focusing downwards we check for presence of the topmostFocusEvent
// already being present in the hierarchy.
//
// If we need to explicitly access a focus reversion point, we can use that.
// This is only ever needed if tabbing forwards from the menu. We explicitly
// push focus to the topmost focusEnter component, and then allow natural
// tabbing to proceed from there.
//
// In all other focus reversion scenarios we use the immediate focusEnter event
if (!hierarchyState.topmostFocusEvent) {
hierarchyState.topmostFocusEvent = e;
}
}
},
onFocusLeave: function(e) {
var me = this;
me.callParent([e]);
// We need to make sure that menus do not "remember" the last focused item
// so that the first menu item is always activated when the menu is shown.
// This is the expected behavior according to WAI-ARIA spec.
me.lastFocusedChild = null;
me.mixins.focusablecontainer.onFocusLeave.call(me, e);
if (me.floating) {
me.hide();
}
},
handleItemOver: function(e, item) {
// Only focus non-menuitem on real mouseover events.
if (!item.containsFocus && (e.pointerType === 'mouse' || item.isMenuItem)) {
item.focus();
}
if (item.expandMenu) {
item.expandMenu(e);
}
},
/**
* @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') {
if (cmp[0] === '@') {
cmp = this.callParent([cmp]);
}
else {
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 type = this.defaultType;
if (!cmp.isComponent) {
if (!cmp.xtype && Ext.isBoolean(cmp.checked)) {
type = 'menucheckitem';
}
cmp = Ext.ComponentManager.create(cmp, type);
}
if (cmp.isMenuItem) {
cmp.parentMenu = this;
}
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();
}
// Click handler on the item could have destroyed the menu
if (me.destroyed) {
return;
}
// 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);
},
doDestroy: function() {
var me = this;
if (me.escapeKeyNav) {
me.escapeKeyNav.destroy();
}
me.itemOverTask.cancel();
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();
},
onMouseLeave: function(e) {
var me = this;
if (me.itemOverTask) {
me.itemOverTask.cancel();
}
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();
parentMenu.itemOverTask.cancel();
}
if (me.disabled) {
return;
}
// Do not activate the item if the mouseover was within the item, and it's already active
if (item) {
// Activate the item in time specified by mouseLeaveDelay.
// If we mouseout, or move to another item this invocation will be canceled.
if (e.pointerType === 'touch') {
me.handleItemOver(e, item);
}
else {
me.itemOverTask.delay(me.expanded ? me.mouseLeaveDelay : 0, null, null, [e, item]);
}
}
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;
if (len === 0) {
return;
}
// 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;
}
}
},
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;
// Constrain the height to the containing element's viewable area
if (me.floating) {
parent = me.hasFloatMenuParent();
if (!parent && !me.allowOtherMenus) {
Ext.menu.Manager.hideAll();
}
}
me.callParent();
},
afterShow: function(animateTarget, callback, scope) {
var me = this,
ariaDom = me.ariaEl.dom;
me.callParent([animateTarget, callback, scope]);
Ext.menu.Manager.onShow(me);
if (me.parentMenu) {
me.parentMenu.expanded = true;
}
if (me.floating && ariaDom) {
ariaDom.setAttribute('aria-expanded', true);
}
// Restore configured maxHeight
if (me.floating) {
me.maxHeight = me.savedMaxHeight;
}
if (me.autoFocus) {
me.focus();
}
},
onHide: function(animateTarget, cb, scope) {
var me = this,
ariaDom = me.ariaEl.dom;
me.callParent([animateTarget, cb, scope]);
me.lastHide = Ext.Date.now();
Ext.menu.Manager.onHide(me);
if (me.parentMenu) {
me.parentMenu.expanded = false;
}
if (me.floating && ariaDom) {
ariaDom.setAttribute('aria-expanded', false);
}
},
afterHide: function(cb, scope) {
this.callParent([cb, scope]);
// Top level focusEnter is only valid when focused
delete this.getInherited().topmostFocusEvent;
},
preventClick: function(e) {
var item = this.getItemFromEvent(e);
if (item && item.isMenuItem && !item.href) {
e.preventDefault();
}
},
privates: {
/**
* @private
*/
applyDefaults: function(config) {
if (!Ext.isString(config)) {
config = this.callParent([config]);
}
return config;
},
initFocusableElement: function() {
var me = this,
tabIndex = me.tabIndex,
el = me.el;
// Floating menus always need to have focusable main el
// so that mouse clicks within the menu would not close it.
// We're not checking focusable property here, Component
// will do that before we can reach this method.
if (me.floating && tabIndex != null && el && el.dom) {
el.dom.setAttribute('tabIndex', tabIndex);
el.dom.setAttribute('data-componentid', me.id);
}
},
processFocusableContainerKeyEvent: 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;
}
// TAB from textual input fields is converted into UP or DOWN.
else if (e.keyCode === e.TAB && Ext.fly(e.target).is('input[type=text],textarea')) {
e.preventDefault();
e.target = this.getItemFromEvent(e).el.dom;
if (e.shiftKey) {
e.shiftKey = false;
e.keyCode = e.UP;
}
else {
e.keyCode = e.DOWN;
}
}
else {
return this.callParent([e]);
}
return e;
},
// 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) {
var me = this;
if (me.floating) {
if (e.shiftKey) {
// We do not want TAB behaviour to proceed.
// SHIFT+TAB reverts "backwards" to the menu's invoker
// which is the automatic behaviour.
e.preventDefault();
}
else {
// If we want to navigate forwards, we cannot allow the
// automatic focus reversion to go to the parent menu.
// It must behave as if it were the topmost menu in the
// floating stack, revert to there, and then TAB onwards.
me.focusEnterEvent = me.getInherited().topmostFocusEvent;
}
me.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);
}
},
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);
}
}
});