d2-ui
Version:
432 lines (356 loc) • 11.7 kB
JSX
import React from 'react';
import Checkbox from '../checkbox';
import TableRowColumn from './table-row-column';
import ClickAwayable from '../mixins/click-awayable';
import StylePropable from '../mixins/style-propable';
import getMuiTheme from '../styles/getMuiTheme';
const TableBody = React.createClass({
propTypes: {
/**
* Set to true to indicate that all rows should be selected.
*/
allRowsSelected: React.PropTypes.bool,
/**
* Children passed to table body.
*/
children: React.PropTypes.node,
/**
* The css class name of the root element.
*/
className: React.PropTypes.string,
/**
* Controls whether or not to deselect all selected
* rows after clicking outside the table.
*/
deselectOnClickaway: React.PropTypes.bool,
/**
* Controls the display of the row checkbox. The default value is true.
*/
displayRowCheckbox: React.PropTypes.bool,
/**
* If true, multiple table rows can be selected.
* CTRL/CMD+Click and SHIFT+Click are valid actions.
* The default value is false.
*/
multiSelectable: React.PropTypes.bool,
/**
* Callback function for when a cell is clicked.
*/
onCellClick: React.PropTypes.func,
/**
* Called when a table cell is hovered. rowNumber
* is the row number of the hovered row and columnId
* is the column number or the column key of the cell.
*/
onCellHover: React.PropTypes.func,
/**
* Called when a table cell is no longer hovered.
* rowNumber is the row number of the row and columnId
* is the column number or the column key of the cell.
*/
onCellHoverExit: React.PropTypes.func,
/**
* Called when a table row is hovered.
* rowNumber is the row number of the hovered row.
*/
onRowHover: React.PropTypes.func,
/**
* Called when a table row is no longer
* hovered. rowNumber is the row number of the row
* that is no longer hovered.
*/
onRowHoverExit: React.PropTypes.func,
/**
* Called when a row is selected. selectedRows is an
* array of all row selections. IF all rows have been selected,
* the string "all" will be returned instead to indicate that
* all rows have been selected.
*/
onRowSelection: React.PropTypes.func,
/**
* Controls whether or not the rows are pre-scanned to determine
* initial state. If your table has a large number of rows and
* you are experiencing a delay in rendering, turn off this property.
*/
preScanRows: React.PropTypes.bool,
/**
* If true, table rows can be selected. If multiple
* row selection is desired, enable multiSelectable.
* The default value is true.
*/
selectable: React.PropTypes.bool,
/**
* If true, table rows will be highlighted when
* the cursor is hovering over the row. The default
* value is false.
*/
showRowHover: React.PropTypes.bool,
/**
* If true, every other table row starting
* with the first row will be striped. The default value is false.
*/
stripedRows: React.PropTypes.bool,
/**
* Override the inline-styles of the root element.
*/
style: React.PropTypes.object,
},
contextTypes: {
muiTheme: React.PropTypes.object,
},
//for passing default theme context to children
childContextTypes: {
muiTheme: React.PropTypes.object,
},
mixins: [
ClickAwayable,
StylePropable,
],
getDefaultProps() {
return {
allRowsSelected: false,
deselectOnClickaway: true,
displayRowCheckbox: true,
multiSelectable: false,
preScanRows: true,
selectable: true,
style: {},
};
},
getInitialState() {
return {
muiTheme: this.context.muiTheme || getMuiTheme(),
selectedRows: this._calculatePreselectedRows(this.props),
};
},
getChildContext() {
return {
muiTheme: this.state.muiTheme,
};
},
//to update theme inside state whenever a new theme is passed down
//from the parent / owner using context
componentWillReceiveProps(nextProps, nextContext) {
let newMuiTheme = nextContext.muiTheme ? nextContext.muiTheme : this.state.muiTheme;
this.setState({muiTheme: newMuiTheme});
let newState = {};
if (this.props.allRowsSelected && !nextProps.allRowsSelected) {
newState.selectedRows = this.state.selectedRows.length > 0
? [this.state.selectedRows[this.state.selectedRows.length - 1]]
: [];
} else {
newState.selectedRows = this._calculatePreselectedRows(nextProps);
}
this.setState(newState);
},
componentClickAway() {
if (this.props.deselectOnClickaway && this.state.selectedRows.length) {
this.setState({selectedRows: []});
if (this.props.onRowSelection) this.props.onRowSelection([]);
}
},
_createRows() {
let numChildren = React.Children.count(this.props.children);
let rowNumber = 0;
const handlers = {
onCellClick: this._onCellClick,
onCellHover: this._onCellHover,
onCellHoverExit: this._onCellHoverExit,
onRowHover: this._onRowHover,
onRowHoverExit: this._onRowHoverExit,
onRowClick: this._onRowClick,
};
return React.Children.map(this.props.children, (child) => {
if (React.isValidElement(child)) {
let props = {
displayRowCheckbox: this.props.displayRowCheckbox,
hoverable: this.props.showRowHover,
selected: this._isRowSelected(rowNumber),
striped: this.props.stripedRows && (rowNumber % 2 === 0),
rowNumber: rowNumber++,
};
let checkboxColumn = this._createRowCheckboxColumn(props);
if (rowNumber === numChildren) {
props.displayBorder = false;
}
let children = [checkboxColumn];
React.Children.forEach(child.props.children, (child) => {
children.push(child);
});
return React.cloneElement(child, {...props, ...handlers}, children);
}
});
},
_createRowCheckboxColumn(rowProps) {
if (!this.props.displayRowCheckbox) return null;
let key = rowProps.rowNumber + '-cb';
const checkbox = (
<Checkbox
ref="rowSelectCB"
name={key}
value="selected"
disabled={!this.props.selectable}
checked={rowProps.selected}
/>
);
return (
<TableRowColumn
key={key}
columnNumber={0}
style={{width: 24}}
>
{checkbox}
</TableRowColumn>
);
},
_calculatePreselectedRows(props) {
// Determine what rows are 'pre-selected'.
let preSelectedRows = [];
if (props.selectable && props.preScanRows) {
let index = 0;
React.Children.forEach(props.children, (child) => {
if (React.isValidElement(child)) {
if (child.props.selected && (preSelectedRows.length === 0 || props.multiSelectable)) {
preSelectedRows.push(index);
}
index++;
}
});
}
return preSelectedRows;
},
_isRowSelected(rowNumber) {
if (this.props.allRowsSelected) {
return true;
}
for (let i = 0; i < this.state.selectedRows.length; i++) {
let selection = this.state.selectedRows[i];
if (typeof selection === 'object') {
if (this._isValueInRange(rowNumber, selection)) return true;
} else {
if (selection === rowNumber) return true;
}
}
return false;
},
_isValueInRange(value, range) {
if (!range) return false;
if ((range.start <= value && value <= range.end) || (range.end <= value && value <= range.start)) {
return true;
}
return false;
},
_onRowClick(e, rowNumber) {
e.stopPropagation();
if (this.props.selectable) {
// Prevent text selection while selecting rows.
window.getSelection().removeAllRanges();
this._processRowSelection(e, rowNumber);
}
},
_processRowSelection(e, rowNumber) {
let selectedRows = this.state.selectedRows;
if (e.shiftKey && this.props.multiSelectable && selectedRows.length) {
let lastIndex = selectedRows.length - 1;
let lastSelection = selectedRows[lastIndex];
if (typeof lastSelection === 'object') {
lastSelection.end = rowNumber;
} else {
selectedRows.splice(lastIndex, 1, {start: lastSelection, end: rowNumber});
}
} else if (((e.ctrlKey && !e.metaKey) || (e.metaKey && !e.ctrlKey)) && this.props.multiSelectable) {
let idx = selectedRows.indexOf(rowNumber);
if (idx < 0) {
let foundRange = false;
for (let i = 0; i < selectedRows.length; i++) {
let range = selectedRows[i];
if (typeof range !== 'object') continue;
if (this._isValueInRange(rowNumber, range)) {
foundRange = true;
let values = this._splitRange(range, rowNumber);
selectedRows.splice(i, 1, ...values);
}
}
if (!foundRange) selectedRows.push(rowNumber);
} else {
selectedRows.splice(idx, 1);
}
} else {
if (selectedRows.length === 1 && selectedRows[0] === rowNumber) {
selectedRows = [];
} else {
selectedRows = [rowNumber];
}
}
this.setState({selectedRows: selectedRows});
if (this.props.onRowSelection) this.props.onRowSelection(this._flattenRanges(selectedRows));
},
_splitRange(range, splitPoint) {
let splitValues = [];
let startOffset = range.start - splitPoint;
let endOffset = range.end - splitPoint;
// Process start half
splitValues.push(...this._genRangeOfValues(splitPoint, startOffset));
// Process end half
splitValues.push(...this._genRangeOfValues(splitPoint, endOffset));
return splitValues;
},
_genRangeOfValues(start, offset) {
let values = [];
let dir = (offset > 0) ? -1 : 1; // This forces offset to approach 0 from either direction.
while (offset !== 0) {
values.push(start + offset);
offset += dir;
}
return values;
},
_flattenRanges(selectedRows) {
let rows = [];
for (let selection of selectedRows) {
if (typeof selection === 'object') {
let values = this._genRangeOfValues(selection.end, selection.start - selection.end);
rows.push(selection.end, ...values);
} else {
rows.push(selection);
}
}
return rows.sort();
},
_onCellClick(e, rowNumber, columnNumber) {
e.stopPropagation();
if (this.props.onCellClick) this.props.onCellClick(rowNumber, this._getColumnId(columnNumber));
},
_onCellHover(e, rowNumber, columnNumber) {
if (this.props.onCellHover) this.props.onCellHover(rowNumber, this._getColumnId(columnNumber));
this._onRowHover(e, rowNumber);
},
_onCellHoverExit(e, rowNumber, columnNumber) {
if (this.props.onCellHoverExit) this.props.onCellHoverExit(rowNumber, this._getColumnId(columnNumber));
this._onRowHoverExit(e, rowNumber);
},
_onRowHover(e, rowNumber) {
if (this.props.onRowHover) this.props.onRowHover(rowNumber);
},
_onRowHoverExit(e, rowNumber) {
if (this.props.onRowHoverExit) this.props.onRowHoverExit(rowNumber);
},
_getColumnId(columnNumber) {
let columnId = columnNumber;
if (this.props.displayRowCheckbox) columnId--;
return columnId;
},
render() {
let {
className,
style,
...other,
} = this.props;
let rows = this._createRows();
return (
<tbody className={className} style={this.prepareStyles(style)}>
{rows}
</tbody>
);
},
});
export default TableBody;