mui-datatables-updated
Version:
MUI Datatable library inspired by the gregnb/mui-datatables project, featuring an up-to-date implementation with Typescript Support.
364 lines (351 loc) • 25.5 kB
JavaScript
'use strict';
var jsxRuntime = require('react/jsx-runtime');
var Checkbox = require('@mui/material/Checkbox');
var Paper = require('@mui/material/Paper');
var Table = require('@mui/material/Table');
var TableBody = require('@mui/material/TableBody');
var TableCell = require('@mui/material/TableCell');
var TableContainer = require('@mui/material/TableContainer');
var TablePagination = require('@mui/material/TablePagination');
var TableRow = require('@mui/material/TableRow');
var React = require('react');
var reactToPrint = require('react-to-print');
var Box = require('@mui/material/Box');
var TableHead = require('@mui/material/TableHead');
var TableSortLabel = require('@mui/material/TableSortLabel');
var utils = require('@mui/utils');
var iconsMaterial = require('@mui/icons-material');
var FilterListIcon = require('@mui/icons-material/FilterList');
var SearchIcon = require('@mui/icons-material/Search');
var material = require('@mui/material');
var styles = require('@mui/material/styles');
/******************************************************************************
Copyright (c) Microsoft Corporation.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
***************************************************************************** */
/* global Reflect, Promise, SuppressedError, Symbol, Iterator */
function __rest(s, e) {
var t = {};
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
t[p] = s[p];
if (s != null && typeof Object.getOwnPropertySymbols === "function")
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
t[p[i]] = s[p[i]];
}
return t;
}
typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
var e = new Error(message);
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
};
function EnhancedTableHead({ onSelectAllClick, order, orderBy, numSelected, rowCount, onRequestSort, columns, deactivateSelectAll, }) {
const createSortHandler = (property) => (event) => {
onRequestSort(event, property);
};
return (jsxRuntime.jsx(TableHead, { children: jsxRuntime.jsxs(TableRow, { children: [!deactivateSelectAll &&
jsxRuntime.jsx(TableCell, { padding: "checkbox", children: jsxRuntime.jsx(Checkbox, { color: "primary", indeterminate: numSelected > 0 && numSelected < rowCount, checked: rowCount > 0 && numSelected === rowCount, onChange: onSelectAllClick, inputProps: {
'aria-label': 'select all items',
} }) }), columns.map((column) => {
var _a;
return (jsxRuntime.jsx(TableCell, { padding: 'normal', sortDirection: orderBy === column.name ? order : false, children: ((_a = column.options) === null || _a === undefined ? undefined : _a.sort) !== false ? (jsxRuntime.jsxs(TableSortLabel, { active: orderBy === column.name, direction: orderBy === column.name ? order : 'asc', onClick: createSortHandler(column.name), sx: { fontWeight: 'bold' }, children: [column.label, orderBy === column.name ? (jsxRuntime.jsx(Box, { component: "span", sx: utils.visuallyHidden, children: order === 'desc' ? 'sorted descending' : 'sorted ascending' })) : null] })) : (jsxRuntime.jsx("span", { style: { fontWeight: 700 }, children: column.label })) }, String(column.name)));
})] }) }));
}
function EnhancedTableToolbar(props) {
var _a, _b, _c, _d, _e, _f, _g, _h;
const { title, numSelected, selected, onFilterChange, onSearch, printFn, columns, CustomToolbar, CustomSelectedToolbar, data, options } = props;
const [anchorEl, setAnchorEl] = React.useState(null);
const [filters, setFilters] = React.useState([]);
const [filterConfig, setFilterConfig] = React.useState([]);
// rest value to force re-render of filters
const [resetCounter, setResetCounter] = React.useState(0);
const [openSearch, setOpenSearch] = React.useState(false);
function downloadCSV(data, filename = "data.csv") {
// Base case
if (data.length === 0) {
console.warn("No data to export.");
return;
}
// Create CSV content
const headers = Object.keys(data[0]);
const csvRows = data.map(obj => headers.map(field => { var _a; return JSON.stringify((_a = obj[field]) !== null && _a !== undefined ? _a : ""); }).join(","));
const csvContent = [headers.join(","), ...csvRows].join("\n");
// Create Blob and download
const blob = new Blob([csvContent], { type: "text/csv" });
const link = document.createElement("a");
link.href = URL.createObjectURL(blob);
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(link.href);
}
React.useEffect(() => {
if (data && data.length > 0) {
const inferredConfig = columns.map((column) => {
const key = column.name;
const values = data.map((row) => row[key]);
const isNumber = values.every((val) => typeof val === "number");
const inferredType = isNumber ? "number" : typeof values[0] === "boolean" ? "boolean" : "string";
return {
key,
type: inferredType,
min: isNumber ? Math.min(...values) : undefined,
max: isNumber ? Math.max(...values) : undefined,
};
});
setFilterConfig(inferredConfig);
}
}, [data, columns]);
React.useEffect(() => {
const newFilterFunc = (row) => {
if (filters.length === 0)
return true;
return filters.every((filter) => {
const rowValue = row[filter.key];
if (filter.type === "number") {
const [min, max] = filter.value;
return rowValue >= min && rowValue <= max;
}
if (filter.type === "string") {
return rowValue.toString().toLowerCase().includes(filter.value.toLowerCase());
}
if (filter.type === "boolean") {
return rowValue === filter.value;
}
return true;
});
};
onFilterChange(newFilterFunc);
}, [filters, onFilterChange]);
const handleOpen = (event) => {
setAnchorEl(event.currentTarget);
};
const handleClose = () => {
setAnchorEl(null);
};
const handleSearchChange = () => {
setOpenSearch((prev) => !prev);
};
const open = Boolean(anchorEl);
const getFilter = (key) => filters.find((filter) => filter.key === key);
const updateFilter = (updatedFilter) => {
setFilters((prevFilters) => {
const existingIndex = prevFilters.findIndex((filter) => filter.key === updatedFilter.key);
if (existingIndex !== -1) {
// Update existing filter
const newFilters = [...prevFilters];
newFilters[existingIndex] = updatedFilter;
return newFilters;
}
// Add new filter
return [...prevFilters, updatedFilter];
});
};
const removeFilter = (key) => {
setFilters((prevFilters) => prevFilters.filter((filter) => filter.key !== key));
};
const handleFilterChange = (key, value) => {
if (value === undefined || value === null || value === "") {
removeFilter(key);
}
else {
const config = filterConfig.find((config) => config.key === key);
if (!config)
return;
updateFilter({
key,
type: config.type,
value,
});
}
};
const resetFilters = () => {
setFilters([]);
setResetCounter((prev) => prev + 1);
};
return (jsxRuntime.jsxs(material.Toolbar, { sx: [
{ px: { sm: 2 }, borderBottom: 1, borderColor: "divider" },
numSelected > 0 && { bgcolor: (theme) => styles.alpha(theme.palette.primary.main, theme.palette.action.activatedOpacity) },
], children: [numSelected > 0 ? (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(material.Typography, { sx: { flex: "1 1 100%" }, color: "inherit", variant: "subtitle1", component: "div", children: ((_a = options === null || options === undefined ? undefined : options.translations) === null || _a === undefined ? undefined : _a.selectedTextRenderer)
? options.translations.selectedTextRenderer(numSelected)
: `${numSelected} selected` }), CustomSelectedToolbar && (jsxRuntime.jsx(CustomSelectedToolbar, { data: data, selected: selected }))] })) : (jsxRuntime.jsxs(material.Stack, { direction: "row", justifyContent: "space-between", width: "100%", alignItems: "center", children: [openSearch ? (jsxRuntime.jsxs(material.Stack, { direction: "row", alignItems: "center", children: [jsxRuntime.jsx(SearchIcon, {}), jsxRuntime.jsx(material.TextField, { placeholder: ((_b = options === null || options === undefined ? undefined : options.translations) === null || _b === undefined ? undefined : _b.searchPlaceholder) || "Search...", onChange: (e) => onSearch(e.target.value), variant: "standard", autoFocus: true, fullWidth: true, sx: { marginLeft: 1 } }), jsxRuntime.jsx(material.IconButton, { onClick: handleSearchChange, sx: { '&:hover': { color: 'error.main' } }, children: jsxRuntime.jsx(iconsMaterial.Close, {}) })] })) : (jsxRuntime.jsx(material.Typography, { sx: { flex: "1 1 100%", alignContent: "center", paddingLeft: 1 }, variant: "h6", id: "tableTitle", component: "div", children: title })), jsxRuntime.jsxs(material.Stack, { direction: "row", spacing: 0.5, children: [jsxRuntime.jsx(material.Tooltip, { title: ((_c = options === null || options === undefined ? undefined : options.translations) === null || _c === undefined ? undefined : _c.searchTooltip) || "Search", children: jsxRuntime.jsx(material.IconButton, { onClick: handleSearchChange, children: jsxRuntime.jsx(SearchIcon, {}) }) }), jsxRuntime.jsx(material.Tooltip, { title: ((_d = options === null || options === undefined ? undefined : options.translations) === null || _d === undefined ? undefined : _d.downloadTooltip) || "Download CSV", children: jsxRuntime.jsx(material.IconButton, { onClick: () => downloadCSV(data, "data.csv"), children: jsxRuntime.jsx(iconsMaterial.CloudDownload, {}) }) }), jsxRuntime.jsx(material.Tooltip, { title: ((_e = options === null || options === undefined ? undefined : options.translations) === null || _e === undefined ? undefined : _e.printTooltip) || "Print", children: jsxRuntime.jsx(material.IconButton, { onClick: () => printFn(), children: jsxRuntime.jsx(iconsMaterial.Print, {}) }) }), jsxRuntime.jsx(material.Tooltip, { title: ((_f = options === null || options === undefined ? undefined : options.translations) === null || _f === undefined ? undefined : _f.filterTooltip) || "Filter list", children: jsxRuntime.jsx(material.IconButton, { onClick: handleOpen, children: jsxRuntime.jsx(FilterListIcon, {}) }) }), CustomToolbar && jsxRuntime.jsx(CustomToolbar, {})] })] })), jsxRuntime.jsxs(material.Popover, { open: open, anchorEl: anchorEl, onClose: handleClose, anchorOrigin: {
vertical: "bottom",
horizontal: "right",
}, transformOrigin: {
vertical: "top",
horizontal: "right",
}, slotProps: {
paper: { sx: { width: "20%", padding: 2, boxShadow: 2 } }
}, children: [jsxRuntime.jsxs(material.Stack, { direction: "row", justifyContent: "space-between", children: [jsxRuntime.jsx(material.Typography, { variant: "h6", sx: { marginBottom: 2 }, children: ((_g = options === null || options === undefined ? undefined : options.translations) === null || _g === undefined ? undefined : _g.filtersTitle) || "Filters" }), jsxRuntime.jsx(material.Button, { variant: "contained", size: "small", sx: { height: "fit-content" }, onClick: resetFilters, children: ((_h = options === null || options === undefined ? undefined : options.translations) === null || _h === undefined ? undefined : _h.resetButtonText) || "Reset" })] }), jsxRuntime.jsx(material.Stack, { children: filterConfig.map(({ key, type, min, max }) => {
var _a, _b, _c, _d, _e, _f;
const currentFilter = getFilter(key);
return (jsxRuntime.jsxs(material.Box, { children: [jsxRuntime.jsx(material.Typography, { variant: "subtitle1", children: (_a = columns.find((cell) => cell.name === key)) === null || _a === undefined ? undefined : _a.label }), type === "number" && min !== undefined && max !== undefined && (jsxRuntime.jsx(material.Slider, { value: [
(_c = (_b = currentFilter === null || currentFilter === undefined ? undefined : currentFilter.value) === null || _b === undefined ? undefined : _b[0]) !== null && _c !== undefined ? _c : min,
(_e = (_d = currentFilter === null || currentFilter === undefined ? undefined : currentFilter.value) === null || _d === undefined ? undefined : _d[1]) !== null && _e !== undefined ? _e : max,
], onChange: (_, newValue) => {
const [newMin, newMax] = newValue;
handleFilterChange(key, [newMin, newMax]);
}, valueLabelDisplay: "auto", min: min || 0, max: max || 100, step: 1 })), type === "string" && (jsxRuntime.jsx(material.TextField, { placeholder: ((_f = options === null || options === undefined ? undefined : options.translations) === null || _f === undefined ? undefined : _f.searchPlaceholder) || "Search...", size: "small", value: (currentFilter === null || currentFilter === undefined ? undefined : currentFilter.value) || "", onChange: (e) => handleFilterChange(key, e.target.value), sx: {
marginBottom: 1,
paddingY: 0.5,
width: "100%",
} })), type === "boolean" && (jsxRuntime.jsxs(material.Box, { display: "flex", alignItems: "center", children: [jsxRuntime.jsx(material.Checkbox, { checked: (currentFilter === null || currentFilter === undefined ? undefined : currentFilter.value) === true, onChange: (e) => handleFilterChange(key, e.target.checked) }), currentFilter && (jsxRuntime.jsx(iconsMaterial.Close, { color: "error", sx: { cursor: "pointer", fontSize: 15 }, onClick: () => handleFilterChange(key, undefined) }))] }))] }, key));
}) }, resetCounter)] })] }));
}
function getComparator(order, orderBy) {
return order === 'desc'
? (a, b) => descendingComparator(a[orderBy], b[orderBy])
: (a, b) => -descendingComparator(a[orderBy], b[orderBy]);
}
function descendingComparator(a, b) {
if (b < a)
return -1;
if (b > a)
return 1;
return 0;
}
const MUITable = (_a) => {
var _b, _c;
var { title, data, deactivateSelect, defaultOrderBy, defaultOrder, excludedColumns, columns: passedColumns, CustomToolbar, CustomSelectedToolbar, options } = _a, rest = __rest(_a, ["title", "data", "deactivateSelect", "defaultOrderBy", "defaultOrder", "excludedColumns", "columns", "CustomToolbar", "CustomSelectedToolbar", "options"]);
const tableRef = React.useRef(null);
const reactToPrintFn = reactToPrint.useReactToPrint({ contentRef: tableRef });
const getDefaultOrderByKey = React.useCallback(() => {
if (data.length === 0)
return "id";
const keys = data.length > 0 ? Object.keys(data[0]) : [];
if (defaultOrderBy) {
if (keys.includes(defaultOrderBy)) {
return defaultOrderBy;
}
else {
console.warn(`{defaultOrderBy}: "${defaultOrderBy}" not found among the object keys. Falling back to automatic key detection.`);
}
}
return (keys.includes("id") ? "id" : keys[0]);
}, [data, defaultOrderBy]);
const [state, setState] = React.useState(() => {
const orderByKey = getDefaultOrderByKey();
return {
order: defaultOrder || 'asc',
orderBy: orderByKey,
selected: [],
page: 0,
rowsPerPage: 5,
searchQuery: "",
filterFunc: () => true,
currentData: data,
visibleRows: data,
emptyRows: 0,
};
});
const generateColumns = React.useCallback(() => {
if (data.length === 0)
return [];
return Object.keys(data[0])
.filter((key) => !(excludedColumns || []).includes(key))
.map((key) => ({
name: key,
label: key.charAt(0).toUpperCase() + key.slice(1),
}));
}, [data, excludedColumns]);
const columns = passedColumns || generateColumns();
const handleSearch = (query) => {
setState((prevState) => (Object.assign(Object.assign({}, prevState), { searchQuery: query.toLowerCase() })));
};
React.useEffect(() => {
const orderByKey = getDefaultOrderByKey();
setState((prevState) => (Object.assign(Object.assign({}, prevState), { orderBy: orderByKey })));
}, [data, getDefaultOrderByKey]);
// Data sorting, filtering, and pagination
React.useEffect(() => {
// Apply filters, search query, and sort data
const sortedData = [...data]
.filter(state.filterFunc)
.filter((row) => Object.values(row).some((value) => typeof value === "string" &&
value.toLowerCase().includes(state.searchQuery)))
.sort(getComparator(state.order, state.orderBy));
// Paginate the processed data
const startIndex = state.page * state.rowsPerPage;
const paginatedData = sortedData.slice(startIndex, startIndex + state.rowsPerPage);
const calculatedEmptyRows = Math.max(0, (1 + state.page) * state.rowsPerPage - sortedData.length);
setState((prevState) => (Object.assign(Object.assign({}, prevState), { visibleRows: paginatedData, emptyRows: calculatedEmptyRows, selected: prevState.selected.filter((selectedRow) => data.includes(selectedRow)) })));
}, [
state.filterFunc,
state.order,
state.orderBy,
state.page,
state.rowsPerPage,
state.searchQuery,
data,
]);
// Remove selected rows deleted from the data to prevent stale selected state
React.useEffect(() => {
setState((prevState) => (Object.assign(Object.assign({}, prevState), { selected: prevState.selected.filter((selectedRow) => data.some((row) => row === selectedRow)) })));
}, [data]);
const handleRequestSort = (_event, property) => {
const isAsc = state.orderBy === property && state.order === 'asc';
setState((prevState) => (Object.assign(Object.assign({}, prevState), { order: isAsc ? 'desc' : 'asc', orderBy: property })));
};
const handleSelectAllClick = (event) => {
if (event.target.checked) {
setState((prevState) => (Object.assign(Object.assign({}, prevState), { selected: [...state.currentData] })));
}
else {
setState((prevState) => (Object.assign(Object.assign({}, prevState), { selected: [] })));
}
};
const handleClick = (_event, row) => {
const selectedIndex = state.selected.findIndex((selectedRow) => selectedRow === row);
let newSelected = [];
if (selectedIndex === -1) {
newSelected = [...state.selected, row];
}
else if (selectedIndex === 0) {
newSelected = state.selected.slice(1);
}
else if (selectedIndex === state.selected.length - 1) {
newSelected = state.selected.slice(0, -1);
}
else if (selectedIndex > 0) {
newSelected = [
...state.selected.slice(0, selectedIndex),
...state.selected.slice(selectedIndex + 1),
];
}
setState((prevState) => (Object.assign(Object.assign({}, prevState), { selected: newSelected })));
};
const handleChangePage = (_event, newPage) => {
setState((prevState) => (Object.assign(Object.assign({}, prevState), { page: newPage })));
};
const handleChangeRowsPerPage = (event) => {
setState((prevState) => (Object.assign(Object.assign({}, prevState), { rowsPerPage: parseInt(event.target.value, 10), page: 0 })));
};
const handleFilterChange = React.useCallback((filterFunc) => {
setState((prevState) => (Object.assign(Object.assign({}, prevState), { filterFunc })));
}, []);
return (jsxRuntime.jsx("div", Object.assign({}, rest, { ref: tableRef, children: jsxRuntime.jsxs(Paper, { sx: { width: '100%', mb: 2 }, children: [jsxRuntime.jsx(EnhancedTableToolbar, { title: title, numSelected: state.selected.length, selected: state.selected, onFilterChange: handleFilterChange, onSearch: handleSearch, printFn: reactToPrintFn, columns: columns, CustomToolbar: CustomToolbar, CustomSelectedToolbar: CustomSelectedToolbar, data: data, options: options }), jsxRuntime.jsx(TableContainer, { children: jsxRuntime.jsxs(Table, { sx: { minWidth: 750 }, "aria-labelledby": "tableTitle", size: "small", children: [jsxRuntime.jsx(EnhancedTableHead, { columns: columns, numSelected: state.selected.length, order: state.order, orderBy: state.orderBy, onSelectAllClick: deactivateSelect ? undefined : handleSelectAllClick, onRequestSort: handleRequestSort, rowCount: state.currentData.length, deactivateSelectAll: deactivateSelect }), jsxRuntime.jsxs(TableBody, { children: [state.visibleRows.map((row, index) => {
const isItemSelected = state.selected.some((selectedRow) => selectedRow === row);
const labelId = `enhanced-table-checkbox-${index}`;
return (jsxRuntime.jsxs(TableRow, { hover: true, onClick: deactivateSelect ? undefined : (event) => handleClick(event, row), role: "checkbox", "aria-checked": isItemSelected, tabIndex: -1, selected: isItemSelected, sx: { cursor: 'pointer' }, children: [!deactivateSelect && (jsxRuntime.jsx(TableCell, { padding: "checkbox", children: jsxRuntime.jsx(Checkbox, { color: "primary", checked: isItemSelected, inputProps: {
'aria-labelledby': labelId,
} }) })), columns.map((column, cellIndex) => {
var _a;
return (jsxRuntime.jsx(TableCell, { component: cellIndex === 0 ? "th" : undefined, id: cellIndex === 0 ? labelId : undefined, scope: cellIndex === 0 ? "row" : undefined, padding: "normal", children: ((_a = column.options) === null || _a === undefined ? undefined : _a.customBodyRender)
? column.options.customBodyRender(row[column.name])
: String(row[column.name]) }, String(column.name)));
})] }, index));
}), state.emptyRows > 0 && (jsxRuntime.jsx(TableRow, { style: {
height: 33 * state.emptyRows,
}, children: jsxRuntime.jsx(TableCell, { colSpan: columns.length + 1 }) }))] })] }) }), jsxRuntime.jsx(TablePagination, { rowsPerPageOptions: [5, 10, 25], component: "div", count: state.currentData.length, rowsPerPage: state.rowsPerPage, page: state.page, onPageChange: handleChangePage, onRowsPerPageChange: handleChangeRowsPerPage, labelRowsPerPage: ((_b = options === null || options === undefined ? undefined : options.translations) === null || _b === undefined ? undefined : _b.rowsPerPageText) || "Rows per page", labelDisplayedRows: ((_c = options === null || options === undefined ? undefined : options.translations) === null || _c === undefined ? undefined : _c.labelDisplayedRows) || (({ from, to, count }) => `${from}-${to} of ${count}`) })] }) })));
};
module.exports = MUITable;
//# sourceMappingURL=index.js.map