mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-04-23 09:18:30 +02:00
Better drag for empty folders
This commit is contained in:
@@ -33,17 +33,19 @@ const icons = {
|
|||||||
chevron_left: lucide.ChevronLeftIcon,
|
chevron_left: lucide.ChevronLeftIcon,
|
||||||
chevron_right: lucide.ChevronRightIcon,
|
chevron_right: lucide.ChevronRightIcon,
|
||||||
circle_alert: lucide.CircleAlertIcon,
|
circle_alert: lucide.CircleAlertIcon,
|
||||||
|
circle_dashed: lucide.CircleDashedIcon,
|
||||||
circle_dollar_sign: lucide.CircleDollarSignIcon,
|
circle_dollar_sign: lucide.CircleDollarSignIcon,
|
||||||
circle_fading_arrow_up: lucide.CircleFadingArrowUpIcon,
|
circle_fading_arrow_up: lucide.CircleFadingArrowUpIcon,
|
||||||
clock: lucide.ClockIcon,
|
clock: lucide.ClockIcon,
|
||||||
code: lucide.CodeIcon,
|
code: lucide.CodeIcon,
|
||||||
columns_2: lucide.Columns2Icon,
|
columns_2: lucide.Columns2Icon,
|
||||||
command: lucide.CommandIcon,
|
command: lucide.CommandIcon,
|
||||||
corner_right_up: lucide.CornerRightUpIcon,
|
|
||||||
credit_card: lucide.CreditCardIcon,
|
|
||||||
cookie: lucide.CookieIcon,
|
cookie: lucide.CookieIcon,
|
||||||
copy: lucide.CopyIcon,
|
copy: lucide.CopyIcon,
|
||||||
copy_check: lucide.CopyCheck,
|
copy_check: lucide.CopyCheck,
|
||||||
|
corner_right_up: lucide.CornerRightUpIcon,
|
||||||
|
credit_card: lucide.CreditCardIcon,
|
||||||
|
dot: lucide.DotIcon,
|
||||||
download: lucide.DownloadIcon,
|
download: lucide.DownloadIcon,
|
||||||
ellipsis: lucide.EllipsisIcon,
|
ellipsis: lucide.EllipsisIcon,
|
||||||
expand: lucide.ExpandIcon,
|
expand: lucide.ExpandIcon,
|
||||||
@@ -55,8 +57,8 @@ const icons = {
|
|||||||
flame: lucide.FlameIcon,
|
flame: lucide.FlameIcon,
|
||||||
flask: lucide.FlaskConicalIcon,
|
flask: lucide.FlaskConicalIcon,
|
||||||
folder: lucide.FolderIcon,
|
folder: lucide.FolderIcon,
|
||||||
folder_cog: lucide.FolderCogIcon,
|
|
||||||
folder_code: lucide.FolderCodeIcon,
|
folder_code: lucide.FolderCodeIcon,
|
||||||
|
folder_cog: lucide.FolderCogIcon,
|
||||||
folder_git: lucide.FolderGitIcon,
|
folder_git: lucide.FolderGitIcon,
|
||||||
folder_input: lucide.FolderInputIcon,
|
folder_input: lucide.FolderInputIcon,
|
||||||
folder_open: lucide.FolderOpenIcon,
|
folder_open: lucide.FolderOpenIcon,
|
||||||
|
|||||||
@@ -267,10 +267,15 @@ function TreeInner<T extends { id: string }>(
|
|||||||
const collapsedMap = jotaiStore.get(collapsedFamily(treeId));
|
const collapsedMap = jotaiStore.get(collapsedFamily(treeId));
|
||||||
const isHoveredItemCollapsed = hovered != null ? collapsedMap[hovered.item.id] : false;
|
const isHoveredItemCollapsed = hovered != null ? collapsedMap[hovered.item.id] : false;
|
||||||
|
|
||||||
if (hovered?.children != null && side === 'below' && !isHoveredItemCollapsed) {
|
if (hovered?.children != null && side === 'below') {
|
||||||
// Move into the folder if it's open and we're moving below it
|
// Move into the folder if it's open and we're moving below it
|
||||||
hoveredParent = hovered;
|
if (isHoveredItemCollapsed) {
|
||||||
hoveredChildIndex = 0;
|
hoveredParent = hovered;
|
||||||
|
hoveredChildIndex = 0;
|
||||||
|
} else {
|
||||||
|
hoveredParent = hovered;
|
||||||
|
hoveredChildIndex = 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const parentId = hoveredParent?.item.id ?? null;
|
const parentId = hoveredParent?.item.id ?? null;
|
||||||
|
|||||||
@@ -3,28 +3,30 @@ import { useAtomValue } from 'jotai';
|
|||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
import { DropMarker } from '../../DropMarker';
|
import { DropMarker } from '../../DropMarker';
|
||||||
import { hoveredParentDepthFamily, isCollapsedFamily, isIndexHoveredFamily } from './atoms';
|
import { hoveredParentDepthFamily, isCollapsedFamily, isIndexHoveredFamily } from './atoms';
|
||||||
|
import type { TreeNode } from './common';
|
||||||
|
|
||||||
export const TreeDropMarker = memo(function TreeDropMarker({
|
export const TreeDropMarker = memo(function TreeDropMarker<T extends { id: string }>({
|
||||||
className,
|
className,
|
||||||
treeId,
|
treeId,
|
||||||
itemId,
|
node,
|
||||||
index,
|
index,
|
||||||
}: {
|
}: {
|
||||||
treeId: string;
|
treeId: string;
|
||||||
index: number;
|
index: number;
|
||||||
itemId: string | null;
|
node: TreeNode<T> | null;
|
||||||
className?: string;
|
className?: string;
|
||||||
}) {
|
}) {
|
||||||
|
const itemId = node?.item.id;
|
||||||
const isHovered = useAtomValue(isIndexHoveredFamily({ treeId, index }));
|
const isHovered = useAtomValue(isIndexHoveredFamily({ treeId, index }));
|
||||||
const parentDepth = useAtomValue(hoveredParentDepthFamily(treeId));
|
const parentDepth = useAtomValue(hoveredParentDepthFamily(treeId));
|
||||||
const collapsed = useAtomValue(isCollapsedFamily({ treeId, itemId: itemId ?? undefined }));
|
const collapsed = useAtomValue(isCollapsedFamily({ treeId, itemId }));
|
||||||
|
|
||||||
// Only show if we're hovering over this index
|
// Only show if we're hovering over this index
|
||||||
if (!isHovered) return null;
|
if (!isHovered) return null;
|
||||||
|
|
||||||
// Don't show if we're right under a collapsed folder. We have a separate delayed expansion
|
// Don't show if we're right under a collapsed folder, or empty folder. We have a separate delayed expansion
|
||||||
// animation for that.
|
// animation for that.
|
||||||
if (collapsed) return null;
|
if (collapsed || node?.children?.length === 0) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="drop-marker" style={{ paddingLeft: `${parentDepth}rem` }}>
|
<div className="drop-marker" style={{ paddingLeft: `${parentDepth}rem` }}>
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ function TreeItem_<T extends { id: string }>({
|
|||||||
const isCollapsed = useAtomValue(isCollapsedFamily({ treeId, itemId: node.item.id }));
|
const isCollapsed = useAtomValue(isCollapsedFamily({ treeId, itemId: node.item.id }));
|
||||||
const isLastSelected = useAtomValue(isLastFocusedFamily({ treeId, itemId: node.item.id }));
|
const isLastSelected = useAtomValue(isLastFocusedFamily({ treeId, itemId: node.item.id }));
|
||||||
const [editing, setEditing] = useState<boolean>(false);
|
const [editing, setEditing] = useState<boolean>(false);
|
||||||
const [isDropHover, setIsDropHover] = useState<boolean>(false);
|
const [dropHover, setDropHover] = useState<null | 'drop' | 'animate'>(null);
|
||||||
const startedHoverTimeout = useRef<NodeJS.Timeout>(undefined);
|
const startedHoverTimeout = useRef<NodeJS.Timeout>(undefined);
|
||||||
|
|
||||||
const isAncestorCollapsedAtom = useMemo(
|
const isAncestorCollapsedAtom = useMemo(
|
||||||
@@ -147,29 +147,35 @@ function TreeItem_<T extends { id: string }>({
|
|||||||
}
|
}
|
||||||
}, [getEditOptions, node.children, toggleCollapsed]);
|
}, [getEditOptions, node.children, toggleCollapsed]);
|
||||||
|
|
||||||
const clearHoverTimer = () => {
|
const clearDropHover = () => {
|
||||||
if (startedHoverTimeout.current) {
|
if (startedHoverTimeout.current) {
|
||||||
setIsDropHover(false); // NEW
|
clearTimeout(startedHoverTimeout.current);
|
||||||
clearTimeout(startedHoverTimeout.current); // NEW
|
startedHoverTimeout.current = undefined;
|
||||||
startedHoverTimeout.current = undefined; // NEW
|
|
||||||
}
|
}
|
||||||
|
setDropHover(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Toggle auto-expand of folders when hovering over them
|
// Toggle auto-expand of folders when hovering over them
|
||||||
useDndMonitor({
|
useDndMonitor({
|
||||||
|
onDragEnd() {
|
||||||
|
clearDropHover();
|
||||||
|
},
|
||||||
onDragMove(e: DragMoveEvent) {
|
onDragMove(e: DragMoveEvent) {
|
||||||
const side = computeSideForDragMove(node, e);
|
const side = computeSideForDragMove(node, e);
|
||||||
const isFolderWithChildren = (node.children?.length ?? 0) > 0;
|
const isFolder = node.children != null;
|
||||||
|
const hasChildren = (node.children?.length ?? 0) > 0;
|
||||||
const isCollapsed = jotaiStore.get(isCollapsedFamily({ treeId, itemId: node.item.id }));
|
const isCollapsed = jotaiStore.get(isCollapsedFamily({ treeId, itemId: node.item.id }));
|
||||||
if (isCollapsed && isFolderWithChildren && side === 'below') {
|
if (isCollapsed && isFolder && hasChildren && side === 'below') {
|
||||||
setIsDropHover(true);
|
setDropHover('animate');
|
||||||
clearTimeout(startedHoverTimeout.current);
|
clearTimeout(startedHoverTimeout.current);
|
||||||
startedHoverTimeout.current = setTimeout(() => {
|
startedHoverTimeout.current = setTimeout(() => {
|
||||||
jotaiStore.set(isCollapsedFamily({ treeId, itemId: node.item.id }), false);
|
jotaiStore.set(isCollapsedFamily({ treeId, itemId: node.item.id }), false);
|
||||||
setIsDropHover(false);
|
clearDropHover();
|
||||||
}, HOVER_CLOSED_FOLDER_DELAY);
|
}, HOVER_CLOSED_FOLDER_DELAY);
|
||||||
|
} else if (isFolder && !hasChildren && side === 'below') {
|
||||||
|
setDropHover('drop');
|
||||||
} else {
|
} else {
|
||||||
clearHoverTimer();
|
clearDropHover();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -227,6 +233,9 @@ function TreeItem_<T extends { id: string }>({
|
|||||||
'tree-item',
|
'tree-item',
|
||||||
'h-sm',
|
'h-sm',
|
||||||
'grid grid-cols-[auto_minmax(0,1fr)]',
|
'grid grid-cols-[auto_minmax(0,1fr)]',
|
||||||
|
editing && 'ring-1 focus-within:ring-focus',
|
||||||
|
dropHover != null && 'relative z-10 ring-2 ring-primary',
|
||||||
|
dropHover === 'animate' && 'animate-blinkRing',
|
||||||
isSelected && 'selected',
|
isSelected && 'selected',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -235,10 +244,7 @@ function TreeItem_<T extends { id: string }>({
|
|||||||
className={classNames(
|
className={classNames(
|
||||||
'tree-item-selectable',
|
'tree-item-selectable',
|
||||||
'text-text-subtle',
|
'text-text-subtle',
|
||||||
isSelected && 'selected',
|
|
||||||
'grid grid-cols-[auto_minmax(0,1fr)] items-center rounded-md',
|
'grid grid-cols-[auto_minmax(0,1fr)] items-center rounded-md',
|
||||||
editing && 'ring-1 focus-within:ring-focus',
|
|
||||||
isDropHover && 'relative z-10 ring-2 ring-primary animate-blinkRing',
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{showContextMenu && (
|
{showContextMenu && (
|
||||||
@@ -251,14 +257,12 @@ function TreeItem_<T extends { id: string }>({
|
|||||||
{node.children != null ? (
|
{node.children != null ? (
|
||||||
<button tabIndex={-1} className="h-full pl-[0.5rem]" onClick={toggleCollapsed}>
|
<button tabIndex={-1} className="h-full pl-[0.5rem]" onClick={toggleCollapsed}>
|
||||||
<Icon
|
<Icon
|
||||||
icon="chevron_right"
|
icon={node.children.length === 0 ? 'dot' : 'chevron_right'}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'transition-transform text-text-subtlest',
|
'transition-transform text-text-subtlest',
|
||||||
'ml-auto',
|
'ml-auto',
|
||||||
'w-[1rem] h-[1rem]',
|
'w-[1rem] h-[1rem]',
|
||||||
// node.children.length == 0 && 'opacity-0',
|
!isCollapsed && node.children.length > 0 && 'rotate-90',
|
||||||
!isCollapsed && 'rotate-90',
|
|
||||||
// isHoveredAsParent && '!text-text',
|
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ function TreeItemList_<T extends { id: string }>({
|
|||||||
}: TreeItemListProps<T>) {
|
}: TreeItemListProps<T>) {
|
||||||
return (
|
return (
|
||||||
<ul role="tree" style={style} className={className}>
|
<ul role="tree" style={style} className={className}>
|
||||||
<TreeDropMarker itemId={null} treeId={treeId} index={0} />
|
<TreeDropMarker node={null} treeId={treeId} index={0} />
|
||||||
{nodes.map((child, i) => (
|
{nodes.map((child, i) => (
|
||||||
<Fragment key={getItemKey(child.node.item)}>
|
<Fragment key={getItemKey(child.node.item)}>
|
||||||
<TreeItem
|
<TreeItem
|
||||||
@@ -46,7 +46,7 @@ function TreeItemList_<T extends { id: string }>({
|
|||||||
getItemKey={getItemKey}
|
getItemKey={getItemKey}
|
||||||
depth={forceDepth == null ? child.depth : forceDepth}
|
depth={forceDepth == null ? child.depth : forceDepth}
|
||||||
/>
|
/>
|
||||||
<TreeDropMarker itemId={child.node.item.id} treeId={treeId} index={i+1} />
|
<TreeDropMarker node={child.node} treeId={treeId} index={i+1} />
|
||||||
</Fragment>
|
</Fragment>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
Reference in New Issue
Block a user