/** @flow */ import cn from 'classnames' import Column from './Column' import React, { PropTypes, PureComponent } from 'react' import { findDOMNode } from 'react-dom' import Grid, { accessibilityOverscanIndicesGetter } from '../Grid' import defaultRowRenderer from './defaultRowRenderer' import defaultHeaderRowRenderer from './defaultHeaderRowRenderer' import SortDirection from './SortDirection' /** * Table component with fixed headers and virtualized rows for improved performance with large data sets. * This component expects explicit width, height, and padding parameters. */ export default class Table extends PureComponent { static propTypes = { 'aria-label': PropTypes.string, /** * Removes fixed height from the scrollingContainer so that the total height * of rows can stretch the window. Intended for use with WindowScroller */ autoHeight: PropTypes.bool, /** One or more Columns describing the data displayed in this row */ children: (props, propName, componentName) => { const children = React.Children.toArray(props.children) for (let i = 0; i < children.length; i++) { if (children[i].type !== Column) { return new Error('Table only accepts children of type Column') } } }, /** Optional CSS class name */ className: PropTypes.string, /** Disable rendering the header at all */ disableHeader: PropTypes.bool, /** * Used to estimate the total height of a Table before all of its rows have actually been measured. * The estimated total height is adjusted as rows are rendered. */ estimatedRowSize: PropTypes.number.isRequired, /** Optional custom CSS class name to attach to inner Grid element. */ gridClassName: PropTypes.string, /** Optional inline style to attach to inner Grid element. */ gridStyle: PropTypes.object, /** Optional CSS class to apply to all column headers */ headerClassName: PropTypes.string, /** Fixed height of header row */ headerHeight: PropTypes.number.isRequired, /** * Responsible for rendering a table row given an array of columns: * Should implement the following interface: ({ * className: string, * columns: any[], * style: any * }): PropTypes.node */ headerRowRenderer: PropTypes.func, /** Optional custom inline style to attach to table header columns. */ headerStyle: PropTypes.object, /** Fixed/available height for out DOM element */ height: PropTypes.number.isRequired, /** Optional id */ id: PropTypes.string, /** Optional renderer to be used in place of table body rows when rowCount is 0 */ noRowsRenderer: PropTypes.func, /** * Optional callback when a column's header is clicked. * ({ columnData: any, dataKey: string }): void */ onHeaderClick: PropTypes.func, /** * Callback invoked when a user clicks on a table row. * ({ index: number }): void */ onRowClick: PropTypes.func, /** * Callback invoked when a user double-clicks on a table row. * ({ index: number }): void */ onRowDoubleClick: PropTypes.func, /** * Callback invoked when the mouse leaves a table row. * ({ index: number }): void */ onRowMouseOut: PropTypes.func, /** * Callback invoked when a user moves the mouse over a table row. * ({ index: number }): void */ onRowMouseOver: PropTypes.func, /** * Callback invoked with information about the slice of rows that were just rendered. * ({ startIndex, stopIndex }): void */ onRowsRendered: PropTypes.func, /** * Callback invoked whenever the scroll offset changes within the inner scrollable region. * This callback can be used to sync scrolling between lists, tables, or grids. * ({ clientHeight, scrollHeight, scrollTop }): void */ onScroll: PropTypes.func.isRequired, /** See Grid#overscanIndicesGetter */ overscanIndicesGetter: PropTypes.func.isRequired, /** * Number of rows to render above/below the visible bounds of the list. * These rows can help for smoother scrolling on touch devices. */ overscanRowCount: PropTypes.number.isRequired, /** * Optional CSS class to apply to all table rows (including the header row). * This property can be a CSS class name (string) or a function that returns a class name. * If a function is provided its signature should be: ({ index: number }): string */ rowClassName: PropTypes.oneOfType([PropTypes.string, PropTypes.func]), /** * Callback responsible for returning a data row given an index. * ({ index: number }): any */ rowGetter: PropTypes.func.isRequired, /** * Either a fixed row height (number) or a function that returns the height of a row given its index. * ({ index: number }): number */ rowHeight: PropTypes.oneOfType([PropTypes.number, PropTypes.func]).isRequired, /** Number of rows in table. */ rowCount: PropTypes.number.isRequired, /** * Responsible for rendering a table row given an array of columns: * Should implement the following interface: ({ * className: string, * columns: Array, * index: number, * isScrolling: boolean, * onRowClick: ?Function, * onRowDoubleClick: ?Function, * onRowMouseOver: ?Function, * onRowMouseOut: ?Function, * rowData: any, * style: any * }): PropTypes.node */ rowRenderer: PropTypes.func, /** Optional custom inline style to attach to table rows. */ rowStyle: PropTypes.oneOfType([PropTypes.object, PropTypes.func]).isRequired, /** See Grid#scrollToAlignment */ scrollToAlignment: PropTypes.oneOf(['auto', 'end', 'start', 'center']).isRequired, /** Row index to ensure visible (by forcefully scrolling if necessary) */ scrollToIndex: PropTypes.number.isRequired, /** Vertical offset. */ scrollTop: PropTypes.number, /** * Sort function to be called if a sortable header is clicked. * ({ sortBy: string, sortDirection: SortDirection }): void */ sort: PropTypes.func, /** Table data is currently sorted by this :dataKey (if it is sorted at all) */ sortBy: PropTypes.string, /** Table data is currently sorted in this direction (if it is sorted at all) */ sortDirection: PropTypes.oneOf([SortDirection.ASC, SortDirection.DESC]), /** Optional inline style */ style: PropTypes.object, /** Tab index for focus */ tabIndex: PropTypes.number, /** Width of list */ width: PropTypes.number.isRequired }; static defaultProps = { disableHeader: false, estimatedRowSize: 30, headerHeight: 0, headerStyle: {}, noRowsRenderer: () => null, onRowsRendered: () => null, onScroll: () => null, overscanIndicesGetter: accessibilityOverscanIndicesGetter, overscanRowCount: 10, rowRenderer: defaultRowRenderer, headerRowRenderer: defaultHeaderRowRenderer, rowStyle: {}, scrollToAlignment: 'auto', scrollToIndex: -1, style: {} }; constructor (props) { super(props) this.state = { scrollbarWidth: 0 } this._createColumn = this._createColumn.bind(this) this._createRow = this._createRow.bind(this) this._onScroll = this._onScroll.bind(this) this._onSectionRendered = this._onSectionRendered.bind(this) this._setRef = this._setRef.bind(this) } forceUpdateGrid () { this.Grid.forceUpdate() } /** See Grid#getOffsetForCell */ getOffsetForRow ({ alignment, index }) { const { scrollTop } = this.Grid.getOffsetForCell({ alignment, rowIndex: index }) return scrollTop } /** See Grid#measureAllCells */ measureAllRows () { this.Grid.measureAllCells() } /** See Grid#recomputeGridSize */ recomputeRowHeights (index = 0) { this.Grid.recomputeGridSize({ rowIndex: index }) } /** See Grid#scrollToPosition */ scrollToPosition (scrollTop = 0) { this.Grid.scrollToPosition({ scrollTop }) } /** See Grid#scrollToCell */ scrollToRow (index = 0) { this.Grid.scrollToCell({ columnIndex: 0, rowIndex: index }) } componentDidMount () { this._setScrollbarWidth() } componentDidUpdate () { this._setScrollbarWidth() } render () { const { children, className, disableHeader, gridClassName, gridStyle, headerHeight, headerRowRenderer, height, id, noRowsRenderer, rowClassName, rowStyle, scrollToIndex, style, width } = this.props const { scrollbarWidth } = this.state const availableRowsHeight = disableHeader ? height : height - headerHeight const rowClass = typeof rowClassName === 'function' ? rowClassName({ index: -1 }) : rowClassName const rowStyleObject = typeof rowStyle === 'function' ? rowStyle({ index: -1 }) : rowStyle // Precompute and cache column styles before rendering rows and columns to speed things up this._cachedColumnStyles = [] React.Children.toArray(children).forEach((column, index) => { const flexStyles = this._getFlexStyleForColumn(column, column.props.style) this._cachedColumnStyles[index] = { ...flexStyles, overflow: 'hidden' } }) // Note that we specify :rowCount, :scrollbarWidth, :sortBy, and :sortDirection as properties on Grid even though these have nothing to do with Grid. // This is done because Grid is a pure component and won't update unless its properties or state has changed. // Any property that should trigger a re-render of Grid then is specified here to avoid a stale display. return (