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-progress-2": "^4.4.2",
|
||||||
"react-select": "1.2.1",
|
"react-select": "1.2.1",
|
||||||
"react-textarea-autosize": "5.1.0",
|
"react-textarea-autosize": "5.1.0",
|
||||||
"react-tiny-virtual-list": "^2.2.0",
|
|
||||||
"react-transition-group": "2.9.0",
|
"react-transition-group": "2.9.0",
|
||||||
"react-visibility-sensor": "3.11.0",
|
"react-visibility-sensor": "3.11.0",
|
||||||
"redux": "^3.7.2",
|
"redux": "^3.7.2",
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
|
|
||||||
import {uncontrollable} from 'uncontrollable';
|
import {uncontrollable} from 'uncontrollable';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import VirtualList from 'react-tiny-virtual-list';
|
import VirtualList from '../utils/virtual-list';
|
||||||
import Overlay from './Overlay';
|
import Overlay from './Overlay';
|
||||||
import PopOver from './PopOver';
|
import PopOver from './PopOver';
|
||||||
import Downshift, {ControllerStateAndHelpers} from 'downshift';
|
import Downshift, {ControllerStateAndHelpers} from 'downshift';
|
||||||
|
@ -786,7 +786,7 @@ export class Select extends React.Component<SelectProps, SelectState> {
|
||||||
|
|
||||||
{filtedOptions.length ? (
|
{filtedOptions.length ? (
|
||||||
<VirtualList
|
<VirtualList
|
||||||
height={280}
|
height={filtedOptions.length > 8 ? 280 : filtedOptions.length * 35}
|
||||||
itemCount={filtedOptions.length}
|
itemCount={filtedOptions.length}
|
||||||
itemSize={35}
|
itemSize={35}
|
||||||
renderItem={({index, style}) => {
|
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