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.
package/src/provider.tsx CHANGED
@@ -1,61 +1,41 @@
1
- import { useImperativeHandle, useMemo, useReducer, useRef } from "react";
2
- import { FixedSizeList } from "react-window";
3
1
  import {
4
- CursorLocationContext,
5
- CursorParentId,
6
- EditingIdContext,
7
- IsCursorOverFolder,
8
- SelectionContext,
9
- Static,
10
- } from "./context";
11
- import { Cursor } from "./dnd/compute-drop";
12
- import { initState, reducer } from "./reducer";
2
+ useImperativeHandle,
3
+ useLayoutEffect,
4
+ useMemo,
5
+ useReducer,
6
+ useRef,
7
+ } from "react";
8
+ import { FixedSizeList } from "react-window";
9
+ import { TreeApiContext } from "./context";
10
+ import { actions, initState, reducer } from "./reducer";
13
11
  import { useSelectionKeys } from "./selection/selection-hook";
14
- import { useTreeApi } from "./tree-api-hook";
15
- import { StateContext, StaticContext, TreeProviderProps } from "./types";
12
+ import { TreeApi } from "./tree-api";
13
+ import { IdObj, TreeProviderProps } from "./types";
16
14
 
17
- export function TreeViewProvider<T>(props: TreeProviderProps<T>) {
15
+ export function TreeViewProvider<T extends IdObj>(props: TreeProviderProps<T>) {
18
16
  const [state, dispatch] = useReducer(reducer, initState());
19
- const list = useRef<FixedSizeList>();
20
- const api = useTreeApi<T>(state, dispatch, props, list.current);
17
+ const list = useRef<FixedSizeList | null>(null);
18
+ const listEl = useRef<HTMLDivElement | null>(null);
21
19
 
22
- useImperativeHandle(props.imperativeHandle, () => api);
23
- useSelectionKeys(props.listEl, api);
24
- const staticValue = useMemo<StaticContext<T>>(
25
- () => ({ ...props, api, list }),
26
- [props, api, list]
20
+ const api = useMemo(
21
+ () => new TreeApi<T>(dispatch, state, props, list, listEl),
22
+ [dispatch, state, props, list, listEl]
27
23
  );
28
24
 
29
25
  /**
30
- * This context pattern is ridiculous, next time use redux.
26
+ * This ensures that the selection remains correct even
27
+ * after opening and closing a folders
31
28
  */
32
- return (
33
- // @ts-ignore
34
- <Static.Provider value={staticValue}>
35
- <EditingIdContext.Provider value={state.editingId}>
36
- <SelectionContext.Provider value={state.selection}>
37
- <CursorParentId.Provider value={getParentId(state.cursor)}>
38
- <IsCursorOverFolder.Provider value={isOverFolder(state)}>
39
- <CursorLocationContext.Provider value={state.cursor}>
40
- {props.children}
41
- </CursorLocationContext.Provider>
42
- </IsCursorOverFolder.Provider>
43
- </CursorParentId.Provider>
44
- </SelectionContext.Provider>
45
- </EditingIdContext.Provider>
46
- </Static.Provider>
47
- );
48
- }
29
+ useLayoutEffect(() => {
30
+ dispatch(actions.setVisibleIds(api.visibleIds, api.idToIndex));
31
+ }, [dispatch, api.visibleIds, api.idToIndex, props.root]);
49
32
 
50
- function getParentId(cursor: Cursor) {
51
- switch (cursor.type) {
52
- case "highlight":
53
- return cursor.id;
54
- default:
55
- return null;
56
- }
57
- }
33
+ useImperativeHandle(props.imperativeHandle, () => api);
34
+ useSelectionKeys(listEl, api);
58
35
 
59
- function isOverFolder(state: StateContext) {
60
- return state.cursor.type === "highlight";
36
+ return (
37
+ <TreeApiContext.Provider value={api}>
38
+ {props.children}
39
+ </TreeApiContext.Provider>
40
+ );
61
41
  }
@@ -1,7 +1,8 @@
1
1
  import { MutableRefObject, useEffect } from "react";
2
2
  import { TreeApi } from "../tree-api";
3
+ import { IdObj } from "../types";
3
4
 
4
- export function useSelectionKeys<T>(
5
+ export function useSelectionKeys<T extends IdObj>(
5
6
  ref: MutableRefObject<HTMLDivElement | null>,
6
7
  api: TreeApi<T>
7
8
  ) {
package/src/tree-api.ts CHANGED
@@ -1,35 +1,41 @@
1
1
  import memoizeOne from "memoize-one";
2
- import { Dispatch } from "react";
2
+ import { Dispatch, MutableRefObject } from "react";
3
3
  import { FixedSizeList } from "react-window";
4
4
  import { flattenTree } from "./data/flatten-tree";
5
5
  import { Cursor } from "./dnd/compute-drop";
6
6
  import { Action, actions } from "./reducer";
7
- import { Node, StateContext, TreeProviderProps, EditResult } from "./types";
7
+ import {
8
+ Node,
9
+ StateContext,
10
+ TreeProviderProps,
11
+ EditResult,
12
+ IdObj,
13
+ DropCursorProps,
14
+ } from "./types";
8
15
  import ReactDOM from "react-dom";
9
-
10
- export class TreeApi<T = unknown> {
16
+ import { noop } from "./utils";
17
+ import { Selection } from "./selection/selection";
18
+ import { defaultDropCursor } from "./components/default-drop-cursor";
19
+ export class TreeApi<T extends IdObj> {
11
20
  private edits = new Map<string, (args: EditResult) => void>();
12
21
 
13
22
  constructor(
14
23
  public dispatch: Dispatch<Action>,
15
24
  public state: StateContext,
16
25
  public props: TreeProviderProps<T>,
17
- public list: FixedSizeList | undefined
26
+ public list: MutableRefObject<FixedSizeList | null>,
27
+ public listEl: MutableRefObject<HTMLDivElement | null>
18
28
  ) {}
19
29
 
20
- assign(
21
- dispatch: Dispatch<Action>,
22
- state: StateContext,
23
- props: TreeProviderProps<T>,
24
- list: FixedSizeList | undefined
25
- ) {
26
- this.dispatch = dispatch;
27
- this.state = state;
28
- this.props = props;
29
- this.list = list;
30
+ sync(other: TreeApi<T>) {
31
+ this.dispatch = other.dispatch;
32
+ this.state = other.state;
33
+ this.props = other.props;
34
+ this.list = other.list;
35
+ this.listEl = other.listEl;
30
36
  }
31
37
 
32
- getNode(id: string): Node<unknown> | null {
38
+ getNode(id: string): Node<T> | null {
33
39
  if (id in this.idToIndex)
34
40
  return this.visibleNodes[this.idToIndex[id]] || null;
35
41
  else return null;
@@ -49,7 +55,7 @@ export class TreeApi<T = unknown> {
49
55
 
50
56
  submit(id: string | number, value: string) {
51
57
  const sid = id.toString();
52
- this.props.onEdit(sid, value);
58
+ this.onEdit(sid, value);
53
59
  this.dispatch(actions.edit(null));
54
60
  this.resolveEdit(sid, { cancelled: false, value });
55
61
  }
@@ -95,20 +101,20 @@ export class TreeApi<T = unknown> {
95
101
  if (!this.list) return;
96
102
  const index = this.idToIndex[id];
97
103
  if (index) {
98
- this.list.scrollToItem(index, "start");
104
+ this.list.current?.scrollToItem(index);
99
105
  } else {
100
106
  this.openParents(id);
101
107
  ReactDOM.flushSync(() => {
102
108
  const index = this.idToIndex[id];
103
109
  if (index) {
104
- this.list?.scrollToItem(index, "start");
110
+ this.list.current?.scrollToItem(index);
105
111
  }
106
112
  });
107
113
  }
108
114
  }
109
115
 
110
116
  open(id: string) {
111
- this.props.onToggle(id, true);
117
+ this.onToggle(id, true);
112
118
  }
113
119
 
114
120
  openParents(id: string) {
@@ -130,7 +136,75 @@ export class TreeApi<T = unknown> {
130
136
  }
131
137
 
132
138
  get visibleNodes() {
133
- return createList(this.props.root);
139
+ return createList(this.props.root) as Node<T>[];
140
+ }
141
+
142
+ get width() {
143
+ return this.props.treeProps.width || 300;
144
+ }
145
+
146
+ get height() {
147
+ return this.props.treeProps.height || 500;
148
+ }
149
+
150
+ get indent() {
151
+ return this.props.treeProps.indent || 24;
152
+ }
153
+
154
+ get renderer() {
155
+ return this.props.treeProps.children;
156
+ }
157
+
158
+ get onToggle() {
159
+ return this.props.treeProps.onToggle || noop;
160
+ }
161
+
162
+ get rowHeight() {
163
+ return this.props.treeProps.rowHeight || 24;
164
+ }
165
+
166
+ get onClick() {
167
+ return this.props.treeProps.onClick || noop;
168
+ }
169
+
170
+ get onContextMenu() {
171
+ return this.props.treeProps.onContextMenu || noop;
172
+ }
173
+
174
+ get onMove() {
175
+ return this.props.treeProps.onMove || noop;
176
+ }
177
+
178
+ get onEdit() {
179
+ return this.props.treeProps.onEdit || noop;
180
+ }
181
+
182
+ get cursorParentId() {
183
+ const { cursor } = this.state;
184
+ switch (cursor.type) {
185
+ case "highlight":
186
+ return cursor.id;
187
+ default:
188
+ return null;
189
+ }
190
+ }
191
+
192
+ get cursorOverFolder() {
193
+ return this.state.cursor.type === "highlight";
194
+ }
195
+
196
+ get editingId() {
197
+ return this.state.editingId;
198
+ }
199
+
200
+ isSelected(index: number | null) {
201
+ const selection = Selection.parse(this.state.selection.data, []);
202
+ return selection.contains(index);
203
+ }
204
+
205
+ renderDropCursor(props: DropCursorProps) {
206
+ const render = this.props.treeProps.dropCursor || defaultDropCursor;
207
+ return render(props);
134
208
  }
135
209
  }
136
210
 
package/src/types.ts CHANGED
@@ -5,6 +5,7 @@ import React, {
5
5
  MouseEventHandler,
6
6
  MutableRefObject,
7
7
  ReactElement,
8
+ ReactNode,
8
9
  Ref,
9
10
  } from "react";
10
11
  import { FixedSizeList } from "react-window";
@@ -38,7 +39,7 @@ export interface IdObj {
38
39
  id: string;
39
40
  }
40
41
 
41
- export type NodeRendererProps<T> = {
42
+ export type NodeRendererProps<T extends IdObj> = {
42
43
  innerRef: (el: HTMLDivElement | null) => void;
43
44
  styles: { row: CSSProperties; indent: CSSProperties };
44
45
  data: T;
@@ -66,7 +67,7 @@ export type NodeHandlers = {
66
67
  reset: () => void;
67
68
  };
68
69
 
69
- export type NodeRenderer<T> = ComponentType<NodeRendererProps<T>>;
70
+ export type NodeRenderer<T extends IdObj> = ComponentType<NodeRendererProps<T>>;
70
71
 
71
72
  export type MoveHandler = (
72
73
  dragIds: string[],
@@ -102,50 +103,46 @@ export type StateContext = {
102
103
  selection: SelectionState;
103
104
  visibleIds: string[];
104
105
  };
105
- export interface TreeProps<T> {
106
+
107
+ type BoolFunc<T> = (data: T) => boolean;
108
+
109
+ export interface TreeProps<T extends IdObj> {
106
110
  children: NodeRenderer<T>;
111
+ className?: string | undefined;
107
112
  data: T;
113
+ disableDrag?: string | boolean | BoolFunc<T>;
114
+ disableDrop?: string | boolean | BoolFunc<T>;
115
+ dndRootElement?: globalThis.Node | null;
116
+ getChildren?: string | ((d: T) => T[]);
117
+ handle?: Ref<TreeApi<T>>; // Deprecated
108
118
  height?: number;
109
- width?: number;
110
- rowHeight?: number;
111
- indent?: number;
112
119
  hideRoot?: boolean;
113
- onToggle?: ToggleHandler;
114
- onMove?: MoveHandler;
115
- onEdit?: EditHandler;
116
- getChildren?: string | ((d: T) => T[]);
117
- isOpen?: string | ((d: T) => boolean);
118
- disableDrag?: string | boolean | ((d: T) => boolean);
119
- disableDrop?: string | boolean | ((d: T) => boolean);
120
- openByDefault?: boolean;
121
- className?: string | undefined;
122
- handle?: Ref<TreeApi<T>>;
120
+ indent?: number;
121
+ isOpen?: string | BoolFunc<T>;
123
122
  onClick?: MouseEventHandler;
124
123
  onContextMenu?: MouseEventHandler;
124
+ onEdit?: EditHandler;
125
+ onMove?: MoveHandler;
126
+ onToggle?: ToggleHandler;
127
+ openByDefault?: boolean;
128
+ rowHeight?: number;
129
+ width?: number;
130
+ dropCursor?: (props: DropCursorProps) => ReactElement;
125
131
  }
126
132
 
127
- export type TreeProviderProps<T> = {
133
+ export type TreeProviderProps<T extends IdObj> = {
134
+ treeProps: TreeProps<T>;
128
135
  imperativeHandle: React.Ref<TreeApi<T>> | undefined;
129
136
  children: ReactElement;
130
- height: number;
131
- indent: number;
132
- listEl: MutableRefObject<HTMLDivElement | null>;
133
- onToggle: ToggleHandler;
134
- onMove: MoveHandler;
135
- onEdit: EditHandler;
136
- onClick?: MouseEventHandler;
137
- onContextMenu?: MouseEventHandler;
138
- renderer: NodeRenderer<any>;
139
- rowHeight: number;
140
137
  root: Node<T>;
141
- width: number;
142
- };
143
-
144
- export type StaticContext<T> = TreeProviderProps<T> & {
145
- api: TreeApi<T>;
146
- list: MutableRefObject<FixedSizeList | undefined>;
147
138
  };
148
139
 
149
140
  export type EditResult =
150
141
  | { cancelled: true }
151
142
  | { cancelled: false; value: string };
143
+
144
+ export type DropCursorProps = {
145
+ top: number;
146
+ left: number;
147
+ indent: number;
148
+ };
@@ -1,6 +0,0 @@
1
- import { Dispatch } from "react";
2
- import { FixedSizeList } from "react-window";
3
- import { Action } from "./reducer";
4
- import { TreeApi } from "./tree-api";
5
- import { StateContext, TreeProviderProps } from "./types";
6
- export declare function useTreeApi<T>(state: StateContext, dispatch: Dispatch<Action>, props: TreeProviderProps<T>, list: FixedSizeList | undefined): TreeApi<T>;
@@ -1,34 +0,0 @@
1
- import { Dispatch, useLayoutEffect, useMemo } from "react";
2
- import { FixedSizeList } from "react-window";
3
- import { Action, actions } from "./reducer";
4
- import { TreeApi } from "./tree-api";
5
- import { StateContext, TreeProviderProps } from "./types";
6
-
7
- export function useTreeApi<T>(
8
- state: StateContext,
9
- dispatch: Dispatch<Action>,
10
- props: TreeProviderProps<T>,
11
- list: FixedSizeList | undefined
12
- ) {
13
- /**
14
- * We only ever want one instance of the api object
15
- * It will get updated as the props change, but the
16
- * reference will not.
17
- */
18
- const api = useMemo(
19
- () => new TreeApi<T>(dispatch, state, props, list),
20
- // eslint-disable-next-line
21
- []
22
- );
23
- api.assign(dispatch, state, props, list);
24
-
25
- /**
26
- * This ensures that the selection remains correct even
27
- * after opening and closing a folders
28
- */
29
- useLayoutEffect(() => {
30
- dispatch(actions.setVisibleIds(api.visibleIds, api.idToIndex));
31
- }, [dispatch, api, props.root]);
32
-
33
- return api;
34
- }