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