121 lines
3.7 KiB
TypeScript
121 lines
3.7 KiB
TypeScript
![]() |
import clsx from 'clsx';
|
||
|
import { AnimatePresence, motion } from 'framer-motion';
|
||
|
import { useCallback, useLayoutEffect, useMemo, useState } from 'react';
|
||
|
|
||
|
import { animateSideAppear } from '@/styling/animations';
|
||
|
import { globals } from '@/utils/constants';
|
||
|
|
||
|
import { IconDropArrow, IconPageRight } from '../Icons';
|
||
|
import { CProps } from '../props';
|
||
|
import MiniButton from './MiniButton';
|
||
|
import Overlay from './Overlay';
|
||
|
|
||
|
interface SelectTreeProps<ItemType> extends CProps.Styling {
|
||
|
items: ItemType[];
|
||
|
value: ItemType;
|
||
|
setValue: (newItem: ItemType) => void;
|
||
|
getParent: (item: ItemType) => ItemType;
|
||
|
getLabel: (item: ItemType) => string;
|
||
|
getDescription: (item: ItemType) => string;
|
||
|
prefix: string;
|
||
|
}
|
||
|
|
||
|
function SelectTree<ItemType>({
|
||
|
items,
|
||
|
value,
|
||
|
getParent,
|
||
|
getLabel,
|
||
|
getDescription,
|
||
|
setValue,
|
||
|
prefix,
|
||
|
...restProps
|
||
|
}: SelectTreeProps<ItemType>) {
|
||
|
const foldable = useMemo(
|
||
|
() => new Set(items.filter(item => getParent(item) !== item).map(item => getParent(item))),
|
||
|
[items, getParent]
|
||
|
);
|
||
|
const [folded, setFolded] = useState<ItemType[]>(items);
|
||
|
|
||
|
useLayoutEffect(() => {
|
||
|
setFolded(items.filter(item => getParent(value) !== item && getParent(getParent(value)) !== item));
|
||
|
}, [value, getParent, items]);
|
||
|
|
||
|
const onFoldItem = useCallback(
|
||
|
(target: ItemType, showChildren: boolean) => {
|
||
|
setFolded(prev =>
|
||
|
items.filter(item => {
|
||
|
if (item === target) {
|
||
|
return !showChildren;
|
||
|
}
|
||
|
if (!showChildren && (getParent(item) === target || getParent(getParent(item)) === target)) {
|
||
|
return true;
|
||
|
} else {
|
||
|
return prev.includes(item);
|
||
|
}
|
||
|
})
|
||
|
);
|
||
|
},
|
||
|
[items, getParent]
|
||
|
);
|
||
|
|
||
|
const handleClickFold = useCallback(
|
||
|
(event: CProps.EventMouse, target: ItemType, showChildren: boolean) => {
|
||
|
event.preventDefault();
|
||
|
event.stopPropagation();
|
||
|
onFoldItem(target, showChildren);
|
||
|
},
|
||
|
[onFoldItem]
|
||
|
);
|
||
|
|
||
|
const handleSetValue = useCallback(
|
||
|
(event: CProps.EventMouse, target: ItemType) => {
|
||
|
event.preventDefault();
|
||
|
event.stopPropagation();
|
||
|
setValue(target);
|
||
|
},
|
||
|
[setValue]
|
||
|
);
|
||
|
|
||
|
return (
|
||
|
<div {...restProps}>
|
||
|
<AnimatePresence initial={false}>
|
||
|
{items.map((item, index) =>
|
||
|
getParent(item) === item || !folded.includes(getParent(item)) ? (
|
||
|
<motion.div
|
||
|
tabIndex={-1}
|
||
|
key={`${prefix}${index}`}
|
||
|
className={clsx(
|
||
|
'pr-3 pl-6 py-1',
|
||
|
'cc-scroll-row',
|
||
|
'clr-controls clr-hover',
|
||
|
'cursor-pointer',
|
||
|
value === item && 'clr-selected'
|
||
|
)}
|
||
|
data-tooltip-id={globals.tooltip}
|
||
|
data-tooltip-html={getDescription(item)}
|
||
|
onClick={event => handleSetValue(event, item)}
|
||
|
initial={{ ...animateSideAppear.initial }}
|
||
|
animate={{ ...animateSideAppear.animate }}
|
||
|
exit={{ ...animateSideAppear.exit }}
|
||
|
>
|
||
|
{foldable.has(item) ? (
|
||
|
<Overlay position='left-[-1.3rem]' className={clsx(!folded.includes(item) && 'top-[0.1rem]')}>
|
||
|
<MiniButton
|
||
|
noPadding
|
||
|
noHover
|
||
|
icon={!folded.includes(item) ? <IconDropArrow size='1rem' /> : <IconPageRight size='1.25rem' />}
|
||
|
onClick={event => handleClickFold(event, item, folded.includes(item))}
|
||
|
/>
|
||
|
</Overlay>
|
||
|
) : null}
|
||
|
{getParent(item) === item ? getLabel(item) : `- ${getLabel(item).toLowerCase()}`}
|
||
|
</motion.div>
|
||
|
) : null
|
||
|
)}
|
||
|
</AnimatePresence>
|
||
|
</div>
|
||
|
);
|
||
|
}
|
||
|
|
||
|
export default SelectTree;
|