@headless-tree/core 0.0.14 → 1.0.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 (125) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/lib/cjs/core/create-tree.js +13 -4
  3. package/lib/cjs/features/async-data-loader/feature.js +73 -48
  4. package/lib/cjs/features/async-data-loader/types.d.ts +17 -14
  5. package/lib/cjs/features/drag-and-drop/feature.js +98 -93
  6. package/lib/cjs/features/drag-and-drop/types.d.ts +17 -29
  7. package/lib/cjs/features/drag-and-drop/types.js +7 -7
  8. package/lib/cjs/features/drag-and-drop/utils.d.ts +25 -3
  9. package/lib/cjs/features/drag-and-drop/utils.js +51 -51
  10. package/lib/cjs/features/expand-all/feature.js +26 -3
  11. package/lib/cjs/features/expand-all/types.d.ts +3 -1
  12. package/lib/cjs/features/hotkeys-core/feature.js +7 -3
  13. package/lib/cjs/features/hotkeys-core/types.d.ts +4 -5
  14. package/lib/cjs/features/keyboard-drag-and-drop/feature.d.ts +2 -0
  15. package/lib/cjs/features/keyboard-drag-and-drop/feature.js +206 -0
  16. package/lib/cjs/features/keyboard-drag-and-drop/types.d.ts +27 -0
  17. package/lib/cjs/features/keyboard-drag-and-drop/types.js +11 -0
  18. package/lib/cjs/features/prop-memoization/feature.js +33 -11
  19. package/lib/cjs/features/prop-memoization/types.d.ts +8 -3
  20. package/lib/cjs/features/renaming/feature.js +1 -1
  21. package/lib/cjs/features/search/feature.js +2 -0
  22. package/lib/cjs/features/search/types.d.ts +2 -2
  23. package/lib/cjs/features/selection/feature.js +4 -4
  24. package/lib/cjs/features/selection/types.d.ts +1 -1
  25. package/lib/cjs/features/sync-data-loader/feature.js +31 -5
  26. package/lib/cjs/features/sync-data-loader/types.d.ts +5 -5
  27. package/lib/cjs/features/tree/feature.js +4 -9
  28. package/lib/cjs/features/tree/types.d.ts +7 -5
  29. package/lib/cjs/index.d.ts +2 -0
  30. package/lib/cjs/index.js +2 -0
  31. package/lib/cjs/mddocs-entry.d.ts +10 -0
  32. package/lib/cjs/test-utils/test-tree-do.d.ts +2 -2
  33. package/lib/cjs/test-utils/test-tree-do.js +19 -6
  34. package/lib/cjs/test-utils/test-tree-expect.d.ts +5 -3
  35. package/lib/cjs/test-utils/test-tree-expect.js +3 -0
  36. package/lib/cjs/test-utils/test-tree.d.ts +2 -1
  37. package/lib/cjs/test-utils/test-tree.js +24 -21
  38. package/lib/cjs/types/core.d.ts +2 -1
  39. package/lib/cjs/utilities/create-on-drop-handler.d.ts +2 -2
  40. package/lib/cjs/utilities/create-on-drop-handler.js +13 -4
  41. package/lib/cjs/utilities/insert-items-at-target.d.ts +2 -2
  42. package/lib/cjs/utilities/insert-items-at-target.js +21 -12
  43. package/lib/cjs/utilities/remove-items-from-parents.d.ts +1 -1
  44. package/lib/cjs/utilities/remove-items-from-parents.js +12 -3
  45. package/lib/esm/core/create-tree.js +13 -4
  46. package/lib/esm/features/async-data-loader/feature.js +73 -48
  47. package/lib/esm/features/async-data-loader/types.d.ts +17 -14
  48. package/lib/esm/features/drag-and-drop/feature.js +99 -94
  49. package/lib/esm/features/drag-and-drop/types.d.ts +17 -29
  50. package/lib/esm/features/drag-and-drop/types.js +6 -6
  51. package/lib/esm/features/drag-and-drop/utils.d.ts +25 -3
  52. package/lib/esm/features/drag-and-drop/utils.js +45 -49
  53. package/lib/esm/features/expand-all/feature.js +26 -3
  54. package/lib/esm/features/expand-all/types.d.ts +3 -1
  55. package/lib/esm/features/hotkeys-core/feature.js +7 -3
  56. package/lib/esm/features/hotkeys-core/types.d.ts +4 -5
  57. package/lib/esm/features/keyboard-drag-and-drop/feature.d.ts +2 -0
  58. package/lib/esm/features/keyboard-drag-and-drop/feature.js +203 -0
  59. package/lib/esm/features/keyboard-drag-and-drop/types.d.ts +27 -0
  60. package/lib/esm/features/keyboard-drag-and-drop/types.js +8 -0
  61. package/lib/esm/features/prop-memoization/feature.js +33 -11
  62. package/lib/esm/features/prop-memoization/types.d.ts +8 -3
  63. package/lib/esm/features/renaming/feature.js +1 -1
  64. package/lib/esm/features/search/feature.js +2 -0
  65. package/lib/esm/features/search/types.d.ts +2 -2
  66. package/lib/esm/features/selection/feature.js +4 -4
  67. package/lib/esm/features/selection/types.d.ts +1 -1
  68. package/lib/esm/features/sync-data-loader/feature.js +31 -5
  69. package/lib/esm/features/sync-data-loader/types.d.ts +5 -5
  70. package/lib/esm/features/tree/feature.js +4 -9
  71. package/lib/esm/features/tree/types.d.ts +7 -5
  72. package/lib/esm/index.d.ts +2 -0
  73. package/lib/esm/index.js +2 -0
  74. package/lib/esm/mddocs-entry.d.ts +10 -0
  75. package/lib/esm/test-utils/test-tree-do.d.ts +2 -2
  76. package/lib/esm/test-utils/test-tree-do.js +19 -6
  77. package/lib/esm/test-utils/test-tree-expect.d.ts +5 -3
  78. package/lib/esm/test-utils/test-tree-expect.js +3 -0
  79. package/lib/esm/test-utils/test-tree.d.ts +2 -1
  80. package/lib/esm/test-utils/test-tree.js +24 -21
  81. package/lib/esm/types/core.d.ts +2 -1
  82. package/lib/esm/utilities/create-on-drop-handler.d.ts +2 -2
  83. package/lib/esm/utilities/create-on-drop-handler.js +13 -4
  84. package/lib/esm/utilities/insert-items-at-target.d.ts +2 -2
  85. package/lib/esm/utilities/insert-items-at-target.js +21 -12
  86. package/lib/esm/utilities/remove-items-from-parents.d.ts +1 -1
  87. package/lib/esm/utilities/remove-items-from-parents.js +12 -3
  88. package/package.json +2 -2
  89. package/src/core/core.spec.ts +31 -0
  90. package/src/core/create-tree.ts +15 -5
  91. package/src/features/async-data-loader/async-data-loader.spec.ts +10 -6
  92. package/src/features/async-data-loader/feature.ts +76 -48
  93. package/src/features/async-data-loader/types.ts +18 -11
  94. package/src/features/drag-and-drop/drag-and-drop.spec.ts +75 -89
  95. package/src/features/drag-and-drop/feature.ts +26 -22
  96. package/src/features/drag-and-drop/types.ts +23 -35
  97. package/src/features/drag-and-drop/utils.ts +70 -57
  98. package/src/features/expand-all/feature.ts +29 -5
  99. package/src/features/expand-all/types.ts +3 -1
  100. package/src/features/hotkeys-core/feature.ts +4 -0
  101. package/src/features/hotkeys-core/types.ts +4 -13
  102. package/src/features/keyboard-drag-and-drop/feature.ts +255 -0
  103. package/src/features/keyboard-drag-and-drop/keyboard-drag-and-drop.spec.ts +402 -0
  104. package/src/features/keyboard-drag-and-drop/types.ts +30 -0
  105. package/src/features/prop-memoization/feature.ts +27 -8
  106. package/src/features/prop-memoization/prop-memoization.spec.ts +2 -2
  107. package/src/features/prop-memoization/types.ts +8 -3
  108. package/src/features/renaming/feature.ts +8 -2
  109. package/src/features/search/feature.ts +2 -0
  110. package/src/features/search/types.ts +2 -2
  111. package/src/features/selection/feature.ts +4 -4
  112. package/src/features/selection/types.ts +1 -1
  113. package/src/features/sync-data-loader/feature.ts +26 -7
  114. package/src/features/sync-data-loader/types.ts +5 -5
  115. package/src/features/tree/feature.ts +8 -13
  116. package/src/features/tree/types.ts +7 -5
  117. package/src/index.ts +2 -0
  118. package/src/mddocs-entry.ts +16 -0
  119. package/src/test-utils/test-tree-do.ts +3 -3
  120. package/src/test-utils/test-tree-expect.ts +7 -2
  121. package/src/test-utils/test-tree.ts +26 -22
  122. package/src/types/core.ts +2 -0
  123. package/src/utilities/create-on-drop-handler.ts +4 -4
  124. package/src/utilities/insert-items-at-target.ts +18 -14
  125. package/src/utilities/remove-items-from-parents.ts +6 -3
