react-arborist 1.1.0 → 2.0.0-rc

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.
Files changed (122) hide show
  1. package/dist/components/{drop-cursor.d.ts → cursor.d.ts} +0 -0
  2. package/dist/components/default-container.d.ts +2 -0
  3. package/dist/components/default-cursor.d.ts +3 -0
  4. package/dist/components/default-drag-preview.d.ts +3 -0
  5. package/dist/components/default-node.d.ts +4 -0
  6. package/dist/components/default-row.d.ts +4 -0
  7. package/dist/components/drag-preview-container.d.ts +2 -0
  8. package/dist/components/list-inner-element.d.ts +2 -0
  9. package/dist/components/list-outer-element.d.ts +2 -0
  10. package/dist/components/outer-drop.d.ts +4 -0
  11. package/dist/components/provider.d.ts +11 -0
  12. package/dist/components/row-container.d.ts +8 -0
  13. package/dist/components/tree-container.d.ts +2 -0
  14. package/dist/components/tree.d.ts +5 -4
  15. package/dist/context.d.ts +23 -18
  16. package/dist/data/create-index.d.ts +5 -0
  17. package/dist/data/create-list.d.ts +4 -0
  18. package/dist/data/create-root.d.ts +5 -0
  19. package/dist/data/flatten-tree.d.ts +4 -2
  20. package/dist/data/simple-tree.d.ts +43 -0
  21. package/dist/dnd/compute-drop.d.ts +4 -4
  22. package/dist/dnd/drag-hook.d.ts +3 -4
  23. package/dist/dnd/drop-hook.d.ts +2 -3
  24. package/dist/hooks/use-fresh-node.d.ts +2 -0
  25. package/dist/hooks/use-simple-tree.d.ts +13 -0
  26. package/dist/hooks/use-uncontrolled-tree.d.ts +24 -0
  27. package/dist/hooks/use-validated-props.d.ts +3 -0
  28. package/dist/index.d.ts +8 -4
  29. package/dist/index.js +2093 -1184
  30. package/dist/index.js.map +1 -1
  31. package/dist/interfaces/node-api.d.ts +67 -0
  32. package/dist/interfaces/tree-api.d.ts +112 -0
  33. package/dist/module.js +2082 -1192
  34. package/dist/module.js.map +1 -1
  35. package/dist/state/dnd-slice.d.ts +20 -0
  36. package/dist/state/drag-slice.d.ts +7 -0
  37. package/dist/state/edit-slice.d.ts +8 -0
  38. package/dist/state/focus-slice.d.ts +12 -0
  39. package/dist/state/initial.d.ts +3 -0
  40. package/dist/state/open-slice.d.ts +30 -0
  41. package/dist/state/root-reducer.d.ts +13 -0
  42. package/dist/state/selection-slice.d.ts +36 -0
  43. package/dist/types/dnd.d.ts +9 -0
  44. package/dist/types/handlers.d.ts +24 -0
  45. package/dist/types/renderers.d.ts +30 -0
  46. package/dist/types/state.d.ts +2 -0
  47. package/dist/types/tree-props.d.ts +43 -0
  48. package/dist/types/utils.d.ts +21 -0
  49. package/dist/utils/props.d.ts +3 -0
  50. package/dist/utils.d.ts +15 -6
  51. package/package.json +10 -7
  52. package/src/components/cursor.tsx +15 -0
  53. package/src/components/default-container.tsx +229 -0
  54. package/src/components/{drop-cursor.tsx → default-cursor.tsx} +15 -20
  55. package/src/components/default-drag-preview.tsx +92 -0
  56. package/src/components/default-node.tsx +15 -0
  57. package/src/components/default-row.tsx +21 -0
  58. package/src/components/drag-preview-container.tsx +26 -0
  59. package/src/components/list-inner-element.tsx +22 -0
  60. package/src/components/list-outer-element.tsx +45 -0
  61. package/src/components/outer-drop.ts +7 -0
  62. package/src/components/provider.tsx +97 -0
  63. package/src/components/row-container.tsx +82 -0
  64. package/src/components/tree-container.tsx +13 -0
  65. package/src/components/tree.tsx +17 -126
  66. package/src/context.ts +36 -0
  67. package/src/data/create-index.ts +9 -0
  68. package/src/data/create-list.ts +56 -0
  69. package/src/data/create-root.ts +53 -0
  70. package/src/data/simple-tree.ts +103 -0
  71. package/src/dnd/compute-drop.ts +16 -16
  72. package/src/dnd/drag-hook.ts +28 -23
  73. package/src/dnd/drop-hook.ts +35 -21
  74. package/src/dnd/outer-drop-hook.ts +6 -6
  75. package/src/hooks/use-fresh-node.ts +16 -0
  76. package/src/hooks/use-simple-tree.ts +55 -0
  77. package/src/hooks/use-validated-props.ts +35 -0
  78. package/src/index.ts +9 -5
  79. package/src/interfaces/node-api.ts +187 -0
  80. package/src/interfaces/tree-api.ts +552 -0
  81. package/src/state/dnd-slice.ts +36 -0
  82. package/src/state/drag-slice.ts +31 -0
  83. package/src/state/edit-slice.ts +19 -0
  84. package/src/state/focus-slice.ts +28 -0
  85. package/src/state/initial.ts +14 -0
  86. package/src/state/open-slice.ts +53 -0
  87. package/src/state/root-reducer.ts +21 -0
  88. package/src/state/selection-slice.ts +75 -0
  89. package/src/types/dnd.ts +10 -0
  90. package/src/types/handlers.ts +24 -0
  91. package/src/types/renderers.ts +34 -0
  92. package/src/types/state.ts +3 -0
  93. package/src/types/tree-props.ts +63 -0
  94. package/src/types/utils.ts +26 -0
  95. package/src/utils/props.ts +8 -0
  96. package/src/utils.ts +125 -11
  97. package/README.md +0 -220
  98. package/dist/components/preview.d.ts +0 -2
  99. package/dist/components/row.d.ts +0 -7
  100. package/dist/data/enrich-tree.d.ts +0 -2
  101. package/dist/provider.d.ts +0 -3
  102. package/dist/reducer.d.ts +0 -46
  103. package/dist/selection/range.d.ts +0 -13
  104. package/dist/selection/selection-hook.d.ts +0 -3
  105. package/dist/selection/selection.d.ts +0 -33
  106. package/dist/tree-api-hook.d.ts +0 -6
  107. package/dist/tree-api.d.ts +0 -34
  108. package/dist/types.d.ts +0 -131
  109. package/src/components/preview.tsx +0 -108
  110. package/src/components/row.tsx +0 -114
  111. package/src/context.tsx +0 -52
  112. package/src/data/enrich-tree.ts +0 -74
  113. package/src/data/flatten-tree.ts +0 -17
  114. package/src/provider.tsx +0 -61
  115. package/src/reducer.ts +0 -161
  116. package/src/selection/range.ts +0 -41
  117. package/src/selection/selection-hook.ts +0 -24
  118. package/src/selection/selection.test.ts +0 -111
  119. package/src/selection/selection.ts +0 -186
  120. package/src/tree-api-hook.ts +0 -34
  121. package/src/tree-api.ts +0 -156
  122. package/src/types.ts +0 -155
