2026-06-24 11:08:40 +03:00

269 lines
7.1 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,
forStage = false
}) => {
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 = forStage ? selectedNodes.length : 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;