diff --git a/assets/index.less b/assets/index.less index a557042e3..5e1559929 100644 --- a/assets/index.less +++ b/assets/index.less @@ -1,15 +1,20 @@ @tablePrefixCls: rc-table; @text-color : #666; @font-size-base : 12px; +@line-height: 1.5; @table-border-color: #e9e9e9; @table-head-background-color: #f3f3f3; +@vertical-padding: 16px; +@horizontal-padding: 8px; +@row-height: @font-size-base * @line-height + @vertical-padding * 2; + .@{tablePrefixCls} { font-size: @font-size-base; color: @text-color; transition: opacity 0.3s ease; position: relative; - line-height: 1.5; + line-height: @line-height; overflow: hidden; .@{tablePrefixCls}-scroll { @@ -40,7 +45,7 @@ } .@{tablePrefixCls}-title { - padding: 16px 8px; + padding: @vertical-padding @horizontal-padding; border-top: 1px solid #e9e9e9; } @@ -49,7 +54,7 @@ } .@{tablePrefixCls}-footer { - padding: 16px 8px; + padding: @vertical-padding @horizontal-padding; border-bottom: 1px solid #e9e9e9; } @@ -81,7 +86,7 @@ } th, td { - padding: 16px 8px; + padding: @vertical-padding @horizontal-padding; } } @@ -164,6 +169,15 @@ width: auto; background: #fff; } + + .generate-rowspan(10); + + .generate-rowspan(@n, @i: 1) when (@i =< @n) { + th.@{tablePrefixCls}-rowspan-@{i} { + height: @row-height * @i - @vertical-padding * 2; + } + .generate-rowspan(@n, (@i + 1)); + } } &-fixed-left { diff --git a/examples/grouping-columns.html b/examples/grouping-columns.html new file mode 100644 index 000000000..e69de29bb diff --git a/examples/grouping-columns.js b/examples/grouping-columns.js new file mode 100644 index 000000000..095286a04 --- /dev/null +++ b/examples/grouping-columns.js @@ -0,0 +1,99 @@ +/* eslint-disable no-console,func-names,react/no-multi-comp */ +const React = require('react'); +const ReactDOM = require('react-dom'); +const Table = require('rc-table'); +require('rc-table/assets/index.less'); + +const columns = [ + { + title: '姓名', + dataIndex: 'name', + key: 'name', + }, + { + title: '其它', + children: [ + { + title: '年龄', + dataIndex: 'age', + key: 'age', + }, + { + title: '住址', + children: [ + { + title: '街道', + dataIndex: 'street', + key: 'street', + }, + { + title: '小区', + children: [ + { + title: '单元', + dataIndex: 'building', + key: 'building', + }, + { + title: '门牌', + dataIndex: 'number', + key: 'number', + }, + ], + }, + ], + }, + ], + }, + { + title: '公司', + children: [ + { + title: '地址', + dataIndex: 'companyAddress', + key: 'companyAddress', + }, + { + title: '名称', + dataIndex: 'companyName', + key: 'companyName', + }, + ], + }, + { + title: '性别', + dataIndex: 'gender', + key: 'gender', + }, +]; + + +const data = [{ + key: '1', + name: '胡彦斌', + age: 32, + street: '拱墅区和睦街道', + building: 1, + number: 2033, + companyAddress: '西湖区湖底公园', + companyName: '湖底有限公司', + gender: '男', +}, { + key: '2', + name: '胡彦祖', + age: 42, + street: '拱墅区和睦街道', + building: 3, + number: 2035, + companyAddress: '西湖区湖底公园', + companyName: '湖底有限公司', + gender: '男', +}]; + +ReactDOM.render( +
+

grouping columns

+ + , + document.getElementById('__react-content') +); diff --git a/src/Table.jsx b/src/Table.jsx index 76209b08a..9a582e349 100644 --- a/src/Table.jsx +++ b/src/Table.jsx @@ -189,30 +189,71 @@ const Table = React.createClass({ getHeader(columns, fixed) { const { showHeader, expandIconAsCell, prefixCls } = this.props; - let ths = []; + let rows; + if (columns) { + // columns are passed from fixed table function that already grouped. + rows = this.getHeaderRows(columns); + } else { + rows = this.getHeaderRows(this.groupColumns(this.getCurrentColumns())); + } + if (expandIconAsCell && fixed !== 'right') { - ths.push({ + rows[0].unshift({ key: 'rc-table-expandIconAsCell', - className: `${prefixCls}-expand-icon-th`, + className: `${prefixCls}-expand-icon-th ${prefixCls}-rowspan-${rows.length}`, title: '', + rowSpan: rows.length, }); } - ths = ths.concat(columns || this.getCurrentColumns()).map(c => { - if (c.colSpan !== 0) { - return ; - } - }); + const { fixedColumnsHeadRowsHeight } = this.state; const trStyle = (fixedColumnsHeadRowsHeight[0] && columns) ? { height: fixedColumnsHeadRowsHeight[0], } : null; return showHeader ? ( - {ths} + {rows.map((r, i) => { + const ths = r.map(c => ; + })} ) : null; }, + getHeaderRows(columns, currentRow = 0, rows) { + const { prefixCls } = this.props; + + rows = rows || []; + rows[currentRow] = rows[currentRow] || []; + + columns.forEach(column => { + if (column.rowSpan && rows.length < column.rowSpan) { + while (rows.length < column.rowSpan) { + rows.push([]); + } + } + const cell = { + key: column.key, + className: column.className || '', + children: column.title, + }; + if (column.children) { + this.getHeaderRows(column.children, currentRow + 1, rows); + } + if ('colSpan' in column) { + cell.colSpan = column.colSpan; + } + if ('rowSpan' in column) { + cell.rowSpan = column.rowSpan; + cell.className += ` ${prefixCls}-rowspan-${cell.rowSpan}`; + } + if (cell.colSpan !== 0) { + rows[currentRow].push(cell); + } + }); + return rows; + }, + getExpandedRow(key, content, visible, className, fixed) { const prefixCls = this.props.prefixCls; return ( @@ -224,7 +265,7 @@ const Table = React.createClass({ {(this.props.expandIconAsCell && fixed !== 'right') ? @@ -271,6 +312,8 @@ const Table = React.createClass({ height: fixedColumnsBodyRowsHeight[i], } : {}; + const leafColumns = this.getLeafColumns(columns || this.getCurrentColumns()); + rst.push( ); } - cols = cols.concat((columns || this.props.columns).map(c => { + const leafColumns = this.getLeafColumns(columns || this.props.columns); + cols = cols.concat(leafColumns.map(c => { return ; })); return {cols}; @@ -361,7 +405,7 @@ const Table = React.createClass({ getLeftFixedTable() { const { columns } = this.props; - const fixedColumns = columns.filter( + const fixedColumns = this.groupColumns(columns).filter( column => column.fixed === 'left' || column.fixed === true ); return this.getTable({ @@ -372,7 +416,7 @@ const Table = React.createClass({ getRightFixedTable() { const { columns } = this.props; - const fixedColumns = columns.filter(column => column.fixed === 'right'); + const fixedColumns = this.groupColumns(columns).filter(column => column.fixed === 'right'); return this.getTable({ columns: fixedColumns, fixed: 'right', @@ -517,6 +561,22 @@ const Table = React.createClass({ return Math.ceil((columnsPageRange[1] - columnsPageRange[0] + 1) / columnsPageSize) - 1; }, + getLeafColumns(columns) { + const leafColumns = []; + columns.forEach(column => { + if (!column.children) { + leafColumns.push(column); + } else { + leafColumns.push(...this.getLeafColumns(column.children)); + } + }); + return leafColumns; + }, + + getLeafColumnsCount(columns) { + return this.getLeafColumns(columns).length; + }, + goToColumnsPage(currentColumnsPage) { const maxColumnsPage = this.getMaxColumnsPage(); let page = currentColumnsPage; @@ -531,6 +591,45 @@ const Table = React.createClass({ }); }, + // add appropriate rowspan and colspan to column + groupColumns(columns, currentRow = 0, parentColumn = {}, rows = []) { + // track how many rows we got + if (!~rows.indexOf(currentRow)) { + rows.push(currentRow); + } + const grouped = []; + const setRowSpan = column => { + const rowSpan = rows.length - currentRow; + if (column && + !column.children && // parent columns are supposed to be one row + rowSpan > 1 && + (!column.rowSpan || column.rowSpan < rowSpan) + ) { + column.rowSpan = rowSpan; + } + }; + columns.forEach((column, index) => { + const newColumn = { ...column }; + parentColumn.colSpan = parentColumn.colSpan || 0; + if (newColumn.children && newColumn.children.length > 0) { + newColumn.children = this.groupColumns(newColumn.children, currentRow + 1, newColumn, rows); + parentColumn.colSpan = parentColumn.colSpan + newColumn.colSpan; + } else { + parentColumn.colSpan++; + } + // update rowspan to all previous columns + for (let i = 0; i < index; ++i) { + setRowSpan(grouped[i]); + } + // last column, update rowspan immediately + if (index + 1 === columns.length) { + setRowSpan(newColumn); + } + grouped.push(newColumn); + }); + return grouped; + }, + syncFixedTableRowHeight() { const { prefixCls } = this.props; const headRows = this.refs.headTable ? this.refs.headTable.querySelectorAll(`tr`) : []; diff --git a/tests/GroupingColumns.spec.js b/tests/GroupingColumns.spec.js new file mode 100644 index 000000000..6e01f2eaf --- /dev/null +++ b/tests/GroupingColumns.spec.js @@ -0,0 +1,121 @@ +/* eslint-disable no-console,func-names,react/no-multi-comp */ +const expect = require('expect.js'); +const Table = require('../'); +const React = require('react'); +const ReactDOM = require('react-dom'); +const $ = require('jquery'); + +describe('Table with grouping columns', () => { + let div; + let node; + + beforeEach(() => { + div = document.createElement('div'); + node = $(div); + }); + + it('group columns', () => { + /** + * +---+---+---------------+-------+---+ + * | | | C | J | | + * | | +---+---------------+---+ | + * | | | | E | | | | + * | A | B | +---+-------+ | | M | + * | | | D | F | G | K | L | | + * | | | | +---+---+ | | | + * | | | | | H | I | | | | + * +---+---+---+---+---+---+---+---+---+ + */ + const columns = [ + { title: '表头A', className: 'title-a', dataIndex: 'a', key: 'a' }, + { title: '表头B', className: 'title-b', dataIndex: 'b', key: 'b' }, + { title: '表头C', className: 'title-c', children: + [ + { title: '表头D', className: 'title-d', dataIndex: 'c', key: 'c' }, + { title: '表头E', className: 'title-e', children: + [ + { title: '表头F', className: 'title-f', dataIndex: 'd', key: 'd' }, + { title: '表头G', className: 'title-g', children: + [ + { title: '表头H', className: 'title-h', dataIndex: 'e', key: 'e' }, + { title: '表头I', className: 'title-i', dataIndex: 'f', key: 'f' }, + ], + }, + ], + }, + ], + }, + { title: '表头J', className: 'title-j', children: + [ + { title: '表头K', className: 'title-k', dataIndex: 'g', key: 'g' }, + { title: '表头L', className: 'title-l', dataIndex: 'h', key: 'h' }, + ], + }, + { title: '表头M', className: 'title-m', dataIndex: 'i', key: 'i' }, + ]; + + const data = [ + { key: '1', a: 'a1', b: 'b1', c: 'c1', d: 'd1', e: 'e1', f: 'f1', g: 'g1', h: 'h1', i: 'i1' }, + { key: '2', a: 'a2', b: 'b2', c: 'c2', d: 'd2', e: 'e2', f: 'f2', g: 'g2', h: 'h2', i: 'i2' }, + { key: '3', a: 'a3', b: 'b3', c: 'c3', d: 'd3', e: 'e3', f: 'f3', g: 'g3', h: 'h3', i: 'i3' }, + ]; + + ReactDOM.render( +
{c.title}
); + return
: null} - + {fixed !== 'right' ? content : ' '}
, + div + ); + + const cells = { + 'title-a': ['4', undefined], + 'title-b': ['4', undefined], + 'title-c': [undefined, '4'], + 'title-d': ['3', undefined], + 'title-e': [undefined, '3'], + 'title-f': ['2', undefined], + 'title-g': [undefined, '2'], + 'title-h': [undefined, undefined], + 'title-i': [undefined, undefined], + 'title-j': [undefined, '2'], + 'title-k': ['3', undefined], + 'title-l': ['3', undefined], + 'title-m': ['4', undefined], + }; + Object.keys(cells).forEach(className => { + const cell = cells[className]; + expect(node.find(`.${className}`).attr('rowspan')).to.be(cell[0]); + expect(node.find(`.${className}`).attr('colspan')).to.be(cell[1]); + }); + }); + + it('work with fixed columns', () => { + const columns = [ + { title: '表头A', className: 'title-a', dataIndex: 'a', key: 'a', fixed: 'left' }, + { title: '表头B', className: 'title-b', children: + [ + { title: '表头C', className: 'title-c', dataIndex: 'b', key: 'b' }, + { title: '表头D', className: 'title-d', dataIndex: 'c', key: 'c' }, + ], + }, + { title: '表头E', className: 'title-e', dataIndex: 'd', key: 'd', fixed: 'right' }, + ]; + + const data = [ + { key: '1', a: 'a1', b: 'b1', c: 'c1', d: 'd1' }, + { key: '2', a: 'a2', b: 'b2', c: 'c2', d: 'd2' }, + { key: '3', a: 'a3', b: 'b3', c: 'c3', d: 'd3' }, + ]; + + ReactDOM.render( +
, + div + ); + + const titleA = node.find('.rc-table-fixed-left .title-a'); + const titleE = node.find('.rc-table-fixed-right .title-e'); + + expect(titleA.attr('rowspan')).to.be('2'); + expect(titleA.hasClass('rc-table-rowspan-2')).to.be(true); + expect(titleE.attr('rowspan')).to.be('2'); + expect(titleE.hasClass('rc-table-rowspan-2')).to.be(true); + }); +}); diff --git a/tests/index.js b/tests/index.js index de1faaea5..5f1d842d2 100644 --- a/tests/index.js +++ b/tests/index.js @@ -1,2 +1,3 @@ require('./index.spec.js'); require('./PagingColumns.spec.js'); +require('./GroupingColumns.spec.js');