mirror of
https://github.com/jeffvli/feishin.git
synced 2026-06-14 23:44:01 +02:00
add some additional folder browsing qol
- automatically scroll to current folder - fix tooltip offset on initial render
This commit is contained in:
@@ -11,6 +11,7 @@ import { useFolderListFilters } from '/@/renderer/features/folders/hooks/use-fol
|
|||||||
import { useDragDrop } from '/@/renderer/hooks/use-drag-drop';
|
import { useDragDrop } from '/@/renderer/hooks/use-drag-drop';
|
||||||
import { Icon } from '/@/shared/components/icon/icon';
|
import { Icon } from '/@/shared/components/icon/icon';
|
||||||
import { Tooltip } from '/@/shared/components/tooltip/tooltip';
|
import { Tooltip } from '/@/shared/components/tooltip/tooltip';
|
||||||
|
import { useMergedRef } from '/@/shared/hooks/use-merged-ref';
|
||||||
import { Folder, LibraryItem } from '/@/shared/types/domain-types';
|
import { Folder, LibraryItem } from '/@/shared/types/domain-types';
|
||||||
import { DragOperation, DragTarget } from '/@/shared/types/drag-and-drop';
|
import { DragOperation, DragTarget } from '/@/shared/types/drag-and-drop';
|
||||||
|
|
||||||
@@ -39,10 +40,12 @@ interface FolderTreeBrowserProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const FolderTreeBrowser = ({ fetchFolder, rootFolderQuery }: FolderTreeBrowserProps) => {
|
export const FolderTreeBrowser = ({ fetchFolder, rootFolderQuery }: FolderTreeBrowserProps) => {
|
||||||
const { currentFolderId, setFolderPath } = useFolderListFilters();
|
const { currentFolderId, folderPath, setFolderPath } = useFolderListFilters();
|
||||||
const [expandedNodes, setExpandedNodes] = useState<Set<string>>(new Set());
|
const [expandedNodes, setExpandedNodes] = useState<Set<string>>(new Set());
|
||||||
const [loadedNodes, setLoadedNodes] = useState<Map<string, Folder[]>>(new Map());
|
const [loadedNodes, setLoadedNodes] = useState<Map<string, Folder[]>>(new Map());
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const previousFolderPathRef = useRef<Array<{ id: string; name: string }>>([]);
|
||||||
|
const lastInternalFolderPathRef = useRef<Array<{ id: string; name: string }> | null>(null);
|
||||||
|
|
||||||
// Initialize root folder children when data is loaded
|
// Initialize root folder children when data is loaded
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -274,6 +277,8 @@ export const FolderTreeBrowser = ({ fetchFolder, rootFolderQuery }: FolderTreeBr
|
|||||||
// Set current folder path (full path from root to clicked folder)
|
// Set current folder path (full path from root to clicked folder)
|
||||||
// Skip the root folder (id: '0') from the path
|
// Skip the root folder (id: '0') from the path
|
||||||
const pathWithoutRoot = path.filter((item) => item.id !== '0');
|
const pathWithoutRoot = path.filter((item) => item.id !== '0');
|
||||||
|
// Mark this path as internal navigation to prevent auto-scroll
|
||||||
|
lastInternalFolderPathRef.current = pathWithoutRoot;
|
||||||
setFolderPath(pathWithoutRoot);
|
setFolderPath(pathWithoutRoot);
|
||||||
},
|
},
|
||||||
[expandNode, loadedNodes, setFolderPath, toggleNode],
|
[expandNode, loadedNodes, setFolderPath, toggleNode],
|
||||||
@@ -327,6 +332,94 @@ export const FolderTreeBrowser = ({ fetchFolder, rootFolderQuery }: FolderTreeBr
|
|||||||
return () => osInstance()?.destroy();
|
return () => osInstance()?.destroy();
|
||||||
}, [initialize, osInstance]);
|
}, [initialize, osInstance]);
|
||||||
|
|
||||||
|
// Track when we need to scroll (set by external navigation detection)
|
||||||
|
const [shouldScrollToFolder, setShouldScrollToFolder] = useState<null | string>(null);
|
||||||
|
|
||||||
|
// Handle external navigation - expand parent folders
|
||||||
|
useEffect(() => {
|
||||||
|
// Skip if folderPath hasn't actually changed
|
||||||
|
const pathChanged =
|
||||||
|
previousFolderPathRef.current.length !== folderPath.length ||
|
||||||
|
previousFolderPathRef.current.some((item, index) => item.id !== folderPath[index]?.id);
|
||||||
|
|
||||||
|
if (!pathChanged || !currentFolderId || currentFolderId === '0') {
|
||||||
|
previousFolderPathRef.current = folderPath;
|
||||||
|
setShouldScrollToFolder(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this is an internal navigation (from clicking in tree browser)
|
||||||
|
const isInternalNavigation =
|
||||||
|
lastInternalFolderPathRef.current &&
|
||||||
|
lastInternalFolderPathRef.current.length === folderPath.length &&
|
||||||
|
lastInternalFolderPathRef.current.every(
|
||||||
|
(item, index) => item.id === folderPath[index]?.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isInternalNavigation) {
|
||||||
|
// Clear the internal navigation marker
|
||||||
|
lastInternalFolderPathRef.current = null;
|
||||||
|
previousFolderPathRef.current = folderPath;
|
||||||
|
setShouldScrollToFolder(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
previousFolderPathRef.current = folderPath;
|
||||||
|
|
||||||
|
// Expand all parent folders in the path to make current folder visible
|
||||||
|
const expandPath = async () => {
|
||||||
|
const foldersToExpand = folderPath.slice(0, -1); // All except the current folder
|
||||||
|
|
||||||
|
for (const pathItem of foldersToExpand) {
|
||||||
|
if (!expandedNodes.has(pathItem.id)) {
|
||||||
|
// Fetch children if not loaded
|
||||||
|
if (!loadedNodes.has(pathItem.id)) {
|
||||||
|
await fetchFolderChildren(pathItem.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expand the folder
|
||||||
|
setExpandedNodes((prev) => {
|
||||||
|
const newSet = new Set(prev);
|
||||||
|
newSet.add(pathItem.id);
|
||||||
|
return newSet;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark that we should scroll to this folder once it appears in the tree
|
||||||
|
setShouldScrollToFolder(currentFolderId);
|
||||||
|
};
|
||||||
|
|
||||||
|
expandPath();
|
||||||
|
}, [folderPath, currentFolderId, expandedNodes, fetchFolderChildren, loadedNodes]);
|
||||||
|
|
||||||
|
// Scroll to current folder when it becomes visible in the tree
|
||||||
|
useEffect(() => {
|
||||||
|
if (!shouldScrollToFolder || !containerRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentIndex = flattenedNodes.findIndex(
|
||||||
|
(node) => node.folder.id === shouldScrollToFolder,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (currentIndex !== -1) {
|
||||||
|
const viewport = containerRef.current.firstElementChild as HTMLElement;
|
||||||
|
if (viewport) {
|
||||||
|
const viewportHeight = viewport.clientHeight;
|
||||||
|
const scrollOffset = currentIndex * ITEM_HEIGHT;
|
||||||
|
const centeredOffset = scrollOffset - viewportHeight / 2 + ITEM_HEIGHT / 2;
|
||||||
|
|
||||||
|
viewport.scrollTo({
|
||||||
|
behavior: 'auto',
|
||||||
|
top: Math.max(0, centeredOffset),
|
||||||
|
});
|
||||||
|
|
||||||
|
setShouldScrollToFolder(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [flattenedNodes, shouldScrollToFolder]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container} ref={containerRef}>
|
<div className={styles.container} ref={containerRef}>
|
||||||
<List
|
<List
|
||||||
@@ -364,33 +457,6 @@ const RowComponent = ({
|
|||||||
const rowRef = useRef<HTMLDivElement>(null);
|
const rowRef = useRef<HTMLDivElement>(null);
|
||||||
const [tooltipOffset, setTooltipOffset] = useState(0);
|
const [tooltipOffset, setTooltipOffset] = useState(0);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
|
||||||
if (!item) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const calculateOffset = () => {
|
|
||||||
if (rowRef.current && folderIconRef.current && expandIconRef.current) {
|
|
||||||
const width = rowRef.current.offsetWidth;
|
|
||||||
const paddingLeft = item.depth * INDENT_SIZE;
|
|
||||||
const folderIconWidth = folderIconRef.current.offsetWidth;
|
|
||||||
const expandIconWidth = expandIconRef.current.offsetWidth;
|
|
||||||
const itemPadding = 8;
|
|
||||||
setTooltipOffset(
|
|
||||||
-width + paddingLeft + folderIconWidth + expandIconWidth + itemPadding,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
calculateOffset();
|
|
||||||
|
|
||||||
const handleResize = () => {
|
|
||||||
calculateOffset();
|
|
||||||
};
|
|
||||||
window.addEventListener('resize', handleResize);
|
|
||||||
return () => window.removeEventListener('resize', handleResize);
|
|
||||||
}, [item]);
|
|
||||||
|
|
||||||
const { isDragging, ref: dragRef } = useDragDrop<HTMLDivElement>({
|
const { isDragging, ref: dragRef } = useDragDrop<HTMLDivElement>({
|
||||||
drag: {
|
drag: {
|
||||||
getId: () => (item ? [item.folder.id] : []),
|
getId: () => (item ? [item.folder.id] : []),
|
||||||
@@ -402,12 +468,49 @@ const RowComponent = ({
|
|||||||
isEnabled: !!item,
|
isEnabled: !!item,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Use dragRef for the element and also update rowRef for tooltip calculations
|
// Merge dragRef with rowRef
|
||||||
useEffect(() => {
|
const mergedRef = useMergedRef(rowRef, dragRef);
|
||||||
if (dragRef && 'current' in dragRef && dragRef.current) {
|
|
||||||
rowRef.current = dragRef.current;
|
const calculateOffset = useCallback(() => {
|
||||||
|
const rowElement = rowRef.current;
|
||||||
|
if (rowElement && folderIconRef.current && expandIconRef.current) {
|
||||||
|
const width = rowElement.offsetWidth;
|
||||||
|
const paddingLeft = item.depth * INDENT_SIZE;
|
||||||
|
const folderIconWidth = folderIconRef.current.offsetWidth;
|
||||||
|
const expandIconWidth = expandIconRef.current.offsetWidth;
|
||||||
|
const itemPadding = 8;
|
||||||
|
setTooltipOffset(
|
||||||
|
-width + paddingLeft + folderIconWidth + expandIconWidth + itemPadding,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}, [dragRef]);
|
}, [item.depth]);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (!item) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use requestAnimationFrame to ensure DOM is fully laid out
|
||||||
|
const rafId = requestAnimationFrame(() => {
|
||||||
|
calculateOffset();
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleResize = () => {
|
||||||
|
calculateOffset();
|
||||||
|
};
|
||||||
|
window.addEventListener('resize', handleResize);
|
||||||
|
return () => {
|
||||||
|
cancelAnimationFrame(rafId);
|
||||||
|
window.removeEventListener('resize', handleResize);
|
||||||
|
};
|
||||||
|
}, [item, calculateOffset]);
|
||||||
|
|
||||||
|
// Recalculate offset when refs become available
|
||||||
|
useEffect(() => {
|
||||||
|
if (rowRef.current && folderIconRef.current && expandIconRef.current) {
|
||||||
|
calculateOffset();
|
||||||
|
}
|
||||||
|
}, [calculateOffset]);
|
||||||
|
|
||||||
if (!item) {
|
if (!item) {
|
||||||
return <div style={style} />;
|
return <div style={style} />;
|
||||||
@@ -455,7 +558,7 @@ const RowComponent = ({
|
|||||||
})}
|
})}
|
||||||
onClick={handleRowClick}
|
onClick={handleRowClick}
|
||||||
onContextMenu={handleContextMenu}
|
onContextMenu={handleContextMenu}
|
||||||
ref={dragRef}
|
ref={mergedRef}
|
||||||
style={{
|
style={{
|
||||||
...style,
|
...style,
|
||||||
paddingLeft: `${paddingLeft}px`,
|
paddingLeft: `${paddingLeft}px`,
|
||||||
|
|||||||
Reference in New Issue
Block a user