@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.
Files changed (190) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/dist/index.d.mts +580 -0
  3. package/dist/index.d.ts +580 -0
  4. package/dist/index.js +2347 -0
  5. package/dist/index.mjs +2302 -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 +107 -24
  15. package/src/features/drag-and-drop/types.ts +21 -0
  16. package/src/features/drag-and-drop/utils.ts +8 -6
  17. package/src/features/keyboard-drag-and-drop/feature.ts +10 -1
  18. package/src/features/keyboard-drag-and-drop/keyboard-drag-and-drop.spec.ts +34 -3
  19. package/src/features/main/types.ts +0 -2
  20. package/src/features/sync-data-loader/feature.ts +5 -1
  21. package/src/features/tree/feature.ts +4 -3
  22. package/src/features/tree/tree.spec.ts +14 -4
  23. package/src/test-utils/test-tree-do.ts +2 -0
  24. package/src/test-utils/test-tree.ts +1 -0
  25. package/tsconfig.json +1 -4
  26. package/vitest.config.ts +3 -1
  27. package/lib/cjs/core/build-proxified-instance.d.ts +0 -2
  28. package/lib/cjs/core/build-proxified-instance.js +0 -58
  29. package/lib/cjs/core/build-static-instance.d.ts +0 -2
  30. package/lib/cjs/core/build-static-instance.js +0 -26
  31. package/lib/cjs/core/create-tree.d.ts +0 -2
  32. package/lib/cjs/core/create-tree.js +0 -191
  33. package/lib/cjs/features/async-data-loader/feature.d.ts +0 -2
  34. package/lib/cjs/features/async-data-loader/feature.js +0 -135
  35. package/lib/cjs/features/async-data-loader/types.d.ts +0 -47
  36. package/lib/cjs/features/async-data-loader/types.js +0 -2
  37. package/lib/cjs/features/checkboxes/feature.d.ts +0 -2
  38. package/lib/cjs/features/checkboxes/feature.js +0 -94
  39. package/lib/cjs/features/checkboxes/types.d.ts +0 -26
  40. package/lib/cjs/features/checkboxes/types.js +0 -9
  41. package/lib/cjs/features/drag-and-drop/feature.d.ts +0 -2
  42. package/lib/cjs/features/drag-and-drop/feature.js +0 -205
  43. package/lib/cjs/features/drag-and-drop/types.d.ts +0 -71
  44. package/lib/cjs/features/drag-and-drop/types.js +0 -9
  45. package/lib/cjs/features/drag-and-drop/utils.d.ts +0 -27
  46. package/lib/cjs/features/drag-and-drop/utils.js +0 -182
  47. package/lib/cjs/features/expand-all/feature.d.ts +0 -2
  48. package/lib/cjs/features/expand-all/feature.js +0 -70
  49. package/lib/cjs/features/expand-all/types.d.ts +0 -19
  50. package/lib/cjs/features/expand-all/types.js +0 -2
  51. package/lib/cjs/features/hotkeys-core/feature.d.ts +0 -2
  52. package/lib/cjs/features/hotkeys-core/feature.js +0 -107
  53. package/lib/cjs/features/hotkeys-core/types.d.ts +0 -27
  54. package/lib/cjs/features/hotkeys-core/types.js +0 -2
  55. package/lib/cjs/features/keyboard-drag-and-drop/feature.d.ts +0 -2
  56. package/lib/cjs/features/keyboard-drag-and-drop/feature.js +0 -206
  57. package/lib/cjs/features/keyboard-drag-and-drop/types.d.ts +0 -27
  58. package/lib/cjs/features/keyboard-drag-and-drop/types.js +0 -11
  59. package/lib/cjs/features/main/types.d.ts +0 -47
  60. package/lib/cjs/features/main/types.js +0 -2
  61. package/lib/cjs/features/prop-memoization/feature.d.ts +0 -2
  62. package/lib/cjs/features/prop-memoization/feature.js +0 -70
  63. package/lib/cjs/features/prop-memoization/types.d.ts +0 -15
  64. package/lib/cjs/features/prop-memoization/types.js +0 -2
  65. package/lib/cjs/features/renaming/feature.d.ts +0 -2
  66. package/lib/cjs/features/renaming/feature.js +0 -86
  67. package/lib/cjs/features/renaming/types.d.ts +0 -27
  68. package/lib/cjs/features/renaming/types.js +0 -2
  69. package/lib/cjs/features/search/feature.d.ts +0 -2
  70. package/lib/cjs/features/search/feature.js +0 -119
  71. package/lib/cjs/features/search/types.d.ts +0 -32
  72. package/lib/cjs/features/search/types.js +0 -2
  73. package/lib/cjs/features/selection/feature.d.ts +0 -2
  74. package/lib/cjs/features/selection/feature.js +0 -132
  75. package/lib/cjs/features/selection/types.d.ts +0 -21
  76. package/lib/cjs/features/selection/types.js +0 -2
  77. package/lib/cjs/features/sync-data-loader/feature.d.ts +0 -2
  78. package/lib/cjs/features/sync-data-loader/feature.js +0 -49
  79. package/lib/cjs/features/sync-data-loader/types.d.ts +0 -28
  80. package/lib/cjs/features/sync-data-loader/types.js +0 -2
  81. package/lib/cjs/features/tree/feature.d.ts +0 -2
  82. package/lib/cjs/features/tree/feature.js +0 -244
  83. package/lib/cjs/features/tree/types.d.ts +0 -63
  84. package/lib/cjs/features/tree/types.js +0 -2
  85. package/lib/cjs/index.d.ts +0 -33
  86. package/lib/cjs/index.js +0 -51
  87. package/lib/cjs/mddocs-entry.d.ts +0 -121
  88. package/lib/cjs/mddocs-entry.js +0 -17
  89. package/lib/cjs/test-utils/test-tree-do.d.ts +0 -23
  90. package/lib/cjs/test-utils/test-tree-do.js +0 -112
  91. package/lib/cjs/test-utils/test-tree-expect.d.ts +0 -17
  92. package/lib/cjs/test-utils/test-tree-expect.js +0 -66
  93. package/lib/cjs/test-utils/test-tree.d.ts +0 -48
  94. package/lib/cjs/test-utils/test-tree.js +0 -207
  95. package/lib/cjs/types/core.d.ts +0 -84
  96. package/lib/cjs/types/core.js +0 -2
  97. package/lib/cjs/types/deep-merge.d.ts +0 -13
  98. package/lib/cjs/types/deep-merge.js +0 -2
  99. package/lib/cjs/utilities/create-on-drop-handler.d.ts +0 -3
  100. package/lib/cjs/utilities/create-on-drop-handler.js +0 -20
  101. package/lib/cjs/utilities/errors.d.ts +0 -2
  102. package/lib/cjs/utilities/errors.js +0 -9
  103. package/lib/cjs/utilities/insert-items-at-target.d.ts +0 -3
  104. package/lib/cjs/utilities/insert-items-at-target.js +0 -40
  105. package/lib/cjs/utilities/remove-items-from-parents.d.ts +0 -2
  106. package/lib/cjs/utilities/remove-items-from-parents.js +0 -32
  107. package/lib/cjs/utils.d.ts +0 -6
  108. package/lib/cjs/utils.js +0 -53
  109. package/lib/esm/core/build-proxified-instance.d.ts +0 -2
  110. package/lib/esm/core/build-proxified-instance.js +0 -54
  111. package/lib/esm/core/build-static-instance.d.ts +0 -2
  112. package/lib/esm/core/build-static-instance.js +0 -22
  113. package/lib/esm/core/create-tree.d.ts +0 -2
  114. package/lib/esm/core/create-tree.js +0 -187
  115. package/lib/esm/features/async-data-loader/feature.d.ts +0 -2
  116. package/lib/esm/features/async-data-loader/feature.js +0 -132
  117. package/lib/esm/features/async-data-loader/types.d.ts +0 -47
  118. package/lib/esm/features/async-data-loader/types.js +0 -1
  119. package/lib/esm/features/checkboxes/feature.d.ts +0 -2
  120. package/lib/esm/features/checkboxes/feature.js +0 -91
  121. package/lib/esm/features/checkboxes/types.d.ts +0 -26
  122. package/lib/esm/features/checkboxes/types.js +0 -6
  123. package/lib/esm/features/drag-and-drop/feature.d.ts +0 -2
  124. package/lib/esm/features/drag-and-drop/feature.js +0 -202
  125. package/lib/esm/features/drag-and-drop/types.d.ts +0 -71
  126. package/lib/esm/features/drag-and-drop/types.js +0 -6
  127. package/lib/esm/features/drag-and-drop/utils.d.ts +0 -27
  128. package/lib/esm/features/drag-and-drop/utils.js +0 -172
  129. package/lib/esm/features/expand-all/feature.d.ts +0 -2
  130. package/lib/esm/features/expand-all/feature.js +0 -67
  131. package/lib/esm/features/expand-all/types.d.ts +0 -19
  132. package/lib/esm/features/expand-all/types.js +0 -1
  133. package/lib/esm/features/hotkeys-core/feature.d.ts +0 -2
  134. package/lib/esm/features/hotkeys-core/feature.js +0 -104
  135. package/lib/esm/features/hotkeys-core/types.d.ts +0 -27
  136. package/lib/esm/features/hotkeys-core/types.js +0 -1
  137. package/lib/esm/features/keyboard-drag-and-drop/feature.d.ts +0 -2
  138. package/lib/esm/features/keyboard-drag-and-drop/feature.js +0 -203
  139. package/lib/esm/features/keyboard-drag-and-drop/types.d.ts +0 -27
  140. package/lib/esm/features/keyboard-drag-and-drop/types.js +0 -8
  141. package/lib/esm/features/main/types.d.ts +0 -47
  142. package/lib/esm/features/main/types.js +0 -1
  143. package/lib/esm/features/prop-memoization/feature.d.ts +0 -2
  144. package/lib/esm/features/prop-memoization/feature.js +0 -67
  145. package/lib/esm/features/prop-memoization/types.d.ts +0 -15
  146. package/lib/esm/features/prop-memoization/types.js +0 -1
  147. package/lib/esm/features/renaming/feature.d.ts +0 -2
  148. package/lib/esm/features/renaming/feature.js +0 -83
  149. package/lib/esm/features/renaming/types.d.ts +0 -27
  150. package/lib/esm/features/renaming/types.js +0 -1
  151. package/lib/esm/features/search/feature.d.ts +0 -2
  152. package/lib/esm/features/search/feature.js +0 -116
  153. package/lib/esm/features/search/types.d.ts +0 -32
  154. package/lib/esm/features/search/types.js +0 -1
  155. package/lib/esm/features/selection/feature.d.ts +0 -2
  156. package/lib/esm/features/selection/feature.js +0 -129
  157. package/lib/esm/features/selection/types.d.ts +0 -21
  158. package/lib/esm/features/selection/types.js +0 -1
  159. package/lib/esm/features/sync-data-loader/feature.d.ts +0 -2
  160. package/lib/esm/features/sync-data-loader/feature.js +0 -46
  161. package/lib/esm/features/sync-data-loader/types.d.ts +0 -28
  162. package/lib/esm/features/sync-data-loader/types.js +0 -1
  163. package/lib/esm/features/tree/feature.d.ts +0 -2
  164. package/lib/esm/features/tree/feature.js +0 -241
  165. package/lib/esm/features/tree/types.d.ts +0 -63
  166. package/lib/esm/features/tree/types.js +0 -1
  167. package/lib/esm/index.d.ts +0 -33
  168. package/lib/esm/index.js +0 -32
  169. package/lib/esm/mddocs-entry.d.ts +0 -121
  170. package/lib/esm/mddocs-entry.js +0 -1
  171. package/lib/esm/test-utils/test-tree-do.d.ts +0 -23
  172. package/lib/esm/test-utils/test-tree-do.js +0 -108
  173. package/lib/esm/test-utils/test-tree-expect.d.ts +0 -17
  174. package/lib/esm/test-utils/test-tree-expect.js +0 -62
  175. package/lib/esm/test-utils/test-tree.d.ts +0 -48
  176. package/lib/esm/test-utils/test-tree.js +0 -203
  177. package/lib/esm/types/core.d.ts +0 -84
  178. package/lib/esm/types/core.js +0 -1
  179. package/lib/esm/types/deep-merge.d.ts +0 -13
  180. package/lib/esm/types/deep-merge.js +0 -1
  181. package/lib/esm/utilities/create-on-drop-handler.d.ts +0 -3
  182. package/lib/esm/utilities/create-on-drop-handler.js +0 -16
  183. package/lib/esm/utilities/errors.d.ts +0 -2
  184. package/lib/esm/utilities/errors.js +0 -4
  185. package/lib/esm/utilities/insert-items-at-target.d.ts +0 -3
  186. package/lib/esm/utilities/insert-items-at-target.js +0 -36
  187. package/lib/esm/utilities/remove-items-from-parents.d.ts +0 -2
  188. package/lib/esm/utilities/remove-items-from-parents.js +0 -28
  189. package/lib/esm/utils.d.ts +0 -6
  190. 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.4.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
  );
@@ -1,22 +1,62 @@
1
- import { FeatureImplementation } from "../../types/core";
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: () => false,
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.getElement()?.getBoundingClientRect();
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 } = config.createForeignDragObject(items);
155
- e.dataTransfer?.setData(format, data);
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 nextDragCode = getDragCode(e, item, tree);
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
- .canDropForeignDragObject?.(e.dataTransfer, target))
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
- 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
- }));
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.getConfig().onCompleteForeignDrop?.(draggedItems);
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
- if (!canDrop(e.dataTransfer, target, tree)) {
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;