@headless-tree/core 0.0.0-20250507225212 → 0.0.0-20250508233207
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 +7 -1
- package/lib/cjs/features/hotkeys-core/feature.js +24 -6
- package/lib/cjs/features/hotkeys-core/types.d.ts +3 -0
- package/lib/esm/features/hotkeys-core/feature.js +24 -6
- package/lib/esm/features/hotkeys-core/types.d.ts +3 -0
- package/package.json +1 -1
- package/src/features/async-data-loader/async-data-loader.spec.ts +45 -1
- package/src/features/hotkeys-core/feature.ts +24 -3
- package/src/features/hotkeys-core/types.ts +4 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
# @headless-tree/core
|
|
2
2
|
|
|
3
|
-
## 0.0.0-
|
|
3
|
+
## 0.0.0-20250508233207
|
|
4
4
|
|
|
5
5
|
### Minor Changes
|
|
6
6
|
|
|
7
7
|
- 64d8e2a: add getChildrenWithData method to data loader to support fetching all children of an item at once
|
|
8
8
|
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- 29b2c64: improved key-handling behavior for hotkeys while input elements are focused (#98)
|
|
12
|
+
- da1e757: fixed a bug where alt-tabbing out of browser will break hotkeys feature
|
|
13
|
+
- 29b2c64: added option to completely ignore hotkey events while input elements are focused (`ignoreHotkeysOnInput`) (#98)
|
|
14
|
+
|
|
9
15
|
## 1.0.1
|
|
10
16
|
|
|
11
17
|
### Patch Changes
|
|
@@ -25,16 +25,25 @@ exports.hotkeysCoreFeature = {
|
|
|
25
25
|
onTreeMount: (tree, element) => {
|
|
26
26
|
const data = tree.getDataRef();
|
|
27
27
|
const keydown = (e) => {
|
|
28
|
-
var _a
|
|
29
|
-
var
|
|
28
|
+
var _a;
|
|
29
|
+
var _b;
|
|
30
|
+
const { ignoreHotkeysOnInputs, onTreeHotkey, hotkeys } = tree.getConfig();
|
|
31
|
+
if (e.target instanceof HTMLInputElement && ignoreHotkeysOnInputs) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
30
34
|
const key = e.key.toLowerCase();
|
|
31
|
-
(_a = (
|
|
35
|
+
(_a = (_b = data.current).pressedKeys) !== null && _a !== void 0 ? _a : (_b.pressedKeys = new Set());
|
|
32
36
|
const newMatch = !data.current.pressedKeys.has(key);
|
|
33
37
|
data.current.pressedKeys.add(key);
|
|
34
|
-
const hotkeyName = findHotkeyMatch(data.current.pressedKeys, tree, tree.getHotkeyPresets(),
|
|
38
|
+
const hotkeyName = findHotkeyMatch(data.current.pressedKeys, tree, tree.getHotkeyPresets(), hotkeys);
|
|
39
|
+
if (e.target instanceof HTMLInputElement) {
|
|
40
|
+
// JS respects composite keydowns while input elements are focused, and
|
|
41
|
+
// doesnt send the associated keyup events with the same key name
|
|
42
|
+
data.current.pressedKeys.delete(key);
|
|
43
|
+
}
|
|
35
44
|
if (!hotkeyName)
|
|
36
45
|
return;
|
|
37
|
-
const hotkeyConfig = Object.assign(Object.assign({}, tree.getHotkeyPresets()[hotkeyName]),
|
|
46
|
+
const hotkeyConfig = Object.assign(Object.assign({}, tree.getHotkeyPresets()[hotkeyName]), hotkeys === null || hotkeys === void 0 ? void 0 : hotkeys[hotkeyName]);
|
|
38
47
|
if (!hotkeyConfig)
|
|
39
48
|
return;
|
|
40
49
|
if (!hotkeyConfig.allowWhenInputFocused &&
|
|
@@ -45,7 +54,7 @@ exports.hotkeysCoreFeature = {
|
|
|
45
54
|
if (hotkeyConfig.preventDefault)
|
|
46
55
|
e.preventDefault();
|
|
47
56
|
hotkeyConfig.handler(e, tree);
|
|
48
|
-
|
|
57
|
+
onTreeHotkey === null || onTreeHotkey === void 0 ? void 0 : onTreeHotkey(hotkeyName, e);
|
|
49
58
|
};
|
|
50
59
|
const keyup = (e) => {
|
|
51
60
|
var _a;
|
|
@@ -53,13 +62,18 @@ exports.hotkeysCoreFeature = {
|
|
|
53
62
|
(_a = (_b = data.current).pressedKeys) !== null && _a !== void 0 ? _a : (_b.pressedKeys = new Set());
|
|
54
63
|
data.current.pressedKeys.delete(e.key.toLowerCase());
|
|
55
64
|
};
|
|
65
|
+
const reset = () => {
|
|
66
|
+
data.current.pressedKeys = new Set();
|
|
67
|
+
};
|
|
56
68
|
// keyup is registered on document, because some hotkeys shift
|
|
57
69
|
// the focus away from the tree (i.e. search)
|
|
58
70
|
// and then we wouldn't get the keyup event anymore
|
|
59
71
|
element.addEventListener("keydown", keydown);
|
|
60
72
|
document.addEventListener("keyup", keyup);
|
|
73
|
+
window.addEventListener("focus", reset);
|
|
61
74
|
data.current.keydownHandler = keydown;
|
|
62
75
|
data.current.keyupHandler = keyup;
|
|
76
|
+
data.current.resetHandler = reset;
|
|
63
77
|
},
|
|
64
78
|
onTreeUnmount: (tree, element) => {
|
|
65
79
|
const data = tree.getDataRef();
|
|
@@ -71,5 +85,9 @@ exports.hotkeysCoreFeature = {
|
|
|
71
85
|
element.removeEventListener("keydown", data.current.keydownHandler);
|
|
72
86
|
delete data.current.keydownHandler;
|
|
73
87
|
}
|
|
88
|
+
if (data.current.resetHandler) {
|
|
89
|
+
window.removeEventListener("focus", data.current.resetHandler);
|
|
90
|
+
delete data.current.resetHandler;
|
|
91
|
+
}
|
|
74
92
|
},
|
|
75
93
|
};
|
|
@@ -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: {};
|
|
@@ -22,16 +22,25 @@ export const hotkeysCoreFeature = {
|
|
|
22
22
|
onTreeMount: (tree, element) => {
|
|
23
23
|
const data = tree.getDataRef();
|
|
24
24
|
const keydown = (e) => {
|
|
25
|
-
var _a
|
|
26
|
-
var
|
|
25
|
+
var _a;
|
|
26
|
+
var _b;
|
|
27
|
+
const { ignoreHotkeysOnInputs, onTreeHotkey, hotkeys } = tree.getConfig();
|
|
28
|
+
if (e.target instanceof HTMLInputElement && ignoreHotkeysOnInputs) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
27
31
|
const key = e.key.toLowerCase();
|
|
28
|
-
(_a = (
|
|
32
|
+
(_a = (_b = data.current).pressedKeys) !== null && _a !== void 0 ? _a : (_b.pressedKeys = new Set());
|
|
29
33
|
const newMatch = !data.current.pressedKeys.has(key);
|
|
30
34
|
data.current.pressedKeys.add(key);
|
|
31
|
-
const hotkeyName = findHotkeyMatch(data.current.pressedKeys, tree, tree.getHotkeyPresets(),
|
|
35
|
+
const hotkeyName = findHotkeyMatch(data.current.pressedKeys, tree, tree.getHotkeyPresets(), hotkeys);
|
|
36
|
+
if (e.target instanceof HTMLInputElement) {
|
|
37
|
+
// JS respects composite keydowns while input elements are focused, and
|
|
38
|
+
// doesnt send the associated keyup events with the same key name
|
|
39
|
+
data.current.pressedKeys.delete(key);
|
|
40
|
+
}
|
|
32
41
|
if (!hotkeyName)
|
|
33
42
|
return;
|
|
34
|
-
const hotkeyConfig = Object.assign(Object.assign({}, tree.getHotkeyPresets()[hotkeyName]),
|
|
43
|
+
const hotkeyConfig = Object.assign(Object.assign({}, tree.getHotkeyPresets()[hotkeyName]), hotkeys === null || hotkeys === void 0 ? void 0 : hotkeys[hotkeyName]);
|
|
35
44
|
if (!hotkeyConfig)
|
|
36
45
|
return;
|
|
37
46
|
if (!hotkeyConfig.allowWhenInputFocused &&
|
|
@@ -42,7 +51,7 @@ export const hotkeysCoreFeature = {
|
|
|
42
51
|
if (hotkeyConfig.preventDefault)
|
|
43
52
|
e.preventDefault();
|
|
44
53
|
hotkeyConfig.handler(e, tree);
|
|
45
|
-
|
|
54
|
+
onTreeHotkey === null || onTreeHotkey === void 0 ? void 0 : onTreeHotkey(hotkeyName, e);
|
|
46
55
|
};
|
|
47
56
|
const keyup = (e) => {
|
|
48
57
|
var _a;
|
|
@@ -50,13 +59,18 @@ export const hotkeysCoreFeature = {
|
|
|
50
59
|
(_a = (_b = data.current).pressedKeys) !== null && _a !== void 0 ? _a : (_b.pressedKeys = new Set());
|
|
51
60
|
data.current.pressedKeys.delete(e.key.toLowerCase());
|
|
52
61
|
};
|
|
62
|
+
const reset = () => {
|
|
63
|
+
data.current.pressedKeys = new Set();
|
|
64
|
+
};
|
|
53
65
|
// keyup is registered on document, because some hotkeys shift
|
|
54
66
|
// the focus away from the tree (i.e. search)
|
|
55
67
|
// and then we wouldn't get the keyup event anymore
|
|
56
68
|
element.addEventListener("keydown", keydown);
|
|
57
69
|
document.addEventListener("keyup", keyup);
|
|
70
|
+
window.addEventListener("focus", reset);
|
|
58
71
|
data.current.keydownHandler = keydown;
|
|
59
72
|
data.current.keyupHandler = keyup;
|
|
73
|
+
data.current.resetHandler = reset;
|
|
60
74
|
},
|
|
61
75
|
onTreeUnmount: (tree, element) => {
|
|
62
76
|
const data = tree.getDataRef();
|
|
@@ -68,5 +82,9 @@ export const hotkeysCoreFeature = {
|
|
|
68
82
|
element.removeEventListener("keydown", data.current.keydownHandler);
|
|
69
83
|
delete data.current.keydownHandler;
|
|
70
84
|
}
|
|
85
|
+
if (data.current.resetHandler) {
|
|
86
|
+
window.removeEventListener("focus", data.current.resetHandler);
|
|
87
|
+
delete data.current.resetHandler;
|
|
88
|
+
}
|
|
71
89
|
},
|
|
72
90
|
};
|
|
@@ -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: {};
|
package/package.json
CHANGED
|
@@ -126,5 +126,49 @@ describe("core-feature/selections", () => {
|
|
|
126
126
|
});
|
|
127
127
|
});
|
|
128
128
|
|
|
129
|
-
|
|
129
|
+
describe("getChildrenWithData", () => {
|
|
130
|
+
const getChildrenWithData = vi.fn(async (id) => [
|
|
131
|
+
{ id: `${id}1`, data: `${id}1-data` },
|
|
132
|
+
{ id: `${id}2`, data: `${id}2-data` },
|
|
133
|
+
]);
|
|
134
|
+
const getItem = vi.fn();
|
|
135
|
+
const suiteTree = tree.with({
|
|
136
|
+
dataLoader: { getItem, getChildrenWithData },
|
|
137
|
+
});
|
|
138
|
+
suiteTree.resetBeforeEach();
|
|
139
|
+
|
|
140
|
+
it("loads children with data", async () => {
|
|
141
|
+
getChildrenWithData.mockClear();
|
|
142
|
+
suiteTree.do.selectItem("x12");
|
|
143
|
+
await suiteTree.resolveAsyncVisibleItems();
|
|
144
|
+
expect(getChildrenWithData).toHaveBeenCalledWith("x12");
|
|
145
|
+
suiteTree.expect.hasChildren("x12", ["x121", "x122"]);
|
|
146
|
+
expect(suiteTree.item("x121").getItemData()).toBe("x121-data");
|
|
147
|
+
expect(suiteTree.item("x122").getItemData()).toBe("x122-data");
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it.skip("invalidates children and reloads with data", async () => {
|
|
151
|
+
await suiteTree.resolveAsyncVisibleItems();
|
|
152
|
+
suiteTree.item("x").invalidateChildrenIds();
|
|
153
|
+
getChildrenWithData.mockResolvedValueOnce([
|
|
154
|
+
{ id: "new1", data: "new1-data" },
|
|
155
|
+
{ id: "new2", data: "new2-data" },
|
|
156
|
+
]);
|
|
157
|
+
getChildrenWithData.mockClear();
|
|
158
|
+
await suiteTree.resolveAsyncVisibleItems();
|
|
159
|
+
expect(getChildrenWithData).toHaveBeenCalledTimes(1);
|
|
160
|
+
suiteTree.expect.hasChildren("x", ["new1", "new2"]);
|
|
161
|
+
expect(suiteTree.item("new1").getItemData()).toBe("new1-data");
|
|
162
|
+
expect(suiteTree.item("new2").getItemData()).toBe("new2-data");
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("does not call getChildrenWithData twice unnecessarily", async () => {
|
|
166
|
+
await suiteTree.resolveAsyncVisibleItems();
|
|
167
|
+
getChildrenWithData.mockClear();
|
|
168
|
+
suiteTree.item("x").invalidateChildrenIds();
|
|
169
|
+
await suiteTree.resolveAsyncVisibleItems();
|
|
170
|
+
suiteTree.expect.hasChildren("x", ["x1", "x2"]);
|
|
171
|
+
expect(getChildrenWithData).toHaveBeenCalledTimes(1);
|
|
172
|
+
});
|
|
173
|
+
});
|
|
130
174
|
});
|
|
@@ -45,6 +45,11 @@ export const hotkeysCoreFeature: FeatureImplementation = {
|
|
|
45
45
|
onTreeMount: (tree, element) => {
|
|
46
46
|
const data = tree.getDataRef<HotkeysCoreDataRef>();
|
|
47
47
|
const keydown = (e: KeyboardEvent) => {
|
|
48
|
+
const { ignoreHotkeysOnInputs, onTreeHotkey, hotkeys } = tree.getConfig();
|
|
49
|
+
if (e.target instanceof HTMLInputElement && ignoreHotkeysOnInputs) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
48
53
|
const key = e.key.toLowerCase();
|
|
49
54
|
data.current.pressedKeys ??= new Set();
|
|
50
55
|
const newMatch = !data.current.pressedKeys.has(key);
|
|
@@ -54,14 +59,20 @@ export const hotkeysCoreFeature: FeatureImplementation = {
|
|
|
54
59
|
data.current.pressedKeys,
|
|
55
60
|
tree as any,
|
|
56
61
|
tree.getHotkeyPresets(),
|
|
57
|
-
|
|
62
|
+
hotkeys as HotkeysConfig<any>,
|
|
58
63
|
);
|
|
59
64
|
|
|
65
|
+
if (e.target instanceof HTMLInputElement) {
|
|
66
|
+
// JS respects composite keydowns while input elements are focused, and
|
|
67
|
+
// doesnt send the associated keyup events with the same key name
|
|
68
|
+
data.current.pressedKeys.delete(key);
|
|
69
|
+
}
|
|
70
|
+
|
|
60
71
|
if (!hotkeyName) return;
|
|
61
72
|
|
|
62
73
|
const hotkeyConfig: HotkeyConfig<any> = {
|
|
63
74
|
...tree.getHotkeyPresets()[hotkeyName],
|
|
64
|
-
...
|
|
75
|
+
...hotkeys?.[hotkeyName],
|
|
65
76
|
};
|
|
66
77
|
|
|
67
78
|
if (!hotkeyConfig) return;
|
|
@@ -74,7 +85,7 @@ export const hotkeysCoreFeature: FeatureImplementation = {
|
|
|
74
85
|
if (hotkeyConfig.preventDefault) e.preventDefault();
|
|
75
86
|
|
|
76
87
|
hotkeyConfig.handler(e, tree as any);
|
|
77
|
-
|
|
88
|
+
onTreeHotkey?.(hotkeyName, e);
|
|
78
89
|
};
|
|
79
90
|
|
|
80
91
|
const keyup = (e: KeyboardEvent) => {
|
|
@@ -82,13 +93,19 @@ export const hotkeysCoreFeature: FeatureImplementation = {
|
|
|
82
93
|
data.current.pressedKeys.delete(e.key.toLowerCase());
|
|
83
94
|
};
|
|
84
95
|
|
|
96
|
+
const reset = () => {
|
|
97
|
+
data.current.pressedKeys = new Set();
|
|
98
|
+
};
|
|
99
|
+
|
|
85
100
|
// keyup is registered on document, because some hotkeys shift
|
|
86
101
|
// the focus away from the tree (i.e. search)
|
|
87
102
|
// and then we wouldn't get the keyup event anymore
|
|
88
103
|
element.addEventListener("keydown", keydown);
|
|
89
104
|
document.addEventListener("keyup", keyup);
|
|
105
|
+
window.addEventListener("focus", reset);
|
|
90
106
|
data.current.keydownHandler = keydown;
|
|
91
107
|
data.current.keyupHandler = keyup;
|
|
108
|
+
data.current.resetHandler = reset;
|
|
92
109
|
},
|
|
93
110
|
|
|
94
111
|
onTreeUnmount: (tree, element) => {
|
|
@@ -101,5 +118,9 @@ export const hotkeysCoreFeature: FeatureImplementation = {
|
|
|
101
118
|
element.removeEventListener("keydown", data.current.keydownHandler);
|
|
102
119
|
delete data.current.keydownHandler;
|
|
103
120
|
}
|
|
121
|
+
if (data.current.resetHandler) {
|
|
122
|
+
window.removeEventListener("focus", data.current.resetHandler);
|
|
123
|
+
delete data.current.resetHandler;
|
|
124
|
+
}
|
|
104
125
|
},
|
|
105
126
|
};
|
|
@@ -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: {};
|