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

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 (38) hide show
  1. package/CHANGELOG.md +1 -1
  2. package/lib/cjs/core/create-tree.js +9 -0
  3. package/lib/cjs/features/async-data-loader/feature.js +19 -6
  4. package/lib/cjs/features/async-data-loader/types.d.ts +4 -0
  5. package/lib/cjs/features/checkboxes/feature.d.ts +2 -0
  6. package/lib/cjs/features/checkboxes/feature.js +128 -0
  7. package/lib/cjs/features/checkboxes/types.d.ts +26 -0
  8. package/lib/cjs/features/checkboxes/types.js +9 -0
  9. package/lib/cjs/features/main/types.d.ts +2 -0
  10. package/lib/cjs/features/sync-data-loader/feature.js +2 -0
  11. package/lib/cjs/features/sync-data-loader/types.d.ts +2 -2
  12. package/lib/cjs/index.d.ts +2 -0
  13. package/lib/cjs/index.js +2 -0
  14. package/lib/cjs/types/core.d.ts +2 -1
  15. package/lib/esm/core/create-tree.js +9 -0
  16. package/lib/esm/features/async-data-loader/feature.js +19 -6
  17. package/lib/esm/features/async-data-loader/types.d.ts +4 -0
  18. package/lib/esm/features/checkboxes/feature.d.ts +2 -0
  19. package/lib/esm/features/checkboxes/feature.js +125 -0
  20. package/lib/esm/features/checkboxes/types.d.ts +26 -0
  21. package/lib/esm/features/checkboxes/types.js +6 -0
  22. package/lib/esm/features/main/types.d.ts +2 -0
  23. package/lib/esm/features/sync-data-loader/feature.js +2 -0
  24. package/lib/esm/features/sync-data-loader/types.d.ts +2 -2
  25. package/lib/esm/index.d.ts +2 -0
  26. package/lib/esm/index.js +2 -0
  27. package/lib/esm/types/core.d.ts +2 -1
  28. package/package.json +1 -1
  29. package/src/core/create-tree.ts +13 -0
  30. package/src/features/async-data-loader/feature.ts +25 -6
  31. package/src/features/async-data-loader/types.ts +4 -0
  32. package/src/features/checkboxes/feature.ts +150 -0
  33. package/src/features/checkboxes/types.ts +28 -0
  34. package/src/features/main/types.ts +2 -0
  35. package/src/features/sync-data-loader/feature.ts +3 -0
  36. package/src/features/sync-data-loader/types.ts +2 -2
  37. package/src/index.ts +2 -0
  38. package/src/types/core.ts +2 -0
package/CHANGELOG.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @headless-tree/core
2
2
 
3
- ## 0.0.0-20250509160455
3
+ ## 0.0.0-20250509212452
4
4
 
5
5
  ### Minor Changes
6
6
 
@@ -100,6 +100,15 @@ const createTree = (initialConfig) => {
100
100
  const externalStateSetter = config[stateHandlerNames[stateName]];
101
101
  externalStateSetter === null || externalStateSetter === void 0 ? void 0 : externalStateSetter(state[stateName]);
102
102
  },
103
+ buildItemInstance: ({}, itemId) => {
104
+ const [instance, finalizeInstance] = buildInstance(features, "itemInstance", (instance) => ({
105
+ item: instance,
106
+ tree: treeInstance,
107
+ itemId,
108
+ }));
109
+ finalizeInstance();
110
+ return instance;
111
+ },
103
112
  // TODO rebuildSubTree: (itemId: string) => void;
104
113
  rebuildTree: () => {
105
114
  var _a;
@@ -29,14 +29,17 @@ 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;
32
33
  });
