Upgrade to ExtJS 3.3.1 - Released 11/30/2010
[extjs.git] / src / widgets / layout / HBoxLayout.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.HBoxLayout
9  * @extends Ext.layout.BoxLayout
10  * <p>A layout that arranges items horizontally across a Container. This layout optionally divides available horizontal
11  * space between child items containing a numeric <code>flex</code> configuration.</p>
12  * This layout may also be used to set the heights of child items by configuring it with the {@link #align} option.
13  */
14 Ext.layout.HBoxLayout = Ext.extend(Ext.layout.BoxLayout, {
15     /**
16      * @cfg {String} align
17      * Controls how the child items of the container are aligned. Acceptable configuration values for this
18      * property are:
19      * <div class="mdetail-params"><ul>
20      * <li><b><tt>top</tt></b> : <b>Default</b><div class="sub-desc">child items are aligned vertically
21      * at the <b>top</b> of the container</div></li>
22      * <li><b><tt>middle</tt></b> : <div class="sub-desc">child items are aligned vertically in the
23      * <b>middle</b> of the container</div></li>
24      * <li><b><tt>stretch</tt></b> : <div class="sub-desc">child items are stretched vertically to fill
25      * the height of the container</div></li>
26      * <li><b><tt>stretchmax</tt></b> : <div class="sub-desc">child items are stretched vertically to
27      * the height of the largest item.</div></li>
28      */
29     align: 'top', // top, middle, stretch, strechmax
30
31     type : 'hbox',
32
33     /**
34      * @cfg {String} pack
35      * Controls how the child items of the container are packed together. Acceptable configuration values
36      * for this property are:
37      * <div class="mdetail-params"><ul>
38      * <li><b><tt>start</tt></b> : <b>Default</b><div class="sub-desc">child items are packed together at
39      * <b>left</b> side of container</div></li>
40      * <li><b><tt>center</tt></b> : <div class="sub-desc">child items are packed together at
41      * <b>mid-width</b> of container</div></li>
42      * <li><b><tt>end</tt></b> : <div class="sub-desc">child items are packed together at <b>right</b>
43      * side of container</div></li>
44      * </ul></div>
45      */
46     /**
47      * @cfg {Number} flex
48      * This configuation option is to be applied to <b>child <tt>items</tt></b> of the container managed
49      * by this layout. Each child item with a <tt>flex</tt> property will be flexed <b>horizontally</b>
50      * according to each item's <b>relative</b> <tt>flex</tt> value compared to the sum of all items with
51      * a <tt>flex</tt> value specified.  Any child items that have either a <tt>flex = 0</tt> or
52      * <tt>flex = undefined</tt> will not be 'flexed' (the initial size will not be changed).
53      */
54
55     /**
56      * @private
57      * Calculates the size and positioning of each item in the HBox. This iterates over all of the rendered,
58      * visible items and returns a height, width, top and left for each, as well as a reference to each. Also
59      * returns meta data such as maxHeight which are useful when resizing layout wrappers such as this.innerCt.
60      * @param {Array} visibleItems The array of all rendered, visible items to be calculated for
61      * @param {Object} targetSize Object containing target size and height
62      * @return {Object} Object containing box measurements for each child, plus meta data
63      */
64     calculateChildBoxes: function(visibleItems, targetSize) {
65         var visibleCount = visibleItems.length,
66
67             padding      = this.padding,
68             topOffset    = padding.top,
69             leftOffset   = padding.left,
70             paddingVert  = topOffset  + padding.bottom,
71             paddingHoriz = leftOffset + padding.right,
72
73             width        = targetSize.width - this.scrollOffset,
74             height       = targetSize.height,
75             availHeight  = Math.max(0, height - paddingVert),
76
77             isStart      = this.pack == 'start',
78             isCenter     = this.pack == 'center',
79             isEnd        = this.pack == 'end',
80
81             nonFlexWidth = 0,
82             maxHeight    = 0,
83             totalFlex    = 0,
84             desiredWidth = 0,
85             minimumWidth = 0,
86
87             //used to cache the calculated size and position values for each child item
88             boxes        = [],
89
90             //used in the for loops below, just declared here for brevity
91             child, childWidth, childHeight, childSize, childMargins, canLayout, i, calcs, flexedWidth, 
92             horizMargins, vertMargins, stretchHeight;
93
94         //gather the total flex of all flexed items and the width taken up by fixed width items
95         for (i = 0; i < visibleCount; i++) {
96             child       = visibleItems[i];
97             childHeight = child.height;
98             childWidth  = child.width;
99             canLayout   = !child.hasLayout && typeof child.doLayout == 'function';
100
101             // Static width (numeric) requires no calcs
102             if (typeof childWidth != 'number') {
103
104                 // flex and not 'auto' width
105                 if (child.flex && !childWidth) {
106                     totalFlex += child.flex;
107
108                 // Not flexed or 'auto' width or undefined width
109                 } else {
110                     //Render and layout sub-containers without a flex or width defined, as otherwise we
111                     //don't know how wide the sub-container should be and cannot calculate flexed widths
112                     if (!childWidth && canLayout) {
113                         child.doLayout();
114                     }
115
116                     childSize   = child.getSize();
117                     childWidth  = childSize.width;
118                     childHeight = childSize.height;
119                 }
120             }
121
122             childMargins = child.margins;
123             horizMargins = childMargins.left + childMargins.right;
124
125             nonFlexWidth += horizMargins + (childWidth || 0);
126             desiredWidth += horizMargins + (child.flex ? child.minWidth || 0 : childWidth);
127             minimumWidth += horizMargins + (child.minWidth || childWidth || 0);
128
129             // Max height for align - force layout of non-laid out subcontainers without a numeric height
130             if (typeof childHeight != 'number') {
131                 if (canLayout) {
132                     child.doLayout();
133                 }
134                 childHeight = child.getHeight();
135             }
136
137             maxHeight = Math.max(maxHeight, childHeight + childMargins.top + childMargins.bottom);
138
139             //cache the size of each child component. Don't set height or width to 0, keep undefined instead
140             boxes.push({
141                 component: child,
142                 height   : childHeight || undefined,
143                 width    : childWidth  || undefined
144             });
145         }
146                 
147         var shortfall = desiredWidth - width,
148             tooNarrow = minimumWidth > width;
149             
150         //the width available to the flexed items
151         var availableWidth = Math.max(0, width - nonFlexWidth - paddingHoriz);
152         
153         if (tooNarrow) {
154             for (i = 0; i < visibleCount; i++) {
155                 boxes[i].width = visibleItems[i].minWidth || visibleItems[i].width || boxes[i].width;
156             }
157         } else {
158             //all flexed items should be sized to their minimum width, other items should be shrunk down until
159             //the shortfall has been accounted for
160             if (shortfall > 0) {
161                 var minWidths = [];
162                 
163                 /**
164                  * When we have a shortfall but are not tooNarrow, we need to shrink the width of each non-flexed item.
165                  * Flexed items are immediately reduced to their minWidth and anything already at minWidth is ignored.
166                  * The remaining items are collected into the minWidths array, which is later used to distribute the shortfall.
167                  */
168                 for (var index = 0, length = visibleCount; index < length; index++) {
169                     var item     = visibleItems[index],
170                         minWidth = item.minWidth || 0;
171
172                     //shrink each non-flex tab by an equal amount to make them all fit. Flexed items are all
173                     //shrunk to their minWidth because they're flexible and should be the first to lose width
174                     if (item.flex) {
175                         boxes[index].width = minWidth;
176                     } else {
177                         minWidths.push({
178                             minWidth : minWidth,
179                             available: boxes[index].width - minWidth,
180                             index    : index
181                         });
182                     }
183                 }
184                 
185                 //sort by descending amount of width remaining before minWidth is reached
186                 minWidths.sort(function(a, b) {
187                     return a.available > b.available ? 1 : -1;
188                 });
189                 
190                 /*
191                  * Distribute the shortfall (difference between total desired with of all items and actual width available)
192                  * between the non-flexed items. We try to distribute the shortfall evenly, but apply it to items with the
193                  * smallest difference between their width and minWidth first, so that if reducing the width by the average
194                  * amount would make that item less than its minWidth, we carry the remainder over to the next item.
195                  */
196                 for (var i = 0, length = minWidths.length; i < length; i++) {
197                     var itemIndex = minWidths[i].index;
198                     
199                     if (itemIndex == undefined) {
200                         continue;
201                     }
202                         
203                     var item      = visibleItems[itemIndex],
204                         box       = boxes[itemIndex],
205                         oldWidth  = box.width,
206                         minWidth  = item.minWidth,
207                         newWidth  = Math.max(minWidth, oldWidth - Math.ceil(shortfall / (length - i))),
208                         reduction = oldWidth - newWidth;
209                     
210                     boxes[itemIndex].width = newWidth;
211                     shortfall -= reduction;                    
212                 }
213             } else {
214                 //temporary variables used in the flex width calculations below
215                 var remainingWidth = availableWidth,
216                     remainingFlex  = totalFlex;
217
218                 //calculate the widths of each flexed item
219                 for (i = 0; i < visibleCount; i++) {
220                     child = visibleItems[i];
221                     calcs = boxes[i];
222
223                     childMargins = child.margins;
224                     vertMargins  = childMargins.top + childMargins.bottom;
225
226                     if (isStart && child.flex && !child.width) {
227                         flexedWidth     = Math.ceil((child.flex / remainingFlex) * remainingWidth);
228                         remainingWidth -= flexedWidth;
229                         remainingFlex  -= child.flex;
230
231                         calcs.width = flexedWidth;
232                         calcs.dirtySize = true;
233                     }
234                 }
235             }
236         }
237         
238         if (isCenter) {
239             leftOffset += availableWidth / 2;
240         } else if (isEnd) {
241             leftOffset += availableWidth;
242         }
243         
244         //finally, calculate the left and top position of each item
245         for (i = 0; i < visibleCount; i++) {
246             child = visibleItems[i];
247             calcs = boxes[i];
248             
249             childMargins = child.margins;
250             leftOffset  += childMargins.left;
251             vertMargins  = childMargins.top + childMargins.bottom;
252             
253             calcs.left = leftOffset;
254             calcs.top  = topOffset + childMargins.top;
255
256             switch (this.align) {
257                 case 'stretch':
258                     stretchHeight = availHeight - vertMargins;
259                     calcs.height  = stretchHeight.constrain(child.minHeight || 0, child.maxHeight || 1000000);
260                     calcs.dirtySize = true;
261                     break;
262                 case 'stretchmax':
263                     stretchHeight = maxHeight - vertMargins;
264                     calcs.height  = stretchHeight.constrain(child.minHeight || 0, child.maxHeight || 1000000);
265                     calcs.dirtySize = true;
266                     break;
267                 case 'middle':
268                     var diff = availHeight - calcs.height - vertMargins;
269                     if (diff > 0) {
270                         calcs.top = topOffset + vertMargins + (diff / 2);
271                     }
272             }
273             
274             leftOffset += calcs.width + childMargins.right;
275         }
276
277         return {
278             boxes: boxes,
279             meta : {
280                 maxHeight   : maxHeight,
281                 nonFlexWidth: nonFlexWidth,
282                 desiredWidth: desiredWidth,
283                 minimumWidth: minimumWidth,
284                 shortfall   : desiredWidth - width,
285                 tooNarrow   : tooNarrow
286             }
287         };
288     }
289 });
290
291 Ext.Container.LAYOUTS.hbox = Ext.layout.HBoxLayout;