UNPKG

reactable

Version:
536 lines (462 loc) 19.3 kB
import React from 'react'; import { filterPropsFrom } from './lib/filter_props_from'; import { extractDataFrom } from './lib/extract_data_from'; import { isUnsafe } from './unsafe'; import { Thead } from './thead'; import { Th } from './th'; import { Tr } from './tr'; import { Tfoot } from './tfoot'; import { Paginator } from './paginator'; export class Table extends React.Component { constructor(props) { super(props); this.state = { currentPage: this.props.currentPage ? this.props.currentPage : 0, currentSort: { column: null, direction: this.props.defaultSortDescending ? -1 : 1 }, filter: '' }; // Set the state of the current sort to the default sort if (props.sortBy !== false || props.defaultSort !== false) { let sortingColumn = props.sortBy || props.defaultSort; this.state.currentSort = this.getCurrentSort(sortingColumn); } } filterBy(filter) { this.setState({ filter: filter }); } // Translate a user defined column array to hold column objects if strings are specified // (e.g. ['column1'] => [{key: 'column1', label: 'column1'}]) translateColumnsArray(columns) { return columns.map(function(column, i) { if (typeof(column) === 'string') { return { key: column, label: column }; } else { if (typeof(column.sortable) !== 'undefined') { let sortFunction = column.sortable === true ? 'default' : column.sortable; this._sortable[column.key] = sortFunction; } return column; } }.bind(this)); } parseChildData(props) { let data = [], tfoot; // Transform any children back to a data array if (typeof(props.children) !== 'undefined') { React.Children.forEach(props.children, function(child) { if (typeof(child) === 'undefined' || child === null) { return; } switch (`${child.type}`) { case `${Thead}`: break; case `${Tfoot}`: if (typeof(tfoot) !== 'undefined') { console.warn ('You can only have one <Tfoot>, but more than one was specified.' + 'Ignoring all but the last one'); } tfoot = child; break; case `${Tr}`: let childData = child.props.data || {}; React.Children.forEach(child.props.children, function(descendant) { // TODO /* if (descendant.type.ConvenienceConstructor === Td) { */ if ( typeof(descendant) !== 'object' || descendant == null ) { return; } else if (typeof(descendant.props.column) !== 'undefined') { let value; if (typeof(descendant.props.data) !== 'undefined') { value = descendant.props.data; } else if (typeof(descendant.props.children) !== 'undefined') { value = descendant.props.children; } else { console.warn('exports.Td specified without ' + 'a `data` property or children, ' + 'ignoring'); return; } childData[descendant.props.column] = { value: value, props: filterPropsFrom(descendant.props), __reactableMeta: true }; } else { console.warn('exports.Td specified without a ' + '`column` property, ignoring'); } }); data.push({ data: childData, props: filterPropsFrom(child.props), __reactableMeta: true }); break; default: console.warn ('The only possible children of <Table> are <Thead>, <Tr>, ' + 'or one <Tfoot>.'); } }.bind(this)); } return { data, tfoot }; } initialize(props) { this.data = props.data || []; let { data, tfoot } = this.parseChildData(props); this.data = this.data.concat(data); this.tfoot = tfoot; this.initializeSorts(props); this.initializeFilters(props); } initializeFilters(props) { this._filterable = {}; // Transform filterable properties into a more friendly list for (let i in props.filterable) { let column = props.filterable[i]; let columnName, filterFunction; if (column instanceof Object) { if (typeof(column.column) !== 'undefined') { columnName = column.column; } else { console.warn('Filterable column specified without column name'); continue; } if (typeof(column.filterFunction) === 'function') { filterFunction = column.filterFunction; } else { filterFunction = 'default'; } } else { columnName = column; filterFunction = 'default'; } this._filterable[columnName] = filterFunction; } } initializeSorts(props) { this._sortable = {}; // Transform sortable properties into a more friendly list for (let i in props.sortable) { let column = props.sortable[i]; let columnName, sortFunction; if (column instanceof Object) { if (typeof(column.column) !== 'undefined') { columnName = column.column; } else { console.warn('Sortable column specified without column name'); return; } if (typeof(column.sortFunction) === 'function') { sortFunction = column.sortFunction; } else { sortFunction = 'default'; } } else { columnName = column; sortFunction = 'default'; } this._sortable[columnName] = sortFunction; } } getCurrentSort(column) { let columnName, sortDirection; if (column instanceof Object) { if (typeof(column.column) !== 'undefined') { columnName = column.column; } else { console.warn('Default column specified without column name'); return; } if (typeof(column.direction) !== 'undefined') { if (column.direction === 1 || column.direction === 'asc') { sortDirection = 1; } else if (column.direction === -1 || column.direction === 'desc') { sortDirection = -1; } else { let defaultDirection = this.props.defaultSortDescending ? 'descending' : 'ascending'; console.warn('Invalid default sort specified. Defaulting to ' + defaultDirection ); sortDirection = this.props.defaultSortDescending ? -1 : 1; } } else { sortDirection = this.props.defaultSortDescending ? -1 : 1; } } else { columnName = column; sortDirection = this.props.defaultSortDescending ? -1 : 1; } return { column: columnName, direction: sortDirection }; } updateCurrentSort(sortBy) { if (sortBy !== false && sortBy.column !== this.state.currentSort.column && sortBy.direction !== this.state.currentSort.direction) { this.setState({ currentSort: this.getCurrentSort(sortBy) }); } } updateCurrentPage(nextPage) { if (typeof(nextPage) !== 'undefined' && nextPage !== this.state.currentPage) { this.setState({ currentPage: nextPage}); } } componentWillMount() { this.initialize(this.props); this.sortByCurrentSort(); this.filterBy(this.props.filterBy); } componentWillReceiveProps(nextProps) { this.initialize(nextProps); this.updateCurrentPage(nextProps.currentPage) this.updateCurrentSort(nextProps.sortBy); this.sortByCurrentSort(); this.filterBy(nextProps.filterBy); } applyFilter(filter, children) { // Helper function to apply filter text to a list of table rows filter = filter.toLowerCase(); let matchedChildren = []; for (let i = 0; i < children.length; i++) { let data = children[i].props.data; for (let filterColumn in this._filterable) { if (typeof(data[filterColumn]) !== 'undefined') { // Default filter if (typeof(this._filterable[filterColumn]) === 'undefined' || this._filterable[filterColumn]=== 'default') { if (extractDataFrom(data, filterColumn).toString().toLowerCase().indexOf(filter) > -1) { matchedChildren.push(children[i]); break; } } else { // Apply custom filter if (this._filterable[filterColumn](extractDataFrom(data, filterColumn).toString(), filter)) { matchedChildren.push(children[i]); break; } } } } } return matchedChildren; } sortByCurrentSort() { // Apply a sort function according to the current sort in the state. // This allows us to perform a default sort even on a non sortable column. let currentSort = this.state.currentSort; if (currentSort.column === null) { return; } this.data.sort(function(a, b){ let keyA = extractDataFrom(a, currentSort.column); keyA = isUnsafe(keyA) ? keyA.toString() : keyA || ''; let keyB = extractDataFrom(b, currentSort.column); keyB = isUnsafe(keyB) ? keyB.toString() : keyB || ''; // Default sort if ( typeof(this._sortable[currentSort.column]) === 'undefined' || this._sortable[currentSort.column] === 'default' ) { // Reverse direction if we're doing a reverse sort if (keyA < keyB) { return -1 * currentSort.direction; } if (keyA > keyB) { return 1 * currentSort.direction; } return 0; } else { // Reverse columns if we're doing a reverse sort if (currentSort.direction === 1) { return this._sortable[currentSort.column](keyA, keyB); } else { return this._sortable[currentSort.column](keyB, keyA); } } }.bind(this)); } onSort(column) { // Don't perform sort on unsortable columns if (typeof(this._sortable[column]) === 'undefined') { return; } let currentSort = this.state.currentSort; if (currentSort.column === column) { currentSort.direction *= -1; } else { currentSort.column = column; currentSort.direction = this.props.defaultSortDescending ? -1 : 1; } // Set the current sort and pass it to the sort function this.setState({ currentSort: currentSort }); this.sortByCurrentSort(); if (typeof(this.props.onSort) === 'function') { this.props.onSort(currentSort); } } render() { let children = []; let columns; let userColumnsSpecified = false; let showHeaders = typeof this.props.hideTableHeader === 'undefined'; let firstChild = null; if (this.props.children) { if ( this.props.children.length > 0 && this.props.children[0] && this.props.children[0].type === Thead ) { firstChild = this.props.children[0] } else if ( this.props.children.type === Thead ) { firstChild = this.props.children } } if (firstChild !== null) { columns = Thead.getColumns(firstChild); } else { columns = this.props.columns || []; } if (columns.length > 0) { userColumnsSpecified = true; columns = this.translateColumnsArray(columns); } // Build up table rows if (this.data && typeof this.data.map === 'function') { // Build up the columns array children = children.concat(this.data.map(function(rawData, i) { let data = rawData; let props = {}; if (rawData.__reactableMeta === true) { data = rawData.data; props = rawData.props; } // Loop through the keys in each data row and build a td for it for (let k in data) { if (data.hasOwnProperty(k)) { // Update the columns array with the data's keys if columns were not // already specified if (userColumnsSpecified === false) { let column = { key: k, label: k }; // Only add a new column if it doesn't already exist in the columns array if ( columns.find(function(element) { return element.key === column.key; }) === undefined ) { columns.push(column); } } } } return ( <Tr columns={columns} key={i} data={data} {...props} /> ); }.bind(this))); } if (this.props.sortable === true) { for (let i = 0; i < columns.length; i++) { this._sortable[columns[i].key] = 'default'; } } // Determine if we render the filter box let filtering = false; if ( this.props.filterable && Array.isArray(this.props.filterable) && this.props.filterable.length > 0 && !this.props.hideFilterInput ) { filtering = true; } // Apply filters let filteredChildren = children; if (this.state.filter !== '') { filteredChildren = this.applyFilter(this.state.filter, filteredChildren); } // Determine pagination properties and which columns to display let itemsPerPage = 0; let pagination = false; let numPages; let currentPage = this.state.currentPage; let pageButtonLimit = this.props.pageButtonLimit || 10; let currentChildren = filteredChildren; if (this.props.itemsPerPage > 0) { itemsPerPage = this.props.itemsPerPage; numPages = Math.ceil(filteredChildren.length / itemsPerPage); if (currentPage > numPages - 1) { currentPage = numPages - 1; } pagination = true; currentChildren = filteredChildren.slice( currentPage * itemsPerPage, (currentPage + 1) * itemsPerPage ); } // Manually transfer props let props = filterPropsFrom(this.props); let noDataText = this.props.noDataText ? <tr className="reactable-no-data"><td colSpan={columns.length}>{this.props.noDataText}</td></tr> : null; var tableHeader = null; if (columns && columns.length > 0 && showHeaders) { tableHeader = ( <Thead columns={columns} filtering={filtering} onFilter={filter => { this.setState({ filter: filter }); if (this.props.onFilter) { this.props.onFilter(filter) } }} filterPlaceholder={this.props.filterPlaceholder} filterClassName={this.props.filterClassName} currentFilter={this.state.filter} sort={this.state.currentSort} sortableColumns={this._sortable} onSort={this.onSort.bind(this)} key="thead"/> ) } return <table {...props}> {tableHeader} <tbody className="reactable-data" key="tbody"> {currentChildren.length > 0 ? currentChildren : noDataText} </tbody> {pagination === true ? <Paginator colSpan={columns.length} pageButtonLimit={pageButtonLimit} numPages={numPages} currentPage={currentPage} onPageChange={page => { this.setState({ currentPage: page }); if (this.props.onPageChange) { this.props.onPageChange(page) } }} previousPageLabel={this.props.previousPageLabel} nextPageLabel={this.props.nextPageLabel} key="paginator"/> : null} {this.tfoot} </table>; } } Table.defaultProps = { sortBy: false, defaultSort: false, defaultSortDescending: false, itemsPerPage: 0, filterBy: '', hideFilterInput: false };