@headless-tree/core 1.4.0 → 1.5.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.
- package/CHANGELOG.md +27 -0
- package/dist/index.d.mts +34 -10
- package/dist/index.d.ts +34 -10
- package/dist/index.js +152 -73
- package/dist/index.mjs +152 -73
- package/package.json +19 -3
- package/readme.md +6 -6
- package/src/core/create-tree.ts +45 -9
- package/src/features/async-data-loader/async-data-loader.spec.ts +1 -0
- package/src/features/async-data-loader/feature.ts +16 -20
- package/src/features/async-data-loader/types.ts +2 -1
- package/src/features/checkboxes/checkboxes.spec.ts +111 -122
- package/src/features/checkboxes/feature.ts +89 -40
- package/src/features/checkboxes/types.ts +16 -3
- package/src/features/drag-and-drop/feature.ts +7 -0
- package/src/features/drag-and-drop/types.ts +6 -0
- package/src/features/hotkeys-core/feature.ts +2 -0
- package/src/features/main/types.ts +9 -0
- package/src/features/sync-data-loader/types.ts +7 -1
- package/src/features/tree/feature.ts +2 -2
- package/src/features/tree/tree.spec.ts +37 -4
- package/src/mddocs-entry.ts +13 -0
- package/src/test-utils/test-tree-do.ts +6 -0
- package/src/test-utils/test-tree.ts +17 -6
- package/src/types/core.ts +5 -5
|
@@ -3,147 +3,136 @@ import { TestTree } from "../../test-utils/test-tree";
|
|
|
3
3
|
import { checkboxesFeature } from "./feature";
|
|
4
4
|
import { CheckedState } from "./types";
|
|
5
5
|
|
|
6
|
-
const factory = TestTree.default({
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
const factory = TestTree.default({
|
|
7
|
+
propagateCheckedState: true,
|
|
8
|
+
canCheckFolders: false,
|
|
9
|
+
}).withFeatures(checkboxesFeature);
|
|
9
10
|
|
|
10
11
|
describe("core-feature/checkboxes", () => {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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");
|
|
12
|
+
factory.forSuits((tree) => {
|
|
13
|
+
it("should initialize with no checked items", async () => {
|
|
14
|
+
expect(tree.instance.getState().checkedItems).toEqual([]);
|
|
15
|
+
});
|
|
34
16
|
|
|
35
|
-
|
|
36
|
-
|
|
17
|
+
it("should check items", async () => {
|
|
18
|
+
await tree.item("x111").setChecked();
|
|
19
|
+
await tree.item("x112").setChecked();
|
|
20
|
+
expect(tree.instance.getState().checkedItems).toEqual(["x111", "x112"]);
|
|
21
|
+
});
|
|
37
22
|
|
|
38
|
-
item
|
|
39
|
-
|
|
40
|
-
|
|
23
|
+
it("should uncheck an item", async () => {
|
|
24
|
+
await tree.item("x111").setChecked();
|
|
25
|
+
await tree.item("x111").setUnchecked();
|
|
26
|
+
expect(tree.instance.getState().checkedItems).not.toContain("x111");
|
|
27
|
+
});
|
|
41
28
|
|
|
42
|
-
describe("props", () => {
|
|
43
29
|
it("should toggle checked state", async () => {
|
|
44
|
-
const tree = await factory.createTestCaseTree();
|
|
45
30
|
const item = tree.item("x111");
|
|
46
|
-
|
|
47
|
-
item.getCheckboxProps().onChange();
|
|
31
|
+
await item.toggleCheckedState();
|
|
48
32
|
expect(tree.instance.getState().checkedItems).toContain("x111");
|
|
49
|
-
|
|
50
|
-
item.getCheckboxProps().onChange();
|
|
33
|
+
await item.toggleCheckedState();
|
|
51
34
|
expect(tree.instance.getState().checkedItems).not.toContain("x111");
|
|
52
35
|
});
|
|
53
36
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
37
|
+
describe("props", () => {
|
|
38
|
+
it("should toggle checked state", async () => {
|
|
39
|
+
const item = tree.item("x111");
|
|
40
|
+
item.getCheckboxProps().onChange();
|
|
41
|
+
expect(tree.instance.getState().checkedItems).toContain("x111");
|
|
42
|
+
item.getCheckboxProps().onChange();
|
|
43
|
+
expect(tree.instance.getState().checkedItems).not.toContain("x111");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("should return checked state in props", async () => {
|
|
47
|
+
tree.item("x111").setChecked();
|
|
48
|
+
expect(tree.item("x111").getCheckboxProps().checked).toBe(true);
|
|
49
|
+
expect(tree.item("x112").getCheckboxProps().checked).toBe(false);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("should create indeterminate state", async () => {
|
|
53
|
+
await tree.item("x111").setChecked();
|
|
54
|
+
const refObject = { indeterminate: undefined };
|
|
55
|
+
tree.item("x11").getCheckboxProps().ref(refObject);
|
|
56
|
+
expect(refObject.indeterminate).toBe(true);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("should not create indeterminate state", async () => {
|
|
60
|
+
const refObject = { indeterminate: undefined };
|
|
61
|
+
tree.item("x11").getCheckboxProps().ref(refObject);
|
|
62
|
+
expect(refObject.indeterminate).toBe(false);
|
|
63
|
+
});
|
|
59
64
|
});
|
|
60
65
|
|
|
61
|
-
it("should
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
expect(
|
|
66
|
+
it("should handle folder checking", async () => {
|
|
67
|
+
const testTree = await tree
|
|
68
|
+
.with({ canCheckFolders: true, propagateCheckedState: false })
|
|
69
|
+
.createTestCaseTree();
|
|
70
|
+
testTree.item("x11").setChecked();
|
|
71
|
+
expect(testTree.instance.getState().checkedItems).toContain("x11");
|
|
67
72
|
});
|
|
68
73
|
|
|
69
|
-
it("should not
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
+
it("should not check folders if disabled", async () => {
|
|
75
|
+
const testTree = await tree
|
|
76
|
+
.with({ canCheckFolders: false, propagateCheckedState: false })
|
|
77
|
+
.createTestCaseTree();
|
|
78
|
+
testTree.item("x11").setChecked();
|
|
79
|
+
expect(testTree.instance.getState().checkedItems.length).toBe(0);
|
|
74
80
|
});
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
it("should handle folder checking", async () => {
|
|
78
|
-
const tree = await factory
|
|
79
|
-
.with({ canCheckFolders: true, propagateCheckedState: false })
|
|
80
|
-
.createTestCaseTree();
|
|
81
|
-
|
|
82
|
-
tree.item("x11").setChecked();
|
|
83
|
-
expect(tree.instance.getState().checkedItems).toContain("x11");
|
|
84
|
-
});
|
|
85
81
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
it("should propagate checked state", async () => {
|
|
96
|
-
const tree = await factory
|
|
97
|
-
.with({ propagateCheckedState: true })
|
|
98
|
-
.createTestCaseTree();
|
|
99
|
-
|
|
100
|
-
tree.item("x11").setChecked();
|
|
101
|
-
expect(tree.instance.getState().checkedItems).toEqual(
|
|
102
|
-
expect.arrayContaining(["x111", "x112", "x113", "x114"]),
|
|
103
|
-
);
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
it("should turn folder indeterminate", async () => {
|
|
107
|
-
const tree = await factory
|
|
108
|
-
.with({ propagateCheckedState: true })
|
|
109
|
-
.createTestCaseTree();
|
|
110
|
-
|
|
111
|
-
tree.item("x111").setChecked();
|
|
112
|
-
expect(tree.item("x11").getCheckedState()).toBe(CheckedState.Indeterminate);
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
it("should turn folder checked if all children are checked", async () => {
|
|
116
|
-
const tree = await factory
|
|
117
|
-
.with({
|
|
118
|
-
isItemFolder: (item) => item.getItemData().length < 4,
|
|
119
|
-
propagateCheckedState: true,
|
|
120
|
-
canCheckFolders: false,
|
|
121
|
-
})
|
|
122
|
-
.createTestCaseTree();
|
|
123
|
-
|
|
124
|
-
tree.item("x11").setChecked();
|
|
125
|
-
tree.item("x12").setChecked();
|
|
126
|
-
tree.item("x13").setChecked();
|
|
127
|
-
expect(tree.item("x1").getCheckedState()).toBe(CheckedState.Indeterminate);
|
|
128
|
-
tree.do.selectItem("x14");
|
|
129
|
-
tree.item("x141").setChecked();
|
|
130
|
-
tree.item("x142").setChecked();
|
|
131
|
-
tree.item("x143").setChecked();
|
|
132
|
-
expect(tree.item("x1").getCheckedState()).toBe(CheckedState.Indeterminate);
|
|
133
|
-
tree.item("x144").setChecked();
|
|
134
|
-
expect(tree.item("x1").getCheckedState()).toBe(CheckedState.Checked);
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
it("should return correct checked state for items", async () => {
|
|
138
|
-
const tree = await factory.createTestCaseTree();
|
|
139
|
-
const item = tree.instance.getItemInstance("x111");
|
|
82
|
+
it("should propagate checked state", async () => {
|
|
83
|
+
const testTree = await tree
|
|
84
|
+
.with({ propagateCheckedState: true })
|
|
85
|
+
.createTestCaseTree();
|
|
86
|
+
await testTree.item("x11").setChecked();
|
|
87
|
+
expect(testTree.instance.getState().checkedItems).toEqual(
|
|
88
|
+
expect.arrayContaining(["x111", "x112", "x113", "x114"]),
|
|
89
|
+
);
|
|
90
|
+
});
|
|
140
91
|
|
|
141
|
-
|
|
92
|
+
it("should turn folder indeterminate", async () => {
|
|
93
|
+
const testTree = await tree
|
|
94
|
+
.with({ propagateCheckedState: true })
|
|
95
|
+
.createTestCaseTree();
|
|
96
|
+
testTree.item("x111").setChecked();
|
|
97
|
+
expect(testTree.item("x11").getCheckedState()).toBe(
|
|
98
|
+
CheckedState.Indeterminate,
|
|
99
|
+
);
|
|
100
|
+
});
|
|
142
101
|
|
|
143
|
-
|
|
144
|
-
|
|
102
|
+
it("should turn folder checked if all children are checked", async () => {
|
|
103
|
+
const testTree = await tree
|
|
104
|
+
.with({
|
|
105
|
+
isItemFolder: (item: any) => item.getItemData().length < 4,
|
|
106
|
+
propagateCheckedState: true,
|
|
107
|
+
canCheckFolders: false,
|
|
108
|
+
})
|
|
109
|
+
.createTestCaseTree();
|
|
110
|
+
testTree.do.selectItem("x14"); // all leafs must be loaded initially, checkpropagation check only respects visibly loaded items
|
|
111
|
+
// TODO ^ might be a restriction we want to avoid
|
|
112
|
+
await testTree.resolveAsyncVisibleItems();
|
|
113
|
+
await testTree.runWhileResolvingItems(testTree.item("x11").setChecked);
|
|
114
|
+
await testTree.runWhileResolvingItems(testTree.item("x12").setChecked);
|
|
115
|
+
await testTree.runWhileResolvingItems(testTree.item("x13").setChecked);
|
|
116
|
+
expect(testTree.item("x1").getCheckedState()).toBe(
|
|
117
|
+
CheckedState.Indeterminate,
|
|
118
|
+
);
|
|
119
|
+
await testTree.runWhileResolvingItems(testTree.item("x141").setChecked);
|
|
120
|
+
await testTree.runWhileResolvingItems(testTree.item("x142").setChecked);
|
|
121
|
+
await testTree.runWhileResolvingItems(testTree.item("x143").setChecked);
|
|
122
|
+
expect(testTree.item("x1").getCheckedState()).toBe(
|
|
123
|
+
CheckedState.Indeterminate,
|
|
124
|
+
);
|
|
125
|
+
await testTree.runWhileResolvingItems(testTree.item("x144").setChecked);
|
|
126
|
+
expect(testTree.item("x1").getCheckedState()).toBe(CheckedState.Checked);
|
|
127
|
+
});
|
|
145
128
|
|
|
146
|
-
|
|
147
|
-
|
|
129
|
+
it("should return correct checked state for items", async () => {
|
|
130
|
+
const item = tree.instance.getItemInstance("x111");
|
|
131
|
+
expect(item.getCheckedState()).toBe(CheckedState.Unchecked);
|
|
132
|
+
item.setChecked();
|
|
133
|
+
expect(item.getCheckedState()).toBe(CheckedState.Checked);
|
|
134
|
+
item.setUnchecked();
|
|
135
|
+
expect(item.getCheckedState()).toBe(CheckedState.Unchecked);
|
|
136
|
+
});
|
|
148
137
|
});
|
|
149
138
|
});
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { FeatureImplementation, TreeInstance } from "../../types/core";
|
|
1
|
+
import { type FeatureImplementation, TreeInstance } from "../../types/core";
|
|
2
2
|
import { makeStateUpdater } from "../../utils";
|
|
3
3
|
import { CheckedState } from "./types";
|
|
4
|
-
import { throwError } from "../../utilities/errors";
|
|
5
4
|
|
|
6
5
|
const getAllLoadedDescendants = <T>(
|
|
7
6
|
tree: TreeInstance<T>,
|
|
@@ -12,12 +11,50 @@ const getAllLoadedDescendants = <T>(
|
|
|
12
11
|
return [itemId];
|
|
13
12
|
}
|
|
14
13
|
const descendants = tree
|
|
15
|
-
.retrieveChildrenIds(itemId)
|
|
14
|
+
.retrieveChildrenIds(itemId, true)
|
|
16
15
|
.map((child) => getAllLoadedDescendants(tree, child, includeFolders))
|
|
17
16
|
.flat();
|
|
18
17
|
return includeFolders ? [itemId, ...descendants] : descendants;
|
|
19
18
|
};
|
|
20
19
|
|
|
20
|
+
const getAllDescendants = async <T>(
|
|
21
|
+
tree: TreeInstance<T>,
|
|
22
|
+
itemId: string,
|
|
23
|
+
includeFolders = false,
|
|
24
|
+
): Promise<string[]> => {
|
|
25
|
+
await tree.loadItemData(itemId);
|
|
26
|
+
if (!tree.getConfig().isItemFolder(tree.getItemInstance(itemId))) {
|
|
27
|
+
return [itemId];
|
|
28
|
+
}
|
|
29
|
+
const childrenIds = await tree.loadChildrenIds(itemId);
|
|
30
|
+
const descendants = (
|
|
31
|
+
await Promise.all(
|
|
32
|
+
childrenIds.map((child) =>
|
|
33
|
+
getAllDescendants(tree, child, includeFolders),
|
|
34
|
+
),
|
|
35
|
+
)
|
|
36
|
+
).flat();
|
|
37
|
+
return includeFolders ? [itemId, ...descendants] : descendants;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const withLoadingState = async <T>(
|
|
41
|
+
tree: TreeInstance<T>,
|
|
42
|
+
itemId: string,
|
|
43
|
+
callback: () => Promise<void>,
|
|
44
|
+
) => {
|
|
45
|
+
tree.applySubStateUpdate("loadingCheckPropagationItems", (items) => [
|
|
46
|
+
...items,
|
|
47
|
+
itemId,
|
|
48
|
+
]);
|
|
49
|
+
try {
|
|
50
|
+
await callback();
|
|
51
|
+
} finally {
|
|
52
|
+
tree.applySubStateUpdate("loadingCheckPropagationItems", (items) =>
|
|
53
|
+
items.filter((id) => id !== itemId),
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
21
58
|
export const checkboxesFeature: FeatureImplementation = {
|
|
22
59
|
key: "checkboxes",
|
|
23
60
|
|
|
@@ -25,22 +62,20 @@ export const checkboxesFeature: FeatureImplementation = {
|
|
|
25
62
|
|
|
26
63
|
getInitialState: (initialState) => ({
|
|
27
64
|
checkedItems: [],
|
|
65
|
+
loadingCheckPropagationItems: [],
|
|
28
66
|
...initialState,
|
|
29
67
|
}),
|
|
30
68
|
|
|
31
69
|
getDefaultConfig: (defaultConfig, tree) => {
|
|
32
|
-
const
|
|
33
|
-
(f) => f.key === "async-data-loader",
|
|
34
|
-
);
|
|
35
|
-
if (hasAsyncLoader && defaultConfig.propagateCheckedState) {
|
|
36
|
-
throwError(`propagateCheckedState not supported with async trees`);
|
|
37
|
-
}
|
|
38
|
-
const propagateCheckedState =
|
|
39
|
-
defaultConfig.propagateCheckedState ?? !hasAsyncLoader;
|
|
70
|
+
const propagateCheckedState = defaultConfig.propagateCheckedState ?? true;
|
|
40
71
|
const canCheckFolders =
|
|
41
72
|
defaultConfig.canCheckFolders ?? !propagateCheckedState;
|
|
42
73
|
return {
|
|
43
74
|
setCheckedItems: makeStateUpdater("checkedItems", tree),
|
|
75
|
+
setLoadingCheckPropagationItems: makeStateUpdater(
|
|
76
|
+
"loadingCheckPropagationItems",
|
|
77
|
+
tree,
|
|
78
|
+
),
|
|
44
79
|
propagateCheckedState,
|
|
45
80
|
canCheckFolders,
|
|
46
81
|
...defaultConfig,
|
|
@@ -49,6 +84,7 @@ export const checkboxesFeature: FeatureImplementation = {
|
|
|
49
84
|
|
|
50
85
|
stateHandlerNames: {
|
|
51
86
|
checkedItems: "setCheckedItems",
|
|
87
|
+
loadingCheckPropagationItems: "setLoadingCheckPropagationItems",
|
|
52
88
|
},
|
|
53
89
|
|
|
54
90
|
treeInstance: {
|
|
@@ -71,11 +107,11 @@ export const checkboxesFeature: FeatureImplementation = {
|
|
|
71
107
|
};
|
|
72
108
|
},
|
|
73
109
|
|
|
74
|
-
toggleCheckedState: ({ item }) => {
|
|
110
|
+
toggleCheckedState: async ({ item }) => {
|
|
75
111
|
if (item.getCheckedState() === CheckedState.Checked) {
|
|
76
|
-
item.setUnchecked();
|
|
112
|
+
await item.setUnchecked();
|
|
77
113
|
} else {
|
|
78
|
-
item.setChecked();
|
|
114
|
+
await item.setChecked();
|
|
79
115
|
}
|
|
80
116
|
},
|
|
81
117
|
|
|
@@ -90,6 +126,7 @@ export const checkboxesFeature: FeatureImplementation = {
|
|
|
90
126
|
|
|
91
127
|
if (item.isFolder() && propagateCheckedState) {
|
|
92
128
|
const descendants = getAllLoadedDescendants(tree, itemId);
|
|
129
|
+
if (descendants.length === 0) return CheckedState.Unchecked;
|
|
93
130
|
if (descendants.every((d) => checkedItems.includes(d))) {
|
|
94
131
|
return CheckedState.Checked;
|
|
95
132
|
}
|
|
@@ -101,34 +138,46 @@ export const checkboxesFeature: FeatureImplementation = {
|
|
|
101
138
|
return CheckedState.Unchecked;
|
|
102
139
|
},
|
|
103
140
|
|
|
104
|
-
setChecked: ({ item, tree, itemId }) => {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
141
|
+
setChecked: async ({ item, tree, itemId }) => {
|
|
142
|
+
await withLoadingState(tree, itemId, async () => {
|
|
143
|
+
const { propagateCheckedState, canCheckFolders } = tree.getConfig();
|
|
144
|
+
if (item.isFolder() && propagateCheckedState) {
|
|
145
|
+
const descendants = await getAllDescendants(
|
|
146
|
+
tree,
|
|
147
|
+
itemId,
|
|
148
|
+
canCheckFolders,
|
|
149
|
+
);
|
|
150
|
+
tree.applySubStateUpdate("checkedItems", (items) => [
|
|
151
|
+
...items,
|
|
152
|
+
...descendants,
|
|
153
|
+
]);
|
|
154
|
+
} else if (!item.isFolder() || canCheckFolders) {
|
|
155
|
+
tree.applySubStateUpdate("checkedItems", (items) => [
|
|
156
|
+
...items,
|
|
157
|
+
itemId,
|
|
158
|
+
]);
|
|
159
|
+
}
|
|
160
|
+
});
|
|
114
161
|
},
|
|
115
162
|
|
|
116
|
-
setUnchecked: ({ item, tree, itemId }) => {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
163
|
+
setUnchecked: async ({ item, tree, itemId }) => {
|
|
164
|
+
await withLoadingState(tree, itemId, async () => {
|
|
165
|
+
const { propagateCheckedState, canCheckFolders } = tree.getConfig();
|
|
166
|
+
if (item.isFolder() && propagateCheckedState) {
|
|
167
|
+
const descendants = await getAllDescendants(
|
|
168
|
+
tree,
|
|
169
|
+
itemId,
|
|
170
|
+
canCheckFolders,
|
|
171
|
+
);
|
|
172
|
+
tree.applySubStateUpdate("checkedItems", (items) =>
|
|
173
|
+
items.filter((id) => !descendants.includes(id) && id !== itemId),
|
|
174
|
+
);
|
|
175
|
+
} else {
|
|
176
|
+
tree.applySubStateUpdate("checkedItems", (items) =>
|
|
177
|
+
items.filter((id) => id !== itemId),
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
});
|
|
132
181
|
},
|
|
133
182
|
},
|
|
134
183
|
};
|
|
@@ -9,9 +9,11 @@ export enum CheckedState {
|
|
|
9
9
|
export type CheckboxesFeatureDef<T> = {
|
|
10
10
|
state: {
|
|
11
11
|
checkedItems: string[];
|
|
12
|
+
loadingCheckPropagationItems: string[];
|
|
12
13
|
};
|
|
13
14
|
config: {
|
|
14
15
|
setCheckedItems?: SetStateFn<string[]>;
|
|
16
|
+
setLoadingCheckPropagationItems?: SetStateFn<string[]>;
|
|
15
17
|
canCheckFolders?: boolean;
|
|
16
18
|
propagateCheckedState?: boolean;
|
|
17
19
|
};
|
|
@@ -19,11 +21,22 @@ export type CheckboxesFeatureDef<T> = {
|
|
|
19
21
|
setCheckedItems: (checkedItems: string[]) => void;
|
|
20
22
|
};
|
|
21
23
|
itemInstance: {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
24
|
+
/** Will recursively load descendants if propagateCheckedState=true and async data loader is used. If not,
|
|
25
|
+
* this will return immediately. */
|
|
26
|
+
setChecked: () => Promise<void>;
|
|
27
|
+
|
|
28
|
+
/** Will recursively load descendants if propagateCheckedState=true and async data loader is used. If not,
|
|
29
|
+
* this will return immediately. */
|
|
30
|
+
setUnchecked: () => Promise<void>;
|
|
31
|
+
|
|
32
|
+
/** Will recursively load descendants if propagateCheckedState=true and async data loader is used. If not,
|
|
33
|
+
* this will return immediately. */
|
|
34
|
+
toggleCheckedState: () => Promise<void>;
|
|
35
|
+
|
|
25
36
|
getCheckedState: () => CheckedState;
|
|
26
37
|
getCheckboxProps: () => Record<string, any>;
|
|
38
|
+
|
|
39
|
+
isLoadingCheckPropagation: () => boolean;
|
|
27
40
|
};
|
|
28
41
|
hotkeys: never;
|
|
29
42
|
};
|
|
@@ -332,6 +332,13 @@ export const dragAndDropFeature: FeatureImplementation = {
|
|
|
332
332
|
return target ? target.item.getId() === item.getId() : false;
|
|
333
333
|
},
|
|
334
334
|
|
|
335
|
+
isUnorderedDragTarget: ({ tree, item }) => {
|
|
336
|
+
const target = tree.getDragTarget();
|
|
337
|
+
return target
|
|
338
|
+
? !isOrderedDragTarget(target) && target.item.getId() === item.getId()
|
|
339
|
+
: false;
|
|
340
|
+
},
|
|
341
|
+
|
|
335
342
|
isDragTargetAbove: ({ tree, item }) => {
|
|
336
343
|
const target = tree.getDragTarget();
|
|
337
344
|
|
|
@@ -108,7 +108,13 @@ export type DragAndDropFeatureDef<T> = {
|
|
|
108
108
|
) => Record<string, any>;
|
|
109
109
|
};
|
|
110
110
|
itemInstance: {
|
|
111
|
+
/** Checks if the user is dragging in a way which makes this the new parent of the dragged items, either by dragging on top of
|
|
112
|
+
* this item, or by dragging inbetween children of this item. See @{isUnorderedDragTarget} if the latter is undesirable. */
|
|
111
113
|
isDragTarget: () => boolean;
|
|
114
|
+
|
|
115
|
+
/** As opposed to @{isDragTarget}, this will not be true if the target is inbetween children of this item. This returns only true
|
|
116
|
+
* if the user is dragging directly on top of this item. */
|
|
117
|
+
isUnorderedDragTarget: () => boolean;
|
|
112
118
|
isDragTargetAbove: () => boolean;
|
|
113
119
|
isDragTargetBelow: () => boolean;
|
|
114
120
|
isDraggingOver: () => boolean;
|
|
@@ -13,6 +13,8 @@ const specialKeys: Record<string, RegExp> = {
|
|
|
13
13
|
minus: /^(NumpadSubtract|Minus)$/,
|
|
14
14
|
control: /^(ControlLeft|ControlRight)$/,
|
|
15
15
|
shift: /^(ShiftLeft|ShiftRight)$/,
|
|
16
|
+
metaorcontrol: /^(MetaLeft|MetaRight|ControlLeft|ControlRight)$/,
|
|
17
|
+
enter: /^(Enter|NumpadEnter)$/,
|
|
16
18
|
};
|
|
17
19
|
|
|
18
20
|
const testHotkeyMatch = (
|
|
@@ -10,6 +10,11 @@ import {
|
|
|
10
10
|
} from "../../types/core";
|
|
11
11
|
import { ItemMeta } from "../tree/types";
|
|
12
12
|
|
|
13
|
+
export interface TreeDataRef {
|
|
14
|
+
isMounted?: boolean;
|
|
15
|
+
waitingForMount?: (() => void)[];
|
|
16
|
+
}
|
|
17
|
+
|
|
13
18
|
export type InstanceTypeMap = {
|
|
14
19
|
itemInstance: ItemInstance<any>;
|
|
15
20
|
treeInstance: TreeInstance<any>;
|
|
@@ -49,6 +54,10 @@ export type MainFeatureDef<T = any> = {
|
|
|
49
54
|
/* @internal */
|
|
50
55
|
getHotkeyPresets: () => HotkeysConfig<T>;
|
|
51
56
|
rebuildTree: () => void;
|
|
57
|
+
/** @deprecated Experimental feature, might get removed or changed in the future. */
|
|
58
|
+
scheduleRebuildTree: () => void;
|
|
59
|
+
/** @internal */
|
|
60
|
+
setMounted: (isMounted: boolean) => void;
|
|
52
61
|
};
|
|
53
62
|
itemInstance: {
|
|
54
63
|
registerElement: (element: HTMLElement | null) => void;
|
|
@@ -18,7 +18,13 @@ export type SyncDataLoaderFeatureDef<T> = {
|
|
|
18
18
|
};
|
|
19
19
|
treeInstance: {
|
|
20
20
|
retrieveItemData: (itemId: string) => T;
|
|
21
|
-
|
|
21
|
+
|
|
22
|
+
/** Retrieve children Ids. If an async data loader is used, skipFetch is set to true, and children have not been retrieved
|
|
23
|
+
* yet for this item, this will initiate fetching the children, and return an empty array. Once the children have loaded,
|
|
24
|
+
* a rerender will be triggered.
|
|
25
|
+
* @param skipFetch - Defaults to false.
|
|
26
|
+
*/
|
|
27
|
+
retrieveChildrenIds: (itemId: string, skipFetch?: boolean) => string[];
|
|
22
28
|
};
|
|
23
29
|
itemInstance: {
|
|
24
30
|
isLoading: () => boolean;
|
|
@@ -203,8 +203,8 @@ export const treeFeature: FeatureImplementation<any> = {
|
|
|
203
203
|
isFocused: ({ tree, item, itemId }) =>
|
|
204
204
|
tree.getState().focusedItem === itemId ||
|
|
205
205
|
(tree.getState().focusedItem === null && item.getItemMeta().index === 0),
|
|
206
|
-
isFolder: ({ tree, item }) =>
|
|
207
|
-
|
|
206
|
+
isFolder: ({ tree, item, itemId }) =>
|
|
207
|
+
itemId === tree.getConfig().rootItemId ||
|
|
208
208
|
tree.getConfig().isItemFolder(item as ItemInstance<any>),
|
|
209
209
|
getItemName: ({ tree, item }) => {
|
|
210
210
|
const config = tree.getConfig();
|
|
@@ -4,8 +4,8 @@ import { propMemoizationFeature } from "../prop-memoization/feature";
|
|
|
4
4
|
|
|
5
5
|
const factory = TestTree.default({}).withFeatures(propMemoizationFeature);
|
|
6
6
|
|
|
7
|
-
describe("core-feature/
|
|
8
|
-
factory.forSuits((tree) => {
|
|
7
|
+
describe("core-feature/tree", () => {
|
|
8
|
+
factory.forSuits((tree, title) => {
|
|
9
9
|
describe("expanded items", () => {
|
|
10
10
|
it("can expand via tree instance", () => {
|
|
11
11
|
const setExpandedItems = tree.mockedHandler("setExpandedItems");
|
|
@@ -156,6 +156,17 @@ describe("core-feature/selections", () => {
|
|
|
156
156
|
});
|
|
157
157
|
});
|
|
158
158
|
|
|
159
|
+
it("unloaded item", () => {
|
|
160
|
+
expect(tree.instance.getItemInstance("x444").getItemMeta()).toEqual({
|
|
161
|
+
index: -1,
|
|
162
|
+
itemId: "x444",
|
|
163
|
+
level: -1,
|
|
164
|
+
parentId: null,
|
|
165
|
+
posInSet: 0,
|
|
166
|
+
setSize: 1,
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
159
170
|
it("expanded container", () => {
|
|
160
171
|
expect(tree.instance.getItemInstance("x11").getItemMeta()).toEqual({
|
|
161
172
|
index: 1,
|
|
@@ -312,8 +323,30 @@ describe("core-feature/selections", () => {
|
|
|
312
323
|
expect(tree.instance.getItemInstance("x1").isFolder()).toBe(true);
|
|
313
324
|
});
|
|
314
325
|
|
|
315
|
-
it("returns correctly for
|
|
316
|
-
expect(tree.instance.getItemInstance("
|
|
326
|
+
it("returns correctly for true cases of isFolder() ", () => {
|
|
327
|
+
expect(tree.instance.getItemInstance("x1").isFolder()).toBe(true);
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it("returns correct isFolder for hidden items", async () => {
|
|
331
|
+
if (title.toLocaleLowerCase().includes("async")) {
|
|
332
|
+
// async test tree defaults to "loading" item names
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
const testTree = await tree
|
|
336
|
+
.with({
|
|
337
|
+
isItemFolder: (item: any) => item.getItemData().length < 4,
|
|
338
|
+
})
|
|
339
|
+
.createTestCaseTree();
|
|
340
|
+
|
|
341
|
+
// Reference: https://github.com/lukasbach/headless-tree/issues/166
|
|
342
|
+
expect(testTree.instance.getItemInstance("x44").isFolder()).toBe(true);
|
|
343
|
+
expect(testTree.instance.getItemInstance("x444").isFolder()).toBe(
|
|
344
|
+
false,
|
|
345
|
+
);
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
it("returns isFolder=true for root item", () => {
|
|
349
|
+
expect(tree.instance.getItemInstance("x").isFolder()).toBe(true);
|
|
317
350
|
});
|
|
318
351
|
|
|
319
352
|
it("returns correctly for getParent()", () => {
|
package/src/mddocs-entry.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { SyncDataLoaderFeatureDef } from "./features/sync-data-loader/types";
|
|
|
10
10
|
import { TreeFeatureDef } from "./features/tree/types";
|
|
11
11
|
import { PropMemoizationFeatureDef } from "./features/prop-memoization/types";
|
|
12
12
|
import { KeyboardDragAndDropFeatureDef } from "./features/keyboard-drag-and-drop/types";
|
|
13
|
+
import type { CheckboxesFeatureDef } from "./features/checkboxes/types";
|
|
13
14
|
|
|
14
15
|
export * from ".";
|
|
15
16
|
|
|
@@ -167,3 +168,15 @@ export type TreeFeatureTreeInstance<T> = TreeFeatureDef<T>["treeInstance"];
|
|
|
167
168
|
/** @interface */
|
|
168
169
|
export type TreeFeatureItemInstance<T> = TreeFeatureDef<T>["itemInstance"];
|
|
169
170
|
export type TreeFeatureHotkeys<T> = TreeFeatureDef<T>["hotkeys"];
|
|
171
|
+
|
|
172
|
+
/** @interface */
|
|
173
|
+
export type CheckboxesFeatureConfig<T> = CheckboxesFeatureDef<T>["config"];
|
|
174
|
+
/** @interface */
|
|
175
|
+
export type CheckboxesFeatureState<T> = CheckboxesFeatureDef<T>["state"];
|
|
176
|
+
/** @interface */
|
|
177
|
+
export type CheckboxesFeatureTreeInstance<T> =
|
|
178
|
+
CheckboxesFeatureDef<T>["treeInstance"];
|
|
179
|
+
/** @interface */
|
|
180
|
+
export type CheckboxesFeatureItemInstance<T> =
|
|
181
|
+
CheckboxesFeatureDef<T>["itemInstance"];
|
|
182
|
+
export type CheckboxesFeatureHotkeys<T> = CheckboxesFeatureDef<T>["hotkeys"];
|