@inbuild/material-react-table
Version:
A fully featured Material UI V5 implementation of TanStack React Table V8, written from the ground up in TypeScript.
382 lines (357 loc) • 11.7 kB
text/typescript
import { MRT_AggregationFns } from './aggregationFns';
import { MRT_FilterFns } from './filterFns';
import { MRT_SortingFns } from './sortingFns';
import { alpha, lighten } from '@mui/material/styles';
import type { Row } from '@tanstack/react-table';
import type { TableCellProps } from '@mui/material/TableCell';
import type { Theme } from '@mui/material/styles';
import type {
MaterialReactTableProps,
MRT_Column,
MRT_ColumnDef,
MRT_ColumnOrderState,
MRT_DefinedColumnDef,
MRT_DisplayColumnIds,
MRT_FilterOption,
MRT_GroupingState,
MRT_Header,
MRT_TableInstance,
} from '.';
export const getColumnId = <TData extends Record<string, any> = {}>(
columnDef: MRT_ColumnDef<TData>,
): string =>
columnDef.id ?? columnDef.accessorKey?.toString?.() ?? columnDef.header;
export const getAllLeafColumnDefs = <TData extends Record<string, any> = {}>(
columns: MRT_ColumnDef<TData>[],
): MRT_ColumnDef<TData>[] => {
const allLeafColumnDefs: MRT_ColumnDef<TData>[] = [];
const getLeafColumns = (cols: MRT_ColumnDef<TData>[]) => {
cols.forEach((col) => {
if (col.columns) {
getLeafColumns(col.columns);
} else {
allLeafColumnDefs.push(col);
}
});
};
getLeafColumns(columns);
return allLeafColumnDefs;
};
export const prepareColumns = <TData extends Record<string, any> = {}>({
aggregationFns,
columnDefs,
columnFilterFns,
defaultDisplayColumn,
filterFns,
sortingFns,
}: {
aggregationFns: typeof MRT_AggregationFns &
MaterialReactTableProps<TData>['aggregationFns'];
columnDefs: MRT_ColumnDef<TData>[];
columnFilterFns: { [key: string]: MRT_FilterOption };
defaultDisplayColumn: Partial<MRT_ColumnDef<TData>>;
filterFns: typeof MRT_FilterFns & MaterialReactTableProps<TData>['filterFns'];
sortingFns: typeof MRT_SortingFns &
MaterialReactTableProps<TData>['sortingFns'];
}): MRT_DefinedColumnDef<TData>[] =>
columnDefs.map((columnDef) => {
//assign columnId
if (!columnDef.id) columnDef.id = getColumnId(columnDef);
if (process.env.NODE_ENV !== 'production' && !columnDef.id) {
console.error(
'Column definitions must have a valid `accessorKey` or `id` property',
);
}
//assign columnDefType
if (!columnDef.columnDefType) columnDef.columnDefType = 'data';
if (columnDef.columns?.length) {
columnDef.columnDefType = 'group';
//recursively prepare columns if this is a group column
columnDef.columns = prepareColumns({
aggregationFns,
columnDefs: columnDef.columns,
columnFilterFns,
defaultDisplayColumn,
filterFns,
sortingFns,
});
} else if (columnDef.columnDefType === 'data') {
//assign aggregationFns if multiple aggregationFns are provided
if (Array.isArray(columnDef.aggregationFn)) {
const aggFns = columnDef.aggregationFn as string[];
columnDef.aggregationFn = (
columnId: string,
leafRows: Row<TData>[],
childRows: Row<TData>[],
) =>
aggFns.map((fn) =>
aggregationFns[fn]?.(columnId, leafRows, childRows),
);
}
//assign filterFns
if (Object.keys(filterFns).includes(columnFilterFns[columnDef.id])) {
columnDef.filterFn =
filterFns[columnFilterFns[columnDef.id]] ?? filterFns.fuzzy;
(columnDef as MRT_DefinedColumnDef)._filterFn =
columnFilterFns[columnDef.id];
}
//assign sortingFns
if (Object.keys(sortingFns).includes(columnDef.sortingFn as string)) {
// @ts-ignore
columnDef.sortingFn = sortingFns[columnDef.sortingFn];
}
} else if (columnDef.columnDefType === 'display') {
columnDef = {
...(defaultDisplayColumn as MRT_ColumnDef<TData>),
...columnDef,
};
}
return columnDef;
}) as MRT_DefinedColumnDef<TData>[];
export const reorderColumn = <TData extends Record<string, any> = {}>(
draggedColumn: MRT_Column<TData>,
targetColumn: MRT_Column<TData>,
columnOrder: MRT_ColumnOrderState,
): MRT_ColumnOrderState => {
if (draggedColumn.getCanPin()) {
draggedColumn.pin(targetColumn.getIsPinned());
}
columnOrder.splice(
columnOrder.indexOf(targetColumn.id),
0,
columnOrder.splice(columnOrder.indexOf(draggedColumn.id), 1)[0],
);
return [...columnOrder];
};
export const showExpandColumn = <TData extends Record<string, any> = {}>(
props: MaterialReactTableProps<TData>,
grouping?: MRT_GroupingState,
) =>
!!(
props.enableExpanding ||
(props.enableGrouping && (grouping === undefined || grouping?.length)) ||
props.renderDetailPanel
);
export const getLeadingDisplayColumnIds = <
TData extends Record<string, any> = {},
>(
props: MaterialReactTableProps<TData>,
) =>
[
(props.enableRowDragging || props.enableRowOrdering) && 'mrt-row-drag',
props.positionActionsColumn === 'first' &&
(props.enableRowActions ||
(props.enableEditing &&
['row', 'modal'].includes(props.editingMode ?? ''))) &&
'mrt-row-actions',
props.positionExpandColumn === 'first' &&
showExpandColumn(props) &&
'mrt-row-expand',
props.enableRowSelection && 'mrt-row-select',
props.enableRowNumbers && 'mrt-row-numbers',
].filter(Boolean) as MRT_DisplayColumnIds[];
export const getTrailingDisplayColumnIds = <
TData extends Record<string, any> = {},
>(
props: MaterialReactTableProps<TData>,
) => [
props.positionActionsColumn === 'last' &&
(props.enableRowActions ||
(props.enableEditing &&
['row', 'modal'].includes(props.editingMode ?? ''))) &&
'mrt-row-actions',
props.positionExpandColumn === 'last' &&
showExpandColumn(props) &&
'mrt-row-expand',
];
export const getDefaultColumnOrderIds = <
TData extends Record<string, any> = {},
>(
props: MaterialReactTableProps<TData>,
) =>
[
...getLeadingDisplayColumnIds(props),
...getAllLeafColumnDefs(props.columns).map((columnDef) =>
getColumnId(columnDef),
),
...getTrailingDisplayColumnIds(props),
].filter(Boolean) as string[];
export const getDefaultColumnFilterFn = <
TData extends Record<string, any> = {},
>(
columnDef: MRT_ColumnDef<TData>,
): MRT_FilterOption => {
if (columnDef.filterVariant === 'multi-select') return 'arrIncludesSome';
if (columnDef.filterVariant === 'range') return 'betweenInclusive';
if (
columnDef.filterVariant === 'select' ||
columnDef.filterVariant === 'checkbox'
)
return 'equals';
return 'fuzzy';
};
export const getIsFirstColumn = (
column: MRT_Column,
table: MRT_TableInstance,
) => {
return table.getVisibleLeafColumns()[0].id === column.id;
};
export const getIsLastColumn = (
column: MRT_Column,
table: MRT_TableInstance,
) => {
const columns = table.getVisibleLeafColumns();
return columns[columns.length - 1].id === column.id;
};
export const getIsLastLeftPinnedColumn = (
table: MRT_TableInstance,
column: MRT_Column,
) => {
return (
column.getIsPinned() === 'left' &&
table.getLeftLeafHeaders().length - 1 === column.getPinnedIndex()
);
};
export const getIsFirstRightPinnedColumn = (column: MRT_Column) => {
return column.getIsPinned() === 'right' && column.getPinnedIndex() === 0;
};
export const getTotalRight = (table: MRT_TableInstance, column: MRT_Column) => {
return (
(table.getRightLeafHeaders().length - 1 - column.getPinnedIndex()) * 200
);
};
export const getCommonCellStyles = ({
column,
header,
table,
tableCellProps,
theme,
}: {
column: MRT_Column;
header?: MRT_Header;
table: MRT_TableInstance;
tableCellProps: TableCellProps;
theme: Theme;
}) => ({
backgroundColor:
column.getIsPinned() && column.columnDef.columnDefType !== 'group'
? alpha(lighten(theme.palette.background.default, 0.04), 0.97)
: 'inherit',
backgroundImage: 'inherit',
boxShadow: getIsLastLeftPinnedColumn(table, column)
? `-4px 0 8px -6px ${alpha(theme.palette.common.black, 0.2)} inset`
: getIsFirstRightPinnedColumn(column)
? `4px 0 8px -6px ${alpha(theme.palette.common.black, 0.2)} inset`
: undefined,
display: table.options.layoutMode === 'grid' ? 'flex' : 'table-cell',
flex:
table.options.layoutMode === 'grid'
? `var(--${header ? 'header' : 'col'}-${parseCSSVarId(
header?.id ?? column.id,
)}-size) 0 auto`
: undefined,
left:
column.getIsPinned() === 'left'
? `${column.getStart('left')}px`
: undefined,
ml:
table.options.enableColumnVirtualization &&
column.getIsPinned() === 'left' &&
column.getPinnedIndex() === 0
? `-${
column.getSize() * (table.getState().columnPinning.left?.length ?? 1)
}px`
: undefined,
mr:
table.options.enableColumnVirtualization &&
column.getIsPinned() === 'right' &&
column.getPinnedIndex() === table.getVisibleLeafColumns().length - 1
? `-${
column.getSize() *
(table.getState().columnPinning.right?.length ?? 1) *
1.2
}px`
: undefined,
opacity:
table.getState().draggingColumn?.id === column.id ||
table.getState().hoveredColumn?.id === column.id
? 0.5
: 1,
position:
column.getIsPinned() && column.columnDef.columnDefType !== 'group'
? 'sticky'
: undefined,
right:
column.getIsPinned() === 'right'
? `${getTotalRight(table, column)}px`
: undefined,
transition: table.options.enableColumnVirtualization
? 'none'
: `padding 150ms ease-in-out`,
...(tableCellProps?.sx instanceof Function
? tableCellProps.sx(theme)
: (tableCellProps?.sx as any)),
minWidth: `max(calc(var(--${header ? 'header' : 'col'}-${parseCSSVarId(
header?.id ?? column.id,
)}-size) * 1px), ${column.columnDef.minSize ?? 30}px)`,
width: `calc(var(--${header ? 'header' : 'col'}-${parseCSSVarId(
header?.id ?? column.id,
)}-size) * 1px)`,
});
export const MRT_DefaultColumn = {
filterVariant: 'text',
minSize: 40,
maxSize: 1000,
size: 180,
} as const;
export const MRT_DefaultDisplayColumn = {
columnDefType: 'display',
enableClickToCopy: false,
enableColumnActions: false,
enableColumnDragging: false,
enableColumnFilter: false,
enableColumnOrdering: false,
enableEditing: false,
enableGlobalFilter: false,
enableGrouping: false,
enableHiding: false,
enableResizing: false,
enableSorting: false,
} as const;
export const parseCSSVarId = (id: string) => id.replace(/[^a-zA-Z0-9]/g, '_');
const padZero = (str: string, len?: number) => {
len = len || 2;
const zeros = new Array(len).join('0');
return (zeros + str).slice(-len);
};
export const invertColor = (hex: string, bw?: boolean, alp?: boolean) => {
let alpha = 'FF';
if (hex.indexOf('#') === 0) {
hex = hex.slice(1);
}
// convert 3-digit hex to 6-digits.
if (hex.length === 3) {
hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
}
if (hex.length === 8) {
alpha = hex.slice(6, 8);
hex = hex.slice(0, 6);
} else if (hex.length !== 6) {
throw new Error('Invalid HEX color.');
}
let r: number | string = parseInt(hex.slice(0, 2), 16);
let g: number | string = parseInt(hex.slice(2, 4), 16);
let b: number | string = parseInt(hex.slice(4, 6), 16);
if (bw) {
// https://stackoverflow.com/a/3943023/112731
return r * 0.299 + g * 0.587 + b * 0.114 > 186
? `#000000${alp ? alpha : ''}`
: `#FFFFFF${alp ? alpha : ''}`;
}
// invert color components
r = (255 - r).toString(16);
g = (255 - g).toString(16);
b = (255 - b).toString(16);
// pad each with zeros and return
return `#${padZero(r)}${padZero(g)}${padZero(b)}${alp ? alpha : ''}`;
};