fixed-data-table-one.com
Version:
A React table component designed to allow presenting thousands of rows of data.
327 lines (294 loc) • 12.2 kB
JavaScript
/**
* 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 FixedDataTableScrollHelper
* @typechecks
*/
'use strict';
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
var _PrefixIntervalTree = require('./PrefixIntervalTree');
var _PrefixIntervalTree2 = _interopRequireDefault(_PrefixIntervalTree);
var _clamp = require('./clamp');
var _clamp2 = _interopRequireDefault(_clamp);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
var BUFFER_ROWS = 5;
var NO_ROWS_SCROLL_RESULT = {
index: 0,
offset: 0,
position: 0,
contentHeight: 0
};
var FixedDataTableScrollHelper = function () {
function FixedDataTableScrollHelper(
/*number*/rowCount,
/*number*/defaultRowHeight,
/*number*/viewportHeight,
/*?function*/rowHeightGetter) {
var _this = this;
var defaultSubRowHeight = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : 0;
var
/*?function*/subRowHeightGetter = arguments[5];
_classCallCheck(this, FixedDataTableScrollHelper);
var defaultFullRowHeight = defaultRowHeight + defaultSubRowHeight;
this._rowOffsets = _PrefixIntervalTree2.default.uniform(rowCount, defaultFullRowHeight);
this._storedHeights = new Array(rowCount);
for (var i = 0; i < rowCount; ++i) {
this._storedHeights[i] = defaultFullRowHeight;
}
this._rowCount = rowCount;
this._position = 0;
this._contentHeight = rowCount * defaultFullRowHeight;
this._rowHeightGetter = rowHeightGetter;
this._subRowHeightGetter = subRowHeightGetter;
this._fullRowHeightGetter = function (rowIdx) {
var rowHeight = _this._rowHeightGetter ? _this._rowHeightGetter(rowIdx) : defaultRowHeight;
var subRowHeight = _this._subRowHeightGetter ? _this._subRowHeightGetter(rowIdx) : defaultSubRowHeight;
return rowHeight + subRowHeight;
};
this._viewportHeight = viewportHeight;
this.scrollRowIntoView = this.scrollRowIntoView.bind(this);
this.setViewportHeight = this.setViewportHeight.bind(this);
this.scrollBy = this.scrollBy.bind(this);
this.scrollTo = this.scrollTo.bind(this);
this.scrollToRow = this.scrollToRow.bind(this);
this.setRowHeightGetter = this.setRowHeightGetter.bind(this);
this.setSubRowHeightGetter = this.setSubRowHeightGetter.bind(this);
this.getContentHeight = this.getContentHeight.bind(this);
this.getRowPosition = this.getRowPosition.bind(this);
this._updateHeightsInViewport(0, 0);
}
_createClass(FixedDataTableScrollHelper, [{
key: 'setRowHeightGetter',
value: function setRowHeightGetter( /*function*/rowHeightGetter) {
this._rowHeightGetter = rowHeightGetter;
}
}, {
key: 'setSubRowHeightGetter',
value: function setSubRowHeightGetter( /*function*/subRowHeightGetter) {
this._subRowHeightGetter = subRowHeightGetter;
}
}, {
key: 'setViewportHeight',
value: function setViewportHeight( /*number*/viewportHeight) {
this._viewportHeight = viewportHeight;
}
}, {
key: 'getContentHeight',
value: function getContentHeight() /*number*/{
return this._contentHeight;
}
}, {
key: '_updateHeightsInViewport',
value: function _updateHeightsInViewport(
/*number*/firstRowIndex,
/*number*/firstRowOffset) {
var top = firstRowOffset;
var index = firstRowIndex;
while (top <= this._viewportHeight && index < this._rowCount) {
this._updateRowHeight(index);
top += this._storedHeights[index];
index++;
}
}
}, {
key: '_updateHeightsAboveViewport',
value: function _updateHeightsAboveViewport( /*number*/firstRowIndex) {
var index = firstRowIndex - 1;
while (index >= 0 && index >= firstRowIndex - BUFFER_ROWS) {
var delta = this._updateRowHeight(index);
this._position += delta;
index--;
}
}
}, {
key: '_updateRowHeight',
value: function _updateRowHeight( /*number*/rowIndex) /*number*/{
if (rowIndex < 0 || rowIndex >= this._rowCount) {
return 0;
}
var newHeight = this._fullRowHeightGetter(rowIndex);
if (newHeight !== this._storedHeights[rowIndex]) {
var change = newHeight - this._storedHeights[rowIndex];
this._rowOffsets.set(rowIndex, newHeight);
this._storedHeights[rowIndex] = newHeight;
this._contentHeight += change;
return change;
}
return 0;
}
}, {
key: 'getRowPosition',
value: function getRowPosition( /*number*/rowIndex) /*number*/{
this._updateRowHeight(rowIndex);
return this._rowOffsets.sumUntil(rowIndex);
}
}, {
key: 'scrollBy',
value: function scrollBy( /*number*/delta) /*object*/{
if (this._rowCount === 0) {
return NO_ROWS_SCROLL_RESULT;
}
var firstRow = this._rowOffsets.greatestLowerBound(this._position);
firstRow = (0, _clamp2.default)(firstRow, 0, Math.max(this._rowCount - 1, 0));
var firstRowPosition = this._rowOffsets.sumUntil(firstRow);
var rowIndex = firstRow;
var position = this._position;
var rowHeightChange = this._updateRowHeight(rowIndex);
if (firstRowPosition !== 0) {
position += rowHeightChange;
}
var visibleRowHeight = this._storedHeights[rowIndex] - (position - firstRowPosition);
if (delta >= 0) {
while (delta > 0 && rowIndex < this._rowCount) {
if (delta < visibleRowHeight) {
position += delta;
delta = 0;
} else {
delta -= visibleRowHeight;
position += visibleRowHeight;
rowIndex++;
}
if (rowIndex < this._rowCount) {
this._updateRowHeight(rowIndex);
visibleRowHeight = this._storedHeights[rowIndex];
}
}
} else if (delta < 0) {
delta = -delta;
var invisibleRowHeight = this._storedHeights[rowIndex] - visibleRowHeight;
while (delta > 0 && rowIndex >= 0) {
if (delta < invisibleRowHeight) {
position -= delta;
delta = 0;
} else {
position -= invisibleRowHeight;
delta -= invisibleRowHeight;
rowIndex--;
}
if (rowIndex >= 0) {
var change = this._updateRowHeight(rowIndex);
invisibleRowHeight = this._storedHeights[rowIndex];
position += change;
}
}
}
var maxPosition = this._contentHeight - this._viewportHeight;
position = (0, _clamp2.default)(position, 0, maxPosition);
this._position = position;
var firstRowIndex = this._rowOffsets.greatestLowerBound(position);
firstRowIndex = (0, _clamp2.default)(firstRowIndex, 0, Math.max(this._rowCount - 1, 0));
firstRowPosition = this._rowOffsets.sumUntil(firstRowIndex);
var firstRowOffset = firstRowPosition - position;
this._updateHeightsInViewport(firstRowIndex, firstRowOffset);
this._updateHeightsAboveViewport(firstRowIndex);
return {
index: firstRowIndex,
offset: firstRowOffset,
position: this._position,
contentHeight: this._contentHeight
};
}
}, {
key: '_getRowAtEndPosition',
value: function _getRowAtEndPosition( /*number*/rowIndex) /*number*/{
// We need to update enough rows above the selected one to be sure that when
// we scroll to selected position all rows between first shown and selected
// one have most recent heights computed and will not resize
this._updateRowHeight(rowIndex);
var currentRowIndex = rowIndex;
var top = this._storedHeights[currentRowIndex];
while (top < this._viewportHeight && currentRowIndex >= 0) {
currentRowIndex--;
if (currentRowIndex >= 0) {
this._updateRowHeight(currentRowIndex);
top += this._storedHeights[currentRowIndex];
}
}
var position = this._rowOffsets.sumTo(rowIndex) - this._viewportHeight;
if (position < 0) {
position = 0;
}
return position;
}
}, {
key: 'scrollTo',
value: function scrollTo( /*number*/position) /*object*/{
if (this._rowCount === 0) {
return NO_ROWS_SCROLL_RESULT;
}
if (position <= 0) {
// If position less than or equal to 0 first row should be fully visible
// on top
this._position = 0;
this._updateHeightsInViewport(0, 0);
return {
index: 0,
offset: 0,
position: this._position,
contentHeight: this._contentHeight
};
} else if (position >= this._contentHeight - this._viewportHeight) {
// If position is equal to or greater than max scroll value, we need
// to make sure to have bottom border of last row visible.
var rowIndex = this._rowCount - 1;
position = this._getRowAtEndPosition(rowIndex);
}
this._position = position;
var firstRowIndex = this._rowOffsets.greatestLowerBound(position);
firstRowIndex = (0, _clamp2.default)(firstRowIndex, 0, Math.max(this._rowCount - 1, 0));
var firstRowPosition = this._rowOffsets.sumUntil(firstRowIndex);
var firstRowOffset = firstRowPosition - position;
this._updateHeightsInViewport(firstRowIndex, firstRowOffset);
this._updateHeightsAboveViewport(firstRowIndex);
return {
index: firstRowIndex,
offset: firstRowOffset,
position: this._position,
contentHeight: this._contentHeight
};
}
/**
* Allows to scroll to selected row with specified offset. It always
* brings that row to top of viewport with that offset
*/
}, {
key: 'scrollToRow',
value: function scrollToRow( /*number*/rowIndex, /*number*/offset) /*object*/{
rowIndex = (0, _clamp2.default)(rowIndex, 0, Math.max(this._rowCount - 1, 0));
offset = (0, _clamp2.default)(offset, -this._storedHeights[rowIndex], 0);
var firstRow = this._rowOffsets.sumUntil(rowIndex);
return this.scrollTo(firstRow - offset);
}
/**
* Allows to scroll to selected row by bringing it to viewport with minimal
* scrolling. This that if row is fully visible, scroll will not be changed.
* If top border of row is above top of viewport it will be scrolled to be
* fully visible on the top of viewport. If the bottom border of row is
* below end of viewport, it will be scrolled up to be fully visible on the
* bottom of viewport.
*/
}, {
key: 'scrollRowIntoView',
value: function scrollRowIntoView( /*number*/rowIndex) /*object*/{
rowIndex = (0, _clamp2.default)(rowIndex, 0, Math.max(this._rowCount - 1, 0));
this._updateRowHeight(rowIndex);
var rowBegin = this._rowOffsets.sumUntil(rowIndex);
var rowEnd = rowBegin + this._storedHeights[rowIndex];
if (rowBegin < this._position) {
return this.scrollTo(rowBegin);
} else if (this._position + this._viewportHeight < rowEnd) {
var position = this._getRowAtEndPosition(rowIndex);
return this.scrollTo(position);
}
return this.scrollTo(this._position);
}
}]);
return FixedDataTableScrollHelper;
}();
module.exports = FixedDataTableScrollHelper;