@headless-tree/core 0.0.10 → 0.0.11

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 (114) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/lib/cjs/core/build-proxified-instance.d.ts +2 -0
  3. package/lib/cjs/core/build-proxified-instance.js +58 -0
  4. package/lib/cjs/core/build-static-instance.d.ts +2 -0
  5. package/lib/cjs/core/build-static-instance.js +27 -0
  6. package/lib/cjs/core/create-tree.js +55 -36
  7. package/lib/cjs/features/async-data-loader/feature.js +37 -23
  8. package/lib/cjs/features/async-data-loader/types.d.ts +2 -1
  9. package/lib/cjs/features/drag-and-drop/feature.js +64 -32
  10. package/lib/cjs/features/drag-and-drop/types.d.ts +13 -4
  11. package/lib/cjs/features/drag-and-drop/utils.d.ts +1 -2
  12. package/lib/cjs/features/drag-and-drop/utils.js +140 -37
  13. package/lib/cjs/features/expand-all/feature.js +12 -6
  14. package/lib/cjs/features/main/types.d.ts +8 -2
  15. package/lib/cjs/features/renaming/feature.js +33 -18
  16. package/lib/cjs/features/renaming/types.d.ts +1 -1
  17. package/lib/cjs/features/search/feature.js +38 -24
  18. package/lib/cjs/features/search/types.d.ts +0 -1
  19. package/lib/cjs/features/selection/feature.js +23 -14
  20. package/lib/cjs/features/sync-data-loader/feature.js +7 -2
  21. package/lib/cjs/features/tree/feature.d.ts +2 -1
  22. package/lib/cjs/features/tree/feature.js +85 -63
  23. package/lib/cjs/features/tree/types.d.ts +5 -3
  24. package/lib/cjs/index.d.ts +3 -1
  25. package/lib/cjs/index.js +2 -1
  26. package/lib/cjs/test-utils/test-tree-do.d.ts +23 -0
  27. package/lib/cjs/test-utils/test-tree-do.js +99 -0
  28. package/lib/cjs/test-utils/test-tree-expect.d.ts +15 -0
  29. package/lib/cjs/test-utils/test-tree-expect.js +62 -0
  30. package/lib/cjs/test-utils/test-tree.d.ts +47 -0
  31. package/lib/cjs/test-utils/test-tree.js +195 -0
  32. package/lib/cjs/types/core.d.ts +31 -15
  33. package/lib/cjs/utilities/errors.d.ts +1 -0
  34. package/lib/cjs/utilities/errors.js +5 -0
  35. package/lib/cjs/utilities/insert-items-at-target.js +10 -3
  36. package/lib/cjs/utilities/remove-items-from-parents.js +14 -8
  37. package/lib/cjs/utils.d.ts +3 -3
  38. package/lib/cjs/utils.js +6 -6
  39. package/lib/esm/core/build-proxified-instance.d.ts +2 -0
  40. package/lib/esm/core/build-proxified-instance.js +54 -0
  41. package/lib/esm/core/build-static-instance.d.ts +2 -0
  42. package/lib/esm/core/build-static-instance.js +23 -0
  43. package/lib/esm/core/create-tree.js +55 -36
  44. package/lib/esm/features/async-data-loader/feature.js +37 -23
  45. package/lib/esm/features/async-data-loader/types.d.ts +2 -1
  46. package/lib/esm/features/drag-and-drop/feature.js +64 -32
  47. package/lib/esm/features/drag-and-drop/types.d.ts +13 -4
  48. package/lib/esm/features/drag-and-drop/utils.d.ts +1 -2
  49. package/lib/esm/features/drag-and-drop/utils.js +138 -34
  50. package/lib/esm/features/expand-all/feature.js +12 -6
  51. package/lib/esm/features/main/types.d.ts +8 -2
  52. package/lib/esm/features/renaming/feature.js +33 -18
  53. package/lib/esm/features/renaming/types.d.ts +1 -1
  54. package/lib/esm/features/search/feature.js +38 -24
  55. package/lib/esm/features/search/types.d.ts +0 -1
  56. package/lib/esm/features/selection/feature.js +23 -14
  57. package/lib/esm/features/sync-data-loader/feature.js +7 -2
  58. package/lib/esm/features/tree/feature.d.ts +2 -1
  59. package/lib/esm/features/tree/feature.js +86 -64
  60. package/lib/esm/features/tree/types.d.ts +5 -3
  61. package/lib/esm/index.d.ts +3 -1
  62. package/lib/esm/index.js +2 -1
  63. package/lib/esm/test-utils/test-tree-do.d.ts +23 -0
  64. package/lib/esm/test-utils/test-tree-do.js +95 -0
  65. package/lib/esm/test-utils/test-tree-expect.d.ts +15 -0
  66. package/lib/esm/test-utils/test-tree-expect.js +58 -0
  67. package/lib/esm/test-utils/test-tree.d.ts +47 -0
  68. package/lib/esm/test-utils/test-tree.js +191 -0
  69. package/lib/esm/types/core.d.ts +31 -15
  70. package/lib/esm/utilities/errors.d.ts +1 -0
  71. package/lib/esm/utilities/errors.js +1 -0
  72. package/lib/esm/utilities/insert-items-at-target.js +10 -3
  73. package/lib/esm/utilities/remove-items-from-parents.js +14 -8
  74. package/lib/esm/utils.d.ts +3 -3
  75. package/lib/esm/utils.js +3 -3
  76. package/package.json +7 -3
  77. package/src/core/build-proxified-instance.ts +115 -0
  78. package/src/core/build-static-instance.ts +28 -0
  79. package/src/core/create-tree.ts +60 -62
  80. package/src/features/async-data-loader/async-data-loader.spec.ts +143 -0
  81. package/src/features/async-data-loader/feature.ts +33 -31
  82. package/src/features/async-data-loader/types.ts +3 -1
  83. package/src/features/drag-and-drop/drag-and-drop.spec.ts +716 -0
  84. package/src/features/drag-and-drop/feature.ts +109 -85
  85. package/src/features/drag-and-drop/types.ts +21 -7
  86. package/src/features/drag-and-drop/utils.ts +196 -55
  87. package/src/features/expand-all/expand-all.spec.ts +52 -0
  88. package/src/features/expand-all/feature.ts +8 -12
  89. package/src/features/hotkeys-core/feature.ts +1 -1
  90. package/src/features/main/types.ts +14 -1
  91. package/src/features/renaming/feature.ts +30 -29
  92. package/src/features/renaming/renaming.spec.ts +125 -0
  93. package/src/features/renaming/types.ts +1 -1
  94. package/src/features/search/feature.ts +34 -38
  95. package/src/features/search/search.spec.ts +115 -0
  96. package/src/features/search/types.ts +0 -1
  97. package/src/features/selection/feature.ts +29 -30
  98. package/src/features/selection/selection.spec.ts +220 -0
  99. package/src/features/sync-data-loader/feature.ts +8 -11
  100. package/src/features/tree/feature.ts +82 -87
  101. package/src/features/tree/tree.spec.ts +515 -0
  102. package/src/features/tree/types.ts +5 -3
  103. package/src/index.ts +4 -1
  104. package/src/test-utils/test-tree-do.ts +136 -0
  105. package/src/test-utils/test-tree-expect.ts +86 -0
  106. package/src/test-utils/test-tree.ts +217 -0
  107. package/src/types/core.ts +92 -33
  108. package/src/utilities/errors.ts +2 -0
  109. package/src/utilities/insert-items-at-target.ts +10 -3
  110. package/src/utilities/remove-items-from-parents.ts +15 -10
  111. package/src/utils.spec.ts +89 -0
  112. package/src/utils.ts +6 -6
  113. package/tsconfig.json +1 -0
  114. package/vitest.config.ts +6 -0
