* @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' * }] * }); * */ 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 * Menus are constrained to the document body by default */ 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`. * @markdown */ hidden: true, hideMode: 'visibility', * @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, cls = [prefix + 'menu'], bodyCls = me.bodyCls ? [me.bodyCls] : []; 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 if (me.plain) { cls.push(prefix + 'menu-plain'); } me.cls = cls.join(' '); // Menu body classes 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; } } }, clearStretch: function () { // the vbox/stretchmax will set the el sizes and subsequent layouts will not // reconsider them unless we clear the dimensions on the el's here: if (this.rendered) { this.items.each(function (item) { // each menuItem component needs to layout again, so clear its cache if (item.componentLayout) { delete item.componentLayout.lastComponentSize; } if (item.el) { item.el.setWidth(null); } }); } }, onAdd: function () { var me = this; me.clearStretch(); me.callParent(arguments); if (Ext.isIE6 || Ext.isIE7) { // TODO - why does this need to be done (and not ok to do now)? Ext.Function.defer(me.doComponentLayout, 10, me); } }, onRemove: function () { this.clearStretch(); this.callParent(arguments); }, redoComponentLayout: function () { if (this.rendered) { this.clearStretch(); this.doComponentLayout(); } }, // 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, cls, intercept; 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', 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, xy, region; if (me.floating && cmp) { me.layout.autoSize = true; // show off-screen first so that we can calc position without causing a visual jump me.doAutoRender(); // Component or Element cmp = cmp.el || cmp; // Convert absolute to floatParent-relative coordinates if necessary. xy = me.el.getAlignToXY(cmp, pos || me.defaultAlign, off); if (me.floatParent) { region = me.floatParent.getTargetEl().getViewRegion(); xy[0] -= region.x; xy[1] -= region.y; } me.showAt(xy); } return me; }, // inherit docs showAt: function(){ this.callParent(arguments); if (this.floating) { this.doConstrain(); } }, doConstrain : function() { var me = this, y = me.el.getY(), max, full, vector, 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); } } vector = me.getConstrainVector(me.el.dom.parentNode); if (vector) { me.setPosition(me.getPosition()[0] + vector[0]); } me.el.setY(returnY); } });