@tanstack/hotkeys 0.11.0 → 0.12.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/dist/adapter.d.ts +108 -19
- package/dist/adapter.js +90 -65
- package/dist/app-hotkeys.d.ts +239 -0
- package/dist/app-hotkeys.js +295 -0
- package/dist/conflicts.d.ts +5 -9
- package/dist/conflicts.js +6 -11
- package/dist/format.d.ts +19 -24
- package/dist/format.js +19 -30
- package/dist/hint.d.ts +1 -0
- package/dist/hint.js +5 -2
- package/dist/hotkey-manager.d.ts +110 -59
- package/dist/hotkey-manager.js +254 -151
- package/dist/hotkey-recorder.d.ts +66 -42
- package/dist/hotkey-recorder.js +148 -107
- package/dist/hotkey.types.d.ts +26 -12
- package/dist/index.d.ts +10 -11
- package/dist/index.js +5 -6
- package/dist/match.d.ts +38 -5
- package/dist/match.js +72 -4
- package/dist/parse.d.ts +20 -8
- package/dist/parse.js +15 -5
- package/dist/recorder-options.d.ts +6 -3
- package/dist/validate.d.ts +10 -4
- package/dist/validate.js +26 -4
- package/package.json +1 -1
- package/dist/hotkey-sequence-recorder.d.ts +0 -84
- package/dist/hotkey-sequence-recorder.js +0 -259
- package/dist/sequence-manager.d.ts +0 -169
- package/dist/sequence-manager.js +0 -443
package/dist/index.js
CHANGED
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
import { ALL_KEYS, EDITING_KEYS, FUNCTION_KEYS, LETTER_KEYS, MODIFIER_ALIASES, MODIFIER_KEYS, MODIFIER_ORDER, NAVIGATION_KEYS, NUMBER_KEYS, PUNCTUATION_CODE_MAP, PUNCTUATION_KEYS, isSingleLetterKey, normalizeKeyName } from "./constants.js";
|
|
2
2
|
import { KEY_DISPLAY_SYMBOLS, LINUX_MODIFIER_LABELS, MAC_MODIFIER_LABELS, MAC_MODIFIER_SYMBOLS, PUNCTUATION_KEY_DISPLAY_LABELS, WINDOWS_MODIFIER_LABELS } from "./display-labels.js";
|
|
3
3
|
import { detectPlatform, resolveModifier } from "./platform.js";
|
|
4
|
-
import { hasNonModifierKey, isModifierKey, normalizeHotkey, normalizeHotkeyFromEvent, normalizeHotkeyFromParsed, normalizeRegisterableHotkey, parseHotkey, parseKeyboardEvent, parseRegisterableHotkey, rawHotkeyToParsedHotkey } from "./parse.js";
|
|
5
|
-
import { formatForDisplay, formatHotkey
|
|
4
|
+
import { hasNonModifierKey, isHotkeySequence, isModifierKey, normalizeHotkey, normalizeHotkeyFromEvent, normalizeHotkeyFromParsed, normalizeRegisterableHotkey, parseHotkey, parseKeyboardEvent, parseRegisterableHotkey, rawHotkeyToParsedHotkey, simplifyHotkey } from "./parse.js";
|
|
5
|
+
import { formatForDisplay, formatHotkey } from "./format.js";
|
|
6
6
|
import { assertValidHotkey, checkHotkey, validateHotkey } from "./validate.js";
|
|
7
|
-
import { areHotkeysEqual, createHotkeyHandler, createMultiHotkeyHandler, matchesKeyboardEvent } from "./match.js";
|
|
7
|
+
import { DEFAULT_SEQUENCE_TIMEOUT, areHotkeysEqual, createHotkeyHandler, createMultiHotkeyHandler, createSequenceMatcher, matchesKeyboardEvent } from "./match.js";
|
|
8
8
|
import { matchesHeldModifiers } from "./hint.js";
|
|
9
9
|
import { HotkeyManager, getHotkeyManager, toHotkeyRegistrationView } from "./hotkey-manager.js";
|
|
10
|
-
import { DEFAULT_SEQUENCE_TIMEOUT, SequenceManager, createSequenceMatcher, getSequenceManager } from "./sequence-manager.js";
|
|
11
10
|
import { findHotkeyConflicts } from "./conflicts.js";
|
|
11
|
+
import { AppHotkeysStore, defineAppHotkeys, getAppHotkeyRegistrationOptions, isAppHotkey, isCustomHotkey, provideAppHotkeys, publishAppHotkeysStore, resolveAppHotkey } from "./app-hotkeys.js";
|
|
12
12
|
import { KeyStateTracker, getKeyStateTracker } from "./key-state-tracker.js";
|
|
13
13
|
import { HotkeyRecorder } from "./hotkey-recorder.js";
|
|
14
|
-
import { HotkeySequenceRecorder } from "./hotkey-sequence-recorder.js";
|
|
15
14
|
|
|
16
|
-
export { ALL_KEYS, DEFAULT_SEQUENCE_TIMEOUT, EDITING_KEYS, FUNCTION_KEYS, HotkeyManager, HotkeyRecorder,
|
|
15
|
+
export { ALL_KEYS, AppHotkeysStore, DEFAULT_SEQUENCE_TIMEOUT, EDITING_KEYS, FUNCTION_KEYS, HotkeyManager, HotkeyRecorder, KEY_DISPLAY_SYMBOLS, KeyStateTracker, LETTER_KEYS, LINUX_MODIFIER_LABELS, MAC_MODIFIER_LABELS, MAC_MODIFIER_SYMBOLS, MODIFIER_ALIASES, MODIFIER_KEYS, MODIFIER_ORDER, NAVIGATION_KEYS, NUMBER_KEYS, PUNCTUATION_CODE_MAP, PUNCTUATION_KEYS, PUNCTUATION_KEY_DISPLAY_LABELS, WINDOWS_MODIFIER_LABELS, areHotkeysEqual, assertValidHotkey, checkHotkey, createHotkeyHandler, createMultiHotkeyHandler, createSequenceMatcher, defineAppHotkeys, detectPlatform, findHotkeyConflicts, formatForDisplay, formatHotkey, getAppHotkeyRegistrationOptions, getHotkeyManager, getKeyStateTracker, hasNonModifierKey, isAppHotkey, isCustomHotkey, isHotkeySequence, isModifierKey, isSingleLetterKey, matchesHeldModifiers, matchesKeyboardEvent, normalizeHotkey, normalizeHotkeyFromEvent, normalizeHotkeyFromParsed, normalizeKeyName, normalizeRegisterableHotkey, parseHotkey, parseKeyboardEvent, parseRegisterableHotkey, provideAppHotkeys, publishAppHotkeysStore, rawHotkeyToParsedHotkey, resolveAppHotkey, resolveModifier, simplifyHotkey, toHotkeyRegistrationView, validateHotkey };
|
package/dist/match.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { HotkeyCallback, HotkeyChord, HotkeySequence, ParsedHotkey, RegisterableHotkey } from "./hotkey.types.js";
|
|
2
2
|
//#region src/match.d.ts
|
|
3
3
|
export interface KeyboardEventMatch {
|
|
4
4
|
matched: boolean;
|
|
@@ -38,7 +38,7 @@ export interface KeyboardEventMatch {
|
|
|
38
38
|
* })
|
|
39
39
|
* ```
|
|
40
40
|
*/
|
|
41
|
-
export declare function matchesKeyboardEvent(event: KeyboardEvent, hotkey:
|
|
41
|
+
export declare function matchesKeyboardEvent(event: KeyboardEvent, hotkey: HotkeyChord | ParsedHotkey, platform?: 'mac' | 'windows' | 'linux'): boolean;
|
|
42
42
|
/**
|
|
43
43
|
* Options for creating a hotkey handler.
|
|
44
44
|
*/
|
|
@@ -68,8 +68,8 @@ export interface CreateHotkeyHandlerOptions {
|
|
|
68
68
|
* document.addEventListener('keydown', handler)
|
|
69
69
|
* ```
|
|
70
70
|
*/
|
|
71
|
-
export declare function createHotkeyHandler(hotkey:
|
|
72
|
-
export type MultiHotkeyHandler = { [K in
|
|
71
|
+
export declare function createHotkeyHandler(hotkey: HotkeyChord | ParsedHotkey, callback: HotkeyCallback, options?: CreateHotkeyHandlerOptions): (event: KeyboardEvent) => void;
|
|
72
|
+
export type MultiHotkeyHandler = { [K in HotkeyChord]?: HotkeyCallback; };
|
|
73
73
|
/**
|
|
74
74
|
* Creates a handler that matches multiple hotkeys.
|
|
75
75
|
*
|
|
@@ -90,9 +90,42 @@ export type MultiHotkeyHandler = { [K in Hotkey]?: HotkeyCallback; };
|
|
|
90
90
|
*/
|
|
91
91
|
export declare function createMultiHotkeyHandler(handlers: MultiHotkeyHandler, options?: CreateHotkeyHandlerOptions): (event: KeyboardEvent) => void;
|
|
92
92
|
/**
|
|
93
|
-
* Compares normalized
|
|
93
|
+
* Compares normalized hotkey identity, including all four modifiers.
|
|
94
|
+
* Sequences are equal when every step is equal; a one-step sequence equals its chord.
|
|
94
95
|
* Aliases resolve on the supplied platform. A physical code and a logical key
|
|
95
96
|
* remain distinct even when one keyboard event could match both.
|
|
96
97
|
*/
|
|
97
98
|
export declare function areHotkeysEqual(left: RegisterableHotkey, right: RegisterableHotkey, platform?: "mac" | "windows" | "linux"): boolean;
|
|
99
|
+
/** Default timeout between sequence steps (in milliseconds). */
|
|
100
|
+
export declare const DEFAULT_SEQUENCE_TIMEOUT = 1000;
|
|
101
|
+
/**
|
|
102
|
+
* Creates a simple sequence matcher for one-off use.
|
|
103
|
+
*
|
|
104
|
+
* This is a low-level helper that does not support ignoreInputs, target,
|
|
105
|
+
* or other HotkeyOptions. Callers must handle input filtering themselves
|
|
106
|
+
* if attaching to document.
|
|
107
|
+
*
|
|
108
|
+
* @param sequence - The sequence of hotkeys to match
|
|
109
|
+
* @param options - Options including timeout
|
|
110
|
+
* @returns An object with match() and reset() methods
|
|
111
|
+
*
|
|
112
|
+
* @example
|
|
113
|
+
* ```ts
|
|
114
|
+
* const matcher = createSequenceMatcher(['G', 'G'], { timeout: 500 })
|
|
115
|
+
*
|
|
116
|
+
* document.addEventListener('keydown', (event) => {
|
|
117
|
+
* if (matcher.match(event)) {
|
|
118
|
+
* console.log('Sequence matched!')
|
|
119
|
+
* }
|
|
120
|
+
* })
|
|
121
|
+
* ```
|
|
122
|
+
*/
|
|
123
|
+
export declare function createSequenceMatcher(sequence: HotkeySequence, options?: {
|
|
124
|
+
timeout?: number;
|
|
125
|
+
platform?: 'mac' | 'windows' | 'linux';
|
|
126
|
+
}): {
|
|
127
|
+
match: (event: KeyboardEvent) => boolean;
|
|
128
|
+
reset: () => void;
|
|
129
|
+
getProgress: () => number;
|
|
130
|
+
};
|
|
98
131
|
//#endregion
|
package/dist/match.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { normalizeKeyName } from "./constants.js";
|
|
1
2
|
import { detectPlatform } from "./platform.js";
|
|
2
|
-
import { keysEqual } from "./_keyboard-event.js";
|
|
3
|
-
import { parseHotkey, parseRegisterableHotkey } from "./parse.js";
|
|
3
|
+
import { keysEqual, normalizeKeyboardEvent } from "./_keyboard-event.js";
|
|
4
|
+
import { isHotkeySequence, isModifierKey, parseHotkey, parseRegisterableHotkey } from "./parse.js";
|
|
4
5
|
import { matchKeyboardEvent } from "./_match.js";
|
|
5
6
|
|
|
6
7
|
//#region src/match.ts
|
|
@@ -126,15 +127,82 @@ function formatParsedHotkey(parsed) {
|
|
|
126
127
|
return parts.join("+");
|
|
127
128
|
}
|
|
128
129
|
/**
|
|
129
|
-
* Compares normalized
|
|
130
|
+
* Compares normalized hotkey identity, including all four modifiers.
|
|
131
|
+
* Sequences are equal when every step is equal; a one-step sequence equals its chord.
|
|
130
132
|
* Aliases resolve on the supplied platform. A physical code and a logical key
|
|
131
133
|
* remain distinct even when one keyboard event could match both.
|
|
132
134
|
*/
|
|
133
135
|
function areHotkeysEqual(left, right, platform = detectPlatform()) {
|
|
136
|
+
const a = isHotkeySequence(left) ? left : [left];
|
|
137
|
+
const b = isHotkeySequence(right) ? right : [right];
|
|
138
|
+
return a.length === b.length && a.every((step, index) => areChordsEqual(step, b[index], platform));
|
|
139
|
+
}
|
|
140
|
+
function areChordsEqual(left, right, platform) {
|
|
134
141
|
const a = parseRegisterableHotkey(left, platform);
|
|
135
142
|
const b = parseRegisterableHotkey(right, platform);
|
|
136
143
|
return a.ctrl === b.ctrl && a.alt === b.alt && a.shift === b.shift && a.meta === b.meta && (a.code !== void 0 || b.code !== void 0 ? a.code === b.code : keysEqual(a.key, b.key));
|
|
137
144
|
}
|
|
145
|
+
/** Default timeout between sequence steps (in milliseconds). */
|
|
146
|
+
const DEFAULT_SEQUENCE_TIMEOUT = 1e3;
|
|
147
|
+
/**
|
|
148
|
+
* Creates a simple sequence matcher for one-off use.
|
|
149
|
+
*
|
|
150
|
+
* This is a low-level helper that does not support ignoreInputs, target,
|
|
151
|
+
* or other HotkeyOptions. Callers must handle input filtering themselves
|
|
152
|
+
* if attaching to document.
|
|
153
|
+
*
|
|
154
|
+
* @param sequence - The sequence of hotkeys to match
|
|
155
|
+
* @param options - Options including timeout
|
|
156
|
+
* @returns An object with match() and reset() methods
|
|
157
|
+
*
|
|
158
|
+
* @example
|
|
159
|
+
* ```ts
|
|
160
|
+
* const matcher = createSequenceMatcher(['G', 'G'], { timeout: 500 })
|
|
161
|
+
*
|
|
162
|
+
* document.addEventListener('keydown', (event) => {
|
|
163
|
+
* if (matcher.match(event)) {
|
|
164
|
+
* console.log('Sequence matched!')
|
|
165
|
+
* }
|
|
166
|
+
* })
|
|
167
|
+
* ```
|
|
168
|
+
*/
|
|
169
|
+
function createSequenceMatcher(sequence, options = {}) {
|
|
170
|
+
const platform = options.platform ?? detectPlatform();
|
|
171
|
+
const timeout = options.timeout ?? 1e3;
|
|
172
|
+
const parsedSequence = sequence.map((hotkey) => parseHotkey(hotkey, platform));
|
|
173
|
+
let currentIndex = 0;
|
|
174
|
+
let lastKeyTime = 0;
|
|
175
|
+
return {
|
|
176
|
+
match(event) {
|
|
177
|
+
if (event.type === "keydown" && event.repeat || normalizeKeyboardEvent(event, platform).isComposing || isModifierKey(normalizeKeyName(event.key))) return false;
|
|
178
|
+
const now = Date.now();
|
|
179
|
+
if (currentIndex > 0 && now - lastKeyTime > timeout) currentIndex = 0;
|
|
180
|
+
const expected = parsedSequence[currentIndex];
|
|
181
|
+
if (!expected) return false;
|
|
182
|
+
if (matchesKeyboardEvent(event, expected, platform)) {
|
|
183
|
+
lastKeyTime = now;
|
|
184
|
+
currentIndex++;
|
|
185
|
+
if (currentIndex >= parsedSequence.length) {
|
|
186
|
+
currentIndex = 0;
|
|
187
|
+
return true;
|
|
188
|
+
}
|
|
189
|
+
} else if (currentIndex > 0) {
|
|
190
|
+
if (matchesKeyboardEvent(event, parsedSequence[0], platform)) {
|
|
191
|
+
currentIndex = 1;
|
|
192
|
+
lastKeyTime = now;
|
|
193
|
+
} else currentIndex = 0;
|
|
194
|
+
}
|
|
195
|
+
return false;
|
|
196
|
+
},
|
|
197
|
+
reset() {
|
|
198
|
+
currentIndex = 0;
|
|
199
|
+
lastKeyTime = 0;
|
|
200
|
+
},
|
|
201
|
+
getProgress() {
|
|
202
|
+
return currentIndex;
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
}
|
|
138
206
|
|
|
139
207
|
//#endregion
|
|
140
|
-
export { areHotkeysEqual, createHotkeyHandler, createMultiHotkeyHandler, matchesKeyboardEvent };
|
|
208
|
+
export { DEFAULT_SEQUENCE_TIMEOUT, areHotkeysEqual, createHotkeyHandler, createMultiHotkeyHandler, createSequenceMatcher, matchesKeyboardEvent };
|
package/dist/parse.d.ts
CHANGED
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
import { Key } from "./key.types.js";
|
|
2
|
-
import { Hotkey, ParsedHotkey, RawHotkey, RegisterableHotkey } from "./hotkey.types.js";
|
|
2
|
+
import { Hotkey, HotkeyChord, HotkeySequence, ParsedHotkey, RawHotkey, RegisterableHotkey, RegisterableHotkeyChord } from "./hotkey.types.js";
|
|
3
3
|
import { MODIFIER_ALIASES } from "./constants.js";
|
|
4
4
|
//#region src/parse.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* Whether a hotkey is a sequence. Use this instead of `Array.isArray`, which
|
|
7
|
+
* does not narrow readonly arrays.
|
|
8
|
+
*/
|
|
9
|
+
export declare function isHotkeySequence(hotkey: RegisterableHotkey): hotkey is HotkeySequence;
|
|
10
|
+
/**
|
|
11
|
+
* Collapses a one-step sequence to its chord without otherwise changing the hotkey.
|
|
12
|
+
* Registration APIs treat `['Mod+S']` exactly like `'Mod+S'`.
|
|
13
|
+
*/
|
|
14
|
+
export declare function simplifyHotkey<T extends RegisterableHotkey>(hotkey: T): T extends HotkeySequence ? Hotkey : T;
|
|
5
15
|
/**
|
|
6
16
|
* Parses a hotkey string into its component parts.
|
|
7
17
|
*
|
|
@@ -16,7 +26,7 @@ import { MODIFIER_ALIASES } from "./constants.js";
|
|
|
16
26
|
* parseHotkey('Control+Shift+A') // { key: 'A', ctrl: true, shift: true, alt: false, meta: false, modifiers: ['Control', 'Shift'] }
|
|
17
27
|
* ```
|
|
18
28
|
*/
|
|
19
|
-
export declare function parseHotkey(hotkey:
|
|
29
|
+
export declare function parseHotkey(hotkey: HotkeyChord | (string & {}), platform?: 'mac' | 'windows' | 'linux'): ParsedHotkey;
|
|
20
30
|
/**
|
|
21
31
|
* Converts a RawHotkey object to a ParsedHotkey.
|
|
22
32
|
* Optional modifier booleans default to false; modifiers array is derived from them.
|
|
@@ -59,15 +69,17 @@ export declare function rawHotkeyToParsedHotkey(raw: RawHotkey, platform?: 'mac'
|
|
|
59
69
|
* normalizeHotkey('esc') // 'Escape'
|
|
60
70
|
* ```
|
|
61
71
|
*/
|
|
62
|
-
export declare function normalizeHotkey(hotkey: Key | (string & {}), platform?: 'mac' | 'windows' | 'linux'):
|
|
72
|
+
export declare function normalizeHotkey(hotkey: Key | (string & {}), platform?: 'mac' | 'windows' | 'linux'): HotkeyChord;
|
|
63
73
|
/**
|
|
64
74
|
* Same canonical string as {@link normalizeHotkey}, but from an already-parsed hotkey.
|
|
65
75
|
*/
|
|
66
|
-
export declare function normalizeHotkeyFromParsed(parsed: ParsedHotkey, platform?: 'mac' | 'windows' | 'linux'):
|
|
76
|
+
export declare function normalizeHotkeyFromParsed(parsed: ParsedHotkey, platform?: 'mac' | 'windows' | 'linux'): HotkeyChord;
|
|
67
77
|
/**
|
|
68
|
-
* Normalizes a string or {@link RawHotkey} object
|
|
78
|
+
* Normalizes a chord (string or {@link RawHotkey} object) or a sequence to canonical form.
|
|
79
|
+
* Each sequence step is normalized, and a one-step sequence becomes its chord.
|
|
69
80
|
* Use this in framework adapters instead of branching on `formatHotkey(rawHotkeyToParsedHotkey(...))`.
|
|
70
81
|
*/
|
|
82
|
+
export declare function normalizeRegisterableHotkey(hotkey: RegisterableHotkeyChord, platform?: 'mac' | 'windows' | 'linux'): HotkeyChord;
|
|
71
83
|
export declare function normalizeRegisterableHotkey(hotkey: RegisterableHotkey, platform?: 'mac' | 'windows' | 'linux'): Hotkey;
|
|
72
84
|
/**
|
|
73
85
|
* Checks if a string is a recognized modifier token (including aliases).
|
|
@@ -104,7 +116,7 @@ export declare function parseKeyboardEvent(event: KeyboardEvent, platform?: 'mac
|
|
|
104
116
|
* @param event - The keyboard event (typically `keydown`)
|
|
105
117
|
* @param platform - Target platform for `Mod` eligibility
|
|
106
118
|
*/
|
|
107
|
-
export declare function normalizeHotkeyFromEvent(event: KeyboardEvent, platform?: 'mac' | 'windows' | 'linux'):
|
|
119
|
+
export declare function normalizeHotkeyFromEvent(event: KeyboardEvent, platform?: 'mac' | 'windows' | 'linux'): HotkeyChord;
|
|
108
120
|
/**
|
|
109
121
|
* Checks if a hotkey or ParsedHotkey contains at least one non-modifier key.
|
|
110
122
|
*
|
|
@@ -122,10 +134,10 @@ export declare function normalizeHotkeyFromEvent(event: KeyboardEvent, platform?
|
|
|
122
134
|
* hasNonModifierKey(parseHotkey('Mod+A')) // true
|
|
123
135
|
* ```
|
|
124
136
|
*/
|
|
125
|
-
export declare function hasNonModifierKey(hotkey:
|
|
137
|
+
export declare function hasNonModifierKey(hotkey: HotkeyChord | ParsedHotkey | (string & {}), platform?: 'mac' | 'windows' | 'linux'): boolean;
|
|
126
138
|
/**
|
|
127
139
|
* Parses a string or raw binding, resolving Mod for the supplied platform.
|
|
128
140
|
* Physical tokens such as `[KeyQ]` retain `code`; they never become logical Q.
|
|
129
141
|
*/
|
|
130
|
-
export declare function parseRegisterableHotkey(hotkey:
|
|
142
|
+
export declare function parseRegisterableHotkey(hotkey: RegisterableHotkeyChord, platform?: "mac" | "windows" | "linux"): ParsedHotkey;
|
|
131
143
|
//#endregion
|
package/dist/parse.js
CHANGED
|
@@ -4,6 +4,16 @@ import { assertLogicalKey, normalizeKeyboardEvent, splitHotkeyParts } from "./_k
|
|
|
4
4
|
|
|
5
5
|
//#region src/parse.ts
|
|
6
6
|
/**
|
|
7
|
+
* Whether a hotkey is a sequence. Use this instead of `Array.isArray`, which
|
|
8
|
+
* does not narrow readonly arrays.
|
|
9
|
+
*/
|
|
10
|
+
function isHotkeySequence(hotkey) {
|
|
11
|
+
return Array.isArray(hotkey);
|
|
12
|
+
}
|
|
13
|
+
function simplifyHotkey(hotkey) {
|
|
14
|
+
return isHotkeySequence(hotkey) && hotkey.length === 1 ? hotkey[0] : hotkey;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
7
17
|
* Parses a hotkey string into its component parts.
|
|
8
18
|
*
|
|
9
19
|
* @param hotkey - The hotkey string to parse (e.g., 'Mod+Shift+S')
|
|
@@ -138,11 +148,11 @@ function normalizeHotkey(hotkey, platform = detectPlatform()) {
|
|
|
138
148
|
function normalizeHotkeyFromParsed(parsed, platform = detectPlatform()) {
|
|
139
149
|
return normalizedHotkeyStringFromParsed(parsed, platform);
|
|
140
150
|
}
|
|
141
|
-
/**
|
|
142
|
-
* Normalizes a string or {@link RawHotkey} object to the same canonical hotkey string.
|
|
143
|
-
* Use this in framework adapters instead of branching on `formatHotkey(rawHotkeyToParsedHotkey(...))`.
|
|
144
|
-
*/
|
|
145
151
|
function normalizeRegisterableHotkey(hotkey, platform = detectPlatform()) {
|
|
152
|
+
if (isHotkeySequence(hotkey)) {
|
|
153
|
+
const steps = hotkey.map((step) => normalizeHotkey(step, platform));
|
|
154
|
+
return steps.length === 1 ? steps[0] : steps;
|
|
155
|
+
}
|
|
146
156
|
return typeof hotkey === "string" ? normalizeHotkey(hotkey, platform) : normalizeHotkeyFromParsed(rawHotkeyToParsedHotkey(hotkey, platform), platform);
|
|
147
157
|
}
|
|
148
158
|
/**
|
|
@@ -234,4 +244,4 @@ function parseRegisterableHotkey(hotkey, platform = detectPlatform()) {
|
|
|
234
244
|
}
|
|
235
245
|
|
|
236
246
|
//#endregion
|
|
237
|
-
export { hasNonModifierKey, isModifierKey, normalizeHotkey, normalizeHotkeyFromEvent, normalizeHotkeyFromParsed, normalizeRegisterableHotkey, parseHotkey, parseKeyboardEvent, parseRegisterableHotkey, rawHotkeyToParsedHotkey };
|
|
247
|
+
export { hasNonModifierKey, isHotkeySequence, isModifierKey, normalizeHotkey, normalizeHotkeyFromEvent, normalizeHotkeyFromParsed, normalizeRegisterableHotkey, parseHotkey, parseKeyboardEvent, parseRegisterableHotkey, rawHotkeyToParsedHotkey, simplifyHotkey };
|
|
@@ -6,8 +6,8 @@ export type RecorderKeyMode = 'code' | 'key';
|
|
|
6
6
|
export interface RecorderRejection {
|
|
7
7
|
reason: 'missing-code' | 'alt-graph' | 'invalid' | 'conflict' | 'validation';
|
|
8
8
|
message: string;
|
|
9
|
+
/** The rejected chord or sequence, when one was recorded. */
|
|
9
10
|
hotkey?: Hotkey;
|
|
10
|
-
sequence?: Array<Hotkey>;
|
|
11
11
|
conflicts?: Array<HotkeyConflict>;
|
|
12
12
|
}
|
|
13
13
|
export interface RecorderOptions {
|
|
@@ -20,12 +20,15 @@ export interface RecorderOptions {
|
|
|
20
20
|
/** Rejected candidates leave recording active. */
|
|
21
21
|
onReject?: (rejection: RecorderRejection) => void;
|
|
22
22
|
}
|
|
23
|
+
/** Validation context for a recorded chord or sequence. */
|
|
23
24
|
export interface HotkeyRecorderValidationContext {
|
|
25
|
+
/** The event that recorded the last chord. */
|
|
24
26
|
event: KeyboardEvent;
|
|
27
|
+
/** The last chord, parsed. */
|
|
25
28
|
parsedHotkey: ParsedHotkey;
|
|
26
|
-
|
|
27
|
-
export interface HotkeySequenceRecorderValidationContext {
|
|
29
|
+
/** The event that recorded each step. */
|
|
28
30
|
events: ReadonlyArray<KeyboardEvent>;
|
|
31
|
+
/** Every step, parsed. */
|
|
29
32
|
parsedSequence: ReadonlyArray<ParsedHotkey>;
|
|
30
33
|
}
|
|
31
34
|
//#endregion
|
package/dist/validate.d.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { Hotkey } from "./hotkey.types.js";
|
|
2
2
|
//#region src/validate.d.ts
|
|
3
|
+
/** A hotkey to validate: a chord string, a sequence, or untyped input such as saved data. */
|
|
4
|
+
export type ValidatableHotkey = Hotkey | (string & {}) | ReadonlyArray<string>;
|
|
3
5
|
/**
|
|
4
|
-
* Validates a hotkey
|
|
6
|
+
* Validates a hotkey and returns any warnings or errors.
|
|
7
|
+
* Sequences are validated step by step, and messages name the failing step.
|
|
5
8
|
*
|
|
6
9
|
* Checks for:
|
|
7
10
|
* - Valid syntax (modifier+...+key format)
|
|
@@ -18,9 +21,12 @@ import { Hotkey } from "./hotkey.types.js";
|
|
|
18
21
|
*
|
|
19
22
|
* validateHotkey('')
|
|
20
23
|
* // { valid: false, warnings: [], errors: ['Hotkey cannot be empty'] }
|
|
24
|
+
*
|
|
25
|
+
* validateHotkey(['G', 'Foo+H'])
|
|
26
|
+
* // { valid: false, warnings: [], errors: ["Step 2: Unknown modifier: 'Foo'"] }
|
|
21
27
|
* ```
|
|
22
28
|
*/
|
|
23
|
-
export declare function validateHotkey(hotkey:
|
|
29
|
+
export declare function validateHotkey(hotkey: ValidatableHotkey): ValidationResult;
|
|
24
30
|
/**
|
|
25
31
|
* Validates a hotkey and throws an error if invalid.
|
|
26
32
|
* Useful for development-time validation.
|
|
@@ -34,7 +40,7 @@ export declare function validateHotkey(hotkey: Hotkey | (string & {})): Validati
|
|
|
34
40
|
* assertValidHotkey('') // Throws Error: Invalid hotkey: Hotkey cannot be empty
|
|
35
41
|
* ```
|
|
36
42
|
*/
|
|
37
|
-
export declare function assertValidHotkey(hotkey:
|
|
43
|
+
export declare function assertValidHotkey(hotkey: ValidatableHotkey): void;
|
|
38
44
|
/**
|
|
39
45
|
* Validates a hotkey and logs warnings to the console.
|
|
40
46
|
* Useful for development-time feedback.
|
|
@@ -49,7 +55,7 @@ export declare function assertValidHotkey(hotkey: Hotkey | (string & {})): void;
|
|
|
49
55
|
* // Returns: true
|
|
50
56
|
* ```
|
|
51
57
|
*/
|
|
52
|
-
export declare function checkHotkey(hotkey:
|
|
58
|
+
export declare function checkHotkey(hotkey: ValidatableHotkey): boolean;
|
|
53
59
|
/**
|
|
54
60
|
* Result of validating a hotkey string.
|
|
55
61
|
*/
|
package/dist/validate.js
CHANGED
|
@@ -3,7 +3,8 @@ import { splitHotkeyParts } from "./_keyboard-event.js";
|
|
|
3
3
|
|
|
4
4
|
//#region src/validate.ts
|
|
5
5
|
/**
|
|
6
|
-
* Validates a hotkey
|
|
6
|
+
* Validates a hotkey and returns any warnings or errors.
|
|
7
|
+
* Sequences are validated step by step, and messages name the failing step.
|
|
7
8
|
*
|
|
8
9
|
* Checks for:
|
|
9
10
|
* - Valid syntax (modifier+...+key format)
|
|
@@ -20,9 +21,30 @@ import { splitHotkeyParts } from "./_keyboard-event.js";
|
|
|
20
21
|
*
|
|
21
22
|
* validateHotkey('')
|
|
22
23
|
* // { valid: false, warnings: [], errors: ['Hotkey cannot be empty'] }
|
|
24
|
+
*
|
|
25
|
+
* validateHotkey(['G', 'Foo+H'])
|
|
26
|
+
* // { valid: false, warnings: [], errors: ["Step 2: Unknown modifier: 'Foo'"] }
|
|
23
27
|
* ```
|
|
24
28
|
*/
|
|
25
29
|
function validateHotkey(hotkey) {
|
|
30
|
+
if (typeof hotkey !== "string") {
|
|
31
|
+
if (hotkey.length === 0) return {
|
|
32
|
+
valid: false,
|
|
33
|
+
warnings: [],
|
|
34
|
+
errors: ["Sequence cannot be empty"]
|
|
35
|
+
};
|
|
36
|
+
if (hotkey.length === 1) return validateChord(hotkey[0]);
|
|
37
|
+
const results = hotkey.map(validateChord);
|
|
38
|
+
const prefixed = (key) => results.flatMap((result, i) => result[key].map((message) => `Step ${i + 1}: ${message}`));
|
|
39
|
+
return {
|
|
40
|
+
valid: results.every((result) => result.valid),
|
|
41
|
+
warnings: prefixed("warnings"),
|
|
42
|
+
errors: prefixed("errors")
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
return validateChord(hotkey);
|
|
46
|
+
}
|
|
47
|
+
function validateChord(hotkey) {
|
|
26
48
|
const warnings = [];
|
|
27
49
|
const errors = [];
|
|
28
50
|
if (!hotkey || hotkey.trim() === "") return {
|
|
@@ -82,7 +104,7 @@ function isKnownKey(key) {
|
|
|
82
104
|
*/
|
|
83
105
|
function assertValidHotkey(hotkey) {
|
|
84
106
|
const result = validateHotkey(hotkey);
|
|
85
|
-
if (!result.valid) throw new Error(`Invalid hotkey '${hotkey}': ${result.errors.join(", ")}`);
|
|
107
|
+
if (!result.valid) throw new Error(`Invalid hotkey '${String(hotkey)}': ${result.errors.join(", ")}`);
|
|
86
108
|
}
|
|
87
109
|
/**
|
|
88
110
|
* Validates a hotkey and logs warnings to the console.
|
|
@@ -100,8 +122,8 @@ function assertValidHotkey(hotkey) {
|
|
|
100
122
|
*/
|
|
101
123
|
function checkHotkey(hotkey) {
|
|
102
124
|
const result = validateHotkey(hotkey);
|
|
103
|
-
if (result.errors.length > 0) console.error(`Hotkey '${hotkey}' errors:`, result.errors.join("; "));
|
|
104
|
-
if (result.warnings.length > 0) console.warn(`Hotkey '${hotkey}' warnings:`, result.warnings.join("; "));
|
|
125
|
+
if (result.errors.length > 0) console.error(`Hotkey '${String(hotkey)}' errors:`, result.errors.join("; "));
|
|
126
|
+
if (result.warnings.length > 0) console.warn(`Hotkey '${String(hotkey)}' warnings:`, result.warnings.join("; "));
|
|
105
127
|
return result.valid;
|
|
106
128
|
}
|
|
107
129
|
|
package/package.json
CHANGED
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
import { HotkeySequence } from "./sequence-manager.js";
|
|
2
|
-
import { HotkeySequenceRecorderValidationContext, RecorderOptions } from "./recorder-options.js";
|
|
3
|
-
import { Store } from "@tanstack/store";
|
|
4
|
-
//#region src/hotkey-sequence-recorder.d.ts
|
|
5
|
-
/**
|
|
6
|
-
* How the user can commit a recorded sequence from the keyboard.
|
|
7
|
-
* - `'enter'`: plain Enter (no modifiers) commits when at least one step exists.
|
|
8
|
-
* - `'none'`: only {@link HotkeySequenceRecorder.commit} finishes recording (or idle timeout if set).
|
|
9
|
-
*/
|
|
10
|
-
export type HotkeySequenceRecorderCommitKeys = 'enter' | 'none';
|
|
11
|
-
/**
|
|
12
|
-
* State interface for the HotkeySequenceRecorder.
|
|
13
|
-
*/
|
|
14
|
-
export interface HotkeySequenceRecorderState {
|
|
15
|
-
/** Whether recording is currently active */
|
|
16
|
-
isRecording: boolean;
|
|
17
|
-
/** Chords captured so far in the current recording session */
|
|
18
|
-
steps: HotkeySequence;
|
|
19
|
-
/** The last successfully committed sequence, or null if none / after starting a new session */
|
|
20
|
-
recordedSequence: HotkeySequence | null;
|
|
21
|
-
}
|
|
22
|
-
/**
|
|
23
|
-
* Options for configuring a HotkeySequenceRecorder instance.
|
|
24
|
-
*/
|
|
25
|
-
export interface HotkeySequenceRecorderOptions extends RecorderOptions {
|
|
26
|
-
/** Validate the completed sequence. Rejection preserves the steps for editing. */
|
|
27
|
-
validate?: (sequence: HotkeySequence, context: HotkeySequenceRecorderValidationContext) => boolean | string;
|
|
28
|
-
/** Callback when a sequence is successfully recorded */
|
|
29
|
-
onRecord: (sequence: HotkeySequence) => void;
|
|
30
|
-
/** Optional callback when recording is cancelled (Escape pressed) */
|
|
31
|
-
onCancel?: () => void;
|
|
32
|
-
/** Optional callback when the sequence is cleared (Backspace/Delete with no steps) */
|
|
33
|
-
onClear?: () => void;
|
|
34
|
-
/**
|
|
35
|
-
* Whether plain Enter commits the current steps. Ignored when {@link commitKeys} is `'none'`.
|
|
36
|
-
* @default true
|
|
37
|
-
*/
|
|
38
|
-
commitOnEnter?: boolean;
|
|
39
|
-
/**
|
|
40
|
-
* Keyboard commit mode. When `'none'`, use {@link HotkeySequenceRecorder.commit} (and optional idle timeout).
|
|
41
|
-
* @default 'enter'
|
|
42
|
-
*/
|
|
43
|
-
commitKeys?: HotkeySequenceRecorderCommitKeys;
|
|
44
|
-
/**
|
|
45
|
-
* Milliseconds of inactivity after the **last completed chord** before auto-committing.
|
|
46
|
-
* The timer does not run while waiting for the first chord (`steps.length === 0`).
|
|
47
|
-
*/
|
|
48
|
-
idleTimeoutMs?: number;
|
|
49
|
-
/**
|
|
50
|
-
* Whether to ignore keyboard events from input-like elements (text inputs,
|
|
51
|
-
* textarea, select, contenteditable). When true, typing in inputs passes
|
|
52
|
-
* through normally instead of being captured as a sequence recording.
|
|
53
|
-
* Escape always works regardless of this setting.
|
|
54
|
-
* @default true
|
|
55
|
-
*/
|
|
56
|
-
ignoreInputs?: boolean;
|
|
57
|
-
}
|
|
58
|
-
/**
|
|
59
|
-
* Framework-agnostic class for recording multi-chord sequences (Vim-style shortcuts).
|
|
60
|
-
*
|
|
61
|
-
* Each step is captured like a single hotkey chord. Press **Enter** (no modifiers) to commit
|
|
62
|
-
* when {@link HotkeySequenceRecorderOptions.commitKeys} is `'enter'` (default), **Escape** to cancel,
|
|
63
|
-
* **Backspace/Delete** to remove the last step or clear when empty.
|
|
64
|
-
*/
|
|
65
|
-
export declare class HotkeySequenceRecorder {
|
|
66
|
-
#private;
|
|
67
|
-
readonly store: Store<HotkeySequenceRecorderState>;
|
|
68
|
-
/** Options may be a getter to read current configuration during an active session. */
|
|
69
|
-
constructor(options: HotkeySequenceRecorderOptions | (() => HotkeySequenceRecorderOptions));
|
|
70
|
-
/** Merges current callbacks and options without discarding recorded steps. */
|
|
71
|
-
setOptions(options: Partial<HotkeySequenceRecorderOptions>): void;
|
|
72
|
-
/** Starts a fresh recording; repeated starts during an active session are ignored. */
|
|
73
|
-
start(): void;
|
|
74
|
-
/**
|
|
75
|
-
* Commit the current steps as a sequence. No-op if fewer than one step.
|
|
76
|
-
*/
|
|
77
|
-
commit(): void;
|
|
78
|
-
/** Stops and discards in-progress steps without invoking onCancel. */
|
|
79
|
-
stop(): void;
|
|
80
|
-
/** Stops, discards in-progress steps, and notifies onCancel. */
|
|
81
|
-
cancel(): void;
|
|
82
|
-
destroy(): void;
|
|
83
|
-
}
|
|
84
|
-
//#endregion
|