react-arborist 0.1.12 → 0.2.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{lib/components → components}/drop-cursor.d.ts +1 -0
- package/dist/{lib/components → components}/preview.d.ts +1 -0
- package/dist/{lib/components → components}/row.d.ts +0 -0
- package/dist/{lib/components → components}/tree.d.ts +0 -0
- package/dist/{lib/context.d.ts → context.d.ts} +4 -3
- package/dist/{lib/data → data}/enrich-tree.d.ts +1 -1
- package/dist/{lib/data → data}/flatten-tree.d.ts +0 -0
- package/dist/dnd/compute-drop.d.ts +37 -0
- package/dist/{lib/dnd → dnd}/drag-hook.d.ts +0 -0
- package/dist/{lib/dnd → dnd}/drop-hook.d.ts +0 -0
- package/dist/{lib/dnd → dnd}/outer-drop-hook.d.ts +0 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +1354 -0
- package/dist/index.js.map +1 -0
- package/dist/module.js +1346 -0
- package/dist/module.js.map +1 -0
- package/dist/{lib/provider.d.ts → provider.d.ts} +1 -0
- package/dist/{lib/reducer.d.ts → reducer.d.ts} +4 -3
- package/dist/{lib/selection → selection}/range.d.ts +0 -0
- package/dist/{lib/selection → selection}/selection-hook.d.ts +0 -0
- package/dist/{lib/selection → selection}/selection.d.ts +0 -0
- package/dist/{lib/tree-api-hook.d.ts → tree-api-hook.d.ts} +0 -0
- package/dist/{lib/tree-api.d.ts → tree-api.d.ts} +4 -3
- package/dist/{lib/types.d.ts → types.d.ts} +8 -3
- package/dist/types.d.ts.map +1 -0
- package/dist/{lib/utils.d.ts → utils.d.ts} +2 -0
- package/package.json +16 -43
- package/src/components/drop-cursor.tsx +47 -0
- package/src/components/preview.tsx +108 -0
- package/src/components/row.tsx +119 -0
- package/src/components/tree.tsx +118 -0
- package/src/context.tsx +52 -0
- package/src/data/enrich-tree.ts +74 -0
- package/src/data/flatten-tree.ts +17 -0
- package/src/data/make-tree.ts +37 -0
- package/src/dnd/compute-drop.ts +184 -0
- package/src/dnd/drag-hook.ts +48 -0
- package/src/dnd/drop-hook.ts +66 -0
- package/src/dnd/measure-hover.ts +26 -0
- package/src/dnd/outer-drop-hook.ts +50 -0
- package/src/index.ts +5 -0
- package/src/provider.tsx +61 -0
- package/src/reducer.ts +161 -0
- package/src/selection/range.ts +41 -0
- package/src/selection/selection-hook.ts +24 -0
- package/src/selection/selection.test.ts +111 -0
- package/src/selection/selection.ts +186 -0
- package/src/tree-api-hook.ts +34 -0
- package/src/tree-api.ts +129 -0
- package/src/types.ts +147 -0
- package/src/utils.ts +35 -0
- package/tsconfig.json +28 -0
- package/README.md +0 -197
- package/dist/lib/components/drop-cursor.js +0 -53
- package/dist/lib/components/preview.js +0 -91
- package/dist/lib/components/row.js +0 -122
- package/dist/lib/components/tree.js +0 -76
- package/dist/lib/context.js +0 -57
- package/dist/lib/data/enrich-tree.js +0 -48
- package/dist/lib/data/flatten-tree.js +0 -20
- package/dist/lib/data/make-tree.d.ts +0 -5
- package/dist/lib/data/make-tree.js +0 -40
- package/dist/lib/data/visible-nodes-hook.d.ts +0 -2
- package/dist/lib/data/visible-nodes-hook.js +0 -19
- package/dist/lib/dnd/compute-drop.d.ts +0 -24
- package/dist/lib/dnd/compute-drop.js +0 -113
- package/dist/lib/dnd/drag-hook.js +0 -36
- package/dist/lib/dnd/drop-hook.js +0 -60
- package/dist/lib/dnd/measure-hover.d.ts +0 -8
- package/dist/lib/dnd/measure-hover.js +0 -21
- package/dist/lib/dnd/outer-drop-hook.js +0 -50
- package/dist/lib/index.d.ts +0 -3
- package/dist/lib/index.js +0 -7
- package/dist/lib/provider.js +0 -44
- package/dist/lib/reducer.js +0 -149
- package/dist/lib/selection/range.js +0 -45
- package/dist/lib/selection/selection-hook.js +0 -24
- package/dist/lib/selection/selection.js +0 -192
- package/dist/lib/selection/selection.test.d.ts +0 -1
- package/dist/lib/selection/selection.test.js +0 -102
- package/dist/lib/tree-api-hook.js +0 -26
- package/dist/lib/tree-api.js +0 -130
- package/dist/lib/tree-monitor.d.ts +0 -15
- package/dist/lib/tree-monitor.js +0 -32
- package/dist/lib/types.js +0 -2
- package/dist/lib/utils.js +0 -31
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { Range } from "./range";
|
|
2
|
+
|
|
3
|
+
type SelectionDirection = "forward" | "backward" | "none";
|
|
4
|
+
|
|
5
|
+
export type SelectionData = {
|
|
6
|
+
ranges: [number, number][];
|
|
7
|
+
currentIndex: number | null;
|
|
8
|
+
direction: SelectionDirection;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export class Selection {
|
|
12
|
+
ranges: Range[] = [];
|
|
13
|
+
currentIndex: number | null;
|
|
14
|
+
direction: SelectionDirection = "none";
|
|
15
|
+
items: any[];
|
|
16
|
+
|
|
17
|
+
static parse(data: SelectionData | null, items: any[]) {
|
|
18
|
+
if (data) {
|
|
19
|
+
return new Selection(
|
|
20
|
+
data.ranges,
|
|
21
|
+
data.currentIndex,
|
|
22
|
+
data.direction,
|
|
23
|
+
items
|
|
24
|
+
);
|
|
25
|
+
} else {
|
|
26
|
+
return new Selection();
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
constructor(
|
|
31
|
+
ranges: [number, number][] = [],
|
|
32
|
+
currentIndex: number | null = ranges.length ? ranges.length - 1 : null,
|
|
33
|
+
direction: SelectionDirection = "none",
|
|
34
|
+
items: any[] = []
|
|
35
|
+
) {
|
|
36
|
+
ranges.forEach(([s, e]) => this.addRange(s, e));
|
|
37
|
+
this.currentIndex = currentIndex;
|
|
38
|
+
this.direction = direction;
|
|
39
|
+
this.items = items;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
get current() {
|
|
43
|
+
if (this.currentIndex === null) return null;
|
|
44
|
+
const range = this.ranges[this.currentIndex];
|
|
45
|
+
if (!range) {
|
|
46
|
+
return null;
|
|
47
|
+
} else {
|
|
48
|
+
return range;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
select(n: number) {
|
|
53
|
+
if (n < 0 || n >= this.items.length) return;
|
|
54
|
+
this.clear();
|
|
55
|
+
this.currentIndex = this.addRange(n, n);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
multiSelect(n: number) {
|
|
59
|
+
if (n < 0 || n >= this.items.length) return;
|
|
60
|
+
if (this.contains(n)) return;
|
|
61
|
+
this.currentIndex = this.addRange(n, n);
|
|
62
|
+
this.compact(n);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
deselect(n: number) {
|
|
66
|
+
if (n < 0 || n >= this.items.length) return;
|
|
67
|
+
const r = this.ranges.find((r) => r.contains(n));
|
|
68
|
+
if (!r) return;
|
|
69
|
+
else if (r.size === 1) this.removeRange(r);
|
|
70
|
+
else if (r.start === n) r.start++;
|
|
71
|
+
else if (r.end === n) r.end--;
|
|
72
|
+
else {
|
|
73
|
+
this.removeRange(r);
|
|
74
|
+
this.addRange(r.start, n - 1);
|
|
75
|
+
this.currentIndex = this.addRange(n + 1, r.end);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
getSelectedItems<T>(): T[] {
|
|
80
|
+
return this.ranges.flatMap((range) =>
|
|
81
|
+
range.map((index) => this.items[index])
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
extend(n: number) {
|
|
86
|
+
if (n < 0 || n >= this.items.length) return;
|
|
87
|
+
if (this.isEmpty()) {
|
|
88
|
+
this.select(n);
|
|
89
|
+
} else {
|
|
90
|
+
const anchor = this.getAnchor();
|
|
91
|
+
if (anchor !== null && this.current) {
|
|
92
|
+
const [start, end] = [n, anchor].sort((a, b) => a - b);
|
|
93
|
+
this.current.start = start;
|
|
94
|
+
this.current.end = end;
|
|
95
|
+
this.compact(n);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
contains(n: number | null) {
|
|
101
|
+
if (n === null) return false;
|
|
102
|
+
return this.ranges.some((r) => r.contains(n));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
getRanges() {
|
|
106
|
+
return this.ranges.map((r) => r.serialize());
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
clear() {
|
|
110
|
+
this.ranges = [];
|
|
111
|
+
this.currentIndex = null;
|
|
112
|
+
this.direction = "none";
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
serialize(): SelectionData {
|
|
116
|
+
return {
|
|
117
|
+
ranges: this.getRanges(),
|
|
118
|
+
currentIndex: this.currentIndex,
|
|
119
|
+
direction: this.direction,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
isEqual(other: Selection) {
|
|
124
|
+
if (other.ranges.length !== this.ranges.length) return false;
|
|
125
|
+
|
|
126
|
+
for (let i = 0; i < this.ranges.length; ++i) {
|
|
127
|
+
if (!this.ranges[i].isEqual(other.ranges[i])) return false;
|
|
128
|
+
}
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
private addRange(start: number, end: number) {
|
|
133
|
+
const r = new Range(start, end);
|
|
134
|
+
// Keep ranges sorted by start
|
|
135
|
+
const index = this.ranges.findIndex((r) => r.start >= start);
|
|
136
|
+
if (index === -1) this.ranges.push(r);
|
|
137
|
+
else this.ranges.splice(index, 0, r);
|
|
138
|
+
return index === -1 ? this.ranges.length - 1 : index;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private removeRange(r: Range) {
|
|
142
|
+
const index = this.ranges.indexOf(r);
|
|
143
|
+
this.ranges.splice(index, 1);
|
|
144
|
+
if (this.isEmpty()) {
|
|
145
|
+
this.currentIndex = null;
|
|
146
|
+
} else if (index === this.currentIndex) {
|
|
147
|
+
this.currentIndex = this.ranges.length - 1;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
private isEmpty() {
|
|
152
|
+
return this.ranges.length === 0;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
getAnchor() {
|
|
156
|
+
if (!this.current) return null;
|
|
157
|
+
return this.direction === "backward"
|
|
158
|
+
? this.current.end
|
|
159
|
+
: this.current.start;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
getFocus() {
|
|
163
|
+
if (!this.current) return -1;
|
|
164
|
+
return this.direction === "backward"
|
|
165
|
+
? this.current.start
|
|
166
|
+
: this.current.end;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
private compact(focus: number) {
|
|
170
|
+
const removals = [];
|
|
171
|
+
const current = this.current;
|
|
172
|
+
for (let r of this.ranges) {
|
|
173
|
+
if (!this.current || r === this.current) continue;
|
|
174
|
+
if (this.current.overlaps(r)) {
|
|
175
|
+
this.current.combine(r);
|
|
176
|
+
removals.push(r);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
removals.forEach((r) => this.removeRange(r));
|
|
180
|
+
if (current) this.currentIndex = this.ranges.indexOf(current);
|
|
181
|
+
if (!this.current) return;
|
|
182
|
+
if (this.current.start < focus) this.direction = "forward";
|
|
183
|
+
else if (this.current.end > focus) this.direction = "backward";
|
|
184
|
+
else this.direction = "none";
|
|
185
|
+
}
|
|
186
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Dispatch, useLayoutEffect, useMemo } from "react";
|
|
2
|
+
import { FixedSizeList } from "react-window";
|
|
3
|
+
import { Action, actions } from "./reducer";
|
|
4
|
+
import { TreeApi } from "./tree-api";
|
|
5
|
+
import { StateContext, TreeProviderProps } from "./types";
|
|
6
|
+
|
|
7
|
+
export function useTreeApi<T>(
|
|
8
|
+
state: StateContext,
|
|
9
|
+
dispatch: Dispatch<Action>,
|
|
10
|
+
props: TreeProviderProps<T>,
|
|
11
|
+
list: FixedSizeList | undefined
|
|
12
|
+
) {
|
|
13
|
+
/**
|
|
14
|
+
* We only ever want one instance of the api object
|
|
15
|
+
* It will get updated as the props change, but the
|
|
16
|
+
* reference will not.
|
|
17
|
+
*/
|
|
18
|
+
const api = useMemo(
|
|
19
|
+
() => new TreeApi<T>(dispatch, state, props, list),
|
|
20
|
+
// eslint-disable-next-line
|
|
21
|
+
[]
|
|
22
|
+
);
|
|
23
|
+
api.assign(dispatch, state, props, list);
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* This ensures that the selection remains correct even
|
|
27
|
+
* after opening and closing a folders
|
|
28
|
+
*/
|
|
29
|
+
useLayoutEffect(() => {
|
|
30
|
+
dispatch(actions.setVisibleIds(api.visibleIds, api.idToIndex));
|
|
31
|
+
}, [dispatch, api, props.root]);
|
|
32
|
+
|
|
33
|
+
return api;
|
|
34
|
+
}
|
package/src/tree-api.ts
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import memoizeOne from "memoize-one";
|
|
2
|
+
import { Dispatch } from "react";
|
|
3
|
+
import { FixedSizeList } from "react-window";
|
|
4
|
+
import { flattenTree } from "./data/flatten-tree";
|
|
5
|
+
import { Cursor } from "./dnd/compute-drop";
|
|
6
|
+
import { Action, actions } from "./reducer";
|
|
7
|
+
import { Node, StateContext, TreeProviderProps } from "./types";
|
|
8
|
+
|
|
9
|
+
export class TreeApi<T = unknown> {
|
|
10
|
+
constructor(
|
|
11
|
+
public dispatch: Dispatch<Action>,
|
|
12
|
+
public state: StateContext,
|
|
13
|
+
public props: TreeProviderProps<T>,
|
|
14
|
+
public list: FixedSizeList | undefined
|
|
15
|
+
) {}
|
|
16
|
+
|
|
17
|
+
assign(
|
|
18
|
+
dispatch: Dispatch<Action>,
|
|
19
|
+
state: StateContext,
|
|
20
|
+
props: TreeProviderProps<T>,
|
|
21
|
+
list: FixedSizeList | undefined
|
|
22
|
+
) {
|
|
23
|
+
this.dispatch = dispatch;
|
|
24
|
+
this.state = state;
|
|
25
|
+
this.props = props;
|
|
26
|
+
this.list = list;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
getNode(id: string): Node<unknown> | null {
|
|
30
|
+
if (id in this.idToIndex)
|
|
31
|
+
return this.visibleNodes[this.idToIndex[id]] || null;
|
|
32
|
+
else return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
getSelectedIds() {
|
|
36
|
+
return this.state.selection.ids;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
edit(id: string | number | null) {
|
|
40
|
+
this.dispatch(actions.edit(id ? id.toString() : null));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
select(index: number | null, meta: boolean, shift: boolean) {
|
|
44
|
+
this.dispatch(actions.select(index, meta, shift));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
selectUpwards(shiftKey: boolean) {
|
|
48
|
+
this.dispatch(actions.stepUp(shiftKey, this.visibleIds));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
selectDownwards(shiftKey: boolean) {
|
|
52
|
+
this.dispatch(actions.stepDown(shiftKey, this.visibleIds));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
hideCursor() {
|
|
56
|
+
this.dispatch(actions.setCursorLocation({ type: "none" }));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
showCursor(cursor: Cursor) {
|
|
60
|
+
this.dispatch(actions.setCursorLocation(cursor));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
scrollToId(id: string) {
|
|
64
|
+
if (!this.list) return;
|
|
65
|
+
const index = this.idToIndex[id];
|
|
66
|
+
if (index) {
|
|
67
|
+
this.list.scrollToItem(index, "start");
|
|
68
|
+
} else {
|
|
69
|
+
this.openParents(id);
|
|
70
|
+
// This appears to be synchronous
|
|
71
|
+
// But I've only tested it in the console and
|
|
72
|
+
// not in an event handler which will be batched...
|
|
73
|
+
// We may need to wrap this in a timeout or trigger an effect somehow
|
|
74
|
+
setTimeout(() => {
|
|
75
|
+
const index = this.idToIndex[id];
|
|
76
|
+
if (index) {
|
|
77
|
+
this.list?.scrollToItem(index, "start");
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
open(id: string) {
|
|
84
|
+
this.props.onToggle(id, true);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
openParents(id: string) {
|
|
88
|
+
const node = dfs(this.props.root, id);
|
|
89
|
+
let parent = node?.parent;
|
|
90
|
+
|
|
91
|
+
while (parent) {
|
|
92
|
+
this.open(parent.id);
|
|
93
|
+
parent = parent.parent;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
get visibleIds() {
|
|
98
|
+
return getIds(this.visibleNodes);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
get idToIndex() {
|
|
102
|
+
return createIndex(this.visibleNodes);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
get visibleNodes() {
|
|
106
|
+
return createList(this.props.root);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const getIds = memoizeOne((nodes: Node[]) => nodes.map((n) => n.id));
|
|
111
|
+
const createIndex = memoizeOne((nodes: Node[]) => {
|
|
112
|
+
return nodes.reduce<{ [id: string]: number }>((map, node, index) => {
|
|
113
|
+
map[node.id] = index;
|
|
114
|
+
return map;
|
|
115
|
+
}, {});
|
|
116
|
+
});
|
|
117
|
+
const createList = memoizeOne(flattenTree);
|
|
118
|
+
|
|
119
|
+
function dfs(node: Node<unknown>, id: string): Node<unknown> | null {
|
|
120
|
+
if (!node) return null;
|
|
121
|
+
if (node.id === id) return node;
|
|
122
|
+
if (node.children) {
|
|
123
|
+
for (let child of node.children) {
|
|
124
|
+
const result = dfs(child, id);
|
|
125
|
+
if (result) return result;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return null;
|
|
129
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import React, {
|
|
2
|
+
ComponentType,
|
|
3
|
+
CSSProperties,
|
|
4
|
+
MouseEvent,
|
|
5
|
+
MouseEventHandler,
|
|
6
|
+
MutableRefObject,
|
|
7
|
+
ReactElement,
|
|
8
|
+
Ref,
|
|
9
|
+
} from "react";
|
|
10
|
+
import { FixedSizeList } from "react-window";
|
|
11
|
+
import { Cursor } from "./dnd/compute-drop";
|
|
12
|
+
import { SelectionData } from "./selection/selection";
|
|
13
|
+
import { TreeApi } from "./tree-api";
|
|
14
|
+
|
|
15
|
+
// Forward ref can't forward generics without this little re-declare
|
|
16
|
+
// https://fettblog.eu/typescript-react-generic-forward-refs/
|
|
17
|
+
declare module "react" {
|
|
18
|
+
function forwardRef<T, P = {}>(
|
|
19
|
+
render: (props: P, ref: React.Ref<T>) => React.ReactElement | null
|
|
20
|
+
): (props: P & React.RefAttributes<T>) => React.ReactElement | null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type Node<T = unknown> = {
|
|
24
|
+
id: string;
|
|
25
|
+
model: T;
|
|
26
|
+
level: number;
|
|
27
|
+
children: Node<T>[] | null;
|
|
28
|
+
parent: Node<T> | null;
|
|
29
|
+
isOpen: boolean;
|
|
30
|
+
isDraggable: boolean;
|
|
31
|
+
isDroppable: boolean;
|
|
32
|
+
rowIndex: number | null;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export type NodesById<T> = { [id: string]: Node<T> };
|
|
36
|
+
|
|
37
|
+
export interface IdObj {
|
|
38
|
+
id: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export type NodeRendererProps<T> = {
|
|
42
|
+
innerRef: (el: HTMLDivElement | null) => void;
|
|
43
|
+
styles: { row: CSSProperties; indent: CSSProperties };
|
|
44
|
+
data: T;
|
|
45
|
+
state: NodeState;
|
|
46
|
+
handlers: NodeHandlers;
|
|
47
|
+
tree: TreeApi<T>;
|
|
48
|
+
preview: boolean;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export type NodeState = {
|
|
52
|
+
isOpen: boolean;
|
|
53
|
+
isSelected: boolean;
|
|
54
|
+
isHoveringOverChild: boolean;
|
|
55
|
+
isDragging: boolean;
|
|
56
|
+
isFirstOfSelected: boolean;
|
|
57
|
+
isLastOfSelected: boolean;
|
|
58
|
+
isEditing: boolean;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export type NodeHandlers = {
|
|
62
|
+
toggle: MouseEventHandler;
|
|
63
|
+
select: (e: MouseEvent, selectOnClick?: boolean) => void;
|
|
64
|
+
edit: () => void;
|
|
65
|
+
submit: (name: string) => void;
|
|
66
|
+
reset: () => void;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export type NodeRenderer<T> = ComponentType<NodeRendererProps<T>>;
|
|
70
|
+
|
|
71
|
+
export type MoveHandler = (
|
|
72
|
+
dragIds: string[],
|
|
73
|
+
parentId: string | null,
|
|
74
|
+
index: number
|
|
75
|
+
) => void;
|
|
76
|
+
|
|
77
|
+
export type ToggleHandler = (id: string, isOpen: boolean) => void;
|
|
78
|
+
export type EditHandler = (id: string, name: string) => void;
|
|
79
|
+
export type NodeClickHandler<T> = (e: MouseEvent, n: Node<T>) => void;
|
|
80
|
+
export type IndexClickHandler = (e: MouseEvent, index: number) => void;
|
|
81
|
+
export type SelectedCheck = (index: number) => boolean;
|
|
82
|
+
|
|
83
|
+
export type CursorLocation = {
|
|
84
|
+
index: number | null;
|
|
85
|
+
level: number | null;
|
|
86
|
+
parentId: string | null;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
export type DragItem = {
|
|
90
|
+
dragIds: string[];
|
|
91
|
+
id: string;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
export type SelectionState = {
|
|
95
|
+
data: SelectionData | null;
|
|
96
|
+
ids: string[];
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export type StateContext = {
|
|
100
|
+
cursor: Cursor;
|
|
101
|
+
editingId: string | null;
|
|
102
|
+
selection: SelectionState;
|
|
103
|
+
visibleIds: string[];
|
|
104
|
+
};
|
|
105
|
+
export interface TreeProps<T> {
|
|
106
|
+
children: NodeRenderer<T>;
|
|
107
|
+
data: T;
|
|
108
|
+
height?: number;
|
|
109
|
+
width?: number;
|
|
110
|
+
rowHeight?: number;
|
|
111
|
+
indent?: number;
|
|
112
|
+
hideRoot?: boolean;
|
|
113
|
+
onToggle?: ToggleHandler;
|
|
114
|
+
onMove?: MoveHandler;
|
|
115
|
+
onEdit?: EditHandler;
|
|
116
|
+
getChildren?: string | ((d: T) => T[]);
|
|
117
|
+
isOpen?: string | ((d: T) => boolean);
|
|
118
|
+
disableDrag?: string | boolean | ((d: T) => boolean);
|
|
119
|
+
disableDrop?: string | boolean | ((d: T) => boolean);
|
|
120
|
+
openByDefault?: boolean;
|
|
121
|
+
className?: string | undefined;
|
|
122
|
+
handle?: Ref<TreeApi<T>>;
|
|
123
|
+
onClick?: MouseEventHandler;
|
|
124
|
+
onContextMenu?: MouseEventHandler;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export type TreeProviderProps<T> = {
|
|
128
|
+
imperativeHandle: React.Ref<TreeApi<T>> | undefined;
|
|
129
|
+
children: ReactElement;
|
|
130
|
+
height: number;
|
|
131
|
+
indent: number;
|
|
132
|
+
listEl: MutableRefObject<HTMLDivElement | null>;
|
|
133
|
+
onToggle: ToggleHandler;
|
|
134
|
+
onMove: MoveHandler;
|
|
135
|
+
onEdit: EditHandler;
|
|
136
|
+
onClick?: MouseEventHandler;
|
|
137
|
+
onContextMenu?: MouseEventHandler;
|
|
138
|
+
renderer: NodeRenderer<any>;
|
|
139
|
+
rowHeight: number;
|
|
140
|
+
root: Node<T>;
|
|
141
|
+
width: number;
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
export type StaticContext<T> = TreeProviderProps<T> & {
|
|
145
|
+
api: TreeApi<T>;
|
|
146
|
+
list: MutableRefObject<FixedSizeList | undefined>;
|
|
147
|
+
};
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Node } from "./types";
|
|
2
|
+
|
|
3
|
+
export function bound(n: number, min: number, max: number) {
|
|
4
|
+
return Math.max(Math.min(n, max), min);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export const isFolder = (node: Node<any>) => !!node.children;
|
|
8
|
+
|
|
9
|
+
export function isItem(node: Node | null) {
|
|
10
|
+
return node && !isFolder(node);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function isClosed(node: Node | null) {
|
|
14
|
+
return node && isFolder(node) && !node.isOpen;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Is first param a decendent of the second param
|
|
19
|
+
*/
|
|
20
|
+
export const isDecendent = (a: Node, b: Node) => {
|
|
21
|
+
let n: Node | null = a;
|
|
22
|
+
while (n) {
|
|
23
|
+
if (n.id === b.id) return true;
|
|
24
|
+
n = n.parent;
|
|
25
|
+
}
|
|
26
|
+
return false;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const indexOf = (node: Node) => {
|
|
30
|
+
// This should probably not throw an error, but instead return null
|
|
31
|
+
if (!node.parent) throw Error("Node does not have a parent");
|
|
32
|
+
return node.parent.children!.findIndex((c) => c.id === node.id);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export function noop() {}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"include": ["src/index.ts"],
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
/* Visit https://aka.ms/tsconfig.json to read more about this file */
|
|
5
|
+
|
|
6
|
+
/* Language and Environment */
|
|
7
|
+
"target": "ES5", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
|
|
8
|
+
"jsx": "preserve", /* Specify what JSX code is generated. */
|
|
9
|
+
|
|
10
|
+
/* Modules */
|
|
11
|
+
"module": "CommonJS", /* Specify what module code is generated. */
|
|
12
|
+
"moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
|
|
13
|
+
|
|
14
|
+
/* Emit */
|
|
15
|
+
"declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
|
|
16
|
+
"emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
|
|
17
|
+
|
|
18
|
+
/* Interop Constraints */
|
|
19
|
+
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */
|
|
20
|
+
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
|
|
21
|
+
|
|
22
|
+
/* Type Checking */
|
|
23
|
+
"strict": true, /* Enable all strict type-checking options. */
|
|
24
|
+
|
|
25
|
+
/* Completeness */
|
|
26
|
+
"skipLibCheck": true, /* Skip type checking all .d.ts files. */
|
|
27
|
+
}
|
|
28
|
+
}
|