@darwino/darwino-react
Version:
A set of Javascript classes and utilities
542 lines (486 loc) • 17.4 kB
JSX
/*
* (c) Copyright Darwino Inc. 2014-2017.
*/
import React, { Component } from "react";
import PropTypes from 'prop-types';
import ReactDataGrid, {Row,SimpleCellFormatter} from './react-grid/react-data-grid';
import { DEV_OPTIONS, MobileUtils, EmptyDataFetcher, ArrayDataFetcher, PagingDataFetcher, CursorEntries, Utils} from '@darwino/darwino';
import BaseCursorList from './BaseCursorList';
function filterColumns(columns, stacked) {
if(stacked) {
return columns.filter( (col) => {
if(col.hideWhenStacked) {
return false;
}
return true
})
}
return columns
}
export class CursorGridRowRenderer extends React.Component {
// Context to read from the parent
static contextTypes = {
cursorGrid: PropTypes.object
};
static propTypes = {
idx: PropTypes.number.isRequired
};
getGrid() {
return this.context.cursorGrid;
}
isRenderAllRows() {
const grid = this.getGrid().refs.reactGrid;
return grid && grid.props.renderAllRows;
}
setScrollLeft(scrollBy) {
// if you want freeze columns to work, you need to make sure you implement this as apass through
if(this.row) {
this.row.setScrollLeft(scrollBy);
}
}
onCellExpand(e) {
e.stopPropagation();
var meta = this.props.cellMetaData;
if (meta != null && meta.onCellExpand) {
meta.onCellExpand({rowData:this.props.row});
}
}
renderCategory() {
const props = this.props;
let treeDepth = props.subRowDetails.treeDepth || 0;
let marginLeft = treeDepth * 20;
const row = props.row;
let style = {
//height: '30px',
border: '1px solid #dddddd',
paddingTop: '5px',
paddingBottom: '5px',
paddingLeft: '5px'
};
let onKeyDown = (e) => {
const onCellExpand = this.props.cellMetaData.onCellExpand;
if (e.key === 'ArrowLeft') {
if(row.__meta._expanded) {
onCellExpand({rowData:row});
}
}
if (e.key === 'ArrowRight') {
if(!row.__meta._expanded) {
onCellExpand({rowData:row});
}
}
if (e.key === 'Enter') {
onCellExpand({rowData:row});
}
};
let value = this.formatCategory(row.key);
return (
<div onKeyDown={onKeyDown} style={style} tabIndex={0} className="category-cell">
<span className="row-expand-icon" style={{float: 'left', marginLeft: marginLeft, cursor: 'pointer'}} onClick={this.onCellExpand.bind(this)} >{row.__meta._expanded ? String.fromCharCode('9660') : String.fromCharCode('9658')}</span>
<strong>{value}</strong>
</div>
);
}
formatCategory(value) {
const props = this.props;
let treeDepth = props.subRowDetails.treeDepth || 0;
const grid = this.getGrid();
let formatter = grid.props.groupBy[treeDepth].formatter
if(formatter) {
return formatter({value,dependentValues:undefined,row:this.props.row,idx:0,category:true})
}
return value;
}
renderCategoryColumns() {
return this.renderRowColumns()
}
renderTotals() {
return this.renderRowColumns()
}
renderDocument() {
return this.renderRowColumns()
}
renderRowColumns() {
if (this.isRenderAllRows()) {
return this.renderResponsive();
} else {
return (<div><Row ref={ node => this.row = node } {...this.props}/></div>);
}
}
renderResponsive2() {
return (<div><Row ref={ node => this.row = node } {...this.props}/></div>);
}
renderResponsive() {
const row = this.props.row;
const columns = this.props.columns;
let style = {
//height: 100, //this.getRowHeight(this.props),
//overflow: 'hidden',
contain: 'layout'
};
return (
<div className="react-grid-Row" style={style} onClick={()=>this.getGrid().handleRowClick(row)}>
<div className="react-grid-Cell">
<table>
{columns
// .filter( (col) => {
// return true
// })
.map( (col,idx) => {
const value = this.getCellValue(col.key)
const Formatter = col.formatter
const colValue = Utils.isFunction(Formatter)
? (<Formatter value={value} row={row} idx={idx} dependentValues={undefined} reponsive={true}/>)
: value
//: (<SimpleCellFormatter value={value}/>)
if(col.name) {
return (
<tr>
<td><strong>{col.name}: </strong></td>
<td>{colValue}</td>
</tr>
)
} else {
return (
<tr>
<td colSpan={2}>{colValue}</td>
</tr>
)
}
})
}
</table>
</div>
</div>
);
}
getCellValue(key) {
let val;
if (key === 'select-row') {
return this.props.isSelected;
} else if (typeof this.props.row.get === 'function') {
val = this.props.row.get(key);
} else {
val = this.props.row[key];
}
return val;
}
render() {
// It happens that the row can be empty when the data fetcher is lazily loading them
// We check __meta to handle this case, and the row will be rendered empty
const row = this.props.row;
if(row.__meta) {
if(row.__meta.category) {
const grid = this.getGrid();
if(grid.props.renderCategoryAsColumns) {
return this.renderCategoryColumns();
}
return this.renderCategory();
}
if(row.__meta.totals) {
return this.renderTotals();
}
return this.renderDocument();
}
return this.renderDocument();
// // React force to return null to not
// return null;
}
}
/*
* Data Grid diplaying the result of a cursor
*/
class BaseCursorGrid extends BaseCursorList {
// Context to pusblish to the children - documentForm
static childContextTypes = {
cursorGrid: PropTypes.object
};
getChildContext() {
return {cursorGrid: this};
}
constructor(props) {
super(props);
this.handleGridSort = this.handleGridSort.bind(this);
this.rowGetter = this.rowGetter.bind(this);
this.getSubRowDetails = this.getSubRowDetails.bind(this);
this.onCellExpand = this.onCellExpand.bind(this);
this.handleRowSelected = this.handleRowSelected.bind(this);
this.handleRowDeselected = this.handleRowDeselected.bind(this);
this.state.selectedIndexes = []
}
//
// Handling selection
//
clearSelection() {
this.setState({selectedIndexes:[]})
}
hasSelectedEntries() {
return this.state.selectedIndexes.length>0;
}
getSelectedEntries() {
return this.state.selectedIndexes.map((idx) => {
return this.rowGetter(idx)
})
}
getSelectedDocIds() {
return this.state.selectedIndexes.map((idx) => {
return this.rowGetter(idx).__meta.id
})
}
findColumn(key) {
const columns = this.props.columns;
if(columns) {
for(let i=0; i<columns.length; i++) {
if(columns[i].key==key) return columns[i]
}
}
return null;
}
getColumnNames() {
let r = []
const columns = this.props.columns;
if(columns) {
for(let i=0; i<columns.length; i++) {
r.push(columns[i].key)
}
}
return r;
}
getColumnTitles() {
let r = []
const columns = this.props.columns;
if(columns) {
for(let i=0; i<columns.length; i++) {
r.push(columns[i].name || columns[i].key)
}
}
return r;
}
//
// Grid data model
//
createDataFetcher(dataLoader) {
if(!dataLoader) return new EmptyDataFetcher();
if(this.props.groupBy || this.props.showResponses || this.props.inMemorySort) {
return this.createArrayDataFetcher(dataLoader);
} else {
return this.createPagingDataFetcher(dataLoader);
}
}
createArrayDataFetcher(dataLoader) {
return new ArrayDataFetcher({
dataLoader,
onDataLoaded: () => {this._initRows(this.props.expandLevel||0)},
...this.props.dataFetcher
})
}
_initRows(expandLevel) {
const rawData = this.dataFetcher.getRows()
const g = new CursorEntries(rawData)
g.inMemorySort = this.props.inMemorySort
g.indentDocuments = this.props.indentDocuments;
g.processEntries(this.props.processEntries)
if(this.sortColumn) {
g.sortBy([{column: this.sortColumn, descending:this.sortDescending}])
} else {
if(this.props.groupBy) {
g.groupBy(this.props.groupBy)
}
if(this.props.sortBy) {
g.sortBy(this.props.sortBy)
}
}
const rows = g.getRows()
this.setState(
{rows: this._addRows([],rows||[],expandLevel)}
)
}
_addRows(rows,r,expandLevel) {
this.updateSubRowDetails(r);
for(let i=0; i<r.length; i++) {
const row = r[i]
rows.push(row)
let expanded = (row.__meta.indentLevel||0)<expandLevel;
if(expanded && row.__meta.children) {
row.__meta._expanded = true
this._addRows(rows,row.__meta.children,expandLevel);
}
}
return rows
}
createJstoreCursor() {
const jsc = super.createJstoreCursor();
if(this.props.showResponses) {
jsc.jsonTree(true)
}
return jsc
}
rowsCount() {
if(this.state.rows) {
return this.state.rows.length
}
return this.dataFetcher ? this.dataFetcher.getRowCount() : 0
}
rowGetter(i) {
if(i<0) { // Happen when there is a click on the header
return null;
}
if(this.state.rows) {
return this.state.rows[i]
}
return this.dataFetcher ? this.dataFetcher.getRow(i) : null
}
//
// Row selection
//
handleRowSelected(rows) {
this.setState({selectedIndexes: this.state.selectedIndexes.concat(rows.map(r => r.rowIdx))});
}
handleRowDeselected(rows) {
let rowIndexes = rows.map(r => r.rowIdx);
this.setState({selectedIndexes: this.state.selectedIndexes.filter(i => rowIndexes.indexOf(i) === -1 )});
}
//
// Row expand/toggle
//
expand(level) {
this._initRows(level)
}
expandAll() {
this.expand(9999)
}
collapseAll() {
this.expand(0)
}
handleGridSort(sortColumn, sortDirection) {
let inMemory = this.props.inMemorySort
let c = this.findColumn(sortColumn);
if(c && sortDirection!="NONE") {
this.sortColumn = inMemory ? sortColumn : (c.sortField||sortColumn)
this.sortDescending = sortDirection=='DESC'
} else {
this.sortColumn = null
}
if(inMemory) {
this._initRows(this.props.expandLevel||0)
} else {
this.reinitData();
}
}
getSubRowDetails(rowItem) {
const cat = rowItem.__meta.category;
if(!cat && (!this.props.showResponses && (this.props.groupBy && !this.props.indentDocuments))) {
return null
}
let isExpanded = this._isExpanded(rowItem)
const children = rowItem.__meta.children;
let treeDepth = rowItem.__meta.indentLevel || 0;
if(!cat && this.props.groupBy && !this.props.indentDocuments) {
// Also works when the view is sorted, as this will lead to 0
treeDepth = Math.max(0,treeDepth-this.props.groupBy.length);
}
return {
grid: this, // Added to get access to the grid in renderers...
group: (cat ||this.props.showResponses) && children && children.length>0,
expanded: isExpanded,
children: children,
field: this.props.expandable || this.props.columns[0].key, // This is field where the expand/collapse arrow appears
treeDepth,
siblingIndex: rowItem.__meta.siblingIndex,
numberSiblings: rowItem.__meta.numberSiblings
};
}
onCellExpand(args) {
let rows = this.state.rows.slice(0);
const row = args.rowData
let rowIndex = rows.indexOf(row);
let subRows = row.__meta.children
// Should we use a state here?
let expanded = !this._isExpanded(row)
row.__meta._expanded = expanded
if(subRows) {
if (expanded) {
this.updateSubRowDetails(subRows);
rows.splice(rowIndex + 1, 0, ...subRows);
} else {
let count = 0;
for(let i=rowIndex+1; i<rows.length; i++) {
if(rows[i].__meta.indentLevel<=row.__meta.indentLevel) break;
count++
rows[i].__meta._expanded = false
}
rows.splice(rowIndex + 1, count);
}
}
this.setState({ rows });
}
updateSubRowDetails(subRows) {
subRows.forEach((sr, i) => {
sr.__meta.siblingIndex = i;
sr.__meta.numberSiblings = subRows.length;
});
}
_isExpanded(row) {
const exp = row.__meta._expanded;
if(exp!==undefined) {
return exp;
}
if(this.props.expandLevel) {
return (row.__meta.indentLevel||0)<this.props.expandLevel;
}
return false;
}
//
// Main render method
//
render() {
const props = this.props;
const height = this.props.height || 500
const responsive =props.responsive && MobileUtils.isMobile();
const columns = props.columns||(props.grid && props.grid.columns)||[]
const rowHeight = this.props.rowHeight;
return (
<div style={{height: '100%'}}>
{this.renderError && this.renderError()}
<ReactDataGrid
ref="reactGrid"
renderAllRows={responsive}
rowGetter={this.rowGetter}
rowsCount={this.rowsCount()}
minHeight={height}
emptyRowsView={this.emptyRow()}
onRowClick={(idx,data) => {
if(idx>=0) this.handleRowClick(data)
}}
onRowDoubleClick={(idx,data) => {
if(idx>=0) this.handleRowDoubleClick(data)
}}
rowRenderer={this.props.rowRenderer||CursorGridRowRenderer}
onGridSort={this.handleGridSort}
{ ...((props.groupBy || props.showResponses) &&
{
getSubRowDetails: this.getSubRowDetails,
onCellExpand: this.onCellExpand
}
)}
{ ...(props.selectRows && !responsive &&
{
rowSelection: {
showCheckbox: true,
enableShiftSelect: true,
onRowsSelected: this.handleRowSelected,
onRowsDeselected: this.handleRowDeselected,
selectBy: {
indexes: this.state.selectedIndexes
}
}
}
)}
columns={filterColumns(columns,responsive)}
rowHeight={rowHeight}
/>
</div>
);
}
}
export default BaseCursorGrid