UNPKG

@john-osullivan/react-window-dynamic-fork

Version:

Temporary Fork: see https://github.com/bvaughn/react-window

1,276 lines (1,065 loc) 101 kB
import { createElement, PureComponent, cloneElement, Component } from 'react'; import _extends from '@babel/runtime/helpers/esm/extends'; import _assertThisInitialized from '@babel/runtime/helpers/esm/assertThisInitialized'; import _inheritsLoose from '@babel/runtime/helpers/esm/inheritsLoose'; import memoizeOne from 'memoize-one'; import { findDOMNode } from 'react-dom'; import _objectWithoutPropertiesLoose from '@babel/runtime/helpers/esm/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 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) }, 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(createElement(children, { data: itemData, key: itemKey(_index, itemData), index: _index, isScrolling: useIsScrolling ? isScrolling : undefined, style: this._getItemStyle(_index) })); } } return items; }; return List; }(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 = 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 cloneElement(this.props.item, { ref: this._refSetter }); }; return ItemMeasurer; }(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 = 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(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). // 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. var calculatedScrollLeft = scrollLeft;