@headless-tree/core 1.2.0 → 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 (183) hide show
  1. package/CHANGELOG.md +27 -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 -2
  8. package/src/features/async-data-loader/feature.ts +9 -4
  9. package/src/features/async-data-loader/types.ts +2 -0
  10. package/src/features/checkboxes/checkboxes.spec.ts +149 -0
  11. package/src/features/checkboxes/feature.ts +134 -0
  12. package/src/features/checkboxes/types.ts +29 -0
  13. package/src/features/drag-and-drop/drag-and-drop.spec.ts +11 -2
  14. package/src/features/drag-and-drop/feature.ts +88 -17
  15. package/src/features/drag-and-drop/types.ts +22 -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/sync-data-loader/feature.ts +5 -1
  19. package/src/features/tree/feature.ts +9 -3
  20. package/src/features/tree/tree.spec.ts +14 -4
  21. package/src/features/tree/types.ts +3 -2
  22. package/src/index.ts +2 -0
  23. package/src/test-utils/test-tree-do.ts +2 -0
  24. package/src/test-utils/test-tree.ts +1 -0
  25. package/src/types/core.ts +2 -0
  26. package/tsconfig.json +1 -4
  27. package/vitest.config.ts +3 -1
  28. package/lib/cjs/core/build-proxified-instance.d.ts +0 -2
  29. package/lib/cjs/core/build-proxified-instance.js +0 -58
  30. package/lib/cjs/core/build-static-instance.d.ts +0 -2
  31. package/lib/cjs/core/build-static-instance.js +0 -26
  32. package/lib/cjs/core/create-tree.d.ts +0 -2
  33. package/lib/cjs/core/create-tree.js +0 -182
  34. package/lib/cjs/features/async-data-loader/feature.d.ts +0 -2
  35. package/lib/cjs/features/async-data-loader/feature.js +0 -135
  36. package/lib/cjs/features/async-data-loader/types.d.ts +0 -47
  37. package/lib/cjs/features/async-data-loader/types.js +0 -2
  38. package/lib/cjs/features/drag-and-drop/feature.d.ts +0 -2
  39. package/lib/cjs/features/drag-and-drop/feature.js +0 -179
  40. package/lib/cjs/features/drag-and-drop/types.d.ts +0 -66
  41. package/lib/cjs/features/drag-and-drop/types.js +0 -9
  42. package/lib/cjs/features/drag-and-drop/utils.d.ts +0 -27
  43. package/lib/cjs/features/drag-and-drop/utils.js +0 -182
  44. package/lib/cjs/features/expand-all/feature.d.ts +0 -2
  45. package/lib/cjs/features/expand-all/feature.js +0 -70
  46. package/lib/cjs/features/expand-all/types.d.ts +0 -19
  47. package/lib/cjs/features/expand-all/types.js +0 -2
  48. package/lib/cjs/features/hotkeys-core/feature.d.ts +0 -2
  49. package/lib/cjs/features/hotkeys-core/feature.js +0 -107
  50. package/lib/cjs/features/hotkeys-core/types.d.ts +0 -27
  51. package/lib/cjs/features/hotkeys-core/types.js +0 -2
  52. package/lib/cjs/features/keyboard-drag-and-drop/feature.d.ts +0 -2
  53. package/lib/cjs/features/keyboard-drag-and-drop/feature.js +0 -206
  54. package/lib/cjs/features/keyboard-drag-and-drop/types.d.ts +0 -27
  55. package/lib/cjs/features/keyboard-drag-and-drop/types.js +0 -11
  56. package/lib/cjs/features/main/types.d.ts +0 -45
  57. package/lib/cjs/features/main/types.js +0 -2
  58. package/lib/cjs/features/prop-memoization/feature.d.ts +0 -2
  59. package/lib/cjs/features/prop-memoization/feature.js +0 -70
  60. package/lib/cjs/features/prop-memoization/types.d.ts +0 -15
  61. package/lib/cjs/features/prop-memoization/types.js +0 -2
  62. package/lib/cjs/features/renaming/feature.d.ts +0 -2
  63. package/lib/cjs/features/renaming/feature.js +0 -86
  64. package/lib/cjs/features/renaming/types.d.ts +0 -27
  65. package/lib/cjs/features/renaming/types.js +0 -2
  66. package/lib/cjs/features/search/feature.d.ts +0 -2
  67. package/lib/cjs/features/search/feature.js +0 -119
  68. package/lib/cjs/features/search/types.d.ts +0 -32
  69. package/lib/cjs/features/search/types.js +0 -2
  70. package/lib/cjs/features/selection/feature.d.ts +0 -2
  71. package/lib/cjs/features/selection/feature.js +0 -132
  72. package/lib/cjs/features/selection/types.d.ts +0 -21
  73. package/lib/cjs/features/selection/types.js +0 -2
  74. package/lib/cjs/features/sync-data-loader/feature.d.ts +0 -2
  75. package/lib/cjs/features/sync-data-loader/feature.js +0 -49
  76. package/lib/cjs/features/sync-data-loader/types.d.ts +0 -28
  77. package/lib/cjs/features/sync-data-loader/types.js +0 -2
  78. package/lib/cjs/features/tree/feature.d.ts +0 -2
  79. package/lib/cjs/features/tree/feature.js +0 -240
  80. package/lib/cjs/features/tree/types.d.ts +0 -62
  81. package/lib/cjs/features/tree/types.js +0 -2
  82. package/lib/cjs/index.d.ts +0 -31
  83. package/lib/cjs/index.js +0 -49
  84. package/lib/cjs/mddocs-entry.d.ts +0 -121
  85. package/lib/cjs/mddocs-entry.js +0 -17
  86. package/lib/cjs/test-utils/test-tree-do.d.ts +0 -23
  87. package/lib/cjs/test-utils/test-tree-do.js +0 -112
  88. package/lib/cjs/test-utils/test-tree-expect.d.ts +0 -17
  89. package/lib/cjs/test-utils/test-tree-expect.js +0 -66
  90. package/lib/cjs/test-utils/test-tree.d.ts +0 -48
  91. package/lib/cjs/test-utils/test-tree.js +0 -207
  92. package/lib/cjs/types/core.d.ts +0 -83
  93. package/lib/cjs/types/core.js +0 -2
  94. package/lib/cjs/types/deep-merge.d.ts +0 -13
  95. package/lib/cjs/types/deep-merge.js +0 -2
  96. package/lib/cjs/utilities/create-on-drop-handler.d.ts +0 -3
  97. package/lib/cjs/utilities/create-on-drop-handler.js +0 -20
  98. package/lib/cjs/utilities/errors.d.ts +0 -2
  99. package/lib/cjs/utilities/errors.js +0 -9
  100. package/lib/cjs/utilities/insert-items-at-target.d.ts +0 -3
  101. package/lib/cjs/utilities/insert-items-at-target.js +0 -40
  102. package/lib/cjs/utilities/remove-items-from-parents.d.ts +0 -2
  103. package/lib/cjs/utilities/remove-items-from-parents.js +0 -32
  104. package/lib/cjs/utils.d.ts +0 -6
  105. package/lib/cjs/utils.js +0 -53
  106. package/lib/esm/core/build-proxified-instance.d.ts +0 -2
  107. package/lib/esm/core/build-proxified-instance.js +0 -54
  108. package/lib/esm/core/build-static-instance.d.ts +0 -2
  109. package/lib/esm/core/build-static-instance.js +0 -22
  110. package/lib/esm/core/create-tree.d.ts +0 -2
  111. package/lib/esm/core/create-tree.js +0 -178
  112. package/lib/esm/features/async-data-loader/feature.d.ts +0 -2
  113. package/lib/esm/features/async-data-loader/feature.js +0 -132
  114. package/lib/esm/features/async-data-loader/types.d.ts +0 -47
  115. package/lib/esm/features/async-data-loader/types.js +0 -1
  116. package/lib/esm/features/drag-and-drop/feature.d.ts +0 -2
  117. package/lib/esm/features/drag-and-drop/feature.js +0 -176
  118. package/lib/esm/features/drag-and-drop/types.d.ts +0 -66
  119. package/lib/esm/features/drag-and-drop/types.js +0 -6
  120. package/lib/esm/features/drag-and-drop/utils.d.ts +0 -27
  121. package/lib/esm/features/drag-and-drop/utils.js +0 -172
  122. package/lib/esm/features/expand-all/feature.d.ts +0 -2
  123. package/lib/esm/features/expand-all/feature.js +0 -67
  124. package/lib/esm/features/expand-all/types.d.ts +0 -19
  125. package/lib/esm/features/expand-all/types.js +0 -1
  126. package/lib/esm/features/hotkeys-core/feature.d.ts +0 -2
  127. package/lib/esm/features/hotkeys-core/feature.js +0 -104
  128. package/lib/esm/features/hotkeys-core/types.d.ts +0 -27
  129. package/lib/esm/features/hotkeys-core/types.js +0 -1
  130. package/lib/esm/features/keyboard-drag-and-drop/feature.d.ts +0 -2
  131. package/lib/esm/features/keyboard-drag-and-drop/feature.js +0 -203
  132. package/lib/esm/features/keyboard-drag-and-drop/types.d.ts +0 -27
  133. package/lib/esm/features/keyboard-drag-and-drop/types.js +0 -8
  134. package/lib/esm/features/main/types.d.ts +0 -45
  135. package/lib/esm/features/main/types.js +0 -1
  136. package/lib/esm/features/prop-memoization/feature.d.ts +0 -2
  137. package/lib/esm/features/prop-memoization/feature.js +0 -67
  138. package/lib/esm/features/prop-memoization/types.d.ts +0 -15
  139. package/lib/esm/features/prop-memoization/types.js +0 -1
  140. package/lib/esm/features/renaming/feature.d.ts +0 -2
  141. package/lib/esm/features/renaming/feature.js +0 -83
  142. package/lib/esm/features/renaming/types.d.ts +0 -27
  143. package/lib/esm/features/renaming/types.js +0 -1
  144. package/lib/esm/features/search/feature.d.ts +0 -2
  145. package/lib/esm/features/search/feature.js +0 -116
  146. package/lib/esm/features/search/types.d.ts +0 -32
  147. package/lib/esm/features/search/types.js +0 -1
  148. package/lib/esm/features/selection/feature.d.ts +0 -2
  149. package/lib/esm/features/selection/feature.js +0 -129
  150. package/lib/esm/features/selection/types.d.ts +0 -21
  151. package/lib/esm/features/selection/types.js +0 -1
  152. package/lib/esm/features/sync-data-loader/feature.d.ts +0 -2
  153. package/lib/esm/features/sync-data-loader/feature.js +0 -46
  154. package/lib/esm/features/sync-data-loader/types.d.ts +0 -28
  155. package/lib/esm/features/sync-data-loader/types.js +0 -1
  156. package/lib/esm/features/tree/feature.d.ts +0 -2
  157. package/lib/esm/features/tree/feature.js +0 -237
  158. package/lib/esm/features/tree/types.d.ts +0 -62
  159. package/lib/esm/features/tree/types.js +0 -1
  160. package/lib/esm/index.d.ts +0 -31
  161. package/lib/esm/index.js +0 -30
  162. package/lib/esm/mddocs-entry.d.ts +0 -121
  163. package/lib/esm/mddocs-entry.js +0 -1
  164. package/lib/esm/test-utils/test-tree-do.d.ts +0 -23
  165. package/lib/esm/test-utils/test-tree-do.js +0 -108
  166. package/lib/esm/test-utils/test-tree-expect.d.ts +0 -17
  167. package/lib/esm/test-utils/test-tree-expect.js +0 -62
  168. package/lib/esm/test-utils/test-tree.d.ts +0 -48
  169. package/lib/esm/test-utils/test-tree.js +0 -203
  170. package/lib/esm/types/core.d.ts +0 -83
  171. package/lib/esm/types/core.js +0 -1
  172. package/lib/esm/types/deep-merge.d.ts +0 -13
  173. package/lib/esm/types/deep-merge.js +0 -1
  174. package/lib/esm/utilities/create-on-drop-handler.d.ts +0 -3
  175. package/lib/esm/utilities/create-on-drop-handler.js +0 -16
  176. package/lib/esm/utilities/errors.d.ts +0 -2
  177. package/lib/esm/utilities/errors.js +0 -4
  178. package/lib/esm/utilities/insert-items-at-target.d.ts +0 -3
  179. package/lib/esm/utilities/insert-items-at-target.js +0 -36
  180. package/lib/esm/utilities/remove-items-from-parents.d.ts +0 -2
  181. package/lib/esm/utilities/remove-items-from-parents.js +0 -28
  182. package/lib/esm/utils.d.ts +0 -6
  183. 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.0",
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
  }
