Upgrade to ExtJS 3.3.1 - Released 11/30/2010
[extjs.git] / src / widgets / layout / ToolbarLayout.js
1 /*!
2  * Ext JS Library 3.3.1
3  * Copyright(c) 2006-2010 Sencha Inc.
4  * licensing@sencha.com
5  * http://www.sencha.com/license
6  */
7 /**
8  * @class Ext.layout.ToolbarLayout
9  * @extends Ext.layout.ContainerLayout
10  * Layout manager used by Ext.Toolbar. This is highly specialised for use by Toolbars and would not
11  * usually be used by any other class.
12  */
13 Ext.layout.ToolbarLayout = Ext.extend(Ext.layout.ContainerLayout, {
14     monitorResize : true,
15
16     type: 'toolbar',
17
18     /**
19      * @property triggerWidth
20      * @type Number
21      * The width allocated for the menu trigger at the extreme right end of the Toolbar
22      */
23     triggerWidth: 18,
24
25     /**
26      * @property noItemsMenuText
27      * @type String
28      * HTML fragment to render into the toolbar overflow menu if there are no items to display
29      */
30     noItemsMenuText : '<div class="x-toolbar-no-items">(None)</div>',
31
32     /**
33      * @private
34      * @property lastOverflow
35      * @type Boolean
36      * Used internally to record whether the last layout caused an overflow or not
37      */
38     lastOverflow: false,
39
40     /**
41      * @private
42      * @property tableHTML
43      * @type String
44      * String used to build the HTML injected to support the Toolbar's layout. The align property is
45      * injected into this string inside the td.x-toolbar-left element during onLayout.
46      */
47     tableHTML: [
48         '<table cellspacing="0" class="x-toolbar-ct">',
49             '<tbody>',
50                 '<tr>',
51                     '<td class="x-toolbar-left" align="{0}">',
52                         '<table cellspacing="0">',
53                             '<tbody>',
54                                 '<tr class="x-toolbar-left-row"></tr>',
55                             '</tbody>',
56                         '</table>',
57                     '</td>',
58                     '<td class="x-toolbar-right" align="right">',
59                         '<table cellspacing="0" class="x-toolbar-right-ct">',
60                             '<tbody>',
61                                 '<tr>',
62                                     '<td>',
63                                         '<table cellspacing="0">',
64                                             '<tbody>',
65                                                 '<tr class="x-toolbar-right-row"></tr>',
66                                             '</tbody>',
67                                         '</table>',
68                                     '</td>',
69                                     '<td>',
70                                         '<table cellspacing="0">',
71                                             '<tbody>',
72                                                 '<tr class="x-toolbar-extras-row"></tr>',
73                                             '</tbody>',
74                                         '</table>',
75                                     '</td>',
76                                 '</tr>',
77                             '</tbody>',
78                         '</table>',
79                     '</td>',
80                 '</tr>',
81             '</tbody>',
82         '</table>'
83     ].join(""),
84
85     /**
86      * @private
87      * Create the wrapping Toolbar HTML and render/move all the items into the correct places
88      */
89     onLayout : function(ct, target) {
90         //render the Toolbar <table> HTML if it's not already present
91         if (!this.leftTr) {
92             var align = ct.buttonAlign == 'center' ? 'center' : 'left';
93
94             target.addClass('x-toolbar-layout-ct');
95             target.insertHtml('beforeEnd', String.format(this.tableHTML, align));
96
97             this.leftTr   = target.child('tr.x-toolbar-left-row', true);
98             this.rightTr  = target.child('tr.x-toolbar-right-row', true);
99             this.extrasTr = target.child('tr.x-toolbar-extras-row', true);
100
101             if (this.hiddenItem == undefined) {
102                 /**
103                  * @property hiddenItems
104                  * @type Array
105                  * Holds all items that are currently hidden due to there not being enough space to render them
106                  * These items will appear on the expand menu.
107                  */
108                 this.hiddenItems = [];
109             }
110         }
111
112         var side     = ct.buttonAlign == 'right' ? this.rightTr : this.leftTr,
113             items    = ct.items.items,
114             position = 0;
115
116         //render each item if not already rendered, place it into the correct (left or right) target
117         for (var i = 0, len = items.length, c; i < len; i++, position++) {
118             c = items[i];
119
120             if (c.isFill) {
121                 side   = this.rightTr;
122                 position = -1;
123             } else if (!c.rendered) {
124                 c.render(this.insertCell(c, side, position));
125                 this.configureItem(c);
126             } else {
127                 if (!c.xtbHidden && !this.isValidParent(c, side.childNodes[position])) {
128                     var td = this.insertCell(c, side, position);
129                     td.appendChild(c.getPositionEl().dom);
130                     c.container = Ext.get(td);
131                 }
132             }
133         }
134
135         //strip extra empty cells
136         this.cleanup(this.leftTr);
137         this.cleanup(this.rightTr);
138         this.cleanup(this.extrasTr);
139         this.fitToSize(target);
140     },
141
142     /**
143      * @private
144      * Removes any empty nodes from the given element
145      * @param {Ext.Element} el The element to clean up
146      */
147     cleanup : function(el) {
148         var cn = el.childNodes, i, c;
149
150         for (i = cn.length-1; i >= 0 && (c = cn[i]); i--) {
151             if (!c.firstChild) {
152                 el.removeChild(c);
153             }
154         }
155     },
156
157     /**
158      * @private
159      * Inserts the given Toolbar item into the given element
160      * @param {Ext.Component} c The component to add
161      * @param {Ext.Element} target The target to add the component to
162      * @param {Number} position The position to add the component at
163      */
164     insertCell : function(c, target, position) {
165         var td = document.createElement('td');
166         td.className = 'x-toolbar-cell';
167
168         target.insertBefore(td, target.childNodes[position] || null);
169
170         return td;
171     },
172
173     /**
174      * @private
175      * Hides an item because it will not fit in the available width. The item will be unhidden again
176      * if the Toolbar is resized to be large enough to show it
177      * @param {Ext.Component} item The item to hide
178      */
179     hideItem : function(item) {
180         this.hiddenItems.push(item);
181
182         item.xtbHidden = true;
183         item.xtbWidth = item.getPositionEl().dom.parentNode.offsetWidth;
184         item.hide();
185     },
186
187     /**
188      * @private
189      * Unhides an item that was previously hidden due to there not being enough space left on the Toolbar
190      * @param {Ext.Component} item The item to show
191      */
192     unhideItem : function(item) {
193         item.show();
194         item.xtbHidden = false;
195         this.hiddenItems.remove(item);
196     },
197
198     /**
199      * @private
200      * Returns the width of the given toolbar item. If the item is currently hidden because there
201      * is not enough room to render it, its previous width is returned
202      * @param {Ext.Component} c The component to measure
203      * @return {Number} The width of the item
204      */
205     getItemWidth : function(c) {
206         return c.hidden ? (c.xtbWidth || 0) : c.getPositionEl().dom.parentNode.offsetWidth;
207     },
208
209     /**
210      * @private
211      * Called at the end of onLayout. At this point the Toolbar has already been resized, so we need
212      * to fit the items into the available width. We add up the width required by all of the items in
213      * the toolbar - if we don't have enough space we hide the extra items and render the expand menu
214      * trigger.
215      * @param {Ext.Element} target The Element the Toolbar is currently laid out within
216      */
217     fitToSize : function(target) {
218         if (this.container.enableOverflow === false) {
219             return;
220         }
221
222         var width       = target.dom.clientWidth,
223             tableWidth  = target.dom.firstChild.offsetWidth,
224             clipWidth   = width - this.triggerWidth,
225             lastWidth   = this.lastWidth || 0,
226
227             hiddenItems = this.hiddenItems,
228             hasHiddens  = hiddenItems.length != 0,
229             isLarger    = width >= lastWidth;
230
231         this.lastWidth  = width;
232
233         if (tableWidth > width || (hasHiddens && isLarger)) {
234             var items     = this.container.items.items,
235                 len       = items.length,
236                 loopWidth = 0,
237                 item;
238
239             for (var i = 0; i < len; i++) {
240                 item = items[i];
241
242                 if (!item.isFill) {
243                     loopWidth += this.getItemWidth(item);
244                     if (loopWidth > clipWidth) {
245                         if (!(item.hidden || item.xtbHidden)) {
246                             this.hideItem(item);
247                         }
248                     } else if (item.xtbHidden) {
249                         this.unhideItem(item);
250                     }
251                 }
252             }
253         }
254
255         //test for number of hidden items again here because they may have changed above
256         hasHiddens = hiddenItems.length != 0;
257
258         if (hasHiddens) {
259             this.initMore();
260
261             if (!this.lastOverflow) {
262                 this.container.fireEvent('overflowchange', this.container, true);
263                 this.lastOverflow = true;
264             }
265         } else if (this.more) {
266             this.clearMenu();
267             this.more.destroy();
268             delete this.more;
269
270             if (this.lastOverflow) {
271                 this.container.fireEvent('overflowchange', this.container, false);
272                 this.lastOverflow = false;
273             }
274         }
275     },
276
277     /**
278      * @private
279      * Returns a menu config for a given component. This config is used to create a menu item
280      * to be added to the expander menu
281      * @param {Ext.Component} component The component to create the config for
282      * @param {Boolean} hideOnClick Passed through to the menu item
283      */
284     createMenuConfig : function(component, hideOnClick){
285         var config = Ext.apply({}, component.initialConfig),
286             group  = component.toggleGroup;
287
288         Ext.copyTo(config, component, [
289             'iconCls', 'icon', 'itemId', 'disabled', 'handler', 'scope', 'menu'
290         ]);
291
292         Ext.apply(config, {
293             text       : component.overflowText || component.text,
294             hideOnClick: hideOnClick
295         });
296
297         if (group || component.enableToggle) {
298             Ext.apply(config, {
299                 group  : group,
300                 checked: component.pressed,
301                 listeners: {
302                     checkchange: function(item, checked){
303                         component.toggle(checked);
304                     }
305                 }
306             });
307         }
308
309         delete config.ownerCt;
310         delete config.xtype;
311         delete config.id;
312
313         return config;
314     },
315
316     /**
317      * @private
318      * Adds the given Toolbar item to the given menu. Buttons inside a buttongroup are added individually.
319      * @param {Ext.menu.Menu} menu The menu to add to
320      * @param {Ext.Component} component The component to add
321      */
322     addComponentToMenu : function(menu, component) {
323         if (component instanceof Ext.Toolbar.Separator) {
324             menu.add('-');
325
326         } else if (Ext.isFunction(component.isXType)) {
327             if (component.isXType('splitbutton')) {
328                 menu.add(this.createMenuConfig(component, true));
329
330             } else if (component.isXType('button')) {
331                 menu.add(this.createMenuConfig(component, !component.menu));
332
333             } else if (component.isXType('buttongroup')) {
334                 component.items.each(function(item){
335                      this.addComponentToMenu(menu, item);
336                 }, this);
337             }
338         }
339     },
340
341     /**
342      * @private
343      * Deletes the sub-menu of each item in the expander menu. Submenus are created for items such as
344      * splitbuttons and buttongroups, where the Toolbar item cannot be represented by a single menu item
345      */
346     clearMenu : function(){
347         var menu = this.moreMenu;
348         if (menu && menu.items) {
349             menu.items.each(function(item){
350                 delete item.menu;
351             });
352         }
353     },
354
355     /**
356      * @private
357      * Called before the expand menu is shown, this rebuilds the menu since it was last shown because
358      * it is possible that the items hidden due to space limitations on the Toolbar have changed since.
359      * @param {Ext.menu.Menu} m The menu
360      */
361     beforeMoreShow : function(menu) {
362         var items = this.container.items.items,
363             len   = items.length,
364             item,
365             prev;
366
367         var needsSep = function(group, item){
368             return group.isXType('buttongroup') && !(item instanceof Ext.Toolbar.Separator);
369         };
370
371         this.clearMenu();
372         menu.removeAll();
373         for (var i = 0; i < len; i++) {
374             item = items[i];
375             if (item.xtbHidden) {
376                 if (prev && (needsSep(item, prev) || needsSep(prev, item))) {
377                     menu.add('-');
378                 }
379                 this.addComponentToMenu(menu, item);
380                 prev = item;
381             }
382         }
383
384         // put something so the menu isn't empty if no compatible items found
385         if (menu.items.length < 1) {
386             menu.add(this.noItemsMenuText);
387         }
388     },
389
390     /**
391      * @private
392      * Creates the expand trigger and menu, adding them to the <tr> at the extreme right of the
393      * Toolbar table
394      */
395     initMore : function(){
396         if (!this.more) {
397             /**
398              * @private
399              * @property moreMenu
400              * @type Ext.menu.Menu
401              * The expand menu - holds items for every Toolbar item that cannot be shown
402              * because the Toolbar is currently not wide enough.
403              */
404             this.moreMenu = new Ext.menu.Menu({
405                 ownerCt : this.container,
406                 listeners: {
407                     beforeshow: this.beforeMoreShow,
408                     scope: this
409                 }
410             });
411
412             /**
413              * @private
414              * @property more
415              * @type Ext.Button
416              * The expand button which triggers the overflow menu to be shown
417              */
418             this.more = new Ext.Button({
419                 iconCls: 'x-toolbar-more-icon',
420                 cls    : 'x-toolbar-more',
421                 menu   : this.moreMenu,
422                 ownerCt: this.container
423             });
424
425             var td = this.insertCell(this.more, this.extrasTr, 100);
426             this.more.render(td);
427         }
428     },
429
430     destroy : function(){
431         Ext.destroy(this.more, this.moreMenu);
432         delete this.leftTr;
433         delete this.rightTr;
434         delete this.extrasTr;
435         Ext.layout.ToolbarLayout.superclass.destroy.call(this);
436     }
437 });
438
439 Ext.Container.LAYOUTS.toolbar = Ext.layout.ToolbarLayout;