mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-07-11 15:22:54 +02:00
Fix indent guide on drag and drop after expand folder
https://feedback.yaak.app/p/displace-moving-caret-on-spring-loaded-folder
This commit is contained in:
@@ -26,13 +26,7 @@ import { computeSideForDragMove } from '../../../lib/dnd';
|
|||||||
import { jotaiStore } from '../../../lib/jotai';
|
import { jotaiStore } from '../../../lib/jotai';
|
||||||
import type { ContextMenuProps, DropdownItem } from '../Dropdown';
|
import type { ContextMenuProps, DropdownItem } from '../Dropdown';
|
||||||
import { ContextMenu } from '../Dropdown';
|
import { ContextMenu } from '../Dropdown';
|
||||||
import {
|
import { draggingIdsFamily, focusIdsFamily, hoveredParentFamily, selectedIdsFamily } from './atoms';
|
||||||
collapsedFamily,
|
|
||||||
draggingIdsFamily,
|
|
||||||
focusIdsFamily,
|
|
||||||
hoveredParentFamily,
|
|
||||||
selectedIdsFamily,
|
|
||||||
} from './atoms';
|
|
||||||
import type { SelectableTreeNode, TreeNode } from './common';
|
import type { SelectableTreeNode, TreeNode } from './common';
|
||||||
import { equalSubtree, getSelectedItems, hasAncestor } from './common';
|
import { equalSubtree, getSelectedItems, hasAncestor } from './common';
|
||||||
import { TreeDragOverlay } from './TreeDragOverlay';
|
import { TreeDragOverlay } from './TreeDragOverlay';
|
||||||
@@ -294,8 +288,8 @@ function TreeInner<T extends { id: string }>(
|
|||||||
if (selectableItem == null) {
|
if (selectableItem == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const node = selectableItem.node;
|
|
||||||
|
|
||||||
|
const node = selectableItem.node;
|
||||||
const side = computeSideForDragMove(node.item.id, e);
|
const side = computeSideForDragMove(node.item.id, e);
|
||||||
|
|
||||||
const item = node.item;
|
const item = node.item;
|
||||||
@@ -305,12 +299,8 @@ function TreeInner<T extends { id: string }>(
|
|||||||
const hoveredIndex = dragIndex + (side === 'above' ? 0 : 1);
|
const hoveredIndex = dragIndex + (side === 'above' ? 0 : 1);
|
||||||
let hoveredChildIndex = selectableItem.index + (side === 'above' ? 0 : 1);
|
let hoveredChildIndex = selectableItem.index + (side === 'above' ? 0 : 1);
|
||||||
|
|
||||||
const collapsedMap = jotaiStore.get(collapsedFamily(treeId));
|
// Move into the folder if it's open and we're moving below it
|
||||||
const isHoveredItemCollapsed =
|
if (hovered?.children != null && side === 'below') {
|
||||||
hovered != null ? hovered.children?.length === 0 || collapsedMap[hovered.item.id] : false;
|
|
||||||
|
|
||||||
if (hovered?.children != null && side === 'below' && isHoveredItemCollapsed) {
|
|
||||||
// Move into the folder if it's open and we're moving below it
|
|
||||||
hoveredParent = hovered;
|
hoveredParent = hovered;
|
||||||
hoveredChildIndex = 0;
|
hoveredChildIndex = 0;
|
||||||
}
|
}
|
||||||
@@ -328,12 +318,7 @@ function TreeInner<T extends { id: string }>(
|
|||||||
childIndex === existing.childIndex
|
childIndex === existing.childIndex
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
jotaiStore.set(hoveredParentFamily(treeId), {
|
jotaiStore.set(hoveredParentFamily(treeId), { parentId, parentDepth, index, childIndex });
|
||||||
parentId: hoveredParent?.item.id ?? null,
|
|
||||||
parentDepth: hoveredParent?.depth ?? null,
|
|
||||||
index: hoveredIndex,
|
|
||||||
childIndex: hoveredChildIndex,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[root.depth, root.item.id, selectableItems, treeId],
|
[root.depth, root.item.id, selectableItems, treeId],
|
||||||
@@ -341,20 +326,23 @@ function TreeInner<T extends { id: string }>(
|
|||||||
|
|
||||||
const handleDragStart = useCallback(
|
const handleDragStart = useCallback(
|
||||||
function handleDragStart(e: DragStartEvent) {
|
function handleDragStart(e: DragStartEvent) {
|
||||||
const item = selectableItems.find((i) => i.node.item.id === e.active.id)?.node.item ?? null;
|
|
||||||
if (item == null) return;
|
|
||||||
|
|
||||||
const selectedItems = getSelectedItems(treeId, selectableItems);
|
const selectedItems = getSelectedItems(treeId, selectableItems);
|
||||||
const isDraggingSelectedItem = selectedItems.find((i) => i.id === item.id);
|
const isDraggingSelectedItem = selectedItems.find((i) => i.id === e.active.id);
|
||||||
|
|
||||||
|
// If we started dragging an already-selected item, we'll use that
|
||||||
if (isDraggingSelectedItem) {
|
if (isDraggingSelectedItem) {
|
||||||
jotaiStore.set(
|
jotaiStore.set(
|
||||||
draggingIdsFamily(treeId),
|
draggingIdsFamily(treeId),
|
||||||
selectedItems.map((i) => i.id),
|
selectedItems.map((i) => i.id),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
jotaiStore.set(draggingIdsFamily(treeId), [item.id]);
|
// If we started dragging a non-selected item, only drag that item
|
||||||
// Also update selection to just be this one
|
const activeItem = selectableItems.find((i) => i.node.item.id === e.active.id)?.node.item;
|
||||||
handleSelect(item, { shiftKey: false, metaKey: false, ctrlKey: false });
|
if (activeItem != null) {
|
||||||
|
jotaiStore.set(draggingIdsFamily(treeId), [activeItem.id]);
|
||||||
|
// Also update selection to just be this one
|
||||||
|
handleSelect(activeItem, { shiftKey: false, metaKey: false, ctrlKey: false });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[handleSelect, selectableItems, treeId],
|
[handleSelect, selectableItems, treeId],
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { useAtomValue } from 'jotai';
|
import { useAtomValue } from 'jotai';
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
import { hoveredParentDepthFamily, isParentHoveredFamily } from './atoms';
|
import { hoveredParentDepthFamily, isAncestorHoveredFamily } from './atoms';
|
||||||
|
|
||||||
export const TreeIndentGuide = memo(function TreeIndentGuide({
|
export const TreeIndentGuide = memo(function TreeIndentGuide({
|
||||||
treeId,
|
treeId,
|
||||||
depth,
|
depth,
|
||||||
parentId,
|
ancestorIds,
|
||||||
}: {
|
}: {
|
||||||
treeId: string;
|
treeId: string;
|
||||||
depth: number;
|
depth: number;
|
||||||
parentId: string | null;
|
ancestorIds: string[];
|
||||||
}) {
|
}) {
|
||||||
const parentDepth = useAtomValue(hoveredParentDepthFamily(treeId));
|
const parentDepth = useAtomValue(hoveredParentDepthFamily(treeId));
|
||||||
const isHovered = useAtomValue(isParentHoveredFamily({ treeId, parentId }));
|
const isHovered = useAtomValue(isAncestorHoveredFamily({ treeId, ancestorIds }));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { DragMoveEvent } from '@dnd-kit/core';
|
import type { DragMoveEvent } from '@dnd-kit/core';
|
||||||
import { useDndMonitor, useDraggable, useDroppable } from '@dnd-kit/core';
|
import { useDndContext, useDndMonitor, useDraggable, useDroppable } from '@dnd-kit/core';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { useAtomValue } from 'jotai';
|
import { useAtomValue } from 'jotai';
|
||||||
import { selectAtom } from 'jotai/utils';
|
import { selectAtom } from 'jotai/utils';
|
||||||
@@ -72,22 +72,28 @@ function TreeItem_<T extends { id: string }>({
|
|||||||
});
|
});
|
||||||
}, [addRef, editing, getEditOptions, node.item]);
|
}, [addRef, editing, getEditOptions, node.item]);
|
||||||
|
|
||||||
|
const ancestorIds = useMemo(() => {
|
||||||
|
const ids: string[] = [];
|
||||||
|
let p = node.parent;
|
||||||
|
|
||||||
|
while (p) {
|
||||||
|
ids.push(p.item.id);
|
||||||
|
p = p.parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ids;
|
||||||
|
}, [node]);
|
||||||
|
|
||||||
const isAncestorCollapsedAtom = useMemo(
|
const isAncestorCollapsedAtom = useMemo(
|
||||||
() =>
|
() =>
|
||||||
selectAtom(
|
selectAtom(
|
||||||
collapsedFamily(treeId),
|
collapsedFamily(treeId),
|
||||||
(collapsed) => {
|
(collapsed) => ancestorIds.some((id) => collapsed[id]),
|
||||||
const next = (n: TreeNode<T>) => {
|
(a, b) => a === b,
|
||||||
if (n.parent == null) return false;
|
|
||||||
if (collapsed[n.parent.item.id]) return true;
|
|
||||||
return next(n.parent);
|
|
||||||
};
|
|
||||||
return next(node);
|
|
||||||
},
|
|
||||||
(a, b) => a === b, // re-render only when boolean flips
|
|
||||||
),
|
),
|
||||||
[node, treeId],
|
[ancestorIds, treeId],
|
||||||
);
|
);
|
||||||
|
const isAncestorCollapsed = useAtomValue(isAncestorCollapsedAtom);
|
||||||
|
|
||||||
const [showContextMenu, setShowContextMenu] = useState<{
|
const [showContextMenu, setShowContextMenu] = useState<{
|
||||||
items: DropdownItem[];
|
items: DropdownItem[];
|
||||||
@@ -176,6 +182,8 @@ function TreeItem_<T extends { id: string }>({
|
|||||||
setDropHover(null);
|
setDropHover(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const dndContext = useDndContext();
|
||||||
|
|
||||||
// Toggle auto-expand of folders when hovering over them
|
// Toggle auto-expand of folders when hovering over them
|
||||||
useDndMonitor({
|
useDndMonitor({
|
||||||
onDragEnd() {
|
onDragEnd() {
|
||||||
@@ -192,6 +200,12 @@ function TreeItem_<T extends { id: string }>({
|
|||||||
startedHoverTimeout.current = setTimeout(() => {
|
startedHoverTimeout.current = setTimeout(() => {
|
||||||
jotaiStore.set(isCollapsedFamily({ treeId, itemId: node.item.id }), false);
|
jotaiStore.set(isCollapsedFamily({ treeId, itemId: node.item.id }), false);
|
||||||
clearDropHover();
|
clearDropHover();
|
||||||
|
// Force re-measure everything because all containers below the folder have been pushed down
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
dndContext.measureDroppableContainers(
|
||||||
|
dndContext.droppableContainers.toArray().map((c) => c.id),
|
||||||
|
);
|
||||||
|
});
|
||||||
}, HOVER_CLOSED_FOLDER_DELAY);
|
}, HOVER_CLOSED_FOLDER_DELAY);
|
||||||
} else if (isFolder && !hasChildren && side === 'below') {
|
} else if (isFolder && !hasChildren && side === 'below') {
|
||||||
setDropHover('drop');
|
setDropHover('drop');
|
||||||
@@ -239,8 +253,6 @@ function TreeItem_<T extends { id: string }>({
|
|||||||
[setDraggableRef, setDroppableRef],
|
[setDraggableRef, setDroppableRef],
|
||||||
);
|
);
|
||||||
|
|
||||||
if (useAtomValue(isAncestorCollapsedAtom)) return null;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
ref={listItemRef}
|
ref={listItemRef}
|
||||||
@@ -254,13 +266,14 @@ 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)]',
|
||||||
|
isAncestorCollapsed && 'hidden',
|
||||||
editing && 'ring-1 focus-within:ring-focus',
|
editing && 'ring-1 focus-within:ring-focus',
|
||||||
dropHover != null && 'relative z-10 ring-2 ring-primary',
|
dropHover != null && 'relative z-10 ring-2 ring-primary',
|
||||||
dropHover === 'animate' && 'animate-blinkRing',
|
dropHover === 'animate' && 'animate-blinkRing',
|
||||||
isSelected && 'selected',
|
isSelected && 'selected',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<TreeIndentGuide treeId={treeId} depth={depth} parentId={node.parent?.item.id ?? null} />
|
<TreeIndentGuide treeId={treeId} depth={depth} ancestorIds={ancestorIds} />
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'text-text-subtle',
|
'text-text-subtle',
|
||||||
|
|||||||
@@ -51,6 +51,16 @@ export const isParentHoveredFamily = atomFamily(
|
|||||||
(a, b) => a.treeId === b.treeId && a.parentId === b.parentId,
|
(a, b) => a.treeId === b.treeId && a.parentId === b.parentId,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const isAncestorHoveredFamily = atomFamily(
|
||||||
|
({ treeId, ancestorIds }: { treeId: string; ancestorIds: string[] }) =>
|
||||||
|
selectAtom(
|
||||||
|
hoveredParentFamily(treeId),
|
||||||
|
(v) => v.parentId && ancestorIds.includes(v.parentId),
|
||||||
|
Object.is,
|
||||||
|
),
|
||||||
|
(a, b) => a.treeId === b.treeId && a.ancestorIds.join(',') === b.ancestorIds.join(','),
|
||||||
|
);
|
||||||
|
|
||||||
export const isIndexHoveredFamily = atomFamily(
|
export const isIndexHoveredFamily = atomFamily(
|
||||||
({ treeId, index }: { treeId: string; index: number }) =>
|
({ treeId, index }: { treeId: string; index: number }) =>
|
||||||
selectAtom(hoveredParentFamily(treeId), (v) => v.index === index, Object.is),
|
selectAtom(hoveredParentFamily(treeId), (v) => v.index === index, Object.is),
|
||||||
|
|||||||
@@ -173,7 +173,8 @@ function handleKeyDown(e: KeyboardEvent) {
|
|||||||
if (e.metaKey) currentKeysWithModifiers.add('Meta');
|
if (e.metaKey) currentKeysWithModifiers.add('Meta');
|
||||||
if (e.shiftKey) currentKeysWithModifiers.add('Shift');
|
if (e.shiftKey) currentKeysWithModifiers.add('Shift');
|
||||||
|
|
||||||
outer: for (const [hkAction, hkKeys] of Object.entries(hotkeys) as [HotkeyAction, string[]][]) {
|
const executed: string[] = [];
|
||||||
|
outer: for (const { action, callback, options } of jotaiStore.get(sortedCallbacksAtom)) {
|
||||||
if (
|
if (
|
||||||
(e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) &&
|
(e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) &&
|
||||||
currentKeysWithModifiers.size === 1 &&
|
currentKeysWithModifiers.size === 1 &&
|
||||||
@@ -184,8 +185,7 @@ function handleKeyDown(e: KeyboardEvent) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const executed: string[] = [];
|
for (const [hkAction, hkKeys] of Object.entries(hotkeys) as [HotkeyAction, string[]][]) {
|
||||||
for (const { action, callback, options } of jotaiStore.get(sortedCallbacksAtom)) {
|
|
||||||
if (hkAction !== action) {
|
if (hkAction !== action) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -207,12 +207,12 @@ function handleKeyDown(e: KeyboardEvent) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (executed.length > 0) {
|
|
||||||
console.log('Executed hotkey', executed.join(', '));
|
|
||||||
jotaiStore.set(currentKeysAtom, new Set([]));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (executed.length > 0) {
|
||||||
|
console.log('Executed hotkey', executed.join(', '));
|
||||||
|
jotaiStore.set(currentKeysAtom, new Set([]));
|
||||||
|
}
|
||||||
clearCurrentKeysDebounced();
|
clearCurrentKeysDebounced();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -272,7 +272,13 @@ const resolveHotkeyKey = (key: string) => {
|
|||||||
|
|
||||||
function compareKeys(keysA: string[], keysB: string[]) {
|
function compareKeys(keysA: string[], keysB: string[]) {
|
||||||
if (keysA.length !== keysB.length) return false;
|
if (keysA.length !== keysB.length) return false;
|
||||||
const sortedA = keysA.map((k) => k.toLowerCase()).sort().join('::');
|
const sortedA = keysA
|
||||||
const sortedB = keysB.map((k) => k.toLowerCase()).sort().join('::');
|
.map((k) => k.toLowerCase())
|
||||||
|
.sort()
|
||||||
|
.join('::');
|
||||||
|
const sortedB = keysB
|
||||||
|
.map((k) => k.toLowerCase())
|
||||||
|
.sort()
|
||||||
|
.join('::');
|
||||||
return sortedA === sortedB;
|
return sortedA === sortedB;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user