react-arborist 0.1.14 → 0.2.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.
Files changed (84) hide show
  1. package/dist/{lib/components → components}/drop-cursor.d.ts +1 -0
  2. package/dist/{lib/components → components}/preview.d.ts +1 -0
  3. package/dist/{lib/components → components}/row.d.ts +0 -0
  4. package/dist/{lib/components → components}/tree.d.ts +0 -0
  5. package/dist/{lib/context.d.ts → context.d.ts} +0 -0
  6. package/dist/{lib/data → data}/enrich-tree.d.ts +0 -0
  7. package/dist/{lib/data → data}/flatten-tree.d.ts +0 -0
  8. package/dist/{lib/dnd → dnd}/compute-drop.d.ts +0 -0
  9. package/dist/{lib/dnd → dnd}/drag-hook.d.ts +0 -0
  10. package/dist/{lib/dnd → dnd}/drop-hook.d.ts +0 -0
  11. package/dist/{lib/dnd → dnd}/outer-drop-hook.d.ts +0 -0
  12. package/dist/index.d.ts +4 -0
  13. package/dist/index.js +1354 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/module.js +1346 -0
  16. package/dist/module.js.map +1 -0
  17. package/dist/{lib/provider.d.ts → provider.d.ts} +1 -0
  18. package/dist/{lib/reducer.d.ts → reducer.d.ts} +0 -0
  19. package/dist/{lib/selection → selection}/range.d.ts +0 -0
  20. package/dist/{lib/selection → selection}/selection-hook.d.ts +0 -0
  21. package/dist/{lib/selection → selection}/selection.d.ts +0 -0
  22. package/dist/{lib/tree-api-hook.d.ts → tree-api-hook.d.ts} +0 -0
  23. package/dist/{lib/tree-api.d.ts → tree-api.d.ts} +1 -1
  24. package/dist/{lib/types.d.ts → types.d.ts} +4 -0
  25. package/dist/{lib/utils.d.ts → utils.d.ts} +0 -0
  26. package/package.json +18 -45
  27. package/src/components/drop-cursor.tsx +47 -0
  28. package/src/components/preview.tsx +108 -0
  29. package/src/components/row.tsx +119 -0
  30. package/src/components/tree.tsx +118 -0
  31. package/src/context.tsx +52 -0
  32. package/src/data/enrich-tree.ts +74 -0
  33. package/src/data/flatten-tree.ts +17 -0
  34. package/src/data/make-tree.ts +37 -0
  35. package/src/dnd/compute-drop.ts +184 -0
  36. package/src/dnd/drag-hook.ts +48 -0
  37. package/src/dnd/drop-hook.ts +66 -0
  38. package/src/dnd/measure-hover.ts +26 -0
  39. package/src/dnd/outer-drop-hook.ts +50 -0
  40. package/src/index.ts +5 -0
  41. package/src/provider.tsx +61 -0
  42. package/src/reducer.ts +161 -0
  43. package/src/selection/range.ts +41 -0
  44. package/src/selection/selection-hook.ts +24 -0
  45. package/src/selection/selection.test.ts +111 -0
  46. package/src/selection/selection.ts +186 -0
  47. package/src/tree-api-hook.ts +34 -0
  48. package/src/tree-api.ts +129 -0
  49. package/src/types.ts +147 -0
  50. package/src/utils.ts +35 -0
  51. package/tsconfig.json +28 -0
  52. package/README.md +0 -197
  53. package/dist/lib/components/drop-cursor.js +0 -53
  54. package/dist/lib/components/preview.js +0 -91
  55. package/dist/lib/components/row.js +0 -122
  56. package/dist/lib/components/tree.js +0 -76
  57. package/dist/lib/context.js +0 -57
  58. package/dist/lib/data/enrich-tree.js +0 -48
  59. package/dist/lib/data/flatten-tree.js +0 -20
  60. package/dist/lib/data/make-tree.d.ts +0 -5
  61. package/dist/lib/data/make-tree.js +0 -40
  62. package/dist/lib/data/visible-nodes-hook.d.ts +0 -2
  63. package/dist/lib/data/visible-nodes-hook.js +0 -19
  64. package/dist/lib/dnd/compute-drop.js +0 -146
  65. package/dist/lib/dnd/drag-hook.js +0 -36
  66. package/dist/lib/dnd/drop-hook.js +0 -59
  67. package/dist/lib/dnd/measure-hover.d.ts +0 -8
  68. package/dist/lib/dnd/measure-hover.js +0 -21
  69. package/dist/lib/dnd/outer-drop-hook.js +0 -51
  70. package/dist/lib/index.d.ts +0 -3
  71. package/dist/lib/index.js +0 -7
  72. package/dist/lib/provider.js +0 -46
  73. package/dist/lib/reducer.js +0 -147
  74. package/dist/lib/selection/range.js +0 -45
  75. package/dist/lib/selection/selection-hook.js +0 -24
  76. package/dist/lib/selection/selection.js +0 -192
  77. package/dist/lib/selection/selection.test.d.ts +0 -1
  78. package/dist/lib/selection/selection.test.js +0 -102
  79. package/dist/lib/tree-api-hook.js +0 -26
  80. package/dist/lib/tree-api.js +0 -130
  81. package/dist/lib/tree-monitor.d.ts +0 -15
  82. package/dist/lib/tree-monitor.js +0 -32
  83. package/dist/lib/types.js +0 -2
  84. package/dist/lib/utils.js +0 -39
