@etsoo/materialui
Version:
TypeScript Material-UI Implementation
331 lines (330 loc) • 14.3 kB
JavaScript
import { jsx as _jsx } from "react/jsx-runtime";
import { css } from "@emotion/css";
import { GridAlignGet, ScrollerGrid, useCombinedRefs } from "@etsoo/react";
import { Utils } from "@etsoo/shared";
import React from "react";
import { DataGridRenderers } from "./DataGridRenderers";
import { useTheme } from "@mui/material/styles";
import Box from "@mui/material/Box";
import TableSortLabel from "@mui/material/TableSortLabel";
import Checkbox from "@mui/material/Checkbox";
import Paper from "@mui/material/Paper";
// Borders
const boldBorder = "2px solid rgba(224, 224, 224, 1)";
const thinBorder = "1px solid rgba(224, 224, 224, 1)";
// Scroll bar size
const scrollbarSize = 16;
// Minimum width
const minWidth = 120;
const createGridStyle = (alternatingColors, selectedColor, hoverColor) => {
return css({
".DataGridEx-Selected": {
backgroundColor: selectedColor
},
".DataGridEx-Hover:not(.DataGridEx-Selected)": {
backgroundColor: hoverColor
},
"& .DataGridEx-Cell0:not(.DataGridEx-Hover):not(.DataGridEx-Selected)": {
backgroundColor: alternatingColors[0]
},
"& .DataGridEx-Cell1:not(.DataGridEx-Hover):not(.DataGridEx-Selected)": {
backgroundColor: alternatingColors[1]
},
"& .DataGridEx-Cell-Border": {
borderBottom: thinBorder
}
});
};
const rowItems = (div, callback) => {
const row = div.dataset["row"];
if (div.parentElement == null || row == null)
return;
doRowItems(div.parentElement, parseFloat(row), callback);
};
const doRowItems = (parent, rowIndex, callback) => {
if (parent == null || rowIndex == null)
return;
parent
?.querySelectorAll(`div[data-row="${rowIndex}"]`)
.forEach((rowItem) => {
callback(rowItem);
});
};
/**
* Extended datagrid columns calculation
* @param columns
* @returns Total width and unset items
*/
export function DataGridExCalColumns(columns) {
return columns.reduce((previousValue, currentItem) => {
previousValue.total +=
currentItem.width ?? currentItem.minWidth ?? minWidth;
if (currentItem.width == null)
previousValue.unset++;
return previousValue;
}, {
total: 0,
unset: 0
});
}
/**
* Extended DataGrid with VariableSizeGrid
* @param props Props
* @returns Component
*/
export function DataGridEx(props) {
// Theme
const theme = useTheme();
const defaultHeaderRenderer = (states) => {
const { orderBy } = states.queryPaging;
return (_jsx(Box, { className: "DataGridEx-Header", display: "flex", alignItems: "center", borderBottom: boldBorder, fontWeight: 500, minWidth: widthCalculator.total, height: headerHeight, children: columns.map((column, index) => {
// Destruct
const { align, field, header, headerCellRenderer, sortable, sortAsc = true, type } = column;
// Header text
const headerText = header ?? field;
// Cell props
const cellProps = {};
// Sortable
let sortLabel;
if (headerCellRenderer) {
sortLabel = headerCellRenderer({
cellProps,
column,
columnIndex: checkable ? index - 1 : index, // Ignore the checkbox case,
states
});
}
else if (sortable && field != null) {
const active = orderBy?.some((o) => o.field.toUpperCase() === field.toUpperCase());
sortLabel = (_jsx(TableSortLabel, { active: active, direction: sortAsc ? "asc" : "desc", onClick: (_event) => {
if (active)
column.sortAsc = !sortAsc;
handleSort(field, column.sortAsc);
}, children: headerText }));
}
else {
sortLabel = headerText;
}
return (_jsx(Box, { textAlign: GridAlignGet(align, type), width: columnWidth(index), children: _jsx(Box, { className: "DataGridEx-Cell", onMouseEnter: handleMouseEnter, ...cellProps, children: sortLabel }) }, field ?? index.toString()));
}) }));
};
function defaultFooterRenderer(rows, states) {
return (_jsx(Box, { className: "DataGridEx-Footer", display: "flex", alignItems: "center", borderTop: thinBorder, marginTop: "1px", minWidth: widthCalculator.total, height: bottomHeight - 1, children: columns.map((column, index) => {
// Destruct
const { align, field, type } = column;
// Cell props
const cellProps = {};
// Cell
const cell = footerItemRenderer
? footerItemRenderer(rows, {
column,
index: checkable ? index - 1 : index, // Ignore the checkbox case
states,
cellProps,
checkable
})
: undefined;
return (_jsx(Box, { textAlign: GridAlignGet(align, type), width: columnWidth(index), children: _jsx(Box, { className: "DataGridEx-Cell", onMouseEnter: handleMouseEnter, ...cellProps, children: cell }) }, "bottom-" + (field ?? index.toString())));
}) }));
}
// Destruct
const { alternatingColors = [theme.palette.grey[100], undefined], borderRowsCount, bottomHeight = 53, checkable = false, className, columns, defaultOrderBy, height, headerHeight = 56, headerRenderer = defaultHeaderRenderer, footerRenderer = defaultFooterRenderer, footerItemRenderer = DataGridRenderers.defaultFooterItemRenderer, hideFooter = false, hoverColor = "#f6f9fb", idField = "id", mRef = React.createRef(), onClick, onDoubleClick, selectable = true, selectedColor = "#edf4fb", width, ...rest } = props;
if (checkable) {
const cbColumn = {
field: "selected", // Avoid validation from data model
header: "",
sortable: false,
width: 50,
cellRenderer: ({ cellProps, data, selected }) => {
cellProps.sx = {
padding: "4px!important"
};
return (_jsx(Checkbox, { color: "primary", checked: selected, onChange: (_event, checked) => {
refs.current.ref?.selectItem(data, checked);
} }));
},
headerCellRenderer: ({ cellProps, states }) => {
// 2 = border height
const hpad = (headerHeight - 42) / 2;
cellProps.sx = {
padding: `${hpad}px 4px ${hpad - 1}px 4px!important`
};
return (_jsx(Checkbox, { color: "primary", indeterminate: states.selectedItems.length > 0 &&
states.selectedItems.length < states.loadedItems, checked: states.selectedItems.length > 0, onChange: (_event, checked) => refs.current.ref?.selectAll(checked) }));
}
};
// Update to the latest version
if (columns[0].field === "selected") {
columns[0] = cbColumn;
}
else {
columns.unshift(cbColumn);
}
}
const refs = React.useRef({});
const mRefLocal = useCombinedRefs(mRef, (ref) => {
if (ref == null)
return;
refs.current.ref = ref;
});
// New sort
const handleSort = (field, asc) => {
reset({
queryPaging: { orderBy: [{ field, desc: !(asc ?? true) }] }
});
};
// Reset
const reset = (add) => {
refs.current.ref?.reset(add);
};
// Show hover tooltip for trucated text
const handleMouseEnter = (event) => {
const div = event.currentTarget;
const { innerText, offsetWidth, scrollWidth } = div;
if (offsetWidth < scrollWidth) {
div.title = innerText;
}
else {
div.title = "";
}
};
// selectedRowIndex state
const selectedRowIndex = React.useRef(-1);
const handleMouseDown = (event) => {
const div = event.currentTarget;
const row = div.dataset["row"];
if (div.parentElement == null || row == null)
return;
const rowIndex = parseFloat(row);
// No change
if (isNaN(rowIndex) || rowIndex === selectedRowIndex.current)
return;
if (selectedRowIndex.current != -1) {
doRowItems(div.parentElement, selectedRowIndex.current, (preDiv) => {
preDiv.classList.remove("DataGridEx-Selected");
});
}
rowItems(div, (currentDiv) => {
currentDiv.classList.add("DataGridEx-Selected");
});
selectedRowIndex.current = rowIndex;
};
const handleMouseOver = (event) => {
rowItems(event.currentTarget, (div) => {
div.classList.add("DataGridEx-Hover");
});
};
const handleMouseOut = (event) => {
rowItems(event.currentTarget, (div) => {
div.classList.remove("DataGridEx-Hover");
});
};
/**
* Item renderer
*/
const itemRenderer = ({ columnIndex, rowIndex, style, data, selectedItems, setItems }) => {
// Column
const { align, cellRenderer = DataGridRenderers.defaultCellRenderer, cellBoxStyle, field, type, valueFormatter, renderProps } = columns[columnIndex];
// Props
const formatProps = {
data,
field,
rowIndex,
columnIndex
};
let rowClass = `DataGridEx-Cell${rowIndex % 2}`;
if (borderRowsCount != null &&
borderRowsCount > 0 &&
(rowIndex + 1) % borderRowsCount === 0) {
rowClass += ` DataGridEx-Cell-Border`;
}
// Selected
const selected = data != null &&
(selectedRowIndex.current === rowIndex ||
selectedItems.some((selectedItem) => selectedItem != null && selectedItem[idField] === data[idField]));
if (selected) {
rowClass += ` DataGridEx-Selected`;
}
// Box style
const boxStyle = data == null || cellBoxStyle == null
? undefined
: typeof cellBoxStyle === "function"
? cellBoxStyle(data)
: cellBoxStyle;
const cellProps = {
className: "DataGridEx-Cell",
textAlign: GridAlignGet(align, type),
sx: { ...boxStyle }
};
const child = cellRenderer({
data,
field,
formattedValue: valueFormatter ? valueFormatter(formatProps) : undefined,
selected,
type,
rowIndex,
columnIndex,
cellProps,
renderProps: typeof renderProps === "function" ? renderProps(data) : renderProps,
setItems
});
return (_jsx("div", { className: rowClass, style: style, "data-row": rowIndex, "data-column": columnIndex, onMouseDown: selectable && !checkable ? handleMouseDown : undefined, onMouseOver: selectable ? handleMouseOver : undefined, onMouseOut: selectable ? handleMouseOut : undefined, onClick: (event) => onClick && data != null && onClick(event, data), onDoubleClick: (event) => onDoubleClick && data != null && onDoubleClick(event, data), children: _jsx(Box, { ...cellProps, onMouseEnter: handleMouseEnter, children: child }) }));
};
// Column width calculator
const widthCalculator = React.useMemo(() => DataGridExCalColumns(columns), [columns]);
// Column width
const columnWidth = React.useCallback((index) => {
// Ignore null case
if (width == null)
return 0;
// Column
const column = columns[index];
if (column.width != null)
return column.width;
// More space
const leftWidth = width - widthCalculator.total - (width < 800 ? 0 : scrollbarSize);
// Shared width
const sharedWidth = leftWidth > 0 ? leftWidth / widthCalculator.unset : 0;
return (column.minWidth || minWidth) + sharedWidth;
}, [columns, width]);
// Table
const table = React.useMemo(() => {
return (_jsx(ScrollerGrid, { className: Utils.mergeClasses("DataGridEx-Body", "DataGridEx-CustomBar", className, createGridStyle(alternatingColors, selectedColor, hoverColor)), columnCount: columns.length, columnWidth: columnWidth, defaultOrderBy: defaultOrderBy, height: height -
headerHeight -
(hideFooter ? 0 : bottomHeight + 1) -
scrollbarSize, headerRenderer: headerRenderer, idField: idField, itemRenderer: itemRenderer, footerRenderer: hideFooter ? undefined : footerRenderer, width: Math.max(width ?? 0, widthCalculator.total), mRef: mRefLocal, ...rest }));
}, [width]);
return (_jsx(Paper, { sx: {
fontSize: "0.875rem",
height,
"& .DataGridEx-Cell": {
padding: 2,
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis"
},
"& .DataGridEx-CustomBar": {
"@media (min-width: 800px)": {
"::-webkit-scrollbar": {
width: scrollbarSize,
height: scrollbarSize,
backgroundColor: "#f6f6f6"
},
"::-webkit-scrollbar-thumb": {
backgroundColor: "rgba(0,0,0,0.4)",
borderRadius: "2px"
},
"::-webkit-scrollbar-track-piece:start": {
background: "transparent"
},
"::-webkit-scrollbar-track-piece:end": {
background: "transparent"
}
}
}
}, children: _jsx("div", { className: "DataGridEx-CustomBar", style: {
width,
overflowX: "auto",
overflowY: "hidden"
}, children: table }) }));
}