@headless-tree/core 1.2.1 → 1.3.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 +19 -0
- package/dist/index.d.mts +577 -0
- package/dist/index.d.ts +577 -0
- package/dist/index.js +2321 -0
- package/dist/index.mjs +2276 -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 +51 -16
- package/src/features/drag-and-drop/types.ts +17 -0
- package/src/features/keyboard-drag-and-drop/feature.ts +8 -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.3.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
|
);
|
|
@@ -8,13 +8,18 @@ import {
|
|
|
8
8
|
} from "./utils";
|
|
9
9
|
import { makeStateUpdater } from "../../utils";
|
|
10
10
|
|
|
11
|
+
const defaultCanDropForeignDragObject = () => false;
|
|
11
12
|
export const dragAndDropFeature: FeatureImplementation = {
|
|
12
13
|
key: "drag-and-drop",
|
|
13
14
|
deps: ["selection"],
|
|
14
15
|
|
|
15
16
|
getDefaultConfig: (defaultConfig, tree) => ({
|
|
16
17
|
canDrop: (_, target) => target.item.isFolder(),
|
|
17
|
-
canDropForeignDragObject:
|
|
18
|
+
canDropForeignDragObject: defaultCanDropForeignDragObject,
|
|
19
|
+
canDragForeignDragObjectOver:
|
|
20
|
+
defaultConfig.canDropForeignDragObject !== defaultCanDropForeignDragObject
|
|
21
|
+
? (dataTransfer) => dataTransfer.effectAllowed !== "none"
|
|
22
|
+
: () => false,
|
|
18
23
|
setDndState: makeStateUpdater("dnd", tree),
|
|
19
24
|
canReorder: true,
|
|
20
25
|
...defaultConfig,
|
|
@@ -24,6 +29,19 @@ export const dragAndDropFeature: FeatureImplementation = {
|
|
|
24
29
|
dnd: "setDndState",
|
|
25
30
|
},
|
|
26
31
|
|
|
32
|
+
onTreeMount: (tree) => {
|
|
33
|
+
const listener = () => {
|
|
34
|
+
tree.applySubStateUpdate("dnd", null);
|
|
35
|
+
};
|
|
36
|
+
tree.getDataRef<DndDataRef>().current.windowDragEndListener = listener;
|
|
37
|
+
window.addEventListener("dragend", listener);
|
|
38
|
+
},
|
|
39
|
+
onTreeUnmount: (tree) => {
|
|
40
|
+
const { windowDragEndListener } = tree.getDataRef<DndDataRef>().current;
|
|
41
|
+
if (!windowDragEndListener) return;
|
|
42
|
+
window.removeEventListener("dragend", windowDragEndListener);
|
|
43
|
+
},
|
|
44
|
+
|
|
27
45
|
treeInstance: {
|
|
28
46
|
getDragTarget: ({ tree }) => {
|
|
29
47
|
return tree.getState().dnd?.dragTarget ?? null;
|
|
@@ -106,7 +124,6 @@ export const dragAndDropFeature: FeatureImplementation = {
|
|
|
106
124
|
const draggedItems = tree.getState().dnd?.draggedItems;
|
|
107
125
|
|
|
108
126
|
dataRef.current.lastDragCode = undefined;
|
|
109
|
-
tree.applySubStateUpdate("dnd", null);
|
|
110
127
|
|
|
111
128
|
if (draggedItems) {
|
|
112
129
|
await config.onDrop?.(draggedItems, target);
|
|
@@ -150,9 +167,13 @@ export const dragAndDropFeature: FeatureImplementation = {
|
|
|
150
167
|
e.dataTransfer?.setDragImage(imgElement, xOffset ?? 0, yOffset ?? 0);
|
|
151
168
|
}
|
|
152
169
|
|
|
153
|
-
if (config.createForeignDragObject) {
|
|
154
|
-
const { format, data } =
|
|
155
|
-
|
|
170
|
+
if (config.createForeignDragObject && e.dataTransfer) {
|
|
171
|
+
const { format, data, dropEffect, effectAllowed } =
|
|
172
|
+
config.createForeignDragObject(items);
|
|
173
|
+
e.dataTransfer.setData(format, data);
|
|
174
|
+
|
|
175
|
+
if (dropEffect) e.dataTransfer.dropEffect = dropEffect;
|
|
176
|
+
if (effectAllowed) e.dataTransfer.effectAllowed = effectAllowed;
|
|
156
177
|
}
|
|
157
178
|
|
|
158
179
|
tree.applySubStateUpdate("dnd", {
|
|
@@ -162,6 +183,7 @@ export const dragAndDropFeature: FeatureImplementation = {
|
|
|
162
183
|
},
|
|
163
184
|
|
|
164
185
|
onDragOver: (e: DragEvent) => {
|
|
186
|
+
e.stopPropagation(); // don't bubble up to container dragover
|
|
165
187
|
const dataRef = tree.getDataRef<DndDataRef>();
|
|
166
188
|
const nextDragCode = getDragCode(e, item, tree);
|
|
167
189
|
if (nextDragCode === dataRef.current.lastDragCode) {
|
|
@@ -171,6 +193,7 @@ export const dragAndDropFeature: FeatureImplementation = {
|
|
|
171
193
|
return;
|
|
172
194
|
}
|
|
173
195
|
dataRef.current.lastDragCode = nextDragCode;
|
|
196
|
+
dataRef.current.lastDragEnter = Date.now();
|
|
174
197
|
|
|
175
198
|
const target = getDragTarget(e, item, tree);
|
|
176
199
|
|
|
@@ -179,7 +202,7 @@ export const dragAndDropFeature: FeatureImplementation = {
|
|
|
179
202
|
(!e.dataTransfer ||
|
|
180
203
|
!tree
|
|
181
204
|
.getConfig()
|
|
182
|
-
.
|
|
205
|
+
.canDragForeignDragObjectOver?.(e.dataTransfer, target))
|
|
183
206
|
) {
|
|
184
207
|
dataRef.current.lastAllowDrop = false;
|
|
185
208
|
return;
|
|
@@ -200,24 +223,37 @@ export const dragAndDropFeature: FeatureImplementation = {
|
|
|
200
223
|
},
|
|
201
224
|
|
|
202
225
|
onDragLeave: () => {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
226
|
+
setTimeout(() => {
|
|
227
|
+
const dataRef = tree.getDataRef<DndDataRef>();
|
|
228
|
+
if ((dataRef.current.lastDragEnter ?? 0) + 100 >= Date.now()) return;
|
|
229
|
+
dataRef.current.lastDragCode = "no-drag";
|
|
230
|
+
tree.applySubStateUpdate("dnd", (state) => ({
|
|
231
|
+
...state,
|
|
232
|
+
draggingOverItem: undefined,
|
|
233
|
+
dragTarget: undefined,
|
|
234
|
+
}));
|
|
235
|
+
}, 100);
|
|
210
236
|
},
|
|
211
237
|
|
|
212
238
|
onDragEnd: (e: DragEvent) => {
|
|
239
|
+
const { onCompleteForeignDrop, canDragForeignDragObjectOver } =
|
|
240
|
+
tree.getConfig();
|
|
213
241
|
const draggedItems = tree.getState().dnd?.draggedItems;
|
|
214
|
-
tree.applySubStateUpdate("dnd", null);
|
|
215
242
|
|
|
216
243
|
if (e.dataTransfer?.dropEffect === "none" || !draggedItems) {
|
|
217
244
|
return;
|
|
218
245
|
}
|
|
219
246
|
|
|
220
|
-
tree
|
|
247
|
+
const target = getDragTarget(e, item, tree);
|
|
248
|
+
if (
|
|
249
|
+
canDragForeignDragObjectOver &&
|
|
250
|
+
e.dataTransfer &&
|
|
251
|
+
!canDragForeignDragObjectOver(e.dataTransfer, target)
|
|
252
|
+
) {
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
onCompleteForeignDrop?.(draggedItems);
|
|
221
257
|
},
|
|
222
258
|
|
|
223
259
|
onDrop: async (e: DragEvent) => {
|
|
@@ -234,7 +270,6 @@ export const dragAndDropFeature: FeatureImplementation = {
|
|
|
234
270
|
const draggedItems = tree.getState().dnd?.draggedItems;
|
|
235
271
|
|
|
236
272
|
dataRef.current.lastDragCode = undefined;
|
|
237
|
-
tree.applySubStateUpdate("dnd", null);
|
|
238
273
|
|
|
239
274
|
if (draggedItems) {
|
|
240
275
|
await config.onDrop?.(draggedItems, target);
|
|
@@ -3,6 +3,8 @@ import { ItemInstance, SetStateFn } from "../../types/core";
|
|
|
3
3
|
export interface DndDataRef {
|
|
4
4
|
lastDragCode?: string;
|
|
5
5
|
lastAllowDrop?: boolean;
|
|
6
|
+
lastDragEnter?: number;
|
|
7
|
+
windowDragEndListener?: () => void;
|
|
6
8
|
}
|
|
7
9
|
|
|
8
10
|
export interface DndState<T> {
|
|
@@ -57,16 +59,31 @@ export type DragAndDropFeatureDef<T> = {
|
|
|
57
59
|
createForeignDragObject?: (items: ItemInstance<T>[]) => {
|
|
58
60
|
format: string;
|
|
59
61
|
data: any;
|
|
62
|
+
dropEffect?: DataTransfer["dropEffect"];
|
|
63
|
+
effectAllowed?: DataTransfer["effectAllowed"];
|
|
60
64
|
};
|
|
61
65
|
setDragImage?: (items: ItemInstance<T>[]) => {
|
|
62
66
|
imgElement: Element;
|
|
63
67
|
xOffset?: number;
|
|
64
68
|
yOffset?: number;
|
|
65
69
|
};
|
|
70
|
+
|
|
71
|
+
/** Checks if a foreign drag object can be dropped on a target, validating that an actual drop can commence based on
|
|
72
|
+
* the data in the DataTransfer object. */
|
|
66
73
|
canDropForeignDragObject?: (
|
|
67
74
|
dataTransfer: DataTransfer,
|
|
68
75
|
target: DragTarget<T>,
|
|
69
76
|
) => boolean;
|
|
77
|
+
|
|
78
|
+
/** Checks if a droppable visualization should be displayed when dragging a foreign object over a target. Since this
|
|
79
|
+
* is executed on a dragover event, `dataTransfer.getData()` is not available, so `dataTransfer.effectAllowed` or
|
|
80
|
+
* `dataTransfer.types` should be used instead. Before actually completing the drag, @{link canDropForeignDragObject}
|
|
81
|
+
* will be called by HT before applying the drop. */
|
|
82
|
+
canDragForeignDragObjectOver?: (
|
|
83
|
+
dataTransfer: DataTransfer,
|
|
84
|
+
target: DragTarget<T>,
|
|
85
|
+
) => boolean;
|
|
86
|
+
|
|
70
87
|
onDrop?: (
|
|
71
88
|
items: ItemInstance<T>[],
|
|
72
89
|
target: DragTarget<T>,
|
|
@@ -191,7 +191,14 @@ export const keyboardDragAndDropFeature: FeatureImplementation = {
|
|
|
191
191
|
preventDefault: true,
|
|
192
192
|
isEnabled: (tree) => !tree.getState().dnd,
|
|
193
193
|
handler: (_, tree) => {
|
|
194
|
-
tree.
|
|
194
|
+
const selectedItems = tree.getSelectedItems();
|
|
195
|
+
const focusedItem = tree.getFocusedItem();
|
|
196
|
+
|
|
197
|
+
tree.startKeyboardDrag(
|
|
198
|
+
selectedItems.includes(focusedItem)
|
|
199
|
+
? selectedItems
|
|
200
|
+
: selectedItems.concat(focusedItem),
|
|
201
|
+
);
|
|
195
202
|
},
|
|
196
203
|
},
|
|
197
204
|
dragUp: {
|
|
@@ -49,6 +49,37 @@ describe("core-feature/keyboard-drag-and-drop", () => {
|
|
|
49
49
|
tree.expect.substate("assistiveDndState", AssistiveDndState.Started);
|
|
50
50
|
});
|
|
51
51
|
|
|
52
|
+
it("starts dragging only focused item", () => {
|
|
53
|
+
tree.item("x3").setFocused();
|
|
54
|
+
tree.do.hotkey("startDrag");
|
|
55
|
+
tree.expect.substate("dnd", {
|
|
56
|
+
draggedItems: [tree.item("x3")],
|
|
57
|
+
dragTarget: {
|
|
58
|
+
childIndex: 3,
|
|
59
|
+
dragLineIndex: 19,
|
|
60
|
+
dragLineLevel: 0,
|
|
61
|
+
insertionIndex: 2,
|
|
62
|
+
item: tree.item("x"),
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("starts dragging both selected and focused item", () => {
|
|
68
|
+
tree.do.selectMultiple("x111", "x112");
|
|
69
|
+
tree.item("x3").setFocused();
|
|
70
|
+
tree.do.hotkey("startDrag");
|
|
71
|
+
tree.expect.substate("dnd", {
|
|
72
|
+
draggedItems: [tree.item("x111"), tree.item("x112"), tree.item("x3")],
|
|
73
|
+
dragTarget: {
|
|
74
|
+
childIndex: 3,
|
|
75
|
+
dragLineIndex: 19,
|
|
76
|
+
dragLineLevel: 0,
|
|
77
|
+
insertionIndex: 2,
|
|
78
|
+
item: tree.item("x"),
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
52
83
|
it("moves down 1", () => {
|
|
53
84
|
tree.do.selectMultiple("x111", "x112");
|
|
54
85
|
tree.do.hotkey("startDrag");
|
|
@@ -355,13 +386,13 @@ describe("core-feature/keyboard-drag-and-drop", () => {
|
|
|
355
386
|
|
|
356
387
|
it("doesnt go below end of tree", () => {
|
|
357
388
|
const lastState = {
|
|
358
|
-
draggedItems: [tree.item("x111")],
|
|
389
|
+
draggedItems: [tree.item("x111"), tree.item("x3")],
|
|
359
390
|
dragTarget: {
|
|
360
391
|
item: tree.item("x"),
|
|
361
392
|
childIndex: 4,
|
|
362
393
|
dragLineIndex: 20,
|
|
363
394
|
dragLineLevel: 0,
|
|
364
|
-
insertionIndex:
|
|
395
|
+
insertionIndex: 3,
|
|
365
396
|
},
|
|
366
397
|
};
|
|
367
398
|
|
|
@@ -378,7 +409,7 @@ describe("core-feature/keyboard-drag-and-drop", () => {
|
|
|
378
409
|
|
|
379
410
|
it("doesnt go above top of tree", () => {
|
|
380
411
|
const firstState = {
|
|
381
|
-
draggedItems: [tree.item("x111")],
|
|
412
|
+
draggedItems: [tree.item("x111"), tree.item("x1")],
|
|
382
413
|
dragTarget: {
|
|
383
414
|
item: tree.item("x"),
|
|
384
415
|
childIndex: 0,
|
|
@@ -36,8 +36,6 @@ export type MainFeatureDef<T = any> = {
|
|
|
36
36
|
stateName: K,
|
|
37
37
|
updater: Updater<TreeState<T>[K]>,
|
|
38
38
|
) => void;
|
|
39
|
-
/** @internal */
|
|
40
|
-
buildItemInstance: (itemId: string) => ItemInstance<T>;
|
|
41
39
|
setState: SetStateFn<TreeState<T>>;
|
|
42
40
|
getState: () => TreeState<T>;
|
|
43
41
|
setConfig: SetStateFn<TreeConfig<T>>;
|
|
@@ -2,9 +2,13 @@ import { FeatureImplementation } from "../../types/core";
|
|
|
2
2
|
import { makeStateUpdater } from "../../utils";
|
|
3
3
|
import { throwError } from "../../utilities/errors";
|
|
4
4
|
|
|
5
|
+
const undefErrorMessage = "sync dataLoader returned undefined";
|
|
5
6
|
const promiseErrorMessage = "sync dataLoader returned promise";
|
|
6
7
|
const unpromise = <T>(data: T | Promise<T>): T => {
|
|
7
|
-
if (!data
|
|
8
|
+
if (!data) {
|
|
9
|
+
throw throwError(undefErrorMessage);
|
|
10
|
+
}
|
|
11
|
+
if (typeof data === "object" && "then" in data) {
|
|
8
12
|
throw throwError(promiseErrorMessage);
|
|
9
13
|
}
|
|
10
14
|
return data;
|
|
@@ -76,8 +76,9 @@ export const treeFeature: FeatureImplementation<any> = {
|
|
|
76
76
|
},
|
|
77
77
|
|
|
78
78
|
getFocusedItem: ({ tree }) => {
|
|
79
|
+
const focusedItemId = tree.getState().focusedItem;
|
|
79
80
|
return (
|
|
80
|
-
tree.getItemInstance(
|
|
81
|
+
(focusedItemId !== null ? tree.getItemInstance(focusedItemId) : null) ??
|
|
81
82
|
tree.getItems()[0]
|
|
82
83
|
);
|
|
83
84
|
},
|
|
@@ -139,10 +140,10 @@ export const treeFeature: FeatureImplementation<any> = {
|
|
|
139
140
|
ref: item.registerElement,
|
|
140
141
|
role: "treeitem",
|
|
141
142
|
"aria-setsize": itemMeta.setSize,
|
|
142
|
-
"aria-posinset": itemMeta.posInSet,
|
|
143
|
+
"aria-posinset": itemMeta.posInSet + 1,
|
|
143
144
|
"aria-selected": "false",
|
|
144
145
|
"aria-label": item.getItemName(),
|
|
145
|
-
"aria-level": itemMeta.level,
|
|
146
|
+
"aria-level": itemMeta.level + 1,
|
|
146
147
|
tabIndex: item.isFocused() ? 0 : -1,
|
|
147
148
|
onClick: (e: MouseEvent) => {
|
|
148
149
|
item.setFocused();
|