react-arborist 0.1.14 → 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} +0 -0
- package/dist/{lib/data → data}/enrich-tree.d.ts +0 -0
- package/dist/{lib/data → data}/flatten-tree.d.ts +0 -0
- package/dist/{lib/dnd → dnd}/compute-drop.d.ts +0 -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} +0 -0
- 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} +1 -1
- package/dist/{lib/types.d.ts → types.d.ts} +4 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/{lib/utils.d.ts → utils.d.ts} +0 -0
- package/package.json +16 -44
- 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.js +0 -146
- package/dist/lib/dnd/drag-hook.js +0 -36
- package/dist/lib/dnd/drop-hook.js +0 -59
- 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 -51
- package/dist/lib/index.d.ts +0 -3
- package/dist/lib/index.js +0 -7
- package/dist/lib/provider.js +0 -46
- package/dist/lib/reducer.js +0 -147
- 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 -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}>
|
|
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
|
+
});
|
package/src/context.tsx
ADDED
|
@@ -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
|
+
}
|