Socket
Socket
Sign inDemoInstall

virtual-scroller

Package Overview
Dependencies
3
Maintainers
1
Versions
78
Alerts
File Explorer

Advanced tools

Install Socket

Detect and block malicious and high-risk dependencies

Install

Comparing version 1.11.3 to 1.12.0

commonjs/ItemNotRenderedError.js

13

commonjs/DOM/ItemsContainer.js

@@ -8,2 +8,6 @@ "use strict";

var _ItemNotRenderedError = _interopRequireDefault(require("../ItemNotRenderedError.js"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

@@ -32,5 +36,8 @@

if (renderedElementIndex > childNodes.length - 1) {
console.log('~ Items Container Contents ~');
console.log(this.getElement().innerHTML);
throw new Error("Element with index ".concat(renderedElementIndex, " was not found in the list of Rendered Item Elements in the Items Container of Virtual Scroller. There're only ").concat(childNodes.length, " Elements there."));
// console.log('~ Items Container Contents ~')
// console.log(this.getElement().innerHTML)
throw new _ItemNotRenderedError["default"]({
renderedElementIndex: renderedElementIndex,
renderedElementsCount: childNodes.length
});
}

@@ -37,0 +44,0 @@

@@ -172,2 +172,8 @@ "use strict";

var shouldResetGridLayout;
var errors = [];
global.VirtualScrollerCatchError = function (error) {
return errors.push(error);
};
layout.getLayoutUpdateForItemsDiff({

@@ -194,2 +200,6 @@ firstShownItemIndex: 3,

});
global.VirtualScrollerCatchError = undefined;
errors.length.should.equal(2);
errors[0].message.should.equal('[virtual-scroller] ~ Prepended items count 5 is not divisible by Columns Count 4 ~');
errors[1].message.should.equal('[virtual-scroller] Layout reset required');
shouldResetGridLayout.should.equal(true);

@@ -196,0 +206,0 @@ });

@@ -8,4 +8,8 @@ "use strict";

