@headless-tree/core 1.0.1 → 1.1.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 +15 -0
- 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/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/test-utils/test-tree-expect.js +1 -0
- 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/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/test-utils/test-tree-expect.js +1 -0
- package/package.json +1 -1
- 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/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/test-utils/test-tree-expect.ts +1 -0
- package/readme.md +0 -157
|
@@ -8,6 +8,51 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
8
8
|
});
|
|
9
9
|
};
|
|
10
10
|
import { makeStateUpdater } from "../../utils";
|
|
11
|
+
const getDataRef = (tree) => {
|
|
12
|
+
var _a, _b;
|
|
13
|
+
var _c, _d;
|
|
14
|
+
const dataRef = tree.getDataRef();
|
|
15
|
+
(_a = (_c = dataRef.current).itemData) !== null && _a !== void 0 ? _a : (_c.itemData = {});
|
|
16
|
+
(_b = (_d = dataRef.current).childrenIds) !== null && _b !== void 0 ? _b : (_d.childrenIds = {});
|
|
17
|
+
return dataRef;
|
|
18
|
+
};
|
|
19
|
+
const loadItemData = (tree, itemId) => __awaiter(void 0, void 0, void 0, function* () {
|
|
20
|
+
var _a;
|
|
21
|
+
const config = tree.getConfig();
|
|
22
|
+
const dataRef = getDataRef(tree);
|
|
23
|
+
const item = yield config.dataLoader.getItem(itemId);
|
|
24
|
+
dataRef.current.itemData[itemId] = item;
|
|
25
|
+
(_a = config.onLoadedItem) === null || _a === void 0 ? void 0 : _a.call(config, itemId, item);
|
|
26
|
+
tree.applySubStateUpdate("loadingItemData", (loadingItemData) => loadingItemData.filter((id) => id !== itemId));
|
|
27
|
+
return item;
|
|
28
|
+
});
|
|
29
|
+
const loadChildrenIds = (tree, itemId) => __awaiter(void 0, void 0, void 0, function* () {
|
|
30
|
+
var _a, _b;
|
|
31
|
+
const config = tree.getConfig();
|
|
32
|
+
const dataRef = getDataRef(tree);
|
|
33
|
+
let childrenIds;
|
|
34
|
+
if ("getChildrenWithData" in config.dataLoader) {
|
|
35
|
+
const children = yield config.dataLoader.getChildrenWithData(itemId);
|
|
36
|
+
childrenIds = children.map((c) => c.id);
|
|
37
|
+
dataRef.current.childrenIds[itemId] = childrenIds;
|
|
38
|
+
children.forEach(({ id, data }) => {
|
|
39
|
+
var _a;
|
|
40
|
+
dataRef.current.itemData[id] = data;
|
|
41
|
+
(_a = config.onLoadedItem) === null || _a === void 0 ? void 0 : _a.call(config, id, data);
|
|
42
|
+
});
|
|
43
|
+
(_a = config.onLoadedChildren) === null || _a === void 0 ? void 0 : _a.call(config, itemId, childrenIds);
|
|
44
|
+
tree.rebuildTree();
|
|
45
|
+
tree.applySubStateUpdate("loadingItemData", (loadingItemData) => loadingItemData.filter((id) => !childrenIds.includes(id)));
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
childrenIds = yield config.dataLoader.getChildren(itemId);
|
|
49
|
+
dataRef.current.childrenIds[itemId] = childrenIds;
|
|
50
|
+
(_b = config.onLoadedChildren) === null || _b === void 0 ? void 0 : _b.call(config, itemId, childrenIds);
|
|
51
|
+
tree.rebuildTree();
|
|
52
|
+
}
|
|
53
|
+
tree.applySubStateUpdate("loadingItemChildrens", (loadingItemChildrens) => loadingItemChildrens.filter((id) => id !== itemId));
|
|
54
|
+
return childrenIds;
|
|
55
|
+
});
|
|
11
56
|
export const asyncDataLoaderFeature = {
|
|
12
57
|
key: "async-data-loader",
|
|
13
58
|
getInitialState: (initialState) => (Object.assign({ loadingItemData: [], loadingItemChildrens: [] }, initialState)),
|
|
@@ -17,41 +62,20 @@ export const asyncDataLoaderFeature = {
|
|
|
17
62
|
loadingItemChildrens: "setLoadingItemChildrens",
|
|
18
63
|
},
|
|
19
64
|
treeInstance: {
|
|
20
|
-
waitForItemDataLoaded: (
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
yield new Promise((resolve) => {
|
|
26
|
-
var _a, _b;
|
|
27
|
-
var _c, _d;
|
|
28
|
-
const dataRef = tree.getDataRef();
|
|
29
|
-
(_a = (_c = dataRef.current).awaitingItemDataLoading) !== null && _a !== void 0 ? _a : (_c.awaitingItemDataLoading = {});
|
|
30
|
-
(_b = (_d = dataRef.current.awaitingItemDataLoading)[itemId]) !== null && _b !== void 0 ? _b : (_d[itemId] = []);
|
|
31
|
-
dataRef.current.awaitingItemDataLoading[itemId].push(resolve);
|
|
32
|
-
});
|
|
65
|
+
waitForItemDataLoaded: ({ tree }, itemId) => tree.loadItemData(itemId),
|
|
66
|
+
waitForItemChildrenLoaded: ({ tree }, itemId) => tree.loadChildrenIds(itemId),
|
|
67
|
+
loadItemData: (_a, itemId_1) => __awaiter(void 0, [_a, itemId_1], void 0, function* ({ tree }, itemId) {
|
|
68
|
+
var _b;
|
|
69
|
+
return ((_b = getDataRef(tree).current.itemData[itemId]) !== null && _b !== void 0 ? _b : (yield loadItemData(tree, itemId)));
|
|
33
70
|
}),
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
return;
|
|
38
|
-
}
|
|
39
|
-
yield new Promise((resolve) => {
|
|
40
|
-
var _a, _b;
|
|
41
|
-
var _c, _d;
|
|
42
|
-
const dataRef = tree.getDataRef();
|
|
43
|
-
(_a = (_c = dataRef.current).awaitingItemChildrensLoading) !== null && _a !== void 0 ? _a : (_c.awaitingItemChildrensLoading = {});
|
|
44
|
-
(_b = (_d = dataRef.current.awaitingItemChildrensLoading)[itemId]) !== null && _b !== void 0 ? _b : (_d[itemId] = []);
|
|
45
|
-
dataRef.current.awaitingItemChildrensLoading[itemId].push(resolve);
|
|
46
|
-
});
|
|
71
|
+
loadChildrenIds: (_a, itemId_1) => __awaiter(void 0, [_a, itemId_1], void 0, function* ({ tree }, itemId) {
|
|
72
|
+
var _b;
|
|
73
|
+
return ((_b = getDataRef(tree).current.childrenIds[itemId]) !== null && _b !== void 0 ? _b : (yield loadChildrenIds(tree, itemId)));
|
|
47
74
|
}),
|
|
48
75
|
retrieveItemData: ({ tree }, itemId) => {
|
|
49
|
-
var _a, _b
|
|
50
|
-
var _e, _f;
|
|
76
|
+
var _a, _b;
|
|
51
77
|
const config = tree.getConfig();
|
|
52
|
-
const dataRef =
|
|
53
|
-
(_a = (_e = dataRef.current).itemData) !== null && _a !== void 0 ? _a : (_e.itemData = {});
|
|
54
|
-
(_b = (_f = dataRef.current).childrenIds) !== null && _b !== void 0 ? _b : (_f.childrenIds = {});
|
|
78
|
+
const dataRef = getDataRef(tree);
|
|
55
79
|
if (dataRef.current.itemData[itemId]) {
|
|
56
80
|
return dataRef.current.itemData[itemId];
|
|
57
81
|
}
|
|
@@ -60,24 +84,12 @@ export const asyncDataLoaderFeature = {
|
|
|
60
84
|
...loadingItemData,
|
|
61
85
|
itemId,
|
|
62
86
|
]);
|
|
63
|
-
(
|
|
64
|
-
var _a, _b, _c;
|
|
65
|
-
const item = yield config.dataLoader.getItem(itemId);
|
|
66
|
-
dataRef.current.itemData[itemId] = item;
|
|
67
|
-
(_a = config.onLoadedItem) === null || _a === void 0 ? void 0 : _a.call(config, itemId, item);
|
|
68
|
-
tree.applySubStateUpdate("loadingItemData", (loadingItemData) => loadingItemData.filter((id) => id !== itemId));
|
|
69
|
-
(_b = dataRef.current.awaitingItemDataLoading) === null || _b === void 0 ? void 0 : _b[itemId].forEach((cb) => cb());
|
|
70
|
-
(_c = dataRef.current.awaitingItemDataLoading) === null || _c === void 0 ? true : delete _c[itemId];
|
|
71
|
-
}))();
|
|
87
|
+
loadItemData(tree, itemId);
|
|
72
88
|
}
|
|
73
|
-
return (
|
|
89
|
+
return (_b = (_a = config.createLoadingItemData) === null || _a === void 0 ? void 0 : _a.call(config)) !== null && _b !== void 0 ? _b : null;
|
|
74
90
|
},
|
|
75
91
|
retrieveChildrenIds: ({ tree }, itemId) => {
|
|
76
|
-
|
|
77
|
-
var _b;
|
|
78
|
-
const config = tree.getConfig();
|
|
79
|
-
const dataRef = tree.getDataRef();
|
|
80
|
-
(_a = (_b = dataRef.current).childrenIds) !== null && _a !== void 0 ? _a : (_b.childrenIds = {});
|
|
92
|
+
const dataRef = getDataRef(tree);
|
|
81
93
|
if (dataRef.current.childrenIds[itemId]) {
|
|
82
94
|
return dataRef.current.childrenIds[itemId];
|
|
83
95
|
}
|
|
@@ -85,34 +97,32 @@ export const asyncDataLoaderFeature = {
|
|
|
85
97
|
return [];
|
|
86
98
|
}
|
|
87
99
|
tree.applySubStateUpdate("loadingItemChildrens", (loadingItemChildrens) => [...loadingItemChildrens, itemId]);
|
|
88
|
-
(
|
|
89
|
-
var _a, _b, _c, _d;
|
|
90
|
-
const childrenIds = yield config.dataLoader.getChildren(itemId);
|
|
91
|
-
dataRef.current.childrenIds[itemId] = childrenIds;
|
|
92
|
-
(_a = config.onLoadedChildren) === null || _a === void 0 ? void 0 : _a.call(config, itemId, childrenIds);
|
|
93
|
-
tree.applySubStateUpdate("loadingItemChildrens", (loadingItemChildrens) => loadingItemChildrens.filter((id) => id !== itemId));
|
|
94
|
-
tree.rebuildTree();
|
|
95
|
-
(_c = (_b = dataRef.current.awaitingItemChildrensLoading) === null || _b === void 0 ? void 0 : _b[itemId]) === null || _c === void 0 ? void 0 : _c.forEach((cb) => cb());
|
|
96
|
-
(_d = dataRef.current.awaitingItemChildrensLoading) === null || _d === void 0 ? true : delete _d[itemId];
|
|
97
|
-
}))();
|
|
100
|
+
loadChildrenIds(tree, itemId);
|
|
98
101
|
return [];
|
|
99
102
|
},
|
|
100
103
|
},
|
|
101
104
|
itemInstance: {
|
|
102
105
|
isLoading: ({ tree, item }) => tree.getState().loadingItemData.includes(item.getItemMeta().itemId) ||
|
|
103
106
|
tree.getState().loadingItemChildrens.includes(item.getItemMeta().itemId),
|
|
104
|
-
invalidateItemData: ({ tree, itemId })
|
|
105
|
-
var
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
(
|
|
114
|
-
|
|
115
|
-
},
|
|
107
|
+
invalidateItemData: (_a, optimistic_1) => __awaiter(void 0, [_a, optimistic_1], void 0, function* ({ tree, itemId }, optimistic) {
|
|
108
|
+
var _b;
|
|
109
|
+
if (!optimistic) {
|
|
110
|
+
(_b = getDataRef(tree).current.itemData) === null || _b === void 0 ? true : delete _b[itemId];
|
|
111
|
+
tree.applySubStateUpdate("loadingItemData", (loadingItemData) => [
|
|
112
|
+
...loadingItemData,
|
|
113
|
+
itemId,
|
|
114
|
+
]);
|
|
115
|
+
}
|
|
116
|
+
yield loadItemData(tree, itemId);
|
|
117
|
+
}),
|
|
118
|
+
invalidateChildrenIds: (_a, optimistic_1) => __awaiter(void 0, [_a, optimistic_1], void 0, function* ({ tree, itemId }, optimistic) {
|
|
119
|
+
var _b;
|
|
120
|
+
if (!optimistic) {
|
|
121
|
+
(_b = getDataRef(tree).current.childrenIds) === null || _b === void 0 ? true : delete _b[itemId];
|
|
122
|
+
tree.applySubStateUpdate("loadingItemChildrens", (loadingItemChildrens) => [...loadingItemChildrens, itemId]);
|
|
123
|
+
}
|
|
124
|
+
yield loadChildrenIds(tree, itemId);
|
|
125
|
+
}),
|
|
116
126
|
updateCachedChildrenIds: ({ tree, itemId }, childrenIds) => {
|
|
117
127
|
const dataRef = tree.getDataRef();
|
|
118
128
|
dataRef.current.childrenIds[itemId] = childrenIds;
|
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
import { SetStateFn } from "../../types/core";
|
|
2
2
|
import { SyncDataLoaderFeatureDef } from "../sync-data-loader/types";
|
|
3
|
-
type AwaitingLoaderCallbacks = Record<string, (() => void)[]>;
|
|
4
3
|
export interface AsyncDataLoaderDataRef<T = any> {
|
|
5
4
|
itemData: Record<string, T>;
|
|
6
5
|
childrenIds: Record<string, string[]>;
|
|
7
|
-
awaitingItemDataLoading: AwaitingLoaderCallbacks;
|
|
8
|
-
awaitingItemChildrensLoading: AwaitingLoaderCallbacks;
|
|
9
6
|
}
|
|
10
7
|
/**
|
|
11
8
|
* @category Async Data Loader/General
|
|
@@ -27,16 +24,24 @@ export type AsyncDataLoaderFeatureDef<T> = {
|
|
|
27
24
|
onLoadedChildren?: (itemId: string, childrenIds: string[]) => void;
|
|
28
25
|
};
|
|
29
26
|
treeInstance: SyncDataLoaderFeatureDef<T>["treeInstance"] & {
|
|
27
|
+
/** @deprecated use loadItemData instead */
|
|
30
28
|
waitForItemDataLoaded: (itemId: string) => Promise<void>;
|
|
29
|
+
/** @deprecated use loadChildrenIds instead */
|
|
31
30
|
waitForItemChildrenLoaded: (itemId: string) => Promise<void>;
|
|
31
|
+
loadItemData: (itemId: string) => Promise<T>;
|
|
32
|
+
loadChildrenIds: (itemId: string) => Promise<string[]>;
|
|
32
33
|
};
|
|
33
34
|
itemInstance: SyncDataLoaderFeatureDef<T>["itemInstance"] & {
|
|
34
|
-
/** Invalidate fetched data for item, and triggers a refetch and subsequent rerender if the item is visible
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
/** Invalidate fetched data for item, and triggers a refetch and subsequent rerender if the item is visible
|
|
36
|
+
* @param optimistic If true, the item will not trigger a state update on `loadingItemData`, and
|
|
37
|
+
* the tree will continue to display the old data until the new data has loaded. */
|
|
38
|
+
invalidateItemData: (optimistic?: boolean) => Promise<void>;
|
|
39
|
+
/** Invalidate fetched children ids 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 `loadingItemChildrens`, and
|
|
41
|
+
* the tree will continue to display the old data until the new data has loaded. */
|
|
42
|
+
invalidateChildrenIds: (optimistic?: boolean) => Promise<void>;
|
|
37
43
|
updateCachedChildrenIds: (childrenIds: string[]) => void;
|
|
38
44
|
isLoading: () => boolean;
|
|
39
45
|
};
|
|
40
46
|
hotkeys: SyncDataLoaderFeatureDef<T>["hotkeys"];
|
|
41
47
|
};
|
|
42
|
-
export {};
|
|
@@ -48,7 +48,7 @@ export const expandAllFeature = {
|
|
|
48
48
|
handler: (_, tree) => __awaiter(void 0, void 0, void 0, function* () {
|
|
49
49
|
const cancelToken = { current: false };
|
|
50
50
|
const cancelHandler = (e) => {
|
|
51
|
-
if (e.
|
|
51
|
+
if (e.code === "Escape") {
|
|
52
52
|
cancelToken.current = true;
|
|
53
53
|
}
|
|
54
54
|
};
|
|
@@ -58,7 +58,7 @@ export const expandAllFeature = {
|
|
|
58
58
|
}),
|
|
59
59
|
},
|
|
60
60
|
collapseSelected: {
|
|
61
|
-
hotkey: "Control+Shift
|
|
61
|
+
hotkey: "Control+Shift+Minus",
|
|
62
62
|
handler: (_, tree) => {
|
|
63
63
|
tree.getSelectedItems().forEach((item) => item.collapseAll());
|
|
64
64
|
},
|
|
@@ -1,14 +1,29 @@
|
|
|
1
1
|
const specialKeys = {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
// TODO:breaking deprecate auto-lowercase
|
|
3
|
+
letter: /^Key[A-Z]$/,
|
|
4
|
+
letterornumber: /^(Key[A-Z]|Digit[0-9])$/,
|
|
5
|
+
plus: /^(NumpadAdd|Plus)$/,
|
|
6
|
+
minus: /^(NumpadSubtract|Minus)$/,
|
|
7
|
+
control: /^(ControlLeft|ControlRight)$/,
|
|
8
|
+
shift: /^(ShiftLeft|ShiftRight)$/,
|
|
6
9
|
};
|
|
7
10
|
const testHotkeyMatch = (pressedKeys, tree, hotkey) => {
|
|
8
|
-
const supposedKeys = hotkey.hotkey.toLowerCase().split("+");
|
|
9
|
-
const doKeysMatch = supposedKeys.every((key) =>
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
const supposedKeys = hotkey.hotkey.toLowerCase().split("+"); // TODO:breaking deprecate auto-lowercase
|
|
12
|
+
const doKeysMatch = supposedKeys.every((key) => {
|
|
13
|
+
if (key in specialKeys) {
|
|
14
|
+
return [...pressedKeys].some((pressedKey) => specialKeys[key].test(pressedKey));
|
|
15
|
+
}
|
|
16
|
+
const pressedKeysLowerCase = [...pressedKeys] // TODO:breaking deprecate auto-lowercase
|
|
17
|
+
.map((k) => k.toLowerCase());
|
|
18
|
+
if (pressedKeysLowerCase.includes(key.toLowerCase())) {
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
if (pressedKeysLowerCase.includes(`key${key.toLowerCase()}`)) {
|
|
22
|
+
// TODO:breaking deprecate e.key character matching
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
return false;
|
|
26
|
+
});
|
|
12
27
|
const isEnabled = !hotkey.isEnabled || hotkey.isEnabled(tree);
|
|
13
28
|
const equalCounts = pressedKeys.size === supposedKeys.length;
|
|
14
29
|
return doKeysMatch && isEnabled && equalCounts;
|
|
@@ -22,16 +37,24 @@ export const hotkeysCoreFeature = {
|
|
|
22
37
|
onTreeMount: (tree, element) => {
|
|
23
38
|
const data = tree.getDataRef();
|
|
24
39
|
const keydown = (e) => {
|
|
25
|
-
var _a
|
|
26
|
-
var
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
40
|
+
var _a;
|
|
41
|
+
var _b;
|
|
42
|
+
const { ignoreHotkeysOnInputs, onTreeHotkey, hotkeys } = tree.getConfig();
|
|
43
|
+
if (e.target instanceof HTMLInputElement && ignoreHotkeysOnInputs) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
(_a = (_b = data.current).pressedKeys) !== null && _a !== void 0 ? _a : (_b.pressedKeys = new Set());
|
|
47
|
+
const newMatch = !data.current.pressedKeys.has(e.code);
|
|
48
|
+
data.current.pressedKeys.add(e.code);
|
|
49
|
+
const hotkeyName = findHotkeyMatch(data.current.pressedKeys, tree, tree.getHotkeyPresets(), hotkeys);
|
|
50
|
+
if (e.target instanceof HTMLInputElement) {
|
|
51
|
+
// JS respects composite keydowns while input elements are focused, and
|
|
52
|
+
// doesnt send the associated keyup events with the same key name
|
|
53
|
+
data.current.pressedKeys.delete(e.code);
|
|
54
|
+
}
|
|
32
55
|
if (!hotkeyName)
|
|
33
56
|
return;
|
|
34
|
-
const hotkeyConfig = Object.assign(Object.assign({}, tree.getHotkeyPresets()[hotkeyName]),
|
|
57
|
+
const hotkeyConfig = Object.assign(Object.assign({}, tree.getHotkeyPresets()[hotkeyName]), hotkeys === null || hotkeys === void 0 ? void 0 : hotkeys[hotkeyName]);
|
|
35
58
|
if (!hotkeyConfig)
|
|
36
59
|
return;
|
|
37
60
|
if (!hotkeyConfig.allowWhenInputFocused &&
|
|
@@ -42,21 +65,26 @@ export const hotkeysCoreFeature = {
|
|
|
42
65
|
if (hotkeyConfig.preventDefault)
|
|
43
66
|
e.preventDefault();
|
|
44
67
|
hotkeyConfig.handler(e, tree);
|
|
45
|
-
|
|
68
|
+
onTreeHotkey === null || onTreeHotkey === void 0 ? void 0 : onTreeHotkey(hotkeyName, e);
|
|
46
69
|
};
|
|
47
70
|
const keyup = (e) => {
|
|
48
71
|
var _a;
|
|
49
72
|
var _b;
|
|
50
73
|
(_a = (_b = data.current).pressedKeys) !== null && _a !== void 0 ? _a : (_b.pressedKeys = new Set());
|
|
51
|
-
data.current.pressedKeys.delete(e.
|
|
74
|
+
data.current.pressedKeys.delete(e.code);
|
|
75
|
+
};
|
|
76
|
+
const reset = () => {
|
|
77
|
+
data.current.pressedKeys = new Set();
|
|
52
78
|
};
|
|
53
79
|
// keyup is registered on document, because some hotkeys shift
|
|
54
80
|
// the focus away from the tree (i.e. search)
|
|
55
81
|
// and then we wouldn't get the keyup event anymore
|
|
56
82
|
element.addEventListener("keydown", keydown);
|
|
57
83
|
document.addEventListener("keyup", keyup);
|
|
84
|
+
window.addEventListener("focus", reset);
|
|
58
85
|
data.current.keydownHandler = keydown;
|
|
59
86
|
data.current.keyupHandler = keyup;
|
|
87
|
+
data.current.resetHandler = reset;
|
|
60
88
|
},
|
|
61
89
|
onTreeUnmount: (tree, element) => {
|
|
62
90
|
const data = tree.getDataRef();
|
|
@@ -68,5 +96,9 @@ export const hotkeysCoreFeature = {
|
|
|
68
96
|
element.removeEventListener("keydown", data.current.keydownHandler);
|
|
69
97
|
delete data.current.keydownHandler;
|
|
70
98
|
}
|
|
99
|
+
if (data.current.resetHandler) {
|
|
100
|
+
window.removeEventListener("focus", data.current.resetHandler);
|
|
101
|
+
delete data.current.resetHandler;
|
|
102
|
+
}
|
|
71
103
|
},
|
|
72
104
|
};
|
|
@@ -10,6 +10,7 @@ export interface HotkeyConfig<T> {
|
|
|
10
10
|
export interface HotkeysCoreDataRef {
|
|
11
11
|
keydownHandler?: (e: KeyboardEvent) => void;
|
|
12
12
|
keyupHandler?: (e: KeyboardEvent) => void;
|
|
13
|
+
resetHandler?: (e: FocusEvent) => void;
|
|
13
14
|
pressedKeys: Set<string>;
|
|
14
15
|
}
|
|
15
16
|
export type HotkeysCoreFeatureDef<T> = {
|
|
@@ -17,6 +18,8 @@ export type HotkeysCoreFeatureDef<T> = {
|
|
|
17
18
|
config: {
|
|
18
19
|
hotkeys?: CustomHotkeysConfig<T>;
|
|
19
20
|
onTreeHotkey?: (name: string, e: KeyboardEvent) => void;
|
|
21
|
+
/** Do not handle key inputs while an HTML input element is focused */
|
|
22
|
+
ignoreHotkeysOnInputs?: boolean;
|
|
20
23
|
};
|
|
21
24
|
treeInstance: {};
|
|
22
25
|
itemInstance: {};
|
|
@@ -10,6 +10,12 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
10
10
|
import { makeStateUpdater } from "../../utils";
|
|
11
11
|
import { throwError } from "../../utilities/errors";
|
|
12
12
|
const promiseErrorMessage = "sync dataLoader returned promise";
|
|
13
|
+
const unpromise = (data) => {
|
|
14
|
+
if (!data || (typeof data === "object" && "then" in data)) {
|
|
15
|
+
throw throwError(promiseErrorMessage);
|
|
16
|
+
}
|
|
17
|
+
return data;
|
|
18
|
+
};
|
|
13
19
|
export const syncDataLoaderFeature = {
|
|
14
20
|
key: "sync-data-loader",
|
|
15
21
|
getInitialState: (initialState) => (Object.assign({ loadingItemData: [], loadingItemChildrens: [] }, initialState)),
|
|
@@ -22,19 +28,17 @@ export const syncDataLoaderFeature = {
|
|
|
22
28
|
waitForItemDataLoaded: () => __awaiter(void 0, void 0, void 0, function* () { }),
|
|
23
29
|
waitForItemChildrenLoaded: () => __awaiter(void 0, void 0, void 0, function* () { }),
|
|
24
30
|
retrieveItemData: ({ tree }, itemId) => {
|
|
25
|
-
|
|
26
|
-
if (typeof data === "object" && "then" in data) {
|
|
27
|
-
throw throwError(promiseErrorMessage);
|
|
28
|
-
}
|
|
29
|
-
return data;
|
|
31
|
+
return unpromise(tree.getConfig().dataLoader.getItem(itemId));
|
|
30
32
|
},
|
|
31
33
|
retrieveChildrenIds: ({ tree }, itemId) => {
|
|
32
|
-
const
|
|
33
|
-
if (
|
|
34
|
-
|
|
34
|
+
const { dataLoader } = tree.getConfig();
|
|
35
|
+
if ("getChildren" in dataLoader) {
|
|
36
|
+
return unpromise(dataLoader.getChildren(itemId));
|
|
35
37
|
}
|
|
36
|
-
return data;
|
|
38
|
+
return unpromise(dataLoader.getChildrenWithData(itemId)).map((c) => c.data);
|
|
37
39
|
},
|
|
40
|
+
loadItemData: ({ tree }, itemId) => tree.retrieveItemData(itemId),
|
|
41
|
+
loadChildrenIds: ({ tree }, itemId) => tree.retrieveChildrenIds(itemId),
|
|
38
42
|
},
|
|
39
43
|
itemInstance: {
|
|
40
44
|
isLoading: () => false,
|
|
@@ -1,7 +1,16 @@
|
|
|
1
|
-
export
|
|
1
|
+
export type TreeDataLoader<T> = {
|
|
2
2
|
getItem: (itemId: string) => T | Promise<T>;
|
|
3
3
|
getChildren: (itemId: string) => string[] | Promise<string[]>;
|
|
4
|
-
}
|
|
4
|
+
} | {
|
|
5
|
+
getItem: (itemId: string) => T | Promise<T>;
|
|
6
|
+
getChildrenWithData: (itemId: string) => {
|
|
7
|
+
id: string;
|
|
8
|
+
data: T;
|
|
9
|
+
}[] | Promise<{
|
|
10
|
+
id: string;
|
|
11
|
+
data: T;
|
|
12
|
+
}[]>;
|
|
13
|
+
};
|
|
5
14
|
export type SyncDataLoaderFeatureDef<T> = {
|
|
6
15
|
state: {};
|
|
7
16
|
config: {
|
package/package.json
CHANGED
|
@@ -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
|
});
|