X-Git-Url: http://git.ithinksw.org/extjs.git/blobdiff_plain/0494b8d9b9bb03ab6c22b34dae81261e3cd7e3e6..7a654f8d43fdb43d78b63d90528bed6e86b608cc:/src/menu/Menu.js diff --git a/src/menu/Menu.js b/src/menu/Menu.js new file mode 100644 index 00000000..ab8e5d1a --- /dev/null +++ b/src/menu/Menu.js @@ -0,0 +1,591 @@ +/** + * @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); + } +}); \ No newline at end of file