Upgrade to ExtJS 3.3.1 - Released 11/30/2010
[extjs.git] / src / widgets / grid / PivotGridView.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.grid.PivotGridView
9  * @extends Ext.grid.GridView
10  * Specialised GridView for rendering Pivot Grid components. Config can be passed to the PivotGridView via the PivotGrid constructor's
11  * viewConfig option:
12 <pre><code>
13 new Ext.grid.PivotGrid({
14     viewConfig: {
15         title: 'My Pivot Grid',
16         getCellCls: function(value) {
17             return value > 10 'red' : 'green';
18         }
19     }
20 });
21 </code></pre>
22  * <p>Currently {@link #title} and {@link #getCellCls} are the only configuration options accepted by PivotGridView. All other 
23  * interaction is performed via the {@link Ext.grid.PivotGrid PivotGrid} class.</p>
24  */
25 Ext.grid.PivotGridView = Ext.extend(Ext.grid.GridView, {
26     
27     /**
28      * The CSS class added to all group header cells. Defaults to 'grid-hd-group-cell'
29      * @property colHeaderCellCls
30      * @type String
31      */
32     colHeaderCellCls: 'grid-hd-group-cell',
33     
34     /**
35      * @cfg {String} title Optional title to be placed in the top left corner of the PivotGrid. Defaults to an empty string.
36      */
37     title: '',
38     
39     /**
40      * @cfg {Function} getCellCls Optional function which should return a CSS class name for each cell value. This is useful when
41      * color coding cells based on their value. Defaults to undefined.
42      */
43     
44     /**
45      * Returns the headers to be rendered at the top of the grid. Should be a 2-dimensional array, where each item specifies the number
46      * of columns it groups (column in this case refers to normal grid columns). In the example below we have 5 city groups, which are
47      * each part of a continent supergroup. The colspan for each city group refers to the number of normal grid columns that group spans,
48      * so in this case the grid would be expected to have a total of 12 columns:
49 <pre><code>
50 [
51     {
52         items: [
53             {header: 'England',   colspan: 5},
54             {header: 'USA',       colspan: 3}
55         ]
56     },
57     {
58         items: [
59             {header: 'London',    colspan: 2},
60             {header: 'Cambridge', colspan: 3},
61             {header: 'Palo Alto', colspan: 3}
62         ]
63     }
64 ]
65 </code></pre>
66      * In the example above we have cities nested under countries. The nesting could be deeper if desired - e.g. Continent -> Country ->
67      * State -> City, or any other structure. The only constaint is that the same depth must be used throughout the structure.
68      * @return {Array} A tree structure containing the headers to be rendered. Must include the colspan property at each level, which should
69      * be the sum of all child nodes beneath this node.
70      */
71     getColumnHeaders: function() {
72         return this.grid.topAxis.buildHeaders();;
73     },
74     
75     /**
76      * Returns the headers to be rendered on the left of the grid. Should be a 2-dimensional array, where each item specifies the number
77      * of rows it groups. In the example below we have 5 city groups, which are each part of a continent supergroup. The rowspan for each 
78      * city group refers to the number of normal grid columns that group spans, so in this case the grid would be expected to have a 
79      * total of 12 rows:
80 <pre><code>
81 [
82     {
83         width: 90,
84         items: [
85             {header: 'England',   rowspan: 5},
86             {header: 'USA',       rowspan: 3}
87         ]
88     },
89     {
90         width: 50,
91         items: [
92             {header: 'London',    rowspan: 2},
93             {header: 'Cambridge', rowspan: 3},
94             {header: 'Palo Alto', rowspan: 3}
95         ]
96     }
97 ]
98 </code></pre>
99      * In the example above we have cities nested under countries. The nesting could be deeper if desired - e.g. Continent -> Country ->
100      * State -> City, or any other structure. The only constaint is that the same depth must be used throughout the structure.
101      * @return {Array} A tree structure containing the headers to be rendered. Must include the colspan property at each level, which should
102      * be the sum of all child nodes beneath this node.
103      * Each group may specify the width it should be rendered with.
104      * @return {Array} The row groups
105      */
106     getRowHeaders: function() {
107         return this.grid.leftAxis.buildHeaders();
108     },
109     
110     /**
111      * @private
112      * Renders rows between start and end indexes
113      * @param {Number} startRow Index of the first row to render
114      * @param {Number} endRow Index of the last row to render
115      */
116     renderRows : function(startRow, endRow) {
117         var grid          = this.grid,
118             rows          = grid.extractData(),
119             rowCount      = rows.length,
120             templates     = this.templates,
121             renderer      = grid.renderer,
122             hasRenderer   = typeof renderer == 'function',
123             getCellCls    = this.getCellCls,
124             hasGetCellCls = typeof getCellCls == 'function',
125             cellTemplate  = templates.cell,
126             rowTemplate   = templates.row,
127             rowBuffer     = [],
128             meta          = {},
129             tstyle        = 'width:' + this.getGridInnerWidth() + 'px;',
130             colBuffer, column, i;
131         
132         startRow = startRow || 0;
133         endRow   = Ext.isDefined(endRow) ? endRow : rowCount - 1;
134         
135         for (i = 0; i < rowCount; i++) {
136             row = rows[i];
137             colCount  = row.length;
138             colBuffer = [];
139             
140             rowIndex = startRow + i;
141
142             //build up each column's HTML
143             for (j = 0; j < colCount; j++) {
144                 cell = row[j];
145
146                 meta.css   = j === 0 ? 'x-grid3-cell-first ' : (j == (colCount - 1) ? 'x-grid3-cell-last ' : '');
147                 meta.attr  = meta.cellAttr = '';
148                 meta.value = cell;
149
150                 if (Ext.isEmpty(meta.value)) {
151                     meta.value = '&#160;';
152                 }
153                 
154                 if (hasRenderer) {
155                     meta.value = renderer(meta.value);
156                 }
157                 
158                 if (hasGetCellCls) {
159                     meta.css += getCellCls(meta.value) + ' ';
160                 }
161
162                 colBuffer[colBuffer.length] = cellTemplate.apply(meta);
163             }
164             
165             rowBuffer[rowBuffer.length] = rowTemplate.apply({
166                 tstyle: tstyle,
167                 cols  : colCount,
168                 cells : colBuffer.join(""),
169                 alt   : ''
170             });
171         }
172         
173         return rowBuffer.join("");
174     },
175     
176     /**
177      * The master template to use when rendering the GridView. Has a default template
178      * @property Ext.Template
179      * @type masterTpl
180      */
181     masterTpl: new Ext.Template(
182         '<div class="x-grid3 x-pivotgrid" hidefocus="true">',
183             '<div class="x-grid3-viewport">',
184                 '<div class="x-grid3-header">',
185                     '<div class="x-grid3-header-title"><span>{title}</span></div>',
186                     '<div class="x-grid3-header-inner">',
187                         '<div class="x-grid3-header-offset" style="{ostyle}"></div>',
188                     '</div>',
189                     '<div class="x-clear"></div>',
190                 '</div>',
191                 '<div class="x-grid3-scroller">',
192                     '<div class="x-grid3-row-headers"></div>',
193                     '<div class="x-grid3-body" style="{bstyle}">{body}</div>',
194                     '<a href="#" class="x-grid3-focus" tabIndex="-1"></a>',
195                 '</div>',
196             '</div>',
197             '<div class="x-grid3-resize-marker">&#160;</div>',
198             '<div class="x-grid3-resize-proxy">&#160;</div>',
199         '</div>'
200     ),
201     
202     /**
203      * @private
204      * Adds a gcell template to the internal templates object. This is used to render the headers in a multi-level column header.
205      */
206     initTemplates: function() {
207         Ext.grid.PivotGridView.superclass.initTemplates.apply(this, arguments);
208         
209         var templates = this.templates || {};
210         if (!templates.gcell) {
211             templates.gcell = new Ext.XTemplate(
212                 '<td class="x-grid3-hd x-grid3-gcell x-grid3-td-{id} ux-grid-hd-group-row-{row} ' + this.colHeaderCellCls + '" style="{style}">',
213                     '<div {tooltip} class="x-grid3-hd-inner x-grid3-hd-{id}" unselectable="on" style="{istyle}">', 
214                         this.grid.enableHdMenu ? '<a class="x-grid3-hd-btn" href="#"></a>' : '', '{value}',
215                     '</div>',
216                 '</td>'
217             );
218         }
219         
220         this.templates = templates;
221         this.hrowRe = new RegExp("ux-grid-hd-group-row-(\\d+)", "");
222     },
223     
224     /**
225      * @private
226      * Sets up the reference to the row headers element
227      */
228     initElements: function() {
229         Ext.grid.PivotGridView.superclass.initElements.apply(this, arguments);
230         
231         /**
232          * @property rowHeadersEl
233          * @type Ext.Element
234          * The element containing all row headers
235          */
236         this.rowHeadersEl = new Ext.Element(this.scroller.child('div.x-grid3-row-headers'));
237         
238         /**
239          * @property headerTitleEl
240          * @type Ext.Element
241          * The element that contains the optional title (top left section of the pivot grid)
242          */
243         this.headerTitleEl = new Ext.Element(this.mainHd.child('div.x-grid3-header-title'));
244     },
245     
246     /**
247      * @private
248      * Takes row headers into account when calculating total available width
249      */
250     getGridInnerWidth: function() {
251         var previousWidth = Ext.grid.PivotGridView.superclass.getGridInnerWidth.apply(this, arguments);
252         
253         return previousWidth - this.getTotalRowHeaderWidth();
254     },
255     
256     /**
257      * Returns the total width of all row headers as specified by {@link #getRowHeaders}
258      * @return {Number} The total width
259      */
260     getTotalRowHeaderWidth: function() {
261         var headers = this.getRowHeaders(),
262             length  = headers.length,
263             total   = 0,
264             i;
265         
266         for (i = 0; i< length; i++) {
267             total += headers[i].width;
268         }
269         
270         return total;
271     },
272     
273     /**
274      * @private
275      * Returns the total height of all column headers
276      * @return {Number} The total height
277      */
278     getTotalColumnHeaderHeight: function() {
279         return this.getColumnHeaders().length * 21;
280     },
281     
282     /**
283      * @private
284      * Slight specialisation of the GridView renderUI - just adds the row headers
285      */
286     renderUI : function() {
287         var templates  = this.templates,
288             innerWidth = this.getGridInnerWidth();
289             
290         return templates.master.apply({
291             body  : templates.body.apply({rows:'&#160;'}),
292             ostyle: 'width:' + innerWidth + 'px',
293             bstyle: 'width:' + innerWidth + 'px'
294         });
295     },
296     
297     /**
298      * @private
299      * Make sure that the headers and rows are all sized correctly during layout
300      */
301     onLayout: function(width, height) {
302         Ext.grid.PivotGridView.superclass.onLayout.apply(this, arguments);
303         
304         var width = this.getGridInnerWidth();
305         
306         this.resizeColumnHeaders(width);
307         this.resizeAllRows(width);
308     },
309     
310     /**
311      * Refreshs the grid UI
312      * @param {Boolean} headersToo (optional) True to also refresh the headers
313      */
314     refresh : function(headersToo) {
315         this.fireEvent('beforerefresh', this);
316         this.grid.stopEditing(true);
317         
318         var result = this.renderBody();
319         this.mainBody.update(result).setWidth(this.getGridInnerWidth());
320         if (headersToo === true) {
321             this.updateHeaders();
322             this.updateHeaderSortState();
323         }
324         this.processRows(0, true);
325         this.layout();
326         this.applyEmptyText();
327         this.fireEvent('refresh', this);
328     },
329     
330     /**
331      * @private
332      * Bypasses GridView's renderHeaders as they are taken care of separately by the PivotAxis instances
333      */
334     renderHeaders: Ext.emptyFn,
335     
336     /**
337      * @private
338      * Taken care of by PivotAxis
339      */
340     fitColumns: Ext.emptyFn,
341     
342     /**
343      * @private
344      * Called on layout, ensures that the width of each column header is correct. Omitting this can lead to faulty
345      * layouts when nested in a container.
346      * @param {Number} width The new width
347      */
348     resizeColumnHeaders: function(width) {
349         var topAxis = this.grid.topAxis;
350         
351         if (topAxis.rendered) {
352             topAxis.el.setWidth(width);
353         }
354     },
355     
356     /**
357      * @private
358      * Sets the row header div to the correct width. Should be called after rendering and reconfiguration of headers
359      */
360     resizeRowHeaders: function() {
361         var rowHeaderWidth = this.getTotalRowHeaderWidth(),
362             marginStyle    = String.format("margin-left: {0}px;", rowHeaderWidth);
363         
364         this.rowHeadersEl.setWidth(rowHeaderWidth);
365         this.mainBody.applyStyles(marginStyle);
366         Ext.fly(this.innerHd).applyStyles(marginStyle);
367         
368         this.headerTitleEl.setWidth(rowHeaderWidth);
369         this.headerTitleEl.setHeight(this.getTotalColumnHeaderHeight());
370     },
371     
372     /**
373      * @private
374      * Resizes all rendered rows to the given width. Usually called by onLayout
375      * @param {Number} width The new width
376      */
377     resizeAllRows: function(width) {
378         var rows   = this.getRows(),
379             length = rows.length,
380             i;
381         
382         for (i = 0; i < length; i++) {
383             Ext.fly(rows[i]).setWidth(width);
384             Ext.fly(rows[i]).child('table').setWidth(width);
385         }
386     },
387     
388     /**
389      * @private
390      * Updates the Row Headers, deferring the updating of Column Headers to GridView
391      */
392     updateHeaders: function() {
393         this.renderGroupRowHeaders();
394         this.renderGroupColumnHeaders();
395     },
396     
397     /**
398      * @private
399      * Renders all row header groups at all levels based on the structure fetched from {@link #getGroupRowHeaders}
400      */
401     renderGroupRowHeaders: function() {
402         var leftAxis = this.grid.leftAxis;
403         
404         this.resizeRowHeaders();
405         leftAxis.rendered = false;
406         leftAxis.render(this.rowHeadersEl);
407         
408         this.setTitle(this.title);
409     },
410     
411     /**
412      * Sets the title text in the top left segment of the PivotGridView
413      * @param {String} title The title
414      */
415     setTitle: function(title) {
416         this.headerTitleEl.child('span').dom.innerHTML = title;
417     },
418     
419     /**
420      * @private
421      * Renders all column header groups at all levels based on the structure fetched from {@link #getColumnHeaders}
422      */
423     renderGroupColumnHeaders: function() {
424         var topAxis = this.grid.topAxis;
425         
426         topAxis.rendered = false;
427         topAxis.render(this.innerHd.firstChild);
428     },
429     
430     /**
431      * @private
432      * Overridden to test whether the user is hovering over a group cell, in which case we don't show the menu
433      */
434     isMenuDisabled: function(cellIndex, el) {
435         return true;
436     }
437 });