@headless-tree/core 0.0.0-20250611234714 → 0.0.0-20250615192258

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 (34) hide show
  1. package/CHANGELOG.md +11 -1
  2. package/lib/cjs/core/create-tree.js +9 -0
  3. package/lib/cjs/features/async-data-loader/feature.js +4 -4
  4. package/lib/cjs/features/checkboxes/feature.d.ts +2 -0
  5. package/lib/cjs/features/checkboxes/feature.js +94 -0
  6. package/lib/cjs/features/checkboxes/types.d.ts +26 -0
  7. package/lib/cjs/features/checkboxes/types.js +9 -0
  8. package/lib/cjs/features/drag-and-drop/feature.js +1 -1
  9. package/lib/cjs/features/main/types.d.ts +2 -0
  10. package/lib/cjs/index.d.ts +2 -0
  11. package/lib/cjs/index.js +2 -0
  12. package/lib/cjs/types/core.d.ts +2 -1
  13. package/lib/esm/core/create-tree.js +9 -0
  14. package/lib/esm/features/async-data-loader/feature.js +4 -4
  15. package/lib/esm/features/checkboxes/feature.d.ts +2 -0
  16. package/lib/esm/features/checkboxes/feature.js +91 -0
  17. package/lib/esm/features/checkboxes/types.d.ts +26 -0
  18. package/lib/esm/features/checkboxes/types.js +6 -0
  19. package/lib/esm/features/drag-and-drop/feature.js +1 -1
  20. package/lib/esm/features/main/types.d.ts +2 -0
  21. package/lib/esm/index.d.ts +2 -0
  22. package/lib/esm/index.js +2 -0
  23. package/lib/esm/types/core.d.ts +2 -1
  24. package/package.json +1 -1
  25. package/src/core/create-tree.ts +13 -0
  26. package/src/features/async-data-loader/feature.ts +4 -4
  27. package/src/features/checkboxes/checkboxes.spec.ts +134 -0
  28. package/src/features/checkboxes/feature.ts +119 -0
  29. package/src/features/checkboxes/types.ts +28 -0
  30. package/src/features/drag-and-drop/feature.ts +2 -0
  31. package/src/features/main/types.ts +2 -0
  32. package/src/features/tree/types.ts +1 -1
  33. package/src/index.ts +2 -0
  34. package/src/types/core.ts +2 -0
package/CHANGELOG.md CHANGED
@@ -1,6 +1,16 @@
1
1
  # @headless-tree/core
2
2
 
