Upgrade to ExtJS 4.0.7 - Released 10/19/2011
[extjs.git] / src / layout / container / Table.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  * This layout allows you to easily render content into an HTML table. The total number of columns can be specified, and
17  * rowspan and colspan can be used to create complex layouts within the table. This class is intended to be extended or
18  * created via the `layout: {type: 'table'}` {@link Ext.container.Container#layout} config, and should generally not
19  * need to be created directly via the new keyword.
20  *
21  * Note that when creating a layout via config, the layout-specific config properties must be passed in via the {@link
22  * Ext.container.Container#layout} object which will then be applied internally to the layout. In the case of
23  * TableLayout, the only valid layout config properties are {@link #columns} and {@link #tableAttrs}. However, the items
24  * added to a TableLayout can supply the following table-specific config properties:
25  *
26  *   - **rowspan** Applied to the table cell containing the item.
27  *   - **colspan** Applied to the table cell containing the item.
28  *   - **cellId** An id applied to the table cell containing the item.
29  *   - **cellCls** A CSS class name added to the table cell containing the item.
30  *
31  * The basic concept of building up a TableLayout is conceptually very similar to building up a standard HTML table. You
32  * simply add each panel (or "cell") that you want to include along with any span attributes specified as the special
33  * config properties of rowspan and colspan which work exactly like their HTML counterparts. Rather than explicitly
34  * creating and nesting rows and columns as you would in HTML, you simply specify the total column count in the
35  * layoutConfig and start adding panels in their natural order from left to right, top to bottom. The layout will
36  * automatically figure out, based on the column count, rowspans and colspans, how to position each panel within the
37  * table. Just like with HTML tables, your rowspans and colspans must add up correctly in your overall layout or you'll
38  * end up with missing and/or extra cells! Example usage:
39  *
40  *     @example
41  *     Ext.create('Ext.panel.Panel', {
42  *         title: 'Table Layout',
43  *         width: 300,
44  *         height: 150,
45  *         layout: {
46  *             type: 'table',
47  *             // The total column count must be specified here
48  *             columns: 3
49  *         },
50  *         defaults: {
51  *             // applied to each contained panel
52  *             bodyStyle: 'padding:20px'
53  *         },
54  *         items: [{
55  *             html: 'Cell A content',
56  *             rowspan: 2
57  *         },{
58  *             html: 'Cell B content',
59  *             colspan: 2
60  *         },{
61  *             html: 'Cell C content',
62  *             cellCls: 'highlight'
63  *         },{
64  *             html: 'Cell D content'
65  *         }],
66  *         renderTo: Ext.getBody()
67  *     });
68  */
69 Ext.define('Ext.layout.container.Table', {
70
71     /* Begin Definitions */
72
73     alias: ['layout.table'],
74     extend: 'Ext.layout.container.Auto',
75     alternateClassName: 'Ext.layout.TableLayout',
76
77     /* End Definitions */
78
79     /**
80      * @cfg {Number} columns
81      * The total number of columns to create in the table for this layout. If not specified, all Components added to
82      * this layout will be rendered into a single row using one column per Component.
83      */
84
85     // private
86     monitorResize:false,
87
88     type: 'table',
89
90     // Table layout is a self-sizing layout. When an item of for example, a dock layout, the Panel must expand to accommodate
91     // a table layout. See in particular AbstractDock::onLayout for use of this flag.
92     autoSize: true,
93
94     clearEl: true, // Base class will not create it if already truthy. Not needed in tables.
95
96     targetCls: Ext.baseCSSPrefix + 'table-layout-ct',
97     tableCls: Ext.baseCSSPrefix + 'table-layout',
98     cellCls: Ext.baseCSSPrefix + 'table-layout-cell',
99
100     /**
101      * @cfg {Object} tableAttrs
102      * An object containing properties which are added to the {@link Ext.DomHelper DomHelper} specification used to
103      * create the layout's `<table>` element. Example:
104      *
105      *     {
106      *         xtype: 'panel',
107      *         layout: {
108      *             type: 'table',
109      *             columns: 3,
110      *             tableAttrs: {
111      *                 style: {
112      *                     width: '100%'
113      *                 }
114      *             }
115      *         }
116      *     }
117      */
118     tableAttrs:null,
119
120     /**
121      * @cfg {Object} trAttrs
122      * An object containing properties which are added to the {@link Ext.DomHelper DomHelper} specification used to
123      * create the layout's <tr> elements.
124      */
125
126     /**
127      * @cfg {Object} tdAttrs
128      * An object containing properties which are added to the {@link Ext.DomHelper DomHelper} specification used to
129      * create the layout's <td> elements.
130      */
131
132     /**
133      * @private
134      * Iterates over all passed items, ensuring they are rendered in a cell in the proper
135      * location in the table structure.
136      */
137     renderItems: function(items) {
138         var tbody = this.getTable().tBodies[0],
139             rows = tbody.rows,
140             i = 0,
141             len = items.length,
142             cells, curCell, rowIdx, cellIdx, item, trEl, tdEl, itemCt;
143
144         // Calculate the correct cell structure for the current items
145         cells = this.calculateCells(items);
146
147         // Loop over each cell and compare to the current cells in the table, inserting/
148         // removing/moving cells as needed, and making sure each item is rendered into
149         // the correct cell.
150         for (; i < len; i++) {
151             curCell = cells[i];
152             rowIdx = curCell.rowIdx;
153             cellIdx = curCell.cellIdx;
154             item = items[i];
155
156             // If no row present, create and insert one
157             trEl = rows[rowIdx];
158             if (!trEl) {
159                 trEl = tbody.insertRow(rowIdx);
160                 if (this.trAttrs) {
161                     trEl.set(this.trAttrs);
162                 }
163             }
164
165             // If no cell present, create and insert one
166             itemCt = tdEl = Ext.get(trEl.cells[cellIdx] || trEl.insertCell(cellIdx));
167             if (this.needsDivWrap()) { //create wrapper div if needed - see docs below
168                 itemCt = tdEl.first() || tdEl.createChild({tag: 'div'});
169                 itemCt.setWidth(null);
170             }
171
172             // Render or move the component into the cell
173             if (!item.rendered) {
174                 this.renderItem(item, itemCt, 0);
175             }
176             else if (!this.isValidParent(item, itemCt, 0)) {
177                 this.moveItem(item, itemCt, 0);
178             }
179
180             // Set the cell properties
181             if (this.tdAttrs) {
182                 tdEl.set(this.tdAttrs);
183             }
184             tdEl.set({
185                 colSpan: item.colspan || 1,
186                 rowSpan: item.rowspan || 1,
187                 id: item.cellId || '',
188                 cls: this.cellCls + ' ' + (item.cellCls || '')
189             });
190
191             // If at the end of a row, remove any extra cells
192             if (!cells[i + 1] || cells[i + 1].rowIdx !== rowIdx) {
193                 cellIdx++;
194                 while (trEl.cells[cellIdx]) {
195                     trEl.deleteCell(cellIdx);
196                 }
197             }
198         }
199
200         // Delete any extra rows
201         rowIdx++;
202         while (tbody.rows[rowIdx]) {
203             tbody.deleteRow(rowIdx);
204         }
205     },
206
207     afterLayout: function() {
208         this.callParent();
209
210         if (this.needsDivWrap()) {
211             // set wrapper div width to match layed out item - see docs below
212             Ext.Array.forEach(this.getLayoutItems(), function(item) {
213                 Ext.fly(item.el.dom.parentNode).setWidth(item.getWidth());
214             });
215         }
216     },
217
218     /**
219      * @private
220      * Determine the row and cell indexes for each component, taking into consideration
221      * the number of columns and each item's configured colspan/rowspan values.
222      * @param {Array} items The layout components
223      * @return {Object[]} List of row and cell indexes for each of the components
224      */
225     calculateCells: function(items) {
226         var cells = [],
227             rowIdx = 0,
228             colIdx = 0,
229             cellIdx = 0,
230             totalCols = this.columns || Infinity,
231             rowspans = [], //rolling list of active rowspans for each column
232             i = 0, j,
233             len = items.length,
234             item;
235
236         for (; i < len; i++) {
237             item = items[i];
238
239             // Find the first available row/col slot not taken up by a spanning cell
240             while (colIdx >= totalCols || rowspans[colIdx] > 0) {
241                 if (colIdx >= totalCols) {
242                     // move down to next row
243                     colIdx = 0;
244                     cellIdx = 0;
245                     rowIdx++;
246
247                     // decrement all rowspans
248                     for (j = 0; j < totalCols; j++) {
249                         if (rowspans[j] > 0) {
250                             rowspans[j]--;
251                         }
252                     }
253                 } else {
254                     colIdx++;
255                 }
256             }
257
258             // Add the cell info to the list
259             cells.push({
260                 rowIdx: rowIdx,
261                 cellIdx: cellIdx
262             });
263
264             // Increment
265             for (j = item.colspan || 1; j; --j) {
266                 rowspans[colIdx] = item.rowspan || 1;
267                 ++colIdx;
268             }
269             ++cellIdx;
270         }
271
272         return cells;
273     },
274
275     /**
276      * @private
277      * Return the layout's table element, creating it if necessary.
278      */
279     getTable: function() {
280         var table = this.table;
281         if (!table) {
282             table = this.table = this.getTarget().createChild(
283                 Ext.apply({
284                     tag: 'table',
285                     role: 'presentation',
286                     cls: this.tableCls,
287                     cellspacing: 0, //TODO should this be specified or should CSS handle it?
288                     cn: {tag: 'tbody'}
289                 }, this.tableAttrs),
290                 null, true
291             );
292         }
293         return table;
294     },
295
296     /**
297      * @private
298      * Opera 10.5 has a bug where if a table cell's child has box-sizing:border-box and padding, it
299      * will include that padding in the size of the cell, making it always larger than the
300      * shrink-wrapped size of its contents. To get around this we have to wrap the contents in a div
301      * and then set that div's width to match the item rendered within it afterLayout. This method
302      * determines whether we need the wrapper div; it currently does a straight UA sniff as this bug
303      * seems isolated to just Opera 10.5, but feature detection could be added here if needed.
304      */
305     needsDivWrap: function() {
306         return Ext.isOpera10_5;
307     }
308 });