* @class Ext.menu.Menu
* @extends Ext.panel.Panel
*
* 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#iconCls iconCls}: 'no-icon'` _or_ `{@link Ext.menu.Item#indent indent}: true`.
* This reserves a space for an icon, and indents the Component in line with the other menu items.
* See {@link Ext.form.field.ComboBox}.{@link Ext.form.field.ComboBox#getListParent getListParent} for an example.
* 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}.
* {@img Ext.menu.Item/Ext.menu.Item.png Ext.menu.Item component}
__Example Usage__
Ext.create('Ext.menu.Menu', {
width: 100,
height: 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,
height: 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'
}]
});
* @xtype menu
* @markdown
* @constructor
* @param {Object} config The config object
*/
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'
],
* @cfg {Boolean} allowOtherMenus
* True to allow multiple menus to be displayed at the same time. Defaults to `false`.
* @markdown
*/
allowOtherMenus: false,
* @cfg {String} ariaRole @hide
*/
ariaRole: 'menu',
* @cfg {Boolean} autoRender @hide
* floating is true, so autoRender always happens
*/
* @cfg {String} defaultAlign
* The default {@link Ext.core.Element#getAlignToXY Ext.core.Element#getAlignToXY} anchor position value for this menu
* relative to its element of origin. Defaults to `'tl-bl?'`.
* @markdown
*/
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}.
* @markdown
*/
floating: true,
* @cfg {Boolean} @hide
* Menu performs its own size changing constraining, so ensure Component's constraining is not applied
*/
constrain: false,
* @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`.
* @markdown
*/
hidden: true,
* @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. Defaults to `false`.
* @markdown
*/
ignoreParentClicks: false,
isMenu: true,
* @cfg {String/Object} layout @hide
*/
* @cfg {Boolean} showSeparator True to show the icon separator. (defaults to true).
*/
showSeparator : true,
* @cfg {Number} minWidth
* The minimum width of the Menu. Defaults to `120`.
* @markdown
*/
minWidth: 120,
* @cfg {Boolean} plain
* True to remove the incised line down the left side of the menu and to not
* indent general Component items. Defaults to `false`.
* @markdown
*/
initComponent: function() {
var me = this,
prefix = Ext.baseCSSPrefix;
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}.
* @markdown
*/
'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}
* @markdown
*/
'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}
* @markdown
*/
'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
var cls = [prefix + 'menu'];
if (me.plain) {
cls.push(prefix + 'menu-plain');
}
me.cls = cls.join(' ');
// Menu body classes
var bodyCls = me.bodyCls ? [me.bodyCls] : [];
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.
me.layout = {
type: 'vbox',
align: 'stretchmax',
autoSize: true,
clearInnerCtOnLayout: true,
overflowHandler: 'Scroller'
};
// hidden defaults to false if floating is configured as false
if (me.floating === false && 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;
});
},
afterRender: function(ct) {
var me = this,
prefix = Ext.baseCSSPrefix,
space = ' ';
me.callParent(arguments);
// TODO: Move this to a subTemplate When we support them in the future
if (me.showSeparator) {
me.iconSepEl = me.layout.getRenderTarget().insertFirst({
cls: prefix + 'menu-icon-separator',
html: space
});
}
me.focusEl = me.el.createChild({
cls: prefix + 'menu-focus',
tabIndex: '-1',
html: space
});
me.mon(me.el, {
click: me.onClick,
mouseover: me.onMouseOver,
scope: me
});
me.mouseMonitor = me.el.monitorMouseLeave(100, me.onMouseLeave, me);
if (me.showSeparator && ((!Ext.isStrict && Ext.isIE) || Ext.isIE6)) {
me.iconSepEl.setHeight(me.el.getHeight());
}
me.keyNav = Ext.create('Ext.menu.KeyNav', me);
},
afterLayout: function() {
var me = this;
me.callParent(arguments);
// For IE6 & IE quirks, we have to resize the el and body since position: absolute
// floating elements inherit their parent's width, making them the width of
// document.body instead of the width of their contents.
// This includes left/right dock items.
if ((!Ext.iStrict && Ext.isIE) || Ext.isIE6) {
var innerCt = me.layout.getRenderTarget(),
innerCtWidth = 0,
dis = me.dockedItems,
l = dis.length,
i = 0,
di, clone, newWidth;
innerCtWidth = innerCt.getWidth();
newWidth = innerCtWidth + me.body.getBorderWidth('lr') + me.body.getPadding('lr');
// First set the body to the new width
me.body.setWidth(newWidth);
// Now we calculate additional width (docked items) and set the el's width
for (; i < l, di = dis.getAt(i); i++) {
if (di.dock == 'left' || di.dock == 'right') {
newWidth += di.getWidth();
}
}
me.el.setWidth(newWidth);
}
},
* 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() {
var me = this;
if (me.activeItem) {
me.activeItem.deactivate();
if (!me.activeItem.activated) {
delete me.activeItem;
}
}
if (me.focusedItem) {
me.focusedItem.blur();
if (!me.focusedItem.$focused) {
delete me.focusedItem;
}
}
},
// inherit docs
getFocusEl: function() {
return this.focusEl;
},
// inherit docs
hide: function() {
this.deactivateActiveItem();
this.callParent(arguments);
},
// private
getItemFromEvent: function(e) {
return this.getChildByElement(e.getTarget());
},
lookupComponent: function(cmp) {
var me = this;
if (Ext.isString(cmp)) {
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;
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) {
var cls = [
prefix + 'menu-item',
prefix + 'menu-item-cmp'
],
intercept = Ext.Function.createInterceptor;
// Wrap focus/blur to control component focus
cmp.focus = intercept(cmp.focus, function() {
this.$focused = true;
}, cmp);
cmp.blur = intercept(cmp.blur, function() {
this.$focused = false;
}, 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(' ');
}
cmp.isMenuItem = true;
}
return cmp;
},
// private
lookupItemFromString: function(cmp) {
return (cmp == 'separator' || cmp == '-') ?
Ext.createWidget('menuseparator')
: Ext.createWidget('menuitem', {
canActivate: false,
hideOnClick: false,
plain: true,
text: cmp
});
},
onClick: function(e) {
var me = this,
item;
if (me.disabled) {
e.stopEvent();
return;
}
if ((e.getTarget() == me.focusEl.dom) || e.within(me.layout.getRenderTarget())) {
item = me.getItemFromEvent(e) || me.activeItem;
if (item) {
if (item.getXTypes().indexOf('menuitem') >= 0) {
if (!item.menu || !me.ignoreParentClicks) {
item.onClick(e);
} else {
e.stopEvent();
}
}
}
me.fireEvent('click', me, item, e);
}
},
onDestroy: function() {
var me = this;
Ext.menu.Manager.unregister(me);
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);
if (mouseEnter && me.parentMenu) {
me.parentMenu.setActiveItem(me.parentItem);
me.parentMenu.mouseMonitor.mouseenter();
}
if (me.disabled) {
return;
}
if (item) {
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 && item != me.focusedItem)) {
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.core.Element Element}.
* @param {Mixed component} The {@link Ext.Component} or {@link Ext.core.Element} to show the menu by.
* @param {String} position (optional) Alignment position as used by {@link Ext.core.Element#getAlignToXY Ext.core.Element.getAlignToXY}. Defaults to `{@link #defaultAlign}`.
* @param {Array} offsets (optional) Alignment offsets as used by {@link Ext.core.Element#getAlignToXY Ext.core.Element.getAlignToXY}. Defaults to `undefined`.
* @return {Menu} This Menu.
* @markdown
*/
showBy: function(cmp, pos, off) {
var me = this;
if (me.floating && cmp) {
me.layout.autoSize = true;
me.show();
// Component or Element
cmp = cmp.el || cmp;
// Convert absolute to floatParent-relative coordinates if necessary.
var xy = me.el.getAlignToXY(cmp, pos || me.defaultAlign, off);
if (me.floatParent) {
var r = me.floatParent.getTargetEl().getViewRegion();
xy[0] -= r.x;
xy[1] -= r.y;
}
me.showAt(xy);
me.doConstrain();
}
return me;
},
doConstrain : function() {
var me = this,
y = this.el.getY(),
max, full,
returnY = y, normalY, parentEl, scrollTop, viewHeight;
delete me.height;
me.setSize();
full = me.getHeight();
if (me.floating) {
parentEl = Ext.fly(me.el.dom.parentNode);
scrollTop = parentEl.getScroll().top;
viewHeight = parentEl.getViewSize().height;
//Normalize y by the scroll position for the parent element. Need to move it into the coordinate space
//of the view.
normalY = y - scrollTop;
max = me.maxHeight ? me.maxHeight : viewHeight - normalY;
if (full > viewHeight) {
max = viewHeight;
//Set returnY equal to (0,0) in view space by reducing y by the value of normalY
returnY = y - normalY;
} else if (max < full) {
returnY = y - (full - max);
max = full;
}
}else{
max = me.getHeight();
}
// Always respect maxHeight
if (me.maxHeight){
max = Math.min(me.maxHeight, max);
}
if (full > max && max > 0){
me.layout.autoSize = false;
me.setHeight(max);
if (me.showSeparator){
me.iconSepEl.setHeight(me.layout.getRenderTarget().dom.scrollHeight);
}
}
me.el.setY(returnY);
}
});