3 This file is part of Ext JS 4
5 Copyright (c) 2011 Sencha Inc
7 Contact: http://www.sencha.com/contact
9 GNU General Public License Usage
10 This file may be used under the terms of the GNU General Public License version 3.0 as published by the Free Software Foundation and appearing in the file LICENSE included in the packaging of this file. Please review the following information to ensure the GNU General Public License version 3.0 requirements will be met: http://www.gnu.org/copyleft/gpl.html.
12 If you are unsure which license is appropriate for your use, please contact the sales department at http://www.sencha.com/contact.
16 * @class Ext.menu.Menu
17 * @extends Ext.panel.Panel
19 * A menu object. This is the container to which you may add {@link Ext.menu.Item menu items}.
21 * Menus may contain either {@link Ext.menu.Item menu items}, or general {@link Ext.Component Components}.
22 * Menus may also contain {@link Ext.panel.AbstractPanel#dockedItems docked items} because it extends {@link Ext.panel.Panel}.
24 * To make a contained general {@link Ext.Component Component} line up with other {@link Ext.menu.Item menu items},
25 * specify `{@link Ext.menu.Item#iconCls iconCls}: 'no-icon'` _or_ `{@link Ext.menu.Item#indent indent}: true`.
26 * This reserves a space for an icon, and indents the Component in line with the other menu items.
27 * See {@link Ext.form.field.ComboBox}.{@link Ext.form.field.ComboBox#getListParent getListParent} for an example.
29 * By default, Menus are absolutely positioned, floating Components. By configuring a Menu with `{@link #floating}:false`,
30 * a Menu may be used as a child of a {@link Ext.container.Container Container}.
32 * {@img Ext.menu.Item/Ext.menu.Item.png Ext.menu.Item component}
36 * Ext.create('Ext.menu.Menu', {
40 * floating: false, // usually you want this set to True (default)
41 * renderTo: Ext.getBody(), // usually rendered by it's containing component
43 * text: 'regular item 1'
45 * text: 'regular item 2'
47 * text: 'regular item 3'
51 * Ext.create('Ext.menu.Menu', {
55 * floating: false, // usually you want this set to True (default)
56 * renderTo: Ext.getBody(), // usually rendered by it's containing component
58 * text: 'plain item 1'
60 * text: 'plain item 2'
62 * text: 'plain item 3'
67 Ext.define('Ext.menu.Menu', {
68 extend: 'Ext.panel.Panel',
71 'Ext.layout.container.Fit',
72 'Ext.layout.container.VBox',
81 * @cfg {Boolean} allowOtherMenus
82 * True to allow multiple menus to be displayed at the same time. Defaults to `false`.
85 allowOtherMenus: false,
88 * @cfg {String} ariaRole @hide
93 * @cfg {Boolean} autoRender @hide
94 * floating is true, so autoRender always happens
98 * @cfg {String} defaultAlign
99 * The default {@link Ext.core.Element#getAlignToXY Ext.core.Element#getAlignToXY} anchor position value for this menu
100 * relative to its element of origin. Defaults to `'tl-bl?'`.
103 defaultAlign: 'tl-bl?',
106 * @cfg {Boolean} floating
107 * A Menu configured as `floating: true` (the default) will be rendered as an absolutely positioned,
108 * {@link Ext.Component#floating floating} {@link Ext.Component Component}. If configured as `floating: false`, the Menu may be
109 * used as a child item of another {@link Ext.container.Container Container}.
115 * @cfg {Boolean} @hide
116 * Menus are constrained to the document body by default
121 * @cfg {Boolean} hidden
122 * True to initially render the Menu as hidden, requiring to be shown manually.
123 * Defaults to `true` when `floating: true`, and defaults to `false` when `floating: false`.
128 hideMode: 'visibility',
131 * @cfg {Boolean} ignoreParentClicks
132 * True to ignore clicks on any item in this menu that is a parent item (displays a submenu)
133 * so that the submenu is not dismissed when clicking the parent item. Defaults to `false`.
136 ignoreParentClicks: false,
141 * @cfg {String/Object} layout @hide
145 * @cfg {Boolean} showSeparator True to show the icon separator. (defaults to true).
147 showSeparator : true,
150 * @cfg {Number} minWidth
151 * The minimum width of the Menu. Defaults to `120`.
157 * @cfg {Boolean} plain
158 * True to remove the incised line down the left side of the menu and to not
159 * indent general Component items. Defaults to `false`.
163 initComponent: function() {
165 prefix = Ext.baseCSSPrefix,
166 cls = [prefix + 'menu'],
167 bodyCls = me.bodyCls ? [me.bodyCls] : [];
172 * Fires when this menu is clicked
173 * @param {Ext.menu.Menu} menu The menu which has been clicked
174 * @param {Ext.Component} item The menu item that was clicked. `undefined` if not applicable.
175 * @param {Ext.EventObject} e The underlying {@link Ext.EventObject}.
182 * Fires when the mouse enters this menu
183 * @param {Ext.menu.Menu} menu The menu
184 * @param {Ext.EventObject} e The underlying {@link Ext.EventObject}
191 * Fires when the mouse leaves this menu
192 * @param {Ext.menu.Menu} menu The menu
193 * @param {Ext.EventObject} e The underlying {@link Ext.EventObject}
200 * Fires when the mouse is hovering over this menu
201 * @param {Ext.menu.Menu} menu The menu
202 * @param {Ext.Component} item The menu item that the mouse is over. `undefined` if not applicable.
203 * @param {Ext.EventObject} e The underlying {@link Ext.EventObject}
208 Ext.menu.Manager.register(me);
212 cls.push(prefix + 'menu-plain');
214 me.cls = cls.join(' ');
217 bodyCls.unshift(prefix + 'menu-body');
218 me.bodyCls = bodyCls.join(' ');
220 // Internal vbox layout, with scrolling overflow
221 // Placed in initComponent (rather than prototype) in order to support dynamic layout/scroller
222 // options if we wish to allow for such configurations on the Menu.
223 // e.g., scrolling speed, vbox align stretch, etc.
228 clearInnerCtOnLayout: true,
229 overflowHandler: 'Scroller'
232 // hidden defaults to false if floating is configured as false
233 if (me.floating === false && me.initialConfig.hidden !== true) {
237 me.callParent(arguments);
239 me.on('beforeshow', function() {
240 var hasItems = !!me.items.length;
241 // FIXME: When a menu has its show cancelled because of no items, it
242 // gets a visibility: hidden applied to it (instead of the default display: none)
243 // Not sure why, but we remove this style when we want to show again.
244 if (hasItems && me.rendered) {
245 me.el.setStyle('visibility', null);
251 afterRender: function(ct) {
253 prefix = Ext.baseCSSPrefix,
256 me.callParent(arguments);
258 // TODO: Move this to a subTemplate When we support them in the future
259 if (me.showSeparator) {
260 me.iconSepEl = me.layout.getRenderTarget().insertFirst({
261 cls: prefix + 'menu-icon-separator',
266 me.focusEl = me.el.createChild({
267 cls: prefix + 'menu-focus',
274 mouseover: me.onMouseOver,
277 me.mouseMonitor = me.el.monitorMouseLeave(100, me.onMouseLeave, me);
279 if (me.showSeparator && ((!Ext.isStrict && Ext.isIE) || Ext.isIE6)) {
280 me.iconSepEl.setHeight(me.el.getHeight());
283 me.keyNav = Ext.create('Ext.menu.KeyNav', me);
286 afterLayout: function() {
288 me.callParent(arguments);
290 // For IE6 & IE quirks, we have to resize the el and body since position: absolute
291 // floating elements inherit their parent's width, making them the width of
292 // document.body instead of the width of their contents.
293 // This includes left/right dock items.
294 if ((!Ext.iStrict && Ext.isIE) || Ext.isIE6) {
295 var innerCt = me.layout.getRenderTarget(),
297 dis = me.dockedItems,
302 innerCtWidth = innerCt.getWidth();
304 newWidth = innerCtWidth + me.body.getBorderWidth('lr') + me.body.getPadding('lr');
306 // First set the body to the new width
307 me.body.setWidth(newWidth);
309 // Now we calculate additional width (docked items) and set the el's width
310 for (; i < l, di = dis.getAt(i); i++) {
311 if (di.dock == 'left' || di.dock == 'right') {
312 newWidth += di.getWidth();
315 me.el.setWidth(newWidth);
320 * Returns whether a menu item can be activated or not.
323 canActivateItem: function(item) {
324 return item && !item.isDisabled() && item.isVisible() && (item.canActivate || item.getXTypes().indexOf('menuitem') < 0);
328 * Deactivates the current active item on the menu, if one exists.
330 deactivateActiveItem: function() {
334 me.activeItem.deactivate();
335 if (!me.activeItem.activated) {
336 delete me.activeItem;
339 if (me.focusedItem) {
340 me.focusedItem.blur();
341 if (!me.focusedItem.$focused) {
342 delete me.focusedItem;
347 clearStretch: function () {
348 // the vbox/stretchmax will set the el sizes and subsequent layouts will not
349 // reconsider them unless we clear the dimensions on the el's here:
351 this.items.each(function (item) {
352 // each menuItem component needs to layout again, so clear its cache
353 if (item.componentLayout) {
354 delete item.componentLayout.lastComponentSize;
357 item.el.setWidth(null);
367 me.callParent(arguments);
369 if (Ext.isIE6 || Ext.isIE7) {
370 // TODO - why does this need to be done (and not ok to do now)?
371 Ext.Function.defer(me.doComponentLayout, 10, me);
375 onRemove: function () {
377 this.callParent(arguments);
381 redoComponentLayout: function () {
384 this.doComponentLayout();
389 getFocusEl: function() {
395 this.deactivateActiveItem();
396 this.callParent(arguments);
400 getItemFromEvent: function(e) {
401 return this.getChildByElement(e.getTarget());
404 lookupComponent: function(cmp) {
407 if (Ext.isString(cmp)) {
408 cmp = me.lookupItemFromString(cmp);
409 } else if (Ext.isObject(cmp)) {
410 cmp = me.lookupItemFromObject(cmp);
413 // Apply our minWidth to all of our child components so it's accounted
414 // for in our VBox layout
415 cmp.minWidth = cmp.minWidth || me.minWidth;
421 lookupItemFromObject: function(cmp) {
423 prefix = Ext.baseCSSPrefix,
427 if (!cmp.isComponent) {
429 cmp = Ext.create('Ext.menu.' + (Ext.isBoolean(cmp.checked) ? 'Check': '') + 'Item', cmp);
431 cmp = Ext.ComponentManager.create(cmp, cmp.xtype);
435 if (cmp.isMenuItem) {
439 if (!cmp.isMenuItem && !cmp.dock) {
440 cls = [prefix + 'menu-item', prefix + 'menu-item-cmp'];
441 intercept = Ext.Function.createInterceptor;
443 // Wrap focus/blur to control component focus
444 cmp.focus = intercept(cmp.focus, function() {
445 this.$focused = true;
447 cmp.blur = intercept(cmp.blur, function() {
448 this.$focused = false;
451 if (!me.plain && (cmp.indent === true || cmp.iconCls === 'no-icon')) {
452 cls.push(prefix + 'menu-item-indent');
458 cmp.cls = (cmp.cls ? cmp.cls : '') + ' ' + cls.join(' ');
460 cmp.isMenuItem = true;
466 lookupItemFromString: function(cmp) {
467 return (cmp == 'separator' || cmp == '-') ?
468 Ext.createWidget('menuseparator')
469 : Ext.createWidget('menuitem', {
477 onClick: function(e) {
486 if ((e.getTarget() == me.focusEl.dom) || e.within(me.layout.getRenderTarget())) {
487 item = me.getItemFromEvent(e) || me.activeItem;
490 if (item.getXTypes().indexOf('menuitem') >= 0) {
491 if (!item.menu || !me.ignoreParentClicks) {
498 me.fireEvent('click', me, item, e);
502 onDestroy: function() {
505 Ext.menu.Manager.unregister(me);
507 me.el.un(me.mouseMonitor);
511 me.callParent(arguments);
514 onMouseLeave: function(e) {
517 me.deactivateActiveItem();
523 me.fireEvent('mouseleave', me, e);
526 onMouseOver: function(e) {
528 fromEl = e.getRelatedTarget(),
529 mouseEnter = !me.el.contains(fromEl),
530 item = me.getItemFromEvent(e);
532 if (mouseEnter && me.parentMenu) {
533 me.parentMenu.setActiveItem(me.parentItem);
534 me.parentMenu.mouseMonitor.mouseenter();
542 me.setActiveItem(item);
543 if (item.activated && item.expandMenu) {
548 me.fireEvent('mouseenter', me, e);
550 me.fireEvent('mouseover', me, item, e);
553 setActiveItem: function(item) {
556 if (item && (item != me.activeItem && item != me.focusedItem)) {
557 me.deactivateActiveItem();
558 if (me.canActivateItem(item)) {
561 if (item.activated) {
562 me.activeItem = item;
563 me.focusedItem = item;
568 me.focusedItem = item;
571 item.el.scrollIntoView(me.layout.getRenderTarget());
576 * Shows the floating menu by the specified {@link Ext.Component Component} or {@link Ext.core.Element Element}.
577 * @param {Mixed component} The {@link Ext.Component} or {@link Ext.core.Element} to show the menu by.
578 * @param {String} position (optional) Alignment position as used by {@link Ext.core.Element#getAlignToXY Ext.core.Element.getAlignToXY}. Defaults to `{@link #defaultAlign}`.
579 * @param {Array} offsets (optional) Alignment offsets as used by {@link Ext.core.Element#getAlignToXY Ext.core.Element.getAlignToXY}. Defaults to `undefined`.
580 * @return {Menu} This Menu.
583 showBy: function(cmp, pos, off) {
588 if (me.floating && cmp) {
589 me.layout.autoSize = true;
591 // show off-screen first so that we can calc position without causing a visual jump
594 // Component or Element
597 // Convert absolute to floatParent-relative coordinates if necessary.
598 xy = me.el.getAlignToXY(cmp, pos || me.defaultAlign, off);
599 if (me.floatParent) {
600 region = me.floatParent.getTargetEl().getViewRegion();
611 this.callParent(arguments);
617 doConstrain : function() {
622 returnY = y, normalY, parentEl, scrollTop, viewHeight;
626 full = me.getHeight();
628 parentEl = Ext.fly(me.el.dom.parentNode);
629 scrollTop = parentEl.getScroll().top;
630 viewHeight = parentEl.getViewSize().height;
631 //Normalize y by the scroll position for the parent element. Need to move it into the coordinate space
633 normalY = y - scrollTop;
634 max = me.maxHeight ? me.maxHeight : viewHeight - normalY;
635 if (full > viewHeight) {
637 //Set returnY equal to (0,0) in view space by reducing y by the value of normalY
638 returnY = y - normalY;
639 } else if (max < full) {
640 returnY = y - (full - max);
644 max = me.getHeight();
646 // Always respect maxHeight
648 max = Math.min(me.maxHeight, max);
650 if (full > max && max > 0){
651 me.layout.autoSize = false;
653 if (me.showSeparator){
654 me.iconSepEl.setHeight(me.layout.getRenderTarget().dom.scrollHeight);
657 vector = me.getConstrainVector(me.el.dom.parentNode);
659 me.setPosition(me.getPosition()[0] + vector[0]);