@mui/x-virtualizer
Version:
452 lines (446 loc) • 16.7 kB
JavaScript
"use strict";
'use client';
var _interopRequireWildcard = require("@babel/runtime/helpers/interopRequireWildcard").default;
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault").default;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.Dimensions = void 0;
var _extends2 = _interopRequireDefault(require("@babel/runtime/helpers/extends"));
var React = _interopRequireWildcard(require("react"));
var _ownerDocument = _interopRequireDefault(require("@mui/utils/ownerDocument"));
var _useLazyRef = _interopRequireDefault(require("@mui/utils/useLazyRef"));
var _useEnhancedEffect = _interopRequireDefault(require("@mui/utils/useEnhancedEffect"));
var _useEventCallback = _interopRequireDefault(require("@mui/utils/useEventCallback"));
var _throttle = require("@mui/x-internals/throttle");
var _isDeepEqual = require("@mui/x-internals/isDeepEqual");
var _math = require("@mui/x-internals/math");
var _store = require("@mui/x-internals/store");
var _models = require("../models");
/* eslint-disable import/export, @typescript-eslint/no-redeclare */
/* eslint-disable no-underscore-dangle */
const EMPTY_DIMENSIONS = {
isReady: false,
root: _models.Size.EMPTY,
viewportOuterSize: _models.Size.EMPTY,
viewportInnerSize: _models.Size.EMPTY,
contentSize: _models.Size.EMPTY,
minimumSize: _models.Size.EMPTY,
hasScrollX: false,
hasScrollY: false,
scrollbarSize: 0,
rowWidth: 0,
rowHeight: 0,
columnsTotalWidth: 0,
leftPinnedWidth: 0,
rightPinnedWidth: 0,
topContainerHeight: 0,
bottomContainerHeight: 0
};
const selectors = {
rootSize: state => state.rootSize,
dimensions: state => state.dimensions,
rowHeight: state => state.dimensions.rowHeight,
contentHeight: state => state.dimensions.contentSize.height,
rowsMeta: state => state.rowsMeta,
columnPositions: (0, _store.createSelectorMemoized)((_, columns) => {
const positions = [];
let currentPosition = 0;
for (let i = 0; i < columns.length; i += 1) {
positions.push(currentPosition);
currentPosition += columns[i].computedWidth;
}
return positions;
}),
needsHorizontalScrollbar: state => state.dimensions.viewportOuterSize.width > 0 && state.dimensions.columnsTotalWidth > state.dimensions.viewportOuterSize.width
};
const Dimensions = exports.Dimensions = {
initialize: initializeState,
use: useDimensions,
selectors
};
function initializeState(params) {
const dimensions = (0, _extends2.default)({}, EMPTY_DIMENSIONS, params.dimensions);
const {
rowCount
} = params;
const {
rowHeight
} = dimensions;
const rowsMeta = {
currentPageTotalHeight: rowCount * rowHeight,
positions: Array.from({
length: rowCount
}, (_, i) => i * rowHeight),
pinnedTopRowsTotalHeight: 0,
pinnedBottomRowsTotalHeight: 0
};
const rowHeights = new Map();
return {
rootSize: _models.Size.EMPTY,
dimensions,
rowsMeta,
rowHeights
};
}
function useDimensions(store, params, _api) {
const isFirstSizing = React.useRef(true);
const {
refs,
dimensions: {
rowHeight,
columnsTotalWidth,
leftPinnedWidth,
rightPinnedWidth,
topPinnedHeight,
bottomPinnedHeight
},
onResize
} = params;
const containerNode = refs.container.current;
const updateDimensions = React.useCallback(() => {
if (isFirstSizing.current) {
return;
}
const rootSize = selectors.rootSize(store.state);
const rowsMeta = selectors.rowsMeta(store.state);
// All the floating point dimensions should be rounded to .1 decimal places to avoid subpixel rendering issues
// https://github.com/mui/mui-x/issues/9550#issuecomment-1619020477
// https://github.com/mui/mui-x/issues/15721
const scrollbarSize = measureScrollbarSize(containerNode, params.dimensions.scrollbarSize);
const topContainerHeight = topPinnedHeight + rowsMeta.pinnedTopRowsTotalHeight;
const bottomContainerHeight = bottomPinnedHeight + rowsMeta.pinnedBottomRowsTotalHeight;
const contentSize = {
width: columnsTotalWidth,
height: (0, _math.roundToDecimalPlaces)(rowsMeta.currentPageTotalHeight, 1)
};
let viewportOuterSize;
let viewportInnerSize;
let hasScrollX = false;
let hasScrollY = false;
if (params.autoHeight) {
hasScrollY = false;
hasScrollX = Math.round(columnsTotalWidth) > Math.round(rootSize.width);
viewportOuterSize = {
width: rootSize.width,
height: topContainerHeight + bottomContainerHeight + contentSize.height
};
viewportInnerSize = {
width: Math.max(0, viewportOuterSize.width - (hasScrollY ? scrollbarSize : 0)),
height: Math.max(0, viewportOuterSize.height - (hasScrollX ? scrollbarSize : 0))
};
} else {
viewportOuterSize = {
width: rootSize.width,
height: rootSize.height
};
viewportInnerSize = {
width: Math.max(0, viewportOuterSize.width),
height: Math.max(0, viewportOuterSize.height - topContainerHeight - bottomContainerHeight)
};
const content = contentSize;
const container = viewportInnerSize;
const hasScrollXIfNoYScrollBar = content.width > container.width;
const hasScrollYIfNoXScrollBar = content.height > container.height;
if (hasScrollXIfNoYScrollBar || hasScrollYIfNoXScrollBar) {
hasScrollY = hasScrollYIfNoXScrollBar;
hasScrollX = content.width + (hasScrollY ? scrollbarSize : 0) > container.width;
// We recalculate the scroll y to consider the size of the x scrollbar.
if (hasScrollX) {
hasScrollY = content.height + scrollbarSize > container.height;
}
}
if (hasScrollY) {
viewportInnerSize.width -= scrollbarSize;
}
if (hasScrollX) {
viewportInnerSize.height -= scrollbarSize;
}
}
const rowWidth = Math.max(viewportOuterSize.width, columnsTotalWidth + (hasScrollY ? scrollbarSize : 0));
const minimumSize = {
width: columnsTotalWidth,
height: topContainerHeight + contentSize.height + bottomContainerHeight
};
const newDimensions = {
isReady: true,
root: rootSize,
viewportOuterSize,
viewportInnerSize,
contentSize,
minimumSize,
hasScrollX,
hasScrollY,
scrollbarSize,
rowWidth,
rowHeight,
columnsTotalWidth,
leftPinnedWidth,
rightPinnedWidth,
topContainerHeight,
bottomContainerHeight
};
const prevDimensions = store.state.dimensions;
if ((0, _isDeepEqual.isDeepEqual)(prevDimensions, newDimensions)) {
return;
}
store.update({
dimensions: newDimensions
});
onResize?.(newDimensions.root);
}, [store, containerNode, params.dimensions.scrollbarSize, params.autoHeight, onResize, rowHeight, columnsTotalWidth, leftPinnedWidth, rightPinnedWidth, topPinnedHeight, bottomPinnedHeight]);
const {
resizeThrottleMs
} = params;
const updateDimensionCallback = (0, _useEventCallback.default)(updateDimensions);
const debouncedUpdateDimensions = React.useMemo(() => resizeThrottleMs > 0 ? (0, _throttle.throttle)(updateDimensionCallback, resizeThrottleMs) : undefined, [resizeThrottleMs, updateDimensionCallback]);
React.useEffect(() => debouncedUpdateDimensions?.clear, [debouncedUpdateDimensions]);
const setRootSize = (0, _useEventCallback.default)(rootSize => {
store.state.rootSize = rootSize;
if (isFirstSizing.current || !debouncedUpdateDimensions) {
// We want to initialize the grid dimensions as soon as possible to avoid flickering
isFirstSizing.current = false;
updateDimensions();
} else {
debouncedUpdateDimensions();
}
});
(0, _useEnhancedEffect.default)(() => observeRootNode(containerNode, store, setRootSize), [containerNode, store, setRootSize]);
(0, _useEnhancedEffect.default)(updateDimensions, [updateDimensions]);
const rowsMeta = useRowsMeta(store, params, updateDimensions);
return {
updateDimensions,
debouncedUpdateDimensions,
rowsMeta
};
}
function useRowsMeta(store, params, updateDimensions) {
const heightCache = store.state.rowHeights;
const {
rows,
getRowHeight: getRowHeightProp,
getRowSpacing,
getEstimatedRowHeight
} = params;
const lastMeasuredRowIndex = React.useRef(-1);
const hasRowWithAutoHeight = React.useRef(false);
const isHeightMetaValid = React.useRef(false);
const pinnedRows = params.pinnedRows;
const rowHeight = (0, _store.useStore)(store, selectors.rowHeight);
const getRowHeightEntry = (0, _useEventCallback.default)(rowId => {
let entry = heightCache.get(rowId);
if (entry === undefined) {
entry = {
content: store.state.dimensions.rowHeight,
spacingTop: 0,
spacingBottom: 0,
detail: 0,
autoHeight: false,
needsFirstMeasurement: true
};
heightCache.set(rowId, entry);
}
return entry;
});
const {
applyRowHeight
} = params;
const processHeightEntry = React.useCallback(row => {
// HACK: rowHeight trails behind the most up-to-date value just enough to
// mess the initial rowsMeta hydration :/
eslintUseValue(rowHeight);
const dimensions = selectors.dimensions(store.state);
const baseRowHeight = dimensions.rowHeight;
const entry = getRowHeightEntry(row.id);
if (!getRowHeightProp) {
entry.content = baseRowHeight;
entry.needsFirstMeasurement = false;
} else {
const rowHeightFromUser = getRowHeightProp(row);
if (rowHeightFromUser === 'auto') {
if (entry.needsFirstMeasurement) {
const estimatedRowHeight = getEstimatedRowHeight ? getEstimatedRowHeight(row) : baseRowHeight;
// If the row was not measured yet use the estimated row height
entry.content = estimatedRowHeight ?? baseRowHeight;
}
hasRowWithAutoHeight.current = true;
entry.autoHeight = true;
} else {
// Default back to base rowHeight if getRowHeight returns null value.
entry.content = rowHeightFromUser ?? dimensions.rowHeight;
entry.needsFirstMeasurement = false;
entry.autoHeight = false;
}
}
if (getRowSpacing) {
const spacing = getRowSpacing(row);
entry.spacingTop = spacing.top ?? 0;
entry.spacingBottom = spacing.bottom ?? 0;
} else {
entry.spacingTop = 0;
entry.spacingBottom = 0;
}
applyRowHeight?.(entry, row);
return entry;
}, [store, getRowHeightProp, getRowHeightEntry, getEstimatedRowHeight, rowHeight, getRowSpacing, applyRowHeight]);
const hydrateRowsMeta = React.useCallback(() => {
hasRowWithAutoHeight.current = false;
const pinnedTopRowsTotalHeight = pinnedRows?.top.reduce((acc, row) => {
const entry = processHeightEntry(row);
return acc + entry.content + entry.spacingTop + entry.spacingBottom + entry.detail;
}, 0) ?? 0;
const pinnedBottomRowsTotalHeight = pinnedRows?.bottom.reduce((acc, row) => {
const entry = processHeightEntry(row);
return acc + entry.content + entry.spacingTop + entry.spacingBottom + entry.detail;
}, 0) ?? 0;
const positions = [];
const currentPageTotalHeight = rows.reduce((acc, row) => {
positions.push(acc);
const entry = processHeightEntry(row);
const total = entry.content + entry.spacingTop + entry.spacingBottom + entry.detail;
return acc + total;
}, 0);
if (!hasRowWithAutoHeight.current) {
// No row has height=auto, so all rows are already measured
lastMeasuredRowIndex.current = Infinity;
}
const didHeightsChange = pinnedTopRowsTotalHeight !== store.state.rowsMeta.pinnedTopRowsTotalHeight || pinnedBottomRowsTotalHeight !== store.state.rowsMeta.pinnedBottomRowsTotalHeight || currentPageTotalHeight !== store.state.rowsMeta.currentPageTotalHeight;
const rowsMeta = {
currentPageTotalHeight,
positions,
pinnedTopRowsTotalHeight,
pinnedBottomRowsTotalHeight
};
store.set('rowsMeta', rowsMeta);
if (didHeightsChange) {
updateDimensions();
}
isHeightMetaValid.current = true;
}, [store, pinnedRows, rows, processHeightEntry, updateDimensions]);
const hydrateRowsMetaLatest = (0, _useEventCallback.default)(hydrateRowsMeta);
const getRowHeight = rowId => {
return heightCache.get(rowId)?.content ?? selectors.rowHeight(store.state);
};
const storeRowHeightMeasurement = (id, height) => {
const entry = getRowHeightEntry(id);
const didChange = entry.content !== height;
entry.needsFirstMeasurement = false;
entry.content = height;
isHeightMetaValid.current && (isHeightMetaValid.current = !didChange);
};
const rowHasAutoHeight = id => {
return heightCache.get(id)?.autoHeight ?? false;
};
const getLastMeasuredRowIndex = () => {
return lastMeasuredRowIndex.current;
};
const setLastMeasuredRowIndex = index => {
if (hasRowWithAutoHeight.current && index > lastMeasuredRowIndex.current) {
lastMeasuredRowIndex.current = index;
}
};
const resetRowHeights = () => {
heightCache.clear();
hydrateRowsMeta();
};
const resizeObserver = (0, _useLazyRef.default)(() => typeof ResizeObserver === 'undefined' ? undefined : new ResizeObserver(entries => {
for (let i = 0; i < entries.length; i += 1) {
const entry = entries[i];
const height = entry.borderBoxSize && entry.borderBoxSize.length > 0 ? entry.borderBoxSize[0].blockSize : entry.contentRect.height;
const rowId = entry.target.__mui_id;
const focusedVirtualRowId = params.focusedVirtualCell?.()?.id;
if (focusedVirtualRowId === rowId && height === 0) {
// Focused virtual row has 0 height.
// We don't want to store it to avoid scroll jumping.
// https://github.com/mui/mui-x/issues/14726
return;
}
storeRowHeightMeasurement(rowId, height);
}
if (!isHeightMetaValid.current) {
// Avoids "ResizeObserver loop completed with undelivered notifications" error
requestAnimationFrame(() => {
hydrateRowsMetaLatest();
});
}
})).current;
const observeRowHeight = (element, rowId) => {
element.__mui_id = rowId;
resizeObserver?.observe(element);
return () => resizeObserver?.unobserve(element);
};
// The effect is used to build the rows meta data - currentPageTotalHeight and positions.
// Because of variable row height this is needed for the virtualization
(0, _useEnhancedEffect.default)(() => {
hydrateRowsMeta();
}, [hydrateRowsMeta]);
return {
getRowHeight,
setLastMeasuredRowIndex,
storeRowHeightMeasurement,
hydrateRowsMeta,
observeRowHeight,
rowHasAutoHeight,
getRowHeightEntry,
getLastMeasuredRowIndex,
resetRowHeights
};
}
function observeRootNode(node, store, setRootSize) {
if (!node) {
return undefined;
}
const bounds = node.getBoundingClientRect();
const initialSize = {
width: (0, _math.roundToDecimalPlaces)(bounds.width, 1),
height: (0, _math.roundToDecimalPlaces)(bounds.height, 1)
};
if (store.state.rootSize === _models.Size.EMPTY || !_models.Size.equals(initialSize, store.state.rootSize)) {
setRootSize(initialSize);
}
if (typeof ResizeObserver === 'undefined') {
return undefined;
}
const observer = new ResizeObserver(([entry]) => {
if (!entry) {
return;
}
const rootSize = {
width: (0, _math.roundToDecimalPlaces)(entry.contentRect.width, 1),
height: (0, _math.roundToDecimalPlaces)(entry.contentRect.height, 1)
};
if (!_models.Size.equals(rootSize, store.state.rootSize)) {
setRootSize(rootSize);
}
});
observer.observe(node);
return () => {
observer.disconnect();
};
}
const scrollbarSizeCache = new WeakMap();
function measureScrollbarSize(element, scrollbarSize) {
if (scrollbarSize !== undefined) {
return scrollbarSize;
}
if (element === null) {
return 0;
}
const cachedSize = scrollbarSizeCache.get(element);
if (cachedSize !== undefined) {
return cachedSize;
}
const doc = (0, _ownerDocument.default)(element);
const scrollDiv = doc.createElement('div');
scrollDiv.style.width = '99px';
scrollDiv.style.height = '99px';
scrollDiv.style.position = 'absolute';
scrollDiv.style.overflow = 'scroll';
scrollDiv.className = 'scrollDiv';
element.appendChild(scrollDiv);
const size = scrollDiv.offsetWidth - scrollDiv.clientWidth;
element.removeChild(scrollDiv);
scrollbarSizeCache.set(element, size);
return size;
}
function eslintUseValue(_) {}