@headless-tree/core 1.2.1 → 1.4.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 +31 -0
- package/dist/index.d.mts +580 -0
- package/dist/index.d.ts +580 -0
- package/dist/index.js +2347 -0
- package/dist/index.mjs +2302 -0
- package/package.json +18 -10
- package/src/core/create-tree.ts +26 -15
- package/src/features/async-data-loader/feature.ts +5 -0
- package/src/features/async-data-loader/types.ts +2 -0
- package/src/features/checkboxes/checkboxes.spec.ts +20 -5
- package/src/features/checkboxes/feature.ts +31 -16
- package/src/features/checkboxes/types.ts +1 -0
- package/src/features/drag-and-drop/drag-and-drop.spec.ts +11 -2
- package/src/features/drag-and-drop/feature.ts +107 -24
- package/src/features/drag-and-drop/types.ts +21 -0
- package/src/features/drag-and-drop/utils.ts +8 -6
- package/src/features/keyboard-drag-and-drop/feature.ts +10 -1
- package/src/features/keyboard-drag-and-drop/keyboard-drag-and-drop.spec.ts +34 -3
- package/src/features/main/types.ts +0 -2
- package/src/features/sync-data-loader/feature.ts +5 -1
- package/src/features/tree/feature.ts +4 -3
- package/src/features/tree/tree.spec.ts +14 -4
- package/src/test-utils/test-tree-do.ts +2 -0
- package/src/test-utils/test-tree.ts +1 -0
- package/tsconfig.json +1 -4
- package/vitest.config.ts +3 -1
- package/lib/cjs/core/build-proxified-instance.d.ts +0 -2
- package/lib/cjs/core/build-proxified-instance.js +0 -58
- package/lib/cjs/core/build-static-instance.d.ts +0 -2
- package/lib/cjs/core/build-static-instance.js +0 -26
- package/lib/cjs/core/create-tree.d.ts +0 -2
- package/lib/cjs/core/create-tree.js +0 -191
- package/lib/cjs/features/async-data-loader/feature.d.ts +0 -2
- package/lib/cjs/features/async-data-loader/feature.js +0 -135
- package/lib/cjs/features/async-data-loader/types.d.ts +0 -47
- package/lib/cjs/features/async-data-loader/types.js +0 -2
- package/lib/cjs/features/checkboxes/feature.d.ts +0 -2
- package/lib/cjs/features/checkboxes/feature.js +0 -94
- package/lib/cjs/features/checkboxes/types.d.ts +0 -26
- package/lib/cjs/features/checkboxes/types.js +0 -9
- package/lib/cjs/features/drag-and-drop/feature.d.ts +0 -2
- package/lib/cjs/features/drag-and-drop/feature.js +0 -205
- package/lib/cjs/features/drag-and-drop/types.d.ts +0 -71
- package/lib/cjs/features/drag-and-drop/types.js +0 -9
- package/lib/cjs/features/drag-and-drop/utils.d.ts +0 -27
- package/lib/cjs/features/drag-and-drop/utils.js +0 -182
- package/lib/cjs/features/expand-all/feature.d.ts +0 -2
- package/lib/cjs/features/expand-all/feature.js +0 -70
- package/lib/cjs/features/expand-all/types.d.ts +0 -19
- package/lib/cjs/features/expand-all/types.js +0 -2
- package/lib/cjs/features/hotkeys-core/feature.d.ts +0 -2
- package/lib/cjs/features/hotkeys-core/feature.js +0 -107
- package/lib/cjs/features/hotkeys-core/types.d.ts +0 -27
- package/lib/cjs/features/hotkeys-core/types.js +0 -2
- package/lib/cjs/features/keyboard-drag-and-drop/feature.d.ts +0 -2
- package/lib/cjs/features/keyboard-drag-and-drop/feature.js +0 -206
- package/lib/cjs/features/keyboard-drag-and-drop/types.d.ts +0 -27
- package/lib/cjs/features/keyboard-drag-and-drop/types.js +0 -11
- package/lib/cjs/features/main/types.d.ts +0 -47
- package/lib/cjs/features/main/types.js +0 -2
- package/lib/cjs/features/prop-memoization/feature.d.ts +0 -2
- package/lib/cjs/features/prop-memoization/feature.js +0 -70
- package/lib/cjs/features/prop-memoization/types.d.ts +0 -15
- package/lib/cjs/features/prop-memoization/types.js +0 -2
- package/lib/cjs/features/renaming/feature.d.ts +0 -2
- package/lib/cjs/features/renaming/feature.js +0 -86
- package/lib/cjs/features/renaming/types.d.ts +0 -27
- package/lib/cjs/features/renaming/types.js +0 -2
- package/lib/cjs/features/search/feature.d.ts +0 -2
- package/lib/cjs/features/search/feature.js +0 -119
- package/lib/cjs/features/search/types.d.ts +0 -32
- package/lib/cjs/features/search/types.js +0 -2
- package/lib/cjs/features/selection/feature.d.ts +0 -2
- package/lib/cjs/features/selection/feature.js +0 -132
- package/lib/cjs/features/selection/types.d.ts +0 -21
- package/lib/cjs/features/selection/types.js +0 -2
- package/lib/cjs/features/sync-data-loader/feature.d.ts +0 -2
- package/lib/cjs/features/sync-data-loader/feature.js +0 -49
- package/lib/cjs/features/sync-data-loader/types.d.ts +0 -28
- package/lib/cjs/features/sync-data-loader/types.js +0 -2
- package/lib/cjs/features/tree/feature.d.ts +0 -2
- package/lib/cjs/features/tree/feature.js +0 -244
- package/lib/cjs/features/tree/types.d.ts +0 -63
- package/lib/cjs/features/tree/types.js +0 -2
- package/lib/cjs/index.d.ts +0 -33
- package/lib/cjs/index.js +0 -51
- package/lib/cjs/mddocs-entry.d.ts +0 -121
- package/lib/cjs/mddocs-entry.js +0 -17
- package/lib/cjs/test-utils/test-tree-do.d.ts +0 -23
- package/lib/cjs/test-utils/test-tree-do.js +0 -112
- package/lib/cjs/test-utils/test-tree-expect.d.ts +0 -17
- package/lib/cjs/test-utils/test-tree-expect.js +0 -66
- package/lib/cjs/test-utils/test-tree.d.ts +0 -48
- package/lib/cjs/test-utils/test-tree.js +0 -207
- package/lib/cjs/types/core.d.ts +0 -84
- package/lib/cjs/types/core.js +0 -2
- package/lib/cjs/types/deep-merge.d.ts +0 -13
- package/lib/cjs/types/deep-merge.js +0 -2
- package/lib/cjs/utilities/create-on-drop-handler.d.ts +0 -3
- package/lib/cjs/utilities/create-on-drop-handler.js +0 -20
- package/lib/cjs/utilities/errors.d.ts +0 -2
- package/lib/cjs/utilities/errors.js +0 -9
- package/lib/cjs/utilities/insert-items-at-target.d.ts +0 -3
- package/lib/cjs/utilities/insert-items-at-target.js +0 -40
- package/lib/cjs/utilities/remove-items-from-parents.d.ts +0 -2
- package/lib/cjs/utilities/remove-items-from-parents.js +0 -32
- package/lib/cjs/utils.d.ts +0 -6
- package/lib/cjs/utils.js +0 -53
- package/lib/esm/core/build-proxified-instance.d.ts +0 -2
- package/lib/esm/core/build-proxified-instance.js +0 -54
- package/lib/esm/core/build-static-instance.d.ts +0 -2
- package/lib/esm/core/build-static-instance.js +0 -22
- package/lib/esm/core/create-tree.d.ts +0 -2
- package/lib/esm/core/create-tree.js +0 -187
- package/lib/esm/features/async-data-loader/feature.d.ts +0 -2
- package/lib/esm/features/async-data-loader/feature.js +0 -132
- package/lib/esm/features/async-data-loader/types.d.ts +0 -47
- package/lib/esm/features/async-data-loader/types.js +0 -1
- package/lib/esm/features/checkboxes/feature.d.ts +0 -2
- package/lib/esm/features/checkboxes/feature.js +0 -91
- package/lib/esm/features/checkboxes/types.d.ts +0 -26
- package/lib/esm/features/checkboxes/types.js +0 -6
- package/lib/esm/features/drag-and-drop/feature.d.ts +0 -2
- package/lib/esm/features/drag-and-drop/feature.js +0 -202
- package/lib/esm/features/drag-and-drop/types.d.ts +0 -71
- package/lib/esm/features/drag-and-drop/types.js +0 -6
- package/lib/esm/features/drag-and-drop/utils.d.ts +0 -27
- package/lib/esm/features/drag-and-drop/utils.js +0 -172
- package/lib/esm/features/expand-all/feature.d.ts +0 -2
- package/lib/esm/features/expand-all/feature.js +0 -67
- package/lib/esm/features/expand-all/types.d.ts +0 -19
- package/lib/esm/features/expand-all/types.js +0 -1
- package/lib/esm/features/hotkeys-core/feature.d.ts +0 -2
- package/lib/esm/features/hotkeys-core/feature.js +0 -104
- package/lib/esm/features/hotkeys-core/types.d.ts +0 -27
- package/lib/esm/features/hotkeys-core/types.js +0 -1
- package/lib/esm/features/keyboard-drag-and-drop/feature.d.ts +0 -2
- package/lib/esm/features/keyboard-drag-and-drop/feature.js +0 -203
- package/lib/esm/features/keyboard-drag-and-drop/types.d.ts +0 -27
- package/lib/esm/features/keyboard-drag-and-drop/types.js +0 -8
- package/lib/esm/features/main/types.d.ts +0 -47
- package/lib/esm/features/main/types.js +0 -1
- package/lib/esm/features/prop-memoization/feature.d.ts +0 -2
- package/lib/esm/features/prop-memoization/feature.js +0 -67
- package/lib/esm/features/prop-memoization/types.d.ts +0 -15
- package/lib/esm/features/prop-memoization/types.js +0 -1
- package/lib/esm/features/renaming/feature.d.ts +0 -2
- package/lib/esm/features/renaming/feature.js +0 -83
- package/lib/esm/features/renaming/types.d.ts +0 -27
- package/lib/esm/features/renaming/types.js +0 -1
- package/lib/esm/features/search/feature.d.ts +0 -2
- package/lib/esm/features/search/feature.js +0 -116
- package/lib/esm/features/search/types.d.ts +0 -32
- package/lib/esm/features/search/types.js +0 -1
- package/lib/esm/features/selection/feature.d.ts +0 -2
- package/lib/esm/features/selection/feature.js +0 -129
- package/lib/esm/features/selection/types.d.ts +0 -21
- package/lib/esm/features/selection/types.js +0 -1
- package/lib/esm/features/sync-data-loader/feature.d.ts +0 -2
- package/lib/esm/features/sync-data-loader/feature.js +0 -46
- package/lib/esm/features/sync-data-loader/types.d.ts +0 -28
- package/lib/esm/features/sync-data-loader/types.js +0 -1
- package/lib/esm/features/tree/feature.d.ts +0 -2
- package/lib/esm/features/tree/feature.js +0 -241
- package/lib/esm/features/tree/types.d.ts +0 -63
- package/lib/esm/features/tree/types.js +0 -1
- package/lib/esm/index.d.ts +0 -33
- package/lib/esm/index.js +0 -32
- package/lib/esm/mddocs-entry.d.ts +0 -121
- package/lib/esm/mddocs-entry.js +0 -1
- package/lib/esm/test-utils/test-tree-do.d.ts +0 -23
- package/lib/esm/test-utils/test-tree-do.js +0 -108
- package/lib/esm/test-utils/test-tree-expect.d.ts +0 -17
- package/lib/esm/test-utils/test-tree-expect.js +0 -62
- package/lib/esm/test-utils/test-tree.d.ts +0 -48
- package/lib/esm/test-utils/test-tree.js +0 -203
- package/lib/esm/types/core.d.ts +0 -84
- package/lib/esm/types/core.js +0 -1
- package/lib/esm/types/deep-merge.d.ts +0 -13
- package/lib/esm/types/deep-merge.js +0 -1
- package/lib/esm/utilities/create-on-drop-handler.d.ts +0 -3
- package/lib/esm/utilities/create-on-drop-handler.js +0 -16
- package/lib/esm/utilities/errors.d.ts +0 -2
- package/lib/esm/utilities/errors.js +0 -4
- package/lib/esm/utilities/insert-items-at-target.d.ts +0 -3
- package/lib/esm/utilities/insert-items-at-target.js +0 -36
- package/lib/esm/utilities/remove-items-from-parents.d.ts +0 -2
- package/lib/esm/utilities/remove-items-from-parents.js +0 -28
- package/lib/esm/utils.d.ts +0 -6
- package/lib/esm/utils.js +0 -46
package/package.json
CHANGED
|
@@ -1,19 +1,26 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@headless-tree/core",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"main": "
|
|
5
|
-
"module": "
|
|
6
|
-
"types": "
|
|
3
|
+
"version": "1.4.0",
|
|
4
|
+
"main": "dist/index.d.ts",
|
|
5
|
+
"module": "dist/index.mjs",
|
|
6
|
+
"types": "dist/index.d.mts",
|
|
7
7
|
"exports": {
|
|
8
|
-
"
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
".": {
|
|
9
|
+
"import": {
|
|
10
|
+
"types": "./dist/index.d.mts",
|
|
11
|
+
"default": "./dist/index.mjs"
|
|
12
|
+
},
|
|
13
|
+
"require": {
|
|
14
|
+
"types": "./dist/index.js",
|
|
15
|
+
"default": "./dist/index.d.ts"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"./package.json": "./package.json"
|
|
11
19
|
},
|
|
12
20
|
"sideEffects": false,
|
|
13
21
|
"scripts": {
|
|
14
|
-
"build
|
|
15
|
-
"
|
|
16
|
-
"start": "tsc -w",
|
|
22
|
+
"build": "tsup ./src/index.ts --format esm,cjs --dts",
|
|
23
|
+
"start": "tsup ./src/index.ts --format esm,cjs --dts --watch",
|
|
17
24
|
"test": "vitest run"
|
|
18
25
|
},
|
|
19
26
|
"repository": {
|
|
@@ -26,6 +33,7 @@
|
|
|
26
33
|
"license": "MIT",
|
|
27
34
|
"devDependencies": {
|
|
28
35
|
"jsdom": "^26.0.0",
|
|
36
|
+
"tsup": "^8.5.0",
|
|
29
37
|
"typescript": "^5.7.2",
|
|
30
38
|
"vitest": "^3.0.3"
|
|
31
39
|
}
|
package/src/core/create-tree.ts
CHANGED
|
@@ -170,19 +170,6 @@ export const createTree = <T>(
|
|
|
170
170
|
] as Function;
|
|
171
171
|
externalStateSetter?.(state[stateName]);
|
|
172
172
|
},
|
|
173
|
-
buildItemInstance: ({}, itemId) => {
|
|
174
|
-
const [instance, finalizeInstance] = buildInstance(
|
|
175
|
-
features,
|
|
176
|
-
"itemInstance",
|
|
177
|
-
(instance) => ({
|
|
178
|
-
item: instance,
|
|
179
|
-
tree: treeInstance,
|
|
180
|
-
itemId,
|
|
181
|
-
}),
|
|
182
|
-
);
|
|
183
|
-
finalizeInstance();
|
|
184
|
-
return instance;
|
|
185
|
-
},
|
|
186
173
|
// TODO rebuildSubTree: (itemId: string) => void;
|
|
187
174
|
rebuildTree: () => {
|
|
188
175
|
rebuildItemMeta();
|
|
@@ -206,7 +193,23 @@ export const createTree = <T>(
|
|
|
206
193
|
config.setState?.(state);
|
|
207
194
|
}
|
|
208
195
|
},
|
|
209
|
-
getItemInstance: ({}, itemId) =>
|
|
196
|
+
getItemInstance: ({}, itemId) => {
|
|
197
|
+
const existingInstance = itemInstancesMap[itemId];
|
|
198
|
+
if (!existingInstance) {
|
|
199
|
+
const [instance, finalizeInstance] = buildInstance(
|
|
200
|
+
features,
|
|
201
|
+
"itemInstance",
|
|
202
|
+
(instance) => ({
|
|
203
|
+
item: instance,
|
|
204
|
+
tree: treeInstance,
|
|
205
|
+
itemId,
|
|
206
|
+
}),
|
|
207
|
+
);
|
|
208
|
+
finalizeInstance();
|
|
209
|
+
return instance;
|
|
210
|
+
}
|
|
211
|
+
return existingInstance;
|
|
212
|
+
},
|
|
210
213
|
getItems: () => itemInstances,
|
|
211
214
|
registerElement: ({}, element) => {
|
|
212
215
|
if (treeElement === element) {
|
|
@@ -249,7 +252,15 @@ export const createTree = <T>(
|
|
|
249
252
|
getElement: ({ itemId }) => itemElementsMap[itemId],
|
|
250
253
|
// eslint-disable-next-line no-return-assign
|
|
251
254
|
getDataRef: ({ itemId }) => (itemDataRefs[itemId] ??= { current: {} }),
|
|
252
|
-
getItemMeta: ({ itemId }) =>
|
|
255
|
+
getItemMeta: ({ itemId }) =>
|
|
256
|
+
itemMetaMap[itemId] ?? {
|
|
257
|
+
itemId,
|
|
258
|
+
parentId: null,
|
|
259
|
+
level: -1,
|
|
260
|
+
index: -1,
|
|
261
|
+
posInSet: 0,
|
|
262
|
+
setSize: 1,
|
|
263
|
+
},
|
|
253
264
|
},
|
|
254
265
|
};
|
|
255
266
|
|
|
@@ -165,5 +165,10 @@ export const asyncDataLoaderFeature: FeatureImplementation = {
|
|
|
165
165
|
dataRef.current.childrenIds[itemId] = childrenIds;
|
|
166
166
|
tree.rebuildTree();
|
|
167
167
|
},
|
|
168
|
+
updateCachedData: ({ tree, itemId }, data) => {
|
|
169
|
+
const dataRef = tree.getDataRef<AsyncDataLoaderDataRef>();
|
|
170
|
+
dataRef.current.itemData[itemId] = data;
|
|
171
|
+
tree.rebuildTree();
|
|
172
|
+
},
|
|
168
173
|
},
|
|
169
174
|
};
|
|
@@ -34,6 +34,7 @@ export type AsyncDataLoaderFeatureDef<T> = {
|
|
|
34
34
|
waitForItemChildrenLoaded: (itemId: string) => Promise<void>;
|
|
35
35
|
loadItemData: (itemId: string) => Promise<T>;
|
|
36
36
|
loadChildrenIds: (itemId: string) => Promise<string[]>;
|
|
37
|
+
/* idea: recursiveLoadItems: (itemId: string, cancelToken?: { current: boolean }, onLoad: (itemIds: string[]) => void) => Promise<T[]> */
|
|
37
38
|
};
|
|
38
39
|
itemInstance: SyncDataLoaderFeatureDef<T>["itemInstance"] & {
|
|
39
40
|
/** Invalidate fetched data for item, and triggers a refetch and subsequent rerender if the item is visible
|
|
@@ -46,6 +47,7 @@ export type AsyncDataLoaderFeatureDef<T> = {
|
|
|
46
47
|
* the tree will continue to display the old data until the new data has loaded. */
|
|
47
48
|
invalidateChildrenIds: (optimistic?: boolean) => Promise<void>;
|
|
48
49
|
|
|
50
|
+
updateCachedData: (data: T) => void;
|
|
49
51
|
updateCachedChildrenIds: (childrenIds: string[]) => void;
|
|
50
52
|
isLoading: () => boolean;
|
|
51
53
|
};
|
|
@@ -74,17 +74,28 @@ describe("core-feature/checkboxes", () => {
|
|
|
74
74
|
});
|
|
75
75
|
});
|
|
76
76
|
|
|
77
|
-
it("should handle folder checking
|
|
77
|
+
it("should handle folder checking", async () => {
|
|
78
78
|
const tree = await factory
|
|
79
|
-
.with({ canCheckFolders: true })
|
|
79
|
+
.with({ canCheckFolders: true, propagateCheckedState: false })
|
|
80
80
|
.createTestCaseTree();
|
|
81
81
|
|
|
82
82
|
tree.item("x11").setChecked();
|
|
83
83
|
expect(tree.instance.getState().checkedItems).toContain("x11");
|
|
84
84
|
});
|
|
85
85
|
|
|
86
|
-
it("should
|
|
87
|
-
const tree = await factory
|
|
86
|
+
it("should not check folders if disabled", async () => {
|
|
87
|
+
const tree = await factory
|
|
88
|
+
.with({ canCheckFolders: false, propagateCheckedState: false })
|
|
89
|
+
.createTestCaseTree();
|
|
90
|
+
|
|
91
|
+
tree.item("x11").setChecked();
|
|
92
|
+
expect(tree.instance.getState().checkedItems.length).toBe(0);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("should propagate checked state", async () => {
|
|
96
|
+
const tree = await factory
|
|
97
|
+
.with({ propagateCheckedState: true })
|
|
98
|
+
.createTestCaseTree();
|
|
88
99
|
|
|
89
100
|
tree.item("x11").setChecked();
|
|
90
101
|
expect(tree.instance.getState().checkedItems).toEqual(
|
|
@@ -93,7 +104,9 @@ describe("core-feature/checkboxes", () => {
|
|
|
93
104
|
});
|
|
94
105
|
|
|
95
106
|
it("should turn folder indeterminate", async () => {
|
|
96
|
-
const tree = await factory
|
|
107
|
+
const tree = await factory
|
|
108
|
+
.with({ propagateCheckedState: true })
|
|
109
|
+
.createTestCaseTree();
|
|
97
110
|
|
|
98
111
|
tree.item("x111").setChecked();
|
|
99
112
|
expect(tree.item("x11").getCheckedState()).toBe(CheckedState.Indeterminate);
|
|
@@ -103,6 +116,8 @@ describe("core-feature/checkboxes", () => {
|
|
|
103
116
|
const tree = await factory
|
|
104
117
|
.with({
|
|
105
118
|
isItemFolder: (item) => item.getItemData().length < 4,
|
|
119
|
+
propagateCheckedState: true,
|
|
120
|
+
canCheckFolders: false,
|
|
106
121
|
})
|
|
107
122
|
.createTestCaseTree();
|
|
108
123
|
|
|
@@ -6,14 +6,16 @@ import { throwError } from "../../utilities/errors";
|
|
|
6
6
|
const getAllLoadedDescendants = <T>(
|
|
7
7
|
tree: TreeInstance<T>,
|
|
8
8
|
itemId: string,
|
|
9
|
+
includeFolders = false,
|
|
9
10
|
): string[] => {
|
|
10
|
-
if (!tree.getConfig().isItemFolder(tree.
|
|
11
|
+
if (!tree.getConfig().isItemFolder(tree.getItemInstance(itemId))) {
|
|
11
12
|
return [itemId];
|
|
12
13
|
}
|
|
13
|
-
|
|
14
|
+
const descendants = tree
|
|
14
15
|
.retrieveChildrenIds(itemId)
|
|
15
|
-
.map((child) => getAllLoadedDescendants(tree, child))
|
|
16
|
+
.map((child) => getAllLoadedDescendants(tree, child, includeFolders))
|
|
16
17
|
.flat();
|
|
18
|
+
return includeFolders ? [itemId, ...descendants] : descendants;
|
|
17
19
|
};
|
|
18
20
|
|
|
19
21
|
export const checkboxesFeature: FeatureImplementation = {
|
|
@@ -30,12 +32,17 @@ export const checkboxesFeature: FeatureImplementation = {
|
|
|
30
32
|
const hasAsyncLoader = defaultConfig.features?.some(
|
|
31
33
|
(f) => f.key === "async-data-loader",
|
|
32
34
|
);
|
|
33
|
-
if (hasAsyncLoader &&
|
|
34
|
-
throwError(
|
|
35
|
+
if (hasAsyncLoader && defaultConfig.propagateCheckedState) {
|
|
36
|
+
throwError(`propagateCheckedState not supported with async trees`);
|
|
35
37
|
}
|
|
38
|
+
const propagateCheckedState =
|
|
39
|
+
defaultConfig.propagateCheckedState ?? !hasAsyncLoader;
|
|
40
|
+
const canCheckFolders =
|
|
41
|
+
defaultConfig.canCheckFolders ?? !propagateCheckedState;
|
|
36
42
|
return {
|
|
37
43
|
setCheckedItems: makeStateUpdater("checkedItems", tree),
|
|
38
|
-
|
|
44
|
+
propagateCheckedState,
|
|
45
|
+
canCheckFolders,
|
|
39
46
|
...defaultConfig,
|
|
40
47
|
};
|
|
41
48
|
},
|
|
@@ -72,14 +79,16 @@ export const checkboxesFeature: FeatureImplementation = {
|
|
|
72
79
|
}
|
|
73
80
|
},
|
|
74
81
|
|
|
75
|
-
getCheckedState: ({ item, tree
|
|
82
|
+
getCheckedState: ({ item, tree }) => {
|
|
76
83
|
const { checkedItems } = tree.getState();
|
|
84
|
+
const { propagateCheckedState } = tree.getConfig();
|
|
85
|
+
const itemId = item.getId();
|
|
77
86
|
|
|
78
87
|
if (checkedItems.includes(itemId)) {
|
|
79
88
|
return CheckedState.Checked;
|
|
80
89
|
}
|
|
81
90
|
|
|
82
|
-
if (item.isFolder() &&
|
|
91
|
+
if (item.isFolder() && propagateCheckedState) {
|
|
83
92
|
const descendants = getAllLoadedDescendants(tree, itemId);
|
|
84
93
|
if (descendants.every((d) => checkedItems.includes(d))) {
|
|
85
94
|
return CheckedState.Checked;
|
|
@@ -93,25 +102,31 @@ export const checkboxesFeature: FeatureImplementation = {
|
|
|
93
102
|
},
|
|
94
103
|
|
|
95
104
|
setChecked: ({ item, tree, itemId }) => {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
} else {
|
|
105
|
+
const { propagateCheckedState, canCheckFolders } = tree.getConfig();
|
|
106
|
+
if (item.isFolder() && propagateCheckedState) {
|
|
99
107
|
tree.applySubStateUpdate("checkedItems", (items) => [
|
|
100
108
|
...items,
|
|
101
|
-
...getAllLoadedDescendants(tree, itemId),
|
|
109
|
+
...getAllLoadedDescendants(tree, itemId, canCheckFolders),
|
|
102
110
|
]);
|
|
111
|
+
} else if (!item.isFolder() || canCheckFolders) {
|
|
112
|
+
tree.applySubStateUpdate("checkedItems", (items) => [...items, itemId]);
|
|
103
113
|
}
|
|
104
114
|
},
|
|
105
115
|
|
|
106
116
|
setUnchecked: ({ item, tree, itemId }) => {
|
|
107
|
-
|
|
117
|
+
const { propagateCheckedState, canCheckFolders } = tree.getConfig();
|
|
118
|
+
if (item.isFolder() && propagateCheckedState) {
|
|
119
|
+
const descendants = getAllLoadedDescendants(
|
|
120
|
+
tree,
|
|
121
|
+
itemId,
|
|
122
|
+
canCheckFolders,
|
|
123
|
+
);
|
|
108
124
|
tree.applySubStateUpdate("checkedItems", (items) =>
|
|
109
|
-
items.filter((id) => id !== itemId),
|
|
125
|
+
items.filter((id) => !descendants.includes(id) && id !== itemId),
|
|
110
126
|
);
|
|
111
127
|
} else {
|
|
112
|
-
const descendants = getAllLoadedDescendants(tree, itemId);
|
|
113
128
|
tree.applySubStateUpdate("checkedItems", (items) =>
|
|
114
|
-
items.filter((id) =>
|
|
129
|
+
items.filter((id) => id !== itemId),
|
|
115
130
|
);
|
|
116
131
|
}
|
|
117
132
|
},
|
|
@@ -182,7 +182,7 @@ describe("core-feature/drag-and-drop", () => {
|
|
|
182
182
|
});
|
|
183
183
|
});
|
|
184
184
|
|
|
185
|
-
it("updates dnd state", () => {
|
|
185
|
+
it("updates dnd state", async () => {
|
|
186
186
|
const setDndState = tree.mockedHandler("setDndState");
|
|
187
187
|
tree.do.startDrag("x111");
|
|
188
188
|
expect(setDndState).toBeCalledWith({
|
|
@@ -198,7 +198,7 @@ describe("core-feature/drag-and-drop", () => {
|
|
|
198
198
|
},
|
|
199
199
|
});
|
|
200
200
|
tree.do.drop("x22");
|
|
201
|
-
expect(setDndState).toBeCalledWith(null);
|
|
201
|
+
await vi.waitFor(() => expect(setDndState).toBeCalledWith(null));
|
|
202
202
|
});
|
|
203
203
|
});
|
|
204
204
|
|
|
@@ -300,6 +300,9 @@ describe("core-feature/drag-and-drop", () => {
|
|
|
300
300
|
|
|
301
301
|
it("drags foreign object inside tree, on folder", () => {
|
|
302
302
|
tree.mockedHandler("canDropForeignDragObject").mockReturnValue(true);
|
|
303
|
+
tree
|
|
304
|
+
.mockedHandler("canDragForeignDragObjectOver")
|
|
305
|
+
.mockReturnValue(true);
|
|
303
306
|
const onDropForeignDragObject = tree.mockedHandler(
|
|
304
307
|
"onDropForeignDragObject",
|
|
305
308
|
);
|
|
@@ -318,6 +321,9 @@ describe("core-feature/drag-and-drop", () => {
|
|
|
318
321
|
tree
|
|
319
322
|
.mockedHandler("canDropForeignDragObject")
|
|
320
323
|
.mockImplementation((_, target) => target.item.isFolder());
|
|
324
|
+
tree
|
|
325
|
+
.mockedHandler("canDragForeignDragObjectOver")
|
|
326
|
+
.mockImplementation((_, target) => target.item.isFolder());
|
|
321
327
|
const onDropForeignDragObject = tree.mockedHandler(
|
|
322
328
|
"onDropForeignDragObject",
|
|
323
329
|
);
|
|
@@ -340,6 +346,9 @@ describe("core-feature/drag-and-drop", () => {
|
|
|
340
346
|
|
|
341
347
|
it("doesnt drag foreign object inside tree if not allowed", () => {
|
|
342
348
|
tree.mockedHandler("canDropForeignDragObject").mockReturnValue(false);
|
|
349
|
+
tree
|
|
350
|
+
.mockedHandler("canDragForeignDragObjectOver")
|
|
351
|
+
.mockReturnValue(false);
|
|
343
352
|
const onDropForeignDragObject = tree.mockedHandler(
|
|
344
353
|
"onDropForeignDragObject",
|
|
345
354
|
);
|
|
@@ -1,22 +1,62 @@
|
|
|
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
|
+
|
|
46
|
+
const defaultCanDropForeignDragObject = () => false;
|
|
11
47
|
export const dragAndDropFeature: FeatureImplementation = {
|
|
12
48
|
key: "drag-and-drop",
|
|
13
|
-
deps: ["selection"],
|
|
14
49
|
|
|
15
50
|
getDefaultConfig: (defaultConfig, tree) => ({
|
|
16
51
|
canDrop: (_, target) => target.item.isFolder(),
|
|
17
|
-
canDropForeignDragObject:
|
|
52
|
+
canDropForeignDragObject: defaultCanDropForeignDragObject,
|
|
53
|
+
canDragForeignDragObjectOver:
|
|
54
|
+
defaultConfig.canDropForeignDragObject !== defaultCanDropForeignDragObject
|
|
55
|
+
? (dataTransfer) => dataTransfer.effectAllowed !== "none"
|
|
56
|
+
: () => false,
|
|
18
57
|
setDndState: makeStateUpdater("dnd", tree),
|
|
19
58
|
canReorder: true,
|
|
59
|
+
openOnDropDelay: 800,
|
|
20
60
|
...defaultConfig,
|
|
21
61
|
}),
|
|
22
62
|
|
|
@@ -24,6 +64,19 @@ export const dragAndDropFeature: FeatureImplementation = {
|
|
|
24
64
|
dnd: "setDndState",
|
|
25
65
|
},
|
|
26
66
|
|
|
67
|
+
onTreeMount: (tree) => {
|
|
68
|
+
const listener = () => {
|
|
69
|
+
tree.applySubStateUpdate("dnd", null);
|
|
70
|
+
};
|
|
71
|
+
tree.getDataRef<DndDataRef>().current.windowDragEndListener = listener;
|
|
72
|
+
window.addEventListener("dragend", listener);
|
|
73
|
+
},
|
|
74
|
+
onTreeUnmount: (tree) => {
|
|
75
|
+
const { windowDragEndListener } = tree.getDataRef<DndDataRef>().current;
|
|
76
|
+
if (!windowDragEndListener) return;
|
|
77
|
+
window.removeEventListener("dragend", windowDragEndListener);
|
|
78
|
+
},
|
|
79
|
+
|
|
27
80
|
treeInstance: {
|
|
28
81
|
getDragTarget: ({ tree }) => {
|
|
29
82
|
return tree.getState().dnd?.dragTarget ?? null;
|
|
@@ -56,7 +109,7 @@ export const dragAndDropFeature: FeatureImplementation = {
|
|
|
56
109
|
}
|
|
57
110
|
}
|
|
58
111
|
|
|
59
|
-
const bb = targetItem
|
|
112
|
+
const bb = targetItem?.getElement()?.getBoundingClientRect();
|
|
60
113
|
|
|
61
114
|
if (bb) {
|
|
62
115
|
return {
|
|
@@ -106,7 +159,6 @@ export const dragAndDropFeature: FeatureImplementation = {
|
|
|
106
159
|
const draggedItems = tree.getState().dnd?.draggedItems;
|
|
107
160
|
|
|
108
161
|
dataRef.current.lastDragCode = undefined;
|
|
109
|
-
tree.applySubStateUpdate("dnd", null);
|
|
110
162
|
|
|
111
163
|
if (draggedItems) {
|
|
112
164
|
await config.onDrop?.(draggedItems, target);
|
|
@@ -132,12 +184,14 @@ export const dragAndDropFeature: FeatureImplementation = {
|
|
|
132
184
|
onDragEnter: (e: DragEvent) => e.preventDefault(),
|
|
133
185
|
|
|
134
186
|
onDragStart: (e: DragEvent) => {
|
|
135
|
-
const selectedItems = tree.getSelectedItems
|
|
187
|
+
const selectedItems = tree.getSelectedItems
|
|
188
|
+
? tree.getSelectedItems()
|
|
189
|
+
: [tree.getFocusedItem()];
|
|
136
190
|
const items = selectedItems.includes(item) ? selectedItems : [item];
|
|
137
191
|
const config = tree.getConfig();
|
|
138
192
|
|
|
139
193
|
if (!selectedItems.includes(item)) {
|
|
140
|
-
tree.setSelectedItems([item.getItemMeta().itemId]);
|
|
194
|
+
tree.setSelectedItems?.([item.getItemMeta().itemId]);
|
|
141
195
|
}
|
|
142
196
|
|
|
143
197
|
if (!(config.canDrag?.(items) ?? true)) {
|
|
@@ -150,9 +204,13 @@ export const dragAndDropFeature: FeatureImplementation = {
|
|
|
150
204
|
e.dataTransfer?.setDragImage(imgElement, xOffset ?? 0, yOffset ?? 0);
|
|
151
205
|
}
|
|
152
206
|
|
|
153
|
-
if (config.createForeignDragObject) {
|
|
154
|
-
const { format, data } =
|
|
155
|
-
|
|
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;
|
|
156
214
|
}
|
|
157
215
|
|
|
158
216
|
tree.applySubStateUpdate("dnd", {
|
|
@@ -162,8 +220,11 @@ export const dragAndDropFeature: FeatureImplementation = {
|
|
|
162
220
|
},
|
|
163
221
|
|
|
164
222
|
onDragOver: (e: DragEvent) => {
|
|
223
|
+
e.stopPropagation(); // don't bubble up to container dragover
|
|
165
224
|
const dataRef = tree.getDataRef<DndDataRef>();
|
|
166
|
-
const
|
|
225
|
+
const placement = getTargetPlacement(e, item, tree, true);
|
|
226
|
+
const nextDragCode = getDragCode(item, placement);
|
|
227
|
+
|
|
167
228
|
if (nextDragCode === dataRef.current.lastDragCode) {
|
|
168
229
|
if (dataRef.current.lastAllowDrop) {
|
|
169
230
|
e.preventDefault();
|
|
@@ -171,6 +232,9 @@ export const dragAndDropFeature: FeatureImplementation = {
|
|
|
171
232
|
return;
|
|
172
233
|
}
|
|
173
234
|
dataRef.current.lastDragCode = nextDragCode;
|
|
235
|
+
dataRef.current.lastDragEnter = Date.now();
|
|
236
|
+
|
|
237
|
+
handleAutoOpenFolder(dataRef, tree, item, placement);
|
|
174
238
|
|
|
175
239
|
const target = getDragTarget(e, item, tree);
|
|
176
240
|
|
|
@@ -179,7 +243,7 @@ export const dragAndDropFeature: FeatureImplementation = {
|
|
|
179
243
|
(!e.dataTransfer ||
|
|
180
244
|
!tree
|
|
181
245
|
.getConfig()
|
|
182
|
-
.
|
|
246
|
+
.canDragForeignDragObjectOver?.(e.dataTransfer, target))
|
|
183
247
|
) {
|
|
184
248
|
dataRef.current.lastAllowDrop = false;
|
|
185
249
|
return;
|
|
@@ -200,41 +264,60 @@ export const dragAndDropFeature: FeatureImplementation = {
|
|
|
200
264
|
},
|
|
201
265
|
|
|
202
266
|
onDragLeave: () => {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
267
|
+
setTimeout(() => {
|
|
268
|
+
const dataRef = tree.getDataRef<DndDataRef>();
|
|
269
|
+
if ((dataRef.current.lastDragEnter ?? 0) + 100 >= Date.now()) return;
|
|
270
|
+
dataRef.current.lastDragCode = "no-drag";
|
|
271
|
+
tree.applySubStateUpdate("dnd", (state) => ({
|
|
272
|
+
...state,
|
|
273
|
+
draggingOverItem: undefined,
|
|
274
|
+
dragTarget: undefined,
|
|
275
|
+
}));
|
|
276
|
+
}, 100);
|
|
210
277
|
},
|
|
211
278
|
|
|
212
279
|
onDragEnd: (e: DragEvent) => {
|
|
280
|
+
const { onCompleteForeignDrop, canDragForeignDragObjectOver } =
|
|
281
|
+
tree.getConfig();
|
|
213
282
|
const draggedItems = tree.getState().dnd?.draggedItems;
|
|
214
|
-
tree.applySubStateUpdate("dnd", null);
|
|
215
283
|
|
|
216
284
|
if (e.dataTransfer?.dropEffect === "none" || !draggedItems) {
|
|
217
285
|
return;
|
|
218
286
|
}
|
|
219
287
|
|
|
220
|
-
tree
|
|
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);
|
|
221
298
|
},
|
|
222
299
|
|
|
223
300
|
onDrop: async (e: DragEvent) => {
|
|
224
301
|
e.stopPropagation();
|
|
225
302
|
const dataRef = tree.getDataRef<DndDataRef>();
|
|
226
303
|
const target = getDragTarget(e, item, tree);
|
|
304
|
+
const draggedItems = tree.getState().dnd?.draggedItems;
|
|
305
|
+
const isValidDrop = canDrop(e.dataTransfer, target, tree);
|
|
227
306
|
|
|
228
|
-
|
|
307
|
+
tree.applySubStateUpdate("dnd", {
|
|
308
|
+
draggedItems: undefined,
|
|
309
|
+
draggingOverItem: undefined,
|
|
310
|
+
dragTarget: undefined,
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
if (!isValidDrop) {
|
|
229
314
|
return;
|
|
230
315
|
}
|
|
231
316
|
|
|
232
317
|
e.preventDefault();
|
|
233
318
|
const config = tree.getConfig();
|
|
234
|
-
const draggedItems = tree.getState().dnd?.draggedItems;
|
|
235
319
|
|
|
236
320
|
dataRef.current.lastDragCode = undefined;
|
|
237
|
-
tree.applySubStateUpdate("dnd", null);
|
|
238
321
|
|
|
239
322
|
if (draggedItems) {
|
|
240
323
|
await config.onDrop?.(draggedItems, target);
|
|
@@ -3,6 +3,9 @@ import { ItemInstance, SetStateFn } from "../../types/core";
|
|
|
3
3
|
export interface DndDataRef {
|
|
4
4
|
lastDragCode?: string;
|
|
5
5
|
lastAllowDrop?: boolean;
|
|
6
|
+
lastDragEnter?: number;
|
|
7
|
+
autoExpandTimeout?: any;
|
|
8
|
+
windowDragEndListener?: () => void;
|
|
6
9
|
}
|
|
7
10
|
|
|
8
11
|
export interface DndState<T> {
|
|
@@ -57,16 +60,31 @@ export type DragAndDropFeatureDef<T> = {
|
|
|
57
60
|
createForeignDragObject?: (items: ItemInstance<T>[]) => {
|
|
58
61
|
format: string;
|
|
59
62
|
data: any;
|
|
63
|
+
dropEffect?: DataTransfer["dropEffect"];
|
|
64
|
+
effectAllowed?: DataTransfer["effectAllowed"];
|
|
60
65
|
};
|
|
61
66
|
setDragImage?: (items: ItemInstance<T>[]) => {
|
|
62
67
|
imgElement: Element;
|
|
63
68
|
xOffset?: number;
|
|
64
69
|
yOffset?: number;
|
|
65
70
|
};
|
|
71
|
+
|
|
72
|
+
/** Checks if a foreign drag object can be dropped on a target, validating that an actual drop can commence based on
|
|
73
|
+
* the data in the DataTransfer object. */
|
|
66
74
|
canDropForeignDragObject?: (
|
|
67
75
|
dataTransfer: DataTransfer,
|
|
68
76
|
target: DragTarget<T>,
|
|
69
77
|
) => boolean;
|
|
78
|
+
|
|
79
|
+
/** Checks if a droppable visualization should be displayed when dragging a foreign object over a target. Since this
|
|
80
|
+
* is executed on a dragover event, `dataTransfer.getData()` is not available, so `dataTransfer.effectAllowed` or
|
|
81
|
+
* `dataTransfer.types` should be used instead. Before actually completing the drag, @{link canDropForeignDragObject}
|
|
82
|
+
* will be called by HT before applying the drop. */
|
|
83
|
+
canDragForeignDragObjectOver?: (
|
|
84
|
+
dataTransfer: DataTransfer,
|
|
85
|
+
target: DragTarget<T>,
|
|
86
|
+
) => boolean;
|
|
87
|
+
|
|
70
88
|
onDrop?: (
|
|
71
89
|
items: ItemInstance<T>[],
|
|
72
90
|
target: DragTarget<T>,
|
|
@@ -76,6 +94,9 @@ export type DragAndDropFeatureDef<T> = {
|
|
|
76
94
|
target: DragTarget<T>,
|
|
77
95
|
) => void | Promise<void>;
|
|
78
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;
|
|
79
100
|
};
|
|
80
101
|
treeInstance: {
|
|
81
102
|
getDragTarget: () => DragTarget<T> | null;
|