Files
react-table/src/index.js
2017-05-09 13:15:14 -06:00

829 lines
26 KiB
JavaScript

import React, { Component } from 'react'
import classnames from 'classnames'
//
import _ from './utils'
import Lifecycle from './lifecycle'
import Methods from './methods'
import defaultProps from './defaultProps'
export const ReactTableDefaults = defaultProps
export default class ReactTable extends Methods(Lifecycle(Component)) {
static defaultProps = defaultProps
constructor (props) {
super()
this.getResolvedState = this.getResolvedState.bind(this)
this.getDataModel = this.getDataModel.bind(this)
this.getSortedData = this.getSortedData.bind(this)
this.fireFetchData = this.fireFetchData.bind(this)
this.getPropOrState = this.getPropOrState.bind(this)
this.getStateOrProp = this.getStateOrProp.bind(this)
this.filterData = this.filterData.bind(this)
this.sortData = this.sortData.bind(this)
this.getMinRows = this.getMinRows.bind(this)
this.onPageChange = this.onPageChange.bind(this)
this.onPageSizeChange = this.onPageSizeChange.bind(this)
this.sortColumn = this.sortColumn.bind(this)
this.filterColumn = this.filterColumn.bind(this)
this.resizeColumnStart = this.resizeColumnStart.bind(this)
this.resizeColumnEnd = this.resizeColumnEnd.bind(this)
this.resizeColumnMoving = this.resizeColumnMoving.bind(this)
this.state = {
page: 0,
pageSize: props.defaultPageSize || 10,
sorting: props.defaultSorting,
expandedRows: {},
filters: props.defaultFilters,
resizing: props.defaultResizing,
currentlyResizing: false,
skipNextSort: false
}
}
render () {
const resolvedState = this.getResolvedState()
const {
children,
className,
style,
getProps,
getTableProps,
getTheadGroupProps,
getTheadGroupTrProps,
getTheadGroupThProps,
getTheadProps,
getTheadTrProps,
getTheadThProps,
getTheadFilterProps,
getTheadFilterTrProps,
getTheadFilterThProps,
getTbodyProps,
getTrGroupProps,
getTrProps,
getTdProps,
getTfootProps,
getTfootTrProps,
getTfootTdProps,
getPaginationProps,
getLoadingProps,
getNoDataProps,
getResizerProps,
showPagination,
manual,
loadingText,
noDataText,
showFilters,
resizable,
// Property keyspivotValKey,
pivotIDKey,
pivotValKey,
pivotBy,
subRowsKey,
aggregatedKey,
originalKey,
indexKey,
groupedByPivotKey,
// State
loading,
pageSize,
page,
sorting,
filters,
resizing,
pages,
// Pivoting State
expandedRows,
onExpandRow,
// Components
TableComponent,
TheadComponent,
TbodyComponent,
TrGroupComponent,
TrComponent,
ThComponent,
TdComponent,
TfootComponent,
PaginationComponent,
LoadingComponent,
SubComponent,
NoDataComponent,
ResizerComponent,
ExpanderComponent,
PivotValueComponent,
PivotComponent,
AggregatedComponent,
FilterComponent,
// Data model
resolvedData,
allVisibleColumns,
headerGroups,
hasHeaderGroups,
// Sorted Data
sortedData,
currentlyResizing
} = resolvedState
// Pagination
const startRow = pageSize * page
const endRow = startRow + pageSize
let pageRows = manual ? resolvedData : sortedData.slice(startRow, endRow)
const minRows = this.getMinRows()
const padRows = _.range(Math.max(minRows - pageRows.length, 0))
const hasColumnFooter = allVisibleColumns.some(d => d.Footer)
const recurseRowsViewIndex = (rows, path = [], index = -1) => {
return [
rows.map((row, i) => {
index++
const rowWithViewIndex = {
...row,
_viewIndex: index
}
const newPath = path.concat([i])
if (rowWithViewIndex[subRowsKey] && _.get(expandedRows, newPath)) {
[rowWithViewIndex[subRowsKey], index] = recurseRowsViewIndex(rowWithViewIndex[subRowsKey], newPath, index)
}
return rowWithViewIndex
}),
index
]
}
[pageRows] = recurseRowsViewIndex(pageRows)
const canPrevious = page > 0
const canNext = page + 1 < pages
const rowMinWidth = _.sum(allVisibleColumns.map(d => {
const resized = resizing.find(x => x.id === d.id) || {}
return _.getFirstDefined(resized.value, d.width, d.minWidth)
}))
let rowIndex = -1
const finalState = {
...resolvedState,
startRow,
endRow,
pageRows,
minRows,
padRows,
hasColumnFooter,
canPrevious,
canNext,
rowMinWidth
}
// Visual Components
const makeHeaderGroups = () => {
const theadGroupProps = _.splitProps(getTheadGroupProps(finalState, undefined, undefined, this))
const theadGroupTrProps = _.splitProps(getTheadGroupTrProps(finalState, undefined, undefined, this))
return (
<TheadComponent
className={classnames('-headerGroups', theadGroupProps.className)}
style={{
...theadGroupProps.style,
minWidth: `${rowMinWidth}px`
}}
{...theadGroupProps.rest}
>
<TrComponent
className={theadGroupTrProps.className}
style={theadGroupTrProps.style}
{...theadGroupTrProps.rest}
>
{headerGroups.map(makeHeaderGroup)}
</TrComponent>
</TheadComponent>
)
}
const makeHeaderGroup = (column, i) => {
const resizedValue = col => (resizing.find(x => x.id === col.id) || {}).value
const flex = _.sum(column.columns.map(col => col.width || resizedValue(col) ? 0 : col.minWidth))
const width = _.sum(column.columns.map(col => _.getFirstDefined(resizedValue(col), col.width, col.minWidth)))
const maxWidth = _.sum(column.columns.map(col => _.getFirstDefined(resizedValue(col), col.width, col.maxWidth)))
const theadGroupThProps = _.splitProps(getTheadGroupThProps(finalState, undefined, column, this))
const columnHeaderProps = _.splitProps(column.getHeaderProps(finalState, undefined, column, this))
const classes = [
column.headerClassName,
theadGroupThProps.className,
columnHeaderProps.className
]
const styles = {
...column.headerStyle,
...theadGroupThProps.style,
...columnHeaderProps.style
}
const rest = {
...theadGroupThProps.rest,
...columnHeaderProps.rest
}
const flexStyles = {
flex: `${flex} 0 auto`,
width: `${width}px`,
maxWidth: `${maxWidth}px`
}
return (
<ThComponent
key={i + '-' + column.id}
className={classnames(
classes
)}
style={{
...styles,
...flexStyles
}}
{...rest}
>
{_.normalizeComponent(column.Header, {
data: sortedData,
column: column
})}
</ThComponent>
)
}
const makeHeaders = () => {
const theadProps = _.splitProps(getTheadProps(finalState, undefined, undefined, this))
const theadTrProps = _.splitProps(getTheadTrProps(finalState, undefined, undefined, this))
return (
<TheadComponent
className={classnames('-header', theadProps.className)}
style={{
...theadProps.style,
minWidth: `${rowMinWidth}px`
}}
{...theadProps.rest}
>
<TrComponent
className={theadTrProps.className}
style={theadTrProps.style}
{...theadTrProps.rest}
>
{allVisibleColumns.map(makeHeader)}
</TrComponent>
</TheadComponent>
)
}
const makeHeader = (column, i) => {
const resized = resizing.find(x => x.id === column.id) || {}
const sort = sorting.find(d => d.id === column.id)
const show = typeof column.show === 'function' ? column.show() : column.show
const width = _.getFirstDefined(resized.value, column.width, column.minWidth)
const maxWidth = _.getFirstDefined(resized.value, column.width, column.maxWidth)
const theadThProps = _.splitProps(getTheadThProps(finalState, undefined, column, this))
const columnHeaderProps = _.splitProps(column.getHeaderProps(finalState, undefined, column, this))
const classes = [
column.headerClassName,
theadThProps.className,
columnHeaderProps.className
]
const styles = {
...column.headerStyle,
...theadThProps.style,
...columnHeaderProps.style
}
const rest = {
...theadThProps.rest,
...columnHeaderProps.rest
}
const resizer = (typeof column.resizable !== 'undefined' ? column.resizable : resizable) ? (
<ResizerComponent
onMouseDown={e => this.resizeColumnStart(column, e, false)}
onTouchStart={e => this.resizeColumnStart(column, e, true)}
{...resizerProps}
/>
) : null
return (
<ThComponent
key={i + '-' + column.id}
className={classnames(
classes,
'rt-resizable-header',
sort ? (sort.desc ? '-sort-desc' : '-sort-asc') : '',
column.sortable && '-cursor-pointer',
!show && '-hidden',
pivotBy && pivotBy.slice(0, -1).includes(column.id) && 'rt-header-pivot'
)}
style={{
...styles,
flex: `${width} 0 auto`,
width: `${width}px`,
maxWidth: `${maxWidth}px`
}}
toggleSort={(e) => {
column.sortable && this.sortColumn(column, e.shiftKey)
}}
{...rest}
>
<div className='rt-resizable-header-content'>
{_.normalizeComponent(column.Header, {
data: sortedData,
column: column
})}
</div>
{resizer}
</ThComponent>
)
}
const makeFilters = () => {
const theadFilterProps = _.splitProps(getTheadFilterProps(finalState, undefined, undefined, this))
const theadFilterTrProps = _.splitProps(getTheadFilterTrProps(finalState, undefined, undefined, this))
return (
<TheadComponent
className={classnames('-filters', theadFilterProps.className)}
style={{
...theadFilterProps.style,
minWidth: `${rowMinWidth}px`
}}
{...theadFilterProps.rest}
>
<TrComponent
className={theadFilterTrProps.className}
style={theadFilterTrProps.style}
{...theadFilterTrProps.rest}
>
{allVisibleColumns.map(makeFilter)}
</TrComponent>
</TheadComponent>
)
}
const makeFilter = (column, i) => {
const resized = resizing.find(x => x.id === column.id) || {}
const width = _.getFirstDefined(resized.value, column.width, column.minWidth)
const maxWidth = _.getFirstDefined(resized.value, column.width, column.maxWidth)
const theadFilterThProps = _.splitProps(getTheadFilterThProps(finalState, undefined, column, this))
const columnHeaderProps = _.splitProps(column.getHeaderProps(finalState, undefined, column, this))
const classes = [
column.headerClassName,
theadFilterThProps.className,
columnHeaderProps.className
]
const styles = {
...column.headerStyle,
...theadFilterThProps.style,
...columnHeaderProps.style
}
const rest = {
...theadFilterThProps.rest,
...columnHeaderProps.rest
}
const filter = filters.find(filter => filter.id === column.id)
const ResolvedFilterComponent = column.Filter || FilterComponent
return (
<ThComponent
key={i + '-' + column.id}
className={classnames(
classes
)}
style={{
...styles,
flex: `${width} 0 auto`,
width: `${width}px`,
maxWidth: `${maxWidth}px`
}}
{...rest}
>
{!column.hideFilter ? (
_.normalizeComponent(ResolvedFilterComponent,
{
column,
filter,
onChange: (value) => (this.filterColumn(column, value))
},
defaultProps.column.Filter
)
) : null}
</ThComponent>
)
}
const makePageRow = (row, i, path = []) => {
const rowInfo = {
original: row[originalKey],
row: row,
index: row[indexKey],
viewIndex: ++rowIndex,
level: path.length,
nestingPath: path.concat([i]),
aggregated: row[aggregatedKey],
groupedByPivot: row[groupedByPivotKey],
subRows: row[subRowsKey]
}
const isExpanded = _.get(expandedRows, rowInfo.nestingPath)
const trGroupProps = getTrGroupProps(finalState, rowInfo, undefined, this)
const trProps = _.splitProps(getTrProps(finalState, rowInfo, undefined, this))
return (
<TrGroupComponent
key={rowInfo.nestingPath.join('_')}
{...trGroupProps}
>
<TrComponent
className={classnames(
trProps.className,
row._viewIndex % 2 ? '-even' : '-odd'
)}
style={trProps.style}
{...trProps.rest}
>
{allVisibleColumns.map((column, i2) => {
const resized = resizing.find(x => x.id === column.id) || {}
const show = typeof column.show === 'function' ? column.show() : column.show
const width = _.getFirstDefined(resized.value, column.width, column.minWidth)
const maxWidth = _.getFirstDefined(resized.value, column.width, column.maxWidth)
const tdProps = _.splitProps(getTdProps(finalState, rowInfo, column, this))
const columnProps = _.splitProps(column.getProps(finalState, rowInfo, column, this))
const classes = [
tdProps.className,
column.className,
columnProps.className
]
const styles = {
...tdProps.style,
...column.style,
...columnProps.style
}
const cellInfo = {
...rowInfo,
isExpanded,
column: {...column},
value: rowInfo.row[column.id],
resized,
show,
width,
maxWidth,
tdProps,
columnProps,
classes,
styles
}
const value = cellInfo.value
let interactionProps
let isBranch
let isPreview
const onExpanderClick = (e) => {
let newExpandedRows = _.clone(expandedRows)
if (isExpanded) {
newExpandedRows = _.set(newExpandedRows, cellInfo.nestingPath, false)
} else {
newExpandedRows = _.set(newExpandedRows, cellInfo.nestingPath, {})
}
if (onExpandRow) {
onExpandRow(newExpandedRows, cellInfo.nestingPath, e)
}
// If expandedRows is being controlled, don't manage internal state
if (this.props.expandedRows) {
return
}
return this.setStateWithData({
expandedRows: newExpandedRows
})
}
// Default to a standard cell
let resolvedCell = _.normalizeComponent(column.Cell, cellInfo, value)
// Resolve Renderers
const ResolvedAggregatedComponent = column.Aggregated || (!column.aggregate ? AggregatedComponent : column.Cell)
const ResolvedExpanderComponent = column.Expander || ExpanderComponent
// Is this column pivoted?
if (pivotBy && column.pivot) {
// Make it expandable
cellInfo.expandable = cellInfo.subRows
interactionProps = {
onClick: onExpanderClick
}
// Is this column a branch?
isBranch = rowInfo.row[pivotIDKey] === column.id &&
cellInfo.subRows
// Should this column be blank?
isPreview = pivotBy.indexOf(column.id) > pivotBy.indexOf(rowInfo.row[pivotIDKey]) &&
cellInfo.subRows
// Resolve Pivot Renderers
const ResolvedPivotValueComponent = column.PivotValue || PivotValueComponent
// Build the default PivotComponent
const DefaultResolvedPivotComponent = PivotComponent || (props => (
<div>
<ResolvedExpanderComponent {...props} />
<ResolvedPivotValueComponent {...props} />
</div>
))
// Allow a completely custom pivotRender
const resolvedPivot = column.Pivot || DefaultResolvedPivotComponent
// Pivot Cell Render Override
if (isBranch) {
// isPivot
resolvedCell = _.normalizeComponent(resolvedPivot, {
...cellInfo,
value: row[pivotValKey]
}, row[pivotValKey])
} else if (isPreview) {
// Show the pivot preview
resolvedCell = _.normalizeComponent(ResolvedAggregatedComponent, cellInfo, value)
} else {
resolvedCell = null
}
} else if (cellInfo.aggregated) {
resolvedCell = _.normalizeComponent(ResolvedAggregatedComponent, cellInfo, value)
}
// Expander onClick event
if (column.expander) {
resolvedCell = _.normalizeComponent(ResolvedExpanderComponent, cellInfo)
cellInfo.expandable = true
interactionProps = {
onClick: onExpanderClick
}
if (pivotBy) {
if (cellInfo.groupedByPivot) {
resolvedCell = null
}
if (!cellInfo.subRows && !SubComponent) {
resolvedCell = null
}
}
}
// Return the cell
return (
<TdComponent
key={i2 + '-' + column.id}
className={classnames(
classes,
!show && 'hidden',
cellInfo.expandable && 'rt-expandable',
(isBranch || isPreview) && 'rt-pivot'
)}
style={{
...styles,
flex: `${width} 0 auto`,
width: `${width}px`,
maxWidth: `${maxWidth}px`
}}
{...tdProps.rest}
{...interactionProps}
>
{resolvedCell}
</TdComponent>
)
})}
</TrComponent>
{(
rowInfo.subRows &&
isExpanded &&
rowInfo.subRows.map((d, i) => makePageRow(d, i, rowInfo.nestingPath))
)}
{SubComponent && !rowInfo.subRows && isExpanded && SubComponent(rowInfo)}
</TrGroupComponent>
)
}
const makePadRow = (row, i) => {
const trGroupProps = getTrGroupProps(finalState, undefined, undefined, this)
const trProps = _.splitProps(getTrProps(finalState, undefined, undefined, this))
return (
<TrGroupComponent
key={i}
{...trGroupProps}
>
<TrComponent
className={classnames(
'-padRow',
(pageRows.length + i) % 2 ? '-even' : '-odd',
trProps.className,
)}
style={trProps.style || {}}
>
{allVisibleColumns.map(makePadColumn)}
</TrComponent>
</TrGroupComponent>
)
}
const makePadColumn = (column, i) => {
const resized = resizing.find(x => x.id === column.id) || {}
const show = typeof column.show === 'function' ? column.show() : column.show
let width = _.getFirstDefined(resized.value, column.width, column.minWidth)
let flex = width
let maxWidth = _.getFirstDefined(resized.value, column.width, column.maxWidth)
const tdProps = _.splitProps(getTdProps(finalState, undefined, column, this))
const columnProps = _.splitProps(column.getProps(finalState, undefined, column, this))
const classes = [
tdProps.className,
column.className,
columnProps.className
]
const styles = {
...tdProps.style,
...column.style,
...columnProps.style
}
return (
<TdComponent
key={i + '-' + column.id}
className={classnames(
classes,
!show && 'hidden'
)}
style={{
...styles,
flex: `${flex} 0 auto`,
width: `${width}px`,
maxWidth: `${maxWidth}px`
}}
{...tdProps.rest}
>
&nbsp;
</TdComponent>
)
}
const makeColumnFooters = () => {
const tFootProps = getTfootProps(finalState, undefined, undefined, this)
const tFootTrProps = _.splitProps(getTfootTrProps(finalState, undefined, undefined, this))
return (
<TfootComponent
className={tFootProps.className}
style={{
...tFootProps.style,
minWidth: `${rowMinWidth}px`
}}
{...tFootProps.rest}
>
<TrComponent
className={classnames(
tFootTrProps.className
)}
style={tFootTrProps.style}
{...tFootTrProps.rest}
>
{allVisibleColumns.map(makeColumnFooter)}
</TrComponent>
</TfootComponent>
)
}
const makeColumnFooter = (column, i) => {
const resized = resizing.find(x => x.id === column.id) || {}
const show = typeof column.show === 'function' ? column.show() : column.show
const width = _.getFirstDefined(resized.value, column.width, column.minWidth)
const maxWidth = _.getFirstDefined(resized.value, column.width, column.maxWidth)
const tFootTdProps = _.splitProps(getTfootTdProps(finalState, undefined, undefined, this))
const columnProps = _.splitProps(column.getProps(finalState, undefined, column, this))
const columnFooterProps = _.splitProps(column.getFooterProps(finalState, undefined, column, this))
const classes = [
tFootTdProps.className,
column.className,
columnProps.className,
columnFooterProps.className
]
const styles = {
...tFootTdProps.style,
...column.style,
...columnProps.style,
...columnFooterProps.style
}
return (
<TdComponent
key={i + '-' + column.id}
className={classnames(
classes,
!show && 'hidden'
)}
style={{
...styles,
flex: `${width} 0 auto`,
width: `${width}px`,
maxWidth: `${maxWidth}px`
}}
{...columnProps.rest}
{...tFootTdProps.rest}
{...columnFooterProps.rest}
>
{_.normalizeComponent(column.Footer, {
data: sortedData,
column: column
})}
</TdComponent>
)
}
const rootProps = _.splitProps(getProps(finalState, undefined, undefined, this))
const tableProps = _.splitProps(getTableProps(finalState, undefined, undefined, this))
const tBodyProps = _.splitProps(getTbodyProps(finalState, undefined, undefined, this))
const paginationProps = _.splitProps(getPaginationProps(finalState, undefined, undefined, this))
const loadingProps = getLoadingProps(finalState, undefined, undefined, this)
const noDataProps = getNoDataProps(finalState, undefined, undefined, this)
const resizerProps = getResizerProps(finalState, undefined, undefined, this)
const makeTable = () => (
<div
className={classnames(
'ReactTable',
className,
rootProps.className
)}
style={{
...style,
...rootProps.style
}}
{...rootProps.rest}
>
<TableComponent
className={classnames(
tableProps.className,
currentlyResizing ? 'rt-resizing' : ''
)}
style={tableProps.style}
{...tableProps.rest}
>
{hasHeaderGroups ? makeHeaderGroups() : null}
{makeHeaders()}
{showFilters ? makeFilters() : null}
<TbodyComponent
className={classnames(tBodyProps.className)}
style={{
...tBodyProps.style,
minWidth: `${rowMinWidth}px`
}}
{...tBodyProps.rest}
>
{pageRows.map((d, i) => makePageRow(d, i))}
{padRows.map(makePadRow)}
</TbodyComponent>
{hasColumnFooter ? makeColumnFooters() : null}
</TableComponent>
{showPagination ? (
<PaginationComponent
{...resolvedState}
pages={pages}
canPrevious={canPrevious}
canNext={canNext}
onPageChange={this.onPageChange}
onPageSizeChange={this.onPageSizeChange}
className={paginationProps.className}
style={paginationProps.style}
{...paginationProps.rest}
/>
) : null}
{!pageRows.length && (
<NoDataComponent
{...noDataProps}
>
{_.normalizeComponent(noDataText)}
</NoDataComponent>
)}
<LoadingComponent
loading={loading}
loadingText={loadingText}
{...loadingProps}
/>
</div>
)
// childProps are optionally passed to a function-as-a-child
return children ? children(finalState, makeTable, this) : makeTable()
}
}