Skip to content

Commit 9cf6a42

Browse files
committed
feat: use react-window as a base for two tree components
1 parent 2515143 commit 9cf6a42

File tree

10 files changed

+496
-834
lines changed

10 files changed

+496
-834
lines changed

src/FixedSizeTree.tsx

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
import * as React from 'react';
2+
import {Align, FixedSizeList, FixedSizeListProps} from 'react-window';
3+
import {
4+
CommonNodeComponentProps,
5+
CommonNodeMetadata,
6+
CommonNodeRecord,
7+
CommonUpdateOptions,
8+
Row,
9+
TreeProps,
10+
TreeState,
11+
} from './utils';
12+
13+
export type FixedSizeUpdateOptions = CommonUpdateOptions;
14+
export type FixedSizeNodeMetadata = CommonNodeMetadata;
15+
export type FixedSizeNodeComponentProps = CommonNodeComponentProps;
16+
export type FixedSizeNodeRecord = CommonNodeRecord;
17+
18+
export interface FixedSizeTreeProps
19+
extends TreeProps<FixedSizeNodeMetadata>,
20+
Omit<FixedSizeListProps, 'children' | 'itemCount'> {
21+
readonly children: React.ComponentType<FixedSizeNodeComponentProps>;
22+
}
23+
24+
export type FixedSizeTreeState = TreeState<FixedSizeNodeRecord>;
25+
26+
export default class FixedSizeTree extends React.PureComponent<
27+
FixedSizeTreeProps,
28+
FixedSizeTreeState
29+
> {
30+
public static defaultProps: Partial<FixedSizeTreeProps> = {
31+
rowComponent: Row,
32+
};
33+
34+
public static getDerivedStateFromProps({
35+
children: component,
36+
itemData: data,
37+
}: FixedSizeTreeProps): Partial<FixedSizeTreeState> {
38+
return {
39+
component,
40+
data,
41+
};
42+
}
43+
44+
private readonly list: React.RefObject<FixedSizeList> = React.createRef();
45+
46+
public constructor(props: FixedSizeTreeProps, context: any) {
47+
super(props, context);
48+
49+
this.createNodeRecord = this.createNodeRecord.bind(this);
50+
51+
const initialState: FixedSizeTreeState = {
52+
component: props.children,
53+
order: [],
54+
records: {},
55+
};
56+
57+
this.state = {
58+
...initialState,
59+
...this.computeTree({refreshNodes: true}, props, initialState),
60+
};
61+
}
62+
63+
public async recomputeTree(options: FixedSizeUpdateOptions): Promise<void> {
64+
return new Promise(resolve => {
65+
this.setState<never>(
66+
prevState => this.computeTree(options, this.props, prevState),
67+
resolve,
68+
);
69+
});
70+
}
71+
72+
public scrollTo(scrollOffset: number): void {
73+
if (this.list.current) {
74+
this.list.current.scrollTo(scrollOffset);
75+
}
76+
}
77+
78+
public scrollToItem(id: string | symbol, align?: Align): void {
79+
if (this.list.current) {
80+
this.list.current.scrollToItem(this.state.order.indexOf(id) || 0, align);
81+
}
82+
}
83+
84+
public render(): React.ReactNode {
85+
const {children, treeWalker, rowComponent, ...rest} = this.props;
86+
87+
return (
88+
<FixedSizeList
89+
{...rest}
90+
itemData={this.state}
91+
itemCount={this.state.order.length}
92+
ref={this.list}
93+
>
94+
{rowComponent!}
95+
</FixedSizeList>
96+
);
97+
}
98+
99+
private computeTree(
100+
{refreshNodes = false, useDefaultOpenness = false}: FixedSizeUpdateOptions,
101+
{treeWalker}: FixedSizeTreeProps,
102+
{records: prevRecords}: FixedSizeTreeState,
103+
): Pick<FixedSizeTreeState, 'order' | 'records'> {
104+
const order: Array<string | symbol> = [];
105+
const records = {...prevRecords};
106+
const iter = treeWalker(refreshNodes);
107+
108+
let isPreviousOpened = false;
109+
110+
while (true) {
111+
const {done, value} = iter.next(isPreviousOpened);
112+
113+
if (done || !value) {
114+
break;
115+
}
116+
117+
let id: string | symbol;
118+
119+
if (typeof value === 'string' || typeof value === 'symbol') {
120+
id = value;
121+
122+
if (useDefaultOpenness) {
123+
records[id as string].isOpen =
124+
records[id as string].metadata.isOpenByDefault;
125+
}
126+
} else {
127+
({id} = value);
128+
const {isOpenByDefault} = value;
129+
const record = records[id as string];
130+
131+
if (!record) {
132+
records[id as string] = this.createNodeRecord(value);
133+
} else {
134+
record.metadata = value;
135+
136+
if (useDefaultOpenness) {
137+
record.isOpen = isOpenByDefault;
138+
}
139+
}
140+
}
141+
142+
order.push(id);
143+
isPreviousOpened = records[id as string].isOpen;
144+
}
145+
146+
return {
147+
order,
148+
records,
149+
};
150+
}
151+
152+
private createNodeRecord(
153+
metadata: FixedSizeNodeMetadata,
154+
): FixedSizeNodeRecord {
155+
const record: FixedSizeNodeRecord = {
156+
isOpen: metadata.isOpenByDefault,
157+
metadata,
158+
toggle: async () => {
159+
record.isOpen = !record.isOpen;
160+
await this.recomputeTree({refreshNodes: record.isOpen});
161+
},
162+
};
163+
164+
return record;
165+
}
166+
}

0 commit comments

Comments
 (0)