268 lines
7.0 KiB
JavaScript
268 lines
7.0 KiB
JavaScript
import { useState, useMemo, useEffect } from 'react';
|
|
import {
|
|
Box,
|
|
Checkbox,
|
|
Typography,
|
|
TextField,
|
|
Popover,
|
|
InputAdornment,
|
|
} from '@mui/material';
|
|
import { ExpandMore, ChevronRight, ArrowDropDown } from '@mui/icons-material';
|
|
import { SimpleTreeView, TreeItem } from '@mui/x-tree-view';
|
|
import {
|
|
CheckBoxOutlineBlank,
|
|
CheckBox,
|
|
IndeterminateCheckBox,
|
|
} from '@mui/icons-material';
|
|
|
|
const CollapsibleTree = ({
|
|
columnTree,
|
|
placeholder = 'Скрыть столбцы',
|
|
placeholderSelect = 'Скрыто столбцов: ',
|
|
selectedIds = [],
|
|
onSelectNode,
|
|
}) => {
|
|
const [expandedItems, setExpandedItems] = useState([]);
|
|
const [anchorEl, setAnchorEl] = useState(null);
|
|
const [selectedNodes, setSelectedNodes] = useState([]);
|
|
|
|
useEffect(() => {
|
|
setSelectedNodes(selectedIds);
|
|
}, [selectedIds]);
|
|
|
|
const handleExpandedItemsChange = (event, itemIds) => {
|
|
setExpandedItems(itemIds);
|
|
};
|
|
|
|
const handleSelectAll = (isSelected) => {
|
|
for (const col of columnTree) {
|
|
onSelectNode?.(col, !isSelected);
|
|
}
|
|
};
|
|
|
|
const getChildrenIds = (node) => {
|
|
const getLeafColumn = (col) => {
|
|
const hasColumns = col.columns && Array.isArray(col.columns);
|
|
|
|
if (!hasColumns || col.columns.length === 0) {
|
|
return [col];
|
|
}
|
|
|
|
const allChildrenAreLeaves = col.columns.every(
|
|
child => !child.columns || !Array.isArray(child.columns) || child.columns.length === 0
|
|
);
|
|
|
|
if (allChildrenAreLeaves) {
|
|
return col.columns;
|
|
}
|
|
|
|
return col.columns.flatMap((childCol) => getLeafColumn(childCol));
|
|
};
|
|
|
|
const leafColumns = getLeafColumn(node);
|
|
return leafColumns.map((c) => c.id || c.accessorKey);
|
|
};
|
|
|
|
const isNodeChecked = (node) => {
|
|
const childrenIds = getChildrenIds(node);
|
|
const selectedChildren = childrenIds.filter((id) =>
|
|
selectedNodes.includes(id)
|
|
|
|
,
|
|
);
|
|
|
|
if (selectedChildren.length === 0) return false;
|
|
if (selectedChildren.length === childrenIds.length) return true;
|
|
return 'indeterminate';
|
|
};
|
|
|
|
const handleNodeCheck = (node, isSelected) => {
|
|
onSelectNode?.(node, !isSelected);
|
|
};
|
|
|
|
const getId = (column) => {
|
|
const getFullColumnPath = (column) => {
|
|
const ancestors = [];
|
|
let current = column;
|
|
|
|
while (current) {
|
|
ancestors.unshift(current.id || current.accessorKey);
|
|
current = current.parent;
|
|
}
|
|
|
|
return ancestors;
|
|
};
|
|
return getFullColumnPath(column).join('_');
|
|
};
|
|
|
|
const renderTree = (nodes) => {
|
|
if (!nodes || nodes.length === 0) return null;
|
|
|
|
return nodes.map((node) => {
|
|
const checkStatus = isNodeChecked(node);
|
|
return (
|
|
<TreeItem
|
|
key={node.id || node.accessorKey}
|
|
itemId={String(getId(node))}
|
|
label={
|
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
|
<Checkbox
|
|
onClick={(e) => e.stopPropagation()}
|
|
checked={checkStatus === true}
|
|
indeterminate={checkStatus === 'indeterminate'}
|
|
onChange={() =>
|
|
handleNodeCheck(
|
|
node,
|
|
checkStatus === 'indeterminate' ? true : !checkStatus,
|
|
)
|
|
}
|
|
icon={<CheckBoxOutlineBlank />}
|
|
checkedIcon={<CheckBox />}
|
|
indeterminateIcon={<IndeterminateCheckBox />}
|
|
/>
|
|
<Typography variant="body2">
|
|
{node.columnDef?.header || node.name || node.header || 'Без названия'}
|
|
</Typography>
|
|
</Box>
|
|
}
|
|
>
|
|
{node.columns && node.columns.length > 0 && renderTree(node.columns)}
|
|
</TreeItem>
|
|
);
|
|
});
|
|
};
|
|
|
|
const treeElements = useMemo(() => {
|
|
return columnTree ? renderTree(columnTree) : null;
|
|
}, [columnTree, selectedNodes]);
|
|
|
|
const handleOpen = (event) => {
|
|
setAnchorEl(event.currentTarget);
|
|
};
|
|
|
|
const handleClose = () => {
|
|
setAnchorEl(null);
|
|
};
|
|
|
|
const selectedCount = selectedNodes.length - 1; //1 колонка всегда скрыта
|
|
|
|
const isAllSelected = () => {
|
|
if (!columnTree) return false;
|
|
let isSelect = true;
|
|
for (const col of columnTree) {
|
|
const isChecked = isNodeChecked(col);
|
|
isSelect = isChecked === 'indeterminate' ? false : isChecked && isSelect;
|
|
}
|
|
return isSelect;
|
|
};
|
|
|
|
const open = Boolean(anchorEl);
|
|
const id = open ? 'tree-popover' : undefined;
|
|
|
|
const getDisplayValue = () => {
|
|
if (selectedCount === 0) {
|
|
return '';
|
|
}
|
|
return `${placeholderSelect} ${selectedCount}`;
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<Box
|
|
sx={{
|
|
position: 'relative',
|
|
width: '100%',
|
|
}}
|
|
>
|
|
<TextField
|
|
fullWidth
|
|
placeholder={placeholder}
|
|
value={getDisplayValue()}
|
|
onClick={handleOpen}
|
|
slotProps={{
|
|
input: {
|
|
readOnly: true,
|
|
endAdornment: (
|
|
<InputAdornment position="end">
|
|
<ArrowDropDown
|
|
sx={{
|
|
transform: open ? 'rotate(180deg)' : 'none',
|
|
transition: 'transform 0.2s',
|
|
}}
|
|
/>
|
|
</InputAdornment>
|
|
),
|
|
},
|
|
}}
|
|
sx={{
|
|
cursor: 'pointer',
|
|
height: '2.5rem',
|
|
padding: 0,
|
|
'& .MuiInputBase-root': {
|
|
cursor: 'pointer',
|
|
height: '2.5rem',
|
|
},
|
|
'& input': {
|
|
cursor: 'pointer',
|
|
},
|
|
}}
|
|
/>
|
|
</Box>
|
|
|
|
<Popover
|
|
id={id}
|
|
open={open}
|
|
anchorEl={anchorEl}
|
|
onClose={handleClose}
|
|
anchorOrigin={{
|
|
vertical: 'bottom',
|
|
horizontal: 'left',
|
|
}}
|
|
transformOrigin={{
|
|
vertical: 'top',
|
|
horizontal: 'left',
|
|
}}
|
|
PaperProps={{
|
|
sx: {
|
|
width: anchorEl ? anchorEl.clientWidth : 300,
|
|
maxHeight: 400,
|
|
mt: 1,
|
|
overflow: 'hidden',
|
|
},
|
|
}}
|
|
>
|
|
<Box sx={{ p: 1, borderBottom: '1px solid #e0e0e0' }}>
|
|
<Checkbox
|
|
checked={isAllSelected()}
|
|
indeterminate={selectedCount > 0 && !isAllSelected()}
|
|
onChange={(e) => handleSelectAll(e.target.checked)}
|
|
icon={<CheckBoxOutlineBlank />}
|
|
checkedIcon={<CheckBox />}
|
|
indeterminateIcon={<IndeterminateCheckBox />}
|
|
/>
|
|
<Typography variant="body2" component="span" sx={{ ml: 1 }}>
|
|
Выбрать все
|
|
</Typography>
|
|
</Box>
|
|
|
|
<SimpleTreeView
|
|
expandedItems={expandedItems}
|
|
onExpandedItemsChange={handleExpandedItemsChange}
|
|
slots={{
|
|
collapseIcon: ExpandMore,
|
|
expandIcon: ChevronRight,
|
|
}}
|
|
sx={{
|
|
maxHeight: 350,
|
|
overflow: 'auto',
|
|
p: 1,
|
|
}}
|
|
>
|
|
{treeElements}
|
|
</SimpleTreeView>
|
|
</Popover>
|
|
</>
|
|
);
|
|
};
|
|
|
|
export default CollapsibleTree;
|