@john-osullivan/react-window-dynamic-fork
Version:
Temporary Fork: see https://github.com/bvaughn/react-window
1,277 lines (1,066 loc) • 102 kB
JavaScript
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; }
var react = require('react');
var _extends = _interopDefault(require('@babel/runtime/helpers/extends'));
var _assertThisInitialized = _interopDefault(require('@babel/runtime/helpers/assertThisInitialized'));
var _inheritsLoose = _interopDefault(require('@babel/runtime/helpers/inheritsLoose'));
var memoizeOne = _interopDefault(require('memoize-one'));
var reactDom = require('react-dom');
var _objectWithoutPropertiesLoose = _interopDefault(require('@babel/runtime/helpers/objectWithoutPropertiesLoose'));
// Animation frame based implementation of setTimeout.
// Inspired by Joe Lambert, https://gist.github.com/joelambert/1002116#file-requesttimeout-js
var hasNativePerformanceNow = typeof performance === 'object' && typeof performance.now === 'function';
var now = hasNativePerformanceNow ? function () {
return performance.now();
} : function () {
return Date.now();
};
function cancelTimeout(timeoutID) {
cancelAnimationFrame(timeoutID.id);
}
function requestTimeout(callback, delay) {
var start = now();
function tick() {
if (now() - start >= delay) {
callback.call(null);
} else {
timeoutID.id = requestAnimationFrame(tick);
}
}
var timeoutID = {
id: requestAnimationFrame(tick)
};
return timeoutID;
}
var size = -1; // This utility copied from "dom-helpers" package.
function getScrollbarSize(recalculate) {
if (recalculate === void 0) {
recalculate = false;
}
if (size === -1 || recalculate) {
var div = document.createElement('div');
var style = div.style;
style.width = '50px';
style.height = '50px';
style.overflow = 'scroll';
document.body.appendChild(div);
size = div.offsetWidth - div.clientWidth;
document.body.removeChild(div);
}
return size;
}
var cachedRTLResult = null; // TRICKY According to the spec, scrollLeft should be negative for RTL aligned elements.
// Chrome does not seem to adhere; its scrollLeft values are positive (measured relative to the left).
// Safari's elastic bounce makes detecting this even more complicated wrt potential false positives.
// The safest way to check this is to intentionally set a negative offset,
// and then verify that the subsequent "scroll" event matches the negative offset.
// If it does not match, then we can assume a non-standard RTL scroll implementation.
function getRTLOffsetType(recalculate) {
if (recalculate === void 0) {
recalculate = false;
}
if (cachedRTLResult === null || recalculate) {
var outerDiv = document.createElement('div');
var outerStyle = outerDiv.style;
outerStyle.width = '50px';
outerStyle.height = '50px';
outerStyle.overflow = 'scroll';
outerStyle.direction = 'rtl';
var innerDiv = document.createElement('div');
var innerStyle = innerDiv.style;
innerStyle.width = '100px';
innerStyle.height = '100px';
outerDiv.appendChild(innerDiv);
document.body.appendChild(outerDiv);
if (outerDiv.scrollLeft > 0) {
cachedRTLResult = 'positive-descending';
} else {
outerDiv.scrollLeft = 1;
if (outerDiv.scrollLeft === 0) {
cachedRTLResult = 'negative';
} else {
cachedRTLResult = 'positive-ascending';
}
}
document.body.removeChild(outerDiv);
return cachedRTLResult;
}
return cachedRTLResult;
}
var IS_SCROLLING_DEBOUNCE_INTERVAL = 150;
var defaultItemKey = function defaultItemKey(index, data) {
return index;
}; // In DEV mode, this Set helps us only log a warning once per component instance.
// This avoids spamming the console every time a render happens.
var devWarningsDirection = null;
var devWarningsTagName = null;
if (process.env.NODE_ENV !== 'production') {
if (typeof window !== 'undefined' && typeof window.WeakSet !== 'undefined') {
devWarningsDirection =
/*#__PURE__*/
new WeakSet();
devWarningsTagName =
/*#__PURE__*/
new WeakSet();
}
}
function createListComponent(_ref) {
var _class, _temp;
var getItemOffset = _ref.getItemOffset,
getEstimatedTotalSize = _ref.getEstimatedTotalSize,
getItemSize = _ref.getItemSize,
getOffsetForIndexAndAlignment = _ref.getOffsetForIndexAndAlignment,
getStartIndexForOffset = _ref.getStartIndexForOffset,
getStopIndexForStartIndex = _ref.getStopIndexForStartIndex,
initInstanceProps = _ref.initInstanceProps,
shouldResetStyleCacheOnItemSizeChange = _ref.shouldResetStyleCacheOnItemSizeChange,
validateProps = _ref.validateProps;
return _temp = _class =
/*#__PURE__*/
function (_PureComponent) {
_inheritsLoose(List, _PureComponent);
// Always use explicit constructor for React components.
// It produces less code after transpilation. (#26)
// eslint-disable-next-line no-useless-constructor
function List(props) {
var _this;
_this = _PureComponent.call(this, props) || this;
_this._instanceProps = initInstanceProps(_this.props, _assertThisInitialized(_this));
_this._outerRef = void 0;
_this._resetIsScrollingTimeoutId = null;
_this.state = {
instance: _assertThisInitialized(_this),
isScrolling: false,
scrollDirection: 'forward',
scrollOffset: typeof _this.props.initialScrollOffset === 'number' ? _this.props.initialScrollOffset : 0,
scrollUpdateWasRequested: false
};
_this._callOnItemsRendered = void 0;
_this._callOnItemsRendered = memoizeOne(function (overscanStartIndex, overscanStopIndex, visibleStartIndex, visibleStopIndex) {
return _this.props.onItemsRendered({
overscanStartIndex: overscanStartIndex,
overscanStopIndex: overscanStopIndex,
visibleStartIndex: visibleStartIndex,
visibleStopIndex: visibleStopIndex
});
});
_this._callOnScroll = void 0;
_this._callOnScroll = memoizeOne(function (scrollDirection, scrollOffset, scrollUpdateWasRequested) {
return _this.props.onScroll({
scrollDirection: scrollDirection,
scrollOffset: scrollOffset,
scrollUpdateWasRequested: scrollUpdateWasRequested
});
});
_this._getItemStyle = void 0;
_this._getItemStyle = function (index) {
var _this$props = _this.props,
direction = _this$props.direction,
itemSize = _this$props.itemSize,
layout = _this$props.layout;
var itemStyleCache = _this._getItemStyleCache(shouldResetStyleCacheOnItemSizeChange && itemSize, shouldResetStyleCacheOnItemSizeChange && layout, shouldResetStyleCacheOnItemSizeChange && direction);
var style;
if (itemStyleCache.hasOwnProperty(index)) {
style = itemStyleCache[index];
} else {
var _style;
var _offset = getItemOffset(_this.props, index, _this._instanceProps);
var size = getItemSize(_this.props, index, _this._instanceProps); // TODO Deprecate direction "horizontal"
var isHorizontal = direction === 'horizontal' || layout === 'horizontal';
itemStyleCache[index] = style = (_style = {
position: 'absolute'
}, _style[direction === 'rtl' ? 'right' : 'left'] = isHorizontal ? _offset : 0, _style.top = !isHorizontal ? _offset : 0, _style.height = !isHorizontal ? size : '100%', _style.width = isHorizontal ? size : '100%', _style);
}
return style;
};
_this._itemStyleCache = void 0;
_this._getItemStyleCache = void 0;
_this._getItemStyleCache = memoizeOne(function (_, __, ___) {
_this._itemStyleCache = {};
return _this._itemStyleCache;
});
_this._onScrollHorizontal = function (event) {
var _event$currentTarget = event.currentTarget,
clientWidth = _event$currentTarget.clientWidth,
scrollLeft = _event$currentTarget.scrollLeft,
scrollWidth = _event$currentTarget.scrollWidth;
_this.setState(function (prevState) {
if (prevState.scrollOffset === scrollLeft) {
// Scroll position may have been updated by cDM/cDU,
// In which case we don't need to trigger another render,
// And we don't want to update state.isScrolling.
return null;
}
var direction = _this.props.direction;
var scrollOffset = scrollLeft;
if (direction === 'rtl') {
// TRICKY According to the spec, scrollLeft should be negative for RTL aligned elements.
// This is not the case for all browsers though (e.g. Chrome reports values as positive, measured relative to the left).
// It's also easier for this component if we convert offsets to the same format as they would be in for ltr.
// So the simplest solution is to determine which browser behavior we're dealing with, and convert based on it.
// eslint-disable-next-line default-case
switch (getRTLOffsetType()) {
case 'negative':
scrollOffset = -scrollLeft;
break;
case 'positive-descending':
scrollOffset = scrollWidth - clientWidth - scrollLeft;
break;
}
} // Prevent Safari's elastic scrolling from causing visual shaking when scrolling past bounds.
scrollOffset = Math.max(0, Math.min(scrollOffset, scrollWidth - clientWidth));
return {
isScrolling: true,
scrollDirection: prevState.scrollOffset < scrollLeft ? 'forward' : 'backward',
scrollOffset: scrollOffset,
scrollUpdateWasRequested: false
};
}, _this._resetIsScrollingDebounced);
};
_this._onScrollVertical = function (event) {
var _event$currentTarget2 = event.currentTarget,
clientHeight = _event$currentTarget2.clientHeight,
scrollHeight = _event$currentTarget2.scrollHeight,
scrollTop = _event$currentTarget2.scrollTop;
_this.setState(function (prevState) {
if (prevState.scrollOffset === scrollTop) {
// Scroll position may have been updated by cDM/cDU,
// In which case we don't need to trigger another render,
// And we don't want to update state.isScrolling.
return null;
} // Prevent Safari's elastic scrolling from causing visual shaking when scrolling past bounds.
var scrollOffset = Math.max(0, Math.min(scrollTop, scrollHeight - clientHeight));
return {
isScrolling: true,
scrollDirection: prevState.scrollOffset < scrollOffset ? 'forward' : 'backward',
scrollOffset: scrollOffset,
scrollUpdateWasRequested: false
};
}, _this._resetIsScrollingDebounced);
};
_this._outerRefSetter = function (ref) {
var outerRef = _this.props.outerRef;
_this._outerRef = ref;
if (typeof outerRef === 'function') {
outerRef(ref);
} else if (outerRef != null && typeof outerRef === 'object' && outerRef.hasOwnProperty('current')) {
outerRef.current = ref;
}
};
_this._resetIsScrollingDebounced = function () {
if (_this._resetIsScrollingTimeoutId !== null) {
cancelTimeout(_this._resetIsScrollingTimeoutId);
}
_this._resetIsScrollingTimeoutId = requestTimeout(_this._resetIsScrolling, IS_SCROLLING_DEBOUNCE_INTERVAL);
};
_this._resetIsScrolling = function () {
_this._resetIsScrollingTimeoutId = null;
_this.setState({
isScrolling: false
}, function () {
// Clear style cache after state update has been committed.
// This way we don't break pure sCU for items that don't use isScrolling param.
_this._getItemStyleCache(-1, null);
});
};
_this._instanceProps = initInstanceProps(_this.props, _assertThisInitialized(_this));
return _this;
}
List.getDerivedStateFromProps = function getDerivedStateFromProps(nextProps, prevState) {
validateSharedProps(nextProps, prevState);
validateProps(nextProps);
return null;
};
var _proto = List.prototype;
_proto.scrollTo = function scrollTo(scrollOffset) {
scrollOffset = Math.max(0, scrollOffset);
this.setState(function (prevState) {
if (prevState.scrollOffset === scrollOffset) {
return null;
}
return {
scrollDirection: prevState.scrollOffset < scrollOffset ? 'forward' : 'backward',
scrollOffset: scrollOffset,
scrollUpdateWasRequested: true
};
}, this._resetIsScrollingDebounced);
};
_proto.scrollToItem = function scrollToItem(index, align) {
if (align === void 0) {
align = 'auto';
}
var itemCount = this.props.itemCount;
var scrollOffset = this.state.scrollOffset;
index = Math.max(0, Math.min(index, itemCount - 1));
this.scrollTo(getOffsetForIndexAndAlignment(this.props, index, align, scrollOffset, this._instanceProps));
};
_proto.componentDidMount = function componentDidMount() {
var _this$props2 = this.props,
direction = _this$props2.direction,
initialScrollOffset = _this$props2.initialScrollOffset,
layout = _this$props2.layout;
if (typeof initialScrollOffset === 'number' && this._outerRef != null) {
var outerRef = this._outerRef; // TODO Deprecate direction "horizontal"
if (direction === 'horizontal' || layout === 'horizontal') {
outerRef.scrollLeft = initialScrollOffset;
} else {
outerRef.scrollTop = initialScrollOffset;
}
}
this._callPropsCallbacks();
this._commitHook();
};
_proto.componentDidUpdate = function componentDidUpdate() {
var _this$props3 = this.props,
direction = _this$props3.direction,
layout = _this$props3.layout;
var _this$state = this.state,
scrollOffset = _this$state.scrollOffset,
scrollUpdateWasRequested = _this$state.scrollUpdateWasRequested;
if (scrollUpdateWasRequested && this._outerRef != null) {
var outerRef = this._outerRef; // TODO Deprecate direction "horizontal"
if (direction === 'horizontal' || layout === 'horizontal') {
if (direction === 'rtl') {
// TRICKY According to the spec, scrollLeft should be negative for RTL aligned elements.
// This is not the case for all browsers though (e.g. Chrome reports values as positive, measured relative to the left).
// So we need to determine which browser behavior we're dealing with, and mimic it.
switch (getRTLOffsetType()) {
case 'negative':
outerRef.scrollLeft = -scrollOffset;
break;
case 'positive-ascending':
outerRef.scrollLeft = scrollOffset;
break;
default:
var clientWidth = outerRef.clientWidth,
scrollWidth = outerRef.scrollWidth;
outerRef.scrollLeft = scrollWidth - clientWidth - scrollOffset;
break;
}
} else {
outerRef.scrollLeft = scrollOffset;
}
} else {
outerRef.scrollTop = scrollOffset;
}
}
this._callPropsCallbacks();
this._commitHook();
};
_proto.componentWillUnmount = function componentWillUnmount() {
if (this._resetIsScrollingTimeoutId !== null) {
cancelTimeout(this._resetIsScrollingTimeoutId);
}
this._unmountHook();
};
_proto.render = function render() {
var _this$props4 = this.props,
className = _this$props4.className,
direction = _this$props4.direction,
height = _this$props4.height,
innerRef = _this$props4.innerRef,
innerElementType = _this$props4.innerElementType,
innerTagName = _this$props4.innerTagName,
layout = _this$props4.layout,
outerElementType = _this$props4.outerElementType,
outerTagName = _this$props4.outerTagName,
style = _this$props4.style,
width = _this$props4.width;
var isScrolling = this.state.isScrolling; // TODO Deprecate direction "horizontal"
var isHorizontal = direction === 'horizontal' || layout === 'horizontal';
var onScroll = isHorizontal ? this._onScrollHorizontal : this._onScrollVertical;
var items = this._renderItems(); // Read this value AFTER items have been created,
// So their actual sizes (if variable) are taken into consideration.
var estimatedTotalSize = getEstimatedTotalSize(this.props, this._instanceProps);
return react.createElement(outerElementType || outerTagName || 'div', {
className: className,
onScroll: onScroll,
ref: this._outerRefSetter,
style: _extends({
height: height,
width: width,
overflow: 'auto',
position: 'relative',
WebkitOverflowScrolling: 'touch',
willChange: 'transform',
direction: direction
}, style)
}, react.createElement(innerElementType || innerTagName || 'div', {
children: items,
ref: innerRef,
style: {
height: isHorizontal ? '100%' : estimatedTotalSize,
pointerEvents: isScrolling ? 'none' : undefined,
width: isHorizontal ? estimatedTotalSize : '100%'
}
}));
};
_proto._callPropsCallbacks = function _callPropsCallbacks() {
if (typeof this.props.onItemsRendered === 'function') {
var itemCount = this.props.itemCount;
if (itemCount > 0) {
var _this$_getRangeToRend = this._getRangeToRender(),
_overscanStartIndex = _this$_getRangeToRend[0],
_overscanStopIndex = _this$_getRangeToRend[1],
_visibleStartIndex = _this$_getRangeToRend[2],
_visibleStopIndex = _this$_getRangeToRend[3];
this._callOnItemsRendered(_overscanStartIndex, _overscanStopIndex, _visibleStartIndex, _visibleStopIndex);
}
}
if (typeof this.props.onScroll === 'function') {
var _this$state2 = this.state,
_scrollDirection = _this$state2.scrollDirection,
_scrollOffset = _this$state2.scrollOffset,
_scrollUpdateWasRequested = _this$state2.scrollUpdateWasRequested;
this._callOnScroll(_scrollDirection, _scrollOffset, _scrollUpdateWasRequested);
}
} // This method is called after mount and update.
// List implementations can override this method to be notified.
;
_proto._commitHook = function _commitHook() {} // This method is called before unmounting.
// List implementations can override this method to be notified.
;
_proto._unmountHook = function _unmountHook() {} // Lazily create and cache item styles while scrolling,
// So that pure component sCU will prevent re-renders.
// We maintain this cache, and pass a style prop rather than index,
// So that List can clear cached styles and force item re-render if necessary.
;
_proto._getRangeToRender = function _getRangeToRender() {
var _this$props5 = this.props,
itemCount = _this$props5.itemCount,
overscanCount = _this$props5.overscanCount;
var _this$state3 = this.state,
isScrolling = _this$state3.isScrolling,
scrollDirection = _this$state3.scrollDirection,
scrollOffset = _this$state3.scrollOffset;
if (itemCount === 0) {
return [0, 0, 0, 0];
}
var startIndex = getStartIndexForOffset(this.props, scrollOffset, this._instanceProps);
var stopIndex = getStopIndexForStartIndex(this.props, startIndex, scrollOffset, this._instanceProps); // Overscan by one item in each direction so that tab/focus works.
// If there isn't at least one extra item, tab loops back around.
var overscanBackward = !isScrolling || scrollDirection === 'backward' ? Math.max(1, overscanCount) : 1;
var overscanForward = !isScrolling || scrollDirection === 'forward' ? Math.max(1, overscanCount) : 1;
return [Math.max(0, startIndex - overscanBackward), Math.max(0, Math.min(itemCount - 1, stopIndex + overscanForward)), startIndex, stopIndex];
};
_proto._renderItems = function _renderItems() {
var _this$props6 = this.props,
children = _this$props6.children,
itemCount = _this$props6.itemCount,
itemData = _this$props6.itemData,
_this$props6$itemKey = _this$props6.itemKey,
itemKey = _this$props6$itemKey === void 0 ? defaultItemKey : _this$props6$itemKey,
useIsScrolling = _this$props6.useIsScrolling;
var isScrolling = this.state.isScrolling;
var _this$_getRangeToRend2 = this._getRangeToRender(),
startIndex = _this$_getRangeToRend2[0],
stopIndex = _this$_getRangeToRend2[1];
var items = [];
if (itemCount > 0) {
for (var _index = startIndex; _index <= stopIndex; _index++) {
items.push(react.createElement(children, {
data: itemData,
key: itemKey(_index, itemData),
index: _index,
isScrolling: useIsScrolling ? isScrolling : undefined,
style: this._getItemStyle(_index)
}));
}
}
return items;
};
return List;
}(react.PureComponent), _class.defaultProps = {
direction: 'ltr',
itemData: undefined,
layout: 'vertical',
overscanCount: 2,
useIsScrolling: false
}, _temp;
} // NOTE: I considered further wrapping individual items with a pure ListItem component.
// This would avoid ever calling the render function for the same index more than once,
// But it would also add the overhead of a lot of components/fibers.
// I assume people already do this (render function returning a class component),
// So my doing it would just unnecessarily double the wrappers.
var validateSharedProps = function validateSharedProps(_ref2, _ref3) {
var children = _ref2.children,
direction = _ref2.direction,
height = _ref2.height,
layout = _ref2.layout,
innerTagName = _ref2.innerTagName,
outerTagName = _ref2.outerTagName,
width = _ref2.width;
var instance = _ref3.instance;
if (process.env.NODE_ENV !== 'production') {
if (innerTagName != null || outerTagName != null) {
if (devWarningsTagName && !devWarningsTagName.has(instance)) {
devWarningsTagName.add(instance);
console.warn('The innerTagName and outerTagName props have been deprecated. ' + 'Please use the innerElementType and outerElementType props instead.');
}
} // TODO Deprecate direction "horizontal"
var isHorizontal = direction === 'horizontal' || layout === 'horizontal';
switch (direction) {
case 'horizontal':
case 'vertical':
if (devWarningsDirection && !devWarningsDirection.has(instance)) {
devWarningsDirection.add(instance);
console.warn('The direction prop should be either "ltr" (default) or "rtl". ' + 'Please use the layout prop to specify "vertical" (default) or "horizontal" orientation.');
}
break;
case 'ltr':
case 'rtl':
// Valid values
break;
default:
throw Error('An invalid "direction" prop has been specified. ' + 'Value should be either "ltr" or "rtl". ' + ("\"" + direction + "\" was specified."));
}
switch (layout) {
case 'horizontal':
case 'vertical':
// Valid values
break;
default:
throw Error('An invalid "layout" prop has been specified. ' + 'Value should be either "horizontal" or "vertical". ' + ("\"" + layout + "\" was specified."));
}
if (children == null) {
throw Error('An invalid "children" prop has been specified. ' + 'Value should be a React component. ' + ("\"" + (children === null ? 'null' : typeof children) + "\" was specified."));
}
if (isHorizontal && typeof width !== 'number') {
throw Error('An invalid "width" prop has been specified. ' + 'Horizontal lists must specify a number for width. ' + ("\"" + (width === null ? 'null' : typeof width) + "\" was specified."));
} else if (!isHorizontal && typeof height !== 'number') {
throw Error('An invalid "height" prop has been specified. ' + 'Vertical lists must specify a number for height. ' + ("\"" + (height === null ? 'null' : typeof height) + "\" was specified."));
}
}
};
var findDOMNodeWarningsSet = null;
if (process.env.NODE_ENV !== 'production') {
findDOMNodeWarningsSet =
/*#__PURE__*/
new Set();
}
var ItemMeasurer =
/*#__PURE__*/
function (_Component) {
_inheritsLoose(ItemMeasurer, _Component);
function ItemMeasurer() {
var _this;
for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
args[_key] = arguments[_key];
}
_this = _Component.call.apply(_Component, [this].concat(args)) || this;
_this._didProvideValidRef = false;
_this._node = null;
_this._resizeObserver = null;
_this._measureItem = function (isCommitPhase, isMount) {
var _this$props = _this.props,
direction = _this$props.direction,
layout = _this$props.layout,
handleNewMeasurements = _this$props.handleNewMeasurements,
index = _this$props.index,
oldSize = _this$props.size;
var node = _this._node;
if (node && node.ownerDocument && node.ownerDocument.defaultView && node instanceof node.ownerDocument.defaultView.HTMLElement) {
var newSize = direction === 'horizontal' || layout === 'horizontal' ? Math.ceil(node.offsetWidth) : Math.ceil(node.offsetHeight);
if (oldSize !== newSize || isMount) {
handleNewMeasurements(index, newSize, isCommitPhase);
}
}
};
_this._refSetter = function (ref) {
if (_this._resizeObserver !== null && _this._node !== null) {
_this._resizeObserver.unobserve(_this._node);
}
if (ref instanceof HTMLElement) {
_this._didProvideValidRef = true;
_this._node = ref;
} else if (ref) {
_this._node = reactDom.findDOMNode(ref);
} else {
_this._node = null;
}
if (_this._resizeObserver !== null && _this._node !== null) {
_this._resizeObserver.observe(_this._node);
}
};
_this._onResize = function () {
_this._measureItem(false, false);
};
return _this;
}
var _proto = ItemMeasurer.prototype;
_proto.componentDidMount = function componentDidMount() {
if (process.env.NODE_ENV !== 'production') {
if (!this._didProvideValidRef) {
var item = this.props.item;
var displayName = item && item.type ? item.type.displayName || item.type.name || '(unknown)' : '(unknown)';
if (!findDOMNodeWarningsSet.has(displayName)) {
findDOMNodeWarningsSet.add(displayName);
console.warn('DynamicSizeList item renderers should attach a ref to the topmost HTMLElement they render. ' + ("The item renderer \"" + displayName + "\" did not attach a ref to a valid HTMLElement. ") + 'findDOMNode() will be used as a fallback, but is slower and more error prone than using a ref.\n\n' + 'Learn more about ref forwarding: ' + 'https://reactjs.org/docs/forwarding-refs.html#forwarding-refs-to-dom-components');
}
}
} // Force sync measure for the initial mount.
// This is necessary to support the DynamicSizeList layout logic.
this._measureItem(true, true);
if (typeof ResizeObserver !== 'undefined') {
// Watch for resizes due to changed content,
// Or changes in the size of the parent container.
this._resizeObserver = new ResizeObserver(this._onResize);
if (this._node !== null) {
this._resizeObserver.observe(this._node);
}
}
};
_proto.componentWillUnmount = function componentWillUnmount() {
if (this._resizeObserver !== null) {
this._resizeObserver.disconnect();
this._resizeObserver = null;
}
};
_proto.render = function render() {
return react.cloneElement(this.props.item, {
ref: this._refSetter
});
};
return ItemMeasurer;
}(react.Component);
var DEFAULT_ESTIMATED_ITEM_SIZE = 50;
var getItemMetadata = function getItemMetadata(props, index, instanceProps) {
var estimatedItemSize = instanceProps.estimatedItemSize,
instance = instanceProps.instance,
itemOffsetMap = instanceProps.itemOffsetMap,
itemSizeMap = instanceProps.itemSizeMap,
lastMeasuredIndex = instanceProps.lastMeasuredIndex,
lastPositionedIndex = instanceProps.lastPositionedIndex; // If the specified item has not yet been measured,
// Just return an estimated size for now.
if (index > lastMeasuredIndex) {
return {
offset: 0,
size: estimatedItemSize
};
} // Lazily update positions if they are stale.
if (index > lastPositionedIndex) {
if (lastPositionedIndex < 0) {
itemOffsetMap[0] = 0;
}
for (var i = Math.max(1, lastPositionedIndex + 1); i <= index; i++) {
var prevOffset = itemOffsetMap[i - 1]; // In some browsers (e.g. Firefox) fast scrolling may skip rows.
// In this case, our assumptions about last measured indices may be incorrect.
// Handle this edge case to prevent NaN values from breaking styles.
// Slow scrolling back over these skipped rows will adjust their sizes.
var prevSize = itemSizeMap[i - 1] || 0;
itemOffsetMap[i] = prevOffset + prevSize; // Reset cached style to clear stale position.
delete instance._itemStyleCache[i];
}
instanceProps.lastPositionedIndex = index;
}
var offset = itemOffsetMap[index];
var size = itemSizeMap[index];
return {
offset: offset,
size: size
};
};
var findNearestItemBinarySearch = function findNearestItemBinarySearch(props, instanceProps, high, low, offset) {
while (low <= high) {
var middle = low + Math.floor((high - low) / 2);
var 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;
}
};
var getEstimatedTotalSize = function getEstimatedTotalSize(_ref, _ref2) {
var itemCount = _ref.itemCount;
var itemSizeMap = _ref2.itemSizeMap,
estimatedItemSize = _ref2.estimatedItemSize,
lastMeasuredIndex = _ref2.lastMeasuredIndex,
totalMeasuredSize = _ref2.totalMeasuredSize;
return totalMeasuredSize + (itemCount - lastMeasuredIndex - 1) * estimatedItemSize;
};
var DynamicSizeList =
/*#__PURE__*/
createListComponent({
getItemOffset: function getItemOffset(props, index, instanceProps) {
return getItemMetadata(props, index, instanceProps).offset;
},
getItemSize: function getItemSize(props, index, instanceProps) {
// Do not hard-code item dimensions.
// We don't know them initially.
// Even once we do, changes in item content or list size should reflow.
return undefined;
},
getEstimatedTotalSize: getEstimatedTotalSize,
getOffsetForIndexAndAlignment: function getOffsetForIndexAndAlignment(props, index, align, scrollOffset, instanceProps) {
var direction = props.direction,
layout = props.layout,
height = props.height,
width = props.width;
if (process.env.NODE_ENV !== 'production') {
var lastMeasuredIndex = instanceProps.lastMeasuredIndex;
if (index > lastMeasuredIndex) {
console.warn("DynamicSizeList does not support scrolling to items that yave not yet measured. " + ("scrollToItem() was called with index " + index + " but the last measured item was " + lastMeasuredIndex + "."));
}
}
var size = direction === 'horizontal' || layout === 'horizontal' ? width : height;
var itemMetadata = getItemMetadata(props, index, instanceProps); // Get estimated total size after ItemMetadata is computed,
// To ensure it reflects actual measurements instead of just estimates.
var estimatedTotalSize = getEstimatedTotalSize(props, instanceProps);
var maxOffset = Math.min(estimatedTotalSize - size, itemMetadata.offset);
var minOffset = Math.max(0, itemMetadata.offset - size + itemMetadata.size);
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 < maxOffset - scrollOffset) {
return minOffset;
} else {
return maxOffset;
}
}
},
getStartIndexForOffset: function getStartIndexForOffset(props, offset, instanceProps) {
var lastMeasuredIndex = instanceProps.lastMeasuredIndex,
totalMeasuredSize = instanceProps.totalMeasuredSize; // If we've already positioned and measured past this point,
// Use a binary search to find the closets cell.
if (offset <= totalMeasuredSize) {
return findNearestItemBinarySearch(props, instanceProps, lastMeasuredIndex, 0, offset);
} // Otherwise render a new batch of items starting from where we left off.
return lastMeasuredIndex + 1;
},
getStopIndexForStartIndex: function getStopIndexForStartIndex(props, startIndex, scrollOffset, instanceProps) {
var direction = props.direction,
layout = props.layout,
height = props.height,
itemCount = props.itemCount,
width = props.width;
var size = direction === 'horizontal' || layout === 'horizontal' ? width : height;
var itemMetadata = getItemMetadata(props, startIndex, instanceProps);
var maxOffset = scrollOffset + size;
var offset = itemMetadata.offset + itemMetadata.size;
var stopIndex = startIndex;
while (stopIndex < itemCount - 1 && offset < maxOffset) {
stopIndex++;
offset += getItemMetadata(props, stopIndex, instanceProps).size;
}
return stopIndex;
},
initInstanceProps: function initInstanceProps(props, instance) {
var _ref3 = props,
estimatedItemSize = _ref3.estimatedItemSize;
var instanceProps = {
estimatedItemSize: estimatedItemSize || DEFAULT_ESTIMATED_ITEM_SIZE,
instance: instance,
itemOffsetMap: {},
itemSizeMap: {},
lastMeasuredIndex: -1,
lastPositionedIndex: -1,
totalMeasuredSize: 0
};
var debounceForceUpdateID = null;
var debounceForceUpdate = function debounceForceUpdate() {
if (debounceForceUpdateID === null) {
debounceForceUpdateID = setTimeout(function () {
debounceForceUpdateID = null;
instance.forceUpdate();
}, 1);
}
}; // This method is called before unmounting.
instance._unmountHook = function () {
if (debounceForceUpdateID !== null) {
clearTimeout(debounceForceUpdateID);
debounceForceUpdateID = null;
}
};
var hasNewMeasurements = false;
var sizeDeltaTotal = 0; // This method is called after mount and update.
instance._commitHook = function () {
if (hasNewMeasurements) {
hasNewMeasurements = false; // Edge case where cell sizes changed, but cancelled each other out.
// We still need to re-render in this case,
// Even though we don't need to adjust scroll offset.
if (sizeDeltaTotal === 0) {
instance.forceUpdate();
return;
}
var shouldForceUpdate; // In the setState commit hook, we'll decrement sizeDeltaTotal.
// In case the state update is processed synchronously,
// And triggers additional size updates itself,
// We should only drecement by the amount we updated state for originally.
var sizeDeltaForStateUpdate = sizeDeltaTotal; // If the user is scrolling up, we need to adjust the scroll offset,
// To prevent items from "jumping" as items before them have been resized.
instance.setState(function (prevState) {
if (prevState.scrollDirection === 'backward' && !prevState.scrollUpdateWasRequested) {
// TRICKY
// If item(s) have changed size since they were last displayed, content will appear to jump.
// To avoid this, we need to make small adjustments as a user scrolls to preserve apparent position.
// This also ensures that the first item eventually aligns with scroll offset 0.
return {
scrollOffset: prevState.scrollOffset + sizeDeltaForStateUpdate
};
} else {
// There's no state to update,
// But we still want to re-render in this case.
shouldForceUpdate = true;
return null;
}
}, function () {
if (shouldForceUpdate) {
instance.forceUpdate();
} else {
var scrollOffset = instance.state.scrollOffset;
var _instance$props = instance.props,
direction = _instance$props.direction,
layout = _instance$props.layout; // Adjusting scroll offset directly interrupts smooth scrolling for some browsers (e.g. Firefox).
// The relative scrollBy() method doesn't interrupt (or at least it won't as of Firefox v65).
// Other browsers (e.g. Chrome, Safari) seem to handle both adjustments equally well.
// See https://bugzilla.mozilla.org/show_bug.cgi?id=1502059
var element = instance._outerRef; // $FlowFixMe Property scrollBy is missing in HTMLDivElement
if (typeof element.scrollBy === 'function') {
element.scrollBy(direction === 'horizontal' || layout === 'horizontal' ? sizeDeltaForStateUpdate : 0, direction === 'horizontal' || layout === 'horizontal' ? 0 : sizeDeltaForStateUpdate);
} else if (direction === 'horizontal' || layout === 'horizontal') {
element.scrollLeft = scrollOffset;
} else {
element.scrollTop = scrollOffset;
}
}
sizeDeltaTotal -= sizeDeltaForStateUpdate;
});
}
}; // This function may be called out of order!
// It is not safe to reposition items here.
// Be careful when comparing index and lastMeasuredIndex.
var handleNewMeasurements = function handleNewMeasurements(index, newSize, isFirstMeasureAfterMounting) {
var itemSizeMap = instanceProps.itemSizeMap,
lastMeasuredIndex = instanceProps.lastMeasuredIndex,
lastPositionedIndex = instanceProps.lastPositionedIndex; // In some browsers (e.g. Firefox) fast scrolling may skip rows.
// In this case, our assumptions about last measured indices may be incorrect.
// Handle this edge case to prevent NaN values from breaking styles.
// Slow scrolling back over these skipped rows will adjust their sizes.
var oldSize = itemSizeMap[index] || 0; // Mark offsets after this as stale so that getItemMetadata() will lazily recalculate it.
if (index < lastPositionedIndex) {
instanceProps.lastPositionedIndex = index;
}
if (index <= lastMeasuredIndex) {
if (oldSize === newSize) {
return;
} // Adjust total size estimate by the delta in size.
instanceProps.totalMeasuredSize += newSize - oldSize; // Record the size delta here in case the user is scrolling up.
// In that event, we need to adjust the scroll offset by thie amount,
// To prevent items from "jumping" as items before them are resized.
// We only do this for items that are newly measured (after mounting).
// Ones that change size later do not need to affect scroll offset.
if (isFirstMeasureAfterMounting) {
sizeDeltaTotal += newSize - oldSize;
}
} else {
instanceProps.lastMeasuredIndex = index;
instanceProps.totalMeasuredSize += newSize;
}
itemSizeMap[index] = newSize; // Even though the size has changed, we don't need to reset the cached style,
// Because dynamic list items don't have constrained sizes.
// This enables them to resize when their content (or container size) changes.
// It also lets us avoid an unnecessary render in this case.
if (isFirstMeasureAfterMounting) {
hasNewMeasurements = true;
} else {
debounceForceUpdate();
}
};
instance._handleNewMeasurements = handleNewMeasurements; // Override the item-rendering process to wrap items with ItemMeasurer.
// This keep the external API simpler.
instance._renderItems = function () {
var _instance$props2 = instance.props,
children = _instance$props2.children,
direction = _instance$props2.direction,
layout = _instance$props2.layout,
itemCount = _instance$props2.itemCount,
itemData = _instance$props2.itemData,
_instance$props2$item = _instance$props2.itemKey,
itemKey = _instance$props2$item === void 0 ? defaultItemKey : _instance$props2$item,
useIsScrolling = _instance$props2.useIsScrolling;
var isScrolling = instance.state.isScrolling;
var _instance$_getRangeTo = instance._getRangeToRender(),
startIndex = _instance$_getRangeTo[0],
stopIndex = _instance$_getRangeTo[1];
var items = [];
if (itemCount > 0) {
for (var _index = startIndex; _index <= stopIndex; _index++) {
var _getItemMetadata = getItemMetadata(instance.props, _index, instanceProps),
size = _getItemMetadata.size; // It's important to read style after fetching item metadata.
// getItemMetadata() will clear stale styles.
var style = instance._getItemStyle(_index);
var item = react.createElement(children, {
data: itemData,
index: _index,
isScrolling: useIsScrolling ? isScrolling : undefined,
style: style
}); // Always wrap children in a ItemMeasurer to detect changes in size.
items.push(react.createElement(ItemMeasurer, {
direction: direction,
layout: layout,
handleNewMeasurements: handleNewMeasurements,
index: _index,
item: item,
key: itemKey(_index, itemData),
size: size
}));
}
}
return items;
};
return instanceProps;
},
shouldResetStyleCacheOnItemSizeChange: false,
validateProps: function validateProps(_ref4) {
var itemSize = _ref4.itemSize;
if (process.env.NODE_ENV !== 'production') {
if (itemSize !== undefined) {
throw Error('An unexpected "itemSize" prop has been provided.');
}
}
}
});
var IS_SCROLLING_DEBOUNCE_INTERVAL$1 = 150;
var defaultItemKey$1 = function defaultItemKey(_ref) {
var columnIndex = _ref.columnIndex,
data = _ref.data,
rowIndex = _ref.rowIndex;
return rowIndex + ":" + columnIndex;
}; // In DEV mode, this Set helps us only log a warning once per component instance.
// This avoids spamming the console every time a render happens.
var devWarningsOverscanCount = null;
var devWarningsOverscanRowsColumnsCount = null;
var devWarningsTagName$1 = null;
if (process.env.NODE_ENV !== 'production') {
if (typeof window !== 'undefined' && typeof window.WeakSet !== 'undefined') {
devWarningsOverscanCount =
/*#__PURE__*/
new WeakSet();
devWarningsOverscanRowsColumnsCount =
/*#__PURE__*/
new WeakSet();
devWarningsTagName$1 =
/*#__PURE__*/
new WeakSet();
}
}
function createGridComponent(_ref2) {
var _class, _temp;
var getColumnOffset = _ref2.getColumnOffset,
getColumnStartIndexForOffset = _ref2.getColumnStartIndexForOffset,
getColumnStopIndexForStartIndex = _ref2.getColumnStopIndexForStartIndex,
getColumnWidth = _ref2.getColumnWidth,
getEstimatedTotalHeight = _ref2.getEstimatedTotalHeight,
getEstimatedTotalWidth = _ref2.getEstimatedTotalWidth,
getOffsetForColumnAndAlignment = _ref2.getOffsetForColumnAndAlignment,
getOffsetForRowAndAlignment = _ref2.getOffsetForRowAndAlignment,
getRowHeight = _ref2.getRowHeight,
getRowOffset = _ref2.getRowOffset,
getRowStartIndexForOffset = _ref2.getRowStartIndexForOffset,
getRowStopIndexForStartIndex = _ref2.getRowStopIndexForStartIndex,
initInstanceProps = _ref2.initInstanceProps,
shouldResetStyleCacheOnItemSizeChange = _ref2.shouldResetStyleCacheOnItemSizeChange,
validateProps = _ref2.validateProps;
return _temp = _class =
/*#__PURE__*/
function (_PureComponent) {
_inheritsLoose(Grid, _PureComponent);
// Always use explicit constructor for React components.
// It produces less code after transpilation. (#26)
// eslint-disable-next-line no-useless-constructor
function Grid(props) {
var _this;
_this = _PureComponent.call(this, props) || this;
_this._instanceProps = initInstanceProps(_this.props, _assertThisInitialized(_this));
_this._resetIsScrollingTimeoutId = null;
_this._outerRef = void 0;
_this.state = {
instance: _assertThisInitialized(_this),
isScrolling: false,
horizontalScrollDirection: 'forward',
scrollLeft: typeof _this.props.initialScrollLeft === 'number' ? _this.props.initialScrollLeft : 0,
scrollTop: typeof _this.props.initialScrollTop === 'number' ? _this.props.initialScrollTop : 0,
scrollUpdateWasRequested: false,
verticalScrollDirection: 'forward'
};
_this._callOnItemsRendered = void 0;
_this._callOnItemsRendered = memoizeOne(function (overscanColumnStartIndex, overscanColumnStopIndex, overscanRowStartIndex, overscanRowStopIndex, visibleColumnStartIndex, visibleColumnStopIndex, visibleRowStartIndex, visibleRowStopIndex) {
return _this.props.onItemsRendered({
overscanColumnStartIndex: overscanColumnStartIndex,
overscanColumnStopIndex: overscanColumnStopIndex,
overscanRowStartIndex: overscanRowStartIndex,
overscanRowStopIndex: overscanRowStopIndex,
visibleColumnStartIndex: visibleColumnStartIndex,
visibleColumnStopIndex: visibleColumnStopIndex,
visibleRowStartIndex: visibleRowStartIndex,
visibleRowStopIndex: visibleRowStopIndex
});
});
_this._callOnScroll = void 0;
_this._callOnScroll = memoizeOne(function (scrollLeft, scrollTop, horizontalScrollDirection, verticalScrollDirection, scrollUpdateWasRequested) {
return _this.props.onScroll({
horizontalScrollDirection: horizontalScrollDirection,
scrollLeft: scrollLeft,
scrollTop: scrollTop,
verticalScrollDirection: verticalScrollDirection,
scrollUpdateWasRequested: scrollUpdateWasRequested
});
});
_this._getItemStyle = void 0;
_this._getItemStyle = function (rowIndex, columnIndex) {
var _this$props = _this.props,
columnWidth = _this$props.columnWidth,
direction = _this$props.direction,
rowHeight = _this$props.rowHeight;
var itemStyleCache = _this._getItemStyleCache(shouldResetStyleCacheOnItemSizeChange && columnWidth, shouldResetStyleCacheOnItemSizeChange && direction, shouldResetStyleCacheOnItemSizeChange && rowHeight);
var key = rowIndex + ":" + columnIndex;
var style;
if (itemStyleCache.hasOwnProperty(key)) {
style = itemStyleCache[key];
} else {
var _style;
itemStyleCache[key] = style = (_style = {
position: 'absolute'
}, _style[direction === 'rtl' ? 'right' : 'left'] = getColumnOffset(_this.props, columnIndex, _this._instanceProps), _style.top = getRowOffset(_this.props, rowIndex, _this._instanceProps), _style.height = getRowHeight(_this.props, rowIndex, _this._instanceProps), _style.width = getColumnWidth(_this.props, columnIndex, _this._instanceProps), _style);
}
return style;
};
_this._getItemStyleCache = void 0;
_this._getItemStyleCache = memoizeOne(function (_, __, ___) {
return {};
});
_this._onScroll = function (event) {
var _event$currentTarget = event.currentTarget,
clientHeight = _event$currentTarget.clientHeight,
clientWidth = _event$currentTarget.clientWidth,
scrollLeft = _event$currentTarget.scrollLeft,
scrollTop = _event$currentTarget.scrollTop,
scrollHeight = _event$currentTarget.scrollHeight,
scrollWidth = _event$currentTarget.scrollWidth;
_this.setState(function (prevState) {
if (prevState.scrollLeft === scrollLeft && prevState.scrollTop === scrollTop) {
// Scroll position may have been updated by cDM/cDU,
// In which case we don't need to trigger another render,
// And we don't want to update state.isScrolling.
return null;
}
var direction = _this.props.direction; // TRICKY According to the spec, scrollLeft should be negative for RTL aligned elements.
// This is not the case for all browsers though (e.g. Chrome reports values as positive, measured relative to the left).
// I