Upgrade to ExtJS 3.3.1 - Released 11/30/2010
[extjs.git] / examples / ux / ColumnHeaderGroup.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 Ext.ns('Ext.ux.grid');
8
9 Ext.ux.grid.ColumnHeaderGroup = Ext.extend(Ext.util.Observable, {
10
11     constructor: function(config){
12         this.config = config;
13     },
14
15     init: function(grid){
16         Ext.applyIf(grid.colModel, this.config);
17         Ext.apply(grid.getView(), this.viewConfig);
18     },
19
20     viewConfig: {
21         initTemplates: function(){
22             this.constructor.prototype.initTemplates.apply(this, arguments);
23             var ts = this.templates || {};
24             if(!ts.gcell){
25                 ts.gcell = new Ext.XTemplate('<td class="x-grid3-hd x-grid3-gcell x-grid3-td-{id} ux-grid-hd-group-row-{row} {cls}" style="{style}">', '<div {tooltip} class="x-grid3-hd-inner x-grid3-hd-{id}" unselectable="on" style="{istyle}">', this.grid.enableHdMenu ? '<a class="x-grid3-hd-btn" href="#"></a>' : '', '{value}</div></td>');
26             }
27             this.templates = ts;
28             this.hrowRe = new RegExp("ux-grid-hd-group-row-(\\d+)", "");
29         },
30
31         renderHeaders: function(){
32             var ts = this.templates, headers = [], cm = this.cm, rows = cm.rows, tstyle = 'width:' + this.getTotalWidth() + ';';
33
34             for(var row = 0, rlen = rows.length; row < rlen; row++){
35                 var r = rows[row], cells = [];
36                 for(var i = 0, gcol = 0, len = r.length; i < len; i++){
37                     var group = r[i];
38                     group.colspan = group.colspan || 1;
39                     var id = this.getColumnId(group.dataIndex ? cm.findColumnIndex(group.dataIndex) : gcol), gs = Ext.ux.grid.ColumnHeaderGroup.prototype.getGroupStyle.call(this, group, gcol);
40                     cells[i] = ts.gcell.apply({
41                         cls: 'ux-grid-hd-group-cell',
42                         id: id,
43                         row: row,
44                         style: 'width:' + gs.width + ';' + (gs.hidden ? 'display:none;' : '') + (group.align ? 'text-align:' + group.align + ';' : ''),
45                         tooltip: group.tooltip ? (Ext.QuickTips.isEnabled() ? 'ext:qtip' : 'title') + '="' + group.tooltip + '"' : '',
46                         istyle: group.align == 'right' ? 'padding-right:16px' : '',
47                         btn: this.grid.enableHdMenu && group.header,
48                         value: group.header || '&nbsp;'
49                     });
50                     gcol += group.colspan;
51                 }
52                 headers[row] = ts.header.apply({
53                     tstyle: tstyle,
54                     cells: cells.join('')
55                 });
56             }
57             headers.push(this.constructor.prototype.renderHeaders.apply(this, arguments));
58             return headers.join('');
59         },
60
61         onColumnWidthUpdated: function(){
62             this.constructor.prototype.onColumnWidthUpdated.apply(this, arguments);
63             Ext.ux.grid.ColumnHeaderGroup.prototype.updateGroupStyles.call(this);
64         },
65
66         onAllColumnWidthsUpdated: function(){
67             this.constructor.prototype.onAllColumnWidthsUpdated.apply(this, arguments);
68             Ext.ux.grid.ColumnHeaderGroup.prototype.updateGroupStyles.call(this);
69         },
70
71         onColumnHiddenUpdated: function(){
72             this.constructor.prototype.onColumnHiddenUpdated.apply(this, arguments);
73             Ext.ux.grid.ColumnHeaderGroup.prototype.updateGroupStyles.call(this);
74         },
75
76         getHeaderCell: function(index){
77             return this.mainHd.query(this.cellSelector)[index];
78         },
79
80         findHeaderCell: function(el){
81             return el ? this.fly(el).findParent('td.x-grid3-hd', this.cellSelectorDepth) : false;
82         },
83
84         findHeaderIndex: function(el){
85             var cell = this.findHeaderCell(el);
86             return cell ? this.getCellIndex(cell) : false;
87         },
88
89         updateSortIcon: function(col, dir){
90             var sc = this.sortClasses, hds = this.mainHd.select(this.cellSelector).removeClass(sc);
91             hds.item(col).addClass(sc[dir == "DESC" ? 1 : 0]);
92         },
93
94         handleHdDown: function(e, t){
95             var el = Ext.get(t);
96             if(el.hasClass('x-grid3-hd-btn')){
97                 e.stopEvent();
98                 var hd = this.findHeaderCell(t);
99                 Ext.fly(hd).addClass('x-grid3-hd-menu-open');
100                 var index = this.getCellIndex(hd);
101                 this.hdCtxIndex = index;
102                 var ms = this.hmenu.items, cm = this.cm;
103                 ms.get('asc').setDisabled(!cm.isSortable(index));
104                 ms.get('desc').setDisabled(!cm.isSortable(index));
105                 this.hmenu.on('hide', function(){
106                     Ext.fly(hd).removeClass('x-grid3-hd-menu-open');
107                 }, this, {
108                     single: true
109                 });
110                 this.hmenu.show(t, 'tl-bl?');
111             }else if(el.hasClass('ux-grid-hd-group-cell') || Ext.fly(t).up('.ux-grid-hd-group-cell')){
112                 e.stopEvent();
113             }
114         },
115
116         handleHdMove: function(e, t){
117             var hd = this.findHeaderCell(this.activeHdRef);
118             if(hd && !this.headersDisabled && !Ext.fly(hd).hasClass('ux-grid-hd-group-cell')){
119                 var hw = this.splitHandleWidth || 5, r = this.activeHdRegion, x = e.getPageX(), ss = hd.style, cur = '';
120                 if(this.grid.enableColumnResize !== false){
121                     if(x - r.left <= hw && this.cm.isResizable(this.activeHdIndex - 1)){
122                         cur = Ext.isAir ? 'move' : Ext.isWebKit ? 'e-resize' : 'col-resize'; // col-resize
123                                                                                                 // not
124                                                                                                 // always
125                                                                                                 // supported
126                     }else if(r.right - x <= (!this.activeHdBtn ? hw : 2) && this.cm.isResizable(this.activeHdIndex)){
127                         cur = Ext.isAir ? 'move' : Ext.isWebKit ? 'w-resize' : 'col-resize';
128                     }
129                 }
130                 ss.cursor = cur;
131             }
132         },
133
134         handleHdOver: function(e, t){
135             var hd = this.findHeaderCell(t);
136             if(hd && !this.headersDisabled){
137                 this.activeHdRef = t;
138                 this.activeHdIndex = this.getCellIndex(hd);
139                 var fly = this.fly(hd);
140                 this.activeHdRegion = fly.getRegion();
141                 if(!(this.cm.isMenuDisabled(this.activeHdIndex) || fly.hasClass('ux-grid-hd-group-cell'))){
142                     fly.addClass('x-grid3-hd-over');
143                     this.activeHdBtn = fly.child('.x-grid3-hd-btn');
144                     if(this.activeHdBtn){
145                         this.activeHdBtn.dom.style.height = (hd.firstChild.offsetHeight - 1) + 'px';
146                     }
147                 }
148             }
149         },
150
151         handleHdOut: function(e, t){
152             var hd = this.findHeaderCell(t);
153             if(hd && (!Ext.isIE || !e.within(hd, true))){
154                 this.activeHdRef = null;
155                 this.fly(hd).removeClass('x-grid3-hd-over');
156                 hd.style.cursor = '';
157             }
158         },
159
160         handleHdMenuClick: function(item){
161             var index = this.hdCtxIndex, cm = this.cm, ds = this.ds, id = item.getItemId();
162             switch(id){
163                 case 'asc':
164                     ds.sort(cm.getDataIndex(index), 'ASC');
165                     break;
166                 case 'desc':
167                     ds.sort(cm.getDataIndex(index), 'DESC');
168                     break;
169                 default:
170                     if(id.substr(0, 6) == 'group-'){
171                         var i = id.split('-'), row = parseInt(i[1], 10), col = parseInt(i[2], 10), r = this.cm.rows[row], group, gcol = 0;
172                         for(var i = 0, len = r.length; i < len; i++){
173                             group = r[i];
174                             if(col >= gcol && col < gcol + group.colspan){
175                                 break;
176                             }
177                             gcol += group.colspan;
178                         }
179                         if(item.checked){
180                             var max = cm.getColumnsBy(this.isHideableColumn, this).length;
181                             for(var i = gcol, len = gcol + group.colspan; i < len; i++){
182                                 if(!cm.isHidden(i)){
183                                     max--;
184                                 }
185                             }
186                             if(max < 1){
187                                 this.onDenyColumnHide();
188                                 return false;
189                             }
190                         }
191                         for(var i = gcol, len = gcol + group.colspan; i < len; i++){
192                             if(cm.config[i].fixed !== true && cm.config[i].hideable !== false){
193                                 cm.setHidden(i, item.checked);
194                             }
195                         }
196                     }else if(id.substr(0, 4) == 'col-'){
197                         index = cm.getIndexById(id.substr(4));
198                         if(index != -1){
199                             if(item.checked && cm.getColumnsBy(this.isHideableColumn, this).length <= 1){
200                                 this.onDenyColumnHide();
201                                 return false;
202                             }
203                             cm.setHidden(index, item.checked);
204                         }
205                     }
206                     if(id.substr(0, 6) == 'group-' || id.substr(0, 4) == 'col-'){
207                         item.checked = !item.checked;
208                         if(item.menu){
209                             var updateChildren = function(menu){
210                                 menu.items.each(function(childItem){
211                                     if(!childItem.disabled){
212                                         childItem.setChecked(item.checked, false);
213                                         if(childItem.menu){
214                                             updateChildren(childItem.menu);
215                                         }
216                                     }
217                                 });
218                             }
219                             updateChildren(item.menu);
220                         }
221                         var parentMenu = item, parentItem;
222                         while(parentMenu = parentMenu.parentMenu){
223                             if(!parentMenu.parentMenu || !(parentItem = parentMenu.parentMenu.items.get(parentMenu.getItemId())) || !parentItem.setChecked){
224                                 break;
225                             }
226                             var checked = parentMenu.items.findIndexBy(function(m){
227                                 return m.checked;
228                             }) >= 0;
229                             parentItem.setChecked(checked, true);
230                         }
231                         item.checked = !item.checked;
232                     }
233             }
234             return true;
235         },
236
237         beforeColMenuShow: function(){
238             var cm = this.cm, rows = this.cm.rows;
239             this.colMenu.removeAll();
240             for(var col = 0, clen = cm.getColumnCount(); col < clen; col++){
241                 var menu = this.colMenu, title = cm.getColumnHeader(col), text = [];
242                 if(cm.config[col].fixed !== true && cm.config[col].hideable !== false){
243                     for(var row = 0, rlen = rows.length; row < rlen; row++){
244                         var r = rows[row], group, gcol = 0;
245                         for(var i = 0, len = r.length; i < len; i++){
246                             group = r[i];
247                             if(col >= gcol && col < gcol + group.colspan){
248                                 break;
249                             }
250                             gcol += group.colspan;
251                         }
252                         if(group && group.header){
253                             if(cm.hierarchicalColMenu){
254                                 var gid = 'group-' + row + '-' + gcol,
255                                     item = menu.items ? menu.getComponent(gid) : null,
256                                     submenu = item ? item.menu : null;
257                                 if(!submenu){
258                                     submenu = new Ext.menu.Menu({
259                                         itemId: gid
260                                     });
261                                     submenu.on("itemclick", this.handleHdMenuClick, this);
262                                     var checked = false, disabled = true;
263                                     for(var c = gcol, lc = gcol + group.colspan; c < lc; c++){
264                                         if(!cm.isHidden(c)){
265                                             checked = true;
266                                         }
267                                         if(cm.config[c].hideable !== false){
268                                             disabled = false;
269                                         }
270                                     }
271                                     menu.add({
272                                         itemId: gid,
273                                         text: group.header,
274                                         menu: submenu,
275                                         hideOnClick: false,
276                                         checked: checked,
277                                         disabled: disabled
278                                     });
279                                 }
280                                 menu = submenu;
281                             }else{
282                                 text.push(group.header);
283                             }
284                         }
285                     }
286                     text.push(title);
287                     menu.add(new Ext.menu.CheckItem({
288                         itemId: "col-" + cm.getColumnId(col),
289                         text: text.join(' '),
290                         checked: !cm.isHidden(col),
291                         hideOnClick: false,
292                         disabled: cm.config[col].hideable === false
293                     }));
294                 }
295             }
296         },
297
298         afterRenderUI: function(){
299             this.constructor.prototype.afterRenderUI.apply(this, arguments);
300             Ext.apply(this.columnDrop, Ext.ux.grid.ColumnHeaderGroup.prototype.columnDropConfig);
301             Ext.apply(this.splitZone, Ext.ux.grid.ColumnHeaderGroup.prototype.splitZoneConfig);
302         }
303     },
304
305     splitZoneConfig: {
306         allowHeaderDrag: function(e){
307             return !e.getTarget(null, null, true).hasClass('ux-grid-hd-group-cell');
308         }
309     },
310
311     columnDropConfig: {
312         getTargetFromEvent: function(e){
313             var t = Ext.lib.Event.getTarget(e);
314             return this.view.findHeaderCell(t);
315         },
316
317         positionIndicator: function(h, n, e){
318             var data = Ext.ux.grid.ColumnHeaderGroup.prototype.getDragDropData.call(this, h, n, e);
319             if(data === false){
320                 return false;
321             }
322             var px = data.px + this.proxyOffsets[0];
323             this.proxyTop.setLeftTop(px, data.r.top + this.proxyOffsets[1]);
324             this.proxyTop.show();
325             this.proxyBottom.setLeftTop(px, data.r.bottom);
326             this.proxyBottom.show();
327             return data.pt;
328         },
329
330         onNodeDrop: function(n, dd, e, data){
331             var h = data.header;
332             if(h != n){
333                 var d = Ext.ux.grid.ColumnHeaderGroup.prototype.getDragDropData.call(this, h, n, e);
334                 if(d === false){
335                     return false;
336                 }
337                 var cm = this.grid.colModel, right = d.oldIndex < d.newIndex, rows = cm.rows;
338                 for(var row = d.row, rlen = rows.length; row < rlen; row++){
339                     var r = rows[row], len = r.length, fromIx = 0, span = 1, toIx = len;
340                     for(var i = 0, gcol = 0; i < len; i++){
341                         var group = r[i];
342                         if(d.oldIndex >= gcol && d.oldIndex < gcol + group.colspan){
343                             fromIx = i;
344                         }
345                         if(d.oldIndex + d.colspan - 1 >= gcol && d.oldIndex + d.colspan - 1 < gcol + group.colspan){
346                             span = i - fromIx + 1;
347                         }
348                         if(d.newIndex >= gcol && d.newIndex < gcol + group.colspan){
349                             toIx = i;
350                         }
351                         gcol += group.colspan;
352                     }
353                     var groups = r.splice(fromIx, span);
354                     rows[row] = r.splice(0, toIx - (right ? span : 0)).concat(groups).concat(r);
355                 }
356                 for(var c = 0; c < d.colspan; c++){
357                     var oldIx = d.oldIndex + (right ? 0 : c), newIx = d.newIndex + (right ? -1 : c);
358                     cm.moveColumn(oldIx, newIx);
359                     this.grid.fireEvent("columnmove", oldIx, newIx);
360                 }
361                 return true;
362             }
363             return false;
364         }
365     },
366
367     getGroupStyle: function(group, gcol){
368         var width = 0, hidden = true;
369         for(var i = gcol, len = gcol + group.colspan; i < len; i++){
370             if(!this.cm.isHidden(i)){
371                 var cw = this.cm.getColumnWidth(i);
372                 if(typeof cw == 'number'){
373                     width += cw;
374                 }
375                 hidden = false;
376             }
377         }
378         return {
379             width: (Ext.isBorderBox || (Ext.isWebKit && !Ext.isSafari2) ? width : Math.max(width - this.borderWidth, 0)) + 'px',
380             hidden: hidden
381         };
382     },
383
384     updateGroupStyles: function(col){
385         var tables = this.mainHd.query('.x-grid3-header-offset > table'), tw = this.getTotalWidth(), rows = this.cm.rows;
386         for(var row = 0; row < tables.length; row++){
387             tables[row].style.width = tw;
388             if(row < rows.length){
389                 var cells = tables[row].firstChild.firstChild.childNodes;
390                 for(var i = 0, gcol = 0; i < cells.length; i++){
391                     var group = rows[row][i];
392                     if((typeof col != 'number') || (col >= gcol && col < gcol + group.colspan)){
393                         var gs = Ext.ux.grid.ColumnHeaderGroup.prototype.getGroupStyle.call(this, group, gcol);
394                         cells[i].style.width = gs.width;
395                         cells[i].style.display = gs.hidden ? 'none' : '';
396                     }
397                     gcol += group.colspan;
398                 }
399             }
400         }
401     },
402
403     getGroupRowIndex: function(el){
404         if(el){
405             var m = el.className.match(this.hrowRe);
406             if(m && m[1]){
407                 return parseInt(m[1], 10);
408             }
409         }
410         return this.cm.rows.length;
411     },
412
413     getGroupSpan: function(row, col){
414         if(row < 0){
415             return {
416                 col: 0,
417                 colspan: this.cm.getColumnCount()
418             };
419         }
420         var r = this.cm.rows[row];
421         if(r){
422             for(var i = 0, gcol = 0, len = r.length; i < len; i++){
423                 var group = r[i];
424                 if(col >= gcol && col < gcol + group.colspan){
425                     return {
426                         col: gcol,
427                         colspan: group.colspan
428                     };
429                 }
430                 gcol += group.colspan;
431             }
432             return {
433                 col: gcol,
434                 colspan: 0
435             };
436         }
437         return {
438             col: col,
439             colspan: 1
440         };
441     },
442
443     getDragDropData: function(h, n, e){
444         if(h.parentNode != n.parentNode){
445             return false;
446         }
447         var cm = this.grid.colModel, x = Ext.lib.Event.getPageX(e), r = Ext.lib.Dom.getRegion(n.firstChild), px, pt;
448         if((r.right - x) <= (r.right - r.left) / 2){
449             px = r.right + this.view.borderWidth;
450             pt = "after";
451         }else{
452             px = r.left;
453             pt = "before";
454         }
455         var oldIndex = this.view.getCellIndex(h), newIndex = this.view.getCellIndex(n);
456         if(cm.isFixed(newIndex)){
457             return false;
458         }
459         var row = Ext.ux.grid.ColumnHeaderGroup.prototype.getGroupRowIndex.call(this.view, h),
460             oldGroup = Ext.ux.grid.ColumnHeaderGroup.prototype.getGroupSpan.call(this.view, row, oldIndex),
461             newGroup = Ext.ux.grid.ColumnHeaderGroup.prototype.getGroupSpan.call(this.view, row, newIndex),
462             oldIndex = oldGroup.col;
463             newIndex = newGroup.col + (pt == "after" ? newGroup.colspan : 0);
464         if(newIndex >= oldGroup.col && newIndex <= oldGroup.col + oldGroup.colspan){
465             return false;
466         }
467         var parentGroup = Ext.ux.grid.ColumnHeaderGroup.prototype.getGroupSpan.call(this.view, row - 1, oldIndex);
468         if(newIndex < parentGroup.col || newIndex > parentGroup.col + parentGroup.colspan){
469             return false;
470         }
471         return {
472             r: r,
473             px: px,
474             pt: pt,
475             row: row,
476             oldIndex: oldIndex,
477             newIndex: newIndex,
478             colspan: oldGroup.colspan
479         };
480     }
481 });