@headless-tree/core 1.2.0 → 1.2.1

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 (42) hide show
  1. package/CHANGELOG.md +8 -0
  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 +31 -5
  9. package/lib/cjs/features/drag-and-drop/types.d.ts +5 -0
  10. package/lib/cjs/features/main/types.d.ts +2 -0
  11. package/lib/cjs/features/tree/feature.js +4 -0
  12. package/lib/cjs/features/tree/types.d.ts +2 -1
  13. package/lib/cjs/index.d.ts +2 -0
  14. package/lib/cjs/index.js +2 -0
  15. package/lib/cjs/types/core.d.ts +2 -1
  16. package/lib/esm/core/create-tree.js +9 -0
  17. package/lib/esm/features/async-data-loader/feature.js +4 -4
  18. package/lib/esm/features/checkboxes/feature.d.ts +2 -0
  19. package/lib/esm/features/checkboxes/feature.js +91 -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/drag-and-drop/feature.js +31 -5
  23. package/lib/esm/features/drag-and-drop/types.d.ts +5 -0
  24. package/lib/esm/features/main/types.d.ts +2 -0
  25. package/lib/esm/features/tree/feature.js +4 -0
  26. package/lib/esm/features/tree/types.d.ts +2 -1
  27. package/lib/esm/index.d.ts +2 -0
  28. package/lib/esm/index.js +2 -0
  29. package/lib/esm/types/core.d.ts +2 -1
  30. package/package.json +1 -1
  31. package/src/core/create-tree.ts +13 -0
  32. package/src/features/async-data-loader/feature.ts +4 -4
  33. package/src/features/checkboxes/checkboxes.spec.ts +134 -0
  34. package/src/features/checkboxes/feature.ts +119 -0
  35. package/src/features/checkboxes/types.ts +28 -0
  36. package/src/features/drag-and-drop/feature.ts +38 -2
  37. package/src/features/drag-and-drop/types.ts +5 -0
  38. package/src/features/main/types.ts +2 -0
  39. package/src/features/tree/feature.ts +5 -0
  40. package/src/features/tree/types.ts +3 -2
  41. package/src/index.ts +2 -0
  42. package/src/types/core.ts +2 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # @headless-tree/core
2
2
 
3
+ ## 1.2.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 344011a: fixed an issue where dropping items on an empty tree didn't trigger any events
8
+ - 9f418f8: support setting the drag preview with the `setDragImage` option (#115)
9
+ - 309feba: fixed an issue where the drag-forbidden cursor is shown briefly between changing drag targets (#114)
10
+
3
11
  ## 1.2.0
4
12
 
5
13
  ### Minor Changes
@@ -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 = {}));
@@ -68,14 +68,35 @@ exports.dragAndDropFeature = {
68
68
  }
69
69
  : { display: "none" };
70
70
  },
71
- getContainerProps: ({ prev }, treeLabel) => {
71
+ getContainerProps: ({ prev, tree }, treeLabel) => {
72
72
  const prevProps = prev === null || prev === void 0 ? void 0 : prev(treeLabel);
73
- return Object.assign(Object.assign({}, prevProps), { style: Object.assign(Object.assign({}, prevProps === null || prevProps === void 0 ? void 0 : prevProps.style), { position: "relative" }) });
73
+ return Object.assign(Object.assign({}, prevProps), { onDragOver: (e) => {
74
+ e.preventDefault();
75
+ }, onDrop: (e) => __awaiter(void 0, void 0, void 0, function* () {
76
+ var _a, _b, _c;
77
+ // TODO merge implementation with itemInstance.onDrop
78
+ const dataRef = tree.getDataRef();
79
+ const target = { item: tree.getRootItem() };
80
+ if (!(0, utils_1.canDrop)(e.dataTransfer, target, tree)) {
81
+ return;
82
+ }
83
+ e.preventDefault();
84
+ const config = tree.getConfig();
85
+ const draggedItems = (_a = tree.getState().dnd) === null || _a === void 0 ? void 0 : _a.draggedItems;
86
+ dataRef.current.lastDragCode = undefined;
87
+ tree.applySubStateUpdate("dnd", null);
88
+ if (draggedItems) {
89
+ yield ((_b = config.onDrop) === null || _b === void 0 ? void 0 : _b.call(config, draggedItems, target));
90
+ }
91
+ else if (e.dataTransfer) {
92
+ yield ((_c = config.onDropForeignDragObject) === null || _c === void 0 ? void 0 : _c.call(config, e.dataTransfer, target));
93
+ }
94
+ }), style: Object.assign(Object.assign({}, prevProps === null || prevProps === void 0 ? void 0 : prevProps.style), { position: "relative" }) });
74
95
  },
75
96
  },
