diff --git a/app/components/JsonTreeView.tsx b/app/components/JsonTreeView.tsx index f787756..eae4abb 100644 --- a/app/components/JsonTreeView.tsx +++ b/app/components/JsonTreeView.tsx @@ -81,7 +81,7 @@ export function JsonTreeView() { tree.toggleNode(virtualNode.node.id)} + onToggle={(node, e) => tree.toggleNode(node.id, e)} selectedNodeId={selectedNodeId} /> ))} @@ -97,7 +97,7 @@ function TreeViewNode({ }: { virtualNode: VirtualNode; selectedNodeId?: string; - onToggle?: (node: JsonTreeViewNode) => void; + onToggle?: (node: JsonTreeViewNode, e: MouseEvent) => void; }) { const { tree } = useJsonTreeViewContext(); @@ -145,7 +145,7 @@ function TreeViewNode({ onClick={(e) => { if (onToggle) { e.preventDefault(); - onToggle(node); + onToggle(node, e.nativeEvent); } }} > diff --git a/app/hooks/useVirtualTree.ts b/app/hooks/useVirtualTree.ts index 40ae6f7..bd28197 100644 --- a/app/hooks/useVirtualTree.ts +++ b/app/hooks/useVirtualTree.ts @@ -34,7 +34,7 @@ export type UseVirtualTreeInstance = { nodes: VirtualNode[]; focusedNodeId: string | null; totalSize: number; - toggleNode: (id: string) => void; + toggleNode: (id: string, source?: KeyboardEvent | MouseEvent) => void; focusNode: (id: string) => void; focusFirst: () => void; scrollToNode: (id: string) => void; @@ -60,6 +60,7 @@ type TreeState = { type ToggleNodeAction = { type: "TOGGLE_NODE"; id: string; + source?: KeyboardEvent | MouseEvent; }; type FocusNodeAction = { @@ -138,6 +139,56 @@ function collapseNode( focusedNodeId: id, }; } +function toggleAllChildren( + state: TreeState, + id: string +): TreeState { + const item = state.items.find(({ id: nodeId }) => nodeId === id); + + if (!item) { + return state; + } + + if (!item.node.children || item.node.children.length === 0) { + return state; + } + + const allCollapsed = item.node.children.every( + (child) => state.collapsedState[child.id] + ); + + if (allCollapsed) { + const collapsedState = item.node.children.reduce( + (acc, child) => ({ + ...acc, + [child.id]: false, + }), + state.collapsedState + ); + + return { + ...state, + collapsedState, + items: createNodeItems(state.nodes, 0, collapsedState), + focusedNodeId: id, + }; + } + + const collapsedState = item.node.children.reduce( + (acc, child) => ({ + ...acc, + [child.id]: true, + }), + state.collapsedState + ); + + return { + ...state, + collapsedState, + items: createNodeItems(state.nodes, 0, collapsedState), + focusedNodeId: id, + }; +} export function useVirtualTree( options: UseVirtualTreeOptions @@ -151,7 +202,14 @@ export function useVirtualTree( if (isCollapsed) { return expandNode(state, action.id); } else { - return collapseNode(state, action.id); + if ( + action.source && + (action.source.shiftKey || action.source.altKey) + ) { + return toggleAllChildren(state, action.id); + } else { + return collapseNode(state, action.id); + } } } case "FOCUS_NODE": { @@ -229,6 +287,12 @@ export function useVirtualTree( if (action.isCollapsed) { return expandNode(state, action.id); } + if ( + action.source && + (action.source.shiftKey || action.source.altKey) + ) { + return toggleAllChildren(state, action.id); + } const nodeIndex = state.items.findIndex( (item) => item.id === action.id @@ -251,7 +315,14 @@ export function useVirtualTree( } case "MOVE_LEFT": { if (action.hasChildren && !action.isCollapsed) { - return collapseNode(state, action.id); + if ( + action.source && + (action.source.shiftKey || action.source.altKey) + ) { + return toggleAllChildren(state, action.id); + } else { + return collapseNode(state, action.id); + } } if (!action.hasChildren || action.isCollapsed) { @@ -392,8 +463,8 @@ export function useVirtualTree( }); const toggleNode = useCallback( - (id: string) => { - dispatch({ type: "TOGGLE_NODE", id }); + (id: string, source?: KeyboardEvent | MouseEvent) => { + dispatch({ type: "TOGGLE_NODE", id, source }); }, [dispatch] );