@@ -1,7 +1,7 @@
1
1
  import { ItemInstance, TreeInstance } from "../../types/core";
2
- import { DropTarget } from "./types";
2
+ import { DragTarget } from "./types";
3
3
 
4
- enum ItemDropCategory {
4
+ export enum ItemDropCategory {
5
5
  Item,
6
6
  ExpandedFolder,
7
7
  LastInGroup,
@@ -26,9 +26,12 @@ type TargetPlacement =
26
26
  reparentLevel: number;
27
27
  };
28
28
 
29
+ export const isOrderedDragTarget = <T>(dragTarget: DragTarget<T>) =>
30
+ "childIndex" in dragTarget;
31
+
29
32
  export const canDrop = (
30
33
  dataTransfer: DataTransfer | null,
31
- target: DropTarget<any>,
34
+ target: DragTarget<any>,
32
35
  tree: TreeInstance<any>,
33
36
  ) => {
34
37
  const draggedItems = tree.getState().dnd?.draggedItems;
@@ -52,7 +55,8 @@ export const canDrop = (
52
55
  if (
53
56
  !draggedItems &&
54
57
  dataTransfer &&
55
- !config.canDropForeignDragObject?.(dataTransfer, target)
58
+ config.canDropForeignDragObject &&
59
+ !config.canDropForeignDragObject(dataTransfer, target)
56
60
  ) {
57
61
  return false;
58
62
  }
@@ -60,19 +64,37 @@ export const canDrop = (
60
64
  return true;
61
65
  };
62
66
 
63
- const getItemDropCategory = (item: ItemInstance<any>) => {
67
+ export const getItemDropCategory = (item: ItemInstance<any>) => {
64
68
  if (item.isExpanded()) {
65
69
  return ItemDropCategory.ExpandedFolder;
66
70
  }
67
71
 
68
72
  const parent = item.getParent();
69
- if (parent && item.getIndexInParent() === parent.getItemMeta().setSize - 1) {
73
+ if (parent && item.getIndexInParent() === item.getItemMeta().setSize - 1) {
70
74
  return ItemDropCategory.LastInGroup;
71
75
  }
72
76
 
73
77
  return ItemDropCategory.Item;
74
78
  };
75
79
 
80
+ export const getInsertionIndex = <T>(
81
+ children: ItemInstance<T>[],
82
+ childIndex: number,
83
+ draggedItems: ItemInstance<T>[] | undefined,
84
+ ) => {
85
+ const numberOfDragItemsBeforeTarget =
86
+ children
87
+ .slice(0, childIndex)
88
+ .reduce(
89
+ (counter, child) =>
90
+ child && draggedItems?.some((i) => i.getId() === child.getId())
91
+ ? ++counter
92
+ : counter,
93
+ 0,
94
+ ) ?? 0;
95
+ return childIndex - numberOfDragItemsBeforeTarget;
96
+ };
97
+
76
98
  const getTargetPlacement = (
77
99
  e: any,
78
100
  item: ItemInstance<any>,
@@ -88,8 +110,8 @@ const getTargetPlacement = (
88
110
  }
89
111
 
90
112
  const bb = item.getElement()?.getBoundingClientRect();
91
- const topPercent = bb ? (e.pageY - bb.top) / bb.height : 0.5;
92
- const leftPixels = bb ? e.pageX - bb.left : 0;
113
+ const topPercent = bb ? (e.clientY - bb.top) / bb.height : 0.5;
114
+ const leftPixels = bb ? e.clientX - bb.left : 0;
93
115
  const targetDropCategory = getItemDropCategory(item);
94
116
  const reorderAreaPercentage = !canMakeChild
95
117
  ? 0.5
@@ -111,9 +133,10 @@ const getTargetPlacement = (
111
133
  if (topPercent < 0.5) {
112
134
  return { type: PlacementType.ReorderAbove };
113
135
  }
136
+ const minLevel = item.getItemBelow()?.getItemMeta().level ?? 0;
114
137
  return {
115
138
  type: PlacementType.Reparent,
116
- reparentLevel: Math.floor(leftPixels / indent),
139
+ reparentLevel: Math.max(minLevel, Math.floor(leftPixels / indent)),
117
140
  };
118
141
  }
119
142
  // if not at left of item area, treat as if it was a normal item
@@ -152,31 +175,41 @@ const getNthParent = (
152
175
  return getNthParent(item.getParent()!, n);
153
176
  };
154
177
 
155
- export const getDropTarget = (
178
+ /** @param item refers to the bottom-most item of the container, at which bottom is being reparented on (e.g. root-1-2-6) */
179
+ export const getReparentTarget = <T>(
180
+ item: ItemInstance<T>,
181
+ reparentLevel: number,
182
+ draggedItems: ItemInstance<T>[] | undefined,
183
+ ) => {
184
+ const itemMeta = item.getItemMeta();
185
+ const reparentedTarget = getNthParent(item, reparentLevel - 1);
186
+ const targetItemAbove = getNthParent(item, reparentLevel); // .getItemBelow()!;
187
+ const targetIndex = targetItemAbove.getIndexInParent() + 1;
188
+
189
+ return {
190
+ item: reparentedTarget,
191
+ childIndex: targetIndex,
192
+ insertionIndex: getInsertionIndex(
193
+ reparentedTarget.getChildren(),
194
+ targetIndex,
195
+ draggedItems,
196
+ ),
197
+ dragLineIndex: itemMeta.index + 1,
198
+ dragLineLevel: reparentLevel,
199
+ };
200
+ };
201
+
202
+ export const getDragTarget = (
156
203
  e: any,
157
204
  item: ItemInstance<any>,
158
205
  tree: TreeInstance<any>,
159
206
  canReorder = tree.getConfig().canReorder,
160
- ): DropTarget<any> => {
161
- const draggedItems = tree.getState().dnd?.draggedItems ?? [];
207
+ ): DragTarget<any> => {
208
+ const draggedItems = tree.getState().dnd?.draggedItems;
162
209
  const itemMeta = item.getItemMeta();
163
210
  const parent = item.getParent();
164
- const itemTarget: DropTarget<any> = {
165
- item,
166
- childIndex: null,
167
- insertionIndex: null,
168
- dragLineIndex: null,
169
- dragLineLevel: null,
170
- };
171
- const parentTarget: DropTarget<any> | null = parent
172
- ? {
173
- item: parent,
174
- childIndex: null,
175
- insertionIndex: null,
176
- dragLineIndex: null,
177
- dragLineLevel: null,
178
- }
179
- : null;
211
+ const itemTarget: DragTarget<any> = { item };
212
+ const parentTarget: DragTarget<any> | null = parent ? { item: parent } : null;
180
213
  const canBecomeSibling =
181
214
  parentTarget && canDrop(e.dataTransfer, parentTarget, tree);
182
215
 
@@ -193,8 +226,8 @@ export const getDropTarget = (
193
226
  }
194
227
 
195
228
  if (!canReorder && parent && !canBecomeSibling) {
196
- // TODO! this breaks in story DND/Can Drop. Maybe move this logic into a composable DropTargetStrategy[] ?
197
- return getDropTarget(e, parent, tree, false);
229
+ // TODO! this breaks in story DND/Can Drop. Maybe move this logic into a composable DragTargetStrategy[] ?
230
+ return getDragTarget(e, parent, tree, false);
198
231
  }
199
232
 
200
233
  if (!parent) {
@@ -207,47 +240,27 @@ export const getDropTarget = (
207
240
  }
208
241
 
209
242
  if (!canBecomeSibling) {
210
- return getDropTarget(e, parent, tree, false);
243
+ return getDragTarget(e, parent, tree, false);
211
244
  }
212
245
 
213
246
  if (placement.type === PlacementType.Reparent) {
214
- const reparentedTarget = getNthParent(item, placement.reparentLevel - 1);
215
- const targetItemAbove = getNthParent(item, placement.reparentLevel); // .getItemBelow()!;
216
- const targetIndex = targetItemAbove.getIndexInParent() + 1;
217
-
218
- // TODO possibly count items dragged out above the new target
219
-
220
- return {
221
- item: reparentedTarget,
222
- childIndex: targetIndex,
223
- insertionIndex: targetIndex,
224
- dragLineIndex: itemMeta.index + 1,
225
- dragLineLevel: placement.reparentLevel,
226
- };
247
+ return getReparentTarget(item, placement.reparentLevel, draggedItems);
227
248
  }
228
249
 
229
250
  const maybeAddOneForBelow =
230
251
  placement.type === PlacementType.ReorderAbove ? 0 : 1;
231
252
  const childIndex = item.getIndexInParent() + maybeAddOneForBelow;
232
253
 
233
- const numberOfDragItemsBeforeTarget =
234
- parent
235
- .getChildren()
236
- .slice(0, childIndex)
237
- .reduce(
238
- (counter, child) =>
239
- child && draggedItems?.some((i) => i.getId() === child.getId())
240
- ? ++counter
241
- : counter,
242
- 0,
243
- ) ?? 0;
244
-
245
254
  return {
246
255
  item: parent,
247
256
  dragLineIndex: itemMeta.index + maybeAddOneForBelow,
248
257
  dragLineLevel: itemMeta.level,
249
258
  childIndex,
250
259
  // TODO performance could be improved by computing this only when dragcode changed
251
- insertionIndex: childIndex - numberOfDragItemsBeforeTarget,
260
+ insertionIndex: getInsertionIndex(
261
+ parent.getChildren(),
262
+ childIndex,
263
+ draggedItems,
264
+ ),
252
265
  };
253
266
  };
@@ -1,5 +1,4 @@
1
1
  import { FeatureImplementation } from "../../types/core";
2
- import { poll } from "../../utils";
3
2
 
4
3
  export const expandAllFeature: FeatureImplementation = {
5
4
  key: "expand-all",
@@ -27,22 +26,47 @@ export const expandAllFeature: FeatureImplementation = {
27
26
  }
28
27
 
29
28
  item.expand();
30
- await poll(() => !tree.getState().loadingItems.includes(item.getId()));
29
+ await tree.waitForItemChildrenLoaded(item.getId());
31
30
  await Promise.all(
32
31
  item.getChildren().map(async (child) => {
33
- await poll(
34
- () => !tree.getState().loadingItems.includes(child.getId()),
35
- );
32
+ await tree.waitForItemChildrenLoaded(item.getId());
36
33
  await child?.expandAll(cancelToken);
37
34
  }),
38
35
  );
39
36
  },
40
37
 
41
38
  collapseAll: ({ item }) => {
39
+ if (!item.isExpanded()) return;
42
40
  for (const child of item.getChildren()) {
43
41
  child?.collapseAll();
44
42
  }
45
43
  item.collapse();
46
44
  },
47
45
  },
46
+
47
+ hotkeys: {
48
+ expandSelected: {
49
+ hotkey: "Control+Shift+Plus",
50
+ handler: async (_, tree) => {
51
+ const cancelToken = { current: false };
52
+ const cancelHandler = (e: KeyboardEvent) => {
53
+ if (e.key === "Escape") {
54
+ cancelToken.current = true;
55
+ }
56
+ };
57
+ document.addEventListener("keydown", cancelHandler);
58
+ await Promise.all(
59
+ tree.getSelectedItems().map((item) => item.expandAll(cancelToken)),
60
+ );
61
+ document.removeEventListener("keydown", cancelHandler);
62
+ },
63
+ },
64
+
65
+ collapseSelected: {
66
+ hotkey: "Control+Shift+-",
67
+ handler: (_, tree) => {
68
+ tree.getSelectedItems().forEach((item) => item.collapseAll());
69
+ },
70
+ },
71
+ },
48
72
  };
@@ -1,3 +1,5 @@
1
+ export interface ExpandAllDataRef {}
2
+
1
3
  export type ExpandAllFeatureDef = {
2
4
  state: {};
3
5
  config: {};
@@ -9,5 +11,5 @@ export type ExpandAllFeatureDef = {
9
11
  expandAll: (cancelToken?: { current: boolean }) => Promise<void>;
10
12
  collapseAll: () => void;
11
13
  };
12
- hotkeys: never;
14
+ hotkeys: "expandSelected" | "collapseSelected";
13
15
  };
@@ -8,6 +8,8 @@ import { HotkeyConfig, HotkeysCoreDataRef } from "./types";
8
8
  const specialKeys: Record<string, RegExp> = {
9
9
  Letter: /^[a-z]$/,
10
10
  LetterOrNumber: /^[a-z0-9]$/,
11
+ Plus: /^\+$/,
12
+ Space: /^ $/,
11
13
  };
12
14
 
13
15
  const testHotkeyMatch = (
@@ -46,6 +48,7 @@ export const hotkeysCoreFeature: FeatureImplementation = {
46
48
  data.current.pressedKeys ??= new Set();
47
49
  const newMatch = !data.current.pressedKeys.has(e.key);
48
50
  data.current.pressedKeys.add(e.key);
51
+ console.log("HOTKEYS", data.current.pressedKeys);
49
52
 
50
53
  const hotkeyName = findHotkeyMatch(
51
54
  data.current.pressedKeys,
@@ -71,6 +74,7 @@ export const hotkeysCoreFeature: FeatureImplementation = {
71
74
  if (hotkeyConfig.preventDefault) e.preventDefault();
72
75
 
73
76
  hotkeyConfig.handler(e, tree as any);
77
+ tree.getConfig().onTreeHotkey?.(hotkeyName, e);
74
78
  };
75
79
 
76
80
  const keyup = (e: KeyboardEvent) => {
@@ -1,8 +1,4 @@
1
- import {
2
- CustomHotkeysConfig,
3
- ItemInstance,
4
- TreeInstance,
5
- } from "../../types/core";
1
+ import { CustomHotkeysConfig, TreeInstance } from "../../types/core";
6
2
 
7
3
  export interface HotkeyConfig<T> {
8
4
  hotkey: string;
@@ -13,22 +9,17 @@ export interface HotkeyConfig<T> {
13
9
  handler: (e: KeyboardEvent, tree: TreeInstance<T>) => void;
14
10
  }
15
11
 
16
- export type HotkeysCoreDataRef = {
12
+ export interface HotkeysCoreDataRef {
17
13
  keydownHandler?: (e: KeyboardEvent) => void;
18
14
  keyupHandler?: (e: KeyboardEvent) => void;
19
15
  pressedKeys: Set<string>;
20
- };
16
+ }
21
17
 
22
18
  export type HotkeysCoreFeatureDef<T> = {
23
19
  state: {};
24
20
  config: {
25
21
  hotkeys?: CustomHotkeysConfig<T>;
26
- onTreeHotkey?: (name: string, element: HTMLElement) => void;
27
- onItemHotkey?: (
28
- name: string,
29
- item: ItemInstance<T>,
30
- element: HTMLElement,
31
- ) => void;
22
+ onTreeHotkey?: (name: string, e: KeyboardEvent) => void;
32
23
  };
33
24
  treeInstance: {};
34
25
  itemInstance: {};
@@ -0,0 +1,255 @@
1
+ import {
2
+ FeatureImplementation,
3
+ ItemInstance,
4
+ TreeInstance,
5
+ } from "../../types/core";
6
+ import { DndDataRef, DragTarget } from "../drag-and-drop/types";
7
+ import {
8
+ ItemDropCategory,
9
+ canDrop,
10
+ getInsertionIndex,
11
+ getItemDropCategory,
12
+ getReparentTarget,
13
+ isOrderedDragTarget,
14
+ } from "../drag-and-drop/utils";
15
+ import { makeStateUpdater } from "../../utils";
16
+ import { AssistiveDndState, KDndDataRef } from "./types";
17
+
18
+ const getNextDragTarget = <T>(
19
+ tree: TreeInstance<T>,
20
+ isUp: boolean,
21
+ dragTarget: DragTarget<T>,
22
+ ): DragTarget<T> | undefined => {
23
+ const direction = isUp ? 0 : 1;
24
+ const draggedItems = tree.getState().dnd?.draggedItems;
25
+
26
+ // currently hovering between items
27
+ if (isOrderedDragTarget(dragTarget)) {
28
+ const parent = dragTarget.item.getParent();
29
+ const targetedItem = tree.getItems()[dragTarget.dragLineIndex - 1]; // item above dragline
30
+
31
+ const targetCategory = targetedItem
32
+ ? getItemDropCategory(targetedItem)
33
+ : ItemDropCategory.Item;
34
+ const maxLevel = targetedItem?.getItemMeta().level ?? 0;
35
+ const minLevel = targetedItem?.getItemBelow()?.getItemMeta().level ?? 0;
36
+
37
+ // reparenting
38
+ if (targetCategory === ItemDropCategory.LastInGroup) {
39
+ if (isUp && dragTarget.dragLineLevel < maxLevel) {
40
+ return getReparentTarget(
41
+ targetedItem,
42
+ dragTarget.dragLineLevel + 1,
43
+ draggedItems,
44
+ );
45
+ }
46
+ if (!isUp && dragTarget.dragLineLevel > minLevel && parent) {
47
+ return getReparentTarget(
48
+ targetedItem,
49
+ dragTarget.dragLineLevel - 1,
50
+ draggedItems,
51
+ );
52
+ }
53
+ }
54
+
55
+ const newIndex = dragTarget.dragLineIndex - 1 + direction;
56
+ const item = tree.getItems()[newIndex];
57
+ return item ? { item } : undefined;
58
+ }
59
+
60
+ // moving upwards outside of an open folder
61
+ const targetingExpandedFolder =
62
+ getItemDropCategory(dragTarget.item) === ItemDropCategory.ExpandedFolder;
63
+ if (targetingExpandedFolder && !isUp) {
64
+ return {
65
+ item: dragTarget.item,
66
+ childIndex: 0,
67
+ insertionIndex: getInsertionIndex(
68
+ dragTarget.item.getChildren(),
69
+ 0,
70
+ draggedItems,
71
+ ),
72
+ dragLineIndex: dragTarget.item.getItemMeta().index + direction,
73
+ dragLineLevel: dragTarget.item.getItemMeta().level + 1,
74
+ };
75
+ }
76
+
77
+ // currently hovering over item
78
+ const childIndex = dragTarget.item.getIndexInParent() + direction;
79
+ return {
80
+ item: dragTarget.item.getParent()!,
81
+ childIndex,
82
+ insertionIndex: getInsertionIndex(
83
+ dragTarget.item.getParent()!.getChildren(),
84
+ childIndex,
85
+ draggedItems,
86
+ ),
87
+ dragLineIndex: dragTarget.item.getItemMeta().index + direction,
88
+ dragLineLevel: dragTarget.item.getItemMeta().level,
89
+ };
90
+ };
91
+
92
+ const getNextValidDragTarget = <T>(
93
+ tree: TreeInstance<T>,
94
+ isUp: boolean,
95
+ previousTarget = tree.getState().dnd?.dragTarget,
96
+ ): DragTarget<T> | undefined => {
97
+ if (!previousTarget) return undefined;
98
+ const nextTarget = getNextDragTarget(tree, isUp, previousTarget);
99
+ const dataTransfer =
100
+ tree.getDataRef<KDndDataRef>().current.kDndDataTransfer ?? null;
101
+ if (!nextTarget) return undefined;
102
+ if (canDrop(dataTransfer, nextTarget, tree)) {
103
+ return nextTarget;
104
+ }
105
+ return getNextValidDragTarget(tree, isUp, nextTarget);
106
+ };
107
+
108
+ const updateScroll = <T>(tree: TreeInstance<T>) => {
109
+ const state = tree.getState().dnd;
110
+ if (!state?.dragTarget || isOrderedDragTarget(state.dragTarget)) return;
111
+ state.dragTarget.item.scrollTo({ block: "nearest", inline: "nearest" });
112
+ };
113
+
114
+ const initiateDrag = <T>(
115
+ tree: TreeInstance<T>,
116
+ draggedItems?: ItemInstance<T>[],
117
+ dataTransfer?: DataTransfer,
118
+ ) => {
119
+ const focusedItem = tree.getFocusedItem();
120
+ const { canDrag } = tree.getConfig();
121
+
122
+ if (draggedItems && canDrag && !canDrag(draggedItems)) {
123
+ return;
124
+ }
125
+
126
+ if (draggedItems) {
127
+ tree.applySubStateUpdate("dnd", { draggedItems });
128
+ // getNextValidDragTarget->canDrop needs the draggedItems in state
129
+ tree.getConfig().onStartKeyboardDrag?.(draggedItems);
130
+ } else if (dataTransfer) {
131
+ tree.getDataRef<KDndDataRef>().current.kDndDataTransfer = dataTransfer;
132
+ }
133
+
134
+ const dragTarget = getNextValidDragTarget(tree, false, {
135
+ item: focusedItem,
136
+ });
137
+ if (!dragTarget) return;
138
+
139
+ tree.applySubStateUpdate("dnd", {
140
+ draggedItems,
141
+ dragTarget,
142
+ });
143
+ tree.applySubStateUpdate("assistiveDndState", AssistiveDndState.Started);
144
+ updateScroll(tree);
145
+ };
146
+
147
+ const moveDragPosition = <T>(tree: TreeInstance<T>, isUp: boolean) => {
148
+ const dragTarget = getNextValidDragTarget(tree, isUp);
149
+ if (!dragTarget) return;
150
+ tree.applySubStateUpdate("dnd", {
151
+ draggedItems: tree.getState().dnd?.draggedItems,
152
+ dragTarget,
153
+ });
154
+ tree.applySubStateUpdate("assistiveDndState", AssistiveDndState.Dragging);
155
+ if (!isOrderedDragTarget(dragTarget)) {
156
+ dragTarget.item.setFocused();
157
+ }
158
+ updateScroll(tree);
159
+ };
160
+
161
+ export const keyboardDragAndDropFeature: FeatureImplementation = {
162
+ key: "keyboard-drag-and-drop",
163
+ deps: ["drag-and-drop"],
164
+
165
+ getDefaultConfig: (defaultConfig, tree) => ({
166
+ setAssistiveDndState: makeStateUpdater("assistiveDndState", tree),
167
+ ...defaultConfig,
168
+ }),
169
+
170
+ stateHandlerNames: {
171
+ assistiveDndState: "setAssistiveDndState",
172
+ },
173
+
174
+ treeInstance: {
175
+ startKeyboardDrag: ({ tree }, draggedItems) => {
176
+ initiateDrag(tree, draggedItems, undefined);
177
+ },
178
+ startKeyboardDragOnForeignObject: ({ tree }, dataTransfer) => {
179
+ initiateDrag(tree, undefined, dataTransfer);
180
+ },
181
+ stopKeyboardDrag: ({ tree }) => {
182
+ tree.getDataRef<KDndDataRef>().current.kDndDataTransfer = undefined;
183
+ tree.applySubStateUpdate("dnd", null);
184
+ tree.applySubStateUpdate("assistiveDndState", AssistiveDndState.None);
185
+ },
186
+ },
187
+
188
+ hotkeys: {
189
+ startDrag: {
190
+ hotkey: "Control+Shift+D",
191
+ preventDefault: true,
192
+ isEnabled: (tree) => !tree.getState().dnd,
193
+ handler: (_, tree) => {
194
+ tree.startKeyboardDrag(tree.getSelectedItems());
195
+ },
196
+ },
197
+ dragUp: {
198
+ hotkey: "ArrowUp",
199
+ preventDefault: true,
200
+ isEnabled: (tree) => !!tree.getState().dnd,
201
+ handler: (_, tree) => {
202
+ moveDragPosition(tree, true);
203
+ },
204
+ },
205
+ dragDown: {
206
+ hotkey: "ArrowDown",
207
+ preventDefault: true,
208
+ isEnabled: (tree) => !!tree.getState().dnd,
209
+ handler: (_, tree) => {
210
+ moveDragPosition(tree, false);
211
+ },
212
+ },
213
+ cancelDrag: {
214
+ hotkey: "Escape",
215
+ isEnabled: (tree) => !!tree.getState().dnd,
216
+ handler: (_, tree) => {
217
+ tree.stopKeyboardDrag();
218
+ },
219
+ },
220
+ completeDrag: {
221
+ hotkey: "Enter",
222
+ preventDefault: true,
223
+ isEnabled: (tree) => !!tree.getState().dnd,
224
+ handler: async (e, tree) => {
225
+ e.stopPropagation();
226
+ // TODO copied from keyboard onDrop, unify them
227
+ const dataRef = tree.getDataRef<DndDataRef & KDndDataRef>();
228
+ const target = tree.getDragTarget();
229
+ const dataTransfer = dataRef.current.kDndDataTransfer ?? null;
230
+
231
+ if (!target || !canDrop(dataTransfer, target, tree)) {
232
+ return;
233
+ }
234
+
235
+ const config = tree.getConfig();
236
+ const draggedItems = tree.getState().dnd?.draggedItems;
237
+
238
+ dataRef.current.lastDragCode = undefined;
239
+ tree.applySubStateUpdate("dnd", null);
240
+
241
+ if (draggedItems) {
242
+ await config.onDrop?.(draggedItems, target);
243
+ tree.getItemInstance(draggedItems[0].getId()).setFocused();
244
+ } else if (dataTransfer) {
245
+ await config.onDropForeignDragObject?.(dataTransfer, target);
246
+ }
247
+
248
+ tree.applySubStateUpdate(
249
+ "assistiveDndState",
250
+ AssistiveDndState.Completed,
251
+ );
252
+ },
253
+ },
254
+ },
255
+ };