@@ -0,0 +1,118 @@
1
+ import { forwardRef, MouseEventHandler, ReactElement, useMemo, useRef } from "react";
2
+ import { DndProvider } from "react-dnd";
3
+ import { HTML5Backend } from "react-dnd-html5-backend";
4
+ import { FixedSizeList } from "react-window";
5
+ import { useStaticContext } from "../context";
6
+ import { enrichTree } from "../data/enrich-tree";
7
+ import { useOuterDrop } from "../dnd/outer-drop-hook";
8
+ import { TreeViewProvider } from "../provider";
9
+ import { TreeApi } from "../tree-api";
10
+ import { IdObj, Node, TreeProps } from "../types";
11
+ import { noop } from "../utils";
12
+ import { DropCursor } from "./drop-cursor";
13
+ import { Preview } from "./preview";
14
+ import { Row } from "./row";
15
+
16
+ const OuterElement = forwardRef(function Outer(
17
+ props: React.HTMLProps<HTMLDivElement>,
18
+ ref
19
+ ) {
20
+ const { children, ...rest } = props;
21
+ const tree = useStaticContext();
22
+ return (
23
+ // @ts-ignore
24
+ <div ref={ref} {...rest} onClick={tree.onClick} onContextMenu={tree.onContextMenu}>
25
+ <div
26
+ style={{
27
+ height: tree.api.visibleNodes.length * tree.rowHeight,
28
+ width: "100%",
29
+ overflow: "hidden",
30
+ position: "absolute",
31
+ left: "0",
32
+ right: "0",
33
+ }}
34
+ >
35
+ <DropCursor />
36
+ </div>
37
+ {children}
38
+ </div>
39
+ );
40
+ });
41
+
42
+ function List(props: { className?: string}) {
43
+ const tree = useStaticContext();
44
+ return (
45
+ <div style={{ height: tree.height, width: tree.width, overflow: "hidden" }}>
46
+ <FixedSizeList
47
+ className={props.className}
48
+ outerRef={tree.listEl}
49
+ itemCount={tree.api.visibleNodes.length}
50
+ height={tree.height}
51
+ width={tree.width}
52
+ itemSize={tree.rowHeight}
53
+ itemKey={(index) => tree.api.visibleNodes[index]?.id || index}
54
+ outerElementType={OuterElement}
55
+ // @ts-ignore
56
+ ref={tree.list}
57
+ >
58
+ {Row}
59
+ </FixedSizeList>
60
+ </div>
61
+ );
62
+ }
63
+
64
+ function OuterDrop(props: { children: ReactElement }) {
65
+ useOuterDrop();
66
+ return props.children;
67
+ }
68
+
69
+ export const Tree = forwardRef(function Tree<T extends IdObj>(
70
+ props: TreeProps<T>,
71
+ ref: React.Ref<TreeApi<T>>
72
+ ) {
73
+ const root = useMemo<Node<T>>(
74
+ () =>
75
+ enrichTree<T>(
76
+ props.data,
77
+ props.hideRoot,
78
+ props.getChildren,
79
+ props.isOpen,
80
+ props.disableDrag,
81
+ props.disableDrop,
82
+ props.openByDefault
83
+ ),
84
+ [
85
+ props.data,
86
+ props.hideRoot,
87
+ props.getChildren,
88
+ props.isOpen,
89
+ props.disableDrag,
90
+ props.disableDrop,
91
+ props.openByDefault,
92
+ ]
93
+ );
94
+ return (
95
+ <TreeViewProvider
96
+ imperativeHandle={ref}
97
+ root={root}
98
+ listEl={useRef<HTMLDivElement | null>(null)}
99
+ renderer={props.children}
100
+ width={props.width === undefined ? 300 : props.width}
101
+ height={props.height === undefined ? 500 : props.height}
102
+ indent={props.indent === undefined ? 24 : props.indent}
103
+ rowHeight={props.rowHeight === undefined ? 24 : props.rowHeight}
104
+ onMove={props.onMove || noop}
105
+ onToggle={props.onToggle || noop}
106
+ onEdit={props.onEdit || noop}
107
+ onClick={props.onClick}
108
+ onContextMenu={props.onContextMenu}
109
+ >
110
+ <DndProvider backend={HTML5Backend}>
111
+ <OuterDrop>
112
+ <List className={props.className}/>
113
+ </OuterDrop>
114
+ <Preview />
115
+ </DndProvider>
116
+ </TreeViewProvider>
117
+ );
118
+ });
@@ -0,0 +1,52 @@
1
+ import { createContext, useContext, useMemo } from "react";
2
+ import { Cursor } from "./dnd/compute-drop";
3
+ import { Selection } from "./selection/selection";
4
+ import { IdObj, SelectionState, StaticContext } from "./types";
5
+
6
+ export const CursorParentId = createContext<string | null>(null);
7
+ export function useCursorParentId() {
8
+ return useContext(CursorParentId);
9
+ }
10
+
11
+ export const IsCursorOverFolder = createContext<boolean>(false);
12
+ export function useIsCursorOverFolder() {
13
+ return useContext(IsCursorOverFolder);
14
+ }
15
+
16
+ export const CursorLocationContext = createContext<Cursor | null>(null);
17
+ export function useCursorLocation() {
18
+ return useContext(CursorLocationContext);
19
+ }
20
+
21
+ export const Static = createContext<StaticContext<IdObj> | null>(null);
22
+ export function useStaticContext() {
23
+ const value = useContext(Static);
24
+ if (!value) throw new Error("Context must be in a provider");
25
+ return value;
26
+ }
27
+
28
+ export const DispatchContext = createContext(null);
29
+ export function useDispatch() {
30
+ const dispatch = useContext(DispatchContext);
31
+ if (!dispatch) throw new Error("No dispatch provided");
32
+ return dispatch;
33
+ }
34
+
35
+ export const SelectionContext = createContext<SelectionState | null>(null);
36
+ export function useSelectedIds(): string[] {
37
+ const value = useContext(SelectionContext);
38
+ if (!value) throw new Error("Must provide selection context");
39
+ return value.ids;
40
+ }
41
+
42
+ export function useIsSelected(): (index: number | null) => boolean {
43
+ const value = useContext(SelectionContext);
44
+ if (!value) throw new Error("Must provide selection context");
45
+ const s = useMemo(() => Selection.parse(value.data, []), [value.data]);
46
+ return (i) => s.contains(i);
47
+ }
48
+
49
+ export const EditingIdContext = createContext<string | null>(null);
50
+ export function useEditingId(): string | null {
51
+ return useContext(EditingIdContext);
52
+ }
@@ -0,0 +1,74 @@
1
+ import { TreeProps, IdObj, Node } from "../types";
2
+
3
+ function createNode<T extends IdObj>(
4
+ model: T,
5
+ level: number,
6
+ parent: Node<T> | null,
7
+ children: Node<T>[] | null,
8
+ isOpen: boolean,
9
+ isDraggable: boolean,
10
+ isDroppable: boolean
11
+ ): Node<T> {
12
+ return {
13
+ id: model.id,
14
+ level,
15
+ parent,
16
+ children,
17
+ isOpen,
18
+ isDraggable,
19
+ isDroppable,
20
+ model,
21
+ rowIndex: null,
22
+ };
23
+ }
24
+
25
+ function access(obj: any, accessor: string | boolean | Function) {
26
+ if (typeof accessor === "boolean") {
27
+ return accessor;
28
+ }
29
+
30
+ if (typeof accessor === "string") {
31
+ return obj[accessor];
32
+ }
33
+
34
+ return accessor(obj);
35
+ }
36
+
37
+ export function enrichTree<T extends IdObj>(
38
+ model: T,
39
+ hideRoot: boolean = false,
40
+ getChildren: TreeProps<T>["getChildren"] = "children",
41
+ isOpen: TreeProps<T>["isOpen"] = "isOpen",
42
+ disableDrag: TreeProps<T>["disableDrag"] = false,
43
+ disableDrop: TreeProps<T>["disableDrop"] = false,
44
+ openByDefault: boolean = true
45
+ ): Node<T> {
46
+ function visitSelfAndChildren(
47
+ model: T,
48
+ level: number,
49
+ parent: Node<T> | null
50
+ ) {
51
+ const open = access(model, isOpen) as boolean;
52
+ const draggable = !access(model, disableDrag) as boolean;
53
+ const droppable = !access(model, disableDrop) as boolean;
54
+ const node = createNode<T>(
55
+ model,
56
+ level,
57
+ parent,
58
+ null,
59
+ open === undefined ? openByDefault : open,
60
+ draggable,
61
+ droppable
62
+ );
63
+ const children = access(model, getChildren) as T[];
64
+
65
+ if (children) {
66
+ node.children = children.map((child: T) =>
67
+ visitSelfAndChildren(child, level + 1, node)
68
+ );
69
+ }
70
+ return node;
71
+ }
72
+
73
+ return visitSelfAndChildren(model, hideRoot ? -1 : 0, null);
74
+ }
@@ -0,0 +1,17 @@
1
+ import { Node } from "../types";
2
+
3
+ export function flattenTree<T>(root: Node<T>): Node<T>[] {
4
+ const list: Node<T>[] = [];
5
+ let index = 0;
6
+ function collect(node: Node<T>) {
7
+ if (node.level >= 0) {
8
+ node.rowIndex = index++;
9
+ list.push(node);
10
+ }
11
+ if (node.isOpen) {
12
+ node.children?.forEach(collect);
13
+ }
14
+ }
15
+ collect(root);
16
+ return list;
17
+ }
@@ -0,0 +1,37 @@
1
+ // A function that turns a string of text into a tree
2
+ // Each line is a node
3
+ // The number of spaces at the beginning indicate the level
4
+
5
+ export function makeTree(string: string) {
6
+ const root = { id: "ROOT", name: "ROOT", isOpen: true };
7
+ let prevNode = root;
8
+ let prevLevel = -1;
9
+ let id = 1;
10
+ string.split("\n").forEach((line) => {
11
+ const name = line.trimStart();
12
+ const level = line.length - name.length;
13
+ const diff = level - prevLevel;
14
+ const node = { id: (id++).toString(), name, isOpen: false };
15
+ if (diff === 1) {
16
+ // First child
17
+ //@ts-ignore
18
+ node.parent = prevNode;
19
+ //@ts-ignore
20
+ prevNode.children = [node];
21
+ } else {
22
+ // Find the parent and go up
23
+ //@ts-ignore
24
+ let parent = prevNode.parent;
25
+ for (let i = diff; i < 0; i++) {
26
+ parent = parent.parent;
27
+ }
28
+ //@ts-ignore
29
+ node.parent = parent;
30
+ parent.children.push(node);
31
+ }
32
+ prevNode = node;
33
+ prevLevel = level;
34
+ });
35
+
36
+ return root;
37
+ }
@@ -0,0 +1,184 @@
1
+ import { XYCoord } from "react-dnd";
2
+ import { Node } from "../types";
3
+ import { bound, indexOf, isClosed, isFolder, isItem } from "../utils";
4
+ import { DropResult } from "./drop-hook";
5
+
6
+ function measureHover(el: HTMLElement, offset: XYCoord) {
7
+ const rect = el.getBoundingClientRect();
8
+ const x = offset.x - Math.round(rect.x);
9
+ const y = offset.y - Math.round(rect.y);
10
+ const height = rect.height;
11
+ const inTopHalf = y < height / 2;
12
+ const inBottomHalf = !inTopHalf;
13
+ const pad = height / 4;
14
+ const inMiddle = y > pad && y < height - pad;
15
+ const atTop = !inMiddle && inTopHalf;
16
+ const atBottom = !inMiddle && inBottomHalf;
17
+ return { x, inTopHalf, inBottomHalf, inMiddle, atTop, atBottom };
18
+ }
19
+
20
+ type HoverData = ReturnType<typeof measureHover>;
21
+
22
+ function getNodesAroundCursor(
23
+ node: Node | null,
24
+ prev: Node | null,
25
+ next: Node | null,
26
+ hover: HoverData
27
+ ): [Node | null, Node | null] {
28
+ if (!node) {
29
+ // We're hoving over the empty part of the list, not over an item,
30
+ // Put the cursor below the last item which is "prev"
31
+ return [prev, null];
32
+ }
33
+ if (isFolder(node)) {
34
+ if (hover.atTop) {
35
+ return [prev, node];
36
+ } else if (hover.inMiddle) {
37
+ return [node, node];
38
+ } else {
39
+ return [node, next];
40
+ }
41
+ } else {
42
+ if (hover.inTopHalf) {
43
+ return [prev, node];
44
+ } else {
45
+ return [node, next];
46
+ }
47
+ }
48
+ }
49
+
50
+ type Args = {
51
+ element: HTMLElement;
52
+ offset: XYCoord;
53
+ indent: number;
54
+ node: Node | null;
55
+ prevNode: Node | null;
56
+ nextNode: Node | null;
57
+ };
58
+
59
+ function getDropLevel(
60
+ hovering: HoverData,
61
+ aboveCursor: Node | null,
62
+ belowCursor: Node | null,
63
+ indent: number
64
+ ) {
65
+ const hoverLevel = Math.round(Math.max(0, hovering.x - indent) / indent);
66
+ let min, max;
67
+ if (!aboveCursor) {
68
+ max = 0;
69
+ min = 0;
70
+ } else if (!belowCursor) {
71
+ max = aboveCursor.level;
72
+ min = 0;
73
+ } else {
74
+ max = aboveCursor.level;
75
+ min = belowCursor.level;
76
+ }
77
+
78
+ return bound(hoverLevel, min, max);
79
+ }
80
+
81
+ function canDrop(above: Node | null, below: Node | null) {
82
+ if (!above) {
83
+ return true;
84
+ }
85
+
86
+ let n: Node | null = above;
87
+ if (isClosed(above) && above !== below) n = above.parent;
88
+
89
+ while (n) {
90
+ if (!n.isDroppable) return false;
91
+ n = n.parent;
92
+ }
93
+ return true;
94
+ }
95
+
96
+ export type ComputedDrop = {
97
+ drop: DropResult | null;
98
+ cursor: Cursor | null;
99
+ };
100
+
101
+ function dropAt(parentId: string | undefined, index: number): DropResult {
102
+ return { parentId: parentId || null, index };
103
+ }
104
+
105
+ function lineCursor(index: number, level: number) {
106
+ return {
107
+ type: "line" as "line",
108
+ index,
109
+ level,
110
+ };
111
+ }
112
+
113
+ function noCursor() {
114
+ return {
115
+ type: "none" as "none",
116
+ };
117
+ }
118
+
119
+ function highlightCursor(id: string) {
120
+ return {
121
+ type: "highlight" as "highlight",
122
+ id,
123
+ };
124
+ }
125
+
126
+ function walkUpFrom(node: Node, level: number) {
127
+ let drop = node;
128
+ while (drop.parent && drop.level > level) {
129
+ drop = drop.parent;
130
+ }
131
+ const parentId = drop.parent?.id || null;
132
+ const index = indexOf(drop) + 1;
133
+ return { parentId, index };
134
+ }
135
+
136
+ export type LineCursor = ReturnType<typeof lineCursor>;
137
+ export type NoCursor = ReturnType<typeof noCursor>;
138
+ export type HighlightCursor = ReturnType<typeof highlightCursor>;
139
+ export type Cursor = LineCursor | NoCursor | HighlightCursor;
140
+
141
+ /**
142
+ * This is the most complex, tricky function in the whole repo.
143
+ * It could be simplified and made more understandable.
144
+ */
145
+ export function computeDrop(args: Args): ComputedDrop {
146
+ const hover = measureHover(args.element, args.offset);
147
+ const { node, nextNode, prevNode } = args;
148
+ const [above, below] = getNodesAroundCursor(node, prevNode, nextNode, hover);
149
+
150
+ if (!canDrop(above, below)) {
151
+ return { drop: null, cursor: noCursor() };
152
+ }
153
+
154
+ /* Hovering over the middle of a folder */
155
+ if (node && isFolder(node) && hover.inMiddle) {
156
+ return {
157
+ drop: dropAt(node.id, 0),
158
+ cursor: highlightCursor(node.id),
159
+ };
160
+ }
161
+
162
+ /* At the top of the list */
163
+ if (!above) {
164
+ return {
165
+ drop: dropAt(below?.parent?.id, 0),
166
+ cursor: lineCursor(0, 0),
167
+ };
168
+ }
169
+
170
+ /* The above node is an item or a closed folder */
171
+ if (isItem(above) || isClosed(above)) {
172
+ const level = getDropLevel(hover, above, below, args.indent);
173
+ return {
174
+ drop: walkUpFrom(above, level),
175
+ cursor: lineCursor(above.rowIndex! + 1, level),
176
+ };
177
+ }
178
+
179
+ /* The above node is an open folder */
180
+ return {
181
+ drop: dropAt(above?.id, 0),
182
+ cursor: lineCursor(above.rowIndex! + 1, above.level + 1),
183
+ };
184
+ }
@@ -0,0 +1,48 @@
1
+ import { useEffect } from "react";
2
+ import { ConnectDragSource, useDrag } from "react-dnd";
3
+ import { getEmptyImage } from "react-dnd-html5-backend";
4
+ import { useIsSelected, useSelectedIds, useStaticContext } from "../context";
5
+ import { DragItem, Node } from "../types";
6
+ import { DropResult } from "./drop-hook";
7
+
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
+ >(
21
+ () => ({
22
+ canDrag: () => node.isDraggable,
23
+ type: "NODE",
24
+ item: () => ({
25
+ id: node.id,
26
+ dragIds: isSelected(node.rowIndex) ? ids : [node.id],
27
+ }),
28
+ collect: (m) => ({
29
+ isDragging: m.isDragging(),
30
+ }),
31
+ end: (item, monitor) => {
32
+ tree.api.hideCursor();
33
+ const drop = monitor.getDropResult();
34
+ if (drop && drop.parentId) {
35
+ tree.onMove(item.dragIds, drop.parentId, drop.index);
36
+ tree.onToggle(drop.parentId, true);
37
+ }
38
+ },
39
+ }),
40
+ [ids, node]
41
+ );
42
+
43
+ useEffect(() => {
44
+ preview(getEmptyImage());
45
+ }, [preview]);
46
+
47
+ return [{ isDragging }, ref];
48
+ }
@@ -0,0 +1,66 @@
1
+ import { RefObject } from "react";
2
+ import { ConnectDropTarget, useDrop } from "react-dnd";
3
+ import { useStaticContext } from "../context";
4
+ import { DragItem, Node } from "../types";
5
+ import { isDecendent, isFolder } from "../utils";
6
+ import { computeDrop } from "./compute-drop";
7
+
8
+ export type DropResult = {
9
+ parentId: string | null;
10
+ index: number;
11
+ };
12
+
13
+ export type CollectedProps = undefined;
14
+
15
+ export function useDropHook(
16
+ 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>(
23
+ () => ({
24
+ accept: "NODE",
25
+ canDrop: (item) => {
26
+ for (let id of item.dragIds) {
27
+ const drag = tree.api.getNode(id);
28
+ if (!drag) return false;
29
+ if (isFolder(drag) && isDecendent(node, drag)) return false;
30
+ }
31
+ return true;
32
+ },
33
+ hover: (item, m) => {
34
+ if (m.canDrop()) {
35
+ const offset = m.getClientOffset();
36
+ if (!el.current || !offset) return;
37
+ const { cursor } = computeDrop({
38
+ element: el.current,
39
+ offset: offset,
40
+ indent: tree.indent,
41
+ node: node,
42
+ prevNode: prev,
43
+ nextNode: next,
44
+ });
45
+ if (cursor) tree.api.showCursor(cursor);
46
+ } else {
47
+ tree.api.hideCursor();
48
+ }
49
+ },
50
+ drop: (item, m): DropResult | undefined | null => {
51
+ const offset = m.getClientOffset();
52
+ if (!el.current || !offset) return;
53
+ const { drop } = computeDrop({
54
+ element: el.current,
55
+ offset: offset,
56
+ indent: tree.indent,
57
+ node: node,
58
+ prevNode: prev,
59
+ nextNode: next,
60
+ });
61
+ return drop;
62
+ },
63
+ }),
64
+ [node, prev, el, tree]
65
+ );
66
+ }
@@ -0,0 +1,26 @@
1
+ import { XYCoord } from "react-dnd";
2
+ import { bound } from "../utils";
3
+
4
+ export function measureHover(el: HTMLElement, offset: XYCoord, indent: number) {
5
+ const nextEl = el.nextElementSibling as HTMLElement | null;
6
+ const prevEl = el.previousElementSibling as HTMLElement | null;
7
+ const rect = el.getBoundingClientRect();
8
+ const x = offset.x - Math.round(rect.x);
9
+ const y = offset.y - Math.round(rect.y);
10
+ const height = rect.height;
11
+ const inTopHalf = y < height / 2;
12
+ const inBottomHalf = !inTopHalf;
13
+ const pad = height / 4;
14
+ const inMiddle = y > pad && y < height - pad;
15
+ const maxLevel = Number(
16
+ inBottomHalf ? el.dataset.level : prevEl ? prevEl.dataset.level : 0
17
+ );
18
+ const minLevel = Number(
19
+ inTopHalf ? el.dataset.level : nextEl ? nextEl.dataset.level : 0
20
+ );
21
+ const level = bound(Math.floor(x / indent), minLevel, maxLevel);
22
+
23
+ return { level, inTopHalf, inBottomHalf, inMiddle };
24
+ }
25
+
26
+ export type HoverData = ReturnType<typeof measureHover>;
@@ -0,0 +1,50 @@
1
+ import { useDrop } from "react-dnd";
2
+ import { useStaticContext } from "../context";
3
+ import { DragItem } from "../types";
4
+ import { computeDrop } from "./compute-drop";
5
+ import { DropResult } from "./drop-hook";
6
+
7
+ export function useOuterDrop() {
8
+ const tree = useStaticContext();
9
+
10
+ // In case we drop an item at the bottom of the list
11
+ const [, drop] = useDrop<DragItem, DropResult | null, { isOver: boolean }>(
12
+ () => ({
13
+ accept: "NODE",
14
+ hover: (item, m) => {
15
+ if (!m.isOver({ shallow: true })) return;
16
+ const offset = m.getClientOffset();
17
+ if (!tree.listEl.current || !offset) return;
18
+ const { cursor } = computeDrop({
19
+ element: tree.listEl.current,
20
+ offset: offset,
21
+ indent: tree.indent,
22
+ node: null,
23
+ prevNode: tree.api.visibleNodes[tree.api.visibleNodes.length - 1],
24
+ nextNode: null,
25
+ });
26
+ if (cursor) tree.api.showCursor(cursor);
27
+ },
28
+ canDrop: (item, m) => {
29
+ return m.isOver({ shallow: true });
30
+ },
31
+ drop: (item, m) => {
32
+ if (m.didDrop()) return;
33
+ const offset = m.getClientOffset();
34
+ if (!tree.listEl.current || !offset) return;
35
+ const { drop } = computeDrop({
36
+ element: tree.listEl.current,
37
+ offset: offset,
38
+ indent: tree.indent,
39
+ node: null,
40
+ prevNode: tree.api.visibleNodes[tree.api.visibleNodes.length - 1],
41
+ nextNode: null,
42
+ });
43
+ return drop;
44
+ },
45
+ }),
46
+ [tree]
47
+ );
48
+
49
+ drop(tree.listEl);
50
+ }
package/src/index.ts ADDED
@@ -0,0 +1,5 @@
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 };