react-arborist 1.0.3 → 1.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.
@@ -0,0 +1,41 @@
1
+ import React, { CSSProperties } from "react";
2
+ import { DropCursorProps } from "../types";
3
+
4
+ const placeholderStyle = {
5
+ display: "flex",
6
+ alignItems: "center",
7
+ };
8
+
9
+ const lineStyle = {
10
+ flex: 1,
11
+ height: "2px",
12
+ background: "#4B91E2",
13
+ borderRadius: "1px",
14
+ };
15
+
16
+ const circleStyle = {
17
+ width: "4px",
18
+ height: "4px",
19
+ boxShadow: "0 0 0 3px #4B91E2",
20
+ borderRadius: "50%",
21
+ };
22
+
23
+ function DefaultCursor({ top, left, indent }: DropCursorProps) {
24
+ const style: CSSProperties = {
25
+ position: "absolute",
26
+ pointerEvents: "none",
27
+ top: top - 2 + "px",
28
+ left: indent + left + "px",
29
+ right: indent + "px",
30
+ };
31
+ return (
32
+ <div style={{ ...placeholderStyle, ...style }}>
33
+ <div style={{ ...circleStyle }}></div>
34
+ <div style={{ ...lineStyle }}></div>
35
+ </div>
36
+ );
37
+ }
38
+
39
+ export function defaultDropCursor(props: DropCursorProps) {
40
+ return <DefaultCursor {...props} />;
41
+ }
@@ -1,47 +1,12 @@
1
- import React, { CSSProperties } from "react";
2
- import { useCursorLocation, useStaticContext } from "../context";
1
+ import { useTreeApi } from "../context";
3
2
 
4
3
  export function DropCursor() {
5
- const treeView = useStaticContext();
6
- const cursor = useCursorLocation();
4
+ const tree = useTreeApi();
5
+ const cursor = tree.state.cursor;
7
6
  if (!cursor || cursor.type !== "line") return null;
8
- const top = treeView.rowHeight * cursor.index;
9
- const left = treeView.indent * cursor.level;
10
- const style: CSSProperties = {
11
- position: "absolute",
12
- pointerEvents: "none",
13
- top: top - 2 + "px",
14
- left: treeView.indent + left + "px",
15
- right: treeView.indent + "px",
16
- };
7
+ const indent = tree.indent;
8
+ const top = tree.rowHeight * cursor.index;
9
+ const left = indent * cursor.level;
17
10
 
18
- return <DefaultCursor style={style} />;
19
- }
20
-
21
- const placeholderStyle = {
22
- display: "flex",
23
- alignItems: "center",
24
- };
25
-
26
- const lineStyle = {
27
- flex: 1,
28
- height: "2px",
29
- background: "#4B91E2",
30
- borderRadius: "1px",
31
- };
32
-
33
- const circleStyle = {
34
- width: "4px",
35
- height: "4px",
36
- boxShadow: "0 0 0 3px #4B91E2",
37
- borderRadius: "50%",
38
- };
39
-
40
- function DefaultCursor({ style }: { style: CSSProperties }) {
41
- return (
42
- <div style={{ ...placeholderStyle, ...style }}>
43
- <div style={{ ...circleStyle }}></div>
44
- <div style={{ ...lineStyle }}></div>
45
- </div>
46
- );
11
+ return tree.renderDropCursor({ top, left, indent });
47
12
  }
@@ -0,0 +1,34 @@
1
+ import { forwardRef } from "react";
2
+ import { useTreeApi } from "../context";
3
+ import { DropCursor } from "./drop-cursor";
4
+
5
+ export const ListOuterElement = forwardRef(function Outer(
6
+ props: React.HTMLProps<HTMLDivElement>,
7
+ ref
8
+ ) {
9
+ const { children, ...rest } = props;
10
+ const tree = useTreeApi();
11
+ return (
12
+ <div
13
+ // @ts-ignore
14
+ ref={ref}
15
+ {...rest}
16
+ onClick={tree.onClick}
17
+ onContextMenu={tree.onContextMenu}
18
+ >
19
+ <div
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>
31
+ {children}
32
+ </div>
33
+ );
34
+ });
@@ -0,0 +1,25 @@
1
+ import { FixedSizeList } from "react-window";
2
+ import { useTreeApi } from "../context";
3
+ import { ListOuterElement } from "./list-outer-element";
4
+ import { Row } from "./row";
5
+
6
+ export function List(props: { className?: string }) {
7
+ const tree = useTreeApi();
8
+ return (
9
+ <div style={{ height: tree.height, width: tree.width, overflow: "hidden" }}>
10
+ <FixedSizeList
11
+ className={props.className}
12
+ outerRef={tree.listEl}
13
+ itemCount={tree.visibleNodes.length}
14
+ height={tree.height}
15
+ width={tree.width}
16
+ itemSize={tree.rowHeight}
17
+ itemKey={(index) => tree.visibleNodes[index]?.id || index}
18
+ outerElementType={ListOuterElement}
19
+ ref={tree.list}
20
+ >
21
+ {Row}
22
+ </FixedSizeList>
23
+ </div>
24
+ );
25
+ }
@@ -0,0 +1,7 @@
1
+ import { ReactElement } from "react";
2
+ import { useOuterDrop } from "../dnd/outer-drop-hook";
3
+
4
+ export function OuterDrop(props: { children: ReactElement }) {
5
+ useOuterDrop();
6
+ return props.children;
7
+ }
@@ -1,7 +1,7 @@
1
1
  import React, { CSSProperties, memo } from "react";