@@ -0,0 +1,53 @@
1
+ import { IdObj } from "../types/utils";
2
+ import { NodeApi } from "../interfaces/node-api";
3
+ import { TreeApi } from "../interfaces/tree-api";
4
+
5
+ export const ROOT_ID = "__REACT_ARBORIST_INTERNAL_ROOT__";
6
+
7
+ export function createRoot<T extends IdObj>(tree: TreeApi<T>): NodeApi<T> {
8
+ function visitSelfAndChildren(
9
+ data: T,
10
+ level: number,
11
+ parent: NodeApi<T> | null
12
+ ) {
13
+ const node = new NodeApi<T>({
14
+ tree,
15
+ data,
16
+ level,
17
+ parent,
18
+ id: data.id,
19
+ children: null,
20
+ isDraggable: tree.isDraggable(data),
21
+ isDroppable: tree.isDroppable(data),
22
+ rowIndex: null,
23
+ });
24
+ const children = tree.getChildren(data);
25
+ if (children) {
26
+ node.children = children.map((child: T) =>
27
+ visitSelfAndChildren(child, level + 1, node)
28
+ );
29
+ }
30
+ return node;
31
+ }
32
+
33
+ const root = new NodeApi<T>({
34
+ tree,
35
+ id: ROOT_ID,
36
+ // @ts-ignore
37
+ data: { id: ROOT_ID },
38
+ level: -1,
39
+ parent: null,
40
+ children: null,
41
+ isDraggable: true,
42
+ isDroppable: true,
43
+ rowIndex: null,
44
+ });
45
+
46
+ const data: T[] = tree.props.data ?? [];
47
+
48
+ root.children = data.map((child) => {
49
+ return visitSelfAndChildren(child, 0, root);
50
+ });
51
+
52
+ return root;
53
+ }
@@ -0,0 +1,103 @@
1
+ type IdObj = { id: string; children?: IdObj[] };
2
+
3
+ export class SimpleTree<T extends IdObj> {
4
+ root: SimpleNode<T>;
5
+ constructor(data: T[]) {
6
+ this.root = createRoot<T>(data);
7
+ }
8
+
9
+ get data() {
10
+ return this.root.children?.map((node) => node.data) ?? [];
11
+ }
12
+
13
+ create(args: { parentId: string | null; index: number; data: T }) {
14
+ const parent = args.parentId ? this.find(args.parentId) : this.root;
15
+ if (!parent) return null;
16
+ parent.addChild(args.data, args.index);
17
+ }
18
+
19
+ move(args: { id: string; parentId: string | null; index: number }) {
20
+ const src = this.find(args.id);
21
+ const parent = args.parentId ? this.find(args.parentId) : this.root;
22
+ if (!src || !parent) return;
23
+ parent.addChild(src.data, args.index);
24
+ src.drop();
25
+ }
26
+
27
+ update(args: { id: string; changes: Partial<T> }) {
28
+ const node = this.find(args.id);
29
+ if (node) node.update(args.changes);
30
+ }
31
+
32
+ drop(args: { id: string }) {
33
+ const node = this.find(args.id);
34
+ if (node) node.drop();
35
+ }
36
+
37
+ find(id: string, node: SimpleNode<T> = this.root): SimpleNode<T> | null {
38
+ if (!node) return null;
39
+ if (node.id === id) return node as SimpleNode<T>;
40
+ if (node.children) {
41
+ for (let child of node.children) {
42
+ const found = this.find(id, child);
43
+ if (found) return found;
44
+ }
45
+ return null;
46
+ }
47
+ return null;
48
+ }
49
+ }
50
+
51
+ function createRoot<T extends IdObj>(data: T[]) {
52
+ const root = new SimpleNode<T>({ id: "ROOT" } as T, null);
53
+ root.children = data.map((d) => createNode(d as T, root));
54
+ return root;
55
+ }
56
+
57
+ function createNode<T extends IdObj>(data: T, parent: SimpleNode<T>) {
58
+ const node = new SimpleNode<T>(data, parent);
59
+ if (data.children)
60
+ node.children = data.children.map((d) => createNode<T>(d as T, node));
61
+ return node;
62
+ }
63
+
64
+ class SimpleNode<T extends IdObj> {
65
+ id: string;
66
+ children?: SimpleNode<T>[];
67
+ constructor(public data: T, public parent: SimpleNode<T> | null) {
68
+ this.id = data.id;
69
+ }
70
+
71
+ hasParent(): this is this & { parent: SimpleNode<T> } {
72
+ return !!this.parent;
73
+ }
74
+
75
+ get childIndex(): number {
76
+ return this.hasParent() ? this.parent.children!.indexOf(this) : -1;
77
+ }
78
+
79
+ addChild(data: T, index: number) {
80
+ const node = createNode(data, this);
81
+ this.children = this.children ?? [];
82
+ this.children.splice(index, 0, node);
83
+ this.data.children = this.data.children ?? [];
84
+ this.data.children.splice(index, 0, data);
85
+ }
86
+
87
+ removeChild(index: number) {
88
+ this.children?.splice(index, 1);
89
+ this.data.children?.splice(index, 1);
90
+ }
91
+
92
+ update(changes: Partial<T>) {
93
+ if (this.hasParent()) {
94
+ const i = this.childIndex;
95
+ this.parent.addChild({ ...this.data, ...changes }, i);
96
+ this.drop();
97
+ }
98
+ }
99
+
100
+ drop() {
101
+ if (this.hasParent()) this.parent.removeChild(this.childIndex);
102
+ }
103
+ }
@@ -1,6 +1,6 @@
1
1
  import { XYCoord } from "react-dnd";
