fixed-data-table
Version:
A React table component designed to allow presenting thousands of rows of data.
518 lines (436 loc) • 16 kB
JavaScript
/**
* Copyright (c) 2015, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule FixedDataTable.react
*/
/**
* TRANSITION SHIM
* This acts to provide an intermediate mapping from the old API to the new API
*
* Remove this entire file and replace the two lines in FixedDataTableRoot
* when ready to continue to the new API.
*/
'use strict';
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
var React = require('./React');
var createReactClass = require('create-react-class');
var ReactChildren = React.Children;
var PropTypes = require('prop-types');
// New Table API
var Table = require('./FixedDataTableNew.react');
var Column = require('./FixedDataTableColumnNew.react');
var ColumnGroup = require('./FixedDataTableColumnGroupNew.react');
// Transition Cell
var TransitionCell = require('./FixedDataTableCellTransition.react');
var NEXT_VERSION = '0.7.0';
var DOCUMENTATION_URL = 'https://fburl.com/FixedDataTable-v0.6';
var EMPTY_OBJECT = {};
/**
* Notify in console that some prop has been deprecated.
*/
var notified = {};
function notifyDeprecated(prop, reason) {
if (process.env.NODE_ENV !== 'production') {
if (!notified[prop]) {
console.warn('`' + prop + '` will be DEPRECATED in version ' + NEXT_VERSION + ' of FixedDataTable and beyond. \n' + reason + '\n' + 'Read the docs at: ' + DOCUMENTATION_URL);
notified[prop] = true;
}
}
}
/**
* Data grid component with fixed or scrollable header and columns.
*
* This is currently in a transition mode, as the new API is used.
* DEPRECATED endpoints work, but will not be supported in later versions.
*
* The layout of the data table is as follows:
*
* ```
* +---------------------------------------------------+
* | Fixed Column Group | Scrollable Column Group |
* | Header | Header |
* | | |
* +---------------------------------------------------+
* | | |
* | Fixed Header Columns | Scrollable Header Columns |
* | | |
* +-----------------------+---------------------------+
* | | |
* | Fixed Body Columns | Scrollable Body Columns |
* | | |
* +-----------------------+---------------------------+
* | | |
* | Fixed Footer Columns | Scrollable Footer Columns |
* | | |
* +-----------------------+---------------------------+
* ```
*
* - Fixed Column Group Header: These are the headers for a group
* of columns if included in the table that do not scroll
* vertically or horizontally.
*
* - Scrollable Column Group Header: The header for a group of columns
* that do not move while scrolling vertically, but move horizontally
* with the horizontal scrolling.
*
* - Fixed Header Columns: The header columns that do not move while scrolling
* vertically or horizontally.
*
* - Scrollable Header Columns: The header columns that do not move
* while scrolling vertically, but move horizontally with the horizontal
* scrolling.
*
* - Fixed Body Columns: The body columns that do not move while scrolling
* horizontally, but move vertically with the vertical scrolling.
*
* - Scrollable Body Columns: The body columns that move while scrolling
* vertically or horizontally.
*/
var TransitionTable = createReactClass({
propTypes: {
/**
* Pixel width of table. If all columns do not fit,
* a horizontal scrollbar will appear.
*/
width: PropTypes.number.isRequired,
/**
* Pixel height of table. If all rows do not fit,
* a vertical scrollbar will appear.
*
* Either `height` or `maxHeight` must be specified.
*/
height: PropTypes.number,
/**
* Maximum pixel height of table. If all rows do not fit,
* a vertical scrollbar will appear.
*
* Either `height` or `maxHeight` must be specified.
*/
maxHeight: PropTypes.number,
/**
* Pixel height of table's owner, this is used in a managed scrolling
* situation when you want to slide the table up from below the fold
* without having to constantly update the height on every scroll tick.
* Instead, vary this property on scroll. By using `ownerHeight`, we
* over-render the table while making sure the footer and horizontal
* scrollbar of the table are visible when the current space for the table
* in view is smaller than the final, over-flowing height of table. It
* allows us to avoid resizing and reflowing table when it is moving in the
* view.
*
* This is used if `ownerHeight < height` (or `maxHeight`).
*/
ownerHeight: PropTypes.number,
overflowX: PropTypes.oneOf(['hidden', 'auto']),
overflowY: PropTypes.oneOf(['hidden', 'auto']),
/**
* Number of rows in the table.
*/
rowsCount: PropTypes.number.isRequired,
/**
* Pixel height of rows unless `rowHeightGetter` is specified and returns
* different value.
*/
rowHeight: PropTypes.number.isRequired,
/**
* If specified, `rowHeightGetter(index)` is called for each row and the
* returned value overrides `rowHeight` for particular row.
*/
rowHeightGetter: PropTypes.func,
/**
* DEPRECATED
*
* To get rows to display in table, `rowGetter(index)`
* is called. `rowGetter` should be smart enough to handle async
* fetching of data and return temporary objects
* while data is being fetched.
*/
rowGetter: PropTypes.func,
/**
* To get any additional CSS classes that should be added to a row,
* `rowClassNameGetter(index)` is called.
*/
rowClassNameGetter: PropTypes.func,
/**
* Pixel height of the column group header.
*/
groupHeaderHeight: PropTypes.number,
/**
* Pixel height of header.
*/
headerHeight: PropTypes.number.isRequired,
/**
* DEPRECATED
*
* Function that is called to get the data for the header row.
* If the function returns null, the header will be set to the
* Column's label property.
*/
headerDataGetter: PropTypes.func,
/**
* Pixel height of footer.
*/
footerHeight: PropTypes.number,
/**
* DEPRECATED - use footerDataGetter instead.
* Data that will be passed to footer cell renderers.
*/
footerData: PropTypes.oneOfType([PropTypes.object, PropTypes.array]),
/**
* DEPRECATED
*
* Function that is called to get the data for the footer row.
*/
footerDataGetter: PropTypes.func,
/**
* Value of horizontal scroll.
*/
scrollLeft: PropTypes.number,
/**
* Index of column to scroll to.
*/
scrollToColumn: PropTypes.number,
/**
* Value of vertical scroll.
*/
scrollTop: PropTypes.number,
/**
* Index of row to scroll to.
*/
scrollToRow: PropTypes.number,
/**
* Callback that is called when scrolling starts with current horizontal
* and vertical scroll values.
*/
onScrollStart: PropTypes.func,
/**
* Callback that is called when scrolling ends or stops with new horizontal
* and vertical scroll values.
*/
onScrollEnd: PropTypes.func,
/**
* Callback that is called when `rowHeightGetter` returns a different height
* for a row than the `rowHeight` prop. This is necessary because initially
* table estimates heights of some parts of the content.
*/
onContentHeightChange: PropTypes.func,
/**
* Callback that is called when a row is clicked.
*/
onRowClick: PropTypes.func,
/**
* Callback that is called when a row is double clicked.
*/
onRowDoubleClick: PropTypes.func,
/**
* Callback that is called when a mouse-down event happens on a row.
*/
onRowMouseDown: PropTypes.func,
/**
* Callback that is called when a mouse-enter event happens on a row.
*/
onRowMouseEnter: PropTypes.func,
/**
* Callback that is called when a mouse-leave event happens on a row.
*/
onRowMouseLeave: PropTypes.func,
/**
* Callback that is called when resizer has been released
* and column needs to be updated.
*
* Required if the isResizable property is true on any column.
*
* ```
* function(
* newColumnWidth: number,
* dataKey: string,
* )
* ```
*/
onColumnResizeEndCallback: PropTypes.func,
/**
* Whether a column is currently being resized.
*/
isColumnResizing: PropTypes.bool
},
getInitialState: function getInitialState() {
// Throw warnings on deprecated props.
var state = {};
state.needsMigration = this._checkDeprecations();
return state;
},
_checkDeprecations: function _checkDeprecations() {
var needsMigration = false;
if (this.props.rowGetter) {
notifyDeprecated('rowGetter', 'Please use the cell API in Column to fetch data for your cells.');
// ROWGETTER??? You need to migrate.
needsMigration = true;
}
if (this.props.headerDataGetter) {
notifyDeprecated('headerDataGetter', 'Please use the header API in Column to ' + 'fetch data for your header cells.');
}
if (this.props.footerData) {
notifyDeprecated('footerData', 'Please use the footer API in Column to ' + 'fetch data for your footer cells.');
}
if (this.props.footerDataGetter) {
notifyDeprecated('footerDataGetter', 'Please use the footer API in Column to ' + 'fetch data for your footer cells.');
}
ReactChildren.forEach(this.props.children, function (child) {
if (!child || !child.props) {
return;
}
var props = child.props;
if (props.label) {
notifyDeprecated('label', 'Please use `header` instead.');
}
if (props.dataKey) {
notifyDeprecated('dataKey', 'Please use the `cell` API to pass in a dataKey');
}
if (props.cellRenderer) {
notifyDeprecated('cellRenderer', 'Please use the `cell` API to pass in a React Element instead.');
}
if (props.headerRenderer) {
notifyDeprecated('headerRenderer', 'Please use the `header` API to pass in a React Element instead.');
}
if (props.columnData) {
notifyDeprecated('columnData', 'Please pass data in through props to your header, cell or footer.');
}
if (props.groupHeaderRenderer) {
notifyDeprecated('groupHeaderRenderer', 'Please use the `header` API in ColumnGroup to ' + 'pass in a React Element instead of a function that creates one.');
}
if (props.groupHeaderData) {
notifyDeprecated('groupHeaderData', 'Please pass in any data through props to your header.');
}
});
return needsMigration;
},
// Wrapper for onRow callbacks, since we don't have rowData at that level.
_onRowAction: function _onRowAction(props, callback) {
if (!callback) {
return undefined;
}
return function (e, rowIndex) {
callback(e, rowIndex, props.rowGetter && props.rowGetter(rowIndex) || EMPTY_OBJECT);
};
},
_transformColumn: function _transformColumn(column, tableProps, key) {
var props = column.props;
if (column.type.__TableColumn__) {
// Constuct the cell to be used using the rowGetter
return React.createElement(Column, _extends({
key: 'column_' + key
}, props, {
header: React.createElement(TransitionCell, {
isHeaderCell: true,
label: props.label,
width: props.width,
dataKey: props.dataKey,
className: props.headerClassName,
columnData: props.columnData || EMPTY_OBJECT,
cellRenderer: props.headerRenderer,
headerDataGetter: tableProps.headerDataGetter
}),
columnKey: props.dataKey,
cell: React.createElement(TransitionCell, {
dataKey: props.dataKey,
className: props.cellClassName,
rowGetter: tableProps.rowGetter,
width: props.width,
columnData: props.columnData || EMPTY_OBJECT,
cellDataGetter: props.cellDataGetter,
cellRenderer: props.cellRenderer
}),
footer: React.createElement(TransitionCell, {
isFooterCell: true,
className: props.footerClassName,
dataKey: props.dataKey,
cellRenderer: props.footerRenderer,
footerDataGetter: tableProps.footerDataGetter,
footerData: tableProps.footerData || EMPTY_OBJECT
})
}));
}
},
_transformColumnGroup: function _transformColumnGroup(group, tableProps, key, labels) {
var _this = this;
var props = group.props;
var j = 0;
var columns = ReactChildren.map(props.children, function (child) {
j++;
return _this._transformColumn(child, tableProps, key + '_' + j);
});
return React.createElement(
ColumnGroup,
_extends({}, props, {
key: 'group_' + key,
header: React.createElement(TransitionCell, {
isHeaderCell: true,
label: group.props.label,
dataKey: key,
groupHeaderRenderer: props.groupHeaderRenderer,
groupHeaderLabels: labels,
groupHeaderData: props.columnGroupData || EMPTY_OBJECT
}) }),
columns
);
},
_convertedColumns: function _convertedColumns(needsMigration) {
var _this2 = this;
// If we don't need to migrate, map directly to the new API.
if (!needsMigration) {
return ReactChildren.map(this.props.children, function (child) {
if (!child) {
return null;
}
if (child.type.__TableColumn__) {
return React.createElement(Column, child.props);
}
if (child.type.__TableColumnGroup__) {
return React.createElement(ColumnGroup, child.props);
}
});
}
var tableProps = this.props;
// Otherwise, if a migration is needed, we need to transform each Column
// or ColumnGroup.
var i = 0;
return ReactChildren.map(this.props.children, function (child) {
if (!child) {
return null;
}
if (child.type.__TableColumn__) {
child = _this2._transformColumn(child, tableProps, i);
}
if (child.type.__TableColumnGroup__) {
// Since we apparently give an array of labels to groupHeaderRenderer
var labels = [];
ReactChildren.forEach(_this2.props.children, function (child) {
labels.push(child.props.label);
});
child = _this2._transformColumnGroup(child, tableProps, i, labels);
}
i++;
return child;
});
},
render: function render() {
var props = this.props;
return React.createElement(
Table,
_extends({}, props, {
onRowMouseDown: this._onRowAction(props, props.onRowMouseDown),
onRowClick: this._onRowAction(props, props.onRowClick),
onRowDoubleClick: this._onRowAction(props, props.onRowDoubleClick),
onRowMouseEnter: this._onRowAction(props, props.onRowMouseEnter),
onRowMouseLeave: this._onRowAction(props, props.onRowMouseLeave)
}),
this._convertedColumns(this.state.needsMigration)
);
}
});
module.exports = TransitionTable;