@headless-tree/core 0.0.0-20250509160455 → 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-20250509160455
3
+ ## 0.0.0-20250511185653
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;
@@ -96,14 +96,14 @@ exports.asyncDataLoaderFeature = {
96
96
  dataRef.current.awaitingItemChildrensLoading[itemId].push(resolve);
97
97
  });
98
98
  }),
99
- retrieveItemData: ({ tree }, itemId) => {
99
+ retrieveItemData: ({ tree }, itemId, skipFetch = false) => {
100
100
  var _a, _b;
101
101
  const config = tree.getConfig();
102
102
  const dataRef = getDataRef(tree);
103
103
  if (dataRef.current.itemData[itemId]) {
104
104
  return dataRef.current.itemData[itemId];
105
105
  }
106
- if (!tree.getState().loadingItemData.includes(itemId)) {
106
+ if (!tree.getState().loadingItemData.includes(itemId) && !skipFetch) {
107
107
  tree.applySubStateUpdate("loadingItemData", (loadingItemData) => [
108
108
  ...loadingItemData,
109
109
  itemId,
@@ -112,12 +112,12 @@ exports.asyncDataLoaderFeature = {
112
112
  }
113
113
  return (_b = (_a = config.createLoadingItemData) === null || _a === void 0 ? void 0 : _a.call(config)) !== null && _b !== void 0 ? _b : null;
114
114
  },
115
- retrieveChildrenIds: ({ tree }, itemId) => {
115
+ retrieveChildrenIds: ({ tree }, itemId, skipFetch = false) => {
116
116
  const dataRef = getDataRef(tree);
117
117
  if (dataRef.current.childrenIds[itemId]) {
118
118
  return dataRef.current.childrenIds[itemId];
119
119
  }
120
- if (tree.getState().loadingItemChildrens.includes(itemId)) {
120
+ if (tree.getState().loadingItemChildrens.includes(itemId) || skipFetch) {
121
121
  return [];
122
122
  }
123
123
  tree.applySubStateUpdate("loadingItemChildrens", (loadingItemChildrens) => [...loadingItemChildrens, itemId]);
@@ -0,0 +1,2 @@
1
+ import { FeatureImplementation } from "../../types/core";
2
+ export declare const checkboxesFeature: FeatureImplementation;
@@ -0,0 +1,94 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.checkboxesFeature = void 0;
4
+ const utils_1 = require("../../utils");
5
+ const types_1 = require("./types");
6
+ const errors_1 = require("../../utilities/errors");
7
+ const getAllLoadedDescendants = (tree, itemId) => {
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();
15
+ };
16
+ exports.checkboxesFeature = {
17
+ key: "checkboxes",
18
+ overwrites: ["selection"],
19
+ getInitialState: (initialState) => (Object.assign({ checkedItems: [] }, initialState)),
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
+ },
28
+ stateHandlerNames: {
29
+ checkedItems: "setCheckedItems",
30
+ },
31
+ treeInstance: {
32
+ setCheckedItems: ({ tree }, checkedItems) => {
33
+ tree.applySubStateUpdate("checkedItems", checkedItems);
34
+ },
35
+ },
36
+ itemInstance: {
37
+ getCheckboxProps: ({ item }) => {
38
+ const checkedState = item.getCheckedState();
39
+ return {
40
+ onChange: item.toggleCheckedState,
41
+ checked: checkedState === types_1.CheckedState.Checked,
42
+ ref: (r) => {
43
+ if (r) {
44
+ r.indeterminate = checkedState === types_1.CheckedState.Indeterminate;
45
+ }
46
+ },
47
+ };
48
+ },
49
+ toggleCheckedState: ({ item }) => {
50
+ if (item.getCheckedState() === types_1.CheckedState.Checked) {
51
+ item.setUnchecked();
52
+ }
53
+ else {
54
+ item.setChecked();
55
+ }
56
+ },
57
+ getCheckedState: ({ item, tree, itemId }) => {
58
+ const { checkedItems } = tree.getState();
59
+ if (checkedItems.includes(itemId)) {
60
+ return types_1.CheckedState.Checked;
61
+ }
62
+ if (item.isFolder() && !tree.getConfig().canCheckFolders) {
63
+ const descendants = getAllLoadedDescendants(tree, itemId);
64
+ if (descendants.every((d) => checkedItems.includes(d))) {
65
+ return types_1.CheckedState.Checked;
66
+ }
67
+ if (descendants.some((d) => checkedItems.includes(d))) {
68
+ return types_1.CheckedState.Indeterminate;
69
+ }
70
+ }
71
+ return types_1.CheckedState.Unchecked;
72
+ },
73
+ setChecked: ({ item, tree, itemId }) => {
74
+ if (!item.isFolder() || tree.getConfig().canCheckFolders) {
75
+ tree.applySubStateUpdate("checkedItems", (items) => [...items, itemId]);
76
+ }
77
+ else {
78
+ tree.applySubStateUpdate("checkedItems", (items) => [
79
+ ...items,
80
+ ...getAllLoadedDescendants(tree, itemId),
81
+ ]);
82
+ }
83
+ },
84
+ setUnchecked: ({ item, tree, itemId }) => {
85
+ if (!item.isFolder() || tree.getConfig().canCheckFolders) {
86
+ tree.applySubStateUpdate("checkedItems", (items) => items.filter((id) => id !== itemId));
87
+ }
88
+ else {
89
+ const descendants = getAllLoadedDescendants(tree, itemId);
90
+ tree.applySubStateUpdate("checkedItems", (items) => items.filter((id) => !descendants.includes(id)));
91
+ }
92
+ },
93
+ },
94
+ };
@@ -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: () => void;
20
+ setUnchecked: () => void;
21
+ toggleCheckedState: () => 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>>;
@@ -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;
@@ -93,14 +93,14 @@ export const asyncDataLoaderFeature = {
93
93
  dataRef.current.awaitingItemChildrensLoading[itemId].push(resolve);
94
94
  });
95
95
  }),
96
- retrieveItemData: ({ tree }, itemId) => {
96
+ retrieveItemData: ({ tree }, itemId, skipFetch = false) => {
97
97
  var _a, _b;
98
98
  const config = tree.getConfig();
99
99
  const dataRef = getDataRef(tree);
100
100
  if (dataRef.current.itemData[itemId]) {
101
101
  return dataRef.current.itemData[itemId];
102
102
  }
103
- if (!tree.getState().loadingItemData.includes(itemId)) {
103
+ if (!tree.getState().loadingItemData.includes(itemId) && !skipFetch) {
104
104
  tree.applySubStateUpdate("loadingItemData", (loadingItemData) => [
105
105
  ...loadingItemData,
106
106
  itemId,
@@ -109,12 +109,12 @@ export const asyncDataLoaderFeature = {
109
109
  }
110
110
  return (_b = (_a = config.createLoadingItemData) === null || _a === void 0 ? void 0 : _a.call(config)) !== null && _b !== void 0 ? _b : null;
111
111
  },
112
- retrieveChildrenIds: ({ tree }, itemId) => {
112
+ retrieveChildrenIds: ({ tree }, itemId, skipFetch = false) => {
113
113
  const dataRef = getDataRef(tree);
114
114
  if (dataRef.current.childrenIds[itemId]) {
115
115
  return dataRef.current.childrenIds[itemId];
116
116
  }
117
- if (tree.getState().loadingItemChildrens.includes(itemId)) {
117
+ if (tree.getState().loadingItemChildrens.includes(itemId) || skipFetch) {
118
118
  return [];
119
119
  }
120
120
  tree.applySubStateUpdate("loadingItemChildrens", (loadingItemChildrens) => [...loadingItemChildrens, itemId]);
@@ -0,0 +1,2 @@
1
+ import { FeatureImplementation } from "../../types/core";
2
+ export declare const checkboxesFeature: FeatureImplementation;
@@ -0,0 +1,91 @@
1
+ import { makeStateUpdater } from "../../utils";
2
+ import { CheckedState } from "./types";
3
+ import { throwError } from "../../utilities/errors";
4
+ const getAllLoadedDescendants = (tree, itemId) => {
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();
12
+ };
13
+ export const checkboxesFeature = {
14
+ key: "checkboxes",
15
+ overwrites: ["selection"],
16
+ getInitialState: (initialState) => (Object.assign({ checkedItems: [] }, initialState)),
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
+ },
25
+ stateHandlerNames: {
26
+ checkedItems: "setCheckedItems",
27
+ },
28
+ treeInstance: {
29
+ setCheckedItems: ({ tree }, checkedItems) => {
30
+ tree.applySubStateUpdate("checkedItems", checkedItems);
31
+ },
32
+ },
33
+ itemInstance: {
34
+ getCheckboxProps: ({ item }) => {
35
+ const checkedState = item.getCheckedState();
36
+ return {
37
+ onChange: item.toggleCheckedState,
38
+ checked: checkedState === CheckedState.Checked,
39
+ ref: (r) => {
40
+ if (r) {
41
+ r.indeterminate = checkedState === CheckedState.Indeterminate;
42
+ }
43
+ },
44
+ };
45
+ },
46
+ toggleCheckedState: ({ item }) => {
47
+ if (item.getCheckedState() === CheckedState.Checked) {
48
+ item.setUnchecked();
49
+ }
50
+ else {
51
+ item.setChecked();
52
+ }
53
+ },
54
+ getCheckedState: ({ item, tree, itemId }) => {
55
+ const { checkedItems } = tree.getState();
56
+ if (checkedItems.includes(itemId)) {
57
+ return CheckedState.Checked;
58
+ }
59
+ if (item.isFolder() && !tree.getConfig().canCheckFolders) {
60
+ const descendants = getAllLoadedDescendants(tree, itemId);
61
+ if (descendants.every((d) => checkedItems.includes(d))) {
62
+ return CheckedState.Checked;
63
+ }
64
+ if (descendants.some((d) => checkedItems.includes(d))) {
65
+ return CheckedState.Indeterminate;
66
+ }
67
+ }
68
+ return CheckedState.Unchecked;
69
+ },
70
+ setChecked: ({ item, tree, itemId }) => {
71
+ if (!item.isFolder() || tree.getConfig().canCheckFolders) {
72
+ tree.applySubStateUpdate("checkedItems", (items) => [...items, itemId]);
73
+ }
74
+ else {
75
+ tree.applySubStateUpdate("checkedItems", (items) => [
76
+ ...items,
77
+ ...getAllLoadedDescendants(tree, itemId),
78
+ ]);
79
+ }
80
+ },
81
+ setUnchecked: ({ item, tree, itemId }) => {
82
+ if (!item.isFolder() || tree.getConfig().canCheckFolders) {
83
+ tree.applySubStateUpdate("checkedItems", (items) => items.filter((id) => id !== itemId));
84
+ }
85
+ else {
86
+ const descendants = getAllLoadedDescendants(tree, itemId);
87
+ tree.applySubStateUpdate("checkedItems", (items) => items.filter((id) => !descendants.includes(id)));
88
+ }
89
+ },
90
+ },
91
+ };
@@ -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: () => void;
20
+ setUnchecked: () => void;
21
+ toggleCheckedState: () => 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>>;
@@ -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-20250511185653",
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();
@@ -106,7 +106,7 @@ export const asyncDataLoaderFeature: FeatureImplementation = {
106
106
  });
107
107
  },
108
108
 
109
- retrieveItemData: ({ tree }, itemId) => {
109
+ retrieveItemData: ({ tree }, itemId, skipFetch = false) => {
110
110
  const config = tree.getConfig();
111
111
  const dataRef = getDataRef(tree);
112
112
 
@@ -114,7 +114,7 @@ export const asyncDataLoaderFeature: FeatureImplementation = {
114
114
  return dataRef.current.itemData[itemId];
115
115
  }
116
116
 
117
- if (!tree.getState().loadingItemData.includes(itemId)) {
117
+ if (!tree.getState().loadingItemData.includes(itemId) && !skipFetch) {
118
118
  tree.applySubStateUpdate("loadingItemData", (loadingItemData) => [
119
119
  ...loadingItemData,
120
120
  itemId,
@@ -126,13 +126,13 @@ export const asyncDataLoaderFeature: FeatureImplementation = {
126
126
  return config.createLoadingItemData?.() ?? null;
127
127
  },
128
128
 
129
- retrieveChildrenIds: ({ tree }, itemId) => {
129
+ retrieveChildrenIds: ({ tree }, itemId, skipFetch = false) => {
130
130
  const dataRef = getDataRef(tree);
131
131
  if (dataRef.current.childrenIds[itemId]) {
132
132
  return dataRef.current.childrenIds[itemId];
133
133
  }
134
134
 
135
- if (tree.getState().loadingItemChildrens.includes(itemId)) {
135
+ if (tree.getState().loadingItemChildrens.includes(itemId) || skipFetch) {
136
136
  return [];
137
137
  }
138
138
 
@@ -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
+ });
@@ -0,0 +1,119 @@
1
+ import { FeatureImplementation, TreeInstance } from "../../types/core";
2
+ import { makeStateUpdater } from "../../utils";
3
+ import { CheckedState } from "./types";
4
+ import { throwError } from "../../utilities/errors";
5
+
6
+ const getAllLoadedDescendants = <T>(
7
+ tree: TreeInstance<T>,
8
+ itemId: string,
9
+ ): string[] => {
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();
17
+ };
18
+
19
+ export const checkboxesFeature: FeatureImplementation = {
20
+ key: "checkboxes",
21
+
22
+ overwrites: ["selection"],
23
+
24
+ getInitialState: (initialState) => ({
25
+ checkedItems: [],
26
+ ...initialState,
27
+ }),
28
+
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
+ },
42
+
43
+ stateHandlerNames: {
44
+ checkedItems: "setCheckedItems",
45
+ },
46
+
47
+ treeInstance: {
48
+ setCheckedItems: ({ tree }, checkedItems) => {
49
+ tree.applySubStateUpdate("checkedItems", checkedItems);
50
+ },
51
+ },
52
+
53
+ itemInstance: {
54
+ getCheckboxProps: ({ item }) => {
55
+ const checkedState = item.getCheckedState();
56
+ return {
57
+ onChange: item.toggleCheckedState,
58
+ checked: checkedState === CheckedState.Checked,
59
+ ref: (r: any) => {
60
+ if (r) {
61
+ r.indeterminate = checkedState === CheckedState.Indeterminate;
62
+ }
63
+ },
64
+ };
65
+ },
66
+
67
+ toggleCheckedState: ({ item }) => {
68
+ if (item.getCheckedState() === CheckedState.Checked) {
69
+ item.setUnchecked();
70
+ } else {
71
+ item.setChecked();
72
+ }
73
+ },
74
+
75
+ getCheckedState: ({ item, tree, itemId }) => {
76
+ const { checkedItems } = tree.getState();
77
+
78
+ if (checkedItems.includes(itemId)) {
79
+ return CheckedState.Checked;
80
+ }
81
+
82
+ if (item.isFolder() && !tree.getConfig().canCheckFolders) {
83
+ const descendants = getAllLoadedDescendants(tree, itemId);
84
+ if (descendants.every((d) => checkedItems.includes(d))) {
85
+ return CheckedState.Checked;
86
+ }
87
+ if (descendants.some((d) => checkedItems.includes(d))) {
88
+ return CheckedState.Indeterminate;
89
+ }
90
+ }
91
+
92
+ return CheckedState.Unchecked;
93
+ },
94
+
95
+ setChecked: ({ item, tree, itemId }) => {
96
+ if (!item.isFolder() || tree.getConfig().canCheckFolders) {
97
+ tree.applySubStateUpdate("checkedItems", (items) => [...items, itemId]);
98
+ } else {
99
+ tree.applySubStateUpdate("checkedItems", (items) => [
100
+ ...items,
101
+ ...getAllLoadedDescendants(tree, itemId),
102
+ ]);
103
+ }
104
+ },
105
+
106
+ setUnchecked: ({ item, tree, itemId }) => {
107
+ if (!item.isFolder() || tree.getConfig().canCheckFolders) {
108
+ tree.applySubStateUpdate("checkedItems", (items) =>
109
+ items.filter((id) => id !== itemId),
110
+ );
111
+ } else {
112
+ const descendants = getAllLoadedDescendants(tree, itemId);
113
+ tree.applySubStateUpdate("checkedItems", (items) =>
114
+ items.filter((id) => !descendants.includes(id)),
115
+ );
116
+ }
117
+ },
118
+ },
119
+ };
@@ -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: () => void;
22
+ setUnchecked: () => void;
23
+ toggleCheckedState: () => 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>>;
@@ -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;
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>