76
97
  itemInstance: {
77
- getProps: ({ tree, item, prev }) => (Object.assign(Object.assign({}, prev === null || prev === void 0 ? void 0 : prev()), { draggable: true, onDragStart: (e) => {
78
- var _a, _b, _c;
98
+ getProps: ({ tree, item, prev }) => (Object.assign(Object.assign({}, prev === null || prev === void 0 ? void 0 : prev()), { draggable: true, onDragEnter: (e) => e.preventDefault(), onDragStart: (e) => {
99
+ var _a, _b, _c, _d;
79
100
  const selectedItems = tree.getSelectedItems();
80
101
  const items = selectedItems.includes(item) ? selectedItems : [item];
81
102
  const config = tree.getConfig();
@@ -86,9 +107,13 @@ exports.dragAndDropFeature = {
86
107
  e.preventDefault();
87
108
  return;
88
109
  }
110
+ if (config.setDragImage) {
111
+ const { imgElement, xOffset, yOffset } = config.setDragImage(items);
112
+ (_c = e.dataTransfer) === null || _c === void 0 ? void 0 : _c.setDragImage(imgElement, xOffset !== null && xOffset !== void 0 ? xOffset : 0, yOffset !== null && yOffset !== void 0 ? yOffset : 0);
113
+ }
89
114
  if (config.createForeignDragObject) {
90
115
  const { format, data } = config.createForeignDragObject(items);
91
- (_c = e.dataTransfer) === null || _c === void 0 ? void 0 : _c.setData(format, data);
116
+ (_d = e.dataTransfer) === null || _d === void 0 ? void 0 : _d.setData(format, data);
92
117
  }
93
118
  tree.applySubStateUpdate("dnd", {
94
119
  draggedItems: items,
@@ -134,6 +159,7 @@ exports.dragAndDropFeature = {
134
159
  (_d = (_c = tree.getConfig()).onCompleteForeignDrop) === null || _d === void 0 ? void 0 : _d.call(_c, draggedItems);
135
160
  }, onDrop: (e) => __awaiter(void 0, void 0, void 0, function* () {
136
161
  var _a, _b, _c;
162
+ e.stopPropagation();
137
163
  const dataRef = tree.getDataRef();
138
164
  const target = (0, utils_1.getDragTarget)(e, item, tree);
139
165
  if (!(0, utils_1.canDrop)(e.dataTransfer, target, tree)) {
@@ -46,6 +46,11 @@ export type DragAndDropFeatureDef<T> = {
46
46
  format: string;
47
47
  data: any;
48
48
  };
49
+ setDragImage?: (items: ItemInstance<T>[]) => {
50
+ imgElement: Element;
51
+ xOffset?: number;
52
+ yOffset?: number;
53
+ };
49
54
  canDropForeignDragObject?: (dataTransfer: DataTransfer, target: DragTarget<T>) => boolean;
50
55
  onDrop?: (items: ItemInstance<T>[], target: DragTarget<T>) => void | Promise<void>;
51
56
  onDropForeignDragObject?: (dataTransfer: DataTransfer, target: DragTarget<T>) => void | Promise<void>;
@@ -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>>;
@@ -59,6 +59,10 @@ exports.treeFeature = {
59
59
  var _a, _b;
60
60
  return ((_b = tree.getItemInstance((_a = tree.getState().focusedItem) !== null && _a !== void 0 ? _a : "")) !== null && _b !== void 0 ? _b : tree.getItems()[0]);
61
61
  },
62
+ getRootItem: ({ tree }) => {
63
+ const { rootItemId } = tree.getConfig();
64
+ return tree.getItemInstance(rootItemId);
65
+ },
62
66
  focusNextItem: ({ tree }) => {
63
67
  var _a;
64
68
  const focused = tree.getFocusedItem().getItemMeta();
@@ -27,7 +27,8 @@ export type TreeFeatureDef<T> = {
27
27
  treeInstance: {
28
28
  /** @internal */
29
29
  getItemsMeta: () => ItemMeta[];
30
- getFocusedItem: () => ItemInstance<any>;
30
+ getFocusedItem: () => ItemInstance<T>;
31
+ getRootItem: () => ItemInstance<T>;
31
32
  focusNextItem: () => void;
32
33
  focusPreviousItem: () => void;
33
34
  updateDomFocus: () => void;
@@ -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 = {}));
@@ -65,14 +65,35 @@ export const dragAndDropFeature = {
65
65
  }
66
66
  : { display: "none" };
67
67
  },
68
- getContainerProps: ({ prev }, treeLabel) => {
68
+ getContainerProps: ({ prev, tree }, treeLabel) => {
69
69
  const prevProps = prev === null || prev === void 0 ? void 0 : prev(treeLabel);
70
- return Object.assign(Object.assign({}, prevProps), { style: Object.assign(Object.assign({}, prevProps === null || prevProps === void 0 ? void 0 : prevProps.style), { position: "relative" }) });
70
+ return Object.assign(Object.assign({}, prevProps), { onDragOver: (e) => {
71
+ e.preventDefault();
72
+ }, onDrop: (e) => __awaiter(void 0, void 0, void 0, function* () {
73
+ var _a, _b, _c;
74
+ // TODO merge implementation with itemInstance.onDrop
75
+ const dataRef = tree.getDataRef();
76
+ const target = { item: tree.getRootItem() };
77
+ if (!canDrop(e.dataTransfer, target, tree)) {
78
+ return;
79
+ }
80
+ e.preventDefault();
81
+ const config = tree.getConfig();
82
+ const draggedItems = (_a = tree.getState().dnd) === null || _a === void 0 ? void 0 : _a.draggedItems;
83
+ dataRef.current.lastDragCode = undefined;
84
+ tree.applySubStateUpdate("dnd", null);
85
+ if (draggedItems) {
86
+ yield ((_b = config.onDrop) === null || _b === void 0 ? void 0 : _b.call(config, draggedItems, target));
87
+ }
88
+ else if (e.dataTransfer) {
89
+ yield ((_c = config.onDropForeignDragObject) === null || _c === void 0 ? void 0 : _c.call(config, e.dataTransfer, target));
90
+ }
91
+ }), style: Object.assign(Object.assign({}, prevProps === null || prevProps === void 0 ? void 0 : prevProps.style), { position: "relative" }) });
71
92
  },
72
93
  },
73
94
  itemInstance: {
74
- getProps: ({ tree, item, prev }) => (Object.assign(Object.assign({}, prev === null || prev === void 0 ? void 0 : prev()), { draggable: true, onDragStart: (e) => {
75
- var _a, _b, _c;
95
+ getProps: ({ tree, item, prev }) => (Object.assign(Object.assign({}, prev === null || prev === void 0 ? void 0 : prev()), { draggable: true, onDragEnter: (e) => e.preventDefault(), onDragStart: (e) => {
96
+ var _a, _b, _c, _d;
76
97
  const selectedItems = tree.getSelectedItems();
77
98
  const items = selectedItems.includes(item) ? selectedItems : [item];
78
99
  const config = tree.getConfig();
@@ -83,9 +104,13 @@ export const dragAndDropFeature = {
83
104
  e.preventDefault();
84
105
  return;
85
106
  }
107
+ if (config.setDragImage) {
108
+ const { imgElement, xOffset, yOffset } = config.setDragImage(items);
109
+ (_c = e.dataTransfer) === null || _c === void 0 ? void 0 : _c.setDragImage(imgElement, xOffset !== null && xOffset !== void 0 ? xOffset : 0, yOffset !== null && yOffset !== void 0 ? yOffset : 0);
110
+ }
86
111
  if (config.createForeignDragObject) {
87
112
  const { format, data } = config.createForeignDragObject(items);
88
- (_c = e.dataTransfer) === null || _c === void 0 ? void 0 : _c.setData(format, data);
113
+ (_d = e.dataTransfer) === null || _d === void 0 ? void 0 : _d.setData(format, data);
89
114
  }
90
115
  tree.applySubStateUpdate("dnd", {
91
116
  draggedItems: items,
@@ -131,6 +156,7 @@ export const dragAndDropFeature = {
131
156
  (_d = (_c = tree.getConfig()).onCompleteForeignDrop) === null || _d === void 0 ? void 0 : _d.call(_c, draggedItems);
132
157
  }, onDrop: (e) => __awaiter(void 0, void 0, void 0, function* () {
133
158
  var _a, _b, _c;
159
+ e.stopPropagation();
134
160
  const dataRef = tree.getDataRef();
135
161
  const target = getDragTarget(e, item, tree);
136
162
  if (!canDrop(e.dataTransfer, target, tree)) {
@@ -46,6 +46,11 @@ export type DragAndDropFeatureDef<T> = {
46
46
  format: string;
47
47
  data: any;
48
48
  };
49
+ setDragImage?: (items: ItemInstance<T>[]) => {
50
+ imgElement: Element;
51
+ xOffset?: number;
52
+ yOffset?: number;
53
+ };
49
54
  canDropForeignDragObject?: (dataTransfer: DataTransfer, target: DragTarget<T>) => boolean;
50
55
  onDrop?: (items: ItemInstance<T>[], target: DragTarget<T>) => void | Promise<void>;
51
56
  onDropForeignDragObject?: (dataTransfer: DataTransfer, target: DragTarget<T>) => void | Promise<void>;
@@ -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>>;
@@ -56,6 +56,10 @@ export const treeFeature = {
56
56
  var _a, _b;
57
57
  return ((_b = tree.getItemInstance((_a = tree.getState().focusedItem) !== null && _a !== void 0 ? _a : "")) !== null && _b !== void 0 ? _b : tree.getItems()[0]);
58
58
  },
59
+ getRootItem: ({ tree }) => {
60
+ const { rootItemId } = tree.getConfig();
61
+ return tree.getItemInstance(rootItemId);
62
+ },
59
63
  focusNextItem: ({ tree }) => {
60
64
  var _a;
61
65
  const focused = tree.getFocusedItem().getItemMeta();
@@ -27,7 +27,8 @@ export type TreeFeatureDef<T> = {
27
27
  treeInstance: {
28
28
  /** @internal */
29
29
  getItemsMeta: () => ItemMeta[];
30
- getFocusedItem: () => ItemInstance<any>;
30
+ getFocusedItem: () => ItemInstance<T>;
31
+ getRootItem: () => ItemInstance<T>;
31
32
  focusNextItem: () => void;
32
33
  focusPreviousItem: () => void;
33
34
  updateDomFocus: () => void;
@@ -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": "1.2.0",
3
+ "version": "1.2.1",
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
+ };
@@ -1,5 +1,5 @@
1
1
  import { FeatureImplementation } from "../../types/core";
2
- import { DndDataRef, DragLineData } from "./types";
2
+ import { DndDataRef, DragLineData, DragTarget } from "./types";
3
3
  import {
4
4
  canDrop,
5
5
  getDragCode,
@@ -83,10 +83,38 @@ export const dragAndDropFeature: FeatureImplementation = {
83
83
  : { display: "none" };
84
84
  },
85
85
 
86
- getContainerProps: ({ prev }, treeLabel) => {
86
+ getContainerProps: ({ prev, tree }, treeLabel) => {
87
87
  const prevProps = prev?.(treeLabel);
88
88
  return {
89
89
  ...prevProps,
90
+
91
+ onDragOver: (e: DragEvent) => {
92
+ e.preventDefault();
93
+ },
94
+
95
+ onDrop: async (e: DragEvent) => {
96
+ // TODO merge implementation with itemInstance.onDrop
97
+ const dataRef = tree.getDataRef<DndDataRef>();
98
+ const target: DragTarget<any> = { item: tree.getRootItem() };
99
+
100
+ if (!canDrop(e.dataTransfer, target, tree)) {
101
+ return;
102
+ }
103
+
104
+ e.preventDefault();
105
+ const config = tree.getConfig();
106
+ const draggedItems = tree.getState().dnd?.draggedItems;
107
+
108
+ dataRef.current.lastDragCode = undefined;
109
+ tree.applySubStateUpdate("dnd", null);
110
+
111
+ if (draggedItems) {
112
+ await config.onDrop?.(draggedItems, target);
113
+ } else if (e.dataTransfer) {
114
+ await config.onDropForeignDragObject?.(e.dataTransfer, target);
115
+ }
116
+ },
117
+
90
118
  style: {
91
119
  ...prevProps?.style,
92
120
  position: "relative",
@@ -101,6 +129,8 @@ export const dragAndDropFeature: FeatureImplementation = {
101
129
 
102
130
  draggable: true,
103
131
 
132
+ onDragEnter: (e: DragEvent) => e.preventDefault(),
133
+
104
134
  onDragStart: (e: DragEvent) => {
105
135
  const selectedItems = tree.getSelectedItems();
106
136
  const items = selectedItems.includes(item) ? selectedItems : [item];
@@ -115,6 +145,11 @@ export const dragAndDropFeature: FeatureImplementation = {
115
145
  return;
116
146
  }
117
147
 
148
+ if (config.setDragImage) {
149
+ const { imgElement, xOffset, yOffset } = config.setDragImage(items);
150
+ e.dataTransfer?.setDragImage(imgElement, xOffset ?? 0, yOffset ?? 0);
151
+ }
152
+
118
153
  if (config.createForeignDragObject) {
119
154
  const { format, data } = config.createForeignDragObject(items);
120
155
  e.dataTransfer?.setData(format, data);
@@ -186,6 +221,7 @@ export const dragAndDropFeature: FeatureImplementation = {
186
221
  },
187
222
 
188
223
  onDrop: async (e: DragEvent) => {
224
+ e.stopPropagation();
189
225
  const dataRef = tree.getDataRef<DndDataRef>();
190
226
  const target = getDragTarget(e, item, tree);
191
227
 
@@ -58,6 +58,11 @@ export type DragAndDropFeatureDef<T> = {
58
58
  format: string;
59
59
  data: any;
60
60
  };
61
+ setDragImage?: (items: ItemInstance<T>[]) => {
62
+ imgElement: Element;
63
+ xOffset?: number;
64
+ yOffset?: number;
65
+ };
61
66
  canDropForeignDragObject?: (
62
67
  dataTransfer: DataTransfer,
63
68
  target: DragTarget<T>,
@@ -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>>;
@@ -82,6 +82,11 @@ export const treeFeature: FeatureImplementation<any> = {
82
82
  );
83
83
  },
84
84
 
85
+ getRootItem: ({ tree }) => {
86
+ const { rootItemId } = tree.getConfig();
87
+ return tree.getItemInstance(rootItemId);
88
+ },
89
+
85
90
  focusNextItem: ({ tree }) => {
86
91
  const focused = tree.getFocusedItem().getItemMeta();
87
92
  if (!focused) return;
@@ -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;
@@ -33,7 +33,8 @@ export type TreeFeatureDef<T> = {
33
33
  /** @internal */
34
34
  getItemsMeta: () => ItemMeta[];
35
35
 
36
- getFocusedItem: () => ItemInstance<any>;
36
+ getFocusedItem: () => ItemInstance<T>;
37
+ getRootItem: () => ItemInstance<T>;
37
38
  focusNextItem: () => void;
38
39
  focusPreviousItem: () => void;
39
40
  updateDomFocus: () => 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>