react-arborist 2.0.0-rc → 2.0.0-rc.2
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/README.md +714 -0
- package/dist/components/cursor.d.ts +1 -1
- package/dist/components/default-container.d.ts +5 -0
- package/dist/components/default-cursor.d.ts +2 -2
- package/dist/components/default-node.d.ts +1 -1
- package/dist/index.js +187 -73
- package/dist/index.js.map +1 -1
- package/dist/interfaces/node-api.d.ts +13 -8
- package/dist/interfaces/tree-api.d.ts +14 -4
- package/dist/module.js +186 -72
- package/dist/module.js.map +1 -1
- package/dist/types/renderers.d.ts +1 -1
- package/dist/types/tree-props.d.ts +6 -2
- package/dist/utils.d.ts +4 -0
- package/package.json +14 -2
- package/src/components/cursor.tsx +1 -1
- package/src/components/default-container.tsx +9 -0
- package/src/components/default-cursor.tsx +2 -2
- package/src/components/default-node.tsx +43 -8
- package/src/components/list-outer-element.tsx +3 -6
- package/src/components/provider.tsx +1 -1
- package/src/components/row-container.tsx +1 -0
- package/src/data/create-root.ts +3 -2
- package/src/dnd/outer-drop-hook.ts +32 -6
- package/src/interfaces/node-api.ts +38 -27
- package/src/interfaces/tree-api.ts +68 -36
- package/src/types/renderers.ts +1 -1
- package/src/types/tree-props.ts +7 -2
- package/src/utils.ts +29 -0
- package/tsconfig.json +1 -1
- package/dist/data/flatten-tree.d.ts +0 -4
- package/dist/hooks/use-uncontrolled-tree.d.ts +0 -24
- package/dist/utils/props.d.ts +0 -3
- package/src/utils/props.ts +0 -8
|
@@ -15,7 +15,7 @@ export interface TreeProps<T extends IdObj> {
|
|
|
15
15
|
children?: ElementType<renderers.NodeRendererProps<T>>;
|
|
16
16
|
renderRow?: ElementType<renderers.RowRendererProps<T>>;
|
|
17
17
|
renderDragPreview?: ElementType<renderers.DragPreviewProps>;
|
|
18
|
-
renderCursor?: ElementType<renderers.
|
|
18
|
+
renderCursor?: ElementType<renderers.CursorProps>;
|
|
19
19
|
renderContainer?: ElementType<{}>;
|
|
20
20
|
rowHeight?: number;
|
|
21
21
|
width?: number;
|
|
@@ -28,15 +28,19 @@ export interface TreeProps<T extends IdObj> {
|
|
|
28
28
|
selectionFollowsFocus?: boolean;
|
|
29
29
|
disableDrag?: string | boolean | BoolFunc<T>;
|
|
30
30
|
disableDrop?: string | boolean | BoolFunc<T>;
|
|
31
|
-
|
|
31
|
+
childrenAccessor?: string | ((d: T) => T[]);
|
|
32
|
+
idAccessor?: string | ((d: T) => string);
|
|
32
33
|
onActivate?: (node: NodeApi<T>) => void;
|
|
33
34
|
onSelect?: (nodes: NodeApi<T>[]) => void;
|
|
34
35
|
onScroll?: (props: ListOnScrollProps) => void;
|
|
36
|
+
onToggle?: (id: string) => void;
|
|
37
|
+
onFocus?: (node: NodeApi<T>) => void;
|
|
35
38
|
selection?: string;
|
|
36
39
|
initialOpenState?: OpenMap;
|
|
37
40
|
searchTerm?: string;
|
|
38
41
|
searchMatch?: (node: NodeApi<T>, searchTerm: string) => boolean;
|
|
39
42
|
className?: string | undefined;
|
|
43
|
+
rowClassName?: string | undefined;
|
|
40
44
|
dndRootElement?: globalThis.Node | null;
|
|
41
45
|
onClick?: MouseEventHandler;
|
|
42
46
|
onContextMenu?: MouseEventHandler;
|
package/dist/utils.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { NodeApi } from "./interfaces/node-api";
|
|
2
|
+
import { TreeApi } from "./interfaces/tree-api";
|
|
2
3
|
import { IdObj } from "./types/utils";
|
|
3
4
|
export declare function bound(n: number, min: number, max: number): number;
|
|
4
5
|
export declare function isItem(node: NodeApi<any> | null): boolean | null;
|
|
@@ -10,6 +11,7 @@ export declare const isDecendent: (a: NodeApi<any>, b: NodeApi<any>) => boolean;
|
|
|
10
11
|
export declare const indexOf: (node: NodeApi<any>) => number;
|
|
11
12
|
export declare function noop(): void;
|
|
12
13
|
export declare function dfs(node: NodeApi<any>, id: string): NodeApi<any> | null;
|
|
14
|
+
export declare function walk(node: NodeApi<any>, fn: (node: NodeApi<any>) => void): void;
|
|
13
15
|
export declare function focusNextElement(target: HTMLElement): void;
|
|
14
16
|
export declare function focusPrevElement(target: HTMLElement): void;
|
|
15
17
|
export declare function access<T = boolean>(obj: any, accessor: string | boolean | Function): T;
|
|
@@ -18,3 +20,5 @@ export declare function identify(obj: string | IdObj): string;
|
|
|
18
20
|
export declare function mergeRefs(...refs: any): (instance: any) => void;
|
|
19
21
|
export declare function safeRun<T extends (...args: any[]) => any>(fn: T | undefined, ...args: Parameters<T>): any;
|
|
20
22
|
export declare function waitFor(fn: () => boolean): Promise<void>;
|
|
23
|
+
export declare function getInsertIndex(tree: TreeApi<any>): number;
|
|
24
|
+
export declare function getInsertParentId(tree: TreeApi<any>): string | null;
|
package/package.json
CHANGED
|
@@ -1,13 +1,24 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-arborist",
|
|
3
|
-
"version": "2.0.0-rc",
|
|
3
|
+
"version": "2.0.0-rc.2",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"source": "src/index.ts",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"module": "dist/module.js",
|
|
8
8
|
"types": "dist/index.d.ts",
|
|
9
|
-
"repository": "github
|
|
9
|
+
"repository": "https://github.com/brimdata/react-arborist",
|
|
10
10
|
"homepage": "https://react-arborist.netlify.app",
|
|
11
|
+
"keywords": [
|
|
12
|
+
"react",
|
|
13
|
+
"arborist",
|
|
14
|
+
"react-arborist",
|
|
15
|
+
"treeview",
|
|
16
|
+
"tree",
|
|
17
|
+
"vitualized",
|
|
18
|
+
"dnd",
|
|
19
|
+
"multiselection",
|
|
20
|
+
"filterable"
|
|
21
|
+
],
|
|
11
22
|
"dependencies": {
|
|
12
23
|
"react-dnd": "^14.0.3",
|
|
13
24
|
"react-dnd-html5-backend": "^14.0.1",
|
|
@@ -29,6 +40,7 @@
|
|
|
29
40
|
"react-dom": ">= 16.14"
|
|
30
41
|
},
|
|
31
42
|
"devDependencies": {
|
|
43
|
+
"@parcel/core": "^2.3.2",
|
|
32
44
|
"@types/jest": "^27.4.1",
|
|
33
45
|
"@types/react": "^18.0.0",
|
|
34
46
|
"@types/react-window": "^1.8.5",
|
|
@@ -8,6 +8,11 @@ import { RowContainer } from "./row-container";
|
|
|
8
8
|
let focusSearchTerm = "";
|
|
9
9
|
let timeoutId: any = null;
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* All these keyboard shortcuts seem like they should be configurable.
|
|
13
|
+
* Each operation should be a given a name and separated from
|
|
14
|
+
* the event handler. Future clean up welcome.
|
|
15
|
+
*/
|
|
11
16
|
export function DefaultContainer() {
|
|
12
17
|
useDataUpdates();
|
|
13
18
|
const tree = useTreeApi();
|
|
@@ -37,6 +42,7 @@ export function DefaultContainer() {
|
|
|
37
42
|
return;
|
|
38
43
|
}
|
|
39
44
|
if (e.key === "Backspace") {
|
|
45
|
+
if (!tree.props.onDelete) return;
|
|
40
46
|
const ids = Array.from(tree.selectedIds);
|
|
41
47
|
if (ids.length > 1) {
|
|
42
48
|
let nextFocus = tree.mostRecentNode;
|
|
@@ -129,10 +135,12 @@ export function DefaultContainer() {
|
|
|
129
135
|
return;
|
|
130
136
|
}
|
|
131
137
|
if (e.key === "a" && !e.metaKey) {
|
|
138
|
+
if (!tree.props.onCreate) return;
|
|
132
139
|
tree.createLeaf();
|
|
133
140
|
return;
|
|
134
141
|
}
|
|
135
142
|
if (e.key === "A" && !e.metaKey) {
|
|
143
|
+
if (!tree.props.onCreate) return;
|
|
136
144
|
tree.createInternal();
|
|
137
145
|
return;
|
|
138
146
|
}
|
|
@@ -150,6 +158,7 @@ export function DefaultContainer() {
|
|
|
150
158
|
return;
|
|
151
159
|
}
|
|
152
160
|
if (e.key === "Enter") {
|
|
161
|
+
if (!tree.props.onRename) return;
|
|
153
162
|
setTimeout(() => {
|
|
154
163
|
if (tree.focusedNode) tree.edit(tree.focusedNode);
|
|
155
164
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React, { CSSProperties } from "react";
|
|
2
|
-
import {
|
|
2
|
+
import { CursorProps } from "../types/renderers";
|
|
3
3
|
|
|
4
4
|
const placeholderStyle = {
|
|
5
5
|
display: "flex",
|
|
@@ -25,7 +25,7 @@ export const DefaultCursor = React.memo(function DefaultCursor({
|
|
|
25
25
|
top,
|
|
26
26
|
left,
|
|
27
27
|
indent,
|
|
28
|
-
}:
|
|
28
|
+
}: CursorProps) {
|
|
29
29
|
const style: CSSProperties = {
|
|
30
30
|
position: "absolute",
|
|
31
31
|
pointerEvents: "none",
|
|
@@ -1,15 +1,50 @@
|
|
|
1
|
-
import React from "react";
|
|
1
|
+
import React, { useEffect, useRef } from "react";
|
|
2
2
|
import { NodeRendererProps } from "../types/renderers";
|
|
3
3
|
import { IdObj } from "../types/utils";
|
|
4
4
|
|
|
5
|
-
export function DefaultNode<T extends IdObj>({
|
|
6
|
-
style,
|
|
7
|
-
node,
|
|
8
|
-
dragHandle,
|
|
9
|
-
}: NodeRendererProps<T>) {
|
|
5
|
+
export function DefaultNode<T extends IdObj>(props: NodeRendererProps<T>) {
|
|
10
6
|
return (
|
|
11
|
-
<div
|
|
12
|
-
|
|
7
|
+
<div ref={props.dragHandle} style={props.style}>
|
|
8
|
+
<span
|
|
9
|
+
onClick={(e) => {
|
|
10
|
+
e.stopPropagation();
|
|
11
|
+
props.node.toggle();
|
|
12
|
+
}}
|
|
13
|
+
>
|
|
14
|
+
{props.node.isLeaf ? "🌳" : props.node.isOpen ? "🗁" : "🗀"}
|
|
15
|
+
</span>{" "}
|
|
16
|
+
{props.node.isEditing ? <Edit {...props} /> : <Show {...props} />}
|
|
13
17
|
</div>
|
|
14
18
|
);
|
|
15
19
|
}
|
|
20
|
+
|
|
21
|
+
function Show<T extends IdObj>(props: NodeRendererProps<T>) {
|
|
22
|
+
return (
|
|
23
|
+
<>
|
|
24
|
+
{/* @ts-ignore */}
|
|
25
|
+
<span>{props.node.data.name}</span>
|
|
26
|
+
</>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function Edit<T extends IdObj>({ node }: NodeRendererProps<T>) {
|
|
31
|
+
const input = useRef<any>();
|
|
32
|
+
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
input.current?.focus();
|
|
35
|
+
input.current?.select();
|
|
36
|
+
}, []);
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<input
|
|
40
|
+
ref={input}
|
|
41
|
+
// @ts-ignore
|
|
42
|
+
defaultValue={node.data.name}
|
|
43
|
+
onBlur={() => node.reset()}
|
|
44
|
+
onKeyDown={(e) => {
|
|
45
|
+
if (e.key === "Escape") node.reset();
|
|
46
|
+
if (e.key === "Enter") node.submit(input.current?.value || "");
|
|
47
|
+
}}
|
|
48
|
+
></input>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { forwardRef } from "react";
|
|
2
2
|
import { useTreeApi } from "../context";
|
|
3
3
|
import { treeBlur } from "../state/focus-slice";
|
|
4
|
-
import {
|
|
4
|
+
import { Cursor } from "./cursor";
|
|
5
5
|
|
|
6
6
|
export const ListOuterElement = forwardRef(function Outer(
|
|
7
7
|
props: React.HTMLProps<HTMLDivElement>,
|
|
@@ -15,7 +15,7 @@ export const ListOuterElement = forwardRef(function Outer(
|
|
|
15
15
|
ref={ref}
|
|
16
16
|
{...rest}
|
|
17
17
|
onClick={(e) => {
|
|
18
|
-
if (e.currentTarget === e.target) tree.
|
|
18
|
+
if (e.currentTarget === e.target) tree.deselectAll();
|
|
19
19
|
}}
|
|
20
20
|
>
|
|
21
21
|
<DropContainer />
|
|
@@ -35,11 +35,8 @@ const DropContainer = () => {
|
|
|
35
35
|
left: "0",
|
|
36
36
|
right: "0",
|
|
37
37
|
}}
|
|
38
|
-
onClick={(e) => {
|
|
39
|
-
console.log(e.currentTarget, e.target);
|
|
40
|
-
}}
|
|
41
38
|
>
|
|
42
|
-
<
|
|
39
|
+
<Cursor />
|
|
43
40
|
</div>
|
|
44
41
|
);
|
|
45
42
|
};
|
package/src/data/create-root.ts
CHANGED
|
@@ -10,18 +10,19 @@ export function createRoot<T extends IdObj>(tree: TreeApi<T>): NodeApi<T> {
|
|
|
10
10
|
level: number,
|
|
11
11
|
parent: NodeApi<T> | null
|
|
12
12
|
) {
|
|
13
|
+
const id = tree.accessId(data);
|
|
13
14
|
const node = new NodeApi<T>({
|
|
14
15
|
tree,
|
|
15
16
|
data,
|
|
16
17
|
level,
|
|
17
18
|
parent,
|
|
18
|
-
id
|
|
19
|
+
id,
|
|
19
20
|
children: null,
|
|
20
21
|
isDraggable: tree.isDraggable(data),
|
|
21
22
|
isDroppable: tree.isDroppable(data),
|
|
22
23
|
rowIndex: null,
|
|
23
24
|
});
|
|
24
|
-
const children = tree.
|
|
25
|
+
const children = tree.accessChildren(data);
|
|
25
26
|
if (children) {
|
|
26
27
|
node.children = children.map((child: T) =>
|
|
27
28
|
visitSelfAndChildren(child, level + 1, node)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useDrop } from "react-dnd";
|
|
2
2
|
import { useTreeApi } from "../context";
|
|
3
3
|
import { DragItem } from "../types/dnd";
|
|
4
|
+
import { isDecendent } from "../utils";
|
|
4
5
|
import { computeDrop } from "./compute-drop";
|
|
5
6
|
import { DropResult } from "./drop-hook";
|
|
6
7
|
|
|
@@ -13,9 +14,28 @@ export function useOuterDrop() {
|
|
|
13
14
|
accept: "NODE",
|
|
14
15
|
hover: (item, m) => {
|
|
15
16
|
if (!m.isOver({ shallow: true })) return;
|
|
17
|
+
if (m.canDrop()) {
|
|
18
|
+
const offset = m.getClientOffset();
|
|
19
|
+
if (!tree.listEl.current || !offset) return;
|
|
20
|
+
const { cursor } = computeDrop({
|
|
21
|
+
element: tree.listEl.current,
|
|
22
|
+
offset: offset,
|
|
23
|
+
indent: tree.indent,
|
|
24
|
+
node: null,
|
|
25
|
+
prevNode: tree.visibleNodes[tree.visibleNodes.length - 1],
|
|
26
|
+
nextNode: null,
|
|
27
|
+
});
|
|
28
|
+
if (cursor) tree.showCursor(cursor);
|
|
29
|
+
} else {
|
|
30
|
+
tree.hideCursor();
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
canDrop: (item, m) => {
|
|
34
|
+
if (!m.isOver({ shallow: true })) return false;
|
|
35
|
+
if (tree.isFiltered) return false;
|
|
16
36
|
const offset = m.getClientOffset();
|
|
17
|
-
if (!tree.listEl.current || !offset) return;
|
|
18
|
-
const {
|
|
37
|
+
if (!tree.listEl.current || !offset) return false;
|
|
38
|
+
const { drop } = computeDrop({
|
|
19
39
|
element: tree.listEl.current,
|
|
20
40
|
offset: offset,
|
|
21
41
|
indent: tree.indent,
|
|
@@ -23,10 +43,16 @@ export function useOuterDrop() {
|
|
|
23
43
|
prevNode: tree.visibleNodes[tree.visibleNodes.length - 1],
|
|
24
44
|
nextNode: null,
|
|
25
45
|
});
|
|
26
|
-
if (
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
46
|
+
if (!drop) return false;
|
|
47
|
+
const dropParent = tree.get(drop.parentId) ?? tree.root;
|
|
48
|
+
|
|
49
|
+
for (let id of item.dragIds) {
|
|
50
|
+
const drag = tree.get(id);
|
|
51
|
+
if (!drag) return false;
|
|
52
|
+
if (!dropParent) return false;
|
|
53
|
+
if (drag.isInternal && isDecendent(dropParent, drag)) return false;
|
|
54
|
+
}
|
|
55
|
+
return true;
|
|
30
56
|
},
|
|
31
57
|
drop: (item, m) => {
|
|
32
58
|
if (m.didDrop()) return;
|
|
@@ -38,21 +38,6 @@ export class NodeApi<T extends IdObj = IdObj> {
|
|
|
38
38
|
this.rowIndex = params.rowIndex;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
get next(): NodeApi<T> | null {
|
|
42
|
-
if (this.rowIndex === null) return null;
|
|
43
|
-
return this.tree.at(this.rowIndex + 1);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
get prev(): NodeApi<T> | null {
|
|
47
|
-
if (this.rowIndex === null) return null;
|
|
48
|
-
return this.tree.at(this.rowIndex - 1);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
get nextSibling(): NodeApi<T> | null {
|
|
52
|
-
const i = this.childIndex;
|
|
53
|
-
return this.parent?.children![i + 1] ?? null;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
41
|
get isRoot() {
|
|
57
42
|
return this.id === ROOT_ID;
|
|
58
43
|
}
|
|
@@ -69,6 +54,10 @@ export class NodeApi<T extends IdObj = IdObj> {
|
|
|
69
54
|
return this.isLeaf ? false : this.tree.isOpen(this.id);
|
|
70
55
|
}
|
|
71
56
|
|
|
57
|
+
get isClosed() {
|
|
58
|
+
return this.isLeaf ? false : !this.tree.isOpen(this.id);
|
|
59
|
+
}
|
|
60
|
+
|
|
72
61
|
get isEditing() {
|
|
73
62
|
return this.tree.editingId === this.id;
|
|
74
63
|
}
|
|
@@ -77,6 +66,10 @@ export class NodeApi<T extends IdObj = IdObj> {
|
|
|
77
66
|
return this.tree.isSelected(this.id);
|
|
78
67
|
}
|
|
79
68
|
|
|
69
|
+
get isOnlySelection() {
|
|
70
|
+
return this.isSelected && this.tree.hasOneSelection;
|
|
71
|
+
}
|
|
72
|
+
|
|
80
73
|
get isSelectedStart() {
|
|
81
74
|
return this.isSelected && !this.prev?.isSelected;
|
|
82
75
|
}
|
|
@@ -89,14 +82,6 @@ export class NodeApi<T extends IdObj = IdObj> {
|
|
|
89
82
|
return this.tree.isFocused(this.id);
|
|
90
83
|
}
|
|
91
84
|
|
|
92
|
-
get childIndex() {
|
|
93
|
-
if (this.parent && this.parent.children) {
|
|
94
|
-
return this.parent.children.findIndex((child) => child.id === this.id);
|
|
95
|
-
} else {
|
|
96
|
-
return -1;
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
85
|
get isDragging() {
|
|
101
86
|
return this.tree.isDragging(this.id);
|
|
102
87
|
}
|
|
@@ -107,17 +92,43 @@ export class NodeApi<T extends IdObj = IdObj> {
|
|
|
107
92
|
|
|
108
93
|
get state() {
|
|
109
94
|
return {
|
|
110
|
-
|
|
95
|
+
isClosed: this.isClosed,
|
|
111
96
|
isDragging: this.isDragging,
|
|
112
|
-
|
|
113
|
-
isSelectedStart: this.isSelectedStart,
|
|
114
|
-
isSelectedEnd: this.isSelectedEnd,
|
|
97
|
+
isEditing: this.isEditing,
|
|
115
98
|
isFocused: this.isFocused,
|
|
99
|
+
isInternal: this.isInternal,
|
|
100
|
+
isLeaf: this.isLeaf,
|
|
116
101
|
isOpen: this.isOpen,
|
|
102
|
+
isSelected: this.isSelected,
|
|
103
|
+
isSelectedEnd: this.isSelectedEnd,
|
|
104
|
+
isSelectedStart: this.isSelectedStart,
|
|
117
105
|
willReceiveDrop: this.willReceiveDrop,
|
|
118
106
|
};
|
|
119
107
|
}
|
|
120
108
|
|
|
109
|
+
get childIndex() {
|
|
110
|
+
if (this.parent && this.parent.children) {
|
|
111
|
+
return this.parent.children.findIndex((child) => child.id === this.id);
|
|
112
|
+
} else {
|
|
113
|
+
return -1;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
get next(): NodeApi<T> | null {
|
|
118
|
+
if (this.rowIndex === null) return null;
|
|
119
|
+
return this.tree.at(this.rowIndex + 1);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
get prev(): NodeApi<T> | null {
|
|
123
|
+
if (this.rowIndex === null) return null;
|
|
124
|
+
return this.tree.at(this.rowIndex - 1);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
get nextSibling(): NodeApi<T> | null {
|
|
128
|
+
const i = this.childIndex;
|
|
129
|
+
return this.parent?.children![i + 1] ?? null;
|
|
130
|
+
}
|
|
131
|
+
|
|
121
132
|
select() {
|
|
122
133
|
this.tree.select(this);
|
|
123
134
|
}
|
|
@@ -2,12 +2,7 @@ import { EditResult } from "../types/handlers";
|
|
|
2
2
|
import { Identity, IdObj } from "../types/utils";
|
|
3
3
|
import { TreeProps } from "../types/tree-props";
|
|
4
4
|
import { MutableRefObject } from "react";
|
|
5
|
-
import {
|
|
6
|
-
Align,
|
|
7
|
-
FixedSizeList,
|
|
8
|
-
ListOnItemsRenderedProps,
|
|
9
|
-
ListOnScrollProps,
|
|
10
|
-
} from "react-window";
|
|
5
|
+
import { Align, FixedSizeList, ListOnItemsRenderedProps } from "react-window";
|
|
11
6
|
import * as utils from "../utils";
|
|
12
7
|
import { DefaultCursor } from "../components/default-cursor";
|
|
13
8
|
import { DefaultRow } from "../components/default-row";
|
|
@@ -73,19 +68,19 @@ export class TreeApi<T extends IdObj> {
|
|
|
73
68
|
/* Tree Props */
|
|
74
69
|
|
|
75
70
|
get width() {
|
|
76
|
-
return this.props.width
|
|
71
|
+
return this.props.width ?? 300;
|
|
77
72
|
}
|
|
78
73
|
|
|
79
74
|
get height() {
|
|
80
|
-
return this.props.height
|
|
75
|
+
return this.props.height ?? 500;
|
|
81
76
|
}
|
|
82
77
|
|
|
83
78
|
get indent() {
|
|
84
|
-
return this.props.indent
|
|
79
|
+
return this.props.indent ?? 24;
|
|
85
80
|
}
|
|
86
81
|
|
|
87
82
|
get rowHeight() {
|
|
88
|
-
return this.props.rowHeight
|
|
83
|
+
return this.props.rowHeight ?? 24;
|
|
89
84
|
}
|
|
90
85
|
|
|
91
86
|
get searchTerm() {
|
|
@@ -102,11 +97,21 @@ export class TreeApi<T extends IdObj> {
|
|
|
102
97
|
return (node: NodeApi<T>) => match(node, this.searchTerm);
|
|
103
98
|
}
|
|
104
99
|
|
|
105
|
-
|
|
106
|
-
const get = this.props.
|
|
100
|
+
accessChildren(data: T) {
|
|
101
|
+
const get = this.props.childrenAccessor || "children";
|
|
107
102
|
return utils.access<T[] | undefined>(data, get) ?? null;
|
|
108
103
|
}
|
|
109
104
|
|
|
105
|
+
accessId(data: T) {
|
|
106
|
+
const get = this.props.idAccessor || "id";
|
|
107
|
+
const id = utils.access<string>(data, get);
|
|
108
|
+
if (!id)
|
|
109
|
+
throw new Error(
|
|
110
|
+
"Data must contain an 'id' property or props.idAccessor must return a string"
|
|
111
|
+
);
|
|
112
|
+
return id;
|
|
113
|
+
}
|
|
114
|
+
|
|
110
115
|
/* Node Access */
|
|
111
116
|
|
|
112
117
|
get firstNode() {
|
|
@@ -171,33 +176,27 @@ export class TreeApi<T extends IdObj> {
|
|
|
171
176
|
}
|
|
172
177
|
|
|
173
178
|
createInternal() {
|
|
174
|
-
return this.create("internal");
|
|
179
|
+
return this.create({ type: "internal" });
|
|
175
180
|
}
|
|
176
181
|
|
|
177
182
|
createLeaf() {
|
|
178
|
-
return this.create("leaf");
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
index = 0;
|
|
189
|
-
} else {
|
|
190
|
-
index = focus.childIndex + 1;
|
|
191
|
-
parentId = focus.parent.isRoot ? null : focus.parent.id;
|
|
192
|
-
}
|
|
193
|
-
} else {
|
|
194
|
-
index = this.root?.children?.length || -1;
|
|
195
|
-
parentId = null;
|
|
196
|
-
}
|
|
183
|
+
return this.create({ type: "leaf" });
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async create(
|
|
187
|
+
opts: {
|
|
188
|
+
type?: "internal" | "leaf";
|
|
189
|
+
parentId?: null | string;
|
|
190
|
+
index?: null | number;
|
|
191
|
+
} = {}
|
|
192
|
+
) {
|
|
197
193
|
const data = await safeRun(this.props.onCreate, {
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
194
|
+
type: opts.type ?? "leaf",
|
|
195
|
+
parentId:
|
|
196
|
+
opts.parentId === undefined
|
|
197
|
+
? utils.getInsertParentId(this)
|
|
198
|
+
: opts.parentId,
|
|
199
|
+
index: opts.index ?? utils.getInsertIndex(this),
|
|
201
200
|
});
|
|
202
201
|
if (data) {
|
|
203
202
|
this.focus(data);
|
|
@@ -279,6 +278,7 @@ export class TreeApi<T extends IdObj> {
|
|
|
279
278
|
} else {
|
|
280
279
|
this.dispatch(focus(identify(node)));
|
|
281
280
|
if (opts.scroll !== false) this.scrollTo(node);
|
|
281
|
+
if (this.focusedNode) safeRun(this.props.onFocus, this.focusedNode);
|
|
282
282
|
}
|
|
283
283
|
}
|
|
284
284
|
|
|
@@ -316,6 +316,7 @@ export class TreeApi<T extends IdObj> {
|
|
|
316
316
|
this.dispatch(selection.anchor(id));
|
|
317
317
|
this.dispatch(selection.mostRecent(id));
|
|
318
318
|
this.scrollTo(id, opts.align);
|
|
319
|
+
if (this.focusedNode) safeRun(this.props.onFocus, this.focusedNode);
|
|
319
320
|
safeRun(this.props.onSelect, this.selectedNodes);
|
|
320
321
|
}
|
|
321
322
|
|
|
@@ -333,6 +334,7 @@ export class TreeApi<T extends IdObj> {
|
|
|
333
334
|
this.dispatch(selection.anchor(node.id));
|
|
334
335
|
this.dispatch(selection.mostRecent(node.id));
|
|
335
336
|
this.scrollTo(node);
|
|
337
|
+
if (this.focusedNode) safeRun(this.props.onFocus, this.focusedNode);
|
|
336
338
|
safeRun(this.props.onSelect, this.selectedNodes);
|
|
337
339
|
}
|
|
338
340
|
|
|
@@ -345,10 +347,11 @@ export class TreeApi<T extends IdObj> {
|
|
|
345
347
|
this.dispatch(selection.add(this.nodesBetween(anchor, identifyNull(id))));
|
|
346
348
|
this.dispatch(selection.mostRecent(id));
|
|
347
349
|
this.scrollTo(id);
|
|
350
|
+
if (this.focusedNode) safeRun(this.props.onFocus, this.focusedNode);
|
|
348
351
|
safeRun(this.props.onSelect, this.selectedNodes);
|
|
349
352
|
}
|
|
350
353
|
|
|
351
|
-
|
|
354
|
+
deselectAll() {
|
|
352
355
|
this.dispatch(selection.clear());
|
|
353
356
|
this.dispatch(selection.anchor(null));
|
|
354
357
|
this.dispatch(selection.mostRecent(null));
|
|
@@ -360,6 +363,7 @@ export class TreeApi<T extends IdObj> {
|
|
|
360
363
|
this.dispatch(focus(this.lastNode?.id));
|
|
361
364
|
this.dispatch(selection.anchor(this.firstNode));
|
|
362
365
|
this.dispatch(selection.mostRecent(this.lastNode));
|
|
366
|
+
if (this.focusedNode) safeRun(this.props.onFocus, this.focusedNode);
|
|
363
367
|
safeRun(this.props.onSelect, this.selectedNodes);
|
|
364
368
|
}
|
|
365
369
|
|
|
@@ -392,13 +396,17 @@ export class TreeApi<T extends IdObj> {
|
|
|
392
396
|
open(identity: Identity) {
|
|
393
397
|
const id = identifyNull(identity);
|
|
394
398
|
if (!id) return;
|
|
399
|
+
if (this.isOpen(id)) return;
|
|
395
400
|
this.dispatch(visibility.open(id, this.isFiltered));
|
|
401
|
+
safeRun(this.props.onToggle, id);
|
|
396
402
|
}
|
|
397
403
|
|
|
398
404
|
close(identity: Identity) {
|
|
399
405
|
const id = identifyNull(identity);
|
|
400
406
|
if (!id) return;
|
|
407
|
+
if (!this.isOpen(id)) return;
|
|
401
408
|
this.dispatch(visibility.close(id, this.isFiltered));
|
|
409
|
+
safeRun(this.props.onToggle, id);
|
|
402
410
|
}
|
|
403
411
|
|
|
404
412
|
toggle(identity: Identity) {
|
|
@@ -434,6 +442,18 @@ export class TreeApi<T extends IdObj> {
|
|
|
434
442
|
}
|
|
435
443
|
}
|
|
436
444
|
|
|
445
|
+
openAll() {
|
|
446
|
+
utils.walk(this.root, (node) => {
|
|
447
|
+
if (node.isInternal) node.open();
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
closeAll() {
|
|
452
|
+
utils.walk(this.root, (node) => {
|
|
453
|
+
if (node.isInternal) node.close();
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
|
|
437
457
|
/* Scrolling */
|
|
438
458
|
|
|
439
459
|
scrollTo(identity: Identity, align: Align = "smart") {
|
|
@@ -466,6 +486,18 @@ export class TreeApi<T extends IdObj> {
|
|
|
466
486
|
return this.state.nodes.focus.treeFocused;
|
|
467
487
|
}
|
|
468
488
|
|
|
489
|
+
get hasNoSelection() {
|
|
490
|
+
return this.state.nodes.selection.ids.size === 0;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
get hasOneSelection() {
|
|
494
|
+
return this.state.nodes.selection.ids.size === 1;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
get hasMultipleSelections() {
|
|
498
|
+
return this.state.nodes.selection.ids.size > 1;
|
|
499
|
+
}
|
|
500
|
+
|
|
469
501
|
isSelected(id?: string) {
|
|
470
502
|
if (!id) return false;
|
|
471
503
|
return this.state.nodes.selection.ids.has(id);
|