2
- import { Node } from "../types";
3
- import { bound, indexOf, isClosed, isFolder, isItem } from "../utils";
2
+ import { NodeApi } from "../interfaces/node-api";
3
+ import { bound, indexOf, isClosed, isItem } from "../utils";
4
4
  import { DropResult } from "./drop-hook";
5
5
 
6
6
  function measureHover(el: HTMLElement, offset: XYCoord) {
@@ -20,17 +20,17 @@ function measureHover(el: HTMLElement, offset: XYCoord) {
20
20
  type HoverData = ReturnType<typeof measureHover>;
21
21
 
22
22
  function getNodesAroundCursor(
23
- node: Node | null,
24
- prev: Node | null,
25
- next: Node | null,
23
+ node: NodeApi | null,
24
+ prev: NodeApi | null,
25
+ next: NodeApi | null,
26
26
  hover: HoverData
27
- ): [Node | null, Node | null] {
27
+ ): [NodeApi | null, NodeApi | null] {
28
28
  if (!node) {
29
29
  // We're hoving over the empty part of the list, not over an item,
30
30
  // Put the cursor below the last item which is "prev"
31
31
  return [prev, null];
32
32
  }
33
- if (isFolder(node)) {
33
+ if (node.isInternal) {
34
34
  if (hover.atTop) {
35
35
  return [prev, node];
36
36
  } else if (hover.inMiddle) {
@@ -51,15 +51,15 @@ type Args = {
51
51
  element: HTMLElement;
52
52
  offset: XYCoord;
53
53
  indent: number;
54
- node: Node | null;
55
- prevNode: Node | null;
56
- nextNode: Node | null;
54
+ node: NodeApi | null;
55
+ prevNode: NodeApi | null;
56
+ nextNode: NodeApi | null;
57
57
  };
58
58
 
59
59
  function getDropLevel(
60
60
  hovering: HoverData,
61
- aboveCursor: Node | null,
62
- belowCursor: Node | null,
61
+ aboveCursor: NodeApi | null,
62
+ belowCursor: NodeApi | null,
63
63
  indent: number
64
64
  ) {
65
65
  const hoverLevel = Math.round(Math.max(0, hovering.x - indent) / indent);
@@ -78,12 +78,12 @@ function getDropLevel(
78
78
  return bound(hoverLevel, min, max);
79
79
  }
80
80
 
81
- function canDrop(above: Node | null, below: Node | null) {
81
+ function canDrop(above: NodeApi | null, below: NodeApi | null) {
82
82
  if (!above) {
83
83
  return true;
84
84
  }
85
85
 
86
- let n: Node | null = above;
86
+ let n: NodeApi | null = above;
87
87
  if (isClosed(above) && above !== below) n = above.parent;
88
88
 
89
89
  while (n) {
@@ -123,7 +123,7 @@ function highlightCursor(id: string) {
123
123
  };
124
124
  }
125
125
 
126
- function walkUpFrom(node: Node, level: number) {
126
+ function walkUpFrom(node: NodeApi, level: number) {
127
127
  let drop = node;
128
128
  while (drop.parent && drop.level > level) {
129
129
  drop = drop.parent;
@@ -152,7 +152,7 @@ export function computeDrop(args: Args): ComputedDrop {
152
152
  }
153
153
 
154
154
  /* Hovering over the middle of a folder */
155
- if (node && isFolder(node) && hover.inMiddle) {
155
+ if (node && node.isInternal && hover.inMiddle) {
156
156
  return {
157
157
  drop: dropAt(node.id, 0),
158
158
  cursor: highlightCursor(node.id),
@@ -1,39 +1,44 @@
1
1
  import { useEffect } from "react";
2
2
  import { ConnectDragSource, useDrag } from "react-dnd";
3
3
  import { getEmptyImage } from "react-dnd-html5-backend";
4
- import { useIsSelected, useSelectedIds, useStaticContext } from "../context";
5
- import { DragItem, Node } from "../types";
4
+ import { useTreeApi } from "../context";
5
+ import { NodeApi } from "../interfaces/node-api";
6
+ import { DragItem } from "../types/dnd";
7
+ import { IdObj } from "../types/utils";
6
8
  import { DropResult } from "./drop-hook";
9
+ import { actions as dnd } from "../state/dnd-slice";
10
+ import { safeRun } from "../utils";
11
+ import { ROOT_ID } from "../data/create-root";
7
12
 
8
- type CollectedProps = { isDragging: boolean };
9
-
10
- export function useDragHook(
11
- node: Node
12
- ): [{ isDragging: boolean }, ConnectDragSource] {
13
- const tree = useStaticContext();
14
- const isSelected = useIsSelected();
15
- const ids = useSelectedIds();
16
- const [{ isDragging }, ref, preview] = useDrag<
17
- DragItem,
18
- DropResult,
19
- CollectedProps
20
- >(
13
+ export function useDragHook<T extends IdObj>(
14
+ node: NodeApi<T>
15
+ ): ConnectDragSource {
16
+ const tree = useTreeApi();
17
+ const ids = tree.selectedIds;
18
+ const [_, ref, preview] = useDrag<DragItem, DropResult, void>(
21
19
  () => ({
22
20
  canDrag: () => node.isDraggable,
23
21
  type: "NODE",
24
22
  item: () => ({
25
23
  id: node.id,
26
- dragIds: isSelected(node.rowIndex) ? ids : [node.id],
27
- }),
28
- collect: (m) => ({
29
- isDragging: m.isDragging(),
24
+ dragIds: tree.isSelected(node.id) ? Array.from(ids) : [node.id],
30
25
  }),
26
+ start: () => {
27
+ tree.dispatch(dnd.dragStart(node.id));
28
+ },
31
29
  end: (item, monitor) => {
32
- tree.api.hideCursor();
30
+ tree.dispatch(dnd.dragEnd());
31
+ tree.hideCursor();
33
32
  const drop = monitor.getDropResult();
33
+ // If they held down meta, we need to create a copy
34
+ // if (drop.dropEffect === "copy")
34
35
  if (drop && drop.parentId) {
35
- tree.onMove(item.dragIds, drop.parentId, drop.index);
36
- tree.onToggle(drop.parentId, true);
36
+ safeRun(tree.props.onMove, {
37
+ dragIds: item.dragIds,
38
+ parentId: drop.parentId === ROOT_ID ? null : drop.parentId,
39
+ index: drop.index,
40
+ });
41
+ tree.open(drop.parentId);
37
42
  }
38
43
  },
39
44
  }),
@@ -44,5 +49,5 @@ export function useDragHook(
44
49
  preview(getEmptyImage());
45
50
  }, [preview]);
46
51
 
47
- return [{ isDragging }, ref];
52
+ return ref;
48
53
  }
@@ -1,8 +1,9 @@
1
1
  import { RefObject } from "react";
2
2
  import { ConnectDropTarget, useDrop } from "react-dnd";
3
- import { useStaticContext } from "../context";
4
- import { DragItem, Node } from "../types";
5
- import { isDecendent, isFolder } from "../utils";
3
+ import { useTreeApi } from "../context";
4
+ import { NodeApi } from "../interfaces/node-api";
5
+ import { DragItem } from "../types/dnd";
6
+ import { isDecendent } from "../utils";
6
7
  import { computeDrop } from "./compute-drop";
7
8
 
8
9
  export type DropResult = {
@@ -10,23 +11,34 @@ export type DropResult = {
10
11
  index: number;
11
12
  };
12
13
 
13
- export type CollectedProps = undefined;
14
-
15
14
  export function useDropHook(
16
15
  el: RefObject<HTMLElement | null>,
17
- node: Node,
18
- prev: Node | null,
19
- next: Node | null
20
- ): [CollectedProps, ConnectDropTarget] {
21
- const tree = useStaticContext();
22
- return useDrop<DragItem, DropResult | null, CollectedProps>(
16
+ node: NodeApi<any>
17
+ ): ConnectDropTarget {
18
+ const tree = useTreeApi();
19
+ const [_, dropRef] = useDrop<DragItem, DropResult | null, void>(
23
20
  () => ({
24
21
  accept: "NODE",
25
- canDrop: (item) => {
22
+ canDrop: (item, m) => {
23
+ if (node.tree.isFiltered) return false;
24
+ const offset = m.getClientOffset();
25
+ if (!el.current || !offset) return false;
26
+ const { drop } = computeDrop({
27
+ element: el.current,
28
+ offset: offset,
29
+ indent: tree.indent,
30
+ node: node,
31
+ prevNode: node.prev,
32
+ nextNode: node.next,
33
+ });
34
+ if (!drop) return false;
35
+ const dropParent = tree.get(drop.parentId) ?? tree.root;
36
+
26
37
  for (let id of item.dragIds) {
27
- const drag = tree.api.getNode(id);
38
+ const drag = tree.get(id);
28
39
  if (!drag) return false;
29
- if (isFolder(drag) && isDecendent(node, drag)) return false;
40
+ if (!dropParent) return false;
41
+ if (drag.isInternal && isDecendent(dropParent, drag)) return false;
30
42
  }
31
43
  return true;
32
44
  },
@@ -39,12 +51,12 @@ export function useDropHook(
39
51
  offset: offset,
40
52
  indent: tree.indent,
41
53
  node: node,
42
- prevNode: prev,
43
- nextNode: next,
54
+ prevNode: node.prev,
55
+ nextNode: node.next,
44
56
  });
45
- if (cursor) tree.api.showCursor(cursor);
57
+ if (cursor) tree.showCursor(cursor);
46
58
  } else {
47
- tree.api.hideCursor();
59
+ tree.hideCursor();
48
60
  }
49
61
  },
50
62
  drop: (item, m): DropResult | undefined | null => {
@@ -55,12 +67,14 @@ export function useDropHook(
55
67
  offset: offset,
56
68
  indent: tree.indent,
57
69
  node: node,
58
- prevNode: prev,
59
- nextNode: next,
70
+ prevNode: node.prev,
71
+ nextNode: node.next,
60
72
  });
61
73
  return drop;
62
74
  },
63
75
  }),
64
- [node, prev, el, tree]
76
+ [node, el.current, tree.props]
65
77
  );
78
+
79
+ return dropRef;
66
80
  }
@@ -1,11 +1,11 @@
1
1
  import { useDrop } from "react-dnd";
2
- import { useStaticContext } from "../context";
3
- import { DragItem } from "../types";
2
+ import { useTreeApi } from "../context";
3
+ import { DragItem } from "../types/dnd";
4
4
  import { computeDrop } from "./compute-drop";
5
5
  import { DropResult } from "./drop-hook";
6
6
 
7
7
  export function useOuterDrop() {
8
- const tree = useStaticContext();
8
+ const tree = useTreeApi();
9
9
 
10
10
  // In case we drop an item at the bottom of the list
11
11
  const [, drop] = useDrop<DragItem, DropResult | null, { isOver: boolean }>(
@@ -20,10 +20,10 @@ export function useOuterDrop() {
20
20
  offset: offset,
21
21
  indent: tree.indent,
22
22
  node: null,
23
- prevNode: tree.api.visibleNodes[tree.api.visibleNodes.length - 1],
23
+ prevNode: tree.visibleNodes[tree.visibleNodes.length - 1],
24
24
  nextNode: null,
25
25
  });
26
- if (cursor) tree.api.showCursor(cursor);
26
+ if (cursor) tree.showCursor(cursor);
27
27
  },
28
28
  canDrop: (item, m) => {
29
29
  return m.isOver({ shallow: true });
@@ -37,7 +37,7 @@ export function useOuterDrop() {
37
37
  offset: offset,
38
38
  indent: tree.indent,
39
39
  node: null,
40
- prevNode: tree.api.visibleNodes[tree.api.visibleNodes.length - 1],
40
+ prevNode: tree.visibleNodes[tree.visibleNodes.length - 1],
41
41
  nextNode: null,
42
42
  });
43
43
  return drop;
@@ -0,0 +1,16 @@
1
+ import { useMemo } from "react";
2
+ import { useTreeApi } from "../context";
3
+ import { IdObj } from "../types/utils";
4
+
5
+ export function useFreshNode<T extends IdObj>(index: number) {
6
+ const tree = useTreeApi<T>();
7
+ const original = tree.at(index);
8
+ if (!original) throw new Error(`Could not find node for index: ${index}`);
9
+
10
+ return useMemo(() => {
11
+ const fresh = original.clone();
12
+ tree.visibleNodes[index] = fresh; // sneaky
13
+ return fresh;
14
+ // Return a fresh instance if the state values change
15
+ }, [...Object.values(original.state), original]);
16
+ }
@@ -0,0 +1,55 @@
1
+ import { useMemo, useState } from "react";
2
+ import { SimpleTree } from "../data/simple-tree";
3
+ import {
4
+ CreateHandler,
5
+ DeleteHandler,
6
+ MoveHandler,
7
+ RenameHandler,
8
+ } from "../types/handlers";
9
+ import { IdObj } from "../types/utils";
10
+
11
+ export type SimpleTreeData = {
12
+ id: string;
13
+ name: string;
14
+ children?: SimpleTreeData[];
15
+ };
16
+
17
+ let nextId = 0;
18
+
19
+ export function useSimpleTree<T extends IdObj>(initialData: T[]) {
20
+ const [data, setData] = useState(initialData);
21
+ const tree = useMemo(() => new SimpleTree<T>(data), [data]);
22
+
23
+ const onMove: MoveHandler = (args: {
24
+ dragIds: string[];
25
+ parentId: null | string;
26
+ index: number;
27
+ }) => {
28
+ for (const id of args.dragIds) {
29
+ tree.move({ id, parentId: args.parentId, index: args.index });
30
+ }
31
+ setData(tree.data);
32
+ };
33
+
34
+ const onRename: RenameHandler = ({ name, id }) => {
35
+ tree.update({ id, changes: { name } as any });
36
+ setData(tree.data);
37
+ };
38
+
39
+ const onCreate: CreateHandler = ({ parentId, index, type }) => {
40
+ const data = { id: `simple-tree-id-${nextId++}`, name: "" } as any;
41
+ if (type === "internal") data.children = [];
42
+ tree.create({ parentId, index, data });
43
+ setData(tree.data);
44
+ return data;
45
+ };
46
+
47
+ const onDelete: DeleteHandler = (args: { ids: string[] }) => {
48
+ args.ids.forEach((id) => tree.drop({ id }));
49
+ setData(tree.data);
50
+ };
51
+
52
+ const controller = { onMove, onRename, onCreate, onDelete };
53
+
54
+ return [data, controller] as const;
55
+ }
@@ -0,0 +1,35 @@
1
+ import { TreeProps } from "../types/tree-props";
2
+ import { IdObj } from "../types/utils";
3
+ import { SimpleTreeData, useSimpleTree } from "./use-simple-tree";
4
+
5
+ export function useValidatedProps<T extends IdObj>(
6
+ props: TreeProps<T>
7
+ ): TreeProps<T> {
8
+ if (props.initialData && props.data) {
9
+ throw new Error(
10
+ `React Arborist Tree => Provide either a data or initialData prop, but not both.`
11
+ );
12
+ }
13
+ if (
14
+ props.initialData &&
15
+ (props.onCreate || props.onDelete || props.onMove || props.onRename)
16
+ ) {
17
+ throw new Error(
18
+ `React Arborist Tree => You passed the initialData prop along with a data handler.
19
+ Use the data prop if you want to provide your own handlers.`
20
+ );
21
+ }
22
+ if (props.initialData) {
23
+ /**
24
+ * Let's break the rules of hooks here. If the initialData prop
25
+ * is provided, we will assume it will not change for the life of
26
+ * the component.
27
+ *
28
+ * We will provide the real data and the handlers to update it.
29
+ * */
30
+ const [data, controller] = useSimpleTree<T>(props.initialData);
31
+ return { ...props, ...controller, data };
32
+ } else {
33
+ return props;
34
+ }
35
+ }
package/src/index.ts CHANGED
@@ -1,5 +1,9 @@
1
- import { Tree } from "./components/tree";
2
- import { TreeApi } from "./tree-api";
3
- import type { NodeRenderer, NodeState, NodeHandlers } from "./types";
4
-
5
- export { Tree, TreeApi, NodeRenderer, NodeState, NodeHandlers };
1
+ /* The Public Api */
2
+ export { Tree } from "./components/tree";
3
+ export * from "./types/handlers";
4
+ export * from "./types/renderers";
5
+ export * from "./types/state";
6
+ export * from "./interfaces/node-api";
7
+ export * from "./interfaces/tree-api";
8
+ export * from "./data/simple-tree";
9
+ export * from "./hooks/use-simple-tree";