@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.
Files changed (189) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/dist/index.d.mts +577 -0
  3. package/dist/index.d.ts +577 -0
  4. package/dist/index.js +2321 -0
  5. package/dist/index.mjs +2276 -0
  6. package/package.json +18 -10
  7. package/src/core/create-tree.ts +26 -15
  8. package/src/features/async-data-loader/feature.ts +5 -0
  9. package/src/features/async-data-loader/types.ts +2 -0
  10. package/src/features/checkboxes/checkboxes.spec.ts +20 -5
  11. package/src/features/checkboxes/feature.ts +31 -16
  12. package/src/features/checkboxes/types.ts +1 -0
  13. package/src/features/drag-and-drop/drag-and-drop.spec.ts +11 -2
  14. package/src/features/drag-and-drop/feature.ts +51 -16
  15. package/src/features/drag-and-drop/types.ts +17 -0
  16. package/src/features/keyboard-drag-and-drop/feature.ts +8 -1
  17. package/src/features/keyboard-drag-and-drop/keyboard-drag-and-drop.spec.ts +34 -3
  18. package/src/features/main/types.ts +0 -2
  19. package/src/features/sync-data-loader/feature.ts +5 -1
  20. package/src/features/tree/feature.ts +4 -3
  21. package/src/features/tree/tree.spec.ts +14 -4
  22. package/src/test-utils/test-tree-do.ts +2 -0
  23. package/src/test-utils/test-tree.ts +1 -0
  24. package/tsconfig.json +1 -4
  25. package/vitest.config.ts +3 -1
  26. package/lib/cjs/core/build-proxified-instance.d.ts +0 -2
  27. package/lib/cjs/core/build-proxified-instance.js +0 -58
  28. package/lib/cjs/core/build-static-instance.d.ts +0 -2
  29. package/lib/cjs/core/build-static-instance.js +0 -26
  30. package/lib/cjs/core/create-tree.d.ts +0 -2
  31. package/lib/cjs/core/create-tree.js +0 -191
  32. package/lib/cjs/features/async-data-loader/feature.d.ts +0 -2
  33. package/lib/cjs/features/async-data-loader/feature.js +0 -135
  34. package/lib/cjs/features/async-data-loader/types.d.ts +0 -47
  35. package/lib/cjs/features/async-data-loader/types.js +0 -2
  36. package/lib/cjs/features/checkboxes/feature.d.ts +0 -2
  37. package/lib/cjs/features/checkboxes/feature.js +0 -94
  38. package/lib/cjs/features/checkboxes/types.d.ts +0 -26
  39. package/lib/cjs/features/checkboxes/types.js +0 -9
  40. package/lib/cjs/features/drag-and-drop/feature.d.ts +0 -2
  41. package/lib/cjs/features/drag-and-drop/feature.js +0 -205
  42. package/lib/cjs/features/drag-and-drop/types.d.ts +0 -71
  43. package/lib/cjs/features/drag-and-drop/types.js +0 -9
  44. package/lib/cjs/features/drag-and-drop/utils.d.ts +0 -27
  45. package/lib/cjs/features/drag-and-drop/utils.js +0 -182
  46. package/lib/cjs/features/expand-all/feature.d.ts +0 -2
  47. package/lib/cjs/features/expand-all/feature.js +0 -70
  48. package/lib/cjs/features/expand-all/types.d.ts +0 -19
  49. package/lib/cjs/features/expand-all/types.js +0 -2
  50. package/lib/cjs/features/hotkeys-core/feature.d.ts +0 -2
  51. package/lib/cjs/features/hotkeys-core/feature.js +0 -107
  52. package/lib/cjs/features/hotkeys-core/types.d.ts +0 -27
  53. package/lib/cjs/features/hotkeys-core/types.js +0 -2
  54. package/lib/cjs/features/keyboard-drag-and-drop/feature.d.ts +0 -2
  55. package/lib/cjs/features/keyboard-drag-and-drop/feature.js +0 -206
  56. package/lib/cjs/features/keyboard-drag-and-drop/types.d.ts +0 -27
  57. package/lib/cjs/features/keyboard-drag-and-drop/types.js +0 -11
  58. package/lib/cjs/features/main/types.d.ts +0 -47
  59. package/lib/cjs/features/main/types.js +0 -2
  60. package/lib/cjs/features/prop-memoization/feature.d.ts +0 -2
  61. package/lib/cjs/features/prop-memoization/feature.js +0 -70
  62. package/lib/cjs/features/prop-memoization/types.d.ts +0 -15
  63. package/lib/cjs/features/prop-memoization/types.js +0 -2
  64. package/lib/cjs/features/renaming/feature.d.ts +0 -2
  65. package/lib/cjs/features/renaming/feature.js +0 -86
  66. package/lib/cjs/features/renaming/types.d.ts +0 -27
  67. package/lib/cjs/features/renaming/types.js +0 -2
  68. package/lib/cjs/features/search/feature.d.ts +0 -2
  69. package/lib/cjs/features/search/feature.js +0 -119
  70. package/lib/cjs/features/search/types.d.ts +0 -32
  71. package/lib/cjs/features/search/types.js +0 -2
  72. package/lib/cjs/features/selection/feature.d.ts +0 -2
  73. package/lib/cjs/features/selection/feature.js +0 -132
  74. package/lib/cjs/features/selection/types.d.ts +0 -21
  75. package/lib/cjs/features/selection/types.js +0 -2
  76. package/lib/cjs/features/sync-data-loader/feature.d.ts +0 -2
  77. package/lib/cjs/features/sync-data-loader/feature.js +0 -49
  78. package/lib/cjs/features/sync-data-loader/types.d.ts +0 -28
  79. package/lib/cjs/features/sync-data-loader/types.js +0 -2
  80. package/lib/cjs/features/tree/feature.d.ts +0 -2
  81. package/lib/cjs/features/tree/feature.js +0 -244
  82. package/lib/cjs/features/tree/types.d.ts +0 -63
  83. package/lib/cjs/features/tree/types.js +0 -2
  84. package/lib/cjs/index.d.ts +0 -33
  85. package/lib/cjs/index.js +0 -51
  86. package/lib/cjs/mddocs-entry.d.ts +0 -121
  87. package/lib/cjs/mddocs-entry.js +0 -17
  88. package/lib/cjs/test-utils/test-tree-do.d.ts +0 -23
  89. package/lib/cjs/test-utils/test-tree-do.js +0 -112
  90. package/lib/cjs/test-utils/test-tree-expect.d.ts +0 -17
  91. package/lib/cjs/test-utils/test-tree-expect.js +0 -66
  92. package/lib/cjs/test-utils/test-tree.d.ts +0 -48
  93. package/lib/cjs/test-utils/test-tree.js +0 -207
  94. package/lib/cjs/types/core.d.ts +0 -84
  95. package/lib/cjs/types/core.js +0 -2
  96. package/lib/cjs/types/deep-merge.d.ts +0 -13
  97. package/lib/cjs/types/deep-merge.js +0 -2
  98. package/lib/cjs/utilities/create-on-drop-handler.d.ts +0 -3
  99. package/lib/cjs/utilities/create-on-drop-handler.js +0 -20
  100. package/lib/cjs/utilities/errors.d.ts +0 -2
  101. package/lib/cjs/utilities/errors.js +0 -9
  102. package/lib/cjs/utilities/insert-items-at-target.d.ts +0 -3
  103. package/lib/cjs/utilities/insert-items-at-target.js +0 -40
  104. package/lib/cjs/utilities/remove-items-from-parents.d.ts +0 -2
  105. package/lib/cjs/utilities/remove-items-from-parents.js +0 -32
  106. package/lib/cjs/utils.d.ts +0 -6
  107. package/lib/cjs/utils.js +0 -53
  108. package/lib/esm/core/build-proxified-instance.d.ts +0 -2
  109. package/lib/esm/core/build-proxified-instance.js +0 -54
  110. package/lib/esm/core/build-static-instance.d.ts +0 -2
  111. package/lib/esm/core/build-static-instance.js +0 -22
  112. package/lib/esm/core/create-tree.d.ts +0 -2
  113. package/lib/esm/core/create-tree.js +0 -187
  114. package/lib/esm/features/async-data-loader/feature.d.ts +0 -2
  115. package/lib/esm/features/async-data-loader/feature.js +0 -132
  116. package/lib/esm/features/async-data-loader/types.d.ts +0 -47
  117. package/lib/esm/features/async-data-loader/types.js +0 -1
  118. package/lib/esm/features/checkboxes/feature.d.ts +0 -2
  119. package/lib/esm/features/checkboxes/feature.js +0 -91
  120. package/lib/esm/features/checkboxes/types.d.ts +0 -26
  121. package/lib/esm/features/checkboxes/types.js +0 -6
  122. package/lib/esm/features/drag-and-drop/feature.d.ts +0 -2
  123. package/lib/esm/features/drag-and-drop/feature.js +0 -202
  124. package/lib/esm/features/drag-and-drop/types.d.ts +0 -71
  125. package/lib/esm/features/drag-and-drop/types.js +0 -6
  126. package/lib/esm/features/drag-and-drop/utils.d.ts +0 -27
  127. package/lib/esm/features/drag-and-drop/utils.js +0 -172
  128. package/lib/esm/features/expand-all/feature.d.ts +0 -2
  129. package/lib/esm/features/expand-all/feature.js +0 -67
  130. package/lib/esm/features/expand-all/types.d.ts +0 -19
  131. package/lib/esm/features/expand-all/types.js +0 -1
  132. package/lib/esm/features/hotkeys-core/feature.d.ts +0 -2
  133. package/lib/esm/features/hotkeys-core/feature.js +0 -104
  134. package/lib/esm/features/hotkeys-core/types.d.ts +0 -27
  135. package/lib/esm/features/hotkeys-core/types.js +0 -1
  136. package/lib/esm/features/keyboard-drag-and-drop/feature.d.ts +0 -2
  137. package/lib/esm/features/keyboard-drag-and-drop/feature.js +0 -203
  138. package/lib/esm/features/keyboard-drag-and-drop/types.d.ts +0 -27
  139. package/lib/esm/features/keyboard-drag-and-drop/types.js +0 -8
  140. package/lib/esm/features/main/types.d.ts +0 -47
  141. package/lib/esm/features/main/types.js +0 -1
  142. package/lib/esm/features/prop-memoization/feature.d.ts +0 -2
  143. package/lib/esm/features/prop-memoization/feature.js +0 -67
  144. package/lib/esm/features/prop-memoization/types.d.ts +0 -15
  145. package/lib/esm/features/prop-memoization/types.js +0 -1
  146. package/lib/esm/features/renaming/feature.d.ts +0 -2
  147. package/lib/esm/features/renaming/feature.js +0 -83
  148. package/lib/esm/features/renaming/types.d.ts +0 -27
  149. package/lib/esm/features/renaming/types.js +0 -1
  150. package/lib/esm/features/search/feature.d.ts +0 -2
  151. package/lib/esm/features/search/feature.js +0 -116
  152. package/lib/esm/features/search/types.d.ts +0 -32
  153. package/lib/esm/features/search/types.js +0 -1
  154. package/lib/esm/features/selection/feature.d.ts +0 -2
  155. package/lib/esm/features/selection/feature.js +0 -129
  156. package/lib/esm/features/selection/types.d.ts +0 -21
  157. package/lib/esm/features/selection/types.js +0 -1
  158. package/lib/esm/features/sync-data-loader/feature.d.ts +0 -2
  159. package/lib/esm/features/sync-data-loader/feature.js +0 -46
  160. package/lib/esm/features/sync-data-loader/types.d.ts +0 -28
  161. package/lib/esm/features/sync-data-loader/types.js +0 -1
  162. package/lib/esm/features/tree/feature.d.ts +0 -2
  163. package/lib/esm/features/tree/feature.js +0 -241
  164. package/lib/esm/features/tree/types.d.ts +0 -63
  165. package/lib/esm/features/tree/types.js +0 -1
  166. package/lib/esm/index.d.ts +0 -33
  167. package/lib/esm/index.js +0 -32
  168. package/lib/esm/mddocs-entry.d.ts +0 -121
  169. package/lib/esm/mddocs-entry.js +0 -1
  170. package/lib/esm/test-utils/test-tree-do.d.ts +0 -23
  171. package/lib/esm/test-utils/test-tree-do.js +0 -108
  172. package/lib/esm/test-utils/test-tree-expect.d.ts +0 -17
  173. package/lib/esm/test-utils/test-tree-expect.js +0 -62
  174. package/lib/esm/test-utils/test-tree.d.ts +0 -48
  175. package/lib/esm/test-utils/test-tree.js +0 -203
  176. package/lib/esm/types/core.d.ts +0 -84
  177. package/lib/esm/types/core.js +0 -1
  178. package/lib/esm/types/deep-merge.d.ts +0 -13
  179. package/lib/esm/types/deep-merge.js +0 -1
  180. package/lib/esm/utilities/create-on-drop-handler.d.ts +0 -3
  181. package/lib/esm/utilities/create-on-drop-handler.js +0 -16
  182. package/lib/esm/utilities/errors.d.ts +0 -2
  183. package/lib/esm/utilities/errors.js +0 -4
  184. package/lib/esm/utilities/insert-items-at-target.d.ts +0 -3
  185. package/lib/esm/utilities/insert-items-at-target.js +0 -36
  186. package/lib/esm/utilities/remove-items-from-parents.d.ts +0 -2
  187. package/lib/esm/utilities/remove-items-from-parents.js +0 -28
  188. package/lib/esm/utils.d.ts +0 -6
  189. 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.2.1",
