react-virtualized
Version:
React components for efficiently rendering large, scrollable lists and tabular data
553 lines (529 loc) • 25.9 kB
JavaScript
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
var _typeof = require("@babel/runtime/helpers/typeof");
Object.defineProperty(exports, "__esModule", {
value: true
});
exports["default"] = void 0;
var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));
var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass"));
var _possibleConstructorReturn2 = _interopRequireDefault(require("@babel/runtime/helpers/possibleConstructorReturn"));
var _getPrototypeOf2 = _interopRequireDefault(require("@babel/runtime/helpers/getPrototypeOf"));
var _inherits2 = _interopRequireDefault(require("@babel/runtime/helpers/inherits"));
var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
var _clsx = _interopRequireDefault(require("clsx"));
var _propTypes = _interopRequireDefault(require("prop-types"));
var React = _interopRequireWildcard(require("react"));
var _reactLifecyclesCompat = require("react-lifecycles-compat");
var _createCallbackMemoizer = _interopRequireDefault(require("../utils/createCallbackMemoizer"));
var _scrollbarSize = _interopRequireDefault(require("dom-helpers/scrollbarSize"));
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(e) { return e ? t : r; })(e); }
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != _typeof(e) && "function" != typeof e) return { "default": e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && {}.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n["default"] = e, t && t.set(e, n), n; }
function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { (0, _defineProperty2["default"])(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
function _callSuper(t, o, e) { return o = (0, _getPrototypeOf2["default"])(o), (0, _possibleConstructorReturn2["default"])(t, _isNativeReflectConstruct() ? Reflect.construct(o, e || [], (0, _getPrototypeOf2["default"])(t).constructor) : o.apply(t, e)); }
function _isNativeReflectConstruct() { try { var t = !Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})); } catch (t) {} return (_isNativeReflectConstruct = function _isNativeReflectConstruct() { return !!t; })(); }
// @TODO Merge Collection and CollectionView
/**
* Specifies the number of milliseconds during which to disable pointer events while a scroll is in progress.
* This improves performance and makes scrolling smoother.
*/
var IS_SCROLLING_TIMEOUT = 150;
/**
* Controls whether the Grid updates the DOM element's scrollLeft/scrollTop based on the current state or just observes it.
* This prevents Grid from interrupting mouse-wheel animations (see issue #2).
*/
var SCROLL_POSITION_CHANGE_REASONS = {
OBSERVED: 'observed',
REQUESTED: 'requested'
};
/**
* Monitors changes in properties (eg. cellCount) and state (eg. scroll offsets) to determine when rendering needs to occur.
* This component does not render any visible content itself; it defers to the specified :cellLayoutManager.
*/
var CollectionView = /*#__PURE__*/function (_React$PureComponent) {
function CollectionView() {
var _this;
(0, _classCallCheck2["default"])(this, CollectionView);
for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
args[_key] = arguments[_key];
}
_this = _callSuper(this, CollectionView, [].concat(args));
// If this component is being rendered server-side, getScrollbarSize() will return undefined.
// We handle this case in componentDidMount()
(0, _defineProperty2["default"])(_this, "state", {
isScrolling: false,
scrollLeft: 0,
scrollTop: 0
});
(0, _defineProperty2["default"])(_this, "_calculateSizeAndPositionDataOnNextUpdate", false);
// Invokes callbacks only when their values have changed.
(0, _defineProperty2["default"])(_this, "_onSectionRenderedMemoizer", (0, _createCallbackMemoizer["default"])());
(0, _defineProperty2["default"])(_this, "_onScrollMemoizer", (0, _createCallbackMemoizer["default"])(false));
(0, _defineProperty2["default"])(_this, "_invokeOnSectionRenderedHelper", function () {
var _this$props = _this.props,
cellLayoutManager = _this$props.cellLayoutManager,
onSectionRendered = _this$props.onSectionRendered;
_this._onSectionRenderedMemoizer({
callback: onSectionRendered,
indices: {
indices: cellLayoutManager.getLastRenderedIndices()
}
});
});
(0, _defineProperty2["default"])(_this, "_setScrollingContainerRef", function (ref) {
_this._scrollingContainer = ref;
});
(0, _defineProperty2["default"])(_this, "_updateScrollPositionForScrollToCell", function () {
var _this$props2 = _this.props,
cellLayoutManager = _this$props2.cellLayoutManager,
height = _this$props2.height,
scrollToAlignment = _this$props2.scrollToAlignment,
scrollToCell = _this$props2.scrollToCell,
width = _this$props2.width;
var _this$state = _this.state,
scrollLeft = _this$state.scrollLeft,
scrollTop = _this$state.scrollTop;
if (scrollToCell >= 0) {
var scrollPosition = cellLayoutManager.getScrollPositionForCell({
align: scrollToAlignment,
cellIndex: scrollToCell,
height: height,
scrollLeft: scrollLeft,
scrollTop: scrollTop,
width: width
});
if (scrollPosition.scrollLeft !== scrollLeft || scrollPosition.scrollTop !== scrollTop) {
_this._setScrollPosition(scrollPosition);
}
}
});
(0, _defineProperty2["default"])(_this, "_onScroll", function (event) {
// In certain edge-cases React dispatches an onScroll event with an invalid target.scrollLeft / target.scrollTop.
// This invalid event can be detected by comparing event.target to this component's scrollable DOM element.
// See issue #404 for more information.
if (event.target !== _this._scrollingContainer) {
return;
}
// Prevent pointer events from interrupting a smooth scroll
_this._enablePointerEventsAfterDelay();
// When this component is shrunk drastically, React dispatches a series of back-to-back scroll events,
// Gradually converging on a scrollTop that is within the bounds of the new, smaller height.
// This causes a series of rapid renders that is slow for long lists.
// We can avoid that by doing some simple bounds checking to ensure that scrollTop never exceeds the total height.
var _this$props3 = _this.props,
cellLayoutManager = _this$props3.cellLayoutManager,
height = _this$props3.height,
isScrollingChange = _this$props3.isScrollingChange,
width = _this$props3.width;
var scrollbarSize = _this._scrollbarSize;
var _cellLayoutManager$ge = cellLayoutManager.getTotalSize(),
totalHeight = _cellLayoutManager$ge.height,
totalWidth = _cellLayoutManager$ge.width;
var scrollLeft = Math.max(0, Math.min(totalWidth - width + scrollbarSize, event.target.scrollLeft));
var scrollTop = Math.max(0, Math.min(totalHeight - height + scrollbarSize, event.target.scrollTop));
// Certain devices (like Apple touchpad) rapid-fire duplicate events.
// Don't force a re-render if this is the case.
// The mouse may move faster then the animation frame does.
// Use requestAnimationFrame to avoid over-updating.
if (_this.state.scrollLeft !== scrollLeft || _this.state.scrollTop !== scrollTop) {
// Browsers with cancelable scroll events (eg. Firefox) interrupt scrolling animations if scrollTop/scrollLeft is set.
// Other browsers (eg. Safari) don't scroll as well without the help under certain conditions (DOM or style changes during scrolling).
// All things considered, this seems to be the best current work around that I'm aware of.
// For more information see https://github.com/bvaughn/react-virtualized/pull/124
var scrollPositionChangeReason = event.cancelable ? SCROLL_POSITION_CHANGE_REASONS.OBSERVED : SCROLL_POSITION_CHANGE_REASONS.REQUESTED;
// Synchronously set :isScrolling the first time (since _setNextState will reschedule its animation frame each time it's called)
if (!_this.state.isScrolling) {
isScrollingChange(true);
}
_this.setState({
isScrolling: true,
scrollLeft: scrollLeft,
scrollPositionChangeReason: scrollPositionChangeReason,
scrollTop: scrollTop
});
}
_this._invokeOnScrollMemoizer({
scrollLeft: scrollLeft,
scrollTop: scrollTop,
totalWidth: totalWidth,
totalHeight: totalHeight
});
});
_this._scrollbarSize = (0, _scrollbarSize["default"])();
if (_this._scrollbarSize === undefined) {
_this._scrollbarSizeMeasured = false;
_this._scrollbarSize = 0;
} else {
_this._scrollbarSizeMeasured = true;
}
return _this;
}
/**
* Forced recompute of cell sizes and positions.
* This function should be called if cell sizes have changed but nothing else has.
* Since cell positions are calculated by callbacks, the collection view has no way of detecting when the underlying data has changed.
*/
(0, _inherits2["default"])(CollectionView, _React$PureComponent);
return (0, _createClass2["default"])(CollectionView, [{
key: "recomputeCellSizesAndPositions",
value: function recomputeCellSizesAndPositions() {
this._calculateSizeAndPositionDataOnNextUpdate = true;
this.forceUpdate();
}
/* ---------------------------- Component lifecycle methods ---------------------------- */
/**
* @private
* This method updates scrollLeft/scrollTop in state for the following conditions:
* 1) Empty content (0 rows or columns)
* 2) New scroll props overriding the current state
* 3) Cells-count or cells-size has changed, making previous scroll offsets invalid
*/
}, {
key: "componentDidMount",
value: function componentDidMount() {
var _this$props4 = this.props,
cellLayoutManager = _this$props4.cellLayoutManager,
scrollLeft = _this$props4.scrollLeft,
scrollToCell = _this$props4.scrollToCell,
scrollTop = _this$props4.scrollTop;
// If this component was first rendered server-side, scrollbar size will be undefined.
// In that event we need to remeasure.
if (!this._scrollbarSizeMeasured) {
this._scrollbarSize = (0, _scrollbarSize["default"])();
this._scrollbarSizeMeasured = true;
this.setState({});
}
if (scrollToCell >= 0) {
this._updateScrollPositionForScrollToCell();
} else if (scrollLeft >= 0 || scrollTop >= 0) {
this._setScrollPosition({
scrollLeft: scrollLeft,
scrollTop: scrollTop
});
}
// Update onSectionRendered callback.
this._invokeOnSectionRenderedHelper();
var _cellLayoutManager$ge2 = cellLayoutManager.getTotalSize(),
totalHeight = _cellLayoutManager$ge2.height,
totalWidth = _cellLayoutManager$ge2.width;
// Initialize onScroll callback.
this._invokeOnScrollMemoizer({
scrollLeft: scrollLeft || 0,
scrollTop: scrollTop || 0,
totalHeight: totalHeight,
totalWidth: totalWidth
});
}
}, {
key: "componentDidUpdate",
value: function componentDidUpdate(prevProps, prevState) {
var _this$props5 = this.props,
height = _this$props5.height,
scrollToAlignment = _this$props5.scrollToAlignment,
scrollToCell = _this$props5.scrollToCell,
width = _this$props5.width;
var _this$state2 = this.state,
scrollLeft = _this$state2.scrollLeft,
scrollPositionChangeReason = _this$state2.scrollPositionChangeReason,
scrollTop = _this$state2.scrollTop;
// Make sure requested changes to :scrollLeft or :scrollTop get applied.
// Assigning to scrollLeft/scrollTop tells the browser to interrupt any running scroll animations,
// And to discard any pending async changes to the scroll position that may have happened in the meantime (e.g. on a separate scrolling thread).
// So we only set these when we require an adjustment of the scroll position.
// See issue #2 for more information.
if (scrollPositionChangeReason === SCROLL_POSITION_CHANGE_REASONS.REQUESTED) {
if (scrollLeft >= 0 && scrollLeft !== prevState.scrollLeft && scrollLeft !== this._scrollingContainer.scrollLeft) {
this._scrollingContainer.scrollLeft = scrollLeft;
}
if (scrollTop >= 0 && scrollTop !== prevState.scrollTop && scrollTop !== this._scrollingContainer.scrollTop) {
this._scrollingContainer.scrollTop = scrollTop;
}
}
// Update scroll offsets if the current :scrollToCell values requires it
if (height !== prevProps.height || scrollToAlignment !== prevProps.scrollToAlignment || scrollToCell !== prevProps.scrollToCell || width !== prevProps.width) {
this._updateScrollPositionForScrollToCell();
}
// Update onRowsRendered callback if start/stop indices have changed
this._invokeOnSectionRenderedHelper();
}
}, {
key: "componentWillUnmount",
value: function componentWillUnmount() {
if (this._disablePointerEventsTimeoutId) {
clearTimeout(this._disablePointerEventsTimeoutId);
}
}
}, {
key: "render",
value: function render() {
var _this$props6 = this.props,
autoHeight = _this$props6.autoHeight,
cellCount = _this$props6.cellCount,
cellLayoutManager = _this$props6.cellLayoutManager,
className = _this$props6.className,
height = _this$props6.height,
horizontalOverscanSize = _this$props6.horizontalOverscanSize,
id = _this$props6.id,
noContentRenderer = _this$props6.noContentRenderer,
style = _this$props6.style,
verticalOverscanSize = _this$props6.verticalOverscanSize,
width = _this$props6.width;
var _this$state3 = this.state,
isScrolling = _this$state3.isScrolling,
scrollLeft = _this$state3.scrollLeft,
scrollTop = _this$state3.scrollTop;
// Memoization reset
if (this._lastRenderedCellCount !== cellCount || this._lastRenderedCellLayoutManager !== cellLayoutManager || this._calculateSizeAndPositionDataOnNextUpdate) {
this._lastRenderedCellCount = cellCount;
this._lastRenderedCellLayoutManager = cellLayoutManager;
this._calculateSizeAndPositionDataOnNextUpdate = false;
cellLayoutManager.calculateSizeAndPositionData();
}
var _cellLayoutManager$ge3 = cellLayoutManager.getTotalSize(),
totalHeight = _cellLayoutManager$ge3.height,
totalWidth = _cellLayoutManager$ge3.width;
// Safely expand the rendered area by the specified overscan amount
var left = Math.max(0, scrollLeft - horizontalOverscanSize);
var top = Math.max(0, scrollTop - verticalOverscanSize);
var right = Math.min(totalWidth, scrollLeft + width + horizontalOverscanSize);
var bottom = Math.min(totalHeight, scrollTop + height + verticalOverscanSize);
var childrenToDisplay = height > 0 && width > 0 ? cellLayoutManager.cellRenderers({
height: bottom - top,
isScrolling: isScrolling,
width: right - left,
x: left,
y: top
}) : [];
var collectionStyle = {
boxSizing: 'border-box',
direction: 'ltr',
height: autoHeight ? 'auto' : height,
position: 'relative',
WebkitOverflowScrolling: 'touch',
width: width,
willChange: 'transform'
};
// Force browser to hide scrollbars when we know they aren't necessary.
// Otherwise once scrollbars appear they may not disappear again.
// For more info see issue #116
var verticalScrollBarSize = totalHeight > height ? this._scrollbarSize : 0;
var horizontalScrollBarSize = totalWidth > width ? this._scrollbarSize : 0;
// Also explicitly init styles to 'auto' if scrollbars are required.
// This works around an obscure edge case where external CSS styles have not yet been loaded,
// But an initial scroll index of offset is set as an external prop.
// Without this style, Grid would render the correct range of cells but would NOT update its internal offset.
// This was originally reported via clauderic/react-infinite-calendar/issues/23
collectionStyle.overflowX = totalWidth + verticalScrollBarSize <= width ? 'hidden' : 'auto';
collectionStyle.overflowY = totalHeight + horizontalScrollBarSize <= height ? 'hidden' : 'auto';
return /*#__PURE__*/React.createElement("div", {
ref: this._setScrollingContainerRef,
"aria-label": this.props['aria-label'],
className: (0, _clsx["default"])('ReactVirtualized__Collection', className),
id: id,
onScroll: this._onScroll,
role: "grid",
style: _objectSpread(_objectSpread({}, collectionStyle), style),
tabIndex: 0
}, cellCount > 0 && /*#__PURE__*/React.createElement("div", {
className: "ReactVirtualized__Collection__innerScrollContainer",
style: {
height: totalHeight,
maxHeight: totalHeight,
maxWidth: totalWidth,
overflow: 'hidden',
pointerEvents: isScrolling ? 'none' : '',
width: totalWidth
}
}, childrenToDisplay), cellCount === 0 && noContentRenderer());
}
/* ---------------------------- Helper methods ---------------------------- */
/**
* Sets an :isScrolling flag for a small window of time.
* This flag is used to disable pointer events on the scrollable portion of the Collection.
* This prevents jerky/stuttery mouse-wheel scrolling.
*/
}, {
key: "_enablePointerEventsAfterDelay",
value: function _enablePointerEventsAfterDelay() {
var _this2 = this;
if (this._disablePointerEventsTimeoutId) {
clearTimeout(this._disablePointerEventsTimeoutId);
}
this._disablePointerEventsTimeoutId = setTimeout(function () {
var isScrollingChange = _this2.props.isScrollingChange;
isScrollingChange(false);
_this2._disablePointerEventsTimeoutId = null;
_this2.setState({
isScrolling: false
});
}, IS_SCROLLING_TIMEOUT);
}
}, {
key: "_invokeOnScrollMemoizer",
value: function _invokeOnScrollMemoizer(_ref) {
var _this3 = this;
var scrollLeft = _ref.scrollLeft,
scrollTop = _ref.scrollTop,
totalHeight = _ref.totalHeight,
totalWidth = _ref.totalWidth;
this._onScrollMemoizer({
callback: function callback(_ref2) {
var scrollLeft = _ref2.scrollLeft,
scrollTop = _ref2.scrollTop;
var _this3$props = _this3.props,
height = _this3$props.height,
onScroll = _this3$props.onScroll,
width = _this3$props.width;
onScroll({
clientHeight: height,
clientWidth: width,
scrollHeight: totalHeight,
scrollLeft: scrollLeft,
scrollTop: scrollTop,
scrollWidth: totalWidth
});
},
indices: {
scrollLeft: scrollLeft,
scrollTop: scrollTop
}
});
}
}, {
key: "_setScrollPosition",
value: function _setScrollPosition(_ref3) {
var scrollLeft = _ref3.scrollLeft,
scrollTop = _ref3.scrollTop;
var newState = {
scrollPositionChangeReason: SCROLL_POSITION_CHANGE_REASONS.REQUESTED
};
if (scrollLeft >= 0) {
newState.scrollLeft = scrollLeft;
}
if (scrollTop >= 0) {
newState.scrollTop = scrollTop;
}
if (scrollLeft >= 0 && scrollLeft !== this.state.scrollLeft || scrollTop >= 0 && scrollTop !== this.state.scrollTop) {
this.setState(newState);
}
}
}], [{
key: "getDerivedStateFromProps",
value: function getDerivedStateFromProps(nextProps, prevState) {
if (nextProps.cellCount === 0 && (prevState.scrollLeft !== 0 || prevState.scrollTop !== 0)) {
return {
scrollLeft: 0,
scrollTop: 0,
scrollPositionChangeReason: SCROLL_POSITION_CHANGE_REASONS.REQUESTED
};
} else if (nextProps.scrollLeft !== prevState.scrollLeft || nextProps.scrollTop !== prevState.scrollTop) {
return {
scrollLeft: nextProps.scrollLeft != null ? nextProps.scrollLeft : prevState.scrollLeft,
scrollTop: nextProps.scrollTop != null ? nextProps.scrollTop : prevState.scrollTop,
scrollPositionChangeReason: SCROLL_POSITION_CHANGE_REASONS.REQUESTED
};
}
return null;
}
}]);
}(React.PureComponent);
(0, _defineProperty2["default"])(CollectionView, "defaultProps", {
'aria-label': 'grid',
horizontalOverscanSize: 0,
noContentRenderer: function noContentRenderer() {
return null;
},
onScroll: function onScroll() {
return null;
},
onSectionRendered: function onSectionRendered() {
return null;
},
scrollToAlignment: 'auto',
scrollToCell: -1,
style: {},
verticalOverscanSize: 0
});
CollectionView.propTypes = process.env.NODE_ENV !== "production" ? {
'aria-label': _propTypes["default"].string,
/**
* Removes fixed height from the scrollingContainer so that the total height
* of rows can stretch the window. Intended for use with WindowScroller
*/
autoHeight: _propTypes["default"].bool,
/**
* Number of cells in collection.
*/
cellCount: _propTypes["default"].number.isRequired,
/**
* Calculates cell sizes and positions and manages rendering the appropriate cells given a specified window.
*/
cellLayoutManager: _propTypes["default"].object.isRequired,
/**
* Optional custom CSS class name to attach to root Collection element.
*/
className: _propTypes["default"].string,
/**
* Height of Collection; this property determines the number of visible (vs virtualized) rows.
*/
height: _propTypes["default"].number.isRequired,
/**
* Optional custom id to attach to root Collection element.
*/
id: _propTypes["default"].string,
/**
* Enables the `Collection` to horiontally "overscan" its content similar to how `Grid` does.
* This can reduce flicker around the edges when a user scrolls quickly.
*/
horizontalOverscanSize: _propTypes["default"].number.isRequired,
isScrollingChange: _propTypes["default"].func,
/**
* Optional renderer to be used in place of rows when either :rowCount or :cellCount is 0.
*/
noContentRenderer: _propTypes["default"].func.isRequired,
/**
* Callback invoked whenever the scroll offset changes within the inner scrollable region.
* This callback can be used to sync scrolling between lists, tables, or grids.
* ({ clientHeight, clientWidth, scrollHeight, scrollLeft, scrollTop, scrollWidth }): void
*/
onScroll: _propTypes["default"].func.isRequired,
/**
* Callback invoked with information about the section of the Collection that was just rendered.
* This callback is passed a named :indices parameter which is an Array of the most recently rendered section indices.
*/
onSectionRendered: _propTypes["default"].func.isRequired,
/**
* Horizontal offset.
*/
scrollLeft: _propTypes["default"].number,
/**
* Controls scroll-to-cell behavior of the Grid.
* The default ("auto") scrolls the least amount possible to ensure that the specified cell is fully visible.
* Use "start" to align cells to the top/left of the Grid and "end" to align bottom/right.
*/
scrollToAlignment: _propTypes["default"].oneOf(['auto', 'end', 'start', 'center']).isRequired,
/**
* Cell index to ensure visible (by forcefully scrolling if necessary).
*/
scrollToCell: _propTypes["default"].number.isRequired,
/**
* Vertical offset.
*/
scrollTop: _propTypes["default"].number,
/**
* Optional custom inline style to attach to root Collection element.
*/
style: _propTypes["default"].object,
/**
* Enables the `Collection` to vertically "overscan" its content similar to how `Grid` does.
* This can reduce flicker around the edges when a user scrolls quickly.
*/
verticalOverscanSize: _propTypes["default"].number.isRequired,
/**
* Width of Collection; this property determines the number of visible (vs virtualized) columns.
*/
width: _propTypes["default"].number.isRequired
} : {};
(0, _reactLifecyclesCompat.polyfill)(CollectionView);
var _default = exports["default"] = CollectionView;