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