@headless-tree/core 1.3.0 → 1.5.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 +29 -0
- package/dist/index.d.mts +37 -10
- package/dist/index.d.ts +37 -10
- package/dist/index.js +190 -86
- package/dist/index.mjs +190 -86
- package/package.json +19 -3
- package/readme.md +6 -6
- package/src/core/create-tree.ts +45 -9
- package/src/features/async-data-loader/async-data-loader.spec.ts +1 -0
- package/src/features/async-data-loader/feature.ts +16 -20
- package/src/features/async-data-loader/types.ts +2 -1
- package/src/features/checkboxes/checkboxes.spec.ts +111 -122
- package/src/features/checkboxes/feature.ts +89 -40
- package/src/features/checkboxes/types.ts +16 -3
- package/src/features/drag-and-drop/feature.ts +63 -8
- package/src/features/drag-and-drop/types.ts +10 -0
- package/src/features/drag-and-drop/utils.ts +8 -6
- package/src/features/hotkeys-core/feature.ts +1 -0
- package/src/features/keyboard-drag-and-drop/feature.ts +3 -1
- package/src/features/main/types.ts +9 -0
- package/src/features/sync-data-loader/types.ts +7 -1
- package/src/mddocs-entry.ts +13 -0
- package/src/test-utils/test-tree-do.ts +6 -0
- package/src/test-utils/test-tree.ts +14 -3
- package/src/types/core.ts +5 -5
|
@@ -1,17 +1,51 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
FeatureImplementation,
|
|
3
|
+
type ItemInstance,
|
|
4
|
+
type TreeInstance,
|
|
5
|
+
} from "../../types/core";
|
|
2
6
|
import { DndDataRef, DragLineData, DragTarget } from "./types";
|
|
3
7
|
import {
|
|
8
|
+
PlacementType,
|
|
9
|
+
type TargetPlacement,
|
|
4
10
|
canDrop,
|
|
5
11
|
getDragCode,
|
|
6
12
|
getDragTarget,
|
|
13
|
+
getTargetPlacement,
|
|
7
14
|
isOrderedDragTarget,
|
|
8
15
|
} from "./utils";
|
|
9
16
|
import { makeStateUpdater } from "../../utils";
|
|
10
17
|
|
|
18
|
+
const handleAutoOpenFolder = (
|
|
19
|
+
dataRef: { current: DndDataRef },
|
|
20
|
+
tree: TreeInstance<any>,
|
|
21
|
+
item: ItemInstance<any>,
|
|
22
|
+
placement: TargetPlacement,
|
|
23
|
+
) => {
|
|
24
|
+
const { openOnDropDelay } = tree.getConfig();
|
|
25
|
+
const dragCode = dataRef.current.lastDragCode;
|
|
26
|
+
|
|
27
|
+
if (
|
|
28
|
+
!openOnDropDelay ||
|
|
29
|
+
!item.isFolder() ||
|
|
30
|
+
item.isExpanded() ||
|
|
31
|
+
placement.type !== PlacementType.MakeChild
|
|
32
|
+
) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
clearTimeout(dataRef.current.autoExpandTimeout);
|
|
36
|
+
dataRef.current.autoExpandTimeout = setTimeout(() => {
|
|
37
|
+
if (
|
|
38
|
+
dragCode !== dataRef.current.lastDragCode ||
|
|
39
|
+
!dataRef.current.lastAllowDrop
|
|
40
|
+
)
|
|
41
|
+
return;
|
|
42
|
+
item.expand();
|
|
43
|
+
}, openOnDropDelay);
|
|
44
|
+
};
|
|
45
|
+
|
|
11
46
|
const defaultCanDropForeignDragObject = () => false;
|
|
12
47
|
export const dragAndDropFeature: FeatureImplementation = {
|
|
13
48
|
key: "drag-and-drop",
|
|
14
|
-
deps: ["selection"],
|
|
15
49
|
|
|
16
50
|
getDefaultConfig: (defaultConfig, tree) => ({
|
|
17
51
|
canDrop: (_, target) => target.item.isFolder(),
|
|
@@ -22,6 +56,7 @@ export const dragAndDropFeature: FeatureImplementation = {
|
|
|
22
56
|
: () => false,
|
|
23
57
|
setDndState: makeStateUpdater("dnd", tree),
|
|
24
58
|
canReorder: true,
|
|
59
|
+
openOnDropDelay: 800,
|
|
25
60
|
...defaultConfig,
|
|
26
61
|
}),
|
|
27
62
|
|
|
@@ -74,7 +109,7 @@ export const dragAndDropFeature: FeatureImplementation = {
|
|
|
74
109
|
}
|
|
75
110
|
}
|
|
76
111
|
|
|
77
|
-
const bb = targetItem
|
|
112
|
+
const bb = targetItem?.getElement()?.getBoundingClientRect();
|
|
78
113
|
|
|
79
114
|
if (bb) {
|
|
80
115
|
return {
|
|
@@ -149,12 +184,14 @@ export const dragAndDropFeature: FeatureImplementation = {
|
|
|
149
184
|
onDragEnter: (e: DragEvent) => e.preventDefault(),
|
|
150
185
|
|
|
151
186
|
onDragStart: (e: DragEvent) => {
|
|
152
|
-
const selectedItems = tree.getSelectedItems
|
|
187
|
+
const selectedItems = tree.getSelectedItems
|
|
188
|
+
? tree.getSelectedItems()
|
|
189
|
+
: [tree.getFocusedItem()];
|
|
153
190
|
const items = selectedItems.includes(item) ? selectedItems : [item];
|
|
154
191
|
const config = tree.getConfig();
|
|
155
192
|
|
|
156
193
|
if (!selectedItems.includes(item)) {
|
|
157
|
-
tree.setSelectedItems([item.getItemMeta().itemId]);
|
|
194
|
+
tree.setSelectedItems?.([item.getItemMeta().itemId]);
|
|
158
195
|
}
|
|
159
196
|
|
|
160
197
|
if (!(config.canDrag?.(items) ?? true)) {
|
|
@@ -185,7 +222,9 @@ export const dragAndDropFeature: FeatureImplementation = {
|
|
|
185
222
|
onDragOver: (e: DragEvent) => {
|
|
186
223
|
e.stopPropagation(); // don't bubble up to container dragover
|
|
187
224
|
const dataRef = tree.getDataRef<DndDataRef>();
|
|
188
|
-
const
|
|
225
|
+
const placement = getTargetPlacement(e, item, tree, true);
|
|
226
|
+
const nextDragCode = getDragCode(item, placement);
|
|
227
|
+
|
|
189
228
|
if (nextDragCode === dataRef.current.lastDragCode) {
|
|
190
229
|
if (dataRef.current.lastAllowDrop) {
|
|
191
230
|
e.preventDefault();
|
|
@@ -195,6 +234,8 @@ export const dragAndDropFeature: FeatureImplementation = {
|
|
|
195
234
|
dataRef.current.lastDragCode = nextDragCode;
|
|
196
235
|
dataRef.current.lastDragEnter = Date.now();
|
|
197
236
|
|
|
237
|
+
handleAutoOpenFolder(dataRef, tree, item, placement);
|
|
238
|
+
|
|
198
239
|
const target = getDragTarget(e, item, tree);
|
|
199
240
|
|
|
200
241
|
if (
|
|
@@ -260,14 +301,21 @@ export const dragAndDropFeature: FeatureImplementation = {
|
|
|
260
301
|
e.stopPropagation();
|
|
261
302
|
const dataRef = tree.getDataRef<DndDataRef>();
|
|
262
303
|
const target = getDragTarget(e, item, tree);
|
|
304
|
+
const draggedItems = tree.getState().dnd?.draggedItems;
|
|
305
|
+
const isValidDrop = canDrop(e.dataTransfer, target, tree);
|
|
263
306
|
|
|
264
|
-
|
|
307
|
+
tree.applySubStateUpdate("dnd", {
|
|
308
|
+
draggedItems: undefined,
|
|
309
|
+
draggingOverItem: undefined,
|
|
310
|
+
dragTarget: undefined,
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
if (!isValidDrop) {
|
|
265
314
|
return;
|
|
266
315
|
}
|
|
267
316
|
|
|
268
317
|
e.preventDefault();
|
|
269
318
|
const config = tree.getConfig();
|
|
270
|
-
const draggedItems = tree.getState().dnd?.draggedItems;
|
|
271
319
|
|
|
272
320
|
dataRef.current.lastDragCode = undefined;
|
|
273
321
|
|
|
@@ -284,6 +332,13 @@ export const dragAndDropFeature: FeatureImplementation = {
|
|
|
284
332
|
return target ? target.item.getId() === item.getId() : false;
|
|
285
333
|
},
|
|
286
334
|
|
|
335
|
+
isUnorderedDragTarget: ({ tree, item }) => {
|
|
336
|
+
const target = tree.getDragTarget();
|
|
337
|
+
return target
|
|
338
|
+
? !isOrderedDragTarget(target) && target.item.getId() === item.getId()
|
|
339
|
+
: false;
|
|
340
|
+
},
|
|
341
|
+
|
|
287
342
|
isDragTargetAbove: ({ tree, item }) => {
|
|
288
343
|
const target = tree.getDragTarget();
|
|
289
344
|
|
|
@@ -4,6 +4,7 @@ export interface DndDataRef {
|
|
|
4
4
|
lastDragCode?: string;
|
|
5
5
|
lastAllowDrop?: boolean;
|
|
6
6
|
lastDragEnter?: number;
|
|
7
|
+
autoExpandTimeout?: any;
|
|
7
8
|
windowDragEndListener?: () => void;
|
|
8
9
|
}
|
|
9
10
|
|
|
@@ -93,6 +94,9 @@ export type DragAndDropFeatureDef<T> = {
|
|
|
93
94
|
target: DragTarget<T>,
|
|
94
95
|
) => void | Promise<void>;
|
|
95
96
|
onCompleteForeignDrop?: (items: ItemInstance<T>[]) => void;
|
|
97
|
+
|
|
98
|
+
/** When dragging for this many ms on a closed folder, the folder will automatically open. Set to zero to disable. */
|
|
99
|
+
openOnDropDelay?: number;
|
|
96
100
|
};
|
|
97
101
|
treeInstance: {
|
|
98
102
|
getDragTarget: () => DragTarget<T> | null;
|
|
@@ -104,7 +108,13 @@ export type DragAndDropFeatureDef<T> = {
|
|
|
104
108
|
) => Record<string, any>;
|
|
105
109
|
};
|
|
106
110
|
itemInstance: {
|
|
111
|
+
/** Checks if the user is dragging in a way which makes this the new parent of the dragged items, either by dragging on top of
|
|
112
|
+
* this item, or by dragging inbetween children of this item. See @{isUnorderedDragTarget} if the latter is undesirable. */
|
|
107
113
|
isDragTarget: () => boolean;
|
|
114
|
+
|
|
115
|
+
/** As opposed to @{isDragTarget}, this will not be true if the target is inbetween children of this item. This returns only true
|
|
116
|
+
* if the user is dragging directly on top of this item. */
|
|
117
|
+
isUnorderedDragTarget: () => boolean;
|
|
108
118
|
isDragTargetAbove: () => boolean;
|
|
109
119
|
isDragTargetBelow: () => boolean;
|
|
110
120
|
isDraggingOver: () => boolean;
|
|
@@ -7,14 +7,14 @@ export enum ItemDropCategory {
|
|
|
7
7
|
LastInGroup,
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
enum PlacementType {
|
|
10
|
+
export enum PlacementType {
|
|
11
11
|
ReorderAbove,
|
|
12
12
|
ReorderBelow,
|
|
13
13
|
MakeChild,
|
|
14
14
|
Reparent,
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
type TargetPlacement =
|
|
17
|
+
export type TargetPlacement =
|
|
18
18
|
| {
|
|
19
19
|
type:
|
|
20
20
|
| PlacementType.ReorderAbove
|
|
@@ -95,7 +95,7 @@ export const getInsertionIndex = <T>(
|
|
|
95
95
|
return childIndex - numberOfDragItemsBeforeTarget;
|
|
96
96
|
};
|
|
97
97
|
|
|
98
|
-
const getTargetPlacement = (
|
|
98
|
+
export const getTargetPlacement = (
|
|
99
99
|
e: any,
|
|
100
100
|
item: ItemInstance<any>,
|
|
101
101
|
tree: TreeInstance<any>,
|
|
@@ -153,11 +153,9 @@ const getTargetPlacement = (
|
|
|
153
153
|
};
|
|
154
154
|
|
|
155
155
|
export const getDragCode = (
|
|
156
|
-
e: any,
|
|
157
156
|
item: ItemInstance<any>,
|
|
158
|
-
|
|
157
|
+
placement: TargetPlacement,
|
|
159
158
|
) => {
|
|
160
|
-
const placement = getTargetPlacement(e, item, tree, true);
|
|
161
159
|
return [
|
|
162
160
|
item.getId(),
|
|
163
161
|
placement.type,
|
|
@@ -222,6 +220,10 @@ export const getDragTarget = (
|
|
|
222
220
|
canBecomeSibling &&
|
|
223
221
|
placement.type !== PlacementType.MakeChild
|
|
224
222
|
) {
|
|
223
|
+
if (draggedItems?.some((item) => item.isDescendentOf(parent.getId()))) {
|
|
224
|
+
// dropping on itself should be illegal, return item, canDrop will then return false
|
|
225
|
+
return itemTarget;
|
|
226
|
+
}
|
|
225
227
|
return parentTarget;
|
|
226
228
|
}
|
|
227
229
|
|
|
@@ -13,6 +13,7 @@ const specialKeys: Record<string, RegExp> = {
|
|
|
13
13
|
minus: /^(NumpadSubtract|Minus)$/,
|
|
14
14
|
control: /^(ControlLeft|ControlRight)$/,
|
|
15
15
|
shift: /^(ShiftLeft|ShiftRight)$/,
|
|
16
|
+
metaorcontrol: /^(MetaLeft|MetaRight|ControlLeft|ControlRight)$/,
|
|
16
17
|
};
|
|
17
18
|
|
|
18
19
|
const testHotkeyMatch = (
|
|
@@ -191,7 +191,9 @@ export const keyboardDragAndDropFeature: FeatureImplementation = {
|
|
|
191
191
|
preventDefault: true,
|
|
192
192
|
isEnabled: (tree) => !tree.getState().dnd,
|
|
193
193
|
handler: (_, tree) => {
|
|
194
|
-
const selectedItems = tree.getSelectedItems()
|
|
194
|
+
const selectedItems = tree.getSelectedItems?.() ?? [
|
|
195
|
+
tree.getFocusedItem(),
|
|
196
|
+
];
|
|
195
197
|
const focusedItem = tree.getFocusedItem();
|
|
196
198
|
|
|
197
199
|
tree.startKeyboardDrag(
|
|
@@ -10,6 +10,11 @@ import {
|
|
|
10
10
|
} from "../../types/core";
|
|
11
11
|
import { ItemMeta } from "../tree/types";
|
|
12
12
|
|
|
13
|
+
export interface TreeDataRef {
|
|
14
|
+
isMounted?: boolean;
|
|
15
|
+
waitingForMount?: (() => void)[];
|
|
16
|
+
}
|
|
17
|
+
|
|
13
18
|
export type InstanceTypeMap = {
|
|
14
19
|
itemInstance: ItemInstance<any>;
|
|
15
20
|
treeInstance: TreeInstance<any>;
|
|
@@ -49,6 +54,10 @@ export type MainFeatureDef<T = any> = {
|
|
|
49
54
|
/* @internal */
|
|
50
55
|
getHotkeyPresets: () => HotkeysConfig<T>;
|
|
51
56
|
rebuildTree: () => void;
|
|
57
|
+
/** @deprecated Experimental feature, might get removed or changed in the future. */
|
|
58
|
+
scheduleRebuildTree: () => void;
|
|
59
|
+
/** @internal */
|
|
60
|
+
setMounted: (isMounted: boolean) => void;
|
|
52
61
|
};
|
|
53
62
|
itemInstance: {
|
|
54
63
|
registerElement: (element: HTMLElement | null) => void;
|
|
@@ -18,7 +18,13 @@ export type SyncDataLoaderFeatureDef<T> = {
|
|
|
18
18
|
};
|
|
19
19
|
treeInstance: {
|
|
20
20
|
retrieveItemData: (itemId: string) => T;
|
|
21
|
-
|
|
21
|
+
|
|
22
|
+
/** Retrieve children Ids. If an async data loader is used, skipFetch is set to true, and children have not been retrieved
|
|
23
|
+
* yet for this item, this will initiate fetching the children, and return an empty array. Once the children have loaded,
|
|
24
|
+
* a rerender will be triggered.
|
|
25
|
+
* @param skipFetch - Defaults to false.
|
|
26
|
+
*/
|
|
27
|
+
retrieveChildrenIds: (itemId: string, skipFetch?: boolean) => string[];
|
|
22
28
|
};
|
|
23
29
|
itemInstance: {
|
|
24
30
|
isLoading: () => boolean;
|
package/src/mddocs-entry.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { SyncDataLoaderFeatureDef } from "./features/sync-data-loader/types";
|
|
|
10
10
|
import { TreeFeatureDef } from "./features/tree/types";
|
|
11
11
|
import { PropMemoizationFeatureDef } from "./features/prop-memoization/types";
|
|
12
12
|
import { KeyboardDragAndDropFeatureDef } from "./features/keyboard-drag-and-drop/types";
|
|
13
|
+
import type { CheckboxesFeatureDef } from "./features/checkboxes/types";
|
|
13
14
|
|
|
14
15
|
export * from ".";
|
|
15
16
|
|
|
@@ -167,3 +168,15 @@ export type TreeFeatureTreeInstance<T> = TreeFeatureDef<T>["treeInstance"];
|
|
|
167
168
|
/** @interface */
|
|
168
169
|
export type TreeFeatureItemInstance<T> = TreeFeatureDef<T>["itemInstance"];
|
|
169
170
|
export type TreeFeatureHotkeys<T> = TreeFeatureDef<T>["hotkeys"];
|
|
171
|
+
|
|
172
|
+
/** @interface */
|
|
173
|
+
export type CheckboxesFeatureConfig<T> = CheckboxesFeatureDef<T>["config"];
|
|
174
|
+
/** @interface */
|
|
175
|
+
export type CheckboxesFeatureState<T> = CheckboxesFeatureDef<T>["state"];
|
|
176
|
+
/** @interface */
|
|
177
|
+
export type CheckboxesFeatureTreeInstance<T> =
|
|
178
|
+
CheckboxesFeatureDef<T>["treeInstance"];
|
|
179
|
+
/** @interface */
|
|
180
|
+
export type CheckboxesFeatureItemInstance<T> =
|
|
181
|
+
CheckboxesFeatureDef<T>["itemInstance"];
|
|
182
|
+
export type CheckboxesFeatureHotkeys<T> = CheckboxesFeatureDef<T>["hotkeys"];
|
|
@@ -78,6 +78,7 @@ export class TestTree<T = string> {
|
|
|
78
78
|
get instance() {
|
|
79
79
|
if (!this.treeInstance) {
|
|
80
80
|
this.treeInstance = createTree(this.config);
|
|
81
|
+
this.treeInstance.setMounted(true);
|
|
81
82
|
this.treeInstance.rebuildTree();
|
|
82
83
|
}
|
|
83
84
|
return this.treeInstance;
|
|
@@ -87,10 +88,9 @@ export class TestTree<T = string> {
|
|
|
87
88
|
|
|
88
89
|
static async resolveAsyncLoaders() {
|
|
89
90
|
do {
|
|
91
|
+
await vi.advanceTimersToNextTimerAsync();
|
|
90
92
|
TestTree.asyncLoaderResolvers.shift()?.();
|
|
91
|
-
await
|
|
92
|
-
setTimeout(r);
|
|
93
|
-
});
|
|
93
|
+
await vi.advanceTimersToNextTimerAsync();
|
|
94
94
|
} while (TestTree.asyncLoaderResolvers.length);
|
|
95
95
|
}
|
|
96
96
|
|
|
@@ -101,6 +101,17 @@ export class TestTree<T = string> {
|
|
|
101
101
|
await TestTree.resolveAsyncLoaders();
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
+
async runWhileResolvingItems(cb: () => Promise<void>) {
|
|
105
|
+
const interval = setInterval(() => {
|
|
106
|
+
TestTree.resolveAsyncLoaders();
|
|
107
|
+
}, 5);
|
|
108
|
+
try {
|
|
109
|
+
await cb();
|
|
110
|
+
} finally {
|
|
111
|
+
clearInterval(interval);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
104
115
|
static default(config: Partial<TreeConfig<string>>) {
|
|
105
116
|
return new TestTree({
|
|
106
117
|
rootItemId: "x",
|
package/src/types/core.ts
CHANGED
|
@@ -93,11 +93,11 @@ type MayReturnNull<T extends (...x: any[]) => any> = (
|
|
|
93
93
|
...args: Parameters<T>
|
|
94
94
|
) => ReturnType<T> | null;
|
|
95
95
|
|
|
96
|
-
export type ItemInstanceOpts<Key extends keyof ItemInstance<any>> = {
|
|
97
|
-
item: ItemInstance<
|
|
98
|
-
tree: TreeInstance<
|
|
96
|
+
export type ItemInstanceOpts<T, Key extends keyof ItemInstance<any>> = {
|
|
97
|
+
item: ItemInstance<T>;
|
|
98
|
+
tree: TreeInstance<T>;
|
|
99
99
|
itemId: string;
|
|
100
|
-
prev?: MayReturnNull<ItemInstance<
|
|
100
|
+
prev?: MayReturnNull<ItemInstance<T>[Key]>;
|
|
101
101
|
};
|
|
102
102
|
|
|
103
103
|
export type TreeInstanceOpts<Key extends keyof TreeInstance<any>> = {
|
|
@@ -131,7 +131,7 @@ export type FeatureImplementation<T = any> = {
|
|
|
131
131
|
|
|
132
132
|
itemInstance?: {
|
|
133
133
|
[key in keyof ItemInstance<T>]?: (
|
|
134
|
-
opts: ItemInstanceOpts<key>,
|
|
134
|
+
opts: ItemInstanceOpts<T, key>,
|
|
135
135
|
...args: Parameters<ItemInstance<T>[key]>
|
|
136
136
|
) => void;
|
|
137
137
|
};
|