4
- "main": "lib/cjs/index.js",
5
- "module": "lib/esm/index.js",
6
- "types": "lib/esm/index.d.ts",
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
- "types": "./lib/esm/index.d.ts",
9
- "import": "./lib/esm/index.js",
10
- "default": "./lib/cjs/index.js"
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:cjs": "tsc -m commonjs --outDir lib/cjs",
15
- "build:esm": "tsc",
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
  }
@@ -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) => itemInstancesMap[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 }) => itemMetaMap[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 when canCheckFolders is true", async () => {
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 handle folder checking when canCheckFolders is false", async () => {
87
- const tree = await factory.createTestCaseTree();
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.createTestCaseTree();
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.buildItemInstance(itemId))) {
11
+ if (!tree.getConfig().isItemFolder(tree.getItemInstance(itemId))) {
11
12
  return [itemId];
12
13
  }
13
- return tree
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 && !defaultConfig.canCheckFolders) {
34
- throwError(`!canCheckFolders not supported with async trees`);
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
- canCheckFolders: hasAsyncLoader ?? false,
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, itemId }) => {
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() && !tree.getConfig().canCheckFolders) {
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
- if (!item.isFolder() || tree.getConfig().canCheckFolders) {
97
- tree.applySubStateUpdate("checkedItems", (items) => [...items, itemId]);
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
- if (!item.isFolder() || tree.getConfig().canCheckFolders) {
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) => !descendants.includes(id)),
129
+ items.filter((id) => id !== itemId),
115
130
  );
116
131
  }
117
132
  },
@@ -13,6 +13,7 @@ export type CheckboxesFeatureDef<T> = {
13
13
  config: {
14
14
  setCheckedItems?: SetStateFn<string[]>;
15
15
  canCheckFolders?: boolean;
16
+ propagateCheckedState?: boolean;
16
17
  };
17
18
  treeInstance: {
18
19
  setCheckedItems: (checkedItems: string[]) => void;
@@ -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: () => false,
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 } = config.createForeignDragObject(items);
155
- e.dataTransfer?.setData(format, data);
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
- .canDropForeignDragObject?.(e.dataTransfer, target))
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
- const dataRef = tree.getDataRef<DndDataRef>();
204
- dataRef.current.lastDragCode = "no-drag";
205
- tree.applySubStateUpdate("dnd", (state) => ({
206
- ...state,
207
- draggingOverItem: undefined,
208
- dragTarget: undefined,
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.getConfig().onCompleteForeignDrop?.(draggedItems);
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.startKeyboardDrag(tree.getSelectedItems());
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: 4,
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 || (typeof data === "object" && "then" in 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(tree.getState().focusedItem ?? "") ??
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();