UNPKG

@mui/x-data-grid

Version:

The Community plan edition of the Data Grid components (MUI X).

715 lines (703 loc) 30.5 kB
import _extends from "@babel/runtime/helpers/esm/extends"; import * as React from 'react'; import * as ReactDOM from 'react-dom'; import { unstable_useEnhancedEffect as useEnhancedEffect, unstable_useEventCallback as useEventCallback } from '@mui/utils'; import useLazyRef from '@mui/utils/useLazyRef'; import useTimeout from '@mui/utils/useTimeout'; import { useTheme } from '@mui/material/styles'; import { useGridPrivateApiContext } from '../../utils/useGridPrivateApiContext'; import { useGridRootProps } from '../../utils/useGridRootProps'; import { useGridSelector } from '../../utils/useGridSelector'; import { useResizeObserver } from '../../utils/useResizeObserver'; import { useRunOnce } from '../../utils/useRunOnce'; import { gridVisibleColumnDefinitionsSelector, gridVisiblePinnedColumnDefinitionsSelector, gridColumnPositionsSelector, gridHasColSpanSelector } from '../columns/gridColumnsSelector'; import { gridDimensionsSelector } from '../dimensions/gridDimensionsSelectors'; import { gridPinnedRowsSelector } from '../rows/gridRowsSelector'; import { gridFocusCellSelector, gridTabIndexCellSelector } from '../focus/gridFocusStateSelector'; import { useGridVisibleRows, getVisibleRows } from '../../utils/useGridVisibleRows'; import { useGridApiEventHandler } from '../../utils'; import { clamp, range } from '../../../utils/utils'; import { selectedIdsLookupSelector } from '../rowSelection/gridRowSelectionSelector'; import { gridRowsMetaSelector } from '../rows/gridRowsMetaSelector'; import { getFirstNonSpannedColumnToRender } from '../columns/gridColumnsUtils'; import { getMinimalContentHeight } from '../rows/gridRowsUtils'; import { gridRenderContextSelector, gridVirtualizationEnabledSelector, gridVirtualizationColumnEnabledSelector } from './gridVirtualizationSelectors'; import { EMPTY_RENDER_CONTEXT } from './useGridVirtualization'; import { jsx as _jsx } from "react/jsx-runtime"; const MINIMUM_COLUMN_WIDTH = 50; var ScrollDirection = /*#__PURE__*/function (ScrollDirection) { ScrollDirection[ScrollDirection["NONE"] = 0] = "NONE"; ScrollDirection[ScrollDirection["UP"] = 1] = "UP"; ScrollDirection[ScrollDirection["DOWN"] = 2] = "DOWN"; ScrollDirection[ScrollDirection["LEFT"] = 3] = "LEFT"; ScrollDirection[ScrollDirection["RIGHT"] = 4] = "RIGHT"; return ScrollDirection; }(ScrollDirection || {}); const EMPTY_SCROLL_POSITION = { top: 0, left: 0 }; export const EMPTY_DETAIL_PANELS = Object.freeze(new Map()); const createScrollCache = (mode, rowBufferPx, columnBufferPx, verticalBuffer, horizontalBuffer) => ({ direction: ScrollDirection.NONE, buffer: bufferForDirection(mode, ScrollDirection.NONE, rowBufferPx, columnBufferPx, verticalBuffer, horizontalBuffer) }); const isJSDOM = typeof window !== 'undefined' ? /jsdom/.test(window.navigator.userAgent) : false; export const useGridVirtualScroller = () => { const apiRef = useGridPrivateApiContext(); const rootProps = useGridRootProps(); const visibleColumns = useGridSelector(apiRef, gridVisibleColumnDefinitionsSelector); const enabled = useGridSelector(apiRef, gridVirtualizationEnabledSelector) && !isJSDOM; const enabledForColumns = useGridSelector(apiRef, gridVirtualizationColumnEnabledSelector) && !isJSDOM; const dimensions = useGridSelector(apiRef, gridDimensionsSelector); const outerSize = dimensions.viewportOuterSize; const pinnedRows = useGridSelector(apiRef, gridPinnedRowsSelector); const pinnedColumns = useGridSelector(apiRef, gridVisiblePinnedColumnDefinitionsSelector); const hasBottomPinnedRows = pinnedRows.bottom.length > 0; const [panels, setPanels] = React.useState(EMPTY_DETAIL_PANELS); const theme = useTheme(); const cellFocus = useGridSelector(apiRef, gridFocusCellSelector); const cellTabIndex = useGridSelector(apiRef, gridTabIndexCellSelector); const rowsMeta = useGridSelector(apiRef, gridRowsMetaSelector); const selectedRowsLookup = useGridSelector(apiRef, selectedIdsLookupSelector); const currentPage = useGridVisibleRows(apiRef, rootProps); const gridRootRef = apiRef.current.rootElementRef; const mainRef = apiRef.current.mainElementRef; const scrollerRef = apiRef.current.virtualScrollerRef; const scrollbarVerticalRef = React.useRef(null); const scrollbarHorizontalRef = React.useRef(null); const contentHeight = dimensions.contentSize.height; const columnsTotalWidth = dimensions.columnsTotalWidth; const hasColSpan = useGridSelector(apiRef, gridHasColSpanSelector); useResizeObserver(mainRef, () => apiRef.current.resize()); /* * Scroll context logic * ==================== * We only render the cells contained in the `renderContext`. However, when the user starts scrolling the grid * in a direction, we want to render as many cells as possible in that direction, as to avoid presenting white * areas if the user scrolls too fast/far and the viewport ends up in a region we haven't rendered yet. To render * more cells, we store some offsets to add to the viewport in `scrollCache.buffer`. Those offsets make the render * context wider in the direction the user is going, but also makes the buffer around the viewport `0` for the * dimension (horizontal or vertical) in which the user is not scrolling. So if the normal viewport is 8 columns * wide, with a 1 column buffer (10 columns total), then we want it to be exactly 8 columns wide during vertical * scroll. * However, we don't want the rows in the old context to re-render from e.g. 10 columns to 8 columns, because that's * work that's not necessary. Thus we store the context at the start of the scroll in `frozenContext`, and the rows * that are part of this old context will keep their same render context as to avoid re-rendering. */ const scrollPosition = React.useRef(EMPTY_SCROLL_POSITION); const previousContextScrollPosition = React.useRef(EMPTY_SCROLL_POSITION); const previousRowContext = React.useRef(EMPTY_RENDER_CONTEXT); const renderContext = useGridSelector(apiRef, gridRenderContextSelector); const scrollTimeout = useTimeout(); const frozenContext = React.useRef(undefined); const scrollCache = useLazyRef(() => createScrollCache(theme.direction, rootProps.rowBufferPx, rootProps.columnBufferPx, dimensions.rowHeight * 15, MINIMUM_COLUMN_WIDTH * 6)).current; const focusedCell = { rowIndex: React.useMemo(() => cellFocus ? currentPage.rows.findIndex(row => row.id === cellFocus.id) : -1, [cellFocus, currentPage.rows]), columnIndex: React.useMemo(() => cellFocus ? visibleColumns.findIndex(column => column.field === cellFocus.field) : -1, [cellFocus, visibleColumns]) }; const updateRenderContext = React.useCallback(nextRenderContext => { if (areRenderContextsEqual(nextRenderContext, apiRef.current.state.virtualization.renderContext)) { return; } const didRowsIntervalChange = nextRenderContext.firstRowIndex !== previousRowContext.current.firstRowIndex || nextRenderContext.lastRowIndex !== previousRowContext.current.lastRowIndex; apiRef.current.setState(state => { return _extends({}, state, { virtualization: _extends({}, state.virtualization, { renderContext: nextRenderContext }) }); }); // The lazy-loading hook is listening to `renderedRowsIntervalChange`, // but only does something if the dimensions are also available. // So we wait until we have valid dimensions before publishing the first event. if (dimensions.isReady && didRowsIntervalChange) { previousRowContext.current = nextRenderContext; apiRef.current.publishEvent('renderedRowsIntervalChange', nextRenderContext); } previousContextScrollPosition.current = scrollPosition.current; }, [apiRef, dimensions.isReady]); const triggerUpdateRenderContext = () => { const newScroll = { top: scrollerRef.current.scrollTop, left: scrollerRef.current.scrollLeft }; const dx = newScroll.left - scrollPosition.current.left; const dy = newScroll.top - scrollPosition.current.top; const isScrolling = dx !== 0 || dy !== 0; scrollPosition.current = newScroll; const direction = isScrolling ? directionForDelta(dx, dy) : ScrollDirection.NONE; // Since previous render, we have scrolled... const rowScroll = Math.abs(scrollPosition.current.top - previousContextScrollPosition.current.top); const columnScroll = Math.abs(scrollPosition.current.left - previousContextScrollPosition.current.left); // PERF: use the computed minimum column width instead of a static one const didCrossThreshold = rowScroll >= dimensions.rowHeight || columnScroll >= MINIMUM_COLUMN_WIDTH; const didChangeDirection = scrollCache.direction !== direction; const shouldUpdate = didCrossThreshold || didChangeDirection; if (!shouldUpdate) { return renderContext; } // Render a new context if (didChangeDirection) { switch (direction) { case ScrollDirection.NONE: case ScrollDirection.LEFT: case ScrollDirection.RIGHT: frozenContext.current = undefined; break; default: frozenContext.current = renderContext; break; } } scrollCache.direction = direction; scrollCache.buffer = bufferForDirection(theme.direction, direction, rootProps.rowBufferPx, rootProps.columnBufferPx, dimensions.rowHeight * 15, MINIMUM_COLUMN_WIDTH * 6); const inputs = inputsSelector(apiRef, rootProps, enabled, enabledForColumns); const nextRenderContext = computeRenderContext(inputs, scrollPosition.current, scrollCache); // Prevents batching render context changes ReactDOM.flushSync(() => { updateRenderContext(nextRenderContext); }); scrollTimeout.start(1000, triggerUpdateRenderContext); return nextRenderContext; }; const forceUpdateRenderContext = () => { const inputs = inputsSelector(apiRef, rootProps, enabled, enabledForColumns); const nextRenderContext = computeRenderContext(inputs, scrollPosition.current, scrollCache); updateRenderContext(nextRenderContext); }; const handleScroll = useEventCallback(event => { const { scrollTop, scrollLeft } = event.currentTarget; // On iOS and macOS, negative offsets are possible when swiping past the start if (scrollTop < 0) { return; } if (theme.direction === 'ltr') { if (scrollLeft < 0) { return; } } if (theme.direction === 'rtl') { if (scrollLeft > 0) { return; } } const nextRenderContext = triggerUpdateRenderContext(); apiRef.current.publishEvent('scrollPositionChange', { top: scrollTop, left: scrollLeft, renderContext: nextRenderContext }); }); const handleWheel = useEventCallback(event => { apiRef.current.publishEvent('virtualScrollerWheel', {}, event); }); const handleTouchMove = useEventCallback(event => { apiRef.current.publishEvent('virtualScrollerTouchMove', {}, event); }); const getRows = (params = {}) => { if (!params.rows && !currentPage.range) { return []; } const baseRenderContext = params.renderContext ?? renderContext; const isLastSection = !hasBottomPinnedRows && params.position === undefined || hasBottomPinnedRows && params.position === 'bottom'; const isPinnedSection = params.position !== undefined; let rowIndexOffset; // FIXME: Why is the switch check exhaustiveness not validated with typescript-eslint? // eslint-disable-next-line default-case switch (params.position) { case 'top': rowIndexOffset = 0; break; case 'bottom': rowIndexOffset = pinnedRows.top.length + currentPage.rows.length; break; case undefined: rowIndexOffset = pinnedRows.top.length; break; } const rowModels = params.rows ?? currentPage.rows; const firstRowToRender = baseRenderContext.firstRowIndex; const lastRowToRender = Math.min(baseRenderContext.lastRowIndex, rowModels.length); const rowIndexes = params.rows ? range(0, params.rows.length) : range(firstRowToRender, lastRowToRender); let virtualRowIndex = -1; if (!isPinnedSection && focusedCell.rowIndex !== -1) { if (focusedCell.rowIndex < firstRowToRender) { virtualRowIndex = focusedCell.rowIndex; rowIndexes.unshift(virtualRowIndex); } if (focusedCell.rowIndex >= lastRowToRender) { virtualRowIndex = focusedCell.rowIndex; rowIndexes.push(virtualRowIndex); } } const rows = []; const rowProps = rootProps.slotProps?.row; const columnPositions = gridColumnPositionsSelector(apiRef); rowIndexes.forEach(rowIndexInPage => { const { id, model } = rowModels[rowIndexInPage]; // NOTE: This is an expensive feature, the colSpan code could be optimized. if (hasColSpan) { const minFirstColumn = pinnedColumns.left.length; const maxLastColumn = visibleColumns.length - pinnedColumns.right.length; apiRef.current.calculateColSpan({ rowId: id, minFirstColumn, maxLastColumn, columns: visibleColumns }); if (pinnedColumns.left.length > 0) { apiRef.current.calculateColSpan({ rowId: id, minFirstColumn: 0, maxLastColumn: pinnedColumns.left.length, columns: visibleColumns }); } if (pinnedColumns.right.length > 0) { apiRef.current.calculateColSpan({ rowId: id, minFirstColumn: visibleColumns.length - pinnedColumns.right.length, maxLastColumn: visibleColumns.length, columns: visibleColumns }); } } const hasFocus = cellFocus?.id === id; const baseRowHeight = !apiRef.current.rowHasAutoHeight(id) ? apiRef.current.unstable_getRowHeight(id) : 'auto'; let isSelected; if (selectedRowsLookup[id] == null) { isSelected = false; } else { isSelected = apiRef.current.isRowSelectable(id); } let isFirstVisible = false; if (params.position === undefined) { isFirstVisible = rowIndexInPage === 0; } let isLastVisible = false; if (isLastSection) { if (!isPinnedSection) { const lastIndex = currentPage.rows.length - 1; const isLastVisibleRowIndex = rowIndexInPage === lastIndex; if (isLastVisibleRowIndex) { isLastVisible = true; } } else { isLastVisible = rowIndexInPage === rowModels.length - 1; } } const isVirtualRow = rowIndexInPage === virtualRowIndex; const isNotVisible = isVirtualRow; let tabbableCell = null; if (cellTabIndex !== null && cellTabIndex.id === id) { const cellParams = apiRef.current.getCellParams(id, cellTabIndex.field); tabbableCell = cellParams.cellMode === 'view' ? cellTabIndex.field : null; } let currentRenderContext = baseRenderContext; if (!isPinnedSection && frozenContext.current && rowIndexInPage >= frozenContext.current.firstRowIndex && rowIndexInPage < frozenContext.current.lastRowIndex) { currentRenderContext = frozenContext.current; } const offsetLeft = computeOffsetLeft(columnPositions, currentRenderContext, theme.direction, pinnedColumns.left.length); const rowIndex = (currentPage?.range?.firstRowIndex || 0) + rowIndexOffset + rowIndexInPage; rows.push( /*#__PURE__*/_jsx(rootProps.slots.row, _extends({ row: model, rowId: id, index: rowIndex, selected: isSelected, offsetTop: params.rows ? undefined : rowsMeta.positions[rowIndexInPage], offsetLeft: offsetLeft, dimensions: dimensions, rowHeight: baseRowHeight, tabbableCell: tabbableCell, pinnedColumns: pinnedColumns, visibleColumns: visibleColumns, renderContext: currentRenderContext, focusedColumnIndex: hasFocus ? focusedCell.columnIndex : undefined, isFirstVisible: isFirstVisible, isLastVisible: isLastVisible, isNotVisible: isNotVisible }, rowProps), id)); const panel = panels.get(id); if (panel) { rows.push(panel); } if (isLastVisible) { rows.push(apiRef.current.getInfiniteLoadingTriggerElement?.({ lastRowId: id })); } }); return rows; }; const needsHorizontalScrollbar = outerSize.width && columnsTotalWidth >= outerSize.width; const scrollerStyle = React.useMemo(() => ({ overflowX: !needsHorizontalScrollbar ? 'hidden' : undefined, overflowY: rootProps.autoHeight ? 'hidden' : undefined }), [needsHorizontalScrollbar, rootProps.autoHeight]); const contentSize = React.useMemo(() => { // In cases where the columns exceed the available width, // the horizontal scrollbar should be shown even when there're no rows. // Keeping 1px as minimum height ensures that the scrollbar will visible if necessary. const height = Math.max(contentHeight, 1); const size = { width: needsHorizontalScrollbar ? columnsTotalWidth : 'auto', height }; if (rootProps.autoHeight) { if (currentPage.rows.length === 0) { size.height = getMinimalContentHeight(apiRef); // Give room to show the overlay when there no rows. } else { size.height = contentHeight; } } return size; }, [apiRef, columnsTotalWidth, contentHeight, needsHorizontalScrollbar, rootProps.autoHeight, currentPage.rows.length]); React.useEffect(() => { apiRef.current.publishEvent('virtualScrollerContentSizeChange'); }, [apiRef, contentSize]); useEnhancedEffect(() => { // FIXME: Is this really necessary? apiRef.current.resize(); }, [apiRef, rowsMeta.currentPageTotalHeight]); useEnhancedEffect(() => { if (enabled) { // TODO a scroll reset should not be necessary scrollerRef.current.scrollLeft = 0; scrollerRef.current.scrollTop = 0; } }, [enabled, gridRootRef, scrollerRef]); useRunOnce(outerSize.width !== 0, () => { const inputs = inputsSelector(apiRef, rootProps, enabled, enabledForColumns); const initialRenderContext = computeRenderContext(inputs, scrollPosition.current, scrollCache); updateRenderContext(initialRenderContext); apiRef.current.publishEvent('scrollPositionChange', { top: scrollPosition.current.top, left: scrollPosition.current.left, renderContext: initialRenderContext }); }); apiRef.current.register('private', { updateRenderContext: forceUpdateRenderContext }); useGridApiEventHandler(apiRef, 'columnsChange', forceUpdateRenderContext); useGridApiEventHandler(apiRef, 'filteredRowsSet', forceUpdateRenderContext); useGridApiEventHandler(apiRef, 'rowExpansionChange', forceUpdateRenderContext); return { renderContext, setPanels, getRows, getContainerProps: () => ({ ref: mainRef }), getScrollerProps: () => ({ ref: scrollerRef, tabIndex: -1, onScroll: handleScroll, onWheel: handleWheel, onTouchMove: handleTouchMove, style: scrollerStyle, role: 'presentation' }), getContentProps: () => ({ style: contentSize, role: 'presentation' }), getRenderZoneProps: () => ({ role: 'rowgroup' }), getScrollbarVerticalProps: () => ({ ref: scrollbarVerticalRef, role: 'presentation' }), getScrollbarHorizontalProps: () => ({ ref: scrollbarHorizontalRef, role: 'presentation' }) }; }; function inputsSelector(apiRef, rootProps, enabled, enabledForColumns) { const dimensions = gridDimensionsSelector(apiRef.current.state); const currentPage = getVisibleRows(apiRef, rootProps); const visibleColumns = gridVisibleColumnDefinitionsSelector(apiRef); const lastRowId = apiRef.current.state.rows.dataRowIds.at(-1); const lastColumn = visibleColumns.at(-1); return { enabled, enabledForColumns, apiRef, autoHeight: rootProps.autoHeight, rowBufferPx: rootProps.rowBufferPx, columnBufferPx: rootProps.columnBufferPx, leftPinnedWidth: dimensions.leftPinnedWidth, columnsTotalWidth: dimensions.columnsTotalWidth, viewportInnerWidth: dimensions.viewportInnerSize.width, viewportInnerHeight: dimensions.viewportInnerSize.height, lastRowHeight: lastRowId !== undefined ? apiRef.current.unstable_getRowHeight(lastRowId) : 0, lastColumnWidth: lastColumn?.computedWidth ?? 0, rowsMeta: gridRowsMetaSelector(apiRef.current.state), columnPositions: gridColumnPositionsSelector(apiRef), rows: currentPage.rows, range: currentPage.range, pinnedColumns: gridVisiblePinnedColumnDefinitionsSelector(apiRef), visibleColumns }; } function computeRenderContext(inputs, scrollPosition, scrollCache) { let renderContext; if (!inputs.enabled) { renderContext = { firstRowIndex: 0, lastRowIndex: inputs.rows.length, firstColumnIndex: 0, lastColumnIndex: inputs.visibleColumns.length }; } else { const { top, left } = scrollPosition; const realLeft = Math.abs(left) + inputs.leftPinnedWidth; // Clamp the value because the search may return an index out of bounds. // In the last index, this is not needed because Array.slice doesn't include it. const firstRowIndex = Math.min(getNearestIndexToRender(inputs, top, { atStart: true, lastPosition: inputs.rowsMeta.positions[inputs.rowsMeta.positions.length - 1] + inputs.lastRowHeight }), inputs.rowsMeta.positions.length - 1); const lastRowIndex = inputs.autoHeight ? firstRowIndex + inputs.rows.length : getNearestIndexToRender(inputs, top + inputs.viewportInnerHeight); let firstColumnIndex = 0; let lastColumnIndex = inputs.columnPositions.length; if (inputs.enabledForColumns) { let hasRowWithAutoHeight = false; const [firstRowToRender, lastRowToRender] = getIndexesToRender({ firstIndex: firstRowIndex, lastIndex: lastRowIndex, minFirstIndex: 0, maxLastIndex: inputs.rows.length, bufferBefore: scrollCache.buffer.rowBefore, bufferAfter: scrollCache.buffer.rowAfter, positions: inputs.rowsMeta.positions, lastSize: inputs.lastRowHeight }); for (let i = firstRowToRender; i < lastRowToRender && !hasRowWithAutoHeight; i += 1) { const row = inputs.rows[i]; hasRowWithAutoHeight = inputs.apiRef.current.rowHasAutoHeight(row.id); } if (!hasRowWithAutoHeight) { firstColumnIndex = binarySearch(realLeft, inputs.columnPositions, { atStart: true, lastPosition: inputs.columnsTotalWidth }); lastColumnIndex = binarySearch(realLeft + inputs.viewportInnerWidth, inputs.columnPositions); } } renderContext = { firstRowIndex, lastRowIndex, firstColumnIndex, lastColumnIndex }; } const actualRenderContext = deriveRenderContext(inputs, renderContext, scrollCache); return actualRenderContext; } function getNearestIndexToRender(inputs, offset, options) { const lastMeasuredIndexRelativeToAllRows = inputs.apiRef.current.getLastMeasuredRowIndex(); let allRowsMeasured = lastMeasuredIndexRelativeToAllRows === Infinity; if (inputs.range?.lastRowIndex && !allRowsMeasured) { // Check if all rows in this page are already measured allRowsMeasured = lastMeasuredIndexRelativeToAllRows >= inputs.range.lastRowIndex; } const lastMeasuredIndexRelativeToCurrentPage = clamp(lastMeasuredIndexRelativeToAllRows - (inputs.range?.firstRowIndex || 0), 0, inputs.rowsMeta.positions.length); if (allRowsMeasured || inputs.rowsMeta.positions[lastMeasuredIndexRelativeToCurrentPage] >= offset) { // If all rows were measured (when no row has "auto" as height) or all rows before the offset // were measured, then use a binary search because it's faster. return binarySearch(offset, inputs.rowsMeta.positions, options); } // Otherwise, use an exponential search. // If rows have "auto" as height, their positions will be based on estimated heights. // In this case, we can skip several steps until we find a position higher than the offset. // Inspired by https://github.com/bvaughn/react-virtualized/blob/master/source/Grid/utils/CellSizeAndPositionManager.js return exponentialSearch(offset, inputs.rowsMeta.positions, lastMeasuredIndexRelativeToCurrentPage, options); } /** * Accepts as input a raw render context (the area visible in the viewport) and adds * computes the actual render context based on pinned elements, buffer dimensions and * spanning. */ function deriveRenderContext(inputs, nextRenderContext, scrollCache) { const [firstRowToRender, lastRowToRender] = getIndexesToRender({ firstIndex: nextRenderContext.firstRowIndex, lastIndex: nextRenderContext.lastRowIndex, minFirstIndex: 0, maxLastIndex: inputs.rows.length, bufferBefore: scrollCache.buffer.rowBefore, bufferAfter: scrollCache.buffer.rowAfter, positions: inputs.rowsMeta.positions, lastSize: inputs.lastRowHeight }); const [initialFirstColumnToRender, lastColumnToRender] = getIndexesToRender({ firstIndex: nextRenderContext.firstColumnIndex, lastIndex: nextRenderContext.lastColumnIndex, minFirstIndex: inputs.pinnedColumns.left.length, maxLastIndex: inputs.visibleColumns.length - inputs.pinnedColumns.right.length, bufferBefore: scrollCache.buffer.columnBefore, bufferAfter: scrollCache.buffer.columnAfter, positions: inputs.columnPositions, lastSize: inputs.lastColumnWidth }); const firstColumnToRender = getFirstNonSpannedColumnToRender({ firstColumnToRender: initialFirstColumnToRender, apiRef: inputs.apiRef, firstRowToRender, lastRowToRender, visibleRows: inputs.rows }); return { firstRowIndex: firstRowToRender, lastRowIndex: lastRowToRender, firstColumnIndex: firstColumnToRender, lastColumnIndex: lastColumnToRender }; } /** * Use binary search to avoid looping through all possible positions. * The `options.atStart` provides the possibility to match for the first element that * intersects the screen, even if said element's start position is before `offset`. In * other words, we search for `offset + width`. */ function binarySearch(offset, positions, options = undefined, sliceStart = 0, sliceEnd = positions.length) { if (positions.length <= 0) { return -1; } if (sliceStart >= sliceEnd) { return sliceStart; } const pivot = sliceStart + Math.floor((sliceEnd - sliceStart) / 2); const position = positions[pivot]; let isBefore; if (options?.atStart) { const width = (pivot === positions.length - 1 ? options.lastPosition : positions[pivot + 1]) - position; isBefore = offset - width < position; } else { isBefore = offset <= position; } return isBefore ? binarySearch(offset, positions, options, sliceStart, pivot) : binarySearch(offset, positions, options, pivot + 1, sliceEnd); } function exponentialSearch(offset, positions, index, options = undefined) { let interval = 1; while (index < positions.length && Math.abs(positions[index]) < offset) { index += interval; interval *= 2; } return binarySearch(offset, positions, options, Math.floor(index / 2), Math.min(index, positions.length)); } function getIndexesToRender({ firstIndex, lastIndex, bufferBefore, bufferAfter, minFirstIndex, maxLastIndex, positions, lastSize }) { const firstPosition = positions[firstIndex] - bufferBefore; const lastPosition = positions[lastIndex] + bufferAfter; const firstIndexPadded = binarySearch(firstPosition, positions, { atStart: true, lastPosition: positions[positions.length - 1] + lastSize }); const lastIndexPadded = binarySearch(lastPosition, positions); return [clamp(firstIndexPadded, minFirstIndex, maxLastIndex), clamp(lastIndexPadded, minFirstIndex, maxLastIndex)]; } export function areRenderContextsEqual(context1, context2) { if (context1 === context2) { return true; } return context1.firstRowIndex === context2.firstRowIndex && context1.lastRowIndex === context2.lastRowIndex && context1.firstColumnIndex === context2.firstColumnIndex && context1.lastColumnIndex === context2.lastColumnIndex; } export function computeOffsetLeft(columnPositions, renderContext, direction, pinnedLeftLength) { const factor = direction === 'ltr' ? 1 : -1; const left = factor * (columnPositions[renderContext.firstColumnIndex] ?? 0) - (columnPositions[pinnedLeftLength] ?? 0); return Math.abs(left); } function directionForDelta(dx, dy) { if (dx === 0 && dy === 0) { return ScrollDirection.NONE; } /* eslint-disable */ if (Math.abs(dy) >= Math.abs(dx)) { if (dy > 0) { return ScrollDirection.DOWN; } else { return ScrollDirection.UP; } } else { if (dx > 0) { return ScrollDirection.RIGHT; } else { return ScrollDirection.LEFT; } } /* eslint-enable */ } function bufferForDirection(mode, direction, rowBufferPx, columnBufferPx, verticalBuffer, horizontalBuffer) { if (mode === 'rtl') { switch (direction) { case ScrollDirection.LEFT: direction = ScrollDirection.RIGHT; break; case ScrollDirection.RIGHT: direction = ScrollDirection.LEFT; break; default: } } switch (direction) { case ScrollDirection.NONE: return { rowAfter: rowBufferPx, rowBefore: rowBufferPx, columnAfter: columnBufferPx, columnBefore: columnBufferPx }; case ScrollDirection.LEFT: return { rowAfter: 0, rowBefore: 0, columnAfter: 0, columnBefore: horizontalBuffer }; case ScrollDirection.RIGHT: return { rowAfter: 0, rowBefore: 0, columnAfter: horizontalBuffer, columnBefore: 0 }; case ScrollDirection.UP: return { rowAfter: 0, rowBefore: verticalBuffer, columnAfter: 0, columnBefore: 0 }; case ScrollDirection.DOWN: return { rowAfter: verticalBuffer, rowBefore: 0, columnAfter: 0, columnBefore: 0 }; default: // eslint unable to figure out enum exhaustiveness throw new Error('unreachable'); } }