react-arborist 1.2.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.
- package/dist/components/{drop-cursor.d.ts → cursor.d.ts} +0 -0
- package/dist/components/default-container.d.ts +2 -0
- package/dist/components/default-cursor.d.ts +3 -0
- package/dist/components/default-drag-preview.d.ts +3 -0
- package/dist/components/default-node.d.ts +4 -0
- package/dist/components/default-row.d.ts +4 -0
- package/dist/components/drag-preview-container.d.ts +2 -0
- package/dist/components/list-inner-element.d.ts +2 -0
- package/dist/components/provider.d.ts +11 -0
- package/dist/components/row-container.d.ts +8 -0
- package/dist/components/tree-container.d.ts +2 -0
- package/dist/components/tree.d.ts +5 -4
- package/dist/context.d.ts +20 -2
- package/dist/data/create-index.d.ts +5 -0
- package/dist/data/create-list.d.ts +4 -0
- package/dist/data/create-root.d.ts +5 -0
- package/dist/data/flatten-tree.d.ts +4 -2
- package/dist/data/simple-tree.d.ts +43 -0
- package/dist/dnd/compute-drop.d.ts +4 -4
- package/dist/dnd/drag-hook.d.ts +3 -4
- package/dist/dnd/drop-hook.d.ts +2 -3
- package/dist/hooks/use-fresh-node.d.ts +2 -0
- package/dist/hooks/use-simple-tree.d.ts +13 -0
- package/dist/hooks/use-uncontrolled-tree.d.ts +24 -0
- package/dist/hooks/use-validated-props.d.ts +3 -0
- package/dist/index.d.ts +8 -4
- package/dist/index.js +1900 -971
- package/dist/index.js.map +1 -1
- package/dist/interfaces/node-api.d.ts +67 -0
- package/dist/interfaces/tree-api.d.ts +112 -0
- package/dist/module.js +1886 -976
- package/dist/module.js.map +1 -1
- package/dist/state/dnd-slice.d.ts +20 -0
- package/dist/state/drag-slice.d.ts +7 -0
- package/dist/state/edit-slice.d.ts +8 -0
- package/dist/state/focus-slice.d.ts +12 -0
- package/dist/state/initial.d.ts +3 -0
- package/dist/state/open-slice.d.ts +30 -0
- package/dist/state/root-reducer.d.ts +13 -0
- package/dist/state/selection-slice.d.ts +36 -0
- package/dist/types/dnd.d.ts +9 -0
- package/dist/types/handlers.d.ts +24 -0
- package/dist/types/renderers.d.ts +30 -0
- package/dist/types/state.d.ts +2 -0
- package/dist/types/tree-props.d.ts +43 -0
- package/dist/types/utils.d.ts +21 -0
- package/dist/utils/props.d.ts +3 -0
- package/dist/utils.d.ts +15 -6
- package/package.json +10 -7
- package/src/components/cursor.tsx +15 -0
- package/src/components/default-container.tsx +229 -0
- package/src/components/{default-drop-cursor.tsx → default-cursor.tsx} +9 -8
- package/src/components/{preview.tsx → default-drag-preview.tsx} +25 -41
- package/src/components/default-node.tsx +15 -0
- package/src/components/default-row.tsx +21 -0
- package/src/components/drag-preview-container.tsx +26 -0
- package/src/components/list-inner-element.tsx +22 -0
- package/src/components/list-outer-element.tsx +26 -15
- package/src/components/provider.tsx +97 -0
- package/src/components/row-container.tsx +82 -0
- package/src/components/tree-container.tsx +13 -0
- package/src/components/tree.tsx +16 -44
- package/src/context.ts +36 -0
- package/src/data/create-index.ts +9 -0
- package/src/data/create-list.ts +56 -0
- package/src/data/create-root.ts +53 -0
- package/src/data/simple-tree.ts +103 -0
- package/src/dnd/compute-drop.ts +16 -16
- package/src/dnd/drag-hook.ts +25 -19
- package/src/dnd/drop-hook.ts +31 -17
- package/src/dnd/outer-drop-hook.ts +1 -1
- package/src/hooks/use-fresh-node.ts +16 -0
- package/src/hooks/use-simple-tree.ts +55 -0
- package/src/hooks/use-validated-props.ts +35 -0
- package/src/index.ts +9 -19
- package/src/interfaces/node-api.ts +187 -0
- package/src/interfaces/tree-api.ts +552 -0
- package/src/state/dnd-slice.ts +36 -0
- package/src/state/drag-slice.ts +31 -0
- package/src/state/edit-slice.ts +19 -0
- package/src/state/focus-slice.ts +28 -0
- package/src/state/initial.ts +14 -0
- package/src/state/open-slice.ts +53 -0
- package/src/state/root-reducer.ts +21 -0
- package/src/state/selection-slice.ts +75 -0
- package/src/types/dnd.ts +10 -0
- package/src/types/handlers.ts +24 -0
- package/src/types/renderers.ts +34 -0
- package/src/types/state.ts +3 -0
- package/src/types/tree-props.ts +63 -0
- package/src/types/utils.ts +26 -0
- package/src/utils/props.ts +8 -0
- package/src/utils.ts +125 -11
- package/README.md +0 -221
- package/dist/components/default-drop-cursor.d.ts +0 -3
- package/dist/components/list.d.ts +0 -4
- package/dist/components/preview.d.ts +0 -2
- package/dist/components/row.d.ts +0 -8
- package/dist/data/enrich-tree.d.ts +0 -2
- package/dist/provider.d.ts +0 -3
- package/dist/reducer.d.ts +0 -46
- package/dist/selection/range.d.ts +0 -13
- package/dist/selection/selection-hook.d.ts +0 -4
- package/dist/selection/selection.d.ts +0 -33
- package/dist/tree-api.d.ts +0 -50
- package/dist/types.d.ts +0 -122
- package/src/components/drop-cursor.tsx +0 -12
- package/src/components/list.tsx +0 -25
- package/src/components/row.tsx +0 -112
- package/src/context.tsx +0 -13
- package/src/data/enrich-tree.ts +0 -74
- package/src/data/flatten-tree.ts +0 -17
- package/src/provider.tsx +0 -41
- package/src/reducer.ts +0 -161
- package/src/selection/range.ts +0 -41
- package/src/selection/selection-hook.ts +0 -25
- package/src/selection/selection.test.ts +0 -111
- package/src/selection/selection.ts +0 -186
- package/src/tree-api.ts +0 -230
- package/src/types.ts +0 -148
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import React, { CSSProperties, memo } from "react";
|
|
2
|
-
import {
|
|
2
|
+
import { XYCoord } from "react-dnd";
|
|
3
3
|
import { useTreeApi } from "../context";
|
|
4
|
-
import {
|
|
4
|
+
import { DragPreviewProps } from "../types/renderers";
|
|
5
|
+
import { IdObj } from "../types/utils";
|
|
5
6
|
|
|
6
7
|
const layerStyles: CSSProperties = {
|
|
7
8
|
position: "fixed",
|
|
@@ -25,20 +26,19 @@ const getCountStyle = (offset: XYCoord | null) => {
|
|
|
25
26
|
return { transform: `translate(${x + 10}px, ${y + 10}px)` };
|
|
26
27
|
};
|
|
27
28
|
|
|
28
|
-
export function
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
29
|
+
export function DefaultDragPreview({
|
|
30
|
+
offset,
|
|
31
|
+
mouse,
|
|
32
|
+
id,
|
|
33
|
+
dragIds,
|
|
34
|
+
isDragging,
|
|
35
|
+
}: DragPreviewProps) {
|
|
36
36
|
return (
|
|
37
37
|
<Overlay isDragging={isDragging}>
|
|
38
38
|
<Position offset={offset}>
|
|
39
|
-
<PreviewNode
|
|
39
|
+
<PreviewNode id={id} dragIds={dragIds} />
|
|
40
40
|
</Position>
|
|
41
|
-
<Count mouse={mouse}
|
|
41
|
+
<Count mouse={mouse} count={dragIds.length} />
|
|
42
42
|
</Overlay>
|
|
43
43
|
);
|
|
44
44
|
}
|
|
@@ -59,50 +59,34 @@ function Position(props: { children: JSX.Element; offset: XYCoord | null }) {
|
|
|
59
59
|
);
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
function Count(props: {
|
|
63
|
-
const {
|
|
64
|
-
if (
|
|
62
|
+
function Count(props: { count: number; mouse: XYCoord | null }) {
|
|
63
|
+
const { count, mouse } = props;
|
|
64
|
+
if (count > 1)
|
|
65
65
|
return (
|
|
66
66
|
<div className="selected-count" style={getCountStyle(mouse)}>
|
|
67
|
-
{
|
|
67
|
+
{count}
|
|
68
68
|
</div>
|
|
69
69
|
);
|
|
70
70
|
else return null;
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
const PreviewNode = memo(function PreviewNode<T extends IdObj>(props: {
|
|
74
|
-
|
|
74
|
+
id: string | null;
|
|
75
|
+
dragIds: string[];
|
|
75
76
|
}) {
|
|
76
77
|
const tree = useTreeApi<T>();
|
|
77
|
-
|
|
78
|
-
const node = tree.getNode(props.item.id);
|
|
78
|
+
const node = tree.get(props.id);
|
|
79
79
|
if (!node) return null;
|
|
80
80
|
return (
|
|
81
|
-
<tree.
|
|
81
|
+
<tree.renderNode
|
|
82
82
|
preview
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
83
|
+
node={node}
|
|
84
|
+
style={{
|
|
85
|
+
paddingLeft: node.level * tree.indent,
|
|
86
|
+
opacity: 0.2,
|
|
87
|
+
background: "transparent",
|
|
88
88
|
}}
|
|
89
89
|
tree={tree}
|
|
90
|
-
state={{
|
|
91
|
-
isDragging: false,
|
|
92
|
-
isEditing: false,
|
|
93
|
-
isSelected: false,
|
|
94
|
-
isSelectedStart: false,
|
|
95
|
-
isSelectedEnd: false,
|
|
96
|
-
isHoveringOverChild: false,
|
|
97
|
-
isOpen: node.isOpen,
|
|
98
|
-
}}
|
|
99
|
-
handlers={{
|
|
100
|
-
edit: () => Promise.resolve({ cancelled: true }),
|
|
101
|
-
select: () => {},
|
|
102
|
-
toggle: () => {},
|
|
103
|
-
submit: () => {},
|
|
104
|
-
reset: () => {},
|
|
105
|
-
}}
|
|
106
90
|
/>
|
|
107
91
|
);
|
|
108
92
|
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { NodeRendererProps } from "../types/renderers";
|
|
3
|
+
import { IdObj } from "../types/utils";
|
|
4
|
+
|
|
5
|
+
export function DefaultNode<T extends IdObj>({
|
|
6
|
+
style,
|
|
7
|
+
node,
|
|
8
|
+
dragHandle,
|
|
9
|
+
}: NodeRendererProps<T>) {
|
|
10
|
+
return (
|
|
11
|
+
<div style={style} ref={dragHandle}>
|
|
12
|
+
ID: {node.data.id}
|
|
13
|
+
</div>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { RowRendererProps } from "../types/renderers";
|
|
3
|
+
import { IdObj } from "../types/utils";
|
|
4
|
+
|
|
5
|
+
export function DefaultRow<T extends IdObj>({
|
|
6
|
+
node,
|
|
7
|
+
attrs,
|
|
8
|
+
innerRef,
|
|
9
|
+
children,
|
|
10
|
+
}: RowRendererProps<T>) {
|
|
11
|
+
return (
|
|
12
|
+
<div
|
|
13
|
+
{...attrs}
|
|
14
|
+
ref={innerRef}
|
|
15
|
+
onFocus={(e) => e.stopPropagation()}
|
|
16
|
+
onClick={node.handleClick}
|
|
17
|
+
>
|
|
18
|
+
{children}
|
|
19
|
+
</div>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { useDragLayer } from "react-dnd";
|
|
2
|
+
import { useDndContext, useTreeApi } from "../context";
|
|
3
|
+
import { DefaultDragPreview } from "./default-drag-preview";
|
|
4
|
+
|
|
5
|
+
export function DragPreviewContainer() {
|
|
6
|
+
const tree = useTreeApi();
|
|
7
|
+
const { offset, mouse, item, isDragging } = useDragLayer((m) => {
|
|
8
|
+
return {
|
|
9
|
+
offset: m.getSourceClientOffset(),
|
|
10
|
+
mouse: m.getClientOffset(),
|
|
11
|
+
item: m.getItem(),
|
|
12
|
+
isDragging: m.isDragging(),
|
|
13
|
+
};
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const DragPreview = tree.props.renderDragPreview || DefaultDragPreview;
|
|
17
|
+
return (
|
|
18
|
+
<DragPreview
|
|
19
|
+
offset={offset}
|
|
20
|
+
mouse={mouse}
|
|
21
|
+
id={item?.id || null}
|
|
22
|
+
dragIds={item?.dragIds || []}
|
|
23
|
+
isDragging={isDragging}
|
|
24
|
+
/>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { forwardRef } from "react";
|
|
3
|
+
import { useTreeApi } from "../context";
|
|
4
|
+
|
|
5
|
+
export const ListInnerElement = forwardRef<any, any>(function InnerElement(
|
|
6
|
+
{ style, ...rest },
|
|
7
|
+
ref
|
|
8
|
+
) {
|
|
9
|
+
const tree = useTreeApi();
|
|
10
|
+
const paddingTop = tree.props.padding ?? tree.props.paddingTop ?? 0;
|
|
11
|
+
const paddingBottom = tree.props.padding ?? tree.props.paddingBottom ?? 0;
|
|
12
|
+
return (
|
|
13
|
+
<div
|
|
14
|
+
ref={ref}
|
|
15
|
+
style={{
|
|
16
|
+
...style,
|
|
17
|
+
height: `${parseFloat(style.height) + paddingTop + paddingBottom}px`,
|
|
18
|
+
}}
|
|
19
|
+
{...rest}
|
|
20
|
+
/>
|
|
21
|
+
);
|
|
22
|
+
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { forwardRef } from "react";
|
|
2
2
|
import { useTreeApi } from "../context";
|
|
3
|
-
import {
|
|
3
|
+
import { treeBlur } from "../state/focus-slice";
|
|
4
|
+
import { DropCursor } from "./cursor";
|
|
4
5
|
|
|
5
6
|
export const ListOuterElement = forwardRef(function Outer(
|
|
6
7
|
props: React.HTMLProps<HTMLDivElement>,
|
|
@@ -13,22 +14,32 @@ export const ListOuterElement = forwardRef(function Outer(
|
|
|
13
14
|
// @ts-ignore
|
|
14
15
|
ref={ref}
|
|
15
16
|
{...rest}
|
|
16
|
-
onClick={
|
|
17
|
-
|
|
17
|
+
onClick={(e) => {
|
|
18
|
+
if (e.currentTarget === e.target) tree.selectNone();
|
|
19
|
+
}}
|
|
18
20
|
>
|
|
19
|
-
<
|
|
20
|
-
style={{
|
|
21
|
-
height: tree.visibleNodes.length * tree.rowHeight,
|
|
22
|
-
width: "100%",
|
|
23
|
-
overflow: "hidden",
|
|
24
|
-
position: "absolute",
|
|
25
|
-
left: "0",
|
|
26
|
-
right: "0",
|
|
27
|
-
}}
|
|
28
|
-
>
|
|
29
|
-
<DropCursor />
|
|
30
|
-
</div>
|
|
21
|
+
<DropContainer />
|
|
31
22
|
{children}
|
|
32
23
|
</div>
|
|
33
24
|
);
|
|
34
25
|
});
|
|
26
|
+
|
|
27
|
+
const DropContainer = () => {
|
|
28
|
+
const tree = useTreeApi();
|
|
29
|
+
return (
|
|
30
|
+
<div
|
|
31
|
+
style={{
|
|
32
|
+
height: tree.visibleNodes.length * tree.rowHeight,
|
|
33
|
+
width: "100%",
|
|
34
|
+
position: "absolute",
|
|
35
|
+
left: "0",
|
|
36
|
+
right: "0",
|
|
37
|
+
}}
|
|
38
|
+
onClick={(e) => {
|
|
39
|
+
console.log(e.currentTarget, e.target);
|
|
40
|
+
}}
|
|
41
|
+
>
|
|
42
|
+
<DropCursor />
|
|
43
|
+
</div>
|
|
44
|
+
);
|
|
45
|
+
};
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ReactNode,
|
|
3
|
+
useEffect,
|
|
4
|
+
useImperativeHandle,
|
|
5
|
+
useMemo,
|
|
6
|
+
useRef,
|
|
7
|
+
} from "react";
|
|
8
|
+
import { useSyncExternalStore } from "use-sync-external-store/shim";
|
|
9
|
+
import { FixedSizeList } from "react-window";
|
|
10
|
+
import {
|
|
11
|
+
DataUpdatesContext,
|
|
12
|
+
DndContext,
|
|
13
|
+
NodesContext,
|
|
14
|
+
TreeApiContext,
|
|
15
|
+
} from "../context";
|
|
16
|
+
import { TreeApi } from "../interfaces/tree-api";
|
|
17
|
+
import { IdObj } from "../types/utils";
|
|
18
|
+
import { initialState } from "../state/initial";
|
|
19
|
+
import { rootReducer, RootState } from "../state/root-reducer";
|
|
20
|
+
import { HTML5Backend } from "react-dnd-html5-backend";
|
|
21
|
+
import { DndProvider } from "react-dnd";
|
|
22
|
+
import { TreeProps } from "../types/tree-props";
|
|
23
|
+
import { createStore, Store } from "redux";
|
|
24
|
+
import { actions as visibility } from "../state/open-slice";
|
|
25
|
+
|
|
26
|
+
type Props<T extends IdObj> = {
|
|
27
|
+
treeProps: TreeProps<T>;
|
|
28
|
+
imperativeHandle: React.Ref<TreeApi<T> | undefined>;
|
|
29
|
+
children: ReactNode;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const SERVER_STATE = initialState();
|
|
33
|
+
|
|
34
|
+
export function TreeProvider<T extends IdObj>({
|
|
35
|
+
treeProps,
|
|
36
|
+
imperativeHandle,
|
|
37
|
+
children,
|
|
38
|
+
}: Props<T>) {
|
|
39
|
+
const list = useRef<FixedSizeList | null>(null);
|
|
40
|
+
const listEl = useRef<HTMLDivElement | null>(null);
|
|
41
|
+
const store = useRef<Store>(
|
|
42
|
+
createStore(rootReducer, initialState(treeProps))
|
|
43
|
+
);
|
|
44
|
+
const state = useSyncExternalStore<RootState>(
|
|
45
|
+
store.current.subscribe,
|
|
46
|
+
store.current.getState,
|
|
47
|
+
() => SERVER_STATE
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
/* The tree api object is stable. */
|
|
51
|
+
const api = useMemo(() => {
|
|
52
|
+
return new TreeApi<T>(store.current, treeProps, list, listEl);
|
|
53
|
+
}, []);
|
|
54
|
+
|
|
55
|
+
/* Make sure the tree instance stays in sync */
|
|
56
|
+
const updateCount = useRef(0);
|
|
57
|
+
useMemo(() => {
|
|
58
|
+
updateCount.current += 1;
|
|
59
|
+
api.update(treeProps);
|
|
60
|
+
}, [...Object.values(treeProps), state.nodes.open]);
|
|
61
|
+
|
|
62
|
+
/* Expose the tree api */
|
|
63
|
+
useImperativeHandle(imperativeHandle, () => api);
|
|
64
|
+
|
|
65
|
+
/* Change selection based on props */
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
if (api.props.selection) {
|
|
68
|
+
api.select(api.props.selection);
|
|
69
|
+
} else {
|
|
70
|
+
api.selectNone();
|
|
71
|
+
}
|
|
72
|
+
}, [api.props.selection]);
|
|
73
|
+
|
|
74
|
+
/* Clear visability for filtered nodes */
|
|
75
|
+
useEffect(() => {
|
|
76
|
+
if (!api.props.searchTerm) {
|
|
77
|
+
store.current.dispatch(visibility.clear(true));
|
|
78
|
+
}
|
|
79
|
+
}, [api.props.searchTerm]);
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<TreeApiContext.Provider value={api}>
|
|
83
|
+
<DataUpdatesContext.Provider value={updateCount.current}>
|
|
84
|
+
<NodesContext.Provider value={state.nodes}>
|
|
85
|
+
<DndContext.Provider value={state.dnd}>
|
|
86
|
+
<DndProvider
|
|
87
|
+
backend={HTML5Backend}
|
|
88
|
+
options={{ rootElement: api.props.dndRootElement || undefined }}
|
|
89
|
+
>
|
|
90
|
+
{children}
|
|
91
|
+
</DndProvider>
|
|
92
|
+
</DndContext.Provider>
|
|
93
|
+
</NodesContext.Provider>
|
|
94
|
+
</DataUpdatesContext.Provider>
|
|
95
|
+
</TreeApiContext.Provider>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import React, { useCallback, useEffect, useMemo, useRef } from "react";
|
|
2
|
+
import { useDataUpdates, useNodesContext, useTreeApi } from "../context";
|
|
3
|
+
import { useDragHook } from "../dnd/drag-hook";
|
|
4
|
+
import { useDropHook } from "../dnd/drop-hook";
|
|
5
|
+
import { IdObj } from "../types/utils";
|
|
6
|
+
import { useFreshNode } from "../hooks/use-fresh-node";
|
|
7
|
+
|
|
8
|
+
type Props = {
|
|
9
|
+
style: React.CSSProperties;
|
|
10
|
+
index: number;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const RowContainer = React.memo(function RowContainer<T extends IdObj>({
|
|
14
|
+
index,
|
|
15
|
+
style,
|
|
16
|
+
}: Props) {
|
|
17
|
+
/* When will the <Row> will re-render.
|
|
18
|
+
*
|
|
19
|
+
* The row component is memo'd so it will only render
|
|
20
|
+
* when a new instance of the NodeApi class is passed
|
|
21
|
+
* to it.
|
|
22
|
+
*
|
|
23
|
+
* The TreeApi instance is stable. It does not
|
|
24
|
+
* change when the internal state changes.
|
|
25
|
+
*
|
|
26
|
+
* The TreeApi has all the references to the nodes.
|
|
27
|
+
* We need to clone the nodes when their state
|
|
28
|
+
* changes. The node class contains no state itself,
|
|
29
|
+
* It always checks the tree for state. The tree's
|
|
30
|
+
* state will always be up to date.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
useDataUpdates(); // Re-render when tree props or visability changes
|
|
34
|
+
const _ = useNodesContext(); // So that we re-render appropriately
|
|
35
|
+
const tree = useTreeApi<T>(); // Tree already has the fresh state
|
|
36
|
+
const node = useFreshNode<T>(index);
|
|
37
|
+
|
|
38
|
+
const el = useRef<HTMLDivElement | null>(null);
|
|
39
|
+
const dragRef = useDragHook<T>(node);
|
|
40
|
+
const dropRef = useDropHook(el, node);
|
|
41
|
+
const innerRef = useCallback(
|
|
42
|
+
(n: any) => {
|
|
43
|
+
el.current = n;
|
|
44
|
+
dropRef(n);
|
|
45
|
+
},
|
|
46
|
+
[dropRef]
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
const indent = tree.indent * node.level;
|
|
50
|
+
const nodeStyle = useMemo(() => ({ paddingLeft: indent }), [indent]);
|
|
51
|
+
const rowStyle = useMemo(
|
|
52
|
+
() => ({
|
|
53
|
+
...style,
|
|
54
|
+
top:
|
|
55
|
+
parseFloat(style.top as string) +
|
|
56
|
+
(tree.props.padding ?? tree.props.paddingTop ?? 0),
|
|
57
|
+
}),
|
|
58
|
+
[style, tree.props.padding, tree.props.paddingTop]
|
|
59
|
+
);
|
|
60
|
+
const rowAttrs: React.HTMLAttributes<any> = {
|
|
61
|
+
role: "treeitem",
|
|
62
|
+
"aria-level": node.level,
|
|
63
|
+
"aria-selected": node.isSelected,
|
|
64
|
+
style: rowStyle,
|
|
65
|
+
tabIndex: -1,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
if (!node.isEditing && node.isFocused) {
|
|
70
|
+
el.current?.focus();
|
|
71
|
+
}
|
|
72
|
+
}, [node.isEditing, node.isFocused, el.current]);
|
|
73
|
+
|
|
74
|
+
const Node = tree.renderNode;
|
|
75
|
+
const Row = tree.renderRow;
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<Row node={node} innerRef={innerRef} attrs={rowAttrs}>
|
|
79
|
+
<Node node={node} tree={tree} style={nodeStyle} dragHandle={dragRef} />
|
|
80
|
+
</Row>
|
|
81
|
+
);
|
|
82
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { useTreeApi } from "../context";
|
|
3
|
+
import { DefaultContainer } from "./default-container";
|
|
4
|
+
|
|
5
|
+
export function TreeContainer() {
|
|
6
|
+
const tree = useTreeApi();
|
|
7
|
+
const Container = tree.props.renderContainer || DefaultContainer;
|
|
8
|
+
return (
|
|
9
|
+
<>
|
|
10
|
+
<Container />
|
|
11
|
+
</>
|
|
12
|
+
);
|
|
13
|
+
}
|
package/src/components/tree.tsx
CHANGED
|
@@ -1,52 +1,24 @@
|
|
|
1
|
-
import { forwardRef
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { enrichTree } from "../data/enrich-tree";
|
|
5
|
-
import { TreeViewProvider } from "../provider";
|
|
6
|
-
import { TreeApi } from "../tree-api";
|
|
7
|
-
import { IdObj, Node, TreeProps } from "../types";
|
|
8
|
-
import { noop } from "../utils";
|
|
9
|
-
import { Preview } from "./preview";
|
|
1
|
+
import { forwardRef } from "react";
|
|
2
|
+
import { TreeProvider } from "./provider";
|
|
3
|
+
import { TreeApi } from "../interfaces/tree-api";
|
|
10
4
|
import { OuterDrop } from "./outer-drop";
|
|
11
|
-
import {
|
|
5
|
+
import { TreeContainer } from "./tree-container";
|
|
6
|
+
import { DragPreviewContainer } from "./drag-preview-container";
|
|
7
|
+
import { TreeProps } from "../types/tree-props";
|
|
8
|
+
import { IdObj } from "../types/utils";
|
|
9
|
+
import { useValidatedProps } from "../hooks/use-validated-props";
|
|
12
10
|
|
|
13
11
|
export const Tree = forwardRef(function Tree<T extends IdObj>(
|
|
14
12
|
props: TreeProps<T>,
|
|
15
|
-
ref: React.Ref<TreeApi<T
|
|
13
|
+
ref: React.Ref<TreeApi<T> | undefined>
|
|
16
14
|
) {
|
|
17
|
-
const
|
|
18
|
-
() =>
|
|
19
|
-
enrichTree<T>(
|
|
20
|
-
props.data,
|
|
21
|
-
props.hideRoot,
|
|
22
|
-
props.getChildren,
|
|
23
|
-
props.isOpen,
|
|
24
|
-
props.disableDrag,
|
|
25
|
-
props.disableDrop,
|
|
26
|
-
props.openByDefault
|
|
27
|
-
),
|
|
28
|
-
[
|
|
29
|
-
props.data,
|
|
30
|
-
props.hideRoot,
|
|
31
|
-
props.getChildren,
|
|
32
|
-
props.isOpen,
|
|
33
|
-
props.disableDrag,
|
|
34
|
-
props.disableDrop,
|
|
35
|
-
props.openByDefault,
|
|
36
|
-
]
|
|
37
|
-
);
|
|
38
|
-
|
|
15
|
+
const treeProps = useValidatedProps(props);
|
|
39
16
|
return (
|
|
40
|
-
<
|
|
41
|
-
<
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
<List className={props.className} />
|
|
47
|
-
</OuterDrop>
|
|
48
|
-
<Preview />
|
|
49
|
-
</DndProvider>
|
|
50
|
-
</TreeViewProvider>
|
|
17
|
+
<TreeProvider treeProps={treeProps} imperativeHandle={ref}>
|
|
18
|
+
<OuterDrop>
|
|
19
|
+
<TreeContainer />
|
|
20
|
+
</OuterDrop>
|
|
21
|
+
<DragPreviewContainer />
|
|
22
|
+
</TreeProvider>
|
|
51
23
|
);
|
|
52
24
|
});
|
package/src/context.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import React, { createContext, useContext, useMemo } from "react";
|
|
2
|
+
import { TreeApi } from "./interfaces/tree-api";
|
|
3
|
+
import { RootState } from "./state/root-reducer";
|
|
4
|
+
import { IdObj } from "./types/utils";
|
|
5
|
+
|
|
6
|
+
export const TreeApiContext = createContext<TreeApi<any> | null>(null);
|
|
7
|
+
|
|
8
|
+
export function useTreeApi<T extends IdObj>() {
|
|
9
|
+
const value = useContext<TreeApi<T> | null>(
|
|
10
|
+
TreeApiContext as unknown as React.Context<TreeApi<T> | null>
|
|
11
|
+
);
|
|
12
|
+
if (value === null) throw new Error("No Tree Api Provided");
|
|
13
|
+
return value;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const NodesContext = createContext<RootState["nodes"] | null>(null);
|
|
17
|
+
|
|
18
|
+
export function useNodesContext() {
|
|
19
|
+
const value = useContext(NodesContext);
|
|
20
|
+
if (value === null) throw new Error("Provide a NodesContext");
|
|
21
|
+
return value;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const DndContext = createContext<RootState["dnd"] | null>(null);
|
|
25
|
+
|
|
26
|
+
export function useDndContext() {
|
|
27
|
+
const value = useContext(DndContext);
|
|
28
|
+
if (value === null) throw new Error("Provide a DnDContext");
|
|
29
|
+
return value;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const DataUpdatesContext = createContext<number>(0);
|
|
33
|
+
|
|
34
|
+
export function useDataUpdates() {
|
|
35
|
+
useContext(DataUpdatesContext);
|
|
36
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { NodeApi } from "../interfaces/node-api";
|
|
2
|
+
import { IdObj } from "../types/utils";
|
|
3
|
+
|
|
4
|
+
export const createIndex = <T extends IdObj>(nodes: NodeApi<T>[]) => {
|
|
5
|
+
return nodes.reduce<{ [id: string]: number }>((map, node, index) => {
|
|
6
|
+
map[node.id] = index;
|
|
7
|
+
return map;
|
|
8
|
+
}, {});
|
|
9
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { NodeApi } from "../interfaces/node-api";
|
|
2
|
+
import { TreeApi } from "../interfaces/tree-api";
|
|
3
|
+
import { IdObj } from "../types/utils";
|
|
4
|
+
|
|
5
|
+
export function createList<T extends IdObj>(tree: TreeApi<T>) {
|
|
6
|
+
if (tree.isFiltered) {
|
|
7
|
+
return flattenAndFilterTree(tree.root, tree.isMatch.bind(tree));
|
|
8
|
+
} else {
|
|
9
|
+
return flattenTree(tree.root);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function flattenTree<T extends IdObj>(root: NodeApi<T>): NodeApi<T>[] {
|
|
14
|
+
const list: NodeApi<T>[] = [];
|
|
15
|
+
function collect(node: NodeApi<T>) {
|
|
16
|
+
if (node.level >= 0) {
|
|
17
|
+
list.push(node);
|
|
18
|
+
}
|
|
19
|
+
if (node.isOpen) {
|
|
20
|
+
node.children?.forEach(collect);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
collect(root);
|
|
24
|
+
list.forEach(assignRowIndex);
|
|
25
|
+
return list;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function flattenAndFilterTree<T extends IdObj>(
|
|
29
|
+
root: NodeApi<T>,
|
|
30
|
+
isMatch: (n: NodeApi<T>) => boolean
|
|
31
|
+
): NodeApi<T>[] {
|
|
32
|
+
function collect(node: NodeApi<T>) {
|
|
33
|
+
let result: NodeApi<T>[] = [];
|
|
34
|
+
const yes = !node.isRoot && isMatch(node);
|
|
35
|
+
|
|
36
|
+
if (node.children) {
|
|
37
|
+
for (let child of node.children) {
|
|
38
|
+
result = result.concat(collect(child));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (result.length) {
|
|
42
|
+
if (!node.isRoot) result.unshift(node);
|
|
43
|
+
return result;
|
|
44
|
+
}
|
|
45
|
+
if (yes) return [node];
|
|
46
|
+
else return [];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const list = collect(root).filter((n) => n.parent?.isOpen);
|
|
50
|
+
list.forEach(assignRowIndex);
|
|
51
|
+
return list;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function assignRowIndex(node: NodeApi<any>, index: number) {
|
|
55
|
+
node.rowIndex = index;
|
|
56
|
+
}
|
|
@@ -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
|
+
}
|