UNPKG

@mui/x-data-grid

Version:

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

315 lines (302 loc) 12 kB
import _extends from "@babel/runtime/helpers/esm/extends"; import { DEFAULT_GRID_COL_TYPE_KEY, GRID_STRING_COL_DEF, getGridDefaultColumnTypes } from '../../../colDef'; import { gridColumnsStateSelector, gridColumnVisibilityModelSelector } from './gridColumnsSelector'; import { clamp } from '../../../utils/utils'; import { gridDensityFactorSelector } from '../density/densitySelector'; import { gridHeaderFilteringEnabledSelector } from '../headerFiltering/gridHeaderFilteringSelectors'; import { gridColumnGroupsHeaderMaxDepthSelector } from '../columnGrouping/gridColumnGroupsSelector'; export const COLUMNS_DIMENSION_PROPERTIES = ['maxWidth', 'minWidth', 'width', 'flex']; const COLUMN_TYPES = getGridDefaultColumnTypes(); /** * Computes width for flex columns. * Based on CSS Flexbox specification: * https://drafts.csswg.org/css-flexbox-1/#resolve-flexible-lengths */ export function computeFlexColumnsWidth({ initialFreeSpace, totalFlexUnits, flexColumns }) { const uniqueFlexColumns = new Set(flexColumns.map(col => col.field)); const flexColumnsLookup = { all: {}, frozenFields: [], freeze: field => { const value = flexColumnsLookup.all[field]; if (value && value.frozen !== true) { flexColumnsLookup.all[field].frozen = true; flexColumnsLookup.frozenFields.push(field); } } }; // Step 5 of https://drafts.csswg.org/css-flexbox-1/#resolve-flexible-lengths function loopOverFlexItems() { // 5a: If all the flex items on the line are frozen, free space has been distributed. if (flexColumnsLookup.frozenFields.length === uniqueFlexColumns.size) { return; } const violationsLookup = { min: {}, max: {} }; let remainingFreeSpace = initialFreeSpace; let flexUnits = totalFlexUnits; let totalViolation = 0; // 5b: Calculate the remaining free space flexColumnsLookup.frozenFields.forEach(field => { remainingFreeSpace -= flexColumnsLookup.all[field].computedWidth; flexUnits -= flexColumnsLookup.all[field].flex; }); for (let i = 0; i < flexColumns.length; i += 1) { const column = flexColumns[i]; if (flexColumnsLookup.all[column.field] && flexColumnsLookup.all[column.field].frozen === true) { continue; } // 5c: Distribute remaining free space proportional to the flex factors const widthPerFlexUnit = remainingFreeSpace / flexUnits; let computedWidth = widthPerFlexUnit * column.flex; // 5d: Fix min/max violations if (computedWidth < column.minWidth) { totalViolation += column.minWidth - computedWidth; computedWidth = column.minWidth; violationsLookup.min[column.field] = true; } else if (computedWidth > column.maxWidth) { totalViolation += column.maxWidth - computedWidth; computedWidth = column.maxWidth; violationsLookup.max[column.field] = true; } flexColumnsLookup.all[column.field] = { frozen: false, computedWidth, flex: column.flex }; } // 5e: Freeze over-flexed items if (totalViolation < 0) { // Freeze all the items with max violations Object.keys(violationsLookup.max).forEach(field => { flexColumnsLookup.freeze(field); }); } else if (totalViolation > 0) { // Freeze all the items with min violations Object.keys(violationsLookup.min).forEach(field => { flexColumnsLookup.freeze(field); }); } else { // Freeze all items flexColumns.forEach(({ field }) => { flexColumnsLookup.freeze(field); }); } // 5f: Return to the start of this loop loopOverFlexItems(); } loopOverFlexItems(); return flexColumnsLookup.all; } /** * Compute the `computedWidth` (ie: the width the column should have during rendering) based on the `width` / `flex` / `minWidth` / `maxWidth` properties of `GridColDef`. * The columns already have been merged with there `type` default values for `minWidth`, `maxWidth` and `width`, thus the `!` for those properties below. * TODO: Unit test this function in depth and only keep basic cases for the whole grid testing. * TODO: Improve the `GridColDef` typing to reflect the fact that `minWidth` / `maxWidth` and `width` can't be null after the merge with the `type` default values. */ export const hydrateColumnsWidth = (rawState, dimensions) => { const columnsLookup = {}; let totalFlexUnits = 0; let widthAllocatedBeforeFlex = 0; const flexColumns = []; // For the non-flex columns, compute their width // For the flex columns, compute there minimum width and how much width must be allocated during the flex allocation rawState.orderedFields.forEach(columnField => { const newColumn = _extends({}, rawState.lookup[columnField]); if (rawState.columnVisibilityModel[columnField] === false) { newColumn.computedWidth = 0; } else { let computedWidth; if (newColumn.flex && newColumn.flex > 0) { totalFlexUnits += newColumn.flex; computedWidth = 0; flexColumns.push(newColumn); } else { computedWidth = clamp(newColumn.width || GRID_STRING_COL_DEF.width, newColumn.minWidth || GRID_STRING_COL_DEF.minWidth, newColumn.maxWidth || GRID_STRING_COL_DEF.maxWidth); } widthAllocatedBeforeFlex += computedWidth; newColumn.computedWidth = computedWidth; } columnsLookup[columnField] = newColumn; }); const availableWidth = dimensions === undefined ? 0 : dimensions.viewportOuterSize.width - (dimensions.hasScrollY ? dimensions.scrollbarSize : 0); const initialFreeSpace = Math.max(availableWidth - widthAllocatedBeforeFlex, 0); // Allocate the remaining space to the flex columns if (totalFlexUnits > 0 && availableWidth > 0) { const computedColumnWidths = computeFlexColumnsWidth({ initialFreeSpace, totalFlexUnits, flexColumns }); Object.keys(computedColumnWidths).forEach(field => { columnsLookup[field].computedWidth = computedColumnWidths[field].computedWidth; }); } return _extends({}, rawState, { lookup: columnsLookup }); }; /** * Apply the order and the dimensions of the initial state. * The columns not registered in `orderedFields` will be placed after the imported columns. */ export const applyInitialState = (columnsState, initialState) => { if (!initialState) { return columnsState; } const { orderedFields = [], dimensions = {} } = initialState; const columnsWithUpdatedDimensions = Object.keys(dimensions); if (columnsWithUpdatedDimensions.length === 0 && orderedFields.length === 0) { return columnsState; } const orderedFieldsLookup = {}; const cleanOrderedFields = []; for (let i = 0; i < orderedFields.length; i += 1) { const field = orderedFields[i]; // Ignores the fields in the initialState that matches no field on the current column state if (columnsState.lookup[field]) { orderedFieldsLookup[field] = true; cleanOrderedFields.push(field); } } const newOrderedFields = cleanOrderedFields.length === 0 ? columnsState.orderedFields : [...cleanOrderedFields, ...columnsState.orderedFields.filter(field => !orderedFieldsLookup[field])]; const newColumnLookup = _extends({}, columnsState.lookup); for (let i = 0; i < columnsWithUpdatedDimensions.length; i += 1) { const field = columnsWithUpdatedDimensions[i]; const newColDef = _extends({}, newColumnLookup[field], { hasBeenResized: true }); Object.entries(dimensions[field]).forEach(([key, value]) => { newColDef[key] = value === -1 ? Infinity : value; }); newColumnLookup[field] = newColDef; } const newColumnsState = _extends({}, columnsState, { orderedFields: newOrderedFields, lookup: newColumnLookup }); return newColumnsState; }; function getDefaultColTypeDef(type) { let colDef = COLUMN_TYPES[DEFAULT_GRID_COL_TYPE_KEY]; if (type && COLUMN_TYPES[type]) { colDef = COLUMN_TYPES[type]; } return colDef; } export const createColumnsState = ({ apiRef, columnsToUpsert, initialState, columnVisibilityModel = gridColumnVisibilityModelSelector(apiRef), keepOnlyColumnsToUpsert = false }) => { const isInsideStateInitializer = !apiRef.current.state.columns; let columnsState; if (isInsideStateInitializer) { columnsState = { orderedFields: [], lookup: {}, columnVisibilityModel }; } else { const currentState = gridColumnsStateSelector(apiRef.current.state); columnsState = { orderedFields: keepOnlyColumnsToUpsert ? [] : [...currentState.orderedFields], lookup: _extends({}, currentState.lookup), // Will be cleaned later if keepOnlyColumnsToUpsert=true columnVisibilityModel }; } let columnsToKeep = {}; if (keepOnlyColumnsToUpsert && !isInsideStateInitializer) { columnsToKeep = Object.keys(columnsState.lookup).reduce((acc, key) => _extends({}, acc, { [key]: false }), {}); } const columnsToUpsertLookup = {}; columnsToUpsert.forEach(newColumn => { const { field } = newColumn; columnsToUpsertLookup[field] = true; columnsToKeep[field] = true; let existingState = columnsState.lookup[field]; if (existingState == null) { existingState = _extends({}, getDefaultColTypeDef(newColumn.type), { field, hasBeenResized: false }); columnsState.orderedFields.push(field); } else if (keepOnlyColumnsToUpsert) { columnsState.orderedFields.push(field); } // If the column type has changed - merge the existing state with the default column type definition if (existingState && existingState.type !== newColumn.type) { existingState = _extends({}, getDefaultColTypeDef(newColumn.type), { field }); } let hasBeenResized = existingState.hasBeenResized; COLUMNS_DIMENSION_PROPERTIES.forEach(key => { if (newColumn[key] !== undefined) { hasBeenResized = true; if (newColumn[key] === -1) { newColumn[key] = Infinity; } } }); columnsState.lookup[field] = _extends({}, existingState, newColumn, { hasBeenResized }); }); if (keepOnlyColumnsToUpsert && !isInsideStateInitializer) { Object.keys(columnsState.lookup).forEach(field => { if (!columnsToKeep[field]) { delete columnsState.lookup[field]; } }); } const columnsStateWithPreProcessing = apiRef.current.unstable_applyPipeProcessors('hydrateColumns', columnsState); const columnsStateWithPortableColumns = applyInitialState(columnsStateWithPreProcessing, initialState); return hydrateColumnsWidth(columnsStateWithPortableColumns, apiRef.current.getRootDimensions?.() ?? undefined); }; export function getFirstNonSpannedColumnToRender({ firstColumnToRender, apiRef, firstRowToRender, lastRowToRender, visibleRows }) { let firstNonSpannedColumnToRender = firstColumnToRender; for (let i = firstRowToRender; i < lastRowToRender; i += 1) { const row = visibleRows[i]; if (row) { const rowId = visibleRows[i].id; const cellColSpanInfo = apiRef.current.unstable_getCellColSpanInfo(rowId, firstColumnToRender); if (cellColSpanInfo && cellColSpanInfo.spannedByColSpan) { firstNonSpannedColumnToRender = cellColSpanInfo.leftVisibleCellIndex; } } } return firstNonSpannedColumnToRender; } export function getTotalHeaderHeight(apiRef, props) { const densityFactor = gridDensityFactorSelector(apiRef); const maxDepth = gridColumnGroupsHeaderMaxDepthSelector(apiRef); const isHeaderFilteringEnabled = gridHeaderFilteringEnabledSelector(apiRef); const columnHeadersHeight = Math.floor(props.columnHeaderHeight * densityFactor); const filterHeadersHeight = isHeaderFilteringEnabled ? Math.floor((props.headerFilterHeight ?? props.columnHeaderHeight) * densityFactor) : 0; return columnHeadersHeight * (1 + (maxDepth ?? 0)) + filterHeadersHeight; }