Upgrade to ExtJS 3.3.0 - Released 10/06/2010
[extjs.git] / src / widgets / layout / box / MenuOverflow.js
1 /*!
2  * Ext JS Library 3.3.0
3  * Copyright(c) 2006-2010 Ext JS, Inc.
4  * licensing@extjs.com
5  * http://www.extjs.com/license
6  */
7 /**
8  * @class Ext.layout.boxOverflow.Menu
9  * @extends Ext.layout.boxOverflow.None
10  * Description
11  */
12 Ext.layout.boxOverflow.Menu = Ext.extend(Ext.layout.boxOverflow.None, {
13     /**
14      * @cfg afterCls
15      * @type String
16      * CSS class added to the afterCt element. This is the element that holds any special items such as scrollers,
17      * which must always be present at the rightmost edge of the Container
18      */
19     afterCls: 'x-strip-right',
20     
21     /**
22      * @property noItemsMenuText
23      * @type String
24      * HTML fragment to render into the toolbar overflow menu if there are no items to display
25      */
26     noItemsMenuText : '<div class="x-toolbar-no-items">(None)</div>',
27     
28     constructor: function(layout) {
29         Ext.layout.boxOverflow.Menu.superclass.constructor.apply(this, arguments);
30         
31         /**
32          * @property menuItems
33          * @type Array
34          * Array of all items that are currently hidden and should go into the dropdown menu
35          */
36         this.menuItems = [];
37     },
38     
39     /**
40      * @private
41      * Creates the beforeCt, innerCt and afterCt elements if they have not already been created
42      * @param {Ext.Container} container The Container attached to this Layout instance
43      * @param {Ext.Element} target The target Element
44      */
45     createInnerElements: function() {
46         if (!this.afterCt) {
47             this.afterCt  = this.layout.innerCt.insertSibling({cls: this.afterCls},  'before');
48         }
49     },
50     
51     /**
52      * @private
53      */
54     clearOverflow: function(calculations, targetSize) {
55         var newWidth = targetSize.width + (this.afterCt ? this.afterCt.getWidth() : 0),
56             items    = this.menuItems;
57         
58         this.hideTrigger();
59         
60         for (var index = 0, length = items.length; index < length; index++) {
61             items.pop().component.show();
62         }
63         
64         return {
65             targetSize: {
66                 height: targetSize.height,
67                 width : newWidth
68             }
69         };
70     },
71     
72     /**
73      * @private
74      */
75     showTrigger: function() {
76         this.createMenu();
77         this.menuTrigger.show();
78     },
79     
80     /**
81      * @private
82      */
83     hideTrigger: function() {
84         if (this.menuTrigger != undefined) {
85             this.menuTrigger.hide();
86         }
87     },
88     
89     /**
90      * @private
91      * Called before the overflow menu is shown. This constructs the menu's items, caching them for as long as it can.
92      */
93     beforeMenuShow: function(menu) {
94         var items = this.menuItems,
95             len   = items.length,
96             item,
97             prev;
98
99         var needsSep = function(group, item){
100             return group.isXType('buttongroup') && !(item instanceof Ext.Toolbar.Separator);
101         };
102         
103         this.clearMenu();
104         menu.removeAll();
105         
106         for (var i = 0; i < len; i++) {
107             item = items[i].component;
108             
109             if (prev && (needsSep(item, prev) || needsSep(prev, item))) {
110                 menu.add('-');
111             }
112             
113             this.addComponentToMenu(menu, item);
114             prev = item;
115         }
116
117         // put something so the menu isn't empty if no compatible items found
118         if (menu.items.length < 1) {
119             menu.add(this.noItemsMenuText);
120         }
121     },
122     
123     /**
124      * @private
125      * Returns a menu config for a given component. This config is used to create a menu item
126      * to be added to the expander menu
127      * @param {Ext.Component} component The component to create the config for
128      * @param {Boolean} hideOnClick Passed through to the menu item
129      */
130     createMenuConfig : function(component, hideOnClick){
131         var config = Ext.apply({}, component.initialConfig),
132             group  = component.toggleGroup;
133
134         Ext.copyTo(config, component, [
135             'iconCls', 'icon', 'itemId', 'disabled', 'handler', 'scope', 'menu'
136         ]);
137
138         Ext.apply(config, {
139             text       : component.overflowText || component.text,
140             hideOnClick: hideOnClick
141         });
142
143         if (group || component.enableToggle) {
144             Ext.apply(config, {
145                 group  : group,
146                 checked: component.pressed,
147                 listeners: {
148                     checkchange: function(item, checked){
149                         component.toggle(checked);
150                     }
151                 }
152             });
153         }
154
155         delete config.ownerCt;
156         delete config.xtype;
157         delete config.id;
158
159         return config;
160     },
161
162     /**
163      * @private
164      * Adds the given Toolbar item to the given menu. Buttons inside a buttongroup are added individually.
165      * @param {Ext.menu.Menu} menu The menu to add to
166      * @param {Ext.Component} component The component to add
167      */
168     addComponentToMenu : function(menu, component) {
169         if (component instanceof Ext.Toolbar.Separator) {
170             menu.add('-');
171
172         } else if (Ext.isFunction(component.isXType)) {
173             if (component.isXType('splitbutton')) {
174                 menu.add(this.createMenuConfig(component, true));
175
176             } else if (component.isXType('button')) {
177                 menu.add(this.createMenuConfig(component, !component.menu));
178
179             } else if (component.isXType('buttongroup')) {
180                 component.items.each(function(item){
181                      this.addComponentToMenu(menu, item);
182                 }, this);
183             }
184         }
185     },
186     
187     /**
188      * @private
189      * Deletes the sub-menu of each item in the expander menu. Submenus are created for items such as
190      * splitbuttons and buttongroups, where the Toolbar item cannot be represented by a single menu item
191      */
192     clearMenu : function(){
193         var menu = this.moreMenu;
194         if (menu && menu.items) {
195             menu.items.each(function(item){
196                 delete item.menu;
197             });
198         }
199     },
200     
201     /**
202      * @private
203      * Creates the overflow trigger and menu used when enableOverflow is set to true and the items
204      * in the layout are too wide to fit in the space available
205      */
206     createMenu: function() {
207         if (!this.menuTrigger) {
208             this.createInnerElements();
209             
210             /**
211              * @private
212              * @property menu
213              * @type Ext.menu.Menu
214              * The expand menu - holds items for every item that cannot be shown
215              * because the container is currently not large enough.
216              */
217             this.menu = new Ext.menu.Menu({
218                 ownerCt : this.layout.container,
219                 listeners: {
220                     scope: this,
221                     beforeshow: this.beforeMenuShow
222                 }
223             });
224
225             /**
226              * @private
227              * @property menuTrigger
228              * @type Ext.Button
229              * The expand button which triggers the overflow menu to be shown
230              */
231             this.menuTrigger = new Ext.Button({
232                 iconCls : 'x-toolbar-more-icon',
233                 cls     : 'x-toolbar-more',
234                 menu    : this.menu,
235                 renderTo: this.afterCt
236             });
237         }
238     },
239     
240     /**
241      * @private
242      */
243     destroy: function() {
244         Ext.destroy(this.menu, this.menuTrigger);
245     }
246 });
247
248 Ext.layout.boxOverflow.menu = Ext.layout.boxOverflow.Menu;
249
250
251 /**
252  * @class Ext.layout.boxOverflow.HorizontalMenu
253  * @extends Ext.layout.boxOverflow.Menu
254  * Description
255  */
256 Ext.layout.boxOverflow.HorizontalMenu = Ext.extend(Ext.layout.boxOverflow.Menu, {
257     
258     constructor: function() {
259         Ext.layout.boxOverflow.HorizontalMenu.superclass.constructor.apply(this, arguments);
260         
261         var me = this,
262             layout = me.layout,
263             origFunction = layout.calculateChildBoxes;
264         
265         layout.calculateChildBoxes = function(visibleItems, targetSize) {
266             var calcs = origFunction.apply(layout, arguments),
267                 meta  = calcs.meta,
268                 items = me.menuItems;
269             
270             //calculate the width of the items currently hidden solely because there is not enough space
271             //to display them
272             var hiddenWidth = 0;
273             for (var index = 0, length = items.length; index < length; index++) {
274                 hiddenWidth += items[index].width;
275             }
276             
277             meta.minimumWidth += hiddenWidth;
278             meta.tooNarrow = meta.minimumWidth > targetSize.width;
279             
280             return calcs;
281         };        
282     },
283     
284     handleOverflow: function(calculations, targetSize) {
285         this.showTrigger();
286         
287         var newWidth    = targetSize.width - this.afterCt.getWidth(),
288             boxes       = calculations.boxes,
289             usedWidth   = 0,
290             recalculate = false;
291         
292         //calculate the width of all visible items and any spare width
293         for (var index = 0, length = boxes.length; index < length; index++) {
294             usedWidth += boxes[index].width;
295         }
296         
297         var spareWidth = newWidth - usedWidth,
298             showCount  = 0;
299         
300         //see if we can re-show any of the hidden components
301         for (var index = 0, length = this.menuItems.length; index < length; index++) {
302             var hidden = this.menuItems[index],
303                 comp   = hidden.component,
304                 width  = hidden.width;
305             
306             if (width < spareWidth) {
307                 comp.show();
308                 
309                 spareWidth -= width;
310                 showCount ++;
311                 recalculate = true;
312             } else {
313                 break;
314             }
315         }
316                 
317         if (recalculate) {
318             this.menuItems = this.menuItems.slice(showCount);
319         } else {
320             for (var i = boxes.length - 1; i >= 0; i--) {
321                 var item  = boxes[i].component,
322                     right = boxes[i].left + boxes[i].width;
323
324                 if (right >= newWidth) {
325                     this.menuItems.unshift({
326                         component: item,
327                         width    : boxes[i].width
328                     });
329
330                     item.hide();
331                 } else {
332                     break;
333                 }
334             }
335         }
336         
337         if (this.menuItems.length == 0) {
338             this.hideTrigger();
339         }
340         
341         return {
342             targetSize: {
343                 height: targetSize.height,
344                 width : newWidth
345             },
346             recalculate: recalculate
347         };
348     }
349 });
350
351 Ext.layout.boxOverflow.menu.hbox = Ext.layout.boxOverflow.HorizontalMenu;