react-arborist 0.1.13 → 0.2.0-beta.1

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.
Files changed (84) hide show
  1. package/dist/{lib/components → components}/drop-cursor.d.ts +1 -0
  2. package/dist/{lib/components → components}/preview.d.ts +1 -0
  3. package/dist/{lib/components → components}/row.d.ts +0 -0
  4. package/dist/{lib/components → components}/tree.d.ts +0 -0
  5. package/dist/{lib/context.d.ts → context.d.ts} +0 -0
  6. package/dist/{lib/data → data}/enrich-tree.d.ts +0 -0
  7. package/dist/{lib/data → data}/flatten-tree.d.ts +0 -0
  8. package/dist/{lib/dnd → dnd}/compute-drop.d.ts +0 -0
  9. package/dist/{lib/dnd → dnd}/drag-hook.d.ts +0 -0
  10. package/dist/{lib/dnd → dnd}/drop-hook.d.ts +0 -0
  11. package/dist/{lib/dnd → dnd}/outer-drop-hook.d.ts +0 -0
  12. package/dist/index.d.ts +4 -0
  13. package/dist/index.js +1353 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/module.js +1345 -0
  16. package/dist/module.js.map +1 -0
  17. package/dist/{lib/provider.d.ts → provider.d.ts} +1 -0
  18. package/dist/{lib/reducer.d.ts → reducer.d.ts} +0 -0
  19. package/dist/{lib/selection → selection}/range.d.ts +0 -0
  20. package/dist/{lib/selection → selection}/selection-hook.d.ts +0 -0
  21. package/dist/{lib/selection → selection}/selection.d.ts +0 -0
  22. package/dist/{lib/tree-api-hook.d.ts → tree-api-hook.d.ts} +0 -0
  23. package/dist/{lib/tree-api.d.ts → tree-api.d.ts} +1 -1
  24. package/dist/{lib/types.d.ts → types.d.ts} +4 -0
  25. package/dist/{lib/utils.d.ts → utils.d.ts} +0 -0
  26. package/package.json +18 -45
  27. package/src/components/drop-cursor.tsx +47 -0
  28. package/src/components/preview.tsx +108 -0
  29. package/src/components/row.tsx +119 -0
  30. package/src/components/tree.tsx +118 -0
  31. package/src/context.tsx +52 -0
  32. package/src/data/enrich-tree.ts +74 -0
  33. package/src/data/flatten-tree.ts +17 -0
  34. package/src/data/make-tree.ts +37 -0
  35. package/src/dnd/compute-drop.ts +184 -0
  36. package/src/dnd/drag-hook.ts +48 -0
  37. package/src/dnd/drop-hook.ts +66 -0
  38. package/src/dnd/measure-hover.ts +26 -0
  39. package/src/dnd/outer-drop-hook.ts +50 -0
  40. package/src/index.ts +5 -0
  41. package/src/provider.tsx +61 -0
  42. package/src/reducer.ts +161 -0
  43. package/src/selection/range.ts +41 -0
  44. package/src/selection/selection-hook.ts +24 -0
  45. package/src/selection/selection.test.ts +111 -0
  46. package/src/selection/selection.ts +186 -0
  47. package/src/tree-api-hook.ts +34 -0
  48. package/src/tree-api.ts +129 -0
  49. package/src/types.ts +147 -0
  50. package/src/utils.ts +35 -0
  51. package/tsconfig.json +28 -0
  52. package/README.md +0 -197
  53. package/dist/lib/components/drop-cursor.js +0 -53
  54. package/dist/lib/components/preview.js +0 -91
  55. package/dist/lib/components/row.js +0 -122
  56. package/dist/lib/components/tree.js +0 -76
  57. package/dist/lib/context.js +0 -57
  58. package/dist/lib/data/enrich-tree.js +0 -48
  59. package/dist/lib/data/flatten-tree.js +0 -20
  60. package/dist/lib/data/make-tree.d.ts +0 -5
  61. package/dist/lib/data/make-tree.js +0 -40
  62. package/dist/lib/data/visible-nodes-hook.d.ts +0 -2
  63. package/dist/lib/data/visible-nodes-hook.js +0 -19
  64. package/dist/lib/dnd/compute-drop.js +0 -143
  65. package/dist/lib/dnd/drag-hook.js +0 -36
  66. package/dist/lib/dnd/drop-hook.js +0 -61
  67. package/dist/lib/dnd/measure-hover.d.ts +0 -8
  68. package/dist/lib/dnd/measure-hover.js +0 -21
  69. package/dist/lib/dnd/outer-drop-hook.js +0 -51
  70. package/dist/lib/index.d.ts +0 -3
  71. package/dist/lib/index.js +0 -7
  72. package/dist/lib/provider.js +0 -46
  73. package/dist/lib/reducer.js +0 -147
  74. package/dist/lib/selection/range.js +0 -45
  75. package/dist/lib/selection/selection-hook.js +0 -24
  76. package/dist/lib/selection/selection.js +0 -192
  77. package/dist/lib/selection/selection.test.d.ts +0 -1
  78. package/dist/lib/selection/selection.test.js +0 -102
  79. package/dist/lib/tree-api-hook.js +0 -26
  80. package/dist/lib/tree-api.js +0 -130
  81. package/dist/lib/tree-monitor.d.ts +0 -15
  82. package/dist/lib/tree-monitor.js +0 -32
  83. package/dist/lib/types.js +0 -2
  84. package/dist/lib/utils.js +0 -39