2
2
  import { useDragLayer, XYCoord } from "react-dnd";
3
- import { useStaticContext } from "../context";
4
- import { DragItem } from "../types";
3
+ import { useTreeApi } from "../context";
4
+ import { DragItem, IdObj } from "../types";
5
5
 
6
6
  const layerStyles: CSSProperties = {
7
7
  position: "fixed",
@@ -70,12 +70,12 @@ function Count(props: { item: DragItem; mouse: XYCoord | null }) {
70
70
  else return null;
71
71
  }
72
72
 
73
- const PreviewNode = memo(function PreviewNode(props: {
73
+ const PreviewNode = memo(function PreviewNode<T extends IdObj>(props: {
74
74
  item: DragItem | null;
75
75
  }) {
76
- const tree = useStaticContext();
76
+ const tree = useTreeApi<T>();
77
77
  if (!props.item) return null;
78
- const node = tree.api.getNode(props.item.id);
78
+ const node = tree.getNode(props.item.id);
79
79
  if (!node) return null;
80
80
  return (
81
81
  <tree.renderer
@@ -86,7 +86,7 @@ const PreviewNode = memo(function PreviewNode(props: {
86
86
  row: {},
87
87
  indent: { paddingLeft: node.level * tree.indent },
88
88
  }}
89
- tree={tree.api}
89
+ tree={tree}
90
90
  state={{
91
91
  isDragging: false,
92
92
  isEditing: false,
@@ -1,36 +1,34 @@
1
1
  import React, { useCallback, useMemo, useRef } from "react";
2
- import {
3
- useCursorParentId,
4
- useEditingId,
5
- useIsCursorOverFolder,
6
- useIsSelected,
7
- useStaticContext,
8
- } from "../context";
2
+ import { useTreeApi } from "../context";
9
3
  import { useDragHook } from "../dnd/drag-hook";
10
4
  import { useDropHook } from "../dnd/drop-hook";
5
+ import { IdObj } from "../types";
11
6
 
12
7
  type Props = {
13
8
  style: React.CSSProperties;
14
9
  index: number;
15
10
  };
16
11
 
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();
12
+ export const Row = React.memo(function Row<T extends IdObj>({
13
+ index,
14
+ style,
15
+ }: Props) {
16
+ const realTree = useTreeApi<T>();
17
+ const tree = useMemo(() => realTree, []);
18
+ tree.sync(realTree);
19
+
20
+ const node = tree.visibleNodes[index];
21
+ const next = tree.visibleNodes[index + 1] || null;
22
+ const prev = tree.visibleNodes[index - 1] || null;
25
23
  const el = useRef<HTMLDivElement | null>(null);
26
24
  const [{ isDragging }, dragRef] = useDragHook(node);
27
25
  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;
26
+ const isEditing = node.id === tree.editingId;
27
+ const isSelected = tree.isSelected(index);
28
+ const nextSelected = next && tree.isSelected(index + 1);
29
+ const prevSelected = prev && tree.isSelected(index - 1);
30
+ const isHoveringOverChild = node.id === tree.cursorParentId;
31
+ const isOverFolder = node.id === tree.cursorParentId && tree.cursorOverFolder;
34
32
  const isOpen = node.isOpen;
35
33
  const indent = tree.indent * node.level;
36
34
  const state = useMemo(() => {
@@ -79,22 +77,22 @@ export const Row = React.memo(function Row({ index, style }: Props) {
79
77
  ) => {
80
78
  if (node.rowIndex === null) return;
81
79
  if (args.selectOnClick || e.metaKey || e.shiftKey) {
82
- tree.api.select(node.rowIndex, e.metaKey, e.shiftKey);
80
+ tree.select(node.rowIndex, e.metaKey, e.shiftKey);
83
81
  } else {
84
- tree.api.select(null, false, false);
82
+ tree.select(null, false, false);
85
83
  }
86
84
  },
87
85
  toggle: (e: React.MouseEvent) => {
88
86
  e.stopPropagation();
89
87
  tree.onToggle(node.id, !node.isOpen);
90
88
  },
91
- edit: () => tree.api.edit(node.id),
89
+ edit: () => tree.edit(node.id),
92
90
  submit: (name: string) => {
93
- name.trim() ? tree.api.submit(node.id, name) : tree.api.reset(node.id);
91
+ name.trim() ? tree.submit(node.id, name) : tree.reset(node.id);
94
92
  },
95
- reset: () => tree.api.reset(node.id),
93
+ reset: () => tree.reset(node.id),
96
94
  };
97
- }, [tree, node]);
95
+ }, [node, tree]);
98
96
 
99
97
  const Renderer = useMemo(() => {
100
98
  return React.memo(tree.renderer);
@@ -108,7 +106,7 @@ export const Row = React.memo(function Row({ index, style }: Props) {
108
106
  state={state}
109
107
  handlers={handlers}
110
108
  preview={false}
111
- tree={tree.api}
109
+ tree={tree}
112
110
  />
113
111
  );
114
112
  });
@@ -1,70 +1,14 @@
1
- import { forwardRef, MouseEventHandler, ReactElement, useMemo, useRef } from "react";
1
+ import { forwardRef, ReactElement, useMemo, useRef } from "react";
2
2
  import { DndProvider } from "react-dnd";
3
3
  import { HTML5Backend } from "react-dnd-html5-backend";
4
- import { FixedSizeList } from "react-window";
5
- import { useStaticContext } from "../context";
6
4
  import { enrichTree } from "../data/enrich-tree";
7
- import { useOuterDrop } from "../dnd/outer-drop-hook";
8
5
  import { TreeViewProvider } from "../provider";
9
6
  import { TreeApi } from "../tree-api";
10
7
  import { IdObj, Node, TreeProps } from "../types";
11
8
  import { noop } from "../utils";
12
- import { DropCursor } from "./drop-cursor";
13
9
  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
- }
10
+ import { OuterDrop } from "./outer-drop";
11
+ import { List } from "./list";
68
12
 
69
13
  export const Tree = forwardRef(function Tree<T extends IdObj>(
70
14
  props: TreeProps<T>,
@@ -91,25 +35,15 @@ export const Tree = forwardRef(function Tree<T extends IdObj>(
91
35
  props.openByDefault,
92
36
  ]
93
37
  );
38
+
94
39
  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}>
40
+ <TreeViewProvider treeProps={props} imperativeHandle={ref} root={root}>
41
+ <DndProvider
42
+ backend={HTML5Backend}
43
+ options={{ rootElement: props.dndRootElement || undefined }}
44
+ >
111
45
  <OuterDrop>
112
- <List className={props.className}/>
46
+ <List className={props.className} />
113
47
  </OuterDrop>
114
48
  <Preview />
115
49
  </DndProvider>
package/src/context.tsx CHANGED
@@ -1,52 +1,13 @@
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";
1
+ import React, { createContext, useContext, useMemo } from "react";
2
+ import { TreeApi } from "./tree-api";
3
+ import { IdObj } from "./types";
5
4
 
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
- }
5
+ export const TreeApiContext = createContext<TreeApi<any> | null>(null);
15
6
 
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");
7
+ export function useTreeApi<T extends IdObj>() {
8
+ const value = useContext<TreeApi<T> | null>(
9
+ TreeApiContext as unknown as React.Context<TreeApi<T> | null>
10
+ );
11
+ if (value === null) throw new Error("No Tree Api Provided");
25
12
  return value;
26
13
  }
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
- }
@@ -1,7 +1,7 @@
1
1
  import { useEffect } from "react";
2
2
  import { ConnectDragSource, useDrag } from "react-dnd";
3
3
  import { getEmptyImage } from "react-dnd-html5-backend";
4
- import { useIsSelected, useSelectedIds, useStaticContext } from "../context";
4
+ import { useTreeApi } from "../context";
5
5
  import { DragItem, Node } from "../types";
6
6
  import { DropResult } from "./drop-hook";
7
7
 
@@ -10,9 +10,8 @@ type CollectedProps = { isDragging: boolean };
10
10
  export function useDragHook(
11
11
  node: Node
12
12
  ): [{ isDragging: boolean }, ConnectDragSource] {
13
- const tree = useStaticContext();
14
- const isSelected = useIsSelected();
15
- const ids = useSelectedIds();
13
+ const tree = useTreeApi();
14
+ const ids = tree.getSelectedIds();
16
15
  const [{ isDragging }, ref, preview] = useDrag<
17
16
  DragItem,
18
17
  DropResult,
@@ -23,13 +22,13 @@ export function useDragHook(
23
22
  type: "NODE",
24
23
  item: () => ({
25
24
  id: node.id,
26
- dragIds: isSelected(node.rowIndex) ? ids : [node.id],
25
+ dragIds: tree.isSelected(node.rowIndex) ? ids : [node.id],
27
26
  }),
28
27
  collect: (m) => ({
29
28
  isDragging: m.isDragging(),
30
29
  }),
31
30
  end: (item, monitor) => {
32
- tree.api.hideCursor();
31
+ tree.hideCursor();
33
32
  const drop = monitor.getDropResult();
34
33
  if (drop && drop.parentId) {
35
34
  tree.onMove(item.dragIds, drop.parentId, drop.index);
@@ -1,6 +1,6 @@
1
1
  import { RefObject } from "react";
2
2
  import { ConnectDropTarget, useDrop } from "react-dnd";
3
- import { useStaticContext } from "../context";
3
+ import { useTreeApi } from "../context";
4
4
  import { DragItem, Node } from "../types";
5
5
  import { isDecendent, isFolder } from "../utils";
6
6
  import { computeDrop } from "./compute-drop";
@@ -18,13 +18,13 @@ export function useDropHook(
18
18
  prev: Node | null,
19
19
  next: Node | null
20
20
  ): [CollectedProps, ConnectDropTarget] {
21
- const tree = useStaticContext();
21
+ const tree = useTreeApi();
22
22
  return useDrop<DragItem, DropResult | null, CollectedProps>(
23
23
  () => ({
24
24
  accept: "NODE",
25
25
  canDrop: (item) => {
26
26
  for (let id of item.dragIds) {
27
- const drag = tree.api.getNode(id);
27
+ const drag = tree.getNode(id);
28
28
  if (!drag) return false;
29
29
  if (isFolder(drag) && isDecendent(node, drag)) return false;
30
30
  }
@@ -42,9 +42,9 @@ export function useDropHook(
42
42
  prevNode: prev,
43
43
  nextNode: next,
44
44
  });
45
- if (cursor) tree.api.showCursor(cursor);
45
+ if (cursor) tree.showCursor(cursor);
46
46
  } else {
47
- tree.api.hideCursor();
47
+ tree.hideCursor();
48
48
  }
49
49
  },
50
50
  drop: (item, m): DropResult | undefined | null => {
@@ -1,11 +1,11 @@
1
1
  import { useDrop } from "react-dnd";
2
- import { useStaticContext } from "../context";
2
+ import { useTreeApi } from "../context";
3
3
  import { DragItem } from "../types";
4
4
  import { computeDrop } from "./compute-drop";
5
5
  import { DropResult } from "./drop-hook";
6
6
 
7
7
  export function useOuterDrop() {
8
- const tree = useStaticContext();
8
+ const tree = useTreeApi();
9
9
 
10
10
  // In case we drop an item at the bottom of the list
11
11
  const [, drop] = useDrop<DragItem, DropResult | null, { isOver: boolean }>(
@@ -20,10 +20,10 @@ export function useOuterDrop() {
20
20
  offset: offset,
21
21
  indent: tree.indent,
22
22
  node: null,
23
- prevNode: tree.api.visibleNodes[tree.api.visibleNodes.length - 1],
23
+ prevNode: tree.visibleNodes[tree.visibleNodes.length - 1],
24
24
  nextNode: null,
25
25
  });
26
- if (cursor) tree.api.showCursor(cursor);
26
+ if (cursor) tree.showCursor(cursor);
27
27
  },
28
28
  canDrop: (item, m) => {
29
29
  return m.isOver({ shallow: true });
@@ -37,7 +37,7 @@ export function useOuterDrop() {
37
37
  offset: offset,
38
38
  indent: tree.indent,
39
39
  node: null,
40
- prevNode: tree.api.visibleNodes[tree.api.visibleNodes.length - 1],
40
+ prevNode: tree.visibleNodes[tree.visibleNodes.length - 1],
41
41
  nextNode: null,
42
42
  });
43
43
  return drop;
package/src/index.ts CHANGED
@@ -1,5 +1,19 @@
1
1
  import { Tree } from "./components/tree";
2
2
  import { TreeApi } from "./tree-api";
3
- import type { NodeRenderer, NodeState, NodeHandlers } from "./types";
3
+ import type {
4
+ NodeRenderer,
5
+ NodeState,
6
+ NodeHandlers,
7
+ NodeRendererProps,
8
+ DropCursorProps,
9
+ } from "./types";
4
10
 
5
- export { Tree, TreeApi, NodeRenderer, NodeState, NodeHandlers };
11
+ export {
12
+ Tree,
13
+ TreeApi,
14
+ NodeRenderer,
15
+ NodeState,
16
+ NodeHandlers,
17
+ NodeRendererProps,
18
+ DropCursorProps,
19
+ };