Skip to content

Instantly share code, notes, and snippets.

@AgrYpn1a
Last active April 30, 2024 13:30
Show Gist options
  • Save AgrYpn1a/78c033ab4054d9073dfa1bf813508dd1 to your computer and use it in GitHub Desktop.
Save AgrYpn1a/78c033ab4054d9073dfa1bf813508dd1 to your computer and use it in GitHub Desktop.
import { Tree, TreeItem, TreeNode } from './types';
export const treeFind = <T extends TreeItem>(matchId: string, tree: Tree<T>): TreeNode<T> | undefined => {
return tree.reduce(
(result: TreeNode<T> | undefined, curr) =>
curr.item.id === matchId ? curr : result?.item.id === matchId ? result : treeFind(matchId, curr.children || []),
undefined,
);
};
export function treeMap<T extends TreeItem>(tree: Tree<T>, functor: (node: TreeNode<T>) => TreeNode<T>) {
return tree.map((node) => ({ ...functor(node), ...(node.children && { children: treeMap(node.children, functor) }) }));
}
import React from 'react';
import Box from '@mui/material/Box';
import { TreeItem as MuiTreeItem, TreeItemProps, treeItemClasses } from '@mui/x-tree-view/TreeItem';
import Typography from '@mui/material/Typography';
import { styled, useTheme } from '@mui/material/styles';
import { defaultStyledOptions } from '@protecht/ui-library/library/utils/defaultStyledOptions';
import { faTriangle } from '@fortawesome/pro-solid-svg-icons';
import { faTriangle as faTriangleRegular } from '@fortawesome/pro-regular-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
export type StyledTreeItemProps = TreeItemProps & {
labelIcon: React.ReactNode;
labelInfo?: string;
labelText: string;
indentation?: number;
OverrideLabel?: React.ReactNode;
};
export const StyledTreeItemRoot = styled(
MuiTreeItem,
defaultStyledOptions,
)<{ $indentation: number }>(({ theme, $indentation }) => ({
color: theme.palette.text.secondary,
minWidth: '100%',
width: 'max-content',
'& .MuiTreeItem-root': {
width: '100%',
'& .MuiTreeItem-content:hover': {
background: 'linear-gradient(rgba(0, 0, 0, 0.15) 0 0)',
'& .MuiTreeItem-content:hover': {
background: 'none',
},
},
'& .MuiTreeItem-content.Mui-selected': {
background: 'linear-gradient(rgba(0, 0, 0, 0.30) 0 0)',
'& .MuiTreeItem-content.Mui-selected': {
background: 'none',
},
},
},
'& .MuiTreeItem-label': {
width: '100%',
},
[`& .${treeItemClasses.group}`]: {
width: '100%',
},
'& .MuiCollapse-root': {
margin: 0,
width: '100%',
},
'& .MuiCollapse-wrapperInner .MuiTreeItem-content': {
width: '100%',
paddingLeft: `${$indentation}px`,
},
}));
const TreeViewItem = React.forwardRef(function StyledTreeItem(props: StyledTreeItemProps, ref: React.Ref<HTMLLIElement>) {
const { OverrideLabel, labelIcon: LabelIcon, labelInfo, labelText, indentation = 16, ...other } = props;
const theme = useTheme();
return (
<StyledTreeItemRoot
ref={ref}
$indentation={indentation}
collapseIcon={
<FontAwesomeIcon
aria-label="triangle-down"
aria-hidden={false}
icon={faTriangle}
className="fa-rotate-180"
style={{
fontSize: '12px',
color: theme.palette.text.primary,
}}
/>
}
expandIcon={
<FontAwesomeIcon
aria-label="triangle-right"
aria-hidden={false}
icon={faTriangleRegular}
className="fa-rotate-90"
style={{
fontSize: '12px',
color: theme.palette.text.primary,
}}
/>
}
label={
OverrideLabel ?? (
<Box
sx={{
display: 'flex',
alignItems: 'center',
p: 0.5,
pr: 0,
}}
>
<Box
color="inherit"
sx={{ mr: 1 }}
>
{LabelIcon}
</Box>
<Typography
variant="body2"
sx={{ fontWeight: 'inherit', flexGrow: 1 }}
>
{labelText}
</Typography>
<Typography
variant="caption"
color="inherit"
>
{labelInfo}
</Typography>
</Box>
)
}
{...other}
/>
);
});
export default TreeViewItem;
import React, { useCallback, useMemo, useState } from 'react';
import { TreeView as MuiTreeView } from '@mui/x-tree-view/TreeView';
import Grid from '@mui/material/Grid';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faFolder, faFolderOpen, faTag } from '@fortawesome/pro-regular-svg-icons';
import { faTriangle } from '@fortawesome/pro-solid-svg-icons';
import { faTriangle as faTriangleRegular } from '@fortawesome/pro-regular-svg-icons';
import { styled, useTheme } from '@mui/material/styles';
import Box from '@mui/material/Box';
import Loading from '@protecht/ui-library/library/components/Loading';
import TreeViewItem from './TreeItem';
import { TreeItem, TreeNode } from 'lib/tree/types';
import { treeFind, treeMap } from 'lib/tree/functions';
type TreeViewProps<T extends TreeItem> = {
items: TreeNode<T>[];
onChange: (value: T) => void;
onItemDoubleClick?: (value: T) => void;
indentation?: number;
loading?: boolean;
renderTreeOverride?: React.FC<TreeNode<T>>;
TreeNodeLabelOverride?: React.FC<T>;
TreeLeafLabelOverride?: React.FC<T>;
highlightedItemsIds?: (string | number)[];
disableInteraction?: boolean;
};
function TreeView<T extends TreeItem>({
items,
onChange,
onItemDoubleClick,
indentation,
loading,
renderTreeOverride,
TreeNodeLabelOverride,
TreeLeafLabelOverride,
highlightedItemsIds,
disableInteraction,
}: TreeViewProps<T>) {
const theme = useTheme();
const [expanded, setExpanded] = useState<(string | number)[]>([]);
const transformedItems = useMemo(
() =>
treeMap(items, (node) => ({
...node,
item: { ...node.item, isHighlighted: highlightedItemsIds?.includes(node.item.id) },
expanded: expanded.includes(node.item.id),
})),
[items, expanded, highlightedItemsIds],
);
const renderTree = useCallback(
(node: TreeNode<T>) => {
if (!node.children) {
return (
<TreeViewItem
key={node.item.id}
nodeId={node.item.id.toString()}
labelText={node.item.label}
labelIcon={
<LabelIconWrapper>
<FontAwesomeIcon
icon={faTag}
color={theme.palette.primary.main}
/>
</LabelIconWrapper>
}
indentation={indentation}
OverrideLabel={TreeLeafLabelOverride && <TreeLeafLabelOverride {...node.item} />}
/>
);
} else {
return (
<TreeViewItem
key={node.item.id}
nodeId={node.item.id.toString()}
labelText={node.item.label}
labelIcon={
<LabelIconWrapper>
{expanded.some((expItemId) => expItemId === node.item.id) ? (
<FontAwesomeIcon
icon={faFolderOpen}
color={theme.palette.primary.main}
/>
) : (
<FontAwesomeIcon
icon={faFolder}
color={theme.palette.primary.main}
/>
)}
</LabelIconWrapper>
}
indentation={indentation}
OverrideLabel={TreeNodeLabelOverride && <TreeNodeLabelOverride {...node.item} />}
>
{node.children?.map(renderTree)}
</TreeViewItem>
);
}
},
[expanded, indentation, theme, TreeNodeLabelOverride, TreeLeafLabelOverride],
);
const handleOnNodeSelect = useCallback(
(event: React.MouseEvent<HTMLElement>, nodeId) => {
const resultNode = treeFind(nodeId, items);
if (resultNode) {
// Process double-click event
if (event.detail === 2) {
onItemDoubleClick?.(resultNode.item);
}
onChange(resultNode.item);
}
},
[items, onChange, onItemDoubleClick],
);
return (
<Grid
container
direction="column"
sx={{
margin: '0 auto',
flexWrap: 'nowrap',
height: 0,
flexGrow: 1,
overflowY: 'auto',
'*': {
userSelect: 'none',
...(disableInteraction && {
pointerEvents: 'none',
}),
},
}}
>
<BodyGrid item>
{loading ? (
<Box
sx={{
width: '100%',
height: '100%',
position: 'relative',
}}
>
<Loading />
</Box>
) : (
<MuiTreeView
defaultCollapseIcon={
<FontAwesomeIcon
aria-label="triangle-down"
aria-hidden={false}
icon={faTriangle}
className="fa-rotate-180"
style={{
fontSize: '12px',
color: theme.palette.text.primary,
}}
/>
}
defaultExpandIcon={
<FontAwesomeIcon
aria-label="triangle-right"
aria-hidden={false}
icon={faTriangleRegular}
className="fa-rotate-90"
style={{
fontSize: '12px',
color: theme.palette.text.primary,
}}
/>
}
onNodeSelect={handleOnNodeSelect}
onNodeToggle={(_, nodeIds) => setExpanded(nodeIds)}
>
{transformedItems.map(renderTreeOverride ?? renderTree)}
</MuiTreeView>
)}
</BodyGrid>
</Grid>
);
}
export default TreeView;
const BodyGrid = styled(Grid)(({ theme }) => ({
flex: 1,
border: '1px solid ' + theme.palette.protechtGrey?.grey_231,
borderRadius: '4px',
overflow: 'auto',
}));
const LabelIconWrapper = styled(Box)({
marginRight: '6px',
});
export type TreeItem = {
id: string | number;
label: string;
isHighlighted?: boolean;
};
export type TreeNode<T extends TreeItem> = {
item: T;
children?: TreeNode<T>[];
expanded?: boolean;
};
export type Tree<T extends TreeItem> = TreeNode<T>[];
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment