fork 一份 react-tiny-virtual-list 到内部支持宽度自动撑开
This commit is contained in:
parent
cbd56a798e
commit
82e6ea11ce
|
@ -66,7 +66,6 @@
|
|||
"react-progress-2": "^4.4.2",
|
||||
"react-select": "1.2.1",
|
||||
"react-textarea-autosize": "5.1.0",
|
||||
"react-tiny-virtual-list": "^2.2.0",
|
||||
"react-transition-group": "2.9.0",
|
||||
"react-visibility-sensor": "3.11.0",
|
||||
"redux": "^3.7.2",
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import {uncontrollable} from 'uncontrollable';
|
||||
import React from 'react';
|
||||
import VirtualList from 'react-tiny-virtual-list';
|
||||
import VirtualList from '../utils/virtual-list';
|
||||
import Overlay from './Overlay';
|
||||
import PopOver from './PopOver';
|
||||
import Downshift, {ControllerStateAndHelpers} from 'downshift';
|
||||
|
@ -786,7 +786,7 @@ export class Select extends React.Component<SelectProps, SelectState> {
|
|||
|
||||
{filtedOptions.length ? (
|
||||
<VirtualList
|
||||
height={280}
|
||||
height={filtedOptions.length > 8 ? 280 : filtedOptions.length * 35}
|
||||
itemCount={filtedOptions.length}
|
||||
itemSize={35}
|
||||
renderItem={({index, style}) => {
|
||||
|
|
|
@ -0,0 +1,305 @@
|
|||
/* Forked from react-virtualized 💖 */
|
||||
import {ALIGNMENT} from './constants';
|
||||
|
||||
export type ItemSizeGetter = (index: number) => number;
|
||||
export type ItemSize = number | number[] | ItemSizeGetter;
|
||||
|
||||
export interface SizeAndPosition {
|
||||
size: number;
|
||||
offset: number;
|
||||
}
|
||||
|
||||
interface SizeAndPositionData {
|
||||
[id: number]: SizeAndPosition;
|
||||
}
|
||||
|
||||
export interface Options {
|
||||
itemCount: number;
|
||||
itemSizeGetter: ItemSizeGetter;
|
||||
estimatedItemSize: number;
|
||||
}
|
||||
|
||||
export default class SizeAndPositionManager {
|
||||
private itemSizeGetter: ItemSizeGetter;
|
||||
private itemCount: number;
|
||||
private estimatedItemSize: number;
|
||||
private lastMeasuredIndex: number;
|
||||
private itemSizeAndPositionData: SizeAndPositionData;
|
||||
|
||||
constructor({itemCount, itemSizeGetter, estimatedItemSize}: Options) {
|
||||
this.itemSizeGetter = itemSizeGetter;
|
||||
this.itemCount = itemCount;
|
||||
this.estimatedItemSize = estimatedItemSize;
|
||||
|
||||
// Cache of size and position data for items, mapped by item index.
|
||||
this.itemSizeAndPositionData = {};
|
||||
|
||||
// Measurements for items up to this index can be trusted; items afterward should be estimated.
|
||||
this.lastMeasuredIndex = -1;
|
||||
}
|
||||
|
||||
updateConfig({
|
||||
itemCount,
|
||||
itemSizeGetter,
|
||||
estimatedItemSize,
|
||||
}: Partial<Options>) {
|
||||
if (itemCount != null) {
|
||||
this.itemCount = itemCount;
|
||||
}
|
||||
|
||||
if (estimatedItemSize != null) {
|
||||
this.estimatedItemSize = estimatedItemSize;
|
||||
}
|
||||
|
||||
if (itemSizeGetter != null) {
|
||||
this.itemSizeGetter = itemSizeGetter;
|
||||
}
|
||||
}
|
||||
|
||||
getLastMeasuredIndex() {
|
||||
return this.lastMeasuredIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method returns the size and position for the item at the specified index.
|
||||
* It just-in-time calculates (or used cached values) for items leading up to the index.
|
||||
*/
|
||||
getSizeAndPositionForIndex(index: number) {
|
||||
if (index < 0 || index >= this.itemCount) {
|
||||
throw Error(
|
||||
`Requested index ${index} is outside of range 0..${this.itemCount}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (index > this.lastMeasuredIndex) {
|
||||
const lastMeasuredSizeAndPosition = this.getSizeAndPositionOfLastMeasuredItem();
|
||||
let offset =
|
||||
lastMeasuredSizeAndPosition.offset + lastMeasuredSizeAndPosition.size;
|
||||
|
||||
for (let i = this.lastMeasuredIndex + 1; i <= index; i++) {
|
||||
const size = this.itemSizeGetter(i);
|
||||
|
||||
if (size == null || isNaN(size)) {
|
||||
throw Error(`Invalid size returned for index ${i} of value ${size}`);
|
||||
}
|
||||
|
||||
this.itemSizeAndPositionData[i] = {
|
||||
offset,
|
||||
size,
|
||||
};
|
||||
|
||||
offset += size;
|
||||
}
|
||||
|
||||
this.lastMeasuredIndex = index;
|
||||
}
|
||||
|
||||
return this.itemSizeAndPositionData[index];
|
||||
}
|
||||
|
||||
getSizeAndPositionOfLastMeasuredItem() {
|
||||
return this.lastMeasuredIndex >= 0
|
||||
? this.itemSizeAndPositionData[this.lastMeasuredIndex]
|
||||
: {offset: 0, size: 0};
|
||||
}
|
||||
|
||||
/**
|
||||
* Total size of all items being measured.
|
||||
* This value will be completedly estimated initially.
|
||||
* As items as measured the estimate will be updated.
|
||||
*/
|
||||
getTotalSize(): number {
|
||||
const lastMeasuredSizeAndPosition = this.getSizeAndPositionOfLastMeasuredItem();
|
||||
|
||||
return (
|
||||
lastMeasuredSizeAndPosition.offset +
|
||||
lastMeasuredSizeAndPosition.size +
|
||||
(this.itemCount - this.lastMeasuredIndex - 1) * this.estimatedItemSize
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines a new offset that ensures a certain item is visible, given the alignment.
|
||||
*
|
||||
* @param align Desired alignment within container; one of "start" (default), "center", or "end"
|
||||
* @param containerSize Size (width or height) of the container viewport
|
||||
* @return Offset to use to ensure the specified item is visible
|
||||
*/
|
||||
getUpdatedOffsetForIndex({
|
||||
align = ALIGNMENT.START,
|
||||
containerSize,
|
||||
currentOffset,
|
||||
targetIndex,
|
||||
}: {
|
||||
align: ALIGNMENT | undefined;
|
||||
containerSize: number;
|
||||
currentOffset: number;
|
||||
targetIndex: number;
|
||||
}): number {
|
||||
if (containerSize <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const datum = this.getSizeAndPositionForIndex(targetIndex);
|
||||
const maxOffset = datum.offset;
|
||||
const minOffset = maxOffset - containerSize + datum.size;
|
||||
|
||||
let idealOffset;
|
||||
|
||||
switch (align) {
|
||||
case ALIGNMENT.END:
|
||||
idealOffset = minOffset;
|
||||
break;
|
||||
case ALIGNMENT.CENTER:
|
||||
idealOffset = maxOffset - (containerSize - datum.size) / 2;
|
||||
break;
|
||||
case ALIGNMENT.START:
|
||||
idealOffset = maxOffset;
|
||||
break;
|
||||
default:
|
||||
idealOffset = Math.max(minOffset, Math.min(maxOffset, currentOffset));
|
||||
}
|
||||
|
||||
const totalSize = this.getTotalSize();
|
||||
|
||||
return Math.max(0, Math.min(totalSize - containerSize, idealOffset));
|
||||
}
|
||||
|
||||
getVisibleRange({
|
||||
containerSize,
|
||||
offset,
|
||||
overscanCount,
|
||||
}: {
|
||||
containerSize: number;
|
||||
offset: number;
|
||||
overscanCount: number;
|
||||
}): {start?: number; stop?: number} {
|
||||
const totalSize = this.getTotalSize();
|
||||
|
||||
if (totalSize === 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const maxOffset = offset + containerSize;
|
||||
let start = this.findNearestItem(offset);
|
||||
|
||||
if (typeof start === 'undefined') {
|
||||
throw Error(`Invalid offset ${offset} specified`);
|
||||
}
|
||||
|
||||
const datum = this.getSizeAndPositionForIndex(start);
|
||||
offset = datum.offset + datum.size;
|
||||
|
||||
let stop = start;
|
||||
|
||||
while (offset < maxOffset && stop < this.itemCount - 1) {
|
||||
stop++;
|
||||
offset += this.getSizeAndPositionForIndex(stop).size;
|
||||
}
|
||||
|
||||
if (overscanCount) {
|
||||
start = Math.max(0, start - overscanCount);
|
||||
stop = Math.min(stop + overscanCount, this.itemCount - 1);
|
||||
}
|
||||
|
||||
return {
|
||||
start,
|
||||
stop,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all cached values for items after the specified index.
|
||||
* This method should be called for any item that has changed its size.
|
||||
* It will not immediately perform any calculations; they'll be performed the next time getSizeAndPositionForIndex() is called.
|
||||
*/
|
||||
resetItem(index: number) {
|
||||
this.lastMeasuredIndex = Math.min(this.lastMeasuredIndex, index - 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches for the item (index) nearest the specified offset.
|
||||
*
|
||||
* If no exact match is found the next lowest item index will be returned.
|
||||
* This allows partially visible items (with offsets just before/above the fold) to be visible.
|
||||
*/
|
||||
findNearestItem(offset: number) {
|
||||
if (isNaN(offset)) {
|
||||
throw Error(`Invalid offset ${offset} specified`);
|
||||
}
|
||||
|
||||
// Our search algorithms find the nearest match at or below the specified offset.
|
||||
// So make sure the offset is at least 0 or no match will be found.
|
||||
offset = Math.max(0, offset);
|
||||
|
||||
const lastMeasuredSizeAndPosition = this.getSizeAndPositionOfLastMeasuredItem();
|
||||
const lastMeasuredIndex = Math.max(0, this.lastMeasuredIndex);
|
||||
|
||||
if (lastMeasuredSizeAndPosition.offset >= offset) {
|
||||
// If we've already measured items within this range just use a binary search as it's faster.
|
||||
return this.binarySearch({
|
||||
high: lastMeasuredIndex,
|
||||
low: 0,
|
||||
offset,
|
||||
});
|
||||
} else {
|
||||
// If we haven't yet measured this high, fallback to an exponential search with an inner binary search.
|
||||
// The exponential search avoids pre-computing sizes for the full set of items as a binary search would.
|
||||
// The overall complexity for this approach is O(log n).
|
||||
return this.exponentialSearch({
|
||||
index: lastMeasuredIndex,
|
||||
offset,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private binarySearch({
|
||||
low,
|
||||
high,
|
||||
offset,
|
||||
}: {
|
||||
low: number;
|
||||
high: number;
|
||||
offset: number;
|
||||
}) {
|
||||
let middle = 0;
|
||||
let currentOffset = 0;
|
||||
|
||||
while (low <= high) {
|
||||
middle = low + Math.floor((high - low) / 2);
|
||||
currentOffset = this.getSizeAndPositionForIndex(middle).offset;
|
||||
|
||||
if (currentOffset === offset) {
|
||||
return middle;
|
||||
} else if (currentOffset < offset) {
|
||||
low = middle + 1;
|
||||
} else if (currentOffset > offset) {
|
||||
high = middle - 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (low > 0) {
|
||||
return low - 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private exponentialSearch({index, offset}: {index: number; offset: number}) {
|
||||
let interval = 1;
|
||||
|
||||
while (
|
||||
index < this.itemCount &&
|
||||
this.getSizeAndPositionForIndex(index).offset < offset
|
||||
) {
|
||||
index += interval;
|
||||
interval *= 2;
|
||||
}
|
||||
|
||||
return this.binarySearch({
|
||||
high: Math.min(index, this.itemCount - 1),
|
||||
low: Math.floor(index / 2),
|
||||
offset,
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
export enum ALIGNMENT {
|
||||
AUTO = 'auto',
|
||||
START = 'start',
|
||||
CENTER = 'center',
|
||||
END = 'end',
|
||||
}
|
||||
|
||||
export enum DIRECTION {
|
||||
HORIZONTAL = 'horizontal',
|
||||
VERTICAL = 'vertical',
|
||||
}
|
||||
|
||||
export enum SCROLL_CHANGE_REASON {
|
||||
OBSERVED = 'observed',
|
||||
REQUESTED = 'requested',
|
||||
}
|
||||
|
||||
export const scrollProp = {
|
||||
[DIRECTION.VERTICAL]: 'scrollTop',
|
||||
[DIRECTION.HORIZONTAL]: 'scrollLeft',
|
||||
};
|
||||
|
||||
export const sizeProp = {
|
||||
[DIRECTION.VERTICAL]: 'height',
|
||||
[DIRECTION.HORIZONTAL]: 'width',
|
||||
};
|
||||
|
||||
export const positionProp = {
|
||||
[DIRECTION.VERTICAL]: 'top',
|
||||
[DIRECTION.HORIZONTAL]: 'left',
|
||||
};
|
||||
|
||||
export const marginProp = {
|
||||
[DIRECTION.VERTICAL]: 'marginTop',
|
||||
[DIRECTION.HORIZONTAL]: 'marginLeft',
|
||||
};
|
||||
|
||||
export const oppositeMarginProp = {
|
||||
[DIRECTION.VERTICAL]: 'marginBottom',
|
||||
[DIRECTION.HORIZONTAL]: 'marginRight',
|
||||
};
|
|
@ -0,0 +1,456 @@
|
|||
/**
|
||||
* 基于 https://github.com/clauderic/react-tiny-virtual-list 改造,主要是加了宽度自适应
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import {findDOMNode} from 'react-dom';
|
||||
import * as PropTypes from 'prop-types';
|
||||
import SizeAndPositionManager, {ItemSize} from './SizeAndPositionManager';
|
||||
import {
|
||||
ALIGNMENT,
|
||||
DIRECTION,
|
||||
SCROLL_CHANGE_REASON,
|
||||
marginProp,
|
||||
oppositeMarginProp,
|
||||
positionProp,
|
||||
scrollProp,
|
||||
sizeProp
|
||||
} from './constants';
|
||||
|
||||
export {DIRECTION as ScrollDirection} from './constants';
|
||||
|
||||
export type ItemPosition = 'absolute' | 'sticky';
|
||||
|
||||
export interface ItemStyle {
|
||||
position: ItemPosition;
|
||||
top?: number;
|
||||
left: number;
|
||||
width: string | number;
|
||||
height?: number;
|
||||
marginTop?: number;
|
||||
marginLeft?: number;
|
||||
marginRight?: number;
|
||||
marginBottom?: number;
|
||||
zIndex?: number;
|
||||
}
|
||||
|
||||
interface StyleCache {
|
||||
[id: number]: ItemStyle;
|
||||
}
|
||||
|
||||
export interface ItemInfo {
|
||||
index: number;
|
||||
style: ItemStyle;
|
||||
}
|
||||
|
||||
export interface RenderedRows {
|
||||
startIndex: number;
|
||||
stopIndex: number;
|
||||
}
|
||||
|
||||
export interface Props {
|
||||
className?: string;
|
||||
estimatedItemSize?: number;
|
||||
height: number | string;
|
||||
itemCount: number;
|
||||
itemSize: ItemSize;
|
||||
overscanCount?: number;
|
||||
scrollOffset?: number;
|
||||
scrollToIndex?: number;
|
||||
scrollToAlignment?: ALIGNMENT;
|
||||
scrollDirection?: DIRECTION;
|
||||
stickyIndices?: number[];
|
||||
style?: React.CSSProperties;
|
||||
width?: number | string;
|
||||
onItemsRendered?({startIndex, stopIndex}: RenderedRows): void;
|
||||
onScroll?(offset: number, event: UIEvent): void;
|
||||
renderItem(itemInfo: ItemInfo): React.ReactNode;
|
||||
}
|
||||
|
||||
export interface State {
|
||||
offset: number;
|
||||
scrollChangeReason: SCROLL_CHANGE_REASON;
|
||||
}
|
||||
|
||||
const STYLE_WRAPPER: React.CSSProperties = {
|
||||
overflow: 'auto',
|
||||
willChange: 'transform',
|
||||
WebkitOverflowScrolling: 'touch'
|
||||
};
|
||||
|
||||
const STYLE_INNER: React.CSSProperties = {
|
||||
position: 'relative',
|
||||
width: 'auto',
|
||||
whiteSpace: 'nowrap',
|
||||
minHeight: '100%'
|
||||
};
|
||||
|
||||
const STYLE_ITEM: {
|
||||
position: ItemStyle['position'];
|
||||
top: ItemStyle['top'];
|
||||
left: ItemStyle['left'];
|
||||
width: ItemStyle['width'];
|
||||
} = {
|
||||
position: 'absolute' as ItemPosition,
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%'
|
||||
};
|
||||
|
||||
const STYLE_STICKY_ITEM = {
|
||||
...STYLE_ITEM,
|
||||
position: 'sticky' as ItemPosition
|
||||
};
|
||||
|
||||
export default class VirtualList extends React.PureComponent<Props, State> {
|
||||
static defaultProps = {
|
||||
overscanCount: 3,
|
||||
scrollDirection: DIRECTION.VERTICAL,
|
||||
width: '100%'
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
estimatedItemSize: PropTypes.number,
|
||||
height: PropTypes.oneOfType([PropTypes.number, PropTypes.string])
|
||||
.isRequired,
|
||||
itemCount: PropTypes.number.isRequired,
|
||||
itemSize: PropTypes.oneOfType([
|
||||
PropTypes.number,
|
||||
PropTypes.array,
|
||||
PropTypes.func
|
||||
]).isRequired,
|
||||
onScroll: PropTypes.func,
|
||||
onItemsRendered: PropTypes.func,
|
||||
overscanCount: PropTypes.number,
|
||||
renderItem: PropTypes.func.isRequired,
|
||||
scrollOffset: PropTypes.number,
|
||||
scrollToIndex: PropTypes.number,
|
||||
scrollToAlignment: PropTypes.oneOf([
|
||||
ALIGNMENT.AUTO,
|
||||
ALIGNMENT.START,
|
||||
ALIGNMENT.CENTER,
|
||||
ALIGNMENT.END
|
||||
]),
|
||||
scrollDirection: PropTypes.oneOf([
|
||||
DIRECTION.HORIZONTAL,
|
||||
DIRECTION.VERTICAL
|
||||
]),
|
||||
stickyIndices: PropTypes.arrayOf(PropTypes.number),
|
||||
style: PropTypes.object,
|
||||
width: PropTypes.oneOfType([PropTypes.number, PropTypes.string])
|
||||
};
|
||||
|
||||
itemSizeGetter = (itemSize: Props['itemSize']) => {
|
||||
return index => this.getSize(index, itemSize);
|
||||
};
|
||||
|
||||
sizeAndPositionManager = new SizeAndPositionManager({
|
||||
itemCount: this.props.itemCount,
|
||||
itemSizeGetter: this.itemSizeGetter(this.props.itemSize),
|
||||
estimatedItemSize: this.getEstimatedItemSize()
|
||||
});
|
||||
|
||||
readonly state: State = {
|
||||
offset:
|
||||
this.props.scrollOffset ||
|
||||
(this.props.scrollToIndex != null &&
|
||||
this.getOffsetForIndex(this.props.scrollToIndex)) ||
|
||||
0,
|
||||
scrollChangeReason: SCROLL_CHANGE_REASON.REQUESTED
|
||||
};
|
||||
|
||||
private rootNode: HTMLElement;
|
||||
|
||||
private styleCache: StyleCache = {};
|
||||
|
||||
componentDidMount() {
|
||||
const {scrollOffset, scrollToIndex} = this.props;
|
||||
this.rootNode.addEventListener('scroll', this.handleScroll, {
|
||||
passive: true
|
||||
});
|
||||
this.updateRootWidth();
|
||||
if (scrollOffset != null) {
|
||||
this.scrollTo(scrollOffset);
|
||||
} else if (scrollToIndex != null) {
|
||||
this.scrollTo(this.getOffsetForIndex(scrollToIndex));
|
||||
}
|
||||
}
|
||||
|
||||
// 自适应宽度
|
||||
updateRootWidth(isDidUpdate: boolean = false) {
|
||||
let scrollbarWidth =
|
||||
window.innerWidth - document.documentElement.clientWidth || 15;
|
||||
if (isDidUpdate) {
|
||||
scrollbarWidth = 0;
|
||||
}
|
||||
const itemsDom = this.rootNode.children[0].children;
|
||||
const containerWidth = this.rootNode.parentElement!.getBoundingClientRect()
|
||||
.width;
|
||||
let maxItemWidth = 0;
|
||||
for (let i = 0; i < itemsDom.length; i++) {
|
||||
let itemWidth = itemsDom[i].getBoundingClientRect().width;
|
||||
if (itemWidth > maxItemWidth) {
|
||||
maxItemWidth = itemWidth;
|
||||
}
|
||||
}
|
||||
if (containerWidth >= maxItemWidth) {
|
||||
this.rootNode.style.width = containerWidth + 'px';
|
||||
} else {
|
||||
this.rootNode.style.width = maxItemWidth + scrollbarWidth + 'px';
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps: Props) {
|
||||
const {
|
||||
estimatedItemSize,
|
||||
itemCount,
|
||||
itemSize,
|
||||
scrollOffset,
|
||||
scrollToAlignment,
|
||||
scrollToIndex
|
||||
} = this.props;
|
||||
const scrollPropsHaveChanged =
|
||||
nextProps.scrollToIndex !== scrollToIndex ||
|
||||
nextProps.scrollToAlignment !== scrollToAlignment;
|
||||
const itemPropsHaveChanged =
|
||||
nextProps.itemCount !== itemCount ||
|
||||
nextProps.itemSize !== itemSize ||
|
||||
nextProps.estimatedItemSize !== estimatedItemSize;
|
||||
|
||||
if (nextProps.itemSize !== itemSize) {
|
||||
this.sizeAndPositionManager.updateConfig({
|
||||
itemSizeGetter: this.itemSizeGetter(nextProps.itemSize)
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
nextProps.itemCount !== itemCount ||
|
||||
nextProps.estimatedItemSize !== estimatedItemSize
|
||||
) {
|
||||
this.sizeAndPositionManager.updateConfig({
|
||||
itemCount: nextProps.itemCount,
|
||||
estimatedItemSize: this.getEstimatedItemSize(nextProps)
|
||||
});
|
||||
}
|
||||
|
||||
if (itemPropsHaveChanged) {
|
||||
this.recomputeSizes();
|
||||
}
|
||||
|
||||
if (nextProps.scrollOffset !== scrollOffset) {
|
||||
this.setState({
|
||||
offset: nextProps.scrollOffset || 0,
|
||||
scrollChangeReason: SCROLL_CHANGE_REASON.REQUESTED
|
||||
});
|
||||
} else if (
|
||||
typeof nextProps.scrollToIndex === 'number' &&
|
||||
(scrollPropsHaveChanged || itemPropsHaveChanged)
|
||||
) {
|
||||
this.setState({
|
||||
offset: this.getOffsetForIndex(
|
||||
nextProps.scrollToIndex,
|
||||
nextProps.scrollToAlignment,
|
||||
nextProps.itemCount
|
||||
),
|
||||
scrollChangeReason: SCROLL_CHANGE_REASON.REQUESTED
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(_: Props, prevState: State) {
|
||||
this.updateRootWidth(true);
|
||||
const {offset, scrollChangeReason} = this.state;
|
||||
if (
|
||||
prevState.offset !== offset &&
|
||||
scrollChangeReason === SCROLL_CHANGE_REASON.REQUESTED
|
||||
) {
|
||||
this.scrollTo(offset);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.rootNode.removeEventListener('scroll', this.handleScroll);
|
||||
}
|
||||
|
||||
scrollTo(value: number) {
|
||||
const {scrollDirection = DIRECTION.VERTICAL} = this.props;
|
||||
|
||||
this.rootNode[scrollProp[scrollDirection]] = value;
|
||||
}
|
||||
|
||||
getOffsetForIndex(
|
||||
index: number,
|
||||
scrollToAlignment = this.props.scrollToAlignment,
|
||||
itemCount: number = this.props.itemCount
|
||||
): number {
|
||||
const {scrollDirection = DIRECTION.VERTICAL} = this.props;
|
||||
|
||||
if (index < 0 || index >= itemCount) {
|
||||
index = 0;
|
||||
}
|
||||
|
||||
return this.sizeAndPositionManager.getUpdatedOffsetForIndex({
|
||||
align: scrollToAlignment,
|
||||
containerSize: this.props[sizeProp[scrollDirection]],
|
||||
currentOffset: (this.state && this.state.offset) || 0,
|
||||
targetIndex: index
|
||||
});
|
||||
}
|
||||
|
||||
recomputeSizes(startIndex = 0) {
|
||||
this.styleCache = {};
|
||||
this.sizeAndPositionManager.resetItem(startIndex);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
estimatedItemSize,
|
||||
height,
|
||||
overscanCount = 3,
|
||||
renderItem,
|
||||
itemCount,
|
||||
itemSize,
|
||||
onItemsRendered,
|
||||
onScroll,
|
||||
scrollDirection = DIRECTION.VERTICAL,
|
||||
scrollOffset,
|
||||
scrollToIndex,
|
||||
scrollToAlignment,
|
||||
stickyIndices,
|
||||
style,
|
||||
width,
|
||||
...props
|
||||
} = this.props;
|
||||
const {offset} = this.state;
|
||||
const {start, stop} = this.sizeAndPositionManager.getVisibleRange({
|
||||
containerSize: this.props[sizeProp[scrollDirection]] || 0,
|
||||
offset,
|
||||
overscanCount
|
||||
});
|
||||
const items: React.ReactNode[] = [];
|
||||
const wrapperStyle = {...STYLE_WRAPPER, ...style, height, width};
|
||||
const innerStyle = {
|
||||
...STYLE_INNER,
|
||||
[sizeProp[scrollDirection]]: this.sizeAndPositionManager.getTotalSize()
|
||||
};
|
||||
|
||||
if (stickyIndices != null && stickyIndices.length !== 0) {
|
||||
stickyIndices.forEach((index: number) =>
|
||||
items.push(
|
||||
renderItem({
|
||||
index,
|
||||
style: this.getStyle(index, true)
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
if (scrollDirection === DIRECTION.HORIZONTAL) {
|
||||
innerStyle.display = 'flex';
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof start !== 'undefined' && typeof stop !== 'undefined') {
|
||||
for (let index = start; index <= stop; index++) {
|
||||
if (stickyIndices != null && stickyIndices.includes(index)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
items.push(
|
||||
renderItem({
|
||||
index,
|
||||
style: this.getStyle(index, false)
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof onItemsRendered === 'function') {
|
||||
onItemsRendered({
|
||||
startIndex: start,
|
||||
stopIndex: stop
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={this.getRef} {...props} style={wrapperStyle}>
|
||||
<div style={innerStyle}>{items}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private getRef = (node: HTMLDivElement): void => {
|
||||
this.rootNode = node;
|
||||
};
|
||||
|
||||
private handleScroll = (event: UIEvent) => {
|
||||
const {onScroll} = this.props;
|
||||
const offset = this.getNodeOffset();
|
||||
|
||||
if (
|
||||
offset < 0 ||
|
||||
this.state.offset === offset ||
|
||||
event.target !== this.rootNode
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
offset,
|
||||
scrollChangeReason: SCROLL_CHANGE_REASON.OBSERVED
|
||||
});
|
||||
|
||||
if (typeof onScroll === 'function') {
|
||||
onScroll(offset, event);
|
||||
}
|
||||
};
|
||||
|
||||
private getNodeOffset() {
|
||||
const {scrollDirection = DIRECTION.VERTICAL} = this.props;
|
||||
|
||||
return this.rootNode[scrollProp[scrollDirection]];
|
||||
}
|
||||
|
||||
private getEstimatedItemSize(props = this.props) {
|
||||
return (
|
||||
props.estimatedItemSize ||
|
||||
(typeof props.itemSize === 'number' && props.itemSize) ||
|
||||
50
|
||||
);
|
||||
}
|
||||
|
||||
private getSize(index: number, itemSize) {
|
||||
if (typeof itemSize === 'function') {
|
||||
return itemSize(index);
|
||||
}
|
||||
|
||||
return Array.isArray(itemSize) ? itemSize[index] : itemSize;
|
||||
}
|
||||
|
||||
private getStyle(index: number, sticky: boolean) {
|
||||
const style = this.styleCache[index];
|
||||
|
||||
if (style) {
|
||||
return style;
|
||||
}
|
||||
|
||||
const {scrollDirection = DIRECTION.VERTICAL} = this.props;
|
||||
const {
|
||||
size,
|
||||
offset
|
||||
} = this.sizeAndPositionManager.getSizeAndPositionForIndex(index);
|
||||
|
||||
return (this.styleCache[index] = sticky
|
||||
? {
|
||||
...STYLE_STICKY_ITEM,
|
||||
[sizeProp[scrollDirection]]: size,
|
||||
[marginProp[scrollDirection]]: offset,
|
||||
[oppositeMarginProp[scrollDirection]]: -(offset + size),
|
||||
zIndex: 1
|
||||
}
|
||||
: {
|
||||
...STYLE_ITEM,
|
||||
[sizeProp[scrollDirection]]: size,
|
||||
[positionProp[scrollDirection]]: offset
|
||||
});
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue