react-arborist 0.1.12 → 0.2.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{lib/components → components}/drop-cursor.d.ts +1 -0
- package/dist/{lib/components → components}/preview.d.ts +1 -0
- package/dist/{lib/components → components}/row.d.ts +0 -0
- package/dist/{lib/components → components}/tree.d.ts +0 -0
- package/dist/{lib/context.d.ts → context.d.ts} +4 -3
- package/dist/{lib/data → data}/enrich-tree.d.ts +1 -1
- package/dist/{lib/data → data}/flatten-tree.d.ts +0 -0
- package/dist/dnd/compute-drop.d.ts +37 -0
- package/dist/{lib/dnd → dnd}/drag-hook.d.ts +0 -0
- package/dist/{lib/dnd → dnd}/drop-hook.d.ts +0 -0
- package/dist/{lib/dnd → dnd}/outer-drop-hook.d.ts +0 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +1354 -0
- package/dist/index.js.map +1 -0
- package/dist/module.js +1346 -0
- package/dist/module.js.map +1 -0
- package/dist/{lib/provider.d.ts → provider.d.ts} +1 -0
- package/dist/{lib/reducer.d.ts → reducer.d.ts} +4 -3
- package/dist/{lib/selection → selection}/range.d.ts +0 -0
- package/dist/{lib/selection → selection}/selection-hook.d.ts +0 -0
- package/dist/{lib/selection → selection}/selection.d.ts +0 -0
- package/dist/{lib/tree-api-hook.d.ts → tree-api-hook.d.ts} +0 -0
- package/dist/{lib/tree-api.d.ts → tree-api.d.ts} +4 -3
- package/dist/{lib/types.d.ts → types.d.ts} +8 -3
- package/dist/types.d.ts.map +1 -0
- package/dist/{lib/utils.d.ts → utils.d.ts} +2 -0
- package/package.json +16 -43
- package/src/components/drop-cursor.tsx +47 -0
- package/src/components/preview.tsx +108 -0
- package/src/components/row.tsx +119 -0
- package/src/components/tree.tsx +118 -0
- package/src/context.tsx +52 -0
- package/src/data/enrich-tree.ts +74 -0
- package/src/data/flatten-tree.ts +17 -0
- package/src/data/make-tree.ts +37 -0
- package/src/dnd/compute-drop.ts +184 -0
- package/src/dnd/drag-hook.ts +48 -0
- package/src/dnd/drop-hook.ts +66 -0
- package/src/dnd/measure-hover.ts +26 -0
- package/src/dnd/outer-drop-hook.ts +50 -0
- package/src/index.ts +5 -0
- package/src/provider.tsx +61 -0
- package/src/reducer.ts +161 -0
- package/src/selection/range.ts +41 -0
- package/src/selection/selection-hook.ts +24 -0
- package/src/selection/selection.test.ts +111 -0
- package/src/selection/selection.ts +186 -0
- package/src/tree-api-hook.ts +34 -0
- package/src/tree-api.ts +129 -0
- package/src/types.ts +147 -0
- package/src/utils.ts +35 -0
- package/tsconfig.json +28 -0
- package/README.md +0 -197
- package/dist/lib/components/drop-cursor.js +0 -53
- package/dist/lib/components/preview.js +0 -91
- package/dist/lib/components/row.js +0 -122
- package/dist/lib/components/tree.js +0 -76
- package/dist/lib/context.js +0 -57
- package/dist/lib/data/enrich-tree.js +0 -48
- package/dist/lib/data/flatten-tree.js +0 -20
- package/dist/lib/data/make-tree.d.ts +0 -5
- package/dist/lib/data/make-tree.js +0 -40
- package/dist/lib/data/visible-nodes-hook.d.ts +0 -2
- package/dist/lib/data/visible-nodes-hook.js +0 -19
- package/dist/lib/dnd/compute-drop.d.ts +0 -24
- package/dist/lib/dnd/compute-drop.js +0 -113
- package/dist/lib/dnd/drag-hook.js +0 -36
- package/dist/lib/dnd/drop-hook.js +0 -60
- package/dist/lib/dnd/measure-hover.d.ts +0 -8
- package/dist/lib/dnd/measure-hover.js +0 -21
- package/dist/lib/dnd/outer-drop-hook.js +0 -50
- package/dist/lib/index.d.ts +0 -3
- package/dist/lib/index.js +0 -7
- package/dist/lib/provider.js +0 -44
- package/dist/lib/reducer.js +0 -149
- package/dist/lib/selection/range.js +0 -45
- package/dist/lib/selection/selection-hook.js +0 -24
- package/dist/lib/selection/selection.js +0 -192
- package/dist/lib/selection/selection.test.d.ts +0 -1
- package/dist/lib/selection/selection.test.js +0 -102
- package/dist/lib/tree-api-hook.js +0 -26
- package/dist/lib/tree-api.js +0 -130
- package/dist/lib/tree-monitor.d.ts +0 -15
- package/dist/lib/tree-monitor.js +0 -32
- package/dist/lib/types.js +0 -2
- package/dist/lib/utils.js +0 -31
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import React, { useCallback, useMemo, useRef } from "react";
|
|
2
|
+
import {
|
|
3
|
+
useCursorParentId,
|
|
4
|
+
useEditingId,
|
|
5
|
+
useIsCursorOverFolder,
|
|
6
|
+
useIsSelected,
|
|
7
|
+
useStaticContext,
|
|
8
|
+
} from "../context";
|
|
9
|
+
import { useDragHook } from "../dnd/drag-hook";
|
|
10
|
+
import { useDropHook } from "../dnd/drop-hook";
|
|
11
|
+
|
|
12
|
+
type Props = {
|
|
13
|
+
style: React.CSSProperties;
|
|
14
|
+
index: number;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const Row = React.memo(function Row({ index, style }: Props) {
|
|
18
|
+
const tree = useStaticContext();
|
|
19
|
+
const selected = useIsSelected();
|
|
20
|
+
const node = tree.api.visibleNodes[index];
|
|
21
|
+
const next = tree.api.visibleNodes[index + 1] || null;
|
|
22
|
+
const prev = tree.api.visibleNodes[index - 1] || null;
|
|
23
|
+
const cursorParentId = useCursorParentId();
|
|
24
|
+
const cursorOverFolder = useIsCursorOverFolder();
|
|
25
|
+
const el = useRef<HTMLDivElement | null>(null);
|
|
26
|
+
const [{ isDragging }, dragRef] = useDragHook(node);
|
|
27
|
+
const [, dropRef] = useDropHook(el, node, prev, next);
|
|
28
|
+
const isEditing = node.id === useEditingId();
|
|
29
|
+
const isSelected = selected(index);
|
|
30
|
+
const nextSelected = next && selected(index + 1);
|
|
31
|
+
const prevSelected = prev && selected(index - 1);
|
|
32
|
+
const isHoveringOverChild = node.id === cursorParentId;
|
|
33
|
+
const isOverFolder = node.id === cursorParentId && cursorOverFolder;
|
|
34
|
+
const isOpen = node.isOpen;
|
|
35
|
+
const indent = tree.indent * node.level;
|
|
36
|
+
const state = useMemo(() => {
|
|
37
|
+
return {
|
|
38
|
+
isEditing,
|
|
39
|
+
isDragging,
|
|
40
|
+
isFirstOfSelected: isSelected && !prevSelected,
|
|
41
|
+
isLastOfSelected: isSelected && !nextSelected,
|
|
42
|
+
isSelected,
|
|
43
|
+
isHoveringOverChild,
|
|
44
|
+
isOpen,
|
|
45
|
+
isOverFolder,
|
|
46
|
+
};
|
|
47
|
+
}, [
|
|
48
|
+
isEditing,
|
|
49
|
+
isSelected,
|
|
50
|
+
prevSelected,
|
|
51
|
+
nextSelected,
|
|
52
|
+
isHoveringOverChild,
|
|
53
|
+
isOpen,
|
|
54
|
+
isDragging,
|
|
55
|
+
isOverFolder,
|
|
56
|
+
]);
|
|
57
|
+
if (isSelected) {
|
|
58
|
+
console.log({id: node.id, state})
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const ref = useCallback(
|
|
62
|
+
(n: HTMLDivElement | null) => {
|
|
63
|
+
el.current = n;
|
|
64
|
+
dragRef(dropRef(n));
|
|
65
|
+
},
|
|
66
|
+
[dragRef, dropRef]
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
const styles = useMemo(
|
|
70
|
+
() => ({
|
|
71
|
+
row: { ...style },
|
|
72
|
+
indent: { paddingLeft: indent },
|
|
73
|
+
}),
|
|
74
|
+
[indent, style]
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
const handlers = useMemo(() => {
|
|
78
|
+
return {
|
|
79
|
+
select: (e: React.MouseEvent, selectOnClick: boolean = true) => {
|
|
80
|
+
if (node.rowIndex === null) return;
|
|
81
|
+
if (selectOnClick || e.metaKey || e.shiftKey) {
|
|
82
|
+
tree.api.select(node.rowIndex, e.metaKey, e.shiftKey);
|
|
83
|
+
} else {
|
|
84
|
+
tree.api.select(null, false, false);
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
toggle: (e: React.MouseEvent) => {
|
|
88
|
+
e.stopPropagation();
|
|
89
|
+
tree.onToggle(node.id, !node.isOpen);
|
|
90
|
+
},
|
|
91
|
+
edit: () => {
|
|
92
|
+
tree.api.edit(node.id);
|
|
93
|
+
},
|
|
94
|
+
submit: (name: string) => {
|
|
95
|
+
if (name.trim()) tree.onEdit(node.id, name);
|
|
96
|
+
tree.api.edit(null);
|
|
97
|
+
},
|
|
98
|
+
reset: () => {
|
|
99
|
+
tree.api.edit(null);
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
}, [tree, node]);
|
|
103
|
+
|
|
104
|
+
const Renderer = useMemo(() => {
|
|
105
|
+
return React.memo(tree.renderer);
|
|
106
|
+
}, [tree.renderer]);
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<Renderer
|
|
110
|
+
innerRef={ref}
|
|
111
|
+
data={node.model}
|
|
112
|
+
styles={styles}
|
|
113
|
+
state={state}
|
|
114
|
+
handlers={handlers}
|
|
115
|
+
preview={false}
|
|
116
|
+
tree={tree.api}
|
|
117
|
+
/>
|
|
118
|
+
);
|
|
119
|
+
});
|
|
@@ -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
|
+
}
|