@@ -193,7 +193,23 @@ export const createTree = <T>(
193
193
  config.setState?.(state);
194
194
  }
195
195
  },
196
- 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
+ },
197
213
  getItems: () => itemInstances,
198
214
  registerElement: ({}, element) => {
199
215
  if (treeElement === element) {
@@ -236,7 +252,15 @@ export const createTree = <T>(
236
252
  getElement: ({ itemId }) => itemElementsMap[itemId],
237
253
  // eslint-disable-next-line no-return-assign
238
254
  getDataRef: ({ itemId }) => (itemDataRefs[itemId] ??= { current: {} }),
239
- 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
+ },
240
264
  },
241
265
  };
242
266
 
@@ -95,7 +95,7 @@ export const asyncDataLoaderFeature: FeatureImplementation = {
95
95
  );
96
96
  },
97
97
 
98
- retrieveItemData: ({ tree }, itemId) => {
98
+ retrieveItemData: ({ tree }, itemId, skipFetch = false) => {
99
99
  const config = tree.getConfig();
100
100
  const dataRef = getDataRef(tree);
101
101
 
@@ -103,7 +103,7 @@ export const asyncDataLoaderFeature: FeatureImplementation = {
103
103
  return dataRef.current.itemData[itemId];
104
104
  }
105
105
 
106
- if (!tree.getState().loadingItemData.includes(itemId)) {
106
+ if (!tree.getState().loadingItemData.includes(itemId) && !skipFetch) {
107
107
  tree.applySubStateUpdate("loadingItemData", (loadingItemData) => [
108
108
  ...loadingItemData,
109
109
  itemId,
@@ -115,13 +115,13 @@ export const asyncDataLoaderFeature: FeatureImplementation = {
115
115
  return config.createLoadingItemData?.() ?? null;
116
116
  },
117
117
 
118
- retrieveChildrenIds: ({ tree }, itemId) => {
118
+ retrieveChildrenIds: ({ tree }, itemId, skipFetch = false) => {
119
119
  const dataRef = getDataRef(tree);
120
120
  if (dataRef.current.childrenIds[itemId]) {
121
121
  return dataRef.current.childrenIds[itemId];
122
122
  }
123
123
 
124
- if (tree.getState().loadingItemChildrens.includes(itemId)) {
124
+ if (tree.getState().loadingItemChildrens.includes(itemId) || skipFetch) {
125
125
  return [];
126
126
  }
127
127
 
@@ -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
  };
@@ -0,0 +1,149 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { TestTree } from "../../test-utils/test-tree";
3
+ import { checkboxesFeature } from "./feature";
4
+ import { CheckedState } from "./types";
5
+
6
+ const factory = TestTree.default({})
7
+ .withFeatures(checkboxesFeature)
8
+ .suits.sync().tree;
9
+
10
+ describe("core-feature/checkboxes", () => {
11
+ it("should initialize with no checked items", async () => {
12
+ const tree = await factory.createTestCaseTree();
13
+ expect(tree.instance.getState().checkedItems).toEqual([]);
14
+ });
15
+
16
+ it("should check items", async () => {
17
+ const tree = await factory.createTestCaseTree();
18
+ tree.item("x111").setChecked();
19
+ tree.item("x112").setChecked();
20
+ expect(tree.instance.getState().checkedItems).toEqual(["x111", "x112"]);
21
+ });
22
+
23
+ it("should uncheck an item", async () => {
24
+ const tree = await factory
25
+ .with({ state: { checkedItems: ["x111"] } })
26
+ .createTestCaseTree();
27
+ tree.item("x111").setUnchecked();
28
+ expect(tree.instance.getState().checkedItems).not.toContain("x111");
29
+ });
30
+
31
+ it("should toggle checked state", async () => {
32
+ const tree = await factory.createTestCaseTree();
33
+ const item = tree.item("x111");
34
+
35
+ item.toggleCheckedState();
36
+ expect(tree.instance.getState().checkedItems).toContain("x111");
37
+
38
+ item.toggleCheckedState();
39
+ expect(tree.instance.getState().checkedItems).not.toContain("x111");
40
+ });
41
+
42
+ describe("props", () => {
43
+ it("should toggle checked state", async () => {
44
+ const tree = await factory.createTestCaseTree();
45
+ const item = tree.item("x111");
46
+
47
+ item.getCheckboxProps().onChange();
48
+ expect(tree.instance.getState().checkedItems).toContain("x111");
49
+
50
+ item.getCheckboxProps().onChange();
51
+ expect(tree.instance.getState().checkedItems).not.toContain("x111");
52
+ });
53
+
54
+ it("should return checked state in props", async () => {
55
+ const tree = await factory.createTestCaseTree();
56
+ tree.item("x111").setChecked();
57
+ expect(tree.item("x111").getCheckboxProps().checked).toBe(true);
58
+ expect(tree.item("x112").getCheckboxProps().checked).toBe(false);
59
+ });
60
+
61
+ it("should create indeterminate state", async () => {
62
+ const tree = await factory.createTestCaseTree();
63
+ tree.item("x111").setChecked();
64
+ const refObject = { indeterminate: undefined };
65
+ tree.item("x11").getCheckboxProps().ref(refObject);
66
+ expect(refObject.indeterminate).toBe(true);
67
+ });
68
+
69
+ it("should not create indeterminate state", async () => {
70
+ const tree = await factory.createTestCaseTree();
71
+ const refObject = { indeterminate: undefined };
72
+ tree.item("x11").getCheckboxProps().ref(refObject);
73
+ expect(refObject.indeterminate).toBe(false);
74
+ });
75
+ });
76
+
77
+ it("should handle folder checking", async () => {
78
+ const tree = await factory
79
+ .with({ canCheckFolders: true, propagateCheckedState: false })
80
+ .createTestCaseTree();
81
+
82
+ tree.item("x11").setChecked();
83
+ expect(tree.instance.getState().checkedItems).toContain("x11");
84
+ });
85
+
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();
99
+
100
+ tree.item("x11").setChecked();
101
+ expect(tree.instance.getState().checkedItems).toEqual(
102
+ expect.arrayContaining(["x111", "x112", "x113", "x114"]),
103
+ );
104
+ });
105
+
106
+ it("should turn folder indeterminate", async () => {
107
+ const tree = await factory
108
+ .with({ propagateCheckedState: true })
109
+ .createTestCaseTree();
110
+
111
+ tree.item("x111").setChecked();
112
+ expect(tree.item("x11").getCheckedState()).toBe(CheckedState.Indeterminate);
113
+ });
114
+
115
+ it("should turn folder checked if all children are checked", async () => {
116
+ const tree = await factory
117
+ .with({
118
+ isItemFolder: (item) => item.getItemData().length < 4,
119
+ propagateCheckedState: true,
120
+ canCheckFolders: false,
121
+ })
122
+ .createTestCaseTree();
123
+
124
+ tree.item("x11").setChecked();
125
+ tree.item("x12").setChecked();
126
+ tree.item("x13").setChecked();
127
+ expect(tree.item("x1").getCheckedState()).toBe(CheckedState.Indeterminate);
128
+ tree.do.selectItem("x14");
129
+ tree.item("x141").setChecked();
130
+ tree.item("x142").setChecked();
131
+ tree.item("x143").setChecked();
132
+ expect(tree.item("x1").getCheckedState()).toBe(CheckedState.Indeterminate);
133
+ tree.item("x144").setChecked();
134
+ expect(tree.item("x1").getCheckedState()).toBe(CheckedState.Checked);
135
+ });
136
+
137
+ it("should return correct checked state for items", async () => {
138
+ const tree = await factory.createTestCaseTree();
139
+ const item = tree.instance.getItemInstance("x111");
140
+
141
+ expect(item.getCheckedState()).toBe(CheckedState.Unchecked);
142
+
143
+ item.setChecked();
144
+ expect(item.getCheckedState()).toBe(CheckedState.Checked);
145
+
146
+ item.setUnchecked();
147
+ expect(item.getCheckedState()).toBe(CheckedState.Unchecked);
148
+ });
149
+ });
@@ -0,0 +1,134 @@
1
+ import { FeatureImplementation, TreeInstance } from "../../types/core";
2
+ import { makeStateUpdater } from "../../utils";
3
+ import { CheckedState } from "./types";
4
+ import { throwError } from "../../utilities/errors";
5
+
6
+ const getAllLoadedDescendants = <T>(
7
+ tree: TreeInstance<T>,
8
+ itemId: string,
9
+ includeFolders = false,
10
+ ): string[] => {
11
+ if (!tree.getConfig().isItemFolder(tree.getItemInstance(itemId))) {
12
+ return [itemId];
13
+ }
14
+ const descendants = tree
15
+ .retrieveChildrenIds(itemId)
16
+ .map((child) => getAllLoadedDescendants(tree, child, includeFolders))
17
+ .flat();
18
+ return includeFolders ? [itemId, ...descendants] : descendants;
19
+ };
20
+
21
+ export const checkboxesFeature: FeatureImplementation = {
22
+ key: "checkboxes",
23
+
24
+ overwrites: ["selection"],
25
+
26
+ getInitialState: (initialState) => ({
27
+ checkedItems: [],
28
+ ...initialState,
29
+ }),
30
+
31
+ getDefaultConfig: (defaultConfig, tree) => {
32
+ const hasAsyncLoader = defaultConfig.features?.some(
33
+ (f) => f.key === "async-data-loader",
34
+ );
35
+ if (hasAsyncLoader && defaultConfig.propagateCheckedState) {
36
+ throwError(`propagateCheckedState not supported with async trees`);
37
+ }
38
+ const propagateCheckedState =
39
+ defaultConfig.propagateCheckedState ?? !hasAsyncLoader;
40
+ const canCheckFolders =
41
+ defaultConfig.canCheckFolders ?? !propagateCheckedState;
42
+ return {
43
+ setCheckedItems: makeStateUpdater("checkedItems", tree),
44
+ propagateCheckedState,
45
+ canCheckFolders,
46
+ ...defaultConfig,
47
+ };
48
+ },
49
+
50
+ stateHandlerNames: {
51
+ checkedItems: "setCheckedItems",
52
+ },
53
+
54
+ treeInstance: {
55
+ setCheckedItems: ({ tree }, checkedItems) => {
56
+ tree.applySubStateUpdate("checkedItems", checkedItems);
57
+ },
58
+ },
59
+
60
+ itemInstance: {
61
+ getCheckboxProps: ({ item }) => {
62
+ const checkedState = item.getCheckedState();
63
+ return {
64
+ onChange: item.toggleCheckedState,
65
+ checked: checkedState === CheckedState.Checked,
66
+ ref: (r: any) => {
67
+ if (r) {
68
+ r.indeterminate = checkedState === CheckedState.Indeterminate;
69
+ }
70
+ },
71
+ };
72
+ },
73
+
74
+ toggleCheckedState: ({ item }) => {
75
+ if (item.getCheckedState() === CheckedState.Checked) {
76
+ item.setUnchecked();
77
+ } else {
78
+ item.setChecked();
79
+ }
80
+ },
81
+
82
+ getCheckedState: ({ item, tree }) => {
83
+ const { checkedItems } = tree.getState();
84
+ const { propagateCheckedState } = tree.getConfig();
85
+ const itemId = item.getId();
86
+
87
+ if (checkedItems.includes(itemId)) {
88
+ return CheckedState.Checked;
89
+ }
90
+
91
+ if (item.isFolder() && propagateCheckedState) {
92
+ const descendants = getAllLoadedDescendants(tree, itemId);
93
+ if (descendants.every((d) => checkedItems.includes(d))) {
94
+ return CheckedState.Checked;
95
+ }
96
+ if (descendants.some((d) => checkedItems.includes(d))) {
97
+ return CheckedState.Indeterminate;
98
+ }
99
+ }
100
+
101
+ return CheckedState.Unchecked;
102
+ },
103
+
104
+ setChecked: ({ item, tree, itemId }) => {
105
+ const { propagateCheckedState, canCheckFolders } = tree.getConfig();
106
+ if (item.isFolder() && propagateCheckedState) {
107
+ tree.applySubStateUpdate("checkedItems", (items) => [
108
+ ...items,
109
+ ...getAllLoadedDescendants(tree, itemId, canCheckFolders),
110
+ ]);
111
+ } else if (!item.isFolder() || canCheckFolders) {
112
+ tree.applySubStateUpdate("checkedItems", (items) => [...items, itemId]);
113
+ }
114
+ },
115
+
116
+ setUnchecked: ({ item, tree, itemId }) => {
117
+ const { propagateCheckedState, canCheckFolders } = tree.getConfig();
118
+ if (item.isFolder() && propagateCheckedState) {
119
+ const descendants = getAllLoadedDescendants(
120
+ tree,
121
+ itemId,
122
+ canCheckFolders,
123
+ );
124
+ tree.applySubStateUpdate("checkedItems", (items) =>
125
+ items.filter((id) => !descendants.includes(id) && id !== itemId),
126
+ );
127
+ } else {
128
+ tree.applySubStateUpdate("checkedItems", (items) =>
129
+ items.filter((id) => id !== itemId),
130
+ );
131
+ }
132
+ },
133
+ },
134
+ };
@@ -0,0 +1,29 @@
1
+ import { SetStateFn } from "../../types/core";
2
+
3
+ export enum CheckedState {
4
+ Checked = "checked",
5
+ Unchecked = "unchecked",
6
+ Indeterminate = "indeterminate",
7
+ }
8
+
9
+ export type CheckboxesFeatureDef<T> = {
10
+ state: {
11
+ checkedItems: string[];
12
+ };
13
+ config: {
14
+ setCheckedItems?: SetStateFn<string[]>;
15
+ canCheckFolders?: boolean;
16
+ propagateCheckedState?: boolean;
17
+ };
18
+ treeInstance: {
19
+ setCheckedItems: (checkedItems: string[]) => void;
20
+ };
21
+ itemInstance: {
22
+ setChecked: () => void;
23
+ setUnchecked: () => void;
24
+ toggleCheckedState: () => void;
25
+ getCheckedState: () => CheckedState;
26
+ getCheckboxProps: () => Record<string, any>;
27
+ };
28
+ hotkeys: never;
29
+ };
@@ -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
  );