@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.
- package/CHANGELOG.md +13 -0
- package/lib/cjs/core/create-tree.js +13 -4
- package/lib/cjs/features/async-data-loader/feature.js +73 -48
- package/lib/cjs/features/async-data-loader/types.d.ts +17 -14
- package/lib/cjs/features/drag-and-drop/feature.js +98 -93
- package/lib/cjs/features/drag-and-drop/types.d.ts +17 -29
- package/lib/cjs/features/drag-and-drop/types.js +7 -7
- package/lib/cjs/features/drag-and-drop/utils.d.ts +25 -3
- package/lib/cjs/features/drag-and-drop/utils.js +51 -51
- package/lib/cjs/features/expand-all/feature.js +26 -3
- package/lib/cjs/features/expand-all/types.d.ts +3 -1
- package/lib/cjs/features/hotkeys-core/feature.js +7 -3
- package/lib/cjs/features/hotkeys-core/types.d.ts +4 -5
- package/lib/cjs/features/keyboard-drag-and-drop/feature.d.ts +2 -0
- package/lib/cjs/features/keyboard-drag-and-drop/feature.js +206 -0
- package/lib/cjs/features/keyboard-drag-and-drop/types.d.ts +27 -0
- package/lib/cjs/features/keyboard-drag-and-drop/types.js +11 -0
- package/lib/cjs/features/prop-memoization/feature.js +33 -11
- package/lib/cjs/features/prop-memoization/types.d.ts +8 -3
- package/lib/cjs/features/renaming/feature.js +1 -1
- package/lib/cjs/features/search/feature.js +2 -0
- package/lib/cjs/features/search/types.d.ts +2 -2
- package/lib/cjs/features/selection/feature.js +4 -4
- package/lib/cjs/features/selection/types.d.ts +1 -1
- package/lib/cjs/features/sync-data-loader/feature.js +31 -5
- package/lib/cjs/features/sync-data-loader/types.d.ts +5 -5
- package/lib/cjs/features/tree/feature.js +4 -9
- package/lib/cjs/features/tree/types.d.ts +7 -5
- package/lib/cjs/index.d.ts +2 -0
- package/lib/cjs/index.js +2 -0
- package/lib/cjs/mddocs-entry.d.ts +10 -0
- package/lib/cjs/test-utils/test-tree-do.d.ts +2 -2
- package/lib/cjs/test-utils/test-tree-do.js +19 -6
- package/lib/cjs/test-utils/test-tree-expect.d.ts +5 -3
- package/lib/cjs/test-utils/test-tree-expect.js +3 -0
- package/lib/cjs/test-utils/test-tree.d.ts +2 -1
- package/lib/cjs/test-utils/test-tree.js +24 -21
- package/lib/cjs/types/core.d.ts +2 -1
- package/lib/cjs/utilities/create-on-drop-handler.d.ts +2 -2
- package/lib/cjs/utilities/create-on-drop-handler.js +13 -4
- package/lib/cjs/utilities/insert-items-at-target.d.ts +2 -2
- package/lib/cjs/utilities/insert-items-at-target.js +21 -12
- package/lib/cjs/utilities/remove-items-from-parents.d.ts +1 -1
- package/lib/cjs/utilities/remove-items-from-parents.js +12 -3
- package/lib/esm/core/create-tree.js +13 -4
- package/lib/esm/features/async-data-loader/feature.js +73 -48
- package/lib/esm/features/async-data-loader/types.d.ts +17 -14
- package/lib/esm/features/drag-and-drop/feature.js +99 -94
- package/lib/esm/features/drag-and-drop/types.d.ts +17 -29
- package/lib/esm/features/drag-and-drop/types.js +6 -6
- package/lib/esm/features/drag-and-drop/utils.d.ts +25 -3
- package/lib/esm/features/drag-and-drop/utils.js +45 -49
- package/lib/esm/features/expand-all/feature.js +26 -3
- package/lib/esm/features/expand-all/types.d.ts +3 -1
- package/lib/esm/features/hotkeys-core/feature.js +7 -3
- package/lib/esm/features/hotkeys-core/types.d.ts +4 -5
- package/lib/esm/features/keyboard-drag-and-drop/feature.d.ts +2 -0
- package/lib/esm/features/keyboard-drag-and-drop/feature.js +203 -0
- package/lib/esm/features/keyboard-drag-and-drop/types.d.ts +27 -0
- package/lib/esm/features/keyboard-drag-and-drop/types.js +8 -0
- package/lib/esm/features/prop-memoization/feature.js +33 -11
- package/lib/esm/features/prop-memoization/types.d.ts +8 -3
- package/lib/esm/features/renaming/feature.js +1 -1
- package/lib/esm/features/search/feature.js +2 -0
- package/lib/esm/features/search/types.d.ts +2 -2
- package/lib/esm/features/selection/feature.js +4 -4
- package/lib/esm/features/selection/types.d.ts +1 -1
- package/lib/esm/features/sync-data-loader/feature.js +31 -5
- package/lib/esm/features/sync-data-loader/types.d.ts +5 -5
- package/lib/esm/features/tree/feature.js +4 -9
- package/lib/esm/features/tree/types.d.ts +7 -5
- package/lib/esm/index.d.ts +2 -0
- package/lib/esm/index.js +2 -0
- package/lib/esm/mddocs-entry.d.ts +10 -0
- package/lib/esm/test-utils/test-tree-do.d.ts +2 -2
- package/lib/esm/test-utils/test-tree-do.js +19 -6
- package/lib/esm/test-utils/test-tree-expect.d.ts +5 -3
- package/lib/esm/test-utils/test-tree-expect.js +3 -0
- package/lib/esm/test-utils/test-tree.d.ts +2 -1
- package/lib/esm/test-utils/test-tree.js +24 -21
- package/lib/esm/types/core.d.ts +2 -1
- package/lib/esm/utilities/create-on-drop-handler.d.ts +2 -2
- package/lib/esm/utilities/create-on-drop-handler.js +13 -4
- package/lib/esm/utilities/insert-items-at-target.d.ts +2 -2
- package/lib/esm/utilities/insert-items-at-target.js +21 -12
- package/lib/esm/utilities/remove-items-from-parents.d.ts +1 -1
- package/lib/esm/utilities/remove-items-from-parents.js +12 -3
- package/package.json +2 -2
- package/src/core/core.spec.ts +31 -0
- package/src/core/create-tree.ts +15 -5
- package/src/features/async-data-loader/async-data-loader.spec.ts +10 -6
- package/src/features/async-data-loader/feature.ts +76 -48
- package/src/features/async-data-loader/types.ts +18 -11
- package/src/features/drag-and-drop/drag-and-drop.spec.ts +75 -89
- package/src/features/drag-and-drop/feature.ts +26 -22
- package/src/features/drag-and-drop/types.ts +23 -35
- package/src/features/drag-and-drop/utils.ts +70 -57
- package/src/features/expand-all/feature.ts +29 -5
- package/src/features/expand-all/types.ts +3 -1
- package/src/features/hotkeys-core/feature.ts +4 -0
- package/src/features/hotkeys-core/types.ts +4 -13
- package/src/features/keyboard-drag-and-drop/feature.ts +255 -0
- package/src/features/keyboard-drag-and-drop/keyboard-drag-and-drop.spec.ts +402 -0
- package/src/features/keyboard-drag-and-drop/types.ts +30 -0
- package/src/features/prop-memoization/feature.ts +27 -8
- package/src/features/prop-memoization/prop-memoization.spec.ts +2 -2
- package/src/features/prop-memoization/types.ts +8 -3
- package/src/features/renaming/feature.ts +8 -2
- package/src/features/search/feature.ts +2 -0
- package/src/features/search/types.ts +2 -2
- package/src/features/selection/feature.ts +4 -4
- package/src/features/selection/types.ts +1 -1
- package/src/features/sync-data-loader/feature.ts +26 -7
- package/src/features/sync-data-loader/types.ts +5 -5
- package/src/features/tree/feature.ts +8 -13
- package/src/features/tree/types.ts +7 -5
- package/src/index.ts +2 -0
- package/src/mddocs-entry.ts +16 -0
- package/src/test-utils/test-tree-do.ts +3 -3
- package/src/test-utils/test-tree-expect.ts +7 -2
- package/src/test-utils/test-tree.ts +26 -22
- package/src/types/core.ts +2 -0
- package/src/utilities/create-on-drop-handler.ts +4 -4
- package/src/utilities/insert-items-at-target.ts +18 -14
- package/src/utilities/remove-items-from-parents.ts +6 -3
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ItemInstance, TreeInstance } from "../../types/core";
|
|
2
|
-
import {
|
|
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:
|
|
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
|
-
|
|
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() ===
|
|
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.
|
|
92
|
-
const leftPixels = bb ? e.
|
|
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
|
-
|
|
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
|
-
):
|
|
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:
|
|
165
|
-
|
|
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
|
|
197
|
-
return
|
|
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
|
|
243
|
+
return getDragTarget(e, parent, tree, false);
|
|
211
244
|
}
|
|
212
245
|
|
|
213
246
|
if (placement.type === PlacementType.Reparent) {
|
|
214
|
-
|
|
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:
|
|
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
|
|
29
|
+
await tree.waitForItemChildrenLoaded(item.getId());
|
|
31
30
|
await Promise.all(
|
|
32
31
|
item.getChildren().map(async (child) => {
|
|
33
|
-
await
|
|
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:
|
|
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
|
|
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,
|
|
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
|
+
};
|