fixed-react-data-grid-custom
Version:
Excel-like grid component built with React, with editors, keyboard navigation, copy & paste, and the like
444 lines (398 loc) • 14.5 kB
JavaScript
import React from 'react';
import PropTypes from 'prop-types';
import Row from './Row';
import DefaultRowsContainer from './RowsContainer';
import cellMetaDataShape from 'common/prop-shapes/CellActionShape';
import * as rowUtils from './RowUtils';
import RowGroup from './RowGroup';
import { InteractionMasks } from './masks';
import { getColumnScrollPosition } from './utils/canvasUtils';
import { isFunction } from 'common/utils';
import { EventTypes } from 'common/constants';
require('../../../themes/react-data-grid-core.css');
class Canvas extends React.PureComponent {
static displayName = 'Canvas';
static propTypes = {
rowRenderer: PropTypes.oneOfType([PropTypes.func, PropTypes.element]),
rowHeight: PropTypes.number.isRequired,
height: PropTypes.number.isRequired,
width: PropTypes.number,
totalWidth: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
style: PropTypes.string,
className: PropTypes.string,
rowOverscanStartIdx: PropTypes.number.isRequired,
rowOverscanEndIdx: PropTypes.number.isRequired,
rowVisibleStartIdx: PropTypes.number.isRequired,
rowVisibleEndIdx: PropTypes.number.isRequired,
colVisibleStartIdx: PropTypes.number.isRequired,
colVisibleEndIdx: PropTypes.number.isRequired,
colOverscanStartIdx: PropTypes.number.isRequired,
colOverscanEndIdx: PropTypes.number.isRequired,
rowsCount: PropTypes.number.isRequired,
rowGetter: PropTypes.oneOfType([
PropTypes.func.isRequired,
PropTypes.array.isRequired
]),
expandedRows: PropTypes.array,
onRows: PropTypes.func,
onScroll: PropTypes.func,
columns: PropTypes.oneOfType([PropTypes.object, PropTypes.array]).isRequired,
cellMetaData: PropTypes.shape(cellMetaDataShape).isRequired,
selectedRows: PropTypes.array,
rowKey: PropTypes.string,
rowScrollTimeout: PropTypes.number,
scrollToRowIndex: PropTypes.number,
contextMenu: PropTypes.element,
getSubRowDetails: PropTypes.func,
rowSelection: PropTypes.oneOfType([
PropTypes.shape({
indexes: PropTypes.arrayOf(PropTypes.number).isRequired
}),
PropTypes.shape({
isSelectedKey: PropTypes.string.isRequired
}),
PropTypes.shape({
keys: PropTypes.shape({
values: PropTypes.array.isRequired,
rowKey: PropTypes.string.isRequired
}).isRequired
})
]),
rowGroupRenderer: PropTypes.func,
isScrolling: PropTypes.bool,
length: PropTypes.number,
enableCellSelect: PropTypes.bool.isRequired,
enableCellAutoFocus: PropTypes.bool.isRequired,
cellNavigationMode: PropTypes.string.isRequired,
eventBus: PropTypes.object.isRequired,
onCheckCellIsEditable: PropTypes.func,
onCellCopyPaste: PropTypes.func,
onGridRowsUpdated: PropTypes.func.isRequired,
onDragHandleDoubleClick: PropTypes.func.isRequired,
onCellSelected: PropTypes.func,
onCellDeSelected: PropTypes.func,
onCellRangeSelectionStarted: PropTypes.func,
onCellRangeSelectionUpdated: PropTypes.func,
onCellRangeSelectionCompleted: PropTypes.func,
onCommit: PropTypes.func.isRequired,
editorPortalTarget: PropTypes.instanceOf(Element).isRequired
};
static defaultProps = {
onRows: () => { },
selectedRows: [],
rowScrollTimeout: 0,
scrollToRowIndex: 0,
RowsContainer: DefaultRowsContainer
};
state = {
scrollingTimeout: null
};
rows = [];
_currentRowsRange = { start: 0, end: 0 };
_scroll = { scrollTop: 0, scrollLeft: 0 };
componentDidMount() {
this.unsubscribeScrollToColumn = this.props.eventBus.subscribe(EventTypes.SCROLL_TO_COLUMN, this.scrollToColumn);
this.onRows();
}
componentWillUnmount() {
this._currentRowsRange = { start: 0, end: 0 };
this._scroll = { scrollTop: 0, scrollLeft: 0 };
this.rows = [];
this.unsubscribeScrollToColumn();
}
componentDidUpdate(prevProps) {
const { scrollToRowIndex } = this.props;
if (prevProps.scrollToRowIndex !== scrollToRowIndex && scrollToRowIndex !== 0) {
this.scrollToRow(scrollToRowIndex);
}
this.onRows();
}
onRows = () => {
if (this._currentRowsRange !== { start: 0, end: 0 }) {
this.props.onRows(this._currentRowsRange);
this._currentRowsRange = { start: 0, end: 0 };
}
};
scrollToRow = (scrollToRowIndex) => {
const { rowHeight, rowsCount, height } = this.props;
this.canvas.scrollTop = Math.min(
scrollToRowIndex * rowHeight,
rowsCount * rowHeight - height
);
};
onScroll = (e) => {
if (this.canvas !== e.target) {
return;
}
const { scrollLeft, scrollTop } = e.target;
const scroll = { scrollTop, scrollLeft };
this._scroll = scroll;
this.props.onScroll(scroll);
};
getClientScrollTopOffset = (node) => {
const { rowHeight } = this.props;
const scrollVariation = node.scrollTop % rowHeight;
return scrollVariation > 0 ? rowHeight - scrollVariation : 0;
}
onHitBottomCanvas = () => {
const { rowHeight } = this.props;
const node = this.canvas;
node.scrollTop += rowHeight + this.getClientScrollTopOffset(node);
}
onHitTopCanvas = () => {
const { rowHeight } = this.props;
const node = this.canvas;
node.scrollTop -= (rowHeight - this.getClientScrollTopOffset(node));
}
scrollToColumn = (idx) => {
const { scrollLeft, clientWidth } = this.canvas;
const newScrollLeft = getColumnScrollPosition(this.props.columns, idx, scrollLeft, clientWidth);
if (newScrollLeft != null) {
this.canvas.scrollLeft = scrollLeft + newScrollLeft;
}
}
onHitLeftCanvas = ({ idx }) => {
this.scrollToColumn(idx);
}
onHitRightCanvas = ({ idx }) => {
this.scrollToColumn(idx);
}
getRows = (rowOverscanStartIdx, rowOverscanEndIdx) => {
this._currentRowsRange = { start: rowOverscanStartIdx, end: rowOverscanEndIdx };
if (Array.isArray(this.props.rowGetter)) {
return this.props.rowGetter.slice(rowOverscanStartIdx, rowOverscanEndIdx);
}
const rows = [];
let i = rowOverscanStartIdx;
while (i < rowOverscanEndIdx) {
const row = this.props.rowGetter(i);
let subRowDetails = {};
if (this.props.getSubRowDetails) {
subRowDetails = this.props.getSubRowDetails(row);
}
rows.push({ row, subRowDetails });
i++;
}
return rows;
};
getScroll = () => {
const { scrollTop, scrollLeft } = this.canvas;
return { scrollTop, scrollLeft };
};
isRowSelected = (idx, row) => {
// Use selectedRows if set
if (this.props.selectedRows !== null) {
const selectedRows = this.props.selectedRows.filter(r => {
const rowKeyValue = row.get ? row.get(this.props.rowKey) : row[this.props.rowKey];
return r[this.props.rowKey] === rowKeyValue;
});
return selectedRows.length > 0 && selectedRows[0].isSelected;
}
// Else use new rowSelection props
if (this.props.rowSelection) {
const { keys, indexes, isSelectedKey } = this.props.rowSelection;
return rowUtils.isRowSelected(keys, indexes, isSelectedKey, row, idx);
}
return false;
};
setScrollLeft = (scrollLeft) => {
if (this.interactionMasks && this.interactionMasks.setScrollLeft) {
this.interactionMasks.setScrollLeft(scrollLeft);
}
this.rows.forEach((r, idx) => {
if (r) {
const row = this.getRowByRef(idx);
if (row && row.setScrollLeft) {
row.setScrollLeft(scrollLeft);
}
}
});
};
getRowByRef = (i) => {
// check if wrapped with React DND drop target
const wrappedRow = this.rows[i] && this.rows[i].getDecoratedComponentInstance ? this.rows[i].getDecoratedComponentInstance(i) : null;
if (wrappedRow) {
return wrappedRow.row;
}
return this.rows[i];
};
getRowTop = (rowIdx) => {
const row = this.getRowByRef(rowIdx);
if (row && isFunction(row.getRowTop)) {
return row.getRowTop();
}
return this.props.rowHeight * rowIdx;
};
getRowHeight = (rowIdx) => {
const row = this.getRowByRef(rowIdx);
if (row && isFunction(row.getRowHeight)) {
return row.getRowHeight();
}
return this.props.rowHeight;
};
getRowColumns = (rowIdx) => {
const row = this.getRowByRef(rowIdx);
return row && row.props ? row.props.columns : this.props.columns;
};
setCanvasRef = el => {
this.canvas = el;
};
setRowRef = idx => row => {
this.rows[idx] = row;
};
setInteractionMasksRef = el => {
this.interactionMasks = el;
};
renderCustomRowRenderer(props) {
const { ref, ...otherProps } = props;
const CustomRowRenderer = this.props.rowRenderer;
const customRowRendererProps = { ...otherProps, renderBaseRow: (p) => <Row ref={ref} {...p} /> };
if (CustomRowRenderer.type === Row) {
// In the case where Row is specified as the custom render, ensure the correct ref is passed
return <Row {...props} />;
}
if (isFunction(CustomRowRenderer)) {
return <CustomRowRenderer {...customRowRendererProps} />;
}
if (React.isValidElement(CustomRowRenderer)) {
return React.cloneElement(CustomRowRenderer, customRowRendererProps);
}
}
renderGroupRow(props) {
const { ref, ...rowGroupProps } = props;
return (
<RowGroup
{...rowGroupProps}
{...props.row.__metaData}
rowRef={props.ref}
name={props.row.name}
eventBus={this.props.eventBus}
renderer={this.props.rowGroupRenderer}
renderBaseRow={(p) => <Row ref={ref} {...p} />}
/>
);
}
renderRow = (props) => {
const { row } = props;
if (row.__metaData && row.__metaData.getRowRenderer) {
return row.__metaData.getRowRenderer(this.props, props.idx);
}
if (row.__metaData && row.__metaData.isGroup) {
return this.renderGroupRow(props);
}
if (this.props.rowRenderer) {
return this.renderCustomRowRenderer(props);
}
return <Row {...props} />;
};
renderPlaceholder = (key, height) => {
// just renders empty cells
// if we wanted to show gridlines, we'd need classes and position as with renderScrollingPlaceholder
return (
<div key={key} style={{ height }}>
{
this.props.columns.map(
(column, idx) => <div style={{ width: column.width }} key={idx} />
)
}
</div >
);
};
render() {
const { rowOverscanStartIdx, rowOverscanEndIdx, cellMetaData, columns, colOverscanStartIdx, colOverscanEndIdx, colVisibleStartIdx, colVisibleEndIdx, lastFrozenColumnIndex, expandedRows, rowHeight, rowsCount, totalColumnWidth, totalWidth, height, rowGetter, RowsContainer, contextMenu } = this.props;
const rows = this.getRows(rowOverscanStartIdx, rowOverscanEndIdx)
.map((r, idx) => {
const rowIdx = rowOverscanStartIdx + idx;
const key = `row-${rowIdx}`;
return r.row && this.renderRow({
key,
ref: this.setRowRef(rowIdx),
idx: rowIdx,
rowVisibleStartIdx: this.props.rowVisibleStartIdx,
rowVisibleEndIdx: this.props.rowVisibleEndIdx,
row: r.row,
height: rowHeight,
onMouseOver: this.onMouseOver,
columns,
isSelected: this.isRowSelected(rowIdx, r.row, rowOverscanStartIdx, rowOverscanEndIdx),
expandedRows,
cellMetaData,
subRowDetails: r.subRowDetails,
colVisibleStartIdx,
colVisibleEndIdx,
colOverscanStartIdx,
colOverscanEndIdx,
lastFrozenColumnIndex,
isScrolling: this.props.isScrolling,
scrollLeft: this._scroll.scrollLeft
});
});
if (rowOverscanStartIdx > 0) {
rows.unshift(this.renderPlaceholder('top', rowOverscanStartIdx * rowHeight));
}
if (rowsCount - rowOverscanEndIdx > 0) {
rows.push(
this.renderPlaceholder('bottom', (rowsCount - rowOverscanEndIdx) * rowHeight));
}
const style = {
position: 'absolute',
top: 0,
left: 0,
overflowX: 'auto',
overflowY: 'scroll',
width: totalWidth,
height
};
return (
<div
ref={this.setCanvasRef}
style={style}
onScroll={this.onScroll}
className="react-grid-Canvas">
<InteractionMasks
ref={this.setInteractionMasksRef}
rowGetter={rowGetter}
rowsCount={rowsCount}
width={this.props.totalWidth}
height={height}
rowHeight={rowHeight}
columns={columns}
rowOverscanStartIdx={this.props.rowOverscanStartIdx}
rowVisibleStartIdx={this.props.rowVisibleStartIdx}
rowVisibleEndIdx={this.props.rowVisibleEndIdx}
colVisibleStartIdx={colVisibleStartIdx}
colVisibleEndIdx={colVisibleEndIdx}
enableCellSelect={this.props.enableCellSelect}
enableCellAutoFocus={this.props.enableCellAutoFocus}
cellNavigationMode={this.props.cellNavigationMode}
eventBus={this.props.eventBus}
contextMenu={this.props.contextMenu}
onHitBottomBoundary={this.onHitBottomCanvas}
onHitTopBoundary={this.onHitTopCanvas}
onHitLeftBoundary={this.onHitLeftCanvas}
onHitRightBoundary={this.onHitRightCanvas}
onCommit={this.props.onCommit}
onCheckCellIsEditable={this.props.onCheckCellIsEditable}
onCellCopyPaste={this.props.onCellCopyPaste}
onGridRowsUpdated={this.props.onGridRowsUpdated}
onDragHandleDoubleClick={this.props.onDragHandleDoubleClick}
onCellSelected={this.props.onCellSelected}
onCellDeSelected={this.props.onCellDeSelected}
onCellRangeSelectionStarted={this.props.onCellRangeSelectionStarted}
onCellRangeSelectionUpdated={this.props.onCellRangeSelectionUpdated}
onCellRangeSelectionCompleted={this.props.onCellRangeSelectionCompleted}
scrollLeft={this._scroll.scrollLeft}
scrollTop={this._scroll.scrollTop}
getRowHeight={this.getRowHeight}
getRowTop={this.getRowTop}
getRowColumns={this.getRowColumns}
editorPortalTarget={this.props.editorPortalTarget}
/>
<RowsContainer id={contextMenu ? contextMenu.props.id : 'rowsContainer'}>
<div style={{ width: totalColumnWidth }}>{rows}</div>
</RowsContainer>
</div>
);
}
}
module.exports = Canvas;