UNPKG

fixed-data-table-one.com

Version:

A React table component designed to allow presenting thousands of rows of data.

1,266 lines (1,089 loc) 49.1 kB
'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; }; /** * Copyright Schrodinger, LLC * 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 * @typechecks * @noflow */ /*eslint no-bitwise:1*/ var _React = require('./React'); var _React2 = _interopRequireDefault(_React); var _createReactClass = require('create-react-class'); var _createReactClass2 = _interopRequireDefault(_createReactClass); var _propTypes = require('prop-types'); var _propTypes2 = _interopRequireDefault(_propTypes); var _ReactComponentWithPureRenderMixin = require('./ReactComponentWithPureRenderMixin'); var _ReactComponentWithPureRenderMixin2 = _interopRequireDefault(_ReactComponentWithPureRenderMixin); var _ReactWheelHandler = require('./ReactWheelHandler'); var _ReactWheelHandler2 = _interopRequireDefault(_ReactWheelHandler); var _ReactTouchHandler = require('./ReactTouchHandler'); var _ReactTouchHandler2 = _interopRequireDefault(_ReactTouchHandler); var _Scrollbar = require('./Scrollbar'); var _Scrollbar2 = _interopRequireDefault(_Scrollbar); var _FixedDataTableBufferedRows = require('./FixedDataTableBufferedRows'); var _FixedDataTableBufferedRows2 = _interopRequireDefault(_FixedDataTableBufferedRows); var _FixedDataTableColumnResizeHandle = require('./FixedDataTableColumnResizeHandle'); var _FixedDataTableColumnResizeHandle2 = _interopRequireDefault(_FixedDataTableColumnResizeHandle); var _FixedDataTableRow = require('./FixedDataTableRow'); var _FixedDataTableRow2 = _interopRequireDefault(_FixedDataTableRow); var _FixedDataTableScrollHelper = require('./FixedDataTableScrollHelper'); var _FixedDataTableScrollHelper2 = _interopRequireDefault(_FixedDataTableScrollHelper); var _FixedDataTableWidthHelper = require('./FixedDataTableWidthHelper'); var _FixedDataTableWidthHelper2 = _interopRequireDefault(_FixedDataTableWidthHelper); var _cx = require('./cx'); var _cx2 = _interopRequireDefault(_cx); var _debounceCore = require('./debounceCore'); var _debounceCore2 = _interopRequireDefault(_debounceCore); var _emptyFunction = require('./emptyFunction'); var _emptyFunction2 = _interopRequireDefault(_emptyFunction); var _invariant = require('./invariant'); var _invariant2 = _interopRequireDefault(_invariant); var _joinClasses = require('./joinClasses'); var _joinClasses2 = _interopRequireDefault(_joinClasses); var _shallowEqual = require('./shallowEqual'); var _shallowEqual2 = _interopRequireDefault(_shallowEqual); var _FixedDataTableTranslateDOMPosition = require('./FixedDataTableTranslateDOMPosition'); var _FixedDataTableTranslateDOMPosition2 = _interopRequireDefault(_FixedDataTableTranslateDOMPosition); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } var ReactChildren = _React2.default.Children; var EMPTY_OBJECT = {}; var BORDER_HEIGHT = 1; var HEADER = 'header'; var FOOTER = 'footer'; var CELL = 'cell'; var DRAG_SCROLL_SPEED = 15; var DRAG_SCROLL_BUFFER = 100; /** * Data grid component with fixed or scrollable header and columns. * * 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 FixedDataTable = (0, _createReactClass2.default)({ displayName: 'FixedDataTable', propTypes: { /** * Pixel width of table. If all columns do not fit, * a horizontal scrollbar will appear. */ width: _propTypes2.default.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: _propTypes2.default.number, /** * Class name to be passed into parent container */ className: _propTypes2.default.string, /** * Maximum pixel height of table. If all rows do not fit, * a vertical scrollbar will appear. * * Either `height` or `maxHeight` must be specified. */ maxHeight: _propTypes2.default.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: _propTypes2.default.number, overflowX: _propTypes2.default.oneOf(['hidden', 'auto']), overflowY: _propTypes2.default.oneOf(['hidden', 'auto']), /** * Boolean flag indicating of touch scrolling should be enabled * This feature is current in beta and may have bugs */ touchScrollEnabled: _propTypes2.default.bool, /** * Hide the scrollbar but still enable scroll functionality */ showScrollbarX: _propTypes2.default.bool, showScrollbarY: _propTypes2.default.bool, /** * Callback when horizontally scrolling the grid. * * Return false to stop propagation. */ onHorizontalScroll: _propTypes2.default.func, /** * Callback when vertically scrolling the grid. * * Return false to stop propagation. */ onVerticalScroll: _propTypes2.default.func, /** * Number of rows in the table. */ rowsCount: _propTypes2.default.number.isRequired, /** * Pixel height of rows unless `rowHeightGetter` is specified and returns * different value. */ rowHeight: _propTypes2.default.number.isRequired, /** * If specified, `rowHeightGetter(index)` is called for each row and the * returned value overrides `rowHeight` for particular row. */ rowHeightGetter: _propTypes2.default.func, /** * Pixel height of sub-row unless `subRowHeightGetter` is specified and returns * different value. Defaults to 0 and no sub-row being displayed. */ subRowHeight: _propTypes2.default.number, /** * If specified, `subRowHeightGetter(index)` is called for each row and the * returned value overrides `subRowHeight` for particular row. */ subRowHeightGetter: _propTypes2.default.func, /** * The row expanded for table row. * This can either be a React element, or a function that generates * a React Element. By default, the React element passed in can expect to * receive the following props: * * ``` * props: { * rowIndex; number // (the row index) * height: number // (supplied from the Table or rowHeightGetter) * width: number // (supplied from the Table) * } * ``` * * Because you are passing in your own React element, you can feel free to * pass in whatever props you may want or need. * * If you pass in a function, you will receive the same props object as the * first argument. */ rowExpanded: _propTypes2.default.oneOfType([_propTypes2.default.element, _propTypes2.default.func]), /** * To get any additional CSS classes that should be added to a row, * `rowClassNameGetter(index)` is called. */ rowClassNameGetter: _propTypes2.default.func, /** * If specified, `rowKeyGetter(index)` is called for each row and the * returned value overrides `key` for the particular row. */ rowKeyGetter: _propTypes2.default.func, /** * Pixel height of the column group header. */ groupHeaderHeight: _propTypes2.default.number, /** * Pixel height of header. */ headerHeight: _propTypes2.default.number.isRequired, /** * Pixel height of footer. */ footerHeight: _propTypes2.default.number, /** * Value of horizontal scroll. */ scrollLeft: _propTypes2.default.number, /** * Index of column to scroll to. */ scrollToColumn: _propTypes2.default.number, /** * Value of vertical scroll. */ scrollTop: _propTypes2.default.number, /** * Index of row to scroll to. */ scrollToRow: _propTypes2.default.number, /** * Callback that is called when scrolling starts with current horizontal * and vertical scroll values. */ onScrollStart: _propTypes2.default.func, /** * Callback that is called when scrolling ends or stops with new horizontal * and vertical scroll values. */ onScrollEnd: _propTypes2.default.func, /** * If enabled scroll events will not be propagated outside of the table. */ stopScrollPropagation: _propTypes2.default.bool, /** * 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: _propTypes2.default.func, /** * Callback that is called when a row is clicked. */ onRowClick: _propTypes2.default.func, /** * Callback that is called when a row is double clicked. */ onRowDoubleClick: _propTypes2.default.func, /** * Callback that is called when a mouse-down event happens on a row. */ onRowMouseDown: _propTypes2.default.func, /** * Callback that is called when a mouse-enter event happens on a row. */ onRowMouseEnter: _propTypes2.default.func, /** * Callback that is called when a mouse-leave event happens on a row. */ onRowMouseLeave: _propTypes2.default.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, * columnKey: string, * ) * ``` */ onColumnResizeEndCallback: _propTypes2.default.func, /** * Callback that is called when reordering has been completed * and columns need to be updated. * * ``` * function( * event { * columnBefore: string|undefined, // the column before the new location of this one * columnAfter: string|undefined, // the column after the new location of this one * reorderColumn: string, // the column key that was just reordered * } * ) * ``` */ onColumnReorderEndCallback: _propTypes2.default.func, /** * Whether a column is currently being resized. */ isColumnResizing: _propTypes2.default.bool, /** * Whether columns are currently being reordered. */ isColumnReordering: _propTypes2.default.bool, /** * The number of rows outside the viewport to prerender. Defaults to roughly * half of the number of visible rows. */ bufferRowCount: _propTypes2.default.number }, getDefaultProps: function getDefaultProps() /*object*/{ return { footerHeight: 0, groupHeaderHeight: 0, headerHeight: 0, showScrollbarX: true, showScrollbarY: true, touchScrollEnabled: false, stopScrollPropagation: false }; }, componentWillMount: function componentWillMount() { var props = this.props; var viewportHeight = (props.height === undefined ? props.maxHeight : props.height) - (props.headerHeight || 0) - (props.footerHeight || 0) - (props.groupHeaderHeight || 0); this._scrollHelper = new _FixedDataTableScrollHelper2.default(props.rowsCount, props.rowHeight, viewportHeight, props.rowHeightGetter, props.subRowHeight, props.subRowHeightGetter); this._didScrollStop = (0, _debounceCore2.default)(this._didScrollStop, 200, this); this._wheelHandler = new _ReactWheelHandler2.default(this._onScroll, this._shouldHandleWheelX, this._shouldHandleWheelY, props.stopScrollPropagation); this._touchHandler = new _ReactTouchHandler2.default(this._onScroll, this._shouldHandleTouchX, this._shouldHandleTouchY, props.stopScrollPropagation); this.setState(this._calculateState(props)); }, componentWillUnmount: function componentWillUnmount() { this._wheelHandler = null; this._touchHandler = null; }, _shouldHandleTouchX: function _shouldHandleTouchX( /*number*/delta) /*boolean*/{ return this.props.touchScrollEnabled && this._shouldHandleWheelX(delta); }, _shouldHandleTouchY: function _shouldHandleTouchY( /*number*/delta) /*boolean*/{ return this.props.touchScrollEnabled && this._shouldHandleWheelY(delta); }, _shouldHandleWheelX: function _shouldHandleWheelX( /*number*/delta) /*boolean*/{ if (this.props.overflowX === 'hidden') { return false; } delta = Math.round(delta); if (delta === 0) { return false; } return delta < 0 && this.state.scrollX > 0 || delta >= 0 && this.state.scrollX < this.state.maxScrollX; }, _shouldHandleWheelY: function _shouldHandleWheelY( /*number*/delta) /*boolean*/{ if (this.props.overflowY === 'hidden' || delta === 0) { return false; } delta = Math.round(delta); if (delta === 0) { return false; } return delta < 0 && this.state.scrollY > 0 || delta >= 0 && this.state.scrollY < this.state.maxScrollY; }, _reportContentHeight: function _reportContentHeight() { var scrollContentHeight = this.state.scrollContentHeight; var reservedHeight = this.state.reservedHeight; var requiredHeight = scrollContentHeight + reservedHeight; var contentHeight; var useMaxHeight = this.props.height === undefined; if (useMaxHeight && this.props.maxHeight > requiredHeight) { contentHeight = requiredHeight; } else if (this.state.height > requiredHeight && this.props.ownerHeight) { contentHeight = Math.max(requiredHeight, this.props.ownerHeight); } else { contentHeight = this.state.height + this.state.maxScrollY; } if (contentHeight !== this._contentHeight && this.props.onContentHeightChange) { this.props.onContentHeightChange(contentHeight); } this._contentHeight = contentHeight; }, componentDidMount: function componentDidMount() { this._reportContentHeight(); }, componentWillReceiveProps: function componentWillReceiveProps( /*object*/nextProps) { var newOverflowX = nextProps.overflowX; var newOverflowY = nextProps.overflowY; // In the case of controlled scrolling, notify. if (this.props.ownerHeight !== nextProps.ownerHeight || this.props.scrollTop !== nextProps.scrollTop || this.props.scrollLeft !== nextProps.scrollLeft) { this._didScrollStart(); } this._didScrollStop(); this.setState(this._calculateState(nextProps, this.state)); }, componentDidUpdate: function componentDidUpdate() { this._reportContentHeight(); }, render: function render() /*object*/{ var state = this.state; var props = this.props; var onColumnReorder = props.onColumnReorderEndCallback ? this._onColumnReorder : null; var groupHeader; if (state.useGroupHeader) { groupHeader = _React2.default.createElement(_FixedDataTableRow2.default, { key: 'group_header', isScrolling: this._isScrolling, className: (0, _joinClasses2.default)((0, _cx2.default)('fixedDataTableLayout/header'), (0, _cx2.default)('public/fixedDataTable/header')), width: state.width, height: state.groupHeaderHeight, index: 0, zIndex: 1, offsetTop: 0, scrollLeft: state.scrollX, fixedColumns: state.groupHeaderFixedColumns, scrollableColumns: state.groupHeaderScrollableColumns, onColumnResize: this._onColumnResize, onColumnReorder: onColumnReorder, onColumnReorderMove: this._onColumnReorderMove }); } var maxScrollY = this.state.maxScrollY; var showScrollbarX = state.maxScrollX > 0 && state.overflowX !== 'hidden' && state.showScrollbarX !== false; var showScrollbarY = maxScrollY > 0 && state.overflowY !== 'hidden' && state.showScrollbarY !== false; var scrollbarXHeight = showScrollbarX ? _Scrollbar2.default.SIZE : 0; var scrollbarYHeight = state.height - scrollbarXHeight - 2 * BORDER_HEIGHT - state.footerHeight; var headerOffsetTop = state.useGroupHeader ? state.groupHeaderHeight : 0; var bodyOffsetTop = headerOffsetTop + state.headerHeight; scrollbarYHeight -= bodyOffsetTop; var bottomSectionOffset = 0; var footOffsetTop = props.maxHeight != null ? bodyOffsetTop + state.bodyHeight : bodyOffsetTop + scrollbarYHeight; var rowsContainerHeight = footOffsetTop + state.footerHeight; if (props.ownerHeight !== undefined && props.ownerHeight < state.height) { bottomSectionOffset = props.ownerHeight - state.height; footOffsetTop = Math.min(footOffsetTop, props.ownerHeight - state.footerHeight - scrollbarXHeight); scrollbarYHeight = Math.max(0, footOffsetTop - bodyOffsetTop); } var verticalScrollbar; if (showScrollbarY) { verticalScrollbar = _React2.default.createElement(_Scrollbar2.default, { size: scrollbarYHeight, contentSize: scrollbarYHeight + maxScrollY, onScroll: this._onVerticalScroll, verticalTop: bodyOffsetTop, position: state.scrollY }); } var horizontalScrollbar; if (showScrollbarX) { var scrollbarXWidth = state.width; horizontalScrollbar = _React2.default.createElement(HorizontalScrollbar, { contentSize: scrollbarXWidth + state.maxScrollX, offset: bottomSectionOffset, onScroll: this._onHorizontalScroll, position: state.scrollX, size: scrollbarXWidth }); } var dragKnob = _React2.default.createElement(_FixedDataTableColumnResizeHandle2.default, { height: state.height, initialWidth: state.columnResizingData.width || 0, minWidth: state.columnResizingData.minWidth || 0, maxWidth: state.columnResizingData.maxWidth || Number.MAX_VALUE, visible: !!state.isColumnResizing, leftOffset: state.columnResizingData.left || 0, knobHeight: state.headerHeight, initialEvent: state.columnResizingData.initialEvent, onColumnResizeEnd: props.onColumnResizeEndCallback, columnKey: state.columnResizingData.key }); var footer = null; if (state.footerHeight) { footer = _React2.default.createElement(_FixedDataTableRow2.default, { key: 'footer', isScrolling: this._isScrolling, className: (0, _joinClasses2.default)((0, _cx2.default)('fixedDataTableLayout/footer'), (0, _cx2.default)('public/fixedDataTable/footer')), width: state.width, height: state.footerHeight, index: -1, zIndex: 1, offsetTop: footOffsetTop, fixedColumns: state.footFixedColumns, scrollableColumns: state.footScrollableColumns, scrollLeft: state.scrollX }); } var rows = this._renderRows(bodyOffsetTop); var header = _React2.default.createElement(_FixedDataTableRow2.default, { key: 'header', isScrolling: this._isScrolling, className: (0, _joinClasses2.default)((0, _cx2.default)('fixedDataTableLayout/header'), (0, _cx2.default)('public/fixedDataTable/header')), width: state.width, height: state.headerHeight, index: -1, zIndex: 1, offsetTop: headerOffsetTop, scrollLeft: state.scrollX, fixedColumns: state.headFixedColumns, scrollableColumns: state.headScrollableColumns, onColumnResize: this._onColumnResize, onColumnReorder: onColumnReorder, onColumnReorderMove: this._onColumnReorderMove, onColumnReorderEnd: this._onColumnReorderEnd, isColumnReordering: !!state.isColumnReordering, columnReorderingData: state.columnReorderingData }); var topShadow; var bottomShadow; if (state.scrollY) { topShadow = _React2.default.createElement('div', { className: (0, _joinClasses2.default)((0, _cx2.default)('fixedDataTableLayout/topShadow'), (0, _cx2.default)('public/fixedDataTable/topShadow')), style: { top: bodyOffsetTop } }); } if (state.ownerHeight != null && state.ownerHeight < state.height && state.scrollContentHeight + state.reservedHeight > state.ownerHeight || state.scrollY < maxScrollY) { bottomShadow = _React2.default.createElement('div', { className: (0, _joinClasses2.default)((0, _cx2.default)('fixedDataTableLayout/bottomShadow'), (0, _cx2.default)('public/fixedDataTable/bottomShadow')), style: { top: footOffsetTop } }); } return _React2.default.createElement( 'div', { className: (0, _joinClasses2.default)(this.state.className, (0, _cx2.default)('fixedDataTableLayout/main'), (0, _cx2.default)('public/fixedDataTable/main')), onWheel: this._wheelHandler.onWheel, onTouchStart: this._touchHandler.onTouchStart, onTouchEnd: this._touchHandler.onTouchEnd, onTouchMove: this._touchHandler.onTouchMove, onTouchCancel: this._touchHandler.onTouchCancel, style: { height: state.height, width: state.width } }, _React2.default.createElement( 'div', { className: (0, _cx2.default)('fixedDataTableLayout/rowsContainer'), style: { height: rowsContainerHeight, width: state.width } }, dragKnob, groupHeader, header, rows, footer, topShadow, bottomShadow ), verticalScrollbar, horizontalScrollbar ); }, _renderRows: function _renderRows( /*number*/offsetTop) /*object*/{ var state = this.state; return _React2.default.createElement(_FixedDataTableBufferedRows2.default, { isScrolling: this._isScrolling, defaultRowHeight: state.rowHeight, firstRowIndex: state.firstRowIndex, firstRowOffset: state.firstRowOffset, fixedColumns: state.bodyFixedColumns, height: state.bodyHeight, offsetTop: offsetTop, onRowClick: state.onRowClick, onRowDoubleClick: state.onRowDoubleClick, onRowMouseDown: state.onRowMouseDown, onRowMouseEnter: state.onRowMouseEnter, onRowMouseLeave: state.onRowMouseLeave, rowClassNameGetter: state.rowClassNameGetter, rowsCount: state.rowsCount, rowGetter: state.rowGetter, rowHeightGetter: state.rowHeightGetter, subRowHeight: state.subRowHeight, subRowHeightGetter: state.subRowHeightGetter, rowExpanded: state.rowExpanded, rowKeyGetter: state.rowKeyGetter, scrollLeft: state.scrollX, scrollableColumns: state.bodyScrollableColumns, showLastRowBorder: true, width: state.width, rowPositionGetter: this._scrollHelper.getRowPosition, rowLiner: this.props.rowLiner, bufferRowCount: this.state.bufferRowCount }); }, /** * This is called when a cell that is in the header of a column has its * resizer knob clicked on. It displays the resizer and puts in the correct * location on the table. */ _onColumnResize: function _onColumnResize( /*number*/combinedWidth, /*number*/leftOffset, /*number*/cellWidth, /*?number*/cellMinWidth, /*?number*/cellMaxWidth, /*number|string*/columnKey, /*object*/event) { this.setState({ isColumnResizing: true, columnResizingData: { left: leftOffset + combinedWidth - cellWidth, width: cellWidth, minWidth: cellMinWidth, maxWidth: cellMaxWidth, initialEvent: { clientX: event.clientX, clientY: event.clientY, preventDefault: _emptyFunction2.default }, key: columnKey } }); }, _onColumnReorder: function _onColumnReorder( /*string*/columnKey, /*number*/width, /*number*/left, /*object*/event) { // No native support in IE11 for find, findIndex, or includes, so using some. var isFixed = this.state.headFixedColumns.some(function (column) { return column.props.columnKey === columnKey; }); this.setState({ isColumnReordering: true, columnReorderingData: { dragDistance: 0, isFixed: isFixed, scrollStart: this.state.scrollX, columnKey: columnKey, columnWidth: width, originalLeft: left, columnsBefore: [], columnsAfter: [] } }); }, _onColumnReorderMove: function _onColumnReorderMove( /*number*/deltaX) { //NOTE Need to clone this object when use pureRendering var reorderingData = _extends({}, this.state.columnReorderingData); reorderingData.dragDistance = deltaX; reorderingData.columnBefore = undefined; reorderingData.columnAfter = undefined; var isFixedColumn = this.state.columnReorderingData.isFixed; var scrollX = this.state.scrollX; if (!isFixedColumn) { //Relative dragX position on scroll var dragX = reorderingData.originalLeft - reorderingData.scrollStart + reorderingData.dragDistance; var fixedColumnsWidth = this.state.bodyFixedColumns.reduce(function (sum, column) { return sum + column.props.width; }, 0); var relativeWidth = this.props.width - fixedColumnsWidth; //Scroll the table left or right if we drag near the edges of the table if (dragX > relativeWidth - DRAG_SCROLL_BUFFER) { scrollX = Math.min(scrollX + DRAG_SCROLL_SPEED, this.state.maxScrollX); } else if (dragX <= DRAG_SCROLL_BUFFER) { scrollX = Math.max(scrollX - DRAG_SCROLL_SPEED, 0); } reorderingData.dragDistance += this.state.scrollX - reorderingData.scrollStart; } this.setState({ scrollX: scrollX, columnReorderingData: reorderingData }); }, _onColumnReorderEnd: function _onColumnReorderEnd( /*object*/props, /*object*/event) { var columnBefore = this.state.columnReorderingData.columnBefore; var columnAfter = this.state.columnReorderingData.columnAfter; var reorderColumn = this.state.columnReorderingData.columnKey; var cancelReorder = this.state.columnReorderingData.cancelReorder; this.setState({ isColumnReordering: false, columnReorderingData: {} }); if (cancelReorder) { return; } this.props.onColumnReorderEndCallback({ columnBefore: columnBefore, columnAfter: columnAfter, reorderColumn: reorderColumn }); var onHorizontalScroll = this.props.onHorizontalScroll; if (this.state.columnReorderingData.scrollStart !== this.state.scrollX && onHorizontalScroll) { onHorizontalScroll(this.state.scrollX); }; }, _areColumnSettingsIdentical: function _areColumnSettingsIdentical(oldColumns, newColumns) { if (oldColumns.length !== newColumns.length) { return false; } for (var index = 0; index < oldColumns.length; ++index) { if (!(0, _shallowEqual2.default)(oldColumns[index].props, newColumns[index].props)) { return false; } } return true; }, _populateColumnsAndColumnData: function _populateColumnsAndColumnData(columns, columnGroups, oldState) { var canReuseColumnSettings = false; var canReuseColumnGroupSettings = false; if (oldState && oldState.columns) { canReuseColumnSettings = this._areColumnSettingsIdentical(columns, oldState.columns); } if (oldState && oldState.columnGroups && columnGroups) { canReuseColumnGroupSettings = this._areColumnSettingsIdentical(columnGroups, oldState.columnGroups); } var columnInfo = {}; if (canReuseColumnSettings) { columnInfo.bodyFixedColumns = oldState.bodyFixedColumns; columnInfo.bodyScrollableColumns = oldState.bodyScrollableColumns; columnInfo.headFixedColumns = oldState.headFixedColumns; columnInfo.headScrollableColumns = oldState.headScrollableColumns; columnInfo.footFixedColumns = oldState.footFixedColumns; columnInfo.footScrollableColumns = oldState.footScrollableColumns; } else { var bodyColumnTypes = this._splitColumnTypes(columns); columnInfo.bodyFixedColumns = bodyColumnTypes.fixed; columnInfo.bodyScrollableColumns = bodyColumnTypes.scrollable; var headColumnTypes = this._splitColumnTypes(this._selectColumnElement(HEADER, columns)); columnInfo.headFixedColumns = headColumnTypes.fixed; columnInfo.headScrollableColumns = headColumnTypes.scrollable; var footColumnTypes = this._splitColumnTypes(this._selectColumnElement(FOOTER, columns)); columnInfo.footFixedColumns = footColumnTypes.fixed; columnInfo.footScrollableColumns = footColumnTypes.scrollable; } if (canReuseColumnGroupSettings) { columnInfo.groupHeaderFixedColumns = oldState.groupHeaderFixedColumns; columnInfo.groupHeaderScrollableColumns = oldState.groupHeaderScrollableColumns; } else { if (columnGroups) { var groupHeaderColumnTypes = this._splitColumnTypes(this._selectColumnElement(HEADER, columnGroups)); columnInfo.groupHeaderFixedColumns = groupHeaderColumnTypes.fixed; columnInfo.groupHeaderScrollableColumns = groupHeaderColumnTypes.scrollable; } } return columnInfo; }, _calculateState: function _calculateState( /*object*/props, /*?object*/oldState) /*object*/{ (0, _invariant2.default)(props.height !== undefined || props.maxHeight !== undefined, 'You must set either a height or a maxHeight'); var children = []; ReactChildren.forEach(props.children, function (child, index) { if (child == null) { return; } (0, _invariant2.default)(child.type.__TableColumnGroup__ || child.type.__TableColumn__, 'child type should be <FixedDataTableColumn /> or ' + '<FixedDataTableColumnGroup />'); children.push(child); }); // Allow room for the scrollbar, less 1px for the last column's border var adjustedWidth = props.width - _Scrollbar2.default.SIZE - _Scrollbar2.default.OFFSET; var useGroupHeader = false; if (children.length && children[0].type.__TableColumnGroup__) { useGroupHeader = true; } var scrollState; var firstRowIndex = oldState && oldState.firstRowIndex || 0; var firstRowOffset = oldState && oldState.firstRowOffset || 0; var scrollY = oldState ? oldState.scrollY : 0; var scrollX = oldState ? oldState.scrollX : 0; var lastScrollLeft = oldState ? oldState.scrollLeft : 0; if (props.scrollLeft !== undefined && props.scrollLeft !== lastScrollLeft) { scrollX = props.scrollLeft; } var groupHeaderHeight = useGroupHeader ? props.groupHeaderHeight : 0; if (oldState && (props.rowsCount !== oldState.rowsCount || props.rowHeight !== oldState.rowHeight || props.height !== oldState.height)) { // Number of rows changed, try to scroll to the row from before the // change var viewportHeight = (props.height === undefined ? props.maxHeight : props.height) - (props.headerHeight || 0) - (props.footerHeight || 0) - (props.groupHeaderHeight || 0); var oldViewportHeight = this._scrollHelper._viewportHeight; this._scrollHelper = new _FixedDataTableScrollHelper2.default(props.rowsCount, props.rowHeight, viewportHeight, props.rowHeightGetter, props.subRowHeight, props.subRowHeightGetter); scrollState = this._scrollHelper.scrollToRow(firstRowIndex, firstRowOffset); firstRowIndex = scrollState.index; firstRowOffset = scrollState.offset; scrollY = scrollState.position; } else if (oldState) { if (props.rowHeightGetter !== oldState.rowHeightGetter) { this._scrollHelper.setRowHeightGetter(props.rowHeightGetter); } if (props.subRowHeightGetter !== oldState.subRowHeightGetter) { this._scrollHelper.setSubRowHeightGetter(props.subRowHeightGetter); } } var lastScrollToRow = oldState ? oldState.scrollToRow : undefined; if (props.scrollToRow != null && (props.scrollToRow !== lastScrollToRow || viewportHeight !== oldViewportHeight)) { scrollState = this._scrollHelper.scrollRowIntoView(props.scrollToRow); firstRowIndex = scrollState.index; firstRowOffset = scrollState.offset; scrollY = scrollState.position; } var lastScrollTop = oldState ? oldState.scrollTop : undefined; if (props.scrollTop != null && props.scrollTop !== lastScrollTop) { scrollState = this._scrollHelper.scrollTo(props.scrollTop); firstRowIndex = scrollState.index; firstRowOffset = scrollState.offset; scrollY = scrollState.position; } var columnResizingData; if (props.isColumnResizing) { columnResizingData = oldState && oldState.columnResizingData; } else { columnResizingData = EMPTY_OBJECT; } var columns; var columnGroups; if (useGroupHeader) { var columnGroupSettings = _FixedDataTableWidthHelper2.default.adjustColumnGroupWidths(children, adjustedWidth); columns = columnGroupSettings.columns; columnGroups = columnGroupSettings.columnGroups; } else { columns = _FixedDataTableWidthHelper2.default.adjustColumnWidths(children, adjustedWidth); } var columnInfo = this._populateColumnsAndColumnData(columns, columnGroups, oldState); var lastScrollToColumn = oldState ? oldState.scrollToColumn : undefined; if (props.scrollToColumn !== null && props.scrollToColumn !== lastScrollToColumn) { // If selected column is a fixed column, don't scroll var fixedColumnsCount = columnInfo.bodyFixedColumns.length; if (props.scrollToColumn >= fixedColumnsCount) { var totalFixedColumnsWidth = 0; var i, column; for (i = 0; i < columnInfo.bodyFixedColumns.length; ++i) { column = columnInfo.bodyFixedColumns[i]; totalFixedColumnsWidth += column.props.width; } // Convert column index (0 indexed) to scrollable index (0 indexed) // and clamp to max scrollable index var scrollableColumnIndex = Math.min(props.scrollToColumn - fixedColumnsCount, columnInfo.bodyScrollableColumns.length - 1); // Sum width for all columns before column var previousColumnsWidth = 0; for (i = 0; i < scrollableColumnIndex; ++i) { column = columnInfo.bodyScrollableColumns[i]; previousColumnsWidth += column.props.width; } // Get width of scrollable columns in viewport var availableScrollWidth = adjustedWidth - totalFixedColumnsWidth; // Get width of specified column var selectedColumnWidth = columnInfo.bodyScrollableColumns[scrollableColumnIndex].props.width; // Must scroll at least far enough for end of column (prevColWidth + selColWidth) // to be in viewport (availableScrollWidth = viewport width) var minAcceptableScrollPosition = previousColumnsWidth + selectedColumnWidth - availableScrollWidth; // If scrolled less than minimum amount, scroll to minimum amount // so column on right of viewport if (scrollX < minAcceptableScrollPosition) { scrollX = minAcceptableScrollPosition; } // If scrolled more than previous columns, at least part of column will be offscreen to left // Scroll so column is flush with left edge of viewport if (scrollX > previousColumnsWidth) { scrollX = previousColumnsWidth; } } } var useMaxHeight = props.height === undefined; var height = Math.round(useMaxHeight ? props.maxHeight : props.height); var totalHeightReserved = props.footerHeight + props.headerHeight + groupHeaderHeight + 2 * BORDER_HEIGHT; var bodyHeight = height - totalHeightReserved; var scrollContentHeight = this._scrollHelper.getContentHeight(); var totalHeightNeeded = scrollContentHeight + totalHeightReserved; var scrollContentWidth = _FixedDataTableWidthHelper2.default.getTotalWidth(columns); var horizontalScrollbarVisible = scrollContentWidth > adjustedWidth && props.overflowX !== 'hidden' && props.showScrollbarX !== false; if (horizontalScrollbarVisible) { bodyHeight -= _Scrollbar2.default.SIZE; totalHeightNeeded += _Scrollbar2.default.SIZE; totalHeightReserved += _Scrollbar2.default.SIZE; } var maxScrollX = Math.max(0, scrollContentWidth - adjustedWidth); var maxScrollY = Math.max(0, scrollContentHeight - bodyHeight); scrollX = Math.min(scrollX, maxScrollX); scrollY = Math.min(scrollY, maxScrollY); if (!maxScrollY) { // no vertical scrollbar necessary, use the totals we tracked so we // can shrink-to-fit vertically if (useMaxHeight) { height = totalHeightNeeded; } bodyHeight = totalHeightNeeded - totalHeightReserved; } this._scrollHelper.setViewportHeight(bodyHeight); // This calculation is synonymous to Element.scrollTop var scrollTop = Math.abs(firstRowOffset - this._scrollHelper.getRowPosition(firstRowIndex)); // This case can happen when the user is completely scrolled down and resizes the viewport to be taller vertically. // This is because we set the viewport height after having calculated the rows if (scrollTop !== scrollY) { scrollTop = maxScrollY; scrollState = this._scrollHelper.scrollTo(scrollTop); firstRowIndex = scrollState.index; firstRowOffset = scrollState.offset; scrollY = scrollState.position; } // The order of elements in this object metters and bringing bodyHeight, // height or useGroupHeader to the top can break various features var newState = _extends({ isColumnResizing: oldState && oldState.isColumnResizing }, columnInfo, props, { columns: columns, columnGroups: columnGroups, columnResizingData: columnResizingData, firstRowIndex: firstRowIndex, firstRowOffset: firstRowOffset, horizontalScrollbarVisible: horizontalScrollbarVisible, maxScrollX: maxScrollX, maxScrollY: maxScrollY, reservedHeight: totalHeightReserved, scrollContentHeight: scrollContentHeight, scrollX: scrollX, scrollY: scrollY, // These properties may overwrite properties defined in // columnInfo and props bodyHeight: bodyHeight, height: height, groupHeaderHeight: groupHeaderHeight, useGroupHeader: useGroupHeader }); return newState; }, _selectColumnElement: function _selectColumnElement( /*string*/type, /*array*/columns) /*array*/{ var newColumns = []; for (var i = 0; i < columns.length; ++i) { var column = columns[i]; newColumns.push(_React2.default.cloneElement(column, { cell: type ? column.props[type] : column.props[CELL] })); } return newColumns; }, _splitColumnTypes: function _splitColumnTypes( /*array*/columns) /*object*/{ var fixedColumns = []; var scrollableColumns = []; for (var i = 0; i < columns.length; ++i) { if (columns[i].props.fixed) { fixedColumns.push(columns[i]); } else { scrollableColumns.push(columns[i]); } } return { fixed: fixedColumns, scrollable: scrollableColumns }; }, _onScroll: function _onScroll( /*number*/deltaX, /*number*/deltaY) { if (!this._isScrolling) { this._didScrollStart(); } var x = this.state.scrollX; if (Math.abs(deltaY) > Math.abs(deltaX) && this.props.overflowY !== 'hidden') { var scrollState = this._scrollHelper.scrollBy(Math.round(deltaY)); var onVerticalScroll = this.props.onVerticalScroll; if (onVerticalScroll ? onVerticalScroll(scrollState.position) : true) { var maxScrollY = Math.max(0, scrollState.contentHeight - this.state.bodyHeight); this.setState({ firstRowIndex: scrollState.index, firstRowOffset: scrollState.offset, scrollY: scrollState.position, scrollContentHeight: scrollState.contentHeight, maxScrollY: maxScrollY }); } } else if (deltaX && this.props.overflowX !== 'hidden') { x += deltaX; x = x < 0 ? 0 : x; x = x > this.state.maxScrollX ? this.state.maxScrollX : x; //NOTE (asif) This is a hacky workaround to prevent FDT from setting its internal state var onHorizontalScroll = this.props.onHorizontalScroll; if (onHorizontalScroll ? onHorizontalScroll(x) : true) { this.setState({ scrollX: x }); } } this._didScrollStop(); }, _onHorizontalScroll: function _onHorizontalScroll( /*number*/scrollPos) { if (scrollPos === this.state.scrollX) { return; } if (!this._isScrolling) { this._didScrollStart(); } var onHorizontalScroll = this.props.onHorizontalScroll; if (onHorizontalScroll ? onHorizontalScroll(scrollPos) : true) { this.setState({ scrollX: scrollPos }); } this._didScrollStop(); }, _onVerticalScroll: function _onVerticalScroll( /*number*/scrollPos) { if (scrollPos === this.state.scrollY) { return; } if (!this._isScrolling) { this._didScrollStart(); } var scrollState = this._scrollHelper.scrollTo(Math.round(scrollPos)); var onVerticalScroll = this.props.onVerticalScroll; if (onVerticalScroll ? onVerticalScroll(scrollState.position) : true) { this.setState({ firstRowIndex: scrollState.index, firstRowOffset: scrollState.offset, scrollY: scrollState.position, scrollContentHeight: scrollState.contentHeight }); this._didScrollStop(); } }, _didScrollStart: function _didScrollStart() { if (this._isScrolling) { return; } this._isScrolling = true; if (this.props.onScrollStart) { this.props.onScrollStart(this.state.scrollX, this.state.scrollY, this.state.firstRowIndex); } }, _didScrollStop: function _didScrollStop() { if (!this._isScrolling) { return; } this._isScrolling = false; this.setState({ redraw: true }); if (this.props.onScrollEnd) { this.props.onScrollEnd(this.state.scrollX, this.state.scrollY, this.state.firstRowIndex); } } }); var HorizontalScrollbar = (0, _createReactClass2.default)({ displayName: 'HorizontalScrollbar', mixins: [_ReactComponentWithPureRenderMixin2.default], propTypes: { contentSize: _propTypes2.default.number.isRequired, offset: _propTypes2.default.number.isRequired, onScroll: _propTypes2.default.func.isRequired, position: _propTypes2.default.number.isRequired, size: _propTypes2.default.number.isRequired }, componentWillMount: function componentWillMount() { this._initialRender = true; }, componentDidMount: function componentDidMount() { this._initialRender = false; }, render: function render() /*object*/{ var outerContainerStyle = { height: _Scrollbar2.default.SIZE, width: this.props.size }; var innerContainerStyle = { height: _Scrollbar2.default.SIZE, position: 'absolute', overflow: 'hidden', width: this.props.size }; (0, _FixedDataTableTranslateDOMPosition2.default)(innerContainerStyle, 0, this.props.offset, this._initialRender); return _React2.default.createElement( 'div', { className: (0, _joinClasses2.default)((0, _cx2.default)('fixedDataTableLayout/horizontalScrollbar'), (0, _cx2.default)('public/fixedDataTable/horizontalScrollbar')), style: outerContainerStyle }, _React2.default.createElement( 'div', { style: innerContainerStyle }, _React2.default.createElement(_Scrollbar2.default, _extends({}, this.props, { isOpaque: true, orientation: 'horizontal', offset: undefined })) ) ); } }); module.exports = FixedDataTable;