@headless-tree/core 1.6.1 → 1.6.3
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 +15 -0
- package/dist/index.d.mts +14 -3
- package/dist/index.d.ts +14 -3
- package/dist/index.js +93 -66
- package/dist/index.mjs +93 -66
- package/package.json +1 -1
- package/src/features/async-data-loader/feature.ts +14 -8
- package/src/features/async-data-loader/types.ts +11 -3
- package/src/features/drag-and-drop/drag-and-drop.spec.ts +175 -2
- package/src/features/drag-and-drop/feature.ts +76 -62
- package/src/features/drag-and-drop/types.ts +11 -0
- package/src/features/drag-and-drop/utils.ts +10 -4
- package/src/features/hotkeys-core/feature.ts +11 -4
- package/src/features/keyboard-drag-and-drop/feature.ts +1 -0
- package/src/features/prop-memoization/feature.ts +8 -0
- package/src/features/prop-memoization/types.ts +1 -0
- package/src/features/tree/feature.ts +6 -2
- package/src/utils.ts +1 -0
|
@@ -57,6 +57,7 @@ export const dragAndDropFeature: FeatureImplementation = {
|
|
|
57
57
|
setDndState: makeStateUpdater("dnd", tree),
|
|
58
58
|
canReorder: true,
|
|
59
59
|
openOnDropDelay: 800,
|
|
60
|
+
draggedItemOverwritesSelection: true,
|
|
60
61
|
...defaultConfig,
|
|
61
62
|
}),
|
|
62
63
|
|
|
@@ -178,47 +179,10 @@ export const dragAndDropFeature: FeatureImplementation = {
|
|
|
178
179
|
itemInstance: {
|
|
179
180
|
getProps: ({ tree, item, prev }) => ({
|
|
180
181
|
...prev?.(),
|
|
181
|
-
|
|
182
|
-
draggable: true,
|
|
182
|
+
...(tree.getConfig().seperateDragHandle ? {} : item.getDragHandleProps()),
|
|
183
183
|
|
|
184
184
|
onDragEnter: (e: DragEvent) => e.preventDefault(),
|
|
185
185
|
|
|
186
|
-
onDragStart: (e: DragEvent) => {
|
|
187
|
-
const selectedItems = tree.getSelectedItems
|
|
188
|
-
? tree.getSelectedItems()
|
|
189
|
-
: [tree.getFocusedItem()];
|
|
190
|
-
const items = selectedItems.includes(item) ? selectedItems : [item];
|
|
191
|
-
const config = tree.getConfig();
|
|
192
|
-
|
|
193
|
-
if (!selectedItems.includes(item)) {
|
|
194
|
-
tree.setSelectedItems?.([item.getItemMeta().itemId]);
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
if (!(config.canDrag?.(items) ?? true)) {
|
|
198
|
-
e.preventDefault();
|
|
199
|
-
return;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
if (config.setDragImage) {
|
|
203
|
-
const { imgElement, xOffset, yOffset } = config.setDragImage(items);
|
|
204
|
-
e.dataTransfer?.setDragImage(imgElement, xOffset ?? 0, yOffset ?? 0);
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
if (config.createForeignDragObject && e.dataTransfer) {
|
|
208
|
-
const { format, data, dropEffect, effectAllowed } =
|
|
209
|
-
config.createForeignDragObject(items);
|
|
210
|
-
e.dataTransfer.setData(format, data);
|
|
211
|
-
|
|
212
|
-
if (dropEffect) e.dataTransfer.dropEffect = dropEffect;
|
|
213
|
-
if (effectAllowed) e.dataTransfer.effectAllowed = effectAllowed;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
tree.applySubStateUpdate("dnd", {
|
|
217
|
-
draggedItems: items,
|
|
218
|
-
draggingOverItem: tree.getFocusedItem(),
|
|
219
|
-
});
|
|
220
|
-
},
|
|
221
|
-
|
|
222
186
|
onDragOver: (e: DragEvent) => {
|
|
223
187
|
e.stopPropagation(); // don't bubble up to container dragover
|
|
224
188
|
const dataRef = tree.getDataRef<DndDataRef>();
|
|
@@ -236,7 +200,7 @@ export const dragAndDropFeature: FeatureImplementation = {
|
|
|
236
200
|
|
|
237
201
|
handleAutoOpenFolder(dataRef, tree, item, placement);
|
|
238
202
|
|
|
239
|
-
const target = getDragTarget(e, item, tree);
|
|
203
|
+
const target = getDragTarget(e, item, tree, false);
|
|
240
204
|
|
|
241
205
|
if (
|
|
242
206
|
!tree.getState().dnd?.draggedItems &&
|
|
@@ -249,7 +213,8 @@ export const dragAndDropFeature: FeatureImplementation = {
|
|
|
249
213
|
return;
|
|
250
214
|
}
|
|
251
215
|
|
|
252
|
-
|
|
216
|
+
// dataTransfer.payload is not accessible in onDragOver, so just skip entirely here. It'll be checked again in onDrop
|
|
217
|
+
if (!canDrop(null, target, tree)) {
|
|
253
218
|
dataRef.current.lastAllowDrop = false;
|
|
254
219
|
return;
|
|
255
220
|
}
|
|
@@ -276,31 +241,10 @@ export const dragAndDropFeature: FeatureImplementation = {
|
|
|
276
241
|
}, 100);
|
|
277
242
|
},
|
|
278
243
|
|
|
279
|
-
onDragEnd: (e: DragEvent) => {
|
|
280
|
-
const { onCompleteForeignDrop, canDragForeignDragObjectOver } =
|
|
281
|
-
tree.getConfig();
|
|
282
|
-
const draggedItems = tree.getState().dnd?.draggedItems;
|
|
283
|
-
|
|
284
|
-
if (e.dataTransfer?.dropEffect === "none" || !draggedItems) {
|
|
285
|
-
return;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
const target = getDragTarget(e, item, tree);
|
|
289
|
-
if (
|
|
290
|
-
canDragForeignDragObjectOver &&
|
|
291
|
-
e.dataTransfer &&
|
|
292
|
-
!canDragForeignDragObjectOver(e.dataTransfer, target)
|
|
293
|
-
) {
|
|
294
|
-
return;
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
onCompleteForeignDrop?.(draggedItems);
|
|
298
|
-
},
|
|
299
|
-
|
|
300
244
|
onDrop: async (e: DragEvent) => {
|
|
301
245
|
e.stopPropagation();
|
|
302
246
|
const dataRef = tree.getDataRef<DndDataRef>();
|
|
303
|
-
const target = getDragTarget(e, item, tree);
|
|
247
|
+
const target = getDragTarget(e, item, tree, true);
|
|
304
248
|
const draggedItems = tree.getState().dnd?.draggedItems;
|
|
305
249
|
const isValidDrop = canDrop(e.dataTransfer, target, tree);
|
|
306
250
|
|
|
@@ -321,9 +265,79 @@ export const dragAndDropFeature: FeatureImplementation = {
|
|
|
321
265
|
|
|
322
266
|
if (draggedItems) {
|
|
323
267
|
await config.onDrop?.(draggedItems, target);
|
|
268
|
+
draggedItems[0].setFocused();
|
|
324
269
|
} else if (e.dataTransfer) {
|
|
325
270
|
await config.onDropForeignDragObject?.(e.dataTransfer, target);
|
|
326
271
|
}
|
|
272
|
+
|
|
273
|
+
tree.applySubStateUpdate("dnd", null);
|
|
274
|
+
tree.updateDomFocus();
|
|
275
|
+
},
|
|
276
|
+
}),
|
|
277
|
+
|
|
278
|
+
getDragHandleProps: ({ tree, item, prev }) => ({
|
|
279
|
+
...prev?.(),
|
|
280
|
+
|
|
281
|
+
draggable: true,
|
|
282
|
+
|
|
283
|
+
onDragStart: (e: DragEvent) => {
|
|
284
|
+
const { draggedItemOverwritesSelection } = tree.getConfig();
|
|
285
|
+
const selectedItems = tree.getSelectedItems
|
|
286
|
+
? tree.getSelectedItems()
|
|
287
|
+
: [tree.getFocusedItem()];
|
|
288
|
+
const overwriteSelection =
|
|
289
|
+
!selectedItems.includes(item) && draggedItemOverwritesSelection;
|
|
290
|
+
const items = overwriteSelection ? [item] : selectedItems;
|
|
291
|
+
const config = tree.getConfig();
|
|
292
|
+
|
|
293
|
+
if (overwriteSelection) {
|
|
294
|
+
tree.setSelectedItems?.([item.getItemMeta().itemId]);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (!(config.canDrag?.(items) ?? true)) {
|
|
298
|
+
e.preventDefault();
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (config.setDragImage) {
|
|
303
|
+
const { imgElement, xOffset, yOffset } = config.setDragImage(items);
|
|
304
|
+
e.dataTransfer?.setDragImage(imgElement, xOffset ?? 0, yOffset ?? 0);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (config.createForeignDragObject && e.dataTransfer) {
|
|
308
|
+
const { format, data, dropEffect, effectAllowed } =
|
|
309
|
+
config.createForeignDragObject(items);
|
|
310
|
+
e.dataTransfer.setData(format, data);
|
|
311
|
+
|
|
312
|
+
if (dropEffect) e.dataTransfer.dropEffect = dropEffect;
|
|
313
|
+
if (effectAllowed) e.dataTransfer.effectAllowed = effectAllowed;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
tree.applySubStateUpdate("dnd", {
|
|
317
|
+
draggedItems: items,
|
|
318
|
+
draggingOverItem: tree.getFocusedItem(),
|
|
319
|
+
});
|
|
320
|
+
},
|
|
321
|
+
|
|
322
|
+
onDragEnd: (e: DragEvent) => {
|
|
323
|
+
const { onCompleteForeignDrop, canDragForeignDragObjectOver } =
|
|
324
|
+
tree.getConfig();
|
|
325
|
+
const draggedItems = tree.getState().dnd?.draggedItems;
|
|
326
|
+
|
|
327
|
+
if (e.dataTransfer?.dropEffect === "none" || !draggedItems) {
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const target = getDragTarget(e, item, tree, false);
|
|
332
|
+
if (
|
|
333
|
+
canDragForeignDragObjectOver &&
|
|
334
|
+
e.dataTransfer &&
|
|
335
|
+
!canDragForeignDragObjectOver(e.dataTransfer, target)
|
|
336
|
+
) {
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
onCompleteForeignDrop?.(draggedItems);
|
|
327
341
|
},
|
|
328
342
|
}),
|
|
329
343
|
|
|
@@ -97,6 +97,13 @@ export type DragAndDropFeatureDef<T> = {
|
|
|
97
97
|
|
|
98
98
|
/** When dragging for this many ms on a closed folder, the folder will automatically open. Set to zero to disable. */
|
|
99
99
|
openOnDropDelay?: number;
|
|
100
|
+
|
|
101
|
+
/** If true, `item.getProps()` will not include drag event handlers. Use `item.getDragHandleProps()` on the handler element. */
|
|
102
|
+
seperateDragHandle?: boolean;
|
|
103
|
+
|
|
104
|
+
/** If true, the item that is dragged is not selected, the selected items will be overwritten to just the dragged item.
|
|
105
|
+
* Defaults to true */
|
|
106
|
+
draggedItemOverwritesSelection?: boolean;
|
|
100
107
|
};
|
|
101
108
|
treeInstance: {
|
|
102
109
|
getDragTarget: () => DragTarget<T> | null;
|
|
@@ -118,6 +125,10 @@ export type DragAndDropFeatureDef<T> = {
|
|
|
118
125
|
isDragTargetAbove: () => boolean;
|
|
119
126
|
isDragTargetBelow: () => boolean;
|
|
120
127
|
isDraggingOver: () => boolean;
|
|
128
|
+
|
|
129
|
+
/** Note that `item.getProps()` already passes in all drag event handlers by default. Set `seperateDragHandle` to true to
|
|
130
|
+
* disable the default behavior and use this on the handler element instead. */
|
|
131
|
+
getDragHandleProps: () => Record<string, any>;
|
|
121
132
|
};
|
|
122
133
|
hotkeys: never;
|
|
123
134
|
};
|
|
@@ -29,6 +29,8 @@ export type TargetPlacement =
|
|
|
29
29
|
export const isOrderedDragTarget = <T>(dragTarget: DragTarget<T>) =>
|
|
30
30
|
"childIndex" in dragTarget;
|
|
31
31
|
|
|
32
|
+
/** @param dataTransfer - If the data transfer object should not be considered, e.g. because the event is
|
|
33
|
+
* onDragOver where the browser does not allow reading the payload, pass null */
|
|
32
34
|
export const canDrop = (
|
|
33
35
|
dataTransfer: DataTransfer | null,
|
|
34
36
|
target: DragTarget<any>,
|
|
@@ -197,21 +199,25 @@ export const getReparentTarget = <T>(
|
|
|
197
199
|
};
|
|
198
200
|
};
|
|
199
201
|
|
|
202
|
+
/** @param hasDataTransferPayload - If the data transfer object should not be considered, e.g. because the event is
|
|
203
|
+
* onDragOver where the browser does not allow reading the payload, pass false */
|
|
200
204
|
export const getDragTarget = (
|
|
201
205
|
e: any,
|
|
202
206
|
item: ItemInstance<any>,
|
|
203
207
|
tree: TreeInstance<any>,
|
|
208
|
+
hasDataTransferPayload: boolean,
|
|
204
209
|
canReorder = tree.getConfig().canReorder,
|
|
205
210
|
): DragTarget<any> => {
|
|
211
|
+
const dataTransfer = hasDataTransferPayload ? e.dataTransfer : null;
|
|
206
212
|
const draggedItems = tree.getState().dnd?.draggedItems;
|
|
207
213
|
const itemMeta = item.getItemMeta();
|
|
208
214
|
const parent = item.getParent();
|
|
209
215
|
const itemTarget: DragTarget<any> = { item };
|
|
210
216
|
const parentTarget: DragTarget<any> | null = parent ? { item: parent } : null;
|
|
211
217
|
const canBecomeSibling =
|
|
212
|
-
parentTarget && canDrop(
|
|
218
|
+
parentTarget && canDrop(dataTransfer, parentTarget, tree);
|
|
213
219
|
|
|
214
|
-
const canMakeChild = canDrop(
|
|
220
|
+
const canMakeChild = canDrop(dataTransfer, itemTarget, tree);
|
|
215
221
|
const placement = getTargetPlacement(e, item, tree, canMakeChild);
|
|
216
222
|
|
|
217
223
|
if (
|
|
@@ -229,7 +235,7 @@ export const getDragTarget = (
|
|
|
229
235
|
|
|
230
236
|
if (!canReorder && parent && !canBecomeSibling) {
|
|
231
237
|
// TODO! this breaks in story DND/Can Drop. Maybe move this logic into a composable DragTargetStrategy[] ?
|
|
232
|
-
return getDragTarget(e, parent, tree, false);
|
|
238
|
+
return getDragTarget(e, parent, tree, hasDataTransferPayload, false);
|
|
233
239
|
}
|
|
234
240
|
|
|
235
241
|
if (!parent) {
|
|
@@ -242,7 +248,7 @@ export const getDragTarget = (
|
|
|
242
248
|
}
|
|
243
249
|
|
|
244
250
|
if (!canBecomeSibling) {
|
|
245
|
-
return getDragTarget(e, parent, tree, false);
|
|
251
|
+
return getDragTarget(e, parent, tree, hasDataTransferPayload, false);
|
|
246
252
|
}
|
|
247
253
|
|
|
248
254
|
if (placement.type === PlacementType.Reparent) {
|
|
@@ -5,6 +5,11 @@ import {
|
|
|
5
5
|
} from "../../types/core";
|
|
6
6
|
import { HotkeyConfig, HotkeysCoreDataRef } from "./types";
|
|
7
7
|
|
|
8
|
+
// e.code may be empty or "Unidentified" on mobile virtual keyboards
|
|
9
|
+
// or during IME composition, so we fall back to e.key
|
|
10
|
+
const resolveKeyCode = (e: KeyboardEvent): string =>
|
|
11
|
+
e.code !== "" && e.code !== "Unidentified" ? e.code : e.key;
|
|
12
|
+
|
|
8
13
|
const specialKeys: Record<string, RegExp> = {
|
|
9
14
|
// TODO:breaking deprecate auto-lowercase
|
|
10
15
|
letter: /^Key[A-Z]$/,
|
|
@@ -71,9 +76,11 @@ export const hotkeysCoreFeature: FeatureImplementation = {
|
|
|
71
76
|
return;
|
|
72
77
|
}
|
|
73
78
|
|
|
79
|
+
const resolvedCode = resolveKeyCode(e);
|
|
80
|
+
|
|
74
81
|
data.current.pressedKeys ??= new Set();
|
|
75
|
-
const newMatch = !data.current.pressedKeys.has(
|
|
76
|
-
data.current.pressedKeys.add(
|
|
82
|
+
const newMatch = !data.current.pressedKeys.has(resolvedCode);
|
|
83
|
+
data.current.pressedKeys.add(resolvedCode);
|
|
77
84
|
|
|
78
85
|
const hotkeyName = findHotkeyMatch(
|
|
79
86
|
data.current.pressedKeys,
|
|
@@ -85,7 +92,7 @@ export const hotkeysCoreFeature: FeatureImplementation = {
|
|
|
85
92
|
if (e.target instanceof HTMLInputElement) {
|
|
86
93
|
// JS respects composite keydowns while input elements are focused, and
|
|
87
94
|
// doesnt send the associated keyup events with the same key name
|
|
88
|
-
data.current.pressedKeys.delete(
|
|
95
|
+
data.current.pressedKeys.delete(resolvedCode);
|
|
89
96
|
}
|
|
90
97
|
|
|
91
98
|
if (!hotkeyName) return;
|
|
@@ -110,7 +117,7 @@ export const hotkeysCoreFeature: FeatureImplementation = {
|
|
|
110
117
|
|
|
111
118
|
const keyup = (e: KeyboardEvent) => {
|
|
112
119
|
data.current.pressedKeys ??= new Set();
|
|
113
|
-
data.current.pressedKeys.delete(e
|
|
120
|
+
data.current.pressedKeys.delete(resolveKeyCode(e));
|
|
114
121
|
};
|
|
115
122
|
|
|
116
123
|
const reset = () => {
|
|
@@ -254,6 +254,7 @@ export const keyboardDragAndDropFeature: FeatureImplementation = {
|
|
|
254
254
|
await config.onDropForeignDragObject?.(dataTransfer, target);
|
|
255
255
|
}
|
|
256
256
|
|
|
257
|
+
tree.updateDomFocus();
|
|
257
258
|
tree.applySubStateUpdate(
|
|
258
259
|
"assistiveDndState",
|
|
259
260
|
AssistiveDndState.Completed,
|
|
@@ -59,6 +59,14 @@ export const propMemoizationFeature: FeatureImplementation = {
|
|
|
59
59
|
return memoize(props, dataRef.current.memo.item);
|
|
60
60
|
},
|
|
61
61
|
|
|
62
|
+
getDragHandleProps: ({ item, prev }) => {
|
|
63
|
+
const dataRef = item.getDataRef<PropMemoizationDataRef>();
|
|
64
|
+
const props = prev?.() ?? {};
|
|
65
|
+
dataRef.current.memo ??= {};
|
|
66
|
+
dataRef.current.memo.drag ??= {};
|
|
67
|
+
return memoize(props, dataRef.current.memo.drag);
|
|
68
|
+
},
|
|
69
|
+
|
|
62
70
|
getRenameInputProps: ({ item, prev }) => {
|
|
63
71
|
const dataRef = item.getDataRef<PropMemoizationDataRef>();
|
|
64
72
|
const props = prev?.() ?? {};
|
|
@@ -107,9 +107,13 @@ export const treeFeature: FeatureImplementation<any> = {
|
|
|
107
107
|
setTimeout(async () => {
|
|
108
108
|
const focusedItem = tree.getFocusedItem();
|
|
109
109
|
tree.getConfig().scrollToItem?.(focusedItem);
|
|
110
|
-
await poll(() => focusedItem.getElement() !== null, 20);
|
|
110
|
+
await poll(() => focusedItem.getElement() !== null, 20, 500);
|
|
111
111
|
const focusedElement = focusedItem.getElement();
|
|
112
|
-
if (!focusedElement)
|
|
112
|
+
if (!focusedElement) {
|
|
113
|
+
tree.getItems()[0]?.setFocused();
|
|
114
|
+
tree.getItems()[0]?.getElement()?.focus();
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
113
117
|
focusedElement.focus();
|
|
114
118
|
});
|
|
115
119
|
},
|