@@ -15,6 +15,7 @@ export const dragAndDropFeature: FeatureImplementation<
15
15
  canDrop: (_, target) => target.item.isFolder(),
16
16
  canDropForeignDragObject: () => false,
17
17
  setDndState: makeStateUpdater("dnd", tree),
18
+ canDropInbetween: true,
18
19
  ...defaultConfig,
19
20
  }),
20
21
 
@@ -22,116 +23,139 @@ export const dragAndDropFeature: FeatureImplementation<
22
23
  dnd: "setDndState",
23
24
  },
24
25
 
25
- createTreeInstance: (prev, tree) => ({
26
- ...prev,
27
-
28
- getDropTarget: () => {
26
+ treeInstance: {
27
+ getDropTarget: ({ tree }) => {
29
28
  return tree.getState().dnd?.dragTarget ?? null;
30
29
  },
31
30
 
32
- getDragLineData: (): DragLineData | null => {
31
+ getDragLineData: ({ tree }): DragLineData | null => {
32
+ // TODO doesnt work if scrolled down!
33
33
  const target = tree.getDropTarget();
34
- const intend = (target?.item.getItemMeta().level ?? 0) + 1;
34
+ const indent = (target?.item.getItemMeta().level ?? 0) + 1; // TODO rename to indent
35
35
 
36
36
  if (!target || target.childIndex === null) return null;
37
37
 
38
- const children = target.item.getChildren();
38
+ const leftOffset = target.dragLineLevel * (tree.getConfig().indent ?? 1);
39
+ const targetItem = tree.getItems()[target.dragLineIndex];
39
40
 
40
- if (target.childIndex === children.length) {
41
- const bb = children[target.childIndex - 1]
42
- ?.getElement()
41
+ if (!targetItem) {
42
+ const bb = tree
43
+ .getItems()
44
+ [target.dragLineIndex - 1]?.getElement()
43
45
  ?.getBoundingClientRect();
44
46
 
45
47
  if (bb) {
46
48
  return {
47
- intend,
49
+ indent,
48
50
  top: bb.bottom,
49
- left: bb.left,
51
+ left: bb.left + leftOffset,
50
52
  right: bb.right,
51
53
  };
52
54
  }
53
55
  }
54
56
 
55
- const bb = children[target.childIndex]
56
- ?.getElement()
57
- ?.getBoundingClientRect();
57
+ const bb = targetItem.getElement()?.getBoundingClientRect();
58
58
 
59
59
  if (bb) {
60
60
  return {
61
- intend,
61
+ indent,
62
62
  top: bb.top,
63
- left: bb.left,
63
+ left: bb.left + leftOffset,
64
64
  right: bb.right,
65
65
  };
66
66
  }
67
67
 
68
68
  return null;
69
69
  },
70
- }),
71
70
 
72
- createItemInstance: (prev, item, tree) => ({
73
- ...prev,
71
+ getDragLineStyle: ({ tree }, topOffset = -1, leftOffset = -8) => {
72
+ const dragLine = tree.getDragLineData();
73
+ return dragLine
74
+ ? {
75
+ top: `${dragLine.top + topOffset}px`,
76
+ left: `${dragLine.left + leftOffset}px`,
77
+ width: `${dragLine.right - dragLine.left - leftOffset}px`,
78
+ pointerEvents: "none", // important to prevent capturing drag events
79
+ }
80
+ : { display: "none" };
81
+ },
82
+ },
74
83
 
75
- getProps: () => ({
76
- ...prev.getProps(),
84
+ itemInstance: {
85
+ // TODO instead of individual getMemoizedProp calls, use a wrapped getMemoizedProps or something (getProps: () => getMemoized({...})
86
+ getProps: ({ tree, item, prev }) => ({
87
+ ...prev?.(),
77
88
 
78
89
  draggable: tree.getConfig().isItemDraggable?.(item) ?? true,
79
90
 
80
- onDragStart: item.getMemoizedProp("dnd/onDragStart", () => (e) => {
81
- const selectedItems = tree.getSelectedItems();
82
- const items = selectedItems.includes(item) ? selectedItems : [item];
83
- const config = tree.getConfig();
84
-
85
- if (!selectedItems.includes(item)) {
86
- tree.setSelectedItems([item.getItemMeta().itemId]);
87
- }
88
-
89
- if (!(config.canDrag?.(items) ?? true)) {
91
+ onDragStart: item.getMemoizedProp(
92
+ "dnd/onDragStart",
93
+ () => (e: DragEvent) => {
94
+ const selectedItems = tree.getSelectedItems();
95
+ const items = selectedItems.includes(item) ? selectedItems : [item];
96
+ const config = tree.getConfig();
97
+
98
+ if (!selectedItems.includes(item)) {
99
+ tree.setSelectedItems([item.getItemMeta().itemId]);
100
+ }
101
+
102
+ if (!(config.canDrag?.(items) ?? true)) {
103
+ e.preventDefault();
104
+ return;
105
+ }
106
+
107
+ if (config.createForeignDragObject) {
108
+ const { format, data } = config.createForeignDragObject(items);
109
+ e.dataTransfer?.setData(format, data);
110
+ }
111
+
112
+ tree.applySubStateUpdate("dnd", {
113
+ draggedItems: items,
114
+ draggingOverItem: tree.getFocusedItem(),
115
+ });
116
+ },
117
+ ),
118
+
119
+ onDragOver: item.getMemoizedProp(
120
+ "dnd/onDragOver",
121
+ () => (e: DragEvent) => {
122
+ const dataRef = tree.getDataRef<DndDataRef>();
123
+ const nextDragCode = getDragCode(e, item, tree);
124
+ if (nextDragCode === dataRef.current.lastDragCode) {
125
+ if (dataRef.current.lastAllowDrop) {
126
+ e.preventDefault();
127
+ }
128
+ return;
129
+ }
130
+ dataRef.current.lastDragCode = nextDragCode;
131
+
132
+ const target = getDropTarget(e, item, tree);
133
+
134
+ if (
135
+ !tree.getState().dnd?.draggedItems &&
136
+ (!e.dataTransfer ||
137
+ !tree
138
+ .getConfig()
139
+ .canDropForeignDragObject?.(e.dataTransfer, target))
140
+ ) {
141
+ dataRef.current.lastAllowDrop = false;
142
+ return;
143
+ }
144
+
145
+ if (!canDrop(e.dataTransfer, target, tree)) {
146
+ dataRef.current.lastAllowDrop = false;
147
+ return;
148
+ }
149
+
150
+ tree.applySubStateUpdate("dnd", (state) => ({
151
+ ...state,
152
+ dragTarget: target,
153
+ draggingOverItem: item,
154
+ }));
155
+ dataRef.current.lastAllowDrop = true;
90
156
  e.preventDefault();
91
- return;
92
- }
93
-
94
- if (config.createForeignDragObject) {
95
- const { format, data } = config.createForeignDragObject(items);
96
- e.dataTransfer?.setData(format, data);
97
- }
98
-
99
- tree.applySubStateUpdate("dnd", {
100
- draggedItems: items,
101
- draggingOverItem: tree.getFocusedItem(),
102
- });
103
- }),
104
-
105
- onDragOver: item.getMemoizedProp("dnd/onDragOver", () => (e) => {
106
- const target = getDropTarget(e, item, tree);
107
- const dataRef = tree.getDataRef<DndDataRef>();
108
-
109
- if (
110
- !tree.getState().dnd?.draggedItems &&
111
- !tree.getConfig().canDropForeignDragObject?.(e.dataTransfer, target)
112
- ) {
113
- return;
114
- }
115
-
116
- if (!canDrop(e.dataTransfer, target, tree)) {
117
- return;
118
- }
119
-
120
- e.preventDefault();
121
- const nextDragCode = getDragCode(target);
122
-
123
- if (nextDragCode === dataRef.current.lastDragCode) {
124
- return;
125
- }
126
-
127
- dataRef.current.lastDragCode = nextDragCode;
128
-
129
- tree.applySubStateUpdate("dnd", (state) => ({
130
- ...state,
131
- dragTarget: target,
132
- draggingOverItem: item,
133
- }));
134
- }),
157
+ },
158
+ ),
135
159
 
136
160
  onDragLeave: item.getMemoizedProp("dnd/onDragLeave", () => () => {
137
161
  const dataRef = tree.getDataRef<DndDataRef>();
@@ -143,18 +167,18 @@ export const dragAndDropFeature: FeatureImplementation<
143
167
  }));
144
168
  }),
145
169
 
146
- onDragEnd: item.getMemoizedProp("dnd/onDragEnd", () => (e) => {
170
+ onDragEnd: item.getMemoizedProp("dnd/onDragEnd", () => (e: DragEvent) => {
147
171
  const draggedItems = tree.getState().dnd?.draggedItems;
148
172
  tree.applySubStateUpdate("dnd", null);
149
173
 
150
- if (e.dataTransfer.dropEffect === "none" || !draggedItems) {
174
+ if (e.dataTransfer?.dropEffect === "none" || !draggedItems) {
151
175
  return;
152
176
  }
153
177
 
154
178
  tree.getConfig().onCompleteForeignDrop?.(draggedItems);
155
179
  }),
156
180
 
157
- onDrop: item.getMemoizedProp("dnd/onDrop", () => (e) => {
181
+ onDrop: item.getMemoizedProp("dnd/onDrop", () => (e: DragEvent) => {
158
182
  const dataRef = tree.getDataRef<DndDataRef>();
159
183
  const target = getDropTarget(e, item, tree);
160
184
 
@@ -171,19 +195,19 @@ export const dragAndDropFeature: FeatureImplementation<
171
195
 
172
196
  if (draggedItems) {
173
197
  config.onDrop?.(draggedItems, target);
174
- } else {
198
+ } else if (e.dataTransfer) {
175
199
  config.onDropForeignDragObject?.(e.dataTransfer, target);
176
200
  }
177
201
  // TODO rebuild tree?
178
202
  }),
179
203
  }),
180
204
 
181
- isDropTarget: () => {
205
+ isDropTarget: ({ tree, item }) => {
182
206
  const target = tree.getDropTarget();
183
207
  return target ? target.item.getId() === item.getId() : false;
184
208
  },
185
209
 
186
- isDropTargetAbove: () => {
210
+ isDropTargetAbove: ({ tree, item }) => {
187
211
  const target = tree.getDropTarget();
188
212
 
189
213
  if (
@@ -195,7 +219,7 @@ export const dragAndDropFeature: FeatureImplementation<
195
219
  return target.childIndex === item.getItemMeta().posInSet;
196
220
  },
197
221
 
198
- isDropTargetBelow: () => {
222
+ isDropTargetBelow: ({ tree, item }) => {
199
223
  const target = tree.getDropTarget();
200
224
 
201
225
  if (
@@ -207,8 +231,8 @@ export const dragAndDropFeature: FeatureImplementation<
207
231
  return target.childIndex - 1 === item.getItemMeta().posInSet;
208
232
  },
209
233
 
210
- isDraggingOver: () => {
234
+ isDraggingOver: ({ tree, item }) => {
211
235
  return tree.getState().dnd?.draggingOverItem?.getId() === item.getId();
212
236
  },
213
- }),
237
+ },
214
238
  };
@@ -2,6 +2,7 @@ import { ItemInstance, SetStateFn } from "../../types/core";
2
2
 
3
3
  export type DndDataRef = {
4
4
  lastDragCode?: string;
5
+ lastAllowDrop?: boolean;
5
6
  };
6
7
 
7
8
  export type DndState<T> = {
@@ -11,7 +12,7 @@ export type DndState<T> = {
11
12
  };
12
13
 
13
14
  export type DragLineData = {
14
- intend: number;
15
+ indent: number;
15
16
  top: number;
16
17
  left: number;
17
18
  right: number;
@@ -22,11 +23,15 @@ export type DropTarget<T> =
22
23
  item: ItemInstance<T>;
23
24
  childIndex: number;
24
25
  insertionIndex: number;
26
+ dragLineIndex: number;
27
+ dragLineLevel: number;
25
28
  }
26
29
  | {
27
- item: ItemInstance<T>;
30
+ item: ItemInstance<T>; // TODO just omit values instead of nulls; or maybe just make it union of dropTarget+itemInstance?
28
31
  childIndex: null;
29
32
  insertionIndex: null;
33
+ dragLineIndex: null;
34
+ dragLineLevel: null;
30
35
  };
31
36
 
32
37
  export enum DropTargetPosition {
@@ -40,16 +45,21 @@ export type DragAndDropFeatureDef<T> = {
40
45
  dnd?: DndState<T> | null;
41
46
  };
42
47
  config: {
43
- setDndState?: SetStateFn<DndState<T> | null>;
48
+ setDndState?: SetStateFn<DndState<T> | undefined | null>;
44
49
 
45
- topLinePercentage?: number;
46
- bottomLinePercentage?: number;
50
+ /** Defines the size of the area at the top and bottom of an item where, when an item is dropped, the item willö
51
+ * be placed above or below the item within the same parent, as opposed to being placed inside the item.
52
+ * If `canDropInbetween` is `false`, this is ignored. */
53
+ reorderAreaPercentage?: number;
47
54
  canDropInbetween?: boolean;
48
55
 
56
+ // TODO better document difference to canDrag(), or unify both
49
57
  isItemDraggable?: (item: ItemInstance<T>) => boolean;
50
58
  canDrag?: (items: ItemInstance<T>[]) => boolean;
51
59
  canDrop?: (items: ItemInstance<T>[], target: DropTarget<T>) => boolean;
52
60
 
61
+ indent?: number;
62
+
53
63
  createForeignDragObject?: (items: ItemInstance<T>[]) => {
54
64
  format: string;
55
65
  data: any;
@@ -78,11 +88,15 @@ export type DragAndDropFeatureDef<T> = {
78
88
  treeInstance: {
79
89
  getDropTarget: () => DropTarget<T> | null;
80
90
  getDragLineData: () => DragLineData | null;
91
+ getDragLineStyle: (
92
+ topOffset?: number,
93
+ leftOffset?: number,
94
+ ) => Record<string, any>;
81
95
  };
82
96
  itemInstance: {
83
97
  isDropTarget: () => boolean;
84
- isDropTargetAbove: () => boolean;
85
- isDropTargetBelow: () => boolean;
98
+ isDropTargetAbove: () => boolean; // TODO still correct?
99
+ isDropTargetBelow: () => boolean; // TODO still correct?
86
100
  isDraggingOver: () => boolean;
87
101
  };
88
102
  hotkeys: never;
@@ -1,13 +1,30 @@
1
1
  import { ItemInstance, TreeInstance } from "../../types/core";
2
- import { DropTarget, DropTargetPosition } from "./types";
3
-
4
- export const getDragCode = ({ item, childIndex }: DropTarget<any>) =>
5
- `${item.getId()}__${childIndex ?? "none"}`;
6
-
7
- export const getDropOffset = (e: any, item: ItemInstance<any>): number => {
8
- const bb = item.getElement()?.getBoundingClientRect();
9
- return bb ? (e.pageY - bb.top) / bb.height : 0.5;
10
- };
2
+ import { DropTarget } from "./types";
3
+
4
+ enum ItemDropCategory {
5
+ Item,
6
+ ExpandedFolder,
7
+ LastInGroup,
8
+ }
9
+
10
+ enum PlacementType {
11
+ ReorderAbove,
12
+ ReorderBelow,
13
+ MakeChild,
14
+ Reparent,
15
+ }
16
+
17
+ type TargetPlacement =
18
+ | {
19
+ type:
20
+ | PlacementType.ReorderAbove
21
+ | PlacementType.ReorderBelow
22
+ | PlacementType.MakeChild;
23
+ }
24
+ | {
25
+ type: PlacementType.Reparent;
26
+ reparentLevel: number;
27
+ };
11
28
 
12
29
  export const canDrop = (
13
30
  dataTransfer: DataTransfer | null,
@@ -21,6 +38,17 @@ export const canDrop = (
21
38
  return false;
22
39
  }
23
40
 
41
+ if (
42
+ draggedItems &&
43
+ draggedItems.some(
44
+ (draggedItem) =>
45
+ target.item.getId() === draggedItem.getId() ||
46
+ target.item.isDescendentOf(draggedItem.getId()),
47
+ )
48
+ ) {
49
+ return false;
50
+ }
51
+
24
52
  if (
25
53
  !draggedItems &&
26
54
  dataTransfer &&
@@ -32,18 +60,96 @@ export const canDrop = (
32
60
  return true;
33
61
  };
34
62
 
35
- const getDropTargetPosition = (
36
- offset: number,
37
- topLinePercentage: number,
38
- bottomLinePercentage: number,
39
- ) => {
40
- if (offset < topLinePercentage) {
41
- return DropTargetPosition.Top;
63
+ const getItemDropCategory = (item: ItemInstance<any>) => {
64
+ if (item.isExpanded()) {
65
+ return ItemDropCategory.ExpandedFolder;
42
66
  }
43
- if (offset > bottomLinePercentage) {
44
- return DropTargetPosition.Bottom;
67
+
68
+ const parent = item.getParent();
69
+ if (parent && item.getIndexInParent() === parent.getItemMeta().setSize - 1) {
70
+ return ItemDropCategory.LastInGroup;
45
71
  }
46
- return DropTargetPosition.Item;
72
+
73
+ return ItemDropCategory.Item;
74
+ };
75
+
76
+ const getTargetPlacement = (
77
+ e: any,
78
+ item: ItemInstance<any>,
79
+ tree: TreeInstance<any>,
80
+ canMakeChild: boolean,
81
+ ): TargetPlacement => {
82
+ const config = tree.getConfig();
83
+
84
+ if (!config.canDropInbetween) {
85
+ return canMakeChild
86
+ ? { type: PlacementType.MakeChild }
87
+ : { type: PlacementType.ReorderBelow };
88
+ }
89
+
90
+ 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;
93
+ const targetDropCategory = getItemDropCategory(item);
94
+ const reorderAreaPercentage = !canMakeChild
95
+ ? 0.5
96
+ : config.reorderAreaPercentage ?? 0.3;
97
+ const indent = config.indent ?? 20;
98
+ const makeChildType = canMakeChild
99
+ ? PlacementType.MakeChild
100
+ : PlacementType.ReorderBelow;
101
+
102
+ if (targetDropCategory === ItemDropCategory.ExpandedFolder) {
103
+ if (topPercent < reorderAreaPercentage) {
104
+ return { type: PlacementType.ReorderAbove };
105
+ }
106
+ return { type: makeChildType };
107
+ }
108
+
109
+ if (targetDropCategory === ItemDropCategory.LastInGroup) {
110
+ if (leftPixels < item.getItemMeta().level * indent) {
111
+ if (topPercent < 0.5) {
112
+ return { type: PlacementType.ReorderAbove };
113
+ }
114
+ return {
115
+ type: PlacementType.Reparent,
116
+ reparentLevel: Math.floor(leftPixels / indent),
117
+ };
118
+ }
119
+ // if not at left of item area, treat as if it was a normal item
120
+ }
121
+
122
+ // targetDropCategory === ItemDropCategory.Item
123
+ if (topPercent < reorderAreaPercentage) {
124
+ return { type: PlacementType.ReorderAbove };
125
+ }
126
+ if (topPercent > 1 - reorderAreaPercentage) {
127
+ return { type: PlacementType.ReorderBelow };
128
+ }
129
+ return { type: makeChildType };
130
+ };
131
+
132
+ export const getDragCode = (
133
+ e: any,
134
+ item: ItemInstance<any>,
135
+ tree: TreeInstance<any>,
136
+ ) => {
137
+ const placement = getTargetPlacement(e, item, tree, true);
138
+ return [
139
+ item.getId(),
140
+ placement.type,
141
+ placement.type === PlacementType.Reparent ? placement.reparentLevel : 0,
142
+ ].join("__");
143
+ };
144
+
145
+ const getNthParent = (
146
+ item: ItemInstance<any>,
147
+ n: number,
148
+ ): ItemInstance<any> => {
149
+ if (n === item.getItemMeta().level) {
150
+ return item;
151
+ }
152
+ return getNthParent(item.getParent()!, n);
47
153
  };
48
154
 
49
155
  export const getDropTarget = (
@@ -52,59 +158,94 @@ export const getDropTarget = (
52
158
  tree: TreeInstance<any>,
53
159
  canDropInbetween = tree.getConfig().canDropInbetween,
54
160
  ): DropTarget<any> => {
55
- const config = tree.getConfig();
56
161
  const draggedItems = tree.getState().dnd?.draggedItems ?? [];
57
- const itemTarget = { item, childIndex: null, insertionIndex: null };
58
- const parentTarget = {
59
- item: item.getParent(),
162
+ const itemMeta = item.getItemMeta();
163
+ const parent = item.getParent();
164
+ const itemTarget: DropTarget<any> = {
165
+ item,
60
166
  childIndex: null,
61
167
  insertionIndex: null,
168
+ dragLineIndex: null,
169
+ dragLineLevel: null,
62
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;
180
+ const canBecomeSibling =
181
+ parentTarget && canDrop(e.dataTransfer, parentTarget, tree);
182
+
183
+ const canMakeChild = canDrop(e.dataTransfer, itemTarget, tree);
184
+ const placement = getTargetPlacement(e, item, tree, canMakeChild);
63
185
 
64
- if (!canDropInbetween) {
65
- if (!canDrop(e.dataTransfer, parentTarget, tree)) {
66
- return getDropTarget(e, item.getParent(), tree, false);
67
- }
68
- return itemTarget;
186
+ if (
187
+ !canDropInbetween &&
188
+ parent &&
189
+ canBecomeSibling &&
190
+ placement.type !== PlacementType.MakeChild
191
+ ) {
192
+ return parentTarget;
69
193
  }
70
194
 
71
- const canDropInside = canDrop(e.dataTransfer, itemTarget, tree);
72
-
73
- const offset = getDropOffset(e, item);
195
+ if (!canDropInbetween && 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);
198
+ }
74
199
 
75
- const pos = canDropInside
76
- ? getDropTargetPosition(
77
- offset,
78
- config.topLinePercentage ?? 0.3,
79
- config.bottomLinePercentage ?? 0.7,
80
- )
81
- : getDropTargetPosition(offset, 0.5, 0.5);
200
+ if (!parent) {
201
+ // Shouldn't happen, but if dropped "next" to root item, just drop it inside
202
+ return itemTarget;
203
+ }
82
204
 
83
- if (pos === DropTargetPosition.Item) {
205
+ if (placement.type === PlacementType.MakeChild) {
84
206
  return itemTarget;
85
207
  }
86
208
 
87
- if (!canDrop(e.dataTransfer, parentTarget, tree)) {
88
- return getDropTarget(e, item.getParent(), tree, false);
209
+ if (!canBecomeSibling) {
210
+ return getDropTarget(e, parent, tree, false);
89
211
  }
90
212
 
91
- const childIndex =
92
- item.getIndexInParent() + (pos === DropTargetPosition.Top ? 0 : 1);
213
+ 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
+ };
227
+ }
93
228
 
94
- const numberOfDragItemsBeforeTarget = item
95
- .getParent()
96
- .getChildren()
97
- .slice(0, childIndex)
98
- .reduce(
99
- (counter, child) =>
100
- child && draggedItems?.some((i) => i.getId() === child.getId())
101
- ? ++counter
102
- : counter,
103
- 0,
104
- );
229
+ const maybeAddOneForBelow =
230
+ placement.type === PlacementType.ReorderAbove ? 0 : 1;
231
+ const childIndex = item.getIndexInParent() + maybeAddOneForBelow;
232
+
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;
105
244
 
106
245
  return {
107
- item: item.getParent(),
246
+ item: parent,
247
+ dragLineIndex: itemMeta.index + maybeAddOneForBelow,
248
+ dragLineLevel: itemMeta.level,
108
249
  childIndex,
109
250
  // TODO performance could be improved by computing this only when dragcode changed
110
251
  insertionIndex: childIndex - numberOfDragItemsBeforeTarget,