import { ChevronDownIcon, ChevronRightIcon } from "@heroicons/react/outline"; import { useEffect, useRef } from "react"; import { useJsonColumnViewAPI, useJsonColumnViewState, } from "~/hooks/useJsonColumnView"; import { useJsonDoc } from "~/hooks/useJsonDoc"; import { JsonTreeViewNode, useJsonTreeViewContext } from "~/hooks/useJsonTree"; import { VirtualNode } from "~/hooks/useVirtualTree"; import { CopySelectedNodeShortcut } from "./CopySelectedNode"; import { Body } from "./Primitives/Body"; import { Mono } from "./Primitives/Mono"; export function JsonTreeView() { const { selectedNodeId, selectedNodeSource } = useJsonColumnViewState(); const { goToNodeId } = useJsonColumnViewAPI(); const { tree, parentRef } = useJsonTreeViewContext(); // Scroll to the selected node when this component is first rendered. const scrolledToNodeRef = useRef(false); useEffect(() => { if (!scrolledToNodeRef.current && selectedNodeId) { tree.scrollToNode(selectedNodeId); scrolledToNodeRef.current = true; } }, [selectedNodeId, scrolledToNodeRef]); // Yup, this is hacky. // This is to prevent the selection not changing the first time you try to move to a new node in the tree const focusCount = useRef(0); // This focuses and scrolls to the selected node when the selectedNodeId // is set from a source other than this tree (e.g. the search bar, path bar, related values). useEffect(() => { if ( tree.focusedNodeId && selectedNodeId && tree.focusedNodeId !== selectedNodeId ) { if (selectedNodeId === "$") { return; } if (selectedNodeSource !== "tree" && focusCount.current > 0) { focusCount.current = focusCount.current + 1; tree.focusNode(selectedNodeId); tree.scrollToNode(selectedNodeId); } } }, [tree.focusedNodeId, goToNodeId, selectedNodeId, selectedNodeSource]); // This is what syncs the tree view's focused node to the column view selected node const previousFocusedNodeId = useRef(null); useEffect(() => { let updated = false; if (!previousFocusedNodeId.current) { previousFocusedNodeId.current = tree.focusedNodeId; updated = true; } if ( tree.focusedNodeId && (updated || previousFocusedNodeId.current !== tree.focusedNodeId) ) { previousFocusedNodeId.current = tree.focusedNodeId; goToNodeId(tree.focusedNodeId, "tree"); } }, [previousFocusedNodeId, tree.focusedNodeId, tree.focusNode, goToNodeId]); const treeRef = useRef(null); useEffect(() => { if (treeRef.current) { treeRef.current.focus({ preventScroll: true }); } }, [treeRef.current]); const { minimal } = useJsonDoc(); return ( <>
{tree.nodes.map((virtualNode) => ( tree.toggleNode(node.id, e)} selectedNodeId={selectedNodeId} /> ))}
); } function TreeViewNode({ virtualNode, onToggle, selectedNodeId, }: { virtualNode: VirtualNode; selectedNodeId?: string; onToggle?: (node: JsonTreeViewNode, e: MouseEvent) => void; }) { const { node, virtualItem, depth } = virtualNode; const indentClassName = computeTreeNodePaddingClass(depth); const isSelected = selectedNodeId === node.id; return (
{node.children && node.children.length > 0 && ( { if (onToggle) { e.preventDefault(); onToggle(node, e.nativeEvent); } }} > {virtualNode.isCollapsed ? ( ) : ( )} )} {node.longTitle ?? node.name}
{node.icon && ( )} {node.subtitle && ( {node.subtitle} )}
); } function computeTreeNodePaddingClass(depth: number) { switch (depth) { case 0: return "ml-[4px] border-l border-slate-400/70"; case 1: return "ml-[calc(12px_+_4px)] border-l border-pink-400/70"; case 2: return "ml-[calc(12px_*_2_+_4px)] border-l border-blue-400/70"; case 3: return "ml-[calc(12px_*_3_+_4px)] border-l border-orange-400/70"; case 4: return "ml-[calc(12px_*_4_+_4px)] border-l border-emerald-400/70"; case 5: return "ml-[calc(12px_*_5_+_4px)] border-l border-pink-400/70"; case 6: return "ml-[calc(12px_*_6_+_4px)] border-l border-blue-400/70"; case 7: return "ml-[calc(12px_*_7_+_4px)] border-l border-orange-400/70"; case 8: return "ml-[calc(12px_*_8_+_4px)] border-l border-emerald-400/70"; case 9: return "ml-[calc(12px_*_9_+_4px)] border-l border-pink-400/70"; case 10: return "ml-[calc(12px_*_10_+_4px)] border-l border-orange-400/70"; default: return "ml-[calc(12px_*_11_+_4px)] border-l border-slate-400/70"; } }