33
34
  const loadChildrenIds = (tree, itemId) => __awaiter(void 0, void 0, void 0, function* () {
34
35
  var _a, _b, _c, _d, _e;
35
36
  const config = tree.getConfig();
36
37
  const dataRef = getDataRef(tree);
38
+ let childrenIds;
39
+ // TODO is folder check?
37
40
  if ("getChildrenWithData" in config.dataLoader) {
38
41
  const children = yield config.dataLoader.getChildrenWithData(itemId);
39
- const childrenIds = children.map((c) => c.id);
42
+ childrenIds = children.map((c) => c.id);
40
43
  dataRef.current.childrenIds[itemId] = childrenIds;
41
44
  children.forEach(({ id, data }) => {
42
45
  var _a, _b, _c;
@@ -50,7 +53,7 @@ const loadChildrenIds = (tree, itemId) => __awaiter(void 0, void 0, void 0, func
50
53
  tree.applySubStateUpdate("loadingItemData", (loadingItemData) => loadingItemData.filter((id) => !childrenIds.includes(id)));
51
54
  }
52
55
  else {
53
- const childrenIds = yield config.dataLoader.getChildren(itemId);
56
+ childrenIds = yield config.dataLoader.getChildren(itemId);
54
57
  dataRef.current.childrenIds[itemId] = childrenIds;
55
58
  (_b = config.onLoadedChildren) === null || _b === void 0 ? void 0 : _b.call(config, itemId, childrenIds);
56
59
  tree.rebuildTree();
@@ -58,6 +61,7 @@ const loadChildrenIds = (tree, itemId) => __awaiter(void 0, void 0, void 0, func
58
61
  tree.applySubStateUpdate("loadingItemChildrens", (loadingItemChildrens) => loadingItemChildrens.filter((id) => id !== itemId));
59
62
  (_d = (_c = dataRef.current.awaitingItemChildrensLoading) === null || _c === void 0 ? void 0 : _c[itemId]) === null || _d === void 0 ? void 0 : _d.forEach((cb) => cb());
60
63
  (_e = dataRef.current.awaitingItemChildrensLoading) === null || _e === void 0 ? true : delete _e[itemId];
64
+ return childrenIds;
61
65
  });
62
66
  exports.asyncDataLoaderFeature = {
63
67
  key: "async-data-loader",
@@ -83,6 +87,7 @@ exports.asyncDataLoaderFeature = {
83
87
  });
84
88
  }),
85
89
  waitForItemChildrenLoaded: (_a, itemId_1) => __awaiter(void 0, [_a, itemId_1], void 0, function* ({ tree }, itemId) {
90
+ // TODO replace inner implementation with load() fns
86
91
  tree.retrieveChildrenIds(itemId);
87
92
  if (!tree.getState().loadingItemChildrens.includes(itemId)) {
88
93
  return;
@@ -96,14 +101,22 @@ exports.asyncDataLoaderFeature = {
96
101
  dataRef.current.awaitingItemChildrensLoading[itemId].push(resolve);
97
102
  });
98
103
  }),
99
- retrieveItemData: ({ tree }, itemId) => {
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
+ retrieveItemData: ({ tree }, itemId, skipFetch = false) => {
100
113
  var _a, _b;
101
114
  const config = tree.getConfig();
102
115
  const dataRef = getDataRef(tree);
103
116
  if (dataRef.current.itemData[itemId]) {
104
117
  return dataRef.current.itemData[itemId];
105
118
  }
106
- if (!tree.getState().loadingItemData.includes(itemId)) {
119
+ if (!tree.getState().loadingItemData.includes(itemId) && !skipFetch) {
107
120
  tree.applySubStateUpdate("loadingItemData", (loadingItemData) => [
108
121
  ...loadingItemData,
109
122
  itemId,
@@ -112,12 +125,12 @@ exports.asyncDataLoaderFeature = {
112
125
  }
113
126
  return (_b = (_a = config.createLoadingItemData) === null || _a === void 0 ? void 0 : _a.call(config)) !== null && _b !== void 0 ? _b : null;
114
127
  },
115
- retrieveChildrenIds: ({ tree }, itemId) => {
128
+ retrieveChildrenIds: ({ tree }, itemId, skipFetch = false) => {
116
129
  const dataRef = getDataRef(tree);
117
130
  if (dataRef.current.childrenIds[itemId]) {
118
131
  return dataRef.current.childrenIds[itemId];
119
132
  }
120
- if (tree.getState().loadingItemChildrens.includes(itemId)) {
133
+ if (tree.getState().loadingItemChildrens.includes(itemId) || skipFetch) {
121
134
  return [];
122
135
  }
123
136
  tree.applySubStateUpdate("loadingItemChildrens", (loadingItemChildrens) => [...loadingItemChildrens, itemId]);
@@ -27,8 +27,12 @@ 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 */
30
31
  waitForItemDataLoaded: (itemId: string) => Promise<void>;
32
+ /** @deprecated use loadChildrenIds instead */
31
33
  waitForItemChildrenLoaded: (itemId: string) => Promise<void>;
34
+ loadItemData: (itemId: string) => Promise<T>;
35
+ loadChildrenIds: (itemId: string) => Promise<string[]>;
32
36
  };
33
37
  itemInstance: SyncDataLoaderFeatureDef<T>["itemInstance"] & {
34
38
  /** Invalidate fetched data for item, and triggers a refetch and subsequent rerender if the item is visible
@@ -0,0 +1,2 @@
1
+ import { FeatureImplementation } from "../../types/core";
2
+ export declare const checkboxesFeature: FeatureImplementation;
@@ -0,0 +1,128 @@
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
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.checkboxesFeature = void 0;
13
+ const utils_1 = require("../../utils");
14
+ 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
+ });
32
+ const getAllLoadedDescendants = (tree, itemId) => {
33
+ const children = tree.retrieveChildrenIds(itemId, true);
34
+ return [
35
+ itemId,
36
+ ...children.map((child) => getAllLoadedDescendants(tree, child)).flat(),
37
+ ];
38
+ };
39
+ exports.checkboxesFeature = {
40
+ key: "checkboxes",
41
+ overwrites: ["selection"],
42
+ getInitialState: (initialState) => (Object.assign({ checkedItems: [] }, initialState)),
43
+ getDefaultConfig: (defaultConfig, tree) => (Object.assign({ setCheckedItems: (0, utils_1.makeStateUpdater)("checkedItems", tree) }, defaultConfig)),
44
+ stateHandlerNames: {
45
+ checkedItems: "setCheckedItems",
46
+ },
47
+ treeInstance: {
48
+ setCheckedItems: ({ tree }, checkedItems) => {
49
+ tree.applySubStateUpdate("checkedItems", checkedItems);
50
+ },
51
+ },
52
+ itemInstance: {
53
+ getCheckboxProps: ({ item, itemId }) => {
54
+ const checkedState = item.getCheckedState();
55
+ // console.log("prop", itemId, checkedState);
56
+ return {
57
+ onChange: item.toggleCheckedState,
58
+ checked: checkedState === types_1.CheckedState.Checked,
59
+ ref: (r) => {
60
+ if (r) {
61
+ // console.log("ref", itemId, checkedState);
62
+ r.indeterminate = checkedState === types_1.CheckedState.Indeterminate;
63
+ }
64
+ },
65
+ };
66
+ },
67
+ toggleCheckedState: (_a) => __awaiter(void 0, [_a], void 0, function* ({ item }) {
68
+ if (item.getCheckedState() === types_1.CheckedState.Checked) {
69
+ yield item.setUnchecked();
70
+ }
71
+ else {
72
+ yield item.setChecked();
73
+ }
74
+ }),
75
+ getCheckedState: ({ item, tree, itemId }) => {
76
+ // TODO checkedcache
77
+ const { checkedItems } = tree.getState();
78
+ if (checkedItems.includes(itemId)) {
79
+ return types_1.CheckedState.Checked;
80
+ }
81
+ if (item.isFolder()) {
82
+ const descendants = getAllLoadedDescendants(tree, itemId);
83
+ console.log("descendants of ", itemId, descendants);
84
+ if (descendants.every((d) => checkedItems.includes(d))) {
85
+ return types_1.CheckedState.Checked;
86
+ }
87
+ if (descendants.some((d) => checkedItems.includes(d))) {
88
+ return types_1.CheckedState.Indeterminate;
89
+ }
90
+ }
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
+ return types_1.CheckedState.Unchecked;
105
+ },
106
+ setChecked: (_a) => __awaiter(void 0, [_a], void 0, function* ({ item, tree, itemId }) {
107
+ if (!item.isFolder() || tree.getConfig().canCheckFolders) {
108
+ tree.applySubStateUpdate("checkedItems", (items) => [...items, itemId]);
109
+ }
110
+ else {
111
+ const descendants = yield fetchAllDescendants(tree, itemId);
112
+ tree.applySubStateUpdate("checkedItems", (items) => [
113
+ ...items,
114
+ ...descendants,
115
+ ]);
116
+ }
117
+ }),
118
+ setUnchecked: (_a) => __awaiter(void 0, [_a], void 0, function* ({ item, tree, itemId }) {
119
+ if (!item.isFolder() || tree.getConfig().canCheckFolders) {
120
+ tree.applySubStateUpdate("checkedItems", (items) => items.filter((id) => id !== itemId));
121
+ }
122
+ else {
123
+ yield tree.loadChildrenIds(itemId);
124
+ item.getChildren().forEach((item) => item.setUnchecked());
125
+ }
126
+ }),
127
+ },
128
+ };
@@ -0,0 +1,26 @@
1
+ import { SetStateFn } from "../../types/core";
2
+ export declare enum CheckedState {
3
+ Checked = "checked",
4
+ Unchecked = "unchecked",
5
+ Indeterminate = "indeterminate"
6
+ }
7
+ export type CheckboxesFeatureDef<T> = {
8
+ state: {
9
+ checkedItems: string[];
10
+ };
11
+ config: {
12
+ setCheckedItems?: SetStateFn<string[]>;
13
+ canCheckFolders?: boolean;
14
+ };
15
+ treeInstance: {
16
+ setCheckedItems: (checkedItems: string[]) => void;
17
+ };
18
+ itemInstance: {
19
+ setChecked: () => Promise<void>;
20
+ setUnchecked: () => Promise<void>;
21
+ toggleCheckedState: () => Promise<void>;
22
+ getCheckedState: () => CheckedState;
23
+ getCheckboxProps: () => Record<string, any>;
24
+ };
25
+ hotkeys: never;
26
+ };
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CheckedState = void 0;
4
+ var CheckedState;
5
+ (function (CheckedState) {
6
+ CheckedState["Checked"] = "checked";
7
+ CheckedState["Unchecked"] = "unchecked";
8
+ CheckedState["Indeterminate"] = "indeterminate";
9
+ })(CheckedState || (exports.CheckedState = CheckedState = {}));
@@ -17,6 +17,8 @@ export type MainFeatureDef<T = any> = {
17
17
  treeInstance: {
18
18
  /** @internal */
19
19
  applySubStateUpdate: <K extends keyof TreeState<any>>(stateName: K, updater: Updater<TreeState<T>[K]>) => void;
20
+ /** @internal */
21
+ buildItemInstance: (itemId: string) => ItemInstance<T>;
20
22
  setState: SetStateFn<TreeState<T>>;
21
23
  getState: () => TreeState<T>;
22
24
  setConfig: SetStateFn<TreeConfig<T>>;
@@ -40,6 +40,8 @@ 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),
43
45
  },
44
46
  itemInstance: {
45
47
  isLoading: () => false,
@@ -18,8 +18,8 @@ export type SyncDataLoaderFeatureDef<T> = {
18
18
  dataLoader: TreeDataLoader<T>;
19
19
  };
20
20
  treeInstance: {
21
- retrieveItemData: (itemId: string) => T;
22
- retrieveChildrenIds: (itemId: string) => string[];
21
+ retrieveItemData: (itemId: string, skipFetch?: boolean) => T;
22
+ retrieveChildrenIds: (itemId: string, skipFetch?: boolean) => string[];
23
23
  };
24
24
  itemInstance: {
25
25
  isLoading: () => boolean;
@@ -5,6 +5,7 @@ export { MainFeatureDef, InstanceBuilder } from "./features/main/types";
5
5
  export * from "./features/drag-and-drop/types";
6
6
  export * from "./features/keyboard-drag-and-drop/types";
7
7
  export * from "./features/selection/types";
8
+ export * from "./features/checkboxes/types";
8
9
  export * from "./features/async-data-loader/types";
9
10
  export * from "./features/sync-data-loader/types";
10
11
  export * from "./features/hotkeys-core/types";
@@ -13,6 +14,7 @@ export * from "./features/renaming/types";
13
14
  export * from "./features/expand-all/types";
14
15
  export * from "./features/prop-memoization/types";
15
16
  export * from "./features/selection/feature";
17
+ export * from "./features/checkboxes/feature";
16
18
  export * from "./features/hotkeys-core/feature";
17
19
  export * from "./features/async-data-loader/feature";
18
20
  export * from "./features/sync-data-loader/feature";
package/lib/cjs/index.js CHANGED
@@ -20,6 +20,7 @@ __exportStar(require("./features/tree/types"), exports);
20
20
  __exportStar(require("./features/drag-and-drop/types"), exports);
21
21
  __exportStar(require("./features/keyboard-drag-and-drop/types"), exports);
22
22
  __exportStar(require("./features/selection/types"), exports);
23
+ __exportStar(require("./features/checkboxes/types"), exports);
23
24
  __exportStar(require("./features/async-data-loader/types"), exports);
24
25
  __exportStar(require("./features/sync-data-loader/types"), exports);
25
26
  __exportStar(require("./features/hotkeys-core/types"), exports);
@@ -28,6 +29,7 @@ __exportStar(require("./features/renaming/types"), exports);
28
29
  __exportStar(require("./features/expand-all/types"), exports);
29
30
  __exportStar(require("./features/prop-memoization/types"), exports);
30
31
  __exportStar(require("./features/selection/feature"), exports);
32
+ __exportStar(require("./features/checkboxes/feature"), exports);
31
33
  __exportStar(require("./features/hotkeys-core/feature"), exports);
32
34
  __exportStar(require("./features/async-data-loader/feature"), exports);
33
35
  __exportStar(require("./features/sync-data-loader/feature"), exports);
@@ -10,6 +10,7 @@ import { RenamingFeatureDef } from "../features/renaming/types";
10
10
  import { ExpandAllFeatureDef } from "../features/expand-all/types";
11
11
  import { PropMemoizationFeatureDef } from "../features/prop-memoization/types";
12
12
  import { KeyboardDragAndDropFeatureDef } from "../features/keyboard-drag-and-drop/types";
13
+ import { CheckboxesFeatureDef } from "../features/checkboxes/types";
13
14
  export type Updater<T> = T | ((old: T) => T);
14
15
  export type SetStateFn<T> = (updaterOrValue: Updater<T>) => void;
15
16
  export type FeatureDef = {
@@ -34,7 +35,7 @@ type MergedFeatures<F extends FeatureDef> = {
34
35
  itemInstance: UnionToIntersection<F["itemInstance"]>;
35
36
  hotkeys: F["hotkeys"];
36
37
  };
37
- export type RegisteredFeatures<T> = MainFeatureDef<T> | TreeFeatureDef<T> | SelectionFeatureDef<T> | DragAndDropFeatureDef<T> | KeyboardDragAndDropFeatureDef<T> | HotkeysCoreFeatureDef<T> | SyncDataLoaderFeatureDef<T> | AsyncDataLoaderFeatureDef<T> | SearchFeatureDef<T> | RenamingFeatureDef<T> | ExpandAllFeatureDef | PropMemoizationFeatureDef;
38
+ export type RegisteredFeatures<T> = MainFeatureDef<T> | TreeFeatureDef<T> | SelectionFeatureDef<T> | CheckboxesFeatureDef<T> | DragAndDropFeatureDef<T> | KeyboardDragAndDropFeatureDef<T> | HotkeysCoreFeatureDef<T> | SyncDataLoaderFeatureDef<T> | AsyncDataLoaderFeatureDef<T> | SearchFeatureDef<T> | RenamingFeatureDef<T> | ExpandAllFeatureDef | PropMemoizationFeatureDef;
38
39
  type TreeStateType<T> = MergedFeatures<RegisteredFeatures<T>>["state"];
39
40
  export interface TreeState<T> extends TreeStateType<T> {
40
41
  }
@@ -97,6 +97,15 @@ export const createTree = (initialConfig) => {
97
97
  const externalStateSetter = config[stateHandlerNames[stateName]];
98
98
  externalStateSetter === null || externalStateSetter === void 0 ? void 0 : externalStateSetter(state[stateName]);
99
99
  },
100
+ buildItemInstance: ({}, itemId) => {
101
+ const [instance, finalizeInstance] = buildInstance(features, "itemInstance", (instance) => ({
102
+ item: instance,
103
+ tree: treeInstance,
104
+ itemId,
105
+ }));
106
+ finalizeInstance();
107
+ return instance;
108
+ },
100
109
  // TODO rebuildSubTree: (itemId: string) => void;
101
110
  rebuildTree: () => {
102
111
  var _a;
@@ -26,14 +26,17 @@ 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;
29
30
  });
30
31
  const loadChildrenIds = (tree, itemId) => __awaiter(void 0, void 0, void 0, function* () {
31
32
  var _a, _b, _c, _d, _e;
32
33
  const config = tree.getConfig();
33
34
  const dataRef = getDataRef(tree);
35
+ let childrenIds;
36
+ // TODO is folder check?
34
37
  if ("getChildrenWithData" in config.dataLoader) {
35
38
  const children = yield config.dataLoader.getChildrenWithData(itemId);
36
- const childrenIds = children.map((c) => c.id);
39
+ childrenIds = children.map((c) => c.id);
37
40
  dataRef.current.childrenIds[itemId] = childrenIds;
38
41
  children.forEach(({ id, data }) => {
39
42
  var _a, _b, _c;
@@ -47,7 +50,7 @@ const loadChildrenIds = (tree, itemId) => __awaiter(void 0, void 0, void 0, func
47
50
  tree.applySubStateUpdate("loadingItemData", (loadingItemData) => loadingItemData.filter((id) => !childrenIds.includes(id)));
48
51
  }
49
52
  else {
50
- const childrenIds = yield config.dataLoader.getChildren(itemId);
53
+ childrenIds = yield config.dataLoader.getChildren(itemId);
51
54
  dataRef.current.childrenIds[itemId] = childrenIds;
52
55
  (_b = config.onLoadedChildren) === null || _b === void 0 ? void 0 : _b.call(config, itemId, childrenIds);
53
56
  tree.rebuildTree();
@@ -55,6 +58,7 @@ const loadChildrenIds = (tree, itemId) => __awaiter(void 0, void 0, void 0, func
55
58
  tree.applySubStateUpdate("loadingItemChildrens", (loadingItemChildrens) => loadingItemChildrens.filter((id) => id !== itemId));
56
59
  (_d = (_c = dataRef.current.awaitingItemChildrensLoading) === null || _c === void 0 ? void 0 : _c[itemId]) === null || _d === void 0 ? void 0 : _d.forEach((cb) => cb());
57
60
  (_e = dataRef.current.awaitingItemChildrensLoading) === null || _e === void 0 ? true : delete _e[itemId];
61
+ return childrenIds;
58
62
  });
59
63
  export const asyncDataLoaderFeature = {
60
64
  key: "async-data-loader",
@@ -80,6 +84,7 @@ export const asyncDataLoaderFeature = {
80
84
  });
81
85
  }),
82
86
  waitForItemChildrenLoaded: (_a, itemId_1) => __awaiter(void 0, [_a, itemId_1], void 0, function* ({ tree }, itemId) {
87
+ // TODO replace inner implementation with load() fns
83
88
  tree.retrieveChildrenIds(itemId);
84
89
  if (!tree.getState().loadingItemChildrens.includes(itemId)) {
85
90
  return;
@@ -93,14 +98,22 @@ export const asyncDataLoaderFeature = {
93
98
  dataRef.current.awaitingItemChildrensLoading[itemId].push(resolve);
94
99
  });
95
100
  }),
96
- retrieveItemData: ({ tree }, itemId) => {
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
+ retrieveItemData: ({ tree }, itemId, skipFetch = false) => {
97
110
  var _a, _b;
98
111
  const config = tree.getConfig();
99
112
  const dataRef = getDataRef(tree);
100
113
  if (dataRef.current.itemData[itemId]) {
101
114
  return dataRef.current.itemData[itemId];
102
115
  }
103
- if (!tree.getState().loadingItemData.includes(itemId)) {
116
+ if (!tree.getState().loadingItemData.includes(itemId) && !skipFetch) {
104
117
  tree.applySubStateUpdate("loadingItemData", (loadingItemData) => [
105
118
  ...loadingItemData,
106
119
  itemId,
@@ -109,12 +122,12 @@ export const asyncDataLoaderFeature = {
109
122
  }
110
123
  return (_b = (_a = config.createLoadingItemData) === null || _a === void 0 ? void 0 : _a.call(config)) !== null && _b !== void 0 ? _b : null;
111
124
  },
112
- retrieveChildrenIds: ({ tree }, itemId) => {
125
+ retrieveChildrenIds: ({ tree }, itemId, skipFetch = false) => {
113
126
  const dataRef = getDataRef(tree);
114
127
  if (dataRef.current.childrenIds[itemId]) {
115
128
  return dataRef.current.childrenIds[itemId];
116
129
  }
117
- if (tree.getState().loadingItemChildrens.includes(itemId)) {
130
+ if (tree.getState().loadingItemChildrens.includes(itemId) || skipFetch) {
118
131
  return [];
119
132
  }
120
133
  tree.applySubStateUpdate("loadingItemChildrens", (loadingItemChildrens) => [...loadingItemChildrens, itemId]);
@@ -27,8 +27,12 @@ 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 */
30
31
  waitForItemDataLoaded: (itemId: string) => Promise<void>;
32
+ /** @deprecated use loadChildrenIds instead */
31
33
  waitForItemChildrenLoaded: (itemId: string) => Promise<void>;
34
+ loadItemData: (itemId: string) => Promise<T>;
35
+ loadChildrenIds: (itemId: string) => Promise<string[]>;
32
36
  };
33
37
  itemInstance: SyncDataLoaderFeatureDef<T>["itemInstance"] & {
34
38
  /** Invalidate fetched data for item, and triggers a refetch and subsequent rerender if the item is visible
@@ -0,0 +1,2 @@
1
+ import { FeatureImplementation } from "../../types/core";
2
+ export declare const checkboxesFeature: FeatureImplementation;
@@ -0,0 +1,125 @@
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
+ import { makeStateUpdater } from "../../utils";
11
+ 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
+ });
29
+ const getAllLoadedDescendants = (tree, itemId) => {
30
+ const children = tree.retrieveChildrenIds(itemId, true);
31
+ return [
32
+ itemId,
33
+ ...children.map((child) => getAllLoadedDescendants(tree, child)).flat(),
34
+ ];
35
+ };
36
+ export const checkboxesFeature = {
37
+ key: "checkboxes",
38
+ overwrites: ["selection"],
39
+ getInitialState: (initialState) => (Object.assign({ checkedItems: [] }, initialState)),
40
+ getDefaultConfig: (defaultConfig, tree) => (Object.assign({ setCheckedItems: makeStateUpdater("checkedItems", tree) }, defaultConfig)),
41
+ stateHandlerNames: {
42
+ checkedItems: "setCheckedItems",
43
+ },
44
+ treeInstance: {
45
+ setCheckedItems: ({ tree }, checkedItems) => {
46
+ tree.applySubStateUpdate("checkedItems", checkedItems);
47
+ },
48
+ },
49
+ itemInstance: {
50
+ getCheckboxProps: ({ item, itemId }) => {
51
+ const checkedState = item.getCheckedState();
52
+ // console.log("prop", itemId, checkedState);
53
+ return {
54
+ onChange: item.toggleCheckedState,
55
+ checked: checkedState === CheckedState.Checked,
56
+ ref: (r) => {
57
+ if (r) {
58
+ // console.log("ref", itemId, checkedState);
59
+ r.indeterminate = checkedState === CheckedState.Indeterminate;
60
+ }
61
+ },
62
+ };
63
+ },
64
+ toggleCheckedState: (_a) => __awaiter(void 0, [_a], void 0, function* ({ item }) {
65
+ if (item.getCheckedState() === CheckedState.Checked) {
66
+ yield item.setUnchecked();
67
+ }
68
+ else {
69
+ yield item.setChecked();
70
+ }
71
+ }),
72
+ getCheckedState: ({ item, tree, itemId }) => {
73
+ // TODO checkedcache
74
+ const { checkedItems } = tree.getState();
75
+ if (checkedItems.includes(itemId)) {
76
+ return CheckedState.Checked;
77
+ }
78
+ if (item.isFolder()) {
79
+ const descendants = getAllLoadedDescendants(tree, itemId);
80
+ console.log("descendants of ", itemId, descendants);
81
+ if (descendants.every((d) => checkedItems.includes(d))) {
82
+ return CheckedState.Checked;
83
+ }
84
+ if (descendants.some((d) => checkedItems.includes(d))) {
85
+ return CheckedState.Indeterminate;
86
+ }
87
+ }
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
+ return CheckedState.Unchecked;
102
+ },
103
+ setChecked: (_a) => __awaiter(void 0, [_a], void 0, function* ({ item, tree, itemId }) {
104
+ if (!item.isFolder() || tree.getConfig().canCheckFolders) {
105
+ tree.applySubStateUpdate("checkedItems", (items) => [...items, itemId]);
106
+ }
107
+ else {
108
+ const descendants = yield fetchAllDescendants(tree, itemId);
109
+ tree.applySubStateUpdate("checkedItems", (items) => [
110
+ ...items,
111
+ ...descendants,
112
+ ]);
113
+ }
114
+ }),
115
+ setUnchecked: (_a) => __awaiter(void 0, [_a], void 0, function* ({ item, tree, itemId }) {
116
+ if (!item.isFolder() || tree.getConfig().canCheckFolders) {
117
+ tree.applySubStateUpdate("checkedItems", (items) => items.filter((id) => id !== itemId));
118
+ }
119
+ else {
120
+ yield tree.loadChildrenIds(itemId);
121
+ item.getChildren().forEach((item) => item.setUnchecked());
122
+ }
123
+ }),
124
+ },
125
+ };
@@ -0,0 +1,26 @@
1
+ import { SetStateFn } from "../../types/core";
2
+ export declare enum CheckedState {
3
+ Checked = "checked",
4
+ Unchecked = "unchecked",
5
+ Indeterminate = "indeterminate"
6
+ }
7
+ export type CheckboxesFeatureDef<T> = {
8
+ state: {
9
+ checkedItems: string[];
10
+ };
11
+ config: {
12
+ setCheckedItems?: SetStateFn<string[]>;
13
+ canCheckFolders?: boolean;
14
+ };
15
+ treeInstance: {
16
+ setCheckedItems: (checkedItems: string[]) => void;
17
+ };
18
+ itemInstance: {
19
+ setChecked: () => Promise<void>;
20
+ setUnchecked: () => Promise<void>;
21
+ toggleCheckedState: () => Promise<void>;
22
+ getCheckedState: () => CheckedState;
23
+ getCheckboxProps: () => Record<string, any>;
24
+ };
25
+ hotkeys: never;
26
+ };
@@ -0,0 +1,6 @@
1
+ export var CheckedState;
2
+ (function (CheckedState) {
3
+ CheckedState["Checked"] = "checked";
4
+ CheckedState["Unchecked"] = "unchecked";
5
+ CheckedState["Indeterminate"] = "indeterminate";
6
+ })(CheckedState || (CheckedState = {}));
@@ -17,6 +17,8 @@ export type MainFeatureDef<T = any> = {
17
17
  treeInstance: {
18
18
  /** @internal */
19
19
  applySubStateUpdate: <K extends keyof TreeState<any>>(stateName: K, updater: Updater<TreeState<T>[K]>) => void;
20
+ /** @internal */
21
+ buildItemInstance: (itemId: string) => ItemInstance<T>;
20
22
  setState: SetStateFn<TreeState<T>>;
21
23
  getState: () => TreeState<T>;
22
24
  setConfig: SetStateFn<TreeConfig<T>>;
@@ -37,6 +37,8 @@ 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),
40
42
  },
41
43
  itemInstance: {
42
44
  isLoading: () => false,
@@ -18,8 +18,8 @@ export type SyncDataLoaderFeatureDef<T> = {
18
18
  dataLoader: TreeDataLoader<T>;
19
19
  };
20
20
  treeInstance: {
21
- retrieveItemData: (itemId: string) => T;
22
- retrieveChildrenIds: (itemId: string) => string[];
21
+ retrieveItemData: (itemId: string, skipFetch?: boolean) => T;
22
+ retrieveChildrenIds: (itemId: string, skipFetch?: boolean) => string[];
23
23
  };
24
24
  itemInstance: {
25
25
  isLoading: () => boolean;
@@ -5,6 +5,7 @@ export { MainFeatureDef, InstanceBuilder } from "./features/main/types";
5
5
  export * from "./features/drag-and-drop/types";
6
6
  export * from "./features/keyboard-drag-and-drop/types";
7
7
  export * from "./features/selection/types";
8
+ export * from "./features/checkboxes/types";
8
9
  export * from "./features/async-data-loader/types";
9
10
  export * from "./features/sync-data-loader/types";
10
11
  export * from "./features/hotkeys-core/types";
@@ -13,6 +14,7 @@ export * from "./features/renaming/types";
13
14
  export * from "./features/expand-all/types";
14
15
  export * from "./features/prop-memoization/types";
15
16
  export * from "./features/selection/feature";
17
+ export * from "./features/checkboxes/feature";
16
18
  export * from "./features/hotkeys-core/feature";
17
19
  export * from "./features/async-data-loader/feature";
18
20
  export * from "./features/sync-data-loader/feature";
package/lib/esm/index.js CHANGED
@@ -4,6 +4,7 @@ export * from "./features/tree/types";
4
4
  export * from "./features/drag-and-drop/types";
5
5
  export * from "./features/keyboard-drag-and-drop/types";
6
6
  export * from "./features/selection/types";
7
+ export * from "./features/checkboxes/types";
7
8
  export * from "./features/async-data-loader/types";
8
9
  export * from "./features/sync-data-loader/types";
9
10
  export * from "./features/hotkeys-core/types";
@@ -12,6 +13,7 @@ export * from "./features/renaming/types";
12
13
  export * from "./features/expand-all/types";
13
14
  export * from "./features/prop-memoization/types";
14
15
  export * from "./features/selection/feature";
16
+ export * from "./features/checkboxes/feature";
15
17
  export * from "./features/hotkeys-core/feature";
16
18
  export * from "./features/async-data-loader/feature";
17
19
  export * from "./features/sync-data-loader/feature";
@@ -10,6 +10,7 @@ import { RenamingFeatureDef } from "../features/renaming/types";
10
10
  import { ExpandAllFeatureDef } from "../features/expand-all/types";
11
11
  import { PropMemoizationFeatureDef } from "../features/prop-memoization/types";
12
12
  import { KeyboardDragAndDropFeatureDef } from "../features/keyboard-drag-and-drop/types";
13
+ import { CheckboxesFeatureDef } from "../features/checkboxes/types";
13
14
  export type Updater<T> = T | ((old: T) => T);
14
15
  export type SetStateFn<T> = (updaterOrValue: Updater<T>) => void;
15
16
  export type FeatureDef = {
@@ -34,7 +35,7 @@ type MergedFeatures<F extends FeatureDef> = {
34
35
  itemInstance: UnionToIntersection<F["itemInstance"]>;
35
36
  hotkeys: F["hotkeys"];
36
37
  };
37
- export type RegisteredFeatures<T> = MainFeatureDef<T> | TreeFeatureDef<T> | SelectionFeatureDef<T> | DragAndDropFeatureDef<T> | KeyboardDragAndDropFeatureDef<T> | HotkeysCoreFeatureDef<T> | SyncDataLoaderFeatureDef<T> | AsyncDataLoaderFeatureDef<T> | SearchFeatureDef<T> | RenamingFeatureDef<T> | ExpandAllFeatureDef | PropMemoizationFeatureDef;
38
+ export type RegisteredFeatures<T> = MainFeatureDef<T> | TreeFeatureDef<T> | SelectionFeatureDef<T> | CheckboxesFeatureDef<T> | DragAndDropFeatureDef<T> | KeyboardDragAndDropFeatureDef<T> | HotkeysCoreFeatureDef<T> | SyncDataLoaderFeatureDef<T> | AsyncDataLoaderFeatureDef<T> | SearchFeatureDef<T> | RenamingFeatureDef<T> | ExpandAllFeatureDef | PropMemoizationFeatureDef;
38
39
  type TreeStateType<T> = MergedFeatures<RegisteredFeatures<T>>["state"];
39
40
  export interface TreeState<T> extends TreeStateType<T> {
40
41
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@headless-tree/core",
3
- "version": "0.0.0-20250509160455",
3
+ "version": "0.0.0-20250509212452",
4
4
  "type": "module",
5
5
  "main": "lib/cjs/index.js",
6
6
  "module": "lib/esm/index.js",
@@ -151,6 +151,19 @@ export const createTree = <T>(
151
151
  ] as Function;
152
152
  externalStateSetter?.(state[stateName]);
153
153
  },
154
+ buildItemInstance: ({}, itemId) => {
155
+ const [instance, finalizeInstance] = buildInstance(
156
+ features,
157
+ "itemInstance",
158
+ (instance) => ({
159
+ item: instance,
160
+ tree: treeInstance,
161
+ itemId,
162
+ }),
163
+ );
164
+ finalizeInstance();
165
+ return instance;
166
+ },
154
167
  // TODO rebuildSubTree: (itemId: string) => void;
155
168
  rebuildTree: () => {
156
169
  rebuildItemMeta();
@@ -22,15 +22,19 @@ 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;
25
26
  };
26
27
 
27
28
  const loadChildrenIds = async <T>(tree: TreeInstance<T>, itemId: string) => {
28
29
  const config = tree.getConfig();
29
30
  const dataRef = getDataRef(tree);
31
+ let childrenIds: string[];
32
+
33
+ // TODO is folder check?
30
34
 
31
35
  if ("getChildrenWithData" in config.dataLoader) {
32
36
  const children = await config.dataLoader.getChildrenWithData(itemId);
33
- const childrenIds = children.map((c) => c.id);
37
+ childrenIds = children.map((c) => c.id);
34
38
  dataRef.current.childrenIds[itemId] = childrenIds;
35
39
  children.forEach(({ id, data }) => {
36
40
  dataRef.current.itemData[id] = data;
@@ -45,7 +49,7 @@ const loadChildrenIds = async <T>(tree: TreeInstance<T>, itemId: string) => {
45
49
  loadingItemData.filter((id) => !childrenIds.includes(id)),
46
50
  );
47
51
  } else {
48
- const childrenIds = await config.dataLoader.getChildren(itemId);
52
+ childrenIds = await config.dataLoader.getChildren(itemId);
49
53
  dataRef.current.childrenIds[itemId] = childrenIds;
50
54
  config.onLoadedChildren?.(itemId, childrenIds);
51
55
  tree.rebuildTree();
@@ -57,6 +61,7 @@ const loadChildrenIds = async <T>(tree: TreeInstance<T>, itemId: string) => {
57
61
 
58
62
  dataRef.current.awaitingItemChildrensLoading?.[itemId]?.forEach((cb) => cb());
59
63
  delete dataRef.current.awaitingItemChildrensLoading?.[itemId];
64
+ return childrenIds;
60
65
  };
61
66
 
62
67
  export const asyncDataLoaderFeature: FeatureImplementation = {
@@ -94,6 +99,7 @@ export const asyncDataLoaderFeature: FeatureImplementation = {
94
99
  },
95
100
 
96
101
  waitForItemChildrenLoaded: async ({ tree }, itemId) => {
102
+ // TODO replace inner implementation with load() fns
97
103
  tree.retrieveChildrenIds(itemId);
98
104
  if (!tree.getState().loadingItemChildrens.includes(itemId)) {
99
105
  return;
@@ -106,7 +112,20 @@ export const asyncDataLoaderFeature: FeatureImplementation = {
106
112
  });
107
113
  },
108
114
 
109
- retrieveItemData: ({ tree }, itemId) => {
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
+ retrieveItemData: ({ tree }, itemId, skipFetch = false) => {
110
129
  const config = tree.getConfig();
111
130
  const dataRef = getDataRef(tree);
112
131
 
@@ -114,7 +133,7 @@ export const asyncDataLoaderFeature: FeatureImplementation = {
114
133
  return dataRef.current.itemData[itemId];
115
134
  }
116
135
 
117
- if (!tree.getState().loadingItemData.includes(itemId)) {
136
+ if (!tree.getState().loadingItemData.includes(itemId) && !skipFetch) {
118
137
  tree.applySubStateUpdate("loadingItemData", (loadingItemData) => [
119
138
  ...loadingItemData,
120
139
  itemId,
@@ -126,13 +145,13 @@ export const asyncDataLoaderFeature: FeatureImplementation = {
126
145
  return config.createLoadingItemData?.() ?? null;
127
146
  },
128
147
 
129
- retrieveChildrenIds: ({ tree }, itemId) => {
148
+ retrieveChildrenIds: ({ tree }, itemId, skipFetch = false) => {
130
149
  const dataRef = getDataRef(tree);
131
150
  if (dataRef.current.childrenIds[itemId]) {
132
151
  return dataRef.current.childrenIds[itemId];
133
152
  }
134
153
 
135
- if (tree.getState().loadingItemChildrens.includes(itemId)) {
154
+ if (tree.getState().loadingItemChildrens.includes(itemId) || skipFetch) {
136
155
  return [];
137
156
  }
138
157
 
@@ -32,8 +32,12 @@ 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 */
35
36
  waitForItemDataLoaded: (itemId: string) => Promise<void>;
37
+ /** @deprecated use loadChildrenIds instead */
36
38
  waitForItemChildrenLoaded: (itemId: string) => Promise<void>;
39
+ loadItemData: (itemId: string) => Promise<T>;
40
+ loadChildrenIds: (itemId: string) => Promise<string[]>;
37
41
  };
38
42
  itemInstance: SyncDataLoaderFeatureDef<T>["itemInstance"] & {
39
43
  /** Invalidate fetched data for item, and triggers a refetch and subsequent rerender if the item is visible
@@ -0,0 +1,150 @@
1
+ import { FeatureImplementation, TreeInstance } from "../../types/core";
2
+ import { makeStateUpdater } from "../../utils";
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
+ };
30
+
31
+ const getAllLoadedDescendants = <T>(
32
+ tree: TreeInstance<T>,
33
+ itemId: string,
34
+ ): string[] => {
35
+ const children = tree.retrieveChildrenIds(itemId, true);
36
+ return [
37
+ itemId,
38
+ ...children.map((child) => getAllLoadedDescendants(tree, child)).flat(),
39
+ ];
40
+ };
41
+
42
+ export const checkboxesFeature: FeatureImplementation = {
43
+ key: "checkboxes",
44
+
45
+ overwrites: ["selection"],
46
+
47
+ getInitialState: (initialState) => ({
48
+ checkedItems: [],
49
+ ...initialState,
50
+ }),
51
+
52
+ getDefaultConfig: (defaultConfig, tree) => ({
53
+ setCheckedItems: makeStateUpdater("checkedItems", tree),
54
+ ...defaultConfig,
55
+ }),
56
+
57
+ stateHandlerNames: {
58
+ checkedItems: "setCheckedItems",
59
+ },
60
+
61
+ treeInstance: {
62
+ setCheckedItems: ({ tree }, checkedItems) => {
63
+ tree.applySubStateUpdate("checkedItems", checkedItems);
64
+ },
65
+ },
66
+
67
+ itemInstance: {
68
+ getCheckboxProps: ({ item, itemId }) => {
69
+ const checkedState = item.getCheckedState();
70
+ // console.log("prop", itemId, checkedState);
71
+ return {
72
+ onChange: item.toggleCheckedState,
73
+ checked: checkedState === CheckedState.Checked,
74
+ ref: (r: any) => {
75
+ if (r) {
76
+ // console.log("ref", itemId, checkedState);
77
+ r.indeterminate = checkedState === CheckedState.Indeterminate;
78
+ }
79
+ },
80
+ };
81
+ },
82
+
83
+ toggleCheckedState: async ({ item }) => {
84
+ if (item.getCheckedState() === CheckedState.Checked) {
85
+ await item.setUnchecked();
86
+ } else {
87
+ await item.setChecked();
88
+ }
89
+ },
90
+
91
+ getCheckedState: ({ item, tree, itemId }) => {
92
+ // TODO checkedcache
93
+ const { checkedItems } = tree.getState();
94
+
95
+ if (checkedItems.includes(itemId)) {
96
+ return CheckedState.Checked;
97
+ }
98
+
99
+ if (item.isFolder()) {
100
+ const descendants = getAllLoadedDescendants(tree, itemId);
101
+ console.log("descendants of ", itemId, descendants);
102
+ if (descendants.every((d) => checkedItems.includes(d))) {
103
+ return CheckedState.Checked;
104
+ }
105
+ if (descendants.some((d) => checkedItems.includes(d))) {
106
+ return CheckedState.Indeterminate;
107
+ }
108
+ }
109
+
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
+ return CheckedState.Unchecked;
125
+ },
126
+
127
+ setChecked: async ({ item, tree, itemId }) => {
128
+ if (!item.isFolder() || tree.getConfig().canCheckFolders) {
129
+ tree.applySubStateUpdate("checkedItems", (items) => [...items, itemId]);
130
+ } else {
131
+ const descendants = await fetchAllDescendants(tree, itemId);
132
+ tree.applySubStateUpdate("checkedItems", (items) => [
133
+ ...items,
134
+ ...descendants,
135
+ ]);
136
+ }
137
+ },
138
+
139
+ setUnchecked: async ({ item, tree, itemId }) => {
140
+ if (!item.isFolder() || tree.getConfig().canCheckFolders) {
141
+ tree.applySubStateUpdate("checkedItems", (items) =>
142
+ items.filter((id) => id !== itemId),
143
+ );
144
+ } else {
145
+ await tree.loadChildrenIds(itemId);
146
+ item.getChildren().forEach((item) => item.setUnchecked());
147
+ }
148
+ },
149
+ },
150
+ };
@@ -0,0 +1,28 @@
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
+ };
17
+ treeInstance: {
18
+ setCheckedItems: (checkedItems: string[]) => void;
19
+ };
20
+ itemInstance: {
21
+ setChecked: () => Promise<void>;
22
+ setUnchecked: () => Promise<void>;
23
+ toggleCheckedState: () => Promise<void>;
24
+ getCheckedState: () => CheckedState;
25
+ getCheckboxProps: () => Record<string, any>;
26
+ };
27
+ hotkeys: never;
28
+ };
@@ -36,6 +36,8 @@ export type MainFeatureDef<T = any> = {
36
36
  stateName: K,
37
37
  updater: Updater<TreeState<T>[K]>,
38
38
  ) => void;
39
+ /** @internal */
40
+ buildItemInstance: (itemId: string) => ItemInstance<T>;
39
41
  setState: SetStateFn<TreeState<T>>;
40
42
  getState: () => TreeState<T>;
41
43
  setConfig: SetStateFn<TreeConfig<T>>;
@@ -47,6 +47,9 @@ 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),
50
53
  },
51
54
 
52
55
  itemInstance: {
@@ -17,8 +17,8 @@ export type SyncDataLoaderFeatureDef<T> = {
17
17
  dataLoader: TreeDataLoader<T>;
18
18
  };
19
19
  treeInstance: {
20
- retrieveItemData: (itemId: string) => T;
21
- retrieveChildrenIds: (itemId: string) => string[];
20
+ retrieveItemData: (itemId: string, skipFetch?: boolean) => T;
21
+ retrieveChildrenIds: (itemId: string, skipFetch?: boolean) => string[];
22
22
  };
23
23
  itemInstance: {
24
24
  isLoading: () => boolean;
package/src/index.ts CHANGED
@@ -6,6 +6,7 @@ export { MainFeatureDef, InstanceBuilder } from "./features/main/types";
6
6
  export * from "./features/drag-and-drop/types";
7
7
  export * from "./features/keyboard-drag-and-drop/types";
8
8
  export * from "./features/selection/types";
9
+ export * from "./features/checkboxes/types";
9
10
  export * from "./features/async-data-loader/types";
10
11
  export * from "./features/sync-data-loader/types";
11
12
  export * from "./features/hotkeys-core/types";
@@ -15,6 +16,7 @@ export * from "./features/expand-all/types";
15
16
  export * from "./features/prop-memoization/types";
16
17
 
17
18
  export * from "./features/selection/feature";
19
+ export * from "./features/checkboxes/feature";
18
20
  export * from "./features/hotkeys-core/feature";
19
21
  export * from "./features/async-data-loader/feature";
20
22
  export * from "./features/sync-data-loader/feature";
package/src/types/core.ts CHANGED
@@ -13,6 +13,7 @@ import { RenamingFeatureDef } from "../features/renaming/types";
13
13
  import { ExpandAllFeatureDef } from "../features/expand-all/types";
14
14
  import { PropMemoizationFeatureDef } from "../features/prop-memoization/types";
15
15
  import { KeyboardDragAndDropFeatureDef } from "../features/keyboard-drag-and-drop/types";
16
+ import { CheckboxesFeatureDef } from "../features/checkboxes/types";
16
17
 
17
18
  export type Updater<T> = T | ((old: T) => T);
18
19
  export type SetStateFn<T> = (updaterOrValue: Updater<T>) => void;
@@ -53,6 +54,7 @@ export type RegisteredFeatures<T> =
53
54
  | MainFeatureDef<T>
54
55
  | TreeFeatureDef<T>
55
56
  | SelectionFeatureDef<T>
57
+ | CheckboxesFeatureDef<T>
56
58
  | DragAndDropFeatureDef<T>
57
59
  | KeyboardDragAndDropFeatureDef<T>
58
60
  | HotkeysCoreFeatureDef<T>