react-arborist 3.7.0 → 3.9.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.
Files changed (97) hide show
  1. package/README.md +14 -0
  2. package/dist/main/components/cursor.js +1 -2
  3. package/dist/main/components/default-container.js +31 -2
  4. package/dist/main/components/default-cursor.js +1 -1
  5. package/dist/main/components/default-drag-preview.d.ts +1 -1
  6. package/dist/main/components/default-drag-preview.js +1 -1
  7. package/dist/main/components/default-row.d.ts +1 -1
  8. package/dist/main/components/default-row.js +1 -1
  9. package/dist/main/components/list-outer-element.js +1 -1
  10. package/dist/main/components/provider.d.ts +1 -1
  11. package/dist/main/components/provider.js +2 -2
  12. package/dist/main/components/provider.test.js +70 -0
  13. package/dist/main/components/row-container.d.ts +1 -1
  14. package/dist/main/components/row-container.js +2 -3
  15. package/dist/main/dnd/drag-hook.d.ts +2 -0
  16. package/dist/main/dnd/drag-hook.js +13 -4
  17. package/dist/main/dnd/drag-hook.test.d.ts +1 -0
  18. package/dist/main/dnd/drag-hook.test.js +19 -0
  19. package/dist/main/hooks/use-validated-props.js +1 -2
  20. package/dist/main/interfaces/node-api.js +4 -1
  21. package/dist/main/interfaces/tree-api.d.ts +27 -5
  22. package/dist/main/interfaces/tree-api.js +98 -14
  23. package/dist/main/interfaces/tree-api.test.js +31 -0
  24. package/dist/main/state/drag-slice.js +1 -2
  25. package/dist/main/types/dnd.d.ts +3 -1
  26. package/dist/main/types/state.d.ts +1 -1
  27. package/dist/main/types/tree-props.d.ts +4 -1
  28. package/dist/module/components/cursor.js +1 -2
  29. package/dist/module/components/default-container.js +32 -3
  30. package/dist/module/components/default-cursor.js +1 -1
  31. package/dist/module/components/default-drag-preview.d.ts +1 -1
  32. package/dist/module/components/default-drag-preview.js +1 -1
  33. package/dist/module/components/default-row.d.ts +1 -1
  34. package/dist/module/components/default-row.js +1 -1
  35. package/dist/module/components/list-outer-element.js +1 -1
  36. package/dist/module/components/provider.d.ts +1 -1
  37. package/dist/module/components/provider.js +4 -4
  38. package/dist/module/components/provider.test.js +71 -1
  39. package/dist/module/components/row-container.d.ts +1 -1
  40. package/dist/module/components/row-container.js +2 -3
  41. package/dist/module/dnd/compute-drop.js +1 -1
  42. package/dist/module/dnd/drag-hook.d.ts +2 -0
  43. package/dist/module/dnd/drag-hook.js +12 -4
  44. package/dist/module/dnd/drag-hook.test.d.ts +1 -0
  45. package/dist/module/dnd/drag-hook.test.js +17 -0
  46. package/dist/module/hooks/use-validated-props.js +1 -2
  47. package/dist/module/interfaces/node-api.js +4 -1
  48. package/dist/module/interfaces/tree-api.d.ts +27 -5
  49. package/dist/module/interfaces/tree-api.js +98 -14
  50. package/dist/module/interfaces/tree-api.test.js +31 -0
  51. package/dist/module/state/drag-slice.js +1 -2
  52. package/dist/module/types/dnd.d.ts +3 -1
  53. package/dist/module/types/state.d.ts +1 -1
  54. package/dist/module/types/tree-props.d.ts +4 -1
  55. package/package.json +27 -27
  56. package/src/components/cursor.tsx +1 -2
  57. package/src/components/default-container.tsx +40 -19
  58. package/src/components/default-cursor.tsx +1 -5
  59. package/src/components/default-drag-preview.tsx +3 -16
  60. package/src/components/default-node.tsx +0 -1
  61. package/src/components/default-row.tsx +2 -13
  62. package/src/components/drag-preview-container.tsx +1 -1
  63. package/src/components/list-inner-element.tsx +1 -1
  64. package/src/components/list-outer-element.tsx +2 -3
  65. package/src/components/provider.test.tsx +85 -9
  66. package/src/components/provider.tsx +8 -23
  67. package/src/components/row-container.tsx +4 -9
  68. package/src/components/tree.tsx +2 -6
  69. package/src/context.ts +2 -3
  70. package/src/data/create-index.ts +0 -1
  71. package/src/data/create-list.ts +1 -2
  72. package/src/data/create-root.ts +2 -9
  73. package/src/data/simple-tree.ts +5 -3
  74. package/src/dnd/compute-drop.ts +6 -15
  75. package/src/dnd/drag-hook.test.ts +22 -0
  76. package/src/dnd/drag-hook.ts +15 -6
  77. package/src/dnd/measure-hover.ts +2 -6
  78. package/src/dnd/outer-drop-hook.ts +1 -1
  79. package/src/hooks/use-fresh-node.ts +0 -1
  80. package/src/hooks/use-simple-tree.ts +2 -8
  81. package/src/hooks/use-validated-props.ts +4 -8
  82. package/src/interfaces/node-api.ts +2 -2
  83. package/src/interfaces/tree-api.test.ts +35 -0
  84. package/src/interfaces/tree-api.ts +103 -36
  85. package/src/state/dnd-slice.ts +1 -1
  86. package/src/state/drag-slice.ts +2 -5
  87. package/src/state/edit-slice.ts +1 -4
  88. package/src/state/focus-slice.ts +1 -1
  89. package/src/state/open-slice.ts +2 -5
  90. package/src/state/selection-slice.ts +2 -6
  91. package/src/types/dnd.ts +6 -1
  92. package/src/types/handlers.ts +1 -3
  93. package/src/types/renderers.ts +0 -1
  94. package/src/types/state.ts +1 -1
  95. package/src/types/tree-props.ts +15 -10
  96. package/src/types/utils.ts +2 -3
  97. package/src/utils.ts +5 -14
@@ -2,7 +2,7 @@ import { EditResult } from "../types/handlers";
2
2
  import { BoolFunc, Identity, IdObj } from "../types/utils";
3
3
  import { TreeProps } from "../types/tree-props";
4
4
  import { MutableRefObject } from "react";
5
- import { Align, FixedSizeList, ListOnItemsRenderedProps } from "react-window";
5
+ import { Align, FixedSizeList, ListOnItemsRenderedProps, VariableSizeList } from "react-window";
6
6
  import * as utils from "../utils";
7
7
  import { DefaultCursor } from "../components/default-cursor";
8
8
  import { DefaultRow } from "../components/default-row";
@@ -30,12 +30,14 @@ export class TreeApi<T> {
30
30
  visibleStartIndex: number = 0;
31
31
  visibleStopIndex: number = 0;
32
32
  idToIndex: { [id: string]: number };
33
+ /* Memoized prefix-sum of row heights; only used for variable heights. */
34
+ private rowOffsets: number[] | null = null;
33
35
 
34
36
  constructor(
35
37
  public store: Store<RootState, Actions>,
36
38
  public props: TreeProps<T>,
37
- public list: MutableRefObject<FixedSizeList | null>,
38
- public listEl: MutableRefObject<HTMLDivElement | null>
39
+ public list: MutableRefObject<FixedSizeList | VariableSizeList | null>,
40
+ public listEl: MutableRefObject<HTMLDivElement | null>,
39
41
  ) {
40
42
  /* Changes here must also be made in update() */
41
43
  this.root = createRoot<T>(this);
@@ -49,6 +51,18 @@ export class TreeApi<T> {
49
51
  this.root = createRoot<T>(this);
50
52
  this.visibleNodes = createList<T>(this);
51
53
  this.idToIndex = createIndex(this.visibleNodes);
54
+ this.rowOffsets = null;
55
+ /* Variable-height mode renders a VariableSizeList, which caches item
56
+ measurements by index and never invalidates them on its own. When the
57
+ visible nodes change (insert/remove/reorder), those cached sizes belong
58
+ to the wrong rows, so drop them. Fixed-height mode renders a
59
+ FixedSizeList (no cache, nothing to reset). update() runs during render,
60
+ so pass shouldForceUpdate=false: the in-progress render repaints the list
61
+ and a forceUpdate here would warn about setting state mid-render. */
62
+ const list = this.list.current;
63
+ if (list && "resetAfterIndex" in list) {
64
+ list.resetAfterIndex(0, false);
65
+ }
52
66
  }
53
67
 
54
68
  /* Store helpers */
@@ -79,8 +93,65 @@ export class TreeApi<T> {
79
93
  return this.props.indent ?? 24;
80
94
  }
81
95
 
96
+ /**
97
+ * The fixed row height. When a `rowHeight` function is supplied for variable
98
+ * heights, this returns the default (24); use `rowHeightAt(index)` to get the
99
+ * height of a specific row.
100
+ */
82
101
  get rowHeight() {
83
- return this.props.rowHeight ?? 24;
102
+ return typeof this.props.rowHeight === "number" ? this.props.rowHeight : 24;
103
+ }
104
+
105
+ /**
106
+ * The height of the row at `index`, evaluating the `rowHeight` function if
107
+ * given. Falls back to the default height for an out-of-range index so this
108
+ * never feeds an invalid `0` to react-window's `itemSize`.
109
+ */
110
+ rowHeightAt = (index: number): number => {
111
+ const rowHeight = this.props.rowHeight;
112
+ if (typeof rowHeight === "function") {
113
+ const node = this.at(index);
114
+ return node ? rowHeight(node) : this.rowHeight;
115
+ }
116
+ return rowHeight ?? 24;
117
+ };
118
+
119
+ /** The pixel offset of the top of the row at `index` from the top of the list. */
120
+ rowTopPosition = (index: number): number => {
121
+ /* Fixed heights: O(1). */
122
+ if (typeof this.props.rowHeight !== "function") {
123
+ return index * this.rowHeight;
124
+ }
125
+ /* Variable heights: O(1) amortized via a memoized prefix sum. */
126
+ const offsets = this.getRowOffsets();
127
+ const clamped = Math.max(0, Math.min(index, offsets.length - 1));
128
+ return offsets[clamped];
129
+ };
130
+
131
+ /**
132
+ * Tell the underlying virtualized list to recompute row heights at and after
133
+ * `index`. Call this if a `rowHeight` function's output changes for reasons
134
+ * the tree can't observe (e.g. external state).
135
+ */
136
+ redrawList = (afterIndex: number = 0) => {
137
+ this.rowOffsets = null;
138
+ /* Only the VariableSizeList (function rowHeight) caches measurements; a
139
+ FixedSizeList has constant heights and nothing to recompute. */
140
+ const list = this.list.current;
141
+ if (list && "resetAfterIndex" in list) {
142
+ list.resetAfterIndex(Math.max(0, afterIndex));
143
+ }
144
+ };
145
+
146
+ /** Lazily-built prefix sum where offsets[i] is the top of row i. */
147
+ private getRowOffsets(): number[] {
148
+ if (this.rowOffsets) return this.rowOffsets;
149
+ const offsets: number[] = [0];
150
+ for (let i = 0; i < this.visibleNodes.length; i++) {
151
+ offsets.push(offsets[i] + this.rowHeightAt(i));
152
+ }
153
+ this.rowOffsets = offsets;
154
+ return offsets;
84
155
  }
85
156
 
86
157
  get overscanCount() {
@@ -95,9 +166,7 @@ export class TreeApi<T> {
95
166
  const match =
96
167
  this.props.searchMatch ??
97
168
  ((node, term) => {
98
- const string = JSON.stringify(
99
- Object.values(node.data as { [k: string]: unknown })
100
- );
169
+ const string = JSON.stringify(Object.values(node.data as { [k: string]: unknown }));
101
170
  return string.toLocaleLowerCase().includes(term.toLocaleLowerCase());
102
171
  });
103
172
  return (node: NodeApi<T>) => match(node, this.searchTerm);
@@ -113,7 +182,7 @@ export class TreeApi<T> {
113
182
  const id = utils.access<string>(data, get);
114
183
  if (!id)
115
184
  throw new Error(
116
- "Data must contain an 'id' property or props.idAccessor must return a string"
185
+ "Data must contain an 'id' property or props.idAccessor must return a string",
117
186
  );
118
187
  return id;
119
188
  }
@@ -150,8 +219,7 @@ export class TreeApi<T> {
150
219
 
151
220
  get(id: string | null): NodeApi<T> | null {
152
221
  if (!id) return null;
153
- if (id in this.idToIndex)
154
- return this.visibleNodes[this.idToIndex[id]] || null;
222
+ if (id in this.idToIndex) return this.visibleNodes[this.idToIndex[id]] || null;
155
223
  else return null;
156
224
  }
157
225
 
@@ -194,12 +262,9 @@ export class TreeApi<T> {
194
262
  type?: "internal" | "leaf";
195
263
  parentId?: null | string;
196
264
  index?: null | number;
197
- } = {}
265
+ } = {},
198
266
  ) {
199
- const parentId =
200
- opts.parentId === undefined
201
- ? utils.getInsertParentId(this)
202
- : opts.parentId;
267
+ const parentId = opts.parentId === undefined ? utils.getInsertParentId(this) : opts.parentId;
203
268
  const index = opts.index ?? utils.getInsertIndex(this);
204
269
  const type = opts.type ?? "leaf";
205
270
  const data = await safeRun(this.props.onCreate, {
@@ -224,7 +289,10 @@ export class TreeApi<T> {
224
289
  const idents = Array.isArray(node) ? node : [node];
225
290
  const ids = idents.map(identify);
226
291
  const nodes = ids.map((id) => this.get(id)!).filter((n) => !!n);
292
+ /* Guard against Math.min(...[]) === Infinity when no ids resolve to nodes. */
293
+ const fromIndex = nodes.length ? Math.min(...nodes.map((n) => n.rowIndex ?? 0)) : 0;
227
294
  await safeRun(this.props.onDelete, { nodes, ids });
295
+ this.redrawList(fromIndex);
228
296
  }
229
297
 
230
298
  edit(node: string | IdObj): Promise<EditResult> {
@@ -232,6 +300,7 @@ export class TreeApi<T> {
232
300
  this.resolveEdit({ cancelled: true });
233
301
  this.scrollTo(id);
234
302
  this.dispatch(edit(id));
303
+ this.redrawList(this.get(id)?.rowIndex ?? 0);
235
304
  return new Promise((resolve) => {
236
305
  TreeApi.editPromise = resolve;
237
306
  });
@@ -247,12 +316,14 @@ export class TreeApi<T> {
247
316
  });
248
317
  this.dispatch(edit(null));
249
318
  this.resolveEdit({ cancelled: false, value });
319
+ this.redrawList(this.get(id)?.rowIndex ?? 0);
250
320
  setTimeout(() => this.onFocus()); // Return focus to element;
251
321
  }
252
322
 
253
323
  reset() {
254
324
  this.dispatch(edit(null));
255
325
  this.resolveEdit({ cancelled: true });
326
+ this.redrawList();
256
327
  setTimeout(() => this.onFocus()); // Return focus to element;
257
328
  }
258
329
 
@@ -389,9 +460,7 @@ export class TreeApi<T> {
389
460
  }
390
461
 
391
462
  selectAll() {
392
- const allSelectableNodes = this.filterSelectableNodes(
393
- Object.keys(this.idToIndex),
394
- );
463
+ const allSelectableNodes = this.filterSelectableNodes(Object.keys(this.idToIndex));
395
464
  this.setSelection({
396
465
  ids: allSelectableNodes,
397
466
  anchor: allSelectableNodes[0] ?? null,
@@ -408,11 +477,7 @@ export class TreeApi<T> {
408
477
  .filter((n): n is NodeApi<T> => !!n && n.isSelectable);
409
478
  }
410
479
 
411
- setSelection(args: {
412
- ids: (IdObj | string)[] | null;
413
- anchor: Identity;
414
- mostRecent: Identity;
415
- }) {
480
+ setSelection(args: { ids: (IdObj | string)[] | null; anchor: Identity; mostRecent: Identity }) {
416
481
  const ids = new Set(args.ids?.map(identify));
417
482
  const anchor = identifyNull(args.anchor);
418
483
  const mostRecent = identifyNull(args.mostRecent);
@@ -437,9 +502,7 @@ export class TreeApi<T> {
437
502
  }
438
503
 
439
504
  get dragNodes() {
440
- return this.state.dnd.dragIds
441
- .map((id) => this.get(id))
442
- .filter((n) => !!n) as NodeApi<T>[];
505
+ return this.state.dnd.dragIds.map((id) => this.get(id)).filter((n) => !!n) as NodeApi<T>[];
443
506
  }
444
507
 
445
508
  get dragNode() {
@@ -493,19 +556,21 @@ export class TreeApi<T> {
493
556
 
494
557
  /* Visibility */
495
558
 
496
- open(identity: Identity) {
559
+ open(identity: Identity, redraw: boolean = true) {
497
560
  const id = identifyNull(identity);
498
561
  if (!id) return;
499
562
  if (this.isOpen(id)) return;
500
563
  this.dispatch(visibility.open(id, this.isFiltered));
564
+ if (redraw) this.redrawList(this.get(id)?.rowIndex ?? 0);
501
565
  safeRun(this.props.onToggle, id);
502
566
  }
503
567
 
504
- close(identity: Identity) {
568
+ close(identity: Identity, redraw: boolean = true) {
505
569
  const id = identifyNull(identity);
506
570
  if (!id) return;
507
571
  if (!this.isOpen(id)) return;
508
572
  this.dispatch(visibility.close(id, this.isFiltered));
573
+ if (redraw) this.redrawList(this.get(id)?.rowIndex ?? 0);
509
574
  safeRun(this.props.onToggle, id);
510
575
  }
511
576
 
@@ -522,9 +587,10 @@ export class TreeApi<T> {
522
587
  let parent = node?.parent;
523
588
 
524
589
  while (parent) {
525
- this.open(parent.id);
590
+ this.open(parent.id, false);
526
591
  parent = parent.parent;
527
592
  }
593
+ this.redrawList();
528
594
  }
529
595
 
530
596
  openSiblings(node: NodeApi<T>) {
@@ -535,23 +601,27 @@ export class TreeApi<T> {
535
601
  const isOpen = node.isOpen;
536
602
  for (let sibling of parent.children) {
537
603
  if (sibling.isInternal) {
538
- isOpen ? this.close(sibling.id) : this.open(sibling.id);
604
+ if (isOpen) this.close(sibling.id, false);
605
+ else this.open(sibling.id, false);
539
606
  }
540
607
  }
608
+ this.redrawList();
541
609
  this.scrollTo(this.focusedNode);
542
610
  }
543
611
  }
544
612
 
545
613
  openAll() {
546
614
  utils.walk(this.root, (node) => {
547
- if (node.isInternal) node.open();
615
+ if (node.isInternal) this.open(node.id, false);
548
616
  });
617
+ this.redrawList();
549
618
  }
550
619
 
551
620
  closeAll() {
552
621
  utils.walk(this.root, (node) => {
553
- if (node.isInternal) node.close();
622
+ if (node.isInternal) this.close(node.id, false);
554
623
  });
624
+ this.redrawList();
555
625
  }
556
626
 
557
627
  /* Scrolling */
@@ -626,10 +696,7 @@ export class TreeApi<T> {
626
696
  return this.isActionPossible(data, this.props.disableSelect);
627
697
  }
628
698
 
629
- private isActionPossible(
630
- data: T,
631
- disabler: string | boolean | BoolFunc<T> = () => false,
632
- ) {
699
+ private isActionPossible(data: T, disabler: string | boolean | BoolFunc<T> = () => false) {
633
700
  return !utils.access(data, disabler);
634
701
  }
635
702
 
@@ -30,7 +30,7 @@ export const actions = {
30
30
  /* Reducer */
31
31
  export function reducer(
32
32
  state: DndState = initialState()["dnd"],
33
- action: ActionTypes<typeof actions>
33
+ action: ActionTypes<typeof actions>,
34
34
  ): DndState {
35
35
  switch (action.type) {
36
36
  case "DND_CURSOR":
@@ -15,7 +15,7 @@ export type DragSlice = {
15
15
 
16
16
  export function reducer(
17
17
  state: DragSlice = initialState().nodes.drag,
18
- action: ActionTypes<typeof dnd>
18
+ action: ActionTypes<typeof dnd>,
19
19
  ): DragSlice {
20
20
  switch (action.type) {
21
21
  case "DND_DRAG_START":
@@ -29,10 +29,7 @@ export function reducer(
29
29
  selectedIds: [],
30
30
  };
31
31
  case "DND_HOVERING":
32
- if (
33
- action.parentId !== state.destinationParentId ||
34
- action.index != state.destinationIndex
35
- ) {
32
+ if (action.parentId !== state.destinationParentId || action.index != state.destinationIndex) {
36
33
  return {
37
34
  ...state,
38
35
  destinationParentId: action.parentId,
@@ -7,10 +7,7 @@ export function edit(id: string | null) {
7
7
  }
8
8
 
9
9
  /* Reducer */
10
- export function reducer(
11
- state: EditState = { id: null },
12
- action: ReturnType<typeof edit>
13
- ) {
10
+ export function reducer(state: EditState = { id: null }, action: ReturnType<typeof edit>) {
14
11
  if (action.type === "EDIT") {
15
12
  return { ...state, id: action.id };
16
13
  } else {
@@ -16,7 +16,7 @@ export function treeBlur() {
16
16
 
17
17
  export function reducer(
18
18
  state: FocusState = { id: null, treeFocused: false },
19
- action: ReturnType<typeof focus> | ReturnType<typeof treeBlur>
19
+ action: ReturnType<typeof focus> | ReturnType<typeof treeBlur>,
20
20
  ) {
21
21
  if (action.type === "FOCUS") {
22
22
  return { ...state, id: action.id, treeFocused: true };
@@ -22,10 +22,7 @@ export const actions = {
22
22
 
23
23
  /* Reducer */
24
24
 
25
- function openMapReducer(
26
- state: OpenMap = {},
27
- action: ActionTypes<typeof actions>
28
- ) {
25
+ function openMapReducer(state: OpenMap = {}, action: ActionTypes<typeof actions>) {
29
26
  if (action.type === "VISIBILITY_OPEN") {
30
27
  return { ...state, [action.id]: true };
31
28
  } else if (action.type === "VISIBILITY_CLOSE") {
@@ -42,7 +39,7 @@ function openMapReducer(
42
39
 
43
40
  export function reducer(
44
41
  state: OpenSlice = { filtered: {}, unfiltered: {} },
45
- action: ActionTypes<typeof actions>
42
+ action: ActionTypes<typeof actions>,
46
43
  ): OpenSlice {
47
44
  if (!action.type.startsWith("VISIBILITY")) return state;
48
45
  if (action.filtered) {
@@ -28,11 +28,7 @@ export const actions = {
28
28
  ids: (Array.isArray(id) ? id : [id]).map(identify),
29
29
  }),
30
30
 
31
- set: (args: {
32
- ids: Set<string>;
33
- anchor: string | null;
34
- mostRecent: string | null;
35
- }) => ({
31
+ set: (args: { ids: Set<string>; anchor: string | null; mostRecent: string | null }) => ({
36
32
  type: "SELECTION_SET" as const,
37
33
  ...args,
38
34
  }),
@@ -51,7 +47,7 @@ export const actions = {
51
47
  /* Reducer */
52
48
  export function reducer(
53
49
  state: SelectionState = initialState()["nodes"]["selection"],
54
- action: ActionTypes<typeof actions>
50
+ action: ActionTypes<typeof actions>,
55
51
  ): SelectionState {
56
52
  const ids = state.ids;
57
53
  switch (action.type) {
package/src/types/dnd.ts CHANGED
@@ -4,6 +4,11 @@ export type CursorLocation = {
4
4
  parentId: string | null;
5
5
  };
6
6
 
7
- export type DragItem = {
7
+ export type DragItem<T = any> = {
8
+ /* The id of the row the drag started on. */
8
9
  id: string;
10
+ /* Every node carried by the drag (the selection, or just `id`). */
11
+ dragIds: string[];
12
+ /* The dragged node's data, so external drop targets can read it. */
13
+ data: T;
9
14
  };
@@ -27,6 +27,4 @@ export type DeleteHandler<T> = (args: {
27
27
  nodes: NodeApi<T>[];
28
28
  }) => void | Promise<void>;
29
29
 
30
- export type EditResult =
31
- | { cancelled: true }
32
- | { cancelled: false; value: string };
30
+ export type EditResult = { cancelled: true } | { cancelled: false; value: string };
@@ -1,5 +1,4 @@
1
1
  import { CSSProperties, HTMLAttributes, ReactElement } from "react";
2
- import { IdObj } from "./utils";
3
2
  import { NodeApi } from "../interfaces/node-api";
4
3
  import { TreeApi } from "../interfaces/tree-api";
5
4
  import { XYCoord } from "react-dnd";
@@ -1,3 +1,3 @@
1
1
  import { NodeApi } from "../interfaces/node-api";
2
2
 
3
- export type NodeState = typeof NodeApi.prototype["state"];
3
+ export type NodeState = (typeof NodeApi.prototype)["state"];
@@ -7,6 +7,9 @@ import { NodeApi } from "../interfaces/node-api";
7
7
  import { OpenMap } from "../state/open-slice";
8
8
  import { useDragDropManager, DndProviderProps } from "react-dnd";
9
9
 
10
+ /** Returns the height in pixels for a given node's row. */
11
+ export type RowHeightAccessor<T> = (node: NodeApi<T>) => number;
12
+
10
13
  export interface TreeProps<T> {
11
14
  /* Data Options */
12
15
  data?: readonly T[];
@@ -26,7 +29,7 @@ export interface TreeProps<T> {
26
29
  renderContainer?: ElementType<{}>;
27
30
 
28
31
  /* Sizes */
29
- rowHeight?: number;
32
+ rowHeight?: number | RowHeightAccessor<T>;
30
33
  overscanCount?: number;
31
34
  width?: number | string;
32
35
  height?: number;
@@ -47,11 +50,7 @@ export interface TreeProps<T> {
47
50
  disableDrop?:
48
51
  | string
49
52
  | boolean
50
- | ((args: {
51
- parentNode: NodeApi<T>;
52
- dragNodes: NodeApi<T>[];
53
- index: number;
54
- }) => boolean);
53
+ | ((args: { parentNode: NodeApi<T>; dragNodes: NodeApi<T>[]; index: number }) => boolean);
55
54
 
56
55
  /* Event Handlers */
57
56
  onActivate?: (node: NodeApi<T>) => void;
@@ -77,12 +76,18 @@ export interface TreeProps<T> {
77
76
  dndRootElement?: globalThis.Node | null;
78
77
  onClick?: MouseEventHandler;
79
78
  onContextMenu?: MouseEventHandler;
80
- dndBackend?: Extract<
81
- DndProviderProps<unknown, unknown>,
82
- { backend: unknown }
83
- >["backend"];
79
+ dndBackend?: Extract<DndProviderProps<unknown, unknown>, { backend: unknown }>["backend"];
84
80
  dndManager?: ReturnType<typeof useDragDropManager>;
85
81
 
82
+ /* The react-dnd item type each row's drag source advertises. Defaults to
83
+ "NODE". Set a custom value (or a per-node function) so rows can be dropped
84
+ onto external react-dnd targets that accept that type. The dragged node's
85
+ data is always exposed on the drag item, so an external target accepting
86
+ the default "NODE" type can read it without setting this. Note: the tree's
87
+ own drop targets only accept "NODE", so a row given a custom type is no
88
+ longer reorderable within the tree. */
89
+ dragType?: string | ((node: NodeApi<T>) => string);
90
+
86
91
  /* Custom react-window outer/inner elements */
87
92
  outerElementType?: ReactWindowCommonProps["outerElementType"];
88
93
  innerElementType?: ReactWindowCommonProps["innerElementType"];
@@ -9,9 +9,8 @@ export type Identity = string | IdObj | null;
9
9
 
10
10
  export type BoolFunc<T> = (data: T) => boolean;
11
11
 
12
- export type ActionTypes<
13
- Actions extends { [name: string]: (...args: any[]) => AnyAction }
14
- > = ReturnType<Actions[keyof Actions]>;
12
+ export type ActionTypes<Actions extends { [name: string]: (...args: any[]) => AnyAction }> =
13
+ ReturnType<Actions[keyof Actions]>;
15
14
 
16
15
  export type SelectOptions = { multi?: boolean; contiguous?: boolean };
17
16
 
package/src/utils.ts CHANGED
@@ -49,10 +49,7 @@ export function dfs(node: NodeApi<any>, id: string): NodeApi<any> | null {
49
49
  return null;
50
50
  }
51
51
 
52
- export function walk(
53
- node: NodeApi<any>,
54
- fn: (node: NodeApi<any>) => void
55
- ): void {
52
+ export function walk(node: NodeApi<any>, fn: (node: NodeApi<any>) => void): void {
56
53
  fn(node);
57
54
  if (node.children) {
58
55
  for (let child of node.children) {
@@ -110,15 +107,12 @@ function prevItem(list: HTMLElement[], index: number) {
110
107
  function getFocusable(target: HTMLElement) {
111
108
  return Array.from(
112
109
  document.querySelectorAll(
113
- 'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"]):not([disabled]), details:not([disabled]), summary:not(:disabled)'
114
- )
110
+ 'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"]):not([disabled]), details:not([disabled]), summary:not(:disabled)',
111
+ ),
115
112
  ).filter((e) => e === target || !target.contains(e)) as HTMLElement[];
116
113
  }
117
114
 
118
- export function access<T = boolean>(
119
- obj: any,
120
- accessor: string | boolean | Function
121
- ): T {
115
+ export function access<T = boolean>(obj: any, accessor: string | boolean | Function): T {
122
116
  if (typeof accessor === "boolean") return accessor as unknown as T;
123
117
  if (typeof accessor === "string") return obj[accessor] as T;
124
118
  return accessor(obj) as T;
@@ -234,10 +228,7 @@ const defaultTreeLineChars: TreeLineChars = {
234
228
  * getTreeLinePrefix(node, { last: "`- ", middle: "|- ", pipe: "|", blank: " " })
235
229
  * ```
236
230
  */
237
- export function getTreeLinePrefix(
238
- node: NodeApi<any>,
239
- chars: Partial<TreeLineChars> = {}
240
- ): string {
231
+ export function getTreeLinePrefix(node: NodeApi<any>, chars: Partial<TreeLineChars> = {}): string {
241
232
  const c = { ...defaultTreeLineChars, ...chars };
242
233
  if (node.level === 0) return "";
243
234