diff --git a/DataTables.js b/DataTables.js new file mode 100644 index 0000000..a5c22bd --- /dev/null +++ b/DataTables.js @@ -0,0 +1,471 @@ +import React, {Component} from 'react'; +import PropTypes from 'prop-types'; +import {TableHeader, TableRow} from 'material-ui/Table'; +import {Toolbar} from 'material-ui/Toolbar'; +import DropDownMenu from 'material-ui/DropDownMenu'; +import MenuItem from 'material-ui/MenuItem'; +import FlatButton from 'material-ui/FlatButton'; +import ChevronLeft from 'material-ui/svg-icons/navigation/chevron-left'; +import ChevronRight from 'material-ui/svg-icons/navigation/chevron-right'; +// customized components +import DataTablesTable from './DataTablesTable'; +import DataTablesTableBody from './DataTablesTableBody'; +import DataTablesHeaderColumn from './DataTablesHeaderColumn'; +import DataTablesRow from './DataTablesRow'; +import DataTablesRowColumn from './DataTablesRowColumn'; +import DataTablesHeaderToolbar from './DataTablesHeaderToolbar'; + +function getStyles(props, context) { + const { + baseTheme: { + palette, + }, + table, + tableHeaderColumn, + } = context.muiTheme; + + return { + tableHeaderColumn: { + fontWeight: 600, + }, + footerToolbar: { + backgroundColor: table.backgroundColor, + borderTop: `1px solid ${palette.borderColor}`, + }, + footerControlGroup: { + fontSize: 12, + color: tableHeaderColumn.textColor, + marginLeft: 'auto', + display: 'flex', + }, + footerToolbarItem: { + marginLeft: 8, + marginRight: 8, + alignItems: 'center', + display: 'flex', + }, + paginationButtons: { + marginLeft: 24, + }, + paginationButton: { + minWidth: 36, + opacity: 0.54, + }, + rowSizeMenu: { + color: tableHeaderColumn.textColor, + }, + rowSizeControlsWrapper: { + display: 'flex', + }, + }; +} + +function isRowSelected(index, selectedRows) { + if (Array.isArray(selectedRows)) { + return selectedRows.includes(index); + } else { + return undefined; + } +} + +class DataTables extends Component { + static muiName = 'DataTables'; + + static propTypes = { + columns: PropTypes.array.isRequired, + count: PropTypes.number, + data: PropTypes.array, + deselectOnClickaway: PropTypes.bool, + enableSelectAll: PropTypes.bool, + filterHintText: PropTypes.string, + fixedFooter: PropTypes.bool, + fixedHeader: PropTypes.bool, + footerToolbarStyle: PropTypes.object, + headerToolbarStyle: PropTypes.object, + height: PropTypes.string, + initialSort: PropTypes.object, + multiSelectable: PropTypes.bool, + onCellClick: PropTypes.func, + onCellDoubleClick: PropTypes.func, + onFilterValueChange: PropTypes.func, + onNextPageClick: PropTypes.func, + onPreviousPageClick: PropTypes.func, + onRowSelection: PropTypes.func, + onRowSizeChange: PropTypes.func, + onSortOrderChange: PropTypes.func, + page: PropTypes.number, + rowSize: PropTypes.number, + rowSizeLabel: PropTypes.string, + rowSizeList: PropTypes.array, + selectable: PropTypes.bool, + selectedRows: PropTypes.array, + showCheckboxes: PropTypes.bool, + showFooterToolbar: PropTypes.bool, + showHeaderToolbar: PropTypes.bool, + showRowHover: PropTypes.bool, + showRowSizeControls: PropTypes.bool, + stripedRows: PropTypes.bool, + summaryLabelTemplate: PropTypes.func, + tableBodyStyle: PropTypes.object, + tableHeaderColumnStyle: PropTypes.object, + tableHeaderStyle: PropTypes.object, + tableRowColumnStyle: PropTypes.object, + tableRowStyle: PropTypes.object, + tableStyle: PropTypes.object, + tableWrapperStyle: PropTypes.object, + title: PropTypes.string, + titleStyle: PropTypes.object, + toolbarIconRight: PropTypes.node, + }; + + static contextTypes = { + muiTheme: PropTypes.object.isRequired, + }; + + static defaultProps = { + rowSize: 10, + rowSizeLabel: 'Rows per page:', + rowSizeList: [10, 30, 50, 100], + summaryLabelTemplate: (start, end, count) => { + return `${start} - ${end} of ${count}`; + }, + showRowSizeControls: true, + filterHintText: 'Search', + columns: [], + data: [], + page: 1, + count: 0, + fixedHeader: false, + fixedFooter: false, + stripedRows: false, + showRowHover: false, + selectable: false, + selectedRows: undefined, + multiSelectable: false, + enableSelectAll: false, + deselectOnClickaway: false, + showCheckboxes: false, + height: 'inherit', + showHeaderToolbar: false, + showFooterToolbar: true, + initialSort: { + column: '', + order: 'asc', + }, + }; + + constructor(props, context) { + super(props, context); + this.state = { + sort: props.initialSort, + }; + } + + handleHeaderColumnClick = (event, rowIndex, columnIndex) => { + const adjustedColumnIndex = columnIndex - 1; + const column = this.props.columns[adjustedColumnIndex]; + if (column && column.sortable) { + const {sort} = this.state; + const {onSortOrderChange} = this.props; + const key = column.key; + const order = sort.column === column.key && sort.order === 'asc' ? 'desc' : 'asc'; + this.setState({ + sort: { + column: key, + order: order, + }, + }); + if (onSortOrderChange) { + onSortOrderChange(key, order); + } + } + } + + handleCellClick = (rowIndex, columnIndex, event) => { + const {onCellClick, selectable} = this.props; + if (onCellClick && !selectable) { + const adjustedColumnIndex = this.props.showCheckboxes ? columnIndex : columnIndex - 1; + onCellClick( + rowIndex, + adjustedColumnIndex, + // row data + this.props.data[rowIndex], + // clicked column + this.props.data[rowIndex][this.props.columns[adjustedColumnIndex].key], + event + ); + } + } + + handleCellDoubleClick = (rowIndex, columnIndex, event) => { + const {onCellDoubleClick} = this.props; + if (onCellDoubleClick) { + const adjustedColumnIndex = this.props.showCheckboxes ? columnIndex : columnIndex - 1; + onCellDoubleClick( + rowIndex, + adjustedColumnIndex, + // row data + this.props.data[rowIndex], + // clicked column + this.props.data[rowIndex][this.props.columns[adjustedColumnIndex].key], + event + ); + } + } + + handleRowSizeChange = (event, index, value) => { + const {onRowSizeChange} = this.props; + if (onRowSizeChange) { + onRowSizeChange(index, value); + } + } + + handlePreviousPageClick = (event) => { + const {onPreviousPageClick} = this.props; + if (onPreviousPageClick) { + onPreviousPageClick(event); + } + } + + handleNextPageClick = (event) => { + const {onNextPageClick} = this.props; + if (onNextPageClick) { + onNextPageClick(event); + } + } + + handleFilterValueChange = (value) => { + const {onFilterValueChange} = this.props; + if (onFilterValueChange) { + onFilterValueChange(value); + } + } + + handleRowSelection = (selectedRows) => { + const {onRowSelection} = this.props; + if (onRowSelection) { + onRowSelection(selectedRows); + } + } + + renderTableRowColumnData = (row, column) => { + if (column.render) return column.render(row[column.key], row); + return row[column.key]; + } + + render() { + const { + title, + titleStyle, + filterHintText, + fixedHeader, + fixedFooter, + footerToolbarStyle, + headerToolbarStyle, + stripedRows, + showRowHover, + selectable, + multiSelectable, + enableSelectAll, + deselectOnClickaway, + showCheckboxes, + height, + showHeaderToolbar, + showFooterToolbar, + rowSize, + rowSizeLabel, + rowSizeList, + showRowSizeControls, + summaryLabelTemplate, + columns, + data, + page, + toolbarIconRight, + count, + tableStyle, + tableBodyStyle, + tableHeaderColumnStyle, + tableHeaderStyle, + tableRowColumnStyle, + tableRowStyle, + tableWrapperStyle, + ...other, // eslint-disable-line no-unused-vars, comma-dangle + } = this.props; + + const styles = getStyles(this.props, this.context); + + let start = (page - 1) * rowSize + 1; + let end = (page - 1) * rowSize + rowSize; + const totalCount = count === 0 ? data.length : count; + let previousButtonDisabled = page === 1; + let nextButtonDisabled = false; + if (totalCount === 0) { + start = 0; + previousButtonDisabled = true; + } else if (start > totalCount) { + start = 1; + previousButtonDisabled = true; + } + if (end >= totalCount) { + end = totalCount; + nextButtonDisabled = true; + } + + let headerToolbar; + if (showHeaderToolbar) { + headerToolbar = ( + + ); + } + + let rowSizeControls = null; + if (showRowSizeControls) { + rowSizeControls = ( +
+
+
{rowSizeLabel}
+
+ { + rowSizeList.length > 0 ? + ( + + {rowSizeList.map((rowSize) => { + return ( + + ); + })} + + ) : + null + } +
+ ); + } + + let footerToolbar; + if (showFooterToolbar) { + footerToolbar = ( + +
+ {rowSizeControls} +
+
{summaryLabelTemplate(start, end, totalCount)}
+
+
+ } + style={styles.paginationButton} + onClick={this.handlePreviousPageClick} + disabled={previousButtonDisabled} + /> + } + style={styles.paginationButton} + onClick={this.handleNextPageClick} + disabled={nextButtonDisabled} + /> +
+
+
+ ); + } + + return ( +
+ {headerToolbar} + + + + {columns.map((column, index) => { + const style = Object.assign({}, styles.tableHeaderColumn, tableHeaderColumnStyle, column.style || {}); + const sortable = column.sortable; + const sorted = this.state.sort.column === column.key; + const order = sorted ? this.state.sort.order : 'asc'; + return ( + + {column.label} + + ); + }, this)} + + + + {data.map((row, index) => { + return ( + + {columns.map((column, index) => { + return ( + + {this.renderTableRowColumnData(row, column)} + + ); + })} + + ); + })} + + + {footerToolbar} +
+ ); + } +} + +export default DataTables; diff --git a/DataTablesHeaderToolbar.js b/DataTablesHeaderToolbar.js new file mode 100644 index 0000000..dbd7055 --- /dev/null +++ b/DataTablesHeaderToolbar.js @@ -0,0 +1,224 @@ +import React, {Component} from 'react'; +import PropTypes from 'prop-types'; +import {Toolbar, ToolbarGroup, ToolbarTitle} from 'material-ui/Toolbar'; +import IconButton from 'material-ui/IconButton'; +import ClearIcon from 'material-ui/svg-icons/content/clear'; +import FilterListIcon from 'material-ui/svg-icons/content/filter-list'; +import SearchIcon from 'material-ui/svg-icons/action/search'; +import TextField from 'material-ui/TextField'; +import {blue500} from 'material-ui/styles/colors'; + +function getStyles(context) { + const { + table, + } = context.muiTheme; + + return { + headerToolbar: { + backgroundColor: table.backgroundColor, + height: 64, + paddingRight: 8, + }, + icon: { + opacity: 0.64, + }, + headerToolbarSearchIcon: { + marginTop: 12, + }, + headerToolbarIconButton: { + marginTop: 6, + }, + searchToolbarGroup: { + width: '100%', + display: 'flex', + alignItems: 'center', + }, + searchInputTextField: { + marginTop: 6, + marginLeft: 8, + width: '100%', + minWidth: 60, + }, + headerToolbarDefaultIcons: { + display: 'flex', + alignItems: 'center', + }, + toolbarTitle: { + lineHeight: '72px', + }, + }; +} + +class DataTablesHeaderToolbar extends Component { + static muiName = 'DataTablesHeaderToolbar'; + + static propTypes = { + filterHintText: PropTypes.string, + handleFilterValueChange: PropTypes.func, + onFilterValueChange: PropTypes.func, + title: PropTypes.string, + titleStyle: PropTypes.object, + headerToolbarStyle: PropTypes.object, + toolbarIconRight: PropTypes.node, + }; + + static defaultProps = { + + }; + + static contextTypes = { + muiTheme: PropTypes.object.isRequired, + }; + + constructor(props, context) { + super(props, context); + this.filterValueTimer = undefined; + this.filterInput = undefined; + this.state = { + mode: 'default', + filterValue: '', + }; + } + + componentDidUpdate(prevProps, prevState) { + if (prevState.mode === 'default' && this.state.mode === 'filter') { + if (this.filterInput) { + this.filterInput.focus(); + } + } + } + + handleFilterClick = () => { + const mode = this.state.mode === 'default' ? 'filter' : 'default'; + const {filterValue} = this.state; + this.setState({ + mode: mode, + filterValue: '', + }); + if (mode === 'default' && filterValue !== '') { + this.emitFilterValueChange(''); + } + } + + handleClearClick = () => { + const {filterValue} = this.state; + if (filterValue !== '') { + this.setState({ + filterValue: '', + }); + this.emitFilterValueChange(''); + } + } + + handleFilterValueChange = (event) => { + const value = event.target.value; + this.setState({ + filterValue: value, + }); + clearTimeout(this.filterValueTimer); + this.filterValueTimer = setTimeout(() => { + this.emitFilterValueChange(value); + }, 500); + } + + emitFilterValueChange = (value) => { + const {onFilterValueChange} = this.props; + if (onFilterValueChange) { + onFilterValueChange(value); + } + } + + render() { + const { + filterHintText, + toolbarIconRight, + title, // eslint-disable-line no-unused-vars + titleStyle, + headerToolbarStyle, + ...other, // eslint-disable-line no-unused-vars, comma-dangle + } = this.props; + + const { + mode, + filterValue, + } = this.state; + + const styles = getStyles(this.context); + + let contentNode; + + if (mode === 'default') { + contentNode = (); + } else if (mode === 'filter') { + contentNode = ( +
+
+ +
+
+ { + this.filterInput = textField ? textField.input : null; + }} + /> +
+
+ + + +
+
+ ); + } + + const toolbarIconRightChildren = []; + if (toolbarIconRight) { + if (toolbarIconRight.length) { + toolbarIconRight.map((toolbarIcon, i) => { + toolbarIconRightChildren.push(React.cloneElement( + toolbarIcon, + { + style: Object.assign(styles.headerToolbarIconButton, styles.icon), + key: i, + } + )); + }); + } else { + toolbarIconRightChildren.push(React.cloneElement( + toolbarIconRight, + { + style: Object.assign(styles.headerToolbarIconButton, styles.icon), + key: 1, + } + )); + } + } + + return ( + + {contentNode} + + + + + {toolbarIconRightChildren} + + + ); + } +} + +export default DataTablesHeaderToolbar;