Added indent guides to parent and children for selected node
Reviewed By: lblasa Differential Revision: D41995460 fbshipit-source-id: cd4574caa6aa164d2b3a026f656609585cae65c0
This commit is contained in:
committed by
Facebook GitHub Bot
parent
5043e5292f
commit
b686567e2b
@@ -21,13 +21,27 @@ import {
|
|||||||
} from 'flipper-plugin';
|
} from 'flipper-plugin';
|
||||||
import {plugin} from '../index';
|
import {plugin} from '../index';
|
||||||
import {Glyph} from 'flipper';
|
import {Glyph} from 'flipper';
|
||||||
import {head} from 'lodash';
|
import {head, last} from 'lodash';
|
||||||
import {reverse} from 'lodash/fp';
|
import {reverse} from 'lodash/fp';
|
||||||
import {Dropdown, Menu, Typography} from 'antd';
|
import {Dropdown, Menu, Typography} from 'antd';
|
||||||
import {UIDebuggerMenuItem} from './util/UIDebuggerMenuItem';
|
import {UIDebuggerMenuItem} from './util/UIDebuggerMenuItem';
|
||||||
|
|
||||||
const {Text} = Typography;
|
const {Text} = Typography;
|
||||||
|
|
||||||
|
type LineStyle = 'ToParent' | 'ToChildren';
|
||||||
|
|
||||||
|
type NodeIndentGuide = {
|
||||||
|
depth: number;
|
||||||
|
style: LineStyle;
|
||||||
|
addHorizontalMarker: boolean;
|
||||||
|
trimBottom: boolean;
|
||||||
|
};
|
||||||
|
export type TreeNode = UINode & {
|
||||||
|
depth: number;
|
||||||
|
isExpanded: boolean;
|
||||||
|
indentGuide: NodeIndentGuide | null;
|
||||||
|
};
|
||||||
|
|
||||||
export function Tree2({
|
export function Tree2({
|
||||||
nodes,
|
nodes,
|
||||||
rootId,
|
rootId,
|
||||||
@@ -47,14 +61,19 @@ export function Tree2({
|
|||||||
const hoveredNode = head(useValue(instance.uiState.hoveredNodes));
|
const hoveredNode = head(useValue(instance.uiState.hoveredNodes));
|
||||||
|
|
||||||
const {treeNodes, refs} = useMemo(() => {
|
const {treeNodes, refs} = useMemo(() => {
|
||||||
const treeNodes = toTreeList(nodes, focusedNode || rootId, expandedNodes);
|
const treeNodes = toTreeNodes(
|
||||||
|
nodes,
|
||||||
|
focusedNode || rootId,
|
||||||
|
expandedNodes,
|
||||||
|
selectedNode,
|
||||||
|
);
|
||||||
|
|
||||||
const refs: React.RefObject<HTMLLIElement>[] = treeNodes.map(() =>
|
const refs: React.RefObject<HTMLLIElement>[] = treeNodes.map(() =>
|
||||||
React.createRef<HTMLLIElement>(),
|
React.createRef<HTMLLIElement>(),
|
||||||
);
|
);
|
||||||
|
|
||||||
return {treeNodes, refs};
|
return {treeNodes, refs};
|
||||||
}, [expandedNodes, focusedNode, nodes, rootId]);
|
}, [expandedNodes, focusedNode, nodes, rootId, selectedNode]);
|
||||||
|
|
||||||
const isUsingKBToScroll = useRef(false);
|
const isUsingKBToScroll = useRef(false);
|
||||||
|
|
||||||
@@ -116,11 +135,6 @@ export function Tree2({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TreeNode = UINode & {
|
|
||||||
depth: number;
|
|
||||||
isExpanded: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
const MemoTreeItemContainer = React.memo(
|
const MemoTreeItemContainer = React.memo(
|
||||||
TreeItemContainer,
|
TreeItemContainer,
|
||||||
(prevProps, nextProps) => {
|
(prevProps, nextProps) => {
|
||||||
@@ -137,6 +151,41 @@ const MemoTreeItemContainer = React.memo(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
function IndentGuide({indentGuide}: {indentGuide: NodeIndentGuide}) {
|
||||||
|
const verticalLinePadding = `${renderDepthOffset * indentGuide.depth + 8}px`;
|
||||||
|
|
||||||
|
const verticalLineStyle = `${
|
||||||
|
indentGuide.style === 'ToParent' ? 'dashed' : 'solid'
|
||||||
|
}`;
|
||||||
|
const horizontalLineStyle = `${
|
||||||
|
indentGuide.style === 'ToParent' ? 'dotted' : 'solid'
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const color = indentGuide.style === 'ToParent' ? '#B0B0B0' : '#C0C0C0';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
width: verticalLinePadding,
|
||||||
|
height: indentGuide.trimBottom ? HalfTreeItemHeight : TreeItemHeight,
|
||||||
|
borderRight: `1px ${verticalLineStyle} ${color}`,
|
||||||
|
}}></div>
|
||||||
|
{indentGuide.addHorizontalMarker && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
width: 8,
|
||||||
|
height: HalfTreeItemHeight,
|
||||||
|
borderBottom: `2px ${horizontalLineStyle} ${color}`,
|
||||||
|
marginLeft: verticalLinePadding,
|
||||||
|
}}></div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function TreeItemContainer({
|
function TreeItemContainer({
|
||||||
innerRef,
|
innerRef,
|
||||||
treeNode,
|
treeNode,
|
||||||
@@ -161,12 +210,19 @@ function TreeItemContainer({
|
|||||||
onHoverNode: (node: Id) => void;
|
onHoverNode: (node: Id) => void;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<TreeItem
|
<div>
|
||||||
|
{treeNode.indentGuide != null && (
|
||||||
|
<IndentGuide indentGuide={treeNode.indentGuide} />
|
||||||
|
)}
|
||||||
|
<TreeItemContent
|
||||||
ref={innerRef}
|
ref={innerRef}
|
||||||
isSelected={treeNode.id === selectedNode}
|
isSelected={treeNode.id === selectedNode}
|
||||||
isHovered={hoveredNode === treeNode.id}
|
isHovered={hoveredNode === treeNode.id}
|
||||||
onMouseEnter={() => {
|
onMouseEnter={() => {
|
||||||
if (isUsingKBToScroll.current === false && isContextMenuOpen == false) {
|
if (
|
||||||
|
isUsingKBToScroll.current === false &&
|
||||||
|
isContextMenuOpen == false
|
||||||
|
) {
|
||||||
onHoverNode(treeNode.id);
|
onHoverNode(treeNode.id);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
@@ -188,7 +244,8 @@ function TreeItemContainer({
|
|||||||
{nodeIcon(treeNode)}
|
{nodeIcon(treeNode)}
|
||||||
<HighlightedText text={treeNode.name} />
|
<HighlightedText text={treeNode.name} />
|
||||||
<InlineAttributes attributes={treeNode.inlineAttributes} />
|
<InlineAttributes attributes={treeNode.inlineAttributes} />
|
||||||
</TreeItem>
|
</TreeItemContent>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -216,15 +273,18 @@ function InlineAttributes({attributes}: {attributes: Record<string, string>}) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const TreeItem = styled.li<{
|
const TreeItemHeight = '26px';
|
||||||
|
const HalfTreeItemHeight = `calc(${TreeItemHeight} / 2)`;
|
||||||
|
|
||||||
|
const TreeItemContent = styled.li<{
|
||||||
item: TreeNode;
|
item: TreeNode;
|
||||||
isHovered: boolean;
|
isHovered: boolean;
|
||||||
isSelected: boolean;
|
isSelected: boolean;
|
||||||
}>(({item, isHovered, isSelected}) => ({
|
}>(({item, isHovered, isSelected}) => ({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'baseline',
|
alignItems: 'baseline',
|
||||||
height: '26px',
|
height: TreeItemHeight,
|
||||||
paddingLeft: `${(item.depth + 1) * renderDepthOffset}px`,
|
paddingLeft: `${item.depth * renderDepthOffset}px`,
|
||||||
borderWidth: '1px',
|
borderWidth: '1px',
|
||||||
borderRadius: '3px',
|
borderRadius: '3px',
|
||||||
borderColor: isHovered ? theme.selectionBackgroundColor : 'transparent',
|
borderColor: isHovered ? theme.selectionBackgroundColor : 'transparent',
|
||||||
@@ -243,7 +303,10 @@ function ExpandedIconOrSpace(props: {
|
|||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
style={{display: 'flex'}}
|
style={{display: 'flex'}}
|
||||||
onClick={props.onClick}>
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
props.onClick();
|
||||||
|
}}>
|
||||||
<Glyph
|
<Glyph
|
||||||
style={{
|
style={{
|
||||||
transform: props.expanded ? 'rotate(90deg)' : '',
|
transform: props.expanded ? 'rotate(90deg)' : '',
|
||||||
@@ -278,7 +341,7 @@ const DecorationImage = styled.img({
|
|||||||
width: 12,
|
width: 12,
|
||||||
});
|
});
|
||||||
|
|
||||||
const renderDepthOffset = 8;
|
const renderDepthOffset = 16;
|
||||||
|
|
||||||
const ContextMenu: React.FC<{
|
const ContextMenu: React.FC<{
|
||||||
nodes: Map<Id, UINode>;
|
nodes: Map<Id, UINode>;
|
||||||
@@ -328,41 +391,97 @@ const ContextMenu: React.FC<{
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
function toTreeList(
|
type TreeListStackItem = {
|
||||||
|
node: UINode;
|
||||||
|
depth: number;
|
||||||
|
isChildOfSelectedNode: boolean;
|
||||||
|
selectedNodeDepth: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
function toTreeNodes(
|
||||||
nodes: Map<Id, UINode>,
|
nodes: Map<Id, UINode>,
|
||||||
rootId: Id,
|
rootId: Id,
|
||||||
expandedNodes: Set<Id>,
|
expandedNodes: Set<Id>,
|
||||||
|
selectedNode: Id | undefined,
|
||||||
): TreeNode[] {
|
): TreeNode[] {
|
||||||
const root = nodes.get(rootId);
|
const root = nodes.get(rootId);
|
||||||
if (root == null) {
|
if (root == null) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
const stack = [[root, 0]] as [UINode, number][];
|
const stack = [
|
||||||
|
{node: root, depth: 0, isChildOfSelectedNode: false, selectedNodeDepth: 0},
|
||||||
|
] as TreeListStackItem[];
|
||||||
|
|
||||||
const res = [] as TreeNode[];
|
const treeNodes = [] as TreeNode[];
|
||||||
|
|
||||||
while (stack.length > 0) {
|
while (stack.length > 0) {
|
||||||
const [cur, depth] = stack.pop()!!;
|
const stackItem = stack.pop()!!;
|
||||||
|
|
||||||
const isExpanded = expandedNodes.has(cur.id);
|
const {node, depth} = stackItem;
|
||||||
res.push({
|
|
||||||
...cur,
|
//if the previous item has an indent guide but we don't then it was the last segment
|
||||||
|
//so we trim the bottom
|
||||||
|
const prevItemLine = last(treeNodes)?.indentGuide;
|
||||||
|
if (prevItemLine != null && stackItem.isChildOfSelectedNode === false) {
|
||||||
|
prevItemLine.trimBottom = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isExpanded = expandedNodes.has(node.id);
|
||||||
|
const isSelected = node.id === selectedNode;
|
||||||
|
|
||||||
|
treeNodes.push({
|
||||||
|
...node,
|
||||||
depth,
|
depth,
|
||||||
isExpanded,
|
isExpanded,
|
||||||
|
indentGuide: stackItem.isChildOfSelectedNode
|
||||||
|
? {
|
||||||
|
depth: stackItem.selectedNodeDepth,
|
||||||
|
style: 'ToChildren',
|
||||||
|
//if first child of selected node add horizontal marker
|
||||||
|
addHorizontalMarker: depth === stackItem.selectedNodeDepth + 1,
|
||||||
|
trimBottom: false,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let isChildOfSelectedNode = stackItem.isChildOfSelectedNode;
|
||||||
|
let selectedNodeDepth = stackItem.selectedNodeDepth;
|
||||||
|
if (isSelected) {
|
||||||
|
isChildOfSelectedNode = true;
|
||||||
|
selectedNodeDepth = depth;
|
||||||
|
// walk back through tree nodes, while depth is greater or equal than current it is your
|
||||||
|
// parents child / your previous cousin so set dashed line
|
||||||
|
for (let i = treeNodes.length - 1; i >= 0; i--) {
|
||||||
|
const prevNode = treeNodes[i];
|
||||||
|
if (prevNode.depth < depth) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
prevNode.indentGuide = {
|
||||||
|
depth: selectedNodeDepth - 1,
|
||||||
|
style: 'ToParent',
|
||||||
|
addHorizontalMarker: prevNode.depth == depth,
|
||||||
|
trimBottom: prevNode.id === selectedNode,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (isExpanded) {
|
if (isExpanded) {
|
||||||
//since we do dfs and use a stack we have to reverse children to get the order correct
|
//since we do dfs and use a stack we have to reverse children to get the order correct
|
||||||
for (const childId of reverse(cur.children)) {
|
for (const childId of reverse(node.children)) {
|
||||||
const child = nodes.get(childId);
|
const child = nodes.get(childId);
|
||||||
if (child != null) {
|
if (child != null) {
|
||||||
stack.push([child, depth + 1]);
|
stack.push({
|
||||||
|
node: child,
|
||||||
|
depth: depth + 1,
|
||||||
|
isChildOfSelectedNode: isChildOfSelectedNode,
|
||||||
|
selectedNodeDepth: selectedNodeDepth,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return res;
|
return treeNodes;
|
||||||
}
|
}
|
||||||
|
|
||||||
function useKeyboardShortcuts(
|
function useKeyboardShortcuts(
|
||||||
|
|||||||
Reference in New Issue
Block a user