Upgrade to ExtJS 4.0.1 - Released 05/18/2011
[extjs.git] / src / layout / container / boxOverflow / Menu.js
1 /**
2  * @class Ext.layout.container.boxOverflow.Menu
3  * @extends Ext.layout.container.boxOverflow.None
4  * @private
5  */
6 Ext.define('Ext.layout.container.boxOverflow.Menu', {
7
8     /* Begin Definitions */
9
10     extend: 'Ext.layout.container.boxOverflow.None',
11     requires: ['Ext.toolbar.Separator', 'Ext.button.Button'],
12     alternateClassName: 'Ext.layout.boxOverflow.Menu',
13     
14     /* End Definitions */
15
16     /**
17      * @cfg {String} afterCtCls
18      * CSS class added to the afterCt element. This is the element that holds any special items such as scrollers,
19      * which must always be present at the rightmost edge of the Container
20      */
21
22     /**
23      * @property noItemsMenuText
24      * @type String
25      * HTML fragment to render into the toolbar overflow menu if there are no items to display
26      */
27     noItemsMenuText : '<div class="' + Ext.baseCSSPrefix + 'toolbar-no-items">(None)</div>',
28
29     constructor: function(layout) {
30         var me = this;
31
32         me.callParent(arguments);
33
34         // Before layout, we need to re-show all items which we may have hidden due to a previous overflow.
35         layout.beforeLayout = Ext.Function.createInterceptor(layout.beforeLayout, this.clearOverflow, this);
36
37         me.afterCtCls = me.afterCtCls || Ext.baseCSSPrefix + 'box-menu-' + layout.parallelAfter;
38         /**
39          * @property menuItems
40          * @type Array
41          * Array of all items that are currently hidden and should go into the dropdown menu
42          */
43         me.menuItems = [];
44     },
45     
46     onRemove: function(comp){
47         Ext.Array.remove(this.menuItems, comp);
48     },
49
50     handleOverflow: function(calculations, targetSize) {
51         var me = this,
52             layout = me.layout,
53             methodName = 'get' + layout.parallelPrefixCap,
54             newSize = {},
55             posArgs = [null, null];
56
57         me.callParent(arguments);
58         this.createMenu(calculations, targetSize);
59         newSize[layout.perpendicularPrefix] = targetSize[layout.perpendicularPrefix];
60         newSize[layout.parallelPrefix] = targetSize[layout.parallelPrefix] - me.afterCt[methodName]();
61
62         // Center the menuTrigger button.
63         // TODO: Should we emulate align: 'middle' like this, or should we 'stretchmax' the menuTrigger?
64         posArgs[layout.perpendicularSizeIndex] = (calculations.meta.maxSize - me.menuTrigger['get' + layout.perpendicularPrefixCap]()) / 2;
65         me.menuTrigger.setPosition.apply(me.menuTrigger, posArgs);
66
67         return { targetSize: newSize };
68     },
69
70     /**
71      * @private
72      * Called by the layout, when it determines that there is no overflow.
73      * Also called as an interceptor to the layout's onLayout method to reshow
74      * previously hidden overflowing items.
75      */
76     clearOverflow: function(calculations, targetSize) {
77         var me = this,
78             newWidth = targetSize ? targetSize.width + (me.afterCt ? me.afterCt.getWidth() : 0) : 0,
79             items = me.menuItems,
80             i = 0,
81             length = items.length,
82             item;
83
84         me.hideTrigger();
85         for (; i < length; i++) {
86             items[i].show();
87         }
88         items.length = 0;
89
90         return targetSize ? {
91             targetSize: {
92                 height: targetSize.height,
93                 width : newWidth
94             }
95         } : null;
96     },
97
98     /**
99      * @private
100      */
101     showTrigger: function() {
102         this.menuTrigger.show();
103     },
104
105     /**
106      * @private
107      */
108     hideTrigger: function() {
109         if (this.menuTrigger !== undefined) {
110             this.menuTrigger.hide();
111         }
112     },
113
114     /**
115      * @private
116      * Called before the overflow menu is shown. This constructs the menu's items, caching them for as long as it can.
117      */
118     beforeMenuShow: function(menu) {
119         var me = this,
120             items = me.menuItems,
121             i = 0,
122             len   = items.length,
123             item,
124             prev;
125
126         var needsSep = function(group, prev){
127             return group.isXType('buttongroup') && !(prev instanceof Ext.toolbar.Separator);
128         };
129
130         me.clearMenu();
131         menu.removeAll();
132
133         for (; i < len; i++) {
134             item = items[i];
135
136             // Do not show a separator as a first item
137             if (!i && (item instanceof Ext.toolbar.Separator)) {
138                 continue;
139             }
140             if (prev && (needsSep(item, prev) || needsSep(prev, item))) {
141                 menu.add('-');
142             }
143
144             me.addComponentToMenu(menu, item);
145             prev = item;
146         }
147
148         // put something so the menu isn't empty if no compatible items found
149         if (menu.items.length < 1) {
150             menu.add(me.noItemsMenuText);
151         }
152     },
153     
154     /**
155      * @private
156      * Returns a menu config for a given component. This config is used to create a menu item
157      * to be added to the expander menu
158      * @param {Ext.Component} component The component to create the config for
159      * @param {Boolean} hideOnClick Passed through to the menu item
160      */
161     createMenuConfig : function(component, hideOnClick) {
162         var config = Ext.apply({}, component.initialConfig),
163             group  = component.toggleGroup;
164
165         Ext.copyTo(config, component, [
166             'iconCls', 'icon', 'itemId', 'disabled', 'handler', 'scope', 'menu'
167         ]);
168
169         Ext.apply(config, {
170             text       : component.overflowText || component.text,
171             hideOnClick: hideOnClick,
172             destroyMenu: false
173         });
174
175         if (group || component.enableToggle) {
176             Ext.apply(config, {
177                 group  : group,
178                 checked: component.pressed,
179                 listeners: {
180                     checkchange: function(item, checked){
181                         component.toggle(checked);
182                     }
183                 }
184             });
185         }
186
187         delete config.ownerCt;
188         delete config.xtype;
189         delete config.id;
190         return config;
191     },
192
193     /**
194      * @private
195      * Adds the given Toolbar item to the given menu. Buttons inside a buttongroup are added individually.
196      * @param {Ext.menu.Menu} menu The menu to add to
197      * @param {Ext.Component} component The component to add
198      */
199     addComponentToMenu : function(menu, component) {
200         var me = this;
201         if (component instanceof Ext.toolbar.Separator) {
202             menu.add('-');
203         } else if (component.isComponent) {
204             if (component.isXType('splitbutton')) {
205                 menu.add(me.createMenuConfig(component, true));
206
207             } else if (component.isXType('button')) {
208                 menu.add(me.createMenuConfig(component, !component.menu));
209
210             } else if (component.isXType('buttongroup')) {
211                 component.items.each(function(item){
212                      me.addComponentToMenu(menu, item);
213                 });
214             } else {
215                 menu.add(Ext.create(Ext.getClassName(component), me.createMenuConfig(component)));
216             }
217         }
218     },
219
220     /**
221      * @private
222      * Deletes the sub-menu of each item in the expander menu. Submenus are created for items such as
223      * splitbuttons and buttongroups, where the Toolbar item cannot be represented by a single menu item
224      */
225     clearMenu : function() {
226         var menu = this.moreMenu;
227         if (menu && menu.items) {
228             menu.items.each(function(item) {
229                 if (item.menu) {
230                     delete item.menu;
231                 }
232             });
233         }
234     },
235
236     /**
237      * @private
238      * Creates the overflow trigger and menu used when enableOverflow is set to true and the items
239      * in the layout are too wide to fit in the space available
240      */
241     createMenu: function(calculations, targetSize) {
242         var me = this,
243             layout = me.layout,
244             startProp = layout.parallelBefore,
245             sizeProp = layout.parallelPrefix,
246             available = targetSize[sizeProp],
247             boxes = calculations.boxes,
248             i = 0,
249             len = boxes.length,
250             box;
251
252         if (!me.menuTrigger) {
253             me.createInnerElements();
254
255             /**
256              * @private
257              * @property menu
258              * @type Ext.menu.Menu
259              * The expand menu - holds items for every item that cannot be shown
260              * because the container is currently not large enough.
261              */
262             me.menu = Ext.create('Ext.menu.Menu', {
263                 hideMode: 'offsets',
264                 listeners: {
265                     scope: me,
266                     beforeshow: me.beforeMenuShow
267                 }
268             });
269
270             /**
271              * @private
272              * @property menuTrigger
273              * @type Ext.button.Button
274              * The expand button which triggers the overflow menu to be shown
275              */
276             me.menuTrigger = Ext.create('Ext.button.Button', {
277                 ownerCt : me.layout.owner, // To enable the Menu to ascertain a valid zIndexManager owner in the same tree
278                 iconCls : Ext.baseCSSPrefix + layout.owner.getXType() + '-more-icon',
279                 ui      : layout.owner instanceof Ext.toolbar.Toolbar ? 'default-toolbar' : 'default',
280                 menu    : me.menu,
281                 getSplitCls: function() { return '';},
282                 renderTo: me.afterCt
283             });
284         }
285         me.showTrigger();
286         available -= me.afterCt.getWidth();
287
288         // Hide all items which are off the end, and store them to allow them to be restored
289         // before each layout operation.
290         me.menuItems.length = 0;
291         for (; i < len; i++) {
292             box = boxes[i];
293             if (box[startProp] + box[sizeProp] > available) {
294                 me.menuItems.push(box.component);
295                 box.component.hide();
296             }
297         }
298     },
299
300     /**
301      * @private
302      * Creates the beforeCt, innerCt and afterCt elements if they have not already been created
303      * @param {Ext.container.Container} container The Container attached to this Layout instance
304      * @param {Ext.core.Element} target The target Element
305      */
306     createInnerElements: function() {
307         var me = this,
308             target = me.layout.getRenderTarget();
309
310         if (!this.afterCt) {
311             target.addCls(Ext.baseCSSPrefix + me.layout.direction + '-box-overflow-body');
312             this.afterCt  = target.insertSibling({cls: Ext.layout.container.Box.prototype.innerCls + ' ' + this.afterCtCls}, 'before');
313         }
314     },
315
316     /**
317      * @private
318      */
319     destroy: function() {
320         Ext.destroy(this.menu, this.menuTrigger);
321     }
322 });