@inklu/keys 2.0.1 → 2.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/LICENSE +1 -1
- package/README.md +77 -51
- package/dist/attributes.d.mts +11 -2
- package/dist/attributes.mjs +7 -6
- package/dist/{index-BTgscYdI.d.mts → index-3d9f54ln.d.mts} +50 -10
- package/dist/index.d.mts +3 -3
- package/dist/index.mjs +2 -3
- package/dist/react.d.mts +22 -6
- package/dist/react.mjs +130 -97
- package/dist/src-B76YvObL.mjs +1687 -0
- package/dist/{attributes-DotLr2MW.d.mts → types-QEXldPnL.d.mts} +163 -191
- package/package.json +5 -5
- package/dist/src-DgYjA1oS.mjs +0 -1697
package/dist/react.mjs
CHANGED
|
@@ -1,8 +1,73 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
import { a as bindingTokens, c as isSingleKeyBinding, d as isModifierKey, f as keyName, i as ariaKeyShortcuts, l as seq, n as createCommands, o as formatBinding, r as createRebinder, s as spokenBinding, t as matchCommands, u as validateBinding } from "./src-B76YvObL.mjs";
|
|
3
|
+
import { useCallback, useEffect, useMemo, useRef, useSyncExternalStore } from "react";
|
|
4
|
+
//#region src/held.ts
|
|
5
|
+
/** Which keys are down right now. Listens only while something is subscribed. */
|
|
6
|
+
const NONE = {
|
|
7
|
+
keys: [],
|
|
8
|
+
codes: {}
|
|
9
|
+
};
|
|
10
|
+
/** Held keys by physical key, so two keys producing one character stay apart. */
|
|
11
|
+
const down = /* @__PURE__ */ new Map();
|
|
12
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
13
|
+
let state = NONE;
|
|
14
|
+
const identify = (event) => event.code || `key:${keyName(event.key)}`;
|
|
15
|
+
function publish() {
|
|
16
|
+
const entries = [...down.values()];
|
|
17
|
+
state = entries.length === 0 ? NONE : {
|
|
18
|
+
keys: [...new Set(entries.map((entry) => entry.key))],
|
|
19
|
+
codes: Object.fromEntries(entries.map((entry) => [entry.key, entry.code]))
|
|
20
|
+
};
|
|
21
|
+
for (const listener of listeners) listener();
|
|
22
|
+
}
|
|
23
|
+
function onKeyDown(event) {
|
|
24
|
+
if (!event.key || down.has(identify(event))) return;
|
|
25
|
+
down.set(identify(event), {
|
|
26
|
+
key: keyName(event.key),
|
|
27
|
+
code: event.code
|
|
28
|
+
});
|
|
29
|
+
publish();
|
|
30
|
+
}
|
|
31
|
+
function onKeyUp(event) {
|
|
32
|
+
down.delete(identify(event));
|
|
33
|
+
if (event.key === "Meta") {
|
|
34
|
+
for (const [identity, entry] of down) if (!isModifierKey(entry.key)) down.delete(identity);
|
|
35
|
+
}
|
|
36
|
+
publish();
|
|
37
|
+
}
|
|
38
|
+
/** Leaving the window swallows the keyups, so nothing is held any more. */
|
|
39
|
+
function onBlur() {
|
|
40
|
+
if (down.size === 0) return;
|
|
41
|
+
down.clear();
|
|
42
|
+
publish();
|
|
43
|
+
}
|
|
44
|
+
/** The keys held right now. Empty unless something is subscribed. */
|
|
45
|
+
function heldKeys() {
|
|
46
|
+
return state;
|
|
47
|
+
}
|
|
48
|
+
/** Whether `key` is held, by the name `KeyboardEvent.key` gives it. */
|
|
49
|
+
function isHeld(key) {
|
|
50
|
+
return state.keys.includes(keyName(key));
|
|
51
|
+
}
|
|
52
|
+
/** Watches held keys until the returned function is called. */
|
|
53
|
+
function subscribeHeldKeys(listener) {
|
|
54
|
+
listeners.add(listener);
|
|
55
|
+
if (listeners.size === 1) {
|
|
56
|
+
document.addEventListener("keydown", onKeyDown, true);
|
|
57
|
+
document.addEventListener("keyup", onKeyUp, true);
|
|
58
|
+
window.addEventListener("blur", onBlur);
|
|
59
|
+
}
|
|
60
|
+
return () => {
|
|
61
|
+
listeners.delete(listener);
|
|
62
|
+
if (listeners.size > 0) return;
|
|
63
|
+
document.removeEventListener("keydown", onKeyDown, true);
|
|
64
|
+
document.removeEventListener("keyup", onKeyUp, true);
|
|
65
|
+
window.removeEventListener("blur", onBlur);
|
|
66
|
+
down.clear();
|
|
67
|
+
state = NONE;
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
//#endregion
|
|
6
71
|
//#region src/react/hooks.ts
|
|
7
72
|
const EMPTY_TOKENS = [];
|
|
8
73
|
const EMPTY_CODES = {};
|
|
@@ -55,7 +120,10 @@ function useShortcut(registry, id) {
|
|
|
55
120
|
}), [command]);
|
|
56
121
|
}
|
|
57
122
|
/**
|
|
58
|
-
* Activates a scope for the lifecycle of the component.
|
|
123
|
+
* Activates a scope for the lifecycle of the component. Scopes rank in the
|
|
124
|
+
* order they are pushed, which here is the order React runs effects in: a
|
|
125
|
+
* surface opened later ranks higher, but of a parent and a child opening in
|
|
126
|
+
* the same render the child is pushed first.
|
|
59
127
|
*
|
|
60
128
|
* @example
|
|
61
129
|
* ```tsx
|
|
@@ -64,113 +132,85 @@ function useShortcut(registry, id) {
|
|
|
64
132
|
*/
|
|
65
133
|
function useScope(registry, name, active = true, options = {}) {
|
|
66
134
|
const exclusive = options.exclusive === true;
|
|
67
|
-
const strict = options.strict === true;
|
|
68
135
|
useEffect(() => {
|
|
69
136
|
if (!active) return;
|
|
70
|
-
return registry.pushScope(name, {
|
|
71
|
-
exclusive,
|
|
72
|
-
strict
|
|
73
|
-
});
|
|
137
|
+
return registry.pushScope(name, { exclusive });
|
|
74
138
|
}, [
|
|
75
139
|
registry,
|
|
76
140
|
name,
|
|
77
141
|
active,
|
|
78
|
-
exclusive
|
|
79
|
-
strict
|
|
142
|
+
exclusive
|
|
80
143
|
]);
|
|
81
144
|
}
|
|
82
|
-
/**
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
145
|
+
/**
|
|
146
|
+
* Attaches what a command does for the lifecycle of the component, so the
|
|
147
|
+
* behaviour lives where its state does. The latest `run` is always the one
|
|
148
|
+
* called. While `enabled` is `false` the component steps back: the command
|
|
149
|
+
* falls to the handler attached before it, or cannot run if there is none.
|
|
150
|
+
* The handler attached last wins, and React attaches a parent's after its
|
|
151
|
+
* children's when they mount together.
|
|
152
|
+
*
|
|
153
|
+
* @example
|
|
154
|
+
* ```tsx
|
|
155
|
+
* useCommandHandler(commands, "palette.open", () => setOpen(true));
|
|
156
|
+
* useCommandHandler(commands, "doc.save", save, dirty);
|
|
157
|
+
* ```
|
|
158
|
+
*/
|
|
159
|
+
function useCommandHandler(registry, id, run, enabled = true) {
|
|
160
|
+
const latest = useRef(run);
|
|
161
|
+
latest.current = run;
|
|
87
162
|
useEffect(() => {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
run: (context) => latest.current.run(context),
|
|
91
|
-
when: () => latest.current.when?.() !== false
|
|
92
|
-
});
|
|
93
|
-
return () => handle.dispose();
|
|
163
|
+
if (!enabled) return;
|
|
164
|
+
return registry.handle(id, (context) => latest.current(context));
|
|
94
165
|
}, [
|
|
95
166
|
registry,
|
|
96
167
|
id,
|
|
97
|
-
|
|
168
|
+
enabled
|
|
98
169
|
]);
|
|
99
170
|
}
|
|
100
|
-
function definitionSignature(definition) {
|
|
101
|
-
return [
|
|
102
|
-
toBindings(definition.keys).map((binding) => bindingSignature(binding)).join("|"),
|
|
103
|
-
definition.scope ?? "",
|
|
104
|
-
definition.title,
|
|
105
|
-
definition.description ?? "",
|
|
106
|
-
definition.group ?? "",
|
|
107
|
-
(definition.keywords ?? []).join(","),
|
|
108
|
-
definition.hidden === true ? "1" : "0",
|
|
109
|
-
definition.enabled === false ? "0" : "1",
|
|
110
|
-
definition.preventDefault === false ? "0" : "1",
|
|
111
|
-
definition.ignoreInputs === void 0 ? "" : String(definition.ignoreInputs),
|
|
112
|
-
definition.requireReset === true ? "1" : "0",
|
|
113
|
-
definition.sequenceTimeout === void 0 ? "" : String(definition.sequenceTimeout)
|
|
114
|
-
].join(" ");
|
|
115
|
-
}
|
|
116
|
-
const IDLE_REBIND = {
|
|
117
|
-
recording: false,
|
|
118
|
-
binding: null,
|
|
119
|
-
steps: [],
|
|
120
|
-
conflicts: []
|
|
121
|
-
};
|
|
122
171
|
/** Hook for recording and configuring a command's keybindings. */
|
|
123
172
|
function useRebind(registry, id, options = {}) {
|
|
124
173
|
const latest = useRef(options);
|
|
125
174
|
latest.current = options;
|
|
126
|
-
const command = useCommand(registry, id);
|
|
127
|
-
const [rebinder, setRebinder] = useState(null);
|
|
128
|
-
const [state, setState] = useState(IDLE_REBIND);
|
|
129
175
|
const mode = options.mode ?? "chord";
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
const unsubscribe = instance.subscribe(() => setState(instance.state));
|
|
152
|
-
setRebinder(instance);
|
|
153
|
-
return () => {
|
|
154
|
-
unsubscribe();
|
|
155
|
-
instance.destroy();
|
|
156
|
-
setRebinder(null);
|
|
157
|
-
setState(IDLE_REBIND);
|
|
158
|
-
};
|
|
159
|
-
}, [
|
|
176
|
+
const rebinder = useMemo(() => createRebinder(registry, id, {
|
|
177
|
+
mode,
|
|
178
|
+
get apply() {
|
|
179
|
+
return latest.current.apply;
|
|
180
|
+
},
|
|
181
|
+
get add() {
|
|
182
|
+
return latest.current.add;
|
|
183
|
+
},
|
|
184
|
+
get rejectConflicts() {
|
|
185
|
+
return latest.current.rejectConflicts;
|
|
186
|
+
},
|
|
187
|
+
get ignoreInputs() {
|
|
188
|
+
return latest.current.ignoreInputs;
|
|
189
|
+
},
|
|
190
|
+
get idleTimeout() {
|
|
191
|
+
return latest.current.idleTimeout;
|
|
192
|
+
},
|
|
193
|
+
onRecord: (binding, conflicts) => latest.current.onRecord?.(binding, conflicts),
|
|
194
|
+
onCancel: () => latest.current.onCancel?.(),
|
|
195
|
+
onClear: () => latest.current.onClear?.()
|
|
196
|
+
}), [
|
|
160
197
|
registry,
|
|
161
198
|
id,
|
|
162
|
-
mode
|
|
163
|
-
idleTimeout
|
|
199
|
+
mode
|
|
164
200
|
]);
|
|
201
|
+
useEffect(() => rebinder.destroy, [rebinder]);
|
|
202
|
+
const getState = useCallback(() => rebinder.state, [rebinder]);
|
|
203
|
+
const state = useSyncExternalStore(rebinder.subscribe, getState, getState);
|
|
204
|
+
const command = useCommand(registry, id);
|
|
165
205
|
return useMemo(() => ({
|
|
166
206
|
...state,
|
|
167
207
|
display: command?.display[0] ?? "",
|
|
168
208
|
tokens: command?.tokens[0] ?? EMPTY_TOKENS,
|
|
169
209
|
overridden: command?.overridden ?? false,
|
|
170
|
-
start:
|
|
171
|
-
stop:
|
|
172
|
-
cancel:
|
|
173
|
-
commit:
|
|
210
|
+
start: rebinder.start,
|
|
211
|
+
stop: rebinder.stop,
|
|
212
|
+
cancel: rebinder.cancel,
|
|
213
|
+
commit: rebinder.commit,
|
|
174
214
|
clear: () => registry.keymap.set(id, null),
|
|
175
215
|
reset: () => registry.keymap.reset(id)
|
|
176
216
|
}), [
|
|
@@ -181,15 +221,9 @@ function useRebind(registry, id, options = {}) {
|
|
|
181
221
|
id
|
|
182
222
|
]);
|
|
183
223
|
}
|
|
184
|
-
function useKeyState() {
|
|
185
|
-
return useCallback((onChange) => {
|
|
186
|
-
const subscription = getKeyStateTracker().store.subscribe(onChange);
|
|
187
|
-
return () => subscription.unsubscribe();
|
|
188
|
-
}, []);
|
|
189
|
-
}
|
|
190
224
|
/** Returns an array of keys currently held down. */
|
|
191
225
|
function useHeldKeys() {
|
|
192
|
-
return useSyncExternalStore(
|
|
226
|
+
return useSyncExternalStore(subscribeHeldKeys, () => heldKeys().keys, getEmptyTokens);
|
|
193
227
|
}
|
|
194
228
|
/**
|
|
195
229
|
* Checks whether a specific key is currently held down.
|
|
@@ -200,13 +234,12 @@ function useHeldKeys() {
|
|
|
200
234
|
* ```
|
|
201
235
|
*/
|
|
202
236
|
function useKeyHold(key) {
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
return useSyncExternalStore(subscribe, getSnapshot, getFalse);
|
|
237
|
+
const getSnapshot = useCallback(() => isHeld(key), [key]);
|
|
238
|
+
return useSyncExternalStore(subscribeHeldKeys, getSnapshot, getFalse);
|
|
206
239
|
}
|
|
207
240
|
/** Returns a map of held key identifiers to physical event codes. */
|
|
208
241
|
function useHeldKeyCodes() {
|
|
209
|
-
return useSyncExternalStore(
|
|
242
|
+
return useSyncExternalStore(subscribeHeldKeys, () => heldKeys().codes, getEmptyCodes);
|
|
210
243
|
}
|
|
211
244
|
/**
|
|
212
245
|
* Returns currently active partially-typed sequences.
|
|
@@ -222,4 +255,4 @@ function usePendingSequences(registry) {
|
|
|
222
255
|
return useSyncExternalStore(subscribe, getSnapshot, getEmptyPending);
|
|
223
256
|
}
|
|
224
257
|
//#endregion
|
|
225
|
-
export {
|
|
258
|
+
export { ariaKeyShortcuts, bindingTokens, createCommands, createRebinder, formatBinding, isSingleKeyBinding, matchCommands, seq, spokenBinding, useCommand, useCommandHandler, useCommands, useHeldKeyCodes, useHeldKeys, useKeyHold, useMountCommands, usePendingSequences, useRebind, useScope, useShortcut, validateBinding };
|