Skip to content

Commit a9f41f5

Browse files
add hierarcy collapse & expand support
1 parent e21b8b5 commit a9f41f5

5 files changed

Lines changed: 94 additions & 141 deletions

File tree

Lines changed: 47 additions & 122 deletions
Original file line numberDiff line numberDiff line change
@@ -1,145 +1,34 @@
1-
export interface ITreeNode {
1+
import { ReflectSceneNode } from "@design-sdk/figma";
2+
import { visit } from "tree-visit";
3+
4+
export interface ITreeNode<T = any> {
25
id: string;
36
name: string;
47
children?: ITreeNode[];
8+
data?: T;
59
}
610

7-
export interface FlattenedNode {
11+
export interface FlattenedDisplayItemNode<T = any> {
812
id: string;
913
name: string;
1014
depth: number;
1115
parent: string;
12-
}
13-
14-
export class StatefulHierarchyController {
15-
readonly page: ITreeNode[];
16-
public roots: FlattenedNode[][];
17-
18-
private _displaymodes: {
19-
[key: string]: "expanded" | "hidden" | "collapsed" | "normal";
20-
} = {};
21-
22-
public get displaymodes() {
23-
return this._displaymodes;
24-
}
25-
26-
constructor({ page }: { page: ITreeNode[] }) {
27-
this.page = page;
28-
this.flatten();
29-
this.roots.forEach((root) => {
30-
root.forEach((l) => {
31-
this._displaymodes[l.id] = "normal";
32-
});
33-
});
34-
}
35-
36-
collapse(target: string, command: "collapse" | "expand") {
37-
console.log("collapse", target, command);
38-
switch (command) {
39-
case "collapse":
40-
this._displaymodes[target] = "collapsed";
41-
break;
42-
case "expand":
43-
this._displaymodes[target] = "expanded";
44-
}
45-
}
46-
47-
get displayLayers() {
48-
const displayLayers: FlattenedNode[] = [];
49-
this.roots?.forEach((l) => {
50-
l.forEach((layer) => {
51-
if (this._displaymodes[layer.id] !== "hidden") {
52-
displayLayers.push(layer);
53-
}
54-
});
55-
});
56-
return displayLayers;
57-
}
58-
59-
/**
60-
* returns the display mode.
61-
*
62-
* first, find the target node with find().
63-
*
64-
* find the list of parents that has the target node as a direct child or as a nested child.
65-
* iterate through the target's parents, if any of them is collapsed, return hidden.
66-
* if the target itself is collapsed, return collapsed.
67-
* if the target contains children, and not collapsed, return expanded. to check if the target contains a children, use haschildren()
68-
* if none of the above, return normal.
69-
* @param target
70-
*/
71-
displayMode(target: string): "expanded" | "hidden" | "collapsed" | "normal" {
72-
const node = this.find(target);
73-
if (!node) {
74-
// this can't happen
75-
return "normal";
76-
}
77-
const parents = this.findParents(target);
78-
for (const parent of parents) {
79-
if (this._displaymodes[parent.id] === "collapsed") {
80-
return "hidden";
81-
}
82-
}
83-
if (this._displaymodes[target] === "collapsed") {
84-
return "collapsed";
85-
}
86-
if (this.hasChildren(target)) {
87-
return "expanded";
88-
}
89-
return "normal";
90-
}
91-
92-
find(target: string): FlattenedNode | undefined {
93-
const flattened = this.flatten();
94-
for (const layer of flattened) {
95-
for (const node of layer) {
96-
if (node.id === target) {
97-
return node;
98-
}
99-
}
100-
}
101-
}
102-
103-
findParents(target: string): FlattenedNode[] {
104-
const flattened = this.flatten();
105-
const _this = this.find(target);
106-
const parents: FlattenedNode[] = [];
107-
for (const layer of flattened) {
108-
for (const node of layer) {
109-
if (node.id === _this.parent) {
110-
parents.push(node);
111-
}
112-
}
113-
}
114-
return parents;
115-
}
116-
117-
hasChildren(target: string): boolean {
118-
const layers = this.flatten();
119-
return layers.some((l) => l.some((layer) => layer.parent === target));
120-
}
121-
122-
flatten(): FlattenedNode[][] {
123-
if (!this.roots) {
124-
this.roots = this.page.filter((l) => !!l).map((layer) => flatten(layer));
125-
return this.roots;
126-
} else {
127-
return this.roots;
128-
}
129-
}
16+
expanded?: boolean | undefined;
17+
selected?: boolean;
18+
data?: T;
13019
}
13120

13221
export const flatten = <T extends ITreeNode>(
13322
tree: T,
13423
parent?: string,
13524
depth: number = 0
136-
): FlattenedNode[] => {
25+
): FlattenedDisplayItemNode[] => {
13726
const convert = (node: T, depth: number, parent?: string) => {
13827
if (!node) {
13928
return;
14029
}
14130

142-
const result: FlattenedNode = {
31+
const result: FlattenedDisplayItemNode = {
14332
...node,
14433
depth: depth,
14534
parent,
@@ -155,3 +44,39 @@ export const flatten = <T extends ITreeNode>(
15544
}
15645
return final;
15746
};
47+
48+
export function flattenNodeTree(
49+
root: ReflectSceneNode,
50+
selections: string[],
51+
expands: string[]
52+
): FlattenedDisplayItemNode<ReflectSceneNode>[] {
53+
const flattened: FlattenedDisplayItemNode<ReflectSceneNode>[] = [];
54+
55+
visit<ReflectSceneNode>(root, {
56+
getChildren: (layer) => {
57+
if (expands.includes(layer.id)) {
58+
return layer.children;
59+
}
60+
return [];
61+
},
62+
63+
onEnter(layer, indexPath) {
64+
flattened.push({
65+
id: layer.id,
66+
name: layer.name,
67+
parent: layer.parent?.id,
68+
depth: indexPath.length - 1,
69+
expanded:
70+
layer.children.length <= 0
71+
? undefined
72+
: expands.includes(layer.id)
73+
? true
74+
: false,
75+
selected: selections.includes(layer.id),
76+
data: layer,
77+
});
78+
},
79+
});
80+
81+
return flattened;
82+
}

editor/components/editor/editor-layer-hierarchy/editor-layer-hierarchy-item.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,6 @@ export const LayerRow = memo(
9494
name,
9595
selected,
9696
onHoverChange,
97-
onAddClick,
9897
onMenuClick,
9998
onClickChevron,
10099
onPress,
@@ -130,6 +129,7 @@ export const LayerRow = memo(
130129
disabled={false}
131130
onPress={onPress}
132131
onClick={onClick}
132+
onClickChevron={onClickChevron}
133133
onDoubleClick={onDoubleClick}
134134
{...props}
135135
>

editor/components/editor/editor-layer-hierarchy/editor-layer-hierarchy-tree.tsx

Lines changed: 39 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,39 +8,61 @@ import {
88
} from "./editor-layer-hierarchy-item";
99
import { useEditorState } from "core/states";
1010
import { useDispatch } from "core/dispatch";
11-
import { flatten, FlattenedNode } from "./editor-layer-heriarchy-controller";
11+
import {
12+
flattenNodeTree,
13+
FlattenedDisplayItemNode,
14+
} from "./editor-layer-heriarchy-controller";
1215

1316
export function EditorLayerHierarchy() {
1417
const [state] = useEditorState();
1518
const dispatch = useDispatch();
19+
20+
const [expands, setExpands] = useState<string[]>(state?.selectedNodes ?? []);
21+
1622
const root = state.selectedPage
1723
? state.design.pages.find((p) => p.id == state.selectedPage).children
1824
: [state.design?.input?.entry];
1925

20-
const layers: FlattenedNode[][] = useMemo(() => {
21-
return root ? root.filter((l) => !!l).map((layer) => flatten(layer)) : [];
22-
}, [root]);
26+
const layers: FlattenedDisplayItemNode[][] = useMemo(() => {
27+
return root
28+
? root
29+
.filter((l) => !!l)
30+
.map((layer) => flattenNodeTree(layer, state.selectedNodes, expands))
31+
: [];
32+
}, [root, state?.selectedNodes, expands]);
2333

2434
const renderItem = useCallback(
25-
({ id, name, depth, type, origin }) => {
26-
const selected = state?.selectedNodes?.includes(id);
35+
({
36+
id,
37+
name,
38+
expanded,
39+
selected,
40+
depth,
41+
data,
42+
}: FlattenedDisplayItemNode) => {
2743
// const _haschildren = useMemo(() => haschildren(id), [id, depth]);
2844
// const _haschildren = haschildren(id);
2945

3046
return (
3147
<LayerRow
3248
icon={
3349
<IconContainer>
34-
<LayerIcon type={origin} selected={selected} />
50+
<LayerIcon type={data.origin} selected={selected} />
3551
</IconContainer>
3652
}
3753
name={name}
38-
depth={depth}
54+
depth={depth + 1} // because the root is not a layer. it's the page, the array of roots.
3955
id={id}
40-
// expanded={_haschildren == true ? true : undefined}
56+
expanded={expanded}
4157
key={id}
4258
selected={selected}
43-
onAddClick={() => {}}
59+
onClickChevron={() => {
60+
if (expands.includes(id)) {
61+
setExpands(expands.filter((e) => e !== id));
62+
} else {
63+
setExpands([...expands, id]);
64+
}
65+
}}
4466
onMenuClick={() => {}}
4567
onDoubleClick={() => {}}
4668
onPress={() => {
@@ -51,15 +73,15 @@ export function EditorLayerHierarchy() {
5173
/>
5274
);
5375
},
54-
[dispatch, state?.selectedNodes, layers]
76+
[dispatch, state?.selectedNodes, layers, expands]
5577
);
5678

57-
const haschildren = useCallback(
58-
(id: string) => {
59-
return layers.some((l) => l.some((layer) => layer.parent === id));
60-
},
61-
[layers]
62-
);
79+
// const haschildren = useCallback(
80+
// (id: string) => {
81+
// return layers.some((l) => l.some((layer) => layer.parent === id));
82+
// },
83+
// [layers]
84+
// );
6385

6486
return (
6587
<TreeView.Root

editor/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,8 @@
5252
"recoil": "^0.2.0",
5353
"rxdb": "^10.5.4",
5454
"rxjs": "^7.4.0",
55-
"styled-components": "^5.3.3"
55+
"styled-components": "^5.3.3",
56+
"tree-visit": "^0.1.0"
5657
},
5758
"devDependencies": {
5859
"@babel/core": "^7.14.0",

yarn.lock

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13418,6 +13418,11 @@ traverse@0.6.6:
1341813418
resolved "https://registry.yarnpkg.com/traverse/-/traverse-0.6.6.tgz#cbdf560fd7b9af632502fed40f918c157ea97137"
1341913419
integrity sha1-y99WD9e5r2MlAv7UD5GMFX6pcTc=
1342013420

13421+
tree-visit@^0.1.0:
13422+
version "0.1.0"
13423+
resolved "https://registry.yarnpkg.com/tree-visit/-/tree-visit-0.1.0.tgz#ec35cfc13548b41ca5eabd5cbd7f83b574a8ed83"
13424+
integrity sha512-5lARH0BoZY70TwE+MV7v0KRtApSP4LSxC2JLRCUyK6KUCb8f/Qv9dS+PaWx9h3yKyNYjX2/JHwDx8DyQHHFzlw==
13425+
1342113426
ts-jest@^27.0.2, ts-jest@^27.0.3, ts-jest@^27.0.5:
1342213427
version "27.1.1"
1342313428
resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-27.1.1.tgz#5a54aca96db1dac37c681f3029dd10f3a8c36192"

0 commit comments

Comments
 (0)