var _debug = _interopRequireDefault(require("../utility/debug.js"));
var _react = require("react");
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
function useItemKeys(_ref) {

@@ -36,8 +40,11 @@ var getItemId = _ref.getItemId;

generateItemKeyPrefix();
}, []);
}, []); // If `getItemId()` function is defined, then item `id`s are gonna be the item element `key`s.
var usesAutogeneratedItemKeys = !getItemId;
var generateItemKeyPrefixIfNotUsingItemIds = (0, _react.useCallback)(function () {
if (!getItemId) {
if (usesAutogeneratedItemKeys) {
generateItemKeyPrefix();
(0, _debug["default"])('React: ~ Item key prefix:', itemKeyPrefix.current);
}
}, [getItemId, generateItemKeyPrefix]);
}, [usesAutogeneratedItemKeys, generateItemKeyPrefix]);
/**

@@ -59,2 +66,3 @@ * Returns a `key` for an `item`'s element.

getItemKey: getItemKey,
usesAutogeneratedItemKeys: usesAutogeneratedItemKeys,
updateItemKeysForNewItems: generateItemKeyPrefixIfNotUsingItemIds

@@ -61,0 +69,0 @@ };

"use strict";
function _typeof(obj) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (obj) { return typeof obj; } : function (obj) { return obj && "function" == typeof Symbol && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }, _typeof(obj); }
Object.defineProperty(exports, "__esModule", {

@@ -8,9 +10,13 @@ value: true

var _debug = _interopRequireWildcard(require("../utility/debug.js"));
var _getStateSnapshot = _interopRequireDefault(require("../utility/getStateSnapshot.js"));
var _react = require("react");
function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; }
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; }
function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || _typeof(obj) !== "object" && typeof obj !== "function") { return { "default": obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj["default"] = obj; if (cache) { cache.set(obj, newObj); } return newObj; }

@@ -33,12 +39,9 @@ function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest(); }

onRender = _ref.onRender,
itemsProperty = _ref.itemsProperty,
USE_ITEMS_UPDATE_NO_SECOND_RENDER_OPTIMIZATION = _ref.USE_ITEMS_UPDATE_NO_SECOND_RENDER_OPTIMIZATION;
itemsProperty = _ref.itemsProperty;
// This is a utility state variable that is used to re-render the component.
// It should not be used to access the current `VirtualScroller` state.
// It's more of a "requested" `VirtualScroller` state.
//
// It will also be stale in cases when `USE_ITEMS_UPDATE_NO_SECOND_RENDER_OPTIMIZATION`
// feature is used for setting new `items` in state.
//
// This is a state variable that is used to re-render the component.
// Right after the component has finished re-rendering,
// `VirtualScroller` state gets updated from this variable.
// The reason for that is that `VirtualScroller` state must always
// correspond exactly to what's currently rendered on the screen.
var _useState2 = (0, _react.useState)(initialState),

@@ -52,70 +55,160 @@ _useState3 = _slicedToArray(_useState2, 2),

var state = (0, _react.useRef)(initialState);
var getState = (0, _react.useCallback)(function () {
return state.current;
}, []);
var setState = (0, _react.useCallback)(function (newState) {
state.current = newState;
}, []); // Accumulates all "pending" state updates until they have been applied.
}, []); // Updating of the actual `VirtualScroller` state is done in a
// `useInsertionEffect()` rather than in a `useLayoutEffect()`.
//
// The reason is that using `useLayoutEffect()` would result in
// "breaking" the `<VirtualScroller/>` when an `itemComponent`
// called `onHeightDidChange()` from its own `useLayoutEffect()`.
// In those cases, the `itemCompoent`'s effect would run before
// the `<VirtualScroller/>`'s effect, resulting in
// `VirtualScroller.onItemHeightDidChange(i)` being run at a moment in time
// when the DOM has already been updated for the next `VirtualScroller` state
// but the actual `VirtualScroller` state is still a previous ("stale") one
// containing "stale" first/last shown item indexes, which would result in an
// "index out of bounds" error when `onItemHeightDidChange(i)` tries to access
// and measure the DOM element from item index `i` which doesn't already/yet exist.
//
// An example of such situation could be seen from a `VirtualScroller` debug log
// which was captured for a case when using `useLayoutEffect()` to update the
// "actual" `VirtualScroller` state after the corresponding DOM changes have been applied:
// The user has scrolled far enough: perform a re-layout
// ~ Update Layout (on scroll) ~
//
// Item index 2 height is required for calculations but hasn't been measured yet. Mark the item as "shown", rerender the list, measure the item's height and redo the layout.
//
// ~ Calculated Layout ~
// Columns count 1
// First shown item index 2
// Last shown item index 5
// …
// Item heights (231) [1056.578125, 783.125, empty × 229]
// Item states (231) [{…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, …]
//
// ~ Set state ~
// {firstShownItemIndex: 2, lastShownItemIndex: 5, …}
//
// ~ Rendered ~
// State {firstShownItemIndex: 2, lastShownItemIndex: 5, …}
//
// ~ Measure item heights ~
// Item index 2 height 719.8828125
// Item index 3 height 961.640625
// Item index 4 height 677.6640625
// Item index 5 height 1510.1953125
//
// ~ Update Layout (on non-measured item heights have been measured) ~
//
// ~ Calculated Layout ~
// Columns count 1
// First shown item index 4
// Last shown item index 5
// …
// Item heights (231) [1056.578125, 783.125, 719.8828125, 961.640625, 677.6640625, 1510.1953125, empty × 225]
// Item states (231) [{…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, …]
//
// ~ Set state ~
// {firstShownItemIndex: 4, lastShownItemIndex: 5, beforeItemsHeight: 3521.2265625, afterItemsHeight: 214090.72265624942}
//
// ~ On Item Height Did Change was called ~
// Item index 5
// ~ Re-measure item height ~
// ERROR "onItemHeightDidChange()" has been called for item index 5 but the item is not currently rendered and can't be measured. The exact error was: Element with index 3 was not found in the list of Rendered Item Elements in the Items Container of Virtual Scroller. There're only 2 Elements there.
//
// React: ~ The requested state is about to be applied in DOM. Set it as the `VirtualScroller` state. ~
// {firstShownItemIndex: 4, lastShownItemIndex: 5, …}
//
// ~ Rendered ~
// "~ Rendered ~" is what gets output when `onRender()` function gets called.
// It means that `useLayoutEffect()` was triggered after `onItemHeightDidChange(i)`
// was called and after the "ERROR" happened.
//
// The "ERROR" happened because new item indexes 4…5 were actually rendered instead of
// item indexes 2…5 by the time the application called `onItemHeightDidChange(i)` function
// inside `itemComponent`'s `useLayoutEffect()`.
// Item indexes 4…5 is what was requested in a `setState()` call, which called `_setNewState()`.
// This means that `_newState` changes have been applied to the DOM
// but `useLayoutEffect()` wasn't triggered immediately after that.
// Instead, it was triggered a right after the `itemComponent`'s `useLayoutEffect()`
// because child effects run before parent effects.
// So, the `itemComponent`'s `onHeightDidChange()` function call caught the
// `VirtualScroller` in an inconsistent state.
//
// To fix that, `useLayoutEffect()` gets replaced with `useInsertionEffect()`:
// https://blog.saeloun.com/2022/06/02/react-18-useinsertioneffect
// https://beta.reactjs.org/reference/react/useInsertionEffect
//
// After replacing `useLayoutEffect()` with `useInsertionEffect()`,
// the log shows that there's no more error:
//
// ~ Set state ~
// {firstShownItemIndex: 0, lastShownItemIndex: 2, …}
//
// React: ~ The requested state is about to be applied in DOM. Set it as the `VirtualScroller` state. ~
// {firstShownItemIndex: 0, lastShownItemIndex: 2, …}
//
// ~ On Item Height Did Change was called ~
// Item index 0
// ~ Re-measure item height ~
// Previous height 917
// New height 1064.453125
// ~ Item height has changed ~
//
// An alternative solution would be demanding the `itemComponent` to
// accept a `ref` and then measuring the corresponding DOM element height
// directly using the `ref`-ed DOM element rather than searching for that
// DOM element in the `ItemsContainer`.
// So if `useInsertionEffect()` gets removed from React in some hypothetical future,
// it could be replaced with using `ref`s on `ItemComponent`s to measure the DOM element heights.
//
var nextState = (0, _react.useRef)(initialState); // Updates the actual `VirtualScroller` state right after a requested state update
// has been applied. Doesn't do anything at initial render.
(0, _react.useInsertionEffect)(function () {
// Update the actual `VirtualScroller` state right before the DOM changes
// are going to be applied for the requested state update.
//
// This hook will run right before `useLayoutEffect()`.
//
// It doesn't make any difference which one of the two hooks to use to update
// the actual `VirtualScroller` state in this scenario because the two hooks
// run synchronously one right after another (insertion effect → DOM update → layout effect)
// without any free space for any `VirtualScroller` code (like the scroll event handler)
// to squeeze in and run in-between them, so the `VirtualScroller`'s `state`
// is always gonna stay consistent with what's currently rendered on screen
// from the `VirtualScroler`'s point of view, and the short transition period
// it simply doesn't see because it doesn't "wake up" during that period.
//
// Updating the actual `VirtualScroller` state right before `useLayoutEffect()`
// fixes the bug when an `itemComponent` calls `onHeightDidChange()` in its own
// `useLayoutEffect()` which would run before this `useLayoutEffect()`
// because children's effects run before parent's.
//
// This hook doesn't do anything at the initial render.
//
if ((0, _debug.isDebug)()) {
(0, _debug["default"])('React: ~ The requested state is about to be applied in DOM. Set it as the `VirtualScroller` state. ~');
(0, _debug["default"])((0, _getStateSnapshot["default"])(_newState));
}
(0, _react.useLayoutEffect)(function () {
setState(_newState);
}, [_newState]); // Calls `onRender()` right after every state update (which is a re-render),
// and also right after the initial render.
}, [_newState]);
(0, _react.useLayoutEffect)(function () {
// Call `onRender()` right after a requested state update has been applied,
// and also right after the initial render.
onRender();
}, [_newState, // When using `USE_ITEMS_UPDATE_NO_SECOND_RENDER_OPTIMIZATION` feature,
// there won't be a `_setNewState()` function call when `items` property changes,
// hence the additional `itemsProperty` dependency.
USE_ITEMS_UPDATE_NO_SECOND_RENDER_OPTIMIZATION ? itemsProperty : undefined]);
}, [_newState]);
return {
getState: function getState() {
return state.current;
},
getNextState: function getNextState() {
return nextState.current;
},
// This is the state the component should render.
stateToRender: _newState,
// Returns the current state of the `VirtualScroller`.
// This function is used in the `VirtualScroller` itself
// because the `state` is managed outside of it.
getState: getState,
// Requests a state update.
//
// State updates are incremental meaning that this function mimicks
// the classic `React.Component`'s `this.setState()` behavior
// when calling `this.setState()` didn't replace `state` but rather merged
// the updated state properties over the "old" state properties.
//
// The reason for using pending state updates accumulation is that
// `useState()` updates are "asynchronous" (not immediate),
// and simply merging over `...state` would merge over potentially stale
// property values in cases when more than a single `updateState()` call is made
// before the state actually updates, resulting in losing some of those state updates.
//
// Example: the first `updateState()` call updates shown item indexes,
// and the second `updateState()` call updates `verticalSpacing`.
// If it was simply `updateState({ ...state, ...stateUpdate })`
// then the second state update could overwrite the first state update,
// resulting in incorrect items being shown/hidden.
//
updateState: function updateState(stateUpdate) {
nextState.current = _objectSpread(_objectSpread({}, nextState.current), stateUpdate); // If `items` property did change, the component detects it at render time
// and updates `VirtualScroller` items immediately by calling `.setItems()`,
// which, in turn, immediately calls this `updateState()` function
// with a `stateUpdate` argument that contains the new `items`,
// so checking for `stateUpdate.items` could detect situations like that.
//
// When the initial `VirtualScroller` state is being set, it contains the `.items`
// property too, but that initial setting is done using another function called
// `setInitialState()`, so using `if (stateUpdate.items)` condition here for describing
// just the case when `state` has been updated as a result of a `setItems()` call
// seems to be fine.
//
var _newState = nextState.current;
if (stateUpdate.items && USE_ITEMS_UPDATE_NO_SECOND_RENDER_OPTIMIZATION) {
setState(_newState);
} else {
_setNewState(_newState);
}
}
setState: _setNewState
};
}
//# sourceMappingURL=useState.js.map

@@ -14,3 +14,3 @@ "use strict";

var tbody = _ref.tbody,
getNextState = _ref.getNextState;
state = _ref.state;

@@ -21,6 +21,4 @@ if (tbody) {

var _getNextState = getNextState(),
beforeItemsHeight = _getNextState.beforeItemsHeight,
afterItemsHeight = _getNextState.afterItemsHeight;
var beforeItemsHeight = state.beforeItemsHeight,
afterItemsHeight = state.afterItemsHeight;
return {

@@ -27,0 +25,0 @@ paddingTop: (0, _px["default"])(beforeItemsHeight),

@@ -28,5 +28,5 @@ "use strict";

var _useHandleItemsPropertyChange = _interopRequireDefault(require("./useHandleItemsPropertyChange.js"));
var _useSetNewItemsOnItemsPropertyChange = _interopRequireDefault(require("./useSetNewItemsOnItemsPropertyChange.js"));
var _useHandleItemIndexesChange = _interopRequireDefault(require("./useHandleItemIndexesChange.js"));
var _useUpdateItemKeysOnItemsChange = _interopRequireDefault(require("./useUpdateItemKeysOnItemsChange.js"));

@@ -51,24 +51,12 @@ var _useClassName = _interopRequireDefault(require("./useClassName.js"));

// When `items` property changes, `useHandleItemsPropertyChange()` hook detects that
// and calls `VirtualScroller.setItems()` which in turn calls the `updateState()` function.
// At this point, an insignificant optimization could be applied:
// the component could avoid re-rendering the second time.
// Instead, the state update could be applied "immediately" if it originated
// from `.setItems()` function call, eliminating the unneeded second re-render.
//
// I could see how this minor optimization could get brittle when modifiying the code,
// so I put it under a feature flag so that it could potentially be turned off
// in case of any potential weird issues in some future.
//
// Another reason for using this feature is:
//
// Since `useHandleItemsPropertyChange()` runs at render time
// and not after the render has finished (not in an "effect"),
// if the state update was done "conventionally" (by calling `_setNewState()`),
// React would throw an error about updating state during render.
// No one knows what the original error message was.
// Perhaps it's no longer relevant in newer versions of React.
//
var USE_ITEMS_UPDATE_NO_SECOND_RENDER_OPTIMIZATION = true;
// When `items` property changes:
// * A new `items` property is supplied to the React component.
// * The React component re-renders itself.
// * `useSetNewItemsOnItemsPropertyChange()` hook is run.
// * `useSetNewItemsOnItemsPropertyChange()` hook detects that the `items` property
// has changed and calls `VirtualScroller.setItems(items)`.
// * `VirtualScroller.setItems(items)` calls `VirtualScroller.setState()`.
// * `VirtualScroller.setState()` calls the `setState()` function.
// * The `setState()` function calls a setter from a `useState()` hook.
// * The React component re-renders itself the second time.
function VirtualScroller(_ref, ref) {

@@ -144,8 +132,7 @@ var AsComponent = _ref.as,

onRender: virtualScroller.onRender,
itemsProperty: itemsProperty,
USE_ITEMS_UPDATE_NO_SECOND_RENDER_OPTIMIZATION: USE_ITEMS_UPDATE_NO_SECOND_RENDER_OPTIMIZATION
itemsProperty: itemsProperty
}),
getState = _useState.getState,
updateState = _useState.updateState,
getNextState = _useState.getNextState; // Use custom (external) state storage in the `VirtualScroller`.
setState = _useState.setState,
stateToRender = _useState.stateToRender; // Use custom (external) state storage in the `VirtualScroller`.

@@ -156,3 +143,3 @@

getState: getState,
updateState: updateState
setState: setState
});

@@ -169,2 +156,3 @@ }, []); // Start `VirtualScroller` on mount.

getItemKey = _useItemKeys.getItemKey,
usesAutogeneratedItemKeys = _useItemKeys.usesAutogeneratedItemKeys,
updateItemKeysForNewItems = _useItemKeys.updateItemKeysForNewItems; // Cache per-item `setItemState` functions' "references"

@@ -185,3 +173,3 @@ // so that item components don't get re-rendered needlessly.

(0, _useHandleItemsPropertyChange["default"])(itemsProperty, {
(0, _useSetNewItemsOnItemsPropertyChange["default"])(itemsProperty, {
virtualScroller: virtualScroller,

@@ -191,9 +179,8 @@ // `preserveScrollPosition` property name is deprecated,

preserveScrollPosition: preserveScrollPosition,
preserveScrollPositionOnPrependItems: preserveScrollPositionOnPrependItems,
nextItems: getNextState().items
preserveScrollPositionOnPrependItems: preserveScrollPositionOnPrependItems
}); // Updates `key`s if item indexes have changed.
(0, _useHandleItemIndexesChange["default"])({
(0, _useUpdateItemKeysOnItemsChange["default"])(stateToRender.items, {
virtualScroller: virtualScroller,
itemsBeingRendered: getNextState().items,
usesAutogeneratedItemKeys: usesAutogeneratedItemKeys,
updateItemKeysForNewItems: updateItemKeysForNewItems

@@ -232,11 +219,8 @@ }); // Add instance methods to the React component.

tbody: tbody,
getNextState: getNextState
state: stateToRender
});
var _getNextState = getNextState(),
currentItems = _getNextState.items,
itemStates = _getNextState.itemStates,
firstShownItemIndex = _getNextState.firstShownItemIndex,
lastShownItemIndex = _getNextState.lastShownItemIndex;
var currentItems = stateToRender.items,
itemStates = stateToRender.itemStates,
firstShownItemIndex = stateToRender.firstShownItemIndex,
lastShownItemIndex = stateToRender.lastShownItemIndex;
return /*#__PURE__*/_react["default"].createElement(AsComponent, _extends({}, rest, {

@@ -243,0 +227,0 @@ ref: container,

@@ -8,2 +8,6 @@ "use strict";

var _ItemNotRenderedError = _interopRequireDefault(require("../ItemNotRenderedError.js"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

@@ -41,2 +45,10 @@

var startNewRow = true;
if (renderedElementIndex > children.length - 1) {
throw new _ItemNotRenderedError["default"]({
renderedElementIndex: renderedElementIndex,
renderedElementsCount: children.length
});
}
var i = 0;

@@ -78,3 +90,12 @@

value: function getNthRenderedItemHeight(renderedElementIndex) {
return this.getElement().children[renderedElementIndex].height;
var children = this.getElement().children;
if (renderedElementIndex > children.length - 1) {
throw new _ItemNotRenderedError["default"]({
renderedElementIndex: renderedElementIndex,
renderedElementsCount: children.length
});
}
return children[renderedElementIndex].height;
}

@@ -81,0 +102,0 @@ /**

@@ -52,3 +52,5 @@ "use strict";

function reportError() {
function error() {
var _console3;
for (var _len3 = arguments.length, args = new Array(_len3), _key3 = 0; _key3 < _len3; _key3++) {

@@ -58,2 +60,14 @@ args[_key3] = arguments[_key3];

(_console3 = console).error.apply(_console3, _toConsumableArray(['[virtual-scroller]'].concat(args)));
}
function reportError() {
for (var _len4 = arguments.length, args = new Array(_len4), _key4 = 0; _key4 < _len4; _key4++) {
args[_key4] = arguments[_key4];
}
var createError = function createError() {
return new Error(['[virtual-scroller]'].concat(args).join(' '));
};
if (typeof window !== 'undefined') {

@@ -63,3 +77,3 @@ // In a web browser.

// at which point did the error occur between other debug logs.
log.apply(this, ['ERROR'].concat(args));
error.apply(this, ['ERROR'].concat(args));
setTimeout(function () {

@@ -72,9 +86,19 @@ // Throw an error in a timeout so that it doesn't interrupt the application's flow.

// but those don't seem to be used in any of the error messages.
throw new Error(['[virtual-scroller]'].concat(args).join(' '));
throw createError();
}, 0);
} else {
var _console3;
// In Node.js.
// If tests are being run, throw in case of any errors.
var catchError = getGlobalVariable('VirtualScrollerCatchError');
// On a server.
(_console3 = console).error.apply(_console3, _toConsumableArray(['[virtual-scroller]'].concat(args)));
if (catchError) {
return catchError(createError());
}
if (getGlobalVariable('VirtualScrollerThrowErrors')) {
throw createError();
} // Print the error in the console.
error.apply(this, ['ERROR'].concat(args));
}

@@ -81,0 +105,0 @@ }

@@ -103,4 +103,5 @@ "use strict";

if (!isRestart) {
// If no custom state storage has been configured, use the default one.
this.waitingForRender = true; // If no custom state storage has been configured, use the default one.
// Also sets the initial state.
if (!this._usesCustomStateStorage) {

@@ -107,0 +108,0 @@ this.useDefaultStateStorage();

@@ -16,2 +16,6 @@ "use strict";

var _ItemNotRenderedError = _interopRequireDefault(require("./ItemNotRenderedError.js"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }

@@ -168,3 +172,6 @@

this.firstNonMeasuredItemIndex = firstNonMeasuredItemIndex; // Set "previously calculated layout".
this.firstNonMeasuredItemIndex = firstNonMeasuredItemIndex; // if (firstNonMeasuredItemIndex !== undefined) {
// log('Non-measured item index that will be measured at next layout', firstNonMeasuredItemIndex)
// }
// Set "previously calculated layout".
//

@@ -365,17 +372,19 @@ // The "previously calculated layout" feature is not currently used.

function updatePreviouslyCalculatedLayoutOnItemHeightChange(i, previousHeight, newHeight) {
if (this.previouslyCalculatedLayout) {
var prevLayout = this.previouslyCalculatedLayout;
if (prevLayout) {
var heightDifference = newHeight - previousHeight;
if (i < this.previouslyCalculatedLayout.firstShownItemIndex) {
// Patch `this.previouslyCalculatedLayout`'s `.beforeItemsHeight`.
this.previouslyCalculatedLayout.beforeItemsHeight += heightDifference;
} else if (i > this.previouslyCalculatedLayout.lastShownItemIndex) {
// Could patch `.afterItemsHeight` of `this.previouslyCalculatedLayout` here,
// if `.afterItemsHeight` property existed in `this.previouslyCalculatedLayout`.
if (this.previouslyCalculatedLayout.afterItemsHeight !== undefined) {
this.previouslyCalculatedLayout.afterItemsHeight += heightDifference;
if (i < prevLayout.firstShownItemIndex) {
// Patch `prevLayout`'s `.beforeItemsHeight`.
prevLayout.beforeItemsHeight += heightDifference;
} else if (i > prevLayout.lastShownItemIndex) {
// Could patch `.afterItemsHeight` of `prevLayout` here,
// if `.afterItemsHeight` property existed in `prevLayout`.
if (prevLayout.afterItemsHeight !== undefined) {
prevLayout.afterItemsHeight += heightDifference;
}
} else {
// Patch `this.previouslyCalculatedLayout`'s shown items height.
this.previouslyCalculatedLayout.shownItemsHeight += newHeight - previousHeight;
// Patch `prevLayout`'s shown items height.
prevLayout.shownItemsHeight += newHeight - previousHeight;
}

@@ -401,3 +410,3 @@ }

this._onItemHeightDidChange = function (i) {
(0, _debug["default"])('~ Re-measure item height ~');
(0, _debug["default"])('~ On Item Height Did Change was called ~');
(0, _debug["default"])('Item index', i);

@@ -443,6 +452,18 @@

if (previousHeight === undefined) {
return (0, _debug.reportError)("\"onItemHeightDidChange()\" has been called for item ".concat(i, ", but that item hasn't been rendered before."));
return (0, _debug.reportError)("\"onItemHeightDidChange()\" has been called for item index ".concat(i, " but the item hasn't been rendered before."));
}
var newHeight = remeasureItemHeight.call(_this, i);
(0, _debug["default"])('~ Re-measure item height ~');
var newHeight;
try {
newHeight = remeasureItemHeight.call(_this, i);
} catch (error) {
// Successfully finishing an `onItemHeightDidChange(i)` call is not considered
// critical for `VirtualScroller`'s operation, so such errors could be ignored.
if (error instanceof _ItemNotRenderedError["default"]) {
return (0, _debug.reportError)("\"onItemHeightDidChange()\" has been called for item index ".concat(i, " but the item is not currently rendered and can't be measured. The exact error was: ").concat(error.message));
}
}
(0, _debug["default"])('Previous height', previousHeight);

@@ -452,9 +473,30 @@ (0, _debug["default"])('New height', newHeight);

if (previousHeight !== newHeight) {
(0, _debug["default"])('~ Item height has changed ~'); // Update or reset previously calculated layout.
(0, _debug["default"])('~ Item height has changed. Should update layout. ~'); // Update or reset a previously calculated layout
// so that the "diff"s based on that layout in the future
// produce correct results.
updatePreviouslyCalculatedLayoutOnItemHeightChange.call(_this, i, previousHeight, newHeight); // Recalculate layout.
//
// If the `VirtualScroller` is already waiting for a state update to be rendered,
// delay `onItemHeightDidChange(i)`'s re-layout until that state update is rendered.
// The reason is that React `<VirtualScroller/>`'s `onHeightDidChange()` is meant to
// be called inside `useLayoutEffect()` hook. Due to how React is implemented internally,
// that might happen in the middle of the currently pending `setState()` operation
// being applied, resulting in weird "race condition" bugs.
//
_this.onUpdateShownItemIndexes({
reason: _Layout.LAYOUT_REASON.ITEM_HEIGHT_CHANGED
}); // Schedule the item height update for after the new items have been rendered.
if (_this.waitingForRender) {
(0, _debug["default"])('~ Another state update is already waiting to be rendered. Delay the layout update until then. ~');
_this.updateLayoutAfterRenderBecauseItemHeightChanged = true;
} else {
_this.onUpdateShownItemIndexes({
reason: _Layout.LAYOUT_REASON.ITEM_HEIGHT_CHANGED
});
} // If there was a request for `setState()` with new `items`, then the changes
// to `currentState.itemHeights[]` made above in a `remeasureItemHeight()` call
// would be overwritten when that pending `setState()` call gets applied.
// To fix that, the updates to current `itemHeights[]` are noted in
// `this.itemHeightsThatChangedWhileNewItemsWereBeingRendered` variable.
// That variable is then checked when the `setState()` call with the new `items`
// has been updated.

@@ -461,0 +503,0 @@

@@ -41,2 +41,3 @@ "use strict";

this._onRender = function (newState, prevState) {
_this.waitingForRender = false;
(0, _debug["default"])('~ Rendered ~');

@@ -59,6 +60,34 @@

(0, _tbody.setTbodyPadding)(_this.getItemsContainerElement(), newState.beforeItemsHeight, newState.afterItemsHeight);
}
} // `this.mostRecentlySetState` checks that state management behavior is correct:
// that in situations when there're multiple new states waiting to be set,
// only the latest one gets applied.
// It keeps the code simpler and prevents possible race condition bugs.
// For example, `VirtualScroller` keeps track of its latest requested
// state update in different instance variable flags which assume that
// only that latest requested state update gets actually applied.
//
// This check should also be performed for the initial render in order to
// guarantee that no potentially incorrect state update goes unnoticed.
// Incorrect state updates could happen when `VirtualScroller` state
// is managed externally by passing `getState()`/`updateState()` options.
//
// Perform the check only when `this.mostRecentSetStateValue` is defined.
// `this.mostRecentSetStateValue` is normally gonna be `undefined` at the initial render
// because the initial state is not set by calling `this.updateState()`.
// At the same time, it is possible that the initial render is delayed
// for whatever reason, and `this.updateState()` gets called before the initial render,
// so `this.mostRecentSetStateValue` could also be defined at the initial render,
// in which case the check should be performed.
//
if (!prevState) {
return;
if (_this.mostRecentSetStateValue) {
// "Shallow equality" is used here instead of "strict equality"
// because a developer might choose to supply an `updateState()` function
// rather than a `setState()` function, in which case the `updateState()` function
// would construct its own state object.
if (!(0, _shallowEqual["default"])(newState, _this.mostRecentSetStateValue)) {
(0, _debug.warn)('The most recent state that was set', (0, _getStateSnapshot["default"])(_this.mostRecentSetStateValue));
(0, _debug.reportError)('The state that has been rendered is not the most recent one that was set');
}
} // `this.resetStateUpdateFlags()` must be called before calling

@@ -70,5 +99,16 @@ // `this.measureItemHeightsAndSpacing()`.

nonMeasuredItemsHaveBeenRendered = _resetStateUpdateFlag.nonMeasuredItemsHaveBeenRendered,
itemHeightHasChanged = _resetStateUpdateFlag.itemHeightHasChanged,
widthHasChanged = _resetStateUpdateFlag.widthHasChanged;
var layoutUpdateReason; // If the `VirtualScroller`, while calculating layout parameters, encounters
var layoutUpdateReason;
if (_this.updateLayoutAfterRenderBecauseItemHeightChanged) {
layoutUpdateReason = _Layout.LAYOUT_REASON.ITEM_HEIGHT_CHANGED;
}
if (!prevState) {
if (!layoutUpdateReason) {
return;
}
} // If the `VirtualScroller`, while calculating layout parameters, encounters
// a not-shown item with a non-measured height, it calls `updateState()` just to

@@ -78,2 +118,3 @@ // render that item first, and then, after the list has been re-rendered, it measures

if (nonMeasuredItemsHaveBeenRendered) {

@@ -102,39 +143,41 @@ layoutUpdateReason = _Layout.LAYOUT_REASON.NON_MEASURED_ITEMS_HAVE_BEEN_MEASURED;

var previousItems = prevState.items;
var newItems = newState.items; // Even if `this.newItemsWillBeRendered` flag is `true`,
// `newItems` could still be equal to `previousItems`.
// For example, when `updateState()` calls don't update `state` immediately
// and a developer first calls `setItems(newItems)` and then calls `setItems(oldItems)`:
// in that case, `this.newItemsWillBeRendered` flag will be `true` but the actual `items`
// in state wouldn't have changed due to the first `updateState()` call being overwritten
// by the second `updateState()` call (that's called "batching state updates" in React).
if (prevState) {
var previousItems = prevState.items;
var newItems = newState.items; // Even if `this.newItemsWillBeRendered` flag is `true`,
// `newItems` could still be equal to `previousItems`.
// For example, when `updateState()` calls don't update `state` immediately
// and a developer first calls `setItems(newItems)` and then calls `setItems(oldItems)`:
// in that case, `this.newItemsWillBeRendered` flag will be `true` but the actual `items`
// in state wouldn't have changed due to the first `updateState()` call being overwritten
// by the second `updateState()` call (that's called "batching state updates" in React).
if (newItems !== previousItems) {
var itemsDiff = _this.getItemsDiff(previousItems, newItems);
if (newItems !== previousItems) {
var itemsDiff = _this.getItemsDiff(previousItems, newItems);
if (itemsDiff) {
// The call to `.onPrepend()` must precede the call to `.measureItemHeights()`
// which is called in `.onRender()`.
// `this.itemHeights.onPrepend()` updates `firstMeasuredItemIndex`
// and `lastMeasuredItemIndex` of `this.itemHeights`.
var prependedItemsCount = itemsDiff.prependedItemsCount;
if (itemsDiff) {
// The call to `.onPrepend()` must precede the call to `.measureItemHeights()`
// which is called in `.onRender()`.
// `this.itemHeights.onPrepend()` updates `firstMeasuredItemIndex`
// and `lastMeasuredItemIndex` of `this.itemHeights`.
var prependedItemsCount = itemsDiff.prependedItemsCount;
_this.itemHeights.onPrepend(prependedItemsCount);
} else {
_this.itemHeights.reset();
}
_this.itemHeights.onPrepend(prependedItemsCount);
} else {
_this.itemHeights.reset();
}
if (!widthHasChanged) {
// The call to `this.onNewItemsRendered()` must precede the call to
// `.measureItemHeights()` which is called in `.onRender()` because
// `this.onNewItemsRendered()` updates `firstMeasuredItemIndex` and
// `lastMeasuredItemIndex` of `this.itemHeights` in case of a prepend.
//
// If after prepending items the scroll position
// should be "restored" so that there's no "jump" of content
// then it means that all previous items have just been rendered
// in a single pass, and there's no need to update layout again.
//
if (onNewItemsRendered.call(_this, itemsDiff, newState) !== 'SEAMLESS_PREPEND') {
layoutUpdateReason = _Layout.LAYOUT_REASON.ITEMS_CHANGED;
if (!widthHasChanged) {
// The call to `this.onNewItemsRendered()` must precede the call to
// `.measureItemHeights()` which is called in `.onRender()` because
// `this.onNewItemsRendered()` updates `firstMeasuredItemIndex` and
// `lastMeasuredItemIndex` of `this.itemHeights` in case of a prepend.
//
// If after prepending items the scroll position
// should be "restored" so that there's no "jump" of content
// then it means that all previous items have just been rendered
// in a single pass, and there's no need to update layout again.
//
if (onNewItemsRendered.call(_this, itemsDiff, newState) !== 'SEAMLESS_PREPEND') {
layoutUpdateReason = _Layout.LAYOUT_REASON.ITEMS_CHANGED;
}
}

@@ -153,3 +196,3 @@ }

if (newState.firstShownItemIndex !== prevState.firstShownItemIndex || newState.lastShownItemIndex !== prevState.lastShownItemIndex || newState.items !== prevState.items || widthHasChanged) {
if (prevState && (newState.firstShownItemIndex !== prevState.firstShownItemIndex || newState.lastShownItemIndex !== prevState.lastShownItemIndex || newState.items !== prevState.items) || widthHasChanged) {
var verticalSpacingStateUpdate = _this.measureItemHeightsAndSpacing();

@@ -227,3 +270,3 @@

var i = _Object$keys[_i];
itemHeights[prependedItemsCount + parseInt(i)] = this.itemHeightsThatChangedWhileNewItemsWereBeingRendered[i];
itemHeights[prependedItemsCount + Number(i)] = this.itemHeightsThatChangedWhileNewItemsWereBeingRendered[i];
}

@@ -236,3 +279,3 @@ } // See if any items' states changed while new items were being rendered.

var _i3 = _Object$keys2[_i2];
itemStates[prependedItemsCount + parseInt(_i3)] = this.itemStatesThatChangedWhileNewItemsWereBeingRendered[_i3];
itemStates[prependedItemsCount + Number(_i3)] = this.itemStatesThatChangedWhileNewItemsWereBeingRendered[_i3];
}

@@ -350,4 +393,9 @@ }

var nonMeasuredItemsHaveBeenRendered = this.firstNonMeasuredItemIndex !== undefined; // Reset `this.firstNonMeasuredItemIndex` flag.
var nonMeasuredItemsHaveBeenRendered = this.firstNonMeasuredItemIndex !== undefined;
if (nonMeasuredItemsHaveBeenRendered) {
(0, _debug["default"])('Non-measured item index', this.firstNonMeasuredItemIndex);
} // Reset `this.firstNonMeasuredItemIndex` flag.
this.firstNonMeasuredItemIndex = undefined; // Reset `this.newItemsWillBeRendered` flag.

@@ -359,5 +407,9 @@

this.itemStatesThatChangedWhileNewItemsWereBeingRendered = undefined;
this.itemStatesThatChangedWhileNewItemsWereBeingRendered = undefined; // Reset `this.updateLayoutAfterRenderBecauseItemHeightChanged`.
var itemHeightHasChanged = this.updateLayoutAfterRenderBecauseItemHeightChanged;
this.updateLayoutAfterRenderBecauseItemHeightChanged = undefined;
return {
nonMeasuredItemsHaveBeenRendered: nonMeasuredItemsHaveBeenRendered,
itemHeightHasChanged: itemHeightHasChanged,
widthHasChanged: widthHasChanged

@@ -364,0 +416,0 @@ };

@@ -76,3 +76,9 @@ "use strict";

_this.getState().itemStates[i] = newItemState; // Schedule the item state update for after the new items have been rendered.
_this.getState().itemStates[i] = newItemState; // If there was a request for `setState()` with new `items`, then the changes
// to `currentState.itemStates[]` made above would be overwritten when that
// pending `setState()` call gets applied.
// To fix that, the updates to current `itemStates[]` are noted in
// `this.itemStatesThatChangedWhileNewItemsWereBeingRendered` variable.
// That variable is then checked when the `setState()` call with the new `items`
// has been updated.

@@ -106,7 +112,16 @@ if (_this.newItemsWillBeRendered) {

_this._isSettingNewItems = undefined; // Update `state`.
_this._isSettingNewItems = undefined;
_this.waitingForRender = true; // Store previous `state`.
_this.previousState = _this.getState();
_this.previousState = _this.getState(); // If it's the first call to `this.updateState()` then initialize
// the most recent `setState()` value to be the current state.
_this._updateState(stateUpdate);
if (!_this.mostRecentSetStateValue) {
_this.mostRecentSetStateValue = _this.getState();
} // Accumulates all "pending" state updates until they have been applied.
_this.mostRecentSetStateValue = _objectSpread(_objectSpread({}, _this.mostRecentSetStateValue), stateUpdate); // Update `state`.
_this._setState(_this.mostRecentSetStateValue, stateUpdate);
};

@@ -126,2 +141,3 @@

var getState = _ref2.getState,
setState = _ref2.setState,
updateState = _ref2.updateState;

@@ -138,12 +154,23 @@

if (render) {
throw new Error('[virtual-scroller] Creating a `VirtualScroller` class instance with a `render()` parameter means using the default (internal) state storage');
throw new Error('[virtual-scroller] Creating a `VirtualScroller` class instance with a `render()` parameter implies using the default (internal) state storage');
}
if (!getState || !updateState) {
throw new Error('[virtual-scroller] When using a custom state storage, one must supply both `getState()` and `updateState()` functions');
if (setState && updateState) {
throw new Error('[virtual-scroller] When using a custom state storage, one must supply either `setState()` or `updateState()` function but not both');
}
if (!getState || !(setState || updateState)) {
throw new Error('[virtual-scroller] When using a custom state storage, one must supply both `getState()` and `setState()`/`updateState()` functions');
}
_this._usesCustomStateStorage = true;
_this._getState = getState;
_this._updateState = updateState;
_this._setState = function (newState, stateUpdate) {
if (setState) {
setState(newState);
} else {
updateState(stateUpdate);
}
};
};

@@ -154,7 +181,7 @@

throw new Error('[virtual-scroller] When using the default (internal) state management, one must supply a `render(state, prevState)` function parameter');
} // Create default `getState()`/`updateState()` functions.
} // Create default `getState()`/`setState()` functions.
_this._getState = defaultGetState.bind(_this);
_this._updateState = defaultUpdateState.bind(_this); // When `state` is stored externally, a developer is responsible for
_this._setState = defaultSetState.bind(_this); // When `state` is stored externally, a developer is responsible for
// initializing it with the initial value.

@@ -175,10 +202,15 @@ // Otherwise, if default state management is used, set the initial state now.

function defaultUpdateState(stateUpdate) {
// Because this variant of `.updateState()` is "synchronous" (immediate),
// it can be written like `...prevState`, and no state updates would be lost.
// But if it was "asynchronous" (not immediate), then `...prevState`
// wouldn't work in all cases, because it could be stale in cases
// when more than a single `updateState()` call is made before
// the state actually updates, making `prevState` stale.
this.state = _objectSpread(_objectSpread({}, this.state), stateUpdate);
function defaultSetState(newState, stateUpdate) {
// // Because the default state updates are "synchronous" (immediate),
// // the `...stateUpdate` could be applied over `...this.state`,
// // and no state updates would be lost.
// // But if it was "asynchronous" (not immediate), then `...this.state`
// // wouldn't work in all cases, because it could be stale in cases
// // when more than a single `setState()` call is made before
// // the state actually updates, making some properties of `this.state` stale.
// this.state = {
// ...this.state,
// ...stateUpdate
// }
this.state = newState;
render(this.state, this.previousState);

@@ -185,0 +217,0 @@ this.onRender();

@@ -102,1 +102,7 @@ export type ItemHeight = number | undefined;

}
export class ItemNotRenderedError {
constructor(
message: string
);
}
export { default } from './modules/VirtualScroller.js'
export { default as ItemNotRenderedError } from './modules/ItemNotRenderedError.js'

@@ -7,2 +7,4 @@ function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

import ItemNotRenderedError from '../ItemNotRenderedError.js';
var ItemsContainer = /*#__PURE__*/function () {

@@ -25,5 +27,8 @@ /**

if (renderedElementIndex > childNodes.length - 1) {
console.log('~ Items Container Contents ~');
console.log(this.getElement().innerHTML);
throw new Error("Element with index ".concat(renderedElementIndex, " was not found in the list of Rendered Item Elements in the Items Container of Virtual Scroller. There're only ").concat(childNodes.length, " Elements there."));
// console.log('~ Items Container Contents ~')
// console.log(this.getElement().innerHTML)
throw new ItemNotRenderedError({
renderedElementIndex: renderedElementIndex,
renderedElementsCount: childNodes.length
});
}

@@ -30,0 +35,0 @@

@@ -166,2 +166,8 @@ import Layout from './Layout.js';

var shouldResetGridLayout;
var errors = [];
global.VirtualScrollerCatchError = function (error) {
return errors.push(error);
};
layout.getLayoutUpdateForItemsDiff({

@@ -188,2 +194,6 @@ firstShownItemIndex: 3,

});
global.VirtualScrollerCatchError = undefined;
errors.length.should.equal(2);
errors[0].message.should.equal('[virtual-scroller] ~ Prepended items count 5 is not divisible by Columns Count 4 ~');
errors[1].message.should.equal('[virtual-scroller] Layout reset required');
shouldResetGridLayout.should.equal(true);

@@ -190,0 +200,0 @@ });

@@ -0,1 +1,2 @@

import log from '../utility/debug.js';
import { useRef, useMemo, useCallback } from 'react';

@@ -27,8 +28,11 @@ export default function useItemKeys(_ref) {

generateItemKeyPrefix();
}, []);
}, []); // If `getItemId()` function is defined, then item `id`s are gonna be the item element `key`s.
var usesAutogeneratedItemKeys = !getItemId;
var generateItemKeyPrefixIfNotUsingItemIds = useCallback(function () {
if (!getItemId) {
if (usesAutogeneratedItemKeys) {
generateItemKeyPrefix();
log('React: ~ Item key prefix:', itemKeyPrefix.current);
}
}, [getItemId, generateItemKeyPrefix]);
}, [usesAutogeneratedItemKeys, generateItemKeyPrefix]);
/**

@@ -50,2 +54,3 @@ * Returns a `key` for an `item`'s element.

getItemKey: getItemKey,
usesAutogeneratedItemKeys: usesAutogeneratedItemKeys,
updateItemKeysForNewItems: generateItemKeyPrefixIfNotUsingItemIds

@@ -52,0 +57,0 @@ };

@@ -1,7 +0,1 @@

function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; }
function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; }
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest(); }

@@ -19,3 +13,5 @@

import { useState, useRef, useCallback, useLayoutEffect } from 'react'; // Creates state management functions.
import log, { isDebug } from '../utility/debug.js';
import getStateSnapshot from '../utility/getStateSnapshot.js';
import { useState, useRef, useCallback, useLayoutEffect, useInsertionEffect } from 'react'; // Creates state management functions.

@@ -25,12 +21,9 @@ export default function _useState(_ref) {

onRender = _ref.onRender,
itemsProperty = _ref.itemsProperty,
USE_ITEMS_UPDATE_NO_SECOND_RENDER_OPTIMIZATION = _ref.USE_ITEMS_UPDATE_NO_SECOND_RENDER_OPTIMIZATION;
itemsProperty = _ref.itemsProperty;
// This is a utility state variable that is used to re-render the component.
// It should not be used to access the current `VirtualScroller` state.
// It's more of a "requested" `VirtualScroller` state.
//
// It will also be stale in cases when `USE_ITEMS_UPDATE_NO_SECOND_RENDER_OPTIMIZATION`
// feature is used for setting new `items` in state.
//
// This is a state variable that is used to re-render the component.
// Right after the component has finished re-rendering,
// `VirtualScroller` state gets updated from this variable.
// The reason for that is that `VirtualScroller` state must always
// correspond exactly to what's currently rendered on the screen.
var _useState2 = useState(initialState),

@@ -44,70 +37,160 @@ _useState3 = _slicedToArray(_useState2, 2),

var state = useRef(initialState);
var getState = useCallback(function () {
return state.current;
}, []);
var setState = useCallback(function (newState) {
state.current = newState;
}, []); // Accumulates all "pending" state updates until they have been applied.
}, []); // Updating of the actual `VirtualScroller` state is done in a
// `useInsertionEffect()` rather than in a `useLayoutEffect()`.
//
// The reason is that using `useLayoutEffect()` would result in
// "breaking" the `<VirtualScroller/>` when an `itemComponent`
// called `onHeightDidChange()` from its own `useLayoutEffect()`.
// In those cases, the `itemCompoent`'s effect would run before
// the `<VirtualScroller/>`'s effect, resulting in
// `VirtualScroller.onItemHeightDidChange(i)` being run at a moment in time
// when the DOM has already been updated for the next `VirtualScroller` state
// but the actual `VirtualScroller` state is still a previous ("stale") one
// containing "stale" first/last shown item indexes, which would result in an
// "index out of bounds" error when `onItemHeightDidChange(i)` tries to access
// and measure the DOM element from item index `i` which doesn't already/yet exist.
//
// An example of such situation could be seen from a `VirtualScroller` debug log
// which was captured for a case when using `useLayoutEffect()` to update the
// "actual" `VirtualScroller` state after the corresponding DOM changes have been applied:
// The user has scrolled far enough: perform a re-layout
// ~ Update Layout (on scroll) ~
//
// Item index 2 height is required for calculations but hasn't been measured yet. Mark the item as "shown", rerender the list, measure the item's height and redo the layout.
//
// ~ Calculated Layout ~
// Columns count 1
// First shown item index 2
// Last shown item index 5
// …
// Item heights (231) [1056.578125, 783.125, empty × 229]
// Item states (231) [{…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, …]
//
// ~ Set state ~
// {firstShownItemIndex: 2, lastShownItemIndex: 5, …}
//
// ~ Rendered ~
// State {firstShownItemIndex: 2, lastShownItemIndex: 5, …}
//
// ~ Measure item heights ~
// Item index 2 height 719.8828125
// Item index 3 height 961.640625
// Item index 4 height 677.6640625
// Item index 5 height 1510.1953125
//
// ~ Update Layout (on non-measured item heights have been measured) ~
//
// ~ Calculated Layout ~
// Columns count 1
// First shown item index 4
// Last shown item index 5
// …
// Item heights (231) [1056.578125, 783.125, 719.8828125, 961.640625, 677.6640625, 1510.1953125, empty × 225]
// Item states (231) [{…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, …]
//
// ~ Set state ~
// {firstShownItemIndex: 4, lastShownItemIndex: 5, beforeItemsHeight: 3521.2265625, afterItemsHeight: 214090.72265624942}
//
// ~ On Item Height Did Change was called ~
// Item index 5
// ~ Re-measure item height ~
// ERROR "onItemHeightDidChange()" has been called for item index 5 but the item is not currently rendered and can't be measured. The exact error was: Element with index 3 was not found in the list of Rendered Item Elements in the Items Container of Virtual Scroller. There're only 2 Elements there.
//
// React: ~ The requested state is about to be applied in DOM. Set it as the `VirtualScroller` state. ~
// {firstShownItemIndex: 4, lastShownItemIndex: 5, …}
//
// ~ Rendered ~
// "~ Rendered ~" is what gets output when `onRender()` function gets called.
// It means that `useLayoutEffect()` was triggered after `onItemHeightDidChange(i)`
// was called and after the "ERROR" happened.
//
// The "ERROR" happened because new item indexes 4…5 were actually rendered instead of
// item indexes 2…5 by the time the application called `onItemHeightDidChange(i)` function
// inside `itemComponent`'s `useLayoutEffect()`.
// Item indexes 4…5 is what was requested in a `setState()` call, which called `_setNewState()`.
// This means that `_newState` changes have been applied to the DOM
// but `useLayoutEffect()` wasn't triggered immediately after that.
// Instead, it was triggered a right after the `itemComponent`'s `useLayoutEffect()`
// because child effects run before parent effects.
// So, the `itemComponent`'s `onHeightDidChange()` function call caught the
// `VirtualScroller` in an inconsistent state.
//
// To fix that, `useLayoutEffect()` gets replaced with `useInsertionEffect()`:
// https://blog.saeloun.com/2022/06/02/react-18-useinsertioneffect
// https://beta.reactjs.org/reference/react/useInsertionEffect
//
// After replacing `useLayoutEffect()` with `useInsertionEffect()`,
// the log shows that there's no more error:
//
// ~ Set state ~
// {firstShownItemIndex: 0, lastShownItemIndex: 2, …}
//
// React: ~ The requested state is about to be applied in DOM. Set it as the `VirtualScroller` state. ~
// {firstShownItemIndex: 0, lastShownItemIndex: 2, …}
//
// ~ On Item Height Did Change was called ~
// Item index 0
// ~ Re-measure item height ~
// Previous height 917
// New height 1064.453125
// ~ Item height has changed ~
//
// An alternative solution would be demanding the `itemComponent` to
// accept a `ref` and then measuring the corresponding DOM element height
// directly using the `ref`-ed DOM element rather than searching for that
// DOM element in the `ItemsContainer`.
// So if `useInsertionEffect()` gets removed from React in some hypothetical future,
// it could be replaced with using `ref`s on `ItemComponent`s to measure the DOM element heights.
//
var nextState = useRef(initialState); // Updates the actual `VirtualScroller` state right after a requested state update
// has been applied. Doesn't do anything at initial render.
useInsertionEffect(function () {
// Update the actual `VirtualScroller` state right before the DOM changes
// are going to be applied for the requested state update.
//
// This hook will run right before `useLayoutEffect()`.
//
// It doesn't make any difference which one of the two hooks to use to update
// the actual `VirtualScroller` state in this scenario because the two hooks
// run synchronously one right after another (insertion effect → DOM update → layout effect)
// without any free space for any `VirtualScroller` code (like the scroll event handler)
// to squeeze in and run in-between them, so the `VirtualScroller`'s `state`
// is always gonna stay consistent with what's currently rendered on screen
// from the `VirtualScroler`'s point of view, and the short transition period
// it simply doesn't see because it doesn't "wake up" during that period.
//
// Updating the actual `VirtualScroller` state right before `useLayoutEffect()`
// fixes the bug when an `itemComponent` calls `onHeightDidChange()` in its own
// `useLayoutEffect()` which would run before this `useLayoutEffect()`
// because children's effects run before parent's.
//
// This hook doesn't do anything at the initial render.
//
if (isDebug()) {
log('React: ~ The requested state is about to be applied in DOM. Set it as the `VirtualScroller` state. ~');
log(getStateSnapshot(_newState));
}
useLayoutEffect(function () {
setState(_newState);
}, [_newState]); // Calls `onRender()` right after every state update (which is a re-render),
// and also right after the initial render.
}, [_newState]);
useLayoutEffect(function () {
// Call `onRender()` right after a requested state update has been applied,
// and also right after the initial render.
onRender();
}, [_newState, // When using `USE_ITEMS_UPDATE_NO_SECOND_RENDER_OPTIMIZATION` feature,
// there won't be a `_setNewState()` function call when `items` property changes,
// hence the additional `itemsProperty` dependency.
USE_ITEMS_UPDATE_NO_SECOND_RENDER_OPTIMIZATION ? itemsProperty : undefined]);
}, [_newState]);
return {
getState: function getState() {
return state.current;
},
getNextState: function getNextState() {
return nextState.current;
},
// This is the state the component should render.
stateToRender: _newState,
// Returns the current state of the `VirtualScroller`.
// This function is used in the `VirtualScroller` itself
// because the `state` is managed outside of it.
getState: getState,
// Requests a state update.
//
// State updates are incremental meaning that this function mimicks
// the classic `React.Component`'s `this.setState()` behavior
// when calling `this.setState()` didn't replace `state` but rather merged
// the updated state properties over the "old" state properties.
//
// The reason for using pending state updates accumulation is that
// `useState()` updates are "asynchronous" (not immediate),
// and simply merging over `...state` would merge over potentially stale
// property values in cases when more than a single `updateState()` call is made
// before the state actually updates, resulting in losing some of those state updates.
//
// Example: the first `updateState()` call updates shown item indexes,
// and the second `updateState()` call updates `verticalSpacing`.
// If it was simply `updateState({ ...state, ...stateUpdate })`
// then the second state update could overwrite the first state update,
// resulting in incorrect items being shown/hidden.
//
updateState: function updateState(stateUpdate) {
nextState.current = _objectSpread(_objectSpread({}, nextState.current), stateUpdate); // If `items` property did change, the component detects it at render time
// and updates `VirtualScroller` items immediately by calling `.setItems()`,
// which, in turn, immediately calls this `updateState()` function
// with a `stateUpdate` argument that contains the new `items`,
// so checking for `stateUpdate.items` could detect situations like that.
//
// When the initial `VirtualScroller` state is being set, it contains the `.items`
// property too, but that initial setting is done using another function called
// `setInitialState()`, so using `if (stateUpdate.items)` condition here for describing
// just the case when `state` has been updated as a result of a `setItems()` call
// seems to be fine.
//
var _newState = nextState.current;
if (stateUpdate.items && USE_ITEMS_UPDATE_NO_SECOND_RENDER_OPTIMIZATION) {
setState(_newState);
} else {
_setNewState(_newState);
}
}
setState: _setNewState
};
}
//# sourceMappingURL=useState.js.map
import px from '../utility/px.js';
export default function useStyle(_ref) {
var tbody = _ref.tbody,
getNextState = _ref.getNextState;
state = _ref.state;

@@ -10,6 +10,4 @@ if (tbody) {

var _getNextState = getNextState(),
beforeItemsHeight = _getNextState.beforeItemsHeight,
afterItemsHeight = _getNextState.afterItemsHeight;
var beforeItemsHeight = state.beforeItemsHeight,
afterItemsHeight = state.afterItemsHeight;
return {

@@ -16,0 +14,0 @@ paddingTop: px(beforeItemsHeight),

@@ -18,28 +18,16 @@ var _excluded = ["as", "items", "itemComponent", "itemComponentProps", "estimatedItemHeight", "getEstimatedItemHeight", "getEstimatedVisibleItemRowsCount", "bypass", "tbody", "preserveScrollPosition", "preserveScrollPositionOnPrependItems", "measureItemsBatchSize", "scrollableContainer", "getScrollableContainer", "getColumnsCount", "getItemId", "className", "onMount", "onItemFirstRender", "onItemInitialRender", "initialScrollPosition", "onScrollPositionChange", "onStateChange", "initialState", "getInitialItemState"];

import useOnItemHeightDidChange from './useOnItemHeightDidChange.js';
import useHandleItemsPropertyChange from './useHandleItemsPropertyChange.js';
import useHandleItemIndexesChange from './useHandleItemIndexesChange.js';
import useSetNewItemsOnItemsPropertyChange from './useSetNewItemsOnItemsPropertyChange.js';
import useUpdateItemKeysOnItemsChange from './useUpdateItemKeysOnItemsChange.js';
import useClassName from './useClassName.js';
import useStyle from './useStyle.js'; // When `items` property changes, `useHandleItemsPropertyChange()` hook detects that
// and calls `VirtualScroller.setItems()` which in turn calls the `updateState()` function.
// At this point, an insignificant optimization could be applied:
// the component could avoid re-rendering the second time.
// Instead, the state update could be applied "immediately" if it originated
// from `.setItems()` function call, eliminating the unneeded second re-render.
//
// I could see how this minor optimization could get brittle when modifiying the code,
// so I put it under a feature flag so that it could potentially be turned off
// in case of any potential weird issues in some future.
//
// Another reason for using this feature is:
//
// Since `useHandleItemsPropertyChange()` runs at render time
// and not after the render has finished (not in an "effect"),
// if the state update was done "conventionally" (by calling `_setNewState()`),
// React would throw an error about updating state during render.
// No one knows what the original error message was.
// Perhaps it's no longer relevant in newer versions of React.
//
import useStyle from './useStyle.js'; // When `items` property changes:
// * A new `items` property is supplied to the React component.
// * The React component re-renders itself.
// * `useSetNewItemsOnItemsPropertyChange()` hook is run.
// * `useSetNewItemsOnItemsPropertyChange()` hook detects that the `items` property
// has changed and calls `VirtualScroller.setItems(items)`.
// * `VirtualScroller.setItems(items)` calls `VirtualScroller.setState()`.
// * `VirtualScroller.setState()` calls the `setState()` function.
// * The `setState()` function calls a setter from a `useState()` hook.
// * The React component re-renders itself the second time.
var USE_ITEMS_UPDATE_NO_SECOND_RENDER_OPTIMIZATION = true;
function VirtualScroller(_ref, ref) {

@@ -115,8 +103,7 @@ var AsComponent = _ref.as,

onRender: virtualScroller.onRender,
itemsProperty: itemsProperty,
USE_ITEMS_UPDATE_NO_SECOND_RENDER_OPTIMIZATION: USE_ITEMS_UPDATE_NO_SECOND_RENDER_OPTIMIZATION
itemsProperty: itemsProperty
}),
getState = _useState.getState,
updateState = _useState.updateState,
getNextState = _useState.getNextState; // Use custom (external) state storage in the `VirtualScroller`.
setState = _useState.setState,
stateToRender = _useState.stateToRender; // Use custom (external) state storage in the `VirtualScroller`.

@@ -127,3 +114,3 @@

getState: getState,
updateState: updateState
setState: setState
});

@@ -140,2 +127,3 @@ }, []); // Start `VirtualScroller` on mount.

getItemKey = _useItemKeys.getItemKey,
usesAutogeneratedItemKeys = _useItemKeys.usesAutogeneratedItemKeys,
updateItemKeysForNewItems = _useItemKeys.updateItemKeysForNewItems; // Cache per-item `setItemState` functions' "references"

@@ -156,3 +144,3 @@ // so that item components don't get re-rendered needlessly.

useHandleItemsPropertyChange(itemsProperty, {
useSetNewItemsOnItemsPropertyChange(itemsProperty, {
virtualScroller: virtualScroller,

@@ -162,9 +150,8 @@ // `preserveScrollPosition` property name is deprecated,

preserveScrollPosition: preserveScrollPosition,
preserveScrollPositionOnPrependItems: preserveScrollPositionOnPrependItems,
nextItems: getNextState().items
preserveScrollPositionOnPrependItems: preserveScrollPositionOnPrependItems
}); // Updates `key`s if item indexes have changed.
useHandleItemIndexesChange({
useUpdateItemKeysOnItemsChange(stateToRender.items, {
virtualScroller: virtualScroller,
itemsBeingRendered: getNextState().items,
usesAutogeneratedItemKeys: usesAutogeneratedItemKeys,
updateItemKeysForNewItems: updateItemKeysForNewItems

@@ -203,11 +190,8 @@ }); // Add instance methods to the React component.

tbody: tbody,
getNextState: getNextState
state: stateToRender
});
var _getNextState = getNextState(),
currentItems = _getNextState.items,
itemStates = _getNextState.itemStates,
firstShownItemIndex = _getNextState.firstShownItemIndex,
lastShownItemIndex = _getNextState.lastShownItemIndex;
var currentItems = stateToRender.items,
itemStates = stateToRender.itemStates,
firstShownItemIndex = stateToRender.firstShownItemIndex,
lastShownItemIndex = stateToRender.lastShownItemIndex;
return /*#__PURE__*/React.createElement(AsComponent, _extends({}, rest, {

@@ -214,0 +198,0 @@ ref: container,

@@ -7,2 +7,4 @@ function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

import ItemNotRenderedError from '../ItemNotRenderedError.js';
var ItemsContainer = /*#__PURE__*/function () {

@@ -34,2 +36,10 @@ /**

var startNewRow = true;
if (renderedElementIndex > children.length - 1) {
throw new ItemNotRenderedError({
renderedElementIndex: renderedElementIndex,
renderedElementsCount: children.length
});
}
var i = 0;

@@ -71,3 +81,12 @@

value: function getNthRenderedItemHeight(renderedElementIndex) {
return this.getElement().children[renderedElementIndex].height;
var children = this.getElement().children;
if (renderedElementIndex > children.length - 1) {
throw new ItemNotRenderedError({
renderedElementIndex: renderedElementIndex,
renderedElementsCount: children.length
});
}
return children[renderedElementIndex].height;
}

@@ -74,0 +93,0 @@ /**

@@ -39,3 +39,6 @@ function _toConsumableArray(arr) { return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _unsupportedIterableToArray(arr) || _nonIterableSpread(); }

}
export function reportError() {
function error() {
var _console3;
for (var _len3 = arguments.length, args = new Array(_len3), _key3 = 0; _key3 < _len3; _key3++) {

@@ -45,2 +48,14 @@ args[_key3] = arguments[_key3];

(_console3 = console).error.apply(_console3, _toConsumableArray(['[virtual-scroller]'].concat(args)));
}
export function reportError() {
for (var _len4 = arguments.length, args = new Array(_len4), _key4 = 0; _key4 < _len4; _key4++) {
args[_key4] = arguments[_key4];
}
var createError = function createError() {
return new Error(['[virtual-scroller]'].concat(args).join(' '));
};
if (typeof window !== 'undefined') {

@@ -50,3 +65,3 @@ // In a web browser.

// at which point did the error occur between other debug logs.
log.apply(this, ['ERROR'].concat(args));
error.apply(this, ['ERROR'].concat(args));
setTimeout(function () {

@@ -59,9 +74,19 @@ // Throw an error in a timeout so that it doesn't interrupt the application's flow.

// but those don't seem to be used in any of the error messages.
throw new Error(['[virtual-scroller]'].concat(args).join(' '));
throw createError();
}, 0);
} else {
var _console3;
// In Node.js.
// If tests are being run, throw in case of any errors.
var catchError = getGlobalVariable('VirtualScrollerCatchError');
// On a server.
(_console3 = console).error.apply(_console3, _toConsumableArray(['[virtual-scroller]'].concat(args)));
if (catchError) {
return catchError(createError());
}
if (getGlobalVariable('VirtualScrollerThrowErrors')) {
throw createError();
} // Print the error in the console.
error.apply(this, ['ERROR'].concat(args));
}

@@ -68,0 +93,0 @@ }

@@ -85,4 +85,5 @@ function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; }

if (!isRestart) {
// If no custom state storage has been configured, use the default one.
this.waitingForRender = true; // If no custom state storage has been configured, use the default one.
// Also sets the initial state.
if (!this._usesCustomStateStorage) {

@@ -89,0 +90,0 @@ this.useDefaultStateStorage();

@@ -14,2 +14,3 @@ function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; }

import { LAYOUT_REASON } from './Layout.js';
import ItemNotRenderedError from './ItemNotRenderedError.js';
export default function () {

@@ -156,3 +157,6 @@ var _this = this;

this.firstNonMeasuredItemIndex = firstNonMeasuredItemIndex; // Set "previously calculated layout".
this.firstNonMeasuredItemIndex = firstNonMeasuredItemIndex; // if (firstNonMeasuredItemIndex !== undefined) {
// log('Non-measured item index that will be measured at next layout', firstNonMeasuredItemIndex)
// }
// Set "previously calculated layout".
//

@@ -353,17 +357,19 @@ // The "previously calculated layout" feature is not currently used.

function updatePreviouslyCalculatedLayoutOnItemHeightChange(i, previousHeight, newHeight) {
if (this.previouslyCalculatedLayout) {
var prevLayout = this.previouslyCalculatedLayout;
if (prevLayout) {
var heightDifference = newHeight - previousHeight;
if (i < this.previouslyCalculatedLayout.firstShownItemIndex) {
// Patch `this.previouslyCalculatedLayout`'s `.beforeItemsHeight`.
this.previouslyCalculatedLayout.beforeItemsHeight += heightDifference;
} else if (i > this.previouslyCalculatedLayout.lastShownItemIndex) {
// Could patch `.afterItemsHeight` of `this.previouslyCalculatedLayout` here,
// if `.afterItemsHeight` property existed in `this.previouslyCalculatedLayout`.
if (this.previouslyCalculatedLayout.afterItemsHeight !== undefined) {
this.previouslyCalculatedLayout.afterItemsHeight += heightDifference;
if (i < prevLayout.firstShownItemIndex) {
// Patch `prevLayout`'s `.beforeItemsHeight`.
prevLayout.beforeItemsHeight += heightDifference;
} else if (i > prevLayout.lastShownItemIndex) {
// Could patch `.afterItemsHeight` of `prevLayout` here,
// if `.afterItemsHeight` property existed in `prevLayout`.
if (prevLayout.afterItemsHeight !== undefined) {
prevLayout.afterItemsHeight += heightDifference;
}
} else {
// Patch `this.previouslyCalculatedLayout`'s shown items height.
this.previouslyCalculatedLayout.shownItemsHeight += newHeight - previousHeight;
// Patch `prevLayout`'s shown items height.
prevLayout.shownItemsHeight += newHeight - previousHeight;
}

@@ -389,3 +395,3 @@ }

this._onItemHeightDidChange = function (i) {
log('~ Re-measure item height ~');
log('~ On Item Height Did Change was called ~');
log('Item index', i);

@@ -431,6 +437,18 @@

if (previousHeight === undefined) {
return reportError("\"onItemHeightDidChange()\" has been called for item ".concat(i, ", but that item hasn't been rendered before."));
return reportError("\"onItemHeightDidChange()\" has been called for item index ".concat(i, " but the item hasn't been rendered before."));
}
var newHeight = remeasureItemHeight.call(_this, i);
log('~ Re-measure item height ~');
var newHeight;
try {
newHeight = remeasureItemHeight.call(_this, i);
} catch (error) {
// Successfully finishing an `onItemHeightDidChange(i)` call is not considered
// critical for `VirtualScroller`'s operation, so such errors could be ignored.
if (error instanceof ItemNotRenderedError) {
return reportError("\"onItemHeightDidChange()\" has been called for item index ".concat(i, " but the item is not currently rendered and can't be measured. The exact error was: ").concat(error.message));
}
}
log('Previous height', previousHeight);

@@ -440,9 +458,30 @@ log('New height', newHeight);

if (previousHeight !== newHeight) {
log('~ Item height has changed ~'); // Update or reset previously calculated layout.
log('~ Item height has changed. Should update layout. ~'); // Update or reset a previously calculated layout
// so that the "diff"s based on that layout in the future
// produce correct results.
updatePreviouslyCalculatedLayoutOnItemHeightChange.call(_this, i, previousHeight, newHeight); // Recalculate layout.
//
// If the `VirtualScroller` is already waiting for a state update to be rendered,
// delay `onItemHeightDidChange(i)`'s re-layout until that state update is rendered.
// The reason is that React `<VirtualScroller/>`'s `onHeightDidChange()` is meant to
// be called inside `useLayoutEffect()` hook. Due to how React is implemented internally,
// that might happen in the middle of the currently pending `setState()` operation
// being applied, resulting in weird "race condition" bugs.
//
_this.onUpdateShownItemIndexes({
reason: LAYOUT_REASON.ITEM_HEIGHT_CHANGED
}); // Schedule the item height update for after the new items have been rendered.
if (_this.waitingForRender) {
log('~ Another state update is already waiting to be rendered. Delay the layout update until then. ~');
_this.updateLayoutAfterRenderBecauseItemHeightChanged = true;
} else {
_this.onUpdateShownItemIndexes({
reason: LAYOUT_REASON.ITEM_HEIGHT_CHANGED
});
} // If there was a request for `setState()` with new `items`, then the changes
// to `currentState.itemHeights[]` made above in a `remeasureItemHeight()` call
// would be overwritten when that pending `setState()` call gets applied.
// To fix that, the updates to current `itemHeights[]` are noted in
// `this.itemHeightsThatChangedWhileNewItemsWereBeingRendered` variable.
// That variable is then checked when the `setState()` call with the new `items`
// has been updated.

@@ -449,0 +488,0 @@

@@ -7,3 +7,3 @@ function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; }

import log, { warn, isDebug } from './utility/debug.js';
import log, { warn, reportError, isDebug } from './utility/debug.js';
import getStateSnapshot from './utility/getStateSnapshot.js';

@@ -22,2 +22,3 @@ import shallowEqual from './utility/shallowEqual.js';

this._onRender = function (newState, prevState) {
_this.waitingForRender = false;
log('~ Rendered ~');

@@ -40,6 +41,34 @@

setTbodyPadding(_this.getItemsContainerElement(), newState.beforeItemsHeight, newState.afterItemsHeight);
}
} // `this.mostRecentlySetState` checks that state management behavior is correct:
// that in situations when there're multiple new states waiting to be set,
// only the latest one gets applied.
// It keeps the code simpler and prevents possible race condition bugs.
// For example, `VirtualScroller` keeps track of its latest requested
// state update in different instance variable flags which assume that
// only that latest requested state update gets actually applied.
//
// This check should also be performed for the initial render in order to
// guarantee that no potentially incorrect state update goes unnoticed.
// Incorrect state updates could happen when `VirtualScroller` state
// is managed externally by passing `getState()`/`updateState()` options.
//
// Perform the check only when `this.mostRecentSetStateValue` is defined.
// `this.mostRecentSetStateValue` is normally gonna be `undefined` at the initial render
// because the initial state is not set by calling `this.updateState()`.
// At the same time, it is possible that the initial render is delayed
// for whatever reason, and `this.updateState()` gets called before the initial render,
// so `this.mostRecentSetStateValue` could also be defined at the initial render,
// in which case the check should be performed.
//
if (!prevState) {
return;
if (_this.mostRecentSetStateValue) {
// "Shallow equality" is used here instead of "strict equality"
// because a developer might choose to supply an `updateState()` function
// rather than a `setState()` function, in which case the `updateState()` function
// would construct its own state object.
if (!shallowEqual(newState, _this.mostRecentSetStateValue)) {
warn('The most recent state that was set', getStateSnapshot(_this.mostRecentSetStateValue));
reportError('The state that has been rendered is not the most recent one that was set');
}
} // `this.resetStateUpdateFlags()` must be called before calling

@@ -51,5 +80,16 @@ // `this.measureItemHeightsAndSpacing()`.

nonMeasuredItemsHaveBeenRendered = _resetStateUpdateFlag.nonMeasuredItemsHaveBeenRendered,
itemHeightHasChanged = _resetStateUpdateFlag.itemHeightHasChanged,
widthHasChanged = _resetStateUpdateFlag.widthHasChanged;
var layoutUpdateReason; // If the `VirtualScroller`, while calculating layout parameters, encounters
var layoutUpdateReason;
if (_this.updateLayoutAfterRenderBecauseItemHeightChanged) {
layoutUpdateReason = LAYOUT_REASON.ITEM_HEIGHT_CHANGED;
}
if (!prevState) {
if (!layoutUpdateReason) {
return;
}
} // If the `VirtualScroller`, while calculating layout parameters, encounters
// a not-shown item with a non-measured height, it calls `updateState()` just to

@@ -59,2 +99,3 @@ // render that item first, and then, after the list has been re-rendered, it measures

if (nonMeasuredItemsHaveBeenRendered) {

@@ -83,39 +124,41 @@ layoutUpdateReason = LAYOUT_REASON.NON_MEASURED_ITEMS_HAVE_BEEN_MEASURED;

var previousItems = prevState.items;
var newItems = newState.items; // Even if `this.newItemsWillBeRendered` flag is `true`,
// `newItems` could still be equal to `previousItems`.
// For example, when `updateState()` calls don't update `state` immediately
// and a developer first calls `setItems(newItems)` and then calls `setItems(oldItems)`:
// in that case, `this.newItemsWillBeRendered` flag will be `true` but the actual `items`
// in state wouldn't have changed due to the first `updateState()` call being overwritten
// by the second `updateState()` call (that's called "batching state updates" in React).
if (prevState) {
var previousItems = prevState.items;
var newItems = newState.items; // Even if `this.newItemsWillBeRendered` flag is `true`,
// `newItems` could still be equal to `previousItems`.
// For example, when `updateState()` calls don't update `state` immediately
// and a developer first calls `setItems(newItems)` and then calls `setItems(oldItems)`:
// in that case, `this.newItemsWillBeRendered` flag will be `true` but the actual `items`
// in state wouldn't have changed due to the first `updateState()` call being overwritten
// by the second `updateState()` call (that's called "batching state updates" in React).
if (newItems !== previousItems) {
var itemsDiff = _this.getItemsDiff(previousItems, newItems);
if (newItems !== previousItems) {
var itemsDiff = _this.getItemsDiff(previousItems, newItems);
if (itemsDiff) {
// The call to `.onPrepend()` must precede the call to `.measureItemHeights()`
// which is called in `.onRender()`.
// `this.itemHeights.onPrepend()` updates `firstMeasuredItemIndex`
// and `lastMeasuredItemIndex` of `this.itemHeights`.
var prependedItemsCount = itemsDiff.prependedItemsCount;
if (itemsDiff) {
// The call to `.onPrepend()` must precede the call to `.measureItemHeights()`
// which is called in `.onRender()`.
// `this.itemHeights.onPrepend()` updates `firstMeasuredItemIndex`
// and `lastMeasuredItemIndex` of `this.itemHeights`.
var prependedItemsCount = itemsDiff.prependedItemsCount;
_this.itemHeights.onPrepend(prependedItemsCount);
} else {
_this.itemHeights.reset();
}
_this.itemHeights.onPrepend(prependedItemsCount);
} else {
_this.itemHeights.reset();
}
if (!widthHasChanged) {
// The call to `this.onNewItemsRendered()` must precede the call to
// `.measureItemHeights()` which is called in `.onRender()` because
// `this.onNewItemsRendered()` updates `firstMeasuredItemIndex` and
// `lastMeasuredItemIndex` of `this.itemHeights` in case of a prepend.
//
// If after prepending items the scroll position
// should be "restored" so that there's no "jump" of content
// then it means that all previous items have just been rendered
// in a single pass, and there's no need to update layout again.
//
if (onNewItemsRendered.call(_this, itemsDiff, newState) !== 'SEAMLESS_PREPEND') {
layoutUpdateReason = LAYOUT_REASON.ITEMS_CHANGED;
if (!widthHasChanged) {
// The call to `this.onNewItemsRendered()` must precede the call to
// `.measureItemHeights()` which is called in `.onRender()` because
// `this.onNewItemsRendered()` updates `firstMeasuredItemIndex` and
// `lastMeasuredItemIndex` of `this.itemHeights` in case of a prepend.
//
// If after prepending items the scroll position
// should be "restored" so that there's no "jump" of content
// then it means that all previous items have just been rendered
// in a single pass, and there's no need to update layout again.
//
if (onNewItemsRendered.call(_this, itemsDiff, newState) !== 'SEAMLESS_PREPEND') {
layoutUpdateReason = LAYOUT_REASON.ITEMS_CHANGED;
}
}

@@ -134,3 +177,3 @@ }

if (newState.firstShownItemIndex !== prevState.firstShownItemIndex || newState.lastShownItemIndex !== prevState.lastShownItemIndex || newState.items !== prevState.items || widthHasChanged) {
if (prevState && (newState.firstShownItemIndex !== prevState.firstShownItemIndex || newState.lastShownItemIndex !== prevState.lastShownItemIndex || newState.items !== prevState.items) || widthHasChanged) {
var verticalSpacingStateUpdate = _this.measureItemHeightsAndSpacing();

@@ -208,3 +251,3 @@

var i = _Object$keys[_i];
itemHeights[prependedItemsCount + parseInt(i)] = this.itemHeightsThatChangedWhileNewItemsWereBeingRendered[i];
itemHeights[prependedItemsCount + Number(i)] = this.itemHeightsThatChangedWhileNewItemsWereBeingRendered[i];
}

@@ -217,3 +260,3 @@ } // See if any items' states changed while new items were being rendered.

var _i3 = _Object$keys2[_i2];
itemStates[prependedItemsCount + parseInt(_i3)] = this.itemStatesThatChangedWhileNewItemsWereBeingRendered[_i3];
itemStates[prependedItemsCount + Number(_i3)] = this.itemStatesThatChangedWhileNewItemsWereBeingRendered[_i3];
}

@@ -331,4 +374,9 @@ }

var nonMeasuredItemsHaveBeenRendered = this.firstNonMeasuredItemIndex !== undefined; // Reset `this.firstNonMeasuredItemIndex` flag.
var nonMeasuredItemsHaveBeenRendered = this.firstNonMeasuredItemIndex !== undefined;
if (nonMeasuredItemsHaveBeenRendered) {
log('Non-measured item index', this.firstNonMeasuredItemIndex);
} // Reset `this.firstNonMeasuredItemIndex` flag.
this.firstNonMeasuredItemIndex = undefined; // Reset `this.newItemsWillBeRendered` flag.

@@ -340,5 +388,9 @@

this.itemStatesThatChangedWhileNewItemsWereBeingRendered = undefined;
this.itemStatesThatChangedWhileNewItemsWereBeingRendered = undefined; // Reset `this.updateLayoutAfterRenderBecauseItemHeightChanged`.
var itemHeightHasChanged = this.updateLayoutAfterRenderBecauseItemHeightChanged;
this.updateLayoutAfterRenderBecauseItemHeightChanged = undefined;
return {
nonMeasuredItemsHaveBeenRendered: nonMeasuredItemsHaveBeenRendered,
itemHeightHasChanged: itemHeightHasChanged,
widthHasChanged: widthHasChanged

@@ -345,0 +397,0 @@ };

@@ -57,3 +57,9 @@ function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; }

_this.getState().itemStates[i] = newItemState; // Schedule the item state update for after the new items have been rendered.
_this.getState().itemStates[i] = newItemState; // If there was a request for `setState()` with new `items`, then the changes
// to `currentState.itemStates[]` made above would be overwritten when that
// pending `setState()` call gets applied.
// To fix that, the updates to current `itemStates[]` are noted in
// `this.itemStatesThatChangedWhileNewItemsWereBeingRendered` variable.
// That variable is then checked when the `setState()` call with the new `items`
// has been updated.

@@ -87,7 +93,16 @@ if (_this.newItemsWillBeRendered) {

_this._isSettingNewItems = undefined; // Update `state`.
_this._isSettingNewItems = undefined;
_this.waitingForRender = true; // Store previous `state`.
_this.previousState = _this.getState();
_this.previousState = _this.getState(); // If it's the first call to `this.updateState()` then initialize
// the most recent `setState()` value to be the current state.
_this._updateState(stateUpdate);
if (!_this.mostRecentSetStateValue) {
_this.mostRecentSetStateValue = _this.getState();
} // Accumulates all "pending" state updates until they have been applied.
_this.mostRecentSetStateValue = _objectSpread(_objectSpread({}, _this.mostRecentSetStateValue), stateUpdate); // Update `state`.
_this._setState(_this.mostRecentSetStateValue, stateUpdate);
};

@@ -107,2 +122,3 @@

var getState = _ref2.getState,
setState = _ref2.setState,
updateState = _ref2.updateState;

@@ -119,12 +135,23 @@

if (render) {
throw new Error('[virtual-scroller] Creating a `VirtualScroller` class instance with a `render()` parameter means using the default (internal) state storage');
throw new Error('[virtual-scroller] Creating a `VirtualScroller` class instance with a `render()` parameter implies using the default (internal) state storage');
}
if (!getState || !updateState) {
throw new Error('[virtual-scroller] When using a custom state storage, one must supply both `getState()` and `updateState()` functions');
if (setState && updateState) {
throw new Error('[virtual-scroller] When using a custom state storage, one must supply either `setState()` or `updateState()` function but not both');
}
if (!getState || !(setState || updateState)) {
throw new Error('[virtual-scroller] When using a custom state storage, one must supply both `getState()` and `setState()`/`updateState()` functions');
}
_this._usesCustomStateStorage = true;
_this._getState = getState;
_this._updateState = updateState;
_this._setState = function (newState, stateUpdate) {
if (setState) {
setState(newState);
} else {
updateState(stateUpdate);
}
};
};

@@ -135,7 +162,7 @@

throw new Error('[virtual-scroller] When using the default (internal) state management, one must supply a `render(state, prevState)` function parameter');
} // Create default `getState()`/`updateState()` functions.
} // Create default `getState()`/`setState()` functions.
_this._getState = defaultGetState.bind(_this);
_this._updateState = defaultUpdateState.bind(_this); // When `state` is stored externally, a developer is responsible for
_this._setState = defaultSetState.bind(_this); // When `state` is stored externally, a developer is responsible for
// initializing it with the initial value.

@@ -156,10 +183,15 @@ // Otherwise, if default state management is used, set the initial state now.

function defaultUpdateState(stateUpdate) {
// Because this variant of `.updateState()` is "synchronous" (immediate),
// it can be written like `...prevState`, and no state updates would be lost.
// But if it was "asynchronous" (not immediate), then `...prevState`
// wouldn't work in all cases, because it could be stale in cases
// when more than a single `updateState()` call is made before
// the state actually updates, making `prevState` stale.
this.state = _objectSpread(_objectSpread({}, this.state), stateUpdate);
function defaultSetState(newState, stateUpdate) {
// // Because the default state updates are "synchronous" (immediate),
// // the `...stateUpdate` could be applied over `...this.state`,
// // and no state updates would be lost.
// // But if it was "asynchronous" (not immediate), then `...this.state`
// // wouldn't work in all cases, because it could be stale in cases
// // when more than a single `setState()` call is made before
// // the state actually updates, making some properties of `this.state` stale.
// this.state = {
// ...this.state,
// ...stateUpdate
// }
this.state = newState;
render(this.state, this.previousState);

@@ -166,0 +198,0 @@ this.onRender();

{
"name": "virtual-scroller",
"version": "1.11.3",
"version": "1.12.0",
"description": "A component for efficiently rendering large lists of variable height items",

@@ -5,0 +5,0 @@ "main": "index.cjs",

@@ -0,1 +1,3 @@

import ItemNotRenderedError from '../ItemNotRenderedError.js'
export default class ItemsContainer {

@@ -13,5 +15,8 @@ /**

if (renderedElementIndex > childNodes.length - 1) {
console.log('~ Items Container Contents ~')
console.log(this.getElement().innerHTML)
throw new Error(`Element with index ${renderedElementIndex} was not found in the list of Rendered Item Elements in the Items Container of Virtual Scroller. There're only ${childNodes.length} Elements there.`)
// console.log('~ Items Container Contents ~')
// console.log(this.getElement().innerHTML)
throw new ItemNotRenderedError({
renderedElementIndex,
renderedElementsCount: childNodes.length
})
}

@@ -18,0 +23,0 @@ return childNodes[renderedElementIndex]

@@ -151,2 +151,6 @@ import Layout from './Layout.js'

const errors = []
global.VirtualScrollerCatchError = (error) => errors.push(error)
layout.getLayoutUpdateForItemsDiff(

@@ -175,4 +179,9 @@ {

global.VirtualScrollerCatchError = undefined
errors.length.should.equal(2)
errors[0].message.should.equal('[virtual-scroller] ~ Prepended items count 5 is not divisible by Columns Count 4 ~')
errors[1].message.should.equal('[virtual-scroller] Layout reset required')
shouldResetGridLayout.should.equal(true)
})
})

@@ -0,1 +1,3 @@

import log from '../utility/debug.js'
import { useRef, useMemo, useCallback } from 'react'

@@ -30,8 +32,12 @@

// If `getItemId()` function is defined, then item `id`s are gonna be the item element `key`s.
const usesAutogeneratedItemKeys = !getItemId
const generateItemKeyPrefixIfNotUsingItemIds = useCallback(() => {
if (!getItemId) {
if (usesAutogeneratedItemKeys) {
generateItemKeyPrefix()
log('React: ~ Item key prefix:', itemKeyPrefix.current)
}
}, [
getItemId,
usesAutogeneratedItemKeys,
generateItemKeyPrefix

@@ -58,4 +64,5 @@ ])

getItemKey,
usesAutogeneratedItemKeys,
updateItemKeysForNewItems: generateItemKeyPrefixIfNotUsingItemIds
}
}

@@ -1,3 +0,6 @@

import { useState, useRef, useCallback, useLayoutEffect } from 'react'
import log, { isDebug } from '../utility/debug.js'
import getStateSnapshot from '../utility/getStateSnapshot.js'
import { useState, useRef, useCallback, useLayoutEffect, useInsertionEffect } from 'react'
// Creates state management functions.

@@ -7,12 +10,9 @@ export default function _useState({

onRender,
itemsProperty,
USE_ITEMS_UPDATE_NO_SECOND_RENDER_OPTIMIZATION
itemsProperty
}) {
// This is a utility state variable that is used to re-render the component.
// It should not be used to access the current `VirtualScroller` state.
// It's more of a "requested" `VirtualScroller` state.
//
// It will also be stale in cases when `USE_ITEMS_UPDATE_NO_SECOND_RENDER_OPTIMIZATION`
// feature is used for setting new `items` in state.
//
// This is a state variable that is used to re-render the component.
// Right after the component has finished re-rendering,
// `VirtualScroller` state gets updated from this variable.
// The reason for that is that `VirtualScroller` state must always
// correspond exactly to what's currently rendered on the screen.
const [_newState, _setNewState] = useState(initialState)

@@ -24,2 +24,6 @@

const getState = useCallback(() => {
return state.current
}, [])
const setState = useCallback((newState) => {

@@ -29,74 +33,158 @@ state.current = newState

// Accumulates all "pending" state updates until they have been applied.
const nextState = useRef(initialState)
// Updating of the actual `VirtualScroller` state is done in a
// `useInsertionEffect()` rather than in a `useLayoutEffect()`.
//
// The reason is that using `useLayoutEffect()` would result in
// "breaking" the `<VirtualScroller/>` when an `itemComponent`
// called `onHeightDidChange()` from its own `useLayoutEffect()`.
// In those cases, the `itemCompoent`'s effect would run before
// the `<VirtualScroller/>`'s effect, resulting in
// `VirtualScroller.onItemHeightDidChange(i)` being run at a moment in time
// when the DOM has already been updated for the next `VirtualScroller` state
// but the actual `VirtualScroller` state is still a previous ("stale") one
// containing "stale" first/last shown item indexes, which would result in an
// "index out of bounds" error when `onItemHeightDidChange(i)` tries to access
// and measure the DOM element from item index `i` which doesn't already/yet exist.
//
// An example of such situation could be seen from a `VirtualScroller` debug log
// which was captured for a case when using `useLayoutEffect()` to update the
// "actual" `VirtualScroller` state after the corresponding DOM changes have been applied:
// Updates the actual `VirtualScroller` state right after a requested state update
// has been applied. Doesn't do anything at initial render.
useLayoutEffect(() => {
// The user has scrolled far enough: perform a re-layout
// ~ Update Layout (on scroll) ~
//
// Item index 2 height is required for calculations but hasn't been measured yet. Mark the item as "shown", rerender the list, measure the item's height and redo the layout.
//
// ~ Calculated Layout ~
// Columns count 1
// First shown item index 2
// Last shown item index 5
// …
// Item heights (231) [1056.578125, 783.125, empty × 229]
// Item states (231) [{…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, …]
//
// ~ Set state ~
// {firstShownItemIndex: 2, lastShownItemIndex: 5, …}
//
// ~ Rendered ~
// State {firstShownItemIndex: 2, lastShownItemIndex: 5, …}
//
// ~ Measure item heights ~
// Item index 2 height 719.8828125
// Item index 3 height 961.640625
// Item index 4 height 677.6640625
// Item index 5 height 1510.1953125
//
// ~ Update Layout (on non-measured item heights have been measured) ~
//
// ~ Calculated Layout ~
// Columns count 1
// First shown item index 4
// Last shown item index 5
// …
// Item heights (231) [1056.578125, 783.125, 719.8828125, 961.640625, 677.6640625, 1510.1953125, empty × 225]
// Item states (231) [{…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, …]
//
// ~ Set state ~
// {firstShownItemIndex: 4, lastShownItemIndex: 5, beforeItemsHeight: 3521.2265625, afterItemsHeight: 214090.72265624942}
//
// ~ On Item Height Did Change was called ~
// Item index 5
// ~ Re-measure item height ~
// ERROR "onItemHeightDidChange()" has been called for item index 5 but the item is not currently rendered and can't be measured. The exact error was: Element with index 3 was not found in the list of Rendered Item Elements in the Items Container of Virtual Scroller. There're only 2 Elements there.
//
// React: ~ The requested state is about to be applied in DOM. Set it as the `VirtualScroller` state. ~
// {firstShownItemIndex: 4, lastShownItemIndex: 5, …}
//
// ~ Rendered ~
// "~ Rendered ~" is what gets output when `onRender()` function gets called.
// It means that `useLayoutEffect()` was triggered after `onItemHeightDidChange(i)`
// was called and after the "ERROR" happened.
//
// The "ERROR" happened because new item indexes 4…5 were actually rendered instead of
// item indexes 2…5 by the time the application called `onItemHeightDidChange(i)` function
// inside `itemComponent`'s `useLayoutEffect()`.
// Item indexes 4…5 is what was requested in a `setState()` call, which called `_setNewState()`.
// This means that `_newState` changes have been applied to the DOM
// but `useLayoutEffect()` wasn't triggered immediately after that.
// Instead, it was triggered a right after the `itemComponent`'s `useLayoutEffect()`
// because child effects run before parent effects.
// So, the `itemComponent`'s `onHeightDidChange()` function call caught the
// `VirtualScroller` in an inconsistent state.
//
// To fix that, `useLayoutEffect()` gets replaced with `useInsertionEffect()`:
// https://blog.saeloun.com/2022/06/02/react-18-useinsertioneffect
// https://beta.reactjs.org/reference/react/useInsertionEffect
//
// After replacing `useLayoutEffect()` with `useInsertionEffect()`,
// the log shows that there's no more error:
//
// ~ Set state ~
// {firstShownItemIndex: 0, lastShownItemIndex: 2, …}
//
// React: ~ The requested state is about to be applied in DOM. Set it as the `VirtualScroller` state. ~
// {firstShownItemIndex: 0, lastShownItemIndex: 2, …}
//
// ~ On Item Height Did Change was called ~
// Item index 0
// ~ Re-measure item height ~
// Previous height 917
// New height 1064.453125
// ~ Item height has changed ~
//
// An alternative solution would be demanding the `itemComponent` to
// accept a `ref` and then measuring the corresponding DOM element height
// directly using the `ref`-ed DOM element rather than searching for that
// DOM element in the `ItemsContainer`.
// So if `useInsertionEffect()` gets removed from React in some hypothetical future,
// it could be replaced with using `ref`s on `ItemComponent`s to measure the DOM element heights.
//
useInsertionEffect(() => {
// Update the actual `VirtualScroller` state right before the DOM changes
// are going to be applied for the requested state update.
//
// This hook will run right before `useLayoutEffect()`.
//
// It doesn't make any difference which one of the two hooks to use to update
// the actual `VirtualScroller` state in this scenario because the two hooks
// run synchronously one right after another (insertion effect → DOM update → layout effect)
// without any free space for any `VirtualScroller` code (like the scroll event handler)
// to squeeze in and run in-between them, so the `VirtualScroller`'s `state`
// is always gonna stay consistent with what's currently rendered on screen
// from the `VirtualScroler`'s point of view, and the short transition period
// it simply doesn't see because it doesn't "wake up" during that period.
//
// Updating the actual `VirtualScroller` state right before `useLayoutEffect()`
// fixes the bug when an `itemComponent` calls `onHeightDidChange()` in its own
// `useLayoutEffect()` which would run before this `useLayoutEffect()`
// because children's effects run before parent's.
//
// This hook doesn't do anything at the initial render.
//
if (isDebug()) {
log('React: ~ The requested state is about to be applied in DOM. Set it as the `VirtualScroller` state. ~')
log(getStateSnapshot(_newState))
}
setState(_newState)
}, [
_newState
])
}, [_newState])
// Calls `onRender()` right after every state update (which is a re-render),
// and also right after the initial render.
useLayoutEffect(() => {
// Call `onRender()` right after a requested state update has been applied,
// and also right after the initial render.
onRender()
}, [
_newState,
// When using `USE_ITEMS_UPDATE_NO_SECOND_RENDER_OPTIMIZATION` feature,
// there won't be a `_setNewState()` function call when `items` property changes,
// hence the additional `itemsProperty` dependency.
USE_ITEMS_UPDATE_NO_SECOND_RENDER_OPTIMIZATION ? itemsProperty : undefined
])
}, [_newState])
return {
getState: () => state.current,
// This is the state the component should render.
stateToRender: _newState,
getNextState: () => nextState.current,
// Returns the current state of the `VirtualScroller`.
// This function is used in the `VirtualScroller` itself
// because the `state` is managed outside of it.
getState,
// Requests a state update.
//
// State updates are incremental meaning that this function mimicks
// the classic `React.Component`'s `this.setState()` behavior
// when calling `this.setState()` didn't replace `state` but rather merged
// the updated state properties over the "old" state properties.
//
// The reason for using pending state updates accumulation is that
// `useState()` updates are "asynchronous" (not immediate),
// and simply merging over `...state` would merge over potentially stale
// property values in cases when more than a single `updateState()` call is made
// before the state actually updates, resulting in losing some of those state updates.
//
// Example: the first `updateState()` call updates shown item indexes,
// and the second `updateState()` call updates `verticalSpacing`.
// If it was simply `updateState({ ...state, ...stateUpdate })`
// then the second state update could overwrite the first state update,
// resulting in incorrect items being shown/hidden.
//
updateState: (stateUpdate) => {
nextState.current = {
...nextState.current,
...stateUpdate
}
// If `items` property did change, the component detects it at render time
// and updates `VirtualScroller` items immediately by calling `.setItems()`,
// which, in turn, immediately calls this `updateState()` function
// with a `stateUpdate` argument that contains the new `items`,
// so checking for `stateUpdate.items` could detect situations like that.
//
// When the initial `VirtualScroller` state is being set, it contains the `.items`
// property too, but that initial setting is done using another function called
// `setInitialState()`, so using `if (stateUpdate.items)` condition here for describing
// just the case when `state` has been updated as a result of a `setItems()` call
// seems to be fine.
//
const _newState = nextState.current
if (stateUpdate.items && USE_ITEMS_UPDATE_NO_SECOND_RENDER_OPTIMIZATION) {
setState(_newState)
} else {
_setNewState(_newState)
}
}
setState: _setNewState
}
}

@@ -5,3 +5,3 @@ import px from '../utility/px.js'

tbody,
getNextState
state
}) {

@@ -15,3 +15,3 @@ if (tbody) {

afterItemsHeight
} = getNextState()
} = state

@@ -18,0 +18,0 @@ return {

@@ -11,28 +11,17 @@ import React, { useRef, useMemo, useLayoutEffect } from 'react'

import useOnItemHeightDidChange from './useOnItemHeightDidChange.js'
import useHandleItemsPropertyChange from './useHandleItemsPropertyChange.js'
import useHandleItemIndexesChange from './useHandleItemIndexesChange.js'
import useSetNewItemsOnItemsPropertyChange from './useSetNewItemsOnItemsPropertyChange.js'
import useUpdateItemKeysOnItemsChange from './useUpdateItemKeysOnItemsChange.js'
import useClassName from './useClassName.js'
import useStyle from './useStyle.js'
// When `items` property changes, `useHandleItemsPropertyChange()` hook detects that
// and calls `VirtualScroller.setItems()` which in turn calls the `updateState()` function.
// At this point, an insignificant optimization could be applied:
// the component could avoid re-rendering the second time.
// Instead, the state update could be applied "immediately" if it originated
// from `.setItems()` function call, eliminating the unneeded second re-render.
//
// I could see how this minor optimization could get brittle when modifiying the code,
// so I put it under a feature flag so that it could potentially be turned off
// in case of any potential weird issues in some future.
//
// Another reason for using this feature is:
//
// Since `useHandleItemsPropertyChange()` runs at render time
// and not after the render has finished (not in an "effect"),
// if the state update was done "conventionally" (by calling `_setNewState()`),
// React would throw an error about updating state during render.
// No one knows what the original error message was.
// Perhaps it's no longer relevant in newer versions of React.
//
const USE_ITEMS_UPDATE_NO_SECOND_RENDER_OPTIMIZATION = true
// When `items` property changes:
// * A new `items` property is supplied to the React component.
// * The React component re-renders itself.
// * `useSetNewItemsOnItemsPropertyChange()` hook is run.
// * `useSetNewItemsOnItemsPropertyChange()` hook detects that the `items` property
// has changed and calls `VirtualScroller.setItems(items)`.
// * `VirtualScroller.setItems(items)` calls `VirtualScroller.setState()`.
// * `VirtualScroller.setState()` calls the `setState()` function.
// * The `setState()` function calls a setter from a `useState()` hook.
// * The React component re-renders itself the second time.

@@ -117,9 +106,8 @@ function VirtualScroller({

getState,
updateState,
getNextState
setState,
stateToRender
} = useState({
initialState: _initialState,
onRender: virtualScroller.onRender,
itemsProperty,
USE_ITEMS_UPDATE_NO_SECOND_RENDER_OPTIMIZATION
itemsProperty
})

@@ -131,3 +119,3 @@

getState,
updateState
setState
})

@@ -144,2 +132,3 @@ }, [])

getItemKey,
usesAutogeneratedItemKeys,
updateItemKeysForNewItems

@@ -165,3 +154,3 @@ } = useItemKeys({

// Calls `.setItems()` if `items` property has changed.
useHandleItemsPropertyChange(itemsProperty, {
useSetNewItemsOnItemsPropertyChange(itemsProperty, {
virtualScroller,

@@ -171,10 +160,9 @@ // `preserveScrollPosition` property name is deprecated,

preserveScrollPosition,
preserveScrollPositionOnPrependItems,
nextItems: getNextState().items
preserveScrollPositionOnPrependItems
})
// Updates `key`s if item indexes have changed.
useHandleItemIndexesChange({
useUpdateItemKeysOnItemsChange(stateToRender.items, {
virtualScroller,
itemsBeingRendered: getNextState().items,
usesAutogeneratedItemKeys,
updateItemKeysForNewItems

@@ -218,3 +206,3 @@ })

tbody,
getNextState
state: stateToRender
})

@@ -227,3 +215,3 @@

lastShownItemIndex
} = getNextState()
} = stateToRender

@@ -230,0 +218,0 @@ return (

@@ -0,1 +1,3 @@

import ItemNotRenderedError from '../ItemNotRenderedError.js'
export default class ItemsContainer {

@@ -19,5 +21,14 @@ /**

let topOffset = this.getElement().paddingTop
let rowWidth
let rowHeight
let startNewRow = true
if (renderedElementIndex > children.length - 1) {
throw new ItemNotRenderedError({
renderedElementIndex,
renderedElementsCount: children.length
})
}
let i = 0

@@ -43,2 +54,3 @@ while (i <= renderedElementIndex) {

}
return topOffset

@@ -53,3 +65,12 @@ }

getNthRenderedItemHeight(renderedElementIndex) {
return this.getElement().children[renderedElementIndex].height
const children = this.getElement().children
if (renderedElementIndex > children.length - 1) {
throw new ItemNotRenderedError({
renderedElementIndex,
renderedElementsCount: children.length
})
}
return children[renderedElementIndex].height
}

@@ -56,0 +77,0 @@

@@ -16,3 +16,8 @@ export default function log(...args) {

function error(...args) {
console.error(...['[virtual-scroller]'].concat(args))
}
export function reportError(...args) {
const createError = () => new Error(['[virtual-scroller]'].concat(args).join(' '))
if (typeof window !== 'undefined') {

@@ -22,3 +27,3 @@ // In a web browser.

// at which point did the error occur between other debug logs.
log.apply(this, ['ERROR'].concat(args))
error.apply(this, ['ERROR'].concat(args))
setTimeout(() => {

@@ -31,7 +36,16 @@ // Throw an error in a timeout so that it doesn't interrupt the application's flow.

// but those don't seem to be used in any of the error messages.
throw new Error(['[virtual-scroller]'].concat(args).join(' '))
throw createError()
}, 0)
} else {
// On a server.
console.error(...['[virtual-scroller]'].concat(args))
// In Node.js.
// If tests are being run, throw in case of any errors.
const catchError = getGlobalVariable('VirtualScrollerCatchError')
if (catchError) {
return catchError(createError())
}
if (getGlobalVariable('VirtualScrollerThrowErrors')) {
throw createError()
}
// Print the error in the console.
error.apply(this, ['ERROR'].concat(args))
}

@@ -38,0 +52,0 @@ }

@@ -38,2 +38,4 @@ import VirtualScrollerConstructor from './VirtualScroller.constructor.js'

if (!isRestart) {
this.waitingForRender = true
// If no custom state storage has been configured, use the default one.

@@ -40,0 +42,0 @@ // Also sets the initial state.

@@ -10,2 +10,4 @@ // For some weird reason, in Chrome, `setTimeout()` would lag up to a second (or more) behind.

import ItemNotRenderedError from './ItemNotRenderedError.js'
export default function() {

@@ -153,2 +155,5 @@ this.onUpdateShownItemIndexes = ({ reason, stateUpdate }) => {

this.firstNonMeasuredItemIndex = firstNonMeasuredItemIndex
// if (firstNonMeasuredItemIndex !== undefined) {
// log('Non-measured item index that will be measured at next layout', firstNonMeasuredItemIndex)
// }

@@ -345,16 +350,17 @@ // Set "previously calculated layout".

function updatePreviouslyCalculatedLayoutOnItemHeightChange(i, previousHeight, newHeight) {
if (this.previouslyCalculatedLayout) {
const prevLayout = this.previouslyCalculatedLayout
if (prevLayout) {
const heightDifference = newHeight - previousHeight
if (i < this.previouslyCalculatedLayout.firstShownItemIndex) {
// Patch `this.previouslyCalculatedLayout`'s `.beforeItemsHeight`.
this.previouslyCalculatedLayout.beforeItemsHeight += heightDifference
} else if (i > this.previouslyCalculatedLayout.lastShownItemIndex) {
// Could patch `.afterItemsHeight` of `this.previouslyCalculatedLayout` here,
// if `.afterItemsHeight` property existed in `this.previouslyCalculatedLayout`.
if (this.previouslyCalculatedLayout.afterItemsHeight !== undefined) {
this.previouslyCalculatedLayout.afterItemsHeight += heightDifference
if (i < prevLayout.firstShownItemIndex) {
// Patch `prevLayout`'s `.beforeItemsHeight`.
prevLayout.beforeItemsHeight += heightDifference
} else if (i > prevLayout.lastShownItemIndex) {
// Could patch `.afterItemsHeight` of `prevLayout` here,
// if `.afterItemsHeight` property existed in `prevLayout`.
if (prevLayout.afterItemsHeight !== undefined) {
prevLayout.afterItemsHeight += heightDifference
}
} else {
// Patch `this.previouslyCalculatedLayout`'s shown items height.
this.previouslyCalculatedLayout.shownItemsHeight += newHeight - previousHeight
// Patch `prevLayout`'s shown items height.
prevLayout.shownItemsHeight += newHeight - previousHeight
}

@@ -377,3 +383,3 @@ }

this._onItemHeightDidChange = (i) => {
log('~ Re-measure item height ~')
log('~ On Item Height Did Change was called ~')
log('Item index', i)

@@ -419,7 +425,19 @@

if (previousHeight === undefined) {
return reportError(`"onItemHeightDidChange()" has been called for item ${i}, but that item hasn't been rendered before.`)
return reportError(`"onItemHeightDidChange()" has been called for item index ${i} but the item hasn't been rendered before.`)
}
const newHeight = remeasureItemHeight.call(this, i)
log('~ Re-measure item height ~')
let newHeight
try {
newHeight = remeasureItemHeight.call(this, i)
} catch (error) {
// Successfully finishing an `onItemHeightDidChange(i)` call is not considered
// critical for `VirtualScroller`'s operation, so such errors could be ignored.
if (error instanceof ItemNotRenderedError) {
return reportError(`"onItemHeightDidChange()" has been called for item index ${i} but the item is not currently rendered and can\'t be measured. The exact error was: ${error.message}`)
}
}
log('Previous height', previousHeight)

@@ -429,11 +447,32 @@ log('New height', newHeight)

if (previousHeight !== newHeight) {
log('~ Item height has changed ~')
log('~ Item height has changed. Should update layout. ~')
// Update or reset previously calculated layout.
// Update or reset a previously calculated layout
// so that the "diff"s based on that layout in the future
// produce correct results.
updatePreviouslyCalculatedLayoutOnItemHeightChange.call(this, i, previousHeight, newHeight)
// Recalculate layout.
this.onUpdateShownItemIndexes({ reason: LAYOUT_REASON.ITEM_HEIGHT_CHANGED })
//
// If the `VirtualScroller` is already waiting for a state update to be rendered,
// delay `onItemHeightDidChange(i)`'s re-layout until that state update is rendered.
// The reason is that React `<VirtualScroller/>`'s `onHeightDidChange()` is meant to
// be called inside `useLayoutEffect()` hook. Due to how React is implemented internally,
// that might happen in the middle of the currently pending `setState()` operation
// being applied, resulting in weird "race condition" bugs.
//
if (this.waitingForRender) {
log('~ Another state update is already waiting to be rendered. Delay the layout update until then. ~')
this.updateLayoutAfterRenderBecauseItemHeightChanged = true
} else {
this.onUpdateShownItemIndexes({ reason: LAYOUT_REASON.ITEM_HEIGHT_CHANGED })
}
// Schedule the item height update for after the new items have been rendered.
// If there was a request for `setState()` with new `items`, then the changes
// to `currentState.itemHeights[]` made above in a `remeasureItemHeight()` call
// would be overwritten when that pending `setState()` call gets applied.
// To fix that, the updates to current `itemHeights[]` are noted in
// `this.itemHeightsThatChangedWhileNewItemsWereBeingRendered` variable.
// That variable is then checked when the `setState()` call with the new `items`
// has been updated.
if (this.newItemsWillBeRendered) {

@@ -440,0 +479,0 @@ if (!this.itemHeightsThatChangedWhileNewItemsWereBeingRendered) {

@@ -1,2 +0,2 @@

import log, { warn, isDebug } from './utility/debug.js'
import log, { warn, reportError, isDebug } from './utility/debug.js'
import getStateSnapshot from './utility/getStateSnapshot.js'

@@ -14,2 +14,4 @@ import shallowEqual from './utility/shallowEqual.js'

this._onRender = (newState, prevState) => {
this.waitingForRender = false
log('~ Rendered ~')

@@ -37,4 +39,32 @@ if (isDebug()) {

if (!prevState) {
return
// `this.mostRecentlySetState` checks that state management behavior is correct:
// that in situations when there're multiple new states waiting to be set,
// only the latest one gets applied.
// It keeps the code simpler and prevents possible race condition bugs.
// For example, `VirtualScroller` keeps track of its latest requested
// state update in different instance variable flags which assume that
// only that latest requested state update gets actually applied.
//
// This check should also be performed for the initial render in order to
// guarantee that no potentially incorrect state update goes unnoticed.
// Incorrect state updates could happen when `VirtualScroller` state
// is managed externally by passing `getState()`/`updateState()` options.
//
// Perform the check only when `this.mostRecentSetStateValue` is defined.
// `this.mostRecentSetStateValue` is normally gonna be `undefined` at the initial render
// because the initial state is not set by calling `this.updateState()`.
// At the same time, it is possible that the initial render is delayed
// for whatever reason, and `this.updateState()` gets called before the initial render,
// so `this.mostRecentSetStateValue` could also be defined at the initial render,
// in which case the check should be performed.
//
if (this.mostRecentSetStateValue) {
// "Shallow equality" is used here instead of "strict equality"
// because a developer might choose to supply an `updateState()` function
// rather than a `setState()` function, in which case the `updateState()` function
// would construct its own state object.
if (!shallowEqual(newState, this.mostRecentSetStateValue)) {
warn('The most recent state that was set', getStateSnapshot(this.mostRecentSetStateValue))
reportError('The state that has been rendered is not the most recent one that was set')
}
}

@@ -46,2 +76,3 @@

nonMeasuredItemsHaveBeenRendered,
itemHeightHasChanged,
widthHasChanged

@@ -52,2 +83,12 @@ } = resetStateUpdateFlags.call(this)

if (this.updateLayoutAfterRenderBecauseItemHeightChanged) {
layoutUpdateReason = LAYOUT_REASON.ITEM_HEIGHT_CHANGED
}
if (!prevState) {
if (!layoutUpdateReason) {
return
}
}
// If the `VirtualScroller`, while calculating layout parameters, encounters

@@ -81,37 +122,39 @@ // a not-shown item with a non-measured height, it calls `updateState()` just to

const { items: previousItems } = prevState
const { items: newItems } = newState
// Even if `this.newItemsWillBeRendered` flag is `true`,
// `newItems` could still be equal to `previousItems`.
// For example, when `updateState()` calls don't update `state` immediately
// and a developer first calls `setItems(newItems)` and then calls `setItems(oldItems)`:
// in that case, `this.newItemsWillBeRendered` flag will be `true` but the actual `items`
// in state wouldn't have changed due to the first `updateState()` call being overwritten
// by the second `updateState()` call (that's called "batching state updates" in React).
if (newItems !== previousItems) {
const itemsDiff = this.getItemsDiff(previousItems, newItems)
if (itemsDiff) {
// The call to `.onPrepend()` must precede the call to `.measureItemHeights()`
// which is called in `.onRender()`.
// `this.itemHeights.onPrepend()` updates `firstMeasuredItemIndex`
// and `lastMeasuredItemIndex` of `this.itemHeights`.
const { prependedItemsCount } = itemsDiff
this.itemHeights.onPrepend(prependedItemsCount)
} else {
this.itemHeights.reset()
}
if (prevState) {
const { items: previousItems } = prevState
const { items: newItems } = newState
// Even if `this.newItemsWillBeRendered` flag is `true`,
// `newItems` could still be equal to `previousItems`.
// For example, when `updateState()` calls don't update `state` immediately
// and a developer first calls `setItems(newItems)` and then calls `setItems(oldItems)`:
// in that case, `this.newItemsWillBeRendered` flag will be `true` but the actual `items`
// in state wouldn't have changed due to the first `updateState()` call being overwritten
// by the second `updateState()` call (that's called "batching state updates" in React).
if (newItems !== previousItems) {
const itemsDiff = this.getItemsDiff(previousItems, newItems)
if (itemsDiff) {
// The call to `.onPrepend()` must precede the call to `.measureItemHeights()`
// which is called in `.onRender()`.
// `this.itemHeights.onPrepend()` updates `firstMeasuredItemIndex`
// and `lastMeasuredItemIndex` of `this.itemHeights`.
const { prependedItemsCount } = itemsDiff
this.itemHeights.onPrepend(prependedItemsCount)
} else {
this.itemHeights.reset()
}
if (!widthHasChanged) {
// The call to `this.onNewItemsRendered()` must precede the call to
// `.measureItemHeights()` which is called in `.onRender()` because
// `this.onNewItemsRendered()` updates `firstMeasuredItemIndex` and
// `lastMeasuredItemIndex` of `this.itemHeights` in case of a prepend.
//
// If after prepending items the scroll position
// should be "restored" so that there's no "jump" of content
// then it means that all previous items have just been rendered
// in a single pass, and there's no need to update layout again.
//
if (onNewItemsRendered.call(this, itemsDiff, newState) !== 'SEAMLESS_PREPEND') {
layoutUpdateReason = LAYOUT_REASON.ITEMS_CHANGED
if (!widthHasChanged) {
// The call to `this.onNewItemsRendered()` must precede the call to
// `.measureItemHeights()` which is called in `.onRender()` because
// `this.onNewItemsRendered()` updates `firstMeasuredItemIndex` and
// `lastMeasuredItemIndex` of `this.itemHeights` in case of a prepend.
//
// If after prepending items the scroll position
// should be "restored" so that there's no "jump" of content
// then it means that all previous items have just been rendered
// in a single pass, and there's no need to update layout again.
//
if (onNewItemsRendered.call(this, itemsDiff, newState) !== 'SEAMLESS_PREPEND') {
layoutUpdateReason = LAYOUT_REASON.ITEMS_CHANGED
}
}

@@ -132,5 +175,7 @@ }

if (
newState.firstShownItemIndex !== prevState.firstShownItemIndex ||
newState.lastShownItemIndex !== prevState.lastShownItemIndex ||
newState.items !== prevState.items ||
(prevState && (
newState.firstShownItemIndex !== prevState.firstShownItemIndex ||
newState.lastShownItemIndex !== prevState.lastShownItemIndex ||
newState.items !== prevState.items
)) ||
widthHasChanged

@@ -211,3 +256,3 @@ ) {

for (const i of Object.keys(this.itemHeightsThatChangedWhileNewItemsWereBeingRendered)) {
itemHeights[prependedItemsCount + parseInt(i)] = this.itemHeightsThatChangedWhileNewItemsWereBeingRendered[i]
itemHeights[prependedItemsCount + Number(i)] = this.itemHeightsThatChangedWhileNewItemsWereBeingRendered[i]
}

@@ -219,3 +264,3 @@ }

for (const i of Object.keys(this.itemStatesThatChangedWhileNewItemsWereBeingRendered)) {
itemStates[prependedItemsCount + parseInt(i)] = this.itemStatesThatChangedWhileNewItemsWereBeingRendered[i]
itemStates[prependedItemsCount + Number(i)] = this.itemStatesThatChangedWhileNewItemsWereBeingRendered[i]
}

@@ -336,2 +381,5 @@ }

const nonMeasuredItemsHaveBeenRendered = this.firstNonMeasuredItemIndex !== undefined
if (nonMeasuredItemsHaveBeenRendered) {
log('Non-measured item index', this.firstNonMeasuredItemIndex)
}
// Reset `this.firstNonMeasuredItemIndex` flag.

@@ -349,4 +397,9 @@ this.firstNonMeasuredItemIndex = undefined

// Reset `this.updateLayoutAfterRenderBecauseItemHeightChanged`.
const itemHeightHasChanged = this.updateLayoutAfterRenderBecauseItemHeightChanged
this.updateLayoutAfterRenderBecauseItemHeightChanged = undefined
return {
nonMeasuredItemsHaveBeenRendered,
itemHeightHasChanged,
widthHasChanged

@@ -353,0 +406,0 @@ }

@@ -55,3 +55,9 @@ import fillArray from './utility/fillArray.js'

// Schedule the item state update for after the new items have been rendered.
// If there was a request for `setState()` with new `items`, then the changes
// to `currentState.itemStates[]` made above would be overwritten when that
// pending `setState()` call gets applied.
// To fix that, the updates to current `itemStates[]` are noted in
// `this.itemStatesThatChangedWhileNewItemsWereBeingRendered` variable.
// That variable is then checked when the `setState()` call with the new `items`
// has been updated.
if (this.newItemsWillBeRendered) {

@@ -82,5 +88,21 @@ if (!this.itemStatesThatChangedWhileNewItemsWereBeingRendered) {

this.waitingForRender = true
// Store previous `state`.
this.previousState = this.getState()
// If it's the first call to `this.updateState()` then initialize
// the most recent `setState()` value to be the current state.
if (!this.mostRecentSetStateValue) {
this.mostRecentSetStateValue = this.getState()
}
// Accumulates all "pending" state updates until they have been applied.
this.mostRecentSetStateValue = {
...this.mostRecentSetStateValue,
...stateUpdate
}
// Update `state`.
this.previousState = this.getState()
this._updateState(stateUpdate)
this._setState(this.mostRecentSetStateValue, stateUpdate)
}

@@ -97,2 +119,3 @@

getState,
setState,
updateState

@@ -109,13 +132,24 @@ }) => {

if (render) {
throw new Error('[virtual-scroller] Creating a `VirtualScroller` class instance with a `render()` parameter means using the default (internal) state storage')
throw new Error('[virtual-scroller] Creating a `VirtualScroller` class instance with a `render()` parameter implies using the default (internal) state storage')
}
if (!getState || !updateState) {
throw new Error('[virtual-scroller] When using a custom state storage, one must supply both `getState()` and `updateState()` functions')
if (setState && updateState) {
throw new Error('[virtual-scroller] When using a custom state storage, one must supply either `setState()` or `updateState()` function but not both')
}
if (!getState || !(setState || updateState)) {
throw new Error('[virtual-scroller] When using a custom state storage, one must supply both `getState()` and `setState()`/`updateState()` functions')
}
this._usesCustomStateStorage = true
this._getState = getState
this._updateState = updateState
this._setState = (newState, stateUpdate) => {
if (setState) {
setState(newState)
} else {
updateState(stateUpdate)
}
}
}

@@ -128,5 +162,5 @@

// Create default `getState()`/`updateState()` functions.
// Create default `getState()`/`setState()` functions.
this._getState = defaultGetState.bind(this)
this._updateState = defaultUpdateState.bind(this)
this._setState = defaultSetState.bind(this)

@@ -148,14 +182,17 @@ // When `state` is stored externally, a developer is responsible for

function defaultUpdateState(stateUpdate) {
// Because this variant of `.updateState()` is "synchronous" (immediate),
// it can be written like `...prevState`, and no state updates would be lost.
// But if it was "asynchronous" (not immediate), then `...prevState`
// wouldn't work in all cases, because it could be stale in cases
// when more than a single `updateState()` call is made before
// the state actually updates, making `prevState` stale.
this.state = {
...this.state,
...stateUpdate
}
function defaultSetState(newState, stateUpdate) {
// // Because the default state updates are "synchronous" (immediate),
// // the `...stateUpdate` could be applied over `...this.state`,
// // and no state updates would be lost.
// // But if it was "asynchronous" (not immediate), then `...this.state`
// // wouldn't work in all cases, because it could be stale in cases
// // when more than a single `setState()` call is made before
// // the state actually updates, making some properties of `this.state` stale.
// this.state = {
// ...this.state,
// ...stateUpdate
// }
this.state = newState
render(this.state, this.previousState)

@@ -162,0 +199,0 @@

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is too big to display

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc