UNPKG

react-flexigrid

Version:

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

280 lines (242 loc) 8.92 kB
import PrefixIntervalTree from './struct/PrefixIntervalTree' import { clamp } from './utils' const BUFFER_ROWS = 5 const NO_ROWS_SCROLL_RESULT = { index: 0, offset: 0, position: 0, contentHeight: 0, } export default class FlexiGridVerticalScrollHelper { constructor( rowCount: Number, defaultRowHeight: Number, viewportHeight: Number, rowHeightGetter: ?Function, defaultSubRowHeight = 0, subRowHeightGetter: ?Function, ) { const defaultFullRowHeight = defaultRowHeight + defaultSubRowHeight this.rowOffsets = PrefixIntervalTree.uniform(rowCount, defaultFullRowHeight) this.cachedHeights = [] for (let i = 0; i < rowCount; i += 1) { this.cachedHeights[i] = defaultFullRowHeight } this.position = 0 this.rowCount = rowCount this.viewportHeight = viewportHeight this.contentHeight = rowCount * defaultFullRowHeight this.rowHeightGetter = rowHeightGetter this.subRowHeightGetter = subRowHeightGetter this.fullRowHeightGetter = (rowIndex) => { const rowHeight = this.rowHeightGetter ? this.rowHeightGetter(rowIndex) : defaultRowHeight const subRowHeight = this.subRowHeightGetter ? this.subRowHeightGetter(rowIndex) : defaultSubRowHeight return rowHeight + subRowHeight } this.updateHeightsInViewport(0, 0) } setRowHeightGetter(rowHeightGetter: Function) { this.rowHeightGetter = rowHeightGetter } setSubRowHeightGetter(subRowHeightGetter: Function) { this.subRowHeightGetter = subRowHeightGetter } setViewportHeight(viewportHeight: Number) { this.viewportHeight = viewportHeight } getContentHeight() { return this.contentHeight } updateHeightsInViewport(firstRowIndex: Number, firstRowOffset: Number) { let top = firstRowOffset let index = firstRowIndex while (top <= this.viewportHeight && index < this.rowCount) { this.updateRowHeight(index) top += this.cachedHeights[index] index += 1 } } updateHeightsAboveViewport(firstRowIndex: Number) { let index = firstRowIndex - 1 const endRowIndex = firstRowIndex - BUFFER_ROWS while (index >= 0 && index >= endRowIndex) { const delta = this.updateRowHeight(index) this.position += delta index -= 1 } } updateRowHeight(rowIndex: Number) { if (rowIndex < 0 || rowIndex >= this.rowCount) { return 0 } const oldHeight = this.cachedHeights[rowIndex] const newHeight = this.fullRowHeightGetter(rowIndex) if (newHeight !== oldHeight) { const delta = newHeight - oldHeight this.rowOffsets.set(rowIndex, newHeight) this.cachedHeights[rowIndex] = newHeight this.contentHeight += delta return delta } return 0 } getRowPosition = (rowIndex: Number) => { this.updateRowHeight(rowIndex) return this.rowOffsets.sumUntil(rowIndex) } getRowAtEndPosition(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. // 获取 rowIndex 为可视范围的最后一行时的 position this.updateRowHeight(rowIndex) let currentRowIndex = rowIndex let sum = this.cachedHeights[currentRowIndex] while (sum < this.viewportHeight && currentRowIndex >= 0) { currentRowIndex -= 1 if (currentRowIndex >= 0) { this.updateRowHeight(currentRowIndex) sum += this.cachedHeights[currentRowIndex] } } let position = this.rowOffsets.sumTo(rowIndex) - this.viewportHeight if (position < 0) { position = 0 } return position } scrollTo(position) { 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. const rowIndex = this.rowCount - 1 position = this.getRowAtEndPosition(rowIndex) // eslint-disable-line } this.position = position let firstRowIndex = this.rowOffsets.greatestLowerBound(position) firstRowIndex = clamp(firstRowIndex, 0, Math.max(this.rowCount - 1, 0)) const firstRowPosition = this.rowOffsets.sumUntil(firstRowIndex) const firstRowOffset = firstRowPosition - position this.updateHeightsInViewport(firstRowIndex, firstRowOffset) this.updateHeightsAboveViewport(firstRowIndex) return { index: firstRowIndex, offset: firstRowOffset, position: this.position, contentHeight: this.contentHeight, } } scrollBy(delta) { if (this.rowCount === 0) { return NO_ROWS_SCROLL_RESULT } let firstRow = this.rowOffsets.greatestLowerBound(this.position) firstRow = clamp(firstRow, 0, Math.max(this.rowCount - 1, 0)) let firstRowPosition = this.rowOffsets.sumUntil(firstRow) let rowIndex = firstRow let position = this.position const rowHeightChange = this.updateRowHeight(rowIndex) if (firstRowPosition !== 0) { position += rowHeightChange } let visibleRowHeight = this.cachedHeights[rowIndex] - (position - firstRowPosition) if (delta >= 0) { while (delta > 0 && rowIndex < this.rowCount) { if (delta < visibleRowHeight) { position += delta delta = 0 // eslint-disable-line } else { delta -= visibleRowHeight // eslint-disable-line position += visibleRowHeight rowIndex += 1 } if (rowIndex < this.rowCount) { this.updateRowHeight(rowIndex) visibleRowHeight = this.cachedHeights[rowIndex] } } } else if (delta < 0) { delta = -delta // eslint-disable-line let invisibleRowHeight = this.cachedHeights[rowIndex] - visibleRowHeight while (delta > 0 && rowIndex >= 0) { if (delta < invisibleRowHeight) { position -= delta delta = 0 // eslint-disable-line } else { position -= invisibleRowHeight delta -= invisibleRowHeight // eslint-disable-line rowIndex -= 1 } if (rowIndex >= 0) { const change = this.updateRowHeight(rowIndex) invisibleRowHeight = this.cachedHeights[rowIndex] position += change } } } const maxPosition = this.contentHeight - this.viewportHeight position = clamp(position, 0, maxPosition) this.position = position let firstRowIndex = this.rowOffsets.greatestLowerBound(position) firstRowIndex = clamp(firstRowIndex, 0, Math.max(this.rowCount - 1, 0)) firstRowPosition = this.rowOffsets.sumUntil(firstRowIndex) const 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 */ scrollToRow(rowIndex, offset) { rowIndex = clamp(rowIndex, 0, Math.max(this.rowCount - 1, 0)) // eslint-disable-line offset = clamp(offset, -this.cachedHeights[rowIndex], 0) // eslint-disable-line const 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. */ scrollRowIntoView(rowIndex) { rowIndex = clamp(rowIndex, 0, Math.max(this.rowCount - 1, 0)) // eslint-disable-line this.updateRowHeight(rowIndex) const rowBegin = this.rowOffsets.sumUntil(rowIndex) const rowEnd = rowBegin + this.cachedHeights[rowIndex] if (rowBegin < this.position) { return this.scrollTo(rowBegin) } else if (this.position + this.viewportHeight < rowEnd) { const position = this.getRowAtEndPosition(rowIndex) return this.scrollTo(position) } return this.scrollTo(this.position) } }