@headless-tree/core 0.0.0-20250509212452 → 0.0.0-20250511185653

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @headless-tree/core
2
2
 
3
- ## 0.0.0-20250509212452
3
+ ## 0.0.0-20250511185653
4
4
 
5
5
  ### Minor Changes
6
6
 
@@ -29,17 +29,14 @@ const loadItemData = (tree, itemId) => __awaiter(void 0, void 0, void 0, functio
29
29
  tree.applySubStateUpdate("loadingItemData", (loadingItemData) => loadingItemData.filter((id) => id !== itemId));
30
30
  (_b = dataRef.current.awaitingItemDataLoading) === null || _b === void 0 ? void 0 : _b[itemId].forEach((cb) => cb());
31
31
  (_c = dataRef.current.awaitingItemDataLoading) === null || _c === void 0 ? true : delete _c[itemId];
32
- return item;
33
32
  });
34
33
  const loadChildrenIds = (tree, itemId) => __awaiter(void 0, void 0, void 0, function* () {
35
34
  var _a, _b, _c, _d, _e;
36
35
  const config = tree.getConfig();
37
36
  const dataRef = getDataRef(tree);
38
- let childrenIds;
39
- // TODO is folder check?
40
37
  if ("getChildrenWithData" in config.dataLoader) {
41
38
  const children = yield config.dataLoader.getChildrenWithData(itemId);
42
- childrenIds = children.map((c) => c.id);
39
+ const childrenIds = children.map((c) => c.id);
43
40
  dataRef.current.childrenIds[itemId] = childrenIds;
44
41
  children.forEach(({ id, data }) => {
45
42
  var _a, _b, _c;
@@ -53,7 +50,7 @@ const loadChildrenIds = (tree, itemId) => __awaiter(void 0, void 0, void 0, func
53
50
  tree.applySubStateUpdate("loadingItemData", (loadingItemData) => loadingItemData.filter((id) => !childrenIds.includes(id)));
54
51
  }
55
52
  else {
56
- childrenIds = yield config.dataLoader.getChildren(itemId);
53
+ const childrenIds = yield config.dataLoader.getChildren(itemId);
57
54
  dataRef.current.childrenIds[itemId] = childrenIds;
58
55
  (_b = config.onLoadedChildren) === null || _b === void 0 ? void 0 : _b.call(config, itemId, childrenIds);
59
56
  tree.rebuildTree();
@@ -61,7 +58,6 @@ const loadChildrenIds = (tree, itemId) => __awaiter(void 0, void 0, void 0, func
61
58
  tree.applySubStateUpdate("loadingItemChildrens", (loadingItemChildrens) => loadingItemChildrens.filter((id) => id !== itemId));
62
59
  (_d = (_c = dataRef.current.awaitingItemChildrensLoading) === null || _c === void 0 ? void 0 : _c[itemId]) === null || _d === void 0 ? void 0 : _d.forEach((cb) => cb());
63
60
  (_e = dataRef.current.awaitingItemChildrensLoading) === null || _e === void 0 ? true : delete _e[itemId];
64
- return childrenIds;
65
61
  });
66
62
  exports.asyncDataLoaderFeature = {
67
63
  key: "async-data-loader",
@@ -87,7 +83,6 @@ exports.asyncDataLoaderFeature = {
87
83
  });
88
84
  }),
89
85
  waitForItemChildrenLoaded: (_a, itemId_1) => __awaiter(void 0, [_a, itemId_1], void 0, function* ({ tree }, itemId) {
90
- // TODO replace inner implementation with load() fns
91
86
  tree.retrieveChildrenIds(itemId);
92
87
  if (!tree.getState().loadingItemChildrens.includes(itemId)) {
93
88
  return;
@@ -101,14 +96,6 @@ exports.asyncDataLoaderFeature = {
101
96
  dataRef.current.awaitingItemChildrensLoading[itemId].push(resolve);
102
97
  });
103
98
  }),
104
- loadItemData: (_a, itemId_1) => __awaiter(void 0, [_a, itemId_1], void 0, function* ({ tree }, itemId) {
105
- var _b;
106
- return ((_b = getDataRef(tree).current.itemData[itemId]) !== null && _b !== void 0 ? _b : (yield loadItemData(tree, itemId)));
107
- }),
108
- loadChildrenIds: (_a, itemId_1) => __awaiter(void 0, [_a, itemId_1], void 0, function* ({ tree }, itemId) {
109
- var _b;
110
- return ((_b = getDataRef(tree).current.childrenIds[itemId]) !== null && _b !== void 0 ? _b : (yield loadChildrenIds(tree, itemId)));
111
- }),
112
99
  retrieveItemData: ({ tree }, itemId, skipFetch = false) => {
113
100
  var _a, _b;
114
101
  const config = tree.getConfig();
@@ -27,12 +27,8 @@ export type AsyncDataLoaderFeatureDef<T> = {
27
27
  onLoadedChildren?: (itemId: string, childrenIds: string[]) => void;
28
28
  };
29
29
  treeInstance: SyncDataLoaderFeatureDef<T>["treeInstance"] & {
30
- /** @deprecated use loadItemData instead */
31
30
  waitForItemDataLoaded: (itemId: string) => Promise<void>;
32
- /** @deprecated use loadChildrenIds instead */
33
31
  waitForItemChildrenLoaded: (itemId: string) => Promise<void>;
34
- loadItemData: (itemId: string) => Promise<T>;
35
- loadChildrenIds: (itemId: string) => Promise<string[]>;
36
32
  };
37
33
  itemInstance: SyncDataLoaderFeatureDef<T>["itemInstance"] & {
38
34
  /** Invalidate fetched data for item, and triggers a refetch and subsequent rerender if the item is visible
@@ -1,46 +1,30 @@
1
1
  "use strict";
2
- var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
- function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
- return new (P || (P = Promise))(function (resolve, reject) {
5
- function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
- function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
- function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
- step((generator = generator.apply(thisArg, _arguments || [])).next());
9
- });
10
- };
11
2
  Object.defineProperty(exports, "__esModule", { value: true });
12
3
  exports.checkboxesFeature = void 0;
13
4
  const utils_1 = require("../../utils");
14
5
  const types_1 = require("./types");
15
- /*
16
- * Cases for checking:
17
- * - Check an unchecked item in an unchecked or indeterminate folder
18
- * - Check an explicitly unchecked item in a checked folder
19
- * - Check an unchecked folder in an unchecked or indeterminate folder
20
- *
21
- * Cases for unchecking:
22
- * - Uncheck a checked item in an indeterminate folder
23
- * - Uncheck an explicitly unchecked item in an checked folder
24
- */
25
- const fetchAllDescendants = (tree, itemId) => __awaiter(void 0, void 0, void 0, function* () {
26
- const children = yield tree.loadChildrenIds(itemId);
27
- return [
28
- itemId,
29
- ...(yield Promise.all(children.map((child) => fetchAllDescendants(tree, child)))).flat(),
30
- ];
31
- });
6
+ const errors_1 = require("../../utilities/errors");
32
7
  const getAllLoadedDescendants = (tree, itemId) => {
33
- const children = tree.retrieveChildrenIds(itemId, true);
34
- return [
35
- itemId,
36
- ...children.map((child) => getAllLoadedDescendants(tree, child)).flat(),
37
- ];
8
+ if (!tree.getConfig().isItemFolder(tree.buildItemInstance(itemId))) {
9
+ return [itemId];
10
+ }
11
+ return tree
12
+ .retrieveChildrenIds(itemId)
13
+ .map((child) => getAllLoadedDescendants(tree, child))
14
+ .flat();
38
15
  };
39
16
  exports.checkboxesFeature = {
40
17
  key: "checkboxes",
41
18
  overwrites: ["selection"],
42
19
  getInitialState: (initialState) => (Object.assign({ checkedItems: [] }, initialState)),
43
- getDefaultConfig: (defaultConfig, tree) => (Object.assign({ setCheckedItems: (0, utils_1.makeStateUpdater)("checkedItems", tree) }, defaultConfig)),
20
+ getDefaultConfig: (defaultConfig, tree) => {
21
+ var _a;
22
+ const hasAsyncLoader = (_a = defaultConfig.features) === null || _a === void 0 ? void 0 : _a.some((f) => f.key === "async-data-loader");
23
+ if (hasAsyncLoader && !defaultConfig.canCheckFolders) {
24
+ (0, errors_1.throwError)(`!canCheckFolders not supported with async trees`);
25
+ }
26
+ return Object.assign({ setCheckedItems: (0, utils_1.makeStateUpdater)("checkedItems", tree), canCheckFolders: hasAsyncLoader !== null && hasAsyncLoader !== void 0 ? hasAsyncLoader : false }, defaultConfig);
27
+ },
44
28
  stateHandlerNames: {
45
29
  checkedItems: "setCheckedItems",
46
30
  },
@@ -50,37 +34,33 @@ exports.checkboxesFeature = {
50
34
  },
51
35
  },
52
36
  itemInstance: {
53
- getCheckboxProps: ({ item, itemId }) => {
37
+ getCheckboxProps: ({ item }) => {
54
38
  const checkedState = item.getCheckedState();
55
- // console.log("prop", itemId, checkedState);
56
39
  return {
57
40
  onChange: item.toggleCheckedState,
58
41
  checked: checkedState === types_1.CheckedState.Checked,
59
42
  ref: (r) => {
60
43
  if (r) {
61
- // console.log("ref", itemId, checkedState);
62
44
  r.indeterminate = checkedState === types_1.CheckedState.Indeterminate;
63
45
  }
64
46
  },
65
47
  };
66
48
  },
67
- toggleCheckedState: (_a) => __awaiter(void 0, [_a], void 0, function* ({ item }) {
49
+ toggleCheckedState: ({ item }) => {
68
50
  if (item.getCheckedState() === types_1.CheckedState.Checked) {
69
- yield item.setUnchecked();
51
+ item.setUnchecked();
70
52
  }
71
53
  else {
72
- yield item.setChecked();
54
+ item.setChecked();
73
55
  }
74
- }),
56
+ },
75
57
  getCheckedState: ({ item, tree, itemId }) => {
76
- // TODO checkedcache
77
58
  const { checkedItems } = tree.getState();
78
59
  if (checkedItems.includes(itemId)) {
79
60
  return types_1.CheckedState.Checked;
80
61
  }
81
- if (item.isFolder()) {
62
+ if (item.isFolder() && !tree.getConfig().canCheckFolders) {
82
63
  const descendants = getAllLoadedDescendants(tree, itemId);
83
- console.log("descendants of ", itemId, descendants);
84
64
  if (descendants.every((d) => checkedItems.includes(d))) {
85
65
  return types_1.CheckedState.Checked;
86
66
  }
@@ -88,41 +68,27 @@ exports.checkboxesFeature = {
88
68
  return types_1.CheckedState.Indeterminate;
89
69
  }
90
70
  }
91
- // if (
92
- // item.isFolder() &&
93
- // checkedItems.some((checkedItem) =>
94
- // tree.getItemInstance(checkedItem)?.isDescendentOf(itemId),
95
- // )
96
- // ) {
97
- // // TODO for every descendent, not every checked item
98
- // return checkedItems.every((checkedItem) =>
99
- // tree.getItemInstance(checkedItem)?.isDescendentOf(itemId),
100
- // )
101
- // ? CheckedState.Checked
102
- // : CheckedState.Indeterminate;
103
- // }
104
71
  return types_1.CheckedState.Unchecked;
105
72
  },
106
- setChecked: (_a) => __awaiter(void 0, [_a], void 0, function* ({ item, tree, itemId }) {
73
+ setChecked: ({ item, tree, itemId }) => {
107
74
  if (!item.isFolder() || tree.getConfig().canCheckFolders) {
108
75
  tree.applySubStateUpdate("checkedItems", (items) => [...items, itemId]);
109
76
  }
110
77
  else {
111
- const descendants = yield fetchAllDescendants(tree, itemId);
112
78
  tree.applySubStateUpdate("checkedItems", (items) => [
113
79
  ...items,
114
- ...descendants,
80
+ ...getAllLoadedDescendants(tree, itemId),
115
81
  ]);
116
82
  }
117
- }),
118
- setUnchecked: (_a) => __awaiter(void 0, [_a], void 0, function* ({ item, tree, itemId }) {
83
+ },
84
+ setUnchecked: ({ item, tree, itemId }) => {
119
85
  if (!item.isFolder() || tree.getConfig().canCheckFolders) {
120
86
  tree.applySubStateUpdate("checkedItems", (items) => items.filter((id) => id !== itemId));
121
87
  }
122
88
  else {
123
- yield tree.loadChildrenIds(itemId);
124
- item.getChildren().forEach((item) => item.setUnchecked());
89
+ const descendants = getAllLoadedDescendants(tree, itemId);
90
+ tree.applySubStateUpdate("checkedItems", (items) => items.filter((id) => !descendants.includes(id)));
125
91
  }
126
- }),
92
+ },
127
93
  },
128
94
  };
@@ -16,9 +16,9 @@ export type CheckboxesFeatureDef<T> = {
16
16
  setCheckedItems: (checkedItems: string[]) => void;
17
17
  };
18
18
  itemInstance: {
19
- setChecked: () => Promise<void>;
20
- setUnchecked: () => Promise<void>;
21
- toggleCheckedState: () => Promise<void>;
19
+ setChecked: () => void;
20
+ setUnchecked: () => void;
21
+ toggleCheckedState: () => void;
22
22
  getCheckedState: () => CheckedState;
23
23
  getCheckboxProps: () => Record<string, any>;
24
24
  };
@@ -40,8 +40,6 @@ exports.syncDataLoaderFeature = {
40
40
  }
41
41
  return unpromise(dataLoader.getChildrenWithData(itemId)).map((c) => c.data);
42
42
  },
43
- loadItemData: ({ tree }, itemId) => tree.retrieveItemData(itemId),
44
- loadChildrenIds: ({ tree }, itemId) => tree.retrieveChildrenIds(itemId),
45
43
  },
46
44
  itemInstance: {
47
45
  isLoading: () => false,
@@ -18,8 +18,8 @@ export type SyncDataLoaderFeatureDef<T> = {
18
18
  dataLoader: TreeDataLoader<T>;
19
19
  };
20
20
  treeInstance: {
21
- retrieveItemData: (itemId: string, skipFetch?: boolean) => T;
22
- retrieveChildrenIds: (itemId: string, skipFetch?: boolean) => string[];
21
+ retrieveItemData: (itemId: string) => T;
22
+ retrieveChildrenIds: (itemId: string) => string[];
23
23
  };
24
24
  itemInstance: {
25
25
  isLoading: () => boolean;
@@ -26,17 +26,14 @@ const loadItemData = (tree, itemId) => __awaiter(void 0, void 0, void 0, functio
26
26
  tree.applySubStateUpdate("loadingItemData", (loadingItemData) => loadingItemData.filter((id) => id !== itemId));
27
27
  (_b = dataRef.current.awaitingItemDataLoading) === null || _b === void 0 ? void 0 : _b[itemId].forEach((cb) => cb());
28
28
  (_c = dataRef.current.awaitingItemDataLoading) === null || _c === void 0 ? true : delete _c[itemId];
29
- return item;
30
29
  });
31
30
  const loadChildrenIds = (tree, itemId) => __awaiter(void 0, void 0, void 0, function* () {
32
31
  var _a, _b, _c, _d, _e;
33
32
  const config = tree.getConfig();
34
33
  const dataRef = getDataRef(tree);
35
- let childrenIds;
36
- // TODO is folder check?
37
34
  if ("getChildrenWithData" in config.dataLoader) {
38
35
  const children = yield config.dataLoader.getChildrenWithData(itemId);
39
- childrenIds = children.map((c) => c.id);
36
+ const childrenIds = children.map((c) => c.id);
40
37
  dataRef.current.childrenIds[itemId] = childrenIds;
41
38
  children.forEach(({ id, data }) => {
42
39
  var _a, _b, _c;
@@ -50,7 +47,7 @@ const loadChildrenIds = (tree, itemId) => __awaiter(void 0, void 0, void 0, func
50
47
  tree.applySubStateUpdate("loadingItemData", (loadingItemData) => loadingItemData.filter((id) => !childrenIds.includes(id)));
51
48
  }
52
49
  else {
53
- childrenIds = yield config.dataLoader.getChildren(itemId);
50
+ const childrenIds = yield config.dataLoader.getChildren(itemId);
54
51
  dataRef.current.childrenIds[itemId] = childrenIds;
55
52
  (_b = config.onLoadedChildren) === null || _b === void 0 ? void 0 : _b.call(config, itemId, childrenIds);
56
53
  tree.rebuildTree();
@@ -58,7 +55,6 @@ const loadChildrenIds = (tree, itemId) => __awaiter(void 0, void 0, void 0, func
58
55
  tree.applySubStateUpdate("loadingItemChildrens", (loadingItemChildrens) => loadingItemChildrens.filter((id) => id !== itemId));
59
56
  (_d = (_c = dataRef.current.awaitingItemChildrensLoading) === null || _c === void 0 ? void 0 : _c[itemId]) === null || _d === void 0 ? void 0 : _d.forEach((cb) => cb());
60
57
  (_e = dataRef.current.awaitingItemChildrensLoading) === null || _e === void 0 ? true : delete _e[itemId];
61
- return childrenIds;
62
58
  });
63
59
  export const asyncDataLoaderFeature = {
64
60
  key: "async-data-loader",
@@ -84,7 +80,6 @@ export const asyncDataLoaderFeature = {
84
80
  });
85
81
  }),
86
82
  waitForItemChildrenLoaded: (_a, itemId_1) => __awaiter(void 0, [_a, itemId_1], void 0, function* ({ tree }, itemId) {
87
- // TODO replace inner implementation with load() fns
88
83
  tree.retrieveChildrenIds(itemId);
89
84
  if (!tree.getState().loadingItemChildrens.includes(itemId)) {
90
85
  return;
@@ -98,14 +93,6 @@ export const asyncDataLoaderFeature = {
98
93
  dataRef.current.awaitingItemChildrensLoading[itemId].push(resolve);
99
94
  });
100
95
  }),
101
- loadItemData: (_a, itemId_1) => __awaiter(void 0, [_a, itemId_1], void 0, function* ({ tree }, itemId) {
102
- var _b;
103
- return ((_b = getDataRef(tree).current.itemData[itemId]) !== null && _b !== void 0 ? _b : (yield loadItemData(tree, itemId)));
104
- }),
105
- loadChildrenIds: (_a, itemId_1) => __awaiter(void 0, [_a, itemId_1], void 0, function* ({ tree }, itemId) {
106
- var _b;
107
- return ((_b = getDataRef(tree).current.childrenIds[itemId]) !== null && _b !== void 0 ? _b : (yield loadChildrenIds(tree, itemId)));
108
- }),
109
96
  retrieveItemData: ({ tree }, itemId, skipFetch = false) => {
110
97
  var _a, _b;
111
98
  const config = tree.getConfig();
@@ -27,12 +27,8 @@ export type AsyncDataLoaderFeatureDef<T> = {
27
27
  onLoadedChildren?: (itemId: string, childrenIds: string[]) => void;
28
28
  };
29
29
  treeInstance: SyncDataLoaderFeatureDef<T>["treeInstance"] & {
30
- /** @deprecated use loadItemData instead */
31
30
  waitForItemDataLoaded: (itemId: string) => Promise<void>;
32
- /** @deprecated use loadChildrenIds instead */
33
31
  waitForItemChildrenLoaded: (itemId: string) => Promise<void>;
34
- loadItemData: (itemId: string) => Promise<T>;
35
- loadChildrenIds: (itemId: string) => Promise<string[]>;
36
32
  };
37
33
  itemInstance: SyncDataLoaderFeatureDef<T>["itemInstance"] & {
38
34
  /** Invalidate fetched data for item, and triggers a refetch and subsequent rerender if the item is visible
@@ -1,43 +1,27 @@
1
- var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
- function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
- return new (P || (P = Promise))(function (resolve, reject) {
4
- function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
- function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
- function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
- step((generator = generator.apply(thisArg, _arguments || [])).next());
8
- });
9
- };
10
1
  import { makeStateUpdater } from "../../utils";
11
2
  import { CheckedState } from "./types";
12
- /*
13
- * Cases for checking:
14
- * - Check an unchecked item in an unchecked or indeterminate folder
15
- * - Check an explicitly unchecked item in a checked folder
16
- * - Check an unchecked folder in an unchecked or indeterminate folder
17
- *
18
- * Cases for unchecking:
19
- * - Uncheck a checked item in an indeterminate folder
20
- * - Uncheck an explicitly unchecked item in an checked folder
21
- */
22
- const fetchAllDescendants = (tree, itemId) => __awaiter(void 0, void 0, void 0, function* () {
23
- const children = yield tree.loadChildrenIds(itemId);
24
- return [
25
- itemId,
26
- ...(yield Promise.all(children.map((child) => fetchAllDescendants(tree, child)))).flat(),
27
- ];
28
- });
3
+ import { throwError } from "../../utilities/errors";
29
4
  const getAllLoadedDescendants = (tree, itemId) => {
30
- const children = tree.retrieveChildrenIds(itemId, true);
31
- return [
32
- itemId,
33
- ...children.map((child) => getAllLoadedDescendants(tree, child)).flat(),
34
- ];
5
+ if (!tree.getConfig().isItemFolder(tree.buildItemInstance(itemId))) {
6
+ return [itemId];
7
+ }
8
+ return tree
9
+ .retrieveChildrenIds(itemId)
10
+ .map((child) => getAllLoadedDescendants(tree, child))
11
+ .flat();
35
12
  };
36
13
  export const checkboxesFeature = {
37
14
  key: "checkboxes",
38
15
  overwrites: ["selection"],
39
16
  getInitialState: (initialState) => (Object.assign({ checkedItems: [] }, initialState)),
40
- getDefaultConfig: (defaultConfig, tree) => (Object.assign({ setCheckedItems: makeStateUpdater("checkedItems", tree) }, defaultConfig)),
17
+ getDefaultConfig: (defaultConfig, tree) => {
18
+ var _a;
19
+ const hasAsyncLoader = (_a = defaultConfig.features) === null || _a === void 0 ? void 0 : _a.some((f) => f.key === "async-data-loader");
20
+ if (hasAsyncLoader && !defaultConfig.canCheckFolders) {
21
+ throwError(`!canCheckFolders not supported with async trees`);
22
+ }
23
+ return Object.assign({ setCheckedItems: makeStateUpdater("checkedItems", tree), canCheckFolders: hasAsyncLoader !== null && hasAsyncLoader !== void 0 ? hasAsyncLoader : false }, defaultConfig);
24
+ },
41
25
  stateHandlerNames: {
42
26
  checkedItems: "setCheckedItems",
43
27
  },
@@ -47,37 +31,33 @@ export const checkboxesFeature = {
47
31
  },
48
32
  },
49
33
  itemInstance: {
50
- getCheckboxProps: ({ item, itemId }) => {
34
+ getCheckboxProps: ({ item }) => {
51
35
  const checkedState = item.getCheckedState();
52
- // console.log("prop", itemId, checkedState);
53
36
  return {
54
37
  onChange: item.toggleCheckedState,
55
38
  checked: checkedState === CheckedState.Checked,
56
39
  ref: (r) => {
57
40
  if (r) {
58
- // console.log("ref", itemId, checkedState);
59
41
  r.indeterminate = checkedState === CheckedState.Indeterminate;
60
42
  }
61
43
  },
62
44
  };
63
45
  },
64
- toggleCheckedState: (_a) => __awaiter(void 0, [_a], void 0, function* ({ item }) {
46
+ toggleCheckedState: ({ item }) => {
65
47
  if (item.getCheckedState() === CheckedState.Checked) {
66
- yield item.setUnchecked();
48
+ item.setUnchecked();
67
49
  }
68
50
  else {
69
- yield item.setChecked();
51
+ item.setChecked();
70
52
  }
71
- }),
53
+ },
72
54
  getCheckedState: ({ item, tree, itemId }) => {
73
- // TODO checkedcache
74
55
  const { checkedItems } = tree.getState();
75
56
  if (checkedItems.includes(itemId)) {
76
57
  return CheckedState.Checked;
77
58
  }
78
- if (item.isFolder()) {
59
+ if (item.isFolder() && !tree.getConfig().canCheckFolders) {
79
60
  const descendants = getAllLoadedDescendants(tree, itemId);
80
- console.log("descendants of ", itemId, descendants);
81
61
  if (descendants.every((d) => checkedItems.includes(d))) {
82
62
  return CheckedState.Checked;
83
63
  }
@@ -85,41 +65,27 @@ export const checkboxesFeature = {
85
65
  return CheckedState.Indeterminate;
86
66
  }
87
67
  }
88
- // if (
89
- // item.isFolder() &&
90
- // checkedItems.some((checkedItem) =>
91
- // tree.getItemInstance(checkedItem)?.isDescendentOf(itemId),
92
- // )
93
- // ) {
94
- // // TODO for every descendent, not every checked item
95
- // return checkedItems.every((checkedItem) =>
96
- // tree.getItemInstance(checkedItem)?.isDescendentOf(itemId),
97
- // )
98
- // ? CheckedState.Checked
99
- // : CheckedState.Indeterminate;
100
- // }
101
68
  return CheckedState.Unchecked;
102
69
  },
103
- setChecked: (_a) => __awaiter(void 0, [_a], void 0, function* ({ item, tree, itemId }) {
70
+ setChecked: ({ item, tree, itemId }) => {
104
71
  if (!item.isFolder() || tree.getConfig().canCheckFolders) {
105
72
  tree.applySubStateUpdate("checkedItems", (items) => [...items, itemId]);
106
73
  }
107
74
  else {
108
- const descendants = yield fetchAllDescendants(tree, itemId);
109
75
  tree.applySubStateUpdate("checkedItems", (items) => [
110
76
  ...items,
111
- ...descendants,
77
+ ...getAllLoadedDescendants(tree, itemId),
112
78
  ]);
113
79
  }
114
- }),
115
- setUnchecked: (_a) => __awaiter(void 0, [_a], void 0, function* ({ item, tree, itemId }) {
80
+ },
81
+ setUnchecked: ({ item, tree, itemId }) => {
116
82
  if (!item.isFolder() || tree.getConfig().canCheckFolders) {
117
83
  tree.applySubStateUpdate("checkedItems", (items) => items.filter((id) => id !== itemId));
118
84
  }
119
85
  else {
120
- yield tree.loadChildrenIds(itemId);
121
- item.getChildren().forEach((item) => item.setUnchecked());
86
+ const descendants = getAllLoadedDescendants(tree, itemId);
87
+ tree.applySubStateUpdate("checkedItems", (items) => items.filter((id) => !descendants.includes(id)));
122
88
  }
123
- }),
89
+ },
124
90
  },
125
91
  };
@@ -16,9 +16,9 @@ export type CheckboxesFeatureDef<T> = {
16
16
  setCheckedItems: (checkedItems: string[]) => void;
17
17
  };
18
18
  itemInstance: {
19
- setChecked: () => Promise<void>;
20
- setUnchecked: () => Promise<void>;
21
- toggleCheckedState: () => Promise<void>;
19
+ setChecked: () => void;
20
+ setUnchecked: () => void;
21
+ toggleCheckedState: () => void;
22
22
  getCheckedState: () => CheckedState;
23
23
  getCheckboxProps: () => Record<string, any>;
24
24
  };
@@ -37,8 +37,6 @@ export const syncDataLoaderFeature = {
37
37
  }
38
38
  return unpromise(dataLoader.getChildrenWithData(itemId)).map((c) => c.data);
39
39
  },
40
- loadItemData: ({ tree }, itemId) => tree.retrieveItemData(itemId),
41
- loadChildrenIds: ({ tree }, itemId) => tree.retrieveChildrenIds(itemId),
42
40
  },
43
41
  itemInstance: {
44
42
  isLoading: () => false,
@@ -18,8 +18,8 @@ export type SyncDataLoaderFeatureDef<T> = {
18
18
  dataLoader: TreeDataLoader<T>;
19
19
  };
20
20
  treeInstance: {
21
- retrieveItemData: (itemId: string, skipFetch?: boolean) => T;
22
- retrieveChildrenIds: (itemId: string, skipFetch?: boolean) => string[];
21
+ retrieveItemData: (itemId: string) => T;
22
+ retrieveChildrenIds: (itemId: string) => string[];
23
23
  };
24
24
  itemInstance: {
25
25
  isLoading: () => boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@headless-tree/core",
3
- "version": "0.0.0-20250509212452",
3
+ "version": "0.0.0-20250511185653",
4
4
  "type": "module",
5
5
  "main": "lib/cjs/index.js",
6
6
  "module": "lib/esm/index.js",
@@ -22,19 +22,15 @@ const loadItemData = async <T>(tree: TreeInstance<T>, itemId: string) => {
22
22
 
23
23
  dataRef.current.awaitingItemDataLoading?.[itemId].forEach((cb) => cb());
24
24
  delete dataRef.current.awaitingItemDataLoading?.[itemId];
25
- return item;
26
25
  };
27
26
 
28
27
  const loadChildrenIds = async <T>(tree: TreeInstance<T>, itemId: string) => {
29
28
  const config = tree.getConfig();
30
29
  const dataRef = getDataRef(tree);
31
- let childrenIds: string[];
32
-
33
- // TODO is folder check?
34
30
 
35
31
  if ("getChildrenWithData" in config.dataLoader) {
36
32
  const children = await config.dataLoader.getChildrenWithData(itemId);
37
- childrenIds = children.map((c) => c.id);
33
+ const childrenIds = children.map((c) => c.id);
38
34
  dataRef.current.childrenIds[itemId] = childrenIds;
39
35
  children.forEach(({ id, data }) => {
40
36
  dataRef.current.itemData[id] = data;
@@ -49,7 +45,7 @@ const loadChildrenIds = async <T>(tree: TreeInstance<T>, itemId: string) => {
49
45
  loadingItemData.filter((id) => !childrenIds.includes(id)),
50
46
  );
51
47
  } else {
52
- childrenIds = await config.dataLoader.getChildren(itemId);
48
+ const childrenIds = await config.dataLoader.getChildren(itemId);
53
49
  dataRef.current.childrenIds[itemId] = childrenIds;
54
50
  config.onLoadedChildren?.(itemId, childrenIds);
55
51
  tree.rebuildTree();
@@ -61,7 +57,6 @@ const loadChildrenIds = async <T>(tree: TreeInstance<T>, itemId: string) => {
61
57
 
62
58
  dataRef.current.awaitingItemChildrensLoading?.[itemId]?.forEach((cb) => cb());
63
59
  delete dataRef.current.awaitingItemChildrensLoading?.[itemId];
64
- return childrenIds;
65
60
  };
66
61
 
67
62
  export const asyncDataLoaderFeature: FeatureImplementation = {
@@ -99,7 +94,6 @@ export const asyncDataLoaderFeature: FeatureImplementation = {
99
94
  },
100
95
 
101
96
  waitForItemChildrenLoaded: async ({ tree }, itemId) => {
102
- // TODO replace inner implementation with load() fns
103
97
  tree.retrieveChildrenIds(itemId);
104
98
  if (!tree.getState().loadingItemChildrens.includes(itemId)) {
105
99
  return;
@@ -112,19 +106,6 @@ export const asyncDataLoaderFeature: FeatureImplementation = {
112
106
  });
113
107
  },
114
108
 
115
- loadItemData: async ({ tree }, itemId) => {
116
- return (
117
- getDataRef(tree).current.itemData[itemId] ??
118
- (await loadItemData(tree, itemId))
119
- );
120
- },
121
- loadChildrenIds: async ({ tree }, itemId) => {
122
- return (
123
- getDataRef(tree).current.childrenIds[itemId] ??
124
- (await loadChildrenIds(tree, itemId))
125
- );
126
- },
127
-
128
109
  retrieveItemData: ({ tree }, itemId, skipFetch = false) => {
129
110
  const config = tree.getConfig();
130
111
  const dataRef = getDataRef(tree);
@@ -32,12 +32,8 @@ export type AsyncDataLoaderFeatureDef<T> = {
32
32
  onLoadedChildren?: (itemId: string, childrenIds: string[]) => void;
33
33
  };
34
34
  treeInstance: SyncDataLoaderFeatureDef<T>["treeInstance"] & {
35
- /** @deprecated use loadItemData instead */
36
35
  waitForItemDataLoaded: (itemId: string) => Promise<void>;
37
- /** @deprecated use loadChildrenIds instead */
38
36
  waitForItemChildrenLoaded: (itemId: string) => Promise<void>;
39
- loadItemData: (itemId: string) => Promise<T>;
40
- loadChildrenIds: (itemId: string) => Promise<string[]>;
41
37
  };
42
38
  itemInstance: SyncDataLoaderFeatureDef<T>["itemInstance"] & {
43
39
  /** Invalidate fetched data for item, and triggers a refetch and subsequent rerender if the item is visible
@@ -0,0 +1,134 @@
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 when canCheckFolders is true", async () => {
78
+ const tree = await factory
79
+ .with({ canCheckFolders: true })
80
+ .createTestCaseTree();
81
+
82
+ tree.item("x11").setChecked();
83
+ expect(tree.instance.getState().checkedItems).toContain("x11");
84
+ });
85
+
86
+ it("should handle folder checking when canCheckFolders is false", async () => {
87
+ const tree = await factory.createTestCaseTree();
88
+
89
+ tree.item("x11").setChecked();
90
+ expect(tree.instance.getState().checkedItems).toEqual(
91
+ expect.arrayContaining(["x111", "x112", "x113", "x114"]),
92
+ );
93
+ });
94
+
95
+ it("should turn folder indeterminate", async () => {
96
+ const tree = await factory.createTestCaseTree();
97
+
98
+ tree.item("x111").setChecked();
99
+ expect(tree.item("x11").getCheckedState()).toBe(CheckedState.Indeterminate);
100
+ });
101
+
102
+ it("should turn folder checked if all children are checked", async () => {
103
+ const tree = await factory
104
+ .with({
105
+ isItemFolder: (item) => item.getItemData().length < 4,
106
+ })
107
+ .createTestCaseTree();
108
+
109
+ tree.item("x11").setChecked();
110
+ tree.item("x12").setChecked();
111
+ tree.item("x13").setChecked();
112
+ expect(tree.item("x1").getCheckedState()).toBe(CheckedState.Indeterminate);
113
+ tree.do.selectItem("x14");
114
+ tree.item("x141").setChecked();
115
+ tree.item("x142").setChecked();
116
+ tree.item("x143").setChecked();
117
+ expect(tree.item("x1").getCheckedState()).toBe(CheckedState.Indeterminate);
118
+ tree.item("x144").setChecked();
119
+ expect(tree.item("x1").getCheckedState()).toBe(CheckedState.Checked);
120
+ });
121
+
122
+ it("should return correct checked state for items", async () => {
123
+ const tree = await factory.createTestCaseTree();
124
+ const item = tree.instance.getItemInstance("x111");
125
+
126
+ expect(item.getCheckedState()).toBe(CheckedState.Unchecked);
127
+
128
+ item.setChecked();
129
+ expect(item.getCheckedState()).toBe(CheckedState.Checked);
130
+
131
+ item.setUnchecked();
132
+ expect(item.getCheckedState()).toBe(CheckedState.Unchecked);
133
+ });
134
+ });
@@ -1,42 +1,19 @@
1
1
  import { FeatureImplementation, TreeInstance } from "../../types/core";
2
2
  import { makeStateUpdater } from "../../utils";
3
3
  import { CheckedState } from "./types";
4
-
5
- /*
6
- * Cases for checking:
7
- * - Check an unchecked item in an unchecked or indeterminate folder
8
- * - Check an explicitly unchecked item in a checked folder
9
- * - Check an unchecked folder in an unchecked or indeterminate folder
10
- *
11
- * Cases for unchecking:
12
- * - Uncheck a checked item in an indeterminate folder
13
- * - Uncheck an explicitly unchecked item in an checked folder
14
- */
15
-
16
- const fetchAllDescendants = async <T>(
17
- tree: TreeInstance<T>,
18
- itemId: string,
19
- ): Promise<string[]> => {
20
- const children = await tree.loadChildrenIds(itemId);
21
- return [
22
- itemId,
23
- ...(
24
- await Promise.all(
25
- children.map((child) => fetchAllDescendants(tree, child)),
26
- )
27
- ).flat(),
28
- ];
29
- };
4
+ import { throwError } from "../../utilities/errors";
30
5
 
31
6
  const getAllLoadedDescendants = <T>(
32
7
  tree: TreeInstance<T>,
33
8
  itemId: string,
34
9
  ): string[] => {
35
- const children = tree.retrieveChildrenIds(itemId, true);
36
- return [
37
- itemId,
38
- ...children.map((child) => getAllLoadedDescendants(tree, child)).flat(),
39
- ];
10
+ if (!tree.getConfig().isItemFolder(tree.buildItemInstance(itemId))) {
11
+ return [itemId];
12
+ }
13
+ return tree
14
+ .retrieveChildrenIds(itemId)
15
+ .map((child) => getAllLoadedDescendants(tree, child))
16
+ .flat();
40
17
  };
41
18
 
42
19
  export const checkboxesFeature: FeatureImplementation = {
@@ -49,10 +26,19 @@ export const checkboxesFeature: FeatureImplementation = {
49
26
  ...initialState,
50
27
  }),
51
28
 
52
- getDefaultConfig: (defaultConfig, tree) => ({
53
- setCheckedItems: makeStateUpdater("checkedItems", tree),
54
- ...defaultConfig,
55
- }),
29
+ getDefaultConfig: (defaultConfig, tree) => {
30
+ const hasAsyncLoader = defaultConfig.features?.some(
31
+ (f) => f.key === "async-data-loader",
32
+ );
33
+ if (hasAsyncLoader && !defaultConfig.canCheckFolders) {
34
+ throwError(`!canCheckFolders not supported with async trees`);
35
+ }
36
+ return {
37
+ setCheckedItems: makeStateUpdater("checkedItems", tree),
38
+ canCheckFolders: hasAsyncLoader ?? false,
39
+ ...defaultConfig,
40
+ };
41
+ },
56
42
 
57
43
  stateHandlerNames: {
58
44
  checkedItems: "setCheckedItems",
@@ -65,40 +51,36 @@ export const checkboxesFeature: FeatureImplementation = {
65
51
  },
66
52
 
67
53
  itemInstance: {
68
- getCheckboxProps: ({ item, itemId }) => {
54
+ getCheckboxProps: ({ item }) => {
69
55
  const checkedState = item.getCheckedState();
70
- // console.log("prop", itemId, checkedState);
71
56
  return {
72
57
  onChange: item.toggleCheckedState,
73
58
  checked: checkedState === CheckedState.Checked,
74
59
  ref: (r: any) => {
75
60
  if (r) {
76
- // console.log("ref", itemId, checkedState);
77
61
  r.indeterminate = checkedState === CheckedState.Indeterminate;
78
62
  }
79
63
  },
80
64
  };
81
65
  },
82
66
 
83
- toggleCheckedState: async ({ item }) => {
67
+ toggleCheckedState: ({ item }) => {
84
68
  if (item.getCheckedState() === CheckedState.Checked) {
85
- await item.setUnchecked();
69
+ item.setUnchecked();
86
70
  } else {
87
- await item.setChecked();
71
+ item.setChecked();
88
72
  }
89
73
  },
90
74
 
91
75
  getCheckedState: ({ item, tree, itemId }) => {
92
- // TODO checkedcache
93
76
  const { checkedItems } = tree.getState();
94
77
 
95
78
  if (checkedItems.includes(itemId)) {
96
79
  return CheckedState.Checked;
97
80
  }
98
81
 
99
- if (item.isFolder()) {
82
+ if (item.isFolder() && !tree.getConfig().canCheckFolders) {
100
83
  const descendants = getAllLoadedDescendants(tree, itemId);
101
- console.log("descendants of ", itemId, descendants);
102
84
  if (descendants.every((d) => checkedItems.includes(d))) {
103
85
  return CheckedState.Checked;
104
86
  }
@@ -107,43 +89,30 @@ export const checkboxesFeature: FeatureImplementation = {
107
89
  }
108
90
  }
109
91
 
110
- // if (
111
- // item.isFolder() &&
112
- // checkedItems.some((checkedItem) =>
113
- // tree.getItemInstance(checkedItem)?.isDescendentOf(itemId),
114
- // )
115
- // ) {
116
- // // TODO for every descendent, not every checked item
117
- // return checkedItems.every((checkedItem) =>
118
- // tree.getItemInstance(checkedItem)?.isDescendentOf(itemId),
119
- // )
120
- // ? CheckedState.Checked
121
- // : CheckedState.Indeterminate;
122
- // }
123
-
124
92
  return CheckedState.Unchecked;
125
93
  },
126
94
 
127
- setChecked: async ({ item, tree, itemId }) => {
95
+ setChecked: ({ item, tree, itemId }) => {
128
96
  if (!item.isFolder() || tree.getConfig().canCheckFolders) {
129
97
  tree.applySubStateUpdate("checkedItems", (items) => [...items, itemId]);
130
98
  } else {
131
- const descendants = await fetchAllDescendants(tree, itemId);
132
99
  tree.applySubStateUpdate("checkedItems", (items) => [
133
100
  ...items,
134
- ...descendants,
101
+ ...getAllLoadedDescendants(tree, itemId),
135
102
  ]);
136
103
  }
137
104
  },
138
105
 
139
- setUnchecked: async ({ item, tree, itemId }) => {
106
+ setUnchecked: ({ item, tree, itemId }) => {
140
107
  if (!item.isFolder() || tree.getConfig().canCheckFolders) {
141
108
  tree.applySubStateUpdate("checkedItems", (items) =>
142
109
  items.filter((id) => id !== itemId),
143
110
  );
144
111
  } else {
145
- await tree.loadChildrenIds(itemId);
146
- item.getChildren().forEach((item) => item.setUnchecked());
112
+ const descendants = getAllLoadedDescendants(tree, itemId);
113
+ tree.applySubStateUpdate("checkedItems", (items) =>
114
+ items.filter((id) => !descendants.includes(id)),
115
+ );
147
116
  }
148
117
  },
149
118
  },
@@ -18,9 +18,9 @@ export type CheckboxesFeatureDef<T> = {
18
18
  setCheckedItems: (checkedItems: string[]) => void;
19
19
  };
20
20
  itemInstance: {
21
- setChecked: () => Promise<void>;
22
- setUnchecked: () => Promise<void>;
23
- toggleCheckedState: () => Promise<void>;
21
+ setChecked: () => void;
22
+ setUnchecked: () => void;
23
+ toggleCheckedState: () => void;
24
24
  getCheckedState: () => CheckedState;
25
25
  getCheckboxProps: () => Record<string, any>;
26
26
  };
@@ -47,9 +47,6 @@ export const syncDataLoaderFeature: FeatureImplementation = {
47
47
  (c) => c.data,
48
48
  );
49
49
  },
50
-
51
- loadItemData: ({ tree }, itemId) => tree.retrieveItemData(itemId),
52
- loadChildrenIds: ({ tree }, itemId) => tree.retrieveChildrenIds(itemId),
53
50
  },
54
51
 
55
52
  itemInstance: {
@@ -17,8 +17,8 @@ export type SyncDataLoaderFeatureDef<T> = {
17
17
  dataLoader: TreeDataLoader<T>;
18
18
  };
19
19
  treeInstance: {
20
- retrieveItemData: (itemId: string, skipFetch?: boolean) => T;
21
- retrieveChildrenIds: (itemId: string, skipFetch?: boolean) => string[];
20
+ retrieveItemData: (itemId: string) => T;
21
+ retrieveChildrenIds: (itemId: string) => string[];
22
22
  };
23
23
  itemInstance: {
24
24
  isLoading: () => boolean;
@@ -20,7 +20,7 @@ export type TreeFeatureDef<T> = {
20
20
  focusedItem: string | null;
21
21
  };
22
22
  config: {
23
- isItemFolder: (item: ItemInstance<T>) => boolean;
23
+ isItemFolder: (item: ItemInstance<T>) => boolean; // TODO:breaking use item data as payload
24
24
  getItemName: (item: ItemInstance<T>) => string;
25
25
 
26
26
  onPrimaryAction?: (item: ItemInstance<T>) => void;