UNPKG

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
'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