@headless-tree/core 1.0.1 → 1.2.0
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 +35 -0
- package/lib/cjs/core/create-tree.js +13 -2
- package/lib/cjs/features/async-data-loader/feature.js +78 -68
- package/lib/cjs/features/async-data-loader/types.d.ts +12 -7
- package/lib/cjs/features/drag-and-drop/feature.js +1 -0
- package/lib/cjs/features/expand-all/feature.js +2 -2
- package/lib/cjs/features/hotkeys-core/feature.js +50 -18
- package/lib/cjs/features/hotkeys-core/types.d.ts +3 -0
- package/lib/cjs/features/keyboard-drag-and-drop/feature.js +1 -1
- package/lib/cjs/features/renaming/feature.js +8 -0
- package/lib/cjs/features/selection/feature.js +1 -1
- package/lib/cjs/features/sync-data-loader/feature.js +13 -9
- package/lib/cjs/features/sync-data-loader/types.d.ts +11 -2
- package/lib/cjs/features/tree/feature.js +1 -0
- package/lib/cjs/features/tree/types.d.ts +1 -0
- package/lib/cjs/index.d.ts +2 -0
- package/lib/cjs/index.js +5 -0
- package/lib/cjs/test-utils/test-tree-expect.js +1 -0
- package/lib/cjs/test-utils/test-tree.js +1 -0
- package/lib/esm/core/create-tree.js +13 -2
- package/lib/esm/features/async-data-loader/feature.js +78 -68
- package/lib/esm/features/async-data-loader/types.d.ts +12 -7
- package/lib/esm/features/drag-and-drop/feature.js +1 -0
- package/lib/esm/features/expand-all/feature.js +2 -2
- package/lib/esm/features/hotkeys-core/feature.js +50 -18
- package/lib/esm/features/hotkeys-core/types.d.ts +3 -0
- package/lib/esm/features/keyboard-drag-and-drop/feature.js +1 -1
- package/lib/esm/features/renaming/feature.js +8 -0
- package/lib/esm/features/selection/feature.js +1 -1
- package/lib/esm/features/sync-data-loader/feature.js +13 -9
- package/lib/esm/features/sync-data-loader/types.d.ts +11 -2
- package/lib/esm/features/tree/feature.js +1 -0
- package/lib/esm/features/tree/types.d.ts +1 -0
- package/lib/esm/index.d.ts +2 -0
- package/lib/esm/index.js +2 -0
- package/lib/esm/test-utils/test-tree-expect.js +1 -0
- package/lib/esm/test-utils/test-tree.js +1 -0
- package/package.json +7 -2
- package/readme.md +2 -2
- package/src/core/create-tree.ts +20 -2
- package/src/features/async-data-loader/async-data-loader.spec.ts +82 -0
- package/src/features/async-data-loader/feature.ts +92 -67
- package/src/features/async-data-loader/types.ts +14 -7
- package/src/features/drag-and-drop/feature.ts +1 -0
- package/src/features/expand-all/feature.ts +2 -2
- package/src/features/hotkeys-core/feature.ts +56 -17
- package/src/features/hotkeys-core/types.ts +4 -0
- package/src/features/keyboard-drag-and-drop/feature.ts +1 -1
- package/src/features/renaming/feature.ts +13 -0
- package/src/features/renaming/renaming.spec.ts +31 -0
- package/src/features/selection/feature.ts +1 -1
- package/src/features/sync-data-loader/feature.ts +16 -9
- package/src/features/sync-data-loader/types.ts +11 -4
- package/src/features/tree/feature.ts +1 -0
- package/src/features/tree/types.ts +1 -0
- package/src/index.ts +3 -0
- package/src/test-utils/test-tree-expect.ts +1 -0
- package/src/test-utils/test-tree.ts +1 -0
package/src/core/create-tree.ts
CHANGED
|
@@ -24,6 +24,24 @@ const verifyFeatures = (features: FeatureImplementation[] | undefined) => {
|
|
|
24
24
|
}
|
|
25
25
|
};
|
|
26
26
|
|
|
27
|
+
// Check all possible pairs and sort the array
|
|
28
|
+
const exhaustiveSort = <T>(
|
|
29
|
+
arr: T[],
|
|
30
|
+
compareFn: (param1: T, param2: T) => number,
|
|
31
|
+
) => {
|
|
32
|
+
const n = arr.length;
|
|
33
|
+
|
|
34
|
+
for (let i = 0; i < n; i++) {
|
|
35
|
+
for (let j = i + 1; j < n; j++) {
|
|
36
|
+
if (compareFn(arr[j], arr[i]) < 0) {
|
|
37
|
+
[arr[i], arr[j]] = [arr[j], arr[i]];
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return arr;
|
|
43
|
+
};
|
|
44
|
+
|
|
27
45
|
const compareFeatures =
|
|
28
46
|
(originalOrder: FeatureImplementation[]) =>
|
|
29
47
|
(feature1: FeatureImplementation, feature2: FeatureImplementation) => {
|
|
@@ -33,11 +51,12 @@ const compareFeatures =
|
|
|
33
51
|
if (feature1.key && feature2.overwrites?.includes(feature1.key)) {
|
|
34
52
|
return -1;
|
|
35
53
|
}
|
|
54
|
+
|
|
36
55
|
return originalOrder.indexOf(feature1) - originalOrder.indexOf(feature2);
|
|
37
56
|
};
|
|
38
57
|
|
|
39
58
|
const sortFeatures = (features: FeatureImplementation[] = []) =>
|
|
40
|
-
features
|
|
59
|
+
exhaustiveSort(features, compareFeatures(features));
|
|
41
60
|
|
|
42
61
|
export const createTree = <T>(
|
|
43
62
|
initialConfig: TreeConfig<T>,
|
|
@@ -228,7 +247,6 @@ export const createTree = <T>(
|
|
|
228
247
|
}
|
|
229
248
|
|
|
230
249
|
finalizeTree();
|
|
231
|
-
rebuildItemMeta();
|
|
232
250
|
|
|
233
251
|
return treeInstance;
|
|
234
252
|
};
|
|
@@ -86,6 +86,7 @@ describe("core-feature/selections", () => {
|
|
|
86
86
|
suiteTree.resetBeforeEach();
|
|
87
87
|
|
|
88
88
|
it("invalidates item data on item instance", async () => {
|
|
89
|
+
const setLoadingItemData = suiteTree.mockedHandler("setLoadingItemData");
|
|
89
90
|
getItem.mockClear();
|
|
90
91
|
await suiteTree.resolveAsyncVisibleItems();
|
|
91
92
|
getItem.mockResolvedValueOnce("new");
|
|
@@ -93,9 +94,14 @@ describe("core-feature/selections", () => {
|
|
|
93
94
|
await suiteTree.resolveAsyncVisibleItems();
|
|
94
95
|
expect(getItem).toHaveBeenCalledWith("x1");
|
|
95
96
|
expect(suiteTree.item("x1").getItemData()).toBe("new");
|
|
97
|
+
expect(setLoadingItemData).toBeCalledWith(["x1"]);
|
|
98
|
+
expect(setLoadingItemData).toBeCalledWith([]);
|
|
96
99
|
});
|
|
97
100
|
|
|
98
101
|
it("invalidates children ids on item instance", async () => {
|
|
102
|
+
const setLoadingItemChildrens = suiteTree.mockedHandler(
|
|
103
|
+
"setLoadingItemChildrens",
|
|
104
|
+
);
|
|
99
105
|
getChildren.mockClear();
|
|
100
106
|
await suiteTree.resolveAsyncVisibleItems();
|
|
101
107
|
getChildren.mockResolvedValueOnce(["new1", "new2"]);
|
|
@@ -103,6 +109,8 @@ describe("core-feature/selections", () => {
|
|
|
103
109
|
await suiteTree.resolveAsyncVisibleItems();
|
|
104
110
|
expect(getChildren).toHaveBeenCalledWith("x1");
|
|
105
111
|
suiteTree.expect.hasChildren("x1", ["new1", "new2"]);
|
|
112
|
+
expect(setLoadingItemChildrens).toBeCalledWith(["x1"]);
|
|
113
|
+
expect(setLoadingItemChildrens).toBeCalledWith([]);
|
|
106
114
|
});
|
|
107
115
|
|
|
108
116
|
it("doesnt call item data getter twice", async () => {
|
|
@@ -124,5 +132,79 @@ describe("core-feature/selections", () => {
|
|
|
124
132
|
suiteTree.expect.hasChildren("x1", ["x11", "x12", "x13", "x14"]);
|
|
125
133
|
expect(getChildren).toHaveBeenCalledTimes(1);
|
|
126
134
|
});
|
|
135
|
+
|
|
136
|
+
it("optimistic invalidates item data on item instance", async () => {
|
|
137
|
+
const setLoadingItemData = suiteTree.mockedHandler("setLoadingItemData");
|
|
138
|
+
getItem.mockClear();
|
|
139
|
+
await suiteTree.resolveAsyncVisibleItems();
|
|
140
|
+
getItem.mockResolvedValueOnce("new");
|
|
141
|
+
suiteTree.item("x1").invalidateItemData(true);
|
|
142
|
+
await suiteTree.resolveAsyncVisibleItems();
|
|
143
|
+
expect(getItem).toHaveBeenCalledWith("x1");
|
|
144
|
+
expect(suiteTree.item("x1").getItemData()).toBe("new");
|
|
145
|
+
expect(setLoadingItemData).toBeCalledTimes(1);
|
|
146
|
+
expect(setLoadingItemData).toBeCalledWith([]);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("optimistic invalidates children ids on item instance", async () => {
|
|
150
|
+
const setLoadingItemChildrens = suiteTree.mockedHandler(
|
|
151
|
+
"setLoadingItemChildrens",
|
|
152
|
+
);
|
|
153
|
+
getChildren.mockClear();
|
|
154
|
+
await suiteTree.resolveAsyncVisibleItems();
|
|
155
|
+
getChildren.mockResolvedValueOnce(["new1", "new2"]);
|
|
156
|
+
suiteTree.item("x1").invalidateChildrenIds(true);
|
|
157
|
+
await suiteTree.resolveAsyncVisibleItems();
|
|
158
|
+
expect(getChildren).toHaveBeenCalledWith("x1");
|
|
159
|
+
suiteTree.expect.hasChildren("x1", ["new1", "new2"]);
|
|
160
|
+
expect(setLoadingItemChildrens).toBeCalledTimes(1);
|
|
161
|
+
expect(setLoadingItemChildrens).toBeCalledWith([]);
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
describe("getChildrenWithData", () => {
|
|
166
|
+
const getChildrenWithData = vi.fn(async (id) => [
|
|
167
|
+
{ id: `${id}1`, data: `${id}1-data` },
|
|
168
|
+
{ id: `${id}2`, data: `${id}2-data` },
|
|
169
|
+
]);
|
|
170
|
+
const getItem = vi.fn();
|
|
171
|
+
const suiteTree = tree.with({
|
|
172
|
+
dataLoader: { getItem, getChildrenWithData },
|
|
173
|
+
});
|
|
174
|
+
suiteTree.resetBeforeEach();
|
|
175
|
+
|
|
176
|
+
it("loads children with data", async () => {
|
|
177
|
+
getChildrenWithData.mockClear();
|
|
178
|
+
suiteTree.do.selectItem("x12");
|
|
179
|
+
await suiteTree.resolveAsyncVisibleItems();
|
|
180
|
+
expect(getChildrenWithData).toHaveBeenCalledWith("x12");
|
|
181
|
+
suiteTree.expect.hasChildren("x12", ["x121", "x122"]);
|
|
182
|
+
expect(suiteTree.item("x121").getItemData()).toBe("x121-data");
|
|
183
|
+
expect(suiteTree.item("x122").getItemData()).toBe("x122-data");
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it.skip("invalidates children and reloads with data", async () => {
|
|
187
|
+
await suiteTree.resolveAsyncVisibleItems();
|
|
188
|
+
suiteTree.item("x").invalidateChildrenIds();
|
|
189
|
+
getChildrenWithData.mockResolvedValueOnce([
|
|
190
|
+
{ id: "new1", data: "new1-data" },
|
|
191
|
+
{ id: "new2", data: "new2-data" },
|
|
192
|
+
]);
|
|
193
|
+
getChildrenWithData.mockClear();
|
|
194
|
+
await suiteTree.resolveAsyncVisibleItems();
|
|
195
|
+
expect(getChildrenWithData).toHaveBeenCalledTimes(1);
|
|
196
|
+
suiteTree.expect.hasChildren("x", ["new1", "new2"]);
|
|
197
|
+
expect(suiteTree.item("new1").getItemData()).toBe("new1-data");
|
|
198
|
+
expect(suiteTree.item("new2").getItemData()).toBe("new2-data");
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it("does not call getChildrenWithData twice unnecessarily", async () => {
|
|
202
|
+
await suiteTree.resolveAsyncVisibleItems();
|
|
203
|
+
getChildrenWithData.mockClear();
|
|
204
|
+
suiteTree.item("x").invalidateChildrenIds();
|
|
205
|
+
await suiteTree.resolveAsyncVisibleItems();
|
|
206
|
+
suiteTree.expect.hasChildren("x", ["x1", "x2"]);
|
|
207
|
+
expect(getChildrenWithData).toHaveBeenCalledTimes(1);
|
|
208
|
+
});
|
|
127
209
|
});
|
|
128
210
|
});
|
|
@@ -1,7 +1,61 @@
|
|
|
1
|
-
import { FeatureImplementation } from "../../types/core";
|
|
1
|
+
import { FeatureImplementation, TreeInstance } from "../../types/core";
|
|
2
2
|
import { AsyncDataLoaderDataRef } from "./types";
|
|
3
3
|
import { makeStateUpdater } from "../../utils";
|
|
4
4
|
|
|
5
|
+
const getDataRef = <T>(tree: TreeInstance<T>) => {
|
|
6
|
+
const dataRef = tree.getDataRef<AsyncDataLoaderDataRef>();
|
|
7
|
+
dataRef.current.itemData ??= {};
|
|
8
|
+
dataRef.current.childrenIds ??= {};
|
|
9
|
+
return dataRef;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const loadItemData = async <T>(tree: TreeInstance<T>, itemId: string) => {
|
|
13
|
+
const config = tree.getConfig();
|
|
14
|
+
const dataRef = getDataRef(tree);
|
|
15
|
+
|
|
16
|
+
const item = await config.dataLoader.getItem(itemId);
|
|
17
|
+
dataRef.current.itemData[itemId] = item;
|
|
18
|
+
config.onLoadedItem?.(itemId, item);
|
|
19
|
+
tree.applySubStateUpdate("loadingItemData", (loadingItemData) =>
|
|
20
|
+
loadingItemData.filter((id) => id !== itemId),
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
return item;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const loadChildrenIds = async <T>(tree: TreeInstance<T>, itemId: string) => {
|
|
27
|
+
const config = tree.getConfig();
|
|
28
|
+
const dataRef = getDataRef(tree);
|
|
29
|
+
let childrenIds: string[];
|
|
30
|
+
|
|
31
|
+
if ("getChildrenWithData" in config.dataLoader) {
|
|
32
|
+
const children = await config.dataLoader.getChildrenWithData(itemId);
|
|
33
|
+
childrenIds = children.map((c) => c.id);
|
|
34
|
+
dataRef.current.childrenIds[itemId] = childrenIds;
|
|
35
|
+
children.forEach(({ id, data }) => {
|
|
36
|
+
dataRef.current.itemData[id] = data;
|
|
37
|
+
config.onLoadedItem?.(id, data);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
config.onLoadedChildren?.(itemId, childrenIds);
|
|
41
|
+
tree.rebuildTree();
|
|
42
|
+
tree.applySubStateUpdate("loadingItemData", (loadingItemData) =>
|
|
43
|
+
loadingItemData.filter((id) => !childrenIds.includes(id)),
|
|
44
|
+
);
|
|
45
|
+
} else {
|
|
46
|
+
childrenIds = await config.dataLoader.getChildren(itemId);
|
|
47
|
+
dataRef.current.childrenIds[itemId] = childrenIds;
|
|
48
|
+
config.onLoadedChildren?.(itemId, childrenIds);
|
|
49
|
+
tree.rebuildTree();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
tree.applySubStateUpdate("loadingItemChildrens", (loadingItemChildrens) =>
|
|
53
|
+
loadingItemChildrens.filter((id) => id !== itemId),
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
return childrenIds;
|
|
57
|
+
};
|
|
58
|
+
|
|
5
59
|
export const asyncDataLoaderFeature: FeatureImplementation = {
|
|
6
60
|
key: "async-data-loader",
|
|
7
61
|
|
|
@@ -23,37 +77,27 @@ export const asyncDataLoaderFeature: FeatureImplementation = {
|
|
|
23
77
|
},
|
|
24
78
|
|
|
25
79
|
treeInstance: {
|
|
26
|
-
waitForItemDataLoaded:
|
|
27
|
-
tree.retrieveItemData(itemId);
|
|
28
|
-
if (!tree.getState().loadingItemData.includes(itemId)) {
|
|
29
|
-
return;
|
|
30
|
-
}
|
|
31
|
-
await new Promise<void>((resolve) => {
|
|
32
|
-
const dataRef = tree.getDataRef<AsyncDataLoaderDataRef>();
|
|
33
|
-
dataRef.current.awaitingItemDataLoading ??= {};
|
|
34
|
-
dataRef.current.awaitingItemDataLoading[itemId] ??= [];
|
|
35
|
-
dataRef.current.awaitingItemDataLoading[itemId].push(resolve);
|
|
36
|
-
});
|
|
37
|
-
},
|
|
80
|
+
waitForItemDataLoaded: ({ tree }, itemId) => tree.loadItemData(itemId),
|
|
38
81
|
|
|
39
|
-
waitForItemChildrenLoaded:
|
|
40
|
-
tree.
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
82
|
+
waitForItemChildrenLoaded: ({ tree }, itemId) =>
|
|
83
|
+
tree.loadChildrenIds(itemId),
|
|
84
|
+
|
|
85
|
+
loadItemData: async ({ tree }, itemId) => {
|
|
86
|
+
return (
|
|
87
|
+
getDataRef(tree).current.itemData[itemId] ??
|
|
88
|
+
(await loadItemData(tree, itemId))
|
|
89
|
+
);
|
|
90
|
+
},
|
|
91
|
+
loadChildrenIds: async ({ tree }, itemId) => {
|
|
92
|
+
return (
|
|
93
|
+
getDataRef(tree).current.childrenIds[itemId] ??
|
|
94
|
+
(await loadChildrenIds(tree, itemId))
|
|
95
|
+
);
|
|
50
96
|
},
|
|
51
97
|
|
|
52
98
|
retrieveItemData: ({ tree }, itemId) => {
|
|
53
99
|
const config = tree.getConfig();
|
|
54
|
-
const dataRef =
|
|
55
|
-
dataRef.current.itemData ??= {};
|
|
56
|
-
dataRef.current.childrenIds ??= {};
|
|
100
|
+
const dataRef = getDataRef(tree);
|
|
57
101
|
|
|
58
102
|
if (dataRef.current.itemData[itemId]) {
|
|
59
103
|
return dataRef.current.itemData[itemId];
|
|
@@ -65,28 +109,14 @@ export const asyncDataLoaderFeature: FeatureImplementation = {
|
|
|
65
109
|
itemId,
|
|
66
110
|
]);
|
|
67
111
|
|
|
68
|
-
(
|
|
69
|
-
const item = await config.dataLoader.getItem(itemId);
|
|
70
|
-
dataRef.current.itemData[itemId] = item;
|
|
71
|
-
config.onLoadedItem?.(itemId, item);
|
|
72
|
-
tree.applySubStateUpdate("loadingItemData", (loadingItemData) =>
|
|
73
|
-
loadingItemData.filter((id) => id !== itemId),
|
|
74
|
-
);
|
|
75
|
-
|
|
76
|
-
dataRef.current.awaitingItemDataLoading?.[itemId].forEach((cb) =>
|
|
77
|
-
cb(),
|
|
78
|
-
);
|
|
79
|
-
delete dataRef.current.awaitingItemDataLoading?.[itemId];
|
|
80
|
-
})();
|
|
112
|
+
loadItemData(tree, itemId);
|
|
81
113
|
}
|
|
82
114
|
|
|
83
115
|
return config.createLoadingItemData?.() ?? null;
|
|
84
116
|
},
|
|
85
117
|
|
|
86
118
|
retrieveChildrenIds: ({ tree }, itemId) => {
|
|
87
|
-
const
|
|
88
|
-
const dataRef = tree.getDataRef<AsyncDataLoaderDataRef>();
|
|
89
|
-
dataRef.current.childrenIds ??= {};
|
|
119
|
+
const dataRef = getDataRef(tree);
|
|
90
120
|
if (dataRef.current.childrenIds[itemId]) {
|
|
91
121
|
return dataRef.current.childrenIds[itemId];
|
|
92
122
|
}
|
|
@@ -100,22 +130,7 @@ export const asyncDataLoaderFeature: FeatureImplementation = {
|
|
|
100
130
|
(loadingItemChildrens) => [...loadingItemChildrens, itemId],
|
|
101
131
|
);
|
|
102
132
|
|
|
103
|
-
(
|
|
104
|
-
const childrenIds = await config.dataLoader.getChildren(itemId);
|
|
105
|
-
dataRef.current.childrenIds[itemId] = childrenIds;
|
|
106
|
-
config.onLoadedChildren?.(itemId, childrenIds);
|
|
107
|
-
tree.applySubStateUpdate(
|
|
108
|
-
"loadingItemChildrens",
|
|
109
|
-
(loadingItemChildrens) =>
|
|
110
|
-
loadingItemChildrens.filter((id) => id !== itemId),
|
|
111
|
-
);
|
|
112
|
-
tree.rebuildTree();
|
|
113
|
-
|
|
114
|
-
dataRef.current.awaitingItemChildrensLoading?.[itemId]?.forEach((cb) =>
|
|
115
|
-
cb(),
|
|
116
|
-
);
|
|
117
|
-
delete dataRef.current.awaitingItemChildrensLoading?.[itemId];
|
|
118
|
-
})();
|
|
133
|
+
loadChildrenIds(tree, itemId);
|
|
119
134
|
|
|
120
135
|
return [];
|
|
121
136
|
},
|
|
@@ -125,15 +140,25 @@ export const asyncDataLoaderFeature: FeatureImplementation = {
|
|
|
125
140
|
isLoading: ({ tree, item }) =>
|
|
126
141
|
tree.getState().loadingItemData.includes(item.getItemMeta().itemId) ||
|
|
127
142
|
tree.getState().loadingItemChildrens.includes(item.getItemMeta().itemId),
|
|
128
|
-
invalidateItemData: ({ tree, itemId }) => {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
143
|
+
invalidateItemData: async ({ tree, itemId }, optimistic) => {
|
|
144
|
+
if (!optimistic) {
|
|
145
|
+
delete getDataRef(tree).current.itemData?.[itemId];
|
|
146
|
+
tree.applySubStateUpdate("loadingItemData", (loadingItemData) => [
|
|
147
|
+
...loadingItemData,
|
|
148
|
+
itemId,
|
|
149
|
+
]);
|
|
150
|
+
}
|
|
151
|
+
await loadItemData(tree, itemId);
|
|
132
152
|
},
|
|
133
|
-
invalidateChildrenIds: ({ tree, itemId }) => {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
153
|
+
invalidateChildrenIds: async ({ tree, itemId }, optimistic) => {
|
|
154
|
+
if (!optimistic) {
|
|
155
|
+
delete getDataRef(tree).current.childrenIds?.[itemId];
|
|
156
|
+
tree.applySubStateUpdate(
|
|
157
|
+
"loadingItemChildrens",
|
|
158
|
+
(loadingItemChildrens) => [...loadingItemChildrens, itemId],
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
await loadChildrenIds(tree, itemId);
|
|
137
162
|
},
|
|
138
163
|
updateCachedChildrenIds: ({ tree, itemId }, childrenIds) => {
|
|
139
164
|
const dataRef = tree.getDataRef<AsyncDataLoaderDataRef>();
|
|
@@ -1,13 +1,9 @@
|
|
|
1
1
|
import { SetStateFn } from "../../types/core";
|
|
2
2
|
import { SyncDataLoaderFeatureDef } from "../sync-data-loader/types";
|
|
3
3
|
|
|
4
|
-
type AwaitingLoaderCallbacks = Record<string, (() => void)[]>;
|
|
5
|
-
|
|
6
4
|
export interface AsyncDataLoaderDataRef<T = any> {
|
|
7
5
|
itemData: Record<string, T>;
|
|
8
6
|
childrenIds: Record<string, string[]>;
|
|
9
|
-
awaitingItemDataLoading: AwaitingLoaderCallbacks;
|
|
10
|
-
awaitingItemChildrensLoading: AwaitingLoaderCallbacks;
|
|
11
7
|
}
|
|
12
8
|
|
|
13
9
|
/**
|
|
@@ -32,13 +28,24 @@ export type AsyncDataLoaderFeatureDef<T> = {
|
|
|
32
28
|
onLoadedChildren?: (itemId: string, childrenIds: string[]) => void;
|
|
33
29
|
};
|
|
34
30
|
treeInstance: SyncDataLoaderFeatureDef<T>["treeInstance"] & {
|
|
31
|
+
/** @deprecated use loadItemData instead */
|
|
35
32
|
waitForItemDataLoaded: (itemId: string) => Promise<void>;
|
|
33
|
+
/** @deprecated use loadChildrenIds instead */
|
|
36
34
|
waitForItemChildrenLoaded: (itemId: string) => Promise<void>;
|
|
35
|
+
loadItemData: (itemId: string) => Promise<T>;
|
|
36
|
+
loadChildrenIds: (itemId: string) => Promise<string[]>;
|
|
37
37
|
};
|
|
38
38
|
itemInstance: SyncDataLoaderFeatureDef<T>["itemInstance"] & {
|
|
39
|
-
/** Invalidate fetched data for item, and triggers a refetch and subsequent rerender if the item is visible
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
/** Invalidate fetched data for item, and triggers a refetch and subsequent rerender if the item is visible
|
|
40
|
+
* @param optimistic If true, the item will not trigger a state update on `loadingItemData`, and
|
|
41
|
+
* the tree will continue to display the old data until the new data has loaded. */
|
|
42
|
+
invalidateItemData: (optimistic?: boolean) => Promise<void>;
|
|
43
|
+
|
|
44
|
+
/** Invalidate fetched children ids for item, and triggers a refetch and subsequent rerender if the item is visible
|
|
45
|
+
* @param optimistic If true, the item will not trigger a state update on `loadingItemChildrens`, and
|
|
46
|
+
* the tree will continue to display the old data until the new data has loaded. */
|
|
47
|
+
invalidateChildrenIds: (optimistic?: boolean) => Promise<void>;
|
|
48
|
+
|
|
42
49
|
updateCachedChildrenIds: (childrenIds: string[]) => void;
|
|
43
50
|
isLoading: () => boolean;
|
|
44
51
|
};
|
|
@@ -74,6 +74,7 @@ export const dragAndDropFeature: FeatureImplementation = {
|
|
|
74
74
|
const dragLine = tree.getDragLineData();
|
|
75
75
|
return dragLine
|
|
76
76
|
? {
|
|
77
|
+
position: "absolute",
|
|
77
78
|
top: `${dragLine.top + topOffset}px`,
|
|
78
79
|
left: `${dragLine.left + leftOffset}px`,
|
|
79
80
|
width: `${dragLine.width - leftOffset}px`,
|
|
@@ -50,7 +50,7 @@ export const expandAllFeature: FeatureImplementation = {
|
|
|
50
50
|
handler: async (_, tree) => {
|
|
51
51
|
const cancelToken = { current: false };
|
|
52
52
|
const cancelHandler = (e: KeyboardEvent) => {
|
|
53
|
-
if (e.
|
|
53
|
+
if (e.code === "Escape") {
|
|
54
54
|
cancelToken.current = true;
|
|
55
55
|
}
|
|
56
56
|
};
|
|
@@ -63,7 +63,7 @@ export const expandAllFeature: FeatureImplementation = {
|
|
|
63
63
|
},
|
|
64
64
|
|
|
65
65
|
collapseSelected: {
|
|
66
|
-
hotkey: "Control+Shift
|
|
66
|
+
hotkey: "Control+Shift+Minus",
|
|
67
67
|
handler: (_, tree) => {
|
|
68
68
|
tree.getSelectedItems().forEach((item) => item.collapseAll());
|
|
69
69
|
},
|
|
@@ -6,10 +6,13 @@ import {
|
|
|
6
6
|
import { HotkeyConfig, HotkeysCoreDataRef } from "./types";
|
|
7
7
|
|
|
8
8
|
const specialKeys: Record<string, RegExp> = {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
// TODO:breaking deprecate auto-lowercase
|
|
10
|
+
letter: /^Key[A-Z]$/,
|
|
11
|
+
letterornumber: /^(Key[A-Z]|Digit[0-9])$/,
|
|
12
|
+
plus: /^(NumpadAdd|Plus)$/,
|
|
13
|
+
minus: /^(NumpadSubtract|Minus)$/,
|
|
14
|
+
control: /^(ControlLeft|ControlRight)$/,
|
|
15
|
+
shift: /^(ShiftLeft|ShiftRight)$/,
|
|
13
16
|
};
|
|
14
17
|
|
|
15
18
|
const testHotkeyMatch = (
|
|
@@ -17,12 +20,28 @@ const testHotkeyMatch = (
|
|
|
17
20
|
tree: TreeInstance<any>,
|
|
18
21
|
hotkey: HotkeyConfig<any>,
|
|
19
22
|
) => {
|
|
20
|
-
const supposedKeys = hotkey.hotkey.toLowerCase().split("+");
|
|
21
|
-
const doKeysMatch = supposedKeys.every((key) =>
|
|
22
|
-
key in specialKeys
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
const supposedKeys = hotkey.hotkey.toLowerCase().split("+"); // TODO:breaking deprecate auto-lowercase
|
|
24
|
+
const doKeysMatch = supposedKeys.every((key) => {
|
|
25
|
+
if (key in specialKeys) {
|
|
26
|
+
return [...pressedKeys].some((pressedKey) =>
|
|
27
|
+
specialKeys[key].test(pressedKey),
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const pressedKeysLowerCase = [...pressedKeys] // TODO:breaking deprecate auto-lowercase
|
|
32
|
+
.map((k) => k.toLowerCase());
|
|
33
|
+
|
|
34
|
+
if (pressedKeysLowerCase.includes(key.toLowerCase())) {
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (pressedKeysLowerCase.includes(`key${key.toLowerCase()}`)) {
|
|
39
|
+
// TODO:breaking deprecate e.key character matching
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return false;
|
|
44
|
+
});
|
|
26
45
|
const isEnabled = !hotkey.isEnabled || hotkey.isEnabled(tree);
|
|
27
46
|
const equalCounts = pressedKeys.size === supposedKeys.length;
|
|
28
47
|
return doKeysMatch && isEnabled && equalCounts;
|
|
@@ -45,23 +64,33 @@ export const hotkeysCoreFeature: FeatureImplementation = {
|
|
|
45
64
|
onTreeMount: (tree, element) => {
|
|
46
65
|
const data = tree.getDataRef<HotkeysCoreDataRef>();
|
|
47
66
|
const keydown = (e: KeyboardEvent) => {
|
|
48
|
-
const
|
|
67
|
+
const { ignoreHotkeysOnInputs, onTreeHotkey, hotkeys } = tree.getConfig();
|
|
68
|
+
if (e.target instanceof HTMLInputElement && ignoreHotkeysOnInputs) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
49
72
|
data.current.pressedKeys ??= new Set();
|
|
50
|
-
const newMatch = !data.current.pressedKeys.has(
|
|
51
|
-
data.current.pressedKeys.add(
|
|
73
|
+
const newMatch = !data.current.pressedKeys.has(e.code);
|
|
74
|
+
data.current.pressedKeys.add(e.code);
|
|
52
75
|
|
|
53
76
|
const hotkeyName = findHotkeyMatch(
|
|
54
77
|
data.current.pressedKeys,
|
|
55
78
|
tree as any,
|
|
56
79
|
tree.getHotkeyPresets(),
|
|
57
|
-
|
|
80
|
+
hotkeys as HotkeysConfig<any>,
|
|
58
81
|
);
|
|
59
82
|
|
|
83
|
+
if (e.target instanceof HTMLInputElement) {
|
|
84
|
+
// JS respects composite keydowns while input elements are focused, and
|
|
85
|
+
// doesnt send the associated keyup events with the same key name
|
|
86
|
+
data.current.pressedKeys.delete(e.code);
|
|
87
|
+
}
|
|
88
|
+
|
|
60
89
|
if (!hotkeyName) return;
|
|
61
90
|
|
|
62
91
|
const hotkeyConfig: HotkeyConfig<any> = {
|
|
63
92
|
...tree.getHotkeyPresets()[hotkeyName],
|
|
64
|
-
...
|
|
93
|
+
...hotkeys?.[hotkeyName],
|
|
65
94
|
};
|
|
66
95
|
|
|
67
96
|
if (!hotkeyConfig) return;
|
|
@@ -74,12 +103,16 @@ export const hotkeysCoreFeature: FeatureImplementation = {
|
|
|
74
103
|
if (hotkeyConfig.preventDefault) e.preventDefault();
|
|
75
104
|
|
|
76
105
|
hotkeyConfig.handler(e, tree as any);
|
|
77
|
-
|
|
106
|
+
onTreeHotkey?.(hotkeyName, e);
|
|
78
107
|
};
|
|
79
108
|
|
|
80
109
|
const keyup = (e: KeyboardEvent) => {
|
|
81
110
|
data.current.pressedKeys ??= new Set();
|
|
82
|
-
data.current.pressedKeys.delete(e.
|
|
111
|
+
data.current.pressedKeys.delete(e.code);
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const reset = () => {
|
|
115
|
+
data.current.pressedKeys = new Set();
|
|
83
116
|
};
|
|
84
117
|
|
|
85
118
|
// keyup is registered on document, because some hotkeys shift
|
|
@@ -87,8 +120,10 @@ export const hotkeysCoreFeature: FeatureImplementation = {
|
|
|
87
120
|
// and then we wouldn't get the keyup event anymore
|
|
88
121
|
element.addEventListener("keydown", keydown);
|
|
89
122
|
document.addEventListener("keyup", keyup);
|
|
123
|
+
window.addEventListener("focus", reset);
|
|
90
124
|
data.current.keydownHandler = keydown;
|
|
91
125
|
data.current.keyupHandler = keyup;
|
|
126
|
+
data.current.resetHandler = reset;
|
|
92
127
|
},
|
|
93
128
|
|
|
94
129
|
onTreeUnmount: (tree, element) => {
|
|
@@ -101,5 +136,9 @@ export const hotkeysCoreFeature: FeatureImplementation = {
|
|
|
101
136
|
element.removeEventListener("keydown", data.current.keydownHandler);
|
|
102
137
|
delete data.current.keydownHandler;
|
|
103
138
|
}
|
|
139
|
+
if (data.current.resetHandler) {
|
|
140
|
+
window.removeEventListener("focus", data.current.resetHandler);
|
|
141
|
+
delete data.current.resetHandler;
|
|
142
|
+
}
|
|
104
143
|
},
|
|
105
144
|
};
|
|
@@ -12,6 +12,7 @@ export interface HotkeyConfig<T> {
|
|
|
12
12
|
export interface HotkeysCoreDataRef {
|
|
13
13
|
keydownHandler?: (e: KeyboardEvent) => void;
|
|
14
14
|
keyupHandler?: (e: KeyboardEvent) => void;
|
|
15
|
+
resetHandler?: (e: FocusEvent) => void;
|
|
15
16
|
pressedKeys: Set<string>;
|
|
16
17
|
}
|
|
17
18
|
|
|
@@ -20,6 +21,9 @@ export type HotkeysCoreFeatureDef<T> = {
|
|
|
20
21
|
config: {
|
|
21
22
|
hotkeys?: CustomHotkeysConfig<T>;
|
|
22
23
|
onTreeHotkey?: (name: string, e: KeyboardEvent) => void;
|
|
24
|
+
|
|
25
|
+
/** Do not handle key inputs while an HTML input element is focused */
|
|
26
|
+
ignoreHotkeysOnInputs?: boolean;
|
|
23
27
|
};
|
|
24
28
|
treeInstance: {};
|
|
25
29
|
itemInstance: {};
|
|
@@ -187,7 +187,7 @@ export const keyboardDragAndDropFeature: FeatureImplementation = {
|
|
|
187
187
|
|
|
188
188
|
hotkeys: {
|
|
189
189
|
startDrag: {
|
|
190
|
-
hotkey: "Control+Shift+
|
|
190
|
+
hotkey: "Control+Shift+KeyD",
|
|
191
191
|
preventDefault: true,
|
|
192
192
|
isEnabled: (tree) => !tree.getState().dnd,
|
|
193
193
|
handler: (_, tree) => {
|
|
@@ -9,6 +9,7 @@ type InputEvent = {
|
|
|
9
9
|
|
|
10
10
|
export const renamingFeature: FeatureImplementation = {
|
|
11
11
|
key: "renaming",
|
|
12
|
+
overwrites: ["drag-and-drop"],
|
|
12
13
|
|
|
13
14
|
getDefaultConfig: (defaultConfig, tree) => ({
|
|
14
15
|
setRenamingItem: makeStateUpdater("renamingItem", tree),
|
|
@@ -72,6 +73,18 @@ export const renamingFeature: FeatureImplementation = {
|
|
|
72
73
|
|
|
73
74
|
isRenaming: ({ tree, item }) =>
|
|
74
75
|
item.getId() === tree.getState().renamingItem,
|
|
76
|
+
|
|
77
|
+
getProps: ({ prev, item }) => {
|
|
78
|
+
const isRenaming = item.isRenaming();
|
|
79
|
+
const prevProps = prev?.() ?? {};
|
|
80
|
+
return isRenaming
|
|
81
|
+
? {
|
|
82
|
+
...prevProps,
|
|
83
|
+
draggable: false,
|
|
84
|
+
onDragStart: () => {},
|
|
85
|
+
}
|
|
86
|
+
: prevProps;
|
|
87
|
+
},
|
|
75
88
|
},
|
|
76
89
|
|
|
77
90
|
hotkeys: {
|
|
@@ -93,6 +93,37 @@ describe("core-feature/renaming", () => {
|
|
|
93
93
|
expect(setRenamingItem).toHaveBeenCalledWith(null);
|
|
94
94
|
});
|
|
95
95
|
|
|
96
|
+
describe("dragging", async () => {
|
|
97
|
+
const suiteTree = await tree
|
|
98
|
+
.withFeatures({
|
|
99
|
+
key: "drag-and-drop",
|
|
100
|
+
itemInstance: {
|
|
101
|
+
getProps: ({ prev }: any) => ({
|
|
102
|
+
...prev?.(),
|
|
103
|
+
draggable: true,
|
|
104
|
+
onDragStart: "initialOnDragStart",
|
|
105
|
+
}),
|
|
106
|
+
},
|
|
107
|
+
})
|
|
108
|
+
.createTestCaseTree();
|
|
109
|
+
suiteTree.resetBeforeEach();
|
|
110
|
+
|
|
111
|
+
it("sets draggable to undefined for items being renamed", () => {
|
|
112
|
+
const item = suiteTree.item("x1");
|
|
113
|
+
item.startRenaming();
|
|
114
|
+
const props = item.getProps();
|
|
115
|
+
expect(props.draggable).toBe(false);
|
|
116
|
+
expect(props.onDragStart).toStrictEqual(expect.any(Function));
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("retains draggable for items not being renamed", () => {
|
|
120
|
+
const item = suiteTree.item("x1");
|
|
121
|
+
const props = item.getProps();
|
|
122
|
+
expect(props.draggable).toBe(true);
|
|
123
|
+
expect(props.onDragStart).toBe("initialOnDragStart");
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
96
127
|
describe("hotkeys", () => {
|
|
97
128
|
it("starts renaming", () => {
|
|
98
129
|
const setRenamingItem = tree.mockedHandler("setRenamingItem");
|
|
@@ -144,7 +144,7 @@ export const selectionFeature: FeatureImplementation = {
|
|
|
144
144
|
},
|
|
145
145
|
},
|
|
146
146
|
selectAll: {
|
|
147
|
-
hotkey: "Control+
|
|
147
|
+
hotkey: "Control+KeyA",
|
|
148
148
|
preventDefault: true,
|
|
149
149
|
handler: (e, tree) => {
|
|
150
150
|
tree.setSelectedItems(tree.getItems().map((item) => item.getId()));
|