define([ 'dojo/_base/lang', 'dojo/_base/declare', 'dojo/sniff', 'dojo/query', '../util/misc' ], function (lang, declare, has, query, miscUtil) { return declare(null, { // summary: // Extension allowing for specification of columns with additional // header rows spanning multiple columns for strictly display purposes. // Only works on `columns` arrays, not `columns` objects or `subRows` // (nor ColumnSets). // description: // CompoundColumns allows nested header cell configurations, wherein the // higher-level headers may span multiple columns and are for // display purposes only. // These nested header cells are configured using a special recursive // `children` property in the column definition, where only the deepest // children are ultimately rendered in the grid as actual columns. // In addition, the deepest child columns may be rendered without // individual headers by specifying `showChildHeaders: false` on the parent. configStructure: function () { // create a set of sub rows for the header row so we can do compound columns // the first row is a special spacer row var columns = (this.subRows && this.subRows[0]) || this.columns, headerRows = [[]], topHeaderRow = headerRows[0], contentColumns = []; // This first row is spacer row that will be made invisible (zero height) // with CSS, but it must be rendered as the first row since that is what // the table layout is driven by. headerRows[0].className = 'dgrid-spacer-row'; function processColumns(columns, level, hasLabel, parent) { var numColumns = 0, noop = function () {}, children, hasChildLabels; function processColumn(column, i) { // Handle the column config when it is an object rather // than an array. if (typeof column === 'string') { column = {label: column}; } if (!(columns instanceof Array) && !column.field) { column.field = i; } children = column.children; hasChildLabels = children && (column.showChildHeaders !== false); // Set a reference to the parent column so later the children's ids can // be updated to indicate the parent-child relationship. column.parentColumn = parent; if (children) { // it has children // make sure the column has an id if (column.id == null) { column.id = ((parent && parent.id) || level - 1) + '-' + topHeaderRow.length; } else if (parent && parent.id) { // Make sure nested compound columns have ids that are prefixed with // their parent's ids. column.id = parent.id + '-' + column.id; } } else { // it has no children, it is a normal header, add it to the content columns contentColumns.push(column); // add each one to the first spacer header row for proper layout of the header cells topHeaderRow.push(lang.delegate(column, {renderHeaderCell: noop})); numColumns++; } if (!hasChildLabels) { // create a header version of the column where we can define a specific rowSpan // we define the rowSpan as a negative, the number of levels less than the // total number of rows, which we don't know yet column = lang.delegate(column, {rowSpan: -level}); } if (children) { // Recursively process the children; this is specifically // performed *after* any potential lang.delegate calls // so the parent reference will receive additional info numColumns += (column.colSpan = processColumns(children, level + 1, hasChildLabels, column)); } // add the column to the header rows at the appropriate level if (hasLabel) { (headerRows[level] || (headerRows[level] = [])).push(column); } } miscUtil.each(columns, processColumn, this); return numColumns; } processColumns(columns, 1, true); var numHeaderRows = headerRows.length, i, j, headerRow, headerColumn; // Now go back through and increase the rowSpans of the headers to be // total rows minus the number of levels they are at. for (i = 0; i < numHeaderRows; i++) { headerRow = headerRows[i]; for (j = 0; j < headerRow.length; j++) { headerColumn = headerRow[j]; if (headerColumn.rowSpan < 1) { headerColumn.rowSpan += numHeaderRows; } } } // we need to set this to be used for subRows, so we make it a single row contentColumns = [contentColumns]; // set our header rows so that the grid will use the alternate header row // configuration for rendering the headers contentColumns.headerRows = headerRows; this.subRows = contentColumns; this.inherited(arguments); }, renderHeader: function () { var i, columns = this.subRows[0], headerColumns = this.subRows.headerRows[0]; this.inherited(arguments); // The object delegation performed in configStructure unfortunately // "protects" the original column definition objects (referenced by // columns and subRows) from obtaining headerNode information, so // copy them back in. for (i = columns.length; i--;) { columns[i].headerNode = headerColumns[i].headerNode; } }, _findSortArrowParent: function () { var parent = this.inherited(arguments), spacerRow = query('.dgrid-spacer-row', this.headerNode)[0], columnId, nodes; if (parent && spacerRow.contains(parent)) { columnId = parent.columnId; nodes = query('.dgrid-column-' + columnId, this.headerNode); return nodes[nodes.length - 1]; } }, _configColumn: function (column, rowColumns, prefix) { // Updates the id on a column definition that is a child to include // the parent's id. var parent = column.parentColumn; var columnId = column.id; if (parent) { // Adjust the id to incorporate the parent's id. // Remove the prefix if it was used to create the id var id = columnId.indexOf(prefix) === 0 ? columnId.substring(prefix.length) : columnId; prefix = parent.id + '-'; columnId = column.id = prefix + id; } this.inherited(arguments, [column, rowColumns, prefix]); }, cell: function (target, columnId) { // summary: // Get the cell object by node, event, or id, plus a columnId. // This extension prefixes children's column ids with the parents' column ids, // so cell takes that into account when looking for a column id. if (typeof columnId !== 'object') { // Find the columnId that corresponds with the provided id. // The provided id may be a suffix of the actual id. var column = this.column(columnId); if (column) { columnId = column.id; } } return this.inherited(arguments, [target, columnId]); }, column: function (target) { // summary: // Get the column object by node, event, or column id. Take into account parent column id // prefixes that may be added by this extension. var results = this.inherited(arguments); if (results == null && typeof target !== 'object') { // Find a column id that ends with the provided column id. This will locate a child column // by an id that was provided in the original column configuration. For example, if a compound column // was given the id "compound" and a child column was given the id "child", this will find the column // using only "child". If "compound-child" was being searched for, the inherited call // above would have found the cell. var suffix = '-' + target, suffixLength = suffix.length; for (var completeId in this.columns) { if (completeId.indexOf(suffix, completeId.length - suffixLength) !== -1) { return this.columns[completeId]; } } } return results; }, _updateCompoundHiddenStates: function (id, hidden) { // summary: // Called from _hideColumn and _showColumn (for ColumnHider) // to adjust parent header cells var column = this.columns[id], colSpan; if (column && column.hidden === hidden) { // Avoid redundant processing (since it would cause colSpan skew) return; } // column will be undefined when this is called for parents while (column && column.parentColumn) { // Update colSpans / hidden state of parents column = column.parentColumn; colSpan = column.colSpan = column.colSpan + (hidden ? -1 : 1); if (colSpan) { column.headerNode.colSpan = colSpan; } if (colSpan === 1 && !hidden) { this._showColumn(column.id); } else if (!colSpan && hidden) { this._hideColumn(column.id); } } }, _hideColumn: function (id) { var self = this; this._updateCompoundHiddenStates(id, true); this.inherited(arguments); if (has('ff')) { // Firefox causes display quirks in certain situations; // avoid them by forcing reflow of the header this.headerNode.style.display = 'none'; setTimeout(function () { self.headerNode.style.display = ''; self.resize(); }, 0); } }, _showColumn: function (id) { this._updateCompoundHiddenStates(id, false); this.inherited(arguments); }, _getResizedColumnWidths: function () { // Overrides ColumnResizer method to report the total width and // last column correctly for CompoundColumns structures var total = 0, columns = this.columns, id; for (id in columns) { total += columns[id].headerNode.offsetWidth; } return { totalWidth: total, lastColId: this.subRows[0][this.subRows[0].length - 1].id }; } }); });