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