@@ -0,0 +1,61 @@
1
+ import { useImperativeHandle, useMemo, useReducer, useRef } from "react";
2
+ import { FixedSizeList } from "react-window";
3
+ 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";
13
+ import { useSelectionKeys } from "./selection/selection-hook";
14
+ import { useTreeApi } from "./tree-api-hook";
15
+ import { StateContext, StaticContext, TreeProviderProps } from "./types";
16
+
17
+ export function TreeViewProvider<T>(props: TreeProviderProps<T>) {
18
+ const [state, dispatch] = useReducer(reducer, initState());
19
+ const list = useRef<FixedSizeList>();
20
+ const api = useTreeApi<T>(state, dispatch, props, list.current);
21
+
22
+ useImperativeHandle(props.imperativeHandle, () => api);
23
+ useSelectionKeys(props.listEl, api);
24
+ const staticValue = useMemo<StaticContext<T>>(
25
+ () => ({ ...props, api, list }),
26
+ [props, api, list]
27
+ );
28
+
29
+ /**
30
+ * This context pattern is ridiculous, next time use redux.
31
+ */
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
+ }
49
+
50
+ function getParentId(cursor: Cursor) {
51
+ switch (cursor.type) {
52
+ case "highlight":
53
+ return cursor.id;
54
+ default:
55
+ return null;
56
+ }
57
+ }
58
+
59
+ function isOverFolder(state: StateContext) {
60
+ return state.cursor.type === "highlight";
61
+ }
package/src/reducer.ts ADDED
@@ -0,0 +1,161 @@
1
+ import { Cursor } from "./dnd/compute-drop";
2
+ import { Selection } from "./selection/selection";
3
+ import { StateContext } from "./types";
4
+
5
+ export const initState = (): StateContext => ({
6
+ visibleIds: [],
7
+ cursor: { type: "none" } as Cursor,
8
+ editingId: null,
9
+ selection: {
10
+ data: null,
11
+ ids: [],
12
+ },
13
+ });
14
+
15
+ export const actions = {
16
+ setCursorLocation: (cursor: Cursor) => ({
17
+ type: "SET_CURSOR_LOCATION" as "SET_CURSOR_LOCATION",
18
+ cursor,
19
+ }),
20
+
21
+ setVisibleIds: (
22
+ ids: string[], // index to id
23
+ idMap: { [id: string]: number } // id to index
24
+ ) => ({
25
+ type: "SET_VISIBLE_IDS" as "SET_VISIBLE_IDS",
26
+ ids,
27
+ idMap,
28
+ }),
29
+
30
+ select: (index: number | null, meta: boolean, shift: boolean) => ({
31
+ type: "SELECT" as "SELECT",
32
+ index,
33
+ meta,
34
+ shift,
35
+ }),
36
+
37
+ selectId: (id: string) => ({
38
+ type: "SELECT_ID" as "SELECT_ID",
39
+ id,
40
+ }),
41
+
42
+ edit: (id: string | null) => ({
43
+ type: "EDIT" as "EDIT",
44
+ id,
45
+ }),
46
+
47
+ stepUp: (shift: boolean, ids: string[]) => ({
48
+ type: "STEP_UP" as "STEP_UP",
49
+ shift,
50
+ }),
51
+
52
+ stepDown: (shift: boolean, ids: string[]) => ({
53
+ type: "STEP_DOWN" as "STEP_DOWN",
54
+ shift,
55
+ }),
56
+ };
57
+
58
+ type ActionObj = {
59
+ [Prop in keyof typeof actions]: ReturnType<typeof actions[Prop]>;
60
+ };
61
+ export type Action = ActionObj[keyof ActionObj];
62
+
63
+ export function reducer(state: StateContext, action: Action): StateContext {
64
+ switch (action.type) {
65
+ case "EDIT":
66
+ return {
67
+ ...state,
68
+ editingId: action.id,
69
+ };
70
+ case "SET_CURSOR_LOCATION":
71
+ if (equal(state.cursor, action.cursor)) {
72
+ return state;
73
+ } else {
74
+ return { ...state, cursor: action.cursor };
75
+ }
76
+ case "SELECT":
77
+ var s = Selection.parse(state.selection.data, state.visibleIds);
78
+ if (action.index === null) {
79
+ s.clear();
80
+ } else if (action.meta) {
81
+ if (s.contains(action.index)) {
82
+ s.deselect(action.index);
83
+ } else {
84
+ s.multiSelect(action.index);
85
+ }
86
+ } else if (action.shift) {
87
+ s.extend(action.index);
88
+ } else {
89
+ s.select(action.index);
90
+ }
91
+ return {
92
+ ...state,
93
+ selection: {
94
+ data: s.serialize(),
95
+ ids: s.getSelectedItems(),
96
+ },
97
+ };
98
+ case "SELECT_ID":
99
+ return {
100
+ ...state,
101
+ selection: {
102
+ ...state.selection,
103
+ ids: [action.id],
104
+ },
105
+ };
106
+ case "STEP_UP":
107
+ var s3 = Selection.parse(state.selection.data, state.visibleIds);
108
+ var f = s3.getFocus();
109
+ if (action.shift) {
110
+ s3.extend(f - 1);
111
+ } else {
112
+ s3.select(f - 1);
113
+ }
114
+ return {
115
+ ...state,
116
+ selection: {
117
+ data: s3.serialize(),
118
+ ids: s3.getSelectedItems(),
119
+ },
120
+ };
121
+ case "STEP_DOWN":
122
+ var s6 = Selection.parse(state.selection.data, state.visibleIds);
123
+ var f2 = s6.getFocus();
124
+ if (action.shift) {
125
+ s6.extend(f2 + 1);
126
+ } else {
127
+ s6.select(f2 + 1);
128
+ }
129
+ return {
130
+ ...state,
131
+ selection: {
132
+ data: s6.serialize(),
133
+ ids: s6.getSelectedItems(),
134
+ },
135
+ };
136
+ case "SET_VISIBLE_IDS":
137
+ // The visible ids changed
138
+ var ids = state.selection.ids;
139
+ // Start with a blank selection
140
+ var s2 = new Selection([], null, "none", state.visibleIds);
141
+ // Add each of the old selected ids to this new selection
142
+ for (let id of ids) {
143
+ if (id in action.idMap) s2.multiSelect(action.idMap[id]);
144
+ }
145
+ return {
146
+ ...state,
147
+ visibleIds: action.ids,
148
+ selection: {
149
+ ids,
150
+ data: s2.serialize(),
151
+ },
152
+ };
153
+ default:
154
+ return state;
155
+ }
156
+ }
157
+
158
+ function equal(a: Cursor | null, b: Cursor | null) {
159
+ if (a === null || b === null) return false;
160
+ return JSON.stringify(a) === JSON.stringify(b);
161
+ }
@@ -0,0 +1,41 @@
1
+ export class Range {
2
+ constructor(public start: number, public end: number) {
3
+ if (this.start > this.end)
4
+ throw new Error("Invalid range: start larger than end");
5
+ }
6
+
7
+ serialize(): [number, number] {
8
+ return [this.start, this.end];
9
+ }
10
+
11
+ contains(n: number) {
12
+ return n >= this.start && n <= this.end;
13
+ }
14
+
15
+ overlaps(r: Range) {
16
+ return this.contains(r.start - 1) || this.contains(r.end + 1);
17
+ }
18
+
19
+ combine(r: Range) {
20
+ this.start = Math.min(r.start, this.start);
21
+ this.end = Math.max(r.end, this.end);
22
+ }
23
+
24
+ get size() {
25
+ return this.end - this.start + 1;
26
+ }
27
+
28
+ clone() {
29
+ return new Range(this.start, this.end);
30
+ }
31
+
32
+ map(fn: (index: any) => string): any {
33
+ let returns = [];
34
+ for (let i = this.start; i <= this.end; i++) returns.push(fn(i));
35
+ return returns;
36
+ }
37
+
38
+ isEqual(other: Range) {
39
+ return this.start === other.start && this.end === other.end;
40
+ }
41
+ }
@@ -0,0 +1,24 @@
1
+ import { MutableRefObject, useEffect } from "react";
2
+ import { TreeApi } from "../tree-api";
3
+
4
+ export function useSelectionKeys<T>(
5
+ ref: MutableRefObject<HTMLDivElement | null>,
6
+ api: TreeApi<T>
7
+ ) {
8
+ useEffect(() => {
9
+ const el = ref.current;
10
+ const cb = (e: KeyboardEvent) => {
11
+ if (e.code === "ArrowDown") {
12
+ e.preventDefault();
13
+ api.selectDownwards(e.shiftKey);
14
+ } else if (e.code === "ArrowUp") {
15
+ e.preventDefault();
16
+ api.selectUpwards(e.shiftKey);
17
+ }
18
+ };
19
+ el?.addEventListener("keydown", cb);
20
+ return () => {
21
+ el?.removeEventListener("keydown", cb);
22
+ };
23
+ }, [ref, api]);
24
+ }
@@ -0,0 +1,111 @@
1
+ import { Selection } from "./selection";
2
+
3
+ const createSelection = (...ranges: [number, number][]) => {
4
+ return new Selection(ranges);
5
+ };
6
+
7
+ describe("select", () => {
8
+ test("select one after end", () => {
9
+ const s = createSelection([0, 0]);
10
+ s.multiSelect(1);
11
+ expect(s.getRanges()).toEqual([[0, 1]]);
12
+ expect(s.direction).toEqual("forward");
13
+ });
14
+
15
+ test("select one before start", () => {
16
+ const s = createSelection([1, 1]);
17
+ s.multiSelect(0);
18
+ expect(s.getRanges()).toEqual([[0, 1]]);
19
+ expect(s.direction).toEqual("backward");
20
+ });
21
+
22
+ test("select between two ranges", () => {
23
+ const s = createSelection([0, 0], [2, 2]);
24
+ s.multiSelect(1);
25
+ expect(s.getRanges()).toEqual([[0, 2]]);
26
+ expect(s.direction).toEqual("forward");
27
+ });
28
+
29
+ test("select new spot", () => {
30
+ const s = createSelection([0, 0]);
31
+ s.multiSelect(5);
32
+ expect(s.getRanges()).toEqual([
33
+ [0, 0],
34
+ [5, 5],
35
+ ]);
36
+ expect(s.direction).toEqual("none");
37
+ });
38
+ });
39
+
40
+ describe("deselect", () => {
41
+ test("one", () => {
42
+ const s = createSelection([0, 0]);
43
+ s.deselect(0);
44
+ expect(s.getRanges()).toEqual([]);
45
+ });
46
+
47
+ test("start of a range", () => {
48
+ const s = createSelection([0, 5]);
49
+ s.deselect(0);
50
+ expect(s.getRanges()).toEqual([[1, 5]]);
51
+ });
52
+
53
+ test("end of a range", () => {
54
+ const s = createSelection([0, 5]);
55
+ s.deselect(5);
56
+ expect(s.getRanges()).toEqual([[0, 4]]);
57
+ });
58
+
59
+ test("between a range", () => {
60
+ const s = createSelection([0, 5]);
61
+ s.deselect(3);
62
+ expect(s.getRanges()).toEqual([
63
+ [0, 2],
64
+ [4, 5],
65
+ ]);
66
+ });
67
+ });
68
+
69
+ describe("extend", () => {
70
+ test("up", () => {
71
+ const s = createSelection();
72
+ s.multiSelect(5);
73
+ s.extend(6);
74
+ expect(s.getRanges()).toEqual([[5, 6]]);
75
+ });
76
+
77
+ test("down", () => {
78
+ const s = createSelection();
79
+ s.multiSelect(5);
80
+ s.extend(4);
81
+ expect(s.getRanges()).toEqual([[4, 5]]);
82
+ });
83
+
84
+ test("around anchor", () => {
85
+ const s = createSelection([5, 10]);
86
+ s.extend(1);
87
+ expect(s.getRanges()).toEqual([[1, 5]]);
88
+ });
89
+
90
+ test("through other ranges", () => {
91
+ const s = createSelection([0, 0], [5, 5], [9, 10]);
92
+ s.multiSelect(2);
93
+ s.extend(20);
94
+ expect(s.getRanges()).toEqual([
95
+ [0, 0],
96
+ [2, 20],
97
+ ]);
98
+ });
99
+
100
+ test("clicking backward", () => {
101
+ const s = createSelection([15, 15]);
102
+ s.extend(3);
103
+ expect(s.getRanges()).toEqual([[3, 15]]);
104
+ });
105
+
106
+ test("split range then extend", () => {
107
+ const s = createSelection([5, 10]);
108
+ s.deselect(8);
109
+ expect(s.currentIndex).toEqual(1);
110
+ });
111
+ });
@@ -0,0 +1,186 @@
1
+ import { Range } from "./range";
2
+
3
+ type SelectionDirection = "forward" | "backward" | "none";
4
+
5
+ export type SelectionData = {
6
+ ranges: [number, number][];
7
+ currentIndex: number | null;
8
+ direction: SelectionDirection;
9
+ };
10
+
11
+ export class Selection {
12
+ ranges: Range[] = [];
13
+ currentIndex: number | null;
14
+ direction: SelectionDirection = "none";
15
+ items: any[];
16
+
17
+ static parse(data: SelectionData | null, items: any[]) {
18
+ if (data) {
19
+ return new Selection(
20
+ data.ranges,
21
+ data.currentIndex,
22
+ data.direction,
23
+ items
24
+ );
25
+ } else {
26
+ return new Selection();
27
+ }
28
+ }
29
+
30
+ constructor(
31
+ ranges: [number, number][] = [],
32
+ currentIndex: number | null = ranges.length ? ranges.length - 1 : null,
33
+ direction: SelectionDirection = "none",
34
+ items: any[] = []
35
+ ) {
36
+ ranges.forEach(([s, e]) => this.addRange(s, e));
37
+ this.currentIndex = currentIndex;
38
+ this.direction = direction;
39
+ this.items = items;
40
+ }
41
+
42
+ get current() {
43
+ if (this.currentIndex === null) return null;
44
+ const range = this.ranges[this.currentIndex];
45
+ if (!range) {
46
+ return null;
47
+ } else {
48
+ return range;
49
+ }
50
+ }
51
+
52
+ select(n: number) {
53
+ if (n < 0 || n >= this.items.length) return;
54
+ this.clear();
55
+ this.currentIndex = this.addRange(n, n);
56
+ }
57
+
58
+ multiSelect(n: number) {
59
+ if (n < 0 || n >= this.items.length) return;
60
+ if (this.contains(n)) return;
61
+ this.currentIndex = this.addRange(n, n);
62
+ this.compact(n);
63
+ }
64
+
65
+ deselect(n: number) {
66
+ if (n < 0 || n >= this.items.length) return;
67
+ const r = this.ranges.find((r) => r.contains(n));
68
+ if (!r) return;
69
+ else if (r.size === 1) this.removeRange(r);
70
+ else if (r.start === n) r.start++;
71
+ else if (r.end === n) r.end--;
72
+ else {
73
+ this.removeRange(r);
74
+ this.addRange(r.start, n - 1);
75
+ this.currentIndex = this.addRange(n + 1, r.end);
76
+ }
77
+ }
78
+
79
+ getSelectedItems<T>(): T[] {
80
+ return this.ranges.flatMap((range) =>
81
+ range.map((index) => this.items[index])
82
+ );
83
+ }
84
+
85
+ extend(n: number) {
86
+ if (n < 0 || n >= this.items.length) return;
87
+ if (this.isEmpty()) {
88
+ this.select(n);
89
+ } else {
90
+ const anchor = this.getAnchor();
91
+ if (anchor !== null && this.current) {
92
+ const [start, end] = [n, anchor].sort((a, b) => a - b);
93
+ this.current.start = start;
94
+ this.current.end = end;
95
+ this.compact(n);
96
+ }
97
+ }
98
+ }
99
+
100
+ contains(n: number | null) {
101
+ if (n === null) return false;
102
+ return this.ranges.some((r) => r.contains(n));
103
+ }
104
+
105
+ getRanges() {
106
+ return this.ranges.map((r) => r.serialize());
107
+ }
108
+
109
+ clear() {
110
+ this.ranges = [];
111
+ this.currentIndex = null;
112
+ this.direction = "none";
113
+ }
114
+
115
+ serialize(): SelectionData {
116
+ return {
117
+ ranges: this.getRanges(),
118
+ currentIndex: this.currentIndex,
119
+ direction: this.direction,
120
+ };
121
+ }
122
+
123
+ isEqual(other: Selection) {
124
+ if (other.ranges.length !== this.ranges.length) return false;
125
+
126
+ for (let i = 0; i < this.ranges.length; ++i) {
127
+ if (!this.ranges[i].isEqual(other.ranges[i])) return false;
128
+ }
129
+ return true;
130
+ }
131
+
132
+ private addRange(start: number, end: number) {
133
+ const r = new Range(start, end);
134
+ // Keep ranges sorted by start
135
+ const index = this.ranges.findIndex((r) => r.start >= start);
136
+ if (index === -1) this.ranges.push(r);
137
+ else this.ranges.splice(index, 0, r);
138
+ return index === -1 ? this.ranges.length - 1 : index;
139
+ }
140
+
141
+ private removeRange(r: Range) {
142
+ const index = this.ranges.indexOf(r);
143
+ this.ranges.splice(index, 1);
144
+ if (this.isEmpty()) {
145
+ this.currentIndex = null;
146
+ } else if (index === this.currentIndex) {
147
+ this.currentIndex = this.ranges.length - 1;
148
+ }
149
+ }
150
+
151
+ private isEmpty() {
152
+ return this.ranges.length === 0;
153
+ }
154
+
155
+ getAnchor() {
156
+ if (!this.current) return null;
157
+ return this.direction === "backward"
158
+ ? this.current.end
159
+ : this.current.start;
160
+ }
161
+
162
+ getFocus() {
163
+ if (!this.current) return -1;
164
+ return this.direction === "backward"
165
+ ? this.current.start
166
+ : this.current.end;
167
+ }
168
+
169
+ private compact(focus: number) {
170
+ const removals = [];
171
+ const current = this.current;
172
+ for (let r of this.ranges) {
173
+ if (!this.current || r === this.current) continue;
174
+ if (this.current.overlaps(r)) {
175
+ this.current.combine(r);
176
+ removals.push(r);
177
+ }
178
+ }
179
+ removals.forEach((r) => this.removeRange(r));
180
+ if (current) this.currentIndex = this.ranges.indexOf(current);
181
+ if (!this.current) return;
182
+ if (this.current.start < focus) this.direction = "forward";
183
+ else if (this.current.end > focus) this.direction = "backward";
184
+ else this.direction = "none";
185
+ }
186
+ }
@@ -0,0 +1,34 @@
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
+ }