This commit is contained in:
317
node_modules/react-window/src/VariableSizeList.js
generated
vendored
Normal file
317
node_modules/react-window/src/VariableSizeList.js
generated
vendored
Normal file
@@ -0,0 +1,317 @@
|
||||
// @flow
|
||||
|
||||
import createListComponent from './createListComponent';
|
||||
|
||||
import type { Props, ScrollToAlign } from './createListComponent';
|
||||
|
||||
const DEFAULT_ESTIMATED_ITEM_SIZE = 50;
|
||||
|
||||
type VariableSizeProps = {|
|
||||
estimatedItemSize: number,
|
||||
...Props<any>,
|
||||
|};
|
||||
|
||||
type itemSizeGetter = (index: number) => number;
|
||||
|
||||
type ItemMetadata = {|
|
||||
offset: number,
|
||||
size: number,
|
||||
|};
|
||||
type InstanceProps = {|
|
||||
itemMetadataMap: { [index: number]: ItemMetadata },
|
||||
estimatedItemSize: number,
|
||||
lastMeasuredIndex: number,
|
||||
|};
|
||||
|
||||
const getItemMetadata = (
|
||||
props: Props<any>,
|
||||
index: number,
|
||||
instanceProps: InstanceProps
|
||||
): ItemMetadata => {
|
||||
const { itemSize } = ((props: any): VariableSizeProps);
|
||||
const { itemMetadataMap, lastMeasuredIndex } = instanceProps;
|
||||
|
||||
if (index > lastMeasuredIndex) {
|
||||
let offset = 0;
|
||||
if (lastMeasuredIndex >= 0) {
|
||||
const itemMetadata = itemMetadataMap[lastMeasuredIndex];
|
||||
offset = itemMetadata.offset + itemMetadata.size;
|
||||
}
|
||||
|
||||
for (let i = lastMeasuredIndex + 1; i <= index; i++) {
|
||||
let size = ((itemSize: any): itemSizeGetter)(i);
|
||||
|
||||
itemMetadataMap[i] = {
|
||||
offset,
|
||||
size,
|
||||
};
|
||||
|
||||
offset += size;
|
||||
}
|
||||
|
||||
instanceProps.lastMeasuredIndex = index;
|
||||
}
|
||||
|
||||
return itemMetadataMap[index];
|
||||
};
|
||||
|
||||
const findNearestItem = (
|
||||
props: Props<any>,
|
||||
instanceProps: InstanceProps,
|
||||
offset: number
|
||||
) => {
|
||||
const { itemMetadataMap, lastMeasuredIndex } = instanceProps;
|
||||
|
||||
const lastMeasuredItemOffset =
|
||||
lastMeasuredIndex > 0 ? itemMetadataMap[lastMeasuredIndex].offset : 0;
|
||||
|
||||
if (lastMeasuredItemOffset >= offset) {
|
||||
// If we've already measured items within this range just use a binary search as it's faster.
|
||||
return findNearestItemBinarySearch(
|
||||
props,
|
||||
instanceProps,
|
||||
lastMeasuredIndex,
|
||||
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 findNearestItemExponentialSearch(
|
||||
props,
|
||||
instanceProps,
|
||||
Math.max(0, lastMeasuredIndex),
|
||||
offset
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const findNearestItemBinarySearch = (
|
||||
props: Props<any>,
|
||||
instanceProps: InstanceProps,
|
||||
high: number,
|
||||
low: number,
|
||||
offset: number
|
||||
): number => {
|
||||
while (low <= high) {
|
||||
const middle = low + Math.floor((high - low) / 2);
|
||||
const currentOffset = getItemMetadata(props, middle, instanceProps).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;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
const findNearestItemExponentialSearch = (
|
||||
props: Props<any>,
|
||||
instanceProps: InstanceProps,
|
||||
index: number,
|
||||
offset: number
|
||||
): number => {
|
||||
const { itemCount } = props;
|
||||
let interval = 1;
|
||||
|
||||
while (
|
||||
index < itemCount &&
|
||||
getItemMetadata(props, index, instanceProps).offset < offset
|
||||
) {
|
||||
index += interval;
|
||||
interval *= 2;
|
||||
}
|
||||
|
||||
return findNearestItemBinarySearch(
|
||||
props,
|
||||
instanceProps,
|
||||
Math.min(index, itemCount - 1),
|
||||
Math.floor(index / 2),
|
||||
offset
|
||||
);
|
||||
};
|
||||
|
||||
const getEstimatedTotalSize = (
|
||||
{ itemCount }: Props<any>,
|
||||
{ itemMetadataMap, estimatedItemSize, lastMeasuredIndex }: InstanceProps
|
||||
) => {
|
||||
let totalSizeOfMeasuredItems = 0;
|
||||
|
||||
// Edge case check for when the number of items decreases while a scroll is in progress.
|
||||
// https://github.com/bvaughn/react-window/pull/138
|
||||
if (lastMeasuredIndex >= itemCount) {
|
||||
lastMeasuredIndex = itemCount - 1;
|
||||
}
|
||||
|
||||
if (lastMeasuredIndex >= 0) {
|
||||
const itemMetadata = itemMetadataMap[lastMeasuredIndex];
|
||||
totalSizeOfMeasuredItems = itemMetadata.offset + itemMetadata.size;
|
||||
}
|
||||
|
||||
const numUnmeasuredItems = itemCount - lastMeasuredIndex - 1;
|
||||
const totalSizeOfUnmeasuredItems = numUnmeasuredItems * estimatedItemSize;
|
||||
|
||||
return totalSizeOfMeasuredItems + totalSizeOfUnmeasuredItems;
|
||||
};
|
||||
|
||||
const VariableSizeList = createListComponent({
|
||||
getItemOffset: (
|
||||
props: Props<any>,
|
||||
index: number,
|
||||
instanceProps: InstanceProps
|
||||
): number => getItemMetadata(props, index, instanceProps).offset,
|
||||
|
||||
getItemSize: (
|
||||
props: Props<any>,
|
||||
index: number,
|
||||
instanceProps: InstanceProps
|
||||
): number => instanceProps.itemMetadataMap[index].size,
|
||||
|
||||
getEstimatedTotalSize,
|
||||
|
||||
getOffsetForIndexAndAlignment: (
|
||||
props: Props<any>,
|
||||
index: number,
|
||||
align: ScrollToAlign,
|
||||
scrollOffset: number,
|
||||
instanceProps: InstanceProps,
|
||||
scrollbarSize: number
|
||||
): number => {
|
||||
const { direction, height, layout, width } = props;
|
||||
|
||||
// TODO Deprecate direction "horizontal"
|
||||
const isHorizontal = direction === 'horizontal' || layout === 'horizontal';
|
||||
const size = (((isHorizontal ? width : height): any): number);
|
||||
const itemMetadata = getItemMetadata(props, index, instanceProps);
|
||||
|
||||
// Get estimated total size after ItemMetadata is computed,
|
||||
// To ensure it reflects actual measurements instead of just estimates.
|
||||
const estimatedTotalSize = getEstimatedTotalSize(props, instanceProps);
|
||||
|
||||
const maxOffset = Math.max(
|
||||
0,
|
||||
Math.min(estimatedTotalSize - size, itemMetadata.offset)
|
||||
);
|
||||
const minOffset = Math.max(
|
||||
0,
|
||||
itemMetadata.offset - size + itemMetadata.size + scrollbarSize
|
||||
);
|
||||
|
||||
if (align === 'smart') {
|
||||
if (
|
||||
scrollOffset >= minOffset - size &&
|
||||
scrollOffset <= maxOffset + size
|
||||
) {
|
||||
align = 'auto';
|
||||
} else {
|
||||
align = 'center';
|
||||
}
|
||||
}
|
||||
|
||||
switch (align) {
|
||||
case 'start':
|
||||
return maxOffset;
|
||||
case 'end':
|
||||
return minOffset;
|
||||
case 'center':
|
||||
return Math.round(minOffset + (maxOffset - minOffset) / 2);
|
||||
case 'auto':
|
||||
default:
|
||||
if (scrollOffset >= minOffset && scrollOffset <= maxOffset) {
|
||||
return scrollOffset;
|
||||
} else if (scrollOffset < minOffset) {
|
||||
return minOffset;
|
||||
} else {
|
||||
return maxOffset;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
getStartIndexForOffset: (
|
||||
props: Props<any>,
|
||||
offset: number,
|
||||
instanceProps: InstanceProps
|
||||
): number => findNearestItem(props, instanceProps, offset),
|
||||
|
||||
getStopIndexForStartIndex: (
|
||||
props: Props<any>,
|
||||
startIndex: number,
|
||||
scrollOffset: number,
|
||||
instanceProps: InstanceProps
|
||||
): number => {
|
||||
const { direction, height, itemCount, layout, width } = props;
|
||||
|
||||
// TODO Deprecate direction "horizontal"
|
||||
const isHorizontal = direction === 'horizontal' || layout === 'horizontal';
|
||||
const size = (((isHorizontal ? width : height): any): number);
|
||||
const itemMetadata = getItemMetadata(props, startIndex, instanceProps);
|
||||
const maxOffset = scrollOffset + size;
|
||||
|
||||
let offset = itemMetadata.offset + itemMetadata.size;
|
||||
let stopIndex = startIndex;
|
||||
|
||||
while (stopIndex < itemCount - 1 && offset < maxOffset) {
|
||||
stopIndex++;
|
||||
offset += getItemMetadata(props, stopIndex, instanceProps).size;
|
||||
}
|
||||
|
||||
return stopIndex;
|
||||
},
|
||||
|
||||
initInstanceProps(props: Props<any>, instance: any): InstanceProps {
|
||||
const { estimatedItemSize } = ((props: any): VariableSizeProps);
|
||||
|
||||
const instanceProps = {
|
||||
itemMetadataMap: {},
|
||||
estimatedItemSize: estimatedItemSize || DEFAULT_ESTIMATED_ITEM_SIZE,
|
||||
lastMeasuredIndex: -1,
|
||||
};
|
||||
|
||||
instance.resetAfterIndex = (
|
||||
index: number,
|
||||
shouldForceUpdate?: boolean = true
|
||||
) => {
|
||||
instanceProps.lastMeasuredIndex = Math.min(
|
||||
instanceProps.lastMeasuredIndex,
|
||||
index - 1
|
||||
);
|
||||
|
||||
// We could potentially optimize further by only evicting styles after this index,
|
||||
// But since styles are only cached while scrolling is in progress-
|
||||
// It seems an unnecessary optimization.
|
||||
// It's unlikely that resetAfterIndex() will be called while a user is scrolling.
|
||||
instance._getItemStyleCache(-1);
|
||||
|
||||
if (shouldForceUpdate) {
|
||||
instance.forceUpdate();
|
||||
}
|
||||
};
|
||||
|
||||
return instanceProps;
|
||||
},
|
||||
|
||||
shouldResetStyleCacheOnItemSizeChange: false,
|
||||
|
||||
validateProps: ({ itemSize }: Props<any>): void => {
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
if (typeof itemSize !== 'function') {
|
||||
throw Error(
|
||||
'An invalid "itemSize" prop has been specified. ' +
|
||||
'Value should be a function. ' +
|
||||
`"${itemSize === null ? 'null' : typeof itemSize}" was specified.`
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export default VariableSizeList;
|
||||
Reference in New Issue
Block a user