3
- ## 0.0.0-20250611234714
3
+ ## 0.0.0-20250615192258
4
+
5
+ ### Minor Changes
6
+
7
+ - 8dd6231: Introduced new Checkboxes feature (#88)
8
+
9
+ ### Patch Changes
10
+
11
+ - 309feba: fixed an issue where the drag-forbidden cursor is shown briefly between changing drag targets (#114)
12
+
13
+ ## 1.2.0
4
14
 
5
15
  ### Minor Changes
6
16
 
@@ -112,6 +112,15 @@ const createTree = (initialConfig) => {
112
112
  const externalStateSetter = config[stateHandlerNames[stateName]];
113
113
  externalStateSetter === null || externalStateSetter === void 0 ? void 0 : externalStateSetter(state[stateName]);
114
114
  },
115
+ buildItemInstance: ({}, itemId) => {
116
+ const [instance, finalizeInstance] = buildInstance(features, "itemInstance", (instance) => ({
117
+ item: instance,
118
+ tree: treeInstance,
119
+ itemId,
120
+ }));
121
+ finalizeInstance();
122
+ return instance;
123
+ },
115
124
  // TODO rebuildSubTree: (itemId: string) => void;
116
125
  rebuildTree: () => {
117
126
  var _a;
@@ -75,14 +75,14 @@ exports.asyncDataLoaderFeature = {
75
75
  var _b;
76
76
  return ((_b = getDataRef(tree).current.childrenIds[itemId]) !== null && _b !== void 0 ? _b : (yield loadChildrenIds(tree, itemId)));
77
77
  }),
78
- retrieveItemData: ({ tree }, itemId) => {
78
+ retrieveItemData: ({ tree }, itemId, skipFetch = false) => {
79
79
  var _a, _b;
80
80
  const config = tree.getConfig();
81
81
  const dataRef = getDataRef(tree);
82
82
  if (dataRef.current.itemData[itemId]) {
83
83
  return dataRef.current.itemData[itemId];
84
84
  }
85
- if (!tree.getState().loadingItemData.includes(itemId)) {
85
+ if (!tree.getState().loadingItemData.includes(itemId) && !skipFetch) {
86
86
  tree.applySubStateUpdate("loadingItemData", (loadingItemData) => [
87
87
  ...loadingItemData,
88
88
  itemId,
@@ -91,12 +91,12 @@ exports.asyncDataLoaderFeature = {
91
91
  }
92
92
  return (_b = (_a = config.createLoadingItemData) === null || _a === void 0 ? void 0 : _a.call(config)) !== null && _b !== void 0 ? _b : null;
93
93
  },
94
- retrieveChildrenIds: ({ tree }, itemId) => {
94
+ retrieveChildrenIds: ({ tree }, itemId, skipFetch = false) => {
95
95
  const dataRef = getDataRef(tree);
96
96
  if (dataRef.current.childrenIds[itemId]) {
97
97
  return dataRef.current.childrenIds[itemId];
98
98
  }
99
- if (tree.getState().loadingItemChildrens.includes(itemId)) {
99
+ if (tree.getState().loadingItemChildrens.includes(itemId) || skipFetch) {
100
100
  return [];
101
101
  }
102
102
  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 = {}));
@@ -74,7 +74,7 @@ exports.dragAndDropFeature = {
74
74
  },
75
75
  },
76
76
  itemInstance: {
77
- getProps: ({ tree, item, prev }) => (Object.assign(Object.assign({}, prev === null || prev === void 0 ? void 0 : prev()), { draggable: true, onDragStart: (e) => {
77
+ getProps: ({ tree, item, prev }) => (Object.assign(Object.assign({}, prev === null || prev === void 0 ? void 0 : prev()), { draggable: true, onDragEnter: (e) => e.preventDefault(), onDragStart: (e) => {
78
78
  var _a, _b, _c;
79
79
  const selectedItems = tree.getSelectedItems();
80
80
  const items = selectedItems.includes(item) ? selectedItems : [item];
@@ -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
@@ -21,6 +21,7 @@ __exportStar(require("./features/tree/types"), exports);
21
21
  __exportStar(require("./features/drag-and-drop/types"), exports);
22
22
  __exportStar(require("./features/keyboard-drag-and-drop/types"), exports);
23
23
  __exportStar(require("./features/selection/types"), exports);
24
+ __exportStar(require("./features/checkboxes/types"), exports);
24
25
  __exportStar(require("./features/async-data-loader/types"), exports);
25
26
  __exportStar(require("./features/sync-data-loader/types"), exports);
26
27
  __exportStar(require("./features/hotkeys-core/types"), exports);
@@ -29,6 +30,7 @@ __exportStar(require("./features/renaming/types"), exports);
29
30
  __exportStar(require("./features/expand-all/types"), exports);
30
31
  __exportStar(require("./features/prop-memoization/types"), exports);
31
32
  __exportStar(require("./features/selection/feature"), exports);
33
+ __exportStar(require("./features/checkboxes/feature"), exports);
32
34
  __exportStar(require("./features/hotkeys-core/feature"), exports);
33
35
  __exportStar(require("./features/async-data-loader/feature"), exports);
34
36
  __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
  }
@@ -109,6 +109,15 @@ export const createTree = (initialConfig) => {
109
109
  const externalStateSetter = config[stateHandlerNames[stateName]];
110
110
  externalStateSetter === null || externalStateSetter === void 0 ? void 0 : externalStateSetter(state[stateName]);
111
111
  },
112
+ buildItemInstance: ({}, itemId) => {
113
+ const [instance, finalizeInstance] = buildInstance(features, "itemInstance", (instance) => ({
114
+ item: instance,
115
+ tree: treeInstance,
116
+ itemId,
117
+ }));
118
+ finalizeInstance();
119
+ return instance;
120
+ },
112
121
  // TODO rebuildSubTree: (itemId: string) => void;
113
122
  rebuildTree: () => {
114
123
  var _a;
@@ -72,14 +72,14 @@ export const asyncDataLoaderFeature = {
72
72
  var _b;
73
73
  return ((_b = getDataRef(tree).current.childrenIds[itemId]) !== null && _b !== void 0 ? _b : (yield loadChildrenIds(tree, itemId)));
74
74
  }),
75
- retrieveItemData: ({ tree }, itemId) => {
75
+ retrieveItemData: ({ tree }, itemId, skipFetch = false) => {
76
76
  var _a, _b;
77
77
  const config = tree.getConfig();
78
78
  const dataRef = getDataRef(tree);
79
79
  if (dataRef.current.itemData[itemId]) {
80
80
  return dataRef.current.itemData[itemId];
81
81
  }
82
- if (!tree.getState().loadingItemData.includes(itemId)) {
82
+ if (!tree.getState().loadingItemData.includes(itemId) && !skipFetch) {
83
83
  tree.applySubStateUpdate("loadingItemData", (loadingItemData) => [
84
84
  ...loadingItemData,
85
85
  itemId,
@@ -88,12 +88,12 @@ export const asyncDataLoaderFeature = {
88
88
  }
89
89
  return (_b = (_a = config.createLoadingItemData) === null || _a === void 0 ? void 0 : _a.call(config)) !== null && _b !== void 0 ? _b : null;
90
90
  },
91
- retrieveChildrenIds: ({ tree }, itemId) => {
91
+ retrieveChildrenIds: ({ tree }, itemId, skipFetch = false) => {
92
92
  const dataRef = getDataRef(tree);
93
93
  if (dataRef.current.childrenIds[itemId]) {
94
94
  return dataRef.current.childrenIds[itemId];
95
95
  }
96
- if (tree.getState().loadingItemChildrens.includes(itemId)) {
96
+ if (tree.getState().loadingItemChildrens.includes(itemId) || skipFetch) {
97
97
  return [];
98
98
  }
99
99
  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 = {}));
@@ -71,7 +71,7 @@ export const dragAndDropFeature = {
71
71
  },
72
72
  },
73
73
  itemInstance: {
74
- getProps: ({ tree, item, prev }) => (Object.assign(Object.assign({}, prev === null || prev === void 0 ? void 0 : prev()), { draggable: true, onDragStart: (e) => {
74
+ getProps: ({ tree, item, prev }) => (Object.assign(Object.assign({}, prev === null || prev === void 0 ? void 0 : prev()), { draggable: true, onDragEnter: (e) => e.preventDefault(), onDragStart: (e) => {
75
75
  var _a, _b, _c;
76
76
  const selectedItems = tree.getSelectedItems();
77
77
  const items = selectedItems.includes(item) ? selectedItems : [item];
@@ -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-20250611234714",
3
+ "version": "0.0.0-20250615192258",
4
4
  "main": "lib/cjs/index.js",
5
5
  "module": "lib/esm/index.js",
6
6
  "types": "lib/esm/index.d.ts",
@@ -170,6 +170,19 @@ export const createTree = <T>(
170
170
  ] as Function;
171
171
  externalStateSetter?.(state[stateName]);
172
172
  },
173
+ buildItemInstance: ({}, itemId) => {
174
+ const [instance, finalizeInstance] = buildInstance(
175
+ features,
176
+ "itemInstance",
177
+ (instance) => ({
178
+ item: instance,
179
+ tree: treeInstance,
180
+ itemId,
181
+ }),
182
+ );
183
+ finalizeInstance();
184
+ return instance;
185
+ },
173
186
  // TODO rebuildSubTree: (itemId: string) => void;
174
187
  rebuildTree: () => {
175
188
  rebuildItemMeta();
@@ -95,7 +95,7 @@ export const asyncDataLoaderFeature: FeatureImplementation = {
95
95
  );
96
96
  },
97
97
 
98
- retrieveItemData: ({ tree }, itemId) => {
98
+ retrieveItemData: ({ tree }, itemId, skipFetch = false) => {
99
99
  const config = tree.getConfig();
100
100
  const dataRef = getDataRef(tree);
101
101
 
@@ -103,7 +103,7 @@ export const asyncDataLoaderFeature: FeatureImplementation = {
103
103
  return dataRef.current.itemData[itemId];
104
104
  }
105
105
 
106
- if (!tree.getState().loadingItemData.includes(itemId)) {
106
+ if (!tree.getState().loadingItemData.includes(itemId) && !skipFetch) {
107
107
  tree.applySubStateUpdate("loadingItemData", (loadingItemData) => [
108
108
  ...loadingItemData,
109
109
  itemId,
@@ -115,13 +115,13 @@ export const asyncDataLoaderFeature: FeatureImplementation = {
115
115
  return config.createLoadingItemData?.() ?? null;
116
116
  },
117
117
 
118
- retrieveChildrenIds: ({ tree }, itemId) => {
118
+ retrieveChildrenIds: ({ tree }, itemId, skipFetch = false) => {
119
119
  const dataRef = getDataRef(tree);
120
120
  if (dataRef.current.childrenIds[itemId]) {
121
121
  return dataRef.current.childrenIds[itemId];
122
122
  }
123
123
 
124
- if (tree.getState().loadingItemChildrens.includes(itemId)) {
124
+ if (tree.getState().loadingItemChildrens.includes(itemId) || skipFetch) {
125
125
  return [];
126
126
  }
127
127
 
@@ -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
+ };
@@ -101,6 +101,8 @@ export const dragAndDropFeature: FeatureImplementation = {
101
101
 
102
102
  draggable: true,
103
103
 
104
+ onDragEnter: (e: DragEvent) => e.preventDefault(),
105
+
104
106
  onDragStart: (e: DragEvent) => {
105
107
  const selectedItems = tree.getSelectedItems();
106
108
  const items = selectedItems.includes(item) ? selectedItems : [item];
@@ -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>