mirror of
https://github.com/IRBorisov/ConceptPortal.git
synced 2025-06-26 13:00:39 +03:00
140 lines
4.7 KiB
TypeScript
140 lines
4.7 KiB
TypeScript
![]() |
'use client';
|
||
|
|
||
|
import clsx from 'clsx';
|
||
|
import { AnimatePresence, motion } from 'framer-motion';
|
||
|
import { useCallback, useLayoutEffect, useMemo, useState } from 'react';
|
||
|
|
||
|
import { IconFolder, IconFolderClosed, IconFolderOpened, IconFolderTree } from '@/components/Icons';
|
||
|
import { CProps } from '@/components/props';
|
||
|
import MiniButton from '@/components/ui/MiniButton';
|
||
|
import { FolderNode, FolderTree } from '@/models/FolderTree';
|
||
|
import { animateSideAppear, animateSideView } from '@/styling/animations';
|
||
|
import { globals, prefixes } from '@/utils/constants';
|
||
|
import { describeFolderNode, labelFolderNode } from '@/utils/labels';
|
||
|
|
||
|
interface LibraryTableProps {
|
||
|
folders: FolderTree;
|
||
|
currentFolder: string;
|
||
|
setFolder: React.Dispatch<React.SetStateAction<string>>;
|
||
|
toggleFolderMode: () => void;
|
||
|
}
|
||
|
|
||
|
function LibraryFolders({ folders, currentFolder, setFolder, toggleFolderMode }: LibraryTableProps) {
|
||
|
const activeNode = useMemo(() => folders.at(currentFolder), [folders, currentFolder]);
|
||
|
|
||
|
const items = useMemo(() => folders.getTree(), [folders]);
|
||
|
const [folded, setFolded] = useState<FolderNode[]>([]);
|
||
|
|
||
|
useLayoutEffect(() => {
|
||
|
setFolded(items.filter(item => item !== activeNode && (!activeNode || !activeNode.hasPredecessor(item))));
|
||
|
}, [items, activeNode]);
|
||
|
|
||
|
const onFoldItem = useCallback(
|
||
|
(target: FolderNode, showChildren: boolean) => {
|
||
|
setFolded(prev =>
|
||
|
items.filter(item => {
|
||
|
if (item === target) {
|
||
|
return !showChildren;
|
||
|
}
|
||
|
if (!showChildren && item.hasPredecessor(target)) {
|
||
|
return true;
|
||
|
} else {
|
||
|
return prev.includes(item);
|
||
|
}
|
||
|
})
|
||
|
);
|
||
|
},
|
||
|
[items]
|
||
|
);
|
||
|
|
||
|
const handleSetValue = useCallback(
|
||
|
(event: CProps.EventMouse, target: FolderNode) => {
|
||
|
event.preventDefault();
|
||
|
event.stopPropagation();
|
||
|
setFolder(target.getPath());
|
||
|
},
|
||
|
[setFolder]
|
||
|
);
|
||
|
|
||
|
const handleClickFold = useCallback(
|
||
|
(event: CProps.EventMouse, target: FolderNode, showChildren: boolean) => {
|
||
|
event.preventDefault();
|
||
|
event.stopPropagation();
|
||
|
onFoldItem(target, showChildren);
|
||
|
},
|
||
|
[onFoldItem]
|
||
|
);
|
||
|
|
||
|
return (
|
||
|
<motion.div
|
||
|
className='flex flex-col text:xs sm:text-sm'
|
||
|
initial={{ ...animateSideView.initial }}
|
||
|
animate={{ ...animateSideView.animate }}
|
||
|
exit={{ ...animateSideView.exit }}
|
||
|
>
|
||
|
<div className='h-[2.08rem] flex justify-end pr-1'>
|
||
|
<MiniButton
|
||
|
icon={<IconFolderTree size='1.25rem' className='icon-green' />}
|
||
|
title='Режим: проводник'
|
||
|
onClick={toggleFolderMode}
|
||
|
/>
|
||
|
</div>
|
||
|
<div
|
||
|
className={clsx(
|
||
|
'max-w-[10rem] sm:max-w-[15rem] min-w-[10rem] sm:min-w-[15rem]',
|
||
|
'flex flex-col',
|
||
|
'cc-scroll-y'
|
||
|
)}
|
||
|
>
|
||
|
<AnimatePresence initial={false}>
|
||
|
{items.map((item, index) =>
|
||
|
!item.parent || !folded.includes(item.parent) ? (
|
||
|
<motion.div
|
||
|
tabIndex={-1}
|
||
|
key={`${prefixes.folders_list}${index}`}
|
||
|
className={clsx(
|
||
|
'min-h-[2.0825rem] sm:min-h-[2.3125rem]',
|
||
|
'pr-3 flex items-center gap-2',
|
||
|
'cc-scroll-row',
|
||
|
'clr-hover',
|
||
|
'cursor-pointer',
|
||
|
activeNode === item && 'clr-selected'
|
||
|
)}
|
||
|
style={{ paddingLeft: `${(item.rank > 5 ? 5 : item.rank) * 0.5 + 0.5}rem` }}
|
||
|
data-tooltip-id={globals.tooltip}
|
||
|
data-tooltip-html={describeFolderNode(item)}
|
||
|
onClick={event => handleSetValue(event, item)}
|
||
|
initial={{ ...animateSideAppear.initial }}
|
||
|
animate={{ ...animateSideAppear.animate }}
|
||
|
exit={{ ...animateSideAppear.exit }}
|
||
|
>
|
||
|
{item.children.size > 0 ? (
|
||
|
<MiniButton
|
||
|
noPadding
|
||
|
noHover
|
||
|
icon={
|
||
|
folded.includes(item) ? (
|
||
|
<IconFolderClosed size='1rem' className='icon-primary' />
|
||
|
) : (
|
||
|
<IconFolderOpened size='1rem' className='icon-green' />
|
||
|
)
|
||
|
}
|
||
|
onClick={event => handleClickFold(event, item, folded.includes(item))}
|
||
|
/>
|
||
|
) : (
|
||
|
<div>
|
||
|
<IconFolder size='1rem' />
|
||
|
</div>
|
||
|
)}
|
||
|
<div className='self-center'>{labelFolderNode(item)}</div>
|
||
|
</motion.div>
|
||
|
) : null
|
||
|
)}
|
||
|
</AnimatePresence>
|
||
|
</div>
|
||
|
</motion.div>
|
||
|
);
|
||
|
}
|
||
|
|
||
|
export